//
// 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