// // 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: tt-user.c * * Purpose: Keep track of the APRStt users. * * Description: This maintains a list of recently heard APRStt users * and prepares "object" format packets for transmission. * * References: This is based upon APRStt (TM) documents but not 100% * compliant due to ambiguities and inconsistencies in * the specifications. * * http://www.aprs.org/aprstt.html * *---------------------------------------------------------------*/ #include "direwolf.h" #include #include #include #include #include #include #include #include "version.h" #include "ax25_pad.h" #include "textcolor.h" #include "aprs_tt.h" #include "tt_text.h" #include "dedupe.h" #include "tq.h" #include "igate.h" #include "tt_user.h" #include "encode_aprs.h" #include "latlong.h" #include "server.h" #include "kiss.h" #include "kissserial.h" #include "kissnet.h" #include "kiss_frame.h" /* * Information kept about local APRStt users. * * For now, just use a fixed size array for simplicity. */ #if TT_MAIN #define MAX_TT_USERS 3 #else #define MAX_TT_USERS 100 #endif #define MAX_CALLSIGN_LEN 9 /* "Object Report" names can be up to 9 characters. */ #define MAX_COMMENT_LEN 43 /* Max length of comment in "Object Report." */ //#define G_UNKNOWN -999999 /* Should be in one place. */ #define NUM_XMITS 3 #define XMIT_DELAY_1 5 #define XMIT_DELAY_2 8 #define XMIT_DELAY_3 13 static struct tt_user_s { char callsign[MAX_CALLSIGN_LEN+1]; /* Callsign of station heard. */ /* Does not include the "-12" SSID added later. */ /* Possibly other tactical call / object label. */ /* Null string indicates table position is not used. */ int count; /* Number of times we received information for this object. */ /* Value 1 means first time and could be used to send */ /* a welcome greeting. */ int ssid; /* SSID to add. */ /* Default of 12 but not always. */ char overlay; /* Overlay character. Should be 0-9, A-Z. */ /* Could be / or \ for general object. */ char symbol; /* 'A' for traditional. */ /* Can be any symbol for extended objects. */ char digit_suffix[3+1]; /* Suffix abbreviation as 3 digits. */ time_t last_heard; /* Timestamp when last heard. */ /* User information will be deleted at some */ /* point after last time being heard. */ int xmits; /* Number of remaining times to transmit info */ /* about the user. This is set to 3 when */ /* a station is heard and decremented each time */ /* an object packet is sent. The idea is to send */ /* 3 within 30 seconds to improve chances of */ /* being heard while using digipeater duplicate */ /* removal. */ // TODO: I think implementation is different. time_t next_xmit; /* Time for next transmit. Meaningful only */ /* if xmits > 0. */ int corral_slot; /* If location is known, set this to 0. */ /* Otherwise, this is a display offset position */ /* from the gateway. */ char loc_text[24]; /* Text representation of location when a single */ /* lat/lon point would be deceptive. e.g. */ /* 32TPP8049 */ /* 32TPP8179549363 */ /* 32T 681795 4849363 */ /* EM29QE78 */ double latitude, longitude; /* Location either from user or generated */ /* position in the corral. */ int ambiguity; /* Number of digits to omit from location. */ /* Default 0, max. 4. */ char freq[12]; /* Frequency in format 999.999MHz */ char ctcss[5]; /* CTCSS tone. Exactly 3 digits for integer part. */ /* For example 74.4 Hz becomes "074". */ char comment[MAX_COMMENT_LEN+1]; /* Free form comment from user. */ /* Comment sent in final object report includes */ /* other information besides this. */ char mic_e; /* Position status. */ /* Should be a character in range of '1' to '9' for */ /* the predefined status strings or '0' for none. */ char dao[8]; /* Enhanced position information. */ } tt_user[MAX_TT_USERS]; static void clear_user(int i); static void xmit_object_report (int i, int first_time); static void tt_setenv (int i); #if __WIN32__ // setenv is missing on Windows! int setenv(const char *name, const char *value, int overwrite) { char etemp[1000]; snprintf (etemp, sizeof(etemp), "%s=%s", name, value); putenv (etemp); return (0); } #endif /*------------------------------------------------------------------ * * Name: tt_user_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 audio_s *save_audio_config_p; static struct tt_config_s *save_tt_config_p; void tt_user_init (struct audio_s *p_audio_config, struct tt_config_s *p_tt_config) { int i; save_audio_config_p = p_audio_config; save_tt_config_p = p_tt_config; for (i=0; i= 0) or -1 if not found. * This happens to be an index into an array but * the implementation could change so the caller should * not make any assumptions. * *----------------------------------------------------------------*/ int tt_3char_suffix_search (char *suffix, char *callsign) { int i; /* * Look for suffix in list of known calls. */ for (i=0; i= 3 && len <= 6 && strcmp(tt_user[i].callsign + len - 3, suffix) == 0) { strlcpy (callsign, tt_user[i].callsign, MAX_CALLSIGN_LEN+1); return (i); } } /* * Not found. */ strlcpy (callsign, "", MAX_CALLSIGN_LEN+1); return (-1); } /* end tt_3char_suffix_search */ /*------------------------------------------------------------------ * * Name: clear_user * * Purpose: Clear specified user table entry. * * Inputs: handle for user table entry. * *----------------------------------------------------------------*/ static void clear_user(int i) { assert (i >= 0 && i < MAX_TT_USERS); memset (&(tt_user[i]), 0, sizeof (struct tt_user_s)); } /* end clear_user */ /*------------------------------------------------------------------ * * Name: find_avail * * Purpose: Find an available user table location. * * Inputs: none * * Returns: Handle for referring to table position. * * Description: If table is already full, this should delete the * least recently heard user to make room. * *----------------------------------------------------------------*/ static int find_avail (void) { int i; int i_oldest; for (i=0; i= 1 not already in use. * *----------------------------------------------------------------*/ static int corral_slot (void) { int slot, i, used; for (slot=1; ; slot++) { used = 0;; for (i=0; i= 0 && i < MAX_TT_USERS); strlcpy (tt_user[i].callsign, callsign, sizeof(tt_user[i].callsign)); tt_user[i].count = 1; tt_user[i].ssid = ssid; tt_user[i].overlay = overlay; tt_user[i].symbol = symbol; digit_suffix(tt_user[i].callsign, tt_user[i].digit_suffix); strlcpy (tt_user[i].loc_text, loc_text, sizeof(tt_user[i].loc_text)); if (latitude != G_UNKNOWN && longitude != G_UNKNOWN) { /* We have specific location. */ tt_user[i].corral_slot = 0; tt_user[i].latitude = latitude; tt_user[i].longitude = longitude; } else { /* Unknown location, put it in the corral. */ tt_user[i].corral_slot = corral_slot(); } tt_user[i].ambiguity = ambiguity; strlcpy (tt_user[i].freq, freq, sizeof(tt_user[i].freq)); strlcpy (tt_user[i].ctcss, ctcss, sizeof(tt_user[i].ctcss)); strlcpy (tt_user[i].comment, comment, sizeof(tt_user[i].comment)); tt_user[i].mic_e = mic_e; strlcpy(tt_user[i].dao, dao, sizeof(tt_user[i].dao)); } else { /* * Known user. Update with any new information. * Keep any old values where not being updated. */ assert (i >= 0 && i < MAX_TT_USERS); tt_user[i].count++; /* Any reason to look at ssid here? */ /* Update the symbol if not the default. */ if (overlay != APRSTT_DEFAULT_SYMTAB || symbol != APRSTT_DEFAULT_SYMBOL) { tt_user[i].overlay = overlay; tt_user[i].symbol = symbol; } if (strlen(loc_text) > 0) { strlcpy (tt_user[i].loc_text, loc_text, sizeof(tt_user[i].loc_text)); } if (latitude != G_UNKNOWN && longitude != G_UNKNOWN) { /* We have specific location. */ tt_user[i].corral_slot = 0; tt_user[i].latitude = latitude; tt_user[i].longitude = longitude; } if (ambiguity != G_UNKNOWN) { tt_user[i].ambiguity = ambiguity; } if (freq[0] != '\0') { strlcpy (tt_user[i].freq, freq, sizeof(tt_user[i].freq)); } if (ctcss[0] != '\0') { strlcpy (tt_user[i].ctcss, ctcss, sizeof(tt_user[i].ctcss)); } if (comment[0] != '\0') { strlcpy (tt_user[i].comment, comment, MAX_COMMENT_LEN); tt_user[i].comment[MAX_COMMENT_LEN] = '\0'; } if (mic_e != ' ') { tt_user[i].mic_e = mic_e; } if (strlen(dao) > 0) { strlcpy(tt_user[i].dao, dao, sizeof(tt_user[i].dao)); } } /* * In both cases, note last time heard and schedule object report transmission. */ tt_user[i].last_heard = time(NULL); tt_user[i].xmits = 0; tt_user[i].next_xmit = tt_user[i].last_heard + save_tt_config_p->xmit_delay[0]; /* * Send to applications and IGate immediately. */ xmit_object_report (i, 1); /* * Put properties into environment variables in preparation * for calling a user-specified script. */ tt_setenv (i); return (0); /* Success! */ } /* end tt_user_heard */ /*------------------------------------------------------------------ * * Name: tt_user_background * * Purpose: * * Inputs: * * Outputs: Append to transmit queue. * * Returns: None * * Description: ...... TBD * *----------------------------------------------------------------*/ void tt_user_background (void) { time_t now = time(NULL); int i; //text_color_set(DW_COLOR_DEBUG); //dw_printf ("tt_user_background() now = %d\n", (int)now); for (i=0; i= 0 && i < MAX_TT_USERS); if (tt_user[i].callsign[0] != '\0') { if (tt_user[i].xmits < save_tt_config_p->num_xmits && tt_user[i].next_xmit <= now) { //text_color_set(DW_COLOR_DEBUG); //dw_printf ("tt_user_background() now = %d\n", (int)now); //tt_user_dump (); xmit_object_report (i, 0); /* Increase count of number times this one was sent. */ tt_user[i].xmits++; if (tt_user[i].xmits < save_tt_config_p->num_xmits) { /* Schedule next one. */ tt_user[i].next_xmit += save_tt_config_p->xmit_delay[tt_user[i].xmits]; } //tt_user_dump (); } } } /* * Purge if too old. */ for (i=0; iretain_time < now) { //dw_printf ("debug: purging expired user %d\n", i); clear_user (i); } } } } /*------------------------------------------------------------------ * * Name: xmit_object_report * * Purpose: Create object report packet and put into transmit queue. * * Inputs: i - Index into user table. * * first_time - Is this being called immediately after the tone sequence * was received or after some delay? * For the former, we send to any attached applications * and the IGate. * For the latter, we transmit over radio. * * Outputs: Append to transmit queue. * * Returns: None * * Description: Details for specified user are converted to * "Object Report Format" and added to the transmit queue. * * If the user did not report a position, we have to make * up something so the corresponding object will show up on * the map or other list of nearby stations. * * The traditional approach is to put them in different * positions in the "corral" by applying increments of an * offset from the starting position. This has two * unfortunate properties. It gives the illusion we know * where the person is located. Being in the ,,, * *----------------------------------------------------------------*/ static void xmit_object_report (int i, int first_time) { char object_name[20]; // xxxxxxxxx or xxxxxx-nn char info_comment[200]; // usercomment [locationtext] /status !DAO! char object_info[250]; // info part of Object Report packet char stemp[300]; // src>dest,path:object_info double olat, olong; int oambig; // Position ambiguity. packet_t pp; char c4[4]; //text_color_set(DW_COLOR_DEBUG); //printf ("xmit_object_report (index = %d, first_time = %d) rx = %d, tx = %d\n", i, first_time, // save_tt_config_p->obj_recv_chan, save_tt_config_p->obj_xmit_chan); assert (i >= 0 && i < MAX_TT_USERS); /* * Prepare the object name. * Tack on "-12" if it is a callsign. */ strlcpy (object_name, tt_user[i].callsign, sizeof(object_name)); if (strlen(object_name) <= 6 && tt_user[i].ssid != 0) { char stemp8[8]; snprintf (stemp8, sizeof(stemp8), "-%d", tt_user[i].ssid); strlcat (object_name, stemp8, sizeof(object_name)); } if (tt_user[i].corral_slot == 0) { /* * Known location. */ olat = tt_user[i].latitude; olong = tt_user[i].longitude; oambig = tt_user[i].ambiguity; if (oambig == G_UNKNOWN) oambig = 0; } else { /* * Use made up position in the corral. */ double c_lat = save_tt_config_p->corral_lat; // Corral latitude. double c_long = save_tt_config_p->corral_lon; // Corral longitude. double c_offs = save_tt_config_p->corral_offset; // Corral (latitude) offset. olat = c_lat - (tt_user[i].corral_slot - 1) * c_offs; olong = c_long; oambig = 0; } /* * Build comment field from various information. * * usercomment [locationtext] /status !DAO! * * Any frequency is inserted at beginning later. */ strlcpy (info_comment, "", sizeof(info_comment)); if (strlen(tt_user[i].comment) != 0) { strlcat (info_comment, tt_user[i].comment, sizeof(info_comment)); } if (strlen(tt_user[i].loc_text) > 0) { if (strlen(info_comment) > 0) { strlcat (info_comment, " ", sizeof(info_comment)); } strlcat (info_comment, "[", sizeof(info_comment)); strlcat (info_comment, tt_user[i].loc_text, sizeof(info_comment)); strlcat (info_comment, "]", sizeof(info_comment)); } if (tt_user[i].mic_e >= '1' && tt_user[i].mic_e <= '9') { if (strlen(info_comment) > 0) { strlcat (info_comment, " ", sizeof(info_comment)); } // Insert "/" if status does not already begin with it. if (save_tt_config_p->status[tt_user[i].mic_e - '0'][0] != '/') { strlcat (info_comment, "/", sizeof(info_comment)); } strlcat (info_comment, save_tt_config_p->status[tt_user[i].mic_e - '0'], sizeof(info_comment)); } if (strlen(tt_user[i].dao) > 0) { if (strlen(info_comment) > 0) { strlcat (info_comment, " ", sizeof(info_comment)); } strlcat (info_comment, tt_user[i].dao, sizeof(info_comment)); } /* Official limit is 43 characters. */ //info_comment[MAX_COMMENT_LEN] = '\0'; /* * Packet header is built from mycall (of transmit channel) and software version. */ if (save_tt_config_p->obj_xmit_chan >= 0) { strlcpy (stemp, save_audio_config_p->achan[save_tt_config_p->obj_xmit_chan].mycall, sizeof(stemp)); } else { strlcpy (stemp, save_audio_config_p->achan[save_tt_config_p->obj_recv_chan].mycall, sizeof(stemp)); } strlcat (stemp, ">", sizeof(stemp)); strlcat (stemp, APP_TOCALL, sizeof(stemp)); c4[0] = '0' + MAJOR_VERSION; c4[1] = '0' + MINOR_VERSION; c4[2] = '\0'; strlcat (stemp, c4, sizeof(stemp)); /* * Append via path, for transmission, if specified. */ if ( ! first_time && save_tt_config_p->obj_xmit_via[0] != '\0') { strlcat (stemp, ",", sizeof(stemp)); strlcat (stemp, save_tt_config_p->obj_xmit_via, sizeof(stemp)); } strlcat (stemp, ":", sizeof(stemp)); encode_object (object_name, 0, tt_user[i].last_heard, olat, olong, oambig, tt_user[i].overlay, tt_user[i].symbol, 0,0,0,NULL, G_UNKNOWN, G_UNKNOWN, /* PHGD, Course/Speed */ strlen(tt_user[i].freq) > 0 ? atof(tt_user[i].freq) : G_UNKNOWN, strlen(tt_user[i].ctcss) > 0 ? atof(tt_user[i].ctcss) : G_UNKNOWN, G_UNKNOWN, /* CTCSS */ info_comment, object_info, sizeof(object_info)); strlcat (stemp, object_info, sizeof(stemp)); #if TT_MAIN printf ("---> %s\n\n", stemp); #else if (first_time) { text_color_set(DW_COLOR_DEBUG); dw_printf ("[APRStt] %s\n", stemp); } /* * Convert text to packet. */ pp = ax25_from_text (stemp, 1); if (pp == NULL) { text_color_set(DW_COLOR_ERROR); dw_printf ("\"%s\"\n", stemp); return; } /* * Send to one or more of the following depending on configuration: * Transmit queue. * Any attached application(s). * IGate. * * When transmitting over the radio, it gets sent multiple times, to help * probability of being heard, with increasing delays between. * * The other methods are reliable so we only want to send it once. */ if (first_time && save_tt_config_p->obj_send_to_app) { unsigned char fbuf[AX25_MAX_PACKET_LEN]; int flen; // TODO1.3: Put a wrapper around this so we only call one function to send by all methods. // We see the same sequence in direwolf.c. flen = ax25_pack(pp, fbuf); server_send_rec_packet (save_tt_config_p->obj_recv_chan, pp, fbuf, flen); kissnet_send_rec_packet (save_tt_config_p->obj_recv_chan, KISS_CMD_DATA_FRAME, fbuf, flen, NULL, -1); kissserial_send_rec_packet (save_tt_config_p->obj_recv_chan, KISS_CMD_DATA_FRAME, fbuf, flen, NULL, -1); kisspt_send_rec_packet (save_tt_config_p->obj_recv_chan, KISS_CMD_DATA_FRAME, fbuf, flen, NULL, -1); } if (first_time && save_tt_config_p->obj_send_to_ig) { //text_color_set(DW_COLOR_DEBUG); //dw_printf ("xmit_object_report (): send to IGate\n"); igate_send_rec_packet (save_tt_config_p->obj_recv_chan, pp); } if ( ! first_time && save_tt_config_p->obj_xmit_chan >= 0) { /* Remember it so we don't digipeat our own. */ dedupe_remember (pp, save_tt_config_p->obj_xmit_chan); tq_append (save_tt_config_p->obj_xmit_chan, TQ_PRIO_1_LO, pp); } else { ax25_delete (pp); } #endif } static const char *letters[26] = { "Alpha", "Bravo", "Charlie", "Delta", "Echo", "Foxtrot", "Golf", "Hotel", "India", "Juliet", "Kilo", "Lima", "Mike", "November", "Oscar", "Papa", "Quebec", "Romeo", "Sierra", "Tango", "Uniform", "Victor", "Whiskey", "X-ray", "Yankee", "Zulu" }; static const char *digits[10] = { "Zero", "One", "Two", "Three", "Four", "Five", "Six", "Seven", "Eight", "Nine" }; /*------------------------------------------------------------------ * * Name: tt_setenv * * Purpose: Put information in environment variables in preparation * for calling a user-supplied script for custom processing. * * Inputs: i - Index into tt_user table. * * Description: Timestamps displayed relative to current time. * *----------------------------------------------------------------*/ static void tt_setenv (int i) { char stemp[256]; char t2[2]; char *p; assert (i >= 0 && i < MAX_TT_USERS); setenv ("TTCALL", tt_user[i].callsign, 1); strlcpy (stemp, "", sizeof(stemp)); t2[1] = '\0'; for (p = tt_user[i].callsign; *p != '\0'; p++) { t2[0] = *p; strlcat (stemp, t2, sizeof(stemp)); if (p[1] != '\0') strlcat (stemp, " ", sizeof(stemp)); } setenv ("TTCALLSP", stemp, 1); strlcpy (stemp, "", sizeof(stemp)); for (p = tt_user[i].callsign; *p != '\0'; p++) { if (isupper(*p)) { strlcat (stemp, letters[*p - 'A'], sizeof(stemp)); } else if (islower(*p)) { strlcat (stemp, letters[*p - 'a'], sizeof(stemp)); } else if (isdigit(*p)) { strlcat (stemp, digits[*p - '0'], sizeof(stemp)); } else { t2[0] = *p; strlcat (stemp, t2, sizeof(stemp)); } if (p[1] != '\0') strlcat (stemp, " ", sizeof(stemp)); } setenv ("TTCALLPH", stemp, 1); snprintf (stemp, sizeof(stemp), "%d", tt_user[i].ssid); setenv ("TTSSID",stemp , 1); snprintf (stemp, sizeof(stemp), "%d", tt_user[i].count); setenv ("TTCOUNT",stemp , 1); snprintf (stemp, sizeof(stemp), "%c%c", tt_user[i].overlay, tt_user[i].symbol); setenv ("TTSYMBOL",stemp , 1); snprintf (stemp, sizeof(stemp), "%.6f", tt_user[i].latitude); setenv ("TTLAT",stemp , 1); snprintf (stemp, sizeof(stemp), "%.6f", tt_user[i].longitude); setenv ("TTLON",stemp , 1); setenv ("TTFREQ", tt_user[i].freq, 1); // TODO: Should convert to actual frequency. e.g. 074 becomes 74.4 // There is some code for this in decode_aprs.c but not broken out // into a function that we could use from here. // TODO: Document this environment variable after converting. setenv ("TTCTCSS", tt_user[i].ctcss, 1); setenv ("TTCOMMENT", tt_user[i].comment, 1); setenv ("TTLOC", tt_user[i].loc_text, 1); if (tt_user[i].mic_e >= '1' && tt_user[i].mic_e <= '9') { setenv ("TTSTATUS", save_tt_config_p->status[tt_user[i].mic_e - '0'], 1); } else { setenv ("TTSTATUS", "", 1); } setenv ("TTDAO", tt_user[i].dao, 1); } /* end tt_setenv */ /*------------------------------------------------------------------ * * Name: tt_user_dump * * Purpose: Print information about known users for debugging. * * Inputs: None. * * Description: Timestamps displayed relative to current time. * *----------------------------------------------------------------*/ void tt_user_dump (void) { int i; time_t now = time(NULL); printf ("call ov suf lsthrd xmit nxt cor lat long freq ctcss m comment\n"); for (i=0; i