From 81d11c5e93a86f472c460be0be9b7a2c94551aec Mon Sep 17 00:00:00 2001 From: Vladimir K Date: Sun, 5 Apr 2020 19:43:41 -0700 Subject: [PATCH 01/79] 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 +#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 b03a797ec4b1bd320586ebd98474adabe634be4c Mon Sep 17 00:00:00 2001 From: Martin Cooper Date: Wed, 15 Nov 2023 17:00:53 -0800 Subject: [PATCH 02/79] Add support for the use of CM108 for PTT on Mac Support for CM108-based PTT on Mac is provided using the hidapi library in the same way as on Windows. As such, the code changes are limited almost entirely to updated #if conditions, treating Windows and Mac in the same way. --- src/cm108.c | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/src/cm108.c b/src/cm108.c index ff3ff792..27e58875 100644 --- a/src/cm108.c +++ b/src/cm108.c @@ -138,6 +138,8 @@ int main (void) #if __WIN32__ #include #include "hidapi.h" +#elif __APPLE__ +#include "hidapi.h" #else #include #include @@ -240,7 +242,7 @@ static int cm108_write (char *name, int iomask, int iodata); // Used to process regular expression matching results. -#ifndef __WIN32__ +#if !defined(__WIN32__) && !defined(__APPLE__) static void substr_se (char *dest, const char *src, int start, int endp1) { @@ -317,7 +319,7 @@ static void usage(void) dw_printf ("Usage: cm108 [ device-path [ gpio-num ] ]\n"); dw_printf ("\n"); dw_printf ("With no command line arguments, this will produce a list of\n"); -#if __WIN32__ +#if __WIN32__ || __APPLE__ dw_printf ("Human Interface Devices (HID) and indicate which ones can be\n"); dw_printf ("used for GPIO PTT.\n"); #else @@ -375,11 +377,11 @@ int main (int argc, char **argv) num_things = cm108_inventory (things, MAXX_THINGS); -#if __WIN32__ +#if __WIN32__ || __APPLE__ -///////////////////////////////////////////////////// -// Windows - Remove the sound related columns for now. -///////////////////////////////////////////////////// +//////////////////////////////////////////////////////////// +// Windows & Mac - Remove the sound related columns for now. +//////////////////////////////////////////////////////////// dw_printf (" VID PID %-*s %-*s" "\n", (int)sizeof(things[0].product), "Product", @@ -539,7 +541,7 @@ int cm108_inventory (struct thing_s *things, int max_things) int num_things = 0; memset (things, 0, sizeof(struct thing_s) * max_things); -#if __WIN32__ +#if __WIN32__ || __APPLE__ struct hid_device_info *devs, *cur_dev; @@ -779,7 +781,7 @@ void cm108_find_ptt (char *output_audio_device, char *ptt_device, int ptt_devic // Possible improvement: Skip if inventory already taken. num_things = cm108_inventory (things, MAXX_THINGS); -#if __WIN32__ +#if __WIN32__ || __APPLE__ // FIXME - This is just a half baked implementation. // I have not been able to figure out how to find the connection // between the audio device and HID in the same package. @@ -934,7 +936,7 @@ int cm108_set_gpio_pin (char *name, int num, int state) static int cm108_write (char *name, int iomask, int iodata) { -#if __WIN32__ +#if __WIN32__ || __APPLE__ //text_color_set(DW_COLOR_DEBUG); //dw_printf ("TEMP DEBUG cm108_write: %s %d %d\n", name, iomask, iodata); From b8fdf013c53b2597776079d71e06d0664b583ddd Mon Sep 17 00:00:00 2001 From: Martin Cooper Date: Wed, 15 Nov 2023 17:02:32 -0800 Subject: [PATCH 03/79] Build changes for the use of CM108 for PTT on Mac The CMake changes are slightly complicated by the Windows build using a local copy of some hidapi files, for some reason, instead of using the hidapi library itself. The Mac version uses hidapi in the same way as other libraries. In the CMake files, it is unclear to me whether "elseif (NOT WIN32 AND NOT CYGWIN)" means the same thing as "elseif (APPLE)", so they are treated separately in order to avoid breaking other build types. --- CMakeLists.txt | 16 ++++++++++++- cmake/modules/Findhidapi.cmake | 44 ++++++++++++++++++++++++++++++++++ src/CMakeLists.txt | 22 +++++++++++++---- 3 files changed, 77 insertions(+), 5 deletions(-) create mode 100644 cmake/modules/Findhidapi.cmake diff --git a/CMakeLists.txt b/CMakeLists.txt index 84aeb738..20aa28f1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -342,6 +342,17 @@ elseif (HAVE_SNDIO) set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -DUSE_SNDIO") endif() +elseif (APPLE) + find_package(Portaudio REQUIRED) + if(PORTAUDIO_FOUND) + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -DUSE_PORTAUDIO") + endif() + + find_package(hidapi REQUIRED) + if(HIDAPI_FOUND) + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -DUSE_CM108") + endif() + elseif (NOT WIN32 AND NOT CYGWIN) find_package(Portaudio REQUIRED) if(PORTAUDIO_FOUND) @@ -367,7 +378,10 @@ add_subdirectory(data) # external libraries add_subdirectory(${CUSTOM_GEOTRANZ_DIR}) add_subdirectory(${CUSTOM_REGEX_DIR}) -add_subdirectory(${CUSTOM_HIDAPI_DIR}) +if(NOT APPLE) + # Mac builds use the hidapi library, not custom local files + add_subdirectory(${CUSTOM_HIDAPI_DIR}) +endif() add_subdirectory(${CUSTOM_MISC_DIR}) # direwolf source code and utilities diff --git a/cmake/modules/Findhidapi.cmake b/cmake/modules/Findhidapi.cmake new file mode 100644 index 00000000..163c8c2a --- /dev/null +++ b/cmake/modules/Findhidapi.cmake @@ -0,0 +1,44 @@ +# - Try to find hidapi +# +# HIDAPI_FOUND - system has hidapi +# HIDAPI_LIBRARIES - location of the library for hidapi +# HIDAPI_INCLUDE_DIRS - location of the include files for hidapi + +set(HIDAPI_ROOT_DIR + "${HIDAPI_ROOT_DIR}" + CACHE + PATH + "Directory to search for hidapi") + +# no need to check pkg-config + +find_path(HIDAPI_INCLUDE_DIRS + NAMES + hidapi.h + PATHS + /usr/local/include + /usr/include + /opt/local/include + HINTS + ${HIDAPI_ROOT_DIR} + PATH_SUFFIXES + hidapi + ) + +find_library(HIDAPI_LIBRARIES + NAMES + hidapi + PATHS + /usr/local/lib + /usr/lib + /usr/lib64 + /opt/local/lib + HINTS + ${HIDAPI_ROOT_DIR} + ) + + +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(HIDAPI DEFAULT_MSG HIDAPI_INCLUDE_DIRS HIDAPI_LIBRARIES) + +mark_as_advanced(HIDAPI_INCLUDE_DIRS HIDAPI_LIBRARIES) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index a2c3963d..7a05ecce 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -10,15 +10,21 @@ include_directories( ${PORTAUDIO_INCLUDE_DIRS} ${SNDIO_INCLUDE_DIRS} ${CUSTOM_GEOTRANZ_DIR} - ${CUSTOM_HIDAPI_DIR} ) if(WIN32 OR CYGWIN) include_directories( + ${CUSTOM_HIDAPI_DIR} ${CUSTOM_REGEX_DIR} ) endif() +if(APPLE) + include_directories( + ${HIDAPI_INCLUDE_DIRS} + ) +endif() + # direwolf list(APPEND direwolf_SOURCES @@ -131,6 +137,7 @@ if(LINUX) else() # macOS freebsd list(APPEND direwolf_SOURCES audio_portaudio.c + cm108.c ) if(USE_MACOS_DNSSD) list(APPEND direwolf_SOURCES @@ -469,10 +476,11 @@ endif() # List USB audio adapters than can use GPIO for PTT. # Originally for Linux only (using udev). -# Version 1.7 adds it for Windows. Needs hidapi library. +# Version 1.7 adds it for Windows. Uses local hidapi code. +# Post-1.7 adds it for Mac. Uses hidapi library. # cm108 -if(UDEV_FOUND OR WIN32 OR CYGWIN) +if(UDEV_FOUND OR WIN32 OR CYGWIN OR HIDAPI_FOUND) list(APPEND cm108_SOURCES cm108.c textcolor.c @@ -496,6 +504,12 @@ if(UDEV_FOUND OR WIN32 OR CYGWIN) ) endif() + if (APPLE) + target_link_libraries(cm108 + ${HIDAPI_LIBRARIES} + ) + endif() + if (WIN32 OR CYGWIN) target_link_libraries(cm108 ${HIDAPI_LIBRARIES} @@ -568,6 +582,6 @@ install(TARGETS ttcalc DESTINATION ${INSTALL_BIN_DIR}) install(TARGETS kissutil DESTINATION ${INSTALL_BIN_DIR}) install(TARGETS tnctest DESTINATION ${INSTALL_BIN_DIR}) install(TARGETS appserver DESTINATION ${INSTALL_BIN_DIR}) -if(UDEV_FOUND OR WIN32 OR CYGWIN) +if(UDEV_FOUND OR WIN32 OR CYGWIN OR HIDAPI_FOUND) install(TARGETS cm108 DESTINATION ${INSTALL_BIN_DIR}) endif() From c1d00a5eed55a01acc67e14bded6955a4ec51b57 Mon Sep 17 00:00:00 2001 From: Martin Cooper Date: Wed, 15 Nov 2023 17:03:22 -0800 Subject: [PATCH 04/79] Incorporate CM108 support for Mac The generic config file can now be simplified slightly, since the section on using CM108 for PTT is now common to all of Linux, Windows and Mac. --- conf/generic.conf | 22 ++++++++-------------- 1 file changed, 8 insertions(+), 14 deletions(-) diff --git a/conf/generic.conf b/conf/generic.conf index 9a19d8a2..41d50fe1 100644 --- a/conf/generic.conf +++ b/conf/generic.conf @@ -277,20 +277,14 @@ %C%# Radio-Interface-Guide.pdf in https://github.com/wb2osz/direwolf-doc %C%# goes into detail about the various options. %C% -%L%# If using a C-Media CM108/CM119 or similar USB Audio Adapter, -%L%# you can use a GPIO pin for PTT control. This is very convenient -%L%# because a single USB connection is used for both audio and PTT. -%L%# Example: -%L% -%L%#PTT CM108 -%L% -%W%# If using a C-Media CM108/CM119 or similar USB Audio Adapter, -%W%# you can use a GPIO pin for PTT control. This is very convenient -%W%# because a single USB connection is used for both audio and PTT. -%W%# Example: -%W% -%W%#PTT CM108 -%W%%C%# +%C%# If using a C-Media CM108/CM119 or similar USB Audio Adapter, +%C%# you can use a GPIO pin for PTT control. This is very convenient +%C%# because a single USB connection is used for both audio and PTT. +%C%# Example: +%C% +%C%#PTT CM108 +%C% +%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%# From a9c6adc79ec9a0ed507d02f698928ade46f96ff0 Mon Sep 17 00:00:00 2001 From: Martin Cooper Date: Wed, 15 Nov 2023 17:04:05 -0800 Subject: [PATCH 05/79] Include Mac build information in README Since building on Mac using Homebrew is straightforward, include this information in a new section of the README. --- README.md | 36 +++++++++++++++++++++++++++++++++--- 1 file changed, 33 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 3006a1ef..f8de5897 100644 --- a/README.md +++ b/README.md @@ -208,13 +208,43 @@ Results will vary depending on your hardware platform and operating system versi sudo yum install direwolf -### Macintosh OS X ### +### Macintosh macOS - Using Homebrew ### -Read the **User Guide** in the [**doc** directory](https://github.com/wb2osz/direwolf/tree/master/doc). It is more complicated than Linux. +The following instructions have been verified on macOS Ventura 13.6 (M2) and macOS High Sierra 10.13.6 (Intel). + +First make sure that you have the following tools installed on your Mac: + +- [Xcode or Xcode Command Line Tools](https://developer.apple.com/xcode/resources/) +- [Homebrew](https://brew.sh/) + +You will need to install the following packages using Homebrew: + + brew install cmake + brew install portaudio + brew install hidapi + +Then follow the same instructions as above for the Linux `git clone` build: + + cd ~ + git clone https://www.github.com/wb2osz/direwolf + cd direwolf + git checkout dev + mkdir build && cd build + cmake .. + make -j4 + sudo make install + make install-conf + +This gives you the latest development version. Leave out the "git checkout dev" to get the most recent stable release. + +For more information, see the ***User Guide*** in the [**doc** directory](https://github.com/wb2osz/direwolf/tree/master/doc). If you have problems, post them to the [Dire Wolf packet TNC](https://groups.io/g/direwolf) discussion group. -You can also install a pre-built version from Mac Ports. Keeping this up to date depends on volunteers who perform the packaging. This version could lag behind development. + +### Macintosh macOS - Prebuilt version ### + +You can also install a pre-built version from MacPorts. Keeping this up to date depends on volunteers who perform the packaging. This version could lag behind development. sudo port install direwolf From 12abb8d91e4429d342e4057484bcdb03a23cc970 Mon Sep 17 00:00:00 2001 From: wb2osz Date: Wed, 22 Nov 2023 21:29:05 +0000 Subject: [PATCH 06/79] 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 Date: Wed, 22 Nov 2023 21:34:41 +0000 Subject: [PATCH 07/79] 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; channelmaxframe_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 e18a9289a8911e833ff15a97c83e5f778ef0650f Mon Sep 17 00:00:00 2001 From: Martin Cooper Date: Fri, 24 Nov 2023 13:29:37 -0800 Subject: [PATCH 08/79] Include hidapi as a dependency for CI --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0cc4d34d..fc9ba538 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -96,7 +96,7 @@ jobs: elif [ "$RUNNER_OS" == "macOS" ]; then # just to simplify I use homebrew but # we can use macports (latest direwolf is already available as port) - brew install portaudio hamlib gpsd + brew install portaudio hamlib gpsd hidapi elif [ "$RUNNER_OS" == "Windows" ]; then # add the folder to PATH echo "C:\msys64\mingw32\bin" >> $GITHUB_PATH From 6192661f3df331c7abffe921b62db80994e0930e Mon Sep 17 00:00:00 2001 From: wb2osz Date: Sat, 25 Nov 2023 15:32:04 +0000 Subject: [PATCH 09/79] 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 Date: Sun, 26 Nov 2023 01:12:34 +0000 Subject: [PATCH 10/79] 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 Date: Sun, 26 Nov 2023 01:29:13 +0000 Subject: [PATCH 11/79] 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 Date: Thu, 21 Dec 2023 23:01:05 +0000 Subject: [PATCH 12/79] 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 Date: Thu, 21 Dec 2023 23:15:21 +0000 Subject: [PATCH 13/79] 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 Date: Sat, 23 Dec 2023 15:57:03 +0000 Subject: [PATCH 14/79] 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 @@ - -APRS TO-CALL VERSION NUMBERS 14 Dec 2021 ---------------------------------------------------------------------- - WB4APR - - -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 - - - - -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. - - - - - 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 - - - - - - -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: - - - - SATERN - Salvation Army Altnet - AFMARS - Airforce Mars - AMARS - Army Mars - \ 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 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 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 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 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 '%s'\n", n, tocalls[n].len, tocalls[n].prefix, tocalls[n].description); - //} - } - - - for (n=0; ng_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 . +// + + +/*------------------------------------------------------------------ + * + * 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 +#include +#include +#include + +#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 Date: Fri, 26 Jan 2024 00:06:04 +0000 Subject: [PATCH 15/79] 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 Date: Tue, 30 Jan 2024 17:19:46 +0000 Subject: [PATCH 16/79] 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 Date: Wed, 31 Jan 2024 23:50:00 +0000 Subject: [PATCH 17/79] 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 Date: Sun, 4 Feb 2024 22:40:40 +0000 Subject: [PATCH 18/79] 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 Date: Fri, 16 Feb 2024 02:52:28 +0000 Subject: [PATCH 19/79] 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 Date: Tue, 5 Mar 2024 08:58:50 -0300 Subject: [PATCH 20/79] 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 Date: Tue, 5 Mar 2024 09:01:48 -0300 Subject: [PATCH 21/79] 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 Date: Tue, 5 Mar 2024 09:07:45 -0300 Subject: [PATCH 22/79] 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 Date: Tue, 5 Mar 2024 09:18:16 -0300 Subject: [PATCH 23/79] 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 Date: Tue, 5 Mar 2024 14:13:53 -0300 Subject: [PATCH 24/79] 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 Date: Tue, 5 Mar 2024 14:18:29 -0300 Subject: [PATCH 25/79] 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 Date: Wed, 6 Mar 2024 09:39:20 -0300 Subject: [PATCH 26/79] 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 Date: Wed, 6 Mar 2024 09:40:16 -0300 Subject: [PATCH 27/79] 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 Date: Wed, 6 Mar 2024 09:40:45 -0300 Subject: [PATCH 28/79] 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 Date: Wed, 6 Mar 2024 09:41:05 -0300 Subject: [PATCH 29/79] 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 Date: Wed, 6 Mar 2024 09:43:44 -0300 Subject: [PATCH 30/79] 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 Date: Wed, 6 Mar 2024 09:43:59 -0300 Subject: [PATCH 31/79] 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 Date: Sat, 9 Mar 2024 18:50:58 +0000 Subject: [PATCH 32/79] 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 Date: Wed, 13 Mar 2024 01:09:28 +0100 Subject: [PATCH 33/79] 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 34/79] 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 Date: Mon, 6 May 2024 19:17:42 +0100 Subject: [PATCH 35/79] 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 Date: Wed, 15 May 2024 20:12:55 +0100 Subject: [PATCH 36/79] 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 Date: Sun, 16 Jun 2024 01:30:57 +0100 Subject: [PATCH 37/79] 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 Date: Fri, 19 Jul 2024 00:37:38 +0100 Subject: [PATCH 38/79] 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= 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; channeladev[a].bits_per_sample == 0) pa->adev[a].bits_per_sample = DEFAULT_BITS_PER_SAMPLE; - for (chan=0; chanachan[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 #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 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_chanenabled[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_chanenabled[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; channelchan_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; ienabled[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; jchan_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_chanenabled[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_chanenabled[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_chanregen[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= 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; cn = 0; for (j=0; j= MAX_RADIO_CHANS) { + return ('$'); + } + D = &(dd[c]); for (i=0; i 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 // 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; chanchan_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; chchan_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; chchan_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; jchan_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; jchan_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] \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] \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; cchan_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= 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; jachan[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; jchan_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 Date: Fri, 19 Jul 2024 00:41:12 +0100 Subject: [PATCH 39/79] 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 . +// + + + +/*------------------------------------------------------------------ + * + * Module: nettnc.c + * + * Purpose: Attach to Network KISS TNC(s) for NCHANNEL config file item(s). + * + * Description: Called once at application start up. + * + *---------------------------------------------------------------*/ + + +#include "direwolf.h" // Sets _WIN32_WINNT for XP API level needed by ws2tcpip.h + +#if __WIN32__ +#include +#include // _WIN32_WINNT must be set to 0x0501 before including this +#else +#include +#include +#include +#include +#include +#include +#endif + +#include +#include +#include +#include +#include +#include + +#include "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 Date: Fri, 19 Jul 2024 00:44:20 +0100 Subject: [PATCH 40/79] 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 #include #include +#include #include "textcolor.h" #include "audio.h" // configuration. From 1033f8a7bfa7d8164f734f5adc541193fba6a199 Mon Sep 17 00:00:00 2001 From: wb2osz Date: Fri, 19 Jul 2024 00:48:08 +0100 Subject: [PATCH 41/79] 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 Date: Fri, 19 Jul 2024 01:15:30 +0100 Subject: [PATCH 42/79] 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 Date: Fri, 13 Sep 2024 16:13:21 +0100 Subject: [PATCH 43/79] 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 Date: Fri, 13 Sep 2024 17:51:01 +0100 Subject: [PATCH 44/79] 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 Date: Fri, 13 Sep 2024 18:15:18 +0100 Subject: [PATCH 45/79] 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 Date: Sat, 21 Sep 2024 19:12:03 +0100 Subject: [PATCH 46/79] 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 Date: Sat, 28 Sep 2024 01:58:16 +0100 Subject: [PATCH 47/79] 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 Date: Fri, 18 Oct 2024 17:41:55 +0100 Subject: [PATCH 48/79] 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 Date: Fri, 18 Oct 2024 23:45:13 +0100 Subject: [PATCH 49/79] 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; channelchan_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; channelachan[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 Date: Fri, 18 Oct 2024 23:46:42 +0100 Subject: [PATCH 50/79] 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) { 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 Date: Fri, 18 Oct 2024 23:52:24 +0100 Subject: [PATCH 51/79] 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 #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 Date: Sat, 19 Oct 2024 00:16:26 +0100 Subject: [PATCH 52/79] 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 Date: Sat, 19 Oct 2024 00:25:36 +0100 Subject: [PATCH 53/79] 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 @@ -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 Date: Sat, 19 Oct 2024 00:41:40 +0100 Subject: [PATCH 54/79] 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 Date: Sat, 19 Oct 2024 02:17:35 +0100 Subject: [PATCH 55/79] 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 Date: Sat, 19 Oct 2024 02:32:47 +0100 Subject: [PATCH 56/79] 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 Date: Tue, 22 Oct 2024 12:29:39 +0100 Subject: [PATCH 57/79] 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 Date: Tue, 22 Oct 2024 13:58:55 +0100 Subject: [PATCH 58/79] 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 Date: Tue, 22 Oct 2024 15:43:41 +0100 Subject: [PATCH 59/79] 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 Date: Tue, 22 Oct 2024 16:41:15 +0100 Subject: [PATCH 60/79] 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 Date: Tue, 22 Oct 2024 23:38:38 +0100 Subject: [PATCH 61/79] 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 Date: Tue, 29 Oct 2024 19:41:35 +0100 Subject: [PATCH 62/79] 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 Date: Tue, 26 Nov 2024 21:42:17 +0000 Subject: [PATCH 63/79] 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 Date: Thu, 24 Apr 2025 13:27:31 -0400 Subject: [PATCH 64/79] 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 Date: Thu, 24 Apr 2025 13:32:07 -0400 Subject: [PATCH 65/79] 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 Date: Thu, 24 Apr 2025 13:33:53 -0400 Subject: [PATCH 66/79] 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 Date: Thu, 24 Apr 2025 15:11:55 -0400 Subject: [PATCH 67/79] 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 Date: Thu, 24 Apr 2025 15:40:39 -0400 Subject: [PATCH 68/79] 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 Date: Thu, 24 Apr 2025 15:42:09 -0400 Subject: [PATCH 69/79] 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 Date: Thu, 24 Apr 2025 15:43:20 -0400 Subject: [PATCH 70/79] 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 Date: Thu, 24 Apr 2025 15:45:26 -0400 Subject: [PATCH 71/79] 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 Date: Thu, 24 Apr 2025 17:01:52 -0400 Subject: [PATCH 72/79] 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: | From 873b171e48d17a199d39c0b583a83d3bbf90b6d7 Mon Sep 17 00:00:00 2001 From: wb2osz Date: Tue, 29 Apr 2025 15:00:30 -0400 Subject: [PATCH 73/79] Pull Request 481 - Replace direwolf icon --- CHANGES.md | 341 +++++++++++++++------------------- cmake/cpack/direwolf_icon.ico | Bin 370070 -> 44764 bytes cmake/cpack/direwolf_icon.png | Bin 24142 -> 47932 bytes 3 files changed, 149 insertions(+), 192 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 69a1a857..528c7fb3 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,20 +1,20 @@ - -# Revision History # - +# Revision History ## Version 1.8 -- Development Version -### New Features: ### +### New Features: +- Support for CM108 PTT on Mac. - 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 ## +- New direwolf icon. +## Version 1.7 -- October 2023 -### New Documentation: ### +### New Documentation: Additional documentation location to slow down growth of main repository. [https://github.com/wb2osz/direwolf-doc](https://github.com/wb2osz/direwolf-doc) . These are more oriented toward achieving a goal and understanding, as opposed to the User Guide which describes the functionality. @@ -28,20 +28,17 @@ Additional documentation location to slow down growth of main repository. [http - ***Understanding APRS Packets*** - -### New Features: ### - - +### New Features: - New ICHANNEL configuration option to map a KISS client application channel to APRS-IS. Packets from APRS-IS will be presented to client applications as the specified channel. Packets sent, by client applications, to that channel will go to APRS-IS rather than a radio channel. Details in ***Internal-Packet-Routing.pdf***. - New variable speed option for gen_packets. For example, "-v 5,0.1" would generate packets from 5% too slow to 5% too fast with increments of 0.1. Some implementations might have imprecise timing. Use this to test how well TNCs tolerate sloppy timing. - Improved Layer 2 Protocol [(IL2P)](https://en.wikipedia.org/wiki/FX.25_Forward_Error_Correction). Compatible with Nino TNC for 1200 and 9600 bps. Use "-I 1" on command line to enable transmit for first channel. For more general case, add to config file (simplified version, see User Guide for more details): - - > After: "CHANNEL 1" (or other channel) - > - > Add: "IL2PTX 1" + + > After: "CHANNEL 1" (or other channel) + > + > Add: "IL2PTX 1" - Limited support for CM108/CM119 GPIO PTT on Windows. @@ -52,14 +49,12 @@ Additional documentation location to slow down growth of main repository. [http - The BEACON configuration now recognizes the SOURCE= option. This replaces the AX.25 source address rather than using the MYCALL value for the channel. This is useful for sending more than 5 analog telemetry channels. Use two, or more, source addresses with up to 5 analog channels each. - For more flexibility, the FX.25 transmit property can now be set individually by channel, rather than having a global setting for all channels. The -X on the command line applies only to channel 0. For other channels you need to add a new line to the configuration file. You can specify a specific number of parity bytes (16, 32, 64) or 1 to choose automatically based on packet size. + + > After: "CHANNEL 1" (or other channel) + > + > Add: "FX25TX 1" (or 16 or 32 or 64) - > After: "CHANNEL 1" (or other channel) - > - > Add: "FX25TX 1" (or 16 or 32 or 64) - - - -### Bugs Fixed: ### +### Bugs Fixed: - The t/m packet filter incorrectly included bulletins. It now allows only "messages" to specific stations. Use of t/m is discouraged. i/180 is the preferred filter for messages to users recently heard locally. @@ -67,24 +62,21 @@ Additional documentation location to slow down growth of main repository. [http - Fixed build for Alpine Linux. -### Notes: ### +### Notes: The Windows binary distribution now uses gcc (MinGW) version 11.3.0. The Windows version is built for both 32 and 64 bit operating systems. Use the 64 bit version if possible; it runs considerably faster. -## Version 1.6 -- October 2020 ## - -### New Build Procedure: ### +## Version 1.6 -- October 2020 +### New Build Procedure: - Rather than trying to keep a bunch of different platform specific Makefiles in sync, "cmake" is now used for greater portability and easier maintenance. This was contributed by Davide Gerhard. - README.md has a quick summary of the process. More details in the ***User Guide***. - -### New Features: ### - +### New Features: - "-X" option enables FX.25 transmission. FX.25 reception is always enabled so you don't need to do anything special. "What is FX.25?" you might ask. It is forward error correction (FEC) added in a way that is completely compatible with an ordinary AX.25 frame. See new document ***AX25\_plus\_FEC\_equals\_FX25.pdf*** for details. @@ -94,7 +86,6 @@ Use the 64 bit version if possible; it runs considerably faster. - "-t" option now accepts more values to accommodate inconsistent handling of text color control codes by different terminal emulators. The default, 1, should work with most modern terminal types. If the colors are not right, try "-t 9" to see the result of the different choices and pick the best one. If none of them look right, file a bug report and specify: operating system version (e.g. Raspbian Buster), terminal emulator type and version (e.g. LXTerminal 0.3.2). Include a screen capture. - - "-g" option to force G3RUH mode for lower speeds where a different modem type may be the default. - 2400 bps compatibility with MFJ-2400. See ***2400-4800-PSK-for-APRS-Packet-Radio.pdf*** for details @@ -103,15 +94,11 @@ Use the 64 bit version if possible; it runs considerably faster. - Add support for Multi-GNSS NMEA sentences. - - -### Bugs Fixed: ### +### Bugs Fixed: - Proper counting of frames in transmit queue for AGW protocol 'Y' command. - - -### New Documentation: ### +### New Documentation: - ***AX.25 + FEC = FX.25*** @@ -125,18 +112,15 @@ Use the 64 bit version if possible; it runs considerably faster. - [***Dire Wolf PowerPoint Slide Show***](https://github.com/wb2osz/direwolf-presentation) -### Notes: ### +### Notes: The Windows binary distribution now uses gcc (MinGW) version 7.4.0. The Windows version is built for both 32 and 64 bit operating systems. Use the 64 bit version if possible; it runs considerably faster. +## Version 1.5 -- September 2018 - -## Version 1.5 -- September 2018 ## - - -### New Features: ### +### New Features: - PTT using GPIO pin of CM108/CM119 (e.g. DMK URI, RB-USB RIM), Linux only. @@ -162,9 +146,7 @@ Use the 64 bit version if possible; it runs considerably faster. - Allow single log file with fixed name rather than starting a new one each day. - - -### Bugs Fixed: ### +### Bugs Fixed: - Possible crash when CDIGIPEAT did not include the optional alias. @@ -174,27 +156,23 @@ Use the 64 bit version if possible; it runs considerably faster. - Under certain conditions, outgoing connected mode data would get stuck in a queue and not be transmitted. This could happen if client application sends a burst of data larger than the "window" size (MAXFRAME or EMAXFRAME option). - - Little typographical / spelling errors in messages. - -### Documentation: ### - +### Documentation: - New document ***Bluetooth-KISS-TNC.pdf*** explaining how to use KISS over Bluetooth. - Updates describing cheap SDR frequency inaccuracy and how to compensate for it. -### Notes: ### +### Notes: Windows binary distribution now uses gcc (MinGW) version 6.3.0. ---------- -## Version 1.4 -- April 2017 ## +## Version 1.4 -- April 2017 - -### New Features: ### +### New Features: - AX.25 v2.2 connected mode. See chapter 10 of User Guide for details. @@ -205,6 +183,7 @@ Windows binary distribution now uses gcc (MinGW) version 6.3.0. - Expanded debug options so you can understand what is going on with packet filtering. - Added new document ***Successful-APRS-IGate-Operation.pdf*** with IGate background, configuration, and troubleshooting tips. + - 2400 & 4800 bps PSK modems. See ***2400-4800-PSK-for-APRS-Packet-Radio.pdf*** in the doc directory for discussion. - The top speed of 9600 bps has been increased to 38400. You will need a sound card capable of 96k or 192k samples per second for the higher rates. Radios must also have adequate bandwidth. See ***Going-beyond-9600-baud.pdf*** in the doc directory for more details. @@ -212,11 +191,11 @@ Windows binary distribution now uses gcc (MinGW) version 6.3.0. - Better decoder performance for 9600 and higher especially for low audio sample rate to baud ratios. - Generate waypoint sentences for use by AvMap G5 / G6 or other mapping devices or applications. Formats include - - $GPWPL - NMEA generic with only location and name. - - $PGRMW - Garmin, adds altitude, symbol, and comment to previously named waypoint. - - $PMGNWPL - Magellan, more complete for stationary objects. - - $PKWDWPL - Kenwood with APRS style symbol but missing comment. - + + - $GPWPL - NMEA generic with only location and name. + - $PGRMW - Garmin, adds altitude, symbol, and comment to previously named waypoint. + - $PMGNWPL - Magellan, more complete for stationary objects. + - $PKWDWPL - Kenwood with APRS style symbol but missing comment. - DTMF tones can be sent by putting "DTMF" in the destination address, similar to the way that Morse Code is sent. @@ -226,9 +205,7 @@ Windows binary distribution now uses gcc (MinGW) version 6.3.0. - More flexible dw-start.sh start up script for both GUI and CLI environments. - - -### Bugs Fixed: ### +### Bugs Fixed: - The transmitter (PTT control) was being turned off too soon when sending Morse Code. @@ -237,71 +214,64 @@ Windows binary distribution now uses gcc (MinGW) version 6.3.0. - Longer tocall.txt files can now be handled. - Sometimes kissattach would have an issue with the Dire Wolf pseudo terminal. This showed up most often on Raspbian but sometimes occurred with other versions of Linux. - - *kissattach: Error setting line discipline: TIOCSETD: Device or resource busy - Are you sure you have enabled MKISS support in the kernel - or, if you made it a module, that the module is loaded?* - + + *kissattach: Error setting line discipline: TIOCSETD: Device or resource busy + Are you sure you have enabled MKISS support in the kernel + or, if you made it a module, that the module is loaded?* - Sometimes writes to a pseudo terminal would block causing the received -frame processing thread to hang. The first thing you will notice is that -received frames are not being printed. After a while this message will appear: - - *Received frame queue is out of control. Length=... Reader thread is probably - frozen. This can be caused by using a pseudo terminal (direwolf -p) where - another application is not reading the frames from the other side.* + frame processing thread to hang. The first thing you will notice is that + received frames are not being printed. After a while this message will appear: + + *Received frame queue is out of control. Length=... Reader thread is probably + frozen. This can be caused by using a pseudo terminal (direwolf -p) where + another application is not reading the frames from the other side.* - -p command line option caused segmentation fault with glibc >= 2.24. - - The Windows version 1.3 would crash when starting to transmit on Windows XP. There have also been some other reports of erratic behavior on Windows. The crashing problem was fixed in in the 1.3.1 patch release. Linux version was not affected. - IGate did not retain nul characters in the information part of a packet. This should never happen with a valid APRS packet but there are a couple cases where it has. If we encounter these malformed packets, pass them along as-is, rather than truncating. - Don't digipeat packets when the source is my call. - - ---------- -## Version 1.3 -- May 2016 ## +## Version 1.3 -- May 2016 -### New Features: ### +### New Features: - Support for Mac OS X. - Many APRStt enhancements including: Morse code and speech responses to to APRStt tone sequences, new 5 digit callsign suffix abbreviation, -position ambiguity for latitude and longitude in object reports + position ambiguity for latitude and longitude in object reports - APRS Telemetry Toolkit. - + - GPS Tracker beacons are now available for the Windows version. Previously this was only in the Linux version. - SATgate mode for IGate. Packets heard directly are delayed before being sent -to the Internet Server. This favors digipeated packets because the original -arrives later and gets dropped if there are duplicates. + to the Internet Server. This favors digipeated packets because the original + arrives later and gets dropped if there are duplicates. - Added support for hamlib. This provides more flexible options for PTT control. - Implemented AGW network protocol 'M' message for sending UNPROTO information without digipeater path. - - A list of all symbols available can be obtained with the -S -command line option. + command line option. - Command line option "-a n" to print audio device statistics each n seconds. Previously this was always each 100 seconds on Linux and not available on Windows. -### Bugs Fixed: ### - - +### Bugs Fixed: - Fixed several cases where crashes were caused by unexpected packet contents: - - - When receiving packet with unexpected form of GPS NMEA sentence. - - - When receiving packet with comment of a few hundred characters. - - - Address in path, from Internet server, more than 9 characters. + + - When receiving packet with unexpected form of GPS NMEA sentence. + + - When receiving packet with comment of a few hundred characters. + + - Address in path, from Internet server, more than 9 characters. - "INTERNAL ERROR: dlq_append NULL packet pointer." when using PASSALL. @@ -310,27 +280,27 @@ command line option. - Tracker beacons were not always updating the location properly. - AGW network protocol now works properly for big-endian processors -such as PowerPC or MIPS. + such as PowerPC or MIPS. - Packet filtering treated telemetry metadata as messages rather than telemetry. ---------- -## Version 1.2 -- June 2015 ## +## Version 1.2 -- June 2015 -### New Features ### +### New Features - Improved decoder performance. -Over 1000 error-free frames decoded from WA8LMF TNC Test CD. -See ***A-Better-APRS-Packet-Demodulator-Part-1-1200-baud.pdf*** for details. + Over 1000 error-free frames decoded from WA8LMF TNC Test CD. + See ***A-Better-APRS-Packet-Demodulator-Part-1-1200-baud.pdf*** for details. - Up to 3 soundcards and 6 radio channels can be handled at the same time. - New framework for applications which listen for Touch Tone commands -and respond with voice. A sample calculator application is included -as a starting point for building more interesting applications. -For example, if it hears the DTMF sequence "2*3*4#" it will respond -with the spoken words "Twenty Four." + and respond with voice. A sample calculator application is included + as a starting point for building more interesting applications. + For example, if it hears the DTMF sequence "2*3*4#" it will respond + with the spoken words "Twenty Four." - Reduced latency for transfers to/from soundcards. @@ -343,146 +313,140 @@ with the spoken words "Twenty Four." - Attempted fixing of corrupted bits now works for 9600 baud. - Implemented AGW network protocol 'y' message so applications can -throttle generation of packets when sending a large file. + throttle generation of packets when sending a large file. - When using serial port RTS/DTR to activate transmitter, the two -control lines can now be driven with opposite polarity as required -by some interfaces. + control lines can now be driven with opposite polarity as required + by some interfaces. - Data Carrier Detect (DCD) can be sent to an output line (just -like PTT) to activate a carrier detect light. + like PTT) to activate a carrier detect light. - Linux "man" pages for on-line documentation. - AGWPORT and KISSPORT can be set to 0 to disable the interfaces. - APRStt gateway enhancements: MGRS/USNG coordinates, new APRStt3 -format call, satellite grid squares. - + format call, satellite grid squares. -### Bugs fixed ### +### Bugs fixed - Fixed "gen_packets" so it now handles user-specified messages correctly. - Under some circumstances PTT would be held on long after the transmit -audio was finished. - + audio was finished. - -### Known problems ### +### Known problems - Sometimes writes to a pseudo terminal will block causing the received -frame processing thread to hang. The first thing you will notice is that -received frames are not being printed. After a while this message will appear: - + frame processing thread to hang. The first thing you will notice is that + received frames are not being printed. After a while this message will appear: + Received frame queue is out of control. Length=... Reader thread is probably frozen. This can be caused by using a pseudo terminal (direwolf -p) where another application is not reading the frames from the other side. ----------- -## Version 1.1 -- December 2014 ## +## Version 1.1 -- December 2014 -### New Features ### +### New Features - Logging of received packets and utility to convert log file -into GPX format. + into GPX format. - AGW network port formerly allowed only one connection at a -time. It can now accept 3 client applications at the same time. -(Same has not yet been done for network KISS port.) + time. It can now accept 3 client applications at the same time. + (Same has not yet been done for network KISS port.) - Frequency / offset / tone standard formats are now recognized. -Non-standard attempts, in the comment, are often detected and -a message suggests the correct format. + Non-standard attempts, in the comment, are often detected and + a message suggests the correct format. - Telemetry is now recognized. Messages are printed for -usage that does not adhere to the published standard. + usage that does not adhere to the published standard. - Tracker function transmits location from GPS position. -New configuration file options: TBEACON and SMARTBEACONING. -(For Linux only. Warning - has not been well tested.) + New configuration file options: TBEACON and SMARTBEACONING. + (For Linux only. Warning - has not been well tested.) - Experimental packet regeneration feature for HF use. -Will be documented later if proves to be useful... + Will be documented later if proves to be useful... - Several enhancements for trying to fix incorrect CRC: -Additional types of attempts to fix a bad CRC. -Optimized code to reduce execution time. -Improved detection of duplicate packets from different fixup attempts. -Set limit on number of packets in fix up later queue. + Additional types of attempts to fix a bad CRC. + Optimized code to reduce execution time. + Improved detection of duplicate packets from different fixup attempts. + Set limit on number of packets in fix up later queue. - Beacon positions can be specified in either latitude / longitude -or UTM coordinates. + or UTM coordinates. - It is still highly recommended, but no longer mandatory, that -beaconing be enabled for digipeating to work. - + beaconing be enabled for digipeating to work. * Bugs fixed: - - For Windows version, maximum serial port was COM9. -It is now possible to use COM10 and higher. + It is now possible to use COM10 and higher. - Fixed issue with KISS protocol decoder state that showed up -only with "binary" data in packets (e.g. RMS Express). + only with "binary" data in packets (e.g. RMS Express). - An extra 00 byte was being appended to packets from AGW -network protocol 'K' messages. + network protocol 'K' messages. - Invalid data from an AGW client application could cause an -application crash. + application crash. - OSS (audio interface for non-Linux versions of Unix) should -be better now. + be better now. -### Known problems ### +### Known problems - Sometimes kissattach fails to connect with "direwolf -p". -The User Guide and Raspberry Pi APRS document have a couple work-arounds. + The User Guide and Raspberry Pi APRS document have a couple work-arounds. ----------- -## Version 1.0a -- May 2014 ## +## Version 1.0a -- May 2014 -### Bug fixed ### +### Bug fixed - Beacons sent directly to IGate server had incorrect source address. ----------- -## Version 1.0 -- May 2014 ## +## Version 1.0 -- May 2014 -### New Features ### +### New Features - Received audio can be obtained with a UDP socket or stdin. -This can be used to take audio from software defined radios -such as rtl_fm or gqrx. + This can be used to take audio from software defined radios + such as rtl_fm or gqrx. - 9600 baud data rate. - New PBEACON and OBEACON configuration options. Previously -it was necessary to handcraft beacons. + it was necessary to handcraft beacons. - Less CPU power required for 300 baud. This is important -if you want to run a bunch of decoders at the same time -to tolerate off-frequency HF SSB signals. + if you want to run a bunch of decoders at the same time + to tolerate off-frequency HF SSB signals. - Improved support for UTF-8 character set. - Improved troubleshooting display for APRStt macros. - In earlier versions, the DTMF decoder was always active because it -took a negligible amount of CPU time. Unfortunately this sometimes -resulted in too many false positives from some other types of digital -transmissions heard on HF. Starting in version 1.0, the DTMF decoder -is enabled only when the APRStt gateway is configured. - + took a negligible amount of CPU time. Unfortunately this sometimes + resulted in too many false positives from some other types of digital + transmissions heard on HF. Starting in version 1.0, the DTMF decoder + is enabled only when the APRStt gateway is configured. ----------- -## Version 0.9 --November 2013 ## +## Version 0.9 --November 2013 -### New Features ### +### New Features - Selection of non-default audio device for Linux ALSA. @@ -497,25 +461,23 @@ is enabled only when the APRStt gateway is configured. - Command line option "-t 0" to disable text colors. - APRStt macros which allow short numeric only touch tone -sequences to be processed as much longer predefined sequences. - + sequences to be processed as much longer predefined sequences. -### Bugs Fixed ### +### Bugs Fixed - Now works on 64 bit target. -### New Restriction for Windows version ### +### New Restriction for Windows version - Minimum processor is now Pentium 3 or equivalent or later. -It's possible to run on something older but you will need -to rebuild it from source. - + It's possible to run on something older but you will need + to rebuild it from source. ----------- -## Version 0.8 -- August 2013 ## +## Version 0.8 -- August 2013 -### New Features ### +### New Features - Internet Gateway (IGate) including IPv6 support. @@ -524,64 +486,59 @@ to rebuild it from source. - Preemptive digipeating option. - KISS TNC should now work with connected AX.25 protocols -(e.g. AX25 for Linux), not just APRS. - + (e.g. AX25 for Linux), not just APRS. ---------- -## Version 0.7 -- March 2013 ## +## Version 0.7 -- March 2013 -### New Features: ### +### New Features: - Added APRStt gateway capability. For details, see ***APRStt-Implementation-Notes.pdf*** - ----------- -## Version 0.6 -- February 2013 ## +## Version 0.6 -- February 2013 -### New Features ### +### New Features - Improved performance of AFSK demodulator. -Now decodes 965 frames from Track 2 of WA8LMF's TNC Test CD. + Now decodes 965 frames from Track 2 of WA8LMF's TNC Test CD. - KISS protocol now available thru a TCP socket. -Default port is 8001. -Change it with KISSPORT option in configuration file. + Default port is 8001. + Change it with KISSPORT option in configuration file. - Ability to salvage frames with bad FCS. -See section mentioning "bad apple" in the user guide. -Default of fixing 1 bit works well. -Fixing more bits not recommended because there is a high -probability of occasional corrupted data getting thru. + See section mentioning "bad apple" in the user guide. + Default of fixing 1 bit works well. + Fixing more bits not recommended because there is a high + probability of occasional corrupted data getting thru. - Added AGW "monitor" format messages. -Now compatible with APRS-TW for telemetry. - + Now compatible with APRS-TW for telemetry. -### Known Problem ### +### Known Problem - The Linux (but not Cygwin) version eventually hangs if nothing is -reading from the KISS pseudo terminal. Some operating system -queue fills up, the application write blocks, and decoding stops. + reading from the KISS pseudo terminal. Some operating system + queue fills up, the application write blocks, and decoding stops. - -### Workaround ### +### Workaround - If another application is not using the serial KISS interface, -run this in another window: - - tail -f /tmp/kisstnc + run this in another window: + + tail -f /tmp/kisstnc ----------- -## Version 0.5 -- March 2012 ## +## Version 0.5 -- March 2012 - More error checking and messages for invalid APRS data. ----------- -## Version 0.4 -- September 2011 ## +## Version 0.4 -- September 2011 - First general availability. - diff --git a/cmake/cpack/direwolf_icon.ico b/cmake/cpack/direwolf_icon.ico index 00714be0cc8bed83403cd29f6131bcb51842d620..6091cebb28568d224e478acf1ecb53f24ea224bc 100644 GIT binary patch literal 44764 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 literal 370070 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

-gsKf5@KYN0sSO z9$#MF7M1y?6)ubXz{^)&RvFCWgNSSSS9lJgq{)-Z@p)`!irEj;bMRn$X+IC)y1k0jxz_bR)FsP@lIfs#^CZJ=`_Ok8 zpX8UnPoS)XS$uSP#FV zzxU9gX3b@az$T6#KVF899BJ`tY@{mxH~nZa|8G^be5_NJA0^Z2|4E;ZX`*z^TjQWZ zIHu3`f6@~c$xB+~)feM6)afl9|LM5n%Ko2c?5>rjN57$h@qg@5$BOO)&N}Pt*#Ef( z==eUyws;O;{NL=i<>g%$boznl{1D#nIc zzW09l8^oCB!~fUcbW?K{^Hh7m4uJ2v>#nl##v3QD?Y7%ajyU273E4`IL4$-B$M?+f zzZv`=@@qdx$0Pst(=aVw8*q8MVLzW_pHNF4gXJg39&hIPkIJt5?rZ%Yb^_gp{NC*c z5XKs#wwL3NKfdhy=mX&Q>;nMZen6-B>YEm)%dHmA;~No8sIQ29pS-jzY*QD4hV)sm z2(2zhBDB;p(&t=FPHzqfhb zk84wNufVvSZ1v-pTB7zhrd?(KjaAcla9|SgBrz0ETKTs;R3lQ!lo6qS9A^d;hq)FvIK>MtISHG5ffL;TJmdAbibjas*JZS-~y({1E6!3le zx!VUY9uWTD-pu)r{K?T3^MJp<@edic?8-7}O0<5LS8qk<{%QS#Tjo~g8G(Ht5M#e- z1K%9L=hH>v+%jtk(--o?eEtuaz?0C$^OqOvwqAKClZL=s`bGaoA8^QFV=M0SIR~iq zTman%Xdh3*wg1~bpydBp_rqGB_Guq}*=3iNhj@(LpuyRq9XfY4R~H#Fa+Dl1VX|P2 zA3xmJ=hYX^N(XH{{hob-D+8WW(EqLPqa8})K4IkR*w3F_>&P2+%d90_#HS+KkpIX2 zQ=Dh|_>cO5Ct(_Y9}VLOb$-OPgezHmiun(PCx9~fu-*K!^xeILHw zz3(7l3jTo?m8ne%deKCk24wLHA95C8AnwYx02^6D~dg;izOz4on1SM=F> zLf3Zq-LUf9ztj&#>l#sC5c_|1eSlkLtzqpC*?8oONxG;{pd304aR_VZqx0oq9P=_9 zg5>LiS$UA|%h!_PrcRk{?}g$2ybsX#0QJD($MpSfeFh3H`gj@)`9JyqoTEu!zv7B3 zD#m%F;XC1ao$re;qP=@p%)eHa8nuSKYSBd_#(wl8TBaqISi;s5{_Xld;xPWlV?5vI zD?SI%W!3`^`9`t$cpP=}QAWly49GY_#*qf`#F<7~4K2XGBEI=@Hz`ghI${eG(32e|et z`lJg<=f-D+by=ik9KxEfi@S6TlTL@58=p9Frkw{s4)g(9o?7~We#4fL&b_7=9I#Vgv#-TG&+0aCsC4OX?h6dwd)QLa!|b)|+;5P*`YbU*+V8J%Z$9T2iHDZPh6Mn`ak*rtOcMCh{t`m576O!xGu9EIDMrU^pD+?hrAFbPN->o>6(ss zUP#_9)cM^qU0Q~b$A>enB|-Q-<*B9*=s9q>3|eY=>2CJ=^cu9J_5F@L2S_)w*AG4q zUxxn!G7R7DIb^u?{{bVG6Z8SR`Y*Mt3|oF>+fVdcW~8(>dw(|Ea6{|o7himF*}q-n zB=bzqeQ#&MLyyQt+wWYiAM2R~x$XzLdip$hxzGRL|Cr0TzTVRPAMK<4qZp37QGTrt z^3ZWChjF9{!Mt?HPg+79M?6#x)0tO8rbT(; za-@a52QMXEvG?Ef1;8=Rx9`=@Ub-JZe}D_=6Q)j=?RVK-1}(F^oiDWM-rM#Cm>+Z- zI^50ymK?FP^?%prb@4=jig6!v7 zZXclKhOfW(x5$OwH%DHi?FqC#K41wu??+#N`v46w#$#TOIX&n79Qy&U`uTMUU%mF` zTV;)nHk0L6Tg%Q3x(-=FdM~w%Z1I1)CFp#CIj7B);mfTmsLO%{3(6!oqf^(fD2{V} zv;3LkI^gD;Z|;2uP}8_FgvNi@e`^i9<<=`q-zW-EKJsWevUo607G2SF@}(?pex|$e zI?TK=ZS4Ep|Ey_RB0hWe?8F%u?w&ySIIg}!moEGLysM+V^RrJr-Ol@Q@87y}FFVJ_ zoS$=h^aUMy_qXx$ue+h_qZ)bsFE7cssWWAr&9{;sODrWb&AYjiPC7-Cc=6e%o*}wD z@4WL)dHwa*W&Zs6iFi#@6xTlN!{Pgw+v_yM=`e7&T=+lsIlBIhH2^oRmatoHy~5fD zs=-}3s^L*<9>@S5F4S?l4AL>(4b$Iwj}wQJ{T=ePwCmn`?=Ab7yC+clKl=a@W6iBY zkN&oQ=+bAfo#Vsb(Fb(wGr;=(5-W_7Tkg1%SuFg<`8ePc&ps!+A9$!-ck?Ya{nopF zouGFEP1SzY+zYtA@80{5H)n^mGw(Oo;@5feHnN?xZ^SmAzCVX~C>O)c2_cJHzAuh6 zZW_Z8ba`<;P_I59-~NBdQuNY3#(MzxyX*hB|JT1+C!H#rZu2AAY468{W|aL#(`dPL{GxzUcAup#uQAfG|4`{Xa0%%$$gIzs=Xzt08CUj6AhXXp zPfj}bLOJi6t7OJGbLE(s)8yzGQ{}tKPf_VPUyx@EpF?v-Qwf=f? z`(05V(p>FTbUl#s1wHSdFkynMu)+# zXP!e>wQ%h56J^gs4wp5z*j!FI=WN;afc>TKGE2+l*UV4Qxq+8nebtS!;nU7JPgdJx zb6I=Kt>s2@MpwPs2J`P~d4>7`>`8Lh0P*}lkNwA(cmA=rnV0=0k~zDes6Q8_uePp5 z@@c!yi<5sEJ!p`gMXTx3_|+?owyXDSb%J5OE%4PGfVX_y7zO{4msi(;}v&VPh#977$>bK^h z?TPhhG{o6I%Pg}@+4s>0;Jo9azFZA`qw5C#+7HrjO-s2ny_?Q79nUyI%_ohH^o@d1 z*`c;z{G!tbuwCJ__dN8dTzJim!Z<>`UI4mJKKT^D{>Dw7E`!W_xbv=Yp7-B+yS(w% zTP3O_`1s?G1$%sF&dmRvQ_9ku&^M|lezIFL9f z7r8(Uk!X}CP$H-Z4J85r2_I3KV3Rt&_>fJ&8;{)OH1obmI{nzh%=p4lJg>~r3ErLDDQ&6+i9X4bQ2_CEWZm%nZ3D6o%b z|M;!5K6ox|v#NP1#g+6c)s}kD0dMI_9jksS8T}!Z{e&{xZ?E5eCWt!NnDd4Ifq{2z z@F%~I2cJKF_q~Dd(1HB-9{%Gc#q)lAFaKvh`&onM4yoVYLc6(M_$*KRhI@VN_wM%$ zL;O>A)#5h}OR?NXU;8#yIrf~-$GAWDTCr{E!B@&9PuaSu_sPd6&-}hF9CylRw6WSz zzD*SG9Buk|tbHG2`pjoOlf*u9K=S_ZMl4m`rF_$~zJpxWui{_E zvdzobE5Tux?wIw&IeiVWPn=2&JKnXEaybW5PF~l`GW+o|`cvt6_#HBr_-s#NzYG7X z+WT=oFl1bP+UDR-+l{q>UK?{B2eEcT<*pJ1_Jmx8rb~V;@ z8MAw2*zdx97yehd_s8*%d&%kUCqsP5n0LGtyS`1RuXSU3W6RMmZPEvn$-8>! zxHxpWJD@xDx!~@)aDB$0cz*}q-NScv zB(8J$`Qo2@Kk0aRQqFtV-WTe&<6Pndx+s&%x+!_tA9!Bu)72BJvfj(me!Fqeo~LZz zo)_Rf9u^gka4e{w@oC8h$oWyK~a^2pQ*$4e3)jm*O>D$ZS;*hu>iv5F&-z{f8 z(D+}+*&pZsbLY-A;aaZ?|6!af=_%vd6R? z>!B`nN@7>GAzw<_pKJ%2ENfoYOJ3`_avRHjzH~obx~`r+yS~FUg>6~C7kOe{ag`;v}@cWsCGCl1#|&z?=;f3@)-a{yz5v74gQjZw5^o#Y`)+0ChwcBSo{ zwyV;SK5Ab2z&c6WV``rxr2eMz67%FWW`~xEdD3-ZUiN$LGXCRdfc4}69Y-=wEgt%m zlcx>FFe&BKW4lsHJG9I8-Y)mo@Vr93RZ#juUnOP#DIKX#J=spzZ(^BvmvkuBt@gw{ zsKnqp`Hz0|jt0M5IG1(P_n?S1_{NwkrEF`BuhdqTzU<4LWr+Wn|Eynae&AeSJoK4k zW1U5*BimVen1bEA(uS$`cQAj zVcC~~Y2urHvuxp=IwSqn4dMGTM{W*|BgMlg07teTubh%i)V}=kox4)OET{J=$TJdMwYG zKKs=Vo0NaVyQDGPcF7Me{wH6T&MJS?pV-c!96#_s#~i?z1I2pEvc+@FtvXAVW7BF! z8T*se)wOXMo(EjL@1Od>E5}J$e@egL6M5HX+oqILm+fMddYeQU^OAPqcS!xJe#l!)L-5_zR%32XJ(@uw}_hWtT3^`?vWe=E-;AcS!x5ux|Nc z<9~i<0F6Oi)^Un@$fTasadH>)A&zp+01n&Ip6knFD6|>R1#FkAul1x4sFUR!KGr5C z&#^gI9pD{UChl1tg6~vY3vY9i9XWDjIUoG&!`}mx=73WD@>peDNlcSR9e7gmvJH7s z#<--km!@goVb5R8C(b`7hs^0Xf2F>~dwtzbkA>yF6!!bJoqIcpcgd$PJfv=JSdaO` zX8?1~0ckusj3NGr`;>mDr}0L-rl@b@Y`HSypBVL2+HyYnI)^v!x3?XRLCV|xvF{w} z`|{5OiF;CQhyH5$CX^Z1!2Bw3kDNiQ&G9oo`bLWXH+JI0iRIj2esBipe-4;#{|NCQ zG39y0BlR@TGGmr8?Wwmz8=l7T=j^f`HYA_28P?H1^yztyhc+EWee6R2ed^FA)|O*R z>BRb6ryTKYUBKm#y}hd~v9&2wJ_p#u9H8TpxOH-eGZ}}jKExE;w5iq$%j`RB2b~b# zv9k2h*75zxF^tKhUu}=MfY=Y|>hAZnU%6!q_mFjAcWC{lU|#wu&j2_tVgK*UzTH7XTQ|7^!z;lF%}yGBeZczwcP|h>Q}$V(A#H4YI$TM+GN$b|GQKTbN}1Hv zJoO+aPpW0IEp+_6y0*R^W1-CPURRDY#?v;b)FYlLLqFtGltpop? zgmueTkAK`VAqTi~z})bU^>5=$-ZO{Tqdwz?ZJpfNq7LiY)Z>yEaFl&HEVtXqddbT% zaId8ek8|`vzcoc)T9#W*wUvF6CzbY@11??q!D2n;^SS>zu+Mu23s;*W!#%*!qemCG z=QEQ&Ibbz=LJVh!Pgy79RmQy2k(id)jPX)m_CtBBUALTm#(ZZz$0y6H;;kI8s=jmk zLG0@1{g4r#B^1vH@wn|JuMbA+?gZWogi`=HXUm~Lj zPdt~>9Q;qQ|IWdtJOjA%l>42K_!&TB9c5ned)OgyD6y8ZLp`YjnJj~+UYv&5jO$5k zlb3CEU%DJV%g}z-ayyXzlF|-kx=eX4b$v-cEgokFOTO1+z`w*j$`Z?^Jh0z||2gc_Q~Ds^ z36S{}eEb}s<58arbp7;YiSI64*tjQVWBAut?<&(?oObzH>wVnl*hCq*pcL~`zYG6U z;%-j1+n))#&jz~jPu!FG^;&i80@J!rO9q{C{CBkh#-SrGDNE;on4fw)d%0}kUt+lr zpThr~@Hw?F691=9-x-{-x^uw#Fy99c4*MxQ6T=d-mW=qOUQCDeA?@S)UTwp@Ugm%j zyc6e|>Sz2cPmQ;^+4uJVcQn91=b3YC$KCq(eEKE#Psy`hQiuN#i=j;C1`F3Q9j&YN z!hWS+m5=Wuj7L(&WiHjYXL+yU-O4}s{W5$%kod>ka=t;!;uE=H`urfVP7F&w!+M8B z)Kj-jHGYX#-A0$8$F^xo|LQZnINmu&q>Mv8`^&{W^oVtfR{V$WG#oyBxViP@T>#w_M_Z&d{gU&ezAXbQF#*L(Mzonk0J3GW}3GRKr&K_-eD*e#B^h?*VoTEcs zP0yVAZ6jrR?NJw#$#(1HWek?={VDwKrM(~T`2g2?ol|A`ZjhY+{2Y*fz8ex-8p9~( z*t6OJXYSJY*BF=ex{fwH&GBb!9l5r!Ez5oRv3<<7*LUx)_`hQLy$0YH&-hE${m`Ks z8i{-IbHhJki2I|&bSk!_UdR)(65n0C)rPWhb0EHphYMqS@!UT&FP>do3;J#Fi>qXP zseAnBUpHq?{ciU{nROOT&FfTkh`m0a^;`G+)|j?ghI7uTQ>U84N3LtG*jM&H2@f2& zMtxUszW+xk#Bui-K-OzC*xYjLA5DBqtZV+{M?UC` zhpw?qQQrsq75@?ZU%5CB^vD4R4jf#5E{j^<#(+jDK)IscvI{r>s$?=`sIeDmE-e1?0xzpvL=x5{^%{LfDNIb#`H zXDs};_QK5lk9lq#r%Q`%hkmVsf z_M3|TymLA+DrpP<$axF=FXfk?4M_bQ9%q4L$8K9N{Tc72{j@U*>^){D1tm|0z22f8ut3KDhJbeA!QD5zZ>c6updv8 zAAldmB5}+zsnmf?=L1=$jWneV(l9@;5B6`Br7YP01^(Z9I^4qbD9(*D=J_O%9nuKF75*?+~qKSS&ba^4~@E`I9}pAkrW=kUB2NaX)Z!MQloZJKVaMC5EXZ^{E$9p6ex^Q!?}|>wTG+ak-_9A^Hr;cMx43c)Pi{v(sF- zxYNA$z4Og0uYWIljgSlQ?q3}DZtws2ZU5V{dF`RYfegPB!!fLqj&=6mtJnvIt^FVP zzw+{Vh}d`3=ZGBqLkI5&eCt~;?A{B6`+OMd%msa8h?wnaFZCqmD0ec4YiY~jH^jUD zEI`VMZ7GZK%s=5=g#CIQ`paAZ>|^f-_W$(Hz8m1OYmImw_}Fd#9nA&Sx&NU0{}$`o z`z!v-@u*|M!oSuTTE1@aeF2;S@V|r**A~t#nZ!U2PuyC%#I~%HGPVV!4(mOI?~6Tn z@mU*W{<82cWv(6jaC^9G4_oZdqM<+1j^lS0`#$u#$LAQ+b73qAvcZ#z5g0_K5zJP*9Mke$Ey_Tsvf#P5*!#rMT>_kQ@k^qcoO zi?TJwT$$ecufF=)>~{co_8&6#ZD|Yam*StF4Um@3#s2+QE!WE0|FtYP9>&%q=3$q( z^OT<>NDLF#PM_^_Y0Q7?Y(QN+|968&0Y8W z{^E1={ZZW#ywgX_nY=I_{GUM9gK}P_EX8y0Ke#xbU@tGhKJ-iDb({73;vd+TSLqz# zUf}rgo0rc6h5R_e9^uG^C#ZOKhack+yc=fSO{98Fe*4aG!A>K(T&!ND-)C2y1 z_~Sp{m0kS7510IR-}{^94<31}x$cG=o9k{o+C22cSDR;Ed1G1kM^8W3`W|>6+`|t) zHzEJ_W&B@Sb8$a_i}wUEY>$19KztL^q+M|t#=nJe8TUL7$ehr=ujuM?4)t<=!a0!R&Es>< zJ6+h9_?Os{IY7$y3NLXU(46mhT>kfPx(vigX%qBmaRB$l{Wy{8A@}dK`FcJDDtl&+Ehalg>psevP5y$Hx3c%JIa?l(%V)U%-CqxnC}}b&i-@ z%unG@4DVV#Mc+ehp1kGE?mYnBbs^>@<+B0AW-OL048`;{F4>k;_W?O%x}N1Od#*n8 zoi=4(v?pab@6z(VI!9O9j`8%@9vAT)<^$MbJK9B=d>4)F{lq=#(0JR%_8RA@@*Ky1 z^NEiwaj)@@m|;va#u-N%Kg5ofQ(sc8Yn7$G#nT`5=dnT@*k=LD7G&Com>%WqpJhoo zXHpj9iBU=2SU3*n8`_8RaC|ZQIdZ-C1M4{V6Z4Yh#xF3CtGnrXDZJ~txsLUfS6*Gb z-}-}HyywJ?)**(pT$dT^kQ2X-_xly)q|$~ZV?FBJMZB==&k)p=)VFc^sAE4lc-A_) zEc=o?`*8hxU$sAW*^h~Kr0pD@`#veLj<_Yiq=o;pVyhIabR0QevKzc$e`@*_9RlQ9Q}{%H>m799k!RD6w_REw?mJKmBymH=eB6 zv}7s!%mL7`un}u3`_a5C%l1+ZUS3jfY~Jb@efa*FyZrvn`Z0YW?m>CpN4&`R0nZ#z z5=(nYSI<{YdCK{T^`Fz{{rBJB42dxr+kNc-_h_4gf2%)-Bfr{ zwx_;DQ#Mj{maP(*v@Q9VjhIeMF72drh<$#xFXKh$09nqBp}nRj=dXmHVxFx2OZpj( ze}9ga@mRuh?<7Urkay=|C-;0wA2D6oPfSP4Q&^L_)FYL37N4tUwN1%*-Y^IPfIT*yZi3Dn?5mxHjGbDe>u4? z%R0Yrv#qn^_j=-8${}-?ZL5#}^e?xq^jEi$wo-b!omJNDb=hi@l1c0jiT7CiY^of% z88ROh?n-21&71DC#4gT+sn|hXZu~(n_hO&YHgWHf4}{^Z5nrd%0uT?_gP}^*s){HnDK{q zUl99vM-DW0<@Bj@0NXG}piKL$>qB9K?Z}gQdHUI=*K=cLnN-`eyIKSKNoc&zCbd-2Y?v=YFek*teXEU+T$zDC?rQpBW?0 zXoE2g%ACMhkJ0w|z+>FwIlSj*e=+LYwa@di4gHlei`TweI&+j=xNu?do84D0&y+gG zYYqr=0I+}EZx8L5JK7G15dGh4)_najT$KWkrESc=fs@F1%r*F*ObI(1= z`;-SBcwiU*$OFs;ZLG(h5BR;`W7+TXef{p+PU4?7oL@e_P(C;Mr7wMHH?C9ht>a@a zm4O}3O)M|+K1$CE<@o&P-hD2@eE`mTT%Ss9?5A*qI_qpe>|;)+ zz`e9N#=7;z3t}j@f1EjU`f=Eo`~7O3i{<#XZ91R-{O7aRFL#}{O|K9BtvQgGor<^m z&5`wu>rZ{^Q?2;H|EqfP$tN3KSJFp&|L@+7m**eeZ|HK$?+-uxu=&kzezUwj z_~3&krOUdK{GAg=dr>EqA2=UV;a%EgU!|0>P(l$m8sF%b|BqJJb^V`e-8b;BWx6b% z0cu|BuDVRz=JZQli{`Ya4zUmV>tFx6p}dQJ?sK1OJucR$aHP&Ng-EN!9a_?sz$Q>_o`6oa5$(jQeu`1(sOnz+4!`@K; za^u6&v--v!Z#@gN+V_>kux`ow_K|95?a`E_+Kp9b;hcEaw6xEM9(rhSOj&VJW#c~s z95$!!y9%Z80sYvyAA7uf=1Kob&;4CK0q6etp#L1OFaE7|X&FC*jMJ_$45`PtUgJDg zF72m23)qbPpV;b4Po4O;rZhI7|B2gwXWsM2{22e-?=T1a=j4FWF{W{E<(ha7;F%zo zTE4;`Er0+0_nTk-@|RgTK)3BGzyA8`b^o8}9$@VAe++Y7@)IBVyH-qf#RcoFv%S?H zK2zp%K(-x1@!JFF2Uvog^XGqb`Q1SalijkEFXcIa{SD0pZLB}>#1qZMix-z;j`@t| z{IUHXbx%F@RIVq$c`UN^{JKN(kF+;VJ^_OsO&|I8nbN)6=b=N_@75nZ zdTjYW>PoRI``3J#E#My*CJk|&vc-4BLGj%@>4%;p)M;4^|By>tIlNrc%8!>1#yI~E zS&tJ(|FSq17%$)X&UYGXub2MhzS~Een#S@AFd_Si@i-?)9HsE4>$EQ=Wo;{`Ed3;p z{K@N!U;JVteb9K<<F+@syY0Cd+3pse?A3Xmm83%N8@J=22Ap54Dnre)v z{K7NA*cl+z?u|FzXmtJ1HK^^|d4jkdN=xQeejoYt&Hvt7XT9!LxeNPW_`(-@*=1kf z|Ni$!!#?K*mOgO_ImvGcxfttGdg-&kFMjch>>Pmm4r>fm);}+BJ^Roh^PU?wIrsVS z1OEeb$a6mQ6MN1#WSm3V5|46iXpb^kroN=Ck5SqIWxJTJmX|yS;Cfr{1pNHxKhMr1 z+J{>1kA2LYT>lcDZTrms&BuO{z24S+KJE>%&*OW%XV0F^w#69Kbd2-HJWFdFl?GQ@y6KGWHkw*{K5 z&+qX3tLt+-y#LQOxm5Z!lwZ}JJJipq+Lt^J!1F*W2jr|9a`seX*lTu6Fi+jSbAhlpPCiKKjDFtl@!4l;o;JM>--Rm40oEBHCkJf$Jk7=b9Nbf< zug@>P{BrUBoWd~ba`yb^o_nsRH>vxUJfY)?_Y%FpyU7kinOtfBQ;0L}%e_k#BF+>Sl=9PC4Zw&j5gb$YVCT;<47}=Ld=17@sqrY%A9+eFo_M4Q*+wgs;!{ z^VdITZTsltu2<|C0CGUghI}Wak8ib&j}PLui>Bs`ZFBeiCm;W3wYI}@!E3L*w)|a= z=bwLmcJJ_}=53qnV%W9hv3meZ7qa?Wz#hLtV~E)8qFw!TVV|E1UAXXW^Y+`r@Xolv zXA9H5v%hcoETe1ewZ0vXT%#On;r~~^ihn;0u{5_i2Y%+}Q?5T<*k`@8C&w#)Gs9YM zC)`ufF=~5}SIRyUqhzA7%WO7x4|cj`RFBtfyR`b@3(d0^u0|FrR~a#?89M zwT=DeV4gY{KQZ#)gAX=b4@pa~kG=hcZ+ISb_GhcW%Dw#t47fb;e6!w(nZ zzrKsNTI_MH)=kGg3#@xQ+tz>1zAtq!{u2D-x9RFLyS<5hiIv>_y-QEpto-n}lGx7S zOYr~LV~=g@XCpPXhR2qi!;;ShQaM1{to-mb%E7$W`Shnh9Xzj>&+gat{JzG*@K~5~ z4sjol!gWp^&XQC5xEF0}Z0DBeI9Kpp`lZXVuJWtT7emhgxxWKn*S2hJb1_f7In6II zE`1}Q*twx}TU&mV6cd)jsm#4D&SP9-z*1{ydk=m4%xUSx#THm!k_E>VvY(wz95^ zXPZi=iJj7U?fX7JefBr*`gP$Zw?23MX|GG4dKN`nOP-Q-^;hf1jjP=GEIkL*XMe-i zDTR?S>SF6|*|u~oy)k4~UsX0Nmd2ProC8X7z!-h*WgBB)DP;q5i+a+OF6&Bk*`}24 z+>{p{M&-((s2x(x7vRCdxiV%yRV6T z<_CFw_BVVDN^!F-`W3&!V`mj}sGkMMa{b-E;cK`pcrG2sDq^?xF+4s~_W?(a99hYG zCfmG@rMTG^{cT>;Z5vzcK49C%xsLH{3!Y2Iv5vW}efB>O$a8Ke>r4N9@I4f-|q<`H{!9oD}N+kofNG1RzQ zf83R>LFpQ=O22d*bJ1Vb{LHbRxvW9;Bb|rl7(3dhOI~Ews54M!pw2*@fjR?q2I>sd8K^T*XJEr;;Qs;QwWy2$ diff --git a/cmake/cpack/direwolf_icon.png b/cmake/cpack/direwolf_icon.png index 4898018fe311c1d3979cd77dcbb597460cb87f05..01b7d133812a5f5a617383a75203df8faddb4240 100644 GIT binary patch literal 47932 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 From 486b3cf1f685502af7dc87b0f9c9cead6800d47b Mon Sep 17 00:00:00 2001 From: wb2osz Date: Tue, 29 Apr 2025 15:05:08 -0400 Subject: [PATCH 74/79] Include the direwolf icon in the Windows executable. --- CHANGES.md | 2 ++ src/CMakeLists.txt | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 528c7fb3..a1e2c255 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -12,6 +12,8 @@ - New direwolf icon. +- Include the direwolf icon in the Windows executable. Note: When building from source, environment variable RC must point to windres location. + ## Version 1.7 -- October 2023 ### New Documentation: diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 44d1782a..33c8c690 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -128,7 +128,7 @@ if(LINUX) # icon # require plain gcc binary or link - #${CMAKE_SOURCE_DIR}/cmake/cpack/direwolf.rc + ${CMAKE_SOURCE_DIR}/cmake/cpack/direwolf.rc ) list(REMOVE_ITEM direwolf_SOURCES dwgpsd.c From 8831f60d5bbf3a326d2be33f18aa66a0d03d4278 Mon Sep 17 00:00:00 2001 From: wb2osz Date: Wed, 18 Jun 2025 13:27:30 -0400 Subject: [PATCH 75/79] Remove some warnings that upset people and might be excessive. --- src/xid.c | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/src/xid.c b/src/xid.c index 14e67e8d..243341c1 100644 --- a/src/xid.c +++ b/src/xid.c @@ -204,7 +204,7 @@ int xid_parse (unsigned char *info, int info_len, struct xid_param_s *result, ch } pval = 0; for (j=0; jfull_duplex = 0; } @@ -275,8 +277,9 @@ int xid_parse (unsigned char *info, int info_len, struct xid_param_s *result, ch } if ( ! (pval & PV_HDLC_Optional_Functions_Extended_Address)) { - text_color_set (DW_COLOR_ERROR); - dw_printf ("XID error: Expected Extended Address to be set.\n"); + // https://groups.io/g/bpq32/topic/113348033#msg44169 + //text_color_set (DW_COLOR_ERROR); + //dw_printf ("XID error: Expected Extended Address to be set.\n"); } if ( ! (pval & PV_HDLC_Optional_Functions_TEST_cmd_resp)) { From d9c9d20c5381785eab1fa33abb84e5778d88481b Mon Sep 17 00:00:00 2001 From: wb2osz Date: Thu, 19 Jun 2025 12:55:25 -0400 Subject: [PATCH 76/79] Oops. Fix editing error. --- src/xid.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/xid.c b/src/xid.c index 243341c1..84c76a44 100644 --- a/src/xid.c +++ b/src/xid.c @@ -204,7 +204,7 @@ int xid_parse (unsigned char *info, int info_len, struct xid_param_s *result, ch } pval = 0; for (j=0; j Date: Thu, 19 Jun 2025 13:04:30 -0400 Subject: [PATCH 77/79] artifact actions v3 now obsolete --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3eecf0bb..0736b4bf 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -149,7 +149,7 @@ jobs: make package fi - name: archive binary - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: direwolf_${{ matrix.config.os }}_${{ matrix.config.arch }}_${{ github.sha }} path: | From 7365173e34a3198fe3486ea31e819c9f866e71a4 Mon Sep 17 00:00:00 2001 From: wb2osz Date: Thu, 19 Jun 2025 13:56:49 -0400 Subject: [PATCH 78/79] Remove Ubuntu 20 no longer supported by actions. --- .github/workflows/ci.yml | 9 --------- 1 file changed, 9 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0736b4bf..e812e6b0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -71,15 +71,6 @@ jobs: build_type: 'Release', cmake_extra_flags: '' } - - { - name: 'Ubuntu 20.04', - os: ubuntu-20.04, - cc: 'gcc', - cxx: 'g++', - arch: 'x86_64', - build_type: 'Release', - cmake_extra_flags: '' - } steps: - name: checkout From 0a28e0012ca3bcf76aa90f0a92264810ec4b55c3 Mon Sep 17 00:00:00 2001 From: wb2osz Date: Wed, 25 Jun 2025 11:51:32 -0400 Subject: [PATCH 79/79] Latest tocalls.yaml --- data/tocalls.yaml | 318 ++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 308 insertions(+), 10 deletions(-) diff --git a/data/tocalls.yaml b/data/tocalls.yaml index 0df04785..283ad274 100644 --- a/data/tocalls.yaml +++ b/data/tocalls.yaml @@ -26,6 +26,10 @@ classes: shown: Tracker description: Tracker device + - class: gadget + shown: Gadget + description: Small non-tracker APRS device + - class: rig shown: Rig description: Mobile or desktop radio @@ -39,9 +43,21 @@ classes: description: Mobile phone or tablet app - class: software - shown: Software + shown: Desktop software description: Desktop software + - class: daemon + shown: Background software + description: Computer software without user interface + + - class: service + shown: Service + description: Software running as a web service or a message bot + + - class: network + shown: APRS network hardware appliance + description: A hardware appliance with self-contained APRS networking features + - class: digi shown: Digipeater description: Digipeater software @@ -58,10 +74,6 @@ classes: 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. @@ -117,6 +129,11 @@ mice: model: FTM-500D class: rig + - suffix: "_5" + vendor: Yaesu + model: FTM-510D + class: rig + - suffix: "_)" vendor: Yaesu model: FTM-100D @@ -161,6 +178,17 @@ mice: model: VP-Tracker class: tracker + - suffix: " X" + vendor: SainSonic + model: AP510 + class: tracker + + - suffix: "[1" + vendor: Open Source + model: APRSdroid + class: software + os: Android + # # mic-e legacy devices, with an unique comment suffix and prefix # @@ -292,7 +320,7 @@ tocalls: vendor: Anytone - tocall: APATAR - vendor: TA7W/OH2UDS Baris Dinc and TA6AEU + vendor: TA7W/OH2UDS Baris Dinc, TA6AD Emre Keles model: ATA-R APRS Digipeater class: digi @@ -340,9 +368,21 @@ tocalls: vendor: hambsd.org model: HamBSD + - tocall: APBT* + vendor: BTECH + contact: support@baofengtech.com + - tocall: APBT62 - vendor: BTech + vendor: BTECH model: DMR 6x2 + class: ht + contact: support@baofengtech.com + + - tocall: APBTUV + vendor: BTECH + model: UV-PRO + class: ht + contact: support@baofengtech.com - tocall: APC??? vendor: Rob Wittner, KZ5RW @@ -497,6 +537,14 @@ tocalls: os: embedded contact: pegloff@gmail.com + - tocall: APERRB + vendor: KG5JNC + model: APRS Backend for Errbot + class: service + contact: me@kg5jnc.com + features: + - messaging + - tocall: APERS? vendor: Jason, KG7YKZ model: Runner tracking @@ -507,11 +555,28 @@ tocalls: model: PE1RXQ APRS Tracker class: tracker - - tocall: APESP? + - tocall: APESP1 vendor: LY3PH model: APRS-ESP os: embedded + - tocall: APESPG + vendor: OH2TH + model: ESP SmartBeacon APRS-IS Client + os: embedded + + - tocall: APESPW + vendor: OH2TH + model: ESP Weather Station APRS-IS Client + os: embedded + + - tocall: APETBT + vendor: PD7R + model: TBTracker Balloon Telemetry Tracker + class: tracker + os: embedded + contact: roel@kroes.com + - tocall: APFG?? vendor: KP4DJT model: Flood Gage @@ -532,6 +597,10 @@ tocalls: model: GoBalloon class: tracker + - tocall: APGDT? + vendor: VK4FAST + model: Graphic Data Terminal + - tocall: APGO?? vendor: AA3NJ model: APRS-Go @@ -666,7 +735,7 @@ tocalls: model: PinPoint - tocall: APIZCI - vendor: TA7W/OH2UDS and TA6AEU + vendor: TA7W/OH2UDS Baris Dinc, TA6AD Emre Keles model: hymTR IZCI Tracker class: tracker os: embedded @@ -728,6 +797,20 @@ tocalls: model: TM-D700 class: rig + - tocall: APKDXn + vendor: KelateDX, 9M2D + model: LAHKHUANO APRS + class: tracker + os: embedded + contact: mzakiab@gmail.com + + - tocall: APKEYn + vendor: 9W2KEY + model: ATMega328P APRS + class: tracker + os: embedded + contact: mzakiab@gmail.com + - tocall: APKHTW vendor: Kip, W3SN model: Tempest Weather Bridge @@ -745,6 +828,14 @@ tocalls: vendor: DL3DCW model: APRScube + - tocall: APLDAG + vendor: Inigo, EA2CQ + model: DAGA LoRa/APRS SOTA spotting + class: service + contact: ea2cq@irratia.org + features: + - messaging + - tocall: APLDG? vendor: Eddie, 9W2LWK model: LoRAIGate @@ -769,6 +860,12 @@ tocalls: model: LoRa Meteostation class: wx + - tocall: APLER? + vendor: Ercan, TA3OER + model: TROY LoRa Tracker/iGate + os: embedded + contact: ta3oer@gmail.com + - tocall: APLETK vendor: DL5TKL model: T-Echo @@ -783,6 +880,13 @@ tocalls: os: embedded contact: hg3fug@fazi.hu + - tocall: APLFL? + vendor: Damian, SQ2CPA + model: LoRa/APRS Balloon + class: tracker + os: embedded + contact: sq2cpa@gmail.com + - tocall: APLFM? vendor: DO1MA model: FemtoAPRS @@ -794,6 +898,13 @@ tocalls: model: LoRa Gateway/Digipeater class: digi + - tocall: APLHB9 + vendor: SWISS-ARTG + model: LoRa iGate RPI + class: igate + os: Linux/Unix + contact: hb9pae@gmail.com + - tocall: APLHI? vendor: Giovanni, IW1CGW model: LoRa IGate/Digipeater/Telemetry @@ -806,11 +917,22 @@ tocalls: class: wx contact: iw1cgw@libero.it + - tocall: APLIF? + vendor: TA5Y + model: TIF LORA APRS I-GATE + class: igate + - tocall: APLIG? vendor: TA2MUN/TA9OHC model: LightAPRS Tracker class: tracker + - tocall: APLLO? + vendor: HB4LO + model: HAB BOT + class: tracker + contact: david.perrin@hb9hiz.ch + - tocall: APLM?? vendor: WA0TQG class: software @@ -834,6 +956,18 @@ tocalls: os: embedded contact: sq9p.peter@gmail.com + - tocall: APLPS? + vendor: Jose, XE3JAC + model: ESP-32 LoRa + os: embedded + contact: xe3jac@gmail.com + + - tocall: APLRF? + vendor: Damian, SQ2CPA + model: LoRa APRS + os: embedded + contact: sq2cpa@gmail.com + - tocall: APLRG? vendor: Ricardo, CA2RXU model: ESP32 LoRa iGate @@ -872,12 +1006,49 @@ tocalls: os: embedded contact: wajdzik.m@gmail.com + - tocall: APLZA? + vendor: Huang Xuewu, BD5HTY + model: LoRa + os: embedded + contact: bd5hty@gmail.com + + - tocall: APLZX? + vendor: N1AF + model: LoRa-APRS + os: embedded + contact: lora-aprs@n1af.org + - tocall: APMAIL vendor: Mike, NA7Q model: APRS Mailbox class: service contact: mike.ph4@gmail.com + - tocall: APMBL3 + vendor: Mobilinkd LLC + model: TNC3 + class: digi + os: embedded + contact: support@mobilinkd.com + + - tocall: APMBL4 + vendor: Mobilinkd LLC + model: TNC4 + class: digi + os: embedded + contact: support@mobilinkd.com + + - tocall: APMBL? + vendor: Mobilinkd LLC + contact: support@mobilinkd.com + + - tocall: APMBLN + vendor: Mobilinkd LLC + model: NucleoTNC + class: digi + os: embedded + contact: support@mobilinkd.com + - tocall: APMG?? vendor: Alex, AB0TJ model: PiCrumbs and MiniGate @@ -1002,6 +1173,12 @@ tocalls: vendor: Kantronics model: KAM-XL + - tocall: APNL?? + vendor: OE5DXL, OE5HPM + model: dxlAPRS + class: daemon + os: Linux/Unix + - tocall: APNM?? vendor: MFJ model: TNC @@ -1074,6 +1251,12 @@ tocalls: model: Oscar class: satellite + - tocall: APOPEN + vendor: David Platt, AE6EO + model: OpenTNC + os: embedded + contact: dplatt@radagast.org + - tocall: APOPYT vendor: Mike, NA7Q model: NA7Q Messenger @@ -1086,6 +1269,31 @@ tocalls: class: service contact: mike.ph4@gmail.com + - tocall: APOSB + vendor: SharkRF + model: openSPOT3 + class: gadget + os: embedded + contact: info@sharkrf.com + + - tocall: APOSB4 + vendor: SharkRF + model: openSPOT4 + class: gadget + os: embedded + contact: info@sharkrf.com + + - tocall: APOSB? + vendor: SharkRF + contact: info@sharkrf.com + + - tocall: APOSBM + vendor: SharkRF + model: M1KE + class: gadget + os: embedded + contact: info@sharkrf.com + - tocall: APOSMS vendor: Mike, NA7Q model: Open Source SMS Gateway @@ -1094,6 +1302,13 @@ tocalls: features: - messaging + - tocall: APOSW? + vendor: SharkRF + model: openSPOT2 + class: gadget + os: embedded + contact: info@sharkrf.com + - tocall: APOT?? vendor: Argent Data Systems model: OpenTracker @@ -1172,6 +1387,12 @@ tocalls: class: software os: Linux/Unix + - tocall: APREST + vendor: cwop.rest + model: HTTP - TCP CWOP Packet Submission + class: service + contact: leo@herzog.tech + - tocall: APRFG? vendor: RF.Guru contact: info@rf.guru @@ -1262,6 +1483,14 @@ tocalls: class: app os: ipad + - tocall: APRPJU + vendor: Piju 9M2PJU + model: 9M2PJU Bot + class: daemon + contact: 9m2pju@hamradio.my + features: + - messaging + - tocall: APRPR? vendor: Robert DM4RW, Peter DL6MAA model: Teensy RPR TNC @@ -1274,8 +1503,17 @@ tocalls: vendor: DL9RDZ class: tracker + - tocall: APRRES + vendor: xssfox + model: APRS-RepeaterRescue + class: network + os: embedded + contact: repeater-rescue@michaela.lgbt + features: + - messaging + - tocall: APRRF? - vendor: Jean-Francois Huet F1EVM + vendor: RRF - Réseau des Répéteurs Francophones model: Tracker for RRF class: tracker os: embedded @@ -1314,6 +1552,12 @@ tocalls: model: aprsc class: software + - tocall: APSDR? + vendor: Marcus Roskosch, DL8MRE + model: sdr-control + class: app + contact: aprs@ham-radio-apps.com + - tocall: APSF?? vendor: F5OPV, SFCP_LABS model: embedded APRS devices @@ -1352,12 +1596,29 @@ tocalls: model: SMS gateway class: software + - tocall: APSN01 + vendor: CSN Technologies Inc. + model: iGateMini + contact: info@igatemini.com + os: embedded + features: + - messaging + + - tocall: APSN?? + vendor: CSN Technologies Inc. + - tocall: APSRF? vendor: SoftRF model: Ham Edition class: tracker os: embedded + - tocall: APSTAR + vendor: AllStar Link LLC + model: Asterisk/app_rpt + class: daemon + os: Linux/Unix + - tocall: APSTM? vendor: W7QO model: Balloon tracker @@ -1368,6 +1629,13 @@ tocalls: model: Satellite Tracking and Operations class: software + - tocall: APSVX? + vendor: Tobias Blomberg, SM0SVX + model: SvxLink + class: daemon + os: Linux/Unix + contact: aprs-deviceid@cyberspejs.net + - tocall: APT2?? vendor: Byonics model: TinyTrak2 @@ -1404,6 +1672,12 @@ tocalls: os: Linux/Unix contact: kl7af@foghaven.net + - tocall: APTGIK + vendor: Juliet Delta, 9M4GIK + model: APRS Melaka + os: embedded + contact: 9m2ikr@gmail.com + - tocall: APTHUR model: APRSThursday weekly event mapbot daemon contact: harihend1973@gmail.com @@ -1437,6 +1711,12 @@ tocalls: vendor: Motorola model: MotoTRBO + - tocall: APTSLA + vendor: HA2NON + model: tesla-aprs + class: daemon + contact: nonoo@nonoo.hu + - tocall: APTT* vendor: Byonics model: TinyTrak @@ -1476,6 +1756,13 @@ tocalls: vendor: unknown model: IRLP + - tocall: APW2W? + vendor: Joachim Sonnabend, DG3FBL + model: WiresX2Web Software + class: software + os: Windows + contact: mail@dg3fbl.de + - tocall: APW9?? vendor: Mile Strk, 9A9Y model: WX Katarina @@ -1514,6 +1801,12 @@ tocalls: - messaging - item-in-msg + - tocall: APWXS? + vendor: Colin Cogle, W1DNS + model: aprs-weather-submit + class: daemon + contact: https://github.com/rhymeswithmogul/aprs-weather-submit/ + - tocall: APWnnn vendor: Sproul Brothers model: WinAPRS @@ -1565,6 +1858,11 @@ tocalls: model: FTM-500D class: rig + - tocall: APY510 + vendor: Yaesu + model: FTM-510D + class: rig + - tocall: APYS?? vendor: W2GMD model: Python APRS