// // This file is part of Dire Wolf, an amateur radio packet TNC. // // Copyright (C) 2023 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 . // /*------------------------------------------------------------------ * * File: deviceid.c * * Purpose: Determine the device identifier from the destination field, * or from prefix/suffix for MIC-E format. * * Description: Orginally this used the tocalls.txt file and was part of decode_aprs.c. * For release 1.8, we use tocalls.yaml and this is split into a separate file. * *------------------------------------------------------------------*/ //#define TEST 1 // Standalone test. $ gcc -DTEST deviceid.c && ./a.out #if TEST #define HAVE_STRLCPY 1 // prevent defining in direwolf.h #define HAVE_STRLCAT 1 #endif #include "direwolf.h" #include #include #include #include #include "deviceid.h" #include "textcolor.h" static void unquote (int line, char *pin, char *pout); static int tocall_cmp (const void *px, const void *py); static int mice_cmp (const void *px, const void *py); /*------------------------------------------------------------------ * * Function: main * * Purpose: A little self-test used during development. * * Description: Read the yaml file. Decipher a few typical values. * *------------------------------------------------------------------*/ #if TEST // So we don't need to link with any other files. #define dw_printf printf void text_color_set(dw_color_t) { return; } void strlcpy(char *dst, char *src, size_t dlen) { strcpy (dst, src); } void strlcat(char *dst, char *src, size_t dlen) { strcat (dst, src); } int main (int argc, char *argv[]) { char device[80]; char comment_out[80]; deviceid_init (); dw_printf ("\n"); dw_printf ("Testing ...\n"); // MIC-E Legacy (really Kenwood). deviceid_decode_mice (">Comment", comment_out, sizeof(comment_out), device, sizeof(device)); dw_printf ("%s %s\n", comment_out, device); assert (strcmp(comment_out, "Comment") == 0); assert (strcmp(device, "Kenwood TH-D7A") == 0); deviceid_decode_mice (">Comment^", comment_out, sizeof(comment_out), device, sizeof(device)); dw_printf ("%s %s\n", comment_out, device); assert (strcmp(comment_out, "Comment") == 0); assert (strcmp(device, "Kenwood TH-D74") == 0); deviceid_decode_mice ("]Comment", comment_out, sizeof(comment_out), device, sizeof(device)); dw_printf ("%s %s\n", comment_out, device); assert (strcmp(comment_out, "Comment") == 0); assert (strcmp(device, "Kenwood TM-D700") == 0); deviceid_decode_mice ("]Comment=", comment_out, sizeof(comment_out), device, sizeof(device)); dw_printf ("%s %s\n", comment_out, device); assert (strcmp(comment_out, "Comment") == 0); assert (strcmp(device, "Kenwood TM-D710") == 0); deviceid_decode_mice ("]\"4V}=", comment_out, sizeof(comment_out), device, sizeof(device)); dw_printf ("%s %s\n", comment_out, device); assert (strcmp(comment_out, "\"4V}") == 0); assert (strcmp(device, "Kenwood TM-D710") == 0); // Modern MIC-E. deviceid_decode_mice ("`Comment_\"", comment_out, sizeof(comment_out), device, sizeof(device)); dw_printf ("%s %s\n", comment_out, device); assert (strcmp(comment_out, "Comment") == 0); assert (strcmp(device, "Yaesu FTM-350") == 0); deviceid_decode_mice ("`Comment_ ", comment_out, sizeof(comment_out), device, sizeof(device)); dw_printf ("%s %s\n", comment_out, device); assert (strcmp(comment_out, "Comment") == 0); assert (strcmp(device, "Yaesu VX-8") == 0); deviceid_decode_mice ("'Comment|3", comment_out, sizeof(comment_out), device, sizeof(device)); dw_printf ("%s %s\n", comment_out, device); assert (strcmp(comment_out, "Comment") == 0); assert (strcmp(device, "Byonics TinyTrak3") == 0); deviceid_decode_mice ("Comment", comment_out, sizeof(comment_out), device, sizeof(device)); dw_printf ("%s %s\n", comment_out, device); assert (strcmp(comment_out, "Comment") == 0); assert (strcmp(device, "UNKNOWN vendor/model") == 0); deviceid_decode_mice ("", comment_out, sizeof(comment_out), device, sizeof(device)); dw_printf ("%s %s\n", comment_out, device); assert (strcmp(comment_out, "") == 0); assert (strcmp(device, "UNKNOWN vendor/model") == 0); // Tocall deviceid_decode_dest ("APDW18", device, sizeof(device)); dw_printf ("%s\n", device); assert (strcmp(device, "WB2OSZ DireWolf") == 0); deviceid_decode_dest ("APD123", device, sizeof(device)); dw_printf ("%s\n", device); assert (strcmp(device, "Open Source aprsd") == 0); // null for Vendor. deviceid_decode_dest ("APAX", device, sizeof(device)); dw_printf ("%s\n", device); assert (strcmp(device, "AFilterX") == 0); deviceid_decode_dest ("APA123", device, sizeof(device)); dw_printf ("%s\n", device); assert (strcmp(device, "UNKNOWN vendor/model") == 0); dw_printf ("\n"); dw_printf ("Success!\n"); exit (EXIT_SUCCESS); } #endif // TEST // Structures to hold mapping from encoded form to vendor and model. // The .yaml file has two separate sections for MIC-E but they can // both be handled as a single more general case. struct mice { char prefix[4]; // The legacy form has 1 prefix character. // The newer form has none. (more accurately ` or ') char suffix[4]; // The legacy form has 0 or 1. // The newer form has 2. char *vendor; char *model; }; struct tocalls { char tocall[8]; // Up to 6 characters. Some may have wildcards at the end. // Most often they are trailing "??" or "?" or "???" in one case. // Sometimes there is trailing "nnn". Does that imply digits only? // Sometimes we see a trailing "*". Is "*" different than "?"? // There are a couple bizzare cases like APnnnD which can // create an ambigious situation. APMPAD, APRFGD, APY0[125]D. // Screw them if they can't follow the rules. I'm not putting in a special case. char *vendor; char *model; }; static struct mice *pmice = NULL; // Pointer to array. static int mice_count = 0; // Number of allocated elements. static int mice_index = -1; // Current index for filling in. static struct tocalls *ptocalls = NULL; // Pointer to array. static int tocalls_count = 0; // Number of allocated elements. static int tocalls_index = -1; // Current index for filling in. /*------------------------------------------------------------------ * * Function: deviceid_init * * Purpose: Called once at startup to read the tocalls.yaml file which was obtained from * https://github.com/aprsorg/aprs-deviceid . * * Inputs: tocalls.yaml with OS specific directory search list. * * Outputs: static variables listed above. * * Description: For maximum flexibility, we will read the * data file at run time rather than compiling it in. * *------------------------------------------------------------------*/ // Make sure the array is null terminated. // If search order is changed, do the same in symbols.c for consistency. // fopen is perfectly happy with / in file path when running on Windows. static const char *search_locations[] = { (const char *) "tocalls.yaml", // Current working directory (const char *) "data/tocalls.yaml", // Windows with CMake (const char *) "../data/tocalls.yaml", // Source tree #ifndef __WIN32__ (const char *) "/usr/local/share/direwolf/tocalls.yaml", (const char *) "/usr/share/direwolf/tocalls.yaml", #endif #if __APPLE__ // https://groups.yahoo.com/neo/groups/direwolf_packet/conversations/messages/2458 // Adding the /opt/local tree since macports typically installs there. Users might want their // INSTALLDIR (see Makefile.macosx) to mirror that. If so, then we need to search the /opt/local // path as well. (const char *) "/opt/local/share/direwolf/tocalls.yaml", #endif (const char *) NULL // Important - Indicates end of list. }; void deviceid_init(void) { FILE *fp = NULL; for (int n = 0; search_locations[n] != NULL && fp == NULL; n++) { #if TEST text_color_set(DW_COLOR_INFO); dw_printf ("Trying %s\n", search_locations[n]); #endif fp = fopen(search_locations[n], "r"); #if TEST if (fp != NULL) { dw_printf ("Opened %s\n", search_locations[n]); } #endif }; if (fp == NULL) { text_color_set(DW_COLOR_ERROR); dw_printf("Could not open any of these file locations:\n"); for (int n = 0; search_locations[n] != NULL; n++) { dw_printf (" %s\n", search_locations[n]); } dw_printf("It won't be possible to extract device identifiers from packets.\n"); return; }; // Read file first time to get number of items. // Allocate required space. // Rewind. // Read file second time to gather data. enum { no_section, mice_section, tocalls_section} section = no_section; char stuff[80]; for (int pass = 1; pass <=2; pass++) { int line = 0; // Line number within file. while (fgets(stuff, sizeof(stuff), fp)) { line++; // Remove trailing CR/LF or spaces. char *p = stuff + strlen(stuff) - 1; while (p >= (char*)stuff && (*p == '\r' || *p == '\n' || *p == ' ')) { *p-- = '\0'; } // Ignore comment lines. if (stuff[0] == '#') { continue; } #if TEST //dw_printf ("%d: %s\n", line, stuff); #endif // This is not very robust; everything better be in exactly the right format. if (strncmp(stuff, "mice:", strlen("mice:")) == 0) { section = mice_section; #if TEST dw_printf ("Pass %d, line %d, MIC-E section\n", pass, line); #endif } else if (strncmp(stuff, "micelegacy:", strlen("micelegacy:")) == 0) { section = mice_section; // treat both same. #if TEST dw_printf ("Pass %d, line %d, Legacy MIC-E section\n", pass, line); #endif } else if (strncmp(stuff, "tocalls:", strlen("tocalls:")) == 0) { section = tocalls_section; #if TEST dw_printf ("Pass %d, line %d, TOCALLS section\n", pass, line); #endif } // The first property of an item is preceded by " - ". if (pass == 1 && strncmp(stuff, " - ", 3) == 0) { switch (section) { case no_section: break; case mice_section: mice_count++; break; case tocalls_section: tocalls_count++; break; } } if (pass == 2) { switch (section) { case no_section: break; case mice_section: if (strncmp(stuff, " - ", 3) == 0) { mice_index++; assert (mice_index >= 0 && mice_index < mice_count); } if (strncmp(stuff+3, "prefix: ", strlen("prefix: ")) == 0) { unquote (line, stuff+3+8, pmice[mice_index].prefix); } else if (strncmp(stuff+3, "suffix: ", strlen("suffix: ")) == 0) { unquote (line, stuff+3+8, pmice[mice_index].suffix); } else if (strncmp(stuff+3, "vendor: ", strlen("vendor: ")) == 0) { pmice[mice_index].vendor = strdup(stuff+3+8); } else if (strncmp(stuff+3, "model: ", strlen("model: ")) == 0) { pmice[mice_index].model = strdup(stuff+3+7); } break; case tocalls_section: if (strncmp(stuff, " - ", 3) == 0) { tocalls_index++; assert (tocalls_index >= 0 && tocalls_index < tocalls_count); } if (strncmp(stuff+3, "tocall: ", strlen("tocall: ")) == 0) { // Remove trailing wildcard characters ? * n char *r = stuff + strlen(stuff) - 1; while (r >= (char*)stuff && (*r == '?' || *r == '*' || *r == 'n')) { *r-- = '\0'; } strlcpy (ptocalls[tocalls_index].tocall, stuff+3+8, sizeof(ptocalls[tocalls_index].tocall)); // Remove trailing CR/LF or spaces. char *p = stuff + strlen(stuff) - 1; while (p >= (char*)stuff && (*p == '\r' || *p == '\n' || *p == ' ')) { *p-- = '\0'; } } else if (strncmp(stuff+3, "vendor: ", strlen("vendor: ")) == 0) { ptocalls[tocalls_index].vendor = strdup(stuff+3+8); } else if (strncmp(stuff+3, "model: ", strlen("model: ")) == 0) { ptocalls[tocalls_index].model = strdup(stuff+3+7); } break; } } } // while(fgets if (pass == 1) { #if TEST dw_printf ("deviceid sizes %d %d\n", mice_count, tocalls_count); #endif pmice = calloc(sizeof(struct mice), mice_count); ptocalls = calloc(sizeof(struct tocalls), tocalls_count); rewind (fp); section = no_section; } } // for pass = 1 or 2 fclose (fp); assert (mice_index == mice_count - 1); assert (tocalls_index == tocalls_count - 1); // MIC-E Legacy needs to be sorted so those with suffix come first. qsort (pmice, mice_count, sizeof(struct mice), mice_cmp); // Sort tocalls by decreasing length so the search will go from most specific to least specific. // Example: APY350 or APY008 would match those specific models before getting to the more generic APY. qsort (ptocalls, tocalls_count, sizeof(struct tocalls), tocall_cmp); #if TEST dw_printf ("MIC-E:\n"); for (int i = 0; i < mice_count; i++) { dw_printf ("%s %s %s\n", pmice[i].suffix, pmice[i].vendor, pmice[i].model); } dw_printf ("TOCALLS:\n"); for (int i = 0; i < tocalls_count; i++) { dw_printf ("%s %s %s\n", ptocalls[i].tocall, ptocalls[i].vendor, ptocalls[i].model); } #endif return; } // end deviceid_init /*------------------------------------------------------------------ * * Function: unquote * * Purpose: Remove surrounding quotes and undo any escapes. * * Inputs: line - File line number for error message. * * in - String with quotes. Might contain \ escapes. * * Outputs: out - Quotes and escapes removed. * Limited to 2 characters to avoid buffer overflow. * * Examples: in out * "_#" _# * "_\"" _" * "=" = * *------------------------------------------------------------------*/ static void unquote (int line, char *pin, char *pout) { int count = 0; *pout = '\0'; if (*pin != '"') { text_color_set(DW_COLOR_ERROR); dw_printf("Missing leading \" for %s on line %d.\n", pin, line); return; } pin++; while (*pin != '\0' && *pin != '\"' && count < 2) { if (*pin == '\\') { pin++; } *pout++ = *pin++; count++; } *pout = '\0'; if (*pin != '"') { text_color_set(DW_COLOR_ERROR); dw_printf("Missing trailing \" or string too long on line %d.\n", line); return; } } // Used to sort the tocalls by length. // When length is equal, alphabetically. static int tocall_cmp (const void *px, const void *py) { const struct tocalls *x = (struct tocalls *)px; const struct tocalls *y = (struct tocalls *)py; if (strlen(x->tocall) != strlen(y->tocall)) { return (strlen(y->tocall) - strlen(x->tocall)); } return (strcmp(x->tocall, y->tocall)); } // Used to sort the suffixes by length. // Longer at the top. // Example check for >xxx^ before >xxx . static int mice_cmp (const void *px, const void *py) { const struct mice *x = (struct mice *)px; const struct mice *y = (struct mice *)py; return (strlen(y->suffix) - strlen(x->suffix)); } /*------------------------------------------------------------------ * * Function: deviceid_decode_dest * * Purpose: Find vendor/model for destination address of form APxxxx. * * Inputs: dest - Destination address. No SSID. * * device_size - Amount of space available for result to avoid buffer overflow. * * Outputs: device - Vendor and model. * * Description: With the exception of MIC-E format, we expect to find the vendor/model in the * AX.25 destination field. The form should be APxxxx. * * Search the list looking for the maximum length match. * For example, * APXR = Xrouter * APX = Xastir * *------------------------------------------------------------------*/ void deviceid_decode_dest (char *dest, char *device, size_t device_size) { strlcpy (device, "UNKNOWN vendor/model", device_size); if (ptocalls == NULL) { text_color_set(DW_COLOR_ERROR); dw_printf("deviceid_decode_dest called without any deviceid data.\n"); return; } for (int n = 0; n < tocalls_count; n++) { if (strncmp(dest, ptocalls[n].tocall, strlen(ptocalls[n].tocall)) == 0) { if (ptocalls[n].vendor != NULL) { strlcpy (device, ptocalls[n].vendor, device_size); } if (ptocalls[n].vendor != NULL && ptocalls[n].model != NULL) { strlcat (device, " ", device_size); } if (ptocalls[n].model != NULL) { strlcat (device, ptocalls[n].model, device_size); } return; } } // Not found in table. strlcpy (device, "UNKNOWN vendor/model", device_size); } // end deviceid_decode_dest /*------------------------------------------------------------------ * * Function: deviceid_decode_mice * * Purpose: Find vendor/model for MIC-E comment. * * Inputs: comment - MIC-E comment that might have vendor/model encoded as * a prefix and/or suffix. * Any trailing CR has already been removed. * * trimmed_size - Amount of space available for result to avoid buffer overflow. * * device_size - Amount of space available for result to avoid buffer overflow. * * Outputs: trimmed - Final comment with device vendor/model removed. * This would include any altitude. * * device - Vendor and model. * * Description: MIC-E device identification has a tortured history. * * The Kenwood TH-D7A put ">" at the beginning of the comment. * The Kenwood TM-D700 put "]" at the beginning of the comment. * Later Kenwood models also added a single suffix character * using a character very unlikely to appear at the end of a comment. * * The later convention, used by everyone else, is to have a prefix of ` or ' * and a suffix of two characters. The suffix characters need to be * something very unlikely to be found at the end of a comment. * * A receiving device is expected to remove those extra characters * before displaying the comment. * * References: http://www.aprs.org/aprs12/mic-e-types.txt * http://www.aprs.org/aprs12/mic-e-examples.txt * https://github.com/wb2osz/aprsspec containing: * APRS Protocol Specification 1.2 * Understanding APRS Packets *------------------------------------------------------------------*/ // The strncmp documentation doesn't mention behavior if length is zero. // Do our own just to be safe. static inline int strncmp_z (char *a, char *b, size_t len) { int result = 0; if (len > 0) { result = strncmp(a, b, len); } //dw_printf ("Comparing '%s' and '%s' len %d result %d\n", a, b, len, result); return result; } void deviceid_decode_mice (char *comment, char *trimmed, size_t trimmed_size, char *device, size_t device_size) { strlcpy (device, "UNKNOWN vendor/model", device_size); strlcpy (trimmed, comment, trimmed_size); if (strlen(comment) < 1) { return; } if (ptocalls == NULL) { text_color_set(DW_COLOR_ERROR); dw_printf("deviceid_decode_mice called without any deviceid data.\n"); return; } // The Legacy format has an explicit prefix in the table. // For others, it must be ` or ' to indicate whether messaging capable. for (int n = 0; n < mice_count; n++) { if ((strlen(pmice[n].prefix) != 0 && // Legacy strncmp_z(comment, // prefix from table pmice[n].prefix, strlen(pmice[n].prefix)) == 0 && strncmp_z(comment + strlen(comment) - strlen(pmice[n].suffix), // possible suffix pmice[n].suffix, strlen(pmice[n].suffix)) == 0) || (strlen(pmice[n].prefix) == 0 && // Later (comment[0] == '`' || comment[0] == '\'') && // prefix ` or ' strncmp_z(comment + strlen(comment) - strlen(pmice[n].suffix), // suffix pmice[n].suffix, strlen(pmice[n].suffix)) == 0) ) { if (pmice[n].vendor != NULL) { strlcpy (device, pmice[n].vendor, device_size); } if (pmice[n].vendor != NULL && pmice[n].model != NULL) { strlcat (device, " ", device_size); } if (pmice[n].model != NULL) { strlcat (device, pmice[n].model, device_size); } // Remove any prefix/suffix and return what remains. strlcpy (trimmed, comment + 1, trimmed_size); trimmed[strlen(comment) - 1 - strlen(pmice[n].suffix)] = '\0'; return; } } // Not found. strlcpy (device, "UNKNOWN vendor/model", device_size); strlcpy (trimmed, comment, trimmed_size); } // end deviceid_decode_mice // end deviceid.c