// This file is part of Dire Wolf, an amateur radio packet TNC.
//
// Copyright (C) 2020 John Langner, WB2OSZ
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 2 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
//
/********************************************************************************
*
* File: ais.c
*
* Purpose: Functions for processing received AIS transmissions and
* converting to NMEA sentence representation.
*
* References: AIVDM/AIVDO protocol decoding by Eric S. Raymond
* https://gpsd.gitlab.io/gpsd/AIVDM.html
*
* Sample recording with about 100 messages. Test with "atest -B AIS xxx.wav"
* https://github.com/freerange/ais-on-sdr/wiki/example-data/long-beach-160-messages.wav
*
* Useful on-line decoder for AIS NMEA sentences.
* https://www.aggsoft.com/ais-decoder.htm
*
* Future? Add an interface to feed AIS data into aprs.fi.
* https://aprs.fi/page/ais_feeding
*
*******************************************************************************/
#include "direwolf.h"
#include
#include
#include
#include
#include
#include "textcolor.h"
#include "ais.h"
// Lengths, in bits, for the AIS message types.
#define NUM_TYPES 27
static const struct {
short min;
short max;
} valid_len[NUM_TYPES+1] = {
{ -1, -1 }, // 0 not used
{ 168, 168 }, // 1
{ 168, 168 }, // 2
{ 168, 168 }, // 3
{ 168, 168 }, // 4
{ 424, 424 }, // 5
{ 72, 1008 }, // 6 multipurpose
{ 72, 168 }, // 7 increments of 32 bits
{ 168, 1008 }, // 8 multipurpose
{ 168, 168 }, // 9
{ 72, 72 }, // 10
{ 168, 168 }, // 11
{ 72, 1008 }, // 12
{ 72, 168 }, // 13 increments of 32 bits
{ 40, 1008 }, // 14
{ 88, 160 }, // 15
{ 96, 114 }, // 16 96 or 114, not range
{ 80, 816 }, // 17
{ 168, 168 }, // 18
{ 312, 312 }, // 19
{ 72, 160 }, // 20
{ 272, 360 }, // 21
{ 168, 168 }, // 22
{ 160, 160 }, // 23
{ 160, 168 }, // 24
{ 40, 168 }, // 25
{ 60, 1064 }, // 26
{ 96, 168 } // 27 96 or 168, not range
};
/*-------------------------------------------------------------------
*
* Functions to get and set element of a bit vector.
*
*--------------------------------------------------------------------*/
static const unsigned char mask[8] = { 0x80, 0x40, 0x20, 0x10, 0x08, 0x04, 0x02, 0x01 };
static inline unsigned int get_bit (unsigned char *base, unsigned int offset)
{
return ( (base[offset >> 3] & mask[offset & 0x7]) != 0);
}
static inline void set_bit (unsigned char *base, unsigned int offset, int val)
{
if (val) {
base[offset >> 3] |= mask[offset & 0x7];
}
else {
base[offset >> 3] &= ~ mask[offset & 0x7];
}
}
/*-------------------------------------------------------------------
*
* Extract a variable length field from a bit vector.
*
*--------------------------------------------------------------------*/
static unsigned int get_field (unsigned char *base, unsigned int start, unsigned int len)
{
unsigned int result = 0;
for (int k = 0; k < len; k++) {
result <<= 1;
result |= get_bit (base, start + k);
}
return (result);
}
static void set_field (unsigned char *base, unsigned int start, unsigned int len, unsigned int val)
{
for (int k = 0; k < len; k++) {
set_bit (base, start + k, (val >> (len - 1 - k) ) & 1);
}
}
static int get_field_signed (unsigned char *base, unsigned int start, unsigned int len)
{
int result = (int) get_field(base, start, len);
// Sign extend.
result <<= (32 - len);
result >>= (32 - len);
return (result);
}
static double get_field_latlon (unsigned char *base, unsigned int start, unsigned int len)
{
// Latitude of 0x3412140 (91 deg) means not available.
// Longitude of 0x6791AC0 (181 deg) means not available.
return ((double)get_field_signed(base, start, len) / 600000.0);
// Message type 27 uses lower resolution, 17 & 18 bits rather than 27 & 28.
// It encodes minutes/10 rather than normal minutes/10000.
}
static float get_field_speed (unsigned char *base, unsigned int start, unsigned int len)
{
// Raw 1023 means not available.
// Multiply by 0.1 to get knots.
// For aircraft it is knots, not deciknots.
return ((float)get_field(base, start, len) * 0.1);
}
static float get_field_course (unsigned char *base, unsigned int start, unsigned int len)
{
// Raw 3600 means not available.
// Multiply by 0.1 to get degrees
return ((float)get_field(base, start, len) * 0.1);
}
static int get_field_ascii (unsigned char *base, unsigned int start, unsigned int len)
{
assert (len == 6);
int ch = get_field(base, start, len);
if (ch < 32) ch += 64;
return (ch);
}
static void get_field_string (unsigned char *base, unsigned int start, unsigned int len, char *result)
{
assert (len % 6 == 0);
int nc = len / 6; // Number of characters.
// Caller better provide space for at least this +1.
// No bounds checking here.
for (int i = 0; i < nc; i++) {
result[i] = get_field_ascii (base, start + i * 6, 6);
}
result[nc] = '\0';
// Officially it should be terminated/padded with @ but we also see trailing spaces.
char *p = strchr(result, '@');
if (p != NULL) *p = '\0';
for (int k = strlen(result) - 1; k >= 0 && result[k] == ' '; k--) {
result[k] = '\0';
}
}
/*-------------------------------------------------------------------
*
* Convert between 6 bit values and printable characters used in
* in the AIS NMEA sentences.
*
*--------------------------------------------------------------------*/
// Characters '0' thru 'W' become values 0 thru 39.
// Characters '`' thru 'w' become values 40 thru 63.
static int char_to_sextet (char ch)
{
if (ch >= '0' && ch <= 'W') {
return (ch - '0');
}
else if (ch >= '`' && ch <= 'w') {
return (ch - '`' + 40);
}
else {
text_color_set(DW_COLOR_ERROR);
dw_printf ("Invalid character \"%c\" found in AIS NMEA sentence payload.\n", ch);
return (0);
}
}
// Values 0 thru 39 become characters '0' thru 'W'.
// Values 40 thru 63 become characters '`' thru 'w'.
// This is known as "Payload Armoring."
static int sextet_to_char (int val)
{
if (val >= 0 && val <= 39) {
return ('0' + val);
}
else if (val >= 40 && val <= 63) {
return ('`' + val - 40);
}
else {
text_color_set(DW_COLOR_ERROR);
dw_printf ("Invalid 6 bit value %d from AIS HDLC payload.\n", val);
return ('0');
}
}
/*-------------------------------------------------------------------
*
* Convert AIS binary block (from HDLC frame) to NMEA sentence.
*
* In: Pointer to AIS binary block and number of bytes.
* Out: NMEA sentence. Provide size to avoid string overflow.
*
*--------------------------------------------------------------------*/
void ais_to_nmea (unsigned char *ais, int ais_len, char *nmea, int nmea_size)
{
char payload[256];
// Number of resulting characters for payload.
int ns = (ais_len * 8 + 5) / 6;
if (ns+1 > sizeof(payload)) {
text_color_set(DW_COLOR_ERROR);
dw_printf ("AIS HDLC payload of %d bytes is too large.\n", ais_len);
ns = sizeof(payload) - 1;
}
for (int k = 0; k < ns; k++) {
payload[k] = sextet_to_char(get_field(ais, k*6, 6));
}
payload[ns] = '\0';
strlcpy (nmea, "!AIVDM,1,1,,A,", nmea_size);
strlcat (nmea, payload, nmea_size);
// If the number of bytes in is not a multiple of 3, this does not
// produce a whole number of characters out. Extra padding bits were
// added to get the last character. Include this number so the
// decoding application can drop this number of bits from the end.
// At least, I think that is the way it should work.
// The examples all have 0.
char pad_bits[8];
snprintf (pad_bits, sizeof(pad_bits), ",%d", ns * 6 - ais_len * 8);
strlcat (nmea, pad_bits, nmea_size);
// Finally the NMEA style checksum.
int cs = 0;
for (char *p = nmea + 1; *p != '\0'; p++) {
cs ^= *p;
}
char checksum[8];
snprintf (checksum, sizeof(checksum), "*%02X", cs & 0x7f);
strlcat (nmea, checksum, nmea_size);
}
/*-------------------------------------------------------------------
*
* Name: ais_parse
*
* Purpose: Parse AIS sentence and extract interesing parts.
*
* Inputs: sentence NMEA sentence.
*
* quiet Suppress printing of error messages.
*
* Outputs: descr Description of AIS message type.
* mssi 9 digit identifier.
* odlat latitude.
* odlon longitude.
* ofknots speed, knots.
* ofcourse direction of travel.
* ofalt_m altitude, meters.
* symtab APRS symbol table.
* symbol APRS symbol code.
*
* Returns: 0 for success, -1 for error.
*
*--------------------------------------------------------------------*/
// Maximum NMEA sentence length is 82, including CR/LF.
// Make buffer considerably larger to be safe.
#define NMEA_MAX_LEN 240
int ais_parse (char *sentence, int quiet, char *descr, int descr_size, char *mssi, int mssi_size, double *odlat, double *odlon,
float *ofknots, float *ofcourse, float *ofalt_m, char *symtab, char *symbol, char *comment, int comment_size)
{
char stemp[NMEA_MAX_LEN]; /* Make copy because parsing is destructive. */
strlcpy (mssi, "?", mssi_size);
*odlat = G_UNKNOWN;
*odlon = G_UNKNOWN;
*ofknots = G_UNKNOWN;
*ofcourse = G_UNKNOWN;
*ofalt_m = G_UNKNOWN;
strlcpy (stemp, sentence, sizeof(stemp));
// Verify and remove checksum.
unsigned char cs = 0;
char *p;
for (p = stemp+1; *p != '*' && *p != '\0'; p++) {
cs ^= *p;
}
p = strchr (stemp, '*');
if (p == NULL) {
if ( ! quiet) {
text_color_set (DW_COLOR_INFO);
dw_printf("Missing AIS sentence checksum.\n");
}
return (-1);
}
if (cs != strtoul(p+1, NULL, 16)) {
if ( ! quiet) {
text_color_set (DW_COLOR_ERROR);
dw_printf("AIS sentence checksum error. Expected %02x but found %s.\n", cs, p+1);
}
return (-1);
}
*p = '\0'; // Remove the checksum.
// Extract the comma separated fields.
char *next;
char *talker; /* Expecting !AIVDM */
char *frag_count; /* ignored */
char *frag_num; /* ignored */
char *msg_id; /* ignored */
char *radio_chan; /* ignored */
char *payload; /* Encoded as 6 bits per character. */
char *fill_bits; /* Number of bits to discard. */
next = stemp;
talker = strsep(&next, ",");
frag_count = strsep(&next, ",");
frag_num = strsep(&next, ",");
msg_id = strsep(&next, ",");
radio_chan = strsep(&next, ",");
payload = strsep(&next, ",");
fill_bits = strsep(&next, ",");
/* Suppress the 'set but not used' compiler warnings. */
/* Alternatively, we might use __attribute__((unused)) */
(void)(talker);
(void)(frag_count);
(void)(frag_num);
(void)(msg_id);
(void)(radio_chan);
if (payload == NULL || strlen(payload) == 0) {
if ( ! quiet) {
text_color_set (DW_COLOR_ERROR);
dw_printf("Payload is missing from AIS sentence.\n");
}
return (-1);
}
// Convert character representation to bit vector.
unsigned char ais[256];
memset (ais, 0, sizeof(ais));
int plen = strlen(payload);
for (int k = 0; k < plen; k++) {
set_field (ais, k*6, 6, char_to_sextet(payload[k]));
}
// Verify number of filler bits.
int nfill = atoi(fill_bits);
int nbytes = (plen * 6) / 8;
if (nfill != plen * 6 - nbytes * 8) {
if ( ! quiet) {
text_color_set (DW_COLOR_ERROR);
dw_printf("Number of filler bits is %d when %d is expected.\n",
nfill, plen * 6 - nbytes * 8);
}
}
// Extract the fields of interest from a few message types.
// Don't get too carried away.
int type = get_field(ais, 0, 6);
if (type >= 1 && type <= 27) {
snprintf (mssi, mssi_size, "%d", get_field(ais, 8, 30));
}
switch (type) {
case 1: // Position Report Class A
case 2:
case 3:
snprintf (descr, descr_size, "AIS %d: Position Report Class A", type);
*symtab = '/';
*symbol = 's'; // Power boat (ship) side view
*odlon = get_field_latlon(ais, 61, 28);
*odlat = get_field_latlon(ais, 89, 27);
*ofknots = get_field_speed(ais, 50, 10);
*ofcourse = get_field_course(ais, 116, 12);
break;
case 4: // Base Station Report
snprintf (descr, descr_size, "AIS %d: Base Station Report", type);
*symtab = '\\';
*symbol = 'L'; // Lighthouse
//year = get_field(ais, 38, 14);
//month = get_field(ais, 52, 4);
//day = get_field(ais, 56, 5);
//hour = get_field(ais, 61, 5);
//minute = get_field(ais, 66, 6);
//second = get_field(ais, 72, 6);
*odlon = get_field_latlon(ais, 79, 28);
*odlat = get_field_latlon(ais, 107, 27);
break;
case 5: // Static and Voyage Related Data
snprintf (descr, descr_size, "AIS %d: Static and Voyage Related Data", type);
*symtab = '/';
*symbol = 's'; // Power boat (ship) side view
{
char callsign[12];
char shipname[24];
char destination[24];
get_field_string(ais, 70, 42, callsign);
get_field_string(ais, 112, 120, shipname);
get_field_string(ais, 302, 120, destination);
if (strlen(destination) > 0) {
snprintf (comment, comment_size, "%s, %s, dest. %s", shipname, callsign, destination);
}
else {
snprintf (comment, comment_size, "%s, %s", shipname, callsign);
}
}
break;
case 9: // Standard SAR Aircraft Position Report
snprintf (descr, descr_size, "AIS %d: SAR Aircraft Position Report", type);
*symtab = '/';
*symbol = '\''; // Small AIRCRAFT
*ofalt_m = get_field(ais, 38, 12); // meters, 4095 means not available
*odlon = get_field_latlon(ais, 61, 28);
*odlat = get_field_latlon(ais, 89, 27);
*ofknots = get_field_speed(ais, 50, 10) * 10.0; // plane is knots, not knots/10
*ofcourse = get_field_course(ais, 116, 12);
break;
case 18: // Standard Class B CS Position Report
// As an oversimplification, Class A is commercial, B is recreational.
snprintf (descr, descr_size, "AIS %d: Standard Class B CS Position Report", type);
*symtab = '/';
*symbol = 'Y'; // YACHT (sail)
*odlon = get_field_latlon(ais, 57, 28);
*odlat = get_field_latlon(ais, 85, 27);
break;
case 19: // Extended Class B CS Position Report
snprintf (descr, descr_size, "AIS %d: Extended Class B CS Position Report", type);
*symtab = '/';
*symbol = 'Y'; // YACHT (sail)
*odlon = get_field_latlon(ais, 57, 28);
*odlat = get_field_latlon(ais, 85, 27);
break;
case 27: // Long Range AIS Broadcast message
snprintf (descr, descr_size, "AIS %d: Long Range AIS Broadcast message", type);
*symtab = '\\';
*symbol = 's'; // OVERLAY SHIP/boat (top view)
*odlon = get_field_latlon(ais, 44, 18) * 1000; // Note: minutes/10 rather than usual /10000.
*odlat = get_field_latlon(ais, 62, 17) * 1000;
*ofknots = get_field_speed(ais, 79, 6) * 10; // Note: knots, not deciknots.
*ofcourse = get_field_course(ais, 85, 9) * 10; // Note: degrees, not decidegrees.
break;
default:
snprintf (descr, descr_size, "AIS message type %d", type);
break;
}
return (0);
} /* end ais_parse */
/*-------------------------------------------------------------------
*
* Name: ais_check_length
*
* Purpose: Verify frame length against expected.
*
* Inputs: type Message type, 1 - 27.
*
* length Number of data octets in in frame.
*
* Returns: -1 Invalid message type.
* 0 Good length.
* 1 Unexpected lenth.
*
*--------------------------------------------------------------------*/
int ais_check_length (int type, int length)
{
if (type >= 1 && type <= NUM_TYPES) {
int b = length * 8;
if (b >= valid_len[type].min && b <= valid_len[type].max) {
return (0); // Good.
}
else {
//text_color_set (DW_COLOR_ERROR);
//dw_printf("AIS ERROR: type %d, has %d bits when %d to %d expected.\n",
// type, b, valid_len[type].min, valid_len[type].max);
return (1); // Length out of range.
}
}
else {
//text_color_set (DW_COLOR_ERROR);
//dw_printf("AIS ERROR: message type %d is invalid.\n", type);
return (-1); // Invalid type.
}
} // end ais_check_length
// end ais.c