AIS refinements.

This commit is contained in:
wb2osz 2020-04-25 07:59:06 -04:00
parent b99f9f33c2
commit 74a5c34a94
5 changed files with 213 additions and 21 deletions

187
src/ais.c
View File

@ -50,6 +50,42 @@
#include "textcolor.h" #include "textcolor.h"
#include "ais.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
};
/*------------------------------------------------------------------- /*-------------------------------------------------------------------
* *
@ -113,22 +149,53 @@ static double get_field_latlon (unsigned char *base, unsigned int start, unsigne
// Latitude of 0x3412140 (91 deg) means not available. // Latitude of 0x3412140 (91 deg) means not available.
// Longitude of 0x6791AC0 (181 deg) means not available. // Longitude of 0x6791AC0 (181 deg) means not available.
return ((double)get_field_signed(base, start, len) / 600000.0); 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) static float get_field_speed (unsigned char *base, unsigned int start, unsigned int len)
{ {
// Raw 1023 means not available. // Raw 1023 means not available.
// Multiply by 0.1 to get knots. // Multiply by 0.1 to get knots.
return ((float)get_field_signed(base, start, len) * 0.1); // 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) static float get_field_course (unsigned char *base, unsigned int start, unsigned int len)
{ {
// Raw 3600 means not available. // Raw 3600 means not available.
// Multiply by 0.1 to get degrees // Multiply by 0.1 to get degrees
return ((float)get_field_signed(base, start, len) * 0.1); 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';
}
}
/*------------------------------------------------------------------- /*-------------------------------------------------------------------
* *
@ -238,18 +305,22 @@ void ais_to_nmea (unsigned char *ais, int ais_len, char *nmea, int nmea_size)
* mssi 9 digit identifier. * mssi 9 digit identifier.
* odlat latitude. * odlat latitude.
* odlon longitude. * odlon longitude.
* ofknots speed. * ofknots speed, knots.
* ofcourse direction of travel. * ofcourse direction of travel.
* ofalt_m altitude, meters.
* symtab APRS symbol table.
* symbol APRS symbol code.
* *
* Returns: 0 for success, -1 for error. * Returns: 0 for success, -1 for error.
* *
*--------------------------------------------------------------------*/ *--------------------------------------------------------------------*/
// Maximum NMEA sentence length is 82 according to some people. // Maximum NMEA sentence length is 82, including CR/LF.
// Make buffer considerably larger to be safe. // Make buffer considerably larger to be safe.
#define NMEA_MAX_LEN 240 #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) 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. */ char stemp[NMEA_MAX_LEN]; /* Make copy because parsing is destructive. */
@ -258,6 +329,7 @@ int ais_parse (char *sentence, int quiet, char *descr, int descr_size, char *mss
*odlon = G_UNKNOWN; *odlon = G_UNKNOWN;
*ofknots = G_UNKNOWN; *ofknots = G_UNKNOWN;
*ofcourse = G_UNKNOWN; *ofcourse = G_UNKNOWN;
*ofalt_m = G_UNKNOWN;
strlcpy (stemp, sentence, sizeof(stemp)); strlcpy (stemp, sentence, sizeof(stemp));
@ -349,10 +421,15 @@ int ais_parse (char *sentence, int quiet, char *descr, int descr_size, char *mss
} }
} }
// Extract the fields of interest from a few message types. // Extract the fields of interest from a few message types.
// Don't get too carried away. // Don't get too carried away.
int type = get_field(ais, 0, 6); int type = get_field(ais, 0, 6);
if (type >= 1 && type <= 27) {
snprintf (mssi, mssi_size, "%d", get_field(ais, 8, 30));
}
switch (type) { switch (type) {
case 1: // Position Report Class A case 1: // Position Report Class A
@ -360,7 +437,8 @@ int ais_parse (char *sentence, int quiet, char *descr, int descr_size, char *mss
case 3: case 3:
snprintf (descr, descr_size, "AIS %d: Position Report Class A", type); snprintf (descr, descr_size, "AIS %d: Position Report Class A", type);
snprintf (mssi, mssi_size, "%d", get_field(ais, 8, 30)); *symtab = '/';
*symbol = 's'; // Power boat (ship) side view
*odlon = get_field_latlon(ais, 61, 28); *odlon = get_field_latlon(ais, 61, 28);
*odlat = get_field_latlon(ais, 89, 27); *odlat = get_field_latlon(ais, 89, 27);
*ofknots = get_field_speed(ais, 50, 10); *ofknots = get_field_speed(ais, 50, 10);
@ -370,7 +448,8 @@ int ais_parse (char *sentence, int quiet, char *descr, int descr_size, char *mss
case 4: // Base Station Report case 4: // Base Station Report
snprintf (descr, descr_size, "AIS %d: Base Station Report", type); snprintf (descr, descr_size, "AIS %d: Base Station Report", type);
snprintf (mssi, mssi_size, "%d", get_field(ais, 8, 30)); *symtab = '\\';
*symbol = 'L'; // Lighthouse
//year = get_field(ais, 38, 14); //year = get_field(ais, 38, 14);
//month = get_field(ais, 52, 4); //month = get_field(ais, 52, 4);
//day = get_field(ais, 56, 5); //day = get_field(ais, 56, 5);
@ -381,10 +460,47 @@ int ais_parse (char *sentence, int quiet, char *descr, int descr_size, char *mss
*odlat = get_field_latlon(ais, 107, 27); *odlat = get_field_latlon(ais, 107, 27);
break; 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 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); snprintf (descr, descr_size, "AIS %d: Standard Class B CS Position Report", type);
snprintf (mssi, mssi_size, "%d", get_field(ais, 8, 30)); *symtab = '/';
*symbol = 'Y'; // YACHT (sail)
*odlon = get_field_latlon(ais, 57, 28); *odlon = get_field_latlon(ais, 57, 28);
*odlat = get_field_latlon(ais, 85, 27); *odlat = get_field_latlon(ais, 85, 27);
break; break;
@ -392,11 +508,23 @@ int ais_parse (char *sentence, int quiet, char *descr, int descr_size, char *mss
case 19: // Extended Class B CS Position Report case 19: // Extended Class B CS Position Report
snprintf (descr, descr_size, "AIS %d: Extended Class B CS Position Report", type); snprintf (descr, descr_size, "AIS %d: Extended Class B CS Position Report", type);
snprintf (mssi, mssi_size, "%d", get_field(ais, 8, 30)); *symtab = '/';
*symbol = 'Y'; // YACHT (sail)
*odlon = get_field_latlon(ais, 57, 28); *odlon = get_field_latlon(ais, 57, 28);
*odlat = get_field_latlon(ais, 85, 27); *odlat = get_field_latlon(ais, 85, 27);
break; 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: default:
snprintf (descr, descr_size, "AIS message type %d", type); snprintf (descr, descr_size, "AIS message type %d", type);
break; break;
@ -406,4 +534,45 @@ int ais_parse (char *sentence, int quiet, char *descr, int descr_size, char *mss
} /* end ais_parse */ } /* 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 // end ais.c

View File

@ -2,4 +2,7 @@
void ais_to_nmea (unsigned char *ais, int ais_len, char *nema, int nema_size); 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); 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);
int ais_check_length (int type, int length);

View File

@ -2335,13 +2335,17 @@ static void aprs_user_defined (decode_aprs_t *A, char *info, int ilen)
else if (info[0] == '{' && info[1] == USER_DEF_USER_ID && info[2] == USER_DEF_TYPE_AIS) { else if (info[0] == '{' && info[1] == USER_DEF_USER_ID && info[2] == USER_DEF_TYPE_AIS) {
double lat, lon; double lat, lon;
float knots, course; float knots, course;
float alt_meters;
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); 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, &alt_meters, &(A->g_symbol_table), &(A->g_symbol_code),
A->g_comment, sizeof(A->g_comment));
A->g_lat = lat; A->g_lat = lat;
A->g_lon = lon; A->g_lon = lon;
A->g_speed_mph = DW_KNOTS_TO_MPH(knots); A->g_speed_mph = DW_KNOTS_TO_MPH(knots);
A->g_course = course; A->g_course = course;
A->g_altitude_ft = DW_METERS_TO_FEET(alt_meters);
strcpy (A->g_mfr, ""); strcpy (A->g_mfr, "");
} }
else if (strncmp(info, "{{", 2) == 0) { else if (strncmp(info, "{{", 2) == 0) {

View File

@ -622,6 +622,22 @@ int demod_init (struct audio_s *pa)
default: /* Not AFSK */ default: /* Not AFSK */
{ {
// For AIS we will accept only a good CRC without any fixup attempts.
// Even with that, there are still a lot of CRC false matches with random noise.
if (save_audio_config_p->achan[chan].modem_type == MODEM_AIS) {
if (save_audio_config_p->achan[chan].fix_bits != RETRY_NONE) {
text_color_set(DW_COLOR_INFO);
dw_printf ("Channel %d: FIX_BITS option has been turned off for AIS.\n", chan);
save_audio_config_p->achan[chan].fix_bits = RETRY_NONE;
}
if (save_audio_config_p->achan[chan].passall != 0) {
text_color_set(DW_COLOR_INFO);
dw_printf ("Channel %d: PASSALL option has been turned off for AIS.\n", chan);
save_audio_config_p->achan[chan].passall = 0;
}
}
if (strcmp(save_audio_config_p->achan[chan].profiles, "") == 0) { if (strcmp(save_audio_config_p->achan[chan].profiles, "") == 0) {
/* Apply default if not set earlier. */ /* Apply default if not set earlier. */
@ -632,13 +648,10 @@ int demod_init (struct audio_s *pa)
/* We want higher performance to be the default. */ /* We want higher performance to be the default. */
/* "MODEM 9600 -" can be used on very slow CPU if necessary. */ /* "MODEM 9600 -" can be used on very slow CPU if necessary. */
//#ifndef __arm__
strlcpy (save_audio_config_p->achan[chan].profiles, "+", sizeof(save_audio_config_p->achan[chan].profiles)); strlcpy (save_audio_config_p->achan[chan].profiles, "+", sizeof(save_audio_config_p->achan[chan].profiles));
//#endif
} }
#ifdef TUNE_ZEROSTUFF #ifdef TUNE_ZEROSTUFF
zerostuff = TUNE_ZEROSTUFF; zerostuff = TUNE_ZEROSTUFF;
#endif #endif

View File

@ -104,7 +104,7 @@
#include "demod_9600.h" /* for descramble() */ #include "demod_9600.h" /* for descramble() */
#include "audio.h" /* for struct audio_s */ #include "audio.h" /* for struct audio_s */
//#include "ax25_pad.h" /* for AX25_MAX_ADDR_LEN */ //#include "ax25_pad.h" /* for AX25_MAX_ADDR_LEN */
#include "ais.h"
//#define DEBUG 1 //#define DEBUG 1
//#define DEBUGx 1 //#define DEBUGx 1
@ -766,12 +766,15 @@ static int try_decode (rrbb_t block, int chan, int subchan, int slice, alevel_t
if (actual_fcs == expected_fcs && save_audio_config_p->achan[chan].modem_type == MODEM_AIS) { if (actual_fcs == expected_fcs && save_audio_config_p->achan[chan].modem_type == MODEM_AIS) {
// Sanity check for AIS does not seem feasible. // Sanity check for AIS.
// Could possibly check length if we knew all the valid possibilities. if (ais_check_length((H.frame_buf[0] >> 2) & 0x3f, H.frame_len - 2) == 0) {
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. */ 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 */ return 1; /* success */
} }
else {
return 0; /* did not pass sanity check */
}
}
else if (actual_fcs == expected_fcs && 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)) { sanity_check (H.frame_buf, H.frame_len - 2, retry_conf.retry, save_audio_config_p->achan[chan].sanity_test)) {