direwolf/encode_aprs.c

821 lines
21 KiB
C

//
// This file is part of Dire Wolf, an amateur radio packet TNC.
//
// Copyright (C) 2013, 2014 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/>.
//
/*------------------------------------------------------------------
*
* Module: encode_aprs.c
*
* Purpose: Construct APRS packets from components.
*
* Description:
*
* References: APRS Protocol Reference.
*
* Frequency spec.
* http://www.aprs.org/info/freqspec.txt
*
*---------------------------------------------------------------*/
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <unistd.h>
#include <ctype.h>
#include <time.h>
#include <math.h>
#include <assert.h>
#include "direwolf.h"
#include "encode_aprs.h"
#include "latlong.h"
#include "textcolor.h"
/*------------------------------------------------------------------
*
* Name: set_norm_position
*
* Purpose: Fill in the human-readable latitude, longitude,
* symbol part which is common to multiple data formats.
*
* Inputs: symtab - Symbol table id or overlay.
* symbol - Symbol id.
* dlat - Latitude.
* dlong - Longitude.
*
* Outputs: presult - Stored here.
*
* Returns: Number of characters in result.
*
*----------------------------------------------------------------*/
/* Position & symbol fields common to several message formats. */
typedef struct position_s {
char lat[8];
char sym_table_id; /* / \ 0-9 A-Z */
char lon[9];
char symbol_code;
} position_t;
static int set_norm_position (char symtab, char symbol, double dlat, double dlong, position_t *presult)
{
latitude_to_str (dlat, 0, presult->lat);
if (symtab != '/' && symtab != '\\' && ! isdigit(symtab) && ! isupper(symtab)) {
text_color_set(DW_COLOR_ERROR);
dw_printf ("Symbol table identifier is not one of / \\ 0-9 A-Z\n");
}
presult->sym_table_id = symtab;
longitude_to_str (dlong, 0, presult->lon);
if (symbol < '!' || symbol > '~') {
text_color_set(DW_COLOR_ERROR);
dw_printf ("Symbol code is not in range of ! to ~\n");
}
presult->symbol_code = symbol;
return (sizeof(position_t));
}
/*------------------------------------------------------------------
*
* Name: set_comp_position
*
* Purpose: Fill in the compressed latitude, longitude,
* symbol part which is common to multiple data formats.
*
* Inputs: symtab - Symbol table id or overlay.
* symbol - Symbol id.
* dlat - Latitude.
* dlong - Longitude.
*
* power - Watts.
* height - Feet.
* gain - dBi.
*
* course - Degress, 1 - 360. 0 means none or unknown.
* speed - knots.
*
*
* Outputs: presult - Stored here.
*
* Returns: Number of characters in result.
*
* Description: The cst field can have only one of
*
* course/speed - takes priority (this implementation)
* radio range - calculated from PHG
* altitude - not implemented yet.
*
*----------------------------------------------------------------*/
/* Compressed position & symbol fields common to several message formats. */
typedef struct compressed_position_s {
char sym_table_id; /* / \ a-j A-Z */
/* "The presence of the leading Symbol Table Identifier */
/* instead of a digit indicates that this is a compressed */
/* Position Report and not a normal lat/long report." */
char y[4]; /* Compressed Latitude. */
char x[4]; /* Compressed Longitude. */
char symbol_code;
char c; /* Course/speed or radio range or altitude. */
char s;
char t ; /* Compression type. */
} compressed_position_t;
static int set_comp_position (char symtab, char symbol, double dlat, double dlong,
int power, int height, int gain,
int course, int speed,
compressed_position_t *presult)
{
if (symtab != '/' && symtab != '\\' && ! isdigit(symtab) && ! isupper(symtab)) {
text_color_set(DW_COLOR_ERROR);
dw_printf ("Symbol table identifier is not one of / \\ 0-9 A-Z\n");
}
/*
* In compressed format, the characters a-j are used for a numeric overlay.
* This allows the receiver to distinguish between compressed and normal formats.
*/
if (isdigit(symtab)) {
symtab = symtab - '0' + 'a';
}
presult->sym_table_id = symtab;
latitude_to_comp_str (dlat, presult->y);
longitude_to_comp_str (dlong, presult->x);
if (symbol < '!' || symbol > '~') {
text_color_set(DW_COLOR_ERROR);
dw_printf ("Symbol code is not in range of ! to ~\n");
}
presult->symbol_code = symbol;
/*
* The cst field is complicated.
*
* When c is ' ', the cst field is not used.
*
* When the t byte has a certain pattern, c & s represent altitude.
*
* Otherwise, c & s can be either course/speed or radio range.
*
* When c is in range of '!' to 'z',
*
* ('!' - 33) * 4 = 0 degrees.
* ...
* ('z' - 33) * 4 = 356 degrees.
*
* In this case, s represents speed ...
*
* When c is '{', s is range ...
*/
if (course || speed) {
int c;
int s;
c = (course + 1) / 4;
if (c < 0) c += 90;
if (c >= 90) c -= 90;
presult->c = c + '!';
s = (int)round(log(speed+1.0) / log(1.08));
presult->s = s + '!';
presult->t = 0x26 + '!'; /* current, other tracker. */
}
else if (power || height || gain) {
int s;
float range;
presult->c = '{'; /* radio range. */
if (power == 0) power = 10;
if (height == 0) height = 20;
if (gain == 0) gain = 3;
// from protocol reference page 29.
range = sqrt(2.0*height * sqrt((power/10.0) * (gain/2.0)));
s = (int)round(log(range/2.) / log(1.08));
if (s < 0) s = 0;
if (s > 93) s = 93;
presult->s = s + '!';
presult->t = 0x26 + '!'; /* current, other tracker. */
}
else {
presult->c = ' '; /* cst field not used. */
presult->s = ' ';
presult->t = '!'; /* avoid space. */
}
return (sizeof(compressed_position_t));
}
/*------------------------------------------------------------------
*
* Name: phg_data_extension
*
* Purpose: Fill in parts of the power/height/gain data extension.
*
* Inputs: power - Watts.
* height - Feet.
* gain - dB. Not clear if it is dBi or dBd.
* dir - Directivity: N, NE, etc., omni.
*
* Outputs: presult - Stored here.
*
* Returns: Number of characters in result.
*
*----------------------------------------------------------------*/
typedef struct phg_s {
char P;
char H;
char G;
char p;
char h;
char g;
char d;
} phg_t;
static int phg_data_extension (int power, int height, int gain, char *dir, char *presult)
{
phg_t *r = (phg_t*)presult;
int x;
r->P = 'P';
r->H = 'H';
r->G = 'G';
x = (int)round(sqrt((float)power)) + '0';
if (x < '0') x = '0';
else if (x > '9') x = '9';
r->p = x;
x = (int)round(log2(height/10.0)) + '0';
if (x < '0') x = '0';
/* Result can go beyond '9'. */
r->h = x;
x = gain + '0';
if (x < '0') x = '0';
else if (x > '9') x = '0';
r->g = x;
r->d = '0';
if (dir != NULL) {
if (strcasecmp(dir,"NE") == 0) r->d = '1';
if (strcasecmp(dir,"E") == 0) r->d = '2';
if (strcasecmp(dir,"SE") == 0) r->d = '3';
if (strcasecmp(dir,"S") == 0) r->d = '4';
if (strcasecmp(dir,"SW") == 0) r->d = '5';
if (strcasecmp(dir,"W") == 0) r->d = '6';
if (strcasecmp(dir,"NW") == 0) r->d = '7';
if (strcasecmp(dir,"N") == 0) r->d = '8';
}
return (sizeof(phg_t));
}
/*------------------------------------------------------------------
*
* Name: cse_spd_data_extension
*
* Purpose: Fill in parts of the course & speed data extension.
*
* Inputs: course - Degress, 1 - 360.
* speed - knots.
*
* Outputs: presult - Stored here.
*
* Returns: Number of characters in result.
*
*----------------------------------------------------------------*/
typedef struct cs_s {
char cse[3];
char slash;
char spd[3];
} cs_t;
static int cse_spd_data_extension (int course, int speed, char *presult)
{
cs_t *r = (cs_t*)presult;
char stemp[8];
int x;
x = course;
if (x < 0) x = 0;
if (x > 360) x = 360;
sprintf (stemp, "%03d", x);
memcpy (r->cse, stemp, 3);
r->slash = '/';
x = speed;
if (x < 0) x = 0;
if (x > 999) x = 999;
sprintf (stemp, "%03d", x);
memcpy (r->spd, stemp, 3);
return (sizeof(cs_t));
}
/*------------------------------------------------------------------
*
* Name: frequency_spec
*
* Purpose: Put frequency specification in beginning of comment field.
*
* Inputs: freq - MHz.
* tone - Hz.
* offset - MHz.
*
* Outputs: presult - Stored here.
*
* Returns: Number of characters in result.
*
* Description: There are several valid variations.
*
* The frequency could be missing here if it is in the
* object name. In this case we could have tone & offset.
*
* Offset must always be preceded by tone.
*
*----------------------------------------------------------------*/
typedef struct freq_s {
char f[7]; /* format 999.999 */
char mhz[3];
char space;
} freq_t;
typedef struct to_s {
char T;
char ttt[3]; /* format 999 (drop fraction) or 'off'. */
char space1;
char oooo[4]; /* leading sign, 3 digits, tens of KHz. */
char space2;
} to_t;
static int frequency_spec (float freq, float tone, float offset, char *presult)
{
int result_len = 0;
if (freq != 0) {
freq_t *f = (freq_t*)presult;
char stemp[12];
/* Should use letters for > 999.999. */
sprintf (stemp, "%07.3f", freq);
memcpy (f->f, stemp, 7);
memcpy (f->mhz, "MHz", 3);
f->space = ' ';
result_len = sizeof (freq_t);
}
if (tone != 0 || offset != 0) {
to_t *to = (to_t*)(presult + result_len);
char stemp[12];
to->T = 'T';
if (tone == 0) {
memcpy(to->ttt, "off", 3);
}
else {
sprintf (stemp, "%03d", (int)tone);
memcpy (to->ttt, stemp, 3);
}
to->space1 = ' ';
sprintf (stemp, "%+04d", (int)round(offset * 100));
memcpy (to->oooo, stemp, 4);
to->space2 = ' ';
result_len += sizeof (to_t);
}
return (result_len);
}
/*------------------------------------------------------------------
*
* Name: encode_position
*
* Purpose: Construct info part for position report format.
*
* Inputs: messaging - This determines whether the data type indicator
* is set to '!' (false) or '=' (true).
* compressed - Send in compressed form?
* lat - Latitude.
* lon - Longitude.
* alt_ft - Altitude in feet.
* symtab - Symbol table id or overlay.
* symbol - Symbol id.
*
* power - Watts.
* height - Feet.
* gain - dB. Not clear if it is dBi or dBd.
* dir - Directivity: N, NE, etc., omni.
*
* course - Degress, 1 - 360. 0 means none or unknown.
* speed - knots.
*
* freq - MHz.
* tone - Hz.
* offset - MHz.
*
* comment - Additional comment text.
*
*
* Outputs: presult - Stored here. Should be at least ??? bytes.
*
* Returns: Number of characters in result.
*
* Description: There can be a single optional "data extension"
* following the position so there is a choice
* between:
* Power/height/gain/directivity or
* Course/speed.
*
* Afer that,
*
*----------------------------------------------------------------*/
typedef struct aprs_ll_pos_s {
char dti; /* ! or = */
position_t pos;
/* Comment up to 43 characters. */
/* Start of comment could be data extension(s). */
} aprs_ll_pos_t;
typedef struct aprs_compressed_pos_s {
char dti; /* ! or = */
compressed_position_t cpos;
/* Comment up to 40 characters. */
/* No data extension allowed for compressed location. */
} aprs_compressed_pos_t;
int encode_position (int messaging, int compressed, double lat, double lon, int alt_ft,
char symtab, char symbol,
int power, int height, int gain, char *dir,
int course, int speed,
float freq, float tone, float offset,
char *comment,
char *presult)
{
int result_len = 0;
if (compressed) {
aprs_compressed_pos_t *p = (aprs_compressed_pos_t *)presult;
p->dti = messaging ? '=' : '!';
set_comp_position (symtab, symbol, lat, lon,
power, height, gain,
course, speed,
&(p->cpos));
result_len = 1 + sizeof (p->cpos);
}
else {
aprs_ll_pos_t *p = (aprs_ll_pos_t *)presult;
p->dti = messaging ? '=' : '!';
set_norm_position (symtab, symbol, lat, lon, &(p->pos));
result_len = 1 + sizeof (p->pos);
/* Optional data extension. (singular) */
/* Can't have both course/speed and PHG. Former gets priority. */
if (course || speed) {
result_len += cse_spd_data_extension (course, speed, presult + result_len);
}
else if (power || height || gain) {
result_len += phg_data_extension (power, height, gain, dir, presult + result_len);
}
}
/* Optional frequency spec. */
if (freq != 0 || tone != 0 || offset != 0) {
result_len += frequency_spec (freq, tone, offset, presult + result_len);
}
presult[result_len] = '\0';
/* Altitude. Can be anywhere in comment. */
if (alt_ft != G_UNKNOWN) {
char salt[12];
/* Not clear if altitude can be negative. */
/* Be sure it will be converted to 6 digits. */
if (alt_ft < 0) alt_ft = 0;
if (alt_ft > 999999) alt_ft = 999999;
sprintf (salt, "/A=%06d", alt_ft);
strcat (presult, salt);
result_len += strlen(salt);
}
/* Finally, comment text. */
if (comment != NULL) {
strcat (presult, comment);
result_len += strlen(comment);
}
return (result_len);
} /* end encode_position */
/*------------------------------------------------------------------
*
* Name: encode_object
*
* Purpose: Construct info part for object report format.
*
* Inputs: name - Name, up to 9 characters.
* compressed - Send in compressed form?
* thyme - Time stamp or 0 for none.
* lat - Latitude.
* lon - Longitude.
* symtab - Symbol table id or overlay.
* symbol - Symbol id.
*
* power - Watts.
* height - Feet.
* gain - dB. Not clear if it is dBi or dBd.
* dir - Direction: N, NE, etc., omni.
*
* course - Degress, 1 - 360. 0 means none or unknown.
* speed - knots.
*
* freq - MHz.
* tone - Hz.
* offset - MHz.
*
* comment - Additional comment text.
*
* Outputs: presult - Stored here. Should be at least ??? bytes.
*
* Returns: Number of characters in result.
*
* Description:
*
*----------------------------------------------------------------*/
typedef struct aprs_object_s {
struct {
char dti; /* ; */
char name[9];
char live_killed; /* * for live or _ for killed */
char time_stamp[7];
} o;
union {
position_t pos; /* Up to 43 char comment. First 7 bytes could be data extension. */
compressed_position_t cpos; /* Up to 40 char comment. No PHG data extension in this case. */
} u;
} aprs_object_t;
int encode_object (char *name, int compressed, time_t thyme, double lat, double lon,
char symtab, char symbol,
int power, int height, int gain, char *dir,
int course, int speed,
float freq, float tone, float offset, char *comment,
char *presult)
{
aprs_object_t *p = (aprs_object_t *) presult;
int result_len = 0;
int n;
p->o.dti = ';';
memset (p->o.name, ' ', sizeof(p->o.name));
n = strlen(name);
if (n > sizeof(p->o.name)) n = sizeof(p->o.name);
memcpy (p->o.name, name, n);
p->o.live_killed = '*';
if (thyme != 0) {
struct tm tm;
#define XMIT_UTC 1
#if XMIT_UTC
gmtime_r (&thyme, &tm);
#else
/* Using local time, for this application, would make more sense to me. */
/* On Windows, localtime_r produces UTC. */
/* How do we set the time zone? Google for mingw time zone. */
localtime_r (thyme, &tm);
#endif
sprintf (p->o.time_stamp, "%02d%02d%02d", tm.tm_mday, tm.tm_hour, tm.tm_min);
#if XMIT_UTC
p->o.time_stamp[6] = 'z';
#else
p->o.time_stamp[6] = '/';
#endif
}
else {
memcpy (p->o.time_stamp, "111111z", sizeof(p->o.time_stamp));
}
if (compressed) {
set_comp_position (symtab, symbol, lat, lon,
power, height, gain,
course, speed,
&(p->u.cpos));
result_len = sizeof(p->o) + sizeof (p->u.cpos);
}
else {
set_norm_position (symtab, symbol, lat, lon, &(p->u.pos));
result_len = sizeof(p->o) + sizeof (p->u.pos);
/* Optional data extension. (singular) */
/* Can't have both course/speed and PHG. Former gets priority. */
if (course || speed) {
result_len += cse_spd_data_extension (course, speed, presult + result_len);
}
else if (power || height || gain) {
result_len += phg_data_extension (power, height, gain, dir, presult + result_len);
}
}
/* Optional frequency spec. */
if (freq != 0 || tone != 0 || offset != 0) {
result_len += frequency_spec (freq, tone, offset, presult + result_len);
}
presult[result_len] = '\0';
/* Finally, comment text. */
if (comment != NULL) {
strcat (presult, comment);
result_len += strlen(comment);
}
return (result_len);
} /* end encode_object */
/*------------------------------------------------------------------
*
* Name: main
*
* Purpose: Quick test for some functions in this file.
*
* Description: Just a smattering, not an organized test.
*
* $ rm a.exe ; gcc -DEN_MAIN encode_aprs.c latlong.c textcolor.c ; ./a.exe
*
*----------------------------------------------------------------*/
#if EN_MAIN
int main (int argc, char *argv[])
{
char result[100];
/*********** Position ***********/
encode_position (0, 42+34.61/60, -(71+26.47/60), G_UNKNOWN, 'D', '&',
0, 0, 0, NULL, 0, 0, 0, 0, 0, NULL, result);
dw_printf ("%s\n", result);
if (strcmp(result, "!4234.61ND07126.47W&") != 0) dw_printf ("ERROR! line %d\n", __LINE__);
/* with PHG. */
encode_position (0, 42+34.61/60, -(71+26.47/60), G_UNKNOWN, 'D', '&',
50, 100, 6, "N", 0, 0, 0, 0, 0, NULL, result);
dw_printf ("%s\n", result);
if (strcmp(result, "!4234.61ND07126.47W&PHG7368") != 0) dw_printf ("ERROR! line %d\n", __LINE__);
/* with freq. */
encode_position (0, 42+34.61/60, -(71+26.47/60), G_UNKNOWN, 'D', '&',
0, 0, 0, NULL, 0, 0, 146.955, 74.4, -0.6, NULL, result);
dw_printf ("%s\n", result);
if (strcmp(result, "!4234.61ND07126.47W&146.955MHz T074 -060 ") != 0) dw_printf ("ERROR! line %d\n", __LINE__);
/* with course/speed, freq, and comment! */
encode_position (0, 42+34.61/60, -(71+26.47/60), G_UNKNOWN, 'D', '&',
0, 0, 0, NULL, 180, 55, 146.955, 74.4, -0.6, "River flooding", result);
dw_printf ("%s\n", result);
if (strcmp(result, "!4234.61ND07126.47W&180/055146.955MHz T074 -060 River flooding") != 0) dw_printf ("ERROR! line %d\n", __LINE__);
/* Course speed, no tone, + offset */
encode_position (0, 42+34.61/60, -(71+26.47/60), G_UNKNOWN, 'D', '&',
0, 0, 0, NULL, 180, 55, 146.955, 0, 0.6, "River flooding", result);
dw_printf ("%s\n", result);
if (strcmp(result, "!4234.61ND07126.47W&180/055146.955MHz Toff +060 River flooding") != 0) dw_printf ("ERROR! line %d\n", __LINE__);
/* Course speed, no tone, + offset + altitude */
encode_position (0, 42+34.61/60, -(71+26.47/60), 12345, 'D', '&',
0, 0, 0, NULL, 180, 55, 146.955, 0, 0.6, "River flooding", result);
dw_printf ("%s\n", result);
if (strcmp(result, "!4234.61ND07126.47W&180/055146.955MHz Toff +060 /A=012345River flooding") != 0) dw_printf ("ERROR! line %d\n", __LINE__);
/*********** Compressed position. ***********/
encode_position (1, 42+34.61/60, -(71+26.47/60), G_UNKNOWN, 'D', '&',
0, 0, 0, NULL, 0, 0, 0, 0, 0, NULL, result);
dw_printf ("%s\n", result);
if (strcmp(result, "!D8yKC<Hn[& !") != 0) dw_printf ("ERROR! line %d\n", __LINE__);
/* with PHG. In this case it is converted to precomputed radio range. TODO: check on this. Is 27.4 correct? */
encode_position (1, 42+34.61/60, -(71+26.47/60), G_UNKNOWN, 'D', '&',
50, 100, 6, "N", 0, 0, 0, 0, 0, NULL, result);
dw_printf ("%s\n", result);
if (strcmp(result, "!D8yKC<Hn[&{CG") != 0) dw_printf ("ERROR! line %d\n", __LINE__);
/* with course/speed, freq, and comment! TODO: check on this 55 knots should be 63 MPH. we get 62. */
encode_position (1, 42+34.61/60, -(71+26.47/60), G_UNKNOWN, 'D', '&',
0, 0, 0, NULL, 180, 55, 146.955, 74.4, -0.6, "River flooding", result);
dw_printf ("%s\n", result);
if (strcmp(result, "!D8yKC<Hn[& !146.955MHz T074 -060 River flooding") != 0) dw_printf ("ERROR! line %d\n", __LINE__);
//$ echo 'A>B:!D8yKC<Hn[&NUG146.955MHz T074 -060 River flooding' | decode_aprs
//A>B:!D8yKC<Hn[&NUG146.955MHz T074 -060 River flooding
//Position, I=Igte IGate R=RX T=1hopTX 2=2hopTX w/overlay D
//N 42 34.6100, W 071 26.4700, 62 MPH, course 180, 146.955 MHz, -600k, PL 74.4
// River flooding
// TODO: test alt; cs+alt
/*********** Object. ***********/
encode_object ("WB1GOF-C", 0, 0, 42+34.61/60, -(71+26.47/60), 'D', '&',
0, 0, 0, NULL, 0, 0, 0, 0, 0, NULL, result);
dw_printf ("%s\n", result);
if (strcmp(result, ";WB1GOF-C *111111z4234.61ND07126.47W&") != 0) dw_printf ("ERROR! line %d\n", __LINE__);
// TODO: need more tests.
return(0);
} /* end main */
#endif /* unit test */
/* end encode_aprs.c */