//
// This file is part of Dire Wolf, an amateur radio packet TNC.
//
// Copyright (C) 2013, 2014, 2015 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 .
//
/*------------------------------------------------------------------
*
* Module: aprs_tt.c
*
* Purpose: First half of APRStt gateway.
*
* Description: This file contains functions to parse the tone sequences
* and extract meaning from them.
*
* tt_user.c maintains information about users and
* generates the APRS Object Reports.
*
*
* References: This is based upon APRStt (TM) documents with some
* artistic freedom.
*
* http://www.aprs.org/aprstt.html
*
*---------------------------------------------------------------*/
#define APRS_TT_C 1
#include "direwolf.h"
// TODO: clean up terminolgy.
// "Message" has a specific meaning in APRS and this is not it.
// Touch Tone sequence should be appropriate.
// What do we call the parts separated by * key? Field.
#include
#include
#include
#include
#include
#include
#include
#include
#include "version.h"
#include "ax25_pad.h"
#include "hdlc_rec2.h" /* for process_rec_frame */
#include "textcolor.h"
#include "aprs_tt.h"
#include "tt_text.h"
#include "tt_user.h"
#include "symbols.h"
#include "latlong.h"
#include "dlq.h"
#include "demod.h" /* for alevel_t & demod_get_audio_level() */
#include "tq.h"
// geotranz
#include "utm.h"
#include "mgrs.h"
#include "usng.h"
#include "error_string.h"
/* Convert between degrees and radians. */
#define D2R(d) ((d) * M_PI / 180.)
#define R2D(r) ((r) * 180. / M_PI)
/*
* Touch Tone sequences are accumulated here until # terminator found.
* Kept separate for each audio channel so the gateway CAN be listening
* on multiple channels at the same time.
*/
#define MAX_MSG_LEN 100
static char msg_str[MAX_CHANS][MAX_MSG_LEN+1];
static int msg_len[MAX_CHANS];
static int parse_fields (char *msg);
static int parse_callsign (char *e);
static int parse_object_name (char *e);
static int parse_symbol (char *e);
static int parse_aprstt3_call (char *e);
static int parse_location (char *e);
static int parse_comment (char *e);
static int expand_macro (char *e);
#ifndef TT_MAIN
static void raw_tt_data_to_app (int chan, char *msg);
#endif
static int find_ttloc_match (char *e, char *xstr, char *ystr, char *zstr, char *bstr, char *dstr, size_t valstrsize);
#if TT_MAIN
static void check_result (void);
#endif
/*------------------------------------------------------------------
*
* Name: aprs_tt_init
*
* Purpose: Initialize the APRStt gateway at system startup time.
*
* Inputs: Configuration options gathered by config.c.
*
* Global out: Make our own local copy of the structure here.
*
* Returns: None
*
* Description: The main program needs to call this at application
* start up time after reading the configuration file.
*
* TT_MAIN is defined for unit testing.
*
*----------------------------------------------------------------*/
static struct tt_config_s tt_config;
#if TT_MAIN
#define NUM_TEST_CONFIG (sizeof(test_config) / sizeof (struct ttloc_s))
static struct ttloc_s test_config[] = {
{ TTLOC_POINT, "B01", .point.lat = 12.25, .point.lon = 56.25 },
{ TTLOC_POINT, "B988", .point.lat = 12.50, .point.lon = 56.50 },
{ TTLOC_VECTOR, "B5bbbdddd", .vector.lat = 53., .vector.lon = -1., .vector.scale = 1000. }, /* km units */
/* Hilltop Tower http://www.aprs.org/aprs-jamboree-2013.html */
{ TTLOC_VECTOR, "B5bbbddd", .vector.lat = 37+55.37/60., .vector.lon = -(81+7.86/60.), .vector.scale = 16.09344 }, /* .01 mile units */
{ TTLOC_GRID, "B2xxyy", .grid.lat0 = 12.00, .grid.lon0 = 56.00,
.grid.lat9 = 12.99, .grid.lon9 = 56.99 },
{ TTLOC_GRID, "Byyyxxx", .grid.lat0 = 37 + 50./60.0, .grid.lon0 = 81,
.grid.lat9 = 37 + 59.99/60.0, .grid.lon9 = 81 + 9.99/60.0 },
{ TTLOC_MHEAD, "BAxxxxxx", .mhead.prefix = "326129" },
{ TTLOC_SATSQ, "BAxxxx" },
{ TTLOC_MACRO, "xxyyy", .macro.definition = "B9xx*AB166*AA2B4C5B3B0Ayyy" },
{ TTLOC_MACRO, "xxxxzzzzzzzzzz", .macro.definition = "BAxxxx*ACzzzzzzzzzz" },
};
#endif
void aprs_tt_init (struct tt_config_s *p)
{
int c;
#if TT_MAIN
/* For unit testing. */
memset (&tt_config, 0, sizeof(struct tt_config_s));
tt_config.ttloc_size = NUM_TEST_CONFIG;
tt_config.ttloc_ptr = test_config;
tt_config.ttloc_len = NUM_TEST_CONFIG;
/* Don't care about xmit timing or corral here. */
#else
// TODO: Keep ptr instead of making a copy.
memcpy (&tt_config, p, sizeof(struct tt_config_s));
#endif
for (c=0; c= 0 && chan < MAX_CHANS);
//if (button != '.') {
// dw_printf ("aprs_tt_button (%d, '%c')\n", chan, button);
//}
// TODO: Might make more sense to put timeout here rather in the dtmf decoder.
if (button == '$') {
/* Timeout reset. */
msg_len[chan] = 0;
msg_str[chan][0] = '\0';
}
else if (button != '.' && button != ' ') {
if (msg_len[chan] < MAX_MSG_LEN) {
msg_str[chan][msg_len[chan]++] = button;
msg_str[chan][msg_len[chan]] = '\0';
}
if (button == '#') {
/*
* Put into the receive queue like any other packet.
* This way they are all processed by the common receive thread
* rather than the thread associated with the particular audio device.
*/
raw_tt_data_to_app (chan, msg_str[chan]);
msg_len[chan] = 0;
msg_str[chan][0] = '\0';
}
}
else {
/*
* Idle time. Poll occasionally for processing.
* Timing would be off we we are listening to more than
* one channel so do this only for the one specified
* in the TTOBJ command.
*/
if (chan == tt_config.obj_recv_chan) {
poll_period++;
if (poll_period >= 39) {
poll_period = 0;
tt_user_background ();
}
}
}
} /* end aprs_tt_button */
#endif
/*------------------------------------------------------------------
*
* Name: aprs_tt_sequence
*
* Purpose: Process complete received touch tone sequence
* terminated by #.
*
* Inputs: chan - Audio channel it came from.
*
* msg - String of DTMF buttons.
* # should be the final character.
*
* Returns: None
*
* Description: Process a complete tone sequence.
* It should have one or more fields separated by *
* and terminated by a final # like these:
*
* callsign #
* entry1 * callsign #
* entry1 * entry * callsign #
*
* Limitation: Has one set of static data for communication among
* group of functions. This shouldn't be a problem
* when receiving on multiple channels at once
* because they get serialized thru the receive packet queue.
*
*----------------------------------------------------------------*/
static char m_callsign[20]; /* really object name */
/*
* Standard APRStt has symbol code 'A' (box) with overlay of 0-9, A-Z.
*
* Dire Wolf extension allows:
* Symbol table '/' (primary), any symbol code.
* Symbol table '\' (alternate), any symbol code.
* Alternate table symbol code, overlay of 0-9, A-Z.
*/
static char m_symtab_or_overlay;
static char m_symbol_code; // Default 'A'
static char m_loc_text[24];
static double m_longitude; // Set to G_UNKNOWN if not defined.
static double m_latitude; // Set to G_UNKNOWN if not defined.
static int m_ambiguity;
static char m_comment[200];
static char m_freq[12];
static char m_ctcss[8];
static char m_mic_e;
static char m_dao[6];
static int m_ssid; // Default 12 for APRStt user.
void aprs_tt_sequence (int chan, char *msg)
{
int err;
#if DEBUG
text_color_set(DW_COLOR_DEBUG);
dw_printf ("\n\"%s\"\n", msg);
#endif
/*
* Discard empty message.
* In case # is there as optional start.
*/
if (msg[0] == '#') return;
/*
* The parse functions will fill these in.
*/
strlcpy (m_callsign, "", sizeof(m_callsign));
m_symtab_or_overlay = APRSTT_DEFAULT_SYMTAB;
m_symbol_code = APRSTT_DEFAULT_SYMBOL;
strlcpy (m_loc_text, "", sizeof(m_loc_text));
m_longitude = G_UNKNOWN;
m_latitude = G_UNKNOWN;
m_ambiguity = 0;
strlcpy (m_comment, "", sizeof(m_comment));
strlcpy (m_freq, "", sizeof(m_freq));
strlcpy (m_ctcss, "", sizeof(m_ctcss));
m_mic_e = ' ';
strlcpy (m_dao, "!T !", sizeof(m_dao)); /* start out unknown */
m_ssid = 12;
/*
* Parse the touch tone sequence.
*/
err = parse_fields (msg);
#if defined(DEBUG)
text_color_set(DW_COLOR_DEBUG);
dw_printf ("callsign=\"%s\", ssid=%d, symbol=\"%c%c\", freq=\"%s\", ctcss=\"%s\", comment=\"%s\", lat=%.4f, lon=%.4f, dao=\"%s\"\n",
m_callsign, m_ssid, m_symtab_or_overlay, m_symbol_code, m_freq, m_ctcss, m_comment, m_latitude, m_longitude, m_dao);
#endif
#if TT_MAIN
(void)err; // suppress variable set but not used warning.
check_result (); // for unit testing.
#else
/*
* If digested successfully. Add to our list of users and schedule transmissions.
*/
if (err == 0) {
err = tt_user_heard (m_callsign, m_ssid, m_symtab_or_overlay, m_symbol_code,
m_loc_text, m_latitude, m_longitude, m_ambiguity,
m_freq, m_ctcss, m_comment, m_mic_e, m_dao);
}
/*
* If a command / script was supplied, run it now.
* This can do additional processing and provide a custom audible response.
* This is done only for the success case.
* It might be useful to run it for error cases as well but we currently
* don't pass in the success / failure code to know the difference.
*/
char script_response[1000];
strlcpy (script_response, "", sizeof(script_response));
if (err == 0 && strlen(tt_config.ttcmd) > 0) {
dw_run_cmd (tt_config.ttcmd, 1, script_response, sizeof(script_response));
}
/*
* Send response to user by constructing packet with SPEECH or MORSE as destination.
* Source shouldn't matter because it doesn't get transmitted as AX.25 frame.
* Use high priority queue for consistent timing.
*
* Anything from script, above, will override other predefined responses.
*/
char audible_response[sizeof(script_response) + 16];
snprintf (audible_response, sizeof(audible_response),
"APRSTT>%s:%s",
tt_config.response[err].method,
(strlen(script_response) > 0) ? script_response : tt_config.response[err].mtext);
packet_t pp;
pp = ax25_from_text (audible_response, 0);
if (pp == NULL) {
text_color_set(DW_COLOR_ERROR);
dw_printf ("Internal error. Couldn't make frame from \"%s\"\n", audible_response);
return;
}
tq_append (chan, TQ_PRIO_0_HI, pp);
#endif /* ifndef TT_MAIN */
} /* end aprs_tt_sequence */
/*------------------------------------------------------------------
*
* Name: parse_fields
*
* Purpose: Separate the complete string of touch tone characters
* into fields, delimited by *, and process each.
*
* Inputs: msg - String of DTMF buttons.
*
* Returns: None
*
* Description: It should have one or more fields separated by *.
*
* callsign #
* entry1 * callsign #
* entry1 * entry * callsign #
*
* Note that this will be used recursively when macros
* are expanded.
*
* "To iterate is human, to recurse divine."
*
* Returns: 0 for success or one of the TT_ERROR_... codes.
*
*----------------------------------------------------------------*/
static int parse_fields (char *msg)
{
char stemp[MAX_MSG_LEN+1];
char *e;
char *save;
int err;
//text_color_set(DW_COLOR_DEBUG);
//dw_printf ("parse_fields (%s).\n", msg);
strlcpy (stemp, msg, sizeof(stemp));
e = strtok_r (stemp, "*#", &save);
while (e != NULL) {
//text_color_set(DW_COLOR_DEBUG);
//dw_printf ("parse_fields () field = %s\n", e);
switch (*e) {
case 'A':
switch (e[1]) {
case 'A': /* AA object-name */
err = parse_object_name (e);
if (err != 0) return (err);
break;
case 'B': /* AB symbol */
err = parse_symbol (e);
if (err != 0) return (err);
break;
case 'C': /* AC new-style-callsign */
err = parse_aprstt3_call (e);
if (err != 0) return (err);
break;
default: /* Traditional style call or suffix */
err = parse_callsign (e);
if (err != 0) return (err);
break;
}
break;
case 'B':
err = parse_location (e);
if (err != 0) return (err);
break;
case 'C':
err = parse_comment (e);
if (err != 0) return (err);
break;
case '0':
case '1':
case '2':
case '3':
case '4':
case '5':
case '6':
case '7':
case '8':
case '9':
err = expand_macro (e);
if (err != 0) return (err);
break;
case '\0':
/* Empty field. Just ignore it. */
/* This would happen if someone uses a leading *. */
break;
default:
text_color_set(DW_COLOR_ERROR);
dw_printf ("Field does not start with A, B, C, or digit: \"%s\"\n", msg);
return (TT_ERROR_D_MSG);
}
e = strtok_r (NULL, "*#", &save);
}
//text_color_set(DW_COLOR_DEBUG);
//dw_printf ("parse_fields () normal return\n");
return (0);
} /* end parse_fields */
/*------------------------------------------------------------------
*
* Name: expand_macro
*
* Purpose: Expand compact form "macro" to full format then process.
*
* Inputs: e - An "entry" extracted from a complete
* APRStt messsage.
* In this case, it should contain only digits.
*
* Returns: 0 for success or one of the TT_ERROR_... codes.
*
* Description: Separate out the fields, perform substitution,
* call parse_fields for processing.
*
*
* Future: Generalize this to allow any lower case letter for substitution?
*
*----------------------------------------------------------------*/
#define VALSTRSIZE 20
static int expand_macro (char *e)
{
//int len;
int ipat;
char xstr[VALSTRSIZE], ystr[VALSTRSIZE], zstr[VALSTRSIZE], bstr[VALSTRSIZE], dstr[VALSTRSIZE];
char stemp[MAX_MSG_LEN+1];
char *d;
text_color_set(DW_COLOR_DEBUG);
dw_printf ("Macro tone sequence: '%s'\n", e);
//len = strlen(e);
ipat = find_ttloc_match (e, xstr, ystr, zstr, bstr, dstr, VALSTRSIZE);
if (ipat >= 0) {
// Why did we print b & d here?
// Documentation says only x, y, z can be used with macros.
// Only those 3 are processed below.
//dw_printf ("Matched pattern %3d: '%s', x=%s, y=%s, z=%s, b=%s, d=%s\n", ipat, tt_config.ttloc_ptr[ipat].pattern, xstr, ystr, zstr, bstr, dstr);
dw_printf ("Matched pattern %3d: '%s', x=%s, y=%s, z=%s\n", ipat, tt_config.ttloc_ptr[ipat].pattern, xstr, ystr, zstr);
dw_printf ("Replace with: '%s'\n", tt_config.ttloc_ptr[ipat].macro.definition);
if (tt_config.ttloc_ptr[ipat].type != TTLOC_MACRO) {
/* Found match to a different type. Really shouldn't be here. */
/* Print internal error message... */
dw_printf ("expand_macro: type != TTLOC_MACRO\n");
return (TT_ERROR_INTERNAL);
}
/*
* We found a match for the length and any fixed digits.
* Substitute values in to the definition.
*/
strlcpy (stemp, "", sizeof(stemp));
for (d = tt_config.ttloc_ptr[ipat].macro.definition; *d != '\0'; d++) {
while (( *d == 'x' || *d == 'y' || *d == 'z') && *d == d[1]) {
/* Collapse adjacent matching substitution characters. */
d++;
}
switch (*d) {
case 'x':
strlcat (stemp, xstr, sizeof(stemp));
break;
case 'y':
strlcat (stemp, ystr, sizeof(stemp));
break;
case 'z':
strlcat (stemp, zstr, sizeof(stemp));
break;
default:
{
char c1[2];
c1[0] = *d;
c1[1] = '\0';
strlcat (stemp, c1, sizeof(stemp));
}
break;
}
}
/*
* Process as if we heard this over the air.
*/
dw_printf ("After substitution: '%s'\n", stemp);
return (parse_fields (stemp));
}
else {
/* Send reject sound. */
/* Does not match any macro definitions. */
text_color_set(DW_COLOR_ERROR);
dw_printf ("Tone sequence did not match any pattern\n");
return (TT_ERROR_MACRO_NOMATCH);
}
/* should be unreachable */
return (0);
}
/*------------------------------------------------------------------
*
* Name: parse_callsign
*
* Purpose: Extract traditional format callsign or object name from touch tone sequence.
*
* Inputs: e - An "entry" extracted from a complete
* APRStt messsage.
* In this case, it should start with "A".
*
* Outputs: m_callsign
*
* m_symtab_or_overlay - Set to 0-9 or A-Z if specified.
*
* m_symbol_code - Always set to 'A'.
* NO! This should be applied only if we
* have the default value at this point.
* The symbol might have been explicitly
* set already and we don't want to overwrite that.
*
* Returns: 0 for success or one of the TT_ERROR_... codes.
*
* Description: We recognize 3 different formats:
*
* Annn - 3 digits are a tactical callsign. No overlay.
*
* Annnvk - Abbreviation with 3 digits, numeric overlay, checksum.
* Annnvvk - Abbreviation with 3 digits, letter overlay, checksum.
*
* Att...ttvk - Full callsign in two key method, numeric overlay, checksum.
* Att...ttvvk - Full callsign in two key method, letter overlay, checksum.
*
*
*----------------------------------------------------------------*/
static int checksum_not_ok (char *str, int len, char found)
{
int i;
int sum;
char expected;
sum = 0;
for (i=0; i= 'A' && str[i] <= 'D') {
sum += str[i] - 'A' + 10;
}
else {
text_color_set(DW_COLOR_ERROR);
dw_printf ("aprs_tt: checksum: bad character \"%c\" in checksum calculation!\n", str[i]);
}
}
expected = '0' + (sum % 10);
if (expected != found) {
text_color_set(DW_COLOR_ERROR);
dw_printf ("Bad checksum for \"%.*s\". Expected %c but received %c.\n", len, str, expected, found);
return (TT_ERROR_BAD_CHECKSUM);
}
return (0);
}
static int parse_callsign (char *e)
{
int len;
char tttemp[40], stemp[30];
assert (*e == 'A');
len = strlen(e);
/*
* special case: 3 digit tactical call.
*/
if (len == 4 && isdigit(e[1]) && isdigit(e[2]) && isdigit(e[3])) {
strlcpy (m_callsign, e+1, sizeof(m_callsign));
return (0);
}
/*
* 3 digit abbreviation: We only do the parsing here.
* Another part of application will try to find corresponding full call.
*/
if ((len == 6 && isdigit(e[1]) && isdigit(e[2]) && isdigit(e[3]) && isdigit(e[4]) && isdigit(e[5])) ||
(len == 7 && isdigit(e[1]) && isdigit(e[2]) && isdigit(e[3]) && isdigit(e[4]) && isupper(e[5]) && isdigit(e[6]))) {
int cs_err = checksum_not_ok (e+1, len-2, e[len-1]);
if (cs_err != 0) {
return (cs_err);
}
strncpy (m_callsign, e+1, 3);
m_callsign[3] = '\0';
if (len == 7) {
tttemp[0] = e[len-3];
tttemp[1] = e[len-2];
tttemp[2] = '\0';
tt_two_key_to_text (tttemp, 0, stemp);
m_symbol_code = APRSTT_DEFAULT_SYMBOL;
m_symtab_or_overlay = stemp[0];
}
else {
m_symbol_code = APRSTT_DEFAULT_SYMBOL;
m_symtab_or_overlay = e[len-2];
}
return (0);
}
/*
* Callsign in two key format.
*/
if (len >= 7 && len <= 24) {
int cs_err = checksum_not_ok (e+1, len-2, e[len-1]);
if (cs_err != 0) {
return (cs_err);
}
if (isupper(e[len-2])) {
strncpy (tttemp, e+1, len-4);
tttemp[len-4] = '\0';
tt_two_key_to_text (tttemp, 0, m_callsign);
tttemp[0] = e[len-3];
tttemp[1] = e[len-2];
tttemp[2] = '\0';
tt_two_key_to_text (tttemp, 0, stemp);
m_symbol_code = APRSTT_DEFAULT_SYMBOL;
m_symtab_or_overlay = stemp[0];
}
else {
strncpy (tttemp, e+1, len-3);
tttemp[len-3] = '\0';
tt_two_key_to_text (tttemp, 0, m_callsign);
m_symbol_code = APRSTT_DEFAULT_SYMBOL;
m_symtab_or_overlay = e[len-2];
}
return (0);
}
text_color_set(DW_COLOR_ERROR);
dw_printf ("Touch tone callsign not valid: \"%s\"\n", e);
return (TT_ERROR_INVALID_CALL);
}
/*------------------------------------------------------------------
*
* Name: parse_object_name
*
* Purpose: Extract object name from touch tone sequence.
*
* Inputs: e - An "entry" extracted from a complete
* APRStt messsage.
* In this case, it should start with "AA".
*
* Outputs: m_callsign
*
* m_ssid - Cleared to remove the default of 12.
*
* Returns: 0 for success or one of the TT_ERROR_... codes.
*
* Description: Data format
*
* AAtt...tt - Symbol name, two key method, up to 9 characters.
*
*----------------------------------------------------------------*/
static int parse_object_name (char *e)
{
int len;
//int c_length;
//char tttemp[40];
//char stemp[30];
assert (e[0] == 'A');
assert (e[1] == 'A');
len = strlen(e);
/*
* Object name in two key format.
*/
if (len >= 2 + 1 && len <= 30) {
if (tt_two_key_to_text (e+2, 0, m_callsign) == 0) {
m_callsign[9] = '\0'; /* truncate to 9 */
m_ssid = 0; /* No ssid for object name */
return (0);
}
}
text_color_set(DW_COLOR_ERROR);
dw_printf ("Touch tone object name not valid: \"%s\"\n", e);
return (TT_ERROR_INVALID_OBJNAME);
} /* end parse_oject_name */
/*------------------------------------------------------------------
*
* Name: parse_symbol
*
* Purpose: Extract symbol from touch tone sequence.
*
* Inputs: e - An "entry" extracted from a complete
* APRStt messsage.
* In this case, it should start with "AB".
*
* Outputs: m_symtab_or_overlay
*
* m_symbol_code
*
* Returns: 0 for success or one of the TT_ERROR_... codes.
*
* Description: Data format
*
* AB1nn - Symbol from primary symbol table.
* Two digits nn are the same as in the GPSCnn
* generic address used as a destination.
*
* AB2nn - Symbol from alternate symbol table.
* Two digits nn are the same as in the GPSEnn
* generic address used as a destination.
*
* AB0nnvv - Symbol from alternate symbol table.
* Two digits nn are the same as in the GPSEnn
* generic address used as a destination.
* vv is an overlay digit or letter in two key method.
*
*----------------------------------------------------------------*/
static int parse_symbol (char *e)
{
int len;
char nstr[3];
int nn;
char stemp[10];
assert (e[0] == 'A');
assert (e[1] == 'B');
len = strlen(e);
if (len >= 4 && len <= 10) {
nstr[0] = e[3];
nstr[1] = e[4];
nstr[2] = '\0';
nn = atoi (nstr);
if (nn < 1) {
nn = 1;
}
else if (nn > 94) {
nn = 94;
}
switch (e[2]) {
case '1':
m_symtab_or_overlay = '/';
m_symbol_code = 32 + nn;
return (0);
break;
case '2':
m_symtab_or_overlay = '\\';
m_symbol_code = 32 + nn;
return (0);
break;
case '0':
if (len >= 6) {
if (tt_two_key_to_text (e+5, 0, stemp) == 0) {
m_symbol_code = 32 + nn;
m_symtab_or_overlay = stemp[0];
return (0);
}
}
break;
}
}
text_color_set(DW_COLOR_ERROR);
dw_printf ("Touch tone symbol not valid: \"%s\"\n", e);
return (TT_ERROR_INVALID_SYMBOL);
} /* end parse_oject_name */
/*------------------------------------------------------------------
*
* Name: parse_aprstt3_call
*
* Purpose: Extract QIKcom-2 / APRStt 3 ten digit call or five digit suffix.
*
* Inputs: e - An "entry" extracted from a complete
* APRStt messsage.
* In this case, it should start with "AC".
*
* Outputs: m_callsign
*
* Returns: 0 for success or one of the TT_ERROR_... codes.
*
* Description: We recognize 3 different formats:
*
* ACxxxxxxxxxx - 10 digit full callsign.
*
* ACxxxxx - 5 digit suffix. If we can find a corresponding full
* callsign, that will be substituted.
* Error condition is returned if we can't find one.
*
*----------------------------------------------------------------*/
static int parse_aprstt3_call (char *e)
{
assert (e[0] == 'A');
assert (e[1] == 'C');
if (strlen(e) == 2+10) {
char call[12];
if (tt_call10_to_text(e+2,1,call) == 0) {
strlcpy(m_callsign, call, sizeof(m_callsign));
}
else {
return (TT_ERROR_INVALID_CALL); /* Could not convert to text */
}
}
else if (strlen(e) == 2+5) {
char suffix[8];
if (tt_call5_suffix_to_text(e+2,1,suffix) == 0) {
#if TT_MAIN
/* For unit test, use suffix rather than trying lookup. */
strlcpy (m_callsign, suffix, sizeof(m_callsign));
#else
char call[12];
/* In normal operation, try to find full callsign for the suffix received. */
if (tt_3char_suffix_search (suffix, call) >= 0) {
text_color_set(DW_COLOR_INFO);
dw_printf ("Suffix \"%s\" was converted to full callsign \"%s\"\n", suffix, call);
strlcpy(m_callsign, call, sizeof(m_callsign));
}
else {
text_color_set(DW_COLOR_ERROR);
dw_printf ("Couldn't find full callsign for suffix \"%s\"\n", suffix);
return (TT_ERROR_SUFFIX_NO_CALL); /* Don't know this user. */
}
#endif
}
else {
return (TT_ERROR_INVALID_CALL); /* Could not convert to text */
}
}
else {
return (TT_ERROR_INVALID_CALL); /* Invalid length, not 2+ (10 ir 5) */
}
return (0);
} /* end parse_aprstt3_call */
/*------------------------------------------------------------------
*
* Name: parse_location
*
* Purpose: Extract location from touch tone sequence.
*
* Inputs: e - An "entry" extracted from a complete
* APRStt messsage.
* In this case, it should start with "B".
*
* Outputs: m_latitude
* m_longitude
*
* m_dao It should previously be "!T !" to mean unknown or none.
* We generally take the first two tones of the field.
* For example, "!TB5!" for the standard bearing & range.
* The point type is an exception where we use "!Tn !" for
* one of ten positions or "!Tnn" for one of a hundred.
* If this ever changes, be sure to update corresponding
* section in process_comment() in decode_aprs.c
*
* m_ambiguity
*
* Returns: 0 for success or one of the TT_ERROR_... codes.
*
* Description: There are many different formats recognizable
* by total number of digits and sometimes the first digit.
*
* We handle most of them in a general way, processing
* them in 5 groups:
*
* * points
* * vector
* * grid
* * utm
* * usng / mgrs
*
* Position ambiguity is also handled here.
* Latitude, Longitude, and DAO should not be touched in this case.
* We only record a position ambiguity value.
*
*----------------------------------------------------------------*/
/* Average radius of earth in meters. */
#define R 6371000.
static int parse_location (char *e)
{
int ipat;
char xstr[VALSTRSIZE], ystr[VALSTRSIZE], zstr[VALSTRSIZE], bstr[VALSTRSIZE], dstr[VALSTRSIZE];
double x, y, dist, bearing;
double lat0, lon0;
double lat9, lon9;
long lerr;
double easting, northing;
char mh[20];
char stemp[32];
assert (*e == 'B');
ipat = find_ttloc_match (e, xstr, ystr, zstr, bstr, dstr, VALSTRSIZE);
if (ipat >= 0) {
//dw_printf ("ipat=%d, x=%s, y=%s, b=%s, d=%s\n", ipat, xstr, ystr, bstr, dstr);
switch (tt_config.ttloc_ptr[ipat].type) {
case TTLOC_POINT:
m_latitude = tt_config.ttloc_ptr[ipat].point.lat;
m_longitude = tt_config.ttloc_ptr[ipat].point.lon;
/* Is it one of ten or a hundred positions? */
/* It's not hardwired to always be B0n or B9nn. */
/* This is a pretty good approximation. */
m_dao[2] = e[0];
m_dao[3] = e[1];
if (strlen(e) == 3) { /* probably B0n --> !Tn ! */
m_dao[2] = e[2];
m_dao[3] = ' ';
}
if (strlen(e) == 4) { /* probably B9nn --> !Tnn! */
m_dao[2] = e[2];
m_dao[3] = e[3];
}
break;
case TTLOC_VECTOR:
if (strlen(bstr) != 3) {
text_color_set(DW_COLOR_ERROR);
dw_printf ("Bearing \"%s\" should be 3 digits.\n", bstr);
// return error code?
}
if (strlen(dstr) < 1) {
text_color_set(DW_COLOR_ERROR);
dw_printf ("Distance \"%s\" should 1 or more digits.\n", dstr);
// return error code?
}
lat0 = D2R(tt_config.ttloc_ptr[ipat].vector.lat);
lon0 = D2R(tt_config.ttloc_ptr[ipat].vector.lon);
dist = atof(dstr) * tt_config.ttloc_ptr[ipat].vector.scale;
bearing = D2R(atof(bstr));
/* Equations and caluculators found here: */
/* http://movable-type.co.uk/scripts/latlong.html */
/* This should probably be a function in latlong.c in case we have another use for it someday. */
m_latitude = R2D(asin(sin(lat0) * cos(dist/R) + cos(lat0) * sin(dist/R) * cos(bearing)));
m_longitude = R2D(lon0 + atan2(sin(bearing) * sin(dist/R) * cos(lat0),
cos(dist/R) - sin(lat0) * sin(D2R(m_latitude))));
m_dao[2] = e[0];
m_dao[3] = e[1];
break;
case TTLOC_GRID:
if (strlen(xstr) == 0) {
text_color_set(DW_COLOR_ERROR);
dw_printf ("Missing X coordinate.\n");
strlcpy (xstr, "0", sizeof(xstr));
}
if (strlen(ystr) == 0) {
text_color_set(DW_COLOR_ERROR);
dw_printf ("Missing Y coordinate.\n");
strlcpy (ystr, "0", sizeof(ystr));
}
lat0 = tt_config.ttloc_ptr[ipat].grid.lat0;
lat9 = tt_config.ttloc_ptr[ipat].grid.lat9;
y = atof(ystr);
m_latitude = lat0 + y * (lat9-lat0) / (pow(10., strlen(ystr)) - 1.);
lon0 = tt_config.ttloc_ptr[ipat].grid.lon0;
lon9 = tt_config.ttloc_ptr[ipat].grid.lon9;
x = atof(xstr);
m_longitude = lon0 + x * (lon9-lon0) / (pow(10., strlen(xstr)) - 1.);
m_dao[2] = e[0];
m_dao[3] = e[1];
break;
case TTLOC_UTM:
if (strlen(xstr) == 0) {
text_color_set(DW_COLOR_ERROR);
dw_printf ("Missing X coordinate.\n");
/* Avoid divide by zero later. Put in middle of range. */
strlcpy (xstr, "5", sizeof(xstr));
}
if (strlen(ystr) == 0) {
text_color_set(DW_COLOR_ERROR);
dw_printf ("Missing Y coordinate.\n");
/* Avoid divide by zero later. Put in middle of range. */
strlcpy (ystr, "5", sizeof(ystr));
}
x = atof(xstr);
easting = x * tt_config.ttloc_ptr[ipat].utm.scale + tt_config.ttloc_ptr[ipat].utm.x_offset;
y = atof(ystr);
northing = y * tt_config.ttloc_ptr[ipat].utm.scale + tt_config.ttloc_ptr[ipat].utm.y_offset;
if (isalpha(tt_config.ttloc_ptr[ipat].utm.latband)) {
snprintf (m_loc_text, sizeof(m_loc_text), "%d%c %.0f %.0f", (int)(tt_config.ttloc_ptr[ipat].utm.lzone), tt_config.ttloc_ptr[ipat].utm.latband, easting, northing);
}
else if (tt_config.ttloc_ptr[ipat].utm.latband == '-') {
snprintf (m_loc_text, sizeof(m_loc_text), "%d %.0f %.0f", (int)(- tt_config.ttloc_ptr[ipat].utm.lzone), easting, northing);
}
else {
snprintf (m_loc_text, sizeof(m_loc_text), "%d %.0f %.0f", (int)(tt_config.ttloc_ptr[ipat].utm.lzone), easting, northing);
}
lerr = Convert_UTM_To_Geodetic(tt_config.ttloc_ptr[ipat].utm.lzone,
tt_config.ttloc_ptr[ipat].utm.hemi, easting, northing, &lat0, &lon0);
if (lerr == 0) {
m_latitude = R2D(lat0);
m_longitude = R2D(lon0);
//dw_printf ("DEBUG: from UTM, latitude = %.6f, longitude = %.6f\n", m_latitude, m_longitude);
}
else {
char message[300];
text_color_set(DW_COLOR_ERROR);
utm_error_string (lerr, message);
dw_printf ("Conversion from UTM failed:\n%s\n\n", message);
}
m_dao[2] = e[0];
m_dao[3] = e[1];
break;
case TTLOC_MGRS:
case TTLOC_USNG:
if (strlen(xstr) == 0) {
text_color_set(DW_COLOR_ERROR);
dw_printf ("MGRS/USNG: Missing X (easting) coordinate.\n");
/* Should not be possible to get here. Fake it and carry on. */
strlcpy (xstr, "5", sizeof(xstr));
}
if (strlen(ystr) == 0) {
text_color_set(DW_COLOR_ERROR);
dw_printf ("MGRS/USNG: Missing Y (northing) coordinate.\n");
/* Should not be possible to get here. Fake it and carry on. */
strlcpy (ystr, "5", sizeof(ystr));
}
char loc[40];
strlcpy (loc, tt_config.ttloc_ptr[ipat].mgrs.zone, sizeof(loc));
strlcat (loc, xstr, sizeof(loc));
strlcat (loc, ystr, sizeof(loc));
//text_color_set(DW_COLOR_DEBUG);
//dw_printf ("MGRS/USNG location debug: %s\n", loc);
strlcpy (m_loc_text, loc, sizeof(m_loc_text));
if (tt_config.ttloc_ptr[ipat].type == TTLOC_MGRS)
lerr = Convert_MGRS_To_Geodetic(loc, &lat0, &lon0);
else
lerr = Convert_USNG_To_Geodetic(loc, &lat0, &lon0);
if (lerr == 0) {
m_latitude = R2D(lat0);
m_longitude = R2D(lon0);
//dw_printf ("DEBUG: from MGRS/USNG, latitude = %.6f, longitude = %.6f\n", m_latitude, m_longitude);
}
else {
char message[300];
text_color_set(DW_COLOR_ERROR);
mgrs_error_string (lerr, message);
dw_printf ("Conversion from MGRS/USNG failed:\n%s\n\n", message);
}
m_dao[2] = e[0];
m_dao[3] = e[1];
break;
case TTLOC_MHEAD:
/* Combine prefix from configuration and digits from user. */
strlcpy (stemp, tt_config.ttloc_ptr[ipat].mhead.prefix, sizeof(stemp));
strlcat (stemp, xstr, sizeof(stemp));
if (strlen(stemp) != 4 && strlen(stemp) != 6 && strlen(stemp) != 10 && strlen(stemp) != 12) {
text_color_set(DW_COLOR_ERROR);
dw_printf ("Expected total of 4, 6, 10, or 12 digits for the Maidenhead Locator \"%s\" + \"%s\"\n",
tt_config.ttloc_ptr[ipat].mhead.prefix, xstr);
return (TT_ERROR_INVALID_MHEAD);
}
//text_color_set(DW_COLOR_DEBUG);
//dw_printf ("Case MHEAD: Convert to text \"%s\".\n", stemp);
if (tt_mhead_to_text (stemp, 0, mh, sizeof(mh)) == 0) {
//text_color_set(DW_COLOR_DEBUG);
//dw_printf ("Case MHEAD: Resulting text \"%s\".\n", mh);
strlcpy (m_loc_text, mh, sizeof(m_loc_text));
ll_from_grid_square (mh, &m_latitude, &m_longitude);
}
m_dao[2] = e[0];
m_dao[3] = e[1];
break;
case TTLOC_SATSQ:
if (strlen(xstr) != 4) {
text_color_set(DW_COLOR_ERROR);
dw_printf ("Expected 4 digits for the Satellite Square.\n");
return (TT_ERROR_INVALID_SATSQ);
}
/* Convert 4 digits to usual AA99 form, then to location. */
if (tt_satsq_to_text (xstr, 0, mh) == 0) {
strlcpy (m_loc_text, mh, sizeof(m_loc_text));
ll_from_grid_square (mh, &m_latitude, &m_longitude);
}
m_dao[2] = e[0];
m_dao[3] = e[1];
break;
case TTLOC_AMBIG:
if (strlen(xstr) != 1) {
text_color_set(DW_COLOR_ERROR);
dw_printf ("Expected 1 digits for the position ambiguity.\n");
return (TT_ERROR_INVALID_LOC);
}
m_ambiguity = atoi(xstr);
break;
default:
assert (0);
}
return (0);
}
/* Does not match any location specification. */
text_color_set(DW_COLOR_ERROR);
dw_printf ("Received location \"%s\" does not match any definitions.\n", e);
/* Send reject sound. */
return (TT_ERROR_INVALID_LOC);
} /* end parse_location */
/*------------------------------------------------------------------
*
* Name: find_ttloc_match
*
* Purpose: Try to match the received position report to a pattern
* defined in the configuration file.
*
* Inputs: e - An "entry" extracted from a complete
* APRStt messsage.
* In this case, it should start with "B".
*
* valstrsize - size of the outputs so we can check for buffer overflow.
*
* Outputs: xstr - All digits matching x positions in configuration.
* ystr - y
* zstr - z
* bstr - b
* dstr - d
*
* Returns: >= 0 for index into table if found.
* -1 if not found.
*
* Description:
*
*----------------------------------------------------------------*/
static int find_ttloc_match (char *e, char *xstr, char *ystr, char *zstr, char *bstr, char *dstr, size_t valstrsize)
{
int ipat; /* Index into patterns from configuration file */
int len; /* Length of pattern we are trying to match. */
int match;
char mc;
int k;
// debug dw_printf ("find_ttloc_match: e=%s\n", e);
for (ipat=0; ipat%s:t%s", src, dest, msg);
pp = ax25_from_text (raw_tt_msg, 1);
/*
* Process like a normal received frame.
* NOTE: This goes directly to application rather than
* thru the multi modem duplicate processing.
*
* Should we use a different type so it can be easily
* distinguished later?
*
* We try to capture an overall audio level here.
* Mark and space do not apply in this case.
* This currently doesn't get displayed but we might want it someday.
*/
if (pp != NULL) {
alevel = demod_get_audio_level (chan, 0);
alevel.mark = -2;
alevel.space = -2;
dlq_rec_frame (chan, -1, 0, pp, alevel, RETRY_NONE, "tt");
}
else {
text_color_set(DW_COLOR_ERROR);
dw_printf ("Could not convert \"%s\" into APRS packet.\n", raw_tt_msg);
}
#endif
}
#endif
/*------------------------------------------------------------------
*
* Name: dw_run_cmd
*
* Purpose: Run a command and capture the output.
*
* Inputs: cmd - The command.
*
* oneline - 0 = Keep original line separators. Caller
* must deal with operating system differences.
* 1 = Change CR, LF, TAB to space so result
* is one line of text.
* 2 = Also remove any trailing whitespace.
*
* resultsiz - Amount of space available for result.
*
* Outputs: result - Output captured from running command.
*
* Returns: -1 for any sort of error.
* >0 for number of characters returned (= strlen(result))
*
* Description: This is currently used for running a user-specified
* script to generate a custom speech response.
*
* Future: There are potential other uses so it should probably
* be relocated to a file of other misc. utilities.
*
*----------------------------------------------------------------*/
int dw_run_cmd (char *cmd, int oneline, char *result, size_t resultsiz)
{
FILE *fp;
strlcpy (result, "", resultsiz);
fp = popen (cmd, "r");
if (fp != NULL) {
int remaining = (int)resultsiz;
char *pr = result;
int err;
while (remaining > 2 && fgets(pr, remaining, fp) != NULL) {
pr = result + strlen(result);
remaining = (int)resultsiz - strlen(result);
}
if ((err = pclose(fp)) != 0) {
text_color_set(DW_COLOR_ERROR);
dw_printf ("ERROR: Unable to run \"%s\"\n", cmd);
// On Windows, non-existent file produces "Operation not permitted"
// Maybe we should put in a test for whether file exists.
dw_printf ("%s\n", strerror(err));
return (-1);
}
// take out any newline characters.
if (oneline) {
for (pr = result; *pr != '\0'; pr++) {
if (*pr == '\r' || *pr == '\n' || *pr == '\t') {
*pr = ' ';
}
}
if (oneline > 1) {
pr = result + strlen(result) - 1;
while (pr >= result && *pr == ' ') {
*pr = '\0';
pr--;
}
}
}
//text_color_set(DW_COLOR_DEBUG);
//dw_printf ("%s returns \"%s\"\n", cmd, result);
return (strlen(result));
}
else {
// explain_popen() would be nice but doesn't seem to be commonly available.
// We get here only if fork or pipe fails.
// The command not existing must be caught above.
text_color_set(DW_COLOR_ERROR);
dw_printf ("ERROR: Unable to run \"%s\"\n", cmd);
dw_printf ("%s\n", strerror(errno));
return (-1);
}
} /* end dw_run_cmd */
/*------------------------------------------------------------------
*
* Name: main
*
* Purpose: Unit test for this file.
*
* Description: Run unit test like this:
*
* rm a.exe ; gcc tt_text.c -DTT_MAIN -Igeotranz aprs_tt.c latlong.o textcolor.o geotranz.a misc.a ; ./a.exe
* or
* make ttest
*
*----------------------------------------------------------------*/
#if TT_MAIN
/*
* Regression test for the parsing.
* It does not maintain any history so abbreviation will not invoke previous full call.
*/
/* Some examples are derived from http://www.aprs.org/aprstt/aprstt-coding24.txt */
static const struct {
char *toneseq; /* Tone sequence in. */
char *callsign; /* Expected results... */
char *ssid;
char *symbol;
char *freq;
char *comment;
char *lat;
char *lon;
char *dao;
} testcases[] = {
/* Callsigns & abbreviations, traditional */
{ "A9A2B42A7A7C71#", "WB4APR", "12", "7A", "", "", "-999999.0000", "-999999.0000", "!T !" }, /* WB4APR/7 */
{ "A27773#", "277", "12", "7A", "", "", "-999999.0000", "-999999.0000", "!T !" }, /* abbreviated form */
/* Intentionally wrong - Has 6 for checksum when it should be 3. */
{ "A27776#", "", "12", "\\A", "", "", "-999999.0000", "-999999.0000", "!T !" }, /* Expect error message. */
/* Example in spec is wrong. checksum should be 5 in this case. */
{ "A2A7A7C71#", "", "12", "\\A", "", "", "-999999.0000", "-999999.0000", "!T !" }, /* Spelled suffix, overlay, checksum */
{ "A2A7A7C75#", "APR", "12", "7A", "", "", "-999999.0000", "-999999.0000", "!T !" }, /* Spelled suffix, overlay, checksum */
{ "A27773#", "277", "12", "7A", "", "", "-999999.0000", "-999999.0000", "!T !" }, /* Suffix digits, overlay, checksum */
{ "A9A2B26C7D9D71#", "WB2OSZ", "12", "7A", "", "", "-999999.0000", "-999999.0000", "!T !" }, /* WB2OSZ/7 numeric overlay */
{ "A67979#", "679", "12", "7A", "", "", "-999999.0000", "-999999.0000", "!T !" }, /* abbreviated form */
{ "A9A2B26C7D9D5A9#", "WB2OSZ", "12", "JA", "", "", "-999999.0000", "-999999.0000", "!T !" }, /* WB2OSZ/J letter overlay */
{ "A6795A7#", "679", "12", "JA", "", "", "-999999.0000", "-999999.0000", "!T !" }, /* abbreviated form */
{ "A277#", "277", "12", "\\A", "", "", "-999999.0000", "-999999.0000", "!T !" }, /* Tactical call "277" no overlay and no checksum */
/* QIKcom-2 style 10 digit call & 5 digit suffix */
{ "AC9242771558#", "WB4APR", "12", "\\A", "", "", "-999999.0000", "-999999.0000", "!T !" },
{ "AC27722#", "APR", "12", "\\A", "", "", "-999999.0000", "-999999.0000", "!T !" },
/* Locations */
{ "B01*A67979#", "679", "12", "7A", "", "", "12.2500", "56.2500", "!T1 !" },
{ "B988*A67979#", "679", "12", "7A", "", "", "12.5000", "56.5000", "!T88!" },
{ "B51000125*A67979#", "679", "12", "7A", "", "", "52.7907", "0.8309", "!TB5!" }, /* expect about 52.79 +0.83 */
{ "B5206070*A67979#", "679", "12", "7A", "", "", "37.9137", "-81.1366", "!TB5!" }, /* Try to get from Hilltop Tower to Archery & Target Range. */
/* Latitude comes out ok, 37.9137 -> 55.82 min. */
/* Longitude -81.1254 -> 8.20 min */
{ "B21234*A67979#", "679", "12", "7A", "", "", "12.3400", "56.1200", "!TB2!" },
{ "B533686*A67979#", "679", "12", "7A", "", "", "37.9222", "81.1143", "!TB5!" },
// TODO: should test other coordinate systems.
/* Comments */
{ "C1", "", "12", "\\A", "", "", "-999999.0000", "-999999.0000", "!T !" },
{ "C2", "", "12", "\\A", "", "", "-999999.0000", "-999999.0000", "!T !" },
{ "C146520", "", "12", "\\A", "146.520MHz", "", "-999999.0000", "-999999.0000", "!T !" },
{ "C7788444222550227776669660333666990122223333",
"", "12", "\\A", "", "QUICK BROWN FOX 123", "-999999.0000", "-999999.0000", "!T !" },
/* Macros */
{ "88345", "BIKE 345", "0", "/b", "", "", "12.5000", "56.5000", "!T88!" },
/* 10 digit representation for callsign & satellite grid. WB4APR near 39.5, -77 */
{ "AC9242771558*BA1819", "WB4APR", "12", "\\A", "", "", "39.5000", "-77.0000", "!TBA!" },
{ "18199242771558", "WB4APR", "12", "\\A", "", "", "39.5000", "-77.0000", "!TBA!" },
};
static int test_num;
static int error_count;
static void check_result (void)
{
char stemp[32];
text_color_set(DW_COLOR_DEBUG);
dw_printf ("callsign=\"%s\", ssid=%d, symbol=\"%c%c\", freq=\"%s\", comment=\"%s\", lat=%.4f, lon=%.4f, dao=\"%s\"\n",
m_callsign, m_ssid, m_symtab_or_overlay, m_symbol_code, m_freq, m_comment, m_latitude, m_longitude, m_dao);
if (strcmp(m_callsign, testcases[test_num].callsign) != 0) {
text_color_set(DW_COLOR_ERROR);
dw_printf ("ERROR: Expected \"%s\" for callsign.\n", testcases[test_num].callsign);
error_count++;
}
snprintf (stemp, sizeof(stemp), "%d", m_ssid);
if (strcmp(stemp, testcases[test_num].ssid) != 0) {
text_color_set(DW_COLOR_ERROR);
dw_printf ("ERROR: Expected \"%s\" for SSID.\n", testcases[test_num].ssid);
error_count++;
}
stemp[0] = m_symtab_or_overlay;
stemp[1] = m_symbol_code;
stemp[2] = '\0';
if (strcmp(stemp, testcases[test_num].symbol) != 0) {
text_color_set(DW_COLOR_ERROR);
dw_printf ("ERROR: Expected \"%s\" for Symbol.\n", testcases[test_num].symbol);
error_count++;
}
if (strcmp(m_freq, testcases[test_num].freq) != 0) {
text_color_set(DW_COLOR_ERROR);
dw_printf ("ERROR: Expected \"%s\" for Freq.\n", testcases[test_num].freq);
error_count++;
}
if (strcmp(m_comment, testcases[test_num].comment) != 0) {
text_color_set(DW_COLOR_ERROR);
dw_printf ("ERROR: Expected \"%s\" for Comment.\n", testcases[test_num].comment);
error_count++;
}
snprintf (stemp, sizeof(stemp), "%.4f", m_latitude);
if (strcmp(stemp, testcases[test_num].lat) != 0) {
text_color_set(DW_COLOR_ERROR);
dw_printf ("ERROR: Expected \"%s\" for Latitude.\n", testcases[test_num].lat);
error_count++;
}
snprintf (stemp, sizeof(stemp), "%.4f", m_longitude);
if (strcmp(stemp, testcases[test_num].lon) != 0) {
text_color_set(DW_COLOR_ERROR);
dw_printf ("ERROR: Expected \"%s\" for Longitude.\n", testcases[test_num].lon);
error_count++;
}
if (strcmp(m_dao, testcases[test_num].dao) != 0) {
text_color_set(DW_COLOR_ERROR);
dw_printf ("ERROR: Expected \"%s\" for DAO.\n", testcases[test_num].dao);
error_count++;
}
}
int main (int argc, char *argv[])
{
aprs_tt_init (NULL);
error_count = 0;
for (test_num = 0; test_num < sizeof(testcases) / sizeof(testcases[0]); test_num++) {
text_color_set(DW_COLOR_INFO);
dw_printf ("\nTest case %d: %s\n", test_num, testcases[test_num].toneseq);
aprs_tt_sequence (0, testcases[test_num].toneseq);
}
if (error_count != 0) {
text_color_set(DW_COLOR_ERROR);
dw_printf ("\n\nTEST FAILED, Total of %d errors.\n", error_count);
return (EXIT_FAILURE);
}
text_color_set(DW_COLOR_REC);
dw_printf ("\n\nAll tests passed.\n");
return (EXIT_SUCCESS);
} /* end main */
#endif
/* end aprs_tt.c */