// // 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, 0, 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 */