From 0dc7cba1c565964ad163cb1dcd63bd697b5310d3 Mon Sep 17 00:00:00 2001 From: wb2osz Date: Sun, 19 Apr 2020 00:50:18 -0400 Subject: [PATCH] AIS reception. --- CHANGES.md | 6 +- src/CMakeLists.txt | 4 + src/ais.c | 409 ++++++++++++++++++++++++++++++++++++++++++++ src/ais.h | 5 + src/atest.c | 16 +- src/audio.h | 9 +- src/config.c | 71 +++++++- src/decode_aprs.c | 92 ++++++---- src/demod_9600.c | 9 +- src/demod_9600.h | 2 +- src/direwolf.c | 21 ++- src/hdlc_rec2.c | 11 +- src/multi_modem.c | 19 +- src/version.h | 12 ++ test/CMakeLists.txt | 6 + 15 files changed, 648 insertions(+), 44 deletions(-) create mode 100644 src/ais.c create mode 100644 src/ais.h diff --git a/CHANGES.md b/CHANGES.md index 0b9f8d0..28f4dd9 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -10,13 +10,15 @@ - Rather than trying to keep a bunch of different platform specific Makefiles in sync, "cmake" is now used for greater portability and easier maintenance. - +- README.md has a quick summary of the process. More details in the User Guide. ### New Features: ### -- "-X" option enables FX.25 transmission. FX.25 reception is always enabled so you don't need to do anything special. +- "-X" option enables FX.25 transmission. FX.25 reception is always enabled so you don't need to do anything special. "What is FX.25?" you might ask. It is forward error correction (FEC) added in a way that is completely compatible with an ordinary AX.25 frame. See new document ***AX25\_plus\_FEC\_equals\_FX25.pdf*** for details. + +- Receive AIS location data from ships. Enable by using "-B AIS" command line option or "MODEM AIS" in the configuration file. AIS NMEA sentences are encapsulated in APRS user-defined data with a "{DA" prefix. This uses 9600 bps so you need to use wide band audio, not what comes out of the speaker. - "-t" option now accepts more values to accommodate inconsistent handling of text color control codes by different terminal emulators. The default, 1, should work with most modern terminal types. If the colors are not right, try "-t 9" to see the result of the different choices and pick the best one. If none of them look right, file a bug report and specify: operating system version (e.g. Raspbian Buster), terminal emulator type and version (e.g. LXTerminal 0.3.2). Include a screen capture. diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 474957c..4e02a13 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -20,6 +20,7 @@ endif() # direwolf list(APPEND direwolf_SOURCES direwolf.c + ais.c aprs_tt.c audio_stats.c ax25_link.c @@ -49,6 +50,7 @@ list(APPEND direwolf_SOURCES fx25_init.c fx25_rec.c fx25_send.c + fx25_auto.c gen_tone.c hdlc_rec.c hdlc_rec2.c @@ -136,6 +138,7 @@ endif() # decode_aprs list(APPEND decode_aprs_SOURCES decode_aprs.c + ais.c kiss_frame.c ax25_pad.c dwgpsnmea.c @@ -287,6 +290,7 @@ target_link_libraries(gen_packets # atest list(APPEND atest_SOURCES atest.c + ais.c demod.c demod_afsk.c demod_psk.c diff --git a/src/ais.c b/src/ais.c new file mode 100644 index 0000000..9386210 --- /dev/null +++ b/src/ais.c @@ -0,0 +1,409 @@ + +// 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" + + +/*------------------------------------------------------------------- + * + * 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 \ No newline at end of file diff --git a/src/ais.h b/src/ais.h new file mode 100644 index 0000000..abc259f --- /dev/null +++ b/src/ais.h @@ -0,0 +1,5 @@ + + +void ais_to_nmea (unsigned char *ais, int ais_len, char *nema, int nema_size); + +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); \ No newline at end of file diff --git a/src/atest.c b/src/atest.c index b1afaeb..e395c09 100644 --- a/src/atest.c +++ b/src/atest.c @@ -270,7 +270,13 @@ int main (int argc, char *argv[]) case 'B': /* -B for data Bit rate */ /* Also implies modem type based on speed. */ - B_opt = atoi(optarg); + /* Special case "AIS" rather than number. */ + if (strcasecmp(optarg, "AIS") == 0) { + B_opt = 12345; // See special case below. + } + else { + B_opt = atoi(optarg); + } break; case 'g': /* -G Force G3RUH regardless of speed. */ @@ -447,6 +453,13 @@ int main (int argc, char *argv[]) my_audio_config.achan[0].space_freq = 0; strlcpy (my_audio_config.achan[0].profiles, "", sizeof(my_audio_config.achan[0].profiles)); } + else if (my_audio_config.achan[0].baud == 12345) { + my_audio_config.achan[0].modem_type = MODEM_AIS; + my_audio_config.achan[0].baud = 9600; + my_audio_config.achan[0].mark_freq = 0; + my_audio_config.achan[0].space_freq = 0; + strlcpy (my_audio_config.achan[0].profiles, " ", sizeof(my_audio_config.achan[0].profiles)); // avoid getting default later. + } else { my_audio_config.achan[0].modem_type = MODEM_SCRAMBLE; my_audio_config.achan[0].mark_freq = 0; @@ -890,6 +903,7 @@ static void usage (void) { dw_printf (" 2400 bps uses QPSK based on V.26 standard.\n"); dw_printf (" 4800 bps uses 8PSK based on V.27 standard.\n"); dw_printf (" 9600 bps and up uses K9NG/G3RUH standard.\n"); + dw_printf (" AIS for ship Automatic Identification System.\n"); dw_printf ("\n"); dw_printf (" -g Use G3RUH modem rather rather than default for data rate.\n"); dw_printf (" -j 2400 bps QPSK compatible with direwolf <= 1.5.\n"); diff --git a/src/audio.h b/src/audio.h index a4a2ecb..08dc48f 100644 --- a/src/audio.h +++ b/src/audio.h @@ -112,6 +112,13 @@ struct audio_s { /* Initially this applies to all channels. */ /* This should probably be per channel. One step at a time. */ + int fx25_auto_enable; /* Turn on FX.25 for current connected mode session */ + /* under poor conditions. */ + /* Set to 0 to disable feature. */ + /* I put it here, rather than with the rest of the link layer */ + /* parameters because it is really a part of the HDLC layer */ + /* and is part of the KISS TNC functionality rather than our data link layer. */ + char timestamp_format[40]; /* -T option */ /* Precede received & transmitted frames with timestamp. */ /* Command line option uses "strftime" format string. */ @@ -141,7 +148,7 @@ struct audio_s { /* Could all be the same or different. */ - enum modem_t { MODEM_AFSK, MODEM_BASEBAND, MODEM_SCRAMBLE, MODEM_QPSK, MODEM_8PSK, MODEM_OFF, MODEM_16_QAM, MODEM_64_QAM } modem_type; + enum modem_t { MODEM_AFSK, MODEM_BASEBAND, MODEM_SCRAMBLE, MODEM_QPSK, MODEM_8PSK, MODEM_OFF, MODEM_16_QAM, MODEM_64_QAM, MODEM_AIS } modem_type; /* Usual AFSK. */ /* Baseband signal. Not used yet. */ diff --git a/src/config.c b/src/config.c index 6a207ef..5b9dc34 100644 --- a/src/config.c +++ b/src/config.c @@ -799,6 +799,8 @@ void config_init (char *fname, struct audio_s *p_audio_config, p_audio_config->achan[channel].fulldup = DEFAULT_FULLDUP; } + p_audio_config->fx25_auto_enable = AX25_N2_RETRY_DEFAULT / 2; + /* First channel should always be valid. */ /* If there is no ADEVICE, it uses default device in mono. */ @@ -1274,7 +1276,12 @@ void config_init (char *fname, struct audio_s *p_audio_config, dw_printf ("Line %d: Missing data transmission speed for MODEM command.\n", line); continue; } - n = atoi(t); + if (strcasecmp(t,"AIS") == 0) { + n = 12345; // See special case later. + } + else { + n = atoi(t); + } if (n >= MIN_BAUD && n <= MAX_BAUD) { p_audio_config->achan[channel].baud = n; if (n != 300 && n != 1200 && n != 2400 && n != 4800 && n != 9600 && n != 19200) { @@ -1316,6 +1323,11 @@ void config_init (char *fname, struct audio_s *p_audio_config, p_audio_config->achan[channel].mark_freq = 0; p_audio_config->achan[channel].space_freq = 0; } + else if (p_audio_config->achan[channel].baud == 12345) { + p_audio_config->achan[channel].modem_type = MODEM_AIS; + p_audio_config->achan[channel].mark_freq = 0; + p_audio_config->achan[channel].space_freq = 0; + } else { p_audio_config->achan[channel].modem_type = MODEM_SCRAMBLE; p_audio_config->achan[channel].mark_freq = 0; @@ -2190,6 +2202,63 @@ void config_init (char *fname, struct audio_s *p_audio_config, } } +/* + * FX25TX n - Enable FX.25 transmission. Default off. + * 0 = off, 1 = auto mode, others are suggestions for testing + * or special cases. 16, 32, 64 is number of parity bytes to add. + * Also set by "-X n" command line option. + * Current a global setting. Could be per channel someday. + */ + + else if (strcasecmp(t, "FX25TX") == 0) { + int n; + t = split(NULL,0); + if (t == NULL) { + text_color_set(DW_COLOR_ERROR); + dw_printf ("Line %d: Missing FEC mode for FX25TX command.\n", line); + continue; + } + n = atoi(t); + if (n >= 0 && n < 200) { + p_audio_config->fx25_xmit_enable = n; + } + else { + p_audio_config->fx25_xmit_enable = 1; + text_color_set(DW_COLOR_ERROR); + dw_printf ("Line %d: Unreasonable value for FX.25 transmission mode. Using %d.\n", + line, p_audio_config->fx25_xmit_enable); + } + } + +/* + * FX25AUTO n - Enable Automatic use of FX.25 for connected mode. + * Automatically enable, for that session only, when an identical + * frame is sent more than this number of times. + * Default 5 based on half of default RETRY. + * 0 to disable feature. + * Current a global setting. Could be per channel someday. + */ + + else if (strcasecmp(t, "FX25AUTO") == 0) { + int n; + t = split(NULL,0); + if (t == NULL) { + text_color_set(DW_COLOR_ERROR); + dw_printf ("Line %d: Missing count for FX25AUTO command.\n", line); + continue; + } + n = atoi(t); + if (n >= 0 && n < 20) { + p_audio_config->fx25_auto_enable = n; + } + else { + p_audio_config->fx25_auto_enable = AX25_N2_RETRY_DEFAULT / 2; + text_color_set(DW_COLOR_ERROR); + dw_printf ("Line %d: Unreasonable count for connected mode automatic FX.25. Using %d.\n", + line, p_audio_config->fx25_auto_enable); + } + } + /* * ==================== APRS Digipeater parameters ==================== */ diff --git a/src/decode_aprs.c b/src/decode_aprs.c index def7bee..343535c 100644 --- a/src/decode_aprs.c +++ b/src/decode_aprs.c @@ -54,6 +54,7 @@ #include "dwgpsnmea.h" #include "decode_aprs.h" #include "telemetry.h" +#include "ais.h" #define TRUE 1 @@ -109,6 +110,7 @@ static void aprs_status_report (decode_aprs_t *A, char *, int); static void aprs_general_query (decode_aprs_t *A, char *, int, int quiet); static void aprs_directed_station_query (decode_aprs_t *A, char *addressee, char *query, int quiet); static void aprs_telemetry (decode_aprs_t *A, char *info, int info_len, int quiet); +static void aprs_user_defined (decode_aprs_t *A, char *, int); static void aprs_raw_touch_tone (decode_aprs_t *A, char *, int); static void aprs_morse_code (decode_aprs_t *A, char *, int); static void aprs_positionless_weather_report (decode_aprs_t *A, unsigned char *, int); @@ -232,6 +234,22 @@ void decode_aprs (decode_aprs_t *A, packet_t pp, int quiet) } } +/* + * Application might be in the destination field for most message types. + * MIC-E format has part of location in the destination field. + */ + + switch (*pinfo) { /* "DTI" data type identifier. */ + + case '\'': /* Old Mic-E Data */ + case '`': /* Current Mic-E Data */ + break; + + default: + decode_tocall (A, dest); + break; + } + switch (*pinfo) { /* "DTI" data type identifier. */ case '!': /* Position without timestamp (no APRS messaging). */ @@ -323,20 +341,8 @@ void decode_aprs (decode_aprs_t *A, packet_t pp, int quiet) break; case '{': /* user defined data */ - /* http://www.aprs.org/aprs11/expfmts.txt */ - if (strncmp((char*)pinfo, "{tt", 3) == 0) { - aprs_raw_touch_tone (A, (char*)pinfo, info_len); - } - else if (strncmp((char*)pinfo, "{mc", 3) == 0) { - aprs_morse_code (A, (char*)pinfo, info_len); - } - else if (strncmp((char*)pinfo, "{{", 2) == 0) { - snprintf (A->g_msg_type, sizeof(A->g_msg_type), "User-Defined Experimental"); - } - else { - //aprs_user_defined (A, pinfo, info_len); - } + aprs_user_defined (A, (char*)pinfo, info_len); break; case 't': /* Raw touch tone data - NOT PART OF STANDARD */ @@ -381,22 +387,6 @@ void decode_aprs (decode_aprs_t *A, packet_t pp, int quiet) symbols_from_dest_or_src (*pinfo, A->g_src, dest, &A->g_symbol_table, &A->g_symbol_code); } - -/* - * Application might be in the destination field for most message types. - * MIC-E format has part of location in the destination field. - */ - - switch (*pinfo) { /* "DTI" data type identifier. */ - - case '\'': /* Old Mic-E Data */ - case '`': /* Current Mic-E Data */ - break; - - default: - decode_tocall (A, dest); - break; - } } /* end decode_aprs */ @@ -2320,6 +2310,50 @@ static void aprs_telemetry (decode_aprs_t *A, char *info, int ilen, int quiet) } /* end aprs_telemetry */ +/*------------------------------------------------------------------ + * + * Function: aprs_user_defined + * + * Purpose: Decode user defined data. + * + * Inputs: info - Pointer to Information field. + * ilen - Information field length. + * + * Description: APRS Protocol Specification, Chapter 18 + * User IDs allocated here: http://www.aprs.org/aprs11/expfmts.txt + * + *------------------------------------------------------------------*/ + +static void aprs_user_defined (decode_aprs_t *A, char *info, int ilen) +{ + if (strncmp(info, "{tt", 3) == 0) { // Historical. Should probably use DT. + aprs_raw_touch_tone (A, info, ilen); + } + else if (strncmp(info, "{mc", 3) == 0) { // Historical. Should probably use DM. + aprs_morse_code (A, info, ilen); + } + else if (info[0] == '{' && info[1] == USER_DEF_USER_ID && info[2] == USER_DEF_TYPE_AIS) { + double lat, lon; + float knots, course; + + ais_parse (info+3, 0, A->g_msg_type, sizeof(A->g_msg_type), A->g_name, sizeof(A->g_name), &lat, &lon, &knots, &course); + + A->g_lat = lat; + A->g_lon = lon; + A->g_speed_mph = DW_KNOTS_TO_MPH(knots); + A->g_course = course; + strcpy (A->g_mfr, ""); + } + else if (strncmp(info, "{{", 2) == 0) { + snprintf (A->g_msg_type, sizeof(A->g_msg_type), "User-Defined Experimental"); + } + else { + snprintf (A->g_msg_type, sizeof(A->g_msg_type), "User-Defined Data"); + } + +} /* end aprs_user_defined */ + + /*------------------------------------------------------------------ * * Function: aprs_raw_touch_tone diff --git a/src/demod_9600.c b/src/demod_9600.c index ecee10b..a0e1ff0 100644 --- a/src/demod_9600.c +++ b/src/demod_9600.c @@ -113,7 +113,9 @@ static inline float agc (float in, float fast_attack, float slow_decay, float *p * * Purpose: Initialize the 9600 (or higher) baud demodulator. * - * Inputs: samples_per_sec - Number of samples per second. + * Inputs: modem_type - Determines whether scrambling is used. + * + * samples_per_sec - Number of samples per second. * Might be upsampled in hopes of * reducing the PLL jitter. * @@ -125,12 +127,13 @@ static inline float agc (float in, float fast_attack, float slow_decay, float *p * *----------------------------------------------------------------*/ -void demod_9600_init (int samples_per_sec, int baud, struct demodulator_state_s *D) +void demod_9600_init (enum modem_t modem_type, int samples_per_sec, int baud, struct demodulator_state_s *D) { float fc; int j; memset (D, 0, sizeof(struct demodulator_state_s)); + D->modem_type = modem_type; D->num_slicers = 1; // Multiple profiles in future? @@ -512,7 +515,7 @@ inline static void nudge_pll (int chan, int subchan, int slice, float demod_out_ /* Overflow. Was large positive, wrapped around, now large negative. */ - hdlc_rec_bit (chan, subchan, slice, demod_out_f > 0, 1, D->slicer[slice].lfsr); + hdlc_rec_bit (chan, subchan, slice, demod_out_f > 0, D->modem_type == MODEM_SCRAMBLE, D->slicer[slice].lfsr); } /* diff --git a/src/demod_9600.h b/src/demod_9600.h index a764711..ac3e747 100644 --- a/src/demod_9600.h +++ b/src/demod_9600.h @@ -6,7 +6,7 @@ #include "fsk_demod_state.h" -void demod_9600_init (int samples_per_sec, int baud, struct demodulator_state_s *D); +void demod_9600_init (enum modem_t modem_type, int samples_per_sec, int baud, struct demodulator_state_s *D); void demod_9600_process_sample (int chan, int sam, struct demodulator_state_s *D); diff --git a/src/direwolf.c b/src/direwolf.c index 32db610..5412ae5 100644 --- a/src/direwolf.c +++ b/src/direwolf.c @@ -1,7 +1,7 @@ // // This file is part of Dire Wolf, an amateur radio packet TNC. // -// Copyright (C) 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2019 John Langner, WB2OSZ +// Copyright (C) 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2019, 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 @@ -284,7 +284,7 @@ int main (int argc, char *argv[]) text_color_init(t_opt); text_color_set(DW_COLOR_INFO); //dw_printf ("Dire Wolf version %d.%d (%s) Beta Test 4\n", MAJOR_VERSION, MINOR_VERSION, __DATE__); - dw_printf ("Dire Wolf DEVELOPMENT version %d.%d %s (%s)\n", MAJOR_VERSION, MINOR_VERSION, "D", __DATE__); + dw_printf ("Dire Wolf DEVELOPMENT version %d.%d %s (%s)\n", MAJOR_VERSION, MINOR_VERSION, "E", __DATE__); //dw_printf ("Dire Wolf version %d.%d\n", MAJOR_VERSION, MINOR_VERSION); @@ -416,8 +416,14 @@ int main (int argc, char *argv[]) #endif case 'B': /* -B baud rate and modem properties. */ - - B_opt = atoi(optarg); + /* Also implies modem type based on speed. */ + /* Special case "AIS" rather than number. */ + if (strcasecmp(optarg, "AIS") == 0) { + B_opt = 12345; // See special case below. + } + else { + B_opt = atoi(optarg); + } if (B_opt < MIN_BAUD || B_opt > MAX_BAUD) { text_color_set(DW_COLOR_ERROR); dw_printf ("Use a more reasonable data baud rate in range of %d - %d.\n", MIN_BAUD, MAX_BAUD); @@ -716,6 +722,12 @@ int main (int argc, char *argv[]) dw_printf ("Bit rate should be standard 4800 rather than specified %d.\n", audio_config.achan[0].baud); } } + else if (audio_config.achan[0].baud == 12345) { + audio_config.achan[0].modem_type = MODEM_AIS; + audio_config.achan[0].baud = 9600; + audio_config.achan[0].mark_freq = 0; + audio_config.achan[0].space_freq = 0; + } else { audio_config.achan[0].modem_type = MODEM_SCRAMBLE; audio_config.achan[0].mark_freq = 0; @@ -1367,6 +1379,7 @@ static void usage (char **argv) dw_printf (" 2400 bps uses QPSK based on V.26 standard.\n"); dw_printf (" 4800 bps uses 8PSK based on V.27 standard.\n"); dw_printf (" 9600 bps and up uses K9NG/G3RUH standard.\n"); + dw_printf (" AIS for ship Automatic Identification System.\n"); dw_printf (" -g Force G3RUH modem regardless of speed.\n"); dw_printf (" -j 2400 bps QPSK compatible with direwolf <= 1.5.\n"); dw_printf (" -J 2400 bps QPSK compatible with MFJ-2400.\n"); diff --git a/src/hdlc_rec2.c b/src/hdlc_rec2.c index 0891f79..c709c43 100644 --- a/src/hdlc_rec2.c +++ b/src/hdlc_rec2.c @@ -17,6 +17,7 @@ // + /******************************************************************************** * * File: hdlc_rec2.c @@ -763,7 +764,15 @@ static int try_decode (rrbb_t block, int chan, int subchan, int slice, alevel_t expected_fcs = fcs_calc (H.frame_buf, H.frame_len - 2); - if (actual_fcs == expected_fcs && + if (actual_fcs == expected_fcs && save_audio_config_p->achan[chan].modem_type == MODEM_AIS) { + + // Sanity check for AIS does not seem feasible. + // Could possibly check length if we knew all the valid possibilities. + + multi_modem_process_rec_frame (chan, subchan, slice, H.frame_buf, H.frame_len - 2, alevel, retry_conf.retry, 0); /* len-2 to remove FCS. */ + return 1; /* success */ + } + else if (actual_fcs == expected_fcs && sanity_check (H.frame_buf, H.frame_len - 2, retry_conf.retry, save_audio_config_p->achan[chan].sanity_test)) { // TODO: Shouldn't be necessary to pass chan, subchan, alevel into diff --git a/src/multi_modem.c b/src/multi_modem.c index b52fef0..d2d4408 100644 --- a/src/multi_modem.c +++ b/src/multi_modem.c @@ -101,6 +101,9 @@ #include "hdlc_rec2.h" #include "dlq.h" #include "fx25.h" +#include "version.h" +#include "ais.h" + // Properties of the radio channels. @@ -319,7 +322,21 @@ void multi_modem_process_rec_frame (int chan, int subchan, int slice, unsigned c assert (subchan >= 0 && subchan < MAX_SUBCHANS); assert (slice >= 0 && slice < MAX_SUBCHANS); - pp = ax25_from_frame (fbuf, flen, alevel); +// Special encapsulation for AIS so it can be treated normally pretty much everywhere else. + + if (save_audio_config_p->achan[chan].modem_type == MODEM_AIS) { + char nmea[256]; + ais_to_nmea (fbuf, flen, nmea, sizeof(nmea)); + + char monfmt[276]; + snprintf (monfmt, sizeof(monfmt), "AIS>%s%1d%1d:{%c%c%s", APP_TOCALL, MAJOR_VERSION, MINOR_VERSION, USER_DEF_USER_ID, USER_DEF_TYPE_AIS, nmea); + pp = ax25_from_text (monfmt, 1); + + // alevel gets in there somehow making me question why it is passed thru here. + } + else { + pp = ax25_from_frame (fbuf, flen, alevel); + } if (pp == NULL) { text_color_set(DW_COLOR_ERROR); diff --git a/src/version.h b/src/version.h index 0c72d45..78cfd3d 100644 --- a/src/version.h +++ b/src/version.h @@ -1,8 +1,20 @@ /* Dire Wolf version 1.6 */ +// Put in destination field to identify the equipment used. + #define APP_TOCALL "APDW" // Assigned by WB4APR in tocalls.txt +// This now comes from compile command line options. + //#define MAJOR_VERSION 1 //#define MINOR_VERSION 6 //#define EXTRA_VERSION "Beta Test" + + +// For user-defined data format. +// APRS protocol spec Chapter 18 and http://www.aprs.org/aprs11/expfmts.txt + +#define USER_DEF_USER_ID 'D' // user id D for direwolf + +#define USER_DEF_TYPE_AIS 'A' // data type A for AIS NMEA sentence diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index c4a38b9..6d4336e 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -105,6 +105,7 @@ endif() # Unit test for demodulators list(APPEND atest9_SOURCES ${CUSTOM_SRC_DIR}/atest.c + ${CUSTOM_SRC_DIR}/ais.c ${CUSTOM_SRC_DIR}/demod.c ${CUSTOM_SRC_DIR}/dsp.c ${CUSTOM_SRC_DIR}/demod_afsk.c @@ -160,6 +161,7 @@ endif() # Unit test for inner digipeater algorithm list(APPEND dtest_SOURCES ${CUSTOM_SRC_DIR}/digipeater.c + ${CUSTOM_SRC_DIR}/ais.c ${CUSTOM_SRC_DIR}/dedupe.c ${CUSTOM_SRC_DIR}/pfilter.c ${CUSTOM_SRC_DIR}/ax25_pad.c @@ -247,6 +249,7 @@ target_link_libraries(tttexttest # Unit test for Packet Filtering. list(APPEND pftest_SOURCES ${CUSTOM_SRC_DIR}/pfilter.c + ${CUSTOM_SRC_DIR}/ais.c ${CUSTOM_SRC_DIR}/ax25_pad.c ${CUSTOM_SRC_DIR}/textcolor.c ${CUSTOM_SRC_DIR}/fcs_calc.c @@ -499,6 +502,7 @@ if(OPTIONAL_TEST) # Unit test for IGate list(APPEND itest_SOURCES ${CUSTOM_SRC_DIR}/igate.c + ${CUSTOM_SRC_DIR}/ais.c ${CUSTOM_SRC_DIR}/ax25_pad.c ${CUSTOM_SRC_DIR}/fcs_calc.c ${CUSTOM_SRC_DIR}/mheard.c @@ -544,6 +548,7 @@ if(OPTIONAL_TEST) # For demodulator tweaking experiments. list(APPEND testagc_SOURCES ${CUSTOM_SRC_DIR}/atest.c + ${CUSTOM_SRC_DIR}/ais.c ${CUSTOM_SRC_DIR}/demod.c ${CUSTOM_SRC_DIR}/dsp.c ${CUSTOM_SRC_DIR}/demod_afsk.c @@ -592,6 +597,7 @@ if(OPTIONAL_TEST) # Send GPS location to KISS TNC each second. list(APPEND walk96_SOURCES ${CUSTOM_SRC_DIR}/walk96.c + ${CUSTOM_SRC_DIR}/ais.c ${CUSTOM_SRC_DIR}/dwgps.c ${CUSTOM_SRC_DIR}/dwgpsnmea.c ${CUSTOM_SRC_DIR}/dwgpsd.c