From 81d11c5e93a86f472c460be0be9b7a2c94551aec Mon Sep 17 00:00:00 2001
From: Vladimir K <ew1abz@gmail.com>
Date: Sun, 5 Apr 2020 19:43:41 -0700
Subject: [PATCH 01/67] Add libgpiod support

---
 .gitignore                       |  2 +-
 CMakeLists.txt                   | 12 +++++-
 cmake/modules/FindCompiler.cmake |  4 +-
 cmake/modules/Findgpiod.cmake    | 23 +++++++++++
 src/CMakeLists.txt               |  2 +
 src/audio.h                      |  4 +-
 src/config.c                     | 37 +++++++++++++++++
 src/direwolf.c                   |  3 ++
 src/ptt.c                        | 70 ++++++++++++++++++++++++++++++--
 9 files changed, 149 insertions(+), 8 deletions(-)
 create mode 100644 cmake/modules/Findgpiod.cmake

diff --git a/.gitignore b/.gitignore
index 659c845b..b917a7ab 100644
--- a/.gitignore
+++ b/.gitignore
@@ -109,5 +109,5 @@ $RECYCLE.BIN/
 *.dSYM
 
 # cmake
-build/
+build*/
 tmp/
\ No newline at end of file
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 9a1cb8e2..930f2718 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -143,8 +143,8 @@ elseif(APPLE)
   message(STATUS "RPATH support: ${CMAKE_MACOSX_RPATH}")
 
 elseif (WIN32)
-  if(NOT VS2015 AND NOT VS2017)
-    message(FATAL_ERROR "You must use Microsoft Visual Studio 2015 or 2017 as compiler")
+  if(NOT VS2015 AND NOT VS2017 AND NOT VS2019)
+    message(FATAL_ERROR "You must use Microsoft Visual Studio 2015 | 2017 | 2019 as compiler")
   endif()
 
   # compile with full multicore
@@ -251,6 +251,14 @@ else()
   set(HAMLIB_LIBRARIES "")
 endif()
 
+find_package(gpiod)
+if(GPIOD_FOUND)
+  set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -DUSE_GPIOD")
+else()
+  set(GPIOD_INCLUDE_DIRS "")
+  set(GPIOD_LIBRARIES "")
+endif()
+
 if(LINUX)
   find_package(ALSA REQUIRED)
   if(ALSA_FOUND)
diff --git a/cmake/modules/FindCompiler.cmake b/cmake/modules/FindCompiler.cmake
index f339a73e..91e1b89c 100644
--- a/cmake/modules/FindCompiler.cmake
+++ b/cmake/modules/FindCompiler.cmake
@@ -5,7 +5,9 @@ elseif(NOT DEFINED C_GCC AND CMAKE_CXX_COMPILER_ID MATCHES "GNU")
   set(C_GCC 1)
 elseif(NOT DEFINED C_MSVC AND CMAKE_CXX_COMPILER_ID MATCHES "MSVC")
   set(C_MSVC 1)
-  if(MSVC_VERSION GREATER 1910 AND MSVC_VERSION LESS 1919)
+  if(MSVC_VERSION GREATER 1919 AND MSVC_VERSION LESS 1926)
+    set(VS2019 ON)
+  elseif(MSVC_VERSION GREATER 1910 AND MSVC_VERSION LESS 1919)
     set(VS2017 ON)
   elseif(MSVC_VERSION GREATER 1899 AND MSVC_VERSION LESS 1910)
     set(VS2015 ON)
diff --git a/cmake/modules/Findgpiod.cmake b/cmake/modules/Findgpiod.cmake
new file mode 100644
index 00000000..bf5be305
--- /dev/null
+++ b/cmake/modules/Findgpiod.cmake
@@ -0,0 +1,23 @@
+# - Try to find libgpiod
+# Once done this will define
+#  GPIOD_FOUND - System has libgpiod
+#  GPIOD_INCLUDE_DIRS - The libgpiod include directories
+#  GPIOD_LIBRARIES - The libraries needed to use libgpiod
+#  GPIOD_DEFINITIONS - Compiler switches required for using libgpiod
+
+find_package(PkgConfig)
+pkg_check_modules(PC_GPIOD QUIET gpiod)
+
+find_path(GPIOD_INCLUDE_DIR gpiod.h)
+find_library(GPIOD_LIBRARY NAMES gpiod)
+
+include(FindPackageHandleStandardArgs)
+# handle the QUIETLY and REQUIRED arguments and set GPIOD_FOUND to TRUE
+# if all listed variables are TRUE
+find_package_handle_standard_args(gpiod  DEFAULT_MSG
+                                  GPIOD_LIBRARY GPIOD_INCLUDE_DIR)
+
+mark_as_advanced(GPIOD_INCLUDE_DIR GPIOD_LIBRARY)
+
+set(GPIOD_LIBRARIES ${GPIOD_LIBRARY})
+set(GPIOD_INCLUDE_DIRS ${GPIOD_INCLUDE_DIR})
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 46d3ac7a..4f8e647f 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -8,6 +8,7 @@ include_directories(
   ${UDEV_INCLUDE_DIRS}
   ${PORTAUDIO_INCLUDE_DIRS}
   ${CUSTOM_GEOTRANZ_DIR}
+  ${GPIOD_INCLUDE_DIRS}
   )
 
 if(WIN32 OR CYGWIN)
@@ -127,6 +128,7 @@ target_link_libraries(direwolf
   ${ALSA_LIBRARIES}
   ${UDEV_LIBRARIES}
   ${PORTAUDIO_LIBRARIES}
+  ${GPIOD_LIBRARIES}
   )
 
 if(WIN32 OR CYGWIN)
diff --git a/src/audio.h b/src/audio.h
index 53768f20..654d16fc 100644
--- a/src/audio.h
+++ b/src/audio.h
@@ -28,7 +28,8 @@
 enum ptt_method_e { 
 	PTT_METHOD_NONE,	/* VOX or no transmit. */
 	PTT_METHOD_SERIAL,	/* Serial port RTS or DTR. */
-	PTT_METHOD_GPIO,	/* General purpose I/O, Linux only. */
+	PTT_METHOD_GPIO,	/* General purpose I/O using sysfs, deprecated after 2020, Linux only. */
+	PTT_METHOD_GPIOD,	/* General purpose I/O, using libgpiod, Linux only. */
 	PTT_METHOD_LPT,	    	/* Parallel printer port, Linux only. */
 	PTT_METHOD_HAMLIB, 	/* HAMLib, Linux only. */
 	PTT_METHOD_CM108 };	/* GPIO pin of CM108/CM119/etc.  Linux only. */
@@ -266,6 +267,7 @@ struct audio_s {
 					/* the case for CubieBoard where it was longer. */
 					/* This is filled in by ptt_init so we don't have to */
 					/* recalculate it each time we access it. */
+					/* Also GPIO chip name for GPIOD method. Looks like 'gpiochip4' */
 
 					/* This could probably be collapsed into ptt_device instead of being separate. */
 
diff --git a/src/config.c b/src/config.c
index 8f9cb9f9..e40a6521 100644
--- a/src/config.c
+++ b/src/config.c
@@ -1743,6 +1743,43 @@ void config_init (char *fname, struct audio_s *p_audio_config,
 	      }
 	      p_audio_config->achan[channel].octrl[ot].ptt_method = PTT_METHOD_GPIO;
 #endif
+	    }
+	    else if (strcasecmp(t, "GPIOD") == 0) {
+#if __WIN32__
+	      text_color_set(DW_COLOR_ERROR);
+	      dw_printf ("Config file line %d: %s with GPIOD is only available on Linux.\n", line, otname);
+#else		
+#if defined(USE_GPIOD)
+	      t = split(NULL,0);
+	      if (t == NULL) {
+	        text_color_set(DW_COLOR_ERROR);
+	        dw_printf ("Config file line %d: Missing GPIO chip for %s.\n", line, otname);
+	        continue;
+	      }
+	      strlcpy(p_audio_config->achan[channel].octrl[ot].out_gpio_name, t, 
+	              sizeof(p_audio_config->achan[channel].octrl[ot].out_gpio_name));
+
+	      t = split(NULL,0);
+	      if (t == NULL) {
+	        text_color_set(DW_COLOR_ERROR);
+	        dw_printf("Config file line %d: Missing GPIO number for %s.\n", line, otname);
+	        continue;
+	      }
+
+	      if (*t == '-') {
+	        p_audio_config->achan[channel].octrl[ot].out_gpio_num = atoi(t+1);
+		p_audio_config->achan[channel].octrl[ot].ptt_invert = 1;
+	      }
+	      else {
+	        p_audio_config->achan[channel].octrl[ot].out_gpio_num = atoi(t);
+		p_audio_config->achan[channel].octrl[ot].ptt_invert = 0;
+	      }
+	      p_audio_config->achan[channel].octrl[ot].ptt_method = PTT_METHOD_GPIOD;
+#else
+	      text_color_set(DW_COLOR_ERROR);
+	      dw_printf ("GPIOD is not supported.\n");
+#endif /* USE_GPIOD*/
+#endif /* __WIN32__ */
 	    }
 	    else if (strcasecmp(t, "LPT") == 0) {
 
diff --git a/src/direwolf.c b/src/direwolf.c
index 8d6e8a7c..0e41f4fc 100644
--- a/src/direwolf.c
+++ b/src/direwolf.c
@@ -302,6 +302,9 @@ int main (int argc, char *argv[])
 #endif
 #if defined(USE_CM108)
 	dw_printf (" cm108-ptt");
+#endif
+#if defined(USE_GPIOD)
+	dw_printf (" libgpiod");
 #endif
 	dw_printf ("\n");
 #endif
diff --git a/src/ptt.c b/src/ptt.c
index cf49bbab..05eeeda3 100644
--- a/src/ptt.c
+++ b/src/ptt.c
@@ -166,6 +166,10 @@
 #include "cm108.h"
 #endif
 
+#ifdef USE_GPIOD
+#include <gpiod.h>
+#endif 
+
 /* So we can have more common code for fd. */
 typedef int HANDLE;
 #define INVALID_HANDLE_VALUE (-1)
@@ -623,6 +627,31 @@ void export_gpio(int ch, int ot, int invert, int direction)
 	get_access_to_gpio (gpio_value_path);
 }
 
+#if defined(USE_GPIOD)
+int gpiod_probe(const char *chip_name, int line_number)
+{
+	struct gpiod_chip *chip;
+	chip = gpiod_chip_open_by_name(chip_name);
+	if (chip == NULL) {
+		text_color_set(DW_COLOR_ERROR);
+		dw_printf ("Can't open GPIOD chip %s.\n", chip_name);
+		return -1;
+	}
+
+	struct gpiod_line *line;
+	line = gpiod_chip_get_line(chip, line_number);
+	if (line == NULL) {
+		text_color_set(DW_COLOR_ERROR);
+		dw_printf ("Can't get GPIOD line %d.\n", line_number);
+		return -1;
+	}
+	if (ptt_debug_level >= 2) {
+		text_color_set(DW_COLOR_DEBUG);
+		dw_printf("GPIOD probe OK. Chip: %s line: %d\n", chip_name, line_number);
+	}
+	return 0;
+}
+#endif   /* USE_GPIOD */
 #endif   /* not __WIN32__ */
 
 
@@ -639,7 +668,8 @@ void export_gpio(int ch, int ot, int invert, int direction)
  *			ptt_method	Method for PTT signal. 
  *					PTT_METHOD_NONE - not configured.  Could be using VOX. 
  *					PTT_METHOD_SERIAL - serial (com) port. 
- *					PTT_METHOD_GPIO - general purpose I/O. 
+ *					PTT_METHOD_GPIO - general purpose I/O (sysfs). 
+ *					PTT_METHOD_GPIOD - general purpose I/O (libgpiod). 
  *					PTT_METHOD_LPT - Parallel printer port. 
  *                  			PTT_METHOD_HAMLIB - HAMLib rig control.
  *					PTT_METHOD_CM108 - GPIO pins of CM108 etc. USB Audio.
@@ -718,12 +748,13 @@ void ptt_init (struct audio_s *audio_config_p)
 	    if (ptt_debug_level >= 2) {
 
 	      text_color_set(DW_COLOR_DEBUG);
-              dw_printf ("ch=%d, %s method=%d, device=%s, line=%d, gpio=%d, lpt_bit=%d, invert=%d\n",
+              dw_printf ("ch=%d, %s method=%d, device=%s, line=%d, name=%s, gpio=%d, lpt_bit=%d, invert=%d\n",
 		ch,
 		otnames[ot],
 		audio_config_p->achan[ch].octrl[ot].ptt_method, 
 		audio_config_p->achan[ch].octrl[ot].ptt_device,
 		audio_config_p->achan[ch].octrl[ot].ptt_line,
+		audio_config_p->achan[ch].octrl[ot].out_gpio_name,
 		audio_config_p->achan[ch].octrl[ot].out_gpio_num,
 		audio_config_p->achan[ch].octrl[ot].ptt_lpt_bit,
 		audio_config_p->achan[ch].octrl[ot].ptt_invert);
@@ -869,7 +900,28 @@ void ptt_init (struct audio_s *audio_config_p)
 	if (using_gpio) {
 	  get_access_to_gpio ("/sys/class/gpio/export");
 	}
-
+#if defined(USE_GPIOD)
+    // GPIOD
+	for (ch = 0; ch < MAX_CHANS; ch++) {
+	  if (save_audio_config_p->achan[ch].medium == MEDIUM_RADIO) {
+	    for (int ot = 0; ot < NUM_OCTYPES; ot++) {
+	      if (audio_config_p->achan[ch].octrl[ot].ptt_method == PTT_METHOD_GPIOD) {
+	        const char *chip_name = audio_config_p->achan[ch].octrl[ot].out_gpio_name;
+	        int line_number = audio_config_p->achan[ch].octrl[ot].out_gpio_num;
+	        int rc = gpiod_probe(chip_name, line_number);
+	        if (rc < 0) {
+	          text_color_set(DW_COLOR_ERROR);
+	          dw_printf ("Disable PTT for channel %d\n", ch);
+	          audio_config_p->achan[ch].octrl[ot].ptt_method = PTT_METHOD_NONE;
+	        } else {
+	          // Set initial state off ptt_set will invert output signal if appropriate.
+	          ptt_set (ot, ch, 0);
+	        }
+	      }
+	    }
+	  }
+	}
+#endif /* USE_GPIOD */
 /*
  * We should now be able to create the device nodes for 
  * the pins we want to use.
@@ -1226,6 +1278,18 @@ void ptt_set (int ot, int chan, int ptt_signal)
 	  close (fd);
 
 	}
+
+#if defined(USE_GPIOD)
+	if (save_audio_config_p->achan[chan].octrl[ot].ptt_method == PTT_METHOD_GPIOD) {
+		const char *chip = save_audio_config_p->achan[chan].octrl[ot].out_gpio_name;
+		int line = save_audio_config_p->achan[chan].octrl[ot].out_gpio_num;
+		int rc = gpiod_ctxless_set_value(chip, line, ptt, false, "direwolf", NULL, NULL);
+		if (ptt_debug_level >= 1) {
+			text_color_set(DW_COLOR_DEBUG);
+			dw_printf("PTT_METHOD_GPIOD chip: %s line: %d ptt: %d  rc: %d\n", chip, line, ptt, rc);
+		}
+	}
+#endif /* USE_GPIOD */
 #endif
 	
 /*

From 12abb8d91e4429d342e4057484bcdb03a23cc970 Mon Sep 17 00:00:00 2001
From: wb2osz <wb2osz@comcast.net>
Date: Wed, 22 Nov 2023 21:29:05 +0000
Subject: [PATCH 02/67] dev branch is now 1.8 development.

---
 CMakeLists.txt |  2 +-
 src/direwolf.c | 24 ++++++++++++------------
 2 files changed, 13 insertions(+), 13 deletions(-)

diff --git a/CMakeLists.txt b/CMakeLists.txt
index 84aeb738..3c01045b 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -4,7 +4,7 @@ project(direwolf)
 
 # configure version
 set(direwolf_VERSION_MAJOR "1")
-set(direwolf_VERSION_MINOR "7")
+set(direwolf_VERSION_MINOR "8")
 set(direwolf_VERSION_PATCH "0")
 set(direwolf_VERSION_SUFFIX "Development")
 
diff --git a/src/direwolf.c b/src/direwolf.c
index e23aecb4..c6ed9d4d 100644
--- a/src/direwolf.c
+++ b/src/direwolf.c
@@ -186,7 +186,7 @@ static int d_u_opt = 0;			/* "-d u" command line option to print UTF-8 also in h
 static int d_p_opt = 0;			/* "-d p" option for dumping packets over radio. */				
 
 static int q_h_opt = 0;			/* "-q h" Quiet, suppress the "heard" line with audio level. */
-static int q_d_opt = 0;			/* "-q d" Quiet, suppress the printing of decoded of APRS packets. */
+static int q_d_opt = 0;			/* "-q d" Quiet, suppress the printing of description of APRS packets. */
 
 static int A_opt_ais_to_obj = 0;	/* "-A" Convert received AIS to APRS "Object Report." */
 
@@ -302,24 +302,24 @@ int main (int argc, char *argv[])
 	text_color_init(t_opt);
 	text_color_set(DW_COLOR_INFO);
 	//dw_printf ("Dire Wolf version %d.%d (%s) BETA TEST 7\n", MAJOR_VERSION, MINOR_VERSION, __DATE__);
-	//dw_printf ("Dire Wolf DEVELOPMENT version %d.%d %s (%s)\n", MAJOR_VERSION, MINOR_VERSION, "G", __DATE__);
-	dw_printf ("Dire Wolf version %d.%d\n", MAJOR_VERSION, MINOR_VERSION);
+	dw_printf ("Dire Wolf DEVELOPMENT version %d.%d %s (%s)\n", MAJOR_VERSION, MINOR_VERSION, "A", __DATE__);
+	//dw_printf ("Dire Wolf version %d.%d\n", MAJOR_VERSION, MINOR_VERSION);
 
 
 #if defined(ENABLE_GPSD) || defined(USE_HAMLIB) || defined(USE_CM108) || USE_AVAHI_CLIENT || USE_MACOS_DNSSD
 	dw_printf ("Includes optional support for: ");
-#if defined(ENABLE_GPSD)
+ #if defined(ENABLE_GPSD)
 	dw_printf (" gpsd");
-#endif
-#if defined(USE_HAMLIB)
+ #endif
+ #if defined(USE_HAMLIB)
 	dw_printf (" hamlib");
-#endif
-#if defined(USE_CM108)
+ #endif
+ #if defined(USE_CM108)
 	dw_printf (" cm108-ptt");
-#endif
-#if (USE_AVAHI_CLIENT|USE_MACOS_DNSSD)
+ #endif
+ #if (USE_AVAHI_CLIENT|USE_MACOS_DNSSD)
 	dw_printf (" dns-sd");
-#endif
+ #endif
 	dw_printf ("\n");
 #endif
 
@@ -1708,7 +1708,7 @@ static void usage (char **argv)
 	dw_printf ("       d             d = APRStt (DTMF to APRS object translation).\n");
 	dw_printf ("    -q             Quiet (suppress output) options:\n");
 	dw_printf ("       h             h = Heard line with the audio level.\n");
-	dw_printf ("       d             d = Decoding of APRS packets.\n");
+	dw_printf ("       d             d = Description of APRS packets.\n");
 	dw_printf ("       x             x = Silence FX.25 information.\n");
 	dw_printf ("    -t n           Text colors.  0=disabled. 1=default.  2,3,4,... alternatives.\n");
 	dw_printf ("                     Use 9 to test compatibility with your terminal.\n");

From 6f0c1518c0186e6af9442d7e3276c44a6411f0e0 Mon Sep 17 00:00:00 2001
From: wb2osz <wb2osz@comcast.net>
Date: Wed, 22 Nov 2023 21:34:41 +0000
Subject: [PATCH 03/67] More error checking.

---
 src/config.c | 86 +++++++++++++++++++++++++++++++++++++++++++---------
 1 file changed, 72 insertions(+), 14 deletions(-)

diff --git a/src/config.c b/src/config.c
index 1ad6c081..739eb2f6 100644
--- a/src/config.c
+++ b/src/config.c
@@ -1,7 +1,7 @@
 //
 //    This file is part of Dire Wolf, an amateur radio packet TNC.
 //
-//    Copyright (C) 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2021  John Langner, WB2OSZ
+//    Copyright (C) 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2021, 2023  John Langner, WB2OSZ
 //
 //    This program is free software: you can redistribute it and/or modify
 //    it under the terms of the GNU General Public License as published by
@@ -755,12 +755,13 @@ void config_init (char *fname, struct audio_s *p_audio_config,
 	  strlcpy (p_audio_config->adev[adevice].adevice_out, DEFAULT_ADEVICE, sizeof(p_audio_config->adev[adevice].adevice_out));
 
 	  p_audio_config->adev[adevice].defined = 0;
+	  p_audio_config->adev[adevice].copy_from = -1;
 	  p_audio_config->adev[adevice].num_channels = DEFAULT_NUM_CHANNELS;		/* -2 stereo */
 	  p_audio_config->adev[adevice].samples_per_sec = DEFAULT_SAMPLES_PER_SEC;	/* -r option */
 	  p_audio_config->adev[adevice].bits_per_sample = DEFAULT_BITS_PER_SAMPLE;	/* -8 option for 8 instead of 16 bits */
 	}
 
-	p_audio_config->adev[0].defined = 1;
+	p_audio_config->adev[0].defined = 2;		// 2 means it was done by default and not the user's config file.
 
 	for (channel=0; channel<MAX_CHANS; channel++) {
 	  int ot, it;
@@ -925,10 +926,13 @@ void config_init (char *fname, struct audio_s *p_audio_config,
 
 	p_misc_config->maxframe_extended = AX25_K_MAXFRAME_EXTENDED_DEFAULT;	/* Max frames to send before ACK.  mod 128 "Window" size. */
 
-	p_misc_config->maxv22 = AX25_N2_RETRY_DEFAULT / 3;	/* Max SABME before falling back to SABM. */
-	p_misc_config->v20_addrs = NULL;			/* Go directly to v2.0 for stations listed. */
+	p_misc_config->maxv22 = AX25_N2_RETRY_DEFAULT / 3;	/* Send SABME this many times before falling back to SABM. */
+	p_misc_config->v20_addrs = NULL;			/* Go directly to v2.0 for stations listed */
+								/* without trying v2.2 first. */
 	p_misc_config->v20_count = 0;
 	p_misc_config->noxid_addrs = NULL;			/* Don't send XID to these stations. */
+								/* Might work with a partial v2.2 implementation */
+								/* on the other end. */
 	p_misc_config->noxid_count = 0;
 
 /* 
@@ -1012,7 +1016,11 @@ void config_init (char *fname, struct audio_s *p_audio_config,
  *			ADEVICE    plughw:1,0			-- same for in and out.
  *			ADEVICE	   plughw:2,0  plughw:3,0	-- different in/out for a channel or channel pair.
  *			ADEVICE1   udp:7355  default		-- from Software defined radio (SDR) via UDP.
- *	
+ *
+ *	New in 1.8: Ability to map to another audio device.
+ *	This allows multiple modems (i.e. data speeds) on the same audio interface.
+ *
+ *			ADEVICEn   = n				-- Copy from different already defined channel.
  */
 
 	  /* Note that ALSA name can contain comma such as hw:1,0 */
@@ -1040,17 +1048,42 @@ void config_init (char *fname, struct audio_s *p_audio_config,
 	      exit(EXIT_FAILURE);
 	    }
 
+	    // Do not allow same adevice to be defined more than once.
+	    // Overriding the default for adevice 0 is ok.
+	    // In that case definded was 2.  That's why we check for 1, not just non-zero.
+
+	    if (p_audio_config->adev[adevice].defined == 1) {		// 1 means defined by user.
+	      text_color_set(DW_COLOR_ERROR);
+	      dw_printf ("Config file: ADEVICE%d can't be defined more than once. Line %d.\n", adevice, line);
+	      continue;
+	    }
+
 	    p_audio_config->adev[adevice].defined = 1;
-	
-	    /* First channel of device is valid. */
-	    p_audio_config->chan_medium[ADEVFIRSTCHAN(adevice)] = MEDIUM_RADIO;
 
-	    strlcpy (p_audio_config->adev[adevice].adevice_in, t, sizeof(p_audio_config->adev[adevice].adevice_in));
-	    strlcpy (p_audio_config->adev[adevice].adevice_out, t, sizeof(p_audio_config->adev[adevice].adevice_out));
+	    // New case for release 1.8.
 
-	    t = split(NULL,0);
-	    if (t != NULL) {
+	    if (strcmp(t, "=") == 0) {
+	      t = split(NULL,0);
+	      if (t != NULL) {
+	        
+	      }
+
+/////////  to be continued....  FIXME
+
+	    }
+	    else {
+	      /* First channel of device is valid. */
+	      // This might be changed to UDP or STDIN when the device name is examined.
+	      p_audio_config->chan_medium[ADEVFIRSTCHAN(adevice)] = MEDIUM_RADIO;
+
+	      strlcpy (p_audio_config->adev[adevice].adevice_in, t, sizeof(p_audio_config->adev[adevice].adevice_in));
 	      strlcpy (p_audio_config->adev[adevice].adevice_out, t, sizeof(p_audio_config->adev[adevice].adevice_out));
+
+	      t = split(NULL,0);
+	      if (t != NULL) {
+		// Different audio devices for receive and transmit.
+	        strlcpy (p_audio_config->adev[adevice].adevice_out, t, sizeof(p_audio_config->adev[adevice].adevice_out));
+	      }
 	    }
 	  }
 
@@ -2173,7 +2206,7 @@ void config_init (char *fname, struct audio_s *p_audio_config,
 	    else {
 	      p_audio_config->achan[channel].slottime = DEFAULT_SLOTTIME;
 	      text_color_set(DW_COLOR_ERROR);
-              dw_printf ("Line %d: Invalid delay time for persist algorithm. Using %d.\n", 
+              dw_printf ("Line %d: Invalid delay time for persist algorithm. Using default %d.\n", 
 			line, p_audio_config->achan[channel].slottime);
    	    }
 	  }
@@ -2197,7 +2230,7 @@ void config_init (char *fname, struct audio_s *p_audio_config,
 	    else {
 	      p_audio_config->achan[channel].persist = DEFAULT_PERSIST;
 	      text_color_set(DW_COLOR_ERROR);
-              dw_printf ("Line %d: Invalid probability for persist algorithm. Using %d.\n", 
+              dw_printf ("Line %d: Invalid probability for persist algorithm. Using default %d.\n", 
 			line, p_audio_config->achan[channel].persist);
    	    }
 	  }
@@ -2216,6 +2249,19 @@ void config_init (char *fname, struct audio_s *p_audio_config,
 	    }
 	    n = atoi(t);
             if (n >= 0 && n <= 255) {
+	      text_color_set(DW_COLOR_ERROR);
+	      if (n == 0) {
+                dw_printf ("Line %d: Setting TXDELAY to 0 is a REALLY BAD idea if you want other stations to hear you.\n", 
+			line);
+                dw_printf ("Line %d: See User Guide, \"Radio Channel - Transmit Timing\" for an explanation.\n", 
+			line);
+	      }
+	      if (n >= 100) {
+                dw_printf ("Line %d: Keeping with tradition, going back to the 1980s, TXDELAY is in 10 millisecond units.\n", 
+			line);
+                dw_printf ("Line %d: The value %d would be %.3f seconds which seems rather excessive.  Are you sure you want that?\n", 
+			line, n, (double)n * 10. / 1000.);
+	      }
 	      p_audio_config->achan[channel].txdelay = n;
 	    }
 	    else {
@@ -2240,6 +2286,18 @@ void config_init (char *fname, struct audio_s *p_audio_config,
 	    }
 	    n = atoi(t);
             if (n >= 0 && n <= 255) {
+	      if (n == 0) {
+                dw_printf ("Line %d: Setting TXTAIL to 0 is a REALLY BAD idea if you want other stations to hear you.\n", 
+			line);
+                dw_printf ("Line %d: See User Guide, \"Radio Channel - Transmit Timing\" for an explanation.\n", 
+			line);
+	      }
+	      if (n >= 50) {
+                dw_printf ("Line %d: Keeping with tradition, going back to the 1980s, TXTAIL is in 10 millisecond units.\n", 
+			line);
+                dw_printf ("Line %d: The value %d would be %.3f seconds which seems rather excessive.  Are you sure you want that?\n", 
+			line, n, (double)n * 10. / 1000.);
+	      }
 	      p_audio_config->achan[channel].txtail = n;
 	    }
 	    else {

From 6192661f3df331c7abffe921b62db80994e0930e Mon Sep 17 00:00:00 2001
From: wb2osz <wb2osz@comcast.net>
Date: Sat, 25 Nov 2023 15:32:04 +0000
Subject: [PATCH 04/67] Compile error.

---
 src/audio.h | 15 +++++++++++----
 1 file changed, 11 insertions(+), 4 deletions(-)

diff --git a/src/audio.h b/src/audio.h
index cb5ca94e..ae1035d9 100644
--- a/src/audio.h
+++ b/src/audio.h
@@ -74,16 +74,23 @@ struct audio_s {
 
 	    /* Properties of the sound device. */
 
-	    int defined;		/* Was device defined? */
-					/* First one defaults to yes. */
+	    int defined;		/* Was device defined?   0=no.  >0 for yes.  */
+					/* First channel defaults to 2 for yes with default config. */
+					/* 1 means it was defined by user. */
+
+	    int copy_from;		/* >=0  means copy contents from another audio device. */
+					/* In this case we don't have device names, below. */
+					/* Num channels, samples/sec, and bit/sample are copied from */
+					/* original device and can't be changed. */
+					/* -1 for normal case. */
 
 	    char adevice_in[80];	/* Name of the audio input device (or file?). */
-					/* TODO: Can be "-" to read from stdin. */
+					/* Can be udp:nnn for UDP or "-" to read from stdin. */
 
 	    char adevice_out[80];	/* Name of the audio output device (or file?). */
 
 	    int num_channels;		/* Should be 1 for mono or 2 for stereo. */
-	    int samples_per_sec;	/* Audio sampling rate.  Typically 11025, 22050, or 44100. */
+	    int samples_per_sec;	/* Audio sampling rate.  Typically 11025, 22050, 44100, or 48000. */
 	    int bits_per_sample;	/* 8 (unsigned char) or 16 (signed short). */
 
 	} adev[MAX_ADEVS];

From ad5dbaec73c1ac280f88177bc0aa635fc6e108e3 Mon Sep 17 00:00:00 2001
From: wb2osz <wb2osz@comcast.net>
Date: Sun, 26 Nov 2023 01:12:34 +0000
Subject: [PATCH 05/67] Refine ptt gpiod.

---
 src/config.c |  6 ++++--
 src/ptt.c    | 16 +++++++++++++++-
 2 files changed, 19 insertions(+), 3 deletions(-)

diff --git a/src/config.c b/src/config.c
index faf4e8ff..747d0e60 100644
--- a/src/config.c
+++ b/src/config.c
@@ -1852,7 +1852,8 @@ void config_init (char *fname, struct audio_s *p_audio_config,
 	      t = split(NULL,0);
 	      if (t == NULL) {
 	        text_color_set(DW_COLOR_ERROR);
-	        dw_printf ("Config file line %d: Missing GPIO chip for %s.\n", line, otname);
+	        dw_printf ("Config file line %d: Missing GPIO chip name for %s.\n", line, otname);
+	        dw_printf ("Use the \"gpioinfo\" command to get a list of gpio chip names and corresponding I/O lines.\n");
 	        continue;
 	      }
 	      strlcpy(p_audio_config->achan[channel].octrl[ot].out_gpio_name, t, 
@@ -1876,7 +1877,8 @@ void config_init (char *fname, struct audio_s *p_audio_config,
 	      p_audio_config->achan[channel].octrl[ot].ptt_method = PTT_METHOD_GPIOD;
 #else
 	      text_color_set(DW_COLOR_ERROR);
-	      dw_printf ("GPIOD is not supported.\n");
+	      dw_printf ("Application was not built with optional support for GPIOD.\n");
+	      dw_printf ("Install packages gpiod and libgpiod-dev, remove 'build' subdirectory, then rebuild.\n");
 #endif /* USE_GPIOD*/
 #endif /* __WIN32__ */
 	    }
diff --git a/src/ptt.c b/src/ptt.c
index 6177d6d7..f6020394 100644
--- a/src/ptt.c
+++ b/src/ptt.c
@@ -472,6 +472,20 @@ void export_gpio(int ch, int ot, int invert, int direction)
 	    text_color_set(DW_COLOR_ERROR);
 	    dw_printf ("Error writing \"%s\" to %s, errno=%d\n", stemp, gpio_export_path, e);
 	    dw_printf ("%s\n", strerror(e));
+
+	    if (e == 22) {
+	      // It appears that error 22 occurs when sysfs gpio is not available.
+	      // (See https://github.com/wb2osz/direwolf/issues/503)
+	      //
+	      // The solution might be to use the new gpiod approach.
+
+	      dw_printf ("It looks like gpio with sysfs is not supported on this operating system.\n");
+	      dw_printf ("Rather than the following form, in the configuration file,\n);
+	      dw_printf ("    PTT GPIO  %s\n", stemp);
+	      dw_printf ("try using gpiod form instead.  e.g.\n");
+	      dw_printf ("    PTT GPIOD  gpiochip0  %s\n", stemp);
+	      dw_printf ("You can get a list of gpio chip names and corresponding I/O lines with \"gpioinfo\" command.\n");
+	    }
 	    exit (1);
 	  }
 	}
@@ -914,7 +928,7 @@ void ptt_init (struct audio_s *audio_config_p)
 #if defined(USE_GPIOD)
     // GPIOD
 	for (ch = 0; ch < MAX_CHANS; ch++) {
-	  if (save_audio_config_p->achan[ch].medium == MEDIUM_RADIO) {
+	  if (save_audio_config_p->chan_medium[ch] == MEDIUM_RADIO) {
 	    for (int ot = 0; ot < NUM_OCTYPES; ot++) {
 	      if (audio_config_p->achan[ch].octrl[ot].ptt_method == PTT_METHOD_GPIOD) {
 	        const char *chip_name = audio_config_p->achan[ch].octrl[ot].out_gpio_name;

From 5d35780498e6c14c56e1cdf998409fff7b5ba380 Mon Sep 17 00:00:00 2001
From: wb2osz <wb2osz@comcast.net>
Date: Sun, 26 Nov 2023 01:29:13 +0000
Subject: [PATCH 06/67] missing quote

---
 src/ptt.c | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/ptt.c b/src/ptt.c
index f6020394..a75cb8de 100644
--- a/src/ptt.c
+++ b/src/ptt.c
@@ -480,7 +480,7 @@ void export_gpio(int ch, int ot, int invert, int direction)
 	      // The solution might be to use the new gpiod approach.
 
 	      dw_printf ("It looks like gpio with sysfs is not supported on this operating system.\n");
-	      dw_printf ("Rather than the following form, in the configuration file,\n);
+	      dw_printf ("Rather than the following form, in the configuration file,\n");
 	      dw_printf ("    PTT GPIO  %s\n", stemp);
 	      dw_printf ("try using gpiod form instead.  e.g.\n");
 	      dw_printf ("    PTT GPIOD  gpiochip0  %s\n", stemp);

From b069d0f031eb2b92d189ecbc4e36f7d71739e321 Mon Sep 17 00:00:00 2001
From: wb2osz <wb2osz@comcast.net>
Date: Thu, 21 Dec 2023 23:01:05 +0000
Subject: [PATCH 07/67] More config file checking.

---
 src/config.c | 61 +++++++++++++++++++++++++++++++++++++---------------
 1 file changed, 44 insertions(+), 17 deletions(-)

diff --git a/src/config.c b/src/config.c
index 747d0e60..de8d74d4 100644
--- a/src/config.c
+++ b/src/config.c
@@ -2204,6 +2204,9 @@ void config_init (char *fname, struct audio_s *p_audio_config,
 
 /*
  * DWAIT n		- Extra delay for receiver squelch. n = 10 mS units.
+ *
+ * Why did I do this?  Just add more to TXDELAY.
+ * Now undocumented in User Guide.  Might disappear someday.
  */
 
 	  else if (strcasecmp(t, "DWAIT") == 0) {
@@ -2239,14 +2242,20 @@ void config_init (char *fname, struct audio_s *p_audio_config,
 	      continue;
 	    }
 	    n = atoi(t);
-            if (n >= 0 && n <= 255) {
+            if (n >= 5 && n < 50) {
+	      // 0 = User has no clue.  This would be no delay.
+	      // 10 = Default.
+	      // 50 = Half second.  User might think it is mSec and use 100.
 	      p_audio_config->achan[channel].slottime = n;
 	    }
 	    else {
 	      p_audio_config->achan[channel].slottime = DEFAULT_SLOTTIME;
 	      text_color_set(DW_COLOR_ERROR);
-              dw_printf ("Line %d: Invalid delay time for persist algorithm. Using default %d.\n", 
+              dw_printf ("Line %d: Invalid delay time for persist algorithm. Using default %d.\n",
 			line, p_audio_config->achan[channel].slottime);
+              dw_printf ("Read the Dire Wolf User Guide, \"Radio Channel - Transmit Timing\"\n");
+              dw_printf ("section, to understand what this means.\n");
+              dw_printf ("Why don't you just use the default?\n");
    	    }
 	  }
 
@@ -2263,14 +2272,17 @@ void config_init (char *fname, struct audio_s *p_audio_config,
 	      continue;
 	    }
 	    n = atoi(t);
-            if (n >= 0 && n <= 255) {
+            if (n >= 5 && n <= 250) {
 	      p_audio_config->achan[channel].persist = n;
 	    }
 	    else {
 	      p_audio_config->achan[channel].persist = DEFAULT_PERSIST;
 	      text_color_set(DW_COLOR_ERROR);
-              dw_printf ("Line %d: Invalid probability for persist algorithm. Using default %d.\n", 
+              dw_printf ("Line %d: Invalid probability for persist algorithm. Using default %d.\n",
 			line, p_audio_config->achan[channel].persist);
+              dw_printf ("Read the Dire Wolf User Guide, \"Radio Channel - Transmit Timing\"\n");
+              dw_printf ("section, to understand what this means.\n");
+              dw_printf ("Why don't you just use the default?\n");
    	    }
 	  }
 
@@ -2289,17 +2301,21 @@ void config_init (char *fname, struct audio_s *p_audio_config,
 	    n = atoi(t);
             if (n >= 0 && n <= 255) {
 	      text_color_set(DW_COLOR_ERROR);
-	      if (n == 0) {
-                dw_printf ("Line %d: Setting TXDELAY to 0 is a REALLY BAD idea if you want other stations to hear you.\n", 
-			line);
-                dw_printf ("Line %d: See User Guide, \"Radio Channel - Transmit Timing\" for an explanation.\n", 
+	      if (n < 10) {
+                dw_printf ("Line %d: Setting TXDELAY this small is a REALLY BAD idea if you want other stations to hear you.\n", 
 			line);
+                dw_printf ("Read the Dire Wolf User Guide, \"Radio Channel - Transmit Timing\"\n");
+                dw_printf ("section, to understand what this means.\n");
+                dw_printf ("Why don't you just use the default rather than reducing reliability?\n");
 	      }
-	      if (n >= 100) {
+	      else if (n >= 100) {
                 dw_printf ("Line %d: Keeping with tradition, going back to the 1980s, TXDELAY is in 10 millisecond units.\n", 
 			line);
                 dw_printf ("Line %d: The value %d would be %.3f seconds which seems rather excessive.  Are you sure you want that?\n", 
 			line, n, (double)n * 10. / 1000.);
+                dw_printf ("Read the Dire Wolf User Guide, \"Radio Channel - Transmit Timing\"\n");
+                dw_printf ("section, to understand what this means.\n");
+                dw_printf ("Why don't you just use the default?\n");
 	      }
 	      p_audio_config->achan[channel].txdelay = n;
 	    }
@@ -2325,24 +2341,28 @@ void config_init (char *fname, struct audio_s *p_audio_config,
 	    }
 	    n = atoi(t);
             if (n >= 0 && n <= 255) {
-	      if (n == 0) {
-                dw_printf ("Line %d: Setting TXTAIL to 0 is a REALLY BAD idea if you want other stations to hear you.\n", 
-			line);
-                dw_printf ("Line %d: See User Guide, \"Radio Channel - Transmit Timing\" for an explanation.\n", 
+	      if (n < 5) {
+                dw_printf ("Line %d: Setting TXTAIL that small is a REALLY BAD idea if you want other stations to hear you.\n", 
 			line);
+                dw_printf ("Read the Dire Wolf User Guide, \"Radio Channel - Transmit Timing\"\n");
+                dw_printf ("section, to understand what this means.\n");
+                dw_printf ("Why don't you just use the default rather than reducing reliability?\n");
 	      }
-	      if (n >= 50) {
+	      else if (n >= 50) {
                 dw_printf ("Line %d: Keeping with tradition, going back to the 1980s, TXTAIL is in 10 millisecond units.\n", 
 			line);
                 dw_printf ("Line %d: The value %d would be %.3f seconds which seems rather excessive.  Are you sure you want that?\n", 
 			line, n, (double)n * 10. / 1000.);
+                dw_printf ("Read the Dire Wolf User Guide, \"Radio Channel - Transmit Timing\"\n");
+                dw_printf ("section, to understand what this means.\n");
+                dw_printf ("Why don't you just use the default?\n");
 	      }
 	      p_audio_config->achan[channel].txtail = n;
 	    }
 	    else {
 	      p_audio_config->achan[channel].txtail = DEFAULT_TXTAIL;
 	      text_color_set(DW_COLOR_ERROR);
-              dw_printf ("Line %d: Invalid time for transmit timing. Using %d.\n", 
+              dw_printf ("Line %d: Invalid time for transmit timing. Using %d.\n",
 			line, p_audio_config->achan[channel].txtail);
    	    }
 	  }
@@ -2891,7 +2911,7 @@ void config_init (char *fname, struct audio_s *p_audio_config,
 	      dw_printf ("Config file: FILTER IG ... on line %d.\n", line);
 	      dw_printf ("Warning! Don't mess with IS>RF filtering unless you are an expert and have an unusual situation.\n");
 	      dw_printf ("Warning! The default is fine for nearly all situations.\n");
-	      dw_printf ("Warning! Be sure to read carefully and understand  Successful-APRS-Gateway-Operation.pdf .\n");
+	      dw_printf ("Warning! Be sure to read carefully and understand  \"Successful-APRS-Gateway-Operation.pdf\" .\n");
 	      dw_printf ("Warning! If you insist, be sure to add \" | i/180 \" so you don't break messaging.\n");
 	    }
 	    else {
@@ -2931,7 +2951,7 @@ void config_init (char *fname, struct audio_s *p_audio_config,
 	      dw_printf ("Warning! Don't mess with RF>IS filtering unless you are an expert and have an unusual situation.\n");
 	      dw_printf ("Warning! Expected behavior is for everything to go from RF to IS.\n");
 	      dw_printf ("Warning! The default is fine for nearly all situations.\n");
-	      dw_printf ("Warning! Be sure to read carefully and understand  Successful-APRS-Gateway-Operation.pdf .\n");
+	      dw_printf ("Warning! Be sure to read carefully and understand  \"Successful-APRS-Gateway-Operation.pdf\" .\n");
 	    }
 	    else {
 	      to_chan = isdigit(*t) ? atoi(t) : -999;
@@ -4567,6 +4587,13 @@ void config_init (char *fname, struct audio_s *p_audio_config,
 
 	    if (t != NULL && strlen(t) > 0) {
 	      p_igate_config->t2_filter = strdup (t);
+
+	      text_color_set(DW_COLOR_ERROR);
+	      dw_printf ("Line %d: Warning - IGFILTER is a rarely needed expert level feature.\n", line);
+	      dw_printf ("If you don't have a special situation and a good understanding of\n");
+	      dw_printf ("how this works, you probably should not be messing with it.\n");
+	      dw_printf ("The default behavior is appropriate for most situations.\n");
+	      dw_printf ("Please read \"Successful-APRS-IGate-Operation.pdf\".\n");
 	    }
 	  }
 

From d679e06846e4f163dbf1a020e151065e5243d4ba Mon Sep 17 00:00:00 2001
From: wb2osz <wb2osz@comcast.net>
Date: Thu, 21 Dec 2023 23:15:21 +0000
Subject: [PATCH 08/67] Warnings about using VOX rather than wired PTT.

---
 src/ptt.c | 22 +++++++++++++++++++++-
 1 file changed, 21 insertions(+), 1 deletion(-)

diff --git a/src/ptt.c b/src/ptt.c
index a75cb8de..af746626 100644
--- a/src/ptt.c
+++ b/src/ptt.c
@@ -1189,7 +1189,27 @@ void ptt_init (struct audio_s *audio_config_p)
 	  if (audio_config_p->chan_medium[ch] == MEDIUM_RADIO) {
 	    if(audio_config_p->achan[ch].octrl[OCTYPE_PTT].ptt_method == PTT_METHOD_NONE) {
 	      text_color_set(DW_COLOR_INFO);
-	      dw_printf ("Note: PTT not configured for channel %d. (Ignore this if using VOX.)\n", ch);
+	      dw_printf ("\n");
+	      dw_printf ("Note: PTT not configured for channel %d. (OK if using VOX.)\n", ch);
+	      dw_printf ("When using VOX, ensure that it adds very little delay (e.g. 10-20) milliseconds\n");
+	      dw_printf ("between the time that transmit audio ends and PTT is deactivated.\n");
+	      dw_printf ("For example, if using a SignaLink USB, turn the DLY control all the\n");
+	      dw_printf ("way counter clockwise.\n");
+	      dw_printf ("\n");
+	      dw_printf ("Using VOX built in to the radio is a VERY BAD idea.  This is intended\n");
+	      dw_printf ("for voice operation, with gaps in the sound, and typically has a delay of about a\n");
+	      dw_printf ("half second between the time the audio stops and the transmitter is turned off.\n");
+	      dw_printf ("When using APRS your transmiter will be sending a quiet carrier for\n");
+	      dw_printf ("about a half second after your packet ends.  This may interfere with the\n");
+	      dw_printf ("the next station to transmit.  This is being inconsiderate.\n");
+	      dw_printf ("\n");
+	      dw_printf ("If you are trying to use VOX with connected mode packet, expect\n");
+	      dw_printf ("frustration and disappointment.  Connected mode involves rapid responses\n");
+	      dw_printf ("which you will probably miss because your transmitter is still on when\n");
+	      dw_printf ("the response is being transmitted.\n");
+	      dw_printf ("\n");
+	      dw_printf ("Read the User Guide 'Transmit Timing' section for more details.\n");
+	      dw_printf ("\n");
 	    }
 	  }
 	}

From 46f31d4453ed133c99db71a514deacebae19f877 Mon Sep 17 00:00:00 2001
From: wb2osz <wb2osz@comcast.net>
Date: Sat, 23 Dec 2023 15:57:03 +0000
Subject: [PATCH 09/67] Use tocalls.yaml rather than tocalls.txt which is no
 longer maintained.

---
 CHANGES.md          |    6 +
 data/CMakeLists.txt |    8 +-
 data/tocalls.txt    |  326 ----------
 data/tocalls.yaml   | 1481 +++++++++++++++++++++++++++++++++++++++++++
 src/CMakeLists.txt  |    3 +
 src/decode_aprs.c   |  356 +----------
 src/deviceid.c      |  660 +++++++++++++++++++
 src/deviceid.h      |    6 +
 src/direwolf.c      |    2 +
 test/CMakeLists.txt |    4 +
 10 files changed, 2199 insertions(+), 653 deletions(-)
 delete mode 100644 data/tocalls.txt
 create mode 100644 data/tocalls.yaml
 create mode 100644 src/deviceid.c
 create mode 100644 src/deviceid.h

diff --git a/CHANGES.md b/CHANGES.md
index 4b78ca14..0903e9ea 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -2,6 +2,12 @@
 # Revision History #
 
 
+## Version 1.8  --  Development Version
+
+### New Features: ###
+
+- [http://www.aprs.org/aprs11/tocalls.txt](http://www.aprs.org/aprs11/tocalls.txt) has been abandoned since the end of 2021.  [https://github.com/aprsorg/aprs-deviceid](https://github.com/aprsorg/aprs-deviceid) is now considered to be the authoritative source of truth for the vendor/model encoding.
+
 ## Version 1.7  --  October 2023 ##
 
 
diff --git a/data/CMakeLists.txt b/data/CMakeLists.txt
index 7972cc23..11a82a43 100644
--- a/data/CMakeLists.txt
+++ b/data/CMakeLists.txt
@@ -16,7 +16,7 @@
 #
 # The destination field is often used to identify the manufacturer/model.
 # These are not hardcoded into Dire Wolf.  Instead they are read from
-# a file called tocalls.txt at application start up time.
+# a file called tocalls.yaml at application start up time.
 #
 # The original permanent symbols are built in but the "new" symbols,
 # using overlays, are often updated.  These are also read from files.
@@ -25,17 +25,17 @@
 
 include(ExternalProject)
 
-set(TOCALLS_TXT "tocalls.txt")
+set(TOCALLS_YAML "tocalls.yaml")
 set(SYMBOLS-NEW_TXT "symbols-new.txt")
 set(SYMBOLSX_TXT "symbolsX.txt")
 set(CUSTOM_BINARY_DATA_DIR "${CMAKE_BINARY_DIR}/data")
 
 # we can also move to a separate cmake file and use file(download)
 # see conf/install_conf.cmake as example
-file(COPY "${CUSTOM_DATA_DIR}/${TOCALLS_TXT}" DESTINATION "${CUSTOM_BINARY_DATA_DIR}")
+file(COPY "${CUSTOM_DATA_DIR}/${TOCALLS_YAML}" DESTINATION "${CUSTOM_BINARY_DATA_DIR}")
 file(COPY "${CUSTOM_DATA_DIR}/${SYMBOLS-NEW_TXT}" DESTINATION "${CUSTOM_BINARY_DATA_DIR}")
 file(COPY "${CUSTOM_DATA_DIR}/${SYMBOLSX_TXT}" DESTINATION "${CUSTOM_BINARY_DATA_DIR}")
 
-install(FILES "${CUSTOM_BINARY_DATA_DIR}/${TOCALLS_TXT}" DESTINATION ${INSTALL_DATA_DIR})
+install(FILES "${CUSTOM_BINARY_DATA_DIR}/${TOCALLS_YAML}" DESTINATION ${INSTALL_DATA_DIR})
 install(FILES "${CUSTOM_BINARY_DATA_DIR}/${SYMBOLS-NEW_TXT}" DESTINATION ${INSTALL_DATA_DIR})
 install(FILES "${CUSTOM_BINARY_DATA_DIR}/${SYMBOLSX_TXT}" DESTINATION ${INSTALL_DATA_DIR})
diff --git a/data/tocalls.txt b/data/tocalls.txt
deleted file mode 100644
index 169c9868..00000000
--- a/data/tocalls.txt
+++ /dev/null
@@ -1,326 +0,0 @@
-<title>
-APRS TO-CALL VERSION NUMBERS                              14 Dec 2021
----------------------------------------------------------------------
-                                                               WB4APR
-</title>
-<version_notes>
-07 Jun 23 Added APK005 for Kenwood TH-D75
-14 Dec 21 Added APATAR ATA-R APRS Digipeater by TA7W/OH2UDS and TA6AEU
-26 Sep 21 Added APRRDZ EPS32 https://github.com/dl9rdz/rdz_ttgo_sonde
-18 Sep 21 Added APCSS  for AMSAT Cubesat Simulator https://cubesatsim.org
-16 Sep 21 Added APY05D for Yaesu FT5D series
-04 Sep 21 APLOxx  LoRa KISS TNC/Tracker https://github.com/SQ9MDD/TTGO-T-Beam-LoRa-APRS
-24 Aug 21 Added APLSxx SARIMESH http://www.sarimesh.net  
-22 Aug 21 Added APE2Ax for VA3NNW's Email-2-APRS ap
-30 Jun 21 Added APCNxx for carNET by DG5OAW
-14 Jun 21 Added APN2xx for NOSaprs JNOS 2.0 - VE4KLM
-24 Apr 21 Added APMPAD for DF1JSL's WXBot clone and extension
-20 Apr 21 Added APLCxx for APRScube by DL3DCW
-19 Apr 21 Added APVMxx for DRCC-DVM Voice (Digital Radio China Club)     
-13 Apr 21 Added APIxxx for all Dstar ICOMS (APRS via DPRS)
-23 MAr 20 Added APW9xx For 9A9Y Weather Tracker
-16 Feb 21 Added API970 for I com 9700
-
-2020 Added APHBLx,APIZCI,APLGxx,APLTxx,APNVxx,APY300,APESPG,APESPW
-           APGDTx,APOSWx,APOSBx,APBT62,APCLUB,APMQxx
-2019 Added APTPNx,APJ8xx,APBSDx,APNKMX,APAT51,APMGxx,APTCMA,
-           APATxx,APQTHx,APLIGx 
-2018 added APRARX,APELKx,APGBLN,APBKxx,APERSx,APTCHE
-2017 Added APHWxx,APDVxx,APPICO,APBMxx,APP6xx,APTAxx,APOCSG,APCSMS,
-           APPMxx,APOFF,APDTMF,APRSON,APDIGI,APSAT,APTBxx,APIExx,
-           APSFxx
-2016 added APYSxx,APINxx,APNICx,APTKPT,APK004,APFPRS,APCDS0,APDNOx
-2015 Added APSTPO,APAND1,APDRxx,APZ247,APHTxx,APMTxx,APZMAJ
-           APB2MF,APR2MF,APAVT5
-
-</version_notes>
-<description>
-
-In APRS, the AX.25  Destination address is not used for packet 
-routing as is normally done in AX.25.  So APRS uses it for two 
-things.  The initial APxxxx is used as a group identifier to make 
-APRS packets instanantly recognizable on shared channels.  Most 
-applicaitons ignore all non APRS packets.  The remaining 4 xxxx
-bytes of the field are available to indicate the software version 
-number or application.  The following applications have requested 
-a TOCALL number series:
-
-Authors with similar alphabetic requirements are encouraged to share
-their address space with other software.  Work out agreements amongst
-yourselves and keep me informed.
-
-</description>
-<tocalls>
-
- APn  3rd digit is a number
-      AP1WWX  TAPR T-238+ WX station
-      AP1MAJ  Martyn M1MAJ DeLorme inReach Tracker
-      AP4Rxy  APRS4R software interface
-      APnnnD  Painter Engineering uSmartDigi D-Gate DSTAR Gateway
-      APnnnU  Painter Engineering uSmartDigi Digipeater
- APA  APAFxx  AFilter.
-      APAGxx  AGATE
-      APAGWx  SV2AGW's AGWtracker
-      APALxx  Alinco DR-620/635 internal TNC digis. "Hachi" ,JF1AJE
-      APAXxx  AFilterX.
-      APAHxx  AHub
-      APAND1  APRSdroid (pre-release) http://aprsdroid.org/
-      APAMxx  Altus Metrum GPS trackers
-      APATAR  ATA-R APRS Digipeater by TA7W/OH2UDS and TA6AEU
-      APAT8x  for Anytone. 81 for 878 HT 
-      APAT51  for Anytone AT-D578UV APRS mobile radio 
-      APAVT5  SainSonic AP510 which is a 1watt tracker
-      APAWxx  AGWPE 
- APB  APBxxx  Beacons or Rabbit TCPIP micros?
-      APB2MF  DL2MF - MF2APRS Radiosonde for balloons
-      APBLxx  BigRedBee BeeLine  
-      APBLO   MOdel Rocketry K7RKT
-      APBKxx  PY5BK Bravo Tracker in Brazil
-      APBPQx  John G8BPQ Digipeater/IGate
-      APBMxx  BrandMeister DMR Server for R3ABM
-      APBSDx  HamBSD https://hambsd.org/
-      APBT62  BTech DMR 6x2 
- APC  APCxxx  Cellular applications
-      APCBBx  VE7UDP Blackberry Applications
-      APCDS0  Leon Lessing ZS6LMG's cell tracker
-      APCLEY  EYTraker GPRS/GSM tracker by ZS6EY
-      APCLEZ  Telit EZ10 GSM application ZS6CEY
-      APCLUB  Brazil APRS network
-      APCLWX  EYWeather GPRS/GSM WX station by ZS6EY
-      APCNxx  for carNET by DG5OAW
-      APCSMS  for Cosmos (used for sending commands @USNA)
-      APCSS   for AMSAT cubesats https://cubesatsim.org
-      APCWP8  John GM7HHB, WinphoneAPRS
-      APCYxx  Cybiko applications
- APD  APD4xx  UP4DAR platform
-      APDDxx  DV-RPTR Modem and Control Center Software 
-      APDFxx  Automatic DF units
-      APDGxx  D-Star Gateways by G4KLX ircDDB
-      APDHxx  WinDV (DUTCH*Star DV Node for Windows)
-      APDInn  DIXPRS - Bela, HA5DI
-      APDIGI  Used by PSAT2 to indicate the digi is ON
-      APDIGI  digi ON for PSAT2 and QIKCOM-2
-      APDKxx  KI4LKF g2_ircddb Dstar gateway software
-      APDNOx  APRSduino by DO3SWW
-      APDOxx  ON8JL Standalone DStar Node
-      APDPRS  D-Star originated posits
-      APDRxx  APRSdroid Android App http://aprsdroid.org/
-      APDSXX  SP9UOB for dsDigi and ds-tracker
-      APDTxx  APRStouch Tone (DTMF)
-      APDTMF  digi off mode on QIKCOM2 and DTMF ON
-      APDUxx  U2APRS by JA7UDE
-      APDVxx  OE6PLD's SSTV with APRS status exchange
-      APDWxx  DireWolf, WB2OSZ
- APE  APExxx  Telemetry devices
-      APE2Ax  VA3NNW's Email-2-APRS ap
-      APECAN  Pecan Pico APRS Balloon Tracker
-      APELKx  WB8ELK balloons
-      APERXQ  Experimental tracker by PE1RXQ
-      APERSx  Runner tracking by Jason,KG7YKZ
-      APESPG  ESP SmartBeacon APRS-IS Client
-      APESPW  ESP Weather Station APRS-IS Client
- APF  APFxxx  Firenet
-      APFGxx  Flood Gage (KP4DJT)
-      APFIxx  for APRS.FI OH7LZB, Hessu
-      APFPRS  for FreeDV by Jeroen PE1RXQ
- APG  APGxxx  Gates, etc
-      APGOxx  for AA3NJ PDA application
-      APGBLN  for NW5W's GoBalloon
-      APGDTx  for VK4FAST's Graphic Data Terminal
- APH  APHKxx  for LA1BR tracker/digipeater
-      APHAXn  SM2APRS by PY2UEP
-      APHBLx  for DMR Gateway by Eric - KF7EEL
-      APHTxx  HMTracker by IU0AAC 
-      APHWxx  for use in "HamWAN
- API  API282  for ICOM IC-2820
-      API31   for ICOM ID-31
-      API410  for ICOM ID-4100
-      API51   for ICOM ID-51
-      API510  for ICOM ID-5100
-      API710  for ICOM IC-7100
-      API80   for ICOM IC-80
-      API880  for ICOM ID-880
-      API910  for ICOM IC-9100
-      API92   for ICOM IC-92      
-      API970  for ICOM 9700
-      APICQx  for ICQ
-      APICxx  HA9MCQ's Pic IGate
-      APIExx  W7KMV's PiAPRS system
-      APINxx  PinPoint by AB0WV
-      APIZCI  hymTR IZCI Tracker by TA7W/OH2UDS and TA6AEU
- APJ  APJ8xx  Jordan / KN4CRD JS8Call application
-      APJAxx  JavAPRS
-      APJExx  JeAPRS
-      APJIxx  jAPRSIgate
-      APJSxx  javAPRSSrvr
-      APJYnn  KA2DDO Yet another APRS system
- APK  APK0xx  Kenwood TH-D7's
-      APK003  Kenwood TH-D72
-      APK004  Kenwood TH-D74
-      APK005  Kenwood TH-D75
-      APK1xx  Kenwood D700's
-      APK102  Kenwood D710
-      APKRAM  KRAMstuff.com - Mark. G7LEU
- APL  APLCxx  APRScube by DL3DCW
-      APLGxx  LoRa Gateway/Digipeater OE5BPA
-      APLIGx  LightAPRS - TA2MUN and TA9OHC
-      APLOxx  LoRa KISS TNC/Tracker
-      APLQRU  Charlie - QRU Server
-      APLMxx  WA0TQG transceiver controller
-      APLSxx  SARIMESH  ( http://www.sarimesh.net ) 
-      APLTxx  LoRa Tracker - OE5BPA
- APM  APMxxx  MacAPRS, 
-      APMGxx  PiCrumbs and MiniGate - Alex, AB0TJ
-      APMIxx  SQ3PLX http://microsat.com.pl/
-      APMPAD  DF1JSL's WXBot clone and extension
-      APMQxx  Ham Radio of Things WB2OSZ
-      APMTxx  LZ1PPL for tracker
- APN  APNxxx  Network nodes, digis, etc
-      APN2xx  NOSaprs for JNOS 2.0 - VE4KLM
-      APN3xx  Kantronics KPC-3 rom versions
-      APN9xx  Kantronics KPC-9612 Roms
-      APNAxx  WB6ZSU's APRServe
-      APNDxx  DIGI_NED
-      APNICx  SQ5EKU http://sq5eku.blogspot.com/
-      APNK01  Kenwood D700 (APK101) type
-      APNK80  KAM version 8.0
-      APNKMP  KAM+
-      APNKMX  KAM-XL
-      APNMxx  MJF TNC roms
-      APNPxx  Paccom TNC roms
-      APNTxx  SV2AGW's TNT tnc as a digi
-      APNUxx  UIdigi
-      APNVxx  SQ8L's VP digi and Nodes
-      APNXxx  TNC-X  (K6DBG)
-      APNWxx  SQ3FYK.com WX/Digi and SQ3PLX http://microsat.com.pl/
- APO  APRSpoint
-      APOFF   Used by PSAT and PSAT2 to indicate the digi is OFF
-      APOLUx  for OSCAR satellites for AMSAT-LU by LU9DO
-      APOAxx  OpenAPRS - Greg Carter
-      APOCSG  For N0AGI's APRS to POCSAG project
-      APOD1w  Open Track with 1 wire WX
-      APOSBx  openSPOT3 by HA2NON at sharkrf.com
-      APOSWx  openSPOT2
-      APOTxx  Open Track
-      APOU2k  Open Track for Ultimeter
-      APOZxx  www.KissOZ.dk Tracker. OZ1EKD and OZ7HVO
- APP  APP6xx  for APRSlib
-      APPICx  DB1NTO' PicoAPRS 
-      APPMxx  DL1MX's RTL-SDR pytohon Igate
-      APPTxx  KetaiTracker by  JF6LZE, Takeki (msg capable)
- APQ  APQxxx  Earthquake data
-      APQTHx  W8WJB's QTH.app
- APR  APR8xx  APRSdos versions 800+
-      APR2MF  DL2MF - MF2APRS Radiosonde WX reporting
-      APRARX  VK5QI's radiosonde tracking
-      APRDxx  APRSdata, APRSdr
-      APRGxx  aprsg igate software, OH2GVE
-      APRHH2  HamHud 2
-      APRKxx  APRStk
-      APRNOW  W5GGW ipad application
-      APRRTx  RPC electronics
-      APRS    Generic, (obsolete. Digis should use APNxxx instead)
-      APRSON  Used by PSAT to indicate the DIGI is ON
-      APRXxx  >40 APRSmax
-      APRXxx  <39 for OH2MQK's igate
-      APRTLM  used in MIM's and Mic-lites, etc
-      APRtfc  APRStraffic
-      APRSTx  APRStt (Touch tone)
- APS  APSxxx  APRS+SA, etc
-      APSARx  ZL4FOX's SARTRACK
-      APSAT   digi ON for QIKCOM-1
-      APSCxx  aprsc APRS-IS core server (OH7LZB, OH2MQK)
-      APSFxx  F5OPV embedded devices - was APZ40
-      APSK63  APRS Messenger -over-PSK63
-      APSK25  APRS Messenger GMSK-250
-      APSMSx  Paul Dufresne's SMSGTE - SMS Gateway
-      APSTMx  for W7QO's Balloon trackers
-      APSTPO  for N0AGI Satellite Tracking and Operations
- APT  APT2xx  Tiny Track II
-      APT3xx  Tiny Track III
-      APTAxx  K4ATM's tiny track
-      APTBxx  TinyAPRS by BG5HHP Was APTAxx till Sep 2017
-      APTCHE  PU3IKE in Brazil TcheTracker/Tcheduino
-      APTCMA  CAPI tracker - PU1CMA Brazil
-      APTIGR  TigerTrack
-      APTKPT  TrackPoint N0LP
-      APTPNx  TARPN Packet Node Tracker by KN4ORB http://tarpn.net/ 
-      APTTxx  Tiny Track
-      APTWxx  Byons WXTrac
-      APTVxx  for ATV/APRN and SSTV applications
- APU  APU1xx  UIview 16 bit applications
-      APU2xx  UIview 32 bit apps
-      APU3xx  UIview terminal program
-      APUDRx  NW Digital Radio's UDR (APRS/Dstar)
- APV  APVxxx  Voice over Internet  applications
-      APVMxx  DRCC-DVM Digital Voice (Digital Radio China Club)
-      APVRxx  for IRLP
-      APVLxx  for I-LINK
-      APVExx  for ECHO link
- APW  APWxxx  WinAPRS, etc
-      APW9xx  9A9Y Weather Tracker
-      APWAxx  APRSISCE Android version
-      APWSxx  DF4IAN's WS2300 WX station
-      APWMxx  APRSISCE KJ4ERJ
-      APWWxx  APRSISCE win32 version
- APX  APXnnn  Xastir
-      APXRnn  Xrouter
- APY  APYxxx  Yaesu Radios
-      APY008  Yaesu VX-8 series
-      APY01D  Yaesu FT1D series
-      APY02D  Yaesu FT2D series
-      APY03D  Yaesu FT3D series
-      APY05D  Yaesu FT5D series
-      APY100  Yaesu FTM-100D series
-      APY300  Yaesu FTM-300D series
-      APY350  Yaesu FTM-350  series
-      APY400  Yaesu FTM-400D series
- APZ  APZxxx  Experimental
-      APZ200  old versions of JNOS 
-      APZ247  for UPRS NR0Q
-      APZ0xx  Xastir (old versions. See APX)
-      APZMAJ  Martyn M1MAJ DeLorme inReach Tracker
-      APZMDM  github/codec2_talkie  - product code not registered
-      APZMDR  for HaMDR trackers - hessu * hes.iki.fi]
-      APZPAD  Smart Palm 
-      APZTKP  TrackPoint, Nick N0LP (Balloon tracking)(depricated)
-      APZWIT  MAP27 radio (Mountain Rescue) EI7IG
-      APZWKR  GM1WKR NetSked application
-</tocalls>
-<notes>
-
-</notes>
-<altnets>
-
-REGISTERED TOCALL ALTNETS:
---------------------------
-
-ALTNETS are uses of the AX-25 tocall to distinguish specialized
-traffic that may be flowing on the APRS-IS, but that are not intended
-to be part of normal APRS distribution to all normal APRS software
-operating in normal (default) modes.  Proper APRS software that
-honors this design are supposed to IGNORE all ALTNETS unless the 
-particular operator has selected an ALTNET to monitor for.    
-
-An example is when testing; an author may want to transmit objects 
-all over his map for on-air testing, but does not want these to
-clutter everyone's maps or databases.  He could use the ALTNET of
-"TEST" and client APRS software that respects the ALTNET concept
-should ignore these packets.
-
-An ALTNET is defined to be ANY AX.25 TOCALL that is NOT one of the
-normal APRS TOCALL's.  The normal TOCALL's that APRS is supposed to
-process are:  ALL, BEACON, CQ, QST, GPSxxx and of course APxxxx.
-
-The following is a list of ALTNETS that may be of interest to other
-users.  This list is by no means complete, since ANY combination of
-characters other than APxxxx are considered an ALTNET.  But this list 
-can give consisntecy to ALTNETS that may be using the global APRS-IS 
-and need some special recognition.  Here are some ideas:
-</altnets>
-<altnet_list>
-
-  SATERN - Salvation Army Altnet
-  AFMARS - Airforce Mars
-  AMARS  - Army Mars
-</altnet_list>
\ No newline at end of file
diff --git a/data/tocalls.yaml b/data/tocalls.yaml
new file mode 100644
index 00000000..adf26a1f
--- /dev/null
+++ b/data/tocalls.yaml
@@ -0,0 +1,1481 @@
+#
+# This is a machine-readable index of APRS device and software
+# identification strings.  For easy manual editing and validation, the
+# master file is in YAML format.  A conversion tool and pre-converted
+# versions in XML and JSON are also provided for environments where those
+# are more convenient to parse.
+#
+# This list is maintained by Hessu, OH7LZB, for the aprs.fi service.
+# It is licensed under the CC BY-SA 2.0 license, so you're free to use
+# it in any of your applications. For free. Just mention the source
+# somewhere in the small print.
+# http://creativecommons.org/licenses/by-sa/2.0/
+#
+
+---
+
+#
+# English shown names and descriptions for device classes
+#
+classes:
+ - class: wx
+   shown: Weather station
+   description: Dedicated weather station
+   
+ - class: tracker
+   shown: Tracker
+   description: Tracker device
+ 
+ - class: rig
+   shown: Rig
+   description: Mobile or desktop radio
+ 
+ - class: ht
+   shown: HT
+   description: Hand-held radio
+
+ - class: app
+   shown: Mobile app
+   description: Mobile phone or tablet app
+ 
+ - class: software
+   shown: Software
+   description: Desktop software
+
+ - class: digi
+   shown: Digipeater
+   description: Digipeater software
+
+ - class: igate
+   shown: iGate
+   description: iGate software
+ 
+ - class: dstar
+   shown: D-Star
+   description: D-Star radio
+ 
+ - class: satellite
+   shown: Satellite
+   description: Satellite-based station
+
+ - class: service
+   shown: Service
+   description: Software running as a web service
+
+#
+# mic-e device identifier index for new-style 2-character device
+# suffixes. The first prefix byte indicates messaging capability.
+#
+mice:
+ - suffix: "_ "
+   vendor: Yaesu
+   model: VX-8
+   class: ht
+   
+ - suffix: "_\""
+   vendor: Yaesu
+   model: FTM-350
+   class: rig
+   
+ - suffix: "_#"
+   vendor: Yaesu
+   model: VX-8G
+   class: ht
+   
+ - suffix: "_$"
+   vendor: Yaesu
+   model: FT1D
+   class: ht
+
+ - suffix: "_("
+   vendor: Yaesu
+   model: FT2D
+   class: ht
+
+ - suffix: "_0"
+   vendor: Yaesu
+   model: FT3D
+   class: ht
+   
+ - suffix: "_3"
+   vendor: Yaesu
+   model: FT5D
+   class: ht
+
+ - suffix: "_1"
+   vendor: Yaesu
+   model: FTM-300D
+   class: rig
+
+ - suffix: "_)"
+   vendor: Yaesu
+   model: FTM-100D
+   class: rig 
+   
+ - suffix: "_%"
+   vendor: Yaesu
+   model: FTM-400DR
+   class: rig
+
+ - suffix: "(5"
+   vendor: Anytone
+   model: D578UV
+   class: ht
+
+ - suffix: "(8"
+   vendor: Anytone
+   model: D878UV
+   class: ht
+
+ - suffix: "|3"
+   vendor: Byonics
+   model: TinyTrak3
+   class: tracker
+ 
+ - suffix: "|4"
+   vendor: Byonics
+   model: TinyTrak4
+   class: tracker
+ 
+ - suffix: "^v"
+   vendor: HinzTec
+   model: anyfrog
+ 
+ - suffix: "*v"
+   vendor: KissOZ
+   model: Tracker
+   class: tracker
+
+#
+# mic-e legacy devices, with an unique comment suffix and prefix
+#
+
+micelegacy:
+ - prefix: ">"
+   vendor: Kenwood
+   model: TH-D7A
+   class: ht
+   features:
+     - messaging
+
+ - prefix: ">"
+   suffix: "="
+   vendor: Kenwood
+   model: TH-D72
+   class: ht
+   features:
+     - messaging
+
+ - prefix: ">"
+   suffix: "^"
+   vendor: Kenwood
+   model: TH-D74
+   class: ht
+   features:
+     - messaging
+
+ - prefix: ">"
+   suffix: "&"
+   vendor: Kenwood
+   model: TH-D75
+   class: ht
+   features:
+     - messaging
+
+ - prefix: "]"
+   vendor: Kenwood
+   model: TM-D700
+   class: rig
+   features:
+     - messaging
+
+ - prefix: "]"
+   suffix: "="
+   vendor: Kenwood
+   model: TM-D710
+   class: rig
+   features:
+     - messaging
+
+#
+# TOCALL index
+#
+tocalls:
+ - tocall: AP1WWX
+   vendor: TAPR
+   model: T-238+
+   class: wx
+
+ - tocall: AP4R??
+   vendor: Open Source
+   model: APRS4R
+   class: software
+
+ - tocall: APAEP1
+   vendor: Paraguay Space Agency (AEP)
+   model: "EIRUAPRSDIGIS&FV1"
+   class: satellite
+
+ - tocall: APAF??
+   model: AFilter
+
+ - tocall: APAG??
+   model: AGate
+
+ - tocall: APAGW
+   vendor: SV2AGW
+   model: AGWtracker
+   class: software
+   os: Windows
+
+ - tocall: APAGW?
+   vendor: SV2AGW
+   model: AGWtracker
+   class: software
+   os: Windows
+
+ - tocall: APAH??
+   model: AHub
+
+ - tocall: APAM??
+   vendor: Altus Metrum
+   model: AltOS
+   class: tracker
+
+ - tocall: APAND?
+   vendor: Open Source
+   model: APRSdroid
+   os: Android
+   class: app
+
+ - tocall: APAT51
+   vendor: Anytone
+   model: AT-D578
+   class: rig
+
+ - tocall: APAT81
+   vendor: Anytone
+   model: AT-D878
+   class: ht
+
+ - tocall: APAT??
+   vendor: Anytone
+
+ - tocall: APATAR
+   vendor: TA7W/OH2UDS Baris Dinc and TA6AEU
+   model: ATA-R APRS Digipeater
+   class: digi
+
+ - tocall: APAVT5
+   vendor: SainSonic
+   model: AP510
+   class: tracker
+
+ - tocall: APAW??
+   vendor: SV2AGW
+   model: AGWPE
+   class: software
+   os: Windows
+
+ - tocall: APAX??
+   model: AFilterX
+
+ - tocall: APB2MF
+   vendor: Mike, DL2MF
+   model: MF2APRS Radiosonde tracking tool
+   class: software
+   os: Windows
+
+ - tocall: APBK??
+   vendor: PY5BK
+   model: Bravo Tracker
+   class: tracker
+
+ - tocall: APBL??
+   vendor: BigRedBee
+   model: BeeLine GPS
+   class: tracker
+
+ - tocall: APBM??
+   vendor: R3ABM
+   model: BrandMeister DMR
+
+ - tocall: APBPQ?
+   vendor: John Wiseman, G8BPQ
+   model: BPQ32
+   class: software
+   os: Windows
+
+ - tocall: APBSD?
+   vendor: hambsd.org
+   model: HamBSD
+
+ - tocall: APBT62
+   vendor: BTech
+   model: DMR 6x2
+
+ - tocall: APC???
+   vendor: Rob Wittner, KZ5RW
+   model: APRS/CE
+   class: app
+
+ - tocall: APCDS0
+   vendor: ZS6LMG
+   model: cell tracker
+   class: tracker
+
+ - tocall: APCLEY
+   vendor: ZS6EY
+   model: EYTraker
+   class: tracker
+
+ - tocall: APCLEZ
+   vendor: ZS6EY
+   model: Telit EZ10 GSM application
+   class: tracker
+
+ - tocall: APCLUB
+   model: Brazil APRS network
+
+ - tocall: APCLWX
+   vendor: ZS6EY
+   model: EYWeather
+   class: wx
+
+ - tocall: APCN??
+   vendor: DG5OAW
+   model: carNET
+
+ - tocall: APCSMS
+   vendor: USNA
+   model: Cosmos
+
+ - tocall: APCSS
+   vendor: AMSAT
+   model: CubeSatSim CubeSat Simulator
+
+ - tocall: APCTLK
+   vendor: Open Source
+   model: Codec2Talkie
+   class: app
+
+ - tocall: APCWP8
+   vendor: GM7HHB
+   model: WinphoneAPRS
+   class: app
+
+ - tocall: APDF??
+   model: Automatic DF units
+
+ - tocall: APDG??
+   vendor: Jonathan, G4KLX
+   model: ircDDB Gateway
+   class: dstar
+
+ - tocall: APDI??
+   vendor: Bela, HA5DI
+   model: DIXPRS
+   class: software
+
+ - tocall: APDNO?
+   vendor: DO3SWW
+   model: APRSduino
+   class: tracker
+   os: embedded
+
+ - tocall: APDPRS
+   vendor: unknown
+   model: D-Star APDPRS
+   class: dstar
+
+ - tocall: APDR??
+   vendor: Open Source
+   model: APRSdroid
+   os: Android
+   class: app
+
+ - tocall: APDS??
+   vendor: SP9UOB
+   model: dsDIGI
+   os: embedded
+
+ - tocall: APDST?
+   vendor: SP9UOB
+   model: dsTracker
+   os: embedded
+
+ - tocall: APDT??
+   vendor: unknown
+   model: APRStouch Tone (DTMF)
+
+ - tocall: APDU??
+   vendor: JA7UDE
+   model: U2APRS
+   class: app
+   os: Android
+
+ - tocall: APDV??
+   vendor: OE6PLD
+   model: SSTV with APRS
+   class: software
+
+ - tocall: APDW??
+   vendor: WB2OSZ
+   model: DireWolf
+
+ - tocall: APDnnn
+   vendor: Open Source
+   model: aprsd
+   class: software
+   os: Linux/Unix
+
+ - tocall: APE2A?
+   vendor: NoseyNick, VA3NNW
+   model: Email-2-APRS gateway
+   class: software
+   os: Linux/Unix
+
+ - tocall: APE???
+   model: Telemetry devices
+
+ - tocall: APECAN
+   vendor: KT5TK/DL7AD
+   model: Pecan Pico APRS Balloon Tracker
+   class: tracker
+
+ - tocall: APELK?
+   vendor: WB8ELK
+   model: Balloon tracker
+   class: tracker
+
+ - tocall: APERS?
+   vendor: Jason, KG7YKZ
+   model: Runner tracking
+   class: tracker
+
+ - tocall: APERXQ
+   vendor: PE1RXQ
+   model: PE1RXQ APRS Tracker
+   class: tracker
+
+ - tocall: APESP?
+   vendor: LY3PH
+   model: APRS-ESP
+   os: embedded
+ 
+ - tocall: APFG??
+   vendor: KP4DJT
+   model: Flood Gage
+   class: software
+
+ - tocall: APFI??
+   vendor: aprs.fi
+   class: app
+
+ - tocall: APFII?
+   model: iPhone/iPad app
+   vendor: aprs.fi
+   os: ios
+   class: app
+
+ - tocall: APGBLN
+   vendor: NW5W
+   model: GoBalloon
+   class: tracker
+
+ - tocall: APGO??
+   vendor: AA3NJ
+   model: APRS-Go
+   class: app
+
+ - tocall: APHAX?
+   vendor: PY2UEP
+   model: SM2APRS SondeMonitor
+   class: software
+   os: Windows
+
+ - tocall: APHBL?
+   vendor: KF7EEL
+   model: HBLink D-APRS Gateway
+   class: software
+
+ - tocall: APHH?
+   vendor: Steven D. Bragg, KA9MVA
+   model: HamHud
+   class: tracker
+
+ - tocall: APHK??
+   vendor: LA1BR
+   model: Digipeater/tracker
+
+ - tocall: APHMEY
+   vendor: Tapio Heiskanen, OH2TH
+   model: APRS-IS Client for Athom Homey
+   contact: oh2th@iki.fi
+
+ - tocall: APHPIA
+   vendor: HP3ICC
+   model: Arduino APRS
+
+ - tocall: APHPIB
+   vendor: HP3ICC
+   model: Python APRS Beacon
+
+ - tocall: APHPIW
+   vendor: HP3ICC
+   model: Python APRS WX
+
+ - tocall: APHT??
+   vendor: IU0AAC
+   model: HMTracker
+   class: tracker
+
+ - tocall: APHW??
+   vendor: HamWAN
+
+ - tocall: API282
+   vendor: Icom
+   model: IC-2820
+   class: dstar
+
+ - tocall: API31
+   vendor: Icom
+   model: IC-31
+   class: dstar
+
+ - tocall: API410
+   vendor: Icom
+   model: IC-4100
+   class: dstar
+
+ - tocall: API51
+   vendor: Icom
+   model: IC-51
+   class: dstar
+
+ - tocall: API510
+   vendor: Icom
+   model: IC-5100
+   class: dstar
+
+ - tocall: API710
+   vendor: Icom
+   model: IC-7100
+   class: dstar
+
+ - tocall: API80
+   vendor: Icom
+   model: IC-80
+   class: dstar
+
+ - tocall: API880
+   vendor: Icom
+   model: IC-880
+   class: dstar
+
+ - tocall: API910
+   vendor: Icom
+   model: IC-9100
+   class: dstar
+
+ - tocall: API92
+   vendor: Icom
+   model: IC-92
+   class: dstar
+
+ - tocall: API970
+   vendor: Icom
+   model: IC-9700
+   class: dstar
+
+ - tocall: API???
+   vendor: Icom
+   model: unknown
+   class: dstar
+
+ - tocall: APIC??
+   vendor: HA9MCQ
+   model: PICiGATE
+
+ - tocall: APIE??
+   vendor: W7KMV
+   model: PiAPRS
+
+ - tocall: APIN??
+   vendor: AB0WV
+   model: PinPoint
+
+ - tocall: APIZCI
+   vendor: TA7W/OH2UDS and TA6AEU
+   model: hymTR IZCI Tracker
+   class: tracker
+   os: embedded
+
+ - tocall: APJ8??
+   vendor: KN4CRD
+   model: JS8Call
+   class: software
+
+ - tocall: APJA??
+   vendor: K4HG & AE5PL
+   model: JavAPRS
+
+ - tocall: APJE??
+   vendor: Gregg Wonderly, W5GGW
+   model: JeAPRS
+
+ - tocall: APJI??
+   vendor: Peter Loveall, AE5PL
+   model: jAPRSIgate
+   class: software
+
+ - tocall: APJID2
+   vendor: Peter Loveall, AE5PL
+   model: D-Star APJID2
+   class: dstar
+
+ - tocall: APJS??
+   vendor: Peter Loveall, AE5PL
+   model: javAPRSSrvr
+
+ - tocall: APJY??
+   vendor: KA2DDO
+   model: YAAC
+   class: software
+
+ - tocall: APK003
+   vendor: Kenwood
+   model: TH-D72
+   class: ht
+
+ - tocall: APK004
+   vendor: Kenwood
+   model: TH-D74
+   class: ht
+
+ - tocall: APK005
+   vendor: Kenwood
+   model: TH-D75
+   class: ht
+
+ - tocall: APK0??
+   vendor: Kenwood
+   model: TH-D7
+   class: ht
+
+ - tocall: APK1??
+   vendor: Kenwood
+   model: TM-D700
+   class: rig
+
+ - tocall: APKHTW
+   vendor: Kip, W3SN
+   model: Tempest Weather Bridge
+   class: wx
+   os: embedded
+   contact: w3sn@moxracing.33mail.com
+
+ - tocall: APKRAM
+   vendor: kramstuff.com
+   model: Ham Tracker
+   class: app
+   os: ios
+
+ - tocall: APLC??
+   vendor: DL3DCW
+   model: APRScube
+
+ - tocall: APLDI?
+   vendor: David, OK2DDS
+   model: LoRa IGate/Digipeater
+   class: digi
+
+ - tocall: APLDM?
+   vendor: David, OK2DDS
+   model: LoRa Meteostation
+   class: wx
+
+ - tocall: APLETK
+   vendor: DL5TKL
+   model: T-Echo
+   class: tracker
+   os: embedded
+   contact: cfr34k-git@tkolb.de
+
+ - tocall: APLG??
+   vendor: OE5BPA
+   model: LoRa Gateway/Digipeater
+   class: digi
+
+ - tocall: APLIG?
+   vendor: TA2MUN/TA9OHC
+   model: LightAPRS Tracker
+   class: tracker
+
+ - tocall: APLM??
+   vendor: WA0TQG
+   class: software
+
+ - tocall: APLO??
+   vendor: SQ9MDD
+   model: LoRa KISS TNC/Tracker
+   class: tracker
+
+ - tocall: APLP0?
+   vendor: SQ9P
+   model: fajne digi
+   class: digi
+   os: embedded
+   contact: sq9p.peter@gmail.com
+
+ - tocall: APLP1?
+   vendor: SQ9P
+   model: LORA/FSK/AFSK fajny tracker
+   class: tracker
+   os: embedded
+   contact: sq9p.peter@gmail.com
+
+ - tocall: APLRG?
+   vendor: Ricardo, CD2RXU
+   model: ESP32 LoRa iGate
+   class: igate
+   os: embedded
+   contact: richonguzman@gmail.com
+
+ - tocall: APLRT?
+   vendor: Ricardo, CD2RXU
+   model: ESP32 LoRa Tracker
+   class: tracker
+   os: embedded
+   contact: richonguzman@gmail.com
+
+ - tocall: APLS??
+   vendor: SARIMESH
+   model: SARIMESH
+   class: software
+
+ - tocall: APLT??
+   vendor: OE5BPA
+   model: LoRa Tracker
+   class: tracker
+
+ - tocall: APLU0?
+   vendor: SP9UP
+   model: ESP32/SX12xx LoRa iGate / Digi
+   class: digi
+   os: embedded
+   contact: wajdzik.m@gmail.com
+
+ - tocall: APLU1?
+   vendor: SP9UP
+   model: ESP32/SX12xx LoRa Tracker
+   class: tracker
+   os: embedded
+   contact: wajdzik.m@gmail.com
+
+ - tocall: APMG??
+   vendor: Alex, AB0TJ
+   model: PiCrumbs and MiniGate
+   class: software
+
+ - tocall: APMI01
+   vendor: Microsat
+   os: embedded
+   model: WX3in1
+
+ - tocall: APMI02
+   vendor: Microsat
+   os: embedded
+   model: WXEth
+
+ - tocall: APMI03
+   vendor: Microsat
+   os: embedded
+   model: PLXDigi
+
+ - tocall: APMI04
+   vendor: Microsat
+   os: embedded
+   model: WX3in1 Mini
+
+ - tocall: APMI05
+   vendor: Microsat
+   os: embedded
+   model: PLXTracker
+
+ - tocall: APMI06
+   vendor: Microsat
+   os: embedded
+   model: WX3in1 Plus 2.0
+
+ - tocall: APMI??
+   vendor: Microsat
+   os: embedded
+
+ - tocall: APMON?
+   vendor: Amon Schumann, DL9AS
+   model: APRS Balloon Tracker
+   class: tracker
+   os: embedded
+
+ - tocall: APMPAD
+   vendor: DF1JSL
+   model: Multi-Purpose APRS Daemon
+   class: service
+   contact: joerg.schultze.lutter@gmail.com
+   features:
+     - messaging
+
+ - tocall: APMQ??
+   vendor: WB2OSZ
+   model: Ham Radio of Things
+ 
+ - tocall: APMT??
+   vendor: LZ1PPL
+   model: Micro APRS Tracker
+   class: tracker
+
+ - tocall: APN102
+   vendor: Gregg Wonderly, W5GGW
+   model: APRSNow
+   class: app
+   os: ipad
+
+ - tocall: APN2??
+   vendor: VE4KLM
+   model: NOSaprs for JNOS 2.0
+
+ - tocall: APN3??
+   vendor: Kantronics
+   model: KPC-3
+
+ - tocall: APN9??
+   vendor: Kantronics
+   model: KPC-9612
+
+ - tocall: APNCM
+   vendor: Keith Kaiser, WA0TJT
+   model: Net Control Manager
+   class: software
+   os: browser
+   contact: wa0tjt@gmail.com
+
+ - tocall: APND??
+   vendor: PE1MEW
+   model: DIGI_NED
+
+ - tocall: APNIC4
+   vendor: SQ5EKU
+   model: BidaTrak
+   class: tracker
+   os: embedded
+
+ - tocall: APNJS?
+   vendor: Julien Sansonnens, HB9HRD
+   model: Web messaging service
+   class: service
+   contact: julien.owls@gmail.com
+   features:
+     - messaging
+
+ - tocall: APNK01
+   vendor: Kenwood
+   model: TM-D700
+   class: rig
+   features:
+     - messaging
+
+ - tocall: APNK80
+   vendor: Kantronics
+   model: KAM
+
+ - tocall: APNKMP
+   vendor: Kantronics
+   model: KAM+
+
+ - tocall: APNKMX
+   vendor: Kantronics
+   model: KAM-XL
+
+ - tocall: APNM??
+   vendor: MFJ
+   model: TNC
+
+ - tocall: APNP??
+   vendor: PacComm
+   model: TNC
+
+ - tocall: APNT??
+   vendor: SV2AGW
+   model: TNT TNC as a digipeater
+   class: digi
+
+ - tocall: APNU??
+   vendor: IW3FQG
+   model: UIdigi
+   class: digi
+
+ - tocall: APNV0?
+   vendor: SQ8L
+   model: VP-Digi
+   os: embedded
+
+ - tocall: APNV1?
+   vendor: SQ8L
+   model: VP-Node
+   os: embedded
+
+ - tocall: APNV??
+   vendor: SQ8L
+
+ - tocall: APNW??
+   vendor: SQ3FYK
+   model: WX3in1
+   os: embedded
+
+ - tocall: APNX??
+   vendor: K6DBG
+   model: TNC-X
+
+ - tocall: APOA??
+   vendor: OpenAPRS
+   model: app
+   class: app
+   os: ios
+
+ - tocall: APOCSG
+   vendor: N0AGI
+   model: POCSAG
+
+ - tocall: APOG7?
+   vendor: OpenGD77
+   model: OpenGD77
+   os: embedded
+   contact: Roger VK3KYY/G4KYF
+
+ - tocall: APOLU?
+   vendor: AMSAT-LU
+   model: Oscar
+   class: satellite
+
+ - tocall: APOSAT
+   vendor: Mike, NA7Q
+   model: Open Source Satellite Gateway
+   class: service
+   contact: mike.ph4@gmail.com
+
+ - tocall: APOSMS
+   vendor: Mike, NA7Q
+   model: Open Source SMS Gateway
+   class: service
+   contact: mike.ph4@gmail.com
+   features:
+     - messaging
+
+ - tocall: APOT??
+   vendor: Argent Data Systems
+   model: OpenTracker
+   class: tracker
+
+ - tocall: APOVU?
+   vendor: K J Somaiya Institute
+   model: BeliefSat
+
+ - tocall: APOZ??
+   vendor: OZ1EKD, OZ7HVO
+   model: KissOZ
+   class: tracker
+
+ - tocall: APP6??
+   model: APRSlib
+
+ - tocall: APPCO?
+   vendor: RadCommSoft, LLC
+   model: PicoAPRSTracker
+   class: tracker
+   os: embedded
+   contact: ab4mw@radcommsoft.com
+
+ - tocall: APPIC?
+   vendor: DB1NTO
+   model: PicoAPRS
+   class: tracker
+
+ - tocall: APPM??
+   vendor: DL1MX
+   model: rtl-sdr Python iGate
+   class: software
+
+ - tocall: APPRIS
+   vendor: DF1JSL
+   model: Apprise APRS plugin
+   class: service
+   contact: joerg.schultze.lutter@gmail.com
+   features:
+     - messaging
+
+ - tocall: APPT??
+   vendor: JF6LZE
+   model: KetaiTracker
+   class: tracker
+
+ - tocall: APQTH?
+   vendor: Weston Bustraan, W8WJB
+   model: QTH.app
+   class: software
+   os: macOS
+   features:
+     - messaging
+
+ - tocall: APR2MF
+   vendor: Mike, DL2MF
+   model: MF2wxAPRS Tinkerforge gateway
+   class: wx
+   os: Windows
+
+ - tocall: APR8??
+   vendor: Bob Bruninga, WB4APR
+   model: APRSdos
+   class: software
+
+ - tocall: APRARX
+   vendor: Open Source
+   model: radiosonde_auto_rx
+   class: software
+   os: Linux/Unix
+
+ - tocall: APRFG?
+   vendor: RF.Guru
+   contact: info@rf.guru
+
+ - tocall: APRFGB
+   vendor: RF.Guru
+   model: APRS LoRa Pager
+   os: embedded
+   contact: info@rf.guru
+
+ - tocall: APRFGD
+   vendor: RF.Guru
+   model: APRS Digipeater
+   class: digi
+   os: embedded
+   contact: info@rf.guru
+
+ - tocall: APRFGH
+   vendor: RF.Guru
+   model: Hotspot
+   class: rig
+   os: embedded
+   contact: info@rf.guru
+
+ - tocall: APRFGI
+   vendor: RF.Guru
+   model: LoRa APRS iGate
+   class: igate
+   os: embedded
+   contact: info@rf.guru
+
+ - tocall: APRFGL
+   vendor: RF.Guru
+   model: Lora APRS Digipeater
+   class: digi
+   os: embedded
+   contact: info@rf.guru
+
+ - tocall: APRFGM
+   vendor: RF.Guru
+   model: Mobile Radio
+   class: rig
+   os: embedded
+   contact: info@rf.guru
+
+ - tocall: APRFGP
+   vendor: RF.Guru
+   model: Portable Radio
+   class: ht
+   os: embedded
+   contact: info@rf.guru
+
+ - tocall: APRFGR
+   vendor: RF.Guru
+   model: Repeater
+   class: rig
+   os: embedded
+   contact: info@rf.guru
+
+ - tocall: APRFGT
+   vendor: RF.Guru
+   model: LoRa APRS Tracker
+   class: tracker
+   os: embedded
+   contact: info@rf.guru
+
+ - tocall: APRFGW
+   vendor: RF.Guru
+   model: LoRa APRS Weather Station
+   class: wx
+   os: embedded
+   contact: info@rf.guru
+
+ - tocall: APRG??
+   vendor: OH2GVE
+   model: aprsg
+   class: software
+   os: Linux/Unix
+
+ - tocall: APRHH?
+   vendor: Steven D. Bragg, KA9MVA
+   model: HamHud
+   class: tracker
+
+ - tocall: APRNOW
+   vendor: Gregg Wonderly, W5GGW
+   model: APRSNow
+   class: app
+   os: ipad
+
+ - tocall: APRPR?
+   vendor: Robert DM4RW, Peter DL6MAA
+   model: Teensy RPR TNC
+   class: tracker
+   os: embedded
+   contact: dm4rw@skywaves.de
+
+ - tocall: APRRDZ
+   model: rdzTTGOsonde
+   vendor: DL9RDZ
+   class: tracker
+
+ - tocall: APRRF?
+   vendor: Jean-Francois Huet F1EVM
+   model: Tracker for RRF
+   class: tracker
+   os: embedded
+   contact: f1evm@f1evm.fr
+   features:
+     - messaging
+
+ - tocall: APRRT?
+   vendor: RPC Electronics
+   model: RTrak
+   class: tracker
+
+ - tocall: APRS
+   vendor: Unknown
+   model: Unknown
+
+ - tocall: APRX??
+   vendor: Kenneth W. Finnegan, W6KWF
+   model: Aprx
+   class: igate
+   os: Linux/Unix
+
+ - tocall: APS???
+   vendor: Brent Hildebrand, KH2Z
+   model: APRS+SA
+   class: software
+
+ - tocall: APSAR
+   vendor: ZL4FOX
+   model: SARTrack
+   class: software
+   os: Windows
+   
+ - tocall: APSC??
+   vendor: OH2MQK, OH7LZB
+   model: aprsc
+   class: software
+
+ - tocall: APSF??
+   vendor: F5OPV, SFCP_LABS
+   model: embedded APRS devices
+   os: embedded
+
+ - tocall: APSFLG
+   vendor: F5OPV, SFCP_LABS
+   model: LoRa/APRS Gateway
+   class: digi
+   os: embedded
+
+ - tocall: APSFRP
+   vendor: F5OPV, SFCP_LABS
+   model: VHF/UHF Repeater
+   os: embedded
+
+ - tocall: APSFTL
+   vendor: F5OPV, SFCP_LABS
+   model: LoRa/APRS Telemetry Reporter
+   os: embedded
+
+ - tocall: APSFWX
+   vendor: F5OPV, SFCP_LABS
+   model: embedded Weather Station
+   class: wx
+   os: embedded
+
+ - tocall: APSK63
+   vendor: Chris Moulding, G4HYG
+   model: APRS Messenger
+   class: software
+   os: Windows
+
+ - tocall: APSMS?
+   vendor: Paul Dufresne
+   model: SMS gateway
+   class: software
+
+ - tocall: APSRF?
+   vendor: SoftRF
+   model: Ham Edition
+   class: tracker
+   os: embedded
+
+ - tocall: APSTM?
+   vendor: W7QO
+   model: Balloon tracker
+   class: tracker
+
+ - tocall: APSTPO
+   vendor: N0AGI
+   model: Satellite Tracking and Operations
+   class: software
+
+ - tocall: APT2??
+   vendor: Byonics
+   model: TinyTrak2
+   class: tracker
+
+ - tocall: APT3??
+   vendor: Byonics
+   model: TinyTrak3
+   class: tracker
+
+ - tocall: APT4??
+   vendor: Byonics
+   model: TinyTrak4
+   class: tracker
+
+ - tocall: APTB??
+   vendor: BG5HHP
+   model: TinyAPRS
+
+ - tocall: APTCHE
+   vendor: PU3IKE
+   model: TcheTracker, Tcheduino
+   class: tracker
+
+ - tocall: APTCMA
+   vendor: Cleber, PU1CMA
+   model: CAPI Tracker
+   class: tracker
+
+ - tocall: APTEMP
+   vendor: KL7AF
+   model: APRS-Tempest Weather Gateway
+   class: wx
+   os: Linux/Unix
+   contact: kl7af@foghaven.net
+
+ - tocall: APTKJ?
+   vendor: W9JAJ
+   model: ATTiny APRS Tracker
+   os: embedded
+
+ - tocall: APTNG?
+   vendor: Filip YU1TTN
+   model: Tango Tracker
+   class: tracker
+
+ - tocall: APTPN?
+   vendor: KN4ORB
+   model: TARPN Packet Node Tracker
+   class: tracker
+
+ - tocall: APTR??
+   vendor: Motorola
+   model: MotoTRBO
+
+ - tocall: APTT*
+   vendor: Byonics
+   model: TinyTrak
+   class: tracker
+
+ - tocall: APTW??
+   vendor: Byonics
+   model: WXTrak
+   class: wx
+
+ - tocall: APU1??
+   vendor: Roger Barker, G4IDE
+   model: UI-View16
+   class: software
+   os: Windows
+
+ - tocall: APU2*
+   vendor: Roger Barker, G4IDE
+   model: UI-View32
+   class: software
+   os: Windows
+
+ - tocall: APUDR?
+   vendor: NW Digital Radio
+   model: UDR
+
+ - tocall: APVE??
+   vendor: unknown
+   model: EchoLink
+
+ - tocall: APVM??
+   vendor: Digital Radio China Club
+   model: DRCC-DVM
+   class: igate
+
+ - tocall: APVR??
+   vendor: unknown
+   model: IRLP
+
+ - tocall: APW9??
+   vendor: Mile Strk, 9A9Y
+   model: WX Katarina
+   class: wx
+   os: embedded
+   features:
+     - messaging
+
+ - tocall: APWA??
+   vendor: KJ4ERJ
+   model: APRSISCE
+   class: software
+   os: Android
+
+ - tocall: APWEE?
+   vendor: Tom Keffer and Matthew Wall
+   model: WeeWX Weather Software
+   class: software
+   os: Linux/Unix
+
+ - tocall: APWM??
+   vendor: KJ4ERJ
+   model: APRSISCE
+   class: software
+   os: Windows Mobile
+   features:
+     - messaging
+     - item-in-msg
+
+ - tocall: APWW??
+   vendor: KJ4ERJ
+   model: APRSIS32
+   class: software
+   os: Windows
+   features:
+     - messaging
+     - item-in-msg
+
+ - tocall: APWnnn
+   vendor: Sproul Brothers
+   model: WinAPRS
+   class: software
+   os: Windows
+
+ - tocall: APX???
+   vendor: Open Source
+   model: Xastir
+   class: software
+   os: Linux/Unix
+
+ - tocall: APXR??
+   vendor: G8PZT
+   model: Xrouter
+
+ - tocall: APY01D
+   vendor: Yaesu
+   model: FT1D
+   class: ht
+
+ - tocall: APY02D
+   vendor: Yaesu
+   model: FT2D
+   class: ht
+
+ - tocall: APY05D
+   vendor: Yaesu
+   model: FT5D
+   class: ht
+
+ - tocall: APY300
+   vendor: Yaesu
+   model: FTM-300D
+   class: rig
+   
+ - tocall: APY400
+   vendor: Yaesu
+   model: FTM-400
+   class: rig
+   
+ - tocall: APYS??
+   vendor: W2GMD
+   model: Python APRS
+   class: software
+
+ - tocall: APZ18
+   vendor: IW3FQG
+   model: UIdigi
+   class: digi
+
+ - tocall: APZ186
+   vendor: IW3FQG
+   model: UIdigi
+   class: digi
+
+ - tocall: APZ19
+   vendor: IW3FQG
+   model: UIdigi
+   class: digi
+
+ - tocall: APZ247
+   model: UPRS
+   vendor: NR0Q
+
+ - tocall: APZG??
+   vendor: OH2GVE
+   model: aprsg
+   class: software
+   os: Linux/Unix
+
+ - tocall: APZMAJ
+   vendor: M1MAJ
+   model: DeLorme inReach Tracker
+
+ - tocall: APZMDR
+   vendor: Open Source
+   model: HaMDR
+   class: tracker
+   os: embedded
+
+ - tocall: APZTKP
+   vendor: Nick Hanks, N0LP
+   model: TrackPoint
+   class: tracker
+   os: embedded
+   
+ - tocall: APZWKR
+   vendor: GM1WKR
+   model: NetSked
+   class: software
+
+ - tocall: APnnnD
+   vendor: Painter Engineering
+   model: uSmartDigi D-Gate
+   class: dstar
+
+ - tocall: APnnnU
+   vendor: Painter Engineering
+   model: uSmartDigi Digipeater
+   class: digi
+
+ - tocall: PSKAPR
+   vendor: Open Source
+   model: PSKmail
+   class: software
+
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 5320a163..19dada4a 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -33,6 +33,7 @@ list(APPEND direwolf_SOURCES
   beacon.c
   config.c
   decode_aprs.c
+  deviceid.c
   dedupe.c
   demod_9600.c
   demod_afsk.c
@@ -171,6 +172,7 @@ endif()
 # decode_aprs
 list(APPEND decode_aprs_SOURCES
   decode_aprs.c
+  deviceid.c
   ais.c
   kiss_frame.c
   ax25_pad.c
@@ -355,6 +357,7 @@ list(APPEND atest_SOURCES
   ax25_pad.c
   ax25_pad2.c
   decode_aprs.c
+  deviceid.c
   dwgpsnmea.c
   dwgps.c
   dwgpsd.c
diff --git a/src/decode_aprs.c b/src/decode_aprs.c
index ab933278..54c2839d 100644
--- a/src/decode_aprs.c
+++ b/src/decode_aprs.c
@@ -1,7 +1,7 @@
 //
 //    This file is part of Dire Wolf, an amateur radio packet TNC.
 //
-//    Copyright (C) 2011, 2012, 2013, 2014, 2015, 2017, 2022  John Langner, WB2OSZ
+//    Copyright (C) 2011, 2012, 2013, 2014, 2015, 2017, 2022, 2023  John Langner, WB2OSZ
 //
 //    This program is free software: you can redistribute it and/or modify
 //    it under the terms of the GNU General Public License as published by
@@ -56,7 +56,7 @@
 #include "decode_aprs.h"
 #include "telemetry.h"
 #include "ais.h"
-
+#include "deviceid.h"
 
 #define TRUE 1
 #define FALSE 0
@@ -124,7 +124,6 @@ static double get_longitude_9 (char *p, int quiet);
 static time_t get_timestamp (decode_aprs_t *A, char *p);
 static int get_maidenhead (decode_aprs_t *A, char *p);
 static int data_extension_comment (decode_aprs_t *A, char *pdext);
-static void decode_tocall (decode_aprs_t *A, char *dest);
 //static void get_symbol (decode_aprs_t *A, char dti, char *src, char *dest);
 static void process_comment (decode_aprs_t *A, char *pstart, int clen);
 
@@ -292,7 +291,7 @@ void decode_aprs (decode_aprs_t *A, packet_t pp, int quiet, char *third_party_sr
 	}
 
 /*
- * Application might be in the destination field for most message types.
+ * Device/Application is in the destination field for most packet types.
  * MIC-E format has part of location in the destination field.
  */
 
@@ -303,7 +302,7 @@ void decode_aprs (decode_aprs_t *A, packet_t pp, int quiet, char *third_party_sr
 	    break;
 
 	  default:
-	    decode_tocall (A, A->g_dest);
+	    deviceid_decode_dest (A->g_dest, A->g_mfr, sizeof(A->g_mfr));
 	    break;
 	}
 
@@ -1392,7 +1391,6 @@ static void aprs_mic_e (decode_aprs_t *A, packet_t pp, unsigned char *info, int
 	int cust_msg = 0;
 	const char *std_text[8] = {"Emergency", "Priority", "Special", "Committed", "Returning", "In Service", "En Route", "Off Duty" };
 	const char *cust_text[8] = {"Emergency", "Custom-6", "Custom-5", "Custom-4", "Custom-3", "Custom-2", "Custom-1", "Custom-0" }; 
-	unsigned char *pfirst, *plast;
 
 	strlcpy (A->g_data_type_desc, "MIC-E", sizeof(A->g_data_type_desc));
 
@@ -1622,111 +1620,32 @@ static void aprs_mic_e (decode_aprs_t *A, packet_t pp, unsigned char *info, int
 	  A->g_course = n;
 
 
-/* Now try to pick out manufacturer and other optional items. */
-/* The telemetry field, in the original spec, is no longer used. */
-  
-	strlcpy (A->g_mfr, "Unknown manufacturer", sizeof(A->g_mfr));
-
-	pfirst = info + sizeof(struct aprs_mic_e_s);
-	plast = info + ilen - 1;
-
-/* Carriage return character at the end is not mentioned in spec. */
-/* Remove if found because it messes up extraction of manufacturer. */
-/* Don't drop trailing space because that is used for Yaesu VX-8. */
-/* As I recall, the IGate function trims trailing spaces.  */
-/* That would be bad for this particular model. Maybe I'm mistaken? */
-
-
-	if (*plast == '\r') plast--;
-
-#define isT(c) ((c) == ' ' || (c) == '>' || (c) == ']' || (c) == '`' || (c) == '\'')
-
-// Last Updated Dec. 2021
-
-// This does not change very often but I'm wondering if we could parse
-// http://www.aprs.org/aprs12/mic-e-types.txt similar to how we use tocalls.txt.
-
-// TODO:  Use https://github.com/aprsorg/aprs-deviceid rather than hardcoding.
-
-	if (isT(*pfirst)) {
+// The rest is a comment which can have other information cryptically embedded.
+// Remove any trailing CR, which I would argue, violates the protocol spec.
+// It is essential to keep trailing spaces.  e.g. VX-8 suffix is "_ "
 
-// "legacy" formats.
-
-	  if      (*pfirst == ' '                                       )  { strlcpy (A->g_mfr, "Original MIC-E", sizeof(A->g_mfr)); pfirst++; }
-
-	  else if (*pfirst == '>'                       && *plast == '=')  { strlcpy (A->g_mfr, "Kenwood TH-D72", sizeof(A->g_mfr)); pfirst++; plast--; }
-	  else if (*pfirst == '>'                       && *plast == '^')  { strlcpy (A->g_mfr, "Kenwood TH-D74", sizeof(A->g_mfr)); pfirst++; plast--; }
-	  else if (*pfirst == '>'                       && *plast == '&')  { strlcpy (A->g_mfr, "Kenwood TH-D75", sizeof(A->g_mfr)); pfirst++; plast--; }
-	  else if (*pfirst == '>'                                       )  { strlcpy (A->g_mfr, "Kenwood TH-D7A", sizeof(A->g_mfr)); pfirst++; }
-
-	  else if (*pfirst == ']'                       && *plast == '=')  { strlcpy (A->g_mfr, "Kenwood TM-D710", sizeof(A->g_mfr)); pfirst++; plast--; }
-	  else if (*pfirst == ']'                                       )  { strlcpy (A->g_mfr, "Kenwood TM-D700", sizeof(A->g_mfr)); pfirst++; }
-
-// ` should be used for message capable devices.
-
-	  else if (*pfirst == '`'  && *(plast-1) == '_' && *plast == ' ')  { strlcpy (A->g_mfr, "Yaesu VX-8", sizeof(A->g_mfr)); pfirst++; plast-=2; }
-	  else if (*pfirst == '`'  && *(plast-1) == '_' && *plast == '"')  { strlcpy (A->g_mfr, "Yaesu FTM-350", sizeof(A->g_mfr)); pfirst++; plast-=2; }
-	  else if (*pfirst == '`'  && *(plast-1) == '_' && *plast == '#')  { strlcpy (A->g_mfr, "Yaesu VX-8G", sizeof(A->g_mfr)); pfirst++; plast-=2; }
-	  else if (*pfirst == '`'  && *(plast-1) == '_' && *plast == '$')  { strlcpy (A->g_mfr, "Yaesu FT1D", sizeof(A->g_mfr)); pfirst++; plast-=2; }
-	  else if (*pfirst == '`'  && *(plast-1) == '_' && *plast == '%')  { strlcpy (A->g_mfr, "Yaesu FTM-400DR", sizeof(A->g_mfr)); pfirst++; plast-=2; }
-	  else if (*pfirst == '`'  && *(plast-1) == '_' && *plast == ')')  { strlcpy (A->g_mfr, "Yaesu FTM-100D", sizeof(A->g_mfr)); pfirst++; plast-=2; }
-	  else if (*pfirst == '`'  && *(plast-1) == '_' && *plast == '(')  { strlcpy (A->g_mfr, "Yaesu FT2D", sizeof(A->g_mfr)); pfirst++; plast-=2; }
-	  else if (*pfirst == '`'  && *(plast-1) == '_' && *plast == '0')  { strlcpy (A->g_mfr, "Yaesu FT3D", sizeof(A->g_mfr)); pfirst++; plast-=2; }
-	  else if (*pfirst == '`'  && *(plast-1) == '_' && *plast == '3')  { strlcpy (A->g_mfr, "Yaesu FT5D", sizeof(A->g_mfr)); pfirst++; plast-=2; }
-	  else if (*pfirst == '`'  && *(plast-1) == '_' && *plast == '1')  { strlcpy (A->g_mfr, "Yaesu FTM-300D", sizeof(A->g_mfr)); pfirst++; plast-=2; }
-	  else if (*pfirst == '`'  && *(plast-1) == '_' && *plast == '5')  { strlcpy (A->g_mfr, "Yaesu FTM-500D", sizeof(A->g_mfr)); pfirst++; plast-=2; }
-
-	  else if (*pfirst == '`'  && *(plast-1) == ' ' && *plast == 'X')  { strlcpy (A->g_mfr, "AP510", sizeof(A->g_mfr)); pfirst++; plast-=2; }
-
-	  else if (*pfirst == '`'  && *(plast-1) == '(' && *plast == '5')  { strlcpy (A->g_mfr, "Anytone D578UV", sizeof(A->g_mfr)); pfirst++; plast-=2; }
-	  else if (*pfirst == '`'                                       )  { strlcpy (A->g_mfr, "Generic Mic-Emsg", sizeof(A->g_mfr)); pfirst++; }
-
-// ' should be used for trackers (not message capable).
+	char mcomment[256];
+	strlcpy (mcomment, info + sizeof(struct aprs_mic_e_s), sizeof(mcomment));
+	if (mcomment[strlen(mcomment)-1] == '\r') {
+	  mcomment[strlen(mcomment)-1] = '\0';
+	}
 
-	  else if (*pfirst == '\'' && *(plast-1) == '(' && *plast == '5')  { strlcpy (A->g_mfr, "Anytone D578UV", sizeof(A->g_mfr)); pfirst++; plast-=2; }
-	  else if (*pfirst == '\'' && *(plast-1) == '(' && *plast == '8')  { strlcpy (A->g_mfr, "Anytone D878UV", sizeof(A->g_mfr)); pfirst++; plast-=2; }
+/* Now try to pick out manufacturer and other optional items. */
+/* The telemetry field, in the original spec, is no longer used. */
 
-	  else if (*pfirst == '\'' && *(plast-1) == '|' && *plast == '3')  { strlcpy (A->g_mfr, "Byonics TinyTrack3", sizeof(A->g_mfr)); pfirst++; plast-=2; }
-	  else if (*pfirst == '\'' && *(plast-1) == '|' && *plast == '4')  { strlcpy (A->g_mfr, "Byonics TinyTrack4", sizeof(A->g_mfr)); pfirst++; plast-=2; }
+	char trimmed[256];	// Comment with vendor/model removed.
+	deviceid_decode_mice (mcomment, trimmed, sizeof(trimmed), A->g_mfr, sizeof(A->g_mfr));
 
-	  else if (*pfirst == '\'' && *(plast-1) == ':' && *plast == '4')  { strlcpy (A->g_mfr, "SCS GmbH & Co. P4dragon DR-7400 modems", sizeof(A->g_mfr)); pfirst++; plast-=2; }
-	  else if (*pfirst == '\'' && *(plast-1) == ':' && *plast == '8')  { strlcpy (A->g_mfr, "SCS GmbH & Co. P4dragon DR-7800 modems", sizeof(A->g_mfr)); pfirst++; plast-=2; }
 
-	  else if (*pfirst == '\''                                      )  { strlcpy (A->g_mfr, "Generic McTrackr", sizeof(A->g_mfr)); pfirst++; }
 
-	  else if (                   *(plast-1) == '\\'                )  { strlcpy (A->g_mfr, "Hamhud ?", sizeof(A->g_mfr)); pfirst++; plast-=2; }
-	  else if (                   *(plast-1) == '/'                 )  { strlcpy (A->g_mfr, "Argent ?", sizeof(A->g_mfr)); pfirst++; plast-=2; }
-	  else if (                   *(plast-1) == '^'                 )  { strlcpy (A->g_mfr, "HinzTec anyfrog", sizeof(A->g_mfr)); pfirst++; plast-=2; }
-	  else if (                   *(plast-1) == '*'                 )  { strlcpy (A->g_mfr, "APOZxx www.KissOZ.dk Tracker. OZ1EKD and OZ7HVO", sizeof(A->g_mfr)); pfirst++; plast-=2; }
-	  else if (                   *(plast-1) == '~'                 )  { strlcpy (A->g_mfr, "Unknown OTHER", sizeof(A->g_mfr)); pfirst++; plast-=2; }
-	}
+// Possible altitude.  3 characters followed by }
 
-/*
- * An optional altitude is next.
- * It is three base-91 digits followed by "}".
- * The TM-D710A might have encoding bug.  This was observed:
- *
- * KJ4ETP-9>SUUP9Q,KE4OTZ-3,WIDE1*,WIDE2-1,qAR,KI4HDU-2:`oV$n6:>/]"7&}162.475MHz <Knox,TN> clintserman@gmail=
- * N 35 50.9100, W 083 58.0800, 25 MPH, course 230, alt 945 ft, 162.475MHz
- *
- * KJ4ETP-9>SUUP6Y,GRNTOP-3*,WIDE2-1,qAR,KI4HDU-2:`oU~nT >/]<0x9a>xt}162.475MHz <Knox,TN> clintserman@gmail=
- * Invalid character in MIC-E altitude.  Must be in range of '!' to '{'.
- * N 35 50.6900, W 083 57.9800, 29 MPH, course 204, alt 3280843 ft, 162.475MHz
- *
- * KJ4ETP-9>SUUP6Y,N4NEQ-3,K4EGA-1,WIDE2*,qAS,N5CWH-1:`oU~nT >/]?xt}162.475MHz <Knox,TN> clintserman@gmail=
- * N 35 50.6900, W 083 57.9800, 29 MPH, course 204, alt 808497 ft, 162.475MHz
- *
- * KJ4ETP-9>SUUP2W,KE4OTZ-3,WIDE1*,WIDE2-1,qAR,KI4HDU-2:`oV2o"J>/]"7)}162.475MHz <Knox,TN> clintserman@gmail=
- * N 35 50.2700, W 083 58.2200, 35 MPH, course 246, alt 955 ft, 162.475MHz
- * 
- * Note the <0x9a> which is outside of the 7-bit ASCII range.  Clearly very wrong.
- */
 
-	if (plast > pfirst && pfirst[3] == '}') {
+	if (strlen(trimmed) >=4 && trimmed[3] == '}') {
 
-	  A->g_altitude_ft = DW_METERS_TO_FEET((pfirst[0]-33)*91*91 + (pfirst[1]-33)*91 + (pfirst[2]-33) - 10000);
+	  A->g_altitude_ft = DW_METERS_TO_FEET((trimmed[0]-33)*91*91 + (trimmed[1]-33)*91 + (trimmed[2]-33) - 10000);
 
-	  if ( ! isdigit91(pfirst[0]) || ! isdigit91(pfirst[1]) || ! isdigit91(pfirst[2])) 
+	  if ( ! isdigit91(trimmed[0]) || ! isdigit91(trimmed[1]) || ! isdigit91(trimmed[2])) 
 	  {
 	    if ( ! A->g_quiet) {
 	      text_color_set(DW_COLOR_ERROR);
@@ -1736,12 +1655,13 @@ static void aprs_mic_e (decode_aprs_t *A, packet_t pp, unsigned char *info, int
 	    A->g_altitude_ft = G_UNKNOWN;
 	  }
 	  
-	  pfirst += 4;
+	  process_comment (A, mcomment+4, strlen(mcomment) - 4);
+	  return;
 	}
 
-	process_comment (A, (char*)pfirst, (int)(plast - pfirst) + 1);
+	process_comment (A, mcomment, strlen(mcomment));
 
-}
+}  // end aprs_mic_e
 
 
 /*------------------------------------------------------------------
@@ -4251,221 +4171,6 @@ static int data_extension_comment (decode_aprs_t *A, char *pdext)
 }
 
 
-/*------------------------------------------------------------------
- *
- * Function:	decode_tocall
- *
- * Purpose:	Extract application from the destination.
- *
- * Inputs:	dest	- Destination address.
- *			Don't care if SSID is present or not.
- *
- * Outputs:	A->g_mfr
- *
- * Description:	For maximum flexibility, we will read the
- *		data file at run time rather than compiling it in.
- *
- *		For the most recent version, download from:
- *
- *		http://www.aprs.org/aprs11/tocalls.txt
- *
- *		Windows version:  File must be in current working directory.
- *
- *		Linux version: Search order is current working directory then
- *			/usr/local/share/direwolf
- *			/usr/share/direwolf/tocalls.txt
- *
- *		Mac: Like Linux and then
- *			/opt/local/share/direwolf
- *
- *------------------------------------------------------------------*/
-
-// If I was more ambitious, this would dynamically allocate enough
-// storage based on the file contents.  Just stick in a constant for
-// now.  This takes an insignificant amount of space and
-// I don't anticipate tocalls.txt growing that quickly.
-// Version 1.4 - add message if too small instead of silently ignoring the rest.
-
-// Dec. 2016 tocalls.txt has 153 destination addresses.
-
-#define MAX_TOCALLS 250
-
-static struct tocalls_s {
-	unsigned char len;
-	char prefix[7];
-	char *description;
-} tocalls[MAX_TOCALLS];
-
-static int num_tocalls = 0;
-
-// Make sure the array is null terminated.
-// If search order is changed, do the same in symbols.c for consistency.
-
-static const char *search_locations[] = {
-	(const char *) "tocalls.txt",			// CWD
-	(const char *) "data/tocalls.txt",		// Windows with CMake
-	(const char *) "../data/tocalls.txt",		// ?
-#ifndef __WIN32__
-	(const char *) "/usr/local/share/direwolf/tocalls.txt",
-	(const char *) "/usr/share/direwolf/tocalls.txt",
-#endif
-#if __APPLE__
-	// https://groups.yahoo.com/neo/groups/direwolf_packet/conversations/messages/2458
-	// Adding the /opt/local tree since macports typically installs there.  Users might want their
-	// INSTALLDIR (see Makefile.macosx) to mirror that.  If so, then we need to search the /opt/local
-	// path as well.
-	(const char *) "/opt/local/share/direwolf/tocalls.txt",
-#endif
-	(const char *) NULL		// Important - Indicates end of list.
-};
-
-static int tocall_cmp (const void *px, const void *py)
-{
-	const struct tocalls_s *x = (struct tocalls_s *)px;
-	const struct tocalls_s *y = (struct tocalls_s *)py;
-
-	if (x->len != y->len) return (y->len - x->len);
-	return (strcmp(x->prefix, y->prefix));
-}
-
-static void decode_tocall (decode_aprs_t *A, char *dest)
-{
-	FILE *fp = 0;
-	int n = 0;
-	static int first_time = 1;
-	char stuff[100];
-	char *p = NULL;
-	char *r = NULL;
-
-	//dw_printf("debug: decode_tocall(\"%s\")\n", dest);
-
-/*
- * Extract the calls and descriptions from the file.
- *
- * Use only lines with exactly these formats:
- *
- *       APN          Network nodes, digis, etc
- *	      APWWxx  APRSISCE win32 version
- *	|     |       |
- *	00000000001111111111      	
- *	01234567890123456789...
- *
- * Matching will be with only leading upper case and digits.
- */
-
-// TODO:  Look for this in multiple locations.
-// For example, if application was installed in /usr/local/bin,
-// we might want to put this in /usr/local/share/aprs
-
-// If search strategy changes, be sure to keep symbols_init in sync.
-
-	if (first_time) {
-
-	  n = 0;
-	  fp = NULL;
-	  do {
-	    if(search_locations[n] == NULL) break;
-	    fp = fopen(search_locations[n++], "r");
-	  } while (fp == NULL);
-
-	  if (fp != NULL) {
-
-	    while (fgets(stuff, sizeof(stuff), fp) != NULL && num_tocalls < MAX_TOCALLS) {
-	      
-	      p = stuff + strlen(stuff) - 1;
-	      while (p >= stuff && (*p == '\r' || *p == '\n')) {
-	        *p-- = '\0';
-	      }
-
-	      // dw_printf("debug: %s\n", stuff);
-
-	      if (stuff[0] == ' ' && 
-		  stuff[4] == ' ' &&
-		  stuff[5] == ' ' &&
-		  stuff[6] == 'A' && 
-		  stuff[7] == 'P' && 
-		  stuff[12] == ' ' &&
-		  stuff[13] == ' ' ) {
-
-	        p = stuff + 6;
-	        r = tocalls[num_tocalls].prefix;
-	        while (isupper((int)(*p)) || isdigit((int)(*p))) {
-	          *r++ = *p++;
-	        }
-	        *r = '\0';
-	        if (strlen(tocalls[num_tocalls].prefix) > 2) {
-	          tocalls[num_tocalls].description = strdup(stuff+14);
-		  tocalls[num_tocalls].len = strlen(tocalls[num_tocalls].prefix);
-	          // dw_printf("debug %d: %d '%s' -> '%s'\n", num_tocalls, tocalls[num_tocalls].len, tocalls[num_tocalls].prefix, tocalls[num_tocalls].description);
-
-	          num_tocalls++;
-	        }
-	      }
-	      else if (stuff[0] == ' ' && 
-		  stuff[1] == 'A' && 
-		  stuff[2] == 'P' && 
-		  isupper((int)(stuff[3])) &&
-		  stuff[4] == ' ' &&
-		  stuff[5] == ' ' &&
-		  stuff[6] == ' ' &&
-		  stuff[12] == ' ' &&
-		  stuff[13] == ' ' ) {
-
-	        p = stuff + 1;
-	        r = tocalls[num_tocalls].prefix;
-	        while (isupper((int)(*p)) || isdigit((int)(*p))) {
-	          *r++ = *p++;
-	        }
-	        *r = '\0';
-	        if (strlen(tocalls[num_tocalls].prefix) > 2) {
-	          tocalls[num_tocalls].description = strdup(stuff+14);
-		  tocalls[num_tocalls].len = strlen(tocalls[num_tocalls].prefix);
-	          // dw_printf("debug %d: %d '%s' -> '%s'\n", num_tocalls, tocalls[num_tocalls].len, tocalls[num_tocalls].prefix, tocalls[num_tocalls].description);
-
-	          num_tocalls++;
-	        }
-	      }
-	      if (num_tocalls == MAX_TOCALLS) {		// oops. might have discarded some.
-	        text_color_set(DW_COLOR_ERROR);
-	        dw_printf("MAX_TOCALLS needs to be larger than %d to handle contents of 'tocalls.txt'.\n", MAX_TOCALLS);
-	      }
-	    }
-	    fclose(fp);
-
-/*
- * Sort by decreasing length so the search will go
- * from most specific to least specific.
- * Example:  APY350 or APY008 would match those specific
- * models before getting to the more generic APY.
- */
-
-	    qsort (tocalls, num_tocalls, sizeof(struct tocalls_s), tocall_cmp);
-
-	  }
-	  else {
-	    if ( ! A->g_quiet) {
-	      text_color_set(DW_COLOR_ERROR);
-	      dw_printf("Warning: Could not open 'tocalls.txt'.\n");
-	      dw_printf("System types in the destination field will not be decoded.\n");
-	    }
-	  }
-	
-	  first_time = 0;
-
-	  //for (n=0; n<num_tocalls; n++) {
-	  //  dw_printf("sorted %d: %d '%s' -> '%s'\n", n, tocalls[n].len, tocalls[n].prefix, tocalls[n].description);
-	  //}
-	}
-
-
-	for (n=0; n<num_tocalls; n++) {
-	  if (strncmp(dest, tocalls[n].prefix, tocalls[n].len) == 0) {
-	    strlcpy (A->g_mfr, tocalls[n].description, sizeof(A->g_mfr));
-	    return;
-	  }
-	}
-
-} /* end decode_tocall */ 
 
 
 
@@ -4513,7 +4218,7 @@ static void substr_se (char *dest, const char *src, int start, int endp1)
  *		clen		- Length of comment or -1 to take it all.
  *
  * Outputs:	A->g_telemetry	- Base 91 telemetry |ss1122|
- *		A->g_altitude_ft - from /A=123456
+ *		A->g_altitude_ft - from /A=123456 or /A=-12345
  *		A->g_lat	- Might be adjusted from !DAO!
  *		A->g_lon	- Might be adjusted from !DAO!
  *		A->g_aprstt_loc	- Private extension to !DAO!
@@ -4543,6 +4248,10 @@ static void substr_se (char *dest, const char *src, int start, int endp1)
  *		Protocol reference, end of chapter 6.
  *
  *			/A=123456		Altitude
+ *			/A=-12345		Enhancement - There are many places on the earth's
+ *						surface but the APRS spec has no provision for negative
+ *						numbers.  I propose having 5 digits for a consistent
+ *						field width.  6 would be excessive.
  *
  * What can appear in a comment?
  *
@@ -4708,7 +4417,7 @@ static void process_comment (decode_aprs_t *A, char *pstart, int clen)
 	    dw_printf("%s:%d: %s\n", __FILE__, __LINE__, emsg);
 	  }
 
-	  e = regcomp (&alt_re, "/A=[0-9][0-9][0-9][0-9][0-9][0-9]", REG_EXTENDED);
+	  e = regcomp (&alt_re, "/A=[0-9-][0-9][0-9][0-9][0-9][0-9]", REG_EXTENDED);
 	  if (e) {
 	    regerror (e, &alt_re, emsg, sizeof(emsg));
 	    dw_printf("%s:%d: %s\n", __FILE__, __LINE__, emsg);
@@ -5068,7 +4777,7 @@ static void process_comment (decode_aprs_t *A, char *pstart, int clen)
 	}
 
 /*
- * Altitude in feet.  /A=123456
+ * Altitude in feet.  /A=123456 or /A=-12345
  */
 
 	if (regexec (&alt_re, A->g_comment, MAXMATCH, match, 0) == 0) 
@@ -5186,7 +4895,7 @@ static void process_comment (decode_aprs_t *A, char *pstart, int clen)
  *
  * Function:	main
  *
- * Purpose:	Main program for standalone test program.
+ * Purpose:	Main program for standalone application to parse and explain APRS packets.
  *
  * Inputs:	stdin for raw data to decode.
  *		This is in the usual display format either from
@@ -5332,6 +5041,7 @@ int main (int argc, char *argv[])
 	// If you don't like the text colors, use 0 instead of 1 here.
 	text_color_init(1);
 	text_color_set(DW_COLOR_INFO);
+	deviceid_init();
 
 	while (fgets(stuff, sizeof(stuff), stdin) != NULL) 
         {
diff --git a/src/deviceid.c b/src/deviceid.c
new file mode 100644
index 00000000..594b20ea
--- /dev/null
+++ b/src/deviceid.c
@@ -0,0 +1,660 @@
+//
+//    This file is part of Dire Wolf, an amateur radio packet TNC.
+//
+//    Copyright (C) 2023  John Langner, WB2OSZ
+//
+//    This program is free software: you can redistribute it and/or modify
+//    it under the terms of the GNU General Public License as published by
+//    the Free Software Foundation, either version 2 of the License, or
+//    (at your option) any later version.
+//
+//    This program is distributed in the hope that it will be useful,
+//    but WITHOUT ANY WARRANTY; without even the implied warranty of
+//    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+//    GNU General Public License for more details.
+//
+//    You should have received a copy of the GNU General Public License
+//    along with this program.  If not, see <http://www.gnu.org/licenses/>.
+//
+
+
+/*------------------------------------------------------------------
+ *
+ * File:	deviceid.c
+ *
+ * Purpose:	Determine the device identifier from the destination field,
+ *		or from prefix/suffix for MIC-E format.
+ *
+ * Description: Orginally this used the tocalls.txt file and was part of decode_aprs.c.
+ *		For release 1.8, we use tocalls.yaml and this is split into a separate file.
+ *
+ *------------------------------------------------------------------*/
+
+//#define TEST 1		// Standalone test.   $ gcc -DTEST deviceid.c && ./a.out
+
+
+#if TEST
+#define HAVE_STRLCPY 1		// prevent defining in direwolf.h
+#define HAVE_STRLCAT 1
+#endif
+
+#include "direwolf.h"
+
+#include <stdlib.h>
+#include <stdio.h>
+#include <string.h>
+#include <assert.h>
+
+#include "deviceid.h"
+#include "textcolor.h"
+
+
+static void unquote (int line, char *pin, char *pout);
+static int tocall_cmp (const void *px, const void *py);
+static int mice_cmp (const void *px, const void *py);
+
+/*------------------------------------------------------------------
+ *
+ * Function:	main
+ *
+ * Purpose:	A little self-test used during development.
+ *
+ * Description:	Read the yaml file.  Decipher a few typical values.
+ *
+ *------------------------------------------------------------------*/
+
+#if TEST
+// So we don't need to link with any other files.
+#define dw_printf printf
+void text_color_set(dw_color_t)  { return; }
+void strlcpy(char *dst, char *src, size_t dlen) {
+	strcpy (dst, src);
+}
+void strlcat(char *dst, char *src, size_t dlen) {
+	strcat (dst, src);
+}
+
+
+int main (int argc, char *argv[])
+{
+	char device[80];
+	char comment_out[80];
+
+	deviceid_init ();
+
+	dw_printf ("\n");
+	dw_printf ("Testing ...\n");
+
+// MIC-E Legacy (really Kenwood).
+
+	deviceid_decode_mice (">Comment", comment_out, sizeof(comment_out), device, sizeof(device));
+	dw_printf ("%s %s\n", comment_out, device);
+	assert (strcmp(comment_out, "Comment") == 0);
+	assert (strcmp(device, "Kenwood TH-D7A") == 0);
+
+	deviceid_decode_mice (">Comment^", comment_out, sizeof(comment_out), device, sizeof(device));
+	dw_printf ("%s %s\n", comment_out, device);
+	assert (strcmp(comment_out, "Comment") == 0);
+	assert (strcmp(device, "Kenwood TH-D74") == 0);
+
+	deviceid_decode_mice ("]Comment", comment_out, sizeof(comment_out), device, sizeof(device));
+	dw_printf ("%s %s\n", comment_out, device);
+	assert (strcmp(comment_out, "Comment") == 0);
+	assert (strcmp(device, "Kenwood TM-D700") == 0);
+
+	deviceid_decode_mice ("]Comment=", comment_out, sizeof(comment_out), device, sizeof(device));
+	dw_printf ("%s %s\n", comment_out, device);
+	assert (strcmp(comment_out, "Comment") == 0);
+	assert (strcmp(device, "Kenwood TM-D710") == 0);
+
+
+// Modern MIC-E.
+
+	deviceid_decode_mice ("`Comment_\"", comment_out, sizeof(comment_out), device, sizeof(device));
+	dw_printf ("%s %s\n", comment_out, device);
+	assert (strcmp(comment_out, "Comment") == 0);
+	assert (strcmp(device, "Yaesu FTM-350") == 0);
+
+	deviceid_decode_mice ("`Comment_ ", comment_out, sizeof(comment_out), device, sizeof(device));
+	dw_printf ("%s %s\n", comment_out, device);
+	assert (strcmp(comment_out, "Comment") == 0);
+	assert (strcmp(device, "Yaesu VX-8") == 0);
+
+	deviceid_decode_mice ("'Comment|3", comment_out, sizeof(comment_out), device, sizeof(device));
+	dw_printf ("%s %s\n", comment_out, device);
+	assert (strcmp(comment_out, "Comment") == 0);
+	assert (strcmp(device, "Byonics TinyTrak3") == 0);
+
+	deviceid_decode_mice ("Comment", comment_out, sizeof(comment_out), device, sizeof(device));
+	dw_printf ("%s %s\n", comment_out, device);
+	assert (strcmp(comment_out, "Comment") == 0);
+	assert (strcmp(device, "UNKNOWN vendor/model") == 0);
+
+
+// Tocall
+
+	deviceid_decode_dest ("APDW18", device, sizeof(device));
+	dw_printf ("%s\n", device);
+	assert (strcmp(device, "WB2OSZ DireWolf") == 0);
+
+	deviceid_decode_dest ("APD123", device, sizeof(device));
+	dw_printf ("%s\n", device);
+	assert (strcmp(device, "Open Source aprsd") == 0);
+
+	// null for Vendor.
+	deviceid_decode_dest ("APAX", device, sizeof(device));
+	dw_printf ("%s\n", device);
+	assert (strcmp(device, "AFilterX") == 0);
+
+	deviceid_decode_dest ("APA123", device, sizeof(device));
+	dw_printf ("%s\n", device);
+	assert (strcmp(device, "UNKNOWN vendor/model") == 0);
+
+	dw_printf ("\n");
+	dw_printf ("Success!\n");
+
+	exit (EXIT_SUCCESS);
+}
+
+#endif  // TEST
+
+
+
+// Structures to hold mapping from encoded form to vendor and model.
+// The .yaml file has two separate sections for MIC-E but they can
+// both be handled as a single more general case.
+
+struct mice {
+	char prefix[4];		// The legacy form has 1 prefix character.
+				// The newer form has none.  (more accurately ` or ')
+	char suffix[4];		// The legacy form has 0 or 1.
+				// The newer form has 2.
+	char *vendor;
+	char *model;
+};
+
+struct tocalls {
+	char tocall[8];		// Up to 6 characters.  Some may have wildcards at the end.
+				// Most often they are trailing "??" or "?" or "???" in one case.
+				// Sometimes there is trailing "nnn".  Does that imply digits only?
+				// Sometimes we see a trailing "*".  Is "*" different than "?"?
+				// There are a couple bizzare cases like APnnnD which can
+				// create an ambigious situation. APMPAD, APRFGD, APY0[125]D.
+				// Screw them if they can't follow the rules.  I'm not putting in a special case.
+	char *vendor;
+	char *model;
+};
+
+
+static struct mice *pmice = NULL;	// Pointer to array.
+static int mice_count = 0;		// Number of allocated elements.
+static int mice_index = -1;		// Current index for filling in.
+
+static struct tocalls *ptocalls = NULL;	// Pointer to array.
+static int tocalls_count = 0;		// Number of allocated elements.
+static int tocalls_index = -1;		// Current index for filling in.
+
+
+
+
+/*------------------------------------------------------------------
+ *
+ * Function:	deviceid_init
+ *
+ * Purpose:	Called once at startup to read the tocalls.yaml file which was obtained from
+ *		https://github.com/aprsorg/aprs-deviceid .
+ *
+ * Inputs:	tocalls.yaml with OS specific directory search list.
+ *
+ * Outputs:	static variables listed above.
+ *
+ * Description:	For maximum flexibility, we will read the
+ *		data file at run time rather than compiling it in.
+ *
+ *------------------------------------------------------------------*/
+
+// Make sure the array is null terminated.
+// If search order is changed, do the same in symbols.c for consistency.
+// fopen is perfectly happy with / in file path when running on Windows.
+
+static const char *search_locations[] = {
+	(const char *) "tocalls.yaml",			// Current working directory
+	(const char *) "data/tocalls.yaml",		// Windows with CMake
+	(const char *) "../data/tocalls.yaml",		// Source tree
+#ifndef __WIN32__
+	(const char *) "/usr/local/share/direwolf/tocalls.yaml",
+	(const char *) "/usr/share/direwolf/tocalls.yaml",
+#endif
+#if __APPLE__
+	// https://groups.yahoo.com/neo/groups/direwolf_packet/conversations/messages/2458
+	// Adding the /opt/local tree since macports typically installs there.  Users might want their
+	// INSTALLDIR (see Makefile.macosx) to mirror that.  If so, then we need to search the /opt/local
+	// path as well.
+	(const char *) "/opt/local/share/direwolf/tocalls.yaml",
+#endif
+	(const char *) NULL		// Important - Indicates end of list.
+};
+
+
+void deviceid_init(void)
+{
+	FILE *fp = NULL;
+	for (int n = 0; search_locations[n] != NULL && fp == NULL; n++) {
+	  dw_printf ("Trying %s\n", search_locations[n]);
+	  fp = fopen(search_locations[n], "r");
+#if TEST
+	  if (fp != NULL) {
+	    dw_printf ("Opened %s\n", search_locations[n]);
+	  }
+#endif
+	};
+
+	if (fp == NULL) {
+	  text_color_set(DW_COLOR_ERROR);
+	  dw_printf("Could not open any of these file locations:\n");
+	  for (int n = 0; search_locations[n] != NULL; n++) {
+	    dw_printf ("    %s\n", search_locations[n]);
+	  }
+	  dw_printf("It won't be possible to extract device identifiers from packets.\n");
+	  return;
+	};
+
+// Read file first time to get number of items.
+// Allocate required space.
+// Rewind.
+// Read file second time to gather data.
+
+	enum { no_section, mice_section, tocalls_section} section = no_section;
+	char stuff[80];
+
+	for (int pass = 1; pass <=2; pass++) {
+	 int line = 0;		// Line number within file.
+
+	 while (fgets(stuff, sizeof(stuff), fp)) {
+	  line++;
+
+	  // Remove trailing CR/LF or spaces.
+	  char *p = stuff + strlen(stuff) - 1;
+	  while (p >= (char*)stuff && (*p == '\r' || *p == '\n' || *p == ' ')) {
+	    *p-- = '\0';
+	  }
+
+	  // Ignore comment lines.
+	  if (stuff[0] == '#') {
+	    continue;
+	  }
+
+#if TEST
+	  //dw_printf ("%d: %s\n", line, stuff);
+#endif
+	  // This is not very robust; everything better be in exactly the right format.
+
+	  if (strncmp(stuff, "mice:", strlen("mice:")) == 0) {
+	    section = mice_section;
+#if TEST
+	    dw_printf ("Pass %d, line %d, MIC-E section\n", pass, line);
+#endif
+	  }
+	  else if (strncmp(stuff, "micelegacy:", strlen("micelegacy:")) == 0) {
+	    section = mice_section;  // treat both same.
+#if TEST
+	    dw_printf ("Pass %d, line %d, Legacy MIC-E section\n", pass, line);
+#endif
+	  }
+	  else if (strncmp(stuff, "tocalls:", strlen("tocalls:")) == 0) {
+	    section = tocalls_section;
+#if TEST
+	    dw_printf ("Pass %d, line %d, TOCALLS section\n", pass, line);
+#endif
+	  }
+
+	  // The first property of an item is preceded by " - ".
+
+	  if (pass == 1 && strncmp(stuff, " - ", 3) == 0) {
+	    switch (section) {
+	      case no_section:						break;
+	      case mice_section:	mice_count++;			break;
+	      case tocalls_section:	tocalls_count++;		break;
+	    }
+	  }
+
+	  if (pass == 2) {
+	    switch (section) {
+	      case no_section:
+	        break;
+
+	      case mice_section:
+	        if (strncmp(stuff, " - ", 3) == 0) {
+	          mice_index++;
+	          assert (mice_index >= 0 && mice_index < mice_count);
+	        }
+	        if (strncmp(stuff+3, "prefix: ", strlen("prefix: ")) == 0) {
+	          unquote (line, stuff+3+8, pmice[mice_index].prefix);  
+	        }
+	        else if (strncmp(stuff+3, "suffix: ", strlen("suffix: ")) == 0) {
+	          unquote (line, stuff+3+8, pmice[mice_index].suffix);  
+	        }
+	        else if (strncmp(stuff+3, "vendor: ", strlen("vendor: ")) == 0) {
+	          pmice[mice_index].vendor = strdup(stuff+3+8);  
+	        }
+	        else if (strncmp(stuff+3, "model: ", strlen("model: ")) == 0) {
+	          pmice[mice_index].model = strdup(stuff+3+7);  
+	        }
+	        break;
+
+	      case tocalls_section:
+	        if (strncmp(stuff, " - ", 3) == 0) {
+	          tocalls_index++;
+	          assert (tocalls_index >= 0 && tocalls_index < tocalls_count);
+	        }
+	        if (strncmp(stuff+3, "tocall: ", strlen("tocall: ")) == 0) {
+	          // Remove trailing wildcard characters ? * n
+	          char *r = stuff + strlen(stuff) - 1;
+	          while (r >= (char*)stuff && (*r == '?' || *r == '*' || *r == 'n')) {
+	            *r-- = '\0';
+	          }
+
+	          strlcpy (ptocalls[tocalls_index].tocall, stuff+3+8, sizeof(ptocalls[tocalls_index].tocall));
+
+	          // Remove trailing CR/LF or spaces.
+	          char *p = stuff + strlen(stuff) - 1;
+	          while (p >= (char*)stuff && (*p == '\r' || *p == '\n' || *p == ' ')) {
+	            *p-- = '\0';
+	          }
+	        }
+	        else if (strncmp(stuff+3, "vendor: ", strlen("vendor: ")) == 0) {
+	          ptocalls[tocalls_index].vendor = strdup(stuff+3+8);  
+	        }
+	        else if (strncmp(stuff+3, "model: ", strlen("model: ")) == 0) {
+	          ptocalls[tocalls_index].model = strdup(stuff+3+7);  
+	        }
+	        break;
+	    }
+	  }
+	 } // while(fgets
+
+	 if (pass == 1) {
+#if TEST
+	  dw_printf ("deviceid sizes %d %d\n", mice_count, tocalls_count);
+#endif
+	  pmice = calloc(sizeof(struct mice), mice_count);
+	  ptocalls = calloc(sizeof(struct tocalls), tocalls_count);
+
+	  rewind (fp);
+	  section = no_section;
+	 }
+	} // for pass = 1 or 2
+
+	fclose (fp);
+
+	assert (mice_index == mice_count - 1);
+	assert (tocalls_index == tocalls_count - 1);
+
+
+// MIC-E Legacy needs to be sorted so those with suffix come first.
+
+	qsort (pmice, mice_count, sizeof(struct mice), mice_cmp);
+
+
+// Sort tocalls by decreasing length so the search will go from most specific to least specific.
+// Example:  APY350 or APY008 would match those specific models before getting to the more generic APY.
+
+	qsort (ptocalls, tocalls_count, sizeof(struct tocalls), tocall_cmp);
+
+
+#if TEST
+	dw_printf ("MIC-E:\n");
+	for (int i = 0; i < mice_count; i++) {
+	  dw_printf ("%s %s %s\n", pmice[i].suffix, pmice[i].vendor, pmice[i].model);
+	}
+	dw_printf ("TOCALLS:\n");
+	for (int i = 0; i < tocalls_count; i++) {
+	  dw_printf ("%s %s %s\n", ptocalls[i].tocall, ptocalls[i].vendor, ptocalls[i].model);
+	}
+#endif
+
+	return;
+
+} // end deviceid_init
+
+
+/*------------------------------------------------------------------
+ *
+ * Function:	unquote
+ *
+ * Purpose:	Remove surrounding quotes and undo any escapes.
+ *
+ * Inputs:	line - File line number for error message.
+ *
+ *		in - String with quotes. Might contain \ escapes.
+ *
+ * Outputs:	out - Quotes and escapes removed.
+ *			Limited to 2 characters to avoid buffer overflow.
+ *
+ * Examples:	in	out
+ *		"_#"	_#
+ *		"_\""	_"
+ *		"="	=
+ *
+ *------------------------------------------------------------------*/
+
+static void unquote (int line, char *pin, char *pout)
+{
+	int count = 0;
+
+	*pout = '\0';
+	if (*pin != '"') {
+	  text_color_set(DW_COLOR_ERROR);
+	  dw_printf("Missing leading \" for %s on line %d.\n", pin, line);
+	  return;
+	}
+
+	pin++;
+	while (*pin != '\0' && *pin != '\"' && count < 2) {
+	  if (*pin == '\\') {
+	    pin++;
+	  }
+	  *pout++ = *pin++;
+	  count++;
+	}
+	*pout = '\0';
+
+	if (*pin != '"') {
+	  text_color_set(DW_COLOR_ERROR);
+	  dw_printf("Missing trailing \" or string too long on line %d.\n", line);
+	  return;
+	}
+}
+
+// Used to sort the tocalls by length.
+// When length is equal, alphabetically.
+
+static int tocall_cmp (const void *px, const void *py)
+{
+	const struct tocalls *x = (struct tocalls *)px;
+	const struct tocalls *y = (struct tocalls *)py;
+
+	if (strlen(x->tocall) != strlen(y->tocall)) {
+	  return (strlen(y->tocall) - strlen(x->tocall));
+	}
+	return (strcmp(x->tocall, y->tocall));
+}
+
+// Used to sort the suffixes by length.
+// Longer at the top.
+// Example check for  >xxx^ before >xxx .
+
+static int mice_cmp (const void *px, const void *py)
+{
+	const struct mice *x = (struct mice *)px;
+	const struct mice *y = (struct mice *)py;
+
+	return (strlen(y->suffix) - strlen(x->suffix));
+}
+
+
+
+
+
+/*------------------------------------------------------------------
+ *
+ * Function:	deviceid_decode_dest
+ *
+ * Purpose:	Find vendor/model for destination address of form APxxxx.
+ *
+ * Inputs:	dest	- Destination address.  No SSID.
+ *
+ *		device_size - Amount of space available for result to avoid buffer overflow.
+ *
+ * Outputs:	device	- Vendor and model.
+ *
+ * Description:	With the exception of MIC-E format, we expect to find the vendor/model in the
+ *		AX.25 destination field.   The form should be APxxxx.
+ *
+ *		Search the list looking for the maximum length match.
+ *		For example, 
+ *			APXR	= Xrouter
+ *			APX	= Xastir
+ *
+ *------------------------------------------------------------------*/
+
+void deviceid_decode_dest (char *dest, char *device, size_t device_size)
+{
+	*device = '\0';
+
+	if (ptocalls == NULL) {
+	  text_color_set(DW_COLOR_ERROR);
+	  dw_printf("deviceid_decode_dest called without any deviceid data.\n");
+	  return;
+	}
+
+	for (int n = 0; n < tocalls_count; n++) {
+	  if (strncmp(dest, ptocalls[n].tocall, strlen(ptocalls[n].tocall)) == 0) {
+
+	    if (ptocalls[n].vendor != NULL) {
+	      strlcpy (device, ptocalls[n].vendor, device_size);
+	    }
+
+	    if (ptocalls[n].vendor != NULL && ptocalls[n].model != NULL) {
+	      strlcat (device, " ", device_size);
+	    }
+
+	    if (ptocalls[n].model != NULL) {
+	      strlcat (device, ptocalls[n].model, device_size);
+	    }
+	    return;
+	  }
+	}
+
+	strlcpy (device, "UNKNOWN vendor/model", device_size);
+
+} // end deviceid_decode_dest
+
+
+/*------------------------------------------------------------------
+ *
+ * Function:	deviceid_decode_mice
+ *
+ * Purpose:	Find vendor/model for MIC-E comment.
+ *
+ * Inputs:	comment - MIC-E comment that might have vendor/model encoded as
+ *			a prefix and/or suffix.
+ *
+ *		trimmed_size - Amount of space available for result to avoid buffer overflow.
+ *
+ *		device_size - Amount of space available for result to avoid buffer overflow.
+ *
+ * Outputs:	trimmed - Final comment with device vendor/model removed.
+ *
+ *		device	- Vendor and model.
+ *
+ * Description:	This has a tortured history.
+ *
+ *		The Kenwood TH-D7A  put ">" at the beginning of the comment.
+ *		The Kenwood TM-D700 put "]" at the beginning of the comment.
+ *		Later Kenwood models also added a single suffix character
+ *		using a character very unlikely to appear at the end of a comment.
+ *
+ *		The later convention, used by everyone else, is to have a prefix of ` or '
+ *		and a suffix of two characters.  The suffix characters need to be
+ *		something very unlikely to be found at the end of a comment.
+ *
+ *		A receiving device is expected to remove those extra characters
+ *		before displaying the comment.
+ *
+ * References:	http://www.aprs.org/aprs12/mic-e-types.txt
+ *		http://www.aprs.org/aprs12/mic-e-examples.txt
+ *
+ *------------------------------------------------------------------*/
+
+// The strncmp documentation doesn't mention behavior if length is zero.
+// Do our own just to be safe.
+
+static inline int strncmp_z (char *a, char *b, size_t len)
+{
+	int result = 0;
+	if (len > 0) {
+	  result = strncmp(a, b, len);
+	}
+	//dw_printf ("Comparing '%s' and '%s' len %d result %d\n", a, b, len, result);
+	return result;
+}
+
+void deviceid_decode_mice (char *comment, char *trimmed, size_t trimmed_size, char *device, size_t device_size)
+{
+	*device = '\0';
+
+	if (ptocalls == NULL) {
+	  text_color_set(DW_COLOR_ERROR);
+	  dw_printf("deviceid_decode_mice called without any deviceid data.\n");
+	  return;
+	}
+
+
+// The Legacy format has an explicit prefix in the table.
+// For others, it must be ` or ' to indicate whether messaging capable.
+
+	for (int n = 0; n < mice_count; n++) {
+	  if ((strlen(pmice[n].prefix) != 0 &&					// Legacy
+	      strncmp_z(comment, 						// prefix from table
+		  pmice[n].prefix,
+	          strlen(pmice[n].prefix)) == 0 &&
+	      strncmp_z(comment + strlen(comment) - strlen(pmice[n].suffix),	//suffix
+		pmice[n].suffix,
+	        strlen(pmice[n].suffix)) == 0) ||
+
+	     (strlen(pmice[n].prefix) == 0 &&					// Later
+	      (comment[0] == '`' || comment[0] == '\'')	&&			// prefix ` or '
+	      strncmp_z(comment + strlen(comment) - strlen(pmice[n].suffix),	//suffix
+		pmice[n].suffix,
+	        strlen(pmice[n].suffix)) == 0)  ) {
+
+	    if (pmice[n].vendor != NULL) {
+	      strlcpy (device, pmice[n].vendor, device_size);
+	    }
+
+	    if (pmice[n].vendor != NULL && pmice[n].model != NULL) {
+	      strlcat (device, " ", device_size);
+	    }
+
+	    if (pmice[n].model != NULL) {
+	      strlcat (device, pmice[n].model, device_size);
+	    }
+
+	    // Remove any prefix/suffix and return what remains.
+
+	    strlcpy (trimmed, comment + 1, trimmed_size);
+	    trimmed[strlen(comment) - 1 - strlen(pmice[n].suffix)] = '\0';
+
+	    return;
+	  }
+	}
+
+
+// Not found.
+
+	strlcpy (device, "UNKNOWN vendor/model", device_size);
+
+} // end deviceid_decode_mice
+
+// end deviceid.c
diff --git a/src/deviceid.h b/src/deviceid.h
new file mode 100644
index 00000000..d7a1b30e
--- /dev/null
+++ b/src/deviceid.h
@@ -0,0 +1,6 @@
+
+// deviceid.h
+
+void deviceid_init(void);
+void deviceid_decode_dest (char *dest, char *device, size_t device_size);
+void deviceid_decode_mice (char *comment, char *trimmed, size_t trimmed_size, char *device, size_t device_size);
diff --git a/src/direwolf.c b/src/direwolf.c
index c8bb3a1b..2dfa58d3 100644
--- a/src/direwolf.c
+++ b/src/direwolf.c
@@ -129,6 +129,7 @@
 #include "dwsock.h"
 #include "dns_sd_dw.h"
 #include "dlq.h"		// for fec_type_t definition.
+#include "deviceid.h"
 
 
 //static int idx_decoded = 0;
@@ -985,6 +986,7 @@ int main (int argc, char *argv[])
  * Files not supported at this time.
  * Can always "cat" the file and pipe it into stdin.
  */
+	deviceid_init();
 
 	err = audio_open (&audio_config);
 	if (err < 0) {
diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt
index 91e06a2c..da732ac8 100644
--- a/test/CMakeLists.txt
+++ b/test/CMakeLists.txt
@@ -147,6 +147,7 @@ list(APPEND dtest_SOURCES
   ${CUSTOM_SRC_DIR}/tq.c
   ${CUSTOM_SRC_DIR}/textcolor.c
   ${CUSTOM_SRC_DIR}/decode_aprs.c
+  ${CUSTOM_SRC_DIR}/deviceid.c
   ${CUSTOM_SRC_DIR}/dwgpsnmea.c
   ${CUSTOM_SRC_DIR}/dwgps.c
   ${CUSTOM_SRC_DIR}/dwgpsd.c
@@ -232,6 +233,7 @@ list(APPEND pftest_SOURCES
   ${CUSTOM_SRC_DIR}/textcolor.c
   ${CUSTOM_SRC_DIR}/fcs_calc.c
   ${CUSTOM_SRC_DIR}/decode_aprs.c
+  ${CUSTOM_SRC_DIR}/deviceid.c
   ${CUSTOM_SRC_DIR}/dwgpsnmea.c
   ${CUSTOM_SRC_DIR}/dwgps.c
   ${CUSTOM_SRC_DIR}/dwgpsd.c
@@ -522,6 +524,7 @@ if(OPTIONAL_TEST)
     ${CUSTOM_SRC_DIR}/pfilter.c
     ${CUSTOM_SRC_DIR}/telemetry.c
     ${CUSTOM_SRC_DIR}/decode_aprs.c
+    ${CUSTOM_SRC_DIR}/deviceid.c.c
     ${CUSTOM_SRC_DIR}/dwgpsnmea.c
     ${CUSTOM_SRC_DIR}/dwgps.c
     ${CUSTOM_SRC_DIR}/dwgpsd.c
@@ -574,6 +577,7 @@ if(OPTIONAL_TEST)
     ${CUSTOM_SRC_DIR}/fcs_calc.c
     ${CUSTOM_SRC_DIR}/ax25_pad.c
     ${CUSTOM_SRC_DIR}/decode_aprs.c
+    ${CUSTOM_SRC_DIR}/deviceid.c
     ${CUSTOM_SRC_DIR}/dwgpsnmea.c
     ${CUSTOM_SRC_DIR}/dwgps.c
     ${CUSTOM_SRC_DIR}/dwgpsd.c

From c9b7c61f2cb6457ba8000879e5f232ffe86f1969 Mon Sep 17 00:00:00 2001
From: wb2osz <wb2osz@comcast.net>
Date: Fri, 26 Jan 2024 00:06:04 +0000
Subject: [PATCH 10/67] Issue 510 - List only valid channels for AGW G command.

---
 src/server.c | 8 ++------
 1 file changed, 2 insertions(+), 6 deletions(-)

diff --git a/src/server.c b/src/server.c
index 6b41af25..1cd3211d 100644
--- a/src/server.c
+++ b/src/server.c
@@ -1559,6 +1559,7 @@ static THREAD_F cmd_listen_thread (void *arg)
 
 	            case MEDIUM_RADIO:
 	              {
+	                // Misleading if using stdin or udp.
 		        char stemp[100];
 		        int a = ACHAN2ADEV(j);
 		        // If I was really ambitious, some description could be provided.
@@ -1593,12 +1594,7 @@ static THREAD_F cmd_listen_thread (void *arg)
 	              break;
 
 	            default:
-	              {
-	                // could elaborate with hostname, etc.
-		        char stemp[100];
-		        snprintf (stemp, sizeof(stemp), "Port%d INVALID CHANNEL;", j+1);
-		        strlcat (reply.info, stemp, sizeof(reply.info));
-	              }
+	              ; // Only list valid channels.
 	              break;
 
 		  }  // switch

From 78604808f857aa6b2eb193282f60f4d5fe2707fc Mon Sep 17 00:00:00 2001
From: wb2osz <wb2osz@comcast.net>
Date: Tue, 30 Jan 2024 17:19:46 +0000
Subject: [PATCH 11/67] Add hint for better operation.

---
 src/ax25_link.c | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/src/ax25_link.c b/src/ax25_link.c
index 3043a046..98d6c45e 100644
--- a/src/ax25_link.c
+++ b/src/ax25_link.c
@@ -4642,6 +4642,8 @@ static void dm_frame (ax25_dlsm_t *S, int f)
 	    if (f == 1) {
 	      text_color_set(DW_COLOR_INFO);
 	      dw_printf ("%s doesn't understand AX.25 v2.2.  Trying v2.0 ...\n", S->addrs[PEERCALL]);
+	      dw_printf ("You can avoid this failed attempt and speed up the\n");
+	      dw_printf ("process by putting \"V20 %s\" in the configuration file.\n", S->addrs[PEERCALL]);
 	
 	      INIT_T1V_SRT;
 
@@ -4930,6 +4932,8 @@ static void frmr_frame (ax25_dlsm_t *S)
 
 	    text_color_set(DW_COLOR_INFO);
 	    dw_printf ("%s doesn't understand AX.25 v2.2.  Trying v2.0 ...\n", S->addrs[PEERCALL]);
+	    dw_printf ("You can avoid this failed attempt and speed up the\n");
+	    dw_printf ("process by putting \"V20 %s\" in the configuration file.\n", S->addrs[PEERCALL]);
 	
 	    INIT_T1V_SRT;
 

From 4af7b22fa9096335b7c0ef6cf5232bd5bad27fd9 Mon Sep 17 00:00:00 2001
From: wb2osz <wb2osz@comcast.net>
Date: Wed, 31 Jan 2024 23:50:00 +0000
Subject: [PATCH 12/67] AGW 'K' Monitor in Raw Format did not have 'Flag' field
 set with channel.

---
 src/server.c | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/server.c b/src/server.c
index 1cd3211d..9e4fa51c 100644
--- a/src/server.c
+++ b/src/server.c
@@ -820,7 +820,7 @@ void server_send_rec_packet (int chan, packet_t pp, unsigned char *fbuf,  int fl
 
 	    /* Stick in extra byte for the "TNC" to use. */
 
-	    agwpe_msg.data[0] = 0;
+	    agwpe_msg.data[0] = chan << 4;		// Was 0.  Fixed in 1.8.
 	    memcpy (agwpe_msg.data + 1, fbuf, (size_t)flen);
 
 	    if (debug_client) {

From 4d2d814ee1651cdba4f13de5e7fffc82bb6fbad5 Mon Sep 17 00:00:00 2001
From: wb2osz <wb2osz@comcast.net>
Date: Sun, 4 Feb 2024 22:40:40 +0000
Subject: [PATCH 13/67] Proper color for informational text.

---
 src/deviceid.c | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/src/deviceid.c b/src/deviceid.c
index 594b20ea..221e65bd 100644
--- a/src/deviceid.c
+++ b/src/deviceid.c
@@ -240,7 +240,10 @@ void deviceid_init(void)
 {
 	FILE *fp = NULL;
 	for (int n = 0; search_locations[n] != NULL && fp == NULL; n++) {
+#if TEST
+	  text_color_set(DW_COLOR_INFO);
 	  dw_printf ("Trying %s\n", search_locations[n]);
+#endif
 	  fp = fopen(search_locations[n], "r");
 #if TEST
 	  if (fp != NULL) {

From 5a54179c97b8f46a9fb315ee511cd026a559c353 Mon Sep 17 00:00:00 2001
From: wb2osz <wb2osz@comcast.net>
Date: Fri, 16 Feb 2024 02:52:28 +0000
Subject: [PATCH 14/67] more work on mic-e device id.

---
 data/README.txt   |  18 ++++++++
 data/tocalls.yaml | 113 +++++++++++++++++++++++++++++++++++++++++++++-
 src/decode_aprs.c |  23 ++++------
 src/deviceid.c    |   9 +++-
 4 files changed, 145 insertions(+), 18 deletions(-)
 create mode 100644 data/README.txt

diff --git a/data/README.txt b/data/README.txt
new file mode 100644
index 00000000..9d4da43c
--- /dev/null
+++ b/data/README.txt
@@ -0,0 +1,18 @@
+
+tocalls.yaml contains the encoding for the device/system/software
+identifier which created the packet.
+Knowing what generated the packet is very useful for troubleshooting.
+TNCs, digipeaters, and IGates must not change this. 
+
+For MIC-E format, well... it's complicated.
+See  Understanding-APRS-Packets.pdf.   Too long to repeat here.
+
+For all other packet types, the AX.25 destination, or "tocall" field
+contains a code for what generated the packet.
+This is of the form AP????.    For example, APDW18 for direwolf 1.8.
+
+The database of identifiers is currently maintained by Hessu, OH7LZB.
+
+You can update your local copy by running:
+
+wget https://raw.githubusercontent.com/aprsorg/aprs-deviceid/main/tocalls.yaml
diff --git a/data/tocalls.yaml b/data/tocalls.yaml
index adf26a1f..dfc5fd08 100644
--- a/data/tocalls.yaml
+++ b/data/tocalls.yaml
@@ -146,6 +146,11 @@ mice:
    model: Tracker
    class: tracker
 
+ - suffix: ":2"
+   vendor: SQ8L
+   model: VP-Tracker
+   class: tracker
+
 #
 # mic-e legacy devices, with an unique comment suffix and prefix
 #
@@ -237,6 +242,15 @@ tocalls:
  - tocall: APAH??
    model: AHub
 
+ - tocall: APAIOR
+   vendor: J. Angelo Racoma DU2XXR/N2RAC
+   model: APRSPH net bot based on Ioreth
+   class: service
+   os: linux
+   contact: info@aprsph.net
+   features:
+     - messaging
+
  - tocall: APAM??
    vendor: Altus Metrum
    model: AltOS
@@ -248,6 +262,12 @@ tocalls:
    os: Android
    class: app
 
+ - tocall: APAR??
+   vendor: Øyvind, LA7ECA
+   model: Arctic Tracker
+   class: tracker
+   os: embedded
+
  - tocall: APAT51
    vendor: Anytone
    model: AT-D578
@@ -364,6 +384,12 @@ tocalls:
    model: WinphoneAPRS
    class: app
 
+ - tocall: APD5T?
+   vendor: Geoffrey, F4FXL
+   model: Open Source DStarGateway
+   class: dstar
+   contact: f4fxl@dstargateway.digital
+
  - tocall: APDF??
    model: Automatic DF units
 
@@ -448,6 +474,19 @@ tocalls:
    model: Balloon tracker
    class: tracker
 
+ - tocall: APEML?
+   vendor: Leszek, SP9MLI
+   model: SP9MLI for WX, Telemetry
+   class: software
+   contact: sp9mli@gmail.com
+
+ - tocall: APEP??
+   vendor: Patrick EGLOFF, TK5EP
+   model: LoRa WX station
+   class: wx
+   os: embedded
+   contact: pegloff@gmail.com
+
  - tocall: APERS?
    vendor: Jason, KG7YKZ
    model: Runner tracking
@@ -525,6 +564,17 @@ tocalls:
    vendor: HP3ICC
    model: Python APRS WX
 
+ - tocall: APHRM?
+   vendor: Giovanni, IW1CGW
+   model: Meteo
+   class: wx
+   contact: iw1cgw@libero.it
+
+ - tocall: APHRT?
+   vendor: Giovanni, IW1CGW
+   model: Telemetry
+   contact: iw1cgw@libero.it
+
  - tocall: APHT??
    vendor: IU0AAC
    model: HMTracker
@@ -702,11 +752,29 @@ tocalls:
    os: embedded
    contact: cfr34k-git@tkolb.de
 
+ - tocall: APLFM?
+   vendor: DO1MA
+   model: FemtoAPRS
+   class: tracker
+   os: embedded
+
  - tocall: APLG??
    vendor: OE5BPA
    model: LoRa Gateway/Digipeater
    class: digi
 
+ - tocall: APLHI?
+   vendor: Giovanni, IW1CGW
+   model: LoRa IGate/Digipeater/Telemetry
+   class: digi
+   contact: iw1cgw@libero.it
+
+ - tocall: APLHM?
+   vendor: Giovanni, IW1CGW
+   model: LoRa Meteostation
+   class: wx
+   contact: iw1cgw@libero.it
+
  - tocall: APLIG?
    vendor: TA2MUN/TA9OHC
    model: LightAPRS Tracker
@@ -736,14 +804,14 @@ tocalls:
    contact: sq9p.peter@gmail.com
 
  - tocall: APLRG?
-   vendor: Ricardo, CD2RXU
+   vendor: Ricardo, CA2RXU
    model: ESP32 LoRa iGate
    class: igate
    os: embedded
    contact: richonguzman@gmail.com
 
  - tocall: APLRT?
-   vendor: Ricardo, CD2RXU
+   vendor: Ricardo, CA2RXU
    model: ESP32 LoRa Tracker
    class: tracker
    os: embedded
@@ -919,12 +987,18 @@ tocalls:
    vendor: SQ8L
    model: VP-Digi
    os: embedded
+   class: digi
 
  - tocall: APNV1?
    vendor: SQ8L
    model: VP-Node
    os: embedded
 
+ - tocall: APNV2?
+   vendor: SQ8L
+   model: VP-Tracker
+   class: tracker
+
  - tocall: APNV??
    vendor: SQ8L
 
@@ -947,6 +1021,11 @@ tocalls:
    vendor: N0AGI
    model: POCSAG
 
+ - tocall: APODOT
+   vendor: Mike, NA7Q
+   model: Oregon Department of Transportion Traffic Alerts
+   class: service
+
  - tocall: APOG7?
    vendor: OpenGD77
    model: OpenGD77
@@ -958,6 +1037,12 @@ tocalls:
    model: Oscar
    class: satellite
 
+ - tocall: APOPYT
+   vendor: Mike, NA7Q
+   model: NA7Q Messenger
+   class: software
+   contact: mike.ph4@gmail.com
+
  - tocall: APOSAT
    vendor: Mike, NA7Q
    model: Open Source Satellite Gateway
@@ -1014,6 +1099,12 @@ tocalls:
    features:
      - messaging
 
+ - tocall: APPS??
+   vendor: Øyvind, LA7ECA (for the Norwegian Radio Relay League)
+   model: Polaric Server
+   class: software
+   os: Linux
+
  - tocall: APPT??
    vendor: JF6LZE
    model: KetaiTracker
@@ -1276,11 +1367,25 @@ tocalls:
    os: Linux/Unix
    contact: kl7af@foghaven.net
 
+ - tocall: APTHUR
+   model: APRSThursday weekly event mapbot daemon
+   contact: harihend1973@gmail.com
+   vendor: YD0BCX
+   class: service
+   os: linux/unix
+   features:
+     - messaging
+
  - tocall: APTKJ?
    vendor: W9JAJ
    model: ATTiny APRS Tracker
    os: embedded
 
+ - tocall: APTLVC
+   vendor: TA5LVC
+   model: TR80 APRS Tracker
+   class: tracker
+
  - tocall: APTNG?
    vendor: Filip YU1TTN
    model: Tango Tracker
@@ -1418,6 +1523,10 @@ tocalls:
    model: Python APRS
    class: software
 
+ - tocall: "APZ*"
+   vendor: Unknown
+   model: Experimental
+
  - tocall: APZ18
    vendor: IW3FQG
    model: UIdigi
diff --git a/src/decode_aprs.c b/src/decode_aprs.c
index 54c2839d..08534f7a 100644
--- a/src/decode_aprs.c
+++ b/src/decode_aprs.c
@@ -1638,28 +1638,23 @@ static void aprs_mic_e (decode_aprs_t *A, packet_t pp, unsigned char *info, int
 
 
 
-// Possible altitude.  3 characters followed by }
+// Possible altitude at beginning of remaining comment.
+// Three base 91 characters followed by }
 
 
-	if (strlen(trimmed) >=4 && trimmed[3] == '}') {
+	if (strlen(trimmed) >=4 &&
+			isdigit91(trimmed[0]) &&
+			isdigit91(trimmed[1]) &&
+			isdigit91(trimmed[2]) &&
+			trimmed[3] == '}') {
 
 	  A->g_altitude_ft = DW_METERS_TO_FEET((trimmed[0]-33)*91*91 + (trimmed[1]-33)*91 + (trimmed[2]-33) - 10000);
 
-	  if ( ! isdigit91(trimmed[0]) || ! isdigit91(trimmed[1]) || ! isdigit91(trimmed[2])) 
-	  {
-	    if ( ! A->g_quiet) {
-	      text_color_set(DW_COLOR_ERROR);
-	      dw_printf("Invalid character in MIC-E altitude.  Must be in range of '!' to '{'.\n");
-	      dw_printf("Bogus altitude of %.0f changed to unknown.\n", A->g_altitude_ft);
-	    }
-	    A->g_altitude_ft = G_UNKNOWN;
-	  }
-	  
-	  process_comment (A, mcomment+4, strlen(mcomment) - 4);
+	  process_comment (A, trimmed+4, strlen(trimmed) - 4);
 	  return;
 	}
 
-	process_comment (A, mcomment, strlen(mcomment));
+	process_comment (A, trimmed, strlen(trimmed));
 
 }  // end aprs_mic_e
 
diff --git a/src/deviceid.c b/src/deviceid.c
index 221e65bd..de910e50 100644
--- a/src/deviceid.c
+++ b/src/deviceid.c
@@ -107,6 +107,11 @@ int main (int argc, char *argv[])
 	assert (strcmp(comment_out, "Comment") == 0);
 	assert (strcmp(device, "Kenwood TM-D710") == 0);
 
+	deviceid_decode_mice ("]\"4V}=", comment_out, sizeof(comment_out), device, sizeof(device));
+	dw_printf ("%s %s\n", comment_out, device);
+	assert (strcmp(comment_out, "\"4V}") == 0);
+	assert (strcmp(device, "Kenwood TM-D710") == 0);
+
 
 // Modern MIC-E.
 
@@ -622,13 +627,13 @@ void deviceid_decode_mice (char *comment, char *trimmed, size_t trimmed_size, ch
 	      strncmp_z(comment, 						// prefix from table
 		  pmice[n].prefix,
 	          strlen(pmice[n].prefix)) == 0 &&
-	      strncmp_z(comment + strlen(comment) - strlen(pmice[n].suffix),	//suffix
+	      strncmp_z(comment + strlen(comment) - strlen(pmice[n].suffix),	// possible suffix
 		pmice[n].suffix,
 	        strlen(pmice[n].suffix)) == 0) ||
 
 	     (strlen(pmice[n].prefix) == 0 &&					// Later
 	      (comment[0] == '`' || comment[0] == '\'')	&&			// prefix ` or '
-	      strncmp_z(comment + strlen(comment) - strlen(pmice[n].suffix),	//suffix
+	      strncmp_z(comment + strlen(comment) - strlen(pmice[n].suffix),	// suffix
 		pmice[n].suffix,
 	        strlen(pmice[n].suffix)) == 0)  ) {
 

From a508a76a52db55a4f73bc4e18e8f4f4174163b21 Mon Sep 17 00:00:00 2001
From: Rafael Gustavo da Cunha Pereira Pinto <rafaelgcpp@gmail.com>
Date: Tue, 5 Mar 2024 08:58:50 -0300
Subject: [PATCH 15/67] Update codeql-analysis.yml to v2

---
 .github/workflows/codeql-analysis.yml | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml
index 7134f213..5eb1f566 100644
--- a/.github/workflows/codeql-analysis.yml
+++ b/.github/workflows/codeql-analysis.yml
@@ -42,7 +42,7 @@ jobs:
 
     # Initializes the CodeQL tools for scanning.
     - name: Initialize CodeQL
-      uses: github/codeql-action/init@v1
+      uses: github/codeql-action/init@v2
       with:
         languages: ${{ matrix.language }}
         # If you wish to specify custom queries, you can do so here or in a config file.
@@ -53,7 +53,7 @@ jobs:
     # Autobuild attempts to build any compiled languages  (C/C++, C#, or Java).
     # If this step fails, then you should remove it and run the build manually (see below)
     - name: Autobuild
-      uses: github/codeql-action/autobuild@v1
+      uses: github/codeql-action/autobuild@v2
 
     # ℹ️ Command-line programs to run using the OS shell.
     # 📚 https://git.io/JvXDl
@@ -70,4 +70,4 @@ jobs:
        make test
 
     - name: Perform CodeQL Analysis
-      uses: github/codeql-action/analyze@v1
+      uses: github/codeql-action/analyze@v2

From 26013e10579d4e0e357ec5515d337385d5ec0b56 Mon Sep 17 00:00:00 2001
From: Rafael Gustavo da Cunha Pereira Pinto <rafaelgcpp@gmail.com>
Date: Tue, 5 Mar 2024 09:01:48 -0300
Subject: [PATCH 16/67] Update codeql-analysis.yml

---
 .github/workflows/codeql-analysis.yml | 1 +
 1 file changed, 1 insertion(+)

diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml
index 5eb1f566..b2492841 100644
--- a/.github/workflows/codeql-analysis.yml
+++ b/.github/workflows/codeql-analysis.yml
@@ -19,6 +19,7 @@ on:
     branches: [ dev ]
   schedule:
     - cron: '25 8 * * 4'
+  workflow_dispatch:
 
 jobs:
   analyze:

From 2c3e987a266e4c066502c88ebcbca3c7ad980427 Mon Sep 17 00:00:00 2001
From: Rafael Gustavo da Cunha Pereira Pinto <rafaelgcpp@gmail.com>
Date: Tue, 5 Mar 2024 09:07:45 -0300
Subject: [PATCH 17/67] Update CMakeLists.txt

---
 CMakeLists.txt | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/CMakeLists.txt b/CMakeLists.txt
index 84aeb738..182a9b4d 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -1,4 +1,4 @@
-cmake_minimum_required(VERSION 3.1.0)
+cmake_minimum_required(VERSION 3.5.0)
 
 project(direwolf)
 

From fc3d2e52c3dd5bd2794a0cdeb09e5a87abca2aa6 Mon Sep 17 00:00:00 2001
From: Rafael Gustavo da Cunha Pereira Pinto <rafaelgcpp@gmail.com>
Date: Tue, 5 Mar 2024 09:18:16 -0300
Subject: [PATCH 18/67] Update codeql-analysis.yml

---
 .github/workflows/codeql-analysis.yml | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml
index b2492841..4239b3eb 100644
--- a/.github/workflows/codeql-analysis.yml
+++ b/.github/workflows/codeql-analysis.yml
@@ -39,11 +39,11 @@ jobs:
 
     steps:
     - name: Checkout repository
-      uses: actions/checkout@v2
+      uses: actions/checkout@v3
 
     # Initializes the CodeQL tools for scanning.
     - name: Initialize CodeQL
-      uses: github/codeql-action/init@v2
+      uses: github/codeql-action/init@v3
       with:
         languages: ${{ matrix.language }}
         # If you wish to specify custom queries, you can do so here or in a config file.
@@ -54,7 +54,7 @@ jobs:
     # Autobuild attempts to build any compiled languages  (C/C++, C#, or Java).
     # If this step fails, then you should remove it and run the build manually (see below)
     - name: Autobuild
-      uses: github/codeql-action/autobuild@v2
+      uses: github/codeql-action/autobuild@v3
 
     # ℹ️ Command-line programs to run using the OS shell.
     # 📚 https://git.io/JvXDl
@@ -71,4 +71,4 @@ jobs:
        make test
 
     - name: Perform CodeQL Analysis
-      uses: github/codeql-action/analyze@v2
+      uses: github/codeql-action/analyze@v3

From 1325b97475ff10b4d592a9445ccb6ce0ca9801d7 Mon Sep 17 00:00:00 2001
From: Rafael Gustavo da Cunha Pereira Pinto <rafaelgcpp@gmail.com>
Date: Tue, 5 Mar 2024 14:13:53 -0300
Subject: [PATCH 19/67] Update codeql-analysis.yml

---
 .github/workflows/codeql-analysis.yml | 1 +
 1 file changed, 1 insertion(+)

diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml
index 4239b3eb..021dbe55 100644
--- a/.github/workflows/codeql-analysis.yml
+++ b/.github/workflows/codeql-analysis.yml
@@ -46,6 +46,7 @@ jobs:
       uses: github/codeql-action/init@v3
       with:
         languages: ${{ matrix.language }}
+        setup-python-dependencies: false
         # If you wish to specify custom queries, you can do so here or in a config file.
         # By default, queries listed here will override any specified in a config file.
         # Prefix the list here with "+" to use these queries and those in the config file.

From d462fda79f7c5bb2f585dba24b4599aed8ce2606 Mon Sep 17 00:00:00 2001
From: Rafael Gustavo da Cunha Pereira Pinto <rafaelgcpp@gmail.com>
Date: Tue, 5 Mar 2024 14:18:29 -0300
Subject: [PATCH 20/67] Update codeql-analysis.yml

---
 .github/workflows/codeql-analysis.yml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml
index 021dbe55..956bb39e 100644
--- a/.github/workflows/codeql-analysis.yml
+++ b/.github/workflows/codeql-analysis.yml
@@ -46,7 +46,7 @@ jobs:
       uses: github/codeql-action/init@v3
       with:
         languages: ${{ matrix.language }}
-        setup-python-dependencies: false
+        setup-python-dependencies: true
         # If you wish to specify custom queries, you can do so here or in a config file.
         # By default, queries listed here will override any specified in a config file.
         # Prefix the list here with "+" to use these queries and those in the config file.

From 274fed556da38f1d8924b3c9db1bed0114fe9d1f Mon Sep 17 00:00:00 2001
From: Rafael Gustavo da Cunha Pereira Pinto <rafaelgcpp@gmail.com>
Date: Wed, 6 Mar 2024 09:39:20 -0300
Subject: [PATCH 21/67] removing python from codeql-analysis.yml

---
 .github/workflows/codeql-analysis.yml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml
index 956bb39e..edd6a96b 100644
--- a/.github/workflows/codeql-analysis.yml
+++ b/.github/workflows/codeql-analysis.yml
@@ -33,7 +33,7 @@ jobs:
     strategy:
       fail-fast: false
       matrix:
-        language: [ 'cpp', 'python' ]
+        language: [ 'cpp' ]
         # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
         # Learn more about CodeQL language support at https://git.io/codeql-language-support
 

From a2c88f320feeed3876d34a5a50c44ea43775c209 Mon Sep 17 00:00:00 2001
From: Rafael Gustavo da Cunha Pereira Pinto <rafaelgcpp@gmail.com>
Date: Wed, 6 Mar 2024 09:40:16 -0300
Subject: [PATCH 22/67] Create codeql-analysis-python.yml

---
 .github/workflows/codeql-analysis-python.yml | 65 ++++++++++++++++++++
 1 file changed, 65 insertions(+)
 create mode 100644 .github/workflows/codeql-analysis-python.yml

diff --git a/.github/workflows/codeql-analysis-python.yml b/.github/workflows/codeql-analysis-python.yml
new file mode 100644
index 00000000..35cf0404
--- /dev/null
+++ b/.github/workflows/codeql-analysis-python.yml
@@ -0,0 +1,65 @@
+# For most projects, this workflow file will not need changing; you simply need
+# to commit it to your repository.
+#
+# You may wish to alter this file to override the set of languages analyzed,
+# or to provide custom queries or build logic.
+#
+# ******** NOTE ********
+# We have attempted to detect the languages in your repository. Please check
+# the `language` matrix defined below to confirm you have the correct set of
+# supported CodeQL languages.
+#
+name: "CodeQL"
+
+on:
+  push:
+    branches: [ dev ]
+  pull_request:
+    # The branches below must be a subset of the branches above
+    branches: [ dev ]
+  schedule:
+    - cron: '25 8 * * 4'
+  workflow_dispatch:
+
+jobs:
+  analyze:
+    name: Analyze
+    runs-on: ubuntu-latest
+    permissions:
+      actions: read
+      contents: read
+      security-events: write
+
+    strategy:
+      fail-fast: false
+      matrix:
+        language: [ 'python' ]
+        # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
+        # Learn more about CodeQL language support at https://git.io/codeql-language-support
+
+    steps:
+    - name: Checkout repository
+      uses: actions/checkout@v3
+
+    # Initializes the CodeQL tools for scanning.
+    - name: Initialize CodeQL
+      uses: github/codeql-action/init@v3
+      with:
+        languages: ${{ matrix.language }}
+        setup-python-dependencies: true
+        # If you wish to specify custom queries, you can do so here or in a config file.
+        # By default, queries listed here will override any specified in a config file.
+        # Prefix the list here with "+" to use these queries and those in the config file.
+        # queries: ./path/to/local/query, your-org/your-repo/queries@main
+
+    # Autobuild attempts to build any compiled languages  (C/C++, C#, or Java).
+    # If this step fails, then you should remove it and run the build manually (see below)
+    - name: Autobuild
+      uses: github/codeql-action/autobuild@v3
+
+    # ℹ️ Command-line programs to run using the OS shell.
+    # 📚 https://git.io/JvXDl
+
+
+    - name: Perform CodeQL Analysis
+      uses: github/codeql-action/analyze@v3

From 327aba27ce8591e4583db9099240e867c30ad2ca Mon Sep 17 00:00:00 2001
From: Rafael Gustavo da Cunha Pereira Pinto <rafaelgcpp@gmail.com>
Date: Wed, 6 Mar 2024 09:40:45 -0300
Subject: [PATCH 23/67] Update codeql-analysis.yml

---
 .github/workflows/codeql-analysis.yml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml
index edd6a96b..d445a168 100644
--- a/.github/workflows/codeql-analysis.yml
+++ b/.github/workflows/codeql-analysis.yml
@@ -9,7 +9,7 @@
 # the `language` matrix defined below to confirm you have the correct set of
 # supported CodeQL languages.
 #
-name: "CodeQL"
+name: "CodeQL - CPP"
 
 on:
   push:

From 921f3be253040cc65fcd83fcfce0fdc81f1fd282 Mon Sep 17 00:00:00 2001
From: Rafael Gustavo da Cunha Pereira Pinto <rafaelgcpp@gmail.com>
Date: Wed, 6 Mar 2024 09:41:05 -0300
Subject: [PATCH 24/67] Update codeql-analysis-python.yml

---
 .github/workflows/codeql-analysis-python.yml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/.github/workflows/codeql-analysis-python.yml b/.github/workflows/codeql-analysis-python.yml
index 35cf0404..15a4c0b6 100644
--- a/.github/workflows/codeql-analysis-python.yml
+++ b/.github/workflows/codeql-analysis-python.yml
@@ -9,7 +9,7 @@
 # the `language` matrix defined below to confirm you have the correct set of
 # supported CodeQL languages.
 #
-name: "CodeQL"
+name: "CodeQL - Python"
 
 on:
   push:

From 1424cc942b9a5fb1e583fe33a207b7a6c00efebb Mon Sep 17 00:00:00 2001
From: Rafael Gustavo da Cunha Pereira Pinto <rafaelgcpp@gmail.com>
Date: Wed, 6 Mar 2024 09:43:44 -0300
Subject: [PATCH 25/67] Update codeql-analysis-python.yml

---
 .github/workflows/codeql-analysis-python.yml | 1 -
 1 file changed, 1 deletion(-)

diff --git a/.github/workflows/codeql-analysis-python.yml b/.github/workflows/codeql-analysis-python.yml
index 15a4c0b6..a47a8f8d 100644
--- a/.github/workflows/codeql-analysis-python.yml
+++ b/.github/workflows/codeql-analysis-python.yml
@@ -19,7 +19,6 @@ on:
     branches: [ dev ]
   schedule:
     - cron: '25 8 * * 4'
-  workflow_dispatch:
 
 jobs:
   analyze:

From 93572513d4879f97a3d3a2ee993354fb2b04e658 Mon Sep 17 00:00:00 2001
From: Rafael Gustavo da Cunha Pereira Pinto <rafaelgcpp@gmail.com>
Date: Wed, 6 Mar 2024 09:43:59 -0300
Subject: [PATCH 26/67] Update codeql-analysis.yml

---
 .github/workflows/codeql-analysis.yml | 1 -
 1 file changed, 1 deletion(-)

diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml
index d445a168..a86300f3 100644
--- a/.github/workflows/codeql-analysis.yml
+++ b/.github/workflows/codeql-analysis.yml
@@ -19,7 +19,6 @@ on:
     branches: [ dev ]
   schedule:
     - cron: '25 8 * * 4'
-  workflow_dispatch:
 
 jobs:
   analyze:

From e41a7f2278c0512719bc98b71bb4ed855abb9ffc Mon Sep 17 00:00:00 2001
From: wb2osz <wb2osz@comcast.net>
Date: Sat, 9 Mar 2024 18:50:58 +0000
Subject: [PATCH 27/67] update comment

---
 src/server.c | 8 +++++++-
 1 file changed, 7 insertions(+), 1 deletion(-)

diff --git a/src/server.c b/src/server.c
index 9e4fa51c..866b58ae 100644
--- a/src/server.c
+++ b/src/server.c
@@ -1780,11 +1780,17 @@ static THREAD_F cmd_listen_thread (void *arg)
 		//		00=Port 1
 		//		16=Port 2
 		//
-		// I don't know what that means; we already a port number in the header.
+		// The seems to be redundant; we already a port number in the header.
 		// Anyhow, the original code here added one to cmd.data to get the 
 		// first byte of the frame.  Unfortunately, it did not subtract one from
 		// cmd.hdr.data_len so we ended up sending an extra byte.
 
+	        // TODO: Right now I just use the port (channel) number in the header.
+		// What if the second one is inconsistent?  
+		// - Continue to ignore port number at beginning of data?
+		// - Use second one instead?
+		// - Error message if a mismatch?
+
 		memset (&alevel, 0xff, sizeof(alevel));
 		pp = ax25_from_frame ((unsigned char *)cmd.data+1, data_len - 1, alevel);
 

From 6be6f686a907077f2cd6165ab14ea17dc87a581d Mon Sep 17 00:00:00 2001
From: wb2osz <wb2osz@comcast.net>
Date: Wed, 13 Mar 2024 01:09:28 +0100
Subject: [PATCH 28/67] Latest device identifiers.

---
 data/tocalls.yaml | 51 +++++++++++++++++++++++++++++++++++++++++++++--
 1 file changed, 49 insertions(+), 2 deletions(-)

diff --git a/data/tocalls.yaml b/data/tocalls.yaml
index dfc5fd08..0df04785 100644
--- a/data/tocalls.yaml
+++ b/data/tocalls.yaml
@@ -107,10 +107,20 @@ mice:
    model: FTM-300D
    class: rig
 
+ - suffix: "_2"
+   vendor: Yaesu
+   model: FTM-200D
+   class: rig
+
+ - suffix: "_4"
+   vendor: Yaesu
+   model: FTM-500D
+   class: rig
+
  - suffix: "_)"
    vendor: Yaesu
    model: FTM-100D
-   class: rig 
+   class: rig
    
  - suffix: "_%"
    vendor: Yaesu
@@ -735,6 +745,20 @@ tocalls:
    vendor: DL3DCW
    model: APRScube
 
+ - tocall: APLDG?
+   vendor: Eddie, 9W2LWK
+   model: LoRAIGate
+   class: igate
+   os: embedded
+   contact: 9w2lwk@gmail.com
+
+ - tocall: APLDH?
+   vendor: Eddie, 9W2LWK
+   model: LoraTracker
+   class: tracker
+   os: embedded
+   contact: 9w2lwk@gmail.com
+
  - tocall: APLDI?
    vendor: David, OK2DDS
    model: LoRa IGate/Digipeater
@@ -752,6 +776,13 @@ tocalls:
    os: embedded
    contact: cfr34k-git@tkolb.de
 
+ - tocall: APLFG?
+   vendor: Gabor, HG3FUG
+   model: LoRa WX station
+   class: wx
+   os: embedded
+   contact: hg3fug@fazi.hu
+
  - tocall: APLFM?
    vendor: DO1MA
    model: FemtoAPRS
@@ -841,6 +872,12 @@ tocalls:
    os: embedded
    contact: wajdzik.m@gmail.com
 
+ - tocall: APMAIL
+   vendor: Mike, NA7Q
+   model: APRS Mailbox
+   class: service
+   contact: mike.ph4@gmail.com
+
  - tocall: APMG??
    vendor: Alex, AB0TJ
    model: PiCrumbs and MiniGate
@@ -1508,6 +1545,11 @@ tocalls:
    model: FT5D
    class: ht
 
+ - tocall: APY200
+   vendor: Yaesu
+   model: FTM-200D
+   class: rig
+
  - tocall: APY300
    vendor: Yaesu
    model: FTM-300D
@@ -1517,7 +1559,12 @@ tocalls:
    vendor: Yaesu
    model: FTM-400
    class: rig
-   
+
+ - tocall: APY500
+   vendor: Yaesu
+   model: FTM-500D
+   class: rig
+
  - tocall: APYS??
    vendor: W2GMD
    model: Python APRS

From f233bfb7836f638488ad5ca657396fb578305bb4 Mon Sep 17 00:00:00 2001
From: MrColdboot <33491243+MrColdboot@users.noreply.github.com>
Date: Wed, 24 Apr 2024 10:13:59 -0400
Subject: [PATCH 29/67] Update deprecated CMake command `exec_program()` (#526)

Per the cmake documentation:

  > *Changed in version 3.28:* This command is available only if policy CMP0153
  >   is not set to NEW. Port projects to the execute_process() command.
  >
  > *Deprecated since version 3.0:* Use the execute_process() command instead.

To avoid warnings or future errors, calls to `exec_program()` have been
replaced with the recommended `execute_process()` command.
---
 cmake/include/uninstall.cmake.in | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/cmake/include/uninstall.cmake.in b/cmake/include/uninstall.cmake.in
index 2037e365..8ddc56a6 100644
--- a/cmake/include/uninstall.cmake.in
+++ b/cmake/include/uninstall.cmake.in
@@ -7,10 +7,10 @@ string(REGEX REPLACE "\n" ";" files "${files}")
 foreach(file ${files})
   message(STATUS "Uninstalling $ENV{DESTDIR}${file}")
   if(IS_SYMLINK "$ENV{DESTDIR}${file}" OR EXISTS "$ENV{DESTDIR}${file}")
-    exec_program(
-      "@CMAKE_COMMAND@" ARGS "-E remove \"$ENV{DESTDIR}${file}\""
+    execute_process(
+      COMMAND "@CMAKE_COMMAND@" -E remove "$ENV{DESTDIR}${file}"
       OUTPUT_VARIABLE rm_out
-      RETURN_VALUE rm_retval
+      RESULT_VARIABLE rm_retval
       )
     if(NOT "${rm_retval}" STREQUAL 0)
       message(FATAL_ERROR "Problem when removing $ENV{DESTDIR}${file}")

From 75d910c743dc838d8e3d5f4cba3fd30e6d2f45c6 Mon Sep 17 00:00:00 2001
From: wb2osz <wb2osz@comcast.net>
Date: Mon, 6 May 2024 19:17:42 +0100
Subject: [PATCH 30/67] Issues 527, 528: AGW protocol compatible with NET/ROM.

---
 src/ax25_pad.c | 53 ++++++++++++++++++++++++++++++++++++++++
 src/ax25_pad.h |  2 ++
 src/server.c   | 66 ++++++++++++++++++++++++++------------------------
 3 files changed, 90 insertions(+), 31 deletions(-)

diff --git a/src/ax25_pad.c b/src/ax25_pad.c
index 0f075808..4a526386 100644
--- a/src/ax25_pad.c
+++ b/src/ax25_pad.c
@@ -2579,6 +2579,59 @@ int ax25_get_c2 (packet_t this_p)
 }
 
 
+/*------------------------------------------------------------------
+ *
+ * Function:	ax25_set_pid
+ *
+ * Purpose:	Set protocol ID in packet.
+ *
+ * Inputs:	this_p	- pointer to packet object.
+ *
+ *		pid - usually 0xF0 for APRS or 0xCF for NET/ROM.
+ *
+ * AX.25:	"The Protocol Identifier (PID) field appears in information
+ *		 frames (I and UI) only. It identifies which kind of
+ *		 Layer 3 protocol, if any, is in use."
+ *
+ *------------------------------------------------------------------*/
+
+void ax25_set_pid (packet_t this_p, int pid)
+{
+	assert (this_p->magic1 == MAGIC);
+	assert (this_p->magic2 == MAGIC);
+
+	// Some applications set this to 0 which is an error.
+	// Change 0 to 0xF0 meaning no layer 3 protocol.
+
+	if (pid == 0) {
+	  pid = AX25_PID_NO_LAYER_3;
+	}
+
+	// Sanity check: is it I or UI frame?
+
+	if (this_p->frame_len == 0) return;
+
+	ax25_frame_type_t frame_type;
+	cmdres_t cr;			// command or response.
+	char description[64];
+	int pf;				// Poll/Final.
+	int nr, ns;			// Sequence numbers.
+
+	frame_type = ax25_frame_type (this_p, &cr, description, &pf, &nr, &ns);
+
+	if (frame_type != frame_type_I && frame_type != frame_type_U_UI) {
+          text_color_set(DW_COLOR_ERROR);
+          dw_printf ("ax25_set_pid(0x%2x): Packet type is not I or UI.\n", pid);
+	  return;
+	}
+
+	// TODO: handle 2 control byte case.
+	if (this_p->num_addr >= 2) {
+	  this_p->frame_data[ax25_get_pid_offset(this_p)] = pid;
+	}
+}
+
+
 /*------------------------------------------------------------------
  *
  * Function:	ax25_get_pid
diff --git a/src/ax25_pad.h b/src/ax25_pad.h
index cdb84c65..6d3d5cb2 100644
--- a/src/ax25_pad.h
+++ b/src/ax25_pad.h
@@ -66,6 +66,7 @@
 #define AX25_UI_FRAME 3		/* Control field value. */
 
 #define AX25_PID_NO_LAYER_3 0xf0		/* protocol ID used for APRS */
+#define AX25_PID_NETROM 0xcf			/* protocol ID used for NET/ROM */
 #define AX25_PID_SEGMENTATION_FRAGMENT 0x08
 #define AX25_PID_ESCAPE_CHARACTER 0xff
 
@@ -427,6 +428,7 @@ extern int ax25_is_null_frame (packet_t this_p);
 extern int ax25_get_control (packet_t this_p); 
 extern int ax25_get_c2 (packet_t this_p); 
 
+extern void ax25_set_pid (packet_t this_p, int pid);
 extern int ax25_get_pid (packet_t this_p);
 
 extern int ax25_get_frame_len (packet_t this_p);
diff --git a/src/server.c b/src/server.c
index 866b58ae..2cc108b2 100644
--- a/src/server.c
+++ b/src/server.c
@@ -379,7 +379,7 @@ static void debug_print (fromto_t fromto, int client, struct agwpe_s *pmsg, int
 	      case 'C': strlcpy (datakind, "AX.25 Connection Received",			sizeof(datakind)); break;
 	      case 'D': strlcpy (datakind, "Connected AX.25 Data",			sizeof(datakind)); break;
 	      case 'd': strlcpy (datakind, "Disconnected",				sizeof(datakind)); break;
-	      case 'M': strlcpy (datakind, "Monitored Connected Information",		sizeof(datakind)); break;
+	      case 'I': strlcpy (datakind, "Monitored Connected Information",		sizeof(datakind)); break;
 	      case 'S': strlcpy (datakind, "Monitored Supervisory Information",		sizeof(datakind)); break;
 	      case 'U': strlcpy (datakind, "Monitored Unproto Information",		sizeof(datakind)); break;
 	      case 'T': strlcpy (datakind, "Monitoring Own Information",		sizeof(datakind)); break;
@@ -1717,6 +1717,7 @@ static THREAD_F cmd_listen_thread (void *arg)
 	      
 		packet_t pp;
 
+		int pid = cmd.hdr.pid;
 	      	strlcpy (stemp, cmd.hdr.call_from, sizeof(stemp));
 	      	strlcat (stemp, ">", sizeof(stemp));
 	      	strlcat (stemp, cmd.hdr.call_to, sizeof(stemp));
@@ -1730,33 +1731,41 @@ static THREAD_F cmd_listen_thread (void *arg)
 		  strlcat (stemp, p, sizeof(stemp));
 		  p += 10;
 	        }
+	        // At this point, p now points to info part after digipeaters.
+
+	        // Issue 527: NET/ROM routing broadcasts are binary info so we can't treat as string.
+	        // Originally, I just appended the information part.
+		// That was fine until NET/ROM, with binary data, came along.
+		// Now we set the information field after creating the packet object.
+
 		strlcat (stemp, ":", sizeof(stemp));
-		strlcat (stemp, p, sizeof(stemp));
+		strlcat (stemp, " ", sizeof(stemp));
 
 	        //text_color_set(DW_COLOR_DEBUG);
 		//dw_printf ("Transmit '%s'\n", stemp);
 
 		pp = ax25_from_text (stemp, 1);
 
-
 		if (pp == NULL) {
 	          text_color_set(DW_COLOR_ERROR);
 		  dw_printf ("Failed to create frame from AGW 'V' message.\n");
+	          break;
 		}
-		else {
 
-		  /* This goes into the low priority queue because it is an original. */
+	        ax25_set_info (pp, (unsigned char*)p, data_len - ndigi * 10);
+	        // Issue 527: NET/ROM routing broadcasts use PID 0xCF which was not preserved here.
+	        ax25_set_pid (pp, pid);
 
-		  /* Note that the protocol has no way to set the "has been used" */
-		  /* bits in the digipeater fields. */
+		/* This goes into the low priority queue because it is an original. */
 
-		  /* This explains why the digipeating option is grayed out in */
-		  /* xastir when using the AGW interface.  */
-		  /* The current version uses only the 'V' message, not 'K' for transmitting. */
+		/* Note that the protocol has no way to set the "has been used" */
+		/* bits in the digipeater fields. */
 
-		  tq_append (cmd.hdr.portx, TQ_PRIO_1_LO, pp);
+		/* This explains why the digipeating option is grayed out in */
+		/* xastir when using the AGW interface.  */
+		/* The current version uses only the 'V' message, not 'K' for transmitting. */
 
-		}
+		tq_append (cmd.hdr.portx, TQ_PRIO_1_LO, pp);
 	      }
 	      
 	      break;
@@ -1890,7 +1899,7 @@ static THREAD_F cmd_listen_thread (void *arg)
 	          unsigned char num_digi;	/* Expect to be in range 1 to 7.  Why not up to 8? */
 		  char dcall[7][10];
 	        } 
-#if 1
+
 		// October 2017.  gcc ??? complained:
 		//     warning: dereferencing pointer 'v' does break strict-aliasing rules
 		// Try adding this attribute to get rid of the warning.
@@ -1898,7 +1907,6 @@ static THREAD_F cmd_listen_thread (void *arg)
 	        // Let me know.  Maybe we could put in a compiler version check here.
 
 	           __attribute__((__may_alias__))
-#endif
 	                              *v = (struct via_info *)cmd.data;
 
 	        char callsigns[AX25_MAX_ADDRS][AX25_MAX_ADDR_LEN];
@@ -2007,19 +2015,7 @@ static THREAD_F cmd_listen_thread (void *arg)
 	      {
 	      
 		int pid = cmd.hdr.pid;
-		(void)(pid);
-			/* The AGW protocol spec says, */
-			/* "AX.25 PID 0x00 or 0xF0 for AX.25 0xCF NETROM and others" */
-
-			/* BUG: In theory, the AX.25 PID octet should be set from this. */
-			/* All examples seen (above) have 0. */
-			/* The AX.25 protocol spec doesn't list 0 as a valid value. */
-			/* We always send 0xf0, meaning no layer 3. */
-			/* Maybe we should have an ax25_set_pid function for cases when */
-			/* it is neither 0 nor 0xf0. */
-
 	      	char stemp[AX25_MAX_PACKET_LEN];
-		packet_t pp;
 
 	      	strlcpy (stemp, cmd.hdr.call_from, sizeof(stemp));
 	      	strlcat (stemp, ">", sizeof(stemp));
@@ -2027,21 +2023,29 @@ static THREAD_F cmd_listen_thread (void *arg)
 
 		cmd.data[data_len] = '\0';
 
+	        // Issue 527: NET/ROM routing broadcasts are binary info so we can't treat as string.
+	        // Originally, I just appended the information part as a text string.
+		// That was fine until NET/ROM, with binary data, came along.
+		// Now we set the information field after creating the packet object.
+
 		strlcat (stemp, ":", sizeof(stemp));
-		strlcat (stemp, cmd.data, sizeof(stemp));
+		strlcat (stemp, " ", sizeof(stemp));
 
 	        //text_color_set(DW_COLOR_DEBUG);
 		//dw_printf ("Transmit '%s'\n", stemp);
 
-		pp = ax25_from_text (stemp, 1);
+		packet_t pp = ax25_from_text (stemp, 1);
 
 		if (pp == NULL) {
 	          text_color_set(DW_COLOR_ERROR);
 		  dw_printf ("Failed to create frame from AGW 'M' message.\n");
 		}
-		else {
-		  tq_append (cmd.hdr.portx, TQ_PRIO_1_LO, pp);
-		}
+
+	        ax25_set_info (pp, (unsigned char*)cmd.data, data_len);
+	        // Issue 527: NET/ROM routing broadcasts use PID 0xCF which was not preserved here.
+	        ax25_set_pid (pp, pid);
+
+		tq_append (cmd.hdr.portx, TQ_PRIO_1_LO, pp);
 	      }
 	      break;
 

From cae46801c4b50df4cdb085a7d9916846adfc6e8a Mon Sep 17 00:00:00 2001
From: wb2osz <wb2osz@comcast.net>
Date: Wed, 15 May 2024 20:12:55 +0100
Subject: [PATCH 31/67] Check for RELAY,WIDE,TRACE,  Check for RFONLY,NOGATE in
 wrong place.

---
 src/decode_aprs.c | 33 ++++++++++++++++++++++++++++++---
 1 file changed, 30 insertions(+), 3 deletions(-)

diff --git a/src/decode_aprs.c b/src/decode_aprs.c
index 08534f7a..71eb9469 100644
--- a/src/decode_aprs.c
+++ b/src/decode_aprs.c
@@ -1,7 +1,7 @@
 //
 //    This file is part of Dire Wolf, an amateur radio packet TNC.
 //
-//    Copyright (C) 2011, 2012, 2013, 2014, 2015, 2017, 2022, 2023  John Langner, WB2OSZ
+//    Copyright (C) 2011, 2012, 2013, 2014, 2015, 2017, 2022, 2023, 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
@@ -206,9 +206,36 @@ void decode_aprs (decode_aprs_t *A, packet_t pp, int quiet, char *third_party_sr
 	A->g_footprint_lon = G_UNKNOWN;
 	A->g_footprint_radius = G_UNKNOWN;
 
-// TODO: Complain if obsolete WIDE or RELAY is found in via path.
 
-// TODO: complain if unused WIDEn is see in path.
+// Check for RFONLY or NOGATE in the destination field.
+// Actual cases observed.
+// W1KU-4>APDW15,W1IMD,WIDE1,KQ1L-8,N3LLO-3,WIDE2*:}EB1EBT-9>NOGATE,TCPIP,W1KU-4*::DF1AKR-9 :73{4
+// NE1CU-10>RFONLY,KB1AEV-15,N3LLO-3,WIDE2*:}W1HS-11>APMI06,TCPIP,NE1CU-10*:T#050,190,039,008,095,20403,00000000
+
+	char atemp[AX25_MAX_ADDR_LEN];
+	ax25_get_addr_no_ssid (pp, AX25_DESTINATION, atemp);
+	if ( ! quiet) {
+	  if (strcmp("RFONLY", atemp) == 0 || strcmp("NOGATE", atemp) == 0) {
+	    text_color_set(DW_COLOR_ERROR);
+	    dw_printf("RFONLY and NOGATE must not appear in the destination address field.\n");
+	    dw_printf("They should appear only at the end of the digi via path.\n");
+	  }
+	}
+
+// Complain if obsolete WIDE or RELAY is found in via path.
+
+	for (int i = 0; i < ax25_get_num_repeaters(pp); i++) {
+	  ax25_get_addr_no_ssid (pp, AX25_REPEATER_1 + i, atemp);
+	  if ( ! quiet) {
+	    if (strcmp("RELAY", atemp) == 0 || strcmp("WIDE", atemp) == 0 || strcmp("TRACE", atemp) == 0) {
+	      text_color_set(DW_COLOR_ERROR);
+	      dw_printf("RELAY, TRACE, and WIDE (not WIDEn) are obsolete.\n");
+	      dw_printf("Modern digipeaters will not recoginize these.\n");
+	    }
+	  }
+	}
+
+// TODO: complain if unused WIDEn-0 is see in path.
 // There is a report of UIDIGI decrementing ssid 1 to 0 and not marking it used.
 // http://lists.tapr.org/pipermail/aprssig_lists.tapr.org/2022-May/049397.html
 

From c05669a82ba7bf9cb10634a8a7c248ea3219b35e Mon Sep 17 00:00:00 2001
From: wb2osz <wb2osz@comcast.net>
Date: Sun, 16 Jun 2024 01:30:57 +0100
Subject: [PATCH 32/67] Allow longer Windows PTT HID name.

---
 src/cm108.c | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/src/cm108.c b/src/cm108.c
index ff3ff792..787e7bb9 100644
--- a/src/cm108.c
+++ b/src/cm108.c
@@ -260,8 +260,9 @@ static void substr_se (char *dest, const char *src, int start, int endp1)
 // Maximum length of name for PTT HID.
 // For Linux, this was originally 17 to handle names like /dev/hidraw3.
 // Windows has more complicated names.  The longest I saw was 95 but longer have been reported.
+// Then we have this  https://groups.io/g/direwolf/message/9622  where 127 is not enough.
 
-#define MAXX_HIDRAW_NAME_LEN 128
+#define MAXX_HIDRAW_NAME_LEN 150
 
 /*
  * Result of taking inventory of USB soundcards and USB HIDs.

From ae888b0a8d7261491017512fc06a0ba44cbbfc34 Mon Sep 17 00:00:00 2001
From: wb2osz <wb2osz@comcast.net>
Date: Fri, 19 Jul 2024 00:37:38 +0100
Subject: [PATCH 33/67] New NCHANNEL feature.

---
 CHANGES.md            |   3 +
 conf/generic.conf     |   2 +-
 src/CMakeLists.txt    |   1 +
 src/agwlib.c          |   2 +-
 src/appserver.c       |   2 +-
 src/aprs_tt.c         |   8 +-
 src/atest.c           |  11 ++-
 src/audio.c           |   2 +-
 src/audio.h           |  27 ++++--
 src/audio_portaudio.c |   2 +-
 src/audio_win.c       |  20 +++-
 src/ax25_link.c       |  39 ++++++--
 src/ax25_link.h       |   2 +-
 src/ax25_pad.c        |  18 +++-
 src/beacon.c          |  14 +--
 src/cdigipeater.c     |  18 ++--
 src/cdigipeater.h     |  14 ++-
 src/config.c          | 206 +++++++++++++++++++++++++++---------------
 src/config.h          |   2 +-
 src/decode_aprs.c     |   2 +-
 src/demod.c           |  18 ++--
 src/demod_9600.c      |   9 +-
 src/demod_afsk.c      |   2 +-
 src/demod_psk.c       | 118 ++++++++++++++++--------
 src/digipeater.c      |  24 ++---
 src/digipeater.h      |  16 ++--
 src/direwolf.c        |  62 ++++++++++---
 src/direwolf.h        |   7 +-
 src/dlq.c             |  12 +--
 src/dtmf.c            |  11 ++-
 src/dwsock.c          |   2 +-
 src/encode_aprs.c     |  12 ++-
 src/fsk_demod_state.h |   2 +-
 src/fx25_rec.c        |   6 +-
 src/fx25_send.c       |   4 +-
 src/gen_packets.c     |   2 +-
 src/gen_tone.c        |  28 +++---
 src/hdlc_rec.c        |  57 +++++++++---
 src/hdlc_rec.h        |  10 ++
 src/hdlc_rec2.c       |  10 +-
 src/hdlc_send.c       |   6 +-
 src/igate.c           |  26 +++---
 src/il2p_rec.c        |   9 +-
 src/il2p_send.c       |   2 +-
 src/kiss_frame.c      |  18 ++--
 src/multi_modem.c     |  21 +++--
 src/pfilter.c         |  18 ++--
 src/ptt.c             |  34 +++----
 src/recv.c            |   3 +-
 src/rrbb.c            |   4 +-
 src/server.c          |  12 +--
 src/telemetry.c       |   2 +-
 src/tnctest.c         |  24 ++---
 src/tq.c              |  66 +++++++++-----
 src/tt_user.c         |   6 +-
 src/xmit.c            |  30 +++---
 56 files changed, 697 insertions(+), 391 deletions(-)

diff --git a/CHANGES.md b/CHANGES.md
index 0903e9ea..69a1a857 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -6,6 +6,9 @@
 
 ### New Features: ###
 
+
+- New NCHANNEL feature to map a channel number to an external network TCP KISS TNC.  See xxx for example of a bridge to LoRa APRS.  See [APRS-LoRa-VHF-APRS-Bridge.pdf](https://github.com/wb2osz/direwolf-doc/blob/main/APRS-LoRa-VHF-APRS-Bridge.pdf) for explanation.
+
 - [http://www.aprs.org/aprs11/tocalls.txt](http://www.aprs.org/aprs11/tocalls.txt) has been abandoned since the end of 2021.  [https://github.com/aprsorg/aprs-deviceid](https://github.com/aprsorg/aprs-deviceid) is now considered to be the authoritative source of truth for the vendor/model encoding.
 
 ## Version 1.7  --  October 2023 ##
diff --git a/conf/generic.conf b/conf/generic.conf
index 9a19d8a2..4fb63f6b 100644
--- a/conf/generic.conf
+++ b/conf/generic.conf
@@ -274,7 +274,7 @@
 %C%#DTMF
 %C%
 %C%# Push to Talk (PTT) can be confusing because there are so many different cases.
-%C%# Radio-Interface-Guide.pdf  in  https://github.com/wb2osz/direwolf-doc
+%C%# https://github.com/wb2osz/direwolf-doc/blob/main/Radio-Interface-Guide.pdf
 %C%# goes into detail about the various options.
 %C%
 %L%# If using a C-Media CM108/CM119 or similar USB Audio Adapter,
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 19dada4a..f376b7d5 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -79,6 +79,7 @@ list(APPEND direwolf_SOURCES
   morse.c
   multi_modem.c
   waypoint.c
+  nettnc.c
   serial_port.c
   pfilter.c
   ptt.c
diff --git a/src/agwlib.c b/src/agwlib.c
index 2c03adaa..cee4f992 100644
--- a/src/agwlib.c
+++ b/src/agwlib.c
@@ -357,7 +357,7 @@ static void * tnc_listen_thread (void *arg)
 /*
  * Take some precautions to guard against bad data which could cause problems later.
  */
-	    if (cmd.hdr.portx < 0 || cmd.hdr.portx >= MAX_CHANS) {
+	    if (cmd.hdr.portx < 0 || cmd.hdr.portx >= MAX_TOTAL_CHANS) {
 	      text_color_set(DW_COLOR_ERROR);
 	      dw_printf ("Invalid channel number, %d, in command '%c', from network TNC.\n",
 			cmd.hdr.portx, cmd.hdr.datakind);
diff --git a/src/appserver.c b/src/appserver.c
index b0ef7d87..bc2e2818 100644
--- a/src/appserver.c
+++ b/src/appserver.c
@@ -597,7 +597,7 @@ void agw_cb_G_port_information (int num_chan_avail, char *chan_descriptions[])
 	    if (strncasecmp(p, "Port", 4) == 0 && isdigit(p[4])) {
 	    
 	      int chan = atoi(p+4) - 1;	// "Port1" is our channel 0.
-	      if (chan >= 0 && chan < MAX_CHANS) {
+	      if (chan >= 0 && chan < MAX_TOTAL_CHANS) {
 
 	        char *desc = p + 4;
 	        while (*desc != '\0' && (*desc == ' ' || isdigit(*desc))) {
diff --git a/src/aprs_tt.c b/src/aprs_tt.c
index 7b125759..a2d35ec6 100644
--- a/src/aprs_tt.c
+++ b/src/aprs_tt.c
@@ -95,8 +95,8 @@
 
 #define MAX_MSG_LEN 100
 
-static char msg_str[MAX_CHANS][MAX_MSG_LEN+1];
-static int msg_len[MAX_CHANS];
+static char msg_str[MAX_RADIO_CHANS][MAX_MSG_LEN+1];
+static int msg_len[MAX_RADIO_CHANS];
 
 static int parse_fields (char *msg);
 static int parse_callsign (char *e);
@@ -185,7 +185,7 @@ void aprs_tt_init (struct tt_config_s *p, int debug)
 	// TODO: Keep ptr instead of making a copy.
 	memcpy (&tt_config, p, sizeof(struct tt_config_s));
 #endif
-	for (c=0; c<MAX_CHANS; c++) {	
+	for (c=0; c<MAX_RADIO_CHANS; c++) {
 	  msg_len[c] = 0;
 	  msg_str[c][0] = '\0';
 	}
@@ -226,7 +226,7 @@ void aprs_tt_button (int chan, char button)
 {
 	static int poll_period = 0;
 
-	assert (chan >= 0 && chan < MAX_CHANS);
+	assert (chan >= 0 && chan < MAX_RADIO_CHANS);
 
 
 	//if (button != '.') {
diff --git a/src/atest.c b/src/atest.c
index c5f4ec50..a24ed727 100644
--- a/src/atest.c
+++ b/src/atest.c
@@ -231,7 +231,7 @@ int main (int argc, char *argv[])
 	my_audio_config.adev[0].bits_per_sample = DEFAULT_BITS_PER_SAMPLE;	
 
 
-	for (channel=0; channel<MAX_CHANS; channel++) {
+	for (channel=0; channel<MAX_RADIO_CHANS; channel++) {
 
 	  my_audio_config.achan[channel].modem_type = MODEM_AFSK;
 
@@ -628,9 +628,10 @@ int main (int argc, char *argv[])
 	dw_printf ("%d samples per second.  %d bits per sample.  %d audio channels.\n",
 		my_audio_config.adev[0].samples_per_sec,
 		my_audio_config.adev[0].bits_per_sample,
-		my_audio_config.adev[0].num_channels);
+		(int)(my_audio_config.adev[0].num_channels));
+	// nnum_channels is known to be 1 or 2.
 	one_filetime = (double) wav_data.datasize /
-		((my_audio_config.adev[0].bits_per_sample / 8) * my_audio_config.adev[0].num_channels * my_audio_config.adev[0].samples_per_sec);
+		((my_audio_config.adev[0].bits_per_sample / 8) * (int)(my_audio_config.adev[0].num_channels) * my_audio_config.adev[0].samples_per_sec);
 	total_filetime += one_filetime;
 
 	dw_printf ("%d audio bytes in file.  Duration = %.1f seconds.\n",
@@ -654,7 +655,7 @@ int main (int argc, char *argv[])
           int audio_sample;
           int c;
 
-          for (c=0; c<my_audio_config.adev[0].num_channels; c++)
+          for (c=0; c<(int)(my_audio_config.adev[0].num_channels); c++)
           {
 
             /* This reads either 1 or 2 bytes depending on */
@@ -921,7 +922,7 @@ void dlq_rec_frame (int chan, int subchan, int slice, packet_t pp, alevel_t alev
 void ptt_set (int ot, int chan, int ptt_signal)
 {
 	// Should only get here for DCD output control.
-	static double dcd_start_time[MAX_CHANS];
+	static double dcd_start_time[MAX_RADIO_CHANS];
 
 	if (d_o_opt) {
 	  double t = (double)sample_number / my_audio_config.adev[0].samples_per_sec;
diff --git a/src/audio.c b/src/audio.c
index 82dec22a..a522b399 100644
--- a/src/audio.c
+++ b/src/audio.c
@@ -257,7 +257,7 @@ int audio_open (struct audio_s *pa)
 	  if (pa->adev[a].bits_per_sample == 0)
 	    pa->adev[a].bits_per_sample = DEFAULT_BITS_PER_SAMPLE;
 
-	  for (chan=0; chan<MAX_CHANS; chan++) {
+	  for (chan=0; chan<MAX_RADIO_CHANS; chan++) {
 	    if (pa->achan[chan].mark_freq == 0)
 	      pa->achan[chan].mark_freq = DEFAULT_MARK_FREQ;
 
diff --git a/src/audio.h b/src/audio.h
index 4fc05708..92fd944e 100644
--- a/src/audio.h
+++ b/src/audio.h
@@ -16,7 +16,7 @@
 #include <hamlib/rig.h>
 #endif
 
-#include "direwolf.h"		/* for MAX_CHANS used throughout the application. */
+#include "direwolf.h"		/* for MAX_RADIO_CHANS and MAX_TOTAL_CHANS used throughout the application. */
 #include "ax25_pad.h"		/* for AX25_MAX_ADDR_LEN */
 #include "version.h"
 				
@@ -59,7 +59,7 @@ 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.  (possible future)
+		MEDIUM_NETTNC };	// Remote network TNC.  (new in 1.8)
 
 
 typedef enum sanity_e { SANITY_APRS, SANITY_AX25, SANITY_NONE } sanity_t;
@@ -139,10 +139,19 @@ struct audio_s {
 	/* originally a "channel" was always connected to an internal modem. */
 	/* In version 1.6, this is generalized so that a channel (as seen by client application) */
 	/* can be connected to something else.  Initially, this will allow application */
-	/* access to the IGate.  Later we might have network TNCs or other internal functions. */
+	/* access to the IGate.  In version 1.8 we add network KISS TNC. */
+
+	// Watch out for maximum number of channels.
+	//	MAX_CHANS - Originally, this was 6 for internal modem adio channels. Has been phased out.
+	// After adding virtual channels (IGate, network TNC), this is split into two different numbers:
+	//	MAX_RADIO_CHANNELS - For internal modems.
+	//	MAX_TOTAL_CHANNELS - limited by KISS channels/ports.  Needed for digipeating, filtering, etc.
 
 	// Properties for all channels.
 
+	char mycall[MAX_TOTAL_CHANS][AX25_MAX_ADDR_LEN];  /* Call associated with this radio channel. */
+							/* Could all be the same or different. */
+
 	enum medium_e chan_medium[MAX_TOTAL_CHANS];
 					// MEDIUM_NONE for invalid.
 					// MEDIUM_RADIO for internal modem.  (only possibility earlier)
@@ -154,6 +163,14 @@ struct audio_s {
 					/* Redundant but it makes things quicker and simpler */
 					/* than always searching thru above. */
 
+	// Applies only to network TNC type channels.
+
+	char nettnc_addr[MAX_TOTAL_CHANS][80];		// Network TNC address:  hostname or IP addr.
+
+	int nettnc_port[MAX_TOTAL_CHANS];		// Network TNC TCP port.
+
+
+
 	/* Properties for each radio channel, common to receive and transmit. */
 	/* Can be different for each radio channel. */
 
@@ -171,8 +188,6 @@ struct audio_s {
 	    // int audio_source;	// Default would be [0,1,2,3,4,5]
 
 	    // What else should be moved out of structure and enlarged when NETTNC is implemented.  ???
-	    char mycall[AX25_MAX_ADDR_LEN];      /* Call associated with this radio channel. */
-                                	/* Could all be the same or different. */
 
 
 	    enum modem_t { MODEM_AFSK, MODEM_BASEBAND, MODEM_SCRAMBLE, MODEM_QPSK, MODEM_8PSK, MODEM_OFF, MODEM_16_QAM, MODEM_64_QAM, MODEM_AIS, MODEM_EAS } modem_type;
@@ -381,7 +396,7 @@ struct audio_s {
 
 	    int fulldup;		/* Full Duplex. */
 
-	} achan[MAX_CHANS];
+	} achan[MAX_RADIO_CHANS];
 
 #ifdef USE_HAMLIB
     int rigs;               /* Total number of configured rigs */
diff --git a/src/audio_portaudio.c b/src/audio_portaudio.c
index cb6ccf10..92ba2cb3 100644
--- a/src/audio_portaudio.c
+++ b/src/audio_portaudio.c
@@ -578,7 +578,7 @@ int audio_open (struct audio_s *pa)
 		if (pa->adev[a].bits_per_sample == 0)
 			pa->adev[a].bits_per_sample = DEFAULT_BITS_PER_SAMPLE;
 
-		for (chan = 0; chan < MAX_CHANS; chan++) {
+		for (chan = 0; chan < MAX_RADIO_CHANS; chan++) {
 			if (pa->achan[chan].mark_freq == 0)
 				pa->achan[chan].mark_freq = DEFAULT_MARK_FREQ;
 
diff --git a/src/audio_win.c b/src/audio_win.c
index 85a1548b..a133648a 100644
--- a/src/audio_win.c
+++ b/src/audio_win.c
@@ -270,7 +270,7 @@ int audio_open (struct audio_s *pa)
 
 	    A->g_audio_in_type = AUDIO_IN_TYPE_SOUNDCARD;
 
-	    for (chan=0; chan<MAX_CHANS; chan++) {
+	    for (chan=0; chan<MAX_RADIO_CHANS; chan++) {
 	      if (pa -> achan[chan].mark_freq == 0)
 	        pa -> achan[chan].mark_freq = DEFAULT_MARK_FREQ;
 
@@ -660,7 +660,13 @@ int audio_open (struct audio_s *pa)
  */
    	       case AUDIO_IN_TYPE_STDIN:
 
-  	         setmode (STDIN_FILENO, _O_BINARY);
+	         // https://learn.microsoft.com/en-us/cpp/c-runtime-library/reference/setmode?view=msvc-170
+
+	         int err = _setmode (_fileno(stdin), _O_BINARY);
+	         if (err == -1) {
+	           text_color_set (DW_COLOR_ERROR);
+	           dw_printf ("Could not set stdin to binary mode.  Unlikely to get desired result.\n");
+	         }
 	         A->stream_next= 0;
 	         A->stream_len = 0;
 
@@ -888,7 +894,7 @@ int audio_get (int a)
 	    while (A->stream_next >= A->stream_len) {
 	      int res;
 
-	      res = read(STDIN_FILENO, A->stream_data, 1024);
+	      res = read(STDIN_FILENO, A->stream_data, sizeof(A->stream_data));
 	      if (res <= 0) {
 	        text_color_set(DW_COLOR_INFO);
 	        dw_printf ("\nEnd of file on stdin.  Exiting.\n");
@@ -903,9 +909,13 @@ int audio_get (int a)
 	      A->stream_len = res;
 	      A->stream_next = 0;
 	    }
-	    return (A->stream_data[A->stream_next++] & 0xff);
+	    sample = A->stream_data[A->stream_next] & 0xff;
+	    A->stream_next++;
+	    return (sample);
+
 	    break;
-  	}
+
+	}  // end switch audio in type
 
 	return (-1);
 
diff --git a/src/ax25_link.c b/src/ax25_link.c
index 98d6c45e..50495cd8 100644
--- a/src/ax25_link.c
+++ b/src/ax25_link.c
@@ -1,7 +1,7 @@
 //
 //    This file is part of Dire Wolf, an amateur radio packet TNC.
 //
-//    Copyright (C) 2016, 2017, 2018, 2023  John Langner, WB2OSZ
+//    Copyright (C) 2016, 2017, 2018, 2023, 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
@@ -679,11 +679,13 @@ static struct misc_config_s  *g_misc_config_p;
  * Inputs:	pconfig		- misc. configuration from config file or command line.
  *				  Beacon stuff ended up here.
  *
+ *		debug 		- debug level.
+ *
  * Outputs:	Remember required information for future use.  That's all.
  *
  *--------------------------------------------------------------------*/
 
-void ax25_link_init (struct misc_config_s *pconfig)
+void ax25_link_init (struct misc_config_s *pconfig, int debug)
 {
 
 /* 
@@ -691,6 +693,31 @@ void ax25_link_init (struct misc_config_s *pconfig)
  */
 	g_misc_config_p = pconfig;
 
+	if (debug >= 1) {	// Only single level so far.
+
+	  s_debug_protocol_errors = 1;	// Less serious Protocol errors.
+
+	  s_debug_client_app = 1;	// Interaction with client application.
+					// dl_connect_request, dl_data_request, dl_data_indication, etc.
+
+	  s_debug_radio = 1;		// Received frames and channel busy status.
+					// lm_data_indication, lm_channel_busy
+
+	  s_debug_variables = 1;	// Variables, state changes.
+
+	  s_debug_retry = 1;		// Related to lost I frames, REJ, SREJ, timeout, resending.
+
+	  s_debug_link_handle = 1;	// Create data link state machine or pick existing one,
+					// based on my address, peer address, client app index, and radio channel.
+
+	  s_debug_stats = 1;		// Statistics when connection is closed.
+
+	  s_debug_misc = 1;		// Anything left over that might be interesting.
+
+	  s_debug_timers = 1;		// Timer details.
+	}
+
+
 } /* end ax25_link_init */
 
 
@@ -2013,14 +2040,14 @@ static void dl_data_indication (ax25_dlsm_t *S, int pid, char *data, int len)
  *
  *------------------------------------------------------------------------------*/
 
-static int dcd_status[MAX_CHANS];
-static int ptt_status[MAX_CHANS];
+static int dcd_status[MAX_RADIO_CHANS];
+static int ptt_status[MAX_RADIO_CHANS];
 
 void lm_channel_busy (dlq_item_t *E)
 {
 	int busy;
 
-	assert (E->chan >= 0 && E->chan < MAX_CHANS);
+	assert (E->chan >= 0 && E->chan < MAX_RADIO_CHANS);
 	assert (E->activity == OCTYPE_PTT || E->activity == OCTYPE_DCD);
 	assert (E->status == 1 || E->status == 0);
 
@@ -2104,7 +2131,7 @@ void lm_channel_busy (dlq_item_t *E)
 void lm_seize_confirm (dlq_item_t *E)
 {
 
-	assert (E->chan >= 0 && E->chan < MAX_CHANS);
+	assert (E->chan >= 0 && E->chan < MAX_RADIO_CHANS);
 
 	ax25_dlsm_t *S;
 
diff --git a/src/ax25_link.h b/src/ax25_link.h
index 40fa401b..52caceed 100644
--- a/src/ax25_link.h
+++ b/src/ax25_link.h
@@ -43,7 +43,7 @@
 
 // Call once at startup time.
 
-void ax25_link_init (struct misc_config_s *pconfig);
+void ax25_link_init (struct misc_config_s *pconfig, int debug);
 
 
 
diff --git a/src/ax25_pad.c b/src/ax25_pad.c
index 4a526386..57fd79d2 100644
--- a/src/ax25_pad.c
+++ b/src/ax25_pad.c
@@ -1,7 +1,7 @@
 //
 //    This file is part of Dire Wolf, an amateur radio packet TNC.
 //
-//    Copyright (C) 2011 , 2013, 2014, 2015, 2019  John Langner, WB2OSZ
+//    Copyright (C) 2011 , 2013, 2014, 2015, 2019, 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
@@ -355,12 +355,26 @@ void ax25_delete (packet_t this_p)
  *			  The SSID can be 2 alphanumeric characters, not just 1 to 15.
  *
  *			  We can just truncate the name because we will only
- *			  end up discarding it.    TODO:  check on this.
+ *			  end up discarding it.    TODO:  check on this.  WRONG! FIXME
  *
  * Returns:	Pointer to new packet object in the current implementation.
  *
  * Outputs:	Use the "get" functions to retrieve information in different ways.
  *
+ * Evolution:	Originally this was written to handle only valid RF packets.
+ *		There are other places where the rules are not as strict.
+ *		Using decode_aprs with raw data seen on aprs.fi.  e.g.
+ *			EL-CA2JOT>RXTLM-1,TCPIP,qAR,CA2JOT::EL-CA2JOT:UNIT....
+ *			EA4YR>APBM1S,TCPIP*,qAS,BM2142POS:@162124z...
+ *		* Source addr might not comply to RF format.
+ *		* The q-construct has lower case.
+ *		* Tier-2 server name might not comply to RF format.
+ *		We have the same issue with the encapsulated part of a third-party packet.
+ *			WB2OSZ-5>APDW17,WIDE1-1,WIDE2-1:}WHO-IS>APJIW4,TCPIP,WB2OSZ-5*::WB2OSZ-7 :ack0
+ *
+ *		We need a way to keep and retrieve the original name.
+ *		This gets a little messy because the packet object is in the on air frame format.
+ *
  *------------------------------------------------------------------------------*/
 
 #if AX25MEMDEBUG
diff --git a/src/beacon.c b/src/beacon.c
index 69a72701..b868f228 100644
--- a/src/beacon.c
+++ b/src/beacon.c
@@ -162,14 +162,14 @@ void beacon_init (struct audio_s *pmodem, struct misc_config_s *pconfig, struct
 	  int chan = g_misc_config_p->beacon[j].sendto_chan;
 
 	  if (chan < 0) chan = 0;	/* For IGate, use channel 0 call. */
-	  if (chan >= MAX_CHANS) chan = 0;	// For ICHANNEL, use channel 0 call.
+	  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) {
 
-	    if (strlen(g_modem_config_p->achan[chan].mycall) > 0 &&
-			 strcasecmp(g_modem_config_p->achan[chan].mycall, "N0CALL") != 0 &&
-			 strcasecmp(g_modem_config_p->achan[chan].mycall, "NOCALL") != 0) {
+	    if (strlen(g_modem_config_p->mycall[chan]) > 0 &&
+			 strcasecmp(g_modem_config_p->mycall[chan], "N0CALL") != 0 &&
+			 strcasecmp(g_modem_config_p->mycall[chan], "NOCALL") != 0) {
 
               switch (g_misc_config_p->beacon[j].btype) {
 
@@ -809,10 +809,10 @@ static void beacon_send (int j, dwgps_info_t *gpsinfo)
 
 	      if (g_modem_config_p->chan_medium[bp->sendto_chan] == MEDIUM_IGATE) {	// ICHANNEL uses chan 0 mycall.
 									// TODO: Maybe it should be allowed to have own.
-	        strlcpy (mycall, g_modem_config_p->achan[0].mycall, sizeof(mycall));
+	        strlcpy (mycall, g_modem_config_p->mycall[0], sizeof(mycall));
 	      }
 	      else {
-	        strlcpy (mycall, g_modem_config_p->achan[bp->sendto_chan].mycall, sizeof(mycall));
+	        strlcpy (mycall, g_modem_config_p->mycall[bp->sendto_chan], sizeof(mycall));
 	      }
 	      
 	      if (strlen(mycall) == 0 || strcmp(mycall, "NOCALL") == 0) {
@@ -900,7 +900,7 @@ static void beacon_send (int j, dwgps_info_t *gpsinfo)
 
 		case BEACON_OBJECT:
 
-		  encode_object (bp->objname, bp->compress, 0, bp->lat, bp->lon, bp->ambiguity,
+		  encode_object (bp->objname, bp->compress, 1, bp->lat, bp->lon, bp->ambiguity,
 			bp->symtab, bp->symbol,
 			bp->power, bp->height, bp->gain, bp->dir,
 			G_UNKNOWN, G_UNKNOWN, /* course, speed */
diff --git a/src/cdigipeater.c b/src/cdigipeater.c
index 06128b20..844af470 100644
--- a/src/cdigipeater.c
+++ b/src/cdigipeater.c
@@ -76,7 +76,7 @@ static struct cdigi_config_s *save_cdigi_config_p;
  * Maintain count of packets digipeated for each combination of from/to channel.
  */
 
-static int cdigi_count[MAX_CHANS][MAX_CHANS];
+static int cdigi_count[MAX_RADIO_CHANS][MAX_RADIO_CHANS];
 
 int cdigipeater_get_count (int from_chan, int to_chan) {
 	return (cdigi_count[from_chan][to_chan]);
@@ -132,7 +132,9 @@ void cdigipeater (int from_chan, packet_t pp)
 	// Connected mode is allowed only for channels with internal modem.
 	// It probably wouldn't matter for digipeating but let's keep that rule simple and consistent.
 
-	if ( from_chan < 0 || from_chan >= MAX_CHANS || save_audio_config_p->chan_medium[from_chan] != MEDIUM_RADIO) {
+	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)  ) {
 	  text_color_set(DW_COLOR_ERROR);
 	  dw_printf ("cdigipeater: Did not expect to receive on invalid channel %d.\n", from_chan);
 	  return;
@@ -145,13 +147,13 @@ void cdigipeater (int from_chan, packet_t pp)
  * Might not have a benefit here.
  */
 
-	for (to_chan=0; to_chan<MAX_CHANS; to_chan++) {
+	for (to_chan=0; to_chan<MAX_RADIO_CHANS; to_chan++) {
 	  if (save_cdigi_config_p->enabled[from_chan][to_chan]) {
 	    if (to_chan == from_chan) {
 	      packet_t result;
 
-	      result = cdigipeat_match (from_chan, pp, save_audio_config_p->achan[from_chan].mycall, 
-					   save_audio_config_p->achan[to_chan].mycall,
+	      result = cdigipeat_match (from_chan, pp, save_audio_config_p->mycall[from_chan],
+					   save_audio_config_p->mycall[to_chan],
 			save_cdigi_config_p->has_alias[from_chan][to_chan],
 			&(save_cdigi_config_p->alias[from_chan][to_chan]), to_chan,
 				save_cdigi_config_p->cfilter_str[from_chan][to_chan]);
@@ -168,13 +170,13 @@ void cdigipeater (int from_chan, packet_t pp)
  * Second pass:  Look at packets being digipeated to different channel.
  */
 
-	for (to_chan=0; to_chan<MAX_CHANS; to_chan++) {
+	for (to_chan=0; to_chan<MAX_RADIO_CHANS; to_chan++) {
 	  if (save_cdigi_config_p->enabled[from_chan][to_chan]) {
 	    if (to_chan != from_chan) {
 	      packet_t result;
 
-	      result = cdigipeat_match (from_chan, pp, save_audio_config_p->achan[from_chan].mycall, 
-					   save_audio_config_p->achan[to_chan].mycall,
+	      result = cdigipeat_match (from_chan, pp, save_audio_config_p->mycall[from_chan],
+					   save_audio_config_p->mycall[to_chan],
 	                save_cdigi_config_p->has_alias[from_chan][to_chan],
 			&(save_cdigi_config_p->alias[from_chan][to_chan]), to_chan,
 				save_cdigi_config_p->cfilter_str[from_chan][to_chan]);
diff --git a/src/cdigipeater.h b/src/cdigipeater.h
index 69a4b8c7..89b0302a 100644
--- a/src/cdigipeater.h
+++ b/src/cdigipeater.h
@@ -5,7 +5,7 @@
 
 #include "regex.h"
 
-#include "direwolf.h"		/* for MAX_CHANS */
+#include "direwolf.h"		/* for MAX_RADIO_CHANS */
 #include "ax25_pad.h"		/* for packet_t */
 #include "audio.h"		/* for radio channel properties */
 
@@ -23,17 +23,21 @@ struct cdigi_config_s {
 /*
  * Rules for each of the [from_chan][to_chan] combinations.
  */
-	int	enabled[MAX_CHANS][MAX_CHANS];	// Is it enabled for from/to pair?
 
-	int has_alias[MAX_CHANS][MAX_CHANS];	// If there was no alias in the config file,
+// For APRS digipeater, we use MAX_TOTAL_CHANS because we use external TNCs.
+// Connected mode packet must use internal modems we we use MAX_RADIO_CHANS.
+
+	int	enabled[MAX_RADIO_CHANS][MAX_RADIO_CHANS];	// Is it enabled for from/to pair?
+
+	int has_alias[MAX_RADIO_CHANS][MAX_RADIO_CHANS];	// If there was no alias in the config file,
 						// the structure below will not be set up
 						// properly and an attempt to use it could
 						// result in a crash.  (fixed v1.5)
 						// Not needed for [APRS] DIGIPEAT because
 						// the alias is mandatory there.
-	regex_t	alias[MAX_CHANS][MAX_CHANS];
+	regex_t	alias[MAX_RADIO_CHANS][MAX_RADIO_CHANS];
 
-	char *cfilter_str[MAX_CHANS][MAX_CHANS];
+	char *cfilter_str[MAX_RADIO_CHANS][MAX_RADIO_CHANS];
 						// NULL or optional Packet Filter strings such as "t/m".
 };
 
diff --git a/src/config.c b/src/config.c
index de8d74d4..69fa80e2 100644
--- a/src/config.c
+++ b/src/config.c
@@ -763,7 +763,7 @@ void config_init (char *fname, struct audio_s *p_audio_config,
 
 	p_audio_config->adev[0].defined = 2;		// 2 means it was done by default and not the user's config file.
 
-	for (channel=0; channel<MAX_CHANS; channel++) {
+	for (channel=0; channel<MAX_TOTAL_CHANS; channel++) {
 	  int ot, it;
 
 	  p_audio_config->chan_medium[channel] = MEDIUM_NONE;	/* One or both channels will be */
@@ -1221,7 +1221,7 @@ void config_init (char *fname, struct audio_s *p_audio_config,
  */
 
 /*
- * CHANNEL n		- Set channel for channel-specific commands.
+ * CHANNEL n		- Set channel for channel-specific commands.  Only for modem/radio channels.
  */
 
 	  else if (strcasecmp(t, "CHANNEL") == 0) {
@@ -1233,7 +1233,7 @@ void config_init (char *fname, struct audio_s *p_audio_config,
 	      continue;
 	    }
 	    n = atoi(t);
-            if (n >= 0 && n < MAX_CHANS) {
+            if (n >= 0 && n < MAX_RADIO_CHANS) {
 
 	      channel = n;
 
@@ -1253,7 +1253,7 @@ void config_init (char *fname, struct audio_s *p_audio_config,
 	    }
 	    else {
 	      text_color_set(DW_COLOR_ERROR);
-              dw_printf ("Line %d: Channel number must in range of 0 to %d.\n", line, MAX_CHANS-1);
+              dw_printf ("Line %d: Channel number must in range of 0 to %d.\n", line, MAX_RADIO_CHANS-1);
    	    }
 	  }
 
@@ -1274,7 +1274,7 @@ void config_init (char *fname, struct audio_s *p_audio_config,
 	      continue;
 	    }
 	    int ichan = atoi(t);
-            if (ichan >= MAX_CHANS && ichan < MAX_TOTAL_CHANS) {
+            if (ichan >= MAX_RADIO_CHANS && ichan < MAX_TOTAL_CHANS) {
 
 	      if (p_audio_config->chan_medium[ichan] == MEDIUM_NONE) {
 
@@ -1286,15 +1286,73 @@ void config_init (char *fname, struct audio_s *p_audio_config,
 	      }
 	      else {
 	        text_color_set(DW_COLOR_ERROR);
-                dw_printf ("Line %d: ICHANNEL can't use %d because it is already in use.\n", line, ichan);
+                dw_printf ("Line %d: ICHANNEL can't use channel %d because it is already in use.\n", line, ichan);
 	      }
 	    }
 	    else {
 	      text_color_set(DW_COLOR_ERROR);
-              dw_printf ("Line %d: ICHANNEL number must in range of %d to %d.\n", line, MAX_CHANS, MAX_TOTAL_CHANS-1);
+              dw_printf ("Line %d: ICHANNEL number must in range of %d to %d.\n", line, MAX_RADIO_CHANS, MAX_TOTAL_CHANS-1);
 	    }
 	  }
 
+/*
+ * NCHANNEL chan addr port			- Define Network TNC virtual channel.
+ *
+ *	This allows a client application to talk to to an external TNC over TCP 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.
+ *	addr = hostname or IP address of network TNC.
+ *	port = KISS TCP port on network TNC.
+ *
+ *	Future: Might allow selection of channel on the network TNC.
+ *	For now, ignore incoming and set to 0 for outgoing.
+ *
+ * FIXME: Can't set mycall for nchannel.
+ */
+
+	  else if (strcasecmp(t, "NCHANNEL") == 0) {
+	    t = split(NULL,0);
+	    if (t == NULL) {
+	      text_color_set(DW_COLOR_ERROR);
+	      dw_printf ("Line %d: Missing virtual channel number for NCHANNEL 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_NETTNC;
+	      }
+	      else {
+	        text_color_set(DW_COLOR_ERROR);
+                dw_printf ("Line %d: NCHANNEL 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: NCHANNEL 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 network TNC address for NCHANNEL command.\n", line);
+	      continue;
+	    }
+	    strlcpy (p_audio_config->nettnc_addr[nchan], t, sizeof(p_audio_config->nettnc_addr[nchan]));
+
+	    t = split(NULL,0);
+	    if (t == NULL) {
+	      text_color_set(DW_COLOR_ERROR);
+	      dw_printf ("Line %d: Missing network TNC TCP port for NCHANNEL command.\n", line);
+	      continue;
+	    }
+	    p_audio_config->nettnc_port[nchan] = atoi(t);
+	  }
+
 /*
  * MYCALL station
  */
@@ -1330,14 +1388,14 @@ void config_init (char *fname, struct audio_s *p_audio_config,
 
 	      int c;
 
-	      for (c = 0; c < MAX_CHANS; c++) {
+	      for (c = 0; c < MAX_TOTAL_CHANS; c++) {
 
 	        if (c == channel || 
-			strlen(p_audio_config->achan[c].mycall) == 0 || 
-			strcasecmp(p_audio_config->achan[c].mycall, "NOCALL") == 0 ||
-			strcasecmp(p_audio_config->achan[c].mycall, "N0CALL") == 0) {
+			strlen(p_audio_config->mycall[c]) == 0 || 
+			strcasecmp(p_audio_config->mycall[c], "NOCALL") == 0 ||
+			strcasecmp(p_audio_config->mycall[c], "N0CALL") == 0) {
 
-	          strlcpy (p_audio_config->achan[c].mycall, t, sizeof(p_audio_config->achan[c].mycall));
+	          strlcpy (p_audio_config->mycall[c], t, sizeof(p_audio_config->mycall[c]));
 	        }
 	      }
 	    }
@@ -2552,10 +2610,10 @@ void config_init (char *fname, struct audio_s *p_audio_config,
 	      continue;
 	    }
 	    from_chan = atoi(t);
-	    if (from_chan < 0 || from_chan >= MAX_CHANS) {
+	    if (from_chan < 0 || from_chan >= MAX_TOTAL_CHANS) {
 	      text_color_set(DW_COLOR_ERROR);
 	      dw_printf ("Config file: FROM-channel must be in range of 0 to %d on line %d.\n", 
-							MAX_CHANS-1, line);
+							MAX_TOTAL_CHANS-1, line);
 	      continue;
 	    }
 
@@ -2582,10 +2640,10 @@ void config_init (char *fname, struct audio_s *p_audio_config,
 	      continue;
 	    }
 	    to_chan = atoi(t);
-	    if (to_chan < 0 || to_chan >= MAX_CHANS) {
+	    if (to_chan < 0 || to_chan >= MAX_TOTAL_CHANS) {
 	      text_color_set(DW_COLOR_ERROR);
 	      dw_printf ("Config file: TO-channel must be in range of 0 to %d on line %d.\n", 
-							MAX_CHANS-1, line);
+							MAX_TOTAL_CHANS-1, line);
 	      continue;
 	    }
 
@@ -2713,10 +2771,10 @@ void config_init (char *fname, struct audio_s *p_audio_config,
 	      continue;
 	    }
 	    from_chan = atoi(t);
-	    if (from_chan < 0 || from_chan >= MAX_CHANS) {
+	    if (from_chan < 0 || from_chan >= MAX_RADIO_CHANS) {
 	      text_color_set(DW_COLOR_ERROR);
 	      dw_printf ("Config file: FROM-channel must be in range of 0 to %d on line %d.\n", 
-							MAX_CHANS-1, line);
+							MAX_RADIO_CHANS-1, line);
 	      continue;
 	    }
 
@@ -2742,10 +2800,10 @@ void config_init (char *fname, struct audio_s *p_audio_config,
 	      continue;
 	    }
 	    to_chan = atoi(t);
-	    if (to_chan < 0 || to_chan >= MAX_CHANS) {
+	    if (to_chan < 0 || to_chan >= MAX_RADIO_CHANS) {
 	      text_color_set(DW_COLOR_ERROR);
 	      dw_printf ("Config file: TO-channel must be in range of 0 to %d on line %d.\n", 
-							MAX_CHANS-1, line);
+							MAX_RADIO_CHANS-1, line);
 	      continue;
 	    }
 	    if (p_audio_config->chan_medium[to_chan] != MEDIUM_RADIO) {
@@ -2787,10 +2845,10 @@ void config_init (char *fname, struct audio_s *p_audio_config,
 	      continue;
 	    }
 	    from_chan = atoi(t);
-	    if (from_chan < 0 || from_chan >= MAX_CHANS) {
+	    if (from_chan < 0 || from_chan >= MAX_RADIO_CHANS) {
 	      text_color_set(DW_COLOR_ERROR);
 	      dw_printf ("Config file: FROM-channel must be in range of 0 to %d on line %d.\n",
-							MAX_CHANS-1, line);
+							MAX_RADIO_CHANS-1, line);
 	      continue;
 	    }
 
@@ -2820,10 +2878,10 @@ void config_init (char *fname, struct audio_s *p_audio_config,
 	      continue;
 	    }
 	    to_chan = atoi(t);
-	    if (to_chan < 0 || to_chan >= MAX_CHANS) {
+	    if (to_chan < 0 || to_chan >= MAX_RADIO_CHANS) {
 	      text_color_set(DW_COLOR_ERROR);
 	      dw_printf ("Config file: TO-channel must be in range of 0 to %d on line %d.\n",
-							MAX_CHANS-1, line);
+							MAX_RADIO_CHANS-1, line);
 	      continue;
 	    }
 	    if (p_audio_config->chan_medium[to_chan] != MEDIUM_RADIO) {
@@ -2906,7 +2964,7 @@ void config_init (char *fname, struct audio_s *p_audio_config,
 	      continue;
 	    }
 	    if (*t == 'i' || *t == 'I') {
-	      from_chan = MAX_CHANS;
+	      from_chan = MAX_TOTAL_CHANS;
 	      text_color_set(DW_COLOR_ERROR);
 	      dw_printf ("Config file: FILTER IG ... on line %d.\n", line);
 	      dw_printf ("Warning! Don't mess with IS>RF filtering unless you are an expert and have an unusual situation.\n");
@@ -2916,10 +2974,10 @@ void config_init (char *fname, struct audio_s *p_audio_config,
 	    }
 	    else {
 	      from_chan = isdigit(*t) ? atoi(t) : -999;
-	      if (from_chan < 0 || from_chan >= MAX_CHANS) {
+	      if (from_chan < 0 || from_chan >= MAX_TOTAL_CHANS) {
 	        text_color_set(DW_COLOR_ERROR);
 	        dw_printf ("Config file: Filter FROM-channel must be in range of 0 to %d or \"IG\" on line %d.\n", 
-							MAX_CHANS-1, line);
+							MAX_TOTAL_CHANS-1, line);
 	        continue;
 	      }
 
@@ -2945,7 +3003,7 @@ void config_init (char *fname, struct audio_s *p_audio_config,
 	      continue;
 	    }
 	    if (*t == 'i' || *t == 'I') {
-	      to_chan = MAX_CHANS;
+	      to_chan = MAX_TOTAL_CHANS;
 	      text_color_set(DW_COLOR_ERROR);
 	      dw_printf ("Config file: FILTER ... IG ... on line %d.\n", line);
 	      dw_printf ("Warning! Don't mess with RF>IS filtering unless you are an expert and have an unusual situation.\n");
@@ -2955,10 +3013,10 @@ void config_init (char *fname, struct audio_s *p_audio_config,
 	    }
 	    else {
 	      to_chan = isdigit(*t) ? atoi(t) : -999;
-	      if (to_chan < 0 || to_chan >= MAX_CHANS) {
+	      if (to_chan < 0 || to_chan >= MAX_TOTAL_CHANS) {
 	        text_color_set(DW_COLOR_ERROR);
 	        dw_printf ("Config file: Filter TO-channel must be in range of 0 to %d or \"IG\" on line %d.\n", 
-							MAX_CHANS-1, line);
+							MAX_TOTAL_CHANS-1, line);
 	        continue;
 	      }
 	      if (p_audio_config->chan_medium[to_chan] != MEDIUM_RADIO &&
@@ -3020,10 +3078,10 @@ void config_init (char *fname, struct audio_s *p_audio_config,
 	    }
 
 	    from_chan = isdigit(*t) ? atoi(t) : -999;
-	    if (from_chan < 0 || from_chan >= MAX_CHANS) {
+	    if (from_chan < 0 || from_chan >= MAX_RADIO_CHANS) {
 	      text_color_set(DW_COLOR_ERROR);
 	      dw_printf ("Config file: Filter FROM-channel must be in range of 0 to %d on line %d.\n",
-							MAX_CHANS-1, line);
+							MAX_RADIO_CHANS-1, line);
 	      continue;
 	    }
 
@@ -3045,10 +3103,10 @@ void config_init (char *fname, struct audio_s *p_audio_config,
 	    }
 
 	    to_chan = isdigit(*t) ? atoi(t) : -999;
-	    if (to_chan < 0 || to_chan >= MAX_CHANS) {
+	    if (to_chan < 0 || to_chan >= MAX_RADIO_CHANS) {
 	      text_color_set(DW_COLOR_ERROR);
 	      dw_printf ("Config file: Filter TO-channel must be in range of 0 to %d on line %d.\n",
-							MAX_CHANS-1, line);
+							MAX_RADIO_CHANS-1, line);
 	      continue;
 	    }
             if (p_audio_config->chan_medium[to_chan] != MEDIUM_RADIO) {
@@ -4205,10 +4263,10 @@ void config_init (char *fname, struct audio_s *p_audio_config,
 	    }
 
 	    r = atoi(t);
-	    if (r < 0 || r > MAX_CHANS-1) {
+	    if (r < 0 || r > MAX_RADIO_CHANS-1) {
 	      text_color_set(DW_COLOR_ERROR);
 	      dw_printf ("Config file: DTMF receive channel must be in range of 0 to %d on line %d.\n", 
-							MAX_CHANS-1, line);
+							MAX_RADIO_CHANS-1, line);
 	      continue;
 	    }
 
@@ -4236,9 +4294,9 @@ void config_init (char *fname, struct audio_s *p_audio_config,
 
 	      if (isdigit(*p)) {
 	        x = *p - '0';
-	        if (x < 0 || x > MAX_CHANS-1) {
+	        if (x < 0 || x > MAX_TOTAL_CHANS-1) {
 	          text_color_set(DW_COLOR_ERROR);
-	          dw_printf ("Config file: Transmit channel must be in range of 0 to %d on line %d.\n", MAX_CHANS-1, line);
+	          dw_printf ("Config file: Transmit channel must be in range of 0 to %d on line %d.\n", MAX_TOTAL_CHANS-1, line);
 	          x = -1;
 	        }
 	        else if (p_audio_config->chan_medium[x] != MEDIUM_RADIO &&
@@ -4528,10 +4586,10 @@ void config_init (char *fname, struct audio_s *p_audio_config,
 	    }
 
 	    n = atoi(t);
-	    if (n < 0 || n > MAX_CHANS-1) {
+	    if (n < 0 || n > MAX_TOTAL_CHANS-1) {
 	      text_color_set(DW_COLOR_ERROR);
 	      dw_printf ("Config file: Transmit channel must be in range of 0 to %d on line %d.\n", 
-							MAX_CHANS-1, line);
+							MAX_TOTAL_CHANS-1, line);
 	      continue;
 	    }
 	    p_igate_config->tx_chan = n;
@@ -4797,9 +4855,9 @@ void config_init (char *fname, struct audio_s *p_audio_config,
 	    t = split(NULL,0);
 	    if (t != NULL) {
 	      chan = atoi(t);
-	      if (chan < 0 || chan >= MAX_CHANS) {
+	      if (chan < 0 || chan >= MAX_TOTAL_CHANS) {
 	        text_color_set(DW_COLOR_ERROR);
-	        dw_printf ("Line %d: Invalid channel %d for KISSPORT command.  Must be in range 0 thru %d.\n", line, chan, MAX_CHANS-1);
+	        dw_printf ("Line %d: Invalid channel %d for KISSPORT command.  Must be in range 0 thru %d.\n", line, chan, MAX_TOTAL_CHANS-1);
 	        continue;
 	      }
 	    }
@@ -5510,25 +5568,25 @@ void config_init (char *fname, struct audio_s *p_audio_config,
  */
 	int i, j, k, b;
 
-	for (i=0; i<MAX_CHANS; i++) {
-	  for (j=0; j<MAX_CHANS; j++) {
+	for (i=0; i<MAX_TOTAL_CHANS; i++) {
+	  for (j=0; j<MAX_TOTAL_CHANS; j++) {
 
 /* APRS digipeating. */
 
 	    if (p_digi_config->enabled[i][j]) {
 
-	      if ( strcmp(p_audio_config->achan[i].mycall, "") == 0 || 
-		   strcmp(p_audio_config->achan[i].mycall, "NOCALL") == 0 || 
-		   strcmp(p_audio_config->achan[i].mycall, "N0CALL") == 0) {
+	      if ( strcmp(p_audio_config->mycall[i], "") == 0 ||
+		   strcmp(p_audio_config->mycall[i], "NOCALL") == 0 ||
+		   strcmp(p_audio_config->mycall[i], "N0CALL") == 0) {
 
 	        text_color_set(DW_COLOR_ERROR);
 	        dw_printf ("Config file: MYCALL must be set for receive channel %d before digipeating is allowed.\n", i);
 	        p_digi_config->enabled[i][j] = 0;
 	      }
 
-	      if ( strcmp(p_audio_config->achan[j].mycall, "") == 0 || 
-	           strcmp(p_audio_config->achan[j].mycall, "NOCALL") == 0 ||
-		   strcmp(p_audio_config->achan[j].mycall, "N0CALL") == 0) {
+	      if ( strcmp(p_audio_config->mycall[j], "") == 0 ||
+	           strcmp(p_audio_config->mycall[j], "NOCALL") == 0 ||
+		   strcmp(p_audio_config->mycall[j], "N0CALL") == 0) {
 
 	        text_color_set(DW_COLOR_ERROR);
 	        dw_printf ("Config file: MYCALL must be set for transmit channel %d before digipeating is allowed.\n", i); 
@@ -5550,20 +5608,20 @@ void config_init (char *fname, struct audio_s *p_audio_config,
 
 /* Connected mode digipeating. */
 
-	    if (p_cdigi_config->enabled[i][j]) {
+	    if (i < MAX_RADIO_CHANS && j < MAX_RADIO_CHANS && p_cdigi_config->enabled[i][j]) {
 
-	      if ( strcmp(p_audio_config->achan[i].mycall, "") == 0 ||
-		   strcmp(p_audio_config->achan[i].mycall, "NOCALL") == 0 ||
-		   strcmp(p_audio_config->achan[i].mycall, "N0CALL") == 0) {
+	      if ( strcmp(p_audio_config->mycall[i], "") == 0 ||
+		   strcmp(p_audio_config->mycall[i], "NOCALL") == 0 ||
+		   strcmp(p_audio_config->mycall[i], "N0CALL") == 0) {
 
 	        text_color_set(DW_COLOR_ERROR);
 	        dw_printf ("Config file: MYCALL must be set for receive channel %d before digipeating is allowed.\n", i);
 	        p_cdigi_config->enabled[i][j] = 0;
 	      }
 
-	      if ( strcmp(p_audio_config->achan[j].mycall, "") == 0 ||
-	           strcmp(p_audio_config->achan[j].mycall, "NOCALL") == 0 ||
-		   strcmp(p_audio_config->achan[j].mycall, "N0CALL") == 0) {
+	      if ( strcmp(p_audio_config->mycall[j], "") == 0 ||
+	           strcmp(p_audio_config->mycall[j], "NOCALL") == 0 ||
+		   strcmp(p_audio_config->mycall[j], "N0CALL") == 0) {
 
 	        text_color_set(DW_COLOR_ERROR);
 	        dw_printf ("Config file: MYCALL must be set for transmit channel %d before digipeating is allowed.\n", i);
@@ -5587,7 +5645,7 @@ void config_init (char *fname, struct audio_s *p_audio_config,
 	  if (strlen(p_igate_config->t2_login) > 0 &&
 	      (p_audio_config->chan_medium[i] == MEDIUM_RADIO || p_audio_config->chan_medium[i] == MEDIUM_NETTNC)) {
 
-	    if (strcmp(p_audio_config->achan[i].mycall, "NOCALL") == 0  || strcmp(p_audio_config->achan[i].mycall, "N0CALL") == 0) {
+	    if (strcmp(p_audio_config->mycall[i], "NOCALL") == 0  || strcmp(p_audio_config->mycall[i], "N0CALL") == 0) {
 	      text_color_set(DW_COLOR_ERROR);
 	      dw_printf ("Config file: MYCALL must be set for receive channel %d before Rx IGate is allowed.\n", i);
 	      strlcpy (p_igate_config->t2_login, "", sizeof(p_igate_config->t2_login));
@@ -5595,9 +5653,9 @@ void config_init (char *fname, struct audio_s *p_audio_config,
 	    // Currently we can have only one transmit channel.
 	    // This might be generalized someday to allow more.
 	    if (p_igate_config->tx_chan >= 0 && 
-			( strcmp(p_audio_config->achan[p_igate_config->tx_chan].mycall, "") == 0 ||
-		          strcmp(p_audio_config->achan[p_igate_config->tx_chan].mycall, "NOCALL") == 0 ||
-			  strcmp(p_audio_config->achan[p_igate_config->tx_chan].mycall, "N0CALL") == 0)) {
+			( strcmp(p_audio_config->mycall[p_igate_config->tx_chan], "") == 0 ||
+		          strcmp(p_audio_config->mycall[p_igate_config->tx_chan], "NOCALL") == 0 ||
+			  strcmp(p_audio_config->mycall[p_igate_config->tx_chan], "N0CALL") == 0)) {
 
 	      text_color_set(DW_COLOR_ERROR);
 	      dw_printf ("Config file: MYCALL must be set for transmit channel %d before Tx IGate is allowed.\n", i);
@@ -5610,10 +5668,10 @@ void config_init (char *fname, struct audio_s *p_audio_config,
 // This will handle eventual case of multiple transmit channels.
 
 	if (strlen(p_igate_config->t2_login) > 0) {
-	  for (j=0; j<MAX_CHANS; j++) {
+	  for (j=0; j<MAX_TOTAL_CHANS; j++) {
 	    if (p_audio_config->chan_medium[j] == MEDIUM_RADIO || p_audio_config->chan_medium[j] == MEDIUM_NETTNC) {
-	      if (p_digi_config->filter_str[MAX_CHANS][j] == NULL) {
-	        p_digi_config->filter_str[MAX_CHANS][j] = strdup("i/180");
+	      if (p_digi_config->filter_str[MAX_TOTAL_CHANS][j] == NULL) {
+	        p_digi_config->filter_str[MAX_TOTAL_CHANS][j] = strdup("i/180");
 	      }
 	    }
 	  }
@@ -5746,7 +5804,7 @@ static int beacon_options(char *cmd, struct beacon_s *b, int line, struct audio_
 	    }
 	    else if (value[0] == 'r' || value[0] == 'R') {
 	       int n = atoi(value+1);
-	       if (( n < 0 || n >= MAX_CHANS || p_audio_config->chan_medium[n] == MEDIUM_NONE)
+	       if (( n < 0 || n >= MAX_TOTAL_CHANS || p_audio_config->chan_medium[n] == MEDIUM_NONE)
 			&& p_audio_config->chan_medium[n] != MEDIUM_IGATE) {
 	         text_color_set(DW_COLOR_ERROR);
 	         dw_printf ("Config file, line %d: Simulated receive on channel %d is not valid.\n", line, n);
@@ -5757,7 +5815,7 @@ static int beacon_options(char *cmd, struct beacon_s *b, int line, struct audio_
 	    }
 	    else if (value[0] == 't' || value[0] == 'T' || value[0] == 'x' || value[0] == 'X') {
 	      int n = atoi(value+1);
-	      if (( n < 0 || n >= MAX_CHANS || p_audio_config->chan_medium[n] == MEDIUM_NONE)
+	      if (( n < 0 || n >= MAX_TOTAL_CHANS || p_audio_config->chan_medium[n] == MEDIUM_NONE)
 			&& p_audio_config->chan_medium[n] != MEDIUM_IGATE) {
 	        text_color_set(DW_COLOR_ERROR);
 	        dw_printf ("Config file, line %d: Send to channel %d is not valid.\n", line, n);
@@ -5769,7 +5827,7 @@ static int beacon_options(char *cmd, struct beacon_s *b, int line, struct audio_
 	    }
 	    else {
 	       int n = atoi(value);
-	       if (( n < 0 || n >= MAX_CHANS || p_audio_config->chan_medium[n] == MEDIUM_NONE)
+	       if (( n < 0 || n >= MAX_TOTAL_CHANS || p_audio_config->chan_medium[n] == MEDIUM_NONE)
 			&& p_audio_config->chan_medium[n] != MEDIUM_IGATE) {
 	         text_color_set(DW_COLOR_ERROR);
 	         dw_printf ("Config file, line %d: Send to channel %d is not valid.\n", line, n);
@@ -6020,7 +6078,7 @@ static int beacon_options(char *cmd, struct beacon_s *b, int line, struct audio_
 
 	if (b->sendto_type == SENDTO_XMIT) {
 
-	  if (( b->sendto_chan < 0 || b->sendto_chan >= MAX_CHANS || p_audio_config->chan_medium[b->sendto_chan] == MEDIUM_NONE)
+	  if (( b->sendto_chan < 0 || b->sendto_chan >= MAX_TOTAL_CHANS || p_audio_config->chan_medium[b->sendto_chan] == MEDIUM_NONE)
 		&& p_audio_config->chan_medium[b->sendto_chan] != MEDIUM_IGATE) {
 	    text_color_set(DW_COLOR_ERROR);
 	    dw_printf ("Config file, line %d: Send to channel %d is not valid.\n", line, b->sendto_chan);
@@ -6029,18 +6087,18 @@ static int beacon_options(char *cmd, struct beacon_s *b, int line, struct audio_
 
 	  if (p_audio_config->chan_medium[b->sendto_chan] == MEDIUM_IGATE) {  // Prevent subscript out of bounds.
 									     // Will be using call from chan 0 later.
-	    if ( strcmp(p_audio_config->achan[0].mycall, "") == 0 || 
-	         strcmp(p_audio_config->achan[0].mycall, "NOCALL") == 0 || 
-	         strcmp(p_audio_config->achan[0].mycall, "N0CALL") == 0 ) {
+	    if ( strcmp(p_audio_config->mycall[0], "") == 0 ||
+	         strcmp(p_audio_config->mycall[0], "NOCALL") == 0 ||
+	         strcmp(p_audio_config->mycall[0], "N0CALL") == 0 ) {
 
 	      text_color_set(DW_COLOR_ERROR);
 	      dw_printf ("Config file: MYCALL must be set for channel %d before beaconing is allowed.\n", 0); 
 	      return (0);
 	    }
 	  } else {
-	    if ( strcmp(p_audio_config->achan[b->sendto_chan].mycall, "") == 0 || 
-	         strcmp(p_audio_config->achan[b->sendto_chan].mycall, "NOCALL") == 0 || 
-	         strcmp(p_audio_config->achan[b->sendto_chan].mycall, "N0CALL") == 0 ) {
+	    if ( strcmp(p_audio_config->mycall[b->sendto_chan], "") == 0 ||
+	         strcmp(p_audio_config->mycall[b->sendto_chan], "NOCALL") == 0 ||
+	         strcmp(p_audio_config->mycall[b->sendto_chan], "N0CALL") == 0 ) {
 
 	      text_color_set(DW_COLOR_ERROR);
 	      dw_printf ("Config file: MYCALL must be set for channel %d before beaconing is allowed.\n", b->sendto_chan); 
diff --git a/src/config.h b/src/config.h
index 360ac492..e4675238 100644
--- a/src/config.h
+++ b/src/config.h
@@ -30,7 +30,7 @@ enum sendto_type_e { SENDTO_XMIT, SENDTO_IGATE, SENDTO_RECV };
 
 
 #define MAX_BEACONS 30
-#define MAX_KISS_TCP_PORTS (MAX_CHANS+1)
+#define MAX_KISS_TCP_PORTS (MAX_RADIO_CHANS+1)
 
 struct misc_config_s {
 
diff --git a/src/decode_aprs.c b/src/decode_aprs.c
index 71eb9469..ce658eb6 100644
--- a/src/decode_aprs.c
+++ b/src/decode_aprs.c
@@ -1652,7 +1652,7 @@ static void aprs_mic_e (decode_aprs_t *A, packet_t pp, unsigned char *info, int
 // It is essential to keep trailing spaces.  e.g. VX-8 suffix is "_ "
 
 	char mcomment[256];
-	strlcpy (mcomment, info + sizeof(struct aprs_mic_e_s), sizeof(mcomment));
+	strlcpy (mcomment, ((char*)info) + sizeof(struct aprs_mic_e_s), sizeof(mcomment));
 	if (mcomment[strlen(mcomment)-1] == '\r') {
 	  mcomment[strlen(mcomment)-1] = '\0';
 	}
diff --git a/src/demod.c b/src/demod.c
index cc522271..efcfde71 100644
--- a/src/demod.c
+++ b/src/demod.c
@@ -63,11 +63,11 @@ static struct audio_s          *save_audio_config_p;
 
 // Current state of all the decoders.
 
-static struct demodulator_state_s demodulator_state[MAX_CHANS][MAX_SUBCHANS];
+static struct demodulator_state_s demodulator_state[MAX_RADIO_CHANS][MAX_SUBCHANS];
 
 
-static int sample_sum[MAX_CHANS][MAX_SUBCHANS];
-static int sample_count[MAX_CHANS][MAX_SUBCHANS];
+static int sample_sum[MAX_RADIO_CHANS][MAX_SUBCHANS];
+static int sample_count[MAX_RADIO_CHANS][MAX_SUBCHANS];
 
 
 /*------------------------------------------------------------------
@@ -100,7 +100,7 @@ int demod_init (struct audio_s *pa)
 
 	save_audio_config_p = pa;
 
-	for (chan = 0; chan < MAX_CHANS; chan++) {
+	for (chan = 0; chan < MAX_RADIO_CHANS; chan++) {
 
 	 if (save_audio_config_p->chan_medium[chan] == MEDIUM_RADIO) {
 
@@ -812,7 +812,7 @@ int demod_init (struct audio_s *pa)
 
 	// Now the virtual channels.  FIXME:  could be single loop.
 
-	for (chan = MAX_CHANS; chan < MAX_TOTAL_CHANS; chan++) {
+	for (chan = MAX_RADIO_CHANS; chan < MAX_TOTAL_CHANS; chan++) {
 
 // FIXME dw_printf ("-------- virtual channel loop %d \n", chan);
 
@@ -927,7 +927,7 @@ int demod_get_sample (int a)
  *
  *--------------------------------------------------------------------*/
 
-static volatile int mute_input[MAX_CHANS];
+static volatile int mute_input[MAX_RADIO_CHANS];
 
 // New in 1.7.
 // A few people have a really bad audio cross talk situation where they receive their own transmissions.
@@ -939,7 +939,7 @@ static volatile int mute_input[MAX_CHANS];
 
 void demod_mute_input (int chan, int mute_during_xmit)
 {
-	assert (chan >= 0 && chan < MAX_CHANS);
+	assert (chan >= 0 && chan < MAX_RADIO_CHANS);
 	mute_input[chan] = mute_during_xmit;
 }
 
@@ -952,7 +952,7 @@ void demod_process_sample (int chan, int subchan, int sam)
 
 	struct demodulator_state_s *D;
 
-	assert (chan >= 0 && chan < MAX_CHANS);
+	assert (chan >= 0 && chan < MAX_RADIO_CHANS);
 	assert (subchan >= 0 && subchan < MAX_SUBCHANS);
 
 	if (mute_input[chan]) {
@@ -1066,7 +1066,7 @@ alevel_t demod_get_audio_level (int chan, int subchan)
 	struct demodulator_state_s *D;
 	alevel_t alevel;
 
-	assert (chan >= 0 && chan < MAX_CHANS);
+	assert (chan >= 0 && chan < MAX_RADIO_CHANS);
 	assert (subchan >= 0 && subchan < MAX_SUBCHANS);
 
 	/* We have to consider two different cases here. */
diff --git a/src/demod_9600.c b/src/demod_9600.c
index 705d1fa7..99432bfe 100644
--- a/src/demod_9600.c
+++ b/src/demod_9600.c
@@ -395,7 +395,7 @@ void demod_9600_process_sample (int chan, int sam, int upsample, struct demodula
 
 	int subchan = 0;
 
-	assert (chan >= 0 && chan < MAX_CHANS);
+	assert (chan >= 0 && chan < MAX_RADIO_CHANS);
 	assert (subchan >= 0 && subchan < MAX_SUBCHANS);
 
 	/* Scale to nice number for convenience. */
@@ -611,7 +611,10 @@ inline static void nudge_pll (int chan, int subchan, int slice, float demod_out_
 
 	  /* Overflow.  Was large positive, wrapped around, now large negative. */
 
-	  hdlc_rec_bit (chan, subchan, slice, demod_out_f > 0, D->modem_type == MODEM_SCRAMBLE, D->slicer[slice].lfsr);
+	  hdlc_rec_bit_new (chan, subchan, slice, demod_out_f > 0, D->modem_type == MODEM_SCRAMBLE, D->slicer[slice].lfsr,
+			&(D->slicer[slice].pll_nudge_total), &(D->slicer[slice].pll_symbol_count));
+	  D->slicer[slice].pll_symbol_count++;
+
 	  pll_dcd_each_symbol2 (D, chan, subchan, slice);
 	}
 
@@ -627,12 +630,14 @@ inline static void nudge_pll (int chan, int subchan, int slice, float demod_out_
 
 	  float target = D->pll_step_per_sample * demod_out_f / (demod_out_f - D->slicer[slice].prev_demod_out_f);
 
+	  signed int before = (signed int)(D->slicer[slice].data_clock_pll);	// Treat as signed.
 	  if (D->slicer[slice].data_detect) {
 	    D->slicer[slice].data_clock_pll = (int)(D->slicer[slice].data_clock_pll * D->pll_locked_inertia + target * (1.0f - D->pll_locked_inertia) );
 	  }
 	  else {
 	    D->slicer[slice].data_clock_pll = (int)(D->slicer[slice].data_clock_pll * D->pll_searching_inertia + target * (1.0f - D->pll_searching_inertia) );
 	  }
+	  D->slicer[slice].pll_nudge_total += (int64_t)((signed int)(D->slicer[slice].data_clock_pll)) - (int64_t)before;
 	}
 
 
diff --git a/src/demod_afsk.c b/src/demod_afsk.c
index b4d6c295..3e5d03ec 100644
--- a/src/demod_afsk.c
+++ b/src/demod_afsk.c
@@ -609,7 +609,7 @@ void demod_afsk_process_sample (int chan, int subchan, int sam, struct demodulat
 	static int seq = 0;			/* for log file name */
 #endif
 
-	assert (chan >= 0 && chan < MAX_CHANS);
+	assert (chan >= 0 && chan < MAX_RADIO_CHANS);
 	assert (subchan >= 0 && subchan < MAX_SUBCHANS);
 
 /* 
diff --git a/src/demod_psk.c b/src/demod_psk.c
index bc058185..3d06c915 100644
--- a/src/demod_psk.c
+++ b/src/demod_psk.c
@@ -72,7 +72,10 @@
  *		V.26 has two variations, A and B.  Initially I implemented the A alternative.
  *		It later turned out that the MFJ-2400 used the B alternative.  In version 1.6 you have a
  *		choice between compatibility with MFJ (and probably the others) or the original implementation.
- *		
+ *		The B alternative works a little more reliably, perhaps because there is never a
+ *		zero phase difference between adjacent symbols.
+ *		Eventually the A alternative might disappear to reduce confusion.
+ *	
  *---------------------------------------------------------------*/
 
 #include "direwolf.h"
@@ -94,7 +97,7 @@
 #include "fsk_demod_state.h"		// Values above override defaults.
 
 #include "audio.h"
-#include "tune.h"
+//#include "tune.h"	// obsolete. eventually remove all references.
 #include "fsk_gen_filter.h"
 #include "hdlc_rec.h"
 #include "textcolor.h"
@@ -102,7 +105,13 @@
 #include "dsp.h"
 
 
-
+#define TUNE(envvar,param,name,fmt) { 				\
+	char *e = getenv(envvar);				\
+	if (e != NULL) {					\
+	  param = atof(e);					\
+	  text_color_set (DW_COLOR_ERROR);			\
+	  dw_printf ("TUNE: " name " = " fmt "\n", param);	\
+	} }
 
 
 static const int phase_to_gray_v26[4] = {0, 1, 3, 2};	
@@ -202,9 +211,10 @@ void demod_psk_init (enum modem_t modem_type, enum v26_e v26_alt, int samples_pe
 	D->num_slicers = 1;		// Haven't thought about this yet.  Is it even applicable?
 
 
-#ifdef TUNE_PROFILE
-	profile = TUNE_PROFILE;
-#endif
+//#ifdef TUNE_PROFILE
+//	profile = TUNE_PROFILE;
+//#endif
+	TUNE("TUNE_PROFILE", profile, "profile", "%c")
 
 	if (modem_type == MODEM_QPSK) {
 
@@ -290,9 +300,16 @@ void demod_psk_init (enum modem_t modem_type, enum v26_e v26_alt, int samples_pe
 
 	  D->u.psk.delay_line_width_sym = 1.25;		// Delay line > 13/12 * symbol period		
 
+// JWL experiment 11-7.  Should delay be based on audio freq rather than baud?
+#if 0   // experiment made things much worse.   55 went down to 21.
+	  D->u.psk.coffs = (int) round( (11.f / 12.f) * (float)samples_per_sec / (float)carrier_freq );
+	  D->u.psk.boffs = (int) round(                 (float)samples_per_sec / (float)carrier_freq );
+	  D->u.psk.soffs = (int) round( (13.f / 12.f) * (float)samples_per_sec / (float)carrier_freq );
+#else
 	  D->u.psk.coffs = (int) round( (11.f / 12.f) * (float)samples_per_sec / (float)correct_baud );
 	  D->u.psk.boffs = (int) round(                 (float)samples_per_sec / (float)correct_baud );
 	  D->u.psk.soffs = (int) round( (13.f / 12.f) * (float)samples_per_sec / (float)correct_baud );
+#endif
 	}
 	else {
 
@@ -393,26 +410,40 @@ void demod_psk_init (enum modem_t modem_type, enum v26_e v26_alt, int samples_pe
 	  }
 	}
 
-#ifdef TUNE_PRE_BAUD
-	D->u.psk.prefilter_baud = TUNE_PRE_BAUD;
-#endif
-#ifdef TUNE_PRE_WINDOW
-	D->u.psk.pre_window = TUNE_PRE_WINDOW;
-#endif
+//#ifdef TUNE_PRE_BAUD
+//	D->u.psk.prefilter_baud = TUNE_PRE_BAUD;
+//#endif
+	TUNE("TUNE_PRE_BAUD", D->u.psk.prefilter_baud, "prefilter_baud", "%.3f")
 
-#ifdef TUNE_LPF_BAUD
-	D->u.psk.lpf_baud = TUNE_LPF_BAUD;
-#endif
-#ifdef TUNE_LP_WINDOW
-	D->u.psk.lp_window = TUNE_LP_WINDOW;
-#endif
+//#ifdef TUNE_PRE_WINDOW
+//	D->u.psk.pre_window = TUNE_PRE_WINDOW;
+//#endif
+	TUNE("TUNE_PRE_WINDOW", D->u.psk.pre_window, "pre_window", "%d")
+
+//#ifdef TUNE_LPF_BAUD
+//	D->u.psk.lpf_baud = TUNE_LPF_BAUD;
+//#endif
+//#ifdef TUNE_LP_WINDOW
+//	D->u.psk.lp_window = TUNE_LP_WINDOW;
+//#endif
+	TUNE("TUNE_LPF_BAUD", D->u.psk.lpf_baud, "lpf_baud", "%.3f")
+	TUNE("TUNE_LP_WINDOW", D->u.psk.lp_window, "lp_window", "%d")
 
-#if defined(TUNE_PLL_SEARCHING)
-	D->pll_searching_inertia = TUNE_PLL_SEARCHING;
-#endif
-#if defined(TUNE_PLL_LOCKED)
-	D->pll_locked_inertia = TUNE_PLL_LOCKED;
-#endif
+
+	TUNE("TUNE_LP_FILTER_WIDTH_SYM", D->u.psk.lp_filter_width_sym, "lp_filter_width_sym", "%.3f")
+
+
+
+
+
+//#if defined(TUNE_PLL_SEARCHING)
+//	D->pll_searching_inertia = TUNE_PLL_SEARCHING;
+//#endif
+//#if defined(TUNE_PLL_LOCKED)
+//	D->pll_locked_inertia = TUNE_PLL_LOCKED;
+//#endif
+	TUNE("TUNE_PLL_LOCKED", D->pll_locked_inertia, "pll_locked_inertia", "%.2f")
+	TUNE("TUNE_PLL_SEARCHING", D->pll_searching_inertia, "pll_searching_inertia", "%.2f")
 
 
 /*
@@ -427,17 +458,24 @@ void demod_psk_init (enum modem_t modem_type, enum v26_e v26_alt, int samples_pe
  */
 
 	D->u.psk.pre_filter_taps = (int) round( D->u.psk.pre_filter_width_sym * (float)samples_per_sec / (float)correct_baud );
+
+// JWL experiment 11/7 - Should delay line be based on audio frequency?
+	D->u.psk.delay_line_taps  = (int) round( D->u.psk.delay_line_width_sym * (float)samples_per_sec / (float)correct_baud );
 	D->u.psk.delay_line_taps  = (int) round( D->u.psk.delay_line_width_sym * (float)samples_per_sec / (float)correct_baud );
+
+
 	D->u.psk.lp_filter_taps =  (int) round( D->u.psk.lp_filter_width_sym * (float)samples_per_sec / (float)correct_baud );
 
 
-#ifdef TUNE_PRE_FILTER_TAPS
-	D->u.psk.pre_filter_taps = TUNE_PRE_FILTER_TAPS;
-#endif
+//#ifdef TUNE_PRE_FILTER_TAPS
+//	D->u.psk.pre_filter_taps = TUNE_PRE_FILTER_TAPS;
+//#endif
+	  TUNE("TUNE_PRE_FILTER_TAPS", D->u.psk.pre_filter_taps, "pre_filter_taps", "%d")
 
-#ifdef TUNE_lp_filter_taps
-	D->u.psk.lp_filter_taps = TUNE_lp_filter_taps;
-#endif
+//#ifdef TUNE_lp_filter_taps
+//	D->u.psk.lp_filter_taps = TUNE_lp_filter_taps;
+//#endif
+	  TUNE("TUNE_LP_FILTER_TAPS", D->u.psk.lp_filter_taps, "lp_filter_taps (FIR)", "%d")
 
 
 	if (D->u.psk.pre_filter_taps > MAX_FILTER_SIZE) {
@@ -665,7 +703,7 @@ void demod_psk_process_sample (int chan, int subchan, int sam, struct demodulato
 {
 	int slice = 0;		// Would it make sense to have more than one?
 
-	assert (chan >= 0 && chan < MAX_CHANS);
+	assert (chan >= 0 && chan < MAX_RADIO_CHANS);
 	assert (subchan >= 0 && subchan < MAX_SUBCHANS);
 
 	/* Scale to nice number for plotting during debug. */
@@ -800,16 +838,22 @@ static void nudge_pll (int chan, int subchan, int slice, int demod_bits, struct
 
 	    int gray = demod_bits;
 
-	    hdlc_rec_bit (chan, subchan, slice, (gray >> 1) & 1, 0, bit_quality[1]);
-	    hdlc_rec_bit (chan, subchan, slice, gray & 1, 0, bit_quality[0]);
+	    hdlc_rec_bit_new (chan, subchan, slice, (gray >> 1) & 1, 0, bit_quality[1],
+			&(D->slicer[slice].pll_nudge_total), &(D->slicer[slice].pll_symbol_count));
+	    hdlc_rec_bit_new (chan, subchan, slice, gray & 1, 0, bit_quality[0],
+			&(D->slicer[slice].pll_nudge_total), &(D->slicer[slice].pll_symbol_count));
 	  }
 	  else {
 	    int gray = demod_bits;
 
-	    hdlc_rec_bit (chan, subchan, slice, (gray >> 2) & 1, 0, bit_quality[2]);
-	    hdlc_rec_bit (chan, subchan, slice, (gray >> 1) & 1, 0, bit_quality[1]);
-	    hdlc_rec_bit (chan, subchan, slice, gray & 1, 0, bit_quality[0]);
+	    hdlc_rec_bit_new (chan, subchan, slice, (gray >> 2) & 1, 0, bit_quality[2],
+			&(D->slicer[slice].pll_nudge_total), &(D->slicer[slice].pll_symbol_count));
+	    hdlc_rec_bit_new (chan, subchan, slice, (gray >> 1) & 1, 0, bit_quality[1],
+			&(D->slicer[slice].pll_nudge_total), &(D->slicer[slice].pll_symbol_count));
+	    hdlc_rec_bit_new (chan, subchan, slice, gray & 1, 0, bit_quality[0],
+			&(D->slicer[slice].pll_nudge_total), &(D->slicer[slice].pll_symbol_count));
 	  }
+	  D->slicer[slice].pll_symbol_count++;
 	  pll_dcd_each_symbol2 (D, chan, subchan, slice);
 	}
 
@@ -826,12 +870,14 @@ static void nudge_pll (int chan, int subchan, int slice, int demod_bits, struct
 
 	  pll_dcd_signal_transition2 (D, slice, D->slicer[slice].data_clock_pll);
 
+	  signed int before = (signed int)(D->slicer[slice].data_clock_pll);	// Treat as signed.
 	  if (D->slicer[slice].data_detect) {
 	    D->slicer[slice].data_clock_pll = (int)floorf((float)(D->slicer[slice].data_clock_pll) * D->pll_locked_inertia);
 	  }
 	  else {
 	    D->slicer[slice].data_clock_pll = (int)floorf((float)(D->slicer[slice].data_clock_pll) * D->pll_searching_inertia);
 	  }
+	  D->slicer[slice].pll_nudge_total += (int64_t)((signed int)(D->slicer[slice].data_clock_pll)) - (int64_t)before;
 	}
 
 /*
diff --git a/src/digipeater.c b/src/digipeater.c
index fbe89370..fcf59568 100644
--- a/src/digipeater.c
+++ b/src/digipeater.c
@@ -91,7 +91,7 @@ static struct digi_config_s *save_digi_config_p;
  * Maintain count of packets digipeated for each combination of from/to channel.
  */
 
-static int digi_count[MAX_CHANS][MAX_CHANS];
+static int digi_count[MAX_TOTAL_CHANS][MAX_TOTAL_CHANS];
 
 int digipeater_get_count (int from_chan, int to_chan) {
 	return (digi_count[from_chan][to_chan]);
@@ -154,7 +154,7 @@ void digipeater (int from_chan, packet_t pp)
 
 	// Network TNC is OK for UI frames where we don't care about timing.
 
-	if ( from_chan < 0 || from_chan >= MAX_CHANS ||
+	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)) {
 	  text_color_set(DW_COLOR_ERROR);
@@ -195,14 +195,14 @@ void digipeater (int from_chan, packet_t pp)
  *
  */
 
-	for (to_chan=0; to_chan<MAX_CHANS; to_chan++) {
+	for (to_chan=0; to_chan<MAX_TOTAL_CHANS; to_chan++) {
 	  if (save_digi_config_p->enabled[from_chan][to_chan]) {
 	    if (to_chan == from_chan) {
 	      packet_t result;
 
-	      result = digipeat_match (from_chan, pp, save_audio_config_p->achan[from_chan].mycall, 
-					   save_audio_config_p->achan[to_chan].mycall, 
-			&save_digi_config_p->alias[from_chan][to_chan], &save_digi_config_p->wide[from_chan][to_chan], 
+	      result = digipeat_match (from_chan, pp, save_audio_config_p->mycall[from_chan],
+					   save_audio_config_p->mycall[to_chan],
+			&save_digi_config_p->alias[from_chan][to_chan], &save_digi_config_p->wide[from_chan][to_chan],
 			to_chan, save_digi_config_p->preempt[from_chan][to_chan],
 				save_digi_config_p->atgp[from_chan][to_chan],
 				save_digi_config_p->filter_str[from_chan][to_chan]);
@@ -222,14 +222,14 @@ void digipeater (int from_chan, packet_t pp)
  * These are lower priority
  */
 
-	for (to_chan=0; to_chan<MAX_CHANS; to_chan++) {
+	for (to_chan=0; to_chan<MAX_TOTAL_CHANS; to_chan++) {
 	  if (save_digi_config_p->enabled[from_chan][to_chan]) {
 	    if (to_chan != from_chan) {
 	      packet_t result;
 
-	      result = digipeat_match (from_chan, pp, save_audio_config_p->achan[from_chan].mycall, 
-					   save_audio_config_p->achan[to_chan].mycall, 
-			&save_digi_config_p->alias[from_chan][to_chan], &save_digi_config_p->wide[from_chan][to_chan], 
+	      result = digipeat_match (from_chan, pp, save_audio_config_p->mycall[from_chan],
+					   save_audio_config_p->mycall[to_chan],
+			&save_digi_config_p->alias[from_chan][to_chan], &save_digi_config_p->wide[from_chan][to_chan],
 			to_chan, save_digi_config_p->preempt[from_chan][to_chan],
 				save_digi_config_p->atgp[from_chan][to_chan],
 				save_digi_config_p->filter_str[from_chan][to_chan]);
@@ -641,9 +641,9 @@ void digi_regen (int from_chan, packet_t pp)
 
 	// dw_printf ("digi_regen()\n");
 	
-	assert (from_chan >= 0 && from_chan < MAX_CHANS);
+	assert (from_chan >= 0 && from_chan < MAX_TOTAL_CHANS);
 
-	for (to_chan=0; to_chan<MAX_CHANS; to_chan++) {
+	for (to_chan=0; to_chan<MAX_TOTAL_CHANS; to_chan++) {
 	  if (save_digi_config_p->regen[from_chan][to_chan]) {
 	    result = ax25_dup (pp); 
 	    if (result != NULL) {
diff --git a/src/digipeater.h b/src/digipeater.h
index 5c849769..46d955da 100644
--- a/src/digipeater.h
+++ b/src/digipeater.h
@@ -4,7 +4,7 @@
 
 #include "regex.h"
 
-#include "direwolf.h"		/* for MAX_CHANS */
+#include "direwolf.h"		/* for MAX_TOTAL_CHANS */
 #include "ax25_pad.h"		/* for packet_t */
 #include "audio.h"		/* for radio channel properties */
 
@@ -29,25 +29,25 @@ struct digi_config_s {
  * Rules for each of the [from_chan][to_chan] combinations.
  */
 
-	regex_t	alias[MAX_CHANS][MAX_CHANS];
+	regex_t	alias[MAX_TOTAL_CHANS][MAX_TOTAL_CHANS];
 
-	regex_t	wide[MAX_CHANS][MAX_CHANS];
+	regex_t	wide[MAX_TOTAL_CHANS][MAX_TOTAL_CHANS];
 
-	int	enabled[MAX_CHANS][MAX_CHANS];
+	int	enabled[MAX_TOTAL_CHANS][MAX_TOTAL_CHANS];
 
-	enum preempt_e { PREEMPT_OFF, PREEMPT_DROP, PREEMPT_MARK, PREEMPT_TRACE } preempt[MAX_CHANS][MAX_CHANS];
+	enum preempt_e { PREEMPT_OFF, PREEMPT_DROP, PREEMPT_MARK, PREEMPT_TRACE } preempt[MAX_TOTAL_CHANS][MAX_TOTAL_CHANS];
 
 	// ATGP is an ugly hack for the specific need of ATGP which needs more that 8 digipeaters.
 	// DO NOT put this in the User Guide.  On a need to know basis.
 
-	char atgp[MAX_CHANS][MAX_CHANS][AX25_MAX_ADDR_LEN];
+	char atgp[MAX_TOTAL_CHANS][MAX_TOTAL_CHANS][AX25_MAX_ADDR_LEN];
 
-	char *filter_str[MAX_CHANS+1][MAX_CHANS+1];
+	char *filter_str[MAX_TOTAL_CHANS+1][MAX_TOTAL_CHANS+1];
 						// NULL or optional Packet Filter strings such as "t/m".
 						// Notice the size of arrays is one larger than normal.
 						// That extra position is for the IGate.
 
-	int regen[MAX_CHANS][MAX_CHANS];	// Regenerate packet.  
+	int regen[MAX_TOTAL_CHANS][MAX_TOTAL_CHANS];	// Regenerate packet.
 						// Sort of like digipeating but passed along unchanged.
 };
 
diff --git a/src/direwolf.c b/src/direwolf.c
index 2dfa58d3..2bffcc21 100644
--- a/src/direwolf.c
+++ b/src/direwolf.c
@@ -1,7 +1,7 @@
 //
 //    This file is part of Dire Wolf, an amateur radio packet TNC.
 //
-//    Copyright (C) 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2019, 2020, 2021, 2023  John Langner, WB2OSZ
+//    Copyright (C) 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2019, 2020, 2021, 2023, 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
@@ -130,6 +130,7 @@
 #include "dns_sd_dw.h"
 #include "dlq.h"		// for fec_type_t definition.
 #include "deviceid.h"
+#include "nettnc.h"
 
 
 //static int idx_decoded = 0;
@@ -228,6 +229,7 @@ int main (int argc, char *argv[])
 #endif
 	int d_x_opt = 1;	/* "-d x" option for FX.25.  Default minimal. Repeat for more detail.  -qx to silence. */
 	int d_2_opt = 0;	/* "-d 2" option for IL2P.  Default minimal. Repeat for more detail. */
+	int d_c_opt = 0;	/* "-d c" option for connected mode data link state machine. */
 
 	int aprstt_debug = 0;	/* "-d d" option for APRStt (think Dtmf) debug. */
 
@@ -303,7 +305,7 @@ int main (int argc, char *argv[])
 	text_color_init(t_opt);
 	text_color_set(DW_COLOR_INFO);
 	//dw_printf ("Dire Wolf version %d.%d (%s) BETA TEST 7\n", MAJOR_VERSION, MINOR_VERSION, __DATE__);
-	dw_printf ("Dire Wolf DEVELOPMENT version %d.%d %s (%s)\n", MAJOR_VERSION, MINOR_VERSION, "A", __DATE__);
+	dw_printf ("Dire Wolf DEVELOPMENT version %d.%d %s (%s)\n", MAJOR_VERSION, MINOR_VERSION, "D", __DATE__);
 	//dw_printf ("Dire Wolf version %d.%d\n", MAJOR_VERSION, MINOR_VERSION);
 
 
@@ -390,6 +392,7 @@ int main (int argc, char *argv[])
 	    text_color_set(DW_COLOR_ERROR);
 	    for (int n=0; n<15; n++) {
 	      dw_printf ("\n");
+	      dw_printf ("Why are you running this as root user?.\n");
 	      dw_printf ("Dire Wolf requires only privileges available to ordinary users.\n");
 	      dw_printf ("Running this as root is an unnecessary security risk.\n");
 	      //SLEEP_SEC(1);
@@ -558,7 +561,7 @@ int main (int argc, char *argv[])
       	    	break;
       	     }
 	    }
-	    if (x_opt_chan < 0 || x_opt_chan >= MAX_CHANS) {
+	    if (x_opt_chan < 0 || x_opt_chan >= MAX_RADIO_CHANS) {
 	      text_color_set(DW_COLOR_ERROR);
 	      dw_printf ("Invalid channel %d for -x. \n", x_opt_chan);
 	      text_color_set(DW_COLOR_INFO);
@@ -637,6 +640,7 @@ int main (int argc, char *argv[])
 #if USE_HAMLIB
 	      case 'h':  d_h_opt++; break;			// Hamlib verbose level.
 #endif
+	      case 'c':  d_c_opt++; break;			// Connected mode data link state machine
 	      case 'x':  d_x_opt++; break;			// FX.25
 	      case '2':  d_2_opt++; break;			// IL2P
 	      case 'd':	 aprstt_debug++; break;			// APRStt (mnemonic Dtmf)
@@ -1004,6 +1008,13 @@ int main (int argc, char *argv[])
 	fx25_init (d_x_opt);
 	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.
+ * I put it here so channel properties would come out in right order.
+ */
+	nettnc_init (&audio_config);
+
 /*
  * Initialize the touch tone decoder & APRStt gateway.
  */
@@ -1108,7 +1119,7 @@ int main (int argc, char *argv[])
 	igate_init (&audio_config, &igate_config, &digi_config, d_i_opt);
 	cdigipeater_init (&audio_config, &cdigi_config);
 	pfilter_init (&igate_config, d_f_opt);
-	ax25_link_init (&misc_config);
+	ax25_link_init (&misc_config, d_c_opt);
 
 /*
  * Provide the AGW & KISS socket interfaces for use by a client application.
@@ -1167,7 +1178,10 @@ int main (int argc, char *argv[])
  *
  * Inputs:	chan	- Audio channel number, 0 or 1.
  *		subchan	- Which modem caught it.  
- *			  Special case -1 for DTMF decoder.
+ *			  Special cases:
+ *				-1 for DTMF decoder.
+ *				-2 for channel mapped to APRS-IS.
+ *				-3 for channel mapped to network TNC.
  *		slice	- Slicer which caught it.
  *		pp	- Packet handle.
  *		alevel	- Audio level, range of 0 - 100.
@@ -1198,7 +1212,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 >= -2 && subchan < MAX_SUBCHANS);
+	assert (subchan >= -3 && subchan < MAX_SUBCHANS);
 	assert (slice >= 0 && slice < MAX_SLICERS);
 	assert (pp != NULL);	// 1.1J+
      
@@ -1279,7 +1293,13 @@ void app_process_rec_packet (int chan, int subchan, int slice, packet_t pp, alev
 
 	    ax25_get_addr_with_ssid(pp, h-1, probably_really);
 
-	    dw_printf ("%s (probably %s) audio level = %s  %s  %s\n", heard, probably_really, alevel_text, display_retries, spectrum);
+	    // audio level applies only for internal modem channels.
+	    if (subchan >=0) {
+	      dw_printf ("%s (probably %s) audio level = %s  %s  %s\n", heard, probably_really, alevel_text, display_retries, spectrum);
+	    }
+	    else {
+	      dw_printf ("%s (probably %s)\n", heard, probably_really);
+	    }
 
 	  }
 	  else if (strcmp(heard, "DTMF") == 0) {
@@ -1288,7 +1308,13 @@ void app_process_rec_packet (int chan, int subchan, int slice, packet_t pp, alev
 	  }
 	  else {
 
-	    dw_printf ("%s audio level = %s  %s  %s\n", heard, alevel_text, display_retries, spectrum);
+	    // audio level applies only for internal modem channels.
+	    if (subchan >= 0) {
+	      dw_printf ("%s audio level = %s  %s  %s\n", heard, alevel_text, display_retries, spectrum);
+	    }
+	    else {
+	      dw_printf ("%s\n", heard);
+	    }
 	  }
 	 }
 	}
@@ -1305,7 +1331,7 @@ void app_process_rec_packet (int chan, int subchan, int slice, packet_t pp, alev
 	  dw_printf ("Audio input level is too high.  Reduce so most stations are around 50.\n");
 	}
 // FIXME: rather than checking for ichannel, how about checking medium==radio
-	else if (alevel.rec < 5 && chan != audio_config.igate_vchannel) {
+	else if (alevel.rec < 5 && chan != audio_config.igate_vchannel && subchan != -3) {
 
 	  text_color_set(DW_COLOR_ERROR);
 	  dw_printf ("Audio input level is too low.  Increase so most stations are around 50.\n");
@@ -1330,14 +1356,18 @@ void app_process_rec_packet (int chan, int subchan, int slice, packet_t pp, alev
 	  strlcpy (ts, "", sizeof(ts));
 	}
 
-	if (subchan == -1) {
+	if (subchan == -1) {	// dtmf
 	  text_color_set(DW_COLOR_REC);
 	  dw_printf ("[%d.dtmf%s] ", chan, ts);
 	}
-	else if (subchan == -2) {
+	else if (subchan == -2) {	// APRS-IS
 	  text_color_set(DW_COLOR_REC);
 	  dw_printf ("[%d.is%s] ", chan, ts);
 	}
+	else if (subchan == -3) {	// nettnc
+	  text_color_set(DW_COLOR_REC);
+	  dw_printf ("[%d%s] ", chan, ts);
+	}
 	else {
 	  if (ax25_is_aprs(pp)) {
 	    text_color_set(DW_COLOR_REC);
@@ -1498,7 +1528,7 @@ void app_process_rec_packet (int chan, int subchan, int slice, packet_t pp, alev
 		0, 0, 0, A.g_comment,	// freq, tone, offset
 		ais_obj_info, sizeof(ais_obj_info));
 
-	      snprintf (ais_obj_packet, sizeof(ais_obj_packet), "%s>%s%1d%1d:%s", A.g_src, APP_TOCALL, MAJOR_VERSION, MINOR_VERSION, ais_obj_info);
+	      snprintf (ais_obj_packet, sizeof(ais_obj_packet), "%s>%s%1d%1d,NOGATE:%s", A.g_src, APP_TOCALL, MAJOR_VERSION, MINOR_VERSION, ais_obj_info);
 
 	      dw_printf ("[%d.AIS] %s\n", chan, ais_obj_packet);
 
@@ -1614,9 +1644,10 @@ void app_process_rec_packet (int chan, int subchan, int slice, packet_t pp, alev
  * Use only those with correct CRC (or using FEC.)
  */
 
-	  if (retries == RETRY_NONE || fec_type == fec_type_fx25 || fec_type == fec_type_il2p) {
-
-	    cdigipeater (chan, pp);
+	  if (chan < MAX_RADIO_CHANS) {
+	    if (retries == RETRY_NONE || fec_type == fec_type_fx25 || fec_type == fec_type_il2p) {
+	      cdigipeater (chan, pp);
+	    }
 	  }
 	}
 
@@ -1708,6 +1739,7 @@ static void usage (char **argv)
 #if USE_HAMLIB
 	dw_printf ("       h             h = hamlib increase verbose level.\n");
 #endif
+	dw_printf ("       c             c = Connected mode data link state machine.\n");
 	dw_printf ("       x             x = FX.25 increase verbose level.\n");
 	dw_printf ("       2             2 = IL2P.\n");
 	dw_printf ("       d             d = APRStt (DTMF to APRS object translation).\n");
diff --git a/src/direwolf.h b/src/direwolf.h
index 69b09529..a6db3221 100644
--- a/src/direwolf.h
+++ b/src/direwolf.h
@@ -56,15 +56,10 @@
  *
  *	ADevice 0:	channel 0
  *	ADevice 1:	left = 2, right = 3
- *
- * TODO1.2:  Look for any places that have
- *		for (ch=0; ch<MAX_CHANS; ch++) ...
- * and make sure they handle undefined channels correctly.
  */
 
 #define MAX_RADIO_CHANS ((MAX_ADEVS) * 2)
 
-#define MAX_CHANS MAX_RADIO_CHANS	// TODO: Replace all former  with latter to avoid confusion with following.
 
 #define MAX_TOTAL_CHANS 16		// v1.7 allows additional virtual channels which are connected
 					// to something other than radio modems.
@@ -77,7 +72,7 @@
  */
 
 #ifdef USE_HAMLIB
-#define MAX_RIGS MAX_CHANS
+#define MAX_RIGS MAX_RADIO_CHANS
 #endif
 
 /*
diff --git a/src/dlq.c b/src/dlq.c
index f56b8649..59d90d52 100644
--- a/src/dlq.c
+++ b/src/dlq.c
@@ -498,7 +498,7 @@ void dlq_connect_request (char addrs[AX25_MAX_ADDRS][AX25_MAX_ADDR_LEN], int num
 	dw_printf ("dlq_connect_request (...)\n");
 #endif
 
-	assert (chan >= 0 && chan < MAX_CHANS);
+	assert (chan >= 0 && chan < MAX_RADIO_CHANS);
 
 /* Allocate a new queue item. */
 
@@ -556,7 +556,7 @@ void dlq_disconnect_request (char addrs[AX25_MAX_ADDRS][AX25_MAX_ADDR_LEN], int
 	dw_printf ("dlq_disconnect_request (...)\n");
 #endif
 
-	assert (chan >= 0 && chan < MAX_CHANS);
+	assert (chan >= 0 && chan < MAX_RADIO_CHANS);
 
 /* Allocate a new queue item. */
 
@@ -619,7 +619,7 @@ void dlq_outstanding_frames_request (char addrs[AX25_MAX_ADDRS][AX25_MAX_ADDR_LE
 	dw_printf ("dlq_outstanding_frames_request (...)\n");
 #endif
 
-	assert (chan >= 0 && chan < MAX_CHANS);
+	assert (chan >= 0 && chan < MAX_RADIO_CHANS);
 
 /* Allocate a new queue item. */
 
@@ -691,7 +691,7 @@ void dlq_xmit_data_request (char addrs[AX25_MAX_ADDRS][AX25_MAX_ADDR_LEN], int n
 	dw_printf ("dlq_xmit_data_request (...)\n");
 #endif
 
-	assert (chan >= 0 && chan < MAX_CHANS);
+	assert (chan >= 0 && chan < MAX_RADIO_CHANS);
 
 /* Allocate a new queue item. */
 
@@ -758,7 +758,7 @@ void dlq_register_callsign (char *addr, int chan, int client)
 	dw_printf ("dlq_register_callsign (%s, chan=%d, client=%d)\n", addr, chan, client);
 #endif
 
-	assert (chan >= 0 && chan < MAX_CHANS);
+	assert (chan >= 0 && chan < MAX_RADIO_CHANS);
 
 /* Allocate a new queue item. */
 
@@ -793,7 +793,7 @@ void dlq_unregister_callsign (char *addr, int chan, int client)
 	dw_printf ("dlq_unregister_callsign (%s, chan=%d, client=%d)\n", addr, chan, client);
 #endif
 
-	assert (chan >= 0 && chan < MAX_CHANS);
+	assert (chan >= 0 && chan < MAX_RADIO_CHANS);
 
 /* Allocate a new queue item. */
 
diff --git a/src/dtmf.c b/src/dtmf.c
index 953b0f70..447366f9 100644
--- a/src/dtmf.c
+++ b/src/dtmf.c
@@ -80,7 +80,7 @@ static struct dd_s {	 /* Separate for each audio channel. */
 	char prev_debounced;
 	int timeout;
 
-} dd[MAX_CHANS];		
+} dd[MAX_RADIO_CHANS];
 
 
 static int s_amplitude = 100;	// range of 0 .. 100
@@ -129,7 +129,7 @@ void dtmf_init (struct audio_s *p_audio_config, int amp)
  * Larger = narrower bandwidth, slower response.
  */
 
-	for (c=0; c<MAX_CHANS; c++) {
+	for (c=0; c<MAX_RADIO_CHANS; c++) {
 	  struct dd_s *D = &(dd[c]);
 	  int a = ACHAN2ADEV(c);
 
@@ -167,7 +167,7 @@ void dtmf_init (struct audio_s *p_audio_config, int amp)
 	  }
 	}
 
-	for (c=0; c<MAX_CHANS; c++) {
+	for (c=0; c<MAX_RADIO_CHANS; c++) {
 	  struct dd_s *D = &(dd[c]); 
 	  D->n = 0;
 	  for (j=0; j<NUM_TONES; j++) {
@@ -214,6 +214,11 @@ char dtmf_sample (int c, float input)
 						'7', '8', '9', 'C',
 						'*', '0', '#', 'D' };
 
+// Only applies to radio channels.  Should not be here.
+	if (c >= MAX_RADIO_CHANS) {
+	  return ('$');
+	}
+
 	D = &(dd[c]);
 
 	for (i=0; i<NUM_TONES; i++) {
diff --git a/src/dwsock.c b/src/dwsock.c
index 6324a2fc..939253f4 100644
--- a/src/dwsock.c
+++ b/src/dwsock.c
@@ -125,7 +125,7 @@ int dwsock_init(void)
 
 /*-------------------------------------------------------------------
  *
- * Name:        sock_connect
+ * Name:        dwsock_connect
  *
  * Purpose:     Connect to given host / port.  
  *
diff --git a/src/encode_aprs.c b/src/encode_aprs.c
index 20992bf7..a1823cd0 100644
--- a/src/encode_aprs.c
+++ b/src/encode_aprs.c
@@ -596,14 +596,22 @@ int encode_position (int messaging, int compressed, double lat, double lon, int
 	presult[result_len] = '\0';
 
 /* Altitude.  Can be anywhere in comment. */
+// Officially, altitude must be six digits.
+// What about all the places on the earth's surface that are below sea level?
+// https://en.wikipedia.org/wiki/List_of_places_on_land_with_elevations_below_sea_level
+
+// The MIC-E format allows negative altitudes; not allowing it for /A=123456 seems to be an oversight.
+// Most modern applications recognize the form /A=-12345 with minus and five digits.
+// This maintains the same total field width and the range is more than adequate.
 
 	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 < 0) alt_ft = 0;
+	  if (alt_ft < -99999) alt_ft = -99999;
 	  if (alt_ft > 999999) alt_ft = 999999;
-	  snprintf (salt, sizeof(salt), "/A=%06d", alt_ft);
+	  snprintf (salt, sizeof(salt), "/A=%06d", alt_ft);	// /A=123456 ot /A=-12345
 	  strlcat (presult, salt, result_size);
 	  result_len += strlen(salt);
 	}
diff --git a/src/fsk_demod_state.h b/src/fsk_demod_state.h
index c9b26c23..e094bb41 100644
--- a/src/fsk_demod_state.h
+++ b/src/fsk_demod_state.h
@@ -469,7 +469,7 @@ struct demodulator_state_s
  *
  * Inputs:	D		Pointer to demodulator state.
  *
- *		chan		Radio channel: 0 to MAX_CHANS - 1	
+ *		chan		Radio channel: 0 to MAX_RADIO_CHANS - 1
  *
  *		subchan		Which of multiple demodulators: 0 to MAX_SUBCHANS - 1
  *
diff --git a/src/fx25_rec.c b/src/fx25_rec.c
index 9cb5c4d9..8e6d4222 100644
--- a/src/fx25_rec.c
+++ b/src/fx25_rec.c
@@ -59,7 +59,7 @@ struct fx_context_s {
 	unsigned char block[FX25_BLOCK_SIZE+1];
 };
 
-static struct fx_context_s *fx_context[MAX_CHANS][MAX_SUBCHANS][MAX_SLICERS];
+static struct fx_context_s *fx_context[MAX_RADIO_CHANS][MAX_SUBCHANS][MAX_SLICERS];
 
 static void process_rs_block (int chan, int subchan, int slice, struct fx_context_s *F);
 
@@ -157,7 +157,7 @@ void fx25_rec_bit (int chan, int subchan, int slice, int dbit)
 
 	struct fx_context_s *F = fx_context[chan][subchan][slice];
 	if (F == NULL) {
-          assert (chan >= 0 && chan < MAX_CHANS);
+          assert (chan >= 0 && chan < MAX_RADIO_CHANS);
           assert (subchan >= 0 && subchan < MAX_SUBCHANS);
           assert (slice >= 0 && slice < MAX_SLICERS);
 	  F = fx_context[chan][subchan][slice] = (struct fx_context_s *)malloc(sizeof (struct fx_context_s));
@@ -256,7 +256,7 @@ void fx25_rec_bit (int chan, int subchan, int slice, int dbit)
 
 int fx25_rec_busy (int chan)
 {
-	assert (chan >= 0 && chan < MAX_CHANS);
+	assert (chan >= 0 && chan < MAX_RADIO_CHANS);
 
 	// This could be a little faster if we knew number of
 	// subchannels and slicers but it is probably insignificant.
diff --git a/src/fx25_send.c b/src/fx25_send.c
index 7435be9f..0841a3fd 100644
--- a/src/fx25_send.c
+++ b/src/fx25_send.c
@@ -41,7 +41,7 @@ static void send_bit (int chan, int b);
 static int stuff_it (unsigned char *in, int ilen, unsigned char *out, int osize);
 
 
-static int number_of_bits_sent[MAX_CHANS];		// Count number of bits sent by "fx25_send_frame" or "???"
+static int number_of_bits_sent[MAX_RADIO_CHANS];		// Count number of bits sent by "fx25_send_frame" or "???"
 
 
 #if FXTEST
@@ -249,7 +249,7 @@ static void send_bytes (int chan, unsigned char *b, int count)
  */
 static void send_bit (int chan, int b)
 {
-	static int output[MAX_CHANS];
+	static int output[MAX_RADIO_CHANS];
 
 	if (b == 0) {
 	  output[chan] = ! output[chan];
diff --git a/src/gen_packets.c b/src/gen_packets.c
index 57b2741c..e98e774f 100644
--- a/src/gen_packets.c
+++ b/src/gen_packets.c
@@ -242,7 +242,7 @@ int main(int argc, char **argv)
         modem.adev[0].samples_per_sec = DEFAULT_SAMPLES_PER_SEC;        /* -r option */
         modem.adev[0].bits_per_sample = DEFAULT_BITS_PER_SAMPLE;        /* -8 for 8 instead of 16 bits */
         
-	for (chan = 0; chan < MAX_CHANS; chan++) {
+	for (chan = 0; chan < MAX_RADIO_CHANS; chan++) {
 	  modem.achan[chan].modem_type = MODEM_AFSK;			/* change with -g */
 	  modem.achan[chan].mark_freq = DEFAULT_MARK_FREQ;              /* -m option */
           modem.achan[chan].space_freq = DEFAULT_SPACE_FREQ;            /* -s option */
diff --git a/src/gen_tone.c b/src/gen_tone.c
index 6a816556..400c2920 100644
--- a/src/gen_tone.c
+++ b/src/gen_tone.c
@@ -63,14 +63,14 @@ static struct audio_s *save_audio_config_p = NULL;
 
 #define TICKS_PER_CYCLE ( 256.0 * 256.0 * 256.0 * 256.0 )
 
-static int ticks_per_sample[MAX_CHANS];	/* Same for both channels of same soundcard */
+static int ticks_per_sample[MAX_RADIO_CHANS];	/* Same for both channels of same soundcard */
 					/* because they have same sample rate */
 					/* but less confusing to have for each channel. */
 
-static int ticks_per_bit[MAX_CHANS];
-static int f1_change_per_sample[MAX_CHANS];
-static int f2_change_per_sample[MAX_CHANS];
-static float samples_per_symbol[MAX_CHANS];
+static int ticks_per_bit[MAX_RADIO_CHANS];
+static int f1_change_per_sample[MAX_RADIO_CHANS];
+static int f2_change_per_sample[MAX_RADIO_CHANS];
+static float samples_per_symbol[MAX_RADIO_CHANS];
 
 
 static short sine_table[256];
@@ -78,7 +78,7 @@ static short sine_table[256];
 
 /* Accumulators. */
 
-static unsigned int tone_phase[MAX_CHANS]; // Phase accumulator for tone generation.
+static unsigned int tone_phase[MAX_RADIO_CHANS]; // Phase accumulator for tone generation.
 					   // Upper bits are used as index into sine table.
 
 #define PHASE_SHIFT_180 ( 128u << 24 )
@@ -86,11 +86,11 @@ static unsigned int tone_phase[MAX_CHANS]; // Phase accumulator for tone generat
 #define PHASE_SHIFT_45  (  32u << 24 )
 
 
-static int bit_len_acc[MAX_CHANS];	// To accumulate fractional samples per bit.
+static int bit_len_acc[MAX_RADIO_CHANS];	// To accumulate fractional samples per bit.
 
-static int lfsr[MAX_CHANS];		// Shift register for scrambler.
+static int lfsr[MAX_RADIO_CHANS];		// Shift register for scrambler.
 
-static int bit_count[MAX_CHANS];	// Counter incremented for each bit transmitted
+static int bit_count[MAX_RADIO_CHANS];	// Counter incremented for each bit transmitted
 					// on the channel.   This is only used for QPSK.
 					// The LSB determines if we save the bit until
 					// next time, or send this one with the previously saved.
@@ -101,10 +101,10 @@ static int bit_count[MAX_CHANS];	// Counter incremented for each bit transmitted
 					// For 8PSK, it has a different meaning.  It is the
 					// number of bits in 'save_bit' so we can accumulate
 					// three for each symbol.
-static int save_bit[MAX_CHANS];
+static int save_bit[MAX_RADIO_CHANS];
 
 
-static int prev_dat[MAX_CHANS];		// Previous data bit.  Used for G3RUH style.
+static int prev_dat[MAX_RADIO_CHANS];		// Previous data bit.  Used for G3RUH style.
 
 
 
@@ -163,7 +163,7 @@ int gen_tone_init (struct audio_s *audio_config_p, int amp, int gen_packets)
 	
 	amp16bit = (int)((32767 * amp) / 100);
 
-	for (chan = 0; chan < MAX_CHANS; chan++) {
+	for (chan = 0; chan < MAX_RADIO_CHANS; chan++) {
 
 	  if (audio_config_p->chan_medium[chan] == MEDIUM_RADIO) {
 
@@ -352,8 +352,8 @@ static const int gray2phase_v27[8] = {1, 0, 2, 3, 6, 7, 5, 4};
 
 // #define PSKIQ 1  // not ready for prime time yet.
 #if PSKIQ
-static int xmit_octant[MAX_CHANS];	// absolute phase in 45 degree units.
-static int xmit_prev_octant[MAX_CHANS];	// from previous symbol.
+static int xmit_octant[MAX_RADIO_CHANS];	// absolute phase in 45 degree units.
+static int xmit_prev_octant[MAX_RADIO_CHANS];	// from previous symbol.
 
 // For PSK, we generate the final signal by combining fixed frequency cosine and
 // sine by the following weights.
diff --git a/src/hdlc_rec.c b/src/hdlc_rec.c
index d87a1b50..cfae77a6 100644
--- a/src/hdlc_rec.c
+++ b/src/hdlc_rec.c
@@ -114,11 +114,11 @@ struct hdlc_state_s {
 	int eas_fields_after_plus;	/* Number of "-" characters after the "+". */
 };
 
-static struct hdlc_state_s hdlc_state[MAX_CHANS][MAX_SUBCHANS][MAX_SLICERS];
+static struct hdlc_state_s hdlc_state[MAX_RADIO_CHANS][MAX_SUBCHANS][MAX_SLICERS];
 
-static int num_subchan[MAX_CHANS];		//TODO1.2 use ptr rather than copy.
+static int num_subchan[MAX_RADIO_CHANS];		//TODO1.2 use ptr rather than copy.
 
-static int composite_dcd[MAX_CHANS][MAX_SUBCHANS+1];
+static int composite_dcd[MAX_RADIO_CHANS][MAX_SUBCHANS+1];
 
 
 /***********************************************************************************
@@ -149,7 +149,7 @@ void hdlc_rec_init (struct audio_s *pa)
 
 	memset (composite_dcd, 0, sizeof(composite_dcd));
 
-	for (ch = 0; ch < MAX_CHANS; ch++)
+	for (ch = 0; ch < MAX_RADIO_CHANS; ch++)
 	{
 
 	  if (pa->chan_medium[ch] == MEDIUM_RADIO) {
@@ -429,17 +429,24 @@ a good modem here and providing a result when it is received.
  ***********************************************************************************/
 
 void hdlc_rec_bit (int chan, int subchan, int slice, int raw, int is_scrambled, int not_used_remove)
+{
+	static int64_t dummyll = 0;
+	static int dummy = 0;
+	hdlc_rec_bit_new (chan, subchan, slice, raw, is_scrambled, not_used_remove,
+		&dummyll, &dummy);
+}
+
+void hdlc_rec_bit_new (int chan, int subchan, int slice, int raw, int is_scrambled, int not_used_remove,
+		int64_t *pll_nudge_total, int *pll_symbol_count)
 {
 
 	int dbit;			/* Data bit after undoing NRZI. */
 					/* Should be only 0 or 1. */
-	struct hdlc_state_s *H;
 
 	assert (was_init == 1);
 
-	assert (chan >= 0 && chan < MAX_CHANS);
+	assert (chan >= 0 && chan < MAX_RADIO_CHANS);
 	assert (subchan >= 0 && subchan < MAX_SUBCHANS);
-
 	assert (slice >= 0 && slice < MAX_SLICERS);
 
 // -e option can be used to artificially introduce the desired
@@ -467,7 +474,7 @@ void hdlc_rec_bit (int chan, int subchan, int slice, int raw, int is_scrambled,
 /*
  * Different state information for each channel / subchannel / slice.
  */
-	H = &hdlc_state[chan][subchan][slice];
+	struct hdlc_state_s *H = &hdlc_state[chan][subchan][slice];
 
 
 /*
@@ -589,16 +596,44 @@ void hdlc_rec_bit (int chan, int subchan, int slice, int raw, int is_scrambled,
 	  dw_printf ("\nfound flag, channel %d.%d, %d bits in frame\n", chan, subchan, rrbb_get_len(H->rrbb) - 1);
 #endif
 	  if (rrbb_get_len(H->rrbb) >= MIN_FRAME_LEN * 8) {
-		
+
+//JWL - end of frame
+
+	    float speed_error;			// in percentage.
+	    if (*pll_symbol_count > 0) {	// avoid divde by 0.
+
+	      // TODO:
+	      // Fudged to get +-2.0 with gen_packets -b 1224 & 1176.
+	      // Also initialized the symbol counter to -1.
+
+	      speed_error = (float)((double)(*pll_nudge_total) * 100. / (256. * 256. * 256. * 256.) / (double)(*pll_symbol_count) + 0.02);
+
+	      text_color_set(DW_COLOR_DEBUG);
+
+// std	      dw_printf ("DEBUG: total %lld, count %d\n", *pll_nudge_total, *pll_symbol_count);
+// mingw
+//	      dw_printf ("DEBUG: total %I64d, count %d\n", *pll_nudge_total, *pll_symbol_count);
+//	      dw_printf ("DEBUG: speed error  %+0.2f%% -> %+0.1f%% \n", speed_error, speed_error);
+	    }
+	    else {
+	      speed_error = 0;
+	    }
+	    rrbb_set_speed_error (H->rrbb, speed_error);
+
 	    alevel_t alevel = demod_get_audio_level (chan, subchan);
 
 	    rrbb_set_audio_level (H->rrbb, alevel);
 	    hdlc_rec2_block (H->rrbb);
 	    	/* Now owned by someone else who will free it. */
+	    H->rrbb = NULL;
 
 	    H->rrbb = rrbb_new (chan, subchan, slice, is_scrambled, H->lfsr, H->prev_descram); /* Allocate a new one. */
 	  }
 	  else {
+
+//JWL - start of frame
+	    *pll_nudge_total = 0;
+	    *pll_symbol_count = -1;	// comes out better than using 0.
 	    rrbb_clear (H->rrbb, is_scrambled, H->lfsr, H->prev_descram); 
 	  }
 
@@ -730,7 +765,7 @@ void dcd_change (int chan, int subchan, int slice, int state)
 {
 	int old, new;
 
-	assert (chan >= 0 && chan < MAX_CHANS);
+	assert (chan >= 0 && chan < MAX_RADIO_CHANS);
 	assert (subchan >= 0 && subchan <= MAX_SUBCHANS);
 	assert (slice >= 0 && slice < MAX_SLICERS);
 	assert (state == 0 || state == 1);
@@ -791,7 +826,7 @@ int hdlc_rec_data_detect_any (int chan)
 {
 
 	int sc;
-	assert (chan >= 0 && chan < MAX_CHANS);
+	assert (chan >= 0 && chan < MAX_RADIO_CHANS);
 
 	for (sc = 0; sc < num_subchan[chan]; sc++) {
 	  if (composite_dcd[chan][sc] != 0)
diff --git a/src/hdlc_rec.h b/src/hdlc_rec.h
index 69b60a92..21cbf6c8 100644
--- a/src/hdlc_rec.h
+++ b/src/hdlc_rec.h
@@ -1,12 +1,22 @@
 
+/* hdlc_rec.h */
+
+
+
+
+#include <stdint.h>          // int64_t
 
 #include "audio.h"
 
 
 void hdlc_rec_init (struct audio_s *pa);
 
+// TODO: change all to _new.
 void hdlc_rec_bit (int chan, int subchan, int slice, int raw, int is_scrambled, int descram_state);
 
+void hdlc_rec_bit_new (int chan, int subchan, int slice, int raw, int is_scrambled, int descram_state,
+			int64_t *pll_nudge_total, int *pll_nudge_count);
+
 /* Provided elsewhere to process a complete frame. */
 
 //void process_rec_frame (int chan, unsigned char *fbuf, int flen, int level);
diff --git a/src/hdlc_rec2.c b/src/hdlc_rec2.c
index b817018f..ebaac6c0 100644
--- a/src/hdlc_rec2.c
+++ b/src/hdlc_rec2.c
@@ -216,6 +216,8 @@ void hdlc_rec2_init (struct audio_s *p_audio_config)
  * Purpose:	Extract HDLC frame from a stream of bits.
  *
  * Inputs:	block 		- Handle for bit array.
+ *				  This will be deallocated so the caller
+ *				  must not hold on to the address.
  *
  * Description:	The other (original) hdlc decoder took one bit at a time
  *		right out of the demodulator.
@@ -287,12 +289,10 @@ void hdlc_rec2_block (rrbb_t block)
 	  /* Let thru even with bad CRC.  Of course, it still */
 	  /* needs to be a minimum number of whole octets. */
 	  ok = try_decode (block, chan, subchan, slice, alevel, retry_cfg, 1);
-	  rrbb_delete (block);
-	}
-	else {  
-	  rrbb_delete (block); 
 	}
 
+	rrbb_delete (block);
+
 } /* end hdlc_rec2_block */
 
 
@@ -438,7 +438,7 @@ static int try_to_fix_quick_now (rrbb_t block, int chan, int subchan, int slice,
 	retry_cfg.u_bits.sep.bit_idx_c = -1;
 
 #ifdef DEBUG_LATER
-	tstart = dtime_now();
+	tstart = dtime_monotonic();
 	dw_printf ("*** Try flipping TWO SEPARATED BITS %d bits\n", len);
 #endif
 	len = rrbb_get_len(block);
diff --git a/src/hdlc_send.c b/src/hdlc_send.c
index 8a1cdc6d..5b2008d3 100644
--- a/src/hdlc_send.c
+++ b/src/hdlc_send.c
@@ -39,7 +39,7 @@ static void send_bit_nrzi (int, int);
 
 
 
-static int number_of_bits_sent[MAX_CHANS];	// Count number of bits sent by "hdlc_send_frame" or "hdlc_send_flags"
+static int number_of_bits_sent[MAX_RADIO_CHANS];	// Count number of bits sent by "hdlc_send_frame" or "hdlc_send_flags"
 
 
 
@@ -240,7 +240,7 @@ static void send_byte_msb_first (int chan, int x, int polarity)
 // Data (non flags) use bit stuffing.
 
 
-static int stuff[MAX_CHANS];		// Count number of "1" bits to keep track of when we
+static int stuff[MAX_RADIO_CHANS];		// Count number of "1" bits to keep track of when we
 					// need to break up a long run by "bit stuffing."
 					// Needs to be array because we could be transmitting
 					// on multiple channels at the same time.
@@ -284,7 +284,7 @@ static void send_data_nrzi (int chan, int x)
 
 static void send_bit_nrzi (int chan, int b)
 {
-	static int output[MAX_CHANS];
+	static int output[MAX_RADIO_CHANS];
 
 	if (b == 0) {
 	  output[chan] = ! output[chan];
diff --git a/src/igate.c b/src/igate.c
index 7f84228a..1e5d56ed 100644
--- a/src/igate.c
+++ b/src/igate.c
@@ -216,8 +216,8 @@ int main (int argc, char *argv[])
 
 	memset (&audio_config, 0, sizeof(audio_config));
 	audio_config.adev[0].num_channels = 2;
-	strlcpy (audio_config.achan[0].mycall, "WB2OSZ-1", sizeof(audio_config.achan[0].mycall));
-	strlcpy (audio_config.achan[1].mycall, "WB2OSZ-2", sizeof(audio_config.achan[0].mycall));
+	strlcpy (audio_config.mycall[0], "WB2OSZ-1", sizeof(audio_config.achan[0].mycall));
+	strlcpy (audio_config.mycall[1], "WB2OSZ-2", sizeof(audio_config.achan[0].mycall));
 
 	memset (&igate_config, 0, sizeof(igate_config));
 
@@ -909,10 +909,10 @@ void igate_send_rec_packet (int chan, packet_t recv_pp)
 // Beacon will be channel -1.
 // Client app to ICHANNEL is outside of radio channel range.
 
-	if (chan >= 0 && chan < MAX_CHANS && 		// in radio channel range
-		save_digi_config_p->filter_str[chan][MAX_CHANS] != NULL) {
+	if (chan >= 0 && chan < MAX_TOTAL_CHANS && 		// in radio channel range
+		save_digi_config_p->filter_str[chan][MAX_TOTAL_CHANS] != NULL) {
 
-	  if (pfilter(chan, MAX_CHANS, save_digi_config_p->filter_str[chan][MAX_CHANS], recv_pp, 1) != 1) {
+	  if (pfilter(chan, MAX_TOTAL_CHANS, save_digi_config_p->filter_str[chan][MAX_TOTAL_CHANS], recv_pp, 1) != 1) {
 
 	    // Is this useful troubleshooting information or just distracting noise?
 	    // Originally this was always printed but there was a request to add a "quiet" option to suppress this.
@@ -920,7 +920,7 @@ void igate_send_rec_packet (int chan, packet_t recv_pp)
 
 	    if (s_debug >= 1) {
 	      text_color_set(DW_COLOR_INFO);
-	      dw_printf ("Packet from channel %d to IGate was rejected by filter: %s\n", chan, save_digi_config_p->filter_str[chan][MAX_CHANS]);
+	      dw_printf ("Packet from channel %d to IGate was rejected by filter: %s\n", chan, save_digi_config_p->filter_str[chan][MAX_TOTAL_CHANS]);
 	    }
 	    return;
 	  }
@@ -1141,7 +1141,7 @@ static void send_packet_to_server (packet_t pp, int chan)
 	  strlcat (msg, ",qAO,", sizeof(msg));		// new for version 1.4.
 	}
 
-	strlcat (msg, save_audio_config_p->achan[chan].mycall, sizeof(msg));
+	strlcat (msg, save_audio_config_p->mycall[chan], sizeof(msg));
 	strlcat (msg, ":", sizeof(msg));
 
 
@@ -1781,7 +1781,7 @@ static void maybe_xmit_packet_from_igate (char *message, int to_chan)
 {
 	int n;
 
-	assert (to_chan >= 0 && to_chan < MAX_CHANS);
+	assert (to_chan >= 0 && to_chan < MAX_TOTAL_CHANS);
 
 /*
  * Try to parse it into a packet object; we need this for the packet filtering.
@@ -1856,7 +1856,7 @@ static void maybe_xmit_packet_from_igate (char *message, int to_chan)
  * filtering by stations along the way or the q construct.
  */
 
-	assert (to_chan >= 0 && to_chan < MAX_CHANS);
+	assert (to_chan >= 0 && to_chan < MAX_TOTAL_CHANS);
 
 
 /*
@@ -1906,9 +1906,9 @@ static void maybe_xmit_packet_from_igate (char *message, int to_chan)
 
 	if ( ! msp_special_case) {
 
-	  if (save_digi_config_p->filter_str[MAX_CHANS][to_chan] != NULL) {
+	  if (save_digi_config_p->filter_str[MAX_TOTAL_CHANS][to_chan] != NULL) {
 
-	    if (pfilter(MAX_CHANS, to_chan, save_digi_config_p->filter_str[MAX_CHANS][to_chan], pp3, 1) != 1) {
+	    if (pfilter(MAX_TOTAL_CHANS, to_chan, save_digi_config_p->filter_str[MAX_TOTAL_CHANS][to_chan], pp3, 1) != 1) {
 
 	      // Previously there was a debug message here about the packet being dropped by filtering.
 	      // This is now handled better by the "-df" command line option for filtering details.
@@ -1965,7 +1965,7 @@ static void maybe_xmit_packet_from_igate (char *message, int to_chan)
 	char dest[AX25_MAX_ADDR_LEN];		/* Destination field. */
 	ax25_get_addr_with_ssid (pp3, AX25_DESTINATION, dest);
 	snprintf (payload, sizeof(payload), "%s>%s,TCPIP,%s*:%s",
-				src, dest, save_audio_config_p->achan[to_chan].mycall, pinfo);
+				src, dest, save_audio_config_p->mycall[to_chan], pinfo);
 
 
 #if DEBUGx
@@ -1991,7 +1991,7 @@ static void maybe_xmit_packet_from_igate (char *message, int to_chan)
 	if (ig_to_tx_allow (pp3, to_chan)) {
 	  char radio [2400];
 	  snprintf (radio, sizeof(radio), "%s>%s%d%d%s:}%s",
-				save_audio_config_p->achan[to_chan].mycall,
+				save_audio_config_p->mycall[to_chan],
 				APP_TOCALL, MAJOR_VERSION, MINOR_VERSION,
 				save_igate_config_p->tx_via,
 				payload);
diff --git a/src/il2p_rec.c b/src/il2p_rec.c
index 5ad457a0..a1e27263 100644
--- a/src/il2p_rec.c
+++ b/src/il2p_rec.c
@@ -69,7 +69,7 @@ struct il2p_context_s {
 	int corrected;		// Number of symbols corrected by RS FEC.
 };
 
-static struct il2p_context_s *il2p_context[MAX_CHANS][MAX_SUBCHANS][MAX_SLICERS];
+static struct il2p_context_s *il2p_context[MAX_RADIO_CHANS][MAX_SUBCHANS][MAX_SLICERS];
 
 
 
@@ -101,7 +101,7 @@ void il2p_rec_bit (int chan, int subchan, int slice, int dbit)
 
 	struct il2p_context_s *F = il2p_context[chan][subchan][slice];
 	if (F == NULL) {
-          assert (chan >= 0 && chan < MAX_CHANS);
+          assert (chan >= 0 && chan < MAX_RADIO_CHANS);
           assert (subchan >= 0 && subchan < MAX_SUBCHANS);
           assert (slice >= 0 && slice < MAX_SLICERS);
 	  F = il2p_context[chan][subchan][slice] = (struct il2p_context_s *)malloc(sizeof (struct il2p_context_s));
@@ -251,12 +251,11 @@ void il2p_rec_bit (int chan, int subchan, int slice, int dbit)
 	      if (pp != NULL) {
 	          alevel_t alevel = demod_get_audio_level (chan, subchan);
 	          retry_t retries = F->corrected;
-	          int is_fx25 = 1;		// FIXME: distinguish fx.25 and IL2P.
-						// Currently this just means that a FEC mode was used.
+	          fec_type_t fec_type = fec_type_il2p;
 
 	          // TODO: Could we put last 3 arguments in packet object rather than passing around separately?
 
-	          multi_modem_process_rec_packet (chan, subchan, slice, pp, alevel, retries, is_fx25);
+	          multi_modem_process_rec_packet (chan, subchan, slice, pp, alevel, retries, fec_type);
 	      }
 	    }   // end block for local variables.
 
diff --git a/src/il2p_send.c b/src/il2p_send.c
index 3c4554e0..28948766 100644
--- a/src/il2p_send.c
+++ b/src/il2p_send.c
@@ -30,7 +30,7 @@
 #include "gen_tone.h"
 
 
-static int number_of_bits_sent[MAX_CHANS];		// Count number of bits sent by "il2p_send_frame"
+static int number_of_bits_sent[MAX_RADIO_CHANS];		// Count number of bits sent by "il2p_send_frame"
 
 static void send_bytes (int chan, unsigned char *b, int count, int polarity);
 static void send_bit (int chan, int b, int polarity);
diff --git a/src/kiss_frame.c b/src/kiss_frame.c
index aa581dd2..65a09422 100644
--- a/src/kiss_frame.c
+++ b/src/kiss_frame.c
@@ -612,7 +612,7 @@ void kiss_process_msg (unsigned char *kiss_msg, int kiss_len, int debug, struct
 	    /* Verify that the radio channel number is valid. */
 	    /* Any sort of medium should be OK here. */
 
-	    if ((chan < 0 || chan >= MAX_CHANS || save_audio_config_p->chan_medium[chan] == MEDIUM_NONE) 
+	    if ((chan < 0 || chan >= MAX_TOTAL_CHANS || save_audio_config_p->chan_medium[chan] == MEDIUM_NONE)
 		&& save_audio_config_p->chan_medium[chan]  != MEDIUM_IGATE) {
 	      text_color_set(DW_COLOR_ERROR);
 	      dw_printf ("Invalid transmit channel %d from KISS client app.\n", chan);
@@ -663,10 +663,11 @@ void kiss_process_msg (unsigned char *kiss_msg, int kiss_len, int debug, struct
 	  }
           text_color_set(DW_COLOR_INFO);
 	  dw_printf ("KISS protocol set TXDELAY = %d (*10mS units = %d mS), chan %d\n", kiss_msg[1], kiss_msg[1] * 10, chan);
-	  if (kiss_msg[1] < 4 || kiss_msg[1] > 100) {
+	  if (kiss_msg[1] < 10 || kiss_msg[1] >= 100) {
             text_color_set(DW_COLOR_ERROR);
 	    dw_printf ("Are you sure you want such an extreme value for TXDELAY?\n");
-	    dw_printf ("See \"Radio Channel - Transmit Timing\" section of User Guide for explanation.\n");
+            dw_printf ("Read the Dire Wolf User Guide, \"Radio Channel - Transmit Timing\"\n");
+            dw_printf ("section, to understand what this means.\n");
 	  }
 	  xmit_set_txdelay (chan, kiss_msg[1]);
 	  break;
@@ -683,7 +684,8 @@ void kiss_process_msg (unsigned char *kiss_msg, int kiss_len, int debug, struct
 	  if (kiss_msg[1] < 5 || kiss_msg[1] > 250) {
             text_color_set(DW_COLOR_ERROR);
 	    dw_printf ("Are you sure you want such an extreme value for PERSIST?\n");
-	    dw_printf ("See \"Radio Channel - Transmit Timing\" section of User Guide for explanation.\n");
+            dw_printf ("Read the Dire Wolf User Guide, \"Radio Channel - Transmit Timing\"\n");
+            dw_printf ("section, to understand what this means.\n");
 	  }
 	  xmit_set_persist (chan, kiss_msg[1]);
 	  break;
@@ -700,7 +702,8 @@ void kiss_process_msg (unsigned char *kiss_msg, int kiss_len, int debug, struct
 	  if (kiss_msg[1] < 2 || kiss_msg[1] > 50) {
             text_color_set(DW_COLOR_ERROR);
 	    dw_printf ("Are you sure you want such an extreme value for SLOTTIME?\n");
-	    dw_printf ("See \"Radio Channel - Transmit Timing\" section of User Guide for explanation.\n");
+            dw_printf ("Read the Dire Wolf User Guide, \"Radio Channel - Transmit Timing\"\n");
+            dw_printf ("section, to understand what this means.\n");
 	  }
 	  xmit_set_slottime (chan, kiss_msg[1]);
 	  break;
@@ -714,10 +717,11 @@ void kiss_process_msg (unsigned char *kiss_msg, int kiss_len, int debug, struct
 	  }
           text_color_set(DW_COLOR_INFO);
 	  dw_printf ("KISS protocol set TXtail = %d (*10mS units = %d mS), chan %d\n", kiss_msg[1], kiss_msg[1] * 10, chan);
-	  if (kiss_msg[1] < 2) {
+	  if (kiss_msg[1] < 5) {
             text_color_set(DW_COLOR_ERROR);
 	    dw_printf ("Setting TXTAIL so low is asking for trouble.  You probably don't want to do this.\n");
-	    dw_printf ("See \"Radio Channel - Transmit Timing\" section of User Guide for explanation.\n");
+            dw_printf ("Read the Dire Wolf User Guide, \"Radio Channel - Transmit Timing\"\n");
+            dw_printf ("section, to understand what this means.\n");
 	  }
 	  xmit_set_txtail (chan, kiss_msg[1]);
 	  break;
diff --git a/src/multi_modem.c b/src/multi_modem.c
index d2382f1a..7770a19a 100644
--- a/src/multi_modem.c
+++ b/src/multi_modem.c
@@ -126,7 +126,7 @@ static struct {
 	int age;
 	unsigned int crc;
 	int score;
-} candidate[MAX_CHANS][MAX_SUBCHANS][MAX_SLICERS];
+} candidate[MAX_RADIO_CHANS][MAX_SUBCHANS][MAX_SLICERS];
 
 
 
@@ -135,7 +135,7 @@ static struct {
 #define PROCESS_AFTER_BITS 3
 
 
-static int process_age[MAX_CHANS];
+static int process_age[MAX_RADIO_CHANS];
 
 static void pick_best_candidate (int chan);
 
@@ -172,7 +172,7 @@ void multi_modem_init (struct audio_s *pa)
 	demod_init (save_audio_config_p);
 	hdlc_rec_init (save_audio_config_p);
 
-	for (chan=0; chan<MAX_CHANS; chan++) {
+	for (chan=0; chan<MAX_RADIO_CHANS; chan++) {
 	  if (save_audio_config_p->chan_medium[chan] == MEDIUM_RADIO) {
 	    if (save_audio_config_p->achan[chan].baud <= 0) {
 	      text_color_set(DW_COLOR_ERROR);
@@ -222,7 +222,7 @@ void multi_modem_init (struct audio_s *pa)
  *
  *------------------------------------------------------------------------------*/
 
-static float dc_average[MAX_CHANS];
+static float dc_average[MAX_RADIO_CHANS];
 
 int multi_modem_get_dc_average (int chan)
 {
@@ -319,7 +319,7 @@ void multi_modem_process_rec_frame (int chan, int subchan, int slice, unsigned c
 	packet_t pp;
 
 
-	assert (chan >= 0 && chan < MAX_CHANS);
+	assert (chan >= 0 && chan < MAX_RADIO_CHANS);
 	assert (subchan >= 0 && subchan < MAX_SUBCHANS);
 	assert (slice >= 0 && slice < MAX_SUBCHANS);
 
@@ -329,8 +329,15 @@ void multi_modem_process_rec_frame (int chan, int subchan, int slice, unsigned c
 	  char nmea[256];
 	  ais_to_nmea (fbuf, flen, nmea, sizeof(nmea));
 
+	  // The intention is for the AIS sentences to go only to attached applications.
+	  // e.g. SARTrack knows how to parse the AIS sentences.
+
+	  // Put NOGATE in path so RF>IS IGates will block this.
+	  // TODO: Use station callsign, rather than "AIS," so we know where it is coming from,
+	  // if it happens to get onto RF somehow.
+
 	  char monfmt[276];
-	  snprintf (monfmt, sizeof(monfmt), "AIS>%s%1d%1d:{%c%c%s", APP_TOCALL, MAJOR_VERSION, MINOR_VERSION, USER_DEF_USER_ID, USER_DEF_TYPE_AIS, nmea);
+	  snprintf (monfmt, sizeof(monfmt), "AIS>%s%1d%1d,NOGATE:{%c%c%s", APP_TOCALL, MAJOR_VERSION, MINOR_VERSION, USER_DEF_USER_ID, USER_DEF_TYPE_AIS, nmea);
 	  pp = ax25_from_text (monfmt, 1);
 
 	  // alevel gets in there somehow making me question why it is passed thru here.
@@ -338,7 +345,7 @@ void multi_modem_process_rec_frame (int chan, int subchan, int slice, unsigned c
 	else if (save_audio_config_p->achan[chan].modem_type == MODEM_EAS) {
 	  char monfmt[300];	// EAS SAME message max length is 268
 
-	  snprintf (monfmt, sizeof(monfmt), "EAS>%s%1d%1d:{%c%c%s", APP_TOCALL, MAJOR_VERSION, MINOR_VERSION, USER_DEF_USER_ID, USER_DEF_TYPE_EAS, fbuf);
+	  snprintf (monfmt, sizeof(monfmt), "EAS>%s%1d%1d,NOGATE:{%c%c%s", APP_TOCALL, MAJOR_VERSION, MINOR_VERSION, USER_DEF_USER_ID, USER_DEF_TYPE_EAS, fbuf);
 	  pp = ax25_from_text (monfmt, 1);
 
 	  // alevel gets in there somehow making me question why it is passed thru here.
diff --git a/src/pfilter.c b/src/pfilter.c
index 35767a67..cc51519e 100644
--- a/src/pfilter.c
+++ b/src/pfilter.c
@@ -99,7 +99,7 @@ typedef enum token_type_e { TOKEN_AND, TOKEN_OR, TOKEN_NOT, TOKEN_LPAREN, TOKEN_
 
 typedef struct pfstate_s {
 
-	int from_chan;				/* From and to channels.   MAX_CHANS is used for IGate. */
+	int from_chan;				/* From and to channels.   MAX_TOTAL_CHANS is used for IGate. */
 	int to_chan;				/* Used only for debug and error messages. */
 
 /*
@@ -175,7 +175,7 @@ static char *bool2text (int val)
  *
  * Inputs:	from_chan - Channel packet is coming from.  
  *		to_chan	  - Channel packet is going to.
- *				Both are 0 .. MAX_CHANS-1 or MAX_CHANS for IGate.  
+ *				Both are 0 .. MAX_TOTAL_CHANS-1 or MAX_TOTAL_CHANS for IGate.
  *			 	For debug/error messages only.
  *
  *		filter	- String of filter specs and logical operators to combine them.
@@ -201,8 +201,8 @@ int pfilter (int from_chan, int to_chan, char *filter, packet_t pp, int is_aprs)
 	char *p;
 	int result;
 
-	assert (from_chan >= 0 && from_chan <= MAX_CHANS);
-	assert (to_chan >= 0 && to_chan <= MAX_CHANS);
+	assert (from_chan >= 0 && from_chan <= MAX_TOTAL_CHANS);
+	assert (to_chan >= 0 && to_chan <= MAX_TOTAL_CHANS);
 
 	memset (&pfstate, 0, sizeof(pfstate));
 
@@ -258,10 +258,10 @@ int pfilter (int from_chan, int to_chan, char *filter, packet_t pp, int is_aprs)
 
 	if (s_debug >= 1) {
 	  text_color_set(DW_COLOR_DEBUG);
-	  if (from_chan == MAX_CHANS) {
+	  if (from_chan == MAX_TOTAL_CHANS) {
 	    dw_printf (" Packet filter from IGate to radio channel %d returns %s\n", to_chan, bool2text(result));
 	  }
-	  else if (to_chan == MAX_CHANS) {
+	  else if (to_chan == MAX_TOTAL_CHANS) {
 	    dw_printf (" Packet filter from radio channel %d to IGate returns %s\n", from_chan, bool2text(result));
 	  }
 	  else if (is_aprs) {
@@ -1478,9 +1478,9 @@ static void print_error (pfstate_t *pf, char *msg)
 {
 	char intro[50];
 
-	if (pf->from_chan == MAX_CHANS) {
+	if (pf->from_chan == MAX_TOTAL_CHANS) {
 
-	  if (pf->to_chan == MAX_CHANS) {
+	  if (pf->to_chan == MAX_TOTAL_CHANS) {
 	    snprintf (intro, sizeof(intro), "filter[IG,IG]: ");
 	  }
 	  else {
@@ -1489,7 +1489,7 @@ static void print_error (pfstate_t *pf, char *msg)
 	}
 	else {
 
-	  if (pf->to_chan == MAX_CHANS) {
+	  if (pf->to_chan == MAX_TOTAL_CHANS) {
 	    snprintf (intro, sizeof(intro), "filter[%d,IG]: ", pf->from_chan);
 	  }
 	  else {
diff --git a/src/ptt.c b/src/ptt.c
index af746626..ec093878 100644
--- a/src/ptt.c
+++ b/src/ptt.c
@@ -730,12 +730,12 @@ int gpiod_probe(const char *chip_name, int line_number)
 
 
 
-static HANDLE ptt_fd[MAX_CHANS][NUM_OCTYPES];	
+static HANDLE ptt_fd[MAX_RADIO_CHANS][NUM_OCTYPES];
 					/* Serial port handle or fd.  */
 					/* Could be the same for two channels */	
 					/* if using both RTS and DTR. */
 #if USE_HAMLIB
-static RIG *rig[MAX_CHANS][NUM_OCTYPES];
+static RIG *rig[MAX_RADIO_CHANS][NUM_OCTYPES];
 #endif
 
 static char otnames[NUM_OCTYPES][8];
@@ -761,7 +761,7 @@ void ptt_init (struct audio_s *audio_config_p)
 	strlcpy (otnames[OCTYPE_CON], "CON", sizeof(otnames[OCTYPE_CON]));
 
 
-	for (ch = 0; ch < MAX_CHANS; ch++) {
+	for (ch = 0; ch < MAX_RADIO_CHANS; ch++) {
 	  int ot;
 
 	  for (ot = 0; ot < NUM_OCTYPES; ot++) {
@@ -791,7 +791,7 @@ void ptt_init (struct audio_s *audio_config_p)
  * Set up serial ports.
  */
 
-	for (ch = 0; ch < MAX_CHANS; ch++) {
+	for (ch = 0; ch < MAX_RADIO_CHANS; ch++) {
 
 	  if (audio_config_p->chan_medium[ch] == MEDIUM_RADIO) {
 	    int ot;
@@ -906,7 +906,7 @@ void ptt_init (struct audio_s *audio_config_p)
  */
 
 	using_gpio = 0;
-	for (ch=0; ch<MAX_CHANS; ch++) {
+	for (ch=0; ch<MAX_RADIO_CHANS; ch++) {
 	  if (save_audio_config_p->chan_medium[ch] == MEDIUM_RADIO) {
 	    int ot;
 	    for (ot = 0; ot < NUM_OCTYPES; ot++) {
@@ -927,7 +927,7 @@ void ptt_init (struct audio_s *audio_config_p)
 	}
 #if defined(USE_GPIOD)
     // GPIOD
-	for (ch = 0; ch < MAX_CHANS; ch++) {
+	for (ch = 0; ch < MAX_RADIO_CHANS; ch++) {
 	  if (save_audio_config_p->chan_medium[ch] == MEDIUM_RADIO) {
 	    for (int ot = 0; ot < NUM_OCTYPES; ot++) {
 	      if (audio_config_p->achan[ch].octrl[ot].ptt_method == PTT_METHOD_GPIOD) {
@@ -952,7 +952,7 @@ void ptt_init (struct audio_s *audio_config_p)
  * the pins we want to use.
  */
 	    
-	for (ch = 0; ch < MAX_CHANS; ch++) {
+	for (ch = 0; ch < MAX_RADIO_CHANS; ch++) {
 	  if (save_audio_config_p->chan_medium[ch] == MEDIUM_RADIO) {
 
 	    int ot;	// output control type, PTT, DCD, CON, ...
@@ -984,7 +984,7 @@ void ptt_init (struct audio_s *audio_config_p)
 
 #if  ( defined(__i386__) || defined(__x86_64__) ) && ( defined(__linux__) || defined(__unix__) )
 
-	for (ch = 0; ch < MAX_CHANS; ch++) {
+	for (ch = 0; ch < MAX_RADIO_CHANS; ch++) {
 	  if (save_audio_config_p->chan_medium[ch] == MEDIUM_RADIO) {
 	    int ot;
 	    for (ot = 0; ot < NUM_OCTYPES; ot++) {
@@ -1051,7 +1051,7 @@ void ptt_init (struct audio_s *audio_config_p)
 #endif /* x86 Linux */
 
 #ifdef USE_HAMLIB
-	for (ch = 0; ch < MAX_CHANS; ch++) {
+	for (ch = 0; ch < MAX_RADIO_CHANS; ch++) {
 	  if (save_audio_config_p->chan_medium[ch] == MEDIUM_RADIO) {
 	    int ot;
 	    for (ot = 0; ot < NUM_OCTYPES; ot++) {
@@ -1163,7 +1163,7 @@ void ptt_init (struct audio_s *audio_config_p)
 
 #if USE_CM108
 
-	for (ch = 0; ch < MAX_CHANS; ch++) {
+	for (ch = 0; ch < MAX_RADIO_CHANS; ch++) {
 
 	  if (audio_config_p->chan_medium[ch] == MEDIUM_RADIO) {
 	    int ot;
@@ -1185,7 +1185,7 @@ void ptt_init (struct audio_s *audio_config_p)
 
 /* Why doesn't it transmit?  Probably forgot to specify PTT option. */
 
-	for (ch=0; ch<MAX_CHANS; ch++) {
+	for (ch=0; ch<MAX_RADIO_CHANS; ch++) {
 	  if (audio_config_p->chan_medium[ch] == MEDIUM_RADIO) {
 	    if(audio_config_p->achan[ch].octrl[OCTYPE_PTT].ptt_method == PTT_METHOD_NONE) {
 	      text_color_set(DW_COLOR_INFO);
@@ -1251,14 +1251,14 @@ void ptt_set (int ot, int chan, int ptt_signal)
 	int ptt2 = ptt_signal;
 
 	assert (ot >= 0 && ot < NUM_OCTYPES);
-	assert (chan >= 0 && chan < MAX_CHANS);
+	assert (chan >= 0 && chan < MAX_RADIO_CHANS);
 
 	if (ptt_debug_level >= 1) {
 	  text_color_set(DW_COLOR_DEBUG);
 	  dw_printf ("%s %d = %d\n", otnames[ot], chan, ptt_signal);
 	}
 
-	assert (chan >= 0 && chan < MAX_CHANS);
+	assert (chan >= 0 && chan < MAX_RADIO_CHANS);
 
 	if (   save_audio_config_p->chan_medium[chan] != MEDIUM_RADIO) {
 	  text_color_set(DW_COLOR_ERROR);
@@ -1494,7 +1494,7 @@ void ptt_set (int ot, int chan, int ptt_signal)
 int get_input (int it, int chan)
 {
 	assert (it >= 0 && it < NUM_ICTYPES);
-	assert (chan >= 0 && chan < MAX_CHANS);
+	assert (chan >= 0 && chan < MAX_RADIO_CHANS);
 
 	if (   save_audio_config_p->chan_medium[chan] != MEDIUM_RADIO) {
 	  text_color_set(DW_COLOR_ERROR);
@@ -1559,7 +1559,7 @@ void ptt_term (void)
 {
 	int n;
 
-	for (n = 0; n < MAX_CHANS; n++) {
+	for (n = 0; n < MAX_RADIO_CHANS; n++) {
 	  if (save_audio_config_p->chan_medium[n] == MEDIUM_RADIO) {
 	    int ot;
 	    for (ot = 0; ot < NUM_OCTYPES; ot++) {
@@ -1568,7 +1568,7 @@ void ptt_term (void)
 	  }
 	}
 
-	for (n = 0; n < MAX_CHANS; n++) {
+	for (n = 0; n < MAX_RADIO_CHANS; n++) {
 	  if (save_audio_config_p->chan_medium[n] == MEDIUM_RADIO) {
 	    int ot;
 	    for (ot = 0; ot < NUM_OCTYPES; ot++) {
@@ -1586,7 +1586,7 @@ void ptt_term (void)
 
 #ifdef USE_HAMLIB
 
-	for (n = 0; n < MAX_CHANS; n++) {
+	for (n = 0; n < MAX_RADIO_CHANS; n++) {
 	  if (save_audio_config_p->chan_medium[n] == MEDIUM_RADIO) {
 	    int ot;
 	    for (ot = 0; ot < NUM_OCTYPES; ot++) {
diff --git a/src/recv.c b/src/recv.c
index 49040e55..51f2f016 100644
--- a/src/recv.c
+++ b/src/recv.c
@@ -108,6 +108,7 @@
 #include "dtmf.h"
 #include "aprs_tt.h"
 #include "ax25_link.h"
+#include "ring.h"
 
 
 #if __WIN32__
@@ -278,7 +279,7 @@ static void * recv_adev_thread (void *arg)
 // Try to re-init the audio device a couple times before giving up?
 
 	text_color_set(DW_COLOR_ERROR);
-	dw_printf ("Terminating after audio input failure.\n");
+	dw_printf ("Terminating after audio device %d input failure.\n", a);
 	exit (1);
 }
 
diff --git a/src/rrbb.c b/src/rrbb.c
index e787dae5..0666d42b 100644
--- a/src/rrbb.c
+++ b/src/rrbb.c
@@ -83,7 +83,7 @@ rrbb_t rrbb_new (int chan, int subchan, int slice, int is_scrambled, int descram
 {
 	rrbb_t result;
 
-	assert (chan >= 0 && chan < MAX_CHANS);
+	assert (chan >= 0 && chan < MAX_RADIO_CHANS);
 	assert (subchan >= 0 && subchan < MAX_SUBCHANS);
 	assert (slice >= 0 && slice < MAX_SLICERS);
 
@@ -333,7 +333,7 @@ int rrbb_get_chan (rrbb_t b)
 	assert (b->magic1 == MAGIC1);
 	assert (b->magic2 == MAGIC2);
 
-	assert (b->chan >= 0 && b->chan < MAX_CHANS);
+	assert (b->chan >= 0 && b->chan < MAX_RADIO_CHANS);
 
 	return (b->chan);
 }
diff --git a/src/server.c b/src/server.c
index 2cc108b2..7814d9c6 100644
--- a/src/server.c
+++ b/src/server.c
@@ -1413,7 +1413,7 @@ static THREAD_F cmd_listen_thread (void *arg)
 /*
  * Take some precautions to guard against bad data which could cause problems later.
  */
-	if (cmd.hdr.portx < 0 || cmd.hdr.portx >= MAX_CHANS) {
+	if (cmd.hdr.portx < 0 || cmd.hdr.portx >= MAX_TOTAL_CHANS) {
 	  text_color_set(DW_COLOR_ERROR);
 	  dw_printf ("\nInvalid port number, %d, in command '%c', from AGW client application %d.\n",
 			cmd.hdr.portx, cmd.hdr.datakind, client);
@@ -1544,7 +1544,7 @@ static THREAD_F cmd_listen_thread (void *arg)
 		// No other place cares about total number.
 
 		count = 0;
-		for (j=0; j<MAX_CHANS; j++) {
+		for (j=0; j<MAX_TOTAL_CHANS; j++) {
 	          if (save_audio_config_p->chan_medium[j] == MEDIUM_RADIO ||
 	              save_audio_config_p->chan_medium[j] == MEDIUM_IGATE ||
 	              save_audio_config_p->chan_medium[j] == MEDIUM_NETTNC) {
@@ -1553,7 +1553,7 @@ static THREAD_F cmd_listen_thread (void *arg)
 		}
 		snprintf (reply.info, sizeof(reply.info), "%d;", count);
 
-		for (j=0; j<MAX_CHANS; j++) {
+		for (j=0; j<MAX_TOTAL_CHANS; j++) {
 
 	          switch (save_audio_config_p->chan_medium[j]) {
 
@@ -1850,7 +1850,7 @@ static THREAD_F cmd_listen_thread (void *arg)
 
 	        // Connected mode can only be used with internal modems.
 
-		if (chan >= 0 && chan < MAX_CHANS && save_audio_config_p->chan_medium[chan] == MEDIUM_RADIO) {
+		if (chan >= 0 && chan < MAX_RADIO_CHANS && save_audio_config_p->chan_medium[chan] == MEDIUM_RADIO) {
 		  ok = 1;
 	          dlq_register_callsign (cmd.hdr.call_from, chan, client);
 	        }
@@ -1879,7 +1879,7 @@ static THREAD_F cmd_listen_thread (void *arg)
 
 	        // Connected mode can only be used with internal modems.
 
-		if (chan >= 0 && chan < MAX_CHANS && save_audio_config_p->chan_medium[chan] == MEDIUM_RADIO) {
+		if (chan >= 0 && chan < MAX_RADIO_CHANS && save_audio_config_p->chan_medium[chan] == MEDIUM_RADIO) {
 	          dlq_unregister_callsign (cmd.hdr.call_from, chan, client);
 	        }
 		else {
@@ -2066,7 +2066,7 @@ static THREAD_F cmd_listen_thread (void *arg)
 	        reply.hdr.data_len_NETLE = host2netle(4);
 
 	        int n = 0;
-	        if (cmd.hdr.portx >= 0 && cmd.hdr.portx < MAX_CHANS) {
+	        if (cmd.hdr.portx >= 0 && cmd.hdr.portx < MAX_RADIO_CHANS) {
 	          // Count both normal and expedited in transmit queue for given channel.
 		  n = tq_count (cmd.hdr.portx, -1, "", "", 0);
 		}
diff --git a/src/telemetry.c b/src/telemetry.c
index 2a6c690c..d796cf14 100644
--- a/src/telemetry.c
+++ b/src/telemetry.c
@@ -71,7 +71,7 @@
 #define T_NUM_ANALOG 5				/* Number of analog channels. */
 #define T_NUM_DIGITAL 8				/* Number of digital channels. */
 
-#define T_STR_LEN 16				/* Max len for labels and units. */
+#define T_STR_LEN 32				/* Max len for labels and units. */
 
 
 #define MAGIC1  0x5a1111a5			/* For checking storage allocation problems. */
diff --git a/src/tnctest.c b/src/tnctest.c
index 0d4c26b4..f5fb8616 100644
--- a/src/tnctest.c
+++ b/src/tnctest.c
@@ -285,7 +285,7 @@ int main (int argc, char *argv[])
  	setlinebuf (stdout);
 #endif
 
-	start_dtime = dtime_now();
+	start_dtime = dtime_monotonic();
 
 /*
  * Extract command line args.
@@ -615,7 +615,7 @@ void process_rec_data (int my_index, char *data)
  *				  and sent to a common function to check that they
  *				  all arrived in order.
  *
- * Global Out:	is_connected	- Updated when connected/disconnected notifications are received.
+ * Global Out:	is_connected	- Updated when connected/disconnected notfications are received.
  *
  * Description:	Perform any necessary configuration for the TNC then wait
  *		for responses and process them.
@@ -859,7 +859,7 @@ static void * tnc_thread_net (void *arg)
  * What did we get?
  */
 
-	  dnow = dtime_now();
+	  dnow = dtime_monotonic();
 
 	  switch (mon_cmd.datakind) {
 
@@ -943,7 +943,7 @@ static void * tnc_thread_net (void *arg)
  *				  and sent to a common function to check that they
  *				  all arrived in order.
  *
- * Global Out:	is_connected	- Updated when connected/disconnected notifications are received.
+ * Global Out:	is_connected	- Updated when connected/disconnected notfications are received.
  *
  * Description:	Perform any necessary configuration for the TNC then wait
  *		for responses and process them.
@@ -1038,12 +1038,12 @@ static void * tnc_thread_serial (void *arg)
 	      done = 1;
 	    }
 	    else if (ch == XOFF) {
-	      double dnow = dtime_now();
+	      double dnow = dtime_monotonic();
 	      printf("%*s[R %.3f] <XOFF>\n", my_index*column_width, "", dnow-start_dtime);
 	      busy[my_index] = 1;
 	    }
 	    else if (ch == XON) {
-	      double dnow = dtime_now();
+	      double dnow = dtime_monotonic();
 	      printf("%*s[R %.3f] <XON>\n", my_index*column_width, "", dnow-start_dtime);
 	      busy[my_index] = 0;
 	    }
@@ -1070,7 +1070,7 @@ static void * tnc_thread_serial (void *arg)
 
 	  if (len > 0) {
 
-	    double dnow = dtime_now();
+	    double dnow = dtime_monotonic();
 
 	    printf("%*s[R %.3f] %s\n", my_index*column_width, "", dnow-start_dtime, result);
 
@@ -1109,7 +1109,7 @@ static void * tnc_thread_serial (void *arg)
 static void tnc_connect (int from, int to)
 {
 
-	double dnow = dtime_now();
+	double dnow = dtime_monotonic();
 
  	printf("%*s[T %.3f] *** Send connect request ***\n", from*column_width, "", dnow-start_dtime);
 
@@ -1160,7 +1160,7 @@ static void tnc_connect (int from, int to)
 
 static void tnc_disconnect (int from, int to)
 {
-	double dnow = dtime_now();
+	double dnow = dtime_monotonic();
 
  	printf("%*s[T %.3f] *** Send disconnect request ***\n", from*column_width, "", dnow-start_dtime);
 
@@ -1201,7 +1201,7 @@ static void tnc_disconnect (int from, int to)
 
 static void tnc_reset (int from, int to)
 {
-	double dnow = dtime_now();
+	double dnow = dtime_monotonic();
 
  	printf("%*s[T %.3f] *** Send reset ***\n", from*column_width, "", dnow-start_dtime);
 
@@ -1232,7 +1232,7 @@ static void tnc_reset (int from, int to)
 
 static void tnc_send_data (int from, int to, char * data)
 {
-	double dnow = dtime_now();
+	double dnow = dtime_monotonic();
 
  	printf("%*s[T %.3f] %s\n", from*column_width, "", dnow-start_dtime, data);
 
@@ -1257,7 +1257,7 @@ static void tnc_send_data (int from, int to, char * data)
 	else {
 
 	  // The assumption is that we are in CONVERS mode.
-	  // The data should be terminated by carriage return.
+	  // The data sould be terminated by carriage return.
 
 	  int timeout = 600;	// 60 sec.  I've seen it take more than 20.
 	  while (timeout > 0 && busy[from]) {
diff --git a/src/tq.c b/src/tq.c
index c656af54..0738eca1 100644
--- a/src/tq.c
+++ b/src/tq.c
@@ -52,10 +52,10 @@
 #include "dedupe.h"
 #include "igate.h"
 #include "dtime_now.h"
+#include "nettnc.h"
 
 
-
-static packet_t queue_head[MAX_CHANS][TQ_NUM_PRIO];	/* Head of linked list for each queue. */
+static packet_t queue_head[MAX_RADIO_CHANS][TQ_NUM_PRIO];	/* Head of linked list for each queue. */
 
 
 static dw_mutex_t tq_mutex;				/* Critical section for updating queues. */
@@ -63,15 +63,15 @@ static dw_mutex_t tq_mutex;				/* Critical section for updating queues. */
 
 #if __WIN32__
 
-static HANDLE wake_up_event[MAX_CHANS];			/* Notify transmit thread when queue not empty. */
+static HANDLE wake_up_event[MAX_RADIO_CHANS];			/* Notify transmit thread when queue not empty. */
 
 #else
 
-static pthread_cond_t wake_up_cond[MAX_CHANS];		/* Notify transmit thread when queue not empty. */
+static pthread_cond_t wake_up_cond[MAX_RADIO_CHANS];		/* Notify transmit thread when queue not empty. */
 
-static pthread_mutex_t wake_up_mutex[MAX_CHANS];	/* Required by cond_wait. */
+static pthread_mutex_t wake_up_mutex[MAX_RADIO_CHANS];	/* Required by cond_wait. */
 
-static int xmit_thread_is_waiting[MAX_CHANS];
+static int xmit_thread_is_waiting[MAX_RADIO_CHANS];
 
 #endif
 
@@ -128,7 +128,7 @@ void tq_init (struct audio_s *audio_config_p)
 
 	save_audio_config_p = audio_config_p;
 
-	for (c=0; c<MAX_CHANS; c++) {
+	for (c=0; c<MAX_RADIO_CHANS; c++) {
 	  for (p=0; p<TQ_NUM_PRIO; p++) {
 	    queue_head[c][p] = NULL;
 	  }
@@ -147,7 +147,7 @@ void tq_init (struct audio_s *audio_config_p)
 
 #if __WIN32__
 
-	for (c = 0; c < MAX_CHANS; c++) {
+	for (c = 0; c < MAX_RADIO_CHANS; c++) {
 
 	  if (audio_config_p->chan_medium[c] == MEDIUM_RADIO) {
 
@@ -164,7 +164,7 @@ void tq_init (struct audio_s *audio_config_p)
 #else
 	int err;
 
-	for (c = 0; c < MAX_CHANS; c++) {
+	for (c = 0; c < MAX_RADIO_CHANS; c++) {
 
 	  xmit_thread_is_waiting[c] = 0;
 
@@ -199,6 +199,9 @@ void tq_init (struct audio_s *audio_config_p)
  *				New in 1.7:
  *				Channel can be assigned to IGate rather than a radio.
  *
+ *				New in 1.8:
+ *				Channel can be assigned to a network TNC.
+ *
  *		prio	- Priority, use TQ_PRIO_0_HI for digipeated or
  *				TQ_PRIO_1_LO for normal.
  *
@@ -252,10 +255,13 @@ void tq_append (int chan, int prio, packet_t pp)
 #endif
 
 // New in 1.7 - A channel can be assigned to the IGate rather than a radio.
+// New in 1.8: Assign a channel to external network TNC.
+// Send somewhere else, rather than the transmit queue.
 
 #ifndef DIGITEST		// avoid dtest link error
 
-	if (save_audio_config_p->chan_medium[chan] == MEDIUM_IGATE) {
+	if (save_audio_config_p->chan_medium[chan] == MEDIUM_IGATE ||
+		save_audio_config_p->chan_medium[chan] == MEDIUM_NETTNC) {
 
 	  char ts[100];		// optional time stamp.
 
@@ -274,21 +280,39 @@ void tq_append (int chan, int prio, packet_t pp)
 	  unsigned char *pinfo;
 	  int info_len = ax25_get_info (pp, &pinfo);
 	  text_color_set(DW_COLOR_XMIT);
-	  dw_printf ("[%d>is%s] ", chan, ts);
-	  dw_printf ("%s", stemp);			/* stations followed by : */
-	  ax25_safe_print ((char *)pinfo, info_len, ! ax25_is_aprs(pp));
-	  dw_printf ("\n");
 
-	  igate_send_rec_packet (chan, pp);
+	  if (save_audio_config_p->chan_medium[chan] == MEDIUM_IGATE) {
+
+	    dw_printf ("[%d>is%s] ", chan, ts);
+	    dw_printf ("%s", stemp);			/* stations followed by : */
+	    ax25_safe_print ((char *)pinfo, info_len, ! ax25_is_aprs(pp));
+	    dw_printf ("\n");
+
+	    igate_send_rec_packet (chan, pp);
+	  }
+	  else {	// 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));
+	    dw_printf ("\n");
+
+	    nettnc_send_packet (chan, pp);
+
+	  }
+
 	  ax25_delete(pp);
 	  return;
 	}
 #endif
 
+
+
+
+
 // Normal case - put in queue for radio transmission.
 // Error if trying to transmit to a radio channel which was not configured.
 
-	if (chan < 0 || chan >= MAX_CHANS || save_audio_config_p->chan_medium[chan] == MEDIUM_NONE) {
+	if (chan < 0 || chan >= MAX_RADIO_CHANS || save_audio_config_p->chan_medium[chan] == MEDIUM_NONE) {
 	  text_color_set(DW_COLOR_ERROR);
 	  dw_printf ("ERROR - Request to transmit on invalid radio channel %d.\n", chan);
 	  dw_printf ("This is probably a client application error, not a problem with direwolf.\n");
@@ -490,7 +514,7 @@ void lm_data_request (int chan, int prio, packet_t pp)
 	}
 #endif
 
-	if (chan < 0 || chan >= MAX_CHANS || save_audio_config_p->chan_medium[chan] != MEDIUM_RADIO) {
+	if (chan < 0 || chan >= MAX_RADIO_CHANS || save_audio_config_p->chan_medium[chan] != MEDIUM_RADIO) {
 	  // Connected mode is allowed only with internal modems.
 	  text_color_set(DW_COLOR_ERROR);
 	  dw_printf ("ERROR - Request to transmit on invalid radio channel %d.\n", chan);
@@ -648,7 +672,7 @@ void lm_seize_request (int chan)
 #endif
 
 
-	if (chan < 0 || chan >= MAX_CHANS || save_audio_config_p->chan_medium[chan] != MEDIUM_RADIO) {
+	if (chan < 0 || chan >= MAX_RADIO_CHANS || save_audio_config_p->chan_medium[chan] != MEDIUM_RADIO) {
 	  // Connected mode is allowed only with internal modems.
 	  text_color_set(DW_COLOR_ERROR);
 	  dw_printf ("ERROR - Request to transmit on invalid radio channel %d.\n", chan);
@@ -748,7 +772,7 @@ void tq_wait_while_empty (int chan)
 	text_color_set(DW_COLOR_DEBUG);
 	dw_printf ("tq_wait_while_empty (%d) : enter critical section\n", chan);
 #endif
-	assert (chan >= 0 && chan < MAX_CHANS);
+	assert (chan >= 0 && chan < MAX_RADIO_CHANS);
 
 	dw_mutex_lock (&tq_mutex);
 
@@ -944,7 +968,7 @@ static int tq_is_empty (int chan)
 {
 	int p;
 	
-	assert (chan >= 0 && chan < MAX_CHANS);
+	assert (chan >= 0 && chan < MAX_RADIO_CHANS);
 
 
 	for (p=0; p<TQ_NUM_PRIO; p++) {
@@ -1001,7 +1025,7 @@ int tq_count (int chan, int prio, char *source, char *dest, int bytes)
 
 	// Array bounds check.  FIXME: TODO:  should have internal error instead of dying.
 
-	if (chan < 0 || chan >= MAX_CHANS || prio < 0 || prio >= TQ_NUM_PRIO) {
+	if (chan < 0 || chan >= MAX_RADIO_CHANS || prio < 0 || prio >= TQ_NUM_PRIO) {
 	  text_color_set(DW_COLOR_DEBUG);
 	  dw_printf ("INTERNAL ERROR - tq_count(%d, %d, \"%s\", \"%s\", %d)\n", chan, prio, source, dest, bytes);
 	  return (0);
diff --git a/src/tt_user.c b/src/tt_user.c
index a73d6a46..63043b2d 100644
--- a/src/tt_user.c
+++ b/src/tt_user.c
@@ -819,10 +819,10 @@ static void xmit_object_report (int i, int first_time)
  */
 
 	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));
+	  strlcpy (stemp, save_audio_config_p->mycall[save_tt_config_p->obj_xmit_chan], sizeof(stemp));
 	}
 	else {
-	  strlcpy (stemp, save_audio_config_p->achan[save_tt_config_p->obj_recv_chan].mycall, sizeof(stemp));
+	  strlcpy (stemp, save_audio_config_p->mycall[save_tt_config_p->obj_recv_chan], sizeof(stemp));
 	}
 	strlcat (stemp, ">", sizeof(stemp));
 	strlcat (stemp, APP_TOCALL, sizeof(stemp));
@@ -1134,7 +1134,7 @@ int main (int argc, char *argv[])
 
 	memset (&my_audio_config, 0, sizeof(my_audio_config));
 
-	strlcpy (my_audio_config.achan[0].mycall, "WB2OSZ-15", sizeof(my_audio_config.achan[0].mycall));
+	strlcpy (my_audio_config.mycall[0], "WB2OSZ-15", sizeof(my_audio_config.mycall[0]));
 
 /* Fake TT gateway config. */
 
diff --git a/src/xmit.c b/src/xmit.c
index 13bbaecb..0d938efa 100644
--- a/src/xmit.c
+++ b/src/xmit.c
@@ -88,24 +88,24 @@
  */
 
 
-static int xmit_slottime[MAX_CHANS];	/* Slot time in 10 mS units for persistence algorithm. */
+static int xmit_slottime[MAX_RADIO_CHANS];	/* Slot time in 10 mS units for persistence algorithm. */
 
-static int xmit_persist[MAX_CHANS];	/* Sets probability for transmitting after each */
+static int xmit_persist[MAX_RADIO_CHANS];	/* Sets probability for transmitting after each */
 					/* slot time delay.  Transmit if a random number */
 					/* in range of 0 - 255 <= persist value.  */
 					/* Otherwise wait another slot time and try again. */
 
-static int xmit_txdelay[MAX_CHANS];	/* After turning on the transmitter, */
+static int xmit_txdelay[MAX_RADIO_CHANS];	/* After turning on the transmitter, */
 					/* send "flags" for txdelay * 10 mS. */
 
-static int xmit_txtail[MAX_CHANS];	/* Amount of time to keep transmitting after we */
+static int xmit_txtail[MAX_RADIO_CHANS];	/* Amount of time to keep transmitting after we */
 					/* are done sending the data.  This is to avoid */
 					/* dropping PTT too soon and chopping off the end */
 					/* of the frame.  Again 10 mS units. */
 
-static int xmit_fulldup[MAX_CHANS];	/* Full duplex if non-zero. */
+static int xmit_fulldup[MAX_RADIO_CHANS];	/* Full duplex if non-zero. */
 
-static int xmit_bits_per_sec[MAX_CHANS];	/* Data transmission rate. */
+static int xmit_bits_per_sec[MAX_RADIO_CHANS];	/* Data transmission rate. */
 					/* Often called baud rate which is equivalent for */
 					/* 1200 & 9600 cases but could be different with other */
 					/* modulation techniques. */
@@ -211,11 +211,11 @@ void xmit_init (struct audio_s *p_modem, int debug_xmit_packet)
 	int ad;
 
 #if __WIN32__
-	HANDLE xmit_th[MAX_CHANS];
+	HANDLE xmit_th[MAX_RADIO_CHANS];
 #else
 	//pthread_attr_t attr;
 	//struct sched_param sp;
-	pthread_t xmit_tid[MAX_CHANS];
+	pthread_t xmit_tid[MAX_RADIO_CHANS];
 #endif
 	//int e;
 
@@ -247,7 +247,7 @@ void xmit_init (struct audio_s *p_modem, int debug_xmit_packet)
  * TODO1.2:  Any reason to use global config rather than making a copy?
  */
 
-	for (j=0; j<MAX_CHANS; j++) {
+	for (j=0; j<MAX_RADIO_CHANS; j++) {
 	  xmit_bits_per_sec[j] = p_modem->achan[j].baud;
 	  xmit_slottime[j] = p_modem->achan[j].slottime;
 	  xmit_persist[j] = p_modem->achan[j].persist;
@@ -276,7 +276,7 @@ void xmit_init (struct audio_s *p_modem, int debug_xmit_packet)
 // underrun on the audio output device.
 
 
-	for (j=0; j<MAX_CHANS; j++) {
+	for (j=0; j<MAX_RADIO_CHANS; j++) {
 
 	  if (p_modem->chan_medium[j] == MEDIUM_RADIO) {
 #if __WIN32__
@@ -365,35 +365,35 @@ void xmit_init (struct audio_s *p_modem, int debug_xmit_packet)
 
 void xmit_set_txdelay (int channel, int value)
 {
-	if (channel >= 0 && channel < MAX_CHANS) {
+	if (channel >= 0 && channel < MAX_RADIO_CHANS) {
 	  xmit_txdelay[channel] = value;
 	}
 }
 
 void xmit_set_persist (int channel, int value)
 {
-	if (channel >= 0 && channel < MAX_CHANS) {
+	if (channel >= 0 && channel < MAX_RADIO_CHANS) {
 	  xmit_persist[channel] = value;
 	}
 }
 
 void xmit_set_slottime (int channel, int value)
 {
-	if (channel >= 0 && channel < MAX_CHANS) {
+	if (channel >= 0 && channel < MAX_RADIO_CHANS) {
 	  xmit_slottime[channel] = value;
 	}
 }
 
 void xmit_set_txtail (int channel, int value)
 {
-	if (channel >= 0 && channel < MAX_CHANS) {
+	if (channel >= 0 && channel < MAX_RADIO_CHANS) {
 	  xmit_txtail[channel] = value;
 	}
 }
 
 void xmit_set_fulldup (int channel, int value)
 {
-	if (channel >= 0 && channel < MAX_CHANS) {
+	if (channel >= 0 && channel < MAX_RADIO_CHANS) {
 	  xmit_fulldup[channel] = value;
 	}
 }

From b9f654d3c939073b300deb40e802384899c325cf Mon Sep 17 00:00:00 2001
From: wb2osz <wb2osz@comcast.net>
Date: Fri, 19 Jul 2024 00:41:12 +0100
Subject: [PATCH 34/67] New NCHANNEL feature.

---
 src/nettnc.c | 490 +++++++++++++++++++++++++++++++++++++++++++++++++++
 src/nettnc.h |   7 +
 2 files changed, 497 insertions(+)
 create mode 100644 src/nettnc.c
 create mode 100644 src/nettnc.h

diff --git a/src/nettnc.c b/src/nettnc.c
new file mode 100644
index 00000000..9b95ab14
--- /dev/null
+++ b/src/nettnc.c
@@ -0,0 +1,490 @@
+
+//
+//    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 <http://www.gnu.org/licenses/>.
+//
+
+
+
+/*------------------------------------------------------------------
+ *
+ * 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 <winsock2.h>
+#include <ws2tcpip.h>  		// _WIN32_WINNT must be set to 0x0501 before including this
+#else 
+#include <stdlib.h>
+#include <sys/types.h>
+#include <sys/ioctl.h>
+#include <sys/socket.h>
+#include <netinet/in.h>
+#include <errno.h>
+#endif
+
+#include <unistd.h>
+#include <stdio.h>
+#include <assert.h>
+#include <string.h>
+#include <time.h>
+#include <ctype.h>
+
+#include "textcolor.h"
+#include "audio.h"		// configuration.
+#include "kiss.h"
+#include "dwsock.h"		// socket helper functions.
+#include "ax25_pad.h"		// for AX25_MAX_PACKET_LEN
+#include "dlq.h"		// received packet queue
+
+#include "nettnc.h"
+
+
+
+void hex_dump (unsigned char *p, int len);
+
+
+// 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 nettnc_listen_th[MAX_TOTAL_CHANS];
+static THREAD_F nettnc_listen_thread (void *arg);
+#else
+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);
+
+int s_kiss_debug = 0;
+
+
+/*-------------------------------------------------------------------
+ *
+ * Name:        nettnc_init
+ *
+ * Purpose:      Attach to Network KISS TNC(s) for NCHANNEL 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 nettnc_attach for each NCHANNEL configuration item.
+ *
+ *--------------------------------------------------------------------*/
+
+void nettnc_init (struct audio_s *pa)
+{
+	for (int i = 0; i < MAX_TOTAL_CHANS; i++) {
+
+	  if (pa->chan_medium[i] == MEDIUM_NETTNC) {
+	    text_color_set(DW_COLOR_DEBUG);
+	    dw_printf ("Channel %d: Network TNC %s %d\n", i, pa->nettnc_addr[i], pa->nettnc_port[i]);
+	    int e = nettnc_attach (i, pa->nettnc_addr[i], pa->nettnc_port[i]);
+	    if (e < 0) {
+	      exit (1);
+	    }
+	  }
+	}
+
+}  // end nettnc_init
+
+
+
+/*-------------------------------------------------------------------
+ *
+ * Name:        nettnc_attach
+ *
+ * Purpose:      Attach to one Network KISS TNC.
+ *
+ * Inputs:	chan	- channel number from NCHANNEL configuration.
+ *
+ *		host	- Host name or IP address.  Often "localhost".
+ *
+ *		port	- TCP port number.  Typically 8001.
+ *
+ *		init_func - Call this function after establishing communication //
+ *			with the TNC.  We put it here, so that it can be done//
+ *			again automatically if the TNC disappears and we//
+ *			reattach to it.//
+ *			It must return 0 for success.//
+ *			Can be NULL if not needed.//
+ *
+ * Returns:	0 for success, -1 for failure.
+ *
+ * Description:	This starts up a thread, for each socket, which listens to the socket and
+ *		dispatches the messages to the corresponding callback functions.
+ *		It will also attempt to re-establish communication with the
+ *		TNC if it goes away.
+ *
+ *--------------------------------------------------------------------*/
+
+static char s_tnc_host[MAX_TOTAL_CHANS][80];
+static char s_tnc_port[MAX_TOTAL_CHANS][20];
+static volatile int s_tnc_sock[MAX_TOTAL_CHANS];	// Socket handle or file descriptor. -1 for invalid.
+
+
+int nettnc_attach (int chan, char *host, int port)
+{
+	assert (chan >= 0 && chan < MAX_TOTAL_CHANS);
+
+	char tncaddr[DWSOCK_IPADDR_LEN];
+
+	char sport[20];		// need port as text string later.
+	snprintf (sport, sizeof(sport), "%d", port);
+
+	strlcpy (s_tnc_host[chan], host, sizeof(s_tnc_host[chan]));
+	strlcpy (s_tnc_port[chan], sport, sizeof(s_tnc_port[chan]));
+	s_tnc_sock[chan] = -1;
+
+	dwsock_init();
+
+	s_tnc_sock[chan] = dwsock_connect (s_tnc_host[chan], s_tnc_port[chan], "Network TNC", 0, 0, tncaddr);
+
+	if (s_tnc_sock[chan] == -1) {
+	  return (-1);
+	}
+
+
+/*
+ * Read frames from the network TNC.
+ * If the TNC disappears, try to reestablish communication.
+ */
+
+
+#if __WIN32__
+	nettnc_listen_th[chan] = (HANDLE)_beginthreadex (NULL, 0, nettnc_listen_thread, (void *)(ptrdiff_t)chan, 0, NULL);
+	if (nettnc_listen_th[chan] == NULL) {
+	  text_color_set(DW_COLOR_ERROR);
+	  dw_printf ("Internal error: Could not create remore TNC listening thread\n");
+	  return (-1);
+	}
+#else
+	int e = pthread_create (&nettnc_listen_tid[chan], NULL, nettnc_listen_thread, (void *)(ptrdiff_t)chan);
+	if (e != 0) {
+	  text_color_set(DW_COLOR_ERROR);
+	  perror("Internal error: Could not create network 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 nettnc_attach
+
+
+
+/*-------------------------------------------------------------------
+ *
+ * Name:        nettnc_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_host[chan]	- Host & port for re-connection.
+ *		s_tnc_port[chan]
+ *
+ * Outputs:	s_tnc_sock[chan] - File descriptor for communicating with TNC.
+ *				  Will be -1 if not connected.
+ *
+ *--------------------------------------------------------------------*/
+
+#if __WIN32__
+static unsigned __stdcall nettnc_listen_thread (void *arg)
+#else
+static void * nettnc_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));
+
+	char tncaddr[DWSOCK_IPADDR_LEN];	// IP address used by dwsock_connect.
+						// Useful when rotate addresses used.
+
+// Set up buffer for collecting a KISS frame.$CC exttnc.c
+
+	while (1) {
+/*
+ * Re-attach to TNC if not currently attached.
+ */
+	  if (s_tnc_sock[chan] == -1) {
+
+	    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 network TNC...\n");
+
+	    s_tnc_sock[chan] = dwsock_connect (s_tnc_host[chan], s_tnc_port[chan], "Network TNC", 0, 0, tncaddr);
+
+	    if (s_tnc_sock[chan] != -1) {
+	      dw_printf ("Successfully reattached to network TNC.\n");
+	    }
+	  }
+	  else {
+#define NETTNCBUFSIZ 2048
+	    unsigned char buf[NETTNCBUFSIZ];
+	    int n = SOCK_RECV (s_tnc_sock[chan], (char *)buf, sizeof(buf));
+
+	    if (n == -1) {
+	      text_color_set(DW_COLOR_ERROR);
+	      dw_printf ("Lost communication with network TNC. Will try to reattach.\n");
+	      dwsock_close (s_tnc_sock[chan]);
+	      s_tnc_sock[chan] = -1;
+	      SLEEP_SEC(5);
+	      continue;
+	    }
+
+#if 0
+	    text_color_set(DW_COLOR_DEBUG);
+	    dw_printf ("TEMP DEBUG:  %d bytes received from channel %d network TNC.\n", n, chan);
+#endif
+	    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);
+	    }
+	  } // s_tnc_sock != -1
+	} // while (1)
+
+	return (0);	// unreachable but shutup warning.
+
+} // end nettnc_listen_thread
+
+
+
+/*-------------------------------------------------------------------
+ *
+ * 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
+ *
+ * Purpose:	Send packet to a KISS network TNC.
+ *
+ * Inputs:	chan	- Channel number from NCHANNEL configuration.
+ *		pp	- Packet object.
+ *		b	- A byte from the input stream.
+ *
+ * Outputs:	Packet is converted to KISS and send to network TNC.
+ *
+ * Returns:	none.
+ *
+ * Description:	This does not free the packet object; caller is responsible.
+ *
+ *-----------------------------------------------------------------*/
+
+void nettnc_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);
+
+#if __WIN32__	
+	int err = SOCK_SEND(s_tnc_sock[chan], (char*)kiss_buff, kiss_len);
+	if (err == SOCKET_ERROR) {
+	  text_color_set(DW_COLOR_ERROR);
+	  dw_printf ("\nError %d sending packet to KISS Network TNC for channel %d.  Closing connection.\n\n", WSAGetLastError(), chan);
+	  closesocket (s_tnc_sock[chan]);
+	  s_tnc_sock[chan] = -1;
+	}
+#else
+	int err = SOCK_SEND (kps->client_sock[chan], kiss_buff, kiss_len);
+	if (err <= 0) {
+	  text_color_set(DW_COLOR_ERROR);
+	  dw_printf ("\nError %d sending packet to KISS Network TNC for channel %d.  Closing connection.\n\n", err, chan);
+	  close (s_tnc_sock[chan]);
+	  s_tnc_sock[chan] = -1;
+	}
+#endif
+	
+	// Do not free packet object;  caller will take care of it.
+
+} /* end nettnc_send_packet */
+
diff --git a/src/nettnc.h b/src/nettnc.h
new file mode 100644
index 00000000..d8a10f43
--- /dev/null
+++ b/src/nettnc.h
@@ -0,0 +1,7 @@
+
+
+void nettnc_init (struct audio_s *pa);
+
+int nettnc_attach (int chan, char *host, int port);
+
+void nettnc_send_packet (int chan, packet_t pp);
\ No newline at end of file

From dd04883707ccdb55889a14d5b038335d848cf5a3 Mon Sep 17 00:00:00 2001
From: wb2osz <wb2osz@comcast.net>
Date: Fri, 19 Jul 2024 00:44:20 +0100
Subject: [PATCH 35/67] New NCHANNEL feature.

---
 src/nettnc.c | 1 +
 1 file changed, 1 insertion(+)

diff --git a/src/nettnc.c b/src/nettnc.c
index 9b95ab14..e16dd8d0 100644
--- a/src/nettnc.c
+++ b/src/nettnc.c
@@ -51,6 +51,7 @@
 #include <string.h>
 #include <time.h>
 #include <ctype.h>
+#include <stddef.h>
 
 #include "textcolor.h"
 #include "audio.h"		// configuration.

From 1033f8a7bfa7d8164f734f5adc541193fba6a199 Mon Sep 17 00:00:00 2001
From: wb2osz <wb2osz@comcast.net>
Date: Fri, 19 Jul 2024 00:48:08 +0100
Subject: [PATCH 36/67] New NCHANNEL feature.

---
 src/nettnc.c | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/nettnc.c b/src/nettnc.c
index e16dd8d0..72d18cc5 100644
--- a/src/nettnc.c
+++ b/src/nettnc.c
@@ -476,7 +476,7 @@ void nettnc_send_packet (int chan, packet_t pp)
 	  s_tnc_sock[chan] = -1;
 	}
 #else
-	int err = SOCK_SEND (kps->client_sock[chan], kiss_buff, kiss_len);
+	int err = SOCK_SEND (s_tnc_sock[chan], kiss_buff, kiss_len);
 	if (err <= 0) {
 	  text_color_set(DW_COLOR_ERROR);
 	  dw_printf ("\nError %d sending packet to KISS Network TNC for channel %d.  Closing connection.\n\n", err, chan);

From 312d5589235cd49b722dd750b24332e7a20ed062 Mon Sep 17 00:00:00 2001
From: wb2osz <wb2osz@comcast.net>
Date: Fri, 19 Jul 2024 01:15:30 +0100
Subject: [PATCH 37/67] New NCHANNEL feature.

---
 src/recv.c | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/recv.c b/src/recv.c
index 51f2f016..d6281567 100644
--- a/src/recv.c
+++ b/src/recv.c
@@ -108,7 +108,7 @@
 #include "dtmf.h"
 #include "aprs_tt.h"
 #include "ax25_link.h"
-#include "ring.h"
+//#include "ring.h"
 
 
 #if __WIN32__

From 48f524d5fced5f920d9429ace7a7641c301eab6e Mon Sep 17 00:00:00 2001
From: wb2osz <wb2osz@comcast.net>
Date: Fri, 13 Sep 2024 16:13:21 +0100
Subject: [PATCH 38/67] issue 530 - Put * after all used addresses.

---
 src/server.c | 23 +++++++++++++++--------
 1 file changed, 15 insertions(+), 8 deletions(-)

diff --git a/src/server.c b/src/server.c
index 7814d9c6..34e8ab24 100644
--- a/src/server.c
+++ b/src/server.c
@@ -976,6 +976,9 @@ void server_send_monitored (int chan, packet_t pp, int own_xmit)
 // Format addresses in AGWPR monitoring format such as:
 //	 1:Fm ZL4FOX-8 To Q7P2U2 Via WIDE3-3
 
+// Issue 530 pointed out that in this situation it is customary to put * after each used address,
+// not just the last used as in the TNC-2 monitoring format.
+
 static void mon_addrs (int chan, packet_t pp, char *result, int result_size)
 {
 	char src[AX25_MAX_ADDR_LEN];
@@ -986,16 +989,20 @@ static void mon_addrs (int chan, packet_t pp, char *result, int result_size)
 	int num_digi = ax25_get_num_repeaters(pp);
 
 	if (num_digi > 0) {
+	  char via[AX25_MAX_REPEATERS*(AX25_MAX_ADDR_LEN+1)];	// complete via path
+	  strlcpy (via, "", sizeof(via));
 
-	  char via[AX25_MAX_REPEATERS*(AX25_MAX_ADDR_LEN+1)];
-	  char stemp[AX25_MAX_ADDR_LEN+1];
-	  int j;
+	  for (int j = 0; j < num_digi; j++) {
+	    char digiaddr[AX25_MAX_ADDR_LEN];
 
-	  ax25_get_addr_with_ssid (pp, AX25_REPEATER_1, via);
-	  for (j = 1; j < num_digi; j++) {
-	    ax25_get_addr_with_ssid (pp, AX25_REPEATER_1 + j, stemp);
-	    strlcat (via, ",", sizeof(via));
-	    strlcat (via, stemp, sizeof(via));
+	    if (j != 0) {
+	      strlcat (via, ",", sizeof(via));	// comma if not first address
+	    }
+	    ax25_get_addr_with_ssid (pp, AX25_REPEATER_1 + j, digiaddr);
+	    strlcat (via, digiaddr, sizeof(via));
+	    if (ax25_get_h(pp, AX25_REPEATER_1 + j)) {
+	      strlcat (via, "*", sizeof(via));
+	    }
 	  }
 	  snprintf (result, result_size, " %d:Fm %s To %s Via %s ",
 		chan+1, src, dst, via);

From c69252fc7c6a701905789a84ccea408fab96b3b8 Mon Sep 17 00:00:00 2001
From: wb2osz <wb2osz@comcast.net>
Date: Fri, 13 Sep 2024 17:51:01 +0100
Subject: [PATCH 39/67] Fix typo.

---
 test/CMakeLists.txt | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt
index da732ac8..37c2b2c8 100644
--- a/test/CMakeLists.txt
+++ b/test/CMakeLists.txt
@@ -524,7 +524,7 @@ if(OPTIONAL_TEST)
     ${CUSTOM_SRC_DIR}/pfilter.c
     ${CUSTOM_SRC_DIR}/telemetry.c
     ${CUSTOM_SRC_DIR}/decode_aprs.c
-    ${CUSTOM_SRC_DIR}/deviceid.c.c
+    ${CUSTOM_SRC_DIR}/deviceid.c
     ${CUSTOM_SRC_DIR}/dwgpsnmea.c
     ${CUSTOM_SRC_DIR}/dwgps.c
     ${CUSTOM_SRC_DIR}/dwgpsd.c

From 5d7b10abd9759d3287da83fd5a4eefc0369b3a3b Mon Sep 17 00:00:00 2001
From: wb2osz <wb2osz@comcast.net>
Date: Fri, 13 Sep 2024 18:15:18 +0100
Subject: [PATCH 40/67] Mark only heard digi, not all used.

---
 src/server.c | 14 ++++++++++++--
 1 file changed, 12 insertions(+), 2 deletions(-)

diff --git a/src/server.c b/src/server.c
index 34e8ab24..023e43f7 100644
--- a/src/server.c
+++ b/src/server.c
@@ -976,8 +976,13 @@ void server_send_monitored (int chan, packet_t pp, int own_xmit)
 // Format addresses in AGWPR monitoring format such as:
 //	 1:Fm ZL4FOX-8 To Q7P2U2 Via WIDE3-3
 
-// Issue 530 pointed out that in this situation it is customary to put * after each used address,
-// not just the last used as in the TNC-2 monitoring format.
+// There is some disagreement, in the user community, about whether to:
+// * follow the lead of UZ7HO SoundModem and mark all of the used addresses, or
+// * follow the TNC-2 Monitoring format and mark only the last used, i.e. the station heard.
+
+// I think my opinion (which could change) is that we should try to be consistent with TNC-2 format
+// rather than continuing to propagate historical inconsistencies.
+
 
 static void mon_addrs (int chan, packet_t pp, char *result, int result_size)
 {
@@ -1000,9 +1005,14 @@ static void mon_addrs (int chan, packet_t pp, char *result, int result_size)
 	    }
 	    ax25_get_addr_with_ssid (pp, AX25_REPEATER_1 + j, digiaddr);
 	    strlcat (via, digiaddr, sizeof(via));
+#if 0  // Mark each used with * as seen in UZ7HO SoundModem.
 	    if (ax25_get_h(pp, AX25_REPEATER_1 + j)) {
+#else  // Mark only last used (i.e. the heard station) with * as in TNC-2 Monitoring format.
+	    if (AX25_REPEATER_1 + j == ax25_get_heard(pp)) {
+#endif
 	      strlcat (via, "*", sizeof(via));
 	    }
+
 	  }
 	  snprintf (result, result_size, " %d:Fm %s To %s Via %s ",
 		chan+1, src, dst, via);

From 0734e4613439413f65c7053a59b5d1d15ab3ca73 Mon Sep 17 00:00:00 2001
From: wb2osz <wb2osz@comcast.net>
Date: Sat, 21 Sep 2024 19:12:03 +0100
Subject: [PATCH 41/67] Issue 545 - Saved station location overwritten by
 Object report from that station.

---
 src/mheard.c | 14 +++++++++++---
 1 file changed, 11 insertions(+), 3 deletions(-)

diff --git a/src/mheard.c b/src/mheard.c
index f11c68f0..0e88b833 100644
--- a/src/mheard.c
+++ b/src/mheard.c
@@ -406,6 +406,7 @@ void mheard_save_rf (int chan, decode_aprs_t *A, packet_t pp, alevel_t alevel, r
 	  mptr->chan = chan;
 	  mptr->num_digi_hops = hops;
 	  mptr->last_heard_rf = now;
+	  // Why did I do this instead of saving the location for a position report?
 	  mptr->dlat = G_UNKNOWN;
 	  mptr->dlon = G_UNKNOWN;
 	  
@@ -446,9 +447,16 @@ void mheard_save_rf (int chan, decode_aprs_t *A, packet_t pp, alevel_t alevel, r
 	  }
 	}
 
-	if (A->g_lat != G_UNKNOWN && A->g_lon != G_UNKNOWN) {
-	  mptr->dlat = A->g_lat;
-	  mptr->dlon = A->g_lon;
+	// Issue 545.  This was not thought out well.
+	// There was a case where a station sent a position report and the location was stored.
+	// Later, the same station sent an object report and the stations's location was overwritten
+	// by the object location.  Solution: Save location only if position report.
+
+	if (A->g_packet_type == packet_type_position) {
+	  if (A->g_lat != G_UNKNOWN && A->g_lon != G_UNKNOWN) {
+	    mptr->dlat = A->g_lat;
+	    mptr->dlon = A->g_lon;
+	  }
 	}
 
 	if (mheard_debug >= 2) {

From b26c5a4d7d4b2fd19160a1ff20cece13b2c18b13 Mon Sep 17 00:00:00 2001
From: wb2osz <wb2osz@comcast.net>
Date: Sat, 28 Sep 2024 01:58:16 +0100
Subject: [PATCH 42/67] Improve error message.

---
 src/deviceid.c | 5 +++--
 1 file changed, 3 insertions(+), 2 deletions(-)

diff --git a/src/deviceid.c b/src/deviceid.c
index de910e50..d57a6f2f 100644
--- a/src/deviceid.c
+++ b/src/deviceid.c
@@ -528,7 +528,7 @@ static int mice_cmp (const void *px, const void *py)
 
 void deviceid_decode_dest (char *dest, char *device, size_t device_size)
 {
-	*device = '\0';
+	strlcpy (device, "UNKNOWN vendor/model", device_size);
 
 	if (ptocalls == NULL) {
 	  text_color_set(DW_COLOR_ERROR);
@@ -554,6 +554,7 @@ void deviceid_decode_dest (char *dest, char *device, size_t device_size)
 	  }
 	}
 
+// Not found in table.
 	strlcpy (device, "UNKNOWN vendor/model", device_size);
 
 } // end deviceid_decode_dest
@@ -610,7 +611,7 @@ static inline int strncmp_z (char *a, char *b, size_t len)
 
 void deviceid_decode_mice (char *comment, char *trimmed, size_t trimmed_size, char *device, size_t device_size)
 {
-	*device = '\0';
+	strlcpy (device, "UNKNOWN vendor/model", device_size);
 
 	if (ptocalls == NULL) {
 	  text_color_set(DW_COLOR_ERROR);

From a83a1ca5f5d8ec960db6921dc8162bbd7de30dd2 Mon Sep 17 00:00:00 2001
From: wb2osz <wb2osz@comcast.net>
Date: Fri, 18 Oct 2024 17:41:55 +0100
Subject: [PATCH 43/67] Issue 550: Remove extra trailing nul for Send Unproto
 Via.

---
 src/server.c | 6 +++++-
 1 file changed, 5 insertions(+), 1 deletion(-)

diff --git a/src/server.c b/src/server.c
index 023e43f7..a54dd6b4 100644
--- a/src/server.c
+++ b/src/server.c
@@ -1769,7 +1769,11 @@ static THREAD_F cmd_listen_thread (void *arg)
 	          break;
 		}
 
-	        ax25_set_info (pp, (unsigned char*)p, data_len - ndigi * 10);
+		// Issue 550: Info part was one byte too long resulting in an extra nul character.
+		// Original calculation was data_len-ndigi*10 but we need to subtract one
+		// for first byte which is number of digipeaters.
+	        ax25_set_info (pp, (unsigned char*)p, data_len - ndigi * 10 - 1);
+
 	        // Issue 527: NET/ROM routing broadcasts use PID 0xCF which was not preserved here.
 	        ax25_set_pid (pp, pid);
 

From debe703a474675ade40dee9a981b09a17b3fda60 Mon Sep 17 00:00:00 2001
From: wb2osz <wb2osz@comcast.net>
Date: Fri, 18 Oct 2024 23:45:13 +0100
Subject: [PATCH 44/67] MAX_ADEVS==4 enabled debug output options.

---
 src/config.c | 99 ++++++++++++++++++++++++++++++++++++++++++++++++----
 1 file changed, 93 insertions(+), 6 deletions(-)

diff --git a/src/config.c b/src/config.c
index 69fa80e2..863edcdf 100644
--- a/src/config.c
+++ b/src/config.c
@@ -19,8 +19,8 @@
 
 #define CONFIG_C 1		// influences behavior of aprs_tt.h
 
-
-// #define DEBUG 1
+// FIXME:
+#define DEBUG 1
 
 /*------------------------------------------------------------------
  *
@@ -716,6 +716,7 @@ static void rtfm()
 	dw_printf ("    stable release:    https://github.com/wb2osz/direwolf/tree/master/doc\n");
 	dw_printf ("    development version:    https://github.com/wb2osz/direwolf/tree/dev/doc\n");
 	dw_printf ("    additional topics:    https://github.com/wb2osz/direwolf-doc\n");
+	dw_printf ("    general APRS info:    https://how.aprs.works\n");
 }
 
 void config_init (char *fname, struct audio_s *p_audio_config, 
@@ -763,12 +764,17 @@ void config_init (char *fname, struct audio_s *p_audio_config,
 
 	p_audio_config->adev[0].defined = 2;		// 2 means it was done by default and not the user's config file.
 
+// MAX_TOTAL_CHANS
 	for (channel=0; channel<MAX_TOTAL_CHANS; channel++) {
-	  int ot, it;
-
 	  p_audio_config->chan_medium[channel] = MEDIUM_NONE;	/* One or both channels will be */
 								/* set to radio when corresponding */
 								/* audio device is defined. */
+	}
+
+// MAX_RADIO_CHANS for achan[]
+	for (channel=0; channel<MAX_RADIO_CHANS; channel++) {
+	  int ot, it;
+
 	  p_audio_config->achan[channel].modem_type = MODEM_AFSK;			
 	  p_audio_config->achan[channel].v26_alternative = V26_UNSPECIFIED;
 	  p_audio_config->achan[channel].mark_freq = DEFAULT_MARK_FREQ;		/* -m option */
@@ -980,8 +986,13 @@ void config_init (char *fname, struct audio_s *p_audio_config,
 	if (fp == NULL)	{
 	  // TODO: not exactly right for all situations.
 	  text_color_set(DW_COLOR_ERROR);
-	  dw_printf ("ERROR - Could not open config file %s\n", filepath);
+	  dw_printf ("ERROR - Could not open configuration file %s\n", filepath);
 	  dw_printf ("Try using -c command line option for alternate location.\n");
+#ifndef __WIN32__
+	  dw_printf ("A sample direwolf.conf file should be found in one of:\n");
+	  dw_printf ("    /usr/local/share/doc/direwolf/conf/\n");
+	  dw_printf ("    /usr/share/doc/direwolf/conf/\n");
+#endif
 	  rtfm();
 	  exit(EXIT_FAILURE);
 	}
@@ -1423,6 +1434,12 @@ void config_init (char *fname, struct audio_s *p_audio_config,
  */
 
 	  else if (strcasecmp(t, "MODEM") == 0) {
+	    if (channel < 0 || channel >= MAX_RADIO_CHANS) {
+	      text_color_set(DW_COLOR_ERROR);
+	      dw_printf ("Line %d: MODEM can only be used with radio channel 0 - %d.\n", line, MAX_RADIO_CHANS-1);
+	      continue;
+	    }
+	      
 	    int n;
 	    t = split(NULL,0);
 	    if (t == NULL) {
@@ -1758,6 +1775,11 @@ void config_init (char *fname, struct audio_s *p_audio_config,
 
 
 	  else if (strcasecmp(t, "DTMF") == 0) {
+	    if (channel < 0 || channel >= MAX_RADIO_CHANS) {
+	      text_color_set(DW_COLOR_ERROR);
+	      dw_printf ("Line %d: DTMF can only be used with radio channel 0 - %d.\n", line, MAX_RADIO_CHANS-1);
+	      continue;
+	    }
 
 	    p_audio_config->achan[channel].dtmf_decode = DTMF_DECODE_ON;
 
@@ -1773,6 +1795,11 @@ void config_init (char *fname, struct audio_s *p_audio_config,
  */
 
 	  else if (strcasecmp(t, "FIX_BITS") == 0) {
+	    if (channel < 0 || channel >= MAX_RADIO_CHANS) {
+	      text_color_set(DW_COLOR_ERROR);
+	      dw_printf ("Line %d: FIX_BITS can only be used with radio channel 0 - %d.\n", line, MAX_RADIO_CHANS-1);
+	      continue;
+	    }
 	    int n;
 	    t = split(NULL,0);
 	    if (t == NULL) {
@@ -1851,6 +1878,11 @@ void config_init (char *fname, struct audio_s *p_audio_config,
  */
 
 	  else if (strcasecmp(t, "PTT") == 0 || strcasecmp(t, "DCD") == 0 || strcasecmp(t, "CON") == 0) {
+	    if (channel < 0 || channel >= MAX_RADIO_CHANS) {
+	      text_color_set(DW_COLOR_ERROR);
+	      dw_printf ("Line %d: PTT can only be used with radio channel 0 - %d.\n", line, MAX_RADIO_CHANS-1);
+	      continue;
+	    }
 	    int ot;
 	    char otname[8];
 
@@ -2222,6 +2254,11 @@ void config_init (char *fname, struct audio_s *p_audio_config,
  */
 
 	  else if (strcasecmp(t, "TXINH") == 0) {
+	    if (channel < 0 || channel >= MAX_RADIO_CHANS) {
+	      text_color_set(DW_COLOR_ERROR);
+	      dw_printf ("Line %d: TXINH can only be used with radio channel 0 - %d.\n", line, MAX_RADIO_CHANS-1);
+	      continue;
+	    }
 	    char itname[8];
 
 	    strlcpy (itname, "TXINH", sizeof(itname));
@@ -2268,6 +2305,11 @@ void config_init (char *fname, struct audio_s *p_audio_config,
  */
 
 	  else if (strcasecmp(t, "DWAIT") == 0) {
+	    if (channel < 0 || channel >= MAX_RADIO_CHANS) {
+	      text_color_set(DW_COLOR_ERROR);
+	      dw_printf ("Line %d: DWAIT can only be used with radio channel 0 - %d.\n", line, MAX_RADIO_CHANS-1);
+	      continue;
+	    }
 	    int n;
 	    t = split(NULL,0);
 	    if (t == NULL) {
@@ -2292,6 +2334,11 @@ void config_init (char *fname, struct audio_s *p_audio_config,
  */
 
 	  else if (strcasecmp(t, "SLOTTIME") == 0) {
+	    if (channel < 0 || channel >= MAX_RADIO_CHANS) {
+	      text_color_set(DW_COLOR_ERROR);
+	      dw_printf ("Line %d: SLOTTIME can only be used with radio channel 0 - %d.\n", line, MAX_RADIO_CHANS-1);
+	      continue;
+	    }
 	    int n;
 	    t = split(NULL,0);
 	    if (t == NULL) {
@@ -2322,6 +2369,11 @@ void config_init (char *fname, struct audio_s *p_audio_config,
  */
 
 	  else if (strcasecmp(t, "PERSIST") == 0) {
+	    if (channel < 0 || channel >= MAX_RADIO_CHANS) {
+	      text_color_set(DW_COLOR_ERROR);
+	      dw_printf ("Line %d: PERSIST can only be used with radio channel 0 - %d.\n", line, MAX_RADIO_CHANS-1);
+	      continue;
+	    }
 	    int n;
 	    t = split(NULL,0);
 	    if (t == NULL) {
@@ -2349,6 +2401,11 @@ void config_init (char *fname, struct audio_s *p_audio_config,
  */
 
 	  else if (strcasecmp(t, "TXDELAY") == 0) {
+	    if (channel < 0 || channel >= MAX_RADIO_CHANS) {
+	      text_color_set(DW_COLOR_ERROR);
+	      dw_printf ("Line %d: TXDELAY can only be used with radio channel 0 - %d.\n", line, MAX_RADIO_CHANS-1);
+	      continue;
+	    }
 	    int n;
 	    t = split(NULL,0);
 	    if (t == NULL) {
@@ -2390,6 +2447,11 @@ void config_init (char *fname, struct audio_s *p_audio_config,
  */
 
 	  else if (strcasecmp(t, "TXTAIL") == 0) {
+	    if (channel < 0 || channel >= MAX_RADIO_CHANS) {
+	      text_color_set(DW_COLOR_ERROR);
+	      dw_printf ("Line %d: TXTAIL can only be used with radio channel 0 - %d.\n", line, MAX_RADIO_CHANS-1);
+	      continue;
+	    }
 	    int n;
 	    t = split(NULL,0);
 	    if (t == NULL) {
@@ -2430,6 +2492,11 @@ void config_init (char *fname, struct audio_s *p_audio_config,
  */
 	  else if (strcasecmp(t, "FULLDUP") == 0) {
 
+	    if (channel < 0 || channel >= MAX_RADIO_CHANS) {
+	      text_color_set(DW_COLOR_ERROR);
+	      dw_printf ("Line %d: FULLDUP can only be used with radio channel 0 - %d.\n", line, MAX_RADIO_CHANS-1);
+	      continue;
+	    }
 	    t = split(NULL,0);
 	    if (t == NULL) {
 	      text_color_set(DW_COLOR_ERROR);
@@ -2457,6 +2524,11 @@ void config_init (char *fname, struct audio_s *p_audio_config,
 
 	  else if (strcasecmp(t, "SPEECH") == 0) {
 
+	    if (channel < 0 || channel >= MAX_RADIO_CHANS) {
+	      text_color_set(DW_COLOR_ERROR);
+	      dw_printf ("Line %d: SPEECH can only be used with radio channel 0 - %d.\n", line, MAX_RADIO_CHANS-1);
+	      continue;
+	    }
 	    t = split(NULL,0);
 	    if (t == NULL) {
 	      text_color_set(DW_COLOR_ERROR);
@@ -2488,6 +2560,11 @@ void config_init (char *fname, struct audio_s *p_audio_config,
  */
 
 	  else if (strcasecmp(t, "FX25TX") == 0) {
+	    if (channel < 0 || channel >= MAX_RADIO_CHANS) {
+	      text_color_set(DW_COLOR_ERROR);
+	      dw_printf ("Line %d: FX25TX can only be used with radio channel 0 - %d.\n", line, MAX_RADIO_CHANS-1);
+	      continue;
+	    }
 	    int n;
 	    t = split(NULL,0);
 	    if (t == NULL) {
@@ -2510,7 +2587,7 @@ void config_init (char *fname, struct audio_s *p_audio_config,
 	  }
 
 /*
- * FX25AUTO n		- Enable Automatic use of FX.25 for connected mode.
+ * FX25AUTO n		- Enable Automatic use of FX.25 for connected mode.  *** Not Implemented ***
  *				Automatically enable, for that session only, when an identical
  *				frame is sent more than this number of times.
  *				Default 5 based on half of default RETRY.
@@ -2519,6 +2596,11 @@ void config_init (char *fname, struct audio_s *p_audio_config,
  */
 
 	  else if (strcasecmp(t, "FX25AUTO") == 0) {
+	    if (channel < 0 || channel >= MAX_RADIO_CHANS) {
+	      text_color_set(DW_COLOR_ERROR);
+	      dw_printf ("Line %d: FX25AUTO can only be used with radio channel 0 - %d.\n", line, MAX_RADIO_CHANS-1);
+	      continue;
+	    }
 	    int n;
 	    t = split(NULL,0);
 	    if (t == NULL) {
@@ -2550,6 +2632,11 @@ void config_init (char *fname, struct audio_s *p_audio_config,
 
 	  else if (strcasecmp(t, "IL2PTX") == 0) {
 
+	    if (channel < 0 || channel >= MAX_RADIO_CHANS) {
+	      text_color_set(DW_COLOR_ERROR);
+	      dw_printf ("Line %d: IL2PTX can only be used with radio channel 0 - %d.\n", line, MAX_RADIO_CHANS-1);
+	      continue;
+	    }
 	    p_audio_config->achan[channel].layer2_xmit = LAYER2_IL2P;
 	    p_audio_config->achan[channel].il2p_max_fec = 1;
 	    p_audio_config->achan[channel].il2p_invert_polarity = 0;

From e1e5be36e02d0497637e45b81c7ee0137ca6c09c Mon Sep 17 00:00:00 2001
From: wb2osz <wb2osz@comcast.net>
Date: Fri, 18 Oct 2024 23:46:42 +0100
Subject: [PATCH 45/67] MAX_ADEVS==4 enabled debug output options.

---
 src/config.c | 10 +++++++---
 1 file changed, 7 insertions(+), 3 deletions(-)

diff --git a/src/config.c b/src/config.c
index 863edcdf..097c908f 100644
--- a/src/config.c
+++ b/src/config.c
@@ -19,8 +19,8 @@
 
 #define CONFIG_C 1		// influences behavior of aprs_tt.h
 
-// FIXME:
-#define DEBUG 1
+
+//#define DEBUG 1
 
 /*------------------------------------------------------------------
  *
@@ -772,6 +772,7 @@ void config_init (char *fname, struct audio_s *p_audio_config,
 	}
 
 // MAX_RADIO_CHANS for achan[]
+// Maybe achan should be renamed to radiochan to make it clearer.
 	for (channel=0; channel<MAX_RADIO_CHANS; channel++) {
 	  int ot, it;
 
@@ -1235,6 +1236,9 @@ void config_init (char *fname, struct audio_s *p_audio_config,
  * CHANNEL n		- Set channel for channel-specific commands.  Only for modem/radio channels.
  */
 
+// TODO: allow full range so mycall can be set for network channels.
+// Watch out for achan[] out of bounds.
+
 	  else if (strcasecmp(t, "CHANNEL") == 0) {
 	    int n;
 	    t = split(NULL,0);
@@ -1434,12 +1438,12 @@ void config_init (char *fname, struct audio_s *p_audio_config,
  */
 
 	  else if (strcasecmp(t, "MODEM") == 0) {
+
 	    if (channel < 0 || channel >= MAX_RADIO_CHANS) {
 	      text_color_set(DW_COLOR_ERROR);
 	      dw_printf ("Line %d: MODEM can only be used with radio channel 0 - %d.\n", line, MAX_RADIO_CHANS-1);
 	      continue;
 	    }
-	      
 	    int n;
 	    t = split(NULL,0);
 	    if (t == NULL) {

From a627abc4849714ea3ee47085ab935dadaf8a867b Mon Sep 17 00:00:00 2001
From: wb2osz <wb2osz@comcast.net>
Date: Fri, 18 Oct 2024 23:52:24 +0100
Subject: [PATCH 46/67] Fix compile warning.

---
 external/hidapi/hid.c | 5 ++++-
 1 file changed, 4 insertions(+), 1 deletion(-)

diff --git a/external/hidapi/hid.c b/external/hidapi/hid.c
index e483cd4f..f5c9858a 100644
--- a/external/hidapi/hid.c
+++ b/external/hidapi/hid.c
@@ -20,6 +20,8 @@
         https://github.com/libusb/hidapi .
 ********************************************************/
 
+#include "../../src/direwolf.h"	// for strlcpy
+
 #include <windows.h>
 
 #ifndef _NTDEF_
@@ -465,7 +467,8 @@ struct hid_device_info HID_API_EXPORT * HID_API_CALL hid_enumerate(unsigned shor
 			if (str) {
 				len = strlen(str);
 				cur_dev->path = (char*) calloc(len+1, sizeof(char));
-				strncpy(cur_dev->path, str, len+1);
+				//strncpy(cur_dev->path, str, len+1);  // produces warning
+				strlcpy(cur_dev->path, str, len+1);
 				cur_dev->path[len] = '\0';
 			}
 			else

From 88cf0ba55d5deda66371648a1fa32fba5e5351f0 Mon Sep 17 00:00:00 2001
From: wb2osz <wb2osz@comcast.net>
Date: Sat, 19 Oct 2024 00:16:26 +0100
Subject: [PATCH 47/67] Add comments.

---
 src/ax25_pad.c | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/src/ax25_pad.c b/src/ax25_pad.c
index 57fd79d2..2fce2df9 100644
--- a/src/ax25_pad.c
+++ b/src/ax25_pad.c
@@ -174,6 +174,7 @@
 #include "regex.h"
 
 #if __WIN32__
+// TODO:  Why is this here, rather than in direwolf.h?
 char *strtok_r(char *str, const char *delim, char **saveptr);
 #endif
 
@@ -194,6 +195,7 @@ static volatile int last_seq_num = 0;
 
 #if AX25MEMDEBUG
 
+// TODO:  Make static and use function for any extern references.
 int ax25memdebug = 0;
 
 

From 3ba464dfff1e815cef27b27fc3355b05064075f1 Mon Sep 17 00:00:00 2001
From: wb2osz <wb2osz@comcast.net>
Date: Sat, 19 Oct 2024 00:25:36 +0100
Subject: [PATCH 48/67] Add info about layer 2 transmit per channel.

---
 src/audio.h | 11 ++++-------
 src/demod.c |  7 ++++++-
 2 files changed, 10 insertions(+), 8 deletions(-)

diff --git a/src/audio.h b/src/audio.h
index 92fd944e..f69fc1d7 100644
--- a/src/audio.h
+++ b/src/audio.h
@@ -115,12 +115,6 @@ struct audio_s {
 	float recv_ber;			/* Receive Bit Error Rate (BER). */
 					/* Probability of inverting a bit coming out of the modem. */
 
-	//int fx25_xmit_enable;		/* Enable transmission of FX.25.  */
-					/* See fx25_init.c for explanation of values. */
-					/* Initially this applies to all channels. */
-					/* This should probably be per channel. One step at a time. */
-					/* v1.7 - replaced by layer2_xmit==LAYER2_FX25 */
-
 	int fx25_auto_enable;		/* Turn on FX.25 for current connected mode session */
 					/* under poor conditions. */
 					/* Set to 0 to disable feature. */
@@ -198,7 +192,7 @@ struct audio_s {
 					/* Might try MFJ-2400 / CCITT v.26 / Bell 201 someday. */
 					/* No modem.  Might want this for DTMF only channel. */
 
-	    enum layer2_t { LAYER2_AX25 = 0, LAYER2_FX25, LAYER2_IL2P } layer2_xmit;
+	    enum layer2_t { LAYER2_AX25 = 0, LAYER2_FX25, LAYER2_IL2P } layer2_xmit;	// Must keep in sync with layer2_tx, below.
 
 					// IL2P - New for version 1.7.
 					// New layer 2 with FEC.  Much less overhead than FX.25 but no longer backward compatible.
@@ -405,6 +399,9 @@ struct audio_s {
 
 };
 
+#if DEMOD_C
+	const static char *layer2_tx[3] = {"AX.25", "FX.25", "IL2P"};	// Must keep in sync with enum layer2_t above.
+#endif
 
 #if __WIN32__
 #define DEFAULT_ADEVICE	""		/* Windows: Empty string = default audio device. */
diff --git a/src/demod.c b/src/demod.c
index efcfde71..ebbcbed4 100644
--- a/src/demod.c
+++ b/src/demod.c
@@ -31,6 +31,8 @@
  *
  *---------------------------------------------------------------*/
 
+#define DEMOD_C 1
+
 #include "direwolf.h"
 
 #include <stdlib.h>
@@ -306,6 +308,7 @@ int demod_init (struct audio_s *pa)
 		    save_audio_config_p->adev[ACHAN2ADEV(chan)].samples_per_sec);
 	      if (save_audio_config_p->achan[chan].decimate != 1) 
 	        dw_printf (" / %d", save_audio_config_p->achan[chan].decimate);
+	      dw_printf (", Tx %s", layer2_tx[(int)(save_audio_config_p->achan[chan].layer2_xmit)]);
 	      if (save_audio_config_p->achan[chan].dtmf_decode != DTMF_DECODE_OFF) 
 	        dw_printf (", DTMF decoder enabled");
 	      dw_printf (".\n");
@@ -540,7 +543,7 @@ int demod_init (struct audio_s *pa)
 		    save_audio_config_p->adev[ACHAN2ADEV(chan)].samples_per_sec);
 	      if (save_audio_config_p->achan[chan].decimate != 1)
 	        dw_printf (" / %d", save_audio_config_p->achan[chan].decimate);
-
+	      dw_printf (", Tx %s", layer2_tx[(int)(save_audio_config_p->achan[chan].layer2_xmit)]);
 	      if (save_audio_config_p->achan[chan].v26_alternative == V26_B)
 	        dw_printf (", compatible with MFJ-2400");
 	      else
@@ -601,6 +604,7 @@ int demod_init (struct audio_s *pa)
 		    save_audio_config_p->adev[ACHAN2ADEV(chan)].samples_per_sec);
 	      if (save_audio_config_p->achan[chan].decimate != 1)
 	        dw_printf (" / %d", save_audio_config_p->achan[chan].decimate);
+	      dw_printf (", Tx %s", layer2_tx[(int)(save_audio_config_p->achan[chan].layer2_xmit)]);
 	      if (save_audio_config_p->achan[chan].dtmf_decode != DTMF_DECODE_OFF)
 	        dw_printf (", DTMF decoder enabled");
 	      dw_printf (".\n");
@@ -736,6 +740,7 @@ int demod_init (struct audio_s *pa)
 		    save_audio_config_p->achan[chan].profiles,
 		    save_audio_config_p->adev[ACHAN2ADEV(chan)].samples_per_sec,
 	            save_audio_config_p->achan[chan].upsample);
+	      dw_printf (", Tx %s", layer2_tx[(int)(save_audio_config_p->achan[chan].layer2_xmit)]);
 	      if (save_audio_config_p->achan[chan].dtmf_decode != DTMF_DECODE_OFF) 
 	        dw_printf (", DTMF decoder enabled");
 	      dw_printf (".\n");

From f61f33fa51eb22559fb7604693e8983785ef4938 Mon Sep 17 00:00:00 2001
From: wb2osz <wb2osz@comcast.net>
Date: Sat, 19 Oct 2024 00:41:40 +0100
Subject: [PATCH 49/67] Issue 549 - direwolf man page.

---
 man/direwolf.1 | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/man/direwolf.1 b/man/direwolf.1
index 93f786dc..c6c8fa8d 100644
--- a/man/direwolf.1
+++ b/man/direwolf.1
@@ -132,6 +132,8 @@ f = Packet filtering.
 x = FX.25 increase verbose level.
 .P
 d = APRStt (DTMF to APRS object conversion).
+.P
+2 = IL2P.
 .RE
 .RE
 .PD

From 07708110472286acbbf8913dd05af4d340394272 Mon Sep 17 00:00:00 2001
From: wb2osz <wb2osz@comcast.net>
Date: Sat, 19 Oct 2024 02:17:35 +0100
Subject: [PATCH 50/67] patch from
 https://sources.debian.org/src/direwolf/1.7+dfsg-2/debian/patches/desktop-main-category/

---
 cmake/cpack/direwolf.desktop.in | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/cmake/cpack/direwolf.desktop.in b/cmake/cpack/direwolf.desktop.in
index 79c63aa6..6546ad7f 100644
--- a/cmake/cpack/direwolf.desktop.in
+++ b/cmake/cpack/direwolf.desktop.in
@@ -6,5 +6,5 @@ Icon=@CMAKE_PROJECT_NAME@_icon.png
 StartupNotify=true
 Terminal=false
 Type=Application
-Categories=HamRadio
-Keywords=Ham Radio;APRS;Soundcard TNC;KISS;AGWPE;AX.25
\ No newline at end of file
+Categories=Network;HamRadio
+Keywords=Ham Radio;APRS;Soundcard TNC;KISS;AGWPE;AX.25

From 33beb24fb384c8997aa2dfcdfe1e7f821e8f02b5 Mon Sep 17 00:00:00 2001
From: wb2osz <wb2osz@comcast.net>
Date: Sat, 19 Oct 2024 02:32:47 +0100
Subject: [PATCH 51/67] patch from
 https://sources.debian.org/src/direwolf/1.7+dfsg-2/debian/patches/lib-udev-rules/

---
 conf/CMakeLists.txt | 7 ++++++-
 1 file changed, 6 insertions(+), 1 deletion(-)

diff --git a/conf/CMakeLists.txt b/conf/CMakeLists.txt
index d4a229d7..ffc809b3 100644
--- a/conf/CMakeLists.txt
+++ b/conf/CMakeLists.txt
@@ -25,8 +25,13 @@ string(REGEX REPLACE "^%C%([^\n]*)" "\\1" file_content "${file_content}")
 file(WRITE "${CMAKE_BINARY_DIR}/direwolf.conf" "${file_content}")
 
 # install udev rules for CM108
+# There are two locations.  The one in /etc/udev/rules.d is meant for local customization and
+# takes precedence for the same name.
+# https://sources.debian.org/src/direwolf/1.7+dfsg-2/debian/patches/lib-udev-rules/
+# says that we should use the /usr/lib/udev/rules.d location.
 if(LINUX)
-  install(FILES "${CUSTOM_CONF_DIR}/99-direwolf-cmedia.rules" DESTINATION /etc/udev/rules.d/)
+  #install(FILES "${CUSTOM_CONF_DIR}/99-direwolf-cmedia.rules" DESTINATION /etc/udev/rules.d/)
+  install(FILES "${CUSTOM_CONF_DIR}/99-direwolf-cmedia.rules" DESTINATION /usr/lib/udev/rules.d/)
 endif()
 
 install(FILES "${CMAKE_BINARY_DIR}/direwolf.conf" DESTINATION ${INSTALL_CONF_DIR})

From 73943ed67d46d084cb46aceead8e0638300659ec Mon Sep 17 00:00:00 2001
From: wb2osz <wb2osz@comcast.net>
Date: Tue, 22 Oct 2024 12:29:39 +0100
Subject: [PATCH 52/67] More error checking.

---
 src/decode_aprs.c | 18 +++++++++++++++++-
 1 file changed, 17 insertions(+), 1 deletion(-)

diff --git a/src/decode_aprs.c b/src/decode_aprs.c
index ce658eb6..a402473f 100644
--- a/src/decode_aprs.c
+++ b/src/decode_aprs.c
@@ -1421,6 +1421,15 @@ static void aprs_mic_e (decode_aprs_t *A, packet_t pp, unsigned char *info, int
 
 	strlcpy (A->g_data_type_desc, "MIC-E", sizeof(A->g_data_type_desc));
 
+	if (ilen < sizeof(struct aprs_mic_e_s)) {
+	  if ( ! A->g_quiet) {
+	    text_color_set(DW_COLOR_ERROR);
+	    dw_printf("MIC-E format must have at least %d characters in the information part.\n", sizeof(struct aprs_mic_e_s));
+	  }
+	  return;
+	}
+	info[ilen] = '\0';\
+
 	p = (struct aprs_mic_e_s *)info;
 
 /* Destination is really latitude of form ddmmhh. */
@@ -3875,7 +3884,7 @@ double get_longitude_9 (char *p, int quiet)
  *
  * Inputs:	p 	- Pointer to first byte.
  *
- * Returns:	time_t data type. (UTC)
+ * Returns:	time_t data type. (UTC)  Zero if error.
  *
  * Description:	
  *
@@ -3945,6 +3954,13 @@ time_t get_timestamp (decode_aprs_t *A, char *p)
 				/* h = UTC. */
 	} *phms;
 
+	if ( ! (isdigit(p[0]) && isdigit(p[1]) && isdigit(p[2]) && isdigit(p[3]) && isdigit(p[4]) && isdigit(p[5]) &&
+		(p[6] == 'z' || p[6] == '/' || p[6] == 'h'))) {
+	    text_color_set(DW_COLOR_ERROR);
+	    dw_printf("Timestamp must be 6 digits followed by z, h, or /.\n");
+	    return ((time_t)0);
+	}
+
 	struct tm *ptm;
 
 	time_t ts;

From 89029db6106028414fc2e17b960b55186f9cfda8 Mon Sep 17 00:00:00 2001
From: wb2osz <wb2osz@comcast.net>
Date: Tue, 22 Oct 2024 13:58:55 +0100
Subject: [PATCH 53/67] More error checking.

---
 src/decode_aprs.c | 20 +++++++++++++++++---
 1 file changed, 17 insertions(+), 3 deletions(-)

diff --git a/src/decode_aprs.c b/src/decode_aprs.c
index a402473f..81ae9bf2 100644
--- a/src/decode_aprs.c
+++ b/src/decode_aprs.c
@@ -1428,7 +1428,7 @@ static void aprs_mic_e (decode_aprs_t *A, packet_t pp, unsigned char *info, int
 	  }
 	  return;
 	}
-	info[ilen] = '\0';\
+	info[ilen] = '\0';
 
 	p = (struct aprs_mic_e_s *)info;
 
@@ -1658,12 +1658,26 @@ static void aprs_mic_e (decode_aprs_t *A, packet_t pp, unsigned char *info, int
 
 // The rest is a comment which can have other information cryptically embedded.
 // Remove any trailing CR, which I would argue, violates the protocol spec.
-// It is essential to keep trailing spaces.  e.g. VX-8 suffix is "_ "
+// It is essential to keep trailing spaces.  e.g. VX-8 device id suffix is "_ "
+
+	if (ilen <= sizeof(struct aprs_mic_e_s)) {
+	  // Too short for a comment.  We are finished.
+	  strlcpy (A->g_mfr, "UNKNOWN vendor/model", sizeof(A->g_mfr));
+	  return;
+	}
 
 	char mcomment[256];
 	strlcpy (mcomment, ((char*)info) + sizeof(struct aprs_mic_e_s), sizeof(mcomment));
+
+	assert (strlen(mcomment) > 0);
+
 	if (mcomment[strlen(mcomment)-1] == '\r') {
 	  mcomment[strlen(mcomment)-1] = '\0';
+	  if (strlen(mcomment) == 0) {
+	    // Nothing left after removing trailing CR.
+	    strlcpy (A->g_mfr, "UNKNOWN vendor/model", sizeof(A->g_mfr));
+	    return;
+	  }
 	}
 
 /* Now try to pick out manufacturer and other optional items. */
@@ -1678,7 +1692,7 @@ static void aprs_mic_e (decode_aprs_t *A, packet_t pp, unsigned char *info, int
 // Three base 91 characters followed by }
 
 
-	if (strlen(trimmed) >=4 &&
+	if (strlen(trimmed) >= 4 &&
 			isdigit91(trimmed[0]) &&
 			isdigit91(trimmed[1]) &&
 			isdigit91(trimmed[2]) &&

From 0d7e296d6d4d666b7af0b15a8771770c6d075e0c Mon Sep 17 00:00:00 2001
From: wb2osz <wb2osz@comcast.net>
Date: Tue, 22 Oct 2024 15:43:41 +0100
Subject: [PATCH 54/67] Fix MIC-E comment when device id is missing.

---
 src/deviceid.c | 13 +++++++++++--
 1 file changed, 11 insertions(+), 2 deletions(-)

diff --git a/src/deviceid.c b/src/deviceid.c
index d57a6f2f..3a4562d3 100644
--- a/src/deviceid.c
+++ b/src/deviceid.c
@@ -568,16 +568,18 @@ void deviceid_decode_dest (char *dest, char *device, size_t device_size)
  *
  * Inputs:	comment - MIC-E comment that might have vendor/model encoded as
  *			a prefix and/or suffix.
+ *			Any trailing CR has already been removed.
  *
  *		trimmed_size - Amount of space available for result to avoid buffer overflow.
  *
  *		device_size - Amount of space available for result to avoid buffer overflow.
  *
  * Outputs:	trimmed - Final comment with device vendor/model removed.
+ *				This would include any altitude.
  *
  *		device	- Vendor and model.
  *
- * Description:	This has a tortured history.
+ * Description:	MIC-E device identification has a tortured history.
  *
  *		The Kenwood TH-D7A  put ">" at the beginning of the comment.
  *		The Kenwood TM-D700 put "]" at the beginning of the comment.
@@ -593,7 +595,9 @@ void deviceid_decode_dest (char *dest, char *device, size_t device_size)
  *
  * References:	http://www.aprs.org/aprs12/mic-e-types.txt
  *		http://www.aprs.org/aprs12/mic-e-examples.txt
- *
+ *		https://github.com/wb2osz/aprsspec containing:
+ *			APRS Protocol Specification 1.2
+ *			Understanding APRS Packets
  *------------------------------------------------------------------*/
 
 // The strncmp documentation doesn't mention behavior if length is zero.
@@ -612,6 +616,10 @@ static inline int strncmp_z (char *a, char *b, size_t len)
 void deviceid_decode_mice (char *comment, char *trimmed, size_t trimmed_size, char *device, size_t device_size)
 {
 	strlcpy (device, "UNKNOWN vendor/model", device_size);
+	strlcpy (trimmed, comment, sizeof(trimmed));
+	if (strlen(comment) < 1) {
+	  return;
+	}
 
 	if (ptocalls == NULL) {
 	  text_color_set(DW_COLOR_ERROR);
@@ -663,6 +671,7 @@ void deviceid_decode_mice (char *comment, char *trimmed, size_t trimmed_size, ch
 // Not found.
 
 	strlcpy (device, "UNKNOWN vendor/model", device_size);
+	strlcpy (trimmed, comment, sizeof(trimmed));
 
 } // end deviceid_decode_mice
 

From aede01d6ac33077378eb3260c484dacae6d54fd1 Mon Sep 17 00:00:00 2001
From: wb2osz <wb2osz@comcast.net>
Date: Tue, 22 Oct 2024 16:41:15 +0100
Subject: [PATCH 55/67] Add another test case.

---
 src/deviceid.c | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/src/deviceid.c b/src/deviceid.c
index 3a4562d3..036c7b0c 100644
--- a/src/deviceid.c
+++ b/src/deviceid.c
@@ -135,6 +135,10 @@ int main (int argc, char *argv[])
 	assert (strcmp(comment_out, "Comment") == 0);
 	assert (strcmp(device, "UNKNOWN vendor/model") == 0);
 
+	deviceid_decode_mice ("", comment_out, sizeof(comment_out), device, sizeof(device));
+	dw_printf ("%s %s\n", comment_out, device);
+	assert (strcmp(comment_out, "") == 0);
+	assert (strcmp(device, "UNKNOWN vendor/model") == 0);
 
 // Tocall
 

From 44f576cb733b6394f1500058631bc5d25754bc90 Mon Sep 17 00:00:00 2001
From: wb2osz <wb2osz@comcast.net>
Date: Tue, 22 Oct 2024 23:38:38 +0100
Subject: [PATCH 56/67] MIC-E improvements

---
 src/decode_aprs.c | 2 +-
 src/deviceid.c    | 4 ++--
 2 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/src/decode_aprs.c b/src/decode_aprs.c
index 81ae9bf2..acfed6b8 100644
--- a/src/decode_aprs.c
+++ b/src/decode_aprs.c
@@ -1424,7 +1424,7 @@ static void aprs_mic_e (decode_aprs_t *A, packet_t pp, unsigned char *info, int
 	if (ilen < sizeof(struct aprs_mic_e_s)) {
 	  if ( ! A->g_quiet) {
 	    text_color_set(DW_COLOR_ERROR);
-	    dw_printf("MIC-E format must have at least %d characters in the information part.\n", sizeof(struct aprs_mic_e_s));
+	    dw_printf("MIC-E format must have at least %d characters in the information part.\n", (int)(sizeof(struct aprs_mic_e_s)));
 	  }
 	  return;
 	}
diff --git a/src/deviceid.c b/src/deviceid.c
index 036c7b0c..49b9b346 100644
--- a/src/deviceid.c
+++ b/src/deviceid.c
@@ -620,7 +620,7 @@ static inline int strncmp_z (char *a, char *b, size_t len)
 void deviceid_decode_mice (char *comment, char *trimmed, size_t trimmed_size, char *device, size_t device_size)
 {
 	strlcpy (device, "UNKNOWN vendor/model", device_size);
-	strlcpy (trimmed, comment, sizeof(trimmed));
+	strlcpy (trimmed, comment, trimmed_size);
 	if (strlen(comment) < 1) {
 	  return;
 	}
@@ -675,7 +675,7 @@ void deviceid_decode_mice (char *comment, char *trimmed, size_t trimmed_size, ch
 // Not found.
 
 	strlcpy (device, "UNKNOWN vendor/model", device_size);
-	strlcpy (trimmed, comment, sizeof(trimmed));
+	strlcpy (trimmed, comment, trimmed_size);
 
 } // end deviceid_decode_mice
 

From 5736b0f601e55b2fdafe32858679b68db00f5995 Mon Sep 17 00:00:00 2001
From: wb2osz <wb2osz@comcast.net>
Date: Tue, 29 Oct 2024 19:41:35 +0100
Subject: [PATCH 57/67] Check index.

---
 src/igate.c | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/src/igate.c b/src/igate.c
index 1e5d56ed..b11f7a31 100644
--- a/src/igate.c
+++ b/src/igate.c
@@ -1071,6 +1071,8 @@ void igate_send_rec_packet (int chan, packet_t recv_pp)
  * Inputs:	pp 	- Packet object.
  *
  *		chan	- Radio channel where it was received.
+ *				This will be -1 if from a beacon with sendto=ig
+ *				so be careful if using as subscript.
  *
  * Description:	Duplicate detection is handled here.
  *		Suppress if same was sent recently.
@@ -1141,7 +1143,7 @@ static void send_packet_to_server (packet_t pp, int chan)
 	  strlcat (msg, ",qAO,", sizeof(msg));		// new for version 1.4.
 	}
 
-	strlcat (msg, save_audio_config_p->mycall[chan], sizeof(msg));
+	strlcat (msg, save_audio_config_p->mycall[chan>=0 ? chan : 0], sizeof(msg));
 	strlcat (msg, ":", sizeof(msg));
 
 

From 84a243d5ab00609376b772f28248a296dfdc1d23 Mon Sep 17 00:00:00 2001
From: wb2osz <wb2osz@comcast.net>
Date: Tue, 26 Nov 2024 21:42:17 +0000
Subject: [PATCH 58/67] Improve error message.

---
 src/audio.c | 7 ++++++-
 1 file changed, 6 insertions(+), 1 deletion(-)

diff --git a/src/audio.c b/src/audio.c
index a522b399..53e2ae38 100644
--- a/src/audio.c
+++ b/src/audio.c
@@ -1099,7 +1099,12 @@ int audio_get (int a)
 	        dw_printf ("Audio input device %d error code %d: %s\n", a, n, snd_strerror(n));
 
 	        if (n == (-EPIPE)) {
-	          dw_printf ("This is most likely caused by the CPU being too slow to keep up with the audio stream.\n");
+	          dw_printf ("If receiving is fine and strange things happen when transmitting, it is probably RF energy\n"); 
+	          dw_printf ("getting into your audio or digital wiring. This can cause USB to lock up or PTT to get stuck on.\n");
+	          dw_printf ("Move the radio, and especially the antenna, farther away from the computer.\n");
+	          dw_printf ("Use shieled cable and put ferrite beads on the cables to reduce RF going where it is not wanted.\n");
+	          dw_printf ("\n");
+	          dw_printf ("A less likely cause is the CPU being too slow to keep up with the audio stream.\n");
 	          dw_printf ("Use the \"top\" command, in another command window, to look at CPU usage.\n");
 	          dw_printf ("This might be a temporary condition so we will attempt to recover a few times before giving up.\n");
 	          dw_printf ("If using a very slow CPU, try reducing the CPU load by using -P- command\n");

From 22c104288269317688b2db8864f716b6ff94ca52 Mon Sep 17 00:00:00 2001
From: wb2osz <wb2osz@comcast.net>
Date: Thu, 24 Apr 2025 13:27:31 -0400
Subject: [PATCH 59/67] Simplify, simplify, simplify!   --  Henry David Thoreau

---
 conf/generic.conf | 222 +++++-----------------------------------------
 1 file changed, 22 insertions(+), 200 deletions(-)

diff --git a/conf/generic.conf b/conf/generic.conf
index 4fb63f6b..208708b8 100644
--- a/conf/generic.conf
+++ b/conf/generic.conf
@@ -1,6 +1,6 @@
 %C%#############################################################
 %C%#                                                           #
-%C%#               Configuration file for Dire Wolf            #
+%C%#        Sample configuration file for Dire Wolf            #
 %C%#                                                           #
 %L%#                   Linux version                           #
 %W%#                   Windows version                         #
@@ -14,7 +14,7 @@
 %R%	It would be a maintenance burden to keep most of
 %R%	two different versions in sync.
 %R%	This common source is now used to generate the
-%R%	two different variations while having only a single
+%R%	three different variations while having only a single
 %R%	copy of the common parts.
 %R%
 %R%	The first column contains one of the following:
@@ -38,6 +38,10 @@
 %M%# /usr/local/share/doc/direwolf/ or /usr/share/doc/direwolf/
 %M%# Concise "man" pages are also available for Mac OSX.
 %C%#
+%C%# Recommended Reading for everyone:
+%C%# "Understanding APRS Packets"  in https://github.com/wb2osz/aprsspec
+%C%#
+%C%#
 %C%# Questions???  Join the discussion forum:  https://groups.io/g/direwolf
 %C%#
 %C%#
@@ -90,7 +94,7 @@
 %C%#############################################################
 %C%#                                                           #
 %C%#               FIRST AUDIO DEVICE PROPERTIES               #
-%C%#               (Channel 0 + 1 if in stereo)                #
+%C%#               (Channel 0 or 0 + 1 if in stereo)           #
 %C%#                                                           #
 %C%#############################################################
 %C%
@@ -121,11 +125,12 @@
 %W%#	 *    4: Speakers (Realtek High Definiti   (channels 0 & 1)
 %W%#	      5: Realtek Digital Output (Realtek
 %W%#
-%W%# Example: To use the microphone and speaker connections on the
-%W%# system board, either of these forms can be used:
+%W%# It is recommended that you use a unique substring of the device description.
+%W%# For example, use "High" or "Realtek High Def" for the built in sound system.
+%W%# Use "USB", or a longer string to distinguish amount multiple devices for a USB audio.
+%W%# You can also use numbers but you are asking for trouble.  Device numbers can change.
 %W%
-%W%#ADEVICE High
-%W%#ADEVICE  3 4
+%W%#ADEVICE USB
 %W%
 %W%
 %W%# Example: To use the USB Audio, use a command like this with
@@ -158,16 +163,6 @@
 %L%
 %L%# ADEVICE  plughw:1,0
 %L%
-%L%# You can also use "-" or "stdin" to pipe stdout from
-%L%# some other application such as a software defined radio.
-%L%# "stdin" is not an audio device.  Don't use this unless you
-%L%# understand what this means.  Read the User Guide.
-%L%# You can also specify "UDP:" and an optional port for input.
-%L%# Something different must be specified for output.
-%L%
-%L%# ADEVICE stdin plughw:1,0
-%L%# ADEVICE UDP:7355 default
-%L%
 %R% ---------- Mac ----------
 %R%
 %M%# Macintosh Operating System uses portaudio driver for audio
@@ -182,44 +177,9 @@
 %M%
 %M%# ADEVICE  "USB Audio Codec:6"  "USB Audio Codec:5"
 %M%#
-%M%#
-%M%# You can also use "-" or "stdin" to pipe stdout from
-%M%# some other application such as a software defined radio.
-%M%# "stdin" is not an audio device.  Don't use this unless you
-%M%# understand what this means.  Read the User Guide.
-%M%# You can also specify "UDP:" and an optional port for input.
-%M%# Something different must be specified for output.
 %M%
-%M%# ADEVICE UDP:7355 default
-%M%#
-%C%
-%C%#
-%C%# Number of audio channels for this souncard:  1 (mono) or 2 (stereo).
-%C%# 1 is the default so there is no need to specify it.
-%C%#
-%C%
-%C%#ACHANNELS 2
-%C%
-%C%
-%C%#############################################################
-%C%#                                                           #
-%C%#               SECOND AUDIO DEVICE PROPERTIES              #
-%C%#               (Channel 2 + 3 if in stereo)                #
-%C%#                                                           #
-%C%#############################################################
-%C%
-%C%#ADEVICE1  ...
-%C%
-%C%
-%C%#############################################################
-%C%#                                                           #
-%C%#               THIRD AUDIO DEVICE PROPERTIES               #
-%C%#               (Channel 4 + 5 if in stereo)                #
-%C%#                                                           #
-%C%#############################################################
-%C%
-%C%#ADEVICE2  ...
-%C%
+%C%# Many more details and examples can be found in: 
+%C%# https://github.com/wb2osz/direwolf-doc/blob/main/Radio-Interface-Guide.pdf
 %C%
 %C%#############################################################
 %C%#                                                           #
@@ -230,11 +190,6 @@
 %C%CHANNEL 0
 %C%
 %C%#
-%C%# The following MYCALL, MODEM, PTT, etc. configuration items
-%C%# apply to the most recent CHANNEL.
-%C%#
-%C%
-%C%#
 %C%# Station identifier for this channel.
 %C%# Multiple channels can have the same or different names.
 %C%#
@@ -259,7 +214,7 @@
 %C%# In most cases you can just specify the speed.  Examples:
 %C%#
 %C%
-%C%MODEM 1200
+%C%#MODEM 300
 %C%#MODEM 9600
 %C%
 %C%#
@@ -267,12 +222,6 @@
 %C%# See User Guide for details.
 %C%#
 %C%
-%C%#
-%C%# Uncomment line below to enable the DTMF decoder for this channel.
-%C%#
-%C%
-%C%#DTMF
-%C%
 %C%# Push to Talk (PTT) can be confusing because there are so many different cases.
 %C%# https://github.com/wb2osz/direwolf-doc/blob/main/Radio-Interface-Guide.pdf
 %C%# goes into detail about the various options.
@@ -291,59 +240,10 @@
 %W%
 %W%#PTT CM108
 %W%%C%#
-%C%# The transmitter Push to Talk (PTT) control can be wired to a serial port
-%C%# with a suitable interface circuit.  DON'T connect it directly!
-%C%#
-%C%# For the PTT command, specify the device and either RTS or DTR.
-%C%# RTS or DTR may be preceded by "-" to invert the signal.
-%C%# Both can be used for interfaces that want them driven with opposite polarity.
-%C%#
-%L%# COM1 can be used instead of /dev/ttyS0, COM2 for /dev/ttyS1, and so on.
-%L%#
-%C%
-%C%#PTT COM1 RTS
-%C%#PTT COM1 RTS -DTR
-%L%#PTT /dev/ttyUSB0 RTS
-%L%#PTT /dev/ttyUSB0 RTS -DTR
-%C%
-%L%#
-%L%# On Linux, you can also use general purpose I/O pins if
-%L%# your system is configured for user access to them.
-%L%# This would apply mostly to microprocessor boards, not a regular PC.
-%L%# See separate Raspberry Pi document for more details.
-%L%# The number may be preceded by "-" to invert the signal.
-%L%#
-%L%
-%L%#PTT GPIO 25
-%L%
-%C%# The Data Carrier Detect (DCD) signal can be sent to most of the same places
-%C%# as the PTT signal.  This could be used to light up an LED like a normal TNC.
-%C%
-%C%#DCD COM1 -DTR
-%L%#DCD GPIO 24
-%C%
-%C%
-%C%#############################################################
-%C%#                                                           #
-%C%#               CHANNEL 1 PROPERTIES                        #
-%C%#                                                           #
-%C%#############################################################
 %C%
-%C%#CHANNEL 1
-%C%
-%C%#
-%C%# Specify MYCALL, MODEM, PTT, etc. configuration items for
-%C%# CHANNEL 1.   Repeat for any other channels.
-%C%
-%C%
-%C%#############################################################
-%C%#                                                           #
-%C%#               TEXT TO SPEECH COMMAND FILE                 #
-%C%#                                                           #
-%C%#############################################################
-%C%
-%W%#SPEECH dwespeak.bat
-%L%#SPEECH dwespeak.sh
+%C%# There are other possibilities such as serial port RTS, Raspberry Pi GPIO pins,
+%C%# and hamlib for CAT control.   For more details see:
+%C%# https://github.com/wb2osz/direwolf-doc/blob/main/Radio-Interface-Guide.pdf
 %C%
 %C%
 %C%#############################################################
@@ -361,38 +261,6 @@
 %W%#	- KISS TNC via serial port
 %L%#	- KISS TNC via pseudo terminal   (-p command line option)
 %C%#
-%C%
-%C%AGWPORT 8000
-%C%KISSPORT 8001
-%C%
-%W%#
-%W%# Some applications are designed to operate with only a physical
-%W%# TNC attached to a serial port.  For these, we provide a virtual serial
-%W%# port that appears to be connected to a TNC.
-%W%#
-%W%# Take a look at the User Guide for instructions to set up
-%W%# two virtual serial ports named COM3 and COM4 connected by
-%W%# a null modem.
-%W%#
-%W%# Using the  configuration described, Dire Wolf will connect to
-%W%# COM3 and the client application will use COM4.
-%W%#
-%W%# Uncomment following line to use this feature.
-%W%
-%W%#NULLMODEM COM3
-%W%
-%W%
-%C%#
-%C%# It is sometimes possible to recover frames with a bad FCS.
-%C%# This is not a global setting.
-%C%# It applies only the the most recent CHANNEL specified.
-%C%#
-%C%#	0 - Don't try to repair.  (default)
-%C%#	1 - Attempt to fix single bit error.
-%C%#
-%C%
-%C%#FIX_BITS 0
-%C%
 %C%#
 %C%#############################################################
 %C%#                                                           #
@@ -410,16 +278,10 @@
 %C%# Each has a series of keywords and values for options.
 %C%# See User Guide for details.
 %C%#
-%C%# Example:
-%C%#
-%C%# This results in a broadcast once every 10 minutes.
-%C%# Every half hour, it can travel via one digipeater hop.
-%C%# The others are kept local.
+%C%# Example:  PLEASE change the latitude and longitude.  
 %C%#
 %C%
-%C%#PBEACON delay=1  every=30 overlay=S symbol="digi" lat=42^37.14N long=071^20.83W power=50 height=20 gain=4 comment="Chelmsford MA" via=WIDE1-1
-%C%#PBEACON delay=11 every=30 overlay=S symbol="digi" lat=42^37.14N long=071^20.83W power=50 height=20 gain=4 comment="Chelmsford MA"
-%C%#PBEACON delay=21 every=30 overlay=S symbol="digi" lat=42^37.14N long=071^20.83W power=50 height=20 gain=4 comment="Chelmsford MA"
+%C%#PBEACON overlay=S symbol="digi" lat=42^37.14N long=071^20.83W power=50 height=20 gain=4 comment="Chelmsford MA"
 %C%
 %C%#
 %C%# Did you know that APRS comments and messages can contain UTF-8 characters, not only plain ASCII?
@@ -428,29 +290,6 @@
 %C%#PBEACON delay=11 every=30 overlay=S symbol="digi" lat=42^37.14N long=071^20.83W comment=" Did you know that APRS comments and messages can contain UTF-8 characters?   \xce\xa1\xce\xb1\xce\xb4\xce\xb9\xce\xbf\xce\xb5\xcf\x81\xce\xb1\xcf\x83\xce\xb9\xcf\x84\xce\xb5\xcf\x87\xce\xbd\xce\xb9\xcf\x83\xce\xbc\xcf\x8c\xcf\x82"
 %C%#PBEACON delay=21 every=30 overlay=S symbol="digi" lat=42^37.14N long=071^20.83W comment=" Did you know that APRS comments and messages can contain UTF-8 characters?   \xe3\x82\xa2\xe3\x83\x9e\xe3\x83\x81\xe3\x83\xa5\xe3\x82\xa2\xe7\x84\xa1\xe7\xb7\x9a"
 %C%#
-%C%# With UTM coordinates instead of latitude and longitude.
-%C%
-%C%#PBEACON delay=1 every=10 overlay=S symbol="digi" zone=19T easting=307477 northing=4720178
-%C%
-%C%
-%C%#
-%C%# When the destination field is set to "SPEECH" the information part is
-%C%# converted to speech rather than transmitted as a data frame.
-%C%#
-%C%
-%C%#CBEACON dest="SPEECH" info="Club meeting tonight at 7 pm."
-%C%
-%C%# Similar for Morse code.  If SSID is specified, it is multiplied
-%C%# by 2 to get speed in words per minute (WPM).
-%C%
-%C%#CBEACON dest="MORSE-6" info="de MYCALL"
-%C%
-%C%
-%C%#
-%C%# Modify for your particular situation before removing
-%C%# the # comment character from the beginning of appropriate lines above.
-%C%#
-%C%
 %C%
 %C%#############################################################
 %C%#                                                           #
@@ -497,29 +336,12 @@
 %C%# That's all you need for a receive only IGate which relays
 %C%# messages from the local radio channel to the global servers.
 %C%
-%C%# Some might want to send an IGate client position directly to a server
-%C%# without sending it over the air and relying on someone else to
-%C%# forward it to an IGate server.  This is done by using sendto=IG rather
-%C%# than a radio channel number. Overlay R for receive only, T for two way.
-%C%# There is no need to send it as often as you would over the radio.
-%C%
-%C%#PBEACON sendto=IG delay=0:30 every=60:00 symbol="igate" overlay=R lat=42^37.14N long=071^20.83W
-%C%#PBEACON sendto=IG delay=0:30 every=60:00 symbol="igate" overlay=T lat=42^37.14N long=071^20.83W
-%C%
-%C%
-%C%# To relay messages from the Internet to radio, you need to add
+%C%# To relay APRS "messages" from the Internet to radio, you need to add
 %C%# one more option with the transmit channel number and a VIA path.
 %C%
 %C%#IGTXVIA 0 WIDE1-1,WIDE2-1
 %C%
-%C%
-%C%# Finally, we don't want to flood the radio channel.
-%C%# The IGate function will limit the number of packets transmitted
-%C%# during 1 minute and 5 minute intervals.   If a limit would
-%C%# be exceeded, the packet is dropped and message is displayed in red.
-%C%# This might be low for APRS Thursday when there is abnormally high activity.
-%C%
-%C%IGTXLIMIT 6 10
+%C%# For more information see Successful-IGate-Operation.pdf.
 %C%
 %C%
 %C%#############################################################

From ea93fcd424e9320239f939172948ea9f0646795b Mon Sep 17 00:00:00 2001
From: wb2osz <wb2osz@comcast.net>
Date: Thu, 24 Apr 2025 13:32:07 -0400
Subject: [PATCH 60/67] udev rules location

---
 CMakeLists.txt      |  1 +
 conf/CMakeLists.txt | 11 ++++++++---
 2 files changed, 9 insertions(+), 3 deletions(-)

diff --git a/CMakeLists.txt b/CMakeLists.txt
index 58fcb09b..6fed6bda 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -210,6 +210,7 @@ if (C_CLANG OR C_GCC)
   # TODO:
   # Try error checking -fsanitize=bounds-strict -fsanitize=leak
   # Requires libubsan and liblsan, respectively.
+  # Maybe -fstack-protector-all, -fstack-check
 
   ###set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wall -Wextra -Wvla -ffast-math -ftree-vectorize -D_XOPEN_SOURCE=600 -D_DEFAULT_SOURCE ${EXTRA_FLAGS}")
   if(FREEBSD)
diff --git a/conf/CMakeLists.txt b/conf/CMakeLists.txt
index ffc809b3..9f45e15c 100644
--- a/conf/CMakeLists.txt
+++ b/conf/CMakeLists.txt
@@ -28,10 +28,15 @@ file(WRITE "${CMAKE_BINARY_DIR}/direwolf.conf" "${file_content}")
 # There are two locations.  The one in /etc/udev/rules.d is meant for local customization and
 # takes precedence for the same name.
 # https://sources.debian.org/src/direwolf/1.7+dfsg-2/debian/patches/lib-udev-rules/
-# says that we should use the /usr/lib/udev/rules.d location.
+# says that we should use the /usr/lib/udev/rules.d location when building a package.
+# TODO:  I think the proper solution is to select the location based on whether
+# the application installation location is /usr/local or /usr.
 if(LINUX)
-  #install(FILES "${CUSTOM_CONF_DIR}/99-direwolf-cmedia.rules" DESTINATION /etc/udev/rules.d/)
-  install(FILES "${CUSTOM_CONF_DIR}/99-direwolf-cmedia.rules" DESTINATION /usr/lib/udev/rules.d/)
+  if (CMAKE_INSTALL_PREFIX STREQUAL "/usr/local") 
+    install(FILES "${CUSTOM_CONF_DIR}/99-direwolf-cmedia.rules" DESTINATION /etc/udev/rules.d/)
+  else()
+    install(FILES "${CUSTOM_CONF_DIR}/99-direwolf-cmedia.rules" DESTINATION /usr/lib/udev/rules.d/)
+  endif()
 endif()
 
 install(FILES "${CMAKE_BINARY_DIR}/direwolf.conf" DESTINATION ${INSTALL_CONF_DIR})

From babf61f03f08975a0fd6657e5024f1525defe50d Mon Sep 17 00:00:00 2001
From: wb2osz <wb2osz@comcast.net>
Date: Thu, 24 Apr 2025 13:33:53 -0400
Subject: [PATCH 61/67] Issue 544: Longer audio input timeout value.

---
 src/audio_win.c | 5 +++--
 1 file changed, 3 insertions(+), 2 deletions(-)

diff --git a/src/audio_win.c b/src/audio_win.c
index a133648a..21c09ce7 100644
--- a/src/audio_win.c
+++ b/src/audio_win.c
@@ -1,4 +1,4 @@
-
+// FIXME:  Add longer input timeout and more retries
 //
 //    This file is part of Dire Wolf, an amateur radio packet TNC.
 //
@@ -792,7 +792,8 @@ int audio_get (int a)
 	       * Wait if nothing available.
 	       * Could use an event to wake up but this is adequate.
 	       */
-	      int timeout = 25;
+	      // Issue 544: change from 25 to 200.  That's 2 seconds total with current buff time.
+	      int timeout = 200;
 
 	      while (A->in_headp == NULL) {
 	        //SLEEP_MS (ONE_BUF_TIME / 5);

From 36a5dff0ea4dc41cb9af101c3a23390935399536 Mon Sep 17 00:00:00 2001
From: wb2osz <wb2osz@comcast.net>
Date: Thu, 24 Apr 2025 15:11:55 -0400
Subject: [PATCH 62/67] Better error message.

---
 src/cm108.c | 6 +++++-
 1 file changed, 5 insertions(+), 1 deletion(-)

diff --git a/src/cm108.c b/src/cm108.c
index 787e7bb9..9ba8d968 100644
--- a/src/cm108.c
+++ b/src/cm108.c
@@ -1061,8 +1061,12 @@ static int cm108_write (char *name, int iomask, int iodata)
 	    dw_printf ("    crw-rw---- 1 root audio 247, 0 Oct  6 19:24 %s\n", name);
 	    dw_printf ("rather than root-only access like this:\n");
 	    dw_printf ("    crw------- 1 root root 247, 0 Sep 24 09:40 %s\n", name);
+	    dw_printf ("This permission should be set by one of:\n");
+	    dw_printf ("/etc/udev/rules.d/99-direwolf-cmedia.rules\n");
+	    dw_printf ("/usr/lib/udev/rules.d/99-direwolf-cmedia.rules\n");
+	    dw_printf ("which should be created by the installation process.\n");
+	    dw_printf ("Your account must be in the 'audio' group.\n");
 	  }
-
 	  close (fd);
 	  return (-1);
 	}

From 83c7de183ab2e2779e3b7a329cd072f3d4e25019 Mon Sep 17 00:00:00 2001
From: wb2osz <wb2osz@comcast.net>
Date: Thu, 24 Apr 2025 15:40:39 -0400
Subject: [PATCH 63/67] Improve error message.

---
 src/direwolf.c | 11 ++++++++++-
 1 file changed, 10 insertions(+), 1 deletion(-)

diff --git a/src/direwolf.c b/src/direwolf.c
index 2bffcc21..d98cf04d 100644
--- a/src/direwolf.c
+++ b/src/direwolf.c
@@ -339,6 +339,13 @@ int main (int argc, char *argv[])
 #endif
 
 
+// TODO: Display hardware and OS version to help with troubleshooting.
+// cat /proc/cpuinfo | grep ^Model
+// BSD, Deb?: /etc/os-release
+// /etc/issue
+
+
+
 /* 
  * Starting with version 0.9, the prebuilt Windows version 
  * requires a minimum of a Pentium 3 or equivalent so we can
@@ -1328,7 +1335,9 @@ void app_process_rec_packet (int chan, int subchan, int slice, packet_t pp, alev
 	if (alevel.rec > 110) {
 
 	  text_color_set(DW_COLOR_ERROR);
-	  dw_printf ("Audio input level is too high.  Reduce so most stations are around 50.\n");
+	  dw_printf ("Audio input level is too high. This may cause distortion and reduced decode performance.\n");
+	  dw_printf ("Solution is to decrease the audio input level.\n");
+	  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) {

From cc063c605460febef03110938a6e4d7ef78f4517 Mon Sep 17 00:00:00 2001
From: wb2osz <wb2osz@comcast.net>
Date: Thu, 24 Apr 2025 15:42:09 -0400
Subject: [PATCH 64/67] New comment.

---
 src/igate.c | 1 +
 1 file changed, 1 insertion(+)

diff --git a/src/igate.c b/src/igate.c
index b11f7a31..4809e575 100644
--- a/src/igate.c
+++ b/src/igate.c
@@ -998,6 +998,7 @@ void igate_send_rec_packet (int chan, packet_t recv_pp)
 
 /*
  * Do not relay generic query.
+ * TODO:  Should probably block in other direction too, in case rf>is gateway did not drop.
  */
 	if (ax25_get_dti(pp) == '?') {
 	  if (s_debug >= 1) {

From 1922b952aae22a36616666f86aef9548d52e9bc2 Mon Sep 17 00:00:00 2001
From: wb2osz <wb2osz@comcast.net>
Date: Thu, 24 Apr 2025 15:43:20 -0400
Subject: [PATCH 65/67] New comment.

---
 src/dwgpsd.c | 14 +++++++++++---
 1 file changed, 11 insertions(+), 3 deletions(-)

diff --git a/src/dwgpsd.c b/src/dwgpsd.c
index 1bc9d47a..623c6477 100644
--- a/src/dwgpsd.c
+++ b/src/dwgpsd.c
@@ -68,7 +68,7 @@
 // 3.22		28		11	bullseye		OK.
 // 3.23		29		12				OK.
 // 3.25		30		14				OK, Jan. 2023
-
+// 3.25.1	30		14	bookworm		TBD, Feb. 2025
 
 // Previously the compilation would fail if the API version was later
 // than the last one tested.  Now it is just a warning because it changes so
@@ -201,7 +201,14 @@ static void * read_gpsd_thread (void *arg);
  * 	scons prefix=/usr libdir=lib/aarch64-linux-gnu
  *	[ scons check ]
  *	sudo scons udev-install
- *	
+ *
+ *   Start and test
+ *
+ *	sudo killall gpsd
+ *	cat /dev/ttyACM0
+ *
+ *	sudo gpsd /dev/ttyACM0 -F /var/run/gpsd.sock
+ *	cgps	
  */
 
 
@@ -536,7 +543,8 @@ int main (int argc, char *argv[])
 	while (1) {
 	  dwfix_t fix;
 
-	  fix = dwgps_read (&info)
;
+	  fix = dwgps_read (&info)
+;
 	  text_color_set (DW_COLOR_INFO);
 	  switch (fix) {
 	    case DWFIX_2D:

From b0d7af43ad874c93b3b0f32ced8001eaf0ab2403 Mon Sep 17 00:00:00 2001
From: wb2osz <wb2osz@comcast.net>
Date: Thu, 24 Apr 2025 15:45:26 -0400
Subject: [PATCH 66/67] New comment.

---
 systemd/direwolf.service | 1 +
 1 file changed, 1 insertion(+)

diff --git a/systemd/direwolf.service b/systemd/direwolf.service
index c3380fac..8d0709f3 100644
--- a/systemd/direwolf.service
+++ b/systemd/direwolf.service
@@ -25,3 +25,4 @@ WantedBy=multi-user.target
 DefaultInstance=1
 
 # alternate version:  https://www.f4fxl.org/start-direwolf-at-boot-the-systemd-way/
+# or:  https://groups.io/g/direwolf/message/9883

From aeb5153403f6de92ef73521416a9ee19e2cb7dc2 Mon Sep 17 00:00:00 2001
From: wb2osz <wb2osz@comcast.net>
Date: Thu, 24 Apr 2025 17:01:52 -0400
Subject: [PATCH 67/67] Fix github action.

---
 .github/workflows/ci.yml | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 0cc4d34d..91e0971d 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -83,7 +83,7 @@ jobs:
 
     steps:
       - name: checkout
-        uses: actions/checkout@v2
+        uses: actions/checkout@v3
         with:
           fetch-depth: 8
       - name: dependency
@@ -149,7 +149,7 @@ jobs:
             make package
           fi
       - name: archive binary
-        uses: actions/upload-artifact@v2
+        uses: actions/upload-artifact@v3
         with:
           name: direwolf_${{ matrix.config.os }}_${{ matrix.config.arch }}_${{ github.sha }}
           path: |