新工作的第一个任务就是给设备集成上一个AIS功能并进行数据解析,由于是新员工,对于海洋设备还不是特别熟悉,加上没有老师傅带,中间踩了不少坑。所以打算整理一下数据解析过程中自己踩过的坑,留个纪念。
我们先不考虑硬件设计和数据采集生成的过程,只说数据接收和解析的注意事项。
首先,在进行数据解析之前我们需要了解AIS语句的数据格式,这个已经有不少大神的博客详细介绍过了,我这里借鉴一下(凑字数哈哈哈哈)
AIS数据格式如上图所示,它分为明码和暗码两类。
明码:以“$”开头,明码报文内容可以直接读取
暗码:以“!”开头,报文内容需要通过字符转换和格式定义才能读出。我这里解析的就是暗码
详细的船只和位置信息都是封装在电文中的,比如这条语句:
!AIVDM,1,1,,B,169G?I0P007k`vT;BwhP7gv4@d0e,0*0A
位置信息包含在红色的数据中,这也是我们需要解析的内容。当我们接收到一条AIS语句之后,只需要将这条语句按照上述格式进行截取,留下需要解析的报文即可。
但是在此之前,还是要先了解一下除了封装的报文之外的其他信息(这个非常重要,千万不要将这些信息直接丢弃,后面用得到)
1 |
!XXYYY,A,B,C,N,Data,V*HH |
XX:使用的设备,“AI”是船载标志,“BS”是基站标志。
YYY:语句类型,VDM表示封装的是他船信息,VDO表示封装的是本船信息。
A:电文的长度可能很长,需要几句语句。此处规定了发送本条信息需要的报文条数(1-9)。
B:本条报文的序列数(1-9),此字段不能为空。
C:连续报文的识别码(0-9),给每一份新的多语句电文按序列指配编号,每次加1,计数到9后返回0,对要求多语句的电文,电文的每一句包含同样序列的电文号,它用于识别包含同一电文各个部分的语句。这样,使其他语句可以与包含该同一电文的各语句相互穿插。在电文可以使用一个语句时,该字段为空。
N:AIS的信道指示为“A”或“B”,报文是从信道“A”还是“B”接收。本信道指示与接收该数据包时与AIS的运行状态有关。当不提供频道识别时,本数据为空。信道“A”或“B”的VHF信道号,可用AIS的一个ACA语句查询得到。
Data:封装的数据部分,封装的最大长度的限制是语句的总字符数不超过82.对于用多语句传送的电文,本字段支持最多62个有效字符。而对于单语句传送的电文,最多为63个有效字符。
V:填充位数(比特数),二进制比特数必须是6的倍数,如果不是,要加入1-5个填充比特。本参数指示加到最后一个6比特编码字符上的比特数。未加入填充比特时,本数值为0,本字段不可以为空。(即填充字符,由于每条消息语句总位数必须是6的整数倍,否则需填充0-5个字符)
HH:检验字段。AIS数据采用8位CRC,取其8位CRC校验码的高四位,并转化为16进制数,构成AIS校验码的第一位,取其8位CRC校验码的低四位,转化为16进制数后构成校验码的第二位。当AIS接收设备收到一条AIS电文后,按照8位CRC对其数据部分进行重新校验,生成的校验值如果与电文自带的校验值相同,说明电文数据在传输过程中没有出错。如果不同,则说明数据在传输过程中出错了。
< CR > < LF >:语句结束标志。
AIS数据的详细解析可以参照这篇文章:AIS (船舶自动识别系统Automatic Identification System)数据解析 | 码农家园 (codenong.com)
了解完上述格式我们就会发现,有些电文是需要多条语句发送的,这里就引出了第一个坑:需要多条语句发送的数据怎么解析?比如说第五类位置信息:第一条封装电文长度为56,第二条封装电文长度为15。例如:
!AIVDM,2,1,9,B,56:j0tP00003CW34000iD`TpTpLQDwCW340000160`V4540Ht0h000000000,0*68
!AIVDM,2,2,9,B,0000000000<,2*22
这个包是分2次发的,第一次传的是:
56:j0tP00003CW34000iD`TpTpLQDwCW340000160`V4540Ht0h000000000,
第二次传的是:0000000000,
完整包需要把这俩合并为:
56:j0tP00003CW34000iD`TpTpLQDwCW340000160`V4540Ht0h0000000000000000000
也就是说,我们需要先将两条语句封装的报文各自截取下来,然后再进行拼接,最后再解析。
typedef struct{
char Data[128];
char MSG_serial_number[8];
uint8_t State;
}AIS_DATA_FIVE;
我的方法是先将接收到的第五类信息的第一条语句保存在上面这个结构体中,然后等待第二条语句的到来(这里就用到了上面说到的除了封装的报文之外的其他信息)
具体的操作方法是,将除封装的电文之外的其他信息暂时保存在AIS_VDM_TYPE结构体中,当检测到接收到的语句的总语句数为多条时,就将这条语句的语句识别码暂时保存在MSG_serial_number中,然后将封装的电文保存在Data字符串中。并将标志位State置1,表示已经接收到第一条语句,正在等待第二条语句。
typedefstruct{
charMSG_Num[1];//总语句数
charMSG_Type[4];//语句序列号
charMSG_serial_number[8];//语句识别码
charChannel[4];//AIS通道
}AIS_VDM_TYPE;
当第二条语句到来时,通过比对两条语句的语句识别码,判断两者是否来自同一AIS信息,如果是的话,就将两者封装的报文进行拼接,并传给解析函数进行数据解析。同时将标志位State置0,以便下一次接收
if((Ais_Data_Five.State == 1)&&(strcmp(VDM_DATA.MSG_serial_number,Ais_Data_Five.MSG_serial_number) == 0))
{
strcat(Ais_Data_Five.Data,AIVDM_DATA);
if(VDM_DATA.MSG_Type[0] == 0x31)
{
AISGet((unsigned char *) Ais_Data_Five.Data,71);
}
Ais_Data_Five.State = 0;
}
对于AIS数据的解析,首先我们需要知道AIS数据是使用6位ASCII码进行传输的,也就是说每个字节6位,在数据发送时包处理函数就对其进行了编码。
如上图所示,标注部分即为对6位ASCII字符的操作,因此我们在解码的时候需要进行逆操作,while循环的步骤即为将所有数据位按照6位/字节的形式进行保存,每个字节的低6位为有效数据,高2位为0。(对于数据不满6的整数倍会进行字符填充,我们不需要额外关注)
在数据接收完毕后,解析之前需要使用以下语句对数据进行解码,并将数据保存在msg_out数组中,数组中的每个字符的低6位为待解析的数据,高2位为0;
for(i=0;i0x77) break;
if(msg[i]>0x57&&msg[i]<0x60) break;
if(msg[i]>=0x30&&msg[i]<=0x57)
{
msg_out[i] = msg[i]-48;
msg_out[i] &= 0x3f;
}
if(msg[i]>=0x60&&msg[i]<=0x77)
{
msg_out[i] = msg[i]-56;
msg_out[i] &= 0x3f;
}
}
第一个判断是因为经过编码的文本字符只可能为0x30到0x77间的可显示字符。
第二个判断与第三个判断是由于在编码表中,我们可以看到,0x57以后并不是0x58,而是0x60,所以在0x58到0x5F间的字符是无效字符。
这里借鉴了大佬“快乐鹦鹉”的博客:
https://blog.csdn.net/happyparrot/article/details/1585185
在将数据解码并保存后,我们就要对照AIS技术手册《AIVDM/AIVDO协议解码》来进行数据解析了,首先要做的是判断这一条AIS语句是哪类消息,然后根据手册去找到这类消息所封装的数据所占的数据位是什么,再进行位运算解析。每个类别的AIS消息的前6位,即0-5位所代表的信息是固定的,都是MessageType,也就是本条消息的消息类别。这里就以1、2、3类消息为例,这三类消息的协议是相同的。
上文提到,我们将数据保存在msg_out字符数组中,每6位数据占一个字符,因此0-5位数据刚好位于msg_out[0]的低6位。所以我们需要做的就是取其低6位
AIS_DATA.MSG_TYPE = msg_out[0]&0x3f;
通过这样一个简单的位运算就可以得到我们想要的数据。通过这种方法,我们也可以去截取其他信息了。比如Repeat Indicator,位于第6-7位,它在msg_out数组的第二个字符中,是该字符的第5位和第4位也就是00XX0000。因此我们就用位运算取该字符的第4、5位,并将它们左移4位,就得到了000000XX的数据,可以直接用了。
AIS_DATA.Repeat_Indicator = (((msg_out[1] & 0x30)>>4)&0x3);
所有的数据都是需要对比协议手册,按照上述方法进行解析的,一定不能错位。下面就把我解析1、2、3类消息的代码分享出来做个参考。
long message = 0x0;
AIS_DATA.Navigation_Status = (msg_out[6] & 0x0f);
AIS_DATA.ROT = (((msg_out[7] & 0x3f)<<2) | ((msg_out[8] & 0x30)>>4));
message = 0x0;
message = (((msg_out[8] & 0x0f)<<6) | (msg_out[9] & 0x3f));
AIS_DATA.SOG = message/10.0;
AIS_DATA.Position_Accuracy = ((msg_out[10] & 0x20)>>5);
message = 0x0;
message = (((msg_out[10] & 0x1f)<<23) | ((msg_out[11] & 0x3f)<<17) | ((msg_out[12] & 0x3f)<<11) | ((msg_out[13] & 0x3f)<<5) | ((msg_out[14] & 0x3e)>>1));
if((msg_out[10] & 0x10) == 0)
{
AIS_DATA.Longtitude = message/10000.0/60.0;
}
else
{
message = (~message)&0x7ffffff - 1;
message = message*-1;
AIS_DATA.Longtitude = message/10000.0/60.0;
}
message = 0x0;
message = ( ((msg_out[14] & 0x1)<<26) | ((msg_out[15] & 0x3f)<<20) | ((msg_out[16] & 0x3f)<<14) | ((msg_out[17] & 0x3f)<<8) | ((msg_out[18] & 0x3f)<<2) | ((msg_out[19] & 0x30)>>4) );
if((msg_out[14] & 0x01) == 0)
{
AIS_DATA.Latitude = message/10000.0/60.0;
}
else
{
message = (~message)&0x7ffffff - 1;
message = message*-1;
AIS_DATA.Latitude = message/10000.0/60.0;
}
message = 0x0;
message = (((msg_out[19] & 0x0f)<<8) | ((msg_out[20] & 0x3f)<<2) | ((msg_out[21] & 0x30)>>4));
AIS_DATA.COG = message/10;
AIS_DATA.True_Heading = (((msg_out[21] & 0x0f)<<5) | ((msg_out[22] & 0x3e)>>1));
值得注意的是有一些数据是需要进行运算之后才可以使用的,比如对地航向、对地速度,同时经度纬度是需要根据封装的报文来判断正负的。
实际操作中,发现按照上面的操作转化出来的数据,有一些打印出来是乱码,经过仔细对比发现对于本身就是字符的数据,是不能仅仅通过位运算来解析的。换句话讲,对于船名、呼号等数据,它本身就是一个字符串,只不过这个字符串是6位/字节的。那么我们需要做的就是同样使用字符串来承接,原本有多少个字符,就需要使用多少个字符来承接。
比如船名,从协议中可以看到它占120位,也就是20个6位/字节的字符,那么我们就需要使用20个字节来承接。再比如呼号,它是42位,也就是7个6位/字节的字符,那么我们就需要使用7个字节来承接。以第五类消息为例:
它的呼号位于70-111位,是一个二进制的字符串,需要分隔开,每组6位。船名位于112-231位,也是一个二进制字符串,需要分隔开,每组6位。但是这样的话接收完成的字符串的每一个字节都是00XXXXXX的形式,这显然不是正常的8位/字节的字符。查看6比特ASCII与标准ASCII码对照表不难发现规律:
对于6比特ASCII码,如果它的十进制ASCII码值大于等于32小于等于63,那么它所对应的标准ASCII码值十进制还是它本身;即将二进制数据XXXXXX转化为00XXXXXX;
对于6比特ASCII码,如果它的十进制ASCII码值小于等于31,那么它所对应的标准ASCII码值十进制需要加上64,也就是二进制需要data |= 0X40;即将二进制数据XXXXXX转化为01XXXXXX;
有了这样的发现,我们就可以用以下语句进行6比特ASCII码转标准ASCII码了。
for(i=0;i<7;i++){
AIS_DATA.Call_Sign[i] = ((msg_out[i+12]&0x3c)>>2)|((msg_out[i+11]&0x3)<<4);
if((AIS_DATA.Call_Sign[i]<=0x1f) &&(AIS_DATA.Call_Sign[i]>0x00)){
AIS_DATA.Call_Sign[i] |= 0x40;
}
}
for(i=0;i<20;i++){
AIS_DATA.Vessel_Name[i] = ((msg_out[i+19]&0x3c)>>2)|((msg_out[i+18]&0x3)<<4);
if((AIS_DATA.Vessel_Name[i]<=0x1f) && (AIS_DATA.Vessel_Name[i]>0x00)){
AIS_DATA.Vessel_Name[i] |= 0x40;
}
}
对于ASCII码转化以及其他信息的详细解释,博主“最强国服貂蝉”的博客记载的十分详细:
https://blog.csdn.net/zrg_hzr_1/article/details/111029286
由于该项目持续一月时间,并且所涉及的不仅仅是数据接收和解析的模块,所以暂时只想到这么多,后面如果发现新的问题会再次补充。
最后推荐一个AIS在线解码网站:AIS Online Decoder. AIVDM & AIVDO NMEA Messages (aggsoft.com)