// // This file is part of Dire Wolf, an amateur radio packet TNC. // // Copyright (C) 2013, 2014 John Langner, WB2OSZ // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 2 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with this program. If not, see . // /*------------------------------------------------------------------ * * Module: encode_aprs.c * * Purpose: Construct APRS packets from components. * * Description: * * References: APRS Protocol Reference. * * Frequency spec. * http://www.aprs.org/info/freqspec.txt * *---------------------------------------------------------------*/ #include #include #include #include #include #include #include #include #include "direwolf.h" #include "encode_aprs.h" #include "latlong.h" #include "textcolor.h" /*------------------------------------------------------------------ * * Name: set_norm_position * * Purpose: Fill in the human-readable latitude, longitude, * symbol part which is common to multiple data formats. * * Inputs: symtab - Symbol table id or overlay. * symbol - Symbol id. * dlat - Latitude. * dlong - Longitude. * ambiguity - Blank out least significant digits. * * Outputs: presult - Stored here. * * Returns: Number of characters in result. * *----------------------------------------------------------------*/ /* Position & symbol fields common to several message formats. */ typedef struct position_s { char lat[8]; char sym_table_id; /* / \ 0-9 A-Z */ char lon[9]; char symbol_code; } position_t; static int set_norm_position (char symtab, char symbol, double dlat, double dlong, int ambiguity, position_t *presult) { latitude_to_str (dlat, ambiguity, presult->lat); if (symtab != '/' && symtab != '\\' && ! isdigit(symtab) && ! isupper(symtab)) { text_color_set(DW_COLOR_ERROR); dw_printf ("Symbol table identifier is not one of / \\ 0-9 A-Z\n"); } presult->sym_table_id = symtab; longitude_to_str (dlong, ambiguity, presult->lon); if (symbol < '!' || symbol > '~') { text_color_set(DW_COLOR_ERROR); dw_printf ("Symbol code is not in range of ! to ~\n"); } presult->symbol_code = symbol; return (sizeof(position_t)); } /*------------------------------------------------------------------ * * Name: set_comp_position * * Purpose: Fill in the compressed latitude, longitude, * symbol part which is common to multiple data formats. * * Inputs: symtab - Symbol table id or overlay. * symbol - Symbol id. * dlat - Latitude. * dlong - Longitude. * * power - Watts. * height - Feet. * gain - dBi. * * course - Degress, 0 - 360 (360 equiv. to 0). * Use G_UNKNOWN for none or unknown. * speed - knots. * * * Outputs: presult - Stored here. * * Returns: Number of characters in result. * * Description: The cst field can have only one of * * course/speed - takes priority (this implementation) * radio range - calculated from PHG * altitude - not implemented yet. * * Some conversion must be performed for course from * the API definition to what is sent over the air. * *----------------------------------------------------------------*/ /* Compressed position & symbol fields common to several message formats. */ typedef struct compressed_position_s { char sym_table_id; /* / \ a-j A-Z */ /* "The presence of the leading Symbol Table Identifier */ /* instead of a digit indicates that this is a compressed */ /* Position Report and not a normal lat/long report." */ char y[4]; /* Compressed Latitude. */ char x[4]; /* Compressed Longitude. */ char symbol_code; char c; /* Course/speed or radio range or altitude. */ char s; char t ; /* Compression type. */ } compressed_position_t; static int set_comp_position (char symtab, char symbol, double dlat, double dlong, int power, int height, int gain, int course, int speed, compressed_position_t *presult) { if (symtab != '/' && symtab != '\\' && ! isdigit(symtab) && ! isupper(symtab)) { text_color_set(DW_COLOR_ERROR); dw_printf ("Symbol table identifier is not one of / \\ 0-9 A-Z\n"); } /* * In compressed format, the characters a-j are used for a numeric overlay. * This allows the receiver to distinguish between compressed and normal formats. */ if (isdigit(symtab)) { symtab = symtab - '0' + 'a'; } presult->sym_table_id = symtab; latitude_to_comp_str (dlat, presult->y); longitude_to_comp_str (dlong, presult->x); if (symbol < '!' || symbol > '~') { text_color_set(DW_COLOR_ERROR); dw_printf ("Symbol code is not in range of ! to ~\n"); } presult->symbol_code = symbol; /* * The cst field is complicated. * * When c is ' ', the cst field is not used. * * When the t byte has a certain pattern, c & s represent altitude. * * Otherwise, c & s can be either course/speed or radio range. * * When c is in range of '!' to 'z', * * ('!' - 33) * 4 = 0 degrees. * ... * ('z' - 33) * 4 = 356 degrees. * * In this case, s represents speed ... * * When c is '{', s is range ... */ if (speed > 0) { int c; int s; if (course != G_UNKNOWN) { c = (course + 2) / 4; if (c < 0) c += 90; if (c >= 90) c -= 90; } else { c = 0; } presult->c = c + '!'; s = (int)round(log(speed+1.0) / log(1.08)); presult->s = s + '!'; presult->t = 0x26 + '!'; /* current, other tracker. */ } else if (power || height || gain) { int s; float range; presult->c = '{'; /* radio range. */ if (power == 0) power = 10; if (height == 0) height = 20; if (gain == 0) gain = 3; // from protocol reference page 29. range = sqrt(2.0*height * sqrt((power/10.0) * (gain/2.0))); s = (int)round(log(range/2.) / log(1.08)); if (s < 0) s = 0; if (s > 93) s = 93; presult->s = s + '!'; presult->t = 0x26 + '!'; /* current, other tracker. */ } else { presult->c = ' '; /* cst field not used. */ presult->s = ' '; presult->t = '!'; /* avoid space. */ } return (sizeof(compressed_position_t)); } /*------------------------------------------------------------------ * * Name: phg_data_extension * * Purpose: Fill in parts of the power/height/gain data extension. * * Inputs: power - Watts. * height - Feet. * gain - dB. Protocol spec doesn't mention whether it is dBi or dBd. * This says dBi: * http://www.tapr.org/pipermail/aprssig/2008-September/027034.html * dir - Directivity: N, NE, etc., omni. * * Outputs: presult - Stored here. * * Returns: Number of characters in result. * *----------------------------------------------------------------*/ // TODO (bug): Doesn't check for G_UNKNOWN. // could have a case where some, but not all, values were specified. // Callers originally checked for any not zero. // now they check for any > 0. typedef struct phg_s { char P; char H; char G; char p; char h; char g; char d; } phg_t; static int phg_data_extension (int power, int height, int gain, char *dir, char *presult) { phg_t *r = (phg_t*)presult; int x; r->P = 'P'; r->H = 'H'; r->G = 'G'; x = (int)round(sqrt((float)power)) + '0'; if (x < '0') x = '0'; else if (x > '9') x = '9'; r->p = x; x = (int)round(log2(height/10.0)) + '0'; if (x < '0') x = '0'; /* Result can go beyond '9'. */ r->h = x; x = gain + '0'; if (x < '0') x = '0'; else if (x > '9') x = '0'; r->g = x; r->d = '0'; if (dir != NULL) { if (strcasecmp(dir,"NE") == 0) r->d = '1'; if (strcasecmp(dir,"E") == 0) r->d = '2'; if (strcasecmp(dir,"SE") == 0) r->d = '3'; if (strcasecmp(dir,"S") == 0) r->d = '4'; if (strcasecmp(dir,"SW") == 0) r->d = '5'; if (strcasecmp(dir,"W") == 0) r->d = '6'; if (strcasecmp(dir,"NW") == 0) r->d = '7'; if (strcasecmp(dir,"N") == 0) r->d = '8'; } return (sizeof(phg_t)); } /*------------------------------------------------------------------ * * Name: cse_spd_data_extension * * Purpose: Fill in parts of the course & speed data extension. * * Inputs: course - Degress, 0 - 360 (360 equiv. to 0). * Use G_UNKNOWN for none or unknown. * * speed - knots. * * Outputs: presult - Stored here. * * Returns: Number of characters in result. * * Description: Over the air we use: * 0 for unknown or not relevant. * 1 - 360 for valid course. (360 for north) * *----------------------------------------------------------------*/ typedef struct cs_s { char cse[3]; char slash; char spd[3]; } cs_t; static int cse_spd_data_extension (int course, int speed, char *presult) { cs_t *r = (cs_t*)presult; char stemp[8]; int x; if (course != G_UNKNOWN) { x = course; while (x < 1) x += 360; while (x > 360) x -= 360; // Should now be in range of 1 - 360. */ // Original value of 0 for north is transmitted as 360. */ } else { x = 0; } snprintf (stemp, sizeof(stemp), "%03d", x); memcpy (r->cse, stemp, 3); r->slash = '/'; x = speed; if (x < 0) x = 0; // would include G_UNKNOWN if (x > 999) x = 999; snprintf (stemp, sizeof(stemp), "%03d", x); memcpy (r->spd, stemp, 3); return (sizeof(cs_t)); } /*------------------------------------------------------------------ * * Name: frequency_spec * * Purpose: Put frequency specification in beginning of comment field. * * Inputs: freq - MHz. * tone - Hz. * offset - MHz. * * Outputs: presult - Stored here. * * Returns: Number of characters in result. * * Description: There are several valid variations. * * The frequency could be missing here if it is in the * object name. In this case we could have tone & offset. * * Offset must always be preceded by tone. * * Resulting formats are all fixed width and have a trailing space: * * "999.999MHz " * "T999 " * "+999 " (10 kHz units) * * Reference: http://www.aprs.org/info/freqspec.txt * *----------------------------------------------------------------*/ static int frequency_spec (float freq, float tone, float offset, char *presult) { int result_size = 24; // TODO: add as parameter. *presult = '\0'; if (freq > 0) { char stemp[16]; /* TODO: Should use letters for > 999.999. */ /* For now, just be sure we have proper field width. */ if (freq > 999.999) freq = 999.999; snprintf (stemp, sizeof(stemp), "%07.3fMHz ", freq); strlcpy (presult, stemp, result_size); } if (tone != G_UNKNOWN) { char stemp[12]; if (tone == 0) { strlcpy (stemp, "Toff ", sizeof (stemp)); } else { snprintf (stemp, sizeof(stemp), "T%03d ", (int)tone); } strlcat (presult, stemp, result_size); } if (offset != G_UNKNOWN) { char stemp[12]; snprintf (stemp, sizeof(stemp), "%+04d ", (int)round(offset * 100)); strlcat (presult, stemp, result_size); } return (strlen(presult)); } /*------------------------------------------------------------------ * * Name: encode_position * * Purpose: Construct info part for position report format. * * Inputs: messaging - This determines whether the data type indicator * is set to '!' (false) or '=' (true). * compressed - Send in compressed form? * lat - Latitude. * lon - Longitude. * ambiguity - Number of digits to omit from location. * alt_ft - Altitude in feet. * symtab - Symbol table id or overlay. * symbol - Symbol id. * * power - Watts. * height - Feet. * gain - dB. Not clear if it is dBi or dBd. * dir - Directivity: N, NE, etc., omni. * * course - Degress, 0 - 360 (360 equiv. to 0). * Use G_UNKNOWN for none or unknown. * speed - knots. // TODO: should distinguish unknown(not revevant) vs. known zero. * * freq - MHz. * tone - Hz. * offset - MHz. * * comment - Additional comment text. * * result_size - Ammount of space for result, provideed by * caller, to avoid buffer overflow. * * Outputs: presult - Stored here. Should be at least ??? bytes. * Could get into hundreds of characters * because it includes the comment. * * Returns: Number of characters in result. * * Description: There can be a single optional "data extension" * following the position so there is a choice * between: * Power/height/gain/directivity or * Course/speed. * * Afer that, * *----------------------------------------------------------------*/ typedef struct aprs_ll_pos_s { char dti; /* ! or = */ position_t pos; /* Comment up to 43 characters. */ /* Start of comment could be data extension(s). */ } aprs_ll_pos_t; typedef struct aprs_compressed_pos_s { char dti; /* ! or = */ compressed_position_t cpos; /* Comment up to 40 characters. */ /* No data extension allowed for compressed location. */ } aprs_compressed_pos_t; int encode_position (int messaging, int compressed, double lat, double lon, int ambiguity, int alt_ft, char symtab, char symbol, int power, int height, int gain, char *dir, int course, int speed, float freq, float tone, float offset, char *comment, char *presult, size_t result_size) { int result_len = 0; if (compressed) { aprs_compressed_pos_t *p = (aprs_compressed_pos_t *)presult; p->dti = messaging ? '=' : '!'; set_comp_position (symtab, symbol, lat, lon, power, height, gain, course, speed, &(p->cpos)); result_len = 1 + sizeof (p->cpos); } else { aprs_ll_pos_t *p = (aprs_ll_pos_t *)presult; p->dti = messaging ? '=' : '!'; set_norm_position (symtab, symbol, lat, lon, ambiguity, &(p->pos)); result_len = 1 + sizeof (p->pos); /* Optional data extension. (singular) */ /* Can't have both course/speed and PHG. Former gets priority. */ if (course != G_UNKNOWN || speed > 0) { result_len += cse_spd_data_extension (course, speed, presult + result_len); } else if (power > 0 || height > 0 || gain > 0) { result_len += phg_data_extension (power, height, gain, dir, presult + result_len); } } /* Optional frequency spec. */ if (freq != 0 || tone != 0 || offset != 0) { result_len += frequency_spec (freq, tone, offset, presult + result_len); } presult[result_len] = '\0'; /* Altitude. Can be anywhere in comment. */ if (alt_ft != G_UNKNOWN) { char salt[12]; /* Not clear if altitude can be negative. */ /* Be sure it will be converted to 6 digits. */ if (alt_ft < 0) alt_ft = 0; if (alt_ft > 999999) alt_ft = 999999; snprintf (salt, sizeof(salt), "/A=%06d", alt_ft); strlcat (presult, salt, result_size); result_len += strlen(salt); } /* Finally, comment text. */ if (comment != NULL) { strlcat (presult, comment, result_size); result_len += strlen(comment); } if (result_len >= result_size) { text_color_set(DW_COLOR_ERROR); dw_printf ("encode_position result of %d characters won't fit into space provided.\n", result_len); } return (result_len); } /* end encode_position */ /*------------------------------------------------------------------ * * Name: encode_object * * Purpose: Construct info part for object report format. * * Inputs: name - Name, up to 9 characters. * compressed - Send in compressed form? * thyme - Time stamp or 0 for none. * lat - Latitude. * lon - Longitude. * ambiguity - Number of digits to omit from location. * symtab - Symbol table id or overlay. * symbol - Symbol id. * * power - Watts. * height - Feet. * gain - dB. Not clear if it is dBi or dBd. * dir - Direction: N, NE, etc., omni. * * course - Degress, 0 - 360 (360 equiv. to 0). * Use G_UNKNOWN for none or unknown. * speed - knots. * * freq - MHz. * tone - Hz. * offset - MHz. * * comment - Additional comment text. * * result_size - Ammount of space for result, provideed by * caller, to avoid buffer overflow. * * Outputs: presult - Stored here. Should be at least ??? bytes. * 36 for fixed part, * 7 for optional extended data, * ~20 for freq, etc., * comment could be very long... * * Returns: Number of characters in result. * * Description: * *----------------------------------------------------------------*/ typedef struct aprs_object_s { struct { char dti; /* ; */ char name[9]; char live_killed; /* * for live or _ for killed */ char time_stamp[7]; } o; union { position_t pos; /* Up to 43 char comment. First 7 bytes could be data extension. */ compressed_position_t cpos; /* Up to 40 char comment. No PHG data extension in this case. */ } u; } aprs_object_t; int encode_object (char *name, int compressed, time_t thyme, double lat, double lon, int ambiguity, char symtab, char symbol, int power, int height, int gain, char *dir, int course, int speed, float freq, float tone, float offset, char *comment, char *presult, size_t result_size) { aprs_object_t *p = (aprs_object_t *) presult; int result_len = 0; int n; p->o.dti = ';'; memset (p->o.name, ' ', sizeof(p->o.name)); n = strlen(name); if (n > sizeof(p->o.name)) n = sizeof(p->o.name); memcpy (p->o.name, name, n); p->o.live_killed = '*'; if (thyme != 0) { struct tm tm; #define XMIT_UTC 1 #if XMIT_UTC gmtime_r (&thyme, &tm); #else /* Using local time, for this application, would make more sense to me. */ /* On Windows, localtime_r produces UTC. */ /* How do we set the time zone? Google for mingw time zone. */ localtime_r (thyme, &tm); #endif snprintf (p->o.time_stamp, sizeof(p->o.time_stamp), "%02d%02d%02d", tm.tm_mday, tm.tm_hour, tm.tm_min); #if XMIT_UTC p->o.time_stamp[6] = 'z'; #else p->o.time_stamp[6] = '/'; #endif } else { memcpy (p->o.time_stamp, "111111z", sizeof(p->o.time_stamp)); } if (compressed) { set_comp_position (symtab, symbol, lat, lon, power, height, gain, course, speed, &(p->u.cpos)); result_len = sizeof(p->o) + sizeof (p->u.cpos); } else { set_norm_position (symtab, symbol, lat, lon, ambiguity, &(p->u.pos)); result_len = sizeof(p->o) + sizeof (p->u.pos); /* Optional data extension. (singular) */ /* Can't have both course/speed and PHG. Former gets priority. */ if (course != G_UNKNOWN || speed > 0) { result_len += cse_spd_data_extension (course, speed, presult + result_len); } else if (power > 0 || height > 0 || gain > 0) { result_len += phg_data_extension (power, height, gain, dir, presult + result_len); } } /* Optional frequency spec. */ if (freq != 0 || tone != 0 || offset != 0) { result_len += frequency_spec (freq, tone, offset, presult + result_len); } presult[result_len] = '\0'; /* Finally, comment text. */ if (comment != NULL) { strlcat (presult, comment, result_size); result_len += strlen(comment); } if (result_len >= result_size) { text_color_set(DW_COLOR_ERROR); dw_printf ("encode_object result of %d characters won't fit into space provided.\n", result_len); } return (result_len); } /* end encode_object */ /*------------------------------------------------------------------ * * Name: main * * Purpose: Quick test for some functions in this file. * * Description: Just a smattering, not an organized test. * * $ rm a.exe ; gcc -DEN_MAIN encode_aprs.c latlong.c textcolor.c ; ./a.exe * *----------------------------------------------------------------*/ #if EN_MAIN int main (int argc, char *argv[]) { char result[100]; int errors = 0; /*********** Position ***********/ encode_position (0, 0, 42+34.61/60, -(71+26.47/60), 0, G_UNKNOWN, 'D', '&', 0, 0, 0, NULL, G_UNKNOWN, 0, 0, 0, 0, NULL, result, sizeof(result)); dw_printf ("%s\n", result); if (strcmp(result, "!4234.61ND07126.47W&") != 0) { dw_printf ("ERROR! line %d\n", __LINE__); errors++; } /* with PHG. */ // TODO: Need to test specifying some but not all. encode_position (0, 0, 42+34.61/60, -(71+26.47/60), 0, G_UNKNOWN, 'D', '&', 50, 100, 6, "N", G_UNKNOWN, 0, 0, 0, 0, NULL, result, sizeof(result)); dw_printf ("%s\n", result); if (strcmp(result, "!4234.61ND07126.47W&PHG7368") != 0) { dw_printf ("ERROR! line %d\n", __LINE__); errors++; } /* with freq & tone. minus offset, no offset, explict simplex. */ encode_position (0, 0, 42+34.61/60, -(71+26.47/60), 0, G_UNKNOWN, 'D', '&', 0, 0, 0, NULL, G_UNKNOWN, 0, 146.955, 74.4, -0.6, NULL, result, sizeof(result)); dw_printf ("%s\n", result); if (strcmp(result, "!4234.61ND07126.47W&146.955MHz T074 -060 ") != 0) { dw_printf ("ERROR! line %d\n", __LINE__); errors++; } encode_position (0, 0, 42+34.61/60, -(71+26.47/60), 0, G_UNKNOWN, 'D', '&', 0, 0, 0, NULL, G_UNKNOWN, 0, 146.955, 74.4, G_UNKNOWN, NULL, result, sizeof(result)); dw_printf ("%s\n", result); if (strcmp(result, "!4234.61ND07126.47W&146.955MHz T074 ") != 0) { dw_printf ("ERROR! line %d\n", __LINE__); errors++; } encode_position (0, 0, 42+34.61/60, -(71+26.47/60), 0, G_UNKNOWN, 'D', '&', 0, 0, 0, NULL, G_UNKNOWN, 0, 146.955, 74.4, 0, NULL, result, sizeof(result)); dw_printf ("%s\n", result); if (strcmp(result, "!4234.61ND07126.47W&146.955MHz T074 +000 ") != 0) { dw_printf ("ERROR! line %d\n", __LINE__); errors++; } /* with course/speed, freq, and comment! */ encode_position (0, 0, 42+34.61/60, -(71+26.47/60), 0, G_UNKNOWN, 'D', '&', 0, 0, 0, NULL, 180, 55, 146.955, 74.4, -0.6, "River flooding", result, sizeof(result)); dw_printf ("%s\n", result); if (strcmp(result, "!4234.61ND07126.47W&180/055146.955MHz T074 -060 River flooding") != 0) { dw_printf ("ERROR! line %d\n", __LINE__); errors++; } /* Course speed, no tone, + offset */ encode_position (0, 0, 42+34.61/60, -(71+26.47/60), 0, G_UNKNOWN, 'D', '&', 0, 0, 0, NULL, 180, 55, 146.955, G_UNKNOWN, 0.6, "River flooding", result, sizeof(result)); dw_printf ("%s\n", result); if (strcmp(result, "!4234.61ND07126.47W&180/055146.955MHz +060 River flooding") != 0) { dw_printf ("ERROR! line %d\n", __LINE__); errors++; } /* Course speed, no tone, + offset + altitude */ encode_position (0, 0, 42+34.61/60, -(71+26.47/60), 0, 12345, 'D', '&', 0, 0, 0, NULL, 180, 55, 146.955, G_UNKNOWN, 0.6, "River flooding", result, sizeof(result)); dw_printf ("%s\n", result); if (strcmp(result, "!4234.61ND07126.47W&180/055146.955MHz +060 /A=012345River flooding") != 0) { dw_printf ("ERROR! line %d\n", __LINE__); errors++; } encode_position (0, 0, 42+34.61/60, -(71+26.47/60), 0, 12345, 'D', '&', 0, 0, 0, NULL, 180, 55, 146.955, 0, 0.6, "River flooding", result, sizeof(result)); dw_printf ("%s\n", result); if (strcmp(result, "!4234.61ND07126.47W&180/055146.955MHz Toff +060 /A=012345River flooding") != 0) { dw_printf ("ERROR! line %d\n", __LINE__); errors++; } // TODO: try boundary conditions of course = 0, 359, 360 /*********** Compressed position. ***********/ encode_position (0, 1, 42+34.61/60, -(71+26.47/60), 0, G_UNKNOWN, 'D', '&', 0, 0, 0, NULL, G_UNKNOWN, 0, 0, 0, 0, NULL, result, sizeof(result)); dw_printf ("%s\n", result); if (strcmp(result, "!D8yKC 0) { text_color_set (DW_COLOR_ERROR); dw_printf ("Encode APRS test FAILED with %d errors.\n", errors); exit (EXIT_FAILURE); } text_color_set (DW_COLOR_REC); dw_printf ("Encode APRS test PASSED with no errors.\n"); exit (EXIT_SUCCESS); } /* end main */ #endif /* unit test */ /* end encode_aprs.c */