mirror of https://github.com/wb2osz/direwolf.git
409 lines
12 KiB
C
409 lines
12 KiB
C
|
|
||
|
// 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 <http://www.gnu.org/licenses/>.
|
||
|
//
|
||
|
|
||
|
|
||
|
/********************************************************************************
|
||
|
*
|
||
|
* 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 <stdio.h>
|
||
|
#include <stdlib.h>
|
||
|
#include <assert.h>
|
||
|
#include <ctype.h>
|
||
|
#include <string.h>
|
||
|
|
||
|
#include "textcolor.h"
|
||
|
#include "ais.h"
|
||
|
|
||
|
|
||
|
/*-------------------------------------------------------------------
|
||
|
*
|
||
|
* 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);
|
||
|
}
|
||
|
|
||
|
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.
|
||
|
return ((float)get_field_signed(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_signed(base, start, len) * 0.1);
|
||
|
}
|
||
|
|
||
|
|
||
|
/*-------------------------------------------------------------------
|
||
|
*
|
||
|
* 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.
|
||
|
* ofcourse direction of travel.
|
||
|
*
|
||
|
* Returns: 0 for success, -1 for error.
|
||
|
*
|
||
|
*--------------------------------------------------------------------*/
|
||
|
|
||
|
// Maximum NMEA sentence length is 82 according to some people.
|
||
|
// 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)
|
||
|
{
|
||
|
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;
|
||
|
|
||
|
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);
|
||
|
switch (type) {
|
||
|
|
||
|
case 1: // Position Report Class A
|
||
|
case 2:
|
||
|
case 3:
|
||
|
|
||
|
snprintf (descr, descr_size, "AIS %d: Position Report Class A", type);
|
||
|
snprintf (mssi, mssi_size, "%d", get_field(ais, 8, 30));
|
||
|
*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);
|
||
|
snprintf (mssi, mssi_size, "%d", get_field(ais, 8, 30));
|
||
|
//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 18: // Standard Class B CS Position Report
|
||
|
|
||
|
snprintf (descr, descr_size, "AIS %d: Standard Class B CS Position Report", type);
|
||
|
snprintf (mssi, mssi_size, "%d", get_field(ais, 8, 30));
|
||
|
*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);
|
||
|
snprintf (mssi, mssi_size, "%d", get_field(ais, 8, 30));
|
||
|
*odlon = get_field_latlon(ais, 57, 28);
|
||
|
*odlat = get_field_latlon(ais, 85, 27);
|
||
|
break;
|
||
|
|
||
|
default:
|
||
|
snprintf (descr, descr_size, "AIS message type %d", type);
|
||
|
break;
|
||
|
}
|
||
|
|
||
|
return (0);
|
||
|
|
||
|
} /* end ais_parse */
|
||
|
|
||
|
// end ais.c
|