电子海图系统单纯显示海图,并不能起多大的作用,只有将其作为信息集成平台,附加上各种航行相关的信息,才能发挥其功效,而其中一项不可或缺的是AIS的功能。船舶自动识别系统(AIS)是一种新型的助航设备,其基本功能是将船舶的标识信息、位置信息、运动参数和航行状态等与船舶航行安全有关的重要数据,通过VHF数据链路,广播给周围的船舶,以实现对本海区船舶的识别和监视。1995年7月,瑞典、芬兰首次提出“无线电AIS”的概念;2000年12月,IMO MSC73会议通过AIS强制性安装议案。
AIS是基于无线电主动应答的设备,当船舶安装了AIS后,就可以与其它装有AIS的船舶自动交换重要的航行数据,包括:船名、船位、船型(大小)、航线、航速、航向、转向速度等等。驾驶员无须逐个查询船舶,就可以获得所有装有AIS船舶的完整的交通动态信息并进行监控和指挥。如果将定期航行和固定航线的船舶的相关信息加在传送信息中,AIS将成为一种船舶报告系统。AIS设备一旦启动,会根据当前的航速以及转向率按照一定的周期不断主动向外部发送上述信息,与此同时,设备本身也在不断的接收他船信息。
商船上都预留AIS Pilot Plug,用数据线插上后,就可以接收本船AIS接收到的信息。
AIS数据采用NMEA0813标准格式进行传输,其设备类型为AI,报文类型为VDM或VDO。VDM报文表示数据报告来自他船;VDO报文表示数据报告来自本船。AIS数据正文被分为27种消息类型。每一种消息类型有不同的编码规则,连接AIS接收机,根据收到的串口数据判定其消息类型后,依次解析即可。各消息类型的格式详情可参见https://gpsd.gitlab.io/gpsd/AIVDM.html。
例1:!AIVDM,1,1,,B,177KQJ5000G?tO`K>RA1wUbN0TKH,0*5C
- 字段1 (!AIVDM):表示是一个移动AIS基站数据,并且该数据来自他船;
- 字段2 (1):当前报文被拆分的个数,因为NMEA 0183规定每条报文最大为82个字符限制,当数据报文超过限制时,就需要拆分,此处的1表示该报文被拆分分1个(即没有被拆分);
- 字段3 (1):是被拆分后报文片段的编号;
- 字段4 (空):报文序列标识,当同时存在多条需要分段的报文时,该字段用于区分隶属于不同消息报文,此处为空表示没有同时收到多条报文;
- 字段5 (B):无线电频道代码, AIS使用来自两个VHF无线电信道的双工的高端:AIS信道A为161.975Mhz(87B); AIS通道B为162.025Mhz(88B);
- 字段6 (177KQJ5000G?tO`K>RA1wUbN0TKH):是数据有效负载,需先判断其消息类型,然后按照各消息类型格式进行解析;
- 字段7(0):是填充数据有效载荷到6位边界(从0到5)所需的填充位数,等效地,从中减去5可以知道在最后6位半字节中有多少个最低有效位数据有效载荷应被忽略;
- 字段8 (5C):数据完整性校验和。
AIS的有效数据中的字符串以特殊方式编码(六位码),即每个字符由六位二制数表示。将数据的二进制切成连续的六位字节,每个六位字节映射到一个ASCII字符,最后不足六位的则填充相应位数的0。
Char | ASCII | Bits | Char | ASCII | Bits | Char | ASCII | Bits | Char | ASCII | Bits |
---|---|---|---|---|---|---|---|---|---|---|---|
"0" | 48 | 000000 | "@" | 64 | 010000 | "P" | 80 | 100000 | "h" | 104 | 110000 |
"1" | 49 | 000001 | "A" | 65 | 010001 | "Q" | 81 | 100001 | "i" | 105 | 110001 |
"2" | 50 | 000010 | "B" | 66 | 010010 | "R" | 82 | 100010 | "j" | 106 | 110010 |
"3" | 51 | 000011 | "C" | 67 | 010011 | "S" | 83 | 100011 | "k" | 107 | 110011 |
"4" | 52 | 000100 | "D" | 68 | 010100 | "T" | 84 | 100100 | "l" | 108 | 110100 |
"5" | 53 | 000101 | "E" | 69 | 010101 | "U" | 85 | 100101 | "m" | 109 | 110101 |
"6" | 54 | 000110 | "F" | 70 | 010110 | "V" | 86 | 100110 | "n" | 110 | 110110 |
"7" | 55 | 000111 | "G" | 71 | 010111 | "W" | 87 | 100111 | "o" | 111 | 110111 |
"8" | 56 | 001000 | "H" | 72 | 011000 | "`" | 96 | 101000 | "p" | 112 | 111000 |
"9" | 57 | 001001 | "I" | 73 | 011001 | "a" | 97 | 101001 | "q" | 113 | 111001 |
":" | 58 | 001010 | "J" | 74 | 011010 | "b" | 98 | 101010 | "r" | 114 | 111010 |
";" | 59 | 001011 | "K" | 75 | 011011 | "c" | 99 | 101011 | "s" | 115 | 111011 |
"<" | 60 | 001100 | "L" | 76 | 011100 | "d" | 100 | 101100 | "t" | 116 | 111100 |
"=" | 61 | 001101 | "M" | 77 | 011101 | "e" | 101 | 101101 | "u" | 117 | 111101 |
">" | 62 | 001110 | "N" | 78 | 011110 | "f" | 102 | 101110 | "v" | 118 | 111110 |
"?" | 63 | 001111 | "O" | 79 | 011111 | "g" | 103 | 101111 | "w" | 119 | 111111 |
AIS数据解析
将NMEA0813的字段6提取出来,得到二进制串,前6位表示AIS的消息类型。
消息类型 | 描述 | 消息类型 | 描述 |
---|---|---|---|
1 | A类位置报告 | 15 | 询问 |
2 | A类位置报告(分配时间) | 16 | 指配模式命令 |
3 | A类位置报告(对询问的回应) | 17 | GNSS广播二进制消息 |
4 | 基站报告 | 18 | 标准的B类设备位置报告 |
5 | 船舶静态和航行相关数据 | 19 | 扩展的B类设备位置报告 |
6 | 寻址二进制消息 | 20 | 数据链路管理消息 |
7 | 二进制确认 | 21 | 助航设备报告 |
8 | 二进制广播消息 | 22 | 信道管理 |
9 | 标准的SAR航空器位置报告 | 23 | 群组指配命令 |
10 | UTC/日期询问 | 24 | 静态数据报告 |
11 | UTC/日期响应 | 25 | 单时隙二进制消息 |
12 | 寻址安全相关信息 | 26 | 带有通信状态的多时隙二进制消息 |
13 | 安全相关确认 | 27 | 大量程AIS广播消息 |
14 | 安全相关广播消息 |
在通常操作中,AIS收发器将每2到10秒广播一次位置报告(对应消息1、2或3),具体取决于航行过程中船舶的速度,以及每3分钟在锚定和静止的船舶上的位置报告,每6分钟发送一次消息5,更多详细信息请参见IALA Technical Clarifications on Recommendation ITU-R M.1371-1的第2.3部分。消息6用于符合内河未加密结构化扩展消息系统。消息8通常用于私有加密消息,例如军事演习中的位置传输。消息12、14用于文本消息,名义上与安全有关,但也用于流量控制和偶尔的聊天用途。对于商船而言,实际中,很少见到除了1、3、4、5、18和24以外的消息,许多AIS发射器从不发射它们。
以消息1、2和3为例,为共享导航信息的公共报告结构,被称为通用导航横块, 总共168位,占用一个AIVDM语句。其具体格式如下:
位置 | 长度 | 名称 | 类型 | 描述 |
---|---|---|---|---|
0-5 | 6 | 消息类型 | u | 取值: 1-3 |
6-7 | 2 | 重复次数指示 | u | 指示重发的次数 缺省为0,3表示不再重发 |
8-37 | 30 | MMSI | u | 九位数字的MMSI号码 |
38-41 | 4 | 航行状态 | e | 0:动力航行中 1:锚泊 2:失控 3:操作受限 4:受吃水限制 5:锚链系泊 6:搁浅 7:捕捞中 8:风帆动力航行 9:高速船 10:地效翼船 11~13:留作未来用 14:自动识别搜救器已激活 15:未定义(默认值) |
42-49 | 8 | 转向速率 (ROT) | I3 | 真实值=(原始值/4.733)2 原始值=128,表明未提供 |
50-59 | 10 | 对地航速 (SOG) | U1 | 以1/10kn表示的速度 |
60-60 | 1 | 舱位精度 | b | 1:高精度;0:低精度 |
61-88 | 28 | 经度 | I4 | 用1/10000分表示的经度 (西经为-) |
89-115 | 27 | 纬度 | I4 | 用1/10000分表示的纬度 (南纬为-) |
116-127 | 12 | 对地航向 (COG) | U1 | 以1/10度表示的航向 |
128-136 | 9 | 船首真航向 (HDG) | u | 范围:0 ~ 359,511表示未提供 |
137-142 | 6 | 时间戳 | u | 以秒表示的时间戳 |
143-144 | 2 | 操纵指示符 | e | 0:未提供(默认值) 1:无特殊操纵 2:特殊操作(如区域通行) |
145-147 | 3 | 空 | x | 未被使用 |
148-148 | 1 | RAIM标志 | b | 0:未使用RAIM 1:使用RAIM |
149-167 | 19 | 无线电状态 | u | 无线电系统的诊断信息 |
类型一列中,各字母代表:u(无符号整型),U(无符号整型,显示成浮点型),i(有符号整型),I(有符号整型,显示成浮点型),b(布尔型),e(枚举型),x(为空或保留位),t(六位码代表的字符串),d(二进制数据),a(数组类型,‘^’前代表数组的长度)。
例2:177KQJ5000G?tO`K>RA1wUbN0TKH
1 | 7 | 7 | K | Q | J | 5 | 0 | 0 | 0 |
---|---|---|---|---|---|---|---|---|---|
000001 | 000111 | 000111 | 011011 | 100001 | 011010 | 000101 | 000000 | 000000 | 000000 |
G | ? | t | O | ` | K | > | R | A | 1 |
010111 | 001111 | 111100 | 011111 | 101000 | 011011 | 001110 | 100010 | 010001 | 000001 |
w | U | b | N | 0 | T | K | H | ||
111111 | 100101 | 101010 | 011110 | 000000 | 100100 | 011011 | 011000 |
位置 | 二进制串 | 描述 |
---|---|---|
0-5 | 000001 | 消息类型:1 |
6-7 | 00 | 表示重发次数为0 |
8-37 | 0111 000111 011011 100001 011010 00 | MMSI:477553000 |
38-41 | 0101 | 航行状态:5 |
42-49 | 000000 00 | ROT:0 |
50-59 | 0000 000000 | SOG:0 |
60-60 | 0 | 舱位精度:0 |
61-88 | 10111 001111 111100 011111 10100 | 经度:-73407500 = -122.345834 |
89-115 | 0 011011 001110 100010 010001 00 | 纬度:28549700 = 47.582833 |
116-127 | 0001 111111 10 | COG:510 = 51.0 |
128-136 | 0101 10101 | HDG:181 |
137-142 | 0 01111 | 时间戳:15 |
143-144 | 0 0 | 操纵指示符:0 |
145-147 | 000 | 空 |
148-148 | 0 | RAIM标志:0 |
149-167 | 0 100100 011011 011000 | 无线电状态 |
在解码AIS数据中,需要将ASCII码转化成6位码,然后大量对二进制位进行操作。因此新建扩展类BitArrayExt
,添加如下方法:
public static class BitArrayExt
{
//获取无符号整型值
public static uint GetUInt(this BitArray bits, int offset, int bitCnt)
{
uint res = 0;
for (int i = 0; i < bitCnt; i++)
{
if (bits[offset + i])
{
res += (uint)1 << (bitCnt - 1 - i);
}
}
return res;
}
//获取有符号整型值
public static int GetInt(this BitArray bits, int offset, int bitCnt)
{
var res = (int)GetUInt(bits, offset, bitCnt);
int msb = 1 << (bitCnt - 1);
bool isNegative = (res & msb) != 0;
if (isNegative)
{
const int allOnesExceptLsb = -2;
int signBits = allOnesExceptLsb << (bitCnt - 1);
res |= signBits;
}
return res;
}
//获取布尔值
public static bool GetBit(this BitArray bits, int offset)
{
return bits[offset];
}
//获取6位码对应的字符串
public static string GetString(this BitArray bits, int offset, int bitCnt)
{
var res = "";
var cnt = 0;
while (cnt < bitCnt)
{
var c = (byte)GetUInt(bits, offset, 6);
if (c < 32) c = (byte)(c + 64);
//@ 跳过
if(c != 64) res += (char) c;
cnt += 6;
offset += 6;
}
return res;
}
public static BitArray GetData(this BitArray bits, int offset, int bitCnt)
{
var len = bitCnt;
if (bitCnt + offset < bits.Length) len = bits.Length - offset;
var res = new BitArray(len);
for (int i = 0; i < len; i++)
{
res[i] = bits[offset + i];
}
return res;
}
}
新建静态类AISParser
,根据不同消息类型,根据其标准进行解码,解码结果以键值对的形式封装进哈希表中:
public static class AISParser
{
//当报文存在拆分时,存储上一拆分报文的信息。当报文全部收集齐时,需清除。
private static Dictionary<string, string> sequenceDic = new Dictionary<string, string>();
public static Hashtable ParseSentence(string sentence)
{
int fragmentCount; //字段2 拆分总数
int fragmentNo; //字段3 拆分序号
string multiSequenceMessageId; //字段4 报文序列标识
char? channelCode; //字段5 频道代码
BitArray payload; //字段6 数据负载
int bitCount; //负载长度
int padding; //字段7 填充位数
var ss = sentence.Split(',');
fragmentCount = int.Parse(ss[1]);
fragmentNo = int.Parse(ss[2]);
multiSequenceMessageId = ss[3];
channelCode = ss[4].Length > 0 ? ss[4][0] : default(char?);
var payloadAscii = ss[5];
padding = int.Parse(ss[6]);
if (fragmentCount == fragmentNo) //报文已集齐
{
if (fragmentNo != 1) //存在分组
{
if (sequenceDic.ContainsKey(multiSequenceMessageId))
{
payloadAscii = sequenceDic[multiSequenceMessageId] + payloadAscii;
sequenceDic.Remove(multiSequenceMessageId);
}
else
{
throw new Exception($"{multiSequenceMessageId}:{fragmentNo}/{fragmentCount} 数据缺失");
}
}
//开始解析
if (!string.IsNullOrEmpty(payloadAscii))
{
bitCount = payloadAscii.Length * 6 - padding;
payload = new BitArray(bitCount);
for (int i = 0; i < payloadAscii.Length; i++)
{
//将Ais的Ascii码转化成6位码
var c = (byte)payloadAscii[i];
c -= 48;
if (c > 40) c -= 8;
//将6位码存储到BitArray中
for (int j = 0; j < 6; j++)
{
if(i * 6 + j < bitCount) payload[i * 6 + j] = (c >> (5-j)) % 2 == 1;
}
}
//获取消息类型
var messageId = payload.GetUInt(0, 6);
switch (messageId)
{
case 1:
return AisMessage_1(payload);
... //其他消息类型的解码
default:
throw new Exception($"暂不支持解析AIS消息类型[{messageId}]");
}
}
}
else //存在报文拆分
{
if (fragmentNo == 1) //分组中的第一条
{
if (sequenceDic.ContainsKey(multiSequenceMessageId))
{
sequenceDic.Remove(multiSequenceMessageId);
}
sequenceDic.Add(multiSequenceMessageId, payloadAscii);
}
else
{
if (sequenceDic.ContainsKey(multiSequenceMessageId))
{
sequenceDic[multiSequenceMessageId] += payloadAscii;
}
else
{
throw new Exception($"{multiSequenceMessageId}:{fragmentNo}/{fragmentCount} 数据缺失");
}
}
}
return null;
}
private static Hashtable AisMessage_1(BitArray bits)
{
var rot = bits.GetInt(42, 8);
var hdg = bits.GetUInt(128, 9);
var lon = bits.GetInt(61, 28);
var lat = bits.GetInt(89, 27);
var cog = bits.GetUInt(116, 12);
var res = new Hashtable
{
{"MessageType", bits.GetUInt(0, 6)},
{"RepeatIndicator", bits.GetUInt(6, 2)},
{"MMSI", bits.GetUInt(8, 30)},
{"NavigationalStatus", bits.GetUInt(38, 4)},
{"RateOfTurn", rot == 128 ? default(double?) : Math.Pow(rot/4.733, 2)},
{"SpeedOverGround", bits.GetUInt(50, 10)/10.0},
{"PositionAccuracy", bits.GetBit(60)},
{"Longitude", lon == 0x6791AC0 ? default(double?) : lon/600000.0},
{"Latitude", lat == 0x3412140 ? default(double?) : lat/600000.0},
{"CourseOverGround", cog == 0xE10 ? default(double?) : cog/10.0},
{"TrueHeading", hdg == 511 ? default(uint?) : hdg},
{"TimeStamp", bits.GetUInt(137, 6)},
{"ManeuverIndicator", bits.GetUInt(143, 2)},
{"Spare", bits.GetUInt(145, 3)},
{"RAIMFlag", bits.GetBit(148)},
{"SyncState", bits.GetUInt(149, 2)},
{"SlotTimeOut", bits.GetUInt(151, 3)},
{"SubMessage", bits.GetUInt(154, 14)},
};
return res;
}
.... //其他消息类型的解码
}