aprsc/src/parse_aprs.c

1383 lines
37 KiB
C

/*
* aprsc
*
* (c) Heikki Hannikainen, OH7LZB <hessu@hes.iki.fi>
*
* This program is licensed under the BSD license, which can be found
* in the file LICENSE.
*
*/
/*
* A simple APRS parser for aprsc. Translated from Ham::APRS::FAP
* perl module (by OH2KKU).
*
* Only needs to get lat/lng out of the packet, other features would
* be unnecessary in this application, and slow down the parser.
* ... but lets still classify the packet, output filter needs that.
*
*/
#include <stdio.h>
#include <string.h>
#include <math.h>
#include <ctype.h>
#include "parse_aprs.h"
#include "hlog.h"
#include "filter.h"
#include "historydb.h"
#include "incoming.h"
//#define DEBUG_PARSE_APRS 1
#ifdef DEBUG_PARSE_APRS
#define DEBUG_LOG(...) \
do { hlog(LOG_DEBUG, __VA_ARGS__); } while (0)
#else
#define DEBUG_LOG(...) { }
#endif
/*
* Check if the given character is a valid symbol table identifier
* or an overlay character. The set is different for compressed
* and uncompressed packets - the former has the overlaid number (0-9)
* replaced with n-j.
*/
static int valid_sym_table_compressed(char c)
{
return (c == '/' || c == '\\' || (c >= 0x41 && c <= 0x5A)
|| (c >= 0x61 && c <= 0x6A)); /* [\/\\A-Za-j] */
}
static int valid_sym_table_uncompressed(char c)
{
return (c == '/' || c == '\\' || (c >= 0x41 && c <= 0x5A)
|| (c >= 0x30 && c <= 0x39)); /* [\/\\A-Z0-9] */
}
/*
* Fill the pbuf_t structure with a parsed position and
* symbol table & code. Also does range checking for lat/lng
* and pre-calculates cosf(lat) for range filters.
*/
static int pbuf_fill_pos(struct pbuf_t *pb, const float lat, const float lng, const char sym_table, const char sym_code)
{
int bad = 0;
/* symbol table and code */
pb->symbol[0] = sym_table;
pb->symbol[1] = sym_code;
pb->symbol[2] = 0;
/* Is it perhaps a weather report ? */
if (sym_code == '_' && (sym_table == '/' || sym_table == '\\'))
pb->packettype |= T_WX;
if (sym_code == '@' && (sym_table == '/' || sym_table == '\\'))
pb->packettype |= T_WX; /* Hurricane */
bad |= (lat < -89.9 && -0.0001 <= lng && lng <= 0.0001);
bad |= (lat > 89.9 && -0.0001 <= lng && lng <= 0.0001);
if (-0.0001 <= lat && lat <= 0.0001) {
bad |= ( -0.0001 <= lng && lng <= 0.0001);
bad |= ( -90.01 <= lng && lng <= -89.99);
bad |= ( 89.99 <= lng && lng <= 90.01);
}
if (bad || lat < -90.0 || lat > 90.0 || lng < -180.0 || lng > 180.0) {
DEBUG_LOG("\tposition out of range: lat %.5f lng %.5f", lat, lng);
return 0; /* out of range */
}
DEBUG_LOG("\tposition ok: lat %.5f lng %.5f", lat, lng);
/* Pre-calculations for A/R/F/M-filter tests */
pb->lat = filter_lat2rad(lat); /* deg-to-radians */
pb->cos_lat = cosf(pb->lat); /* used in range filters */
pb->lng = filter_lon2rad(lng); /* deg-to-radians */
pb->flags |= F_HASPOS; /* the packet has positional data */
return 1;
}
/*
* Parse symbol from destination callsign
*/
static int get_symbol_from_dstcall_twochar(const char c1, const char c2, char *sym_table, char *sym_code)
{
//DEBUG_LOG("\ttwochar %c %c", c1, c2);
if (c1 == 'B') {
if (c2 >= 'B' && c2 <= 'P') {
*sym_table = '/';
*sym_code = c2 - 'B' + '!';
return 1;
}
return 0;
}
if (c1 == 'P') {
if (c2 >= '0' && c2 <= '9') {
*sym_table = '/';
*sym_code = c2;
return 1;
}
if (c2 >= 'A' && c2 <= 'Z') {
*sym_table = '/';
*sym_code = c2;
return 1;
}
return 0;
}
if (c1 == 'M') {
if (c2 >= 'R' && c2 <= 'X') {
*sym_table = '/';
*sym_code = c2 - 'R' + ':';
return 1;
}
return 0;
}
if (c1 == 'H') {
if (c2 >= 'S' && c2 <= 'X') {
*sym_table = '/';
*sym_code = c2 - 'S' + '[';
return 1;
}
return 0;
}
if (c1 == 'L') {
if (c2 >= 'A' && c2 <= 'Z') {
*sym_table = '/';
*sym_code = c2 - 'A' + 'a';
return 1;
}
return 0;
}
if (c1 == 'J') {
if (c2 >= '1' && c2 <= '4') {
*sym_table = '/';
*sym_code = c2 - '1' + '{';
return 1;
}
return 0;
}
if (c1 == 'O') {
if (c2 >= 'B' && c2 <= 'P') {
*sym_table = '\\';
*sym_code = c2 - 'B' + '!';
return 1;
}
return 0;
}
if (c1 == 'A') {
if (c2 >= '0' && c2 <= '9') {
*sym_table = '\\';
*sym_code = c2;
return 1;
}
if (c2 >= 'A' && c2 <= 'Z') {
*sym_table = '\\';
*sym_code = c2;
return 1;
}
return 0;
}
if (c1 == 'N') {
if (c2 >= 'R' && c2 <= 'X') {
*sym_table = '\\';
*sym_code = c2 - 'R' + ':';
return 1;
}
return 0;
}
if (c1 == 'D') {
if (c2 >= 'S' && c2 <= 'X') {
*sym_table = '\\';
*sym_code = c2 - 'S' + '[';
return 1;
}
return 0;
}
if (c1 == 'S') {
if (c2 >= 'A' && c2 <= 'Z') {
*sym_table = '\\';
*sym_code = c2 - 'A' + 'a';
return 1;
}
return 0;
}
if (c1 == 'Q') {
if (c2 >= '1' && c2 <= '4') {
*sym_table = '\\';
*sym_code = c2 - '1' + '{';
return 1;
}
return 0;
}
return 0;
}
static int get_symbol_from_dstcall(struct pbuf_t *pb, char *sym_table, char *sym_code)
{
const char *d_start;
char type;
char overlay;
int sublength;
int numberid;
/* check that the destination call exists and is of the right size for symbol */
d_start = pb->srccall_end+1;
if (pb->dstcall_end_or_ssid - d_start < 5)
return 0; /* too short */
/* length of the parsed string */
sublength = pb->dstcall_end_or_ssid - d_start - 3;
if (sublength > 3)
sublength = 3;
DEBUG_LOG("get_symbol_from_dstcall: %.*s (%d)", (int)(pb->dstcall_end_or_ssid - d_start), d_start, sublength);
if (strncmp(d_start, "GPS", 3) != 0 && strncmp(d_start, "SPC", 3) != 0 && strncmp(d_start, "SYM", 3) != 0)
return 0;
// DEBUG_LOG("\ttesting %c %c %c", d_start[3], d_start[4], d_start[5]);
if (!isalnum(d_start[3]) || !isalnum(d_start[4]))
return 0;
if (sublength == 3 && !isalnum(d_start[5]))
return 0;
type = d_start[3];
if (sublength == 3) {
if (type == 'C' || type == 'E') {
if (!isdigit(d_start[4]))
return 0;
if (!isdigit(d_start[5]))
return 0;
numberid = (d_start[4] - 48) * 10 + (d_start[5] - 48);
*sym_code = numberid + 32;
if (type == 'C')
*sym_table = '/';
else
*sym_table = '\\';
DEBUG_LOG("\tnumeric symbol id in dstcall: %.*s: table %c code %c",
(int)(pb->dstcall_end_or_ssid - d_start - 3), d_start + 3, *sym_table, *sym_code);
return 1;
} else {
/* secondary symbol table, with overlay
* Check first that we really are in the secondary symbol table
*/
overlay = d_start[5];
if ((type == 'O' || type == 'A' || type == 'N' ||
type == 'D' || type == 'S' || type == 'Q')
&& isalnum(overlay)) {
return get_symbol_from_dstcall_twochar(d_start[3], d_start[4], sym_table, sym_code);
}
return 0;
}
} else {
// primary or secondary table, no overlay
return get_symbol_from_dstcall_twochar(d_start[3], d_start[4], sym_table, sym_code);
}
return 0;
}
/*
* Parse NMEA position packets.
*/
static int parse_aprs_nmea(struct pbuf_t *pb, const char *body, const char *body_end)
{
float lat, lng;
const char *latp, *lngp;
int i, la, lo;
char lac, loc;
char sym_table = ' ', sym_code = ' ';
// Parse symbol from destination callsign, first thing before any possible returns
get_symbol_from_dstcall(pb, &sym_table, &sym_code);
DEBUG_LOG("get_symbol_from_dstcall: %.*s => %c%c",
(int)(pb->dstcall_end_or_ssid - pb->srccall_end-1), pb->srccall_end+1, sym_table, sym_code);
if (memcmp(body,"ULT",3) == 0) {
/* Ah.. "$ULT..." - that is, Ultimeter 2000 weather instrument */
pb->packettype |= T_WX;
return 1;
}
lat = lng = 0.0;
latp = lngp = NULL;
/* NMEA sentences to understand:
$GPGGA Global Positioning System Fix Data
$GPGLL Geographic Position, Latitude/Longitude Data
$GPRMC Remommended Minimum Specific GPS/Transit Data
$GPWPT Way Point Location ?? (bug in APRS specs ?)
$GPWPL Waypoint Load (not in APRS specs, but in NMEA specs)
$PNTS Seen on APRS-IS, private sentense based on NMEA..
$xxTLL Not seen on radio network, usually $RATLL - Target positions
reported by RAdar.
*/
if (memcmp(body, "GPGGA,", 6) == 0) {
/* GPGGA,175059,3347.4969,N,11805.7319,W,2,12,1.0,6.8,M,-32.1,M,,*7D
// v=1, looks fine
// GPGGA,000000,5132.038,N,11310.221,W,1,09,0.8,940.0,M,-17.7,,
// v=1, timestamp odd, coords look fine
// GPGGA,,,,,,0,00,,,,,,,*66
// v=0, invalid
// GPGGA,121230,4518.7931,N,07322.3202,W,2,08,1.0,40.0,M,-32.4,M,,*46
// v=2, looks valid ?
// GPGGA,193115.00,3302.50182,N,11651.22581,W,1,08,01.6,00465.90,M,-32.891,M,,*5F
// $GPGGA,hhmmss.dd,xxmm.dddd,<N|S>,yyymm.dddd,<E|W>,v,
// ss,d.d,h.h,M,g.g,M,a.a,xxxx*hh<CR><LF>
*/
latp = body+6; // over the keyword
while (latp < body_end && *latp != ',')
latp++; // scan over the timestamp
if (*latp == ',')
latp++; // .. and into latitude.
lngp = latp;
while (lngp < body_end && *lngp != ',')
lngp++;
if (*lngp == ',')
lngp++;
if (*lngp != ',')
lngp++;
if (*lngp == ',')
lngp++;
/* latp, and lngp point to start of latitude and longitude substrings
// respectively.
*/
} else if (memcmp(body, "GPGLL,", 6) == 0) {
/* $GPGLL,xxmm.dddd,<N|S>,yyymm.dddd,<E|W>,hhmmss.dd,S,M*hh<CR><LF> */
latp = body+6; // over the keyword
lngp = latp;
while (lngp < body_end && *lngp != ',') // over latitude
lngp++;
if (*lngp == ',')
lngp++; // and lat designator
if (*lngp != ',')
lngp++; // and lat designator
if (*lngp == ',')
lngp++;
/* latp, and lngp point to start of latitude and longitude substrings
// respectively
*/
} else if (memcmp(body, "GPRMC,", 6) == 0) {
/* $GPRMC,hhmmss.dd,S,xxmm.dddd,<N|S>,yyymm.dddd,<E|W>,s.s,h.h,ddmmyy,d.d, <E|W>,M*hh<CR><LF>
// ,S, = Status: 'A' = Valid, 'V' = Invalid
//
// GPRMC,175050,A,4117.8935,N,10535.0871,W,0.0,324.3,100208,10.0,E,A*3B
// GPRMC,000000,V,0000.0000,0,00000.0000,0,000,000,000000,,*01/It wasn't me :)
// invalid..
// GPRMC,000043,V,4411.7761,N,07927.0448,W,0.000,0.0,290697,10.7,W*57
// GPRMC,003803,A,3347.1727,N,11812.7184,W,000.0,000.0,140208,013.7,E*67
// GPRMC,050058,A,4609.1143,N,12258.8184,W,0.000,0.0,100208,18.0,E*5B
*/
latp = body+6; // over the keyword
while (latp < body_end && *latp != ',')
latp++; // scan over the timestamp
if (*latp == ',')
latp++; // .. and into VALIDITY
if (*latp != 'A' && *latp != 'V')
return 0; // INVALID !
if (*latp != ',')
latp++;
if (*latp == ',')
latp++;
/* now it points to latitude substring */
lngp = latp;
while (lngp < body_end && *lngp != ',')
lngp++;
if (*lngp == ',')
lngp++;
if (*lngp != ',')
lngp++;
if (*lngp == ',')
lngp++;
/* latp, and lngp point to start of latitude and longitude substrings
// respectively.
*/
} else if (memcmp(body, "GPWPL,", 6) == 0) {
/* $GPWPL,4610.586,N,00607.754,E,4*70
// $GPWPL,4610.452,N,00607.759,E,5*74
*/
latp = body+6;
} else if (memcmp(body, "PNTS,1,", 7) == 0) { /* PNTS version 1 */
/* $PNTS,1,0,11,01,2002,231932,3539.687,N,13944.480,E,0,000,5,Roppongi UID RELAY,000,1*35
// $PNTS,1,0,14,01,2007,131449,3535.182,N,13941.200,E,0,0.0,6,Oota-Ku KissUIDigi,000,1*1D
// $PNTS,1,0,17,02,2008,120824,3117.165,N,13036.481,E,49,059,1,Kagoshima,000,1*71
// $PNTS,1,0,17,02,2008,120948,3504.283,N,13657.933,E,00,000.0,6,,000,1*36
//
// From Alinco EJ-41U Terminal Node Controller manual:
//
// 5-4-7 $PNTS
// This is a private-sentence based on NMEA-0183. The data contains date,
// time, latitude, longitude, moving speed, direction, altitude plus a short
// message, group codes, and icon numbers. The EJ-41U does not analyze this
// format but can re-structure it.
// The data contains the following information:
// l $PNTS Starts the $PNTS sentence
// l version
// l the registered information. [0]=normal geographical location data.
// This is the only data EJ-41U can re-structure. [s]=Initial position
// for the course setting [E]=ending position for the course setting
// [1]=the course data between initial and ending [P]=the check point
// registration [A]=check data when the automatic position transmission
// is set OFF [R]=check data when the course data or check point data is
// received.
// l dd,mm,yyyy,hhmmss: Date and time indication.
// l Latitude in DMD followed by N or S
// l Longitude in DMD followed by E or W
// l Direction: Shown with the number 360 degrees divided by 64.
// 00 stands for true north, 16 for east. Speed in Km/h
// l One of 15 characters [0] to [9], [A] to [E].
// NTSMRK command determines this character when EJ-41U is used.
// l A short message up to 20 bites. Use NTSMSG command to determine this message.
// l A group code: 3 letters with a combination of [0] to [9], [A] to [Z].
// Use NTSGRP command to determine.
// l Status: [1] for usable information, [0] for non-usable information.
// l *hh<CR><LF> the check-sum and end of PNTS sentence.
*/
if (body+55 > body_end) return 0; /* Too short.. */
latp = body+7; /* Over the keyword */
/* Accept any registered information code */
if (*latp++ == ',') return 0;
if (*latp++ != ',') return 0;
/* Scan over date+time info */
while (*latp != ',' && latp <= body_end) ++latp;
if (*latp == ',') ++latp;
while (*latp != ',' && latp <= body_end) ++latp;
if (*latp == ',') ++latp;
while (*latp != ',' && latp <= body_end) ++latp;
if (*latp == ',') ++latp;
while (*latp != ',' && latp <= body_end) ++latp;
if (*latp == ',') ++latp;
/* now it points to latitude substring */
lngp = latp;
while (lngp < body_end && *lngp != ',')
lngp++;
if (*lngp == ',')
lngp++;
if (*lngp != ',')
lngp++;
if (*lngp == ',')
lngp++;
/* latp, and lngp point to start of latitude and longitude substrings
// respectively.
*/
#if 1
} else if (memcmp(body, "GPGSA,", 6) == 0 ||
memcmp(body, "GPVTG,", 6) == 0 ||
memcmp(body, "GPGSV,", 6) == 0) {
/* Recognized but ignored */
return 1;
#endif
}
if (!latp || !lngp) {
hlog(LOG_DEBUG, "Unknown NMEA: '%.11s' %.*s", pb->data, (int)(body_end - body), body);
return 0; /* Well.. Not NMEA frame */
}
// DEBUG_LOG("NMEA parsing: %.*s", (int)(body_end - body), body);
// DEBUG_LOG(" lat=%.10s lng=%.10s", latp, lngp);
i = sscanf(latp, "%2d%f,%c,", &la, &lat, &lac);
if (i != 3)
return 0; // parse failure
i = sscanf(lngp, "%3d%f,%c,", &lo, &lng, &loc);
if (i != 3)
return 0; // parse failure
if (lac != 'N' && lac != 'S' && lac != 'n' && lac != 's')
return 0; // bad indicator value
if (loc != 'E' && loc != 'W' && loc != 'e' && loc != 'w')
return 0; // bad indicator value
// DEBUG_LOG(" lat: %c %2d %7.4f lng: %c %2d %7.4f",
// lac, la, lat, loc, lo, lng);
lat = (float)la + lat/60.0;
lng = (float)lo + lng/60.0;
if (lac == 'S' || lac == 's')
lat = -lat;
if (loc == 'W' || loc == 'w')
lng = -lng;
pb->packettype |= T_POSITION;
return pbuf_fill_pos(pb, lat, lng, sym_table, sym_code);
}
static int parse_aprs_telem(struct pbuf_t *pb, const char *body, const char *body_end)
{
// float lat = 0.0, lng = 0.0;
DEBUG_LOG("parse_aprs_telem");
//pbuf_fill_pos(pb, lat, lng, 0, 0);
return 0;
}
/*
* Parse a MIC-E position packet
*
* APRS PROTOCOL REFERENCE 1.0.1 Chapter 10, page 42 (52 in PDF)
*/
static int parse_aprs_mice(struct pbuf_t *pb, const unsigned char *body, const unsigned char *body_end)
{
float lat = 0.0, lng = 0.0;
unsigned int lat_deg = 0, lat_min = 0, lat_min_frag = 0, lng_deg = 0, lng_min = 0, lng_min_frag = 0;
const char *d_start;
char dstcall[7];
char *p;
char sym_table, sym_code;
int posambiguity = 0;
int i;
DEBUG_LOG("parse_aprs_mice: %.*s", pb->packet_len-2, pb->data);
/* check packet length */
if (body_end - body < 8)
return 0;
/* check that the destination call exists and is of the right size for mic-e */
d_start = pb->srccall_end+1;
if (pb->dstcall_end_or_ssid - d_start != 6)
return 0; /* eh...? */
/* validate destination call:
* A-K characters are not used in the last 3 characters
* and MNO are never used
*/
for (i = 0; i < 3; i++)
if (!((d_start[i] >= '0' && d_start[i] <= '9')
|| (d_start[i] >= 'A' && d_start[i] <= 'L')
|| (d_start[i] >= 'P' && d_start[i] <= 'Z')))
return 0;
for (i = 3; i < 6; i++)
if (!((d_start[i] >= '0' && d_start[i] <= '9')
|| (d_start[i] == 'L')
|| (d_start[i] >= 'P' && d_start[i] <= 'Z')))
return 0;
DEBUG_LOG("\tpassed dstcall format check");
/* validate information field (longitude, course, speed and
* symbol table and code are checked). Not bullet proof..
*
* 0 1 23 4 5 6 7
* /^[\x26-\x7f][\x26-\x61][\x1c-\x7f]{2}[\x1c-\x7d][\x1c-\x7f][\x21-\x7b\x7d][\/\\A-Z0-9]/
*/
if (body[0] < 0x26 || (unsigned char)body[0] > 0x7f) return 0;
if (body[1] < 0x26 || (unsigned char)body[1] > 0x61) return 0;
if (body[2] < 0x1c || (unsigned char)body[2] > 0x7f) return 0;
if (body[3] < 0x1c || (unsigned char)body[3] > 0x7f) return 0;
if (body[4] < 0x1c || (unsigned char)body[4] > 0x7d) return 0;
if (body[5] < 0x1c || (unsigned char)body[5] > 0x7f) return 0;
if ((body[6] < 0x21 || (unsigned char)body[6] > 0x7b)
&& (unsigned char)body[6] != 0x7d) return 0;
if (!valid_sym_table_uncompressed(body[7])) return 0;
DEBUG_LOG("\tpassed info format check");
/* make a local copy, we're going to modify it */
strncpy(dstcall, d_start, 6);
dstcall[6] = 0;
/* First do the destination callsign
* (latitude, message bits, N/S and W/E indicators and long. offset)
*
* Translate the characters to get the latitude
*/
//fprintf(stderr, "\tuntranslated dstcall: %s\n", dstcall);
for (p = dstcall; *p; p++) {
if (*p >= 'A' && *p <= 'J')
*p -= 'A' - '0';
else if (*p >= 'P' && *p <= 'Y')
*p -= 'P' - '0';
else if (*p == 'K' || *p == 'L' || *p == 'Z')
*p = '_';
}
//fprintf(stderr, "\ttranslated dstcall: %s\n", dstcall);
/* position ambiquity is going to get ignored now, it's not needed in this application. */
if (dstcall[5] == '_') { dstcall[5] = '5'; posambiguity = 1; }
if (dstcall[4] == '_') { dstcall[4] = '5'; posambiguity = 2; }
if (dstcall[3] == '_') { dstcall[3] = '5'; posambiguity = 3; }
if (dstcall[2] == '_') { dstcall[2] = '3'; posambiguity = 4; }
if (dstcall[1] == '_' || dstcall[0] == '_') { return 0; } /* cannot use posamb here */
/* convert to degrees, minutes and decimal degrees, and then to a float lat */
if (sscanf(dstcall, "%2u%2u%2u",
&lat_deg, &lat_min, &lat_min_frag) != 3) {
DEBUG_LOG("\tsscanf failed");
return 0;
}
lat = (float)lat_deg + (float)lat_min / 60.0 + (float)lat_min_frag / 6000.0;
/* check the north/south direction and correct the latitude if necessary */
if (d_start[3] <= 0x4c)
lat = 0 - lat;
/* Decode the longitude, the first three bytes of the body after the data
* type indicator. First longitude degrees, remember the longitude offset.
*/
lng_deg = body[0] - 28;
if (d_start[4] >= 0x50)
lng_deg += 100;
if (lng_deg >= 180 && lng_deg <= 189)
lng_deg -= 80;
else if (lng_deg >= 190 && lng_deg <= 199)
lng_deg -= 190;
/* Decode the longitude minutes */
lng_min = body[1] - 28;
if (lng_min >= 60)
lng_min -= 60;
/* ... and minute decimals */
lng_min_frag = body[2] - 28;
/* apply position ambiguity to longitude */
switch (posambiguity) {
case 0:
/* use everything */
lng = (float)lng_deg + (float)lng_min / 60.0
+ (float)lng_min_frag / 6000.0;
break;
case 1:
/* ignore last number of lng_min_frag */
lng = (float)lng_deg + (float)lng_min / 60.0
+ (float)(lng_min_frag - lng_min_frag % 10 + 5) / 6000.0;
break;
case 2:
/* ignore lng_min_frag */
lng = (float)lng_deg + ((float)lng_min + 0.5) / 60.0;
break;
case 3:
/* ignore lng_min_frag and last number of lng_min */
lng = (float)lng_deg + (float)(lng_min - lng_min % 10 + 5) / 60.0;
break;
case 4:
/* minute is unused -> add 0.5 degrees to longitude */
lng = (float)lng_deg + 0.5;
break;
default:
return 0;
}
/* check the longitude E/W sign */
if (d_start[5] >= 0x50)
lng = 0 - lng;
/* save the symbol table and code */
sym_code = body[6];
sym_table = body[7];
/* ok, we're done */
/*
fprintf(stderr, "\tlat %u %u.%u (%.4f) lng %u %u.%u (%.4f)\n",
lat_deg, lat_min, lat_min_frag, lat,
lng_deg, lng_min, lng_min_frag, lng);
fprintf(stderr, "\tsym '%c' '%c'\n", sym_table, sym_code);
*/
return pbuf_fill_pos(pb, lat, lng, sym_table, sym_code);
}
/*
* Parse a compressed APRS position packet
*
* APRS PROTOCOL REFERENCE 1.0.1 Chapter 9, page 36 (46 in PDF)
*/
static int parse_aprs_compressed(struct pbuf_t *pb, const char *body, const char *body_end)
{
char sym_table, sym_code;
int i;
int lat1, lat2, lat3, lat4, lng1, lng2, lng3, lng4;
double lat = 0.0, lng = 0.0;
DEBUG_LOG("parse_aprs_compressed");
/* A compressed position is always 13 characters long.
* Make sure we get at least 13 characters and that they are ok.
* Also check the allowed base-91 characters at the same time.
*/
if (body_end - body < 13)
return 0; /* too short. */
sym_table = body[0]; /* has been validated before entering this function */
sym_code = body[9];
/* base-91 check */
for (i = 1; i <= 8; i++)
if (body[i] < 0x21 || body[i] > 0x7b)
return 0;
// fprintf(stderr, "\tpassed length and format checks, sym %c%c\n", sym_table, sym_code);
/* decode */
lat1 = (body[1] - 33);
lat2 = (body[2] - 33);
lat3 = (body[3] - 33);
lat4 = (body[4] - 33);
lat1 = ((((lat1 * 91) + lat2) * 91) + lat3) * 91 + lat4;
lng1 = (body[5] - 33);
lng2 = (body[6] - 33);
lng3 = (body[7] - 33);
lng4 = (body[8] - 33);
lng1 = ((((lng1 * 91) + lng2) * 91) + lng3) * 91 + lng4;
/* calculate latitude and longitude */
lat = 90.0F - ((float)(lat1) / 380926.0F);
lng = -180.0F + ((float)(lng1) / 190463.0F);
return pbuf_fill_pos(pb, lat, lng, sym_table, sym_code);
}
/*
* Parse an uncompressed "normal" APRS packet
*
* APRS PROTOCOL REFERENCE 1.0.1 Chapter 8, page 32 (42 in PDF)
*/
static int parse_aprs_uncompressed(struct pbuf_t *pb, const char *body, const char *body_end)
{
char posbuf[20];
unsigned int lat_deg = 0, lat_min = 0, lat_min_frag = 0, lng_deg = 0, lng_min = 0, lng_min_frag = 0;
float lat, lng;
char lat_hemi, lng_hemi;
char sym_table, sym_code;
int issouth = 0;
int iswest = 0;
DEBUG_LOG("parse_aprs_uncompressed");
if (body_end - body < 19) {
DEBUG_LOG("\ttoo short");
return 0;
}
/* make a local copy, so we can overwrite it at will. */
memcpy(posbuf, body, 19);
posbuf[19] = 0;
// fprintf(stderr, "\tposbuf: %s\n", posbuf);
/* position ambiquity is going to get ignored now, it's not needed in this application. */
/* lat */
if (posbuf[2] == ' ') posbuf[2] = '3';
if (posbuf[3] == ' ') posbuf[3] = '5';
if (posbuf[5] == ' ') posbuf[5] = '5';
if (posbuf[6] == ' ') posbuf[6] = '5';
/* lng */
if (posbuf[12] == ' ') posbuf[12] = '3';
if (posbuf[13] == ' ') posbuf[13] = '5';
if (posbuf[15] == ' ') posbuf[15] = '5';
if (posbuf[16] == ' ') posbuf[16] = '5';
// fprintf(stderr, "\tafter filling amb: %s\n", posbuf);
/* 3210.70N/13132.15E# */
if (sscanf(posbuf, "%2u%2u.%2u%c%c%3u%2u.%2u%c%c",
&lat_deg, &lat_min, &lat_min_frag, &lat_hemi, &sym_table,
&lng_deg, &lng_min, &lng_min_frag, &lng_hemi, &sym_code) != 10) {
DEBUG_LOG("\tsscanf failed");
return 0;
}
if (!valid_sym_table_uncompressed(sym_table))
sym_table = 0;
if (lat_hemi == 'S' || lat_hemi == 's')
issouth = 1;
else if (lat_hemi != 'N' && lat_hemi != 'n')
return 0; /* neither north or south? bail out... */
if (lng_hemi == 'W' || lng_hemi == 'w')
iswest = 1;
else if (lng_hemi != 'E' && lng_hemi != 'e')
return 0; /* neither west or east? bail out ... */
if (lat_deg > 89 || lng_deg > 179)
return 0; /* too large values for lat/lng degrees */
lat = (float)lat_deg + (float)lat_min / 60.0 + (float)lat_min_frag / 6000.0;
lng = (float)lng_deg + (float)lng_min / 60.0 + (float)lng_min_frag / 6000.0;
/* Finally apply south/west indicators */
if (issouth)
lat = 0.0 - lat;
if (iswest)
lng = 0.0 - lng;
// fprintf(stderr, "\tlat %u %u.%u %c (%.3f) lng %u %u.%u %c (%.3f)\n",
// lat_deg, lat_min, lat_min_frag, (int)lat_hemi, lat,
// lng_deg, lng_min, lng_min_frag, (int)lng_hemi, lng);
// fprintf(stderr, "\tsym '%c' '%c'\n", sym_table, sym_code);
return pbuf_fill_pos(pb, lat, lng, sym_table, sym_code);
}
/*
* Parse an APRS object
*
* APRS PROTOCOL REFERENCE 1.0.1 Chapter 11, page 58 (68 in PDF)
*/
static int parse_aprs_object(struct pbuf_t *pb, const char *body, const char *body_end)
{
int i;
int namelen = -1;
pb->packettype |= T_OBJECT;
DEBUG_LOG("parse_aprs_object");
/* check that the object name ends with either * or _ */
if (*(body + 9) != '*' && *(body + 9) != '_') {
DEBUG_LOG("\tinvalid object kill character");
return 0;
}
/* check that the timestamp ends with a z */
if (*(body + 16) != 'z' && *(body + 16) != 'Z') {
// fprintf(stderr, "\tinvalid object timestamp z character\n");
return 0;
}
/* check object's name - scan for non-printable characters and the last
* non-space character
*/
for (i = 0; i < 9; i++) {
if (body[i] < 0x20 || body[i] > 0x7e) {
DEBUG_LOG("\tobject name has unprintable characters");
return 0; /* non-printable */
}
if (body[i] != ' ')
namelen = i;
}
if (namelen < 0) {
DEBUG_LOG("\tobject has empty name");
return 0;
}
pb->srcname = body;
pb->srcname_len = namelen+1;
// fprintf(stderr, "\tobject name: '%.*s'\n", pb->srcname_len, pb->srcname);
/* Forward the location parsing onwards */
if (valid_sym_table_compressed(body[17]))
return parse_aprs_compressed(pb, body + 17, body_end);
if (body[17] >= '0' && body[17] <= '9')
return parse_aprs_uncompressed(pb, body + 17, body_end);
DEBUG_LOG("\tno valid position in object");
return 0;
}
/*
* Parse an APRS item
*
* APRS PROTOCOL REFERENCE 1.0.1 Chapter 11, page 59 (69 in PDF)
*/
static int parse_aprs_item(struct pbuf_t *pb, const char *body, const char *body_end)
{
int i;
pb->packettype |= T_ITEM;
DEBUG_LOG("parse_aprs_item");
/* check item's name - scan for non-printable characters and the
* ending character ! or _
*/
for (i = 0; i < 9 && body[i] != '!' && body[i] != '_'; i++) {
if (body[i] < 0x20 || body[i] > 0x7e) {
DEBUG_LOG("\titem name has unprintable characters");
return 0; /* non-printable */
}
}
if (body[i] != '!' && body[i] != '_') {
DEBUG_LOG("\titem name ends with neither ! or _");
return 0;
}
if (i < 3 || i > 9) {
DEBUG_LOG("\titem name has invalid length");
return 0;
}
pb->srcname = body;
pb->srcname_len = i;
//fprintf(stderr, "\titem name: '%.*s'\n", pb->srcname_len, pb->srcname);
/* Forward the location parsing onwards */
i++;
if (valid_sym_table_compressed(body[i]))
return parse_aprs_compressed(pb, body + i, body_end);
if (body[i] >= '0' && body[i] <= '9')
return parse_aprs_uncompressed(pb, body + i, body_end);
DEBUG_LOG("\tno valid position in item");
return 0;
}
/* forward declaration to allow recursive calls */
static int parse_aprs_body(struct pbuf_t *pb, const char *info_start);
/*
* Parse a 3rd-party packet.
* Requires the } > : sequence from src>dst,network,gate: to
* detect a packet as a 3rd-party packet. If the sequence is not found,
* returns 0 for no match (but do not drop packet).
* If the sequence is found, require the packet to match the 3rd-party
* packet spec from APRS101.PDF, and validate callsigns too.
*/
static int parse_aprs_3rdparty(struct pbuf_t *pb, const char *info_start)
{
const char *body;
const char *src_end;
const char *s;
const char *path_start;
const char *dstcall_end;
int pathlen;
/* ignore the CRLF in the end of the body */
s = info_start + 1;
/* find the end of the third-party inner header */
body = memchr(s, ':', pb->packet_len - 2 - (s-pb->data));
/* if not found, bail out */
if (!body)
return 0;
pathlen = body - s;
/* look for the '>' */
src_end = memchr(s, '>', pathlen < CALLSIGNLEN_MAX+1 ? pathlen : CALLSIGNLEN_MAX+1);
if (!src_end)
return 0; // No ">" in packet start..
path_start = src_end+1;
if (path_start >= body) // We're already at the path end
return INERR_INV_3RD_PARTY;
if (check_invalid_src_dst(s, src_end - s) != 0)
return INERR_INV_SRCCALL; /* invalid or too long for source callsign */
dstcall_end = path_start;
while (dstcall_end < body && *dstcall_end != ',' && *dstcall_end != ':')
dstcall_end++;
if (check_invalid_src_dst(path_start, dstcall_end - path_start))
return INERR_INV_DSTCALL; /* invalid or too long for destination callsign */
/* check if there are invalid callsigns in the digipeater path before Q,
* require at least two elements to be present (network ID, gateway callsign)
*/
if (check_path_calls(dstcall_end, body) < 2)
return INERR_INV_3RD_PARTY;
/* Ok, fill "name" parameter in packet with the 3rd-party packet
* srccall, so that filtering can match against it. This will be
* overwritten by object/item names.
*/
pb->srcname = s;
pb->srcname_len = src_end - s;
/* for now, just parse the inner packet content to learn it's type
* and coordinates, etc
*/
return parse_aprs_body(pb, body+1);
}
/*
* Parse the body of an APRS packet
*/
static int parse_aprs_body(struct pbuf_t *pb, const char *info_start)
{
char packettype, poschar;
int paclen;
const char *body;
const char *body_end;
const char *pos_start;
/* the following parsing logic has been translated from Ham::APRS::FAP
* Perl module to C
*/
/* length of the info field: length of the packet - length of header - CRLF */
paclen = pb->packet_len - (pb->info_start - pb->data) - 2;
if (paclen < 1) return 0; /* Empty frame */
/* Check the first character of the packet and determine the packet type */
packettype = *info_start;
/* failed parsing */
// fprintf(stderr, "parse_aprs (%d):\n", paclen);
// fwrite(info_start, paclen, 1, stderr);
// fprintf(stderr, "\n");
/* body is right after the packet type character */
body = info_start + 1;
/* ignore the CRLF in the end of the body */
body_end = pb->data + pb->packet_len - 2;
switch (packettype) {
/* the following are obsolete mic-e types: 0x1c 0x1d
* case 0x1c:
* case 0x1d:
*/
case 0x27: /* ' */
case 0x60: /* ` */
/* could be mic-e, minimum body length 9 chars */
if (paclen >= 9) {
pb->packettype |= T_POSITION;
return parse_aprs_mice(pb, (unsigned char *)body, (unsigned char *)body_end);
}
return 0;
/* normal aprs plaintext packet types: !=/@
* (! might also be a wx packet, so we check for it first and then fall through to
* the position packet code)
*/
case '!':
if (info_start[1] == '!') { /* Ultimeter 2000 */
pb->packettype |= T_WX;
return 0;
}
/* intentionally missing break here */
case '=':
case '/':
case '@':
/* check that we won't run over right away */
if (body_end - body < 10)
return 0;
/* Normal or compressed location packet, with or without
* timestamp, with or without messaging capability
*
* ! and / have messaging, / and @ have a prepended timestamp
*/
pb->packettype |= T_POSITION;
if (packettype == '/' || packettype == '@') {
/* With a prepended timestamp, jump over it. */
body += 7;
}
poschar = *body;
if (valid_sym_table_compressed(poschar)) { /* [\/\\A-Za-j] */
/* compressed position packet */
if (body_end - body >= 13)
return parse_aprs_compressed(pb, body, body_end);
} else if (poschar >= 0x30 && poschar <= 0x39) { /* [0-9] */
/* normal uncompressed position */
if (body_end - body >= 19)
return parse_aprs_uncompressed(pb, body, body_end);
}
return 0;
case '$':
if (body_end - body > 10) {
// Is it OK to declare it as position packet ?
return parse_aprs_nmea(pb, body, body_end);
}
return 0;
case ':':
pb->packettype |= T_MESSAGE;
// quick and loose way to identify NWS and SKYWARN messages
// they do apparently originate from "WXSRV", but that is not
// guaranteed thing...
if (memcmp(body,"NWS-",4) == 0) // as seen on specification
pb->packettype |= T_NWS;
if (memcmp(body,"NWS_",4) == 0) // as seen on data
pb->packettype |= T_NWS;
if (memcmp(body,"SKY",3) == 0) // as seen on specification
pb->packettype |= T_NWS;
// Is it perhaps TELEMETRY related "message" ?
if ( body[9] == ':' &&
( memcmp( body+9, ":PARM.", 6 ) == 0 ||
memcmp( body+9, ":UNIT.", 6 ) == 0 ||
memcmp( body+9, ":EQNS.", 6 ) == 0 ||
memcmp( body+9, ":BITS.", 6 ) == 0 )) {
pb->packettype &= ~T_MESSAGE;
pb->packettype |= T_TELEMETRY;
// Fall through to recipient location lookup
}
// Or perhaps a DIRECTED QUERY ?
/* It might not be a bright idea to mark all messages starting with ?
* queries instead of messages and making them NOT match the
* filter message.
* ALSO: General (non-directed) queries are DROPPED by aprsc.
* Do not mark DIRECTED QUERIES as queries - we don't want to drop them.
if (body[9] == ':' && body[10] == '?') {
pb->packettype &= ~T_MESSAGE;
pb->packettype |= T_QUERY;
// Fall through to recipient location lookup
}
*/
// Now find out if the message RECIPIENT address is known
// to have some location data ? Because then we can treat
// them the same way in filters as we do those with real
// positions..
{
/* collect destination callsign of the message */
//char keybuf[CALLSIGNLEN_MAX+1];
const char *p;
int i;
//struct history_cell_t *history;
pb->dstname = body;
p = body;
for (i = 0; i < CALLSIGNLEN_MAX; ++i) {
//keybuf[i] = *p;
// the recipient address is space padded
// to 9 chars, while our historydb is not.
if (*p == 0 || *p == ' ' || *p == ':')
break;
p++;
}
//keybuf[i] = 0;
pb->dstname_len = p - body;
//DEBUG_LOG("message: dstname len %d", pb->dstname_len);
/*
* This adds a position for a message based on the
* recipient, causing it to match an area filter.
* This is not what javAPRSSrvr does, so let's not do it
* quite yet. Compatibility first, at first.
*/
/*
i = historydb_lookup( keybuf, i, &history );
if (i > 0) {
pb->lat = history->lat;
pb->lng = history->lon;
pb->cos_lat = history->coslat;
pb->flags |= F_HASPOS;
return 1;
}
*/
}
return 0;
case ';':
if (body_end - body > 29)
return parse_aprs_object(pb, body, body_end);
return 0;
case '>':
pb->packettype |= T_STATUS;
return 0;
case '<':
pb->packettype |= T_STATCAPA;
return 0;
case '?':
pb->packettype |= T_QUERY;
return 0;
case ')':
if (body_end - body > 18) {
return parse_aprs_item(pb, body, body_end);
}
return 0;
case 'T':
if (body_end - body > 18) {
pb->packettype |= T_TELEMETRY;
return parse_aprs_telem(pb, body, body_end);
}
return 0;
case '#': /* Peet Bros U-II Weather Station */
case '*': /* Peet Bros U-I Weather Station */
case '_': /* Weather report without position */
pb->packettype |= T_WX;
return 0;
case '{':
pb->packettype |= T_USERDEF;
return 0;
case '}':
pb->packettype |= T_3RDPARTY;
return parse_aprs_3rdparty(pb, info_start);
default:
break;
}
/* When all else fails, try to look for a !-position that can
* occur anywhere within the 40 first characters according
* to the spec. (X1J TNC digipeater bugs...)
*/
pos_start = memchr(body, '!', body_end - body);
if ((pos_start) && pos_start - body <= 39) {
poschar = *pos_start;
if (valid_sym_table_compressed(poschar)) { /* [\/\\A-Za-j] */
/* compressed position packet */
if (body_end - pos_start >= 13)
return parse_aprs_compressed(pb, pos_start, body_end);
return 0;
} else if (poschar >= 0x30 && poschar <= 0x39) { /* [0-9] */
/* normal uncompressed position */
if (body_end - pos_start >= 19)
return parse_aprs_uncompressed(pb, pos_start, body_end);
return 0;
}
}
return 0;
}
/*
* Try to parse an APRS packet.
* Returns 1 if position was parsed successfully,
* 0 if parsing failed, < 0 if packet should be dropped.
*
* Does also front-end part of the output filter's
* packet type classification job.
*
* TODO: Recognize TELEM packets in !/=@ packets too!
*
*/
int parse_aprs(struct pbuf_t *pb)
{
if (!pb->info_start)
return 0;
pb->packettype = T_ALL;
/* T_CW detection - this is not really right, I think they're already
* going with DW and EW prefixes
*/
if (pb->data[0] == 'C' && /* Perhaps CWOP ? */
pb->data[1] == 'W') {
const char *s = pb->data + 2;
const char *pe = pb->data + pb->packet_len;
for ( ; *s && s < pe ; ++s ) {
int c = *s;
if (c < '0' || c > '9')
break;
}
if (*s == '>')
pb->packettype |= T_CWOP;
}
return parse_aprs_body(pb, pb->info_start);
}
/*
* Parse an aprs text message (optional, only done to messages addressed to
* SERVER
*/
int parse_aprs_message(struct pbuf_t *pb, struct aprs_message_t *am)
{
const char *p;
memset(am, 0, sizeof(*am));
if (!(pb->packettype & T_MESSAGE))
return -1;
if (pb->info_start[10] != ':')
return -2;
am->body = pb->info_start + 11;
/* -2 for the CRLF already in place */
am->body_len = pb->packet_len - 2 - (pb->info_start - pb->data);
/* search for { looking backwards from the end of the packet,
* it separates the msgid
*/
p = am->body + am->body_len - 1;
while (p > am->body && *p != '{')
p--;
if (*p == '{') {
am->msgid = p+1;
am->msgid_len = pb->packet_len - 2 - (am->msgid - pb->data);
am->body_len = p - am->body;
}
/* check if this is an ACK */
if ((!am->msgid_len) && am->body_len > 3
&& am->body[0] == 'a' && am->body[1] == 'c' && am->body[2] == 'k') {
am->is_ack = 1;
am->msgid = am->body + 3;
am->msgid_len = am->body_len - 3;
am->body_len = 0;
return 0;
}
return 0;
}