2.3.1 电子海图系统解析及开发 功能开发 - 接入AIS功能

电子海图系统单纯显示海图,并不能起多大的作用,只有将其作为信息集成平台,附加上各种航行相关的信息,才能发挥其功效,而其中一项不可或缺的是AIS的功能。船舶自动识别系统(AIS)是一种新型的助航设备,其基本功能是将船舶的标识信息、位置信息、运动参数和航行状态等与船舶航行安全有关的重要数据,通过VHF数据链路,广播给周围的船舶,以实现对本海区船舶的识别和监视。1995年7月,瑞典、芬兰首次提出“无线电AIS”的概念;2000年12月,IMO MSC73会议通过AIS强制性安装议案。

AIS发展历程

AIS是基于无线电主动应答的设备,当船舶安装了AIS后,就可以与其它装有AIS的船舶自动交换重要的航行数据,包括:船名、船位、船型(大小)、航线、航速、航向、转向速度等等。驾驶员无须逐个查询船舶,就可以获得所有装有AIS船舶的完整的交通动态信息并进行监控和指挥。如果将定期航行和固定航线的船舶的相关信息加在传送信息中,AIS将成为一种船舶报告系统。AIS设备一旦启动,会根据当前的航速以及转向率按照一定的周期不断主动向外部发送上述信息,与此同时,设备本身也在不断的接收他船信息。

商船上都预留AIS Pilot Plug,用数据线插上后,就可以接收本船AIS接收到的信息。

AIS Pilot Plug

AIS数据采用NMEA0813标准格式进行传输,其设备类型为AI,报文类型为VDMVDO。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;
        }

        ....  //其他消息类型的解码
    }
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,319评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,801评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,567评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,156评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,019评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,090评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,500评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,192评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,474评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,566评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,338评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,212评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,572评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,890评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,169评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,478评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,661评论 2 335

推荐阅读更多精彩内容