From f95c4e37dc08989465c32db86cdd7c5736dedd6e Mon Sep 17 00:00:00 2001 From: Martin Cooper Date: Sat, 11 Oct 2025 13:50:25 -0700 Subject: [PATCH] New SCHANNEL feature, like NCHANNEL but for serial TNCs The SCHANNEL feature is essentially the same as the existing NCHANNEL feature but allowing for the mapping of a serial TNC to a channel, as opposed to the network TNC supported by NCHANNEL. Configuration uses the following form in the direwolf.conf file: SCHANNEL [] The implementation is a close parallel to that for NCHANNEL, sharing some of the code, and makes use of existing serial port handling that hides OS differences. This results in fewer changes than might have been expected otherwise. --- src/CMakeLists.txt | 2 + src/audio.h | 10 +- src/beacon.c | 3 +- src/cdigipeater.c | 3 +- src/config.c | 87 +++++++++++- src/digipeater.c | 3 +- src/direwolf.c | 21 ++- src/dlq.h | 6 +- src/nettnc.c | 142 +------------------- src/sertnc.c | 322 +++++++++++++++++++++++++++++++++++++++++++++ src/sertnc.h | 7 + src/server.c | 12 +- src/tnc_common.c | 185 ++++++++++++++++++++++++++ src/tnc_common.h | 1 + src/tq.c | 15 ++- 15 files changed, 658 insertions(+), 161 deletions(-) create mode 100644 src/sertnc.c create mode 100644 src/sertnc.h create mode 100644 src/tnc_common.c create mode 100644 src/tnc_common.h diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index b13cb352..14313efc 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -85,7 +85,9 @@ list(APPEND direwolf_SOURCES morse.c multi_modem.c waypoint.c + tnc_common.c nettnc.c + sertnc.c serial_port.c pfilter.c ptt.c diff --git a/src/audio.h b/src/audio.h index 27940865..e0a58c9d 100644 --- a/src/audio.h +++ b/src/audio.h @@ -60,7 +60,8 @@ typedef enum retry_e { enum medium_e { MEDIUM_NONE = 0, // Channel is not valid for use. MEDIUM_RADIO, // Internal modem for radio. MEDIUM_IGATE, // Access IGate as ordinary channel. - MEDIUM_NETTNC }; // Remote network TNC. (new in 1.8) + MEDIUM_NETTNC, // Remote network TNC. (new in 1.8) + MEDIUM_SERTNC }; // Local serial TNC. (new in 1.8) typedef enum sanity_e { SANITY_APRS, SANITY_AX25, SANITY_NONE } sanity_t; @@ -152,6 +153,7 @@ struct audio_s { // MEDIUM_RADIO for internal modem. (only possibility earlier) // MEDIUM_IGATE allows application access to IGate. // MEDIUM_NETTNC for external TNC via TCP. + // MEDIUM_SERTNC for external TNC via serial port. int igate_vchannel; /* Virtual channel mapped to APRS-IS. */ /* -1 for none. */ @@ -164,6 +166,12 @@ struct audio_s { int nettnc_port[MAX_TOTAL_CHANS]; // Network TNC TCP port. + // Applies only to serial TNC type channels. + + char sertnc_device[MAX_TOTAL_CHANS][80]; // Serial TNC device name. + + int sertnc_baud[MAX_TOTAL_CHANS]; // Serial TNC baud rate. + /* Properties for each radio channel, common to receive and transmit. */ diff --git a/src/beacon.c b/src/beacon.c index b868f228..a518bdfe 100644 --- a/src/beacon.c +++ b/src/beacon.c @@ -165,7 +165,8 @@ void beacon_init (struct audio_s *pmodem, struct misc_config_s *pconfig, struct if (chan >= MAX_TOTAL_CHANS) chan = 0; // For ICHANNEL, use channel 0 call. if (g_modem_config_p->chan_medium[chan] == MEDIUM_RADIO || - g_modem_config_p->chan_medium[chan] == MEDIUM_NETTNC) { + g_modem_config_p->chan_medium[chan] == MEDIUM_NETTNC || + g_modem_config_p->chan_medium[chan] == MEDIUM_SERTNC) { if (strlen(g_modem_config_p->mycall[chan]) > 0 && strcasecmp(g_modem_config_p->mycall[chan], "N0CALL") != 0 && diff --git a/src/cdigipeater.c b/src/cdigipeater.c index 844af470..22bcba1c 100644 --- a/src/cdigipeater.c +++ b/src/cdigipeater.c @@ -134,7 +134,8 @@ void cdigipeater (int from_chan, packet_t pp) if ( from_chan < 0 || from_chan >= MAX_RADIO_CHANS || (save_audio_config_p->chan_medium[from_chan] != MEDIUM_RADIO && - save_audio_config_p->chan_medium[from_chan] != MEDIUM_NETTNC) ) { + save_audio_config_p->chan_medium[from_chan] != MEDIUM_NETTNC && + save_audio_config_p->chan_medium[from_chan] != MEDIUM_SERTNC) ) { text_color_set(DW_COLOR_ERROR); dw_printf ("cdigipeater: Did not expect to receive on invalid channel %d.\n", from_chan); return; diff --git a/src/config.c b/src/config.c index a1134bc5..876a9a6a 100644 --- a/src/config.c +++ b/src/config.c @@ -1381,6 +1381,70 @@ void config_init (char *fname, struct audio_s *p_audio_config, p_audio_config->nettnc_port[nchan] = atoi(t); } +/* + * SCHANNEL chan device baudrate - Define Serial TNC virtual channel. + * + * This allows a client application to talk to to an external TNC over serial KISS + * by using a channel number outside the normal range for modems. + * This does not change the current channel number used by MODEM, PTT, etc. + * + * chan = direwolf channel. + * device = device (serial port) name of serial TNC. + * baudrate = baud rate for communicating with serial TNC. + * + * Future: Might allow selection of channel on the serial TNC. + * For now, ignore incoming and set to 0 for outgoing. + * + * FIXME: Can't set mycall for schannel. + */ + + else if (strcasecmp(t, "SCHANNEL") == 0) { + t = split(NULL,0); + if (t == NULL) { + text_color_set(DW_COLOR_ERROR); + dw_printf ("Line %d: Missing virtual channel number for SCHANNEL command.\n", line); + continue; + } + int nchan = atoi(t); + if (nchan >= MAX_RADIO_CHANS && nchan < MAX_TOTAL_CHANS) { + + if (p_audio_config->chan_medium[nchan] == MEDIUM_NONE) { + + p_audio_config->chan_medium[nchan] = MEDIUM_SERTNC; + } + else { + text_color_set(DW_COLOR_ERROR); + dw_printf ("Line %d: SCHANNEL can't use channel %d because it is already in use.\n", line, nchan); + } + } + else { + text_color_set(DW_COLOR_ERROR); + dw_printf ("Line %d: SCHANNEL number must in range of %d to %d.\n", line, MAX_RADIO_CHANS, MAX_TOTAL_CHANS-1); + } + + t = split(NULL,0); + if (t == NULL) { + text_color_set(DW_COLOR_ERROR); + dw_printf ("Line %d: Missing serial TNC device for SCHANNEL command.\n", line); + continue; + } + strlcpy (p_audio_config->sertnc_device[nchan], t, sizeof(p_audio_config->sertnc_device[nchan])); + int n; + t = split(NULL,0); + if (t != NULL) { + n = atoi(t); + if (n != 1200 && n != 2400 && n != 4800 && n != 9600 && n != 19200 && n != 38400 && n != 57600 && n != 115200) { + text_color_set(DW_COLOR_ERROR); + dw_printf ("Line %d: Warning: Unsupported data rate of %d bits per second. Using 9600.\n", line, n); + n = 9600; + } + p_audio_config->sertnc_baud[nchan] = n; + } + else { + p_audio_config->sertnc_baud[nchan] = 9600; + } + } + /* * MYCALL station */ @@ -2749,7 +2813,8 @@ void config_init (char *fname, struct audio_s *p_audio_config, // Channels specified must be radio channels or network TNCs. if (p_audio_config->chan_medium[from_chan] != MEDIUM_RADIO && - p_audio_config->chan_medium[from_chan] != MEDIUM_NETTNC) { + p_audio_config->chan_medium[from_chan] != MEDIUM_NETTNC && + p_audio_config->chan_medium[from_chan] != MEDIUM_SERTNC) { text_color_set(DW_COLOR_ERROR); dw_printf ("Config file, line %d: FROM-channel %d is not valid.\n", line, from_chan); @@ -2777,7 +2842,8 @@ void config_init (char *fname, struct audio_s *p_audio_config, } if (p_audio_config->chan_medium[to_chan] != MEDIUM_RADIO && - p_audio_config->chan_medium[to_chan] != MEDIUM_NETTNC) { + p_audio_config->chan_medium[to_chan] != MEDIUM_NETTNC && + p_audio_config->chan_medium[to_chan] != MEDIUM_SERTNC) { text_color_set(DW_COLOR_ERROR); dw_printf ("Config file, line %d: TO-channel %d is not valid.\n", line, to_chan); @@ -3111,7 +3177,8 @@ void config_init (char *fname, struct audio_s *p_audio_config, } if (p_audio_config->chan_medium[from_chan] != MEDIUM_RADIO && - p_audio_config->chan_medium[from_chan] != MEDIUM_NETTNC) { + p_audio_config->chan_medium[from_chan] != MEDIUM_NETTNC && + p_audio_config->chan_medium[from_chan] != MEDIUM_SERTNC) { text_color_set(DW_COLOR_ERROR); dw_printf ("Config file, line %d: FROM-channel %d is not valid.\n", line, from_chan); @@ -3149,7 +3216,8 @@ void config_init (char *fname, struct audio_s *p_audio_config, continue; } if (p_audio_config->chan_medium[to_chan] != MEDIUM_RADIO && - p_audio_config->chan_medium[to_chan] != MEDIUM_NETTNC) { + p_audio_config->chan_medium[to_chan] != MEDIUM_NETTNC && + p_audio_config->chan_medium[to_chan] != MEDIUM_SERTNC) { text_color_set(DW_COLOR_ERROR); dw_printf ("Config file, line %d: TO-channel %d is not valid.\n", line, to_chan); @@ -4429,7 +4497,8 @@ void config_init (char *fname, struct audio_s *p_audio_config, x = -1; } else if (p_audio_config->chan_medium[x] != MEDIUM_RADIO && - p_audio_config->chan_medium[x] != MEDIUM_NETTNC) { + p_audio_config->chan_medium[x] != MEDIUM_NETTNC && + p_audio_config->chan_medium[x] != MEDIUM_SERTNC) { text_color_set(DW_COLOR_ERROR); dw_printf ("Config file, line %d: TTOBJ transmit channel %d is not valid.\n", line, x); x = -1; @@ -5784,7 +5853,9 @@ void config_init (char *fname, struct audio_s *p_audio_config, /* When IGate is enabled, all radio channels must have a callsign associated. */ if (strlen(p_igate_config->t2_login) > 0 && - (p_audio_config->chan_medium[i] == MEDIUM_RADIO || p_audio_config->chan_medium[i] == MEDIUM_NETTNC)) { + (p_audio_config->chan_medium[i] == MEDIUM_RADIO || + p_audio_config->chan_medium[i] == MEDIUM_NETTNC || + p_audio_config->chan_medium[i] == MEDIUM_SERTNC)) { if (strcmp(p_audio_config->mycall[i], "NOCALL") == 0 || strcmp(p_audio_config->mycall[i], "N0CALL") == 0) { text_color_set(DW_COLOR_ERROR); @@ -5810,7 +5881,9 @@ void config_init (char *fname, struct audio_s *p_audio_config, if (strlen(p_igate_config->t2_login) > 0) { for (j=0; jchan_medium[j] == MEDIUM_RADIO || p_audio_config->chan_medium[j] == MEDIUM_NETTNC) { + if (p_audio_config->chan_medium[j] == MEDIUM_RADIO || + p_audio_config->chan_medium[j] == MEDIUM_NETTNC || + p_audio_config->chan_medium[j] == MEDIUM_SERTNC) { if (p_digi_config->filter_str[MAX_TOTAL_CHANS][j] == NULL) { p_digi_config->filter_str[MAX_TOTAL_CHANS][j] = strdup("i/180"); } diff --git a/src/digipeater.c b/src/digipeater.c index fcf59568..a2992aed 100644 --- a/src/digipeater.c +++ b/src/digipeater.c @@ -156,7 +156,8 @@ void digipeater (int from_chan, packet_t pp) if ( from_chan < 0 || from_chan >= MAX_TOTAL_CHANS || (save_audio_config_p->chan_medium[from_chan] != MEDIUM_RADIO && - save_audio_config_p->chan_medium[from_chan] != MEDIUM_NETTNC)) { + save_audio_config_p->chan_medium[from_chan] != MEDIUM_NETTNC && + save_audio_config_p->chan_medium[from_chan] != MEDIUM_SERTNC)) { text_color_set(DW_COLOR_ERROR); dw_printf ("APRS digipeater: Did not expect to receive on invalid channel %d.\n", from_chan); } diff --git a/src/direwolf.c b/src/direwolf.c index f89dc6e6..844f0cbc 100644 --- a/src/direwolf.c +++ b/src/direwolf.c @@ -131,6 +131,7 @@ #include "dlq.h" // for fec_type_t definition. #include "deviceid.h" #include "nettnc.h" +#include "sertnc.h" //static int idx_decoded = 0; @@ -1018,11 +1019,12 @@ int main (int argc, char *argv[]) il2p_init (d_2_opt); /* - * New in 1.8 - Allow a channel to be mapped to a network TNC rather than - * an internal modem and radio. + * New in 1.8 - Allow a channel to be mapped to a network or serial TNC rather + * than an internal modem and radio. * I put it here so channel properties would come out in right order. */ nettnc_init (&audio_config); + sertnc_init (&audio_config); /* * Initialize the touch tone decoder & APRStt gateway. @@ -1191,6 +1193,7 @@ int main (int argc, char *argv[]) * -1 for DTMF decoder. * -2 for channel mapped to APRS-IS. * -3 for channel mapped to network TNC. + * -4 for channel mapped to serial TNC. * slice - Slicer which caught it. * pp - Packet handle. * alevel - Audio level, range of 0 - 100. @@ -1221,7 +1224,7 @@ void app_process_rec_packet (int chan, int subchan, int slice, packet_t pp, alev // Can indicate FX.25/IL2P or fix_bits. assert (chan >= 0 && chan < MAX_TOTAL_CHANS); // TOTAL for virtual channels - assert (subchan >= -3 && subchan < MAX_SUBCHANS); + assert (subchan >= -4 && subchan < MAX_SUBCHANS); assert (slice >= 0 && slice < MAX_SLICERS); assert (pp != NULL); // 1.1J+ @@ -1342,7 +1345,7 @@ void app_process_rec_packet (int chan, int subchan, int slice, packet_t pp, alev dw_printf ("Setting audio input level so most stations are around 50 will provide good dyanmic range.\n"); } // FIXME: rather than checking for ichannel, how about checking medium==radio - else if (alevel.rec < 5 && chan != audio_config.igate_vchannel && subchan != -3) { + else if (alevel.rec < 5 && chan != audio_config.igate_vchannel && subchan != SUBCHAN_NETTNC && subchan != SUBCHAN_SERTNC) { text_color_set(DW_COLOR_ERROR); dw_printf ("Audio input level is too low. Increase so most stations are around 50.\n"); @@ -1367,15 +1370,19 @@ void app_process_rec_packet (int chan, int subchan, int slice, packet_t pp, alev strlcpy (ts, "", sizeof(ts)); } - if (subchan == -1) { // dtmf + if (subchan == SUBCHAN_DTMF) { text_color_set(DW_COLOR_REC); dw_printf ("[%d.dtmf%s] ", chan, ts); } - else if (subchan == -2) { // APRS-IS + else if (subchan == SUBCHAN_APRSIS) { text_color_set(DW_COLOR_REC); dw_printf ("[%d.is%s] ", chan, ts); } - else if (subchan == -3) { // nettnc + else if (subchan == SUBCHAN_NETTNC) { + text_color_set(DW_COLOR_REC); + dw_printf ("[%d%s] ", chan, ts); + } + else if (subchan == SUBCHAN_SERTNC) { text_color_set(DW_COLOR_REC); dw_printf ("[%d%s] ", chan, ts); } diff --git a/src/dlq.h b/src/dlq.h index fdac1c0c..2b2a8a56 100644 --- a/src/dlq.h +++ b/src/dlq.h @@ -62,9 +62,13 @@ typedef struct dlq_item_s { int subchan; /* Winning "subchannel" when using multiple */ /* decoders on one channel. */ - /* Special case, -1 means DTMF decoder. */ /* Maybe we should have a different type in this case? */ +#define SUBCHAN_DTMF -1 +#define SUBCHAN_APRSIS -2 +#define SUBCHAN_NETTNC -3 +#define SUBCHAN_SERTNC -4 + int slice; /* Winning slicer. */ packet_t pp; /* Pointer to frame structure. */ diff --git a/src/nettnc.c b/src/nettnc.c index 72d18cc5..271a5b7b 100644 --- a/src/nettnc.c +++ b/src/nettnc.c @@ -61,12 +61,10 @@ #include "dlq.h" // received packet queue #include "nettnc.h" +#include "tnc_common.h" -void hex_dump (unsigned char *p, int len); - - // TODO: define macros in common locaation to hide platform specifics. #if __WIN32__ @@ -83,9 +81,8 @@ static pthread_t nettnc_listen_tid[MAX_TOTAL_CHANS]; static THREAD_F nettnc_listen_thread (void *arg); #endif -static void my_kiss_rec_byte (kiss_frame_t *kf, unsigned char b, int debug, int channel_override); +static int s_kiss_debug = 0; -int s_kiss_debug = 0; /*------------------------------------------------------------------- @@ -285,7 +282,7 @@ static void * nettnc_listen_thread (void *arg) for (int j = 0; j < n; j++) { // Separate the byte stream into KISS frame(s) and make it // look like this came from a radio channel. - my_kiss_rec_byte (&kstate, buf[j], s_kiss_debug, chan); + my_kiss_rec_byte (&kstate, buf[j], s_kiss_debug, chan, SUBCHAN_NETTNC); } } // s_tnc_sock != -1 } // while (1) @@ -296,139 +293,6 @@ static void * nettnc_listen_thread (void *arg) -/*------------------------------------------------------------------- - * - * Name: my_kiss_rec_byte - * - * Purpose: Process one byte from a KISS network TNC. - * - * Inputs: kf - Current state of building a frame. - * b - A byte from the input stream. - * debug - Activates debug output. - * channel_overide - Set incoming channel number to the NCHANNEL - * number rather than the channel in the KISS frame. - * - * Outputs: kf - Current state is updated. - * - * Returns: none. - * - * Description: This is a simplified version of kiss_rec_byte used - * for talking to KISS client applications. It already has - * too many special cases and I don't want to make it worse. - * This also needs to make the packet look like it came from - * a radio channel, not from a client app. - * - *-----------------------------------------------------------------*/ - -static void my_kiss_rec_byte (kiss_frame_t *kf, unsigned char b, int debug, int channel_override) -{ - - //dw_printf ("my_kiss_rec_byte ( %c %02x ) \n", b, b); - - switch (kf->state) { - - case KS_SEARCHING: /* Searching for starting FEND. */ - default: - - if (b == FEND) { - - /* Start of frame. */ - - kf->kiss_len = 0; - kf->kiss_msg[kf->kiss_len++] = b; - kf->state = KS_COLLECTING; - return; - } - return; - break; - - case KS_COLLECTING: /* Frame collection in progress. */ - - - if (b == FEND) { - - unsigned char unwrapped[AX25_MAX_PACKET_LEN]; - int ulen; - - /* End of frame. */ - - if (kf->kiss_len == 0) { - /* Empty frame. Starting a new one. */ - kf->kiss_msg[kf->kiss_len++] = b; - return; - } - if (kf->kiss_len == 1 && kf->kiss_msg[0] == FEND) { - /* Empty frame. Just go on collecting. */ - return; - } - - kf->kiss_msg[kf->kiss_len++] = b; - if (debug) { - /* As received over the wire from network TNC. */ - // May include escapted characters. What about FEND? -// FIXME: make it say Network TNC. - kiss_debug_print (FROM_CLIENT, NULL, kf->kiss_msg, kf->kiss_len); - } - - ulen = kiss_unwrap (kf->kiss_msg, kf->kiss_len, unwrapped); - - if (debug >= 2) { - /* Append CRC to this and it goes out over the radio. */ - text_color_set(DW_COLOR_DEBUG); - dw_printf ("\n"); - dw_printf ("Frame content after removing KISS framing and any escapes:\n"); - /* Don't include the "type" indicator. */ - /* It contains the radio channel and type should always be 0 here. */ - hex_dump (unwrapped+1, ulen-1); - } - - // Convert to packet object and send to received packet queue. - // Note that we use channel associated with the network TNC, not channel in KISS frame. - - int subchan = -3; - int slice = 0; - alevel_t alevel; - memset(&alevel, 0, sizeof(alevel)); - packet_t pp = ax25_from_frame (unwrapped+1, ulen-1, alevel); - if (pp != NULL) { - fec_type_t fec_type = fec_type_none; - retry_t retries; - memset (&retries, 0, sizeof(retries)); - char spectrum[] = "Network TNC"; - dlq_rec_frame (channel_override, subchan, slice, pp, alevel, fec_type, retries, spectrum); - } - else { - text_color_set(DW_COLOR_ERROR); - dw_printf ("Failed to create packet object for KISS frame from channel %d network TNC.\n", channel_override); - } - - kf->state = KS_SEARCHING; - return; - } - - if (kf->kiss_len < MAX_KISS_LEN) { - kf->kiss_msg[kf->kiss_len++] = b; - } - else { - text_color_set(DW_COLOR_ERROR); - dw_printf ("KISS frame from network TNC exceeded maximum length.\n"); - } - return; - break; - } - - return; /* unreachable but suppress compiler warning. */ - -} /* end my_kiss_rec_byte */ - - - - - - - - - /*------------------------------------------------------------------- * * Name: nettnc_send_packet diff --git a/src/sertnc.c b/src/sertnc.c new file mode 100644 index 00000000..dbe75785 --- /dev/null +++ b/src/sertnc.c @@ -0,0 +1,322 @@ + +// +// This file is part of Dire Wolf, an amateur radio packet TNC. +// +// Copyright (C) 2024 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: nettnc.c + * + * Purpose: Attach to Network KISS TNC(s) for NCHANNEL config file item(s). + * + * Description: Called once at application start up. + * + *---------------------------------------------------------------*/ + + +#include "direwolf.h" // Sets _WIN32_WINNT for XP API level needed by ws2tcpip.h + +#if __WIN32__ +#include +#include // _WIN32_WINNT must be set to 0x0501 before including this +#else +#include +#include +#include +#include +#include +#include +#endif + +#include +#include +#include +#include +#include +#include +#include + +#include "textcolor.h" +#include "audio.h" // configuration. +#include "kiss.h" +#include "ax25_pad.h" // for AX25_MAX_PACKET_LEN +#include "dlq.h" // received packet queue + +#include "serial_port.h" +#include "sertnc.h" +#include "tnc_common.h" + + + +// TODO: define macros in common locaation to hide platform specifics. + +#if __WIN32__ +#define THREAD_F unsigned __stdcall +#else +#define THREAD_F void * +#endif + +#if __WIN32__ +static HANDLE sertnc_listen_th[MAX_TOTAL_CHANS]; +static THREAD_F sertnc_listen_thread (void *arg); +#else +static pthread_t sertnc_listen_tid[MAX_TOTAL_CHANS]; +static THREAD_F sertnc_listen_thread (void *arg); +#endif + +static int s_kiss_debug = 0; + + +/*------------------------------------------------------------------- + * + * Name: sertnc_init + * + * Purpose: Attach to Serial KISS TNC(s) for SCHANNEL config file item(s). + * + * Inputs: pa - Address of structure of type audio_s. + * + * debug ? TBD + * + * + * Returns: 0 for success, -1 for failure. + * + * Description: Called once at direwolf application start up time. + * Calls sertnc_attach for each SCHANNEL configuration item. + * + *--------------------------------------------------------------------*/ + +void sertnc_init (struct audio_s *pa) +{ + for (int i = 0; i < MAX_TOTAL_CHANS; i++) { + + if (pa->chan_medium[i] == MEDIUM_SERTNC) { + text_color_set(DW_COLOR_DEBUG); + dw_printf ("Channel %d: Serial TNC %s %d\n", i, pa->sertnc_device[i], pa->sertnc_baud[i]); + int e = sertnc_attach (i, pa->sertnc_device[i], pa->sertnc_baud[i]); + if (e < 0) { + exit (1); + } + } + } + +} // end nettnc_init + + + +/*------------------------------------------------------------------- + * + * Name: sertnc_attach + * + * Purpose: Attach to one Serial KISS TNC. + * + * Inputs: chan - channel number from SCHANNEL configuration. + * + * device - Serial device name. Something like "/dev/ttyS0" or "COM4". + * + * baud - Serial baud rate. Typically 9600. + * + * Returns: 0 for success, -1 for failure. + * + * Description: This starts up a thread, for each device, which listens to the port and + * dispatches the messages to the corresponding callback functions. + * It will also attempt to re-establish communication with the + * device if it goes away. + * + *--------------------------------------------------------------------*/ + +static char s_tnc_device[MAX_TOTAL_CHANS][80]; +static int s_tnc_baud[MAX_TOTAL_CHANS]; +static volatile MYFDTYPE s_tnc_fd[MAX_TOTAL_CHANS]; // File descriptor. MYFDERROR for invalid. + + +int sertnc_attach (int chan, char *device, int baud) +{ + assert (chan >= 0 && chan < MAX_TOTAL_CHANS); + + strlcpy (s_tnc_device[chan], device, sizeof(s_tnc_device[chan])); + s_tnc_baud[chan] = baud; + s_tnc_fd[chan] = MYFDERROR; + + s_tnc_fd[chan] = serial_port_open (s_tnc_device[chan], s_tnc_baud[chan]); + + if (s_tnc_fd[chan] == MYFDERROR) { + return (-1); + } + + +/* + * Read frames from the serial TNC. + * If the TNC disappears, try to reestablish communication. + */ + + +#if __WIN32__ + sertnc_listen_th[chan] = (HANDLE)_beginthreadex (NULL, 0, sertnc_listen_thread, (void *)(ptrdiff_t)chan, 0, NULL); + if (sertnc_listen_th[chan] == NULL) { + text_color_set(DW_COLOR_ERROR); + dw_printf ("Internal error: Could not create serial TNC listening thread\n"); + return (-1); + } +#else + int e = pthread_create (&sertnc_listen_tid[chan], NULL, sertnc_listen_thread, (void *)(ptrdiff_t)chan); + if (e != 0) { + text_color_set(DW_COLOR_ERROR); + perror("Internal error: Could not create serial TNC listening thread"); + return (-1); + } +#endif + +// TNC initialization if specified. + +// if (s_tnc_init_func != NULL) { +// e = (*s_tnc_init_func)(); +// return (e); +// } + + return (0); + +} // end sertnc_attach + + + +/*------------------------------------------------------------------- + * + * Name: sertnc_listen_thread + * + * Purpose: Listen for anything from TNC and process it. + * Reconnect if something goes wrong and we got disconnected. + * + * Inputs: arg - Channel number. + * s_tnc_device[chan] - Device & baud rate for re-connection. + * s_tnc_baud[chan] + * + * Outputs: s_tnc_fd[chan] - File descriptor for communicating with TNC. + * Will be MYFDERROR if not connected. + * + *--------------------------------------------------------------------*/ + +#if __WIN32__ +static unsigned __stdcall sertnc_listen_thread (void *arg) +#else +static void * sertnc_listen_thread (void *arg) +#endif +{ + int chan = (int)(ptrdiff_t)arg; + assert (chan >= 0 && chan < MAX_TOTAL_CHANS); + + kiss_frame_t kstate; // State machine to gather a KISS frame. + memset (&kstate, 0, sizeof(kstate)); + + int ch; // normally 0-255 but -1 for error. + + while (1) { +/* + * Re-attach to TNC if not currently attached. + */ + if (s_tnc_fd[chan] == MYFDERROR) { + + text_color_set(DW_COLOR_ERROR); + // I'm using the term "attach" here, in an attempt to + // avoid confusion with the AX.25 connect. + dw_printf ("Attempting to reattach to serial TNC...\n"); + + s_tnc_fd[chan] = serial_port_open (s_tnc_device[chan], s_tnc_baud[chan]); + + if (s_tnc_fd[chan] != MYFDERROR) { + dw_printf ("Successfully reattached to serial TNC.\n"); + } + } + else { + ch = serial_port_get1 (s_tnc_fd[chan]); + + if (ch == -1) { + text_color_set(DW_COLOR_ERROR); + dw_printf ("Lost communication with serial TNC. Will try to reattach.\n"); + serial_port_close (s_tnc_fd[chan]); + s_tnc_fd[chan] = MYFDERROR; + SLEEP_SEC(5); + continue; + } + +#if 0 + text_color_set(DW_COLOR_DEBUG); + dw_printf ("TEMP DEBUG: byte received from channel %d serial TNC.\n", chan); +#endif + // Separate the byte stream into KISS frame(s) and make it + // look like this came from a radio channel. + my_kiss_rec_byte (&kstate, ch, s_kiss_debug, chan, SUBCHAN_SERTNC); + } // s_tnc_fd != MYFDERROR + } // while (1) + + return (0); // unreachable but shutup warning. + +} // end sertnc_listen_thread + + + +/*------------------------------------------------------------------- + * + * Name: sertnc_send_packet + * + * Purpose: Send packet to a KISS serial TNC. + * + * Inputs: chan - Channel number from SCHANNEL configuration. + * pp - Packet object. + * + * Outputs: Packet is converted to KISS and send to serial TNC. + * + * Returns: none. + * + * Description: This does not free the packet object; caller is responsible. + * + *-----------------------------------------------------------------*/ + +void sertnc_send_packet (int chan, packet_t pp) +{ + +// First, get the on-air frame format from packet object. +// Prepend 0 byte for KISS command and channel. + + unsigned char frame_buff[AX25_MAX_PACKET_LEN + 2]; // One byte for channel/command, + // followed by the AX.25 on-air format frame. + frame_buff[0] = 0; // For now, set channel to 0. + + unsigned char *fbuf = ax25_get_frame_data_ptr (pp); + int flen = ax25_get_frame_len (pp); + + memcpy (frame_buff+1, fbuf, flen); + +// Next, encapsulate into KISS frame with surrounding FENDs and any escapes. + + unsigned char kiss_buff[2 * AX25_MAX_PACKET_LEN]; + int kiss_len = kiss_encapsulate (frame_buff, flen+1, kiss_buff); + + int err = serial_port_write (s_tnc_fd[chan], (char*) kiss_buff, kiss_len); + if (err <= 0) { + text_color_set(DW_COLOR_ERROR); + dw_printf ("\nError %d sending packet to KISS Serial TNC for channel %d. Closing connection.\n\n", err, chan); + serial_port_close (s_tnc_fd[chan]); + s_tnc_fd[chan] = MYFDERROR; + } + + // Do not free packet object; caller will take care of it. + +} /* end nettnc_send_packet */ + diff --git a/src/sertnc.h b/src/sertnc.h new file mode 100644 index 00000000..4664f412 --- /dev/null +++ b/src/sertnc.h @@ -0,0 +1,7 @@ + + +void sertnc_init (struct audio_s *pa); + +int sertnc_attach (int chan, char *devicename, int baud); + +void sertnc_send_packet (int chan, packet_t pp); \ No newline at end of file diff --git a/src/server.c b/src/server.c index 64a99540..f859eb95 100644 --- a/src/server.c +++ b/src/server.c @@ -1569,7 +1569,8 @@ static THREAD_F cmd_listen_thread (void *arg) for (j=0; jchan_medium[j] == MEDIUM_RADIO || save_audio_config_p->chan_medium[j] == MEDIUM_IGATE || - save_audio_config_p->chan_medium[j] == MEDIUM_NETTNC) { + save_audio_config_p->chan_medium[j] == MEDIUM_NETTNC || + save_audio_config_p->chan_medium[j] == MEDIUM_SERTNC) { count++; } } @@ -1615,6 +1616,15 @@ static THREAD_F cmd_listen_thread (void *arg) } break; + case MEDIUM_SERTNC: + { + // could elaborate with device, etc. + char stemp[100]; + snprintf (stemp, sizeof(stemp), "Port%d Serial TNC;", j+1); + strlcat (reply.info, stemp, sizeof(reply.info)); + } + break; + default: ; // Only list valid channels. break; diff --git a/src/tnc_common.c b/src/tnc_common.c new file mode 100644 index 00000000..9426b7cb --- /dev/null +++ b/src/tnc_common.c @@ -0,0 +1,185 @@ + +// +// This file is part of Dire Wolf, an amateur radio packet TNC. +// +// Copyright (C) 2024 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: tnc_common.c + * + * Purpose: Functions common to both network and serial TNCs. + * + *---------------------------------------------------------------*/ + + +#include "direwolf.h" // Sets _WIN32_WINNT for XP API level needed by ws2tcpip.h + +#if __WIN32__ +#include +#include // _WIN32_WINNT must be set to 0x0501 before including this +#else +#include +#include +#include +#include +#include +#include +#endif + +#include + +#include "textcolor.h" +#include "kiss.h" +#include "dlq.h" // received packet queue + + + +void hex_dump (unsigned char *p, int len); + + +/*------------------------------------------------------------------- + * + * Name: my_kiss_rec_byte + * + * Purpose: Process one byte from a KISS network TNC. + * + * Inputs: kf - Current state of building a frame. + * b - A byte from the input stream. + * debug - Activates debug output. + * channel_overide - Set incoming channel number to the NCHANNEL + * or SCHANNEL number rather than the channel in + * the KISS frame. + * subchan - Sub-channel type, used here for identifying the frame + * as associated with either a network or a serial TNC. + * + * Outputs: kf - Current state is updated. + * + * Returns: none. + * + * Description: This is a simplified version of kiss_rec_byte used + * for talking to KISS client applications. It already has + * too many special cases and I don't want to make it worse. + * This also needs to make the packet look like it came from + * a radio channel, not from a client app. + * + *-----------------------------------------------------------------*/ + +void my_kiss_rec_byte (kiss_frame_t *kf, unsigned char b, int debug, int channel_override, int subchan) +{ + + //dw_printf ("my_kiss_rec_byte ( %c %02x ) \n", b, b); + + switch (kf->state) { + + case KS_SEARCHING: /* Searching for starting FEND. */ + default: + + if (b == FEND) { + + /* Start of frame. */ + + kf->kiss_len = 0; + kf->kiss_msg[kf->kiss_len++] = b; + kf->state = KS_COLLECTING; + return; + } + return; + break; + + case KS_COLLECTING: /* Frame collection in progress. */ + + + if (b == FEND) { + + unsigned char unwrapped[AX25_MAX_PACKET_LEN]; + int ulen; + + /* End of frame. */ + + if (kf->kiss_len == 0) { + /* Empty frame. Starting a new one. */ + kf->kiss_msg[kf->kiss_len++] = b; + return; + } + if (kf->kiss_len == 1 && kf->kiss_msg[0] == FEND) { + /* Empty frame. Just go on collecting. */ + return; + } + + kf->kiss_msg[kf->kiss_len++] = b; + if (debug) { + /* As received over the wire from network or serial TNC. */ + // May include escapted characters. What about FEND? +// FIXME: make it say Network TNC or Serial TNC. + kiss_debug_print (FROM_CLIENT, NULL, kf->kiss_msg, kf->kiss_len); + } + + ulen = kiss_unwrap (kf->kiss_msg, kf->kiss_len, unwrapped); + + if (debug >= 2) { + /* Append CRC to this and it goes out over the radio. */ + text_color_set(DW_COLOR_DEBUG); + dw_printf ("\n"); + dw_printf ("Frame content after removing KISS framing and any escapes:\n"); + /* Don't include the "type" indicator. */ + /* It contains the radio channel and type should always be 0 here. */ + hex_dump (unwrapped+1, ulen-1); + } + + // Convert to packet object and send to received packet queue. + // Note that we use channel associated with the network or serial TNC, not channel in KISS frame. + + int slice = 0; + alevel_t alevel; + memset(&alevel, 0, sizeof(alevel)); + packet_t pp = ax25_from_frame (unwrapped+1, ulen-1, alevel); + if (pp != NULL) { + fec_type_t fec_type = fec_type_none; + retry_t retries; + memset (&retries, 0, sizeof(retries)); + char *spectrum = (subchan == SUBCHAN_NETTNC ? "Network TNC" : "Serial TNC"); + dlq_rec_frame (channel_override, subchan, slice, pp, alevel, fec_type, retries, spectrum); + } + else { + text_color_set(DW_COLOR_ERROR); + dw_printf ("Failed to create packet object for KISS frame from channel %d %s TNC.\n", + channel_override, subchan == SUBCHAN_NETTNC ? "network" : "serial"); + } + + kf->state = KS_SEARCHING; + return; + } + + if (kf->kiss_len < MAX_KISS_LEN) { + kf->kiss_msg[kf->kiss_len++] = b; + } + else { + text_color_set(DW_COLOR_ERROR); + dw_printf ("KISS frame from %s TNC exceeded maximum length.\n", + subchan == SUBCHAN_NETTNC ? "network" : "serial"); + } + return; + break; + } + + return; /* unreachable but suppress compiler warning. */ + +} /* end my_kiss_rec_byte */ + diff --git a/src/tnc_common.h b/src/tnc_common.h new file mode 100644 index 00000000..01d715a7 --- /dev/null +++ b/src/tnc_common.h @@ -0,0 +1 @@ +void my_kiss_rec_byte (kiss_frame_t *kf, unsigned char b, int debug, int channel_override, int subchan); diff --git a/src/tq.c b/src/tq.c index 0738eca1..cedd1b01 100644 --- a/src/tq.c +++ b/src/tq.c @@ -53,6 +53,7 @@ #include "igate.h" #include "dtime_now.h" #include "nettnc.h" +#include "sertnc.h" static packet_t queue_head[MAX_RADIO_CHANS][TQ_NUM_PRIO]; /* Head of linked list for each queue. */ @@ -261,7 +262,8 @@ void tq_append (int chan, int prio, packet_t pp) #ifndef DIGITEST // avoid dtest link error if (save_audio_config_p->chan_medium[chan] == MEDIUM_IGATE || - save_audio_config_p->chan_medium[chan] == MEDIUM_NETTNC) { + save_audio_config_p->chan_medium[chan] == MEDIUM_NETTNC || + save_audio_config_p->chan_medium[chan] == MEDIUM_SERTNC) { char ts[100]; // optional time stamp. @@ -290,7 +292,7 @@ void tq_append (int chan, int prio, packet_t pp) igate_send_rec_packet (chan, pp); } - else { // network TNC + else if (save_audio_config_p->chan_medium[chan] == MEDIUM_NETTNC) { // network TNC dw_printf ("[%d>nt%s] ", chan, ts); dw_printf ("%s", stemp); /* stations followed by : */ ax25_safe_print ((char *)pinfo, info_len, ! ax25_is_aprs(pp)); @@ -298,6 +300,15 @@ void tq_append (int chan, int prio, packet_t pp) nettnc_send_packet (chan, pp); + } + else if (save_audio_config_p->chan_medium[chan] == MEDIUM_SERTNC) { // serial TNC + dw_printf ("[%d>st%s] ", chan, ts); + dw_printf ("%s", stemp); /* stations followed by : */ + ax25_safe_print ((char *)pinfo, info_len, ! ax25_is_aprs(pp)); + dw_printf ("\n"); + + sertnc_send_packet (chan, pp); + } ax25_delete(pp);