diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..0cc4d34d --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,162 @@ +name: 'build direwolf' + +on: + # permit to manually trigger the CI + workflow_dispatch: + inputs: + cmake_flags: + description: 'Custom CMAKE flags' + required: false + push: + paths-ignore: + - '.github/**' + pull_request: + paths-ignore: + - '.github/**' + +jobs: + build: + name: ${{ matrix.config.name }} + runs-on: ${{ matrix.config.os }} + strategy: + fail-fast: false + matrix: + config: + - { + name: 'Windows Latest MinGW 64bit', + os: windows-latest, + cc: 'x86_64-w64-mingw32-gcc', + cxx: 'x86_64-w64-mingw32-g++', + ar: 'x86_64-w64-mingw32-ar', + windres: 'x86_64-w64-mingw32-windres', + arch: 'x86_64', + build_type: 'Release', + cmake_extra_flags: '-G "MinGW Makefiles"' + } + - { + name: 'Windows 2019 MinGW 32bit', + os: windows-2019, + cc: 'i686-w64-mingw32-gcc', + cxx: 'i686-w64-mingw32-g++', + ar: 'i686-w64-mingw32-ar', + windres: 'i686-w64-mingw32-windres', + arch: 'i686', + build_type: 'Release', + cmake_extra_flags: '-G "MinGW Makefiles"' + } + - { + name: 'macOS latest', + os: macos-latest, + cc: 'clang', + cxx: 'clang++', + arch: 'x86_64', + build_type: 'Release', + cmake_extra_flags: '' + } + - { + name: 'Ubuntu latest Debug', + os: ubuntu-latest, + cc: 'gcc', + cxx: 'g++', + arch: 'x86_64', + build_type: 'Debug', + cmake_extra_flags: '' + } + - { + name: 'Ubuntu 22.04', + os: ubuntu-22.04, + cc: 'gcc', + cxx: 'g++', + arch: 'x86_64', + 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 + uses: actions/checkout@v2 + with: + fetch-depth: 8 + - name: dependency + shell: bash + run: | + # this is not perfect but enought for now + if [ "$RUNNER_OS" == "Linux" ]; then + sudo apt-get update + sudo apt-get install libasound2-dev libudev-dev libhamlib-dev gpsd + 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 + elif [ "$RUNNER_OS" == "Windows" ]; then + # add the folder to PATH + echo "C:\msys64\mingw32\bin" >> $GITHUB_PATH + fi + - name: create build environment + run: | + cmake -E make_directory ${{github.workspace}}/build + - name: configure + shell: bash + working-directory: ${{github.workspace}}/build + run: | + if [ "$RUNNER_OS" == "Windows" ]; then + export CC=${{ matrix.config.cc }} + export CXX=${{ matrix.config.cxx }} + export AR=${{ matrix.config.ar }} + export WINDRES=${{ matrix.config.windres }} + fi + cmake $GITHUB_WORKSPACE \ + -DCMAKE_BUILD_TYPE=${{ matrix.config.build_type }} \ + -DCMAKE_C_COMPILER=${{ matrix.config.cc }} \ + -DCMAKE_CXX_COMPILER=${{ matrix.config.cxx }} \ + -DCMAKE_CXX_FLAGS="-Werror" -DUNITTEST=1 \ + ${{ matrix.config.cmake_extra_flags }} \ + ${{ github.event.inputs.cmake_flags }} + - name: build + shell: bash + working-directory: ${{github.workspace}}/build + run: | + if [ "$RUNNER_OS" == "Windows" ]; then + export CC=${{ matrix.config.cc }} + export CXX=${{ matrix.config.cxx }} + export AR=${{ matrix.config.ar }} + export WINDRES=${{ matrix.config.windres }} + fi + cmake --build . --config ${{ matrix.config.build_type }} \ + ${{ github.event.inputs.cmake_flags }} + - name: test + continue-on-error: true + shell: bash + working-directory: ${{github.workspace}}/build + run: | + ctest -C ${{ matrix.config.build_type }} \ + --parallel 2 --output-on-failure \ + ${{ github.event.inputs.cmake_flags }} + - name: package + shell: bash + working-directory: ${{github.workspace}}/build + run: | + if [ "$RUNNER_OS" == "Windows" ] || [ "$RUNNER_OS" == "macOS" ]; then + make package + fi + - name: archive binary + uses: actions/upload-artifact@v2 + with: + name: direwolf_${{ matrix.config.os }}_${{ matrix.config.arch }}_${{ github.sha }} + path: | + ${{github.workspace}}/build/direwolf-*.zip + ${{github.workspace}}/build/direwolf.conf + ${{github.workspace}}/build/src/* + ${{github.workspace}}/build/CMakeCache.txt + !${{github.workspace}}/build/src/cmake_install.cmake + !${{github.workspace}}/build/src/CMakeFiles + !${{github.workspace}}/build/src/Makefile diff --git a/CHANGES.md b/CHANGES.md index 4196f7dd..4b78ca14 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -2,8 +2,69 @@ # Revision History # -## Version 1.6 -- October 2020 ## +## Version 1.7 -- October 2023 ## + + +### 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. + +- ***APRS Digipeaters*** + +- ***Internal Packet Routing*** + +- ***Radio Interface Guide*** + +- ***Successful IGate Operation*** + +- ***Understanding APRS Packets*** + + +### 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" +- Limited support for CM108/CM119 GPIO PTT on Windows. + +- Dire Wolf now advertises itself using DNS Service Discovery. This allows suitable APRS / Packet Radio applications to find a network KISS TNC without knowing the IP address or TCP port. Thanks to Hessu for providing this. Currently available only for Linux and Mac OSX. [Read all about it here.](https://github.com/hessu/aprs-specs/blob/master/TCP-KISS-DNS-SD.md) + +- The transmit calibration tone (-x) command line option now accepts a radio channel number and/or a single letter mode: a = alternate tones, m = mark tone, s = space tone, p = PTT only no sound. + +- 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) + + + +### 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. + +- Packet filtering now skips over any third party header before classifying packet types. + +- Fixed build for Alpine Linux. + +### 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: ### diff --git a/CMakeLists.txt b/CMakeLists.txt index 9e710f52..84aeb738 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -4,12 +4,13 @@ project(direwolf) # configure version set(direwolf_VERSION_MAJOR "1") -set(direwolf_VERSION_MINOR "6") +set(direwolf_VERSION_MINOR "7") set(direwolf_VERSION_PATCH "0") -set(direwolf_VERSION_SUFFIX "") +set(direwolf_VERSION_SUFFIX "Development") # options -option(FORCE_SSE "Compile with SSE instruction only" OFF) +# See Issue 297. +option(FORCE_SSE "Compile with SSE instruction only" ON) option(FORCE_SSSE3 "Compile with SSSE3 instruction only" OFF) option(FORCE_SSE41 "Compile with SSE4.1 instruction only" OFF) option(OPTIONAL_TEST "Compile optional test (might be broken)" OFF) @@ -71,6 +72,7 @@ set(CUSTOM_SRC_DIR "${CMAKE_SOURCE_DIR}/src") set(CUSTOM_EXTERNAL_DIR "${CMAKE_SOURCE_DIR}/external") set(CUSTOM_MISC_DIR "${CUSTOM_EXTERNAL_DIR}/misc") set(CUSTOM_REGEX_DIR "${CUSTOM_EXTERNAL_DIR}/regex") +set(CUSTOM_HIDAPI_DIR "${CUSTOM_EXTERNAL_DIR}/hidapi") set(CUSTOM_GEOTRANZ_DIR "${CUSTOM_EXTERNAL_DIR}/geotranz") set(CUSTOM_DATA_DIR "${CMAKE_SOURCE_DIR}/data") set(CUSTOM_SCRIPTS_DIR "${CMAKE_SOURCE_DIR}/scripts") @@ -131,7 +133,7 @@ endif() # auto include current directory set(CMAKE_INCLUDE_CURRENT_DIR ON) -# set OS dependant variables +# set OS dependent variables if(${CMAKE_SYSTEM_NAME} STREQUAL "Linux") set(LINUX TRUE) @@ -143,6 +145,10 @@ elseif(${CMAKE_SYSTEM_NAME} STREQUAL "FreeBSD") configure_file("${CMAKE_SOURCE_DIR}/cmake/cpack/${CMAKE_PROJECT_NAME}.desktop.in" "${CMAKE_BINARY_DIR}/${CMAKE_PROJECT_NAME}.desktop" @ONLY) +elseif(${CMAKE_SYSTEM_NAME} STREQUAL "OpenBSD") + set(OPENBSD TRUE) + set(HAVE_SNDIO TRUE) + elseif(APPLE) if("${CMAKE_OSX_DEPLOYMENT_TARGET}" STREQUAL "") message(STATUS "Build for macOS target: local version") @@ -156,16 +162,21 @@ elseif(APPLE) set(CMAKE_MACOSX_RPATH ON) message(STATUS "RPATH support: ${CMAKE_MACOSX_RPATH}") + # just blindly enable dns-sd + set(USE_MACOS_DNSSD ON) + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -DUSE_MACOS_DNSSD") + elseif (WIN32) - if(NOT VS2015 AND NOT VS2017) - message(FATAL_ERROR "You must use Microsoft Visual Studio 2015 or 2017 as compiler") + if(C_MSVC) + if (NOT VS2015 AND NOT VS2017 AND NOT VS2019) + message(FATAL_ERROR "You must use Microsoft Visual Studio 2015, 2017 or 2019 as compiler") + else() + # compile with full multicore + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} /MP") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /MP") + set(CUSTOM_SHELL_BIN "") + endif() endif() - - # compile with full multicore - set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} /MP") - set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /MP") - - set(CUSTOM_SHELL_BIN "") endif() if (C_CLANG OR C_GCC) @@ -195,10 +206,16 @@ if (C_CLANG OR C_GCC) # I also took out -Wextra because it spews out so much noise a serious problem was not noticed. # It might go back in someday when I have more patience to clean up all the warnings. # + + # TODO: + # Try error checking -fsanitize=bounds-strict -fsanitize=leak + # Requires libubsan and liblsan, respectively. + ###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) set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wall -Wextra -Wvla -ffast-math -ftree-vectorize -D_DEFAULT_SOURCE ${EXTRA_FLAGS}") else() + #set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wall -Wvla -ffast-math -ftree-vectorize -D_GNU_SOURCE -fsanitize=bounds-strict ${EXTRA_FLAGS}") set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wall -Wvla -ffast-math -ftree-vectorize -D_GNU_SOURCE ${EXTRA_FLAGS}") endif() # @@ -246,6 +263,44 @@ else() endif(WIN32 OR CYGWIN) # requirements + +include(CheckSymbolExists) + +# Some platforms provide their own strlcpy & strlcat. (BSD, MacOSX) +# Others don't so we provide our own. (Windows, most, but not all Linux) +# Here we detect whether these are provided by the OS and set a symbol +# so that: +# (1) libgps does not supply its own version. +# (2) we know whether we need to supply our own copy. +# +# This was all working fine until these were added to the gnu c library 2.38. +# References: +# - https://www.gnu.org/software/libc/sources.html +# - https://sourceware.org/git/?p=glibc.git;a=blob_plain;f=NEWS;hb=HEAD +# +# This test is not detecting them for glibc 2.38 resulting in a conflict. +# Why? Are they declared in a different file or in some strange way? +# +# This is how they are declared in include/string.h: +# +# extern __typeof (strlcpy) __strlcpy; +# libc_hidden_proto (__strlcpy) +# extern __typeof (strlcat) __strlcat; +# libc_hidden_proto (__strlcat) +# +# Apparently cmake does not recognize this style. +# Keep this here for BSD type systems where it behaves as expected. +# We will need to add a hack in direwolf.h to define these if glibc version >= 2.38. + +check_symbol_exists(strlcpy string.h HAVE_STRLCPY) +if(HAVE_STRLCPY) + add_compile_options(-DHAVE_STRLCPY) +endif() +check_symbol_exists(strlcat string.h HAVE_STRLCAT) +if(HAVE_STRLCAT) + add_compile_options(-DHAVE_STRLCAT) +endif() + set(THREADS_PREFER_PTHREAD_FLAG ON) find_package(Threads REQUIRED) @@ -276,6 +331,17 @@ if(LINUX) set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -DUSE_CM108") endif() + find_package(Avahi) + if(AVAHI_CLIENT_FOUND) + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -DUSE_AVAHI_CLIENT") + endif() + +elseif (HAVE_SNDIO) + find_package(sndio REQUIRED) + if(SNDIO_FOUND) + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -DUSE_SNDIO") + endif() + elseif (NOT WIN32 AND NOT CYGWIN) find_package(Portaudio REQUIRED) if(PORTAUDIO_FOUND) @@ -287,8 +353,12 @@ else() set(ALSA_LIBRARIES "") set(UDEV_INCLUDE_DIRS "") set(UDEV_LIBRARIES "") + # Version 1.7 supports CM108/CM119 GPIO PTT for Windows. + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -DUSE_CM108") set(PORTAUDIO_INCLUDE_DIRS "") set(PORTAUDIO_LIBRARIES "") + set(SNDIO_INCLUDE_DIRS "") + set(SNDIO_LIBRARIES "") endif() # manage and fetch new data @@ -297,6 +367,7 @@ add_subdirectory(data) # external libraries add_subdirectory(${CUSTOM_GEOTRANZ_DIR}) add_subdirectory(${CUSTOM_REGEX_DIR}) +add_subdirectory(${CUSTOM_HIDAPI_DIR}) add_subdirectory(${CUSTOM_MISC_DIR}) # direwolf source code and utilities diff --git a/README.md b/README.md index 51b29019..3006a1ef 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ - + # Dire Wolf # ### Decoded Information from Radio Emissions for Windows Or Linux Fans ### @@ -9,11 +9,15 @@ Why waste $200 and settle for mediocre receive performance from a 1980's technol ![](tnc-test-cd-results.png) -Dire Wolf now includes [FX.25](https://en.wikipedia.org/wiki/FX.25_Forward_Error_Correction/) which adds Forward Error Correction (FEC) in a way that is completely compatible with existing systems. If both ends are capable of FX.25, your information will continue to get through under conditions where regular AX.25 is completely useless. +Dire Wolf includes [FX.25](https://en.wikipedia.org/wiki/FX.25_Forward_Error_Correction) which adds Forward Error Correction (FEC) in a way that is completely compatible with existing systems. If both ends are capable of FX.25, your information will continue to get through under conditions where regular AX.25 is completely useless. This was originally developed for satellites and is now seeing widespread use on HF. ![](fx25.png) -Dire Wolf is a modern software replacement for the old 1980's style TNC built with special hardware. +Version 1.7 adds [IL2P](https://en.wikipedia.org/wiki/Improved_Layer_2_Protocol), a different method of FEC with less overhead but it is not compatible with AX.25. + + + +### Dire Wolf is a modern software replacement for the old 1980's style TNC built with special hardware. ### Without any additional software, it can perform as: @@ -76,7 +80,21 @@ It can also be used as a virtual TNC for other applications such as [APRSIS32](h -- **Standard 300, 1200 & 9600 bps modems and more.** +- **Modems:** + + 300 bps AFSK for HF + + 1200 bps AFSK most common for VHF/UHF + + 2400 & 4800 bps PSK + + 9600 bps GMSK/G3RUH + + AIS reception + + EAS SAME reception + + - **DTMF ("Touch Tone") Decoding and Encoding.** @@ -96,12 +114,16 @@ It can also be used as a virtual TNC for other applications such as [APRSIS32](h ## Documentation ## + [Stable Version](https://github.com/wb2osz/direwolf/tree/master/doc) -[Latest Development Version](https://github.com/wb2osz/direwolf/tree/dev/doc) +[Latest Development Version ("dev" branch)](https://github.com/wb2osz/direwolf/tree/dev/doc) + +[Additional Topics](https://github.com/wb2osz/direwolf-doc) -[Power Point presentation](https://github.com/wb2osz/direwolf-presentation) -- Why not give a talk at a local club meeting? +[Power Point presentations](https://github.com/wb2osz/direwolf-presentation) -- Why not give a talk at a local club meeting? +Youtube has many interesting and helpful videos. Searching for [direwolf tnc](https://www.youtube.com/results?search_query=direwolf+tnc) or [direwolf aprs](https://www.youtube.com/results?search_query=direwolf+aprs) will produce the most relevant results. ## Installation ## @@ -131,15 +153,17 @@ On Debian / Ubuntu / Raspbian / Raspberry Pi OS: sudo apt-get install cmake sudo apt-get install libasound2-dev sudo apt-get install libudev-dev + sudo apt-get install libavahi-client-dev Or on Red Hat / Fedora / CentOS: - sudo yum install git - sudo yum install gcc - sudo yum install gcc-c++ - sudo yum install make + sudo yum install git + sudo yum install gcc + sudo yum install gcc-c++ + sudo yum install make sudo yum install alsa-lib-devel sudo yum install libudev-devel + sudo yum install avahi-devel CentOS 6 & 7 currently have cmake 2.8 but we need 3.1 or later. First you need to enable the EPEL repository. Add a symlink if you don't already have the older version and want to type cmake rather than cmake3. @@ -190,6 +214,9 @@ Read the **User Guide** in the [**doc** directory](https://github.com/wb2osz/dir 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. + + sudo port install direwolf ## Join the conversation ## diff --git a/cmake/modules/FindAvahi.cmake b/cmake/modules/FindAvahi.cmake new file mode 100644 index 00000000..9dc27618 --- /dev/null +++ b/cmake/modules/FindAvahi.cmake @@ -0,0 +1,19 @@ + +find_library(AVAHI_COMMON_LIBRARY NAMES avahi-common PATHS ${PC_AVAHI_CLIENT_LIBRARY_DIRS}) +if(AVAHI_COMMON_LIBRARY) + set(AVAHI_COMMON_FOUND TRUE) +endif() + +find_library(AVAHI_CLIENT_LIBRARY NAMES avahi-client PATHS ${PC_AVAHI_CLIENT_LIBRARY_DIRS}) +if(AVAHI_CLIENT_LIBRARY) + set(AVAHI_CLIENT_FOUND TRUE) +endif() + +FIND_PACKAGE_HANDLE_STANDARD_ARGS(Avahi DEFAULT_MSG AVAHI_COMMON_FOUND AVAHI_CLIENT_FOUND) + +if (AVAHI_FOUND) + set(AVAHI_INCLUDE_DIRS ${AVAHI_UI_INCLUDE_DIR}) + set(AVAHI_LIBRARIES ${AVAHI_COMMON_LIBRARY} ${AVAHI_CLIENT_LIBRARY}) +endif() + +mark_as_advanced(AVAHI_INCLUDE_DIRS AVAHI_LIBRARIES) diff --git a/cmake/modules/FindCPUflags.cmake b/cmake/modules/FindCPUflags.cmake index 802d1cc0..abb9e184 100644 --- a/cmake/modules/FindCPUflags.cmake +++ b/cmake/modules/FindCPUflags.cmake @@ -78,7 +78,6 @@ endfunction() # The default will be set for maximum portability so packagers won't need to # to anything special. # -set(FORCE_SSE 1) # # While ENABLE_GENERIC also had the desired result (for x86_64), I don't think # it is the right approach. It prevents the detection of the architecture, @@ -354,7 +353,12 @@ elseif(ARCHITECTURE_ARM) if(C_MSVC) try_run(RUN_NEON COMPILE_NEON "${CMAKE_BINARY_DIR}/tmp" "${TEST_DIR}/test_arm_neon.cxx" COMPILE_DEFINITIONS /O0) else() - try_run(RUN_NEON COMPILE_NEON "${CMAKE_BINARY_DIR}/tmp" "${TEST_DIR}/test_arm_neon.cxx" COMPILE_DEFINITIONS -mfpu=neon -O0) + if(${CMAKE_HOST_SYSTEM_PROCESSOR} STREQUAL ${CMAKE_SYSTEM_PROCESSOR}) + try_run(RUN_NEON COMPILE_NEON "${CMAKE_BINARY_DIR}/tmp" "${TEST_DIR}/test_arm_neon.cxx" COMPILE_DEFINITIONS -mfpu=neon -O0) + else() + try_compile(COMPILE_NEON "${CMAKE_BINARY_DIR}/tmp" "${TEST_DIR}/test_arm_neon.cxx" COMPILE_DEFINITIONS -mfpu=neon -O0) + set(RUN_NEON 0) + endif() endif() if(COMPILE_NEON AND RUN_NEON EQUAL 0) set(HAS_NEON ON CACHE BOOL "Architecture has NEON SIMD enabled") diff --git a/cmake/modules/FindCompiler.cmake b/cmake/modules/FindCompiler.cmake index f339a73e..fe036e4b 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_EQUAL 1920 AND MSVC_VERSION LESS_EQUAL 1929) + set(VS2019 ON) + elseif(MSVC_VERSION GREATER_EQUAL 1910 AND MSVC_VERSION LESS_EQUAL 1919) set(VS2017 ON) elseif(MSVC_VERSION GREATER 1899 AND MSVC_VERSION LESS 1910) set(VS2015 ON) diff --git a/cmake/modules/Findhamlib.cmake b/cmake/modules/Findhamlib.cmake index 2086a98f..16ca5685 100644 --- a/cmake/modules/Findhamlib.cmake +++ b/cmake/modules/Findhamlib.cmake @@ -52,7 +52,7 @@ find_library(HAMLIB_LIBRARY ) include(FindPackageHandleStandardArgs) -find_package_handle_standard_args(HAMLIB +find_package_handle_standard_args(hamlib DEFAULT_MSG HAMLIB_LIBRARY HAMLIB_INCLUDE_DIR diff --git a/cmake/modules/Findsndio.cmake b/cmake/modules/Findsndio.cmake new file mode 100644 index 00000000..e7292d5d --- /dev/null +++ b/cmake/modules/Findsndio.cmake @@ -0,0 +1,42 @@ +# - Try to find sndio +# +# SNDIO_FOUND - system has sndio +# SNDIO_LIBRARIES - location of the library for sndio +# SNDIO_INCLUDE_DIRS - location of the include files for sndio + +set(SNDIO_ROOT_DIR + "${SNDIO_ROOT_DIR}" + CACHE + PATH + "Directory to search for sndio") + +# no need to check pkg-config + +find_path(SNDIO_INCLUDE_DIRS + NAMES + sndio.h + PATHS + /usr/local/include + /usr/include + /opt/local/include + HINTS + ${SNDIO_ROOT_DIR} + ) + +find_library(SNDIO_LIBRARIES + NAMES + sndio + PATHS + /usr/local/lib + /usr/lib + /usr/lib64 + /opt/local/lib + HINTS + ${SNDIIO_ROOT_DIR} + ) + + +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(SNDIO DEFAULT_MSG SNDIO_INCLUDE_DIRS SNDIO_LIBRARIES) + +mark_as_advanced(SNDIO_INCLUDE_DIRS SNDIO_LIBRARIES) diff --git a/cmake/modules/Findudev.cmake b/cmake/modules/Findudev.cmake index 38ba2e2f..c8c4b624 100644 --- a/cmake/modules/Findudev.cmake +++ b/cmake/modules/Findudev.cmake @@ -65,7 +65,7 @@ find_path(UDEV_INCLUDE_DIR ) include(FindPackageHandleStandardArgs) -find_package_handle_standard_args(UDEV +find_package_handle_standard_args(udev DEFAULT_MSG UDEV_LIBRARY UDEV_INCLUDE_DIR diff --git a/conf/99-direwolf-cmedia.rules b/conf/99-direwolf-cmedia.rules index 587f6168..94e1828f 100644 --- a/conf/99-direwolf-cmedia.rules +++ b/conf/99-direwolf-cmedia.rules @@ -3,7 +3,7 @@ # $ ls -l /dev/hidraw* # crw------- 1 root root 247, 0 Sep 24 09:40 /dev/hidraw0 # -# An ordinary user, trying to acccess it will be denied. +# An ordinary user, trying to access it will be denied. # # Unnecessarily running applications as root is generally a bad idea because it makes it too easy # to accidentally trash your system. We need to relax the restrictions so ordinary users can use these devices. @@ -28,3 +28,9 @@ SUBSYSTEM=="hidraw", ATTRS{idVendor}=="0d8c", GROUP="audio", MODE="0660" # # Read the User Guide and run the "cm108" application for more information. # + +# +# Same thing for the "All In One Cable." +# + +SUBSYSTEM=="hidraw", ATTRS{idVendor}=="1209", ATTRS{idProduct}=="7388", GROUP="audio", MODE="0660" diff --git a/conf/generic.conf b/conf/generic.conf index b77d5dfa..9a19d8a2 100644 --- a/conf/generic.conf +++ b/conf/generic.conf @@ -29,16 +29,21 @@ %C%# Extensive documentation can be found here: %C%# Stable release - https://github.com/wb2osz/direwolf/tree/master/doc %C%# Latest development - https://github.com/wb2osz/direwolf/tree/dev/doc +%C%# Additional topics - https://github.com/wb2osz/direwolf-doc %C%# -%W%# The complete documentation set can also be found in the doc folder. -%L%# The complete documentation set can also be found in +%W%# The basic documentation set can also be found in the doc folder. +%L%# The basic documentation set can also be found in %L%# /usr/local/share/doc/direwolf/ or /usr/share/doc/direwolf/ %L%# Concise "man" pages are also available for Linux. %M%# /usr/local/share/doc/direwolf/ or /usr/share/doc/direwolf/ %M%# Concise "man" pages are also available for Mac OSX. %C%# +%C%# Questions??? Join the discussion forum: https://groups.io/g/direwolf +%C%# +%C%# %C%# This sample file does not have examples for all of the possibilities. -%C%# Consult the User Guide for more details on configuration options.%C%# +%C%# Consult the User Guide for more details on configuration options +%C%# and other documents for more details for different uses. %C%# %C%# These are the most likely settings you might change: %C%# @@ -93,6 +98,11 @@ %C%# Many people will simply use the default sound device. %C%# Some might want to use an alternative device by choosing it here. %C%# +%C%# +%C%# Many examples of radio interfaces and PTT options can be found in: +%C%# https://github.com/wb2osz/direwolf-doc/blob/main/Radio-Interface-Guide.pdf +%C%# +%C%# %R% ---------- Windows ---------- %R% %W%# When the Windows version starts up, it displays something like @@ -231,14 +241,14 @@ %C%# It can be up to 6 letters and digits with an optional ssid. %C%# The APRS specification requires that it be upper case. %C%# -%C%# Example (don't use this unless you are me): MYCALL WB2OSZ-5 +%C%# Example (don't use this unless you are me): MYCALL WB2OSZ-5 %C%# %C% %C%MYCALL N0CALL %C% %C%# %C%# Pick a suitable modem speed based on your situation. -%C%# 1200 Most common for VHF/UHF. Default if not specified. +%C%# 1200 Most common for VHF/UHF. This is the default if not specified. %C%# 2400 QPSK compatible with MFJ-2400, and probably PK232-2400 & KPC-2400. %C%# 300 Low speed for HF SSB. Default tones 1600 & 1800. %C%# EAS Emergency Alert System (EAS) Specific Area Message Encoding (SAME). @@ -263,10 +273,26 @@ %C% %C%#DTMF %C% -%C%# -%C%# If not using a VOX circuit, the transmitter Push to Talk (PTT) -%C%# control is usually wired to a serial port with a suitable interface circuit. -%C%# DON'T connect it directly! +%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%# 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%# 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. @@ -278,6 +304,7 @@ %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 @@ -289,7 +316,7 @@ %L% %L%#PTT GPIO 25 %L% -%C%# The Data Carrier Detect (DCD) signal can be sent to the same places +%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 @@ -357,11 +384,11 @@ %W% %C%# %C%# It is sometimes possible to recover frames with a bad FCS. -%C%# This applies to all channels. +%C%# This is not a global setting. +%C%# It applies only the the most recent CHANNEL specified. %C%# -%C%# 0 [NONE] - Don't try to repair. -%C%# 1 [SINGLE] - Attempt to fix single bit error. (default) -%C%# ... see User Guide for more values and in-depth discussion. +%C%# 0 - Don't try to repair. (default) +%C%# 1 - Attempt to fix single bit error. %C%# %C% %C%#FIX_BITS 0 @@ -386,15 +413,21 @@ %C%# Example: %C%# %C%# This results in a broadcast once every 10 minutes. -%C%# Every half hour, it can travel via two digipeater hops. +%C%# Every half hour, it can travel via one digipeater hop. %C%# The others are kept local. %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,WIDE2-1 +%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% -%C% +%C%# +%C%# Did you know that APRS comments and messages can contain UTF-8 characters, not only plain ASCII? +%C%# +%C%#PBEACON delay=1 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? \xe0\xb8\xa7\xe0\xb8\xb4\xe0\xb8\x97\xe0\xb8\xa2\xe0\xb8\xb8\xe0\xb8\xaa\xe0\xb8\xa1\xe0\xb8\xb1\xe0\xb8\x84\xe0\xb8\xa3\xe0\xb9\x80\xe0\xb8\xa5\xe0\xb9\x88\xe0\xb8\x99" +%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 @@ -430,17 +463,11 @@ %C%# the "#" from the beginning of the line below. %C%# %C% -%C%#DIGIPEAT 0 0 ^WIDE[3-7]-[1-7]$|^TEST$ ^WIDE[12]-[12]$ TRACE +%C%#DIGIPEAT 0 0 ^WIDE[3-7]-[1-7]$|^TEST$ ^WIDE[12]-[12]$ %C% -%C%# See User Guide for more explanation of what this means and how -%C%# it can be customized for your particular needs. +%C%# See User Guide and "APRS-Digipeaters.pdf" for more explanation of what +%C%# this means and how it can be customized for your particular needs. %C% -%C%# Filtering can be used to limit was is digipeated. -%C%# For example, only weather weather reports, received on channel 0, -%C%# will be retransmitted on channel 1. -%C%# -%C% -%C%#FILTER 0 1 t/wn %C% %C%# Traditional connected mode packet radio uses a different %C%# type of digipeating. See User Guide for details. @@ -474,6 +501,7 @@ %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 @@ -482,13 +510,14 @@ %C%# To relay 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 +%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% @@ -505,82 +534,4 @@ %C%# %C%# See separate "APRStt-Implementation-Notes" document for details. %C%# -%C% -%C%# -%C%# Sample gateway configuration based on: -%C%# -%C%# http://www.aprs.org/aprstt/aprstt-coding24.txt -%C%# http://www.aprs.org/aprs-jamboree-2013.html -%C%# -%C% -%C%# Define specific points. -%C% -%C%TTPOINT B01 37^55.37N 81^7.86W -%C%TTPOINT B7495088 42.605237 -71.34456 -%C%TTPOINT B934 42.605237 -71.34456 -%C% -%C%TTPOINT B901 42.661279 -71.364452 -%C%TTPOINT B902 42.660411 -71.364419 -%C%TTPOINT B903 42.659046 -71.364452 -%C%TTPOINT B904 42.657578 -71.364602 -%C% -%C% -%C%# For location at given bearing and distance from starting point. -%C% -%C%TTVECTOR B5bbbddd 37^55.37N 81^7.86W 0.01 mi -%C% -%C%# For location specified by x, y coordinates. -%C% -%C%TTGRID Byyyxxx 37^50.00N 81^00.00W 37^59.99N 81^09.99W -%C% -%C%# UTM location for Lowell-Dracut-Tyngsborough State Forest. -%C% -%C%TTUTM B6xxxyyy 19T 10 300000 4720000 -%C% -%C% -%C% -%C%# Location for the corral. -%C% -%C%TTCORRAL 37^55.50N 81^7.00W 0^0.02N -%C% -%C%# Compact messages - Fixed locations xx and object yyy where -%C%# Object numbers 100 - 199 = bicycle -%C%# Object numbers 200 - 299 = fire truck -%C%# Others = dog -%C% -%C%TTMACRO xx1yy B9xx*AB166*AA2B4C5B3B0A1yy -%C%TTMACRO xx2yy B9xx*AB170*AA3C4C7C3B0A2yy -%C%TTMACRO xxyyy B9xx*AB180*AA3A6C4A0Ayyy -%C% -%C%TTMACRO z Cz -%C% -%C%# Receive on channel 0, Transmit object reports on channel 1 with optional via path. -%C%# You probably want to put in a transmit delay on the APRStt channel so it -%C%# it doesn't start sending a response before the user releases PTT. -%C%# This is in 10 ms units so 100 means 1000 ms = 1 second. -%C% -%C%#TTOBJ 0 1 WIDE1-1 -%C%#CHANNEL 0 -%C%#DWAIT 100 -%C% -%C%# Advertise gateway position with beacon. -%C% -%C%# OBEACON DELAY=0:15 EVERY=10:00 VIA=WIDE1-1 OBJNAME=WB2OSZ-tt SYMBOL=APRStt LAT=42^37.14N LONG=71^20.83W COMMENT="APRStt Gateway" -%C% -%C% -%C%# Sample speech responses. -%C%# Default is Morse code "R" for received OK and "?" for all errors. -%C% -%C%#TTERR OK SPEECH Message Received. -%C%#TTERR D_MSG SPEECH D not implemented. -%C%#TTERR INTERNAL SPEECH Internal error. -%C%#TTERR MACRO_NOMATCH SPEECH No definition for digit sequence. -%C%#TTERR BAD_CHECKSUM SPEECH Bad checksum on call. -%C%#TTERR INVALID_CALL SPEECH Invalid callsign. -%C%#TTERR INVALID_OBJNAME SPEECH Invalid object name. -%C%#TTERR INVALID_SYMBOL SPEECH Invalid symbol. -%C%#TTERR INVALID_LOC SPEECH Invalid location. -%C%#TTERR NO_CALL SPEECH No call or object name. -%C%#TTERR SATSQ SPEECH Satellite square must be 4 digits. -%C%#TTERR SUFFIX_NO_CALL SPEECH Send full call before using suffix. -%C% \ No newline at end of file + diff --git a/data/CMakeLists.txt b/data/CMakeLists.txt index 9f7c40e4..7972cc23 100644 --- a/data/CMakeLists.txt +++ b/data/CMakeLists.txt @@ -1,4 +1,19 @@ # +# Update: 1 May 2023 (still 1.7 dev version) +# +# The original intention was to allow an easy way to download the most +# recent versions of some files. +# +# "update-data" would only work once. +# +# These locations are no longer being maintained: +# http://www.aprs.org/aprs11/tocalls.txt -- 14 Dec 2021 +# http://www.aprs.org/symbols/symbols-new.txt -- 17 Mar 2021 +# http://www.aprs.org/symbols/symbolsX.txt -- 25 Nov 2015 +# so there is no reason to provide a capability grab the latest version. +# +# Rather than fixing an obsolete capability, it will just be removed. +# # 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. @@ -6,24 +21,13 @@ # The original permanent symbols are built in but the "new" symbols, # using overlays, are often updated. These are also read from files. # -# You can obtain an updated copy by typing "make data-update". -# This is not part of the normal build process. You have to do this explicitly. -# -# The locations below appear to be the most recent. -# The copy at http://www.aprs.org/tocalls.txt is out of date. -# + include(ExternalProject) set(TOCALLS_TXT "tocalls.txt") -set(TOCALLS_TXT_BKP "tocalls.txt.old") -set(TOCALLS_URL "http://www.aprs.org/aprs11/tocalls.txt") set(SYMBOLS-NEW_TXT "symbols-new.txt") -set(SYMBOLS-NEW_TXT_BKP "symbols-new.txt.old") -set(SYMBOLS-NEW_URL "http://www.aprs.org/symbols/symbols-new.txt") set(SYMBOLSX_TXT "symbolsX.txt") -set(SYMBOLSX_TXT_BKP "symbolsX.txt.old") -set(SYMBOLSX_URL "http://www.aprs.org/symbols/symbolsX.txt") set(CUSTOM_BINARY_DATA_DIR "${CMAKE_BINARY_DIR}/data") # we can also move to a separate cmake file and use file(download) @@ -32,63 +36,6 @@ file(COPY "${CUSTOM_DATA_DIR}/${TOCALLS_TXT}" DESTINATION "${CUSTOM_BINARY_DATA_ 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}") -add_custom_target(data_rename - COMMAND ${CMAKE_COMMAND} -E rename "${CUSTOM_BINARY_DATA_DIR}/${TOCALLS_TXT}" "${CUSTOM_BINARY_DATA_DIR}/${TOCALLS_TXT_BKP}" - COMMAND ${CMAKE_COMMAND} -E rename "${CUSTOM_BINARY_DATA_DIR}/${SYMBOLS-NEW_TXT}" "${CUSTOM_BINARY_DATA_DIR}/${SYMBOLS-NEW_TXT_BKP}" - COMMAND ${CMAKE_COMMAND} -E rename "${CUSTOM_BINARY_DATA_DIR}/${SYMBOLSX_TXT}" "${CUSTOM_BINARY_DATA_DIR}/${SYMBOLSX_TXT_BKP}" - ) - -ExternalProject_Add(download_tocalls - DEPENDS data_rename - URL ${TOCALLS_URL} - PREFIX "" - DOWNLOAD_DIR "${CUSTOM_BINARY_DATA_DIR}" - DOWNLOAD_NAME "${TOCALLS_TXT}" - DOWNLOAD_NO_EXTRACT 0 - EXCLUDE_FROM_ALL 1 - UPDATE_COMMAND "" - PATCH_COMMAND "" - CONFIGURE_COMMAND "" - BUILD_COMMAND "" - INSTALL_COMMAND "" - TEST_COMMAND "" - ) - -ExternalProject_Add(download_symbols-new - DEPENDS data_rename - URL ${SYMBOLS-NEW_URL} - PREFIX "" - DOWNLOAD_DIR "${CUSTOM_BINARY_DATA_DIR}" - DOWNLOAD_NAME "${SYMBOLS-NEW_TXT}" - DOWNLOAD_NO_EXTRACT 0 - EXCLUDE_FROM_ALL 1 - UPDATE_COMMAND "" - PATCH_COMMAND "" - CONFIGURE_COMMAND "" - BUILD_COMMAND "" - INSTALL_COMMAND "" - TEST_COMMAND "" - ) - -ExternalProject_Add(download_symbolsx - DEPENDS data_rename - URL ${SYMBOLSX_URL} - PREFIX "" - DOWNLOAD_DIR "${CUSTOM_BINARY_DATA_DIR}" - DOWNLOAD_NAME "${SYMBOLSX_TXT}" - DOWNLOAD_NO_EXTRACT 0 - EXCLUDE_FROM_ALL 1 - UPDATE_COMMAND "" - PATCH_COMMAND "" - CONFIGURE_COMMAND "" - BUILD_COMMAND "" - INSTALL_COMMAND "" - TEST_COMMAND "" - ) - -add_custom_target(update-data) -add_dependencies(update-data data_rename download_tocalls download_symbols-new download_symbolsx) - install(FILES "${CUSTOM_BINARY_DATA_DIR}/${TOCALLS_TXT}" 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/symbols-new.txt b/data/symbols-new.txt index 44dcb6b9..aa6fc7d2 100644 --- a/data/symbols-new.txt +++ b/data/symbols-new.txt @@ -1,7 +1,6 @@ -APRS SYMBOL OVERLAY and EXTENSION TABLES in APRS 1.2 17 Jun 2018 +APRS SYMBOL OVERLAY and EXTENSION TABLES in APRS 1.2 17 Mar 2021 ------------------------------------------------------------------------ - BACKGROUND: Since October 2007, overlay characters (36/symbol) are allowed on all symbols. This allows thousands of uniquely specified symbols instead of the original 188 (94 primary and 94 alternate). @@ -9,7 +8,9 @@ But the master symbol document, http://aprs.org/symbols/symbolsX.txt, only has one line per symbol. So this added overlay list below gives us thousands of new symbol codes. -17 Jun19: Added several overlays for RAIL symbol +17 Mar 21 Added L& for LORA Igate +24 Jun18: Updated CAR symbols +17 Jun18: Added several overlays for RAIL symbol 03 Apr17: Added Methane Hazard symbol "MH" 13 Feb17: Added Ez = Emergency Power (shelter), Cars: P> = Plugin S> = Solar powered. Moved Ham club C- to Buildings Ch. @@ -211,14 +212,18 @@ CARS: #> (Vehicles) /> = normal car (side view) \> = Top view and symbol POINTS in direction of travel #> = Reserve overlays 1-9 for numbered cars (new Aug 2014) -B> = Battery (was E for electric) +3> = Model 3 (Tesla) +B> = BEV - Battery EV(was E for electric) +D> = DIY - Do it yourself E> = Ethanol (was electric) F> = Fuelcell or hydrogen -H> = Homemade -P> = Plugin-hybrid +H> = Hybrid +L> = Leaf +P> = PHEV - Plugin-hybrid S> = Solar powered T> = Tesla (temporary) -V> = GM Volt (temporary) +V> = Volt (temporary) +X> = Model X CIVIL DEFENSE or TRIANGLE: #c /c = Incident Command Post @@ -269,6 +274,7 @@ FE = (F overlay) Fog was \{ GATEWAYS: #& /& = HF Gateway <= the original primary table definition I& = Igate Generic (please use more specific overlay) +L& - Lora Igate R& = Receive only IGate (do not send msgs back to RF) P& = PSKmail node T& = TX igate with path set to 1 hop only) @@ -339,17 +345,17 @@ I; = Islands on the air S; = Summits on the air W; = WOTA -POWER or ENERGY: #% +POWER and ENERGY: #% /% = DX cluster <= the original primary table definition C% = Coal E% = Emergency (new Aug 2014) -G% = Geothermal +G% = Gas Turbine H% = Hydroelectric N% = Nuclear P% = Portable (new Aug 2014) R% = Renewable (hydrogen etc fuels) S% = Solar -T% = Turbine +T% = Thermal (geo) W% = Wind RAIL Symbols: #= diff --git a/data/tocalls.txt b/data/tocalls.txt index 47ba2e3b..169c9868 100644 --- a/data/tocalls.txt +++ b/data/tocalls.txt @@ -1,44 +1,37 @@ -APRS TO-CALL VERSION NUMBERS 13 Oct 2020 -------------------------------------------------------------------- - WB4APR +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 -13 Oct 20 Added APIZCI hymTR IZCI Tracker by TA7W/OH2UDS and TA6AEU -13 Aug 20 Added APLGxx for LoRa Gateway/Digipeater - APLTxx for LoRa Tracker - OE5BPA -02 Aug 20 Added APNVxx for SQ8L's VP digi and Nodes -26 May 20 Added APY300 for Yaesu - 5 May 20 added APESPG ESP SmartBeacon APRS-IS Client - APESPW ESP Weather Station APRS-IS Client -17 Apr 20 Added APGDTx for VK4FAST's Graphic Data Terminal -19 Mar 20 Added APOSW and APOSB for OpenSPOT2 and 3 -20 Jan 20 Added APBT62 for BTech DMR 6x2 -08 Jan 20 Added APCLUB for Brazil APRS network -06 Jan 20 Added APMQxx for Ham Radio of Things WB2OSZ -18 Dec 19 Added APTPNx: TARPN Packet Node Tracker by KN4ORB -02 Dec 19 added APJ8xx For Jordan / KN4CRD JS8Call application - 8 Sep 19 Added APBSDx for OpenBSD or HamBSD https://hambsd.org/ -19 Aug 19 Added APNKMX for KAM-XL - and Added APAT51 for Anytone AT-D578UV APRS radio -16 Jul 19 expanded APMGxx to cover PiCrumbs and MiniGate -24 Jun 19 Added APTCMA for CAPI tracker - PU1CMA Brazil - 4 Jun 19 added APATxx for Anytone - 8 May 19 added APQTHx for W8WJB's QTH.app -12 Mar 19 added APLIGx for LightAPRS - 3 Dec 18 added APRARX forVK5QI's radiosonde tracking - 8 Nov 18 added APELKx for WB8ELK balloons -24 Oct 18 added APGBLN for NW5W's GoBalloon -18 Apr 18 added APBKxx for PY5BK Bravo Tracker in Brazil - 7 Mar 18 added APERSx Runner tracking by Jason,KG7YKZ - 8 Jan 18 added APTCHE PU3IKE in Brazil TcheTracker/Tcheduino +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 + APB2MF,APR2MF,APAVT5 @@ -52,6 +45,10 @@ 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. + @@ -69,7 +66,8 @@ a TOCALL number series: APAHxx AHub APAND1 APRSdroid (pre-release) http://aprsdroid.org/ APAMxx Altus Metrum GPS trackers - APATxx for Anytone. 81 for 878 HT + 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 @@ -89,7 +87,9 @@ a TOCALL number series: 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 @@ -112,6 +112,7 @@ a TOCALL number series: 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 @@ -128,9 +129,21 @@ a TOCALL number series: 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 APICQx for ICQ + 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 @@ -144,20 +157,26 @@ a TOCALL number series: 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 APLGxx LoRa Gateway/Digipeater OE5BPA + 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 @@ -234,10 +253,12 @@ a TOCALL number series: 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 @@ -249,14 +270,17 @@ a TOCALL number 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) @@ -265,15 +289,11 @@ 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. - -REGISTERED ALTNETS: -------------------- +REGISTERED TOCALL ALTNETS: +-------------------------- ALTNETS are uses of the AX-25 tocall to distinguish specialized traffic that may be flowing on the APRS-IS, but that are not intended @@ -301,6 +321,6 @@ and need some special recognition. Here are some ideas: SATERN - Salvation Army Altnet - AFMARS - Airforce Mars + AFMARS - Airforce Mars AMARS - Army Mars \ No newline at end of file diff --git a/doc/README.md b/doc/README.md index 40aa77d6..9f44684e 100644 --- a/doc/README.md +++ b/doc/README.md @@ -1,4 +1,4 @@ -# Documentation for Dire Wolf # +# Documentation for Dire Wolf # Click on the document name to view in your web browser or the link following to download the PDF file. @@ -154,6 +154,14 @@ and a couple things that can be done about it. Here, we take a closer look at some of the frames on the TNC Test CD in hopes of gaining some insights into why some are easily decoded and others are more difficult. There are a lot of ugly signals out there. Many can be improved by decreasing the transmit volume. Others are just plain weird and you have to wonder how they are being generated. + +## Additional Documentation for Dire Wolf Software TNC # + + +When there was little documentation, it was all added to the source code repository [https://github.com/wb2osz/direwolf/tree/master/doc](https://github.com/wb2osz/direwolf/tree/master/doc) + +The growing number of documentation files and revisions are making the source code repository very large which means long download times. Additional documentation, not tied to a specific release, is now being added to [https://github.com/wb2osz/direwolf-doc](https://github.com/wb2osz/direwolf-doc) + ## Questions? Experiences to share? ## Here are some good places to ask questions and share your experiences: diff --git a/doc/Raspberry-Pi-APRS.pdf b/doc/Raspberry-Pi-APRS.pdf index 7bbff455..344c3de6 100644 Binary files a/doc/Raspberry-Pi-APRS.pdf and b/doc/Raspberry-Pi-APRS.pdf differ diff --git a/doc/Successful-APRS-IGate-Operation.pdf b/doc/Successful-APRS-IGate-Operation.pdf index 26f8d241..9a51ef58 100644 Binary files a/doc/Successful-APRS-IGate-Operation.pdf and b/doc/Successful-APRS-IGate-Operation.pdf differ diff --git a/doc/User-Guide.pdf b/doc/User-Guide.pdf index d7010a17..319f882f 100644 Binary files a/doc/User-Guide.pdf and b/doc/User-Guide.pdf differ diff --git a/external/geotranz/mgrs.h b/external/geotranz/mgrs.h index 79a1c28e..bd0453a1 100644 --- a/external/geotranz/mgrs.h +++ b/external/geotranz/mgrs.h @@ -236,7 +236,7 @@ extern "C" { * The function Convert_MGRS_To_UPS converts an MGRS coordinate string * to UPS (hemisphere, easting, and northing) coordinates, according * to the current ellipsoid parameters. If any errors occur, the error - * code(s) are returned by the function, otherwide UPS_NO_ERROR is returned. + * code(s) are returned by the function, otherwise UPS_NO_ERROR is returned. * * MGRS : MGRS coordinate string (input) * Hemisphere : Hemisphere either 'N' or 'S' (output) diff --git a/external/hidapi/CMakeLists.txt b/external/hidapi/CMakeLists.txt new file mode 100644 index 00000000..c9dd66f1 --- /dev/null +++ b/external/hidapi/CMakeLists.txt @@ -0,0 +1,21 @@ +set(HIDAPI_LIBRARIES "" CACHE INTERNAL "") + +if(WIN32 OR CYGWIN) # windows + + set(HIDAPI_LIBRARIES hidapi CACHE INTERNAL "hidapi") + + list(APPEND hidapi_SOURCES + # Functions for accessing HID devices on Windows. + # These were copied from https://github.com/libusb/hidapi + ${CUSTOM_HIDAPI_DIR}/hid.c + ) + + add_library(hidapi STATIC + ${hidapi_SOURCES} + ) + + set_target_properties(hidapi + PROPERTIES COMPILE_FLAGS "-Dbool=int -Dtrue=1 -Dfalse=0 -DUSE_HIDAPI_STATIC" + ) + +endif() diff --git a/external/hidapi/LICENSE-bsd.txt b/external/hidapi/LICENSE-bsd.txt new file mode 100644 index 00000000..538cdf95 --- /dev/null +++ b/external/hidapi/LICENSE-bsd.txt @@ -0,0 +1,26 @@ +Copyright (c) 2010, Alan Ott, Signal 11 Software +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * Neither the name of Signal 11 Software nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. diff --git a/external/hidapi/LICENSE-gpl3.txt b/external/hidapi/LICENSE-gpl3.txt new file mode 100644 index 00000000..94a9ed02 --- /dev/null +++ b/external/hidapi/LICENSE-gpl3.txt @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + 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 3 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 . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/external/hidapi/LICENSE-orig.txt b/external/hidapi/LICENSE-orig.txt new file mode 100644 index 00000000..e3f33808 --- /dev/null +++ b/external/hidapi/LICENSE-orig.txt @@ -0,0 +1,9 @@ + HIDAPI - Multi-Platform library for + communication with HID devices. + + Copyright 2009, Alan Ott, Signal 11 Software. + All Rights Reserved. + + This software may be used by anyone for any reason so + long as the copyright notice in the source files + remains intact. diff --git a/external/hidapi/LICENSE.txt b/external/hidapi/LICENSE.txt new file mode 100644 index 00000000..e1676d4c --- /dev/null +++ b/external/hidapi/LICENSE.txt @@ -0,0 +1,13 @@ +HIDAPI can be used under one of three licenses. + +1. The GNU General Public License, version 3.0, in LICENSE-gpl3.txt +2. A BSD-Style License, in LICENSE-bsd.txt. +3. The more liberal original HIDAPI license. LICENSE-orig.txt + +The license chosen is at the discretion of the user of HIDAPI. For example: +1. An author of GPL software would likely use HIDAPI under the terms of the +GPL. + +2. An author of commercial closed-source software would likely use HIDAPI +under the terms of the BSD-style license or the original HIDAPI license. + diff --git a/external/hidapi/README.md b/external/hidapi/README.md new file mode 100644 index 00000000..6ced0f91 --- /dev/null +++ b/external/hidapi/README.md @@ -0,0 +1 @@ +This is from https://github.com/libusb/hidapi diff --git a/external/hidapi/hid.c b/external/hidapi/hid.c new file mode 100644 index 00000000..e483cd4f --- /dev/null +++ b/external/hidapi/hid.c @@ -0,0 +1,1091 @@ +/******************************************************* + HIDAPI - Multi-Platform library for + communication with HID devices. + + Alan Ott + Signal 11 Software + + 8/22/2009 + + Copyright 2009, All Rights Reserved. + + At the discretion of the user of this library, + this software may be licensed under the terms of the + GNU General Public License v3, a BSD-Style license, or the + original HIDAPI license as outlined in the LICENSE.txt, + LICENSE-gpl3.txt, LICENSE-bsd.txt, and LICENSE-orig.txt + files located at the root of the source distribution. + These files may also be found in the public source + code repository located at: + https://github.com/libusb/hidapi . +********************************************************/ + +#include + +#ifndef _NTDEF_ +typedef LONG NTSTATUS; +#endif + +#ifdef __MINGW32__ +#include +#include +#endif + +#ifdef __CYGWIN__ +#include +#define _wcsdup wcsdup +#endif + +/* The maximum number of characters that can be passed into the + HidD_Get*String() functions without it failing.*/ +#define MAX_STRING_WCHARS 0xFFF + +/*#define HIDAPI_USE_DDK*/ + +#ifdef __cplusplus +extern "C" { +#endif + #include + #include + #ifdef HIDAPI_USE_DDK + #include + #endif + + /* Copied from inc/ddk/hidclass.h, part of the Windows DDK. */ + #define HID_OUT_CTL_CODE(id) \ + CTL_CODE(FILE_DEVICE_KEYBOARD, (id), METHOD_OUT_DIRECT, FILE_ANY_ACCESS) + #define IOCTL_HID_GET_FEATURE HID_OUT_CTL_CODE(100) + #define IOCTL_HID_GET_INPUT_REPORT HID_OUT_CTL_CODE(104) + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#include +#include + + +#include "hidapi.h" + +#undef MIN +#define MIN(x,y) ((x) < (y)? (x): (y)) + +#ifdef _MSC_VER + /* Thanks Microsoft, but I know how to use strncpy(). */ + #pragma warning(disable:4996) +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +static struct hid_api_version api_version = { + .major = HID_API_VERSION_MAJOR, + .minor = HID_API_VERSION_MINOR, + .patch = HID_API_VERSION_PATCH +}; + +#ifndef HIDAPI_USE_DDK + /* Since we're not building with the DDK, and the HID header + files aren't part of the SDK, we have to define all this + stuff here. In lookup_functions(), the function pointers + defined below are set. */ + typedef struct _HIDD_ATTRIBUTES{ + ULONG Size; + USHORT VendorID; + USHORT ProductID; + USHORT VersionNumber; + } HIDD_ATTRIBUTES, *PHIDD_ATTRIBUTES; + + typedef USHORT USAGE; + typedef struct _HIDP_CAPS { + USAGE Usage; + USAGE UsagePage; + USHORT InputReportByteLength; + USHORT OutputReportByteLength; + USHORT FeatureReportByteLength; + USHORT Reserved[17]; + USHORT fields_not_used_by_hidapi[10]; + } HIDP_CAPS, *PHIDP_CAPS; + typedef void* PHIDP_PREPARSED_DATA; + #define HIDP_STATUS_SUCCESS 0x110000 + + typedef BOOLEAN (__stdcall *HidD_GetAttributes_)(HANDLE device, PHIDD_ATTRIBUTES attrib); + typedef BOOLEAN (__stdcall *HidD_GetSerialNumberString_)(HANDLE device, PVOID buffer, ULONG buffer_len); + typedef BOOLEAN (__stdcall *HidD_GetManufacturerString_)(HANDLE handle, PVOID buffer, ULONG buffer_len); + typedef BOOLEAN (__stdcall *HidD_GetProductString_)(HANDLE handle, PVOID buffer, ULONG buffer_len); + typedef BOOLEAN (__stdcall *HidD_SetFeature_)(HANDLE handle, PVOID data, ULONG length); + typedef BOOLEAN (__stdcall *HidD_GetFeature_)(HANDLE handle, PVOID data, ULONG length); + typedef BOOLEAN (__stdcall *HidD_GetInputReport_)(HANDLE handle, PVOID data, ULONG length); + typedef BOOLEAN (__stdcall *HidD_GetIndexedString_)(HANDLE handle, ULONG string_index, PVOID buffer, ULONG buffer_len); + typedef BOOLEAN (__stdcall *HidD_GetPreparsedData_)(HANDLE handle, PHIDP_PREPARSED_DATA *preparsed_data); + typedef BOOLEAN (__stdcall *HidD_FreePreparsedData_)(PHIDP_PREPARSED_DATA preparsed_data); + typedef NTSTATUS (__stdcall *HidP_GetCaps_)(PHIDP_PREPARSED_DATA preparsed_data, HIDP_CAPS *caps); + typedef BOOLEAN (__stdcall *HidD_SetNumInputBuffers_)(HANDLE handle, ULONG number_buffers); + + static HidD_GetAttributes_ HidD_GetAttributes; + static HidD_GetSerialNumberString_ HidD_GetSerialNumberString; + static HidD_GetManufacturerString_ HidD_GetManufacturerString; + static HidD_GetProductString_ HidD_GetProductString; + static HidD_SetFeature_ HidD_SetFeature; + static HidD_GetFeature_ HidD_GetFeature; + static HidD_GetInputReport_ HidD_GetInputReport; + static HidD_GetIndexedString_ HidD_GetIndexedString; + static HidD_GetPreparsedData_ HidD_GetPreparsedData; + static HidD_FreePreparsedData_ HidD_FreePreparsedData; + static HidP_GetCaps_ HidP_GetCaps; + static HidD_SetNumInputBuffers_ HidD_SetNumInputBuffers; + + static HMODULE lib_handle = NULL; + static BOOLEAN initialized = FALSE; +#endif /* HIDAPI_USE_DDK */ + +struct hid_device_ { + HANDLE device_handle; + BOOL blocking; + USHORT output_report_length; + size_t input_report_length; + USHORT feature_report_length; + unsigned char *feature_buf; + void *last_error_str; + DWORD last_error_num; + BOOL read_pending; + char *read_buf; + OVERLAPPED ol; + OVERLAPPED write_ol; +}; + +static hid_device *new_hid_device() +{ + hid_device *dev = (hid_device*) calloc(1, sizeof(hid_device)); + dev->device_handle = INVALID_HANDLE_VALUE; + dev->blocking = TRUE; + dev->output_report_length = 0; + dev->input_report_length = 0; + dev->feature_report_length = 0; + dev->feature_buf = NULL; + dev->last_error_str = NULL; + dev->last_error_num = 0; + dev->read_pending = FALSE; + dev->read_buf = NULL; + memset(&dev->ol, 0, sizeof(dev->ol)); + dev->ol.hEvent = CreateEvent(NULL, FALSE, FALSE /*initial state f=nonsignaled*/, NULL); + memset(&dev->write_ol, 0, sizeof(dev->write_ol)); + dev->write_ol.hEvent = CreateEvent(NULL, FALSE, FALSE /*inital state f=nonsignaled*/, NULL); + + return dev; +} + +static void free_hid_device(hid_device *dev) +{ + CloseHandle(dev->ol.hEvent); + CloseHandle(dev->write_ol.hEvent); + CloseHandle(dev->device_handle); + LocalFree(dev->last_error_str); + free(dev->feature_buf); + free(dev->read_buf); + free(dev); +} + +static void register_error(hid_device *dev, const char *op) +{ + WCHAR *ptr, *msg; + (void)op; // unreferenced param + FormatMessageW(FORMAT_MESSAGE_ALLOCATE_BUFFER | + FORMAT_MESSAGE_FROM_SYSTEM | + FORMAT_MESSAGE_IGNORE_INSERTS, + NULL, + GetLastError(), + MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), + (LPWSTR)&msg, 0/*sz*/, + NULL); + + /* Get rid of the CR and LF that FormatMessage() sticks at the + end of the message. Thanks Microsoft! */ + ptr = msg; + while (*ptr) { + if (*ptr == '\r') { + *ptr = 0x0000; + break; + } + ptr++; + } + + /* Store the message off in the Device entry so that + the hid_error() function can pick it up. */ + LocalFree(dev->last_error_str); + dev->last_error_str = msg; +} + +#ifndef HIDAPI_USE_DDK +static int lookup_functions() +{ + lib_handle = LoadLibraryA("hid.dll"); + if (lib_handle) { +#if defined(__GNUC__) +# pragma GCC diagnostic push +//# pragma GCC diagnostic ignored "-Wcast-function-type" +#endif +#define RESOLVE(x) x = (x##_)GetProcAddress(lib_handle, #x); if (!x) return -1; + RESOLVE(HidD_GetAttributes); + RESOLVE(HidD_GetSerialNumberString); + RESOLVE(HidD_GetManufacturerString); + RESOLVE(HidD_GetProductString); + RESOLVE(HidD_SetFeature); + RESOLVE(HidD_GetFeature); + RESOLVE(HidD_GetInputReport); + RESOLVE(HidD_GetIndexedString); + RESOLVE(HidD_GetPreparsedData); + RESOLVE(HidD_FreePreparsedData); + RESOLVE(HidP_GetCaps); + RESOLVE(HidD_SetNumInputBuffers); +#undef RESOLVE +#if defined(__GNUC__) +# pragma GCC diagnostic pop +#endif + } + else + return -1; + + return 0; +} +#endif + +static HANDLE open_device(const char *path, BOOL open_rw) +{ + HANDLE handle; + DWORD desired_access = (open_rw)? (GENERIC_WRITE | GENERIC_READ): 0; + DWORD share_mode = FILE_SHARE_READ|FILE_SHARE_WRITE; + + handle = CreateFileA(path, + desired_access, + share_mode, + NULL, + OPEN_EXISTING, + FILE_FLAG_OVERLAPPED,/*FILE_ATTRIBUTE_NORMAL,*/ + 0); + + return handle; +} + +HID_API_EXPORT const struct hid_api_version* HID_API_CALL hid_version() +{ + return &api_version; +} + +HID_API_EXPORT const char* HID_API_CALL hid_version_str() +{ + return HID_API_VERSION_STR; +} + +int HID_API_EXPORT hid_init(void) +{ +#ifndef HIDAPI_USE_DDK + if (!initialized) { + if (lookup_functions() < 0) { + hid_exit(); + return -1; + } + initialized = TRUE; + } +#endif + return 0; +} + +int HID_API_EXPORT hid_exit(void) +{ +#ifndef HIDAPI_USE_DDK + if (lib_handle) + FreeLibrary(lib_handle); + lib_handle = NULL; + initialized = FALSE; +#endif + return 0; +} + +struct hid_device_info HID_API_EXPORT * HID_API_CALL hid_enumerate(unsigned short vendor_id, unsigned short product_id) +{ + BOOL res; + struct hid_device_info *root = NULL; /* return object */ + struct hid_device_info *cur_dev = NULL; + + /* Windows objects for interacting with the driver. */ + GUID InterfaceClassGuid = {0x4d1e55b2, 0xf16f, 0x11cf, {0x88, 0xcb, 0x00, 0x11, 0x11, 0x00, 0x00, 0x30} }; + SP_DEVINFO_DATA devinfo_data; + SP_DEVICE_INTERFACE_DATA device_interface_data; + SP_DEVICE_INTERFACE_DETAIL_DATA_A *device_interface_detail_data = NULL; + HDEVINFO device_info_set = INVALID_HANDLE_VALUE; + int device_index = 0; + int i; + + if (hid_init() < 0) + return NULL; + + /* Initialize the Windows objects. */ + memset(&devinfo_data, 0x0, sizeof(devinfo_data)); + devinfo_data.cbSize = sizeof(SP_DEVINFO_DATA); + device_interface_data.cbSize = sizeof(SP_DEVICE_INTERFACE_DATA); + + /* Get information for all the devices belonging to the HID class. */ + device_info_set = SetupDiGetClassDevsA(&InterfaceClassGuid, NULL, NULL, DIGCF_PRESENT | DIGCF_DEVICEINTERFACE); + + /* Iterate over each device in the HID class, looking for the right one. */ + + for (;;) { + HANDLE write_handle = INVALID_HANDLE_VALUE; + DWORD required_size = 0; + HIDD_ATTRIBUTES attrib; + + res = SetupDiEnumDeviceInterfaces(device_info_set, + NULL, + &InterfaceClassGuid, + device_index, + &device_interface_data); + + if (!res) { + /* A return of FALSE from this function means that + there are no more devices. */ + break; + } + + /* Call with 0-sized detail size, and let the function + tell us how long the detail struct needs to be. The + size is put in &required_size. */ + res = SetupDiGetDeviceInterfaceDetailA(device_info_set, + &device_interface_data, + NULL, + 0, + &required_size, + NULL); + + /* Allocate a long enough structure for device_interface_detail_data. */ + device_interface_detail_data = (SP_DEVICE_INTERFACE_DETAIL_DATA_A*) malloc(required_size); + device_interface_detail_data->cbSize = sizeof(SP_DEVICE_INTERFACE_DETAIL_DATA_A); + + /* Get the detailed data for this device. The detail data gives us + the device path for this device, which is then passed into + CreateFile() to get a handle to the device. */ + res = SetupDiGetDeviceInterfaceDetailA(device_info_set, + &device_interface_data, + device_interface_detail_data, + required_size, + NULL, + NULL); + + if (!res) { + /* register_error(dev, "Unable to call SetupDiGetDeviceInterfaceDetail"); + Continue to the next device. */ + goto cont; + } + + /* Make sure this device is of Setup Class "HIDClass" and has a + driver bound to it. */ + for (i = 0; ; i++) { + char driver_name[256]; + + /* Populate devinfo_data. This function will return failure + when there are no more interfaces left. */ + res = SetupDiEnumDeviceInfo(device_info_set, i, &devinfo_data); + if (!res) + goto cont; + + res = SetupDiGetDeviceRegistryPropertyA(device_info_set, &devinfo_data, + SPDRP_CLASS, NULL, (PBYTE)driver_name, sizeof(driver_name), NULL); + if (!res) + goto cont; + + if ((strcmp(driver_name, "HIDClass") == 0) || + (strcmp(driver_name, "Mouse") == 0) || + (strcmp(driver_name, "Keyboard") == 0)) { + /* See if there's a driver bound. */ + res = SetupDiGetDeviceRegistryPropertyA(device_info_set, &devinfo_data, + SPDRP_DRIVER, NULL, (PBYTE)driver_name, sizeof(driver_name), NULL); + if (res) + break; + } + } + + //wprintf(L"HandleName: %s\n", device_interface_detail_data->DevicePath); + + /* Open a handle to the device */ + write_handle = open_device(device_interface_detail_data->DevicePath, FALSE); + + /* Check validity of write_handle. */ + if (write_handle == INVALID_HANDLE_VALUE) { + /* Unable to open the device. */ + //register_error(dev, "CreateFile"); + goto cont_close; + } + + + /* Get the Vendor ID and Product ID for this device. */ + attrib.Size = sizeof(HIDD_ATTRIBUTES); + HidD_GetAttributes(write_handle, &attrib); + //wprintf(L"Product/Vendor: %x %x\n", attrib.ProductID, attrib.VendorID); + + /* Check the VID/PID to see if we should add this + device to the enumeration list. */ + if ((vendor_id == 0x0 || attrib.VendorID == vendor_id) && + (product_id == 0x0 || attrib.ProductID == product_id)) { + + #define WSTR_LEN 512 + const char *str; + struct hid_device_info *tmp; + PHIDP_PREPARSED_DATA pp_data = NULL; + HIDP_CAPS caps; + NTSTATUS nt_res; + wchar_t wstr[WSTR_LEN]; /* TODO: Determine Size */ + size_t len; + + /* VID/PID match. Create the record. */ + tmp = (struct hid_device_info*) calloc(1, sizeof(struct hid_device_info)); + if (cur_dev) { + cur_dev->next = tmp; + } + else { + root = tmp; + } + cur_dev = tmp; + + /* Get the Usage Page and Usage for this device. */ + res = HidD_GetPreparsedData(write_handle, &pp_data); + if (res) { + nt_res = HidP_GetCaps(pp_data, &caps); + if (nt_res == HIDP_STATUS_SUCCESS) { + cur_dev->usage_page = caps.UsagePage; + cur_dev->usage = caps.Usage; + } + + HidD_FreePreparsedData(pp_data); + } + + /* Fill out the record */ + cur_dev->next = NULL; + str = device_interface_detail_data->DevicePath; + if (str) { + len = strlen(str); + cur_dev->path = (char*) calloc(len+1, sizeof(char)); + strncpy(cur_dev->path, str, len+1); + cur_dev->path[len] = '\0'; + } + else + cur_dev->path = NULL; + + /* Serial Number */ + wstr[0]= 0x0000; + res = HidD_GetSerialNumberString(write_handle, wstr, sizeof(wstr)); + wstr[WSTR_LEN-1] = 0x0000; + if (res) { + cur_dev->serial_number = _wcsdup(wstr); + } + + /* Manufacturer String */ + wstr[0]= 0x0000; + res = HidD_GetManufacturerString(write_handle, wstr, sizeof(wstr)); + wstr[WSTR_LEN-1] = 0x0000; + if (res) { + cur_dev->manufacturer_string = _wcsdup(wstr); + } + + /* Product String */ + wstr[0]= 0x0000; + res = HidD_GetProductString(write_handle, wstr, sizeof(wstr)); + wstr[WSTR_LEN-1] = 0x0000; + if (res) { + cur_dev->product_string = _wcsdup(wstr); + } + + /* VID/PID */ + cur_dev->vendor_id = attrib.VendorID; + cur_dev->product_id = attrib.ProductID; + + /* Release Number */ + cur_dev->release_number = attrib.VersionNumber; + + /* Interface Number. It can sometimes be parsed out of the path + on Windows if a device has multiple interfaces. See + http://msdn.microsoft.com/en-us/windows/hardware/gg487473 or + search for "Hardware IDs for HID Devices" at MSDN. If it's not + in the path, it's set to -1. */ + cur_dev->interface_number = -1; + if (cur_dev->path) { + char *interface_component = strstr(cur_dev->path, "&mi_"); + if (interface_component) { + char *hex_str = interface_component + 4; + char *endptr = NULL; + cur_dev->interface_number = strtol(hex_str, &endptr, 16); + if (endptr == hex_str) { + /* The parsing failed. Set interface_number to -1. */ + cur_dev->interface_number = -1; + } + } + } + } + +cont_close: + CloseHandle(write_handle); +cont: + /* We no longer need the detail data. It can be freed */ + free(device_interface_detail_data); + + device_index++; + + } + + /* Close the device information handle. */ + SetupDiDestroyDeviceInfoList(device_info_set); + + return root; + +} + +void HID_API_EXPORT HID_API_CALL hid_free_enumeration(struct hid_device_info *devs) +{ + /* TODO: Merge this with the Linux version. This function is platform-independent. */ + struct hid_device_info *d = devs; + while (d) { + struct hid_device_info *next = d->next; + free(d->path); + free(d->serial_number); + free(d->manufacturer_string); + free(d->product_string); + free(d); + d = next; + } +} + + +HID_API_EXPORT hid_device * HID_API_CALL hid_open(unsigned short vendor_id, unsigned short product_id, const wchar_t *serial_number) +{ + /* TODO: Merge this functions with the Linux version. This function should be platform independent. */ + struct hid_device_info *devs, *cur_dev; + const char *path_to_open = NULL; + hid_device *handle = NULL; + + devs = hid_enumerate(vendor_id, product_id); + cur_dev = devs; + while (cur_dev) { + if (cur_dev->vendor_id == vendor_id && + cur_dev->product_id == product_id) { + if (serial_number) { + if (cur_dev->serial_number && wcscmp(serial_number, cur_dev->serial_number) == 0) { + path_to_open = cur_dev->path; + break; + } + } + else { + path_to_open = cur_dev->path; + break; + } + } + cur_dev = cur_dev->next; + } + + if (path_to_open) { + /* Open the device */ + handle = hid_open_path(path_to_open); + } + + hid_free_enumeration(devs); + + return handle; +} + +HID_API_EXPORT hid_device * HID_API_CALL hid_open_path(const char *path) +{ + hid_device *dev; + HIDP_CAPS caps; + PHIDP_PREPARSED_DATA pp_data = NULL; + BOOLEAN res; + NTSTATUS nt_res; + + if (hid_init() < 0) { + return NULL; + } + + dev = new_hid_device(); + + /* Open a handle to the device */ + dev->device_handle = open_device(path, TRUE); + + /* Check validity of write_handle. */ + if (dev->device_handle == INVALID_HANDLE_VALUE) { + /* System devices, such as keyboards and mice, cannot be opened in + read-write mode, because the system takes exclusive control over + them. This is to prevent keyloggers. However, feature reports + can still be sent and received. Retry opening the device, but + without read/write access. */ + dev->device_handle = open_device(path, FALSE); + + /* Check the validity of the limited device_handle. */ + if (dev->device_handle == INVALID_HANDLE_VALUE) { + /* Unable to open the device, even without read-write mode. */ + register_error(dev, "CreateFile"); + goto err; + } + } + + /* Set the Input Report buffer size to 64 reports. */ + res = HidD_SetNumInputBuffers(dev->device_handle, 64); + if (!res) { + register_error(dev, "HidD_SetNumInputBuffers"); + goto err; + } + + /* Get the Input Report length for the device. */ + res = HidD_GetPreparsedData(dev->device_handle, &pp_data); + if (!res) { + register_error(dev, "HidD_GetPreparsedData"); + goto err; + } + nt_res = HidP_GetCaps(pp_data, &caps); + if (nt_res != HIDP_STATUS_SUCCESS) { + register_error(dev, "HidP_GetCaps"); + goto err_pp_data; + } + dev->output_report_length = caps.OutputReportByteLength; + dev->input_report_length = caps.InputReportByteLength; + dev->feature_report_length = caps.FeatureReportByteLength; + HidD_FreePreparsedData(pp_data); + + dev->read_buf = (char*) malloc(dev->input_report_length); + + return dev; + +err_pp_data: + HidD_FreePreparsedData(pp_data); +err: + free_hid_device(dev); + return NULL; +} + +int HID_API_EXPORT HID_API_CALL hid_write(hid_device *dev, const unsigned char *data, size_t length) +{ + DWORD bytes_written = 0; + int function_result = -1; + BOOL res; + BOOL overlapped = FALSE; + + unsigned char *buf; + + /* Make sure the right number of bytes are passed to WriteFile. Windows + expects the number of bytes which are in the _longest_ report (plus + one for the report number) bytes even if the data is a report + which is shorter than that. Windows gives us this value in + caps.OutputReportByteLength. If a user passes in fewer bytes than this, + create a temporary buffer which is the proper size. */ + if (length >= dev->output_report_length) { + /* The user passed the right number of bytes. Use the buffer as-is. */ + buf = (unsigned char *) data; + } else { + /* Create a temporary buffer and copy the user's data + into it, padding the rest with zeros. */ + buf = (unsigned char *) malloc(dev->output_report_length); + memcpy(buf, data, length); + memset(buf + length, 0, dev->output_report_length - length); + length = dev->output_report_length; + } + + res = WriteFile(dev->device_handle, buf, (DWORD) length, NULL, &dev->write_ol); + + if (!res) { + if (GetLastError() != ERROR_IO_PENDING) { + /* WriteFile() failed. Return error. */ + register_error(dev, "WriteFile"); + goto end_of_function; + } + overlapped = TRUE; + } + + if (overlapped) { + /* Wait for the transaction to complete. This makes + hid_write() synchronous. */ + res = WaitForSingleObject(dev->write_ol.hEvent, 1000); + if (res != WAIT_OBJECT_0) { + /* There was a Timeout. */ + register_error(dev, "WriteFile/WaitForSingleObject Timeout"); + goto end_of_function; + } + + /* Get the result. */ + res = GetOverlappedResult(dev->device_handle, &dev->write_ol, &bytes_written, FALSE/*wait*/); + if (res) { + function_result = bytes_written; + } + else { + /* The Write operation failed. */ + register_error(dev, "WriteFile"); + goto end_of_function; + } + } + +end_of_function: + if (buf != data) + free(buf); + + return function_result; +} + + +int HID_API_EXPORT HID_API_CALL hid_read_timeout(hid_device *dev, unsigned char *data, size_t length, int milliseconds) +{ + DWORD bytes_read = 0; + size_t copy_len = 0; + BOOL res = FALSE; + BOOL overlapped = FALSE; + + /* Copy the handle for convenience. */ + HANDLE ev = dev->ol.hEvent; + + if (!dev->read_pending) { + /* Start an Overlapped I/O read. */ + dev->read_pending = TRUE; + memset(dev->read_buf, 0, dev->input_report_length); + ResetEvent(ev); + res = ReadFile(dev->device_handle, dev->read_buf, (DWORD) dev->input_report_length, &bytes_read, &dev->ol); + + if (!res) { + if (GetLastError() != ERROR_IO_PENDING) { + /* ReadFile() has failed. + Clean up and return error. */ + CancelIo(dev->device_handle); + dev->read_pending = FALSE; + goto end_of_function; + } + overlapped = TRUE; + } + } + else { + overlapped = TRUE; + } + + if (overlapped) { + if (milliseconds >= 0) { + /* See if there is any data yet. */ + res = WaitForSingleObject(ev, milliseconds); + if (res != WAIT_OBJECT_0) { + /* There was no data this time. Return zero bytes available, + but leave the Overlapped I/O running. */ + return 0; + } + } + + /* Either WaitForSingleObject() told us that ReadFile has completed, or + we are in non-blocking mode. Get the number of bytes read. The actual + data has been copied to the data[] array which was passed to ReadFile(). */ + res = GetOverlappedResult(dev->device_handle, &dev->ol, &bytes_read, TRUE/*wait*/); + } + /* Set pending back to false, even if GetOverlappedResult() returned error. */ + dev->read_pending = FALSE; + + if (res && bytes_read > 0) { + if (dev->read_buf[0] == 0x0) { + /* If report numbers aren't being used, but Windows sticks a report + number (0x0) on the beginning of the report anyway. To make this + work like the other platforms, and to make it work more like the + HID spec, we'll skip over this byte. */ + bytes_read--; + copy_len = length > bytes_read ? bytes_read : length; + memcpy(data, dev->read_buf+1, copy_len); + } + else { + /* Copy the whole buffer, report number and all. */ + copy_len = length > bytes_read ? bytes_read : length; + memcpy(data, dev->read_buf, copy_len); + } + } + +end_of_function: + if (!res) { + register_error(dev, "GetOverlappedResult"); + return -1; + } + + return (int) copy_len; +} + +int HID_API_EXPORT HID_API_CALL hid_read(hid_device *dev, unsigned char *data, size_t length) +{ + return hid_read_timeout(dev, data, length, (dev->blocking)? -1: 0); +} + +int HID_API_EXPORT HID_API_CALL hid_set_nonblocking(hid_device *dev, int nonblock) +{ + dev->blocking = !nonblock; + return 0; /* Success */ +} + +int HID_API_EXPORT HID_API_CALL hid_send_feature_report(hid_device *dev, const unsigned char *data, size_t length) +{ + BOOL res = FALSE; + unsigned char *buf; + size_t length_to_send; + + /* Windows expects at least caps.FeatureReportByteLength bytes passed + to HidD_SetFeature(), even if the report is shorter. Any less sent and + the function fails with error ERROR_INVALID_PARAMETER set. Any more + and HidD_SetFeature() silently truncates the data sent in the report + to caps.FeatureReportByteLength. */ + if (length >= dev->feature_report_length) { + buf = (unsigned char *) data; + length_to_send = length; + } else { + if (dev->feature_buf == NULL) + dev->feature_buf = (unsigned char *) malloc(dev->feature_report_length); + buf = dev->feature_buf; + memcpy(buf, data, length); + memset(buf + length, 0, dev->feature_report_length - length); + length_to_send = dev->feature_report_length; + } + + res = HidD_SetFeature(dev->device_handle, (PVOID)buf, (DWORD) length_to_send); + + if (!res) { + register_error(dev, "HidD_SetFeature"); + return -1; + } + + return (int) length; +} + + +int HID_API_EXPORT HID_API_CALL hid_get_feature_report(hid_device *dev, unsigned char *data, size_t length) +{ + BOOL res; +#if 0 + res = HidD_GetFeature(dev->device_handle, data, length); + if (!res) { + register_error(dev, "HidD_GetFeature"); + return -1; + } + return 0; /* HidD_GetFeature() doesn't give us an actual length, unfortunately */ +#else + DWORD bytes_returned; + + OVERLAPPED ol; + memset(&ol, 0, sizeof(ol)); + + res = DeviceIoControl(dev->device_handle, + IOCTL_HID_GET_FEATURE, + data, (DWORD) length, + data, (DWORD) length, + &bytes_returned, &ol); + + if (!res) { + if (GetLastError() != ERROR_IO_PENDING) { + /* DeviceIoControl() failed. Return error. */ + register_error(dev, "Send Feature Report DeviceIoControl"); + return -1; + } + } + + /* Wait here until the write is done. This makes + hid_get_feature_report() synchronous. */ + res = GetOverlappedResult(dev->device_handle, &ol, &bytes_returned, TRUE/*wait*/); + if (!res) { + /* The operation failed. */ + register_error(dev, "Send Feature Report GetOverLappedResult"); + return -1; + } + + /* bytes_returned does not include the first byte which contains the + report ID. The data buffer actually contains one more byte than + bytes_returned. */ + bytes_returned++; + + return bytes_returned; +#endif +} + + +int HID_API_EXPORT HID_API_CALL hid_get_input_report(hid_device *dev, unsigned char *data, size_t length) +{ + BOOL res; +#if 0 + res = HidD_GetInputReport(dev->device_handle, data, length); + if (!res) { + register_error(dev, "HidD_GetInputReport"); + return -1; + } + return length; +#else + DWORD bytes_returned; + + OVERLAPPED ol; + memset(&ol, 0, sizeof(ol)); + + res = DeviceIoControl(dev->device_handle, + IOCTL_HID_GET_INPUT_REPORT, + data, (DWORD) length, + data, (DWORD) length, + &bytes_returned, &ol); + + if (!res) { + if (GetLastError() != ERROR_IO_PENDING) { + /* DeviceIoControl() failed. Return error. */ + register_error(dev, "Send Input Report DeviceIoControl"); + return -1; + } + } + + /* Wait here until the write is done. This makes + hid_get_feature_report() synchronous. */ + res = GetOverlappedResult(dev->device_handle, &ol, &bytes_returned, TRUE/*wait*/); + if (!res) { + /* The operation failed. */ + register_error(dev, "Send Input Report GetOverLappedResult"); + return -1; + } + + /* bytes_returned does not include the first byte which contains the + report ID. The data buffer actually contains one more byte than + bytes_returned. */ + bytes_returned++; + + return bytes_returned; +#endif +} + +void HID_API_EXPORT HID_API_CALL hid_close(hid_device *dev) +{ + if (!dev) + return; + CancelIo(dev->device_handle); + free_hid_device(dev); +} + +int HID_API_EXPORT_CALL HID_API_CALL hid_get_manufacturer_string(hid_device *dev, wchar_t *string, size_t maxlen) +{ + BOOL res; + + res = HidD_GetManufacturerString(dev->device_handle, string, sizeof(wchar_t) * (DWORD) MIN(maxlen, MAX_STRING_WCHARS)); + if (!res) { + register_error(dev, "HidD_GetManufacturerString"); + return -1; + } + + return 0; +} + +int HID_API_EXPORT_CALL HID_API_CALL hid_get_product_string(hid_device *dev, wchar_t *string, size_t maxlen) +{ + BOOL res; + + res = HidD_GetProductString(dev->device_handle, string, sizeof(wchar_t) * (DWORD) MIN(maxlen, MAX_STRING_WCHARS)); + if (!res) { + register_error(dev, "HidD_GetProductString"); + return -1; + } + + return 0; +} + +int HID_API_EXPORT_CALL HID_API_CALL hid_get_serial_number_string(hid_device *dev, wchar_t *string, size_t maxlen) +{ + BOOL res; + + res = HidD_GetSerialNumberString(dev->device_handle, string, sizeof(wchar_t) * (DWORD) MIN(maxlen, MAX_STRING_WCHARS)); + if (!res) { + register_error(dev, "HidD_GetSerialNumberString"); + return -1; + } + + return 0; +} + +int HID_API_EXPORT_CALL HID_API_CALL hid_get_indexed_string(hid_device *dev, int string_index, wchar_t *string, size_t maxlen) +{ + BOOL res; + + res = HidD_GetIndexedString(dev->device_handle, string_index, string, sizeof(wchar_t) * (DWORD) MIN(maxlen, MAX_STRING_WCHARS)); + if (!res) { + register_error(dev, "HidD_GetIndexedString"); + return -1; + } + + return 0; +} + + +HID_API_EXPORT const wchar_t * HID_API_CALL hid_error(hid_device *dev) +{ + if (dev) { + if (dev->last_error_str == NULL) + return L"Success"; + return (wchar_t*)dev->last_error_str; + } + + // Global error messages are not (yet) implemented on Windows. + return L"hid_error for global errors is not implemented yet"; +} + + +/*#define PICPGM*/ +/*#define S11*/ +#define P32 +#ifdef S11 + unsigned short VendorID = 0xa0a0; + unsigned short ProductID = 0x0001; +#endif + +#ifdef P32 + unsigned short VendorID = 0x04d8; + unsigned short ProductID = 0x3f; +#endif + + +#ifdef PICPGM + unsigned short VendorID = 0x04d8; + unsigned short ProductID = 0x0033; +#endif + + +#if 0 +int __cdecl main(int argc, char* argv[]) +{ + int res; + unsigned char buf[65]; + + UNREFERENCED_PARAMETER(argc); + UNREFERENCED_PARAMETER(argv); + + /* Set up the command buffer. */ + memset(buf,0x00,sizeof(buf)); + buf[0] = 0; + buf[1] = 0x81; + + + /* Open the device. */ + int handle = open(VendorID, ProductID, L"12345"); + if (handle < 0) + printf("unable to open device\n"); + + + /* Toggle LED (cmd 0x80) */ + buf[1] = 0x80; + res = write(handle, buf, 65); + if (res < 0) + printf("Unable to write()\n"); + + /* Request state (cmd 0x81) */ + buf[1] = 0x81; + write(handle, buf, 65); + if (res < 0) + printf("Unable to write() (2)\n"); + + /* Read requested state */ + read(handle, buf, 65); + if (res < 0) + printf("Unable to read()\n"); + + /* Print out the returned buffer. */ + for (int i = 0; i < 4; i++) + printf("buf[%d]: %d\n", i, buf[i]); + + return 0; +} +#endif + +#ifdef __cplusplus +} /* extern "C" */ +#endif diff --git a/external/hidapi/hidapi.h b/external/hidapi/hidapi.h new file mode 100644 index 00000000..efba3188 --- /dev/null +++ b/external/hidapi/hidapi.h @@ -0,0 +1,498 @@ +/******************************************************* + HIDAPI - Multi-Platform library for + communication with HID devices. + + Alan Ott + Signal 11 Software + + 8/22/2009 + + Copyright 2009, All Rights Reserved. + + At the discretion of the user of this library, + this software may be licensed under the terms of the + GNU General Public License v3, a BSD-Style license, or the + original HIDAPI license as outlined in the LICENSE.txt, + LICENSE-gpl3.txt, LICENSE-bsd.txt, and LICENSE-orig.txt + files located at the root of the source distribution. + These files may also be found in the public source + code repository located at: + https://github.com/libusb/hidapi . +********************************************************/ + +/** @file + * @defgroup API hidapi API + */ + +#ifndef HIDAPI_H__ +#define HIDAPI_H__ + +#include + +#ifdef _WIN32 + #define HID_API_EXPORT __declspec(dllexport) + #define HID_API_CALL +#else + #define HID_API_EXPORT /**< API export macro */ + #define HID_API_CALL /**< API call macro */ +#endif + +#define HID_API_EXPORT_CALL HID_API_EXPORT HID_API_CALL /**< API export and call macro*/ + +/** @brief Static/compile-time major version of the library. + + @ingroup API +*/ +#define HID_API_VERSION_MAJOR 0 +/** @brief Static/compile-time minor version of the library. + + @ingroup API +*/ +#define HID_API_VERSION_MINOR 10 +/** @brief Static/compile-time patch version of the library. + + @ingroup API +*/ +#define HID_API_VERSION_PATCH 1 + +/* Helper macros */ +#define HID_API_AS_STR_IMPL(x) #x +#define HID_API_AS_STR(x) HID_API_AS_STR_IMPL(x) +#define HID_API_TO_VERSION_STR(v1, v2, v3) HID_API_AS_STR(v1.v2.v3) + +/** @brief Static/compile-time string version of the library. + + @ingroup API +*/ +#define HID_API_VERSION_STR HID_API_TO_VERSION_STR(HID_API_VERSION_MAJOR, HID_API_VERSION_MINOR, HID_API_VERSION_PATCH) + +#ifdef __cplusplus +extern "C" { +#endif + struct hid_api_version { + int major; + int minor; + int patch; + }; + + struct hid_device_; + typedef struct hid_device_ hid_device; /**< opaque hidapi structure */ + + /** hidapi info structure */ + struct hid_device_info { + /** Platform-specific device path */ + char *path; + /** Device Vendor ID */ + unsigned short vendor_id; + /** Device Product ID */ + unsigned short product_id; + /** Serial Number */ + wchar_t *serial_number; + /** Device Release Number in binary-coded decimal, + also known as Device Version Number */ + unsigned short release_number; + /** Manufacturer String */ + wchar_t *manufacturer_string; + /** Product string */ + wchar_t *product_string; + /** Usage Page for this Device/Interface + (Windows/Mac/hidraw only) */ + unsigned short usage_page; + /** Usage for this Device/Interface + (Windows/Mac/hidraw only) */ + unsigned short usage; + /** The USB interface which this logical device + represents. + + * Valid on both Linux implementations in all cases. + * Valid on the Windows implementation only if the device + contains more than one interface. + * Valid on the Mac implementation if and only if the device + is a USB HID device. */ + int interface_number; + + /** Pointer to the next device */ + struct hid_device_info *next; + }; + + + /** @brief Initialize the HIDAPI library. + + This function initializes the HIDAPI library. Calling it is not + strictly necessary, as it will be called automatically by + hid_enumerate() and any of the hid_open_*() functions if it is + needed. This function should be called at the beginning of + execution however, if there is a chance of HIDAPI handles + being opened by different threads simultaneously. + + @ingroup API + + @returns + This function returns 0 on success and -1 on error. + */ + int HID_API_EXPORT HID_API_CALL hid_init(void); + + /** @brief Finalize the HIDAPI library. + + This function frees all of the static data associated with + HIDAPI. It should be called at the end of execution to avoid + memory leaks. + + @ingroup API + + @returns + This function returns 0 on success and -1 on error. + */ + int HID_API_EXPORT HID_API_CALL hid_exit(void); + + /** @brief Enumerate the HID Devices. + + This function returns a linked list of all the HID devices + attached to the system which match vendor_id and product_id. + If @p vendor_id is set to 0 then any vendor matches. + If @p product_id is set to 0 then any product matches. + If @p vendor_id and @p product_id are both set to 0, then + all HID devices will be returned. + + @ingroup API + @param vendor_id The Vendor ID (VID) of the types of device + to open. + @param product_id The Product ID (PID) of the types of + device to open. + + @returns + This function returns a pointer to a linked list of type + struct #hid_device_info, containing information about the HID devices + attached to the system, or NULL in the case of failure. Free + this linked list by calling hid_free_enumeration(). + */ + struct hid_device_info HID_API_EXPORT * HID_API_CALL hid_enumerate(unsigned short vendor_id, unsigned short product_id); + + /** @brief Free an enumeration Linked List + + This function frees a linked list created by hid_enumerate(). + + @ingroup API + @param devs Pointer to a list of struct_device returned from + hid_enumerate(). + */ + void HID_API_EXPORT HID_API_CALL hid_free_enumeration(struct hid_device_info *devs); + + /** @brief Open a HID device using a Vendor ID (VID), Product ID + (PID) and optionally a serial number. + + If @p serial_number is NULL, the first device with the + specified VID and PID is opened. + + This function sets the return value of hid_error(). + + @ingroup API + @param vendor_id The Vendor ID (VID) of the device to open. + @param product_id The Product ID (PID) of the device to open. + @param serial_number The Serial Number of the device to open + (Optionally NULL). + + @returns + This function returns a pointer to a #hid_device object on + success or NULL on failure. + */ + HID_API_EXPORT hid_device * HID_API_CALL hid_open(unsigned short vendor_id, unsigned short product_id, const wchar_t *serial_number); + + /** @brief Open a HID device by its path name. + + The path name be determined by calling hid_enumerate(), or a + platform-specific path name can be used (eg: /dev/hidraw0 on + Linux). + + This function sets the return value of hid_error(). + + @ingroup API + @param path The path name of the device to open + + @returns + This function returns a pointer to a #hid_device object on + success or NULL on failure. + */ + HID_API_EXPORT hid_device * HID_API_CALL hid_open_path(const char *path); + + /** @brief Write an Output report to a HID device. + + The first byte of @p data[] must contain the Report ID. For + devices which only support a single report, this must be set + to 0x0. The remaining bytes contain the report data. Since + the Report ID is mandatory, calls to hid_write() will always + contain one more byte than the report contains. For example, + if a hid report is 16 bytes long, 17 bytes must be passed to + hid_write(), the Report ID (or 0x0, for devices with a + single report), followed by the report data (16 bytes). In + this example, the length passed in would be 17. + + hid_write() will send the data on the first OUT endpoint, if + one exists. If it does not, it will send the data through + the Control Endpoint (Endpoint 0). + + This function sets the return value of hid_error(). + + @ingroup API + @param dev A device handle returned from hid_open(). + @param data The data to send, including the report number as + the first byte. + @param length The length in bytes of the data to send. + + @returns + This function returns the actual number of bytes written and + -1 on error. + */ + int HID_API_EXPORT HID_API_CALL hid_write(hid_device *dev, const unsigned char *data, size_t length); + + /** @brief Read an Input report from a HID device with timeout. + + Input reports are returned + to the host through the INTERRUPT IN endpoint. The first byte will + contain the Report number if the device uses numbered reports. + + This function sets the return value of hid_error(). + + @ingroup API + @param dev A device handle returned from hid_open(). + @param data A buffer to put the read data into. + @param length The number of bytes to read. For devices with + multiple reports, make sure to read an extra byte for + the report number. + @param milliseconds timeout in milliseconds or -1 for blocking wait. + + @returns + This function returns the actual number of bytes read and + -1 on error. If no packet was available to be read within + the timeout period, this function returns 0. + */ + int HID_API_EXPORT HID_API_CALL hid_read_timeout(hid_device *dev, unsigned char *data, size_t length, int milliseconds); + + /** @brief Read an Input report from a HID device. + + Input reports are returned + to the host through the INTERRUPT IN endpoint. The first byte will + contain the Report number if the device uses numbered reports. + + This function sets the return value of hid_error(). + + @ingroup API + @param dev A device handle returned from hid_open(). + @param data A buffer to put the read data into. + @param length The number of bytes to read. For devices with + multiple reports, make sure to read an extra byte for + the report number. + + @returns + This function returns the actual number of bytes read and + -1 on error. If no packet was available to be read and + the handle is in non-blocking mode, this function returns 0. + */ + int HID_API_EXPORT HID_API_CALL hid_read(hid_device *dev, unsigned char *data, size_t length); + + /** @brief Set the device handle to be non-blocking. + + In non-blocking mode calls to hid_read() will return + immediately with a value of 0 if there is no data to be + read. In blocking mode, hid_read() will wait (block) until + there is data to read before returning. + + Nonblocking can be turned on and off at any time. + + @ingroup API + @param dev A device handle returned from hid_open(). + @param nonblock enable or not the nonblocking reads + - 1 to enable nonblocking + - 0 to disable nonblocking. + + @returns + This function returns 0 on success and -1 on error. + */ + int HID_API_EXPORT HID_API_CALL hid_set_nonblocking(hid_device *dev, int nonblock); + + /** @brief Send a Feature report to the device. + + Feature reports are sent over the Control endpoint as a + Set_Report transfer. The first byte of @p data[] must + contain the Report ID. For devices which only support a + single report, this must be set to 0x0. The remaining bytes + contain the report data. Since the Report ID is mandatory, + calls to hid_send_feature_report() will always contain one + more byte than the report contains. For example, if a hid + report is 16 bytes long, 17 bytes must be passed to + hid_send_feature_report(): the Report ID (or 0x0, for + devices which do not use numbered reports), followed by the + report data (16 bytes). In this example, the length passed + in would be 17. + + This function sets the return value of hid_error(). + + @ingroup API + @param dev A device handle returned from hid_open(). + @param data The data to send, including the report number as + the first byte. + @param length The length in bytes of the data to send, including + the report number. + + @returns + This function returns the actual number of bytes written and + -1 on error. + */ + int HID_API_EXPORT HID_API_CALL hid_send_feature_report(hid_device *dev, const unsigned char *data, size_t length); + + /** @brief Get a feature report from a HID device. + + Set the first byte of @p data[] to the Report ID of the + report to be read. Make sure to allow space for this + extra byte in @p data[]. Upon return, the first byte will + still contain the Report ID, and the report data will + start in data[1]. + + This function sets the return value of hid_error(). + + @ingroup API + @param dev A device handle returned from hid_open(). + @param data A buffer to put the read data into, including + the Report ID. Set the first byte of @p data[] to the + Report ID of the report to be read, or set it to zero + if your device does not use numbered reports. + @param length The number of bytes to read, including an + extra byte for the report ID. The buffer can be longer + than the actual report. + + @returns + This function returns the number of bytes read plus + one for the report ID (which is still in the first + byte), or -1 on error. + */ + int HID_API_EXPORT HID_API_CALL hid_get_feature_report(hid_device *dev, unsigned char *data, size_t length); + + /** @brief Get a input report from a HID device. + + Set the first byte of @p data[] to the Report ID of the + report to be read. Make sure to allow space for this + extra byte in @p data[]. Upon return, the first byte will + still contain the Report ID, and the report data will + start in data[1]. + + @ingroup API + @param device A device handle returned from hid_open(). + @param data A buffer to put the read data into, including + the Report ID. Set the first byte of @p data[] to the + Report ID of the report to be read, or set it to zero + if your device does not use numbered reports. + @param length The number of bytes to read, including an + extra byte for the report ID. The buffer can be longer + than the actual report. + + @returns + This function returns the number of bytes read plus + one for the report ID (which is still in the first + byte), or -1 on error. + */ + int HID_API_EXPORT HID_API_CALL hid_get_input_report(hid_device *dev, unsigned char *data, size_t length); + + /** @brief Close a HID device. + + This function sets the return value of hid_error(). + + @ingroup API + @param dev A device handle returned from hid_open(). + */ + void HID_API_EXPORT HID_API_CALL hid_close(hid_device *dev); + + /** @brief Get The Manufacturer String from a HID device. + + @ingroup API + @param dev A device handle returned from hid_open(). + @param string A wide string buffer to put the data into. + @param maxlen The length of the buffer in multiples of wchar_t. + + @returns + This function returns 0 on success and -1 on error. + */ + int HID_API_EXPORT_CALL hid_get_manufacturer_string(hid_device *dev, wchar_t *string, size_t maxlen); + + /** @brief Get The Product String from a HID device. + + @ingroup API + @param dev A device handle returned from hid_open(). + @param string A wide string buffer to put the data into. + @param maxlen The length of the buffer in multiples of wchar_t. + + @returns + This function returns 0 on success and -1 on error. + */ + int HID_API_EXPORT_CALL hid_get_product_string(hid_device *dev, wchar_t *string, size_t maxlen); + + /** @brief Get The Serial Number String from a HID device. + + @ingroup API + @param dev A device handle returned from hid_open(). + @param string A wide string buffer to put the data into. + @param maxlen The length of the buffer in multiples of wchar_t. + + @returns + This function returns 0 on success and -1 on error. + */ + int HID_API_EXPORT_CALL hid_get_serial_number_string(hid_device *dev, wchar_t *string, size_t maxlen); + + /** @brief Get a string from a HID device, based on its string index. + + @ingroup API + @param dev A device handle returned from hid_open(). + @param string_index The index of the string to get. + @param string A wide string buffer to put the data into. + @param maxlen The length of the buffer in multiples of wchar_t. + + @returns + This function returns 0 on success and -1 on error. + */ + int HID_API_EXPORT_CALL hid_get_indexed_string(hid_device *dev, int string_index, wchar_t *string, size_t maxlen); + + /** @brief Get a string describing the last error which occurred. + + Whether a function sets the last error is noted in its + documentation. These functions will reset the last error + to NULL before their execution. + + Strings returned from hid_error() must not be freed by the user! + + This function is thread-safe, and error messages are thread-local. + + @ingroup API + @param dev A device handle returned from hid_open(), + or NULL to get the last non-device-specific error + (e.g. for errors in hid_open() itself). + + @returns + This function returns a string containing the last error + which occurred or NULL if none has occurred. + */ + HID_API_EXPORT const wchar_t* HID_API_CALL hid_error(hid_device *dev); + + /** @brief Get a runtime version of the library. + + @ingroup API + + @returns + Pointer to statically allocated struct, that contains version. + */ + HID_API_EXPORT const struct hid_api_version* HID_API_CALL hid_version(void); + + + /** @brief Get a runtime version string of the library. + + @ingroup API + + @returns + Pointer to statically allocated string, that contains version string. + */ + HID_API_EXPORT const char* HID_API_CALL hid_version_str(void); + +#ifdef __cplusplus +} +#endif + +#endif + diff --git a/external/misc/CMakeLists.txt b/external/misc/CMakeLists.txt index 07b10b2e..685b89ad 100644 --- a/external/misc/CMakeLists.txt +++ b/external/misc/CMakeLists.txt @@ -6,16 +6,48 @@ include_directories( ) if(LINUX) - list(APPEND misc_SOURCES - # Provide our own copy of strlcpy and strlcat - # because they are not included with Linux. - ${CUSTOM_MISC_DIR}/strlcpy.c - ${CUSTOM_MISC_DIR}/strlcat.c - ) +# Previously - +# list(APPEND misc_SOURCES +# # Provide our own copy of strlcpy and strlcat +# # because they are not included with Linux. +# ${CUSTOM_MISC_DIR}/strlcpy.c +# ${CUSTOM_MISC_DIR}/strlcat.c +# ) +# It seems that Alpine Linux and Void Linux have strlcpy and +# strlcat so we need to handle the situation more delicately. +# When doing it this way, there is probably no reason to +# distinguish between Linux and BSD-like systems here. +# If we kept going, the same thing could be done for each +# of the functions and no OS check would be needed. + + if (NOT HAVE_STRLCPY) + list(APPEND misc_SOURCES + ${CUSTOM_MISC_DIR}/strlcpy.c + ) + endif() + + if (NOT HAVE_STRLCAT) + list(APPEND misc_SOURCES + ${CUSTOM_MISC_DIR}/strlcat.c + ) + endif() + + # Add_library doesn't like to get an empty source file list. + # I tried several variations on this theme to test whether the list + # was not empty and was not successful in getting it to work + # on both Alpine and RPi. + #if("${misc_SOURCES}") + # This is less elegant and less maintainable but it works. + + if ((NOT HAVE_STRLCPY) OR (NOT HAVE_STRLCAT)) + add_library(misc STATIC + ${misc_SOURCES} + ) + else() + set(MISC_LIBRARIES "" CACHE INTERNAL "") + endif() + - add_library(misc STATIC - ${misc_SOURCES} - ) elseif(WIN32 OR CYGWIN) # windows diff --git a/external/regex/CMakeLists.txt b/external/regex/CMakeLists.txt index 67207639..76bbf9ba 100644 --- a/external/regex/CMakeLists.txt +++ b/external/regex/CMakeLists.txt @@ -18,7 +18,7 @@ if(WIN32 OR CYGWIN) # windows ) set_target_properties(regex - PROPERTIES COMPILE_FLAGS "-Dbool=int -Dtrue=1 -Dfalse=0 -DUSE_REGEX_STATIC" + PROPERTIES COMPILE_FLAGS "-Dbool=int -Dtrue=1 -Dfalse=0 -DREGEX_STATIC" ) endif() diff --git a/external/regex/regex.h b/external/regex/regex.h index c2a9a4c3..a84f6a99 100644 --- a/external/regex/regex.h +++ b/external/regex/regex.h @@ -35,17 +35,23 @@ #if (defined __WIN32__) || (defined _WIN32) # ifdef BUILD_REGEX_DLL # define REGEX_DLL_IMPEXP __DLL_EXPORT__ +# define REGEX_VARIABLE_IMPEXP __DLL_EXPORT__ # elif defined(REGEX_STATIC) # define REGEX_DLL_IMPEXP +# define REGEX_VARIABLE_IMPEXP # elif defined (USE_REGEX_DLL) # define REGEX_DLL_IMPEXP __DLL_IMPORT__ +# define REGEX_VARIABLE_IMPEXP __DLL_IMPORT__ # elif defined (USE_REGEX_STATIC) # define REGEX_DLL_IMPEXP +# define REGEX_VARIABLE_IMPEXP extern # else /* assume USE_REGEX_DLL */ # define REGEX_DLL_IMPEXP __DLL_IMPORT__ +# define REGEX_VARIABLE_IMPEXP __DLL_IMPORT__ # endif #else /* __WIN32__ */ # define REGEX_DLL_IMPEXP +# define REGEX_VARIABLE_IMPEXP #endif /* Allow the use in C++ code. */ @@ -202,7 +208,8 @@ typedef unsigned long int reg_syntax_t; some interfaces). When a regexp is compiled, the syntax used is stored in the pattern buffer, so changing this does not affect already-compiled regexps. */ -REGEX_DLL_IMPEXP reg_syntax_t re_syntax_options; +//REGEX_VARIABLE_IMPEXP reg_syntax_t re_syntax_options; +extern reg_syntax_t re_syntax_options; /* Define combinations of the above bits for the standard possibilities. (The [[[ comments delimit what gets put into the Texinfo file, so diff --git a/man/atest.1 b/man/atest.1 index a1b554c2..58c90f64 100644 --- a/man/atest.1 +++ b/man/atest.1 @@ -37,6 +37,10 @@ Data rate in bits/sec. Standard values are 300, 1200, 2400, 4800, 9600. 4800 bps uses 8PSK based on V.27 standard. .P 9600 bps and up uses K9NG/G3RUH standard. +.P +AIS for ship Automatic Identification System. +.P +EAS for Emergency Alert System (EAS) Specific Area Message Encoding (SAME). .RE .RE .PD diff --git a/man/direwolf.1 b/man/direwolf.1 index 76bc195d..93f786dc 100644 --- a/man/direwolf.1 +++ b/man/direwolf.1 @@ -87,7 +87,15 @@ Divide audio sample by n for first channel. .TP .BI "-X " "n" -1 to enable FX.25 transmit. +1 to enable FX.25 transmit. 16, 32, 64 for specific number of check bytes. + +.TP +.BI "-I " "n" +Enable IL2P transmit. n=1 is recommended. 0 uses weaker FEC. + +.TP +.BI "-i " "n" +Enable IL2P transmit, inverted polarity. n=1 is recommended. 0 uses weaker FEC. .TP .BI "-d " "x" @@ -122,6 +130,8 @@ m = Monitor heard station list. f = Packet filtering. .P x = FX.25 increase verbose level. +.P +d = APRStt (DTMF to APRS object conversion). .RE .RE .PD @@ -135,6 +145,8 @@ Quiet (suppress output). Specify one or more of the following in place of x. h = Heard line with the audio level. .P d = Decoding of APRS packets. +.P +x = Silence FX.25 information. .RE .RE .PD @@ -149,11 +161,26 @@ Text colors. 0=disabled. 1=default. 2,3,4,... alternatives. Use 9 to test com Enable pseudo terminal for KISS protocol. .TP -.B "-x " +.BI "-x " Send Xmit level calibration tones. +.PD 0 +.RS +.RS +a = Alternating mark/space tones. +.P +m = steady Mark tone (e.g. 1200 Hz) +.P +s = steady Space tone (e.g. 2200 Hz) +.P +p = selence (set Ptt only). +.P +Optionally add a number to specify radio channel. +.RE +.RE +.PD .TP -.B "-U " +.B "-u " Print UTF-8 test string and exit. .TP diff --git a/man/gen_packets.1 b/man/gen_packets.1 index 773a88eb..740d4db4 100644 --- a/man/gen_packets.1 +++ b/man/gen_packets.1 @@ -46,6 +46,8 @@ Data rate in bits/sec for first channel. Standard values are 300, 1200, 2400, 4 4800 bps uses 8PSK based on V.27 standard. .P 9600 bps and up uses K9NG/G3RUH standard. +.P +EAS for Emergency Alert System (EAS) Specific Area Message Encoding (SAME). .RE .RE .PD @@ -62,6 +64,18 @@ Force G3RUH modem regardless of data rate. .BI "-J " 2400 bps QPSK compatible with MFJ-2400. +.TP +.BI "-X " "n" +1 to enable FX.25 transmit. 16, 32, 64 for specific number of check bytes. + +.TP +.BI "-I " "n" +Enable IL2P transmit. n=1 is recommended. 0 uses weaker FEC. + +.TP +.BI "-i " "n" +Enable IL2P transmit, inverted polarity. n=1 is recommended. 0 uses weaker FEC. + .TP .BI "-m " "n" @@ -88,9 +102,13 @@ Send output to .wav file. 8 bit audio rather than 16. .TP -.B "-2" +.BI "-2" 2 channels of audio rather than 1. +.TP +.BI "-v" "max[,incr]" +Variable speed with specified maximum error and optional increment. + .SH EXAMPLES .P diff --git a/man/kissutil.1 b/man/kissutil.1 index 09eb12c6..a7968f97 100644 --- a/man/kissutil.1 +++ b/man/kissutil.1 @@ -45,7 +45,7 @@ Example: %H:%M:%S for current time in hours, minutes, seconds. .TP .BI "-f " "xmit-directory" -Files in this directory are transmited and deleted. +Files in this directory are transmitted and deleted. Another application places a file here when it wants something to be transmitted. .TP diff --git a/man/tt2text.1 b/man/tt2text.1 index b3c3266c..c6214c27 100644 --- a/man/tt2text.1 +++ b/man/tt2text.1 @@ -11,7 +11,7 @@ tt2text \- Convert Touch Tone sequence to text .SH DESCRIPTION -\fBtt2text\fR converts a Touch Tone squence to text. There are two types +\fBtt2text\fR converts a Touch Tone sequence to text. There are two types of encoding: .RS .HP diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 46d3ac7a..a2c3963d 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -1,3 +1,4 @@ + # global includes # not ideal but not so slow # otherwise use target_include_directories @@ -7,7 +8,9 @@ include_directories( ${ALSA_INCLUDE_DIRS} ${UDEV_INCLUDE_DIRS} ${PORTAUDIO_INCLUDE_DIRS} + ${SNDIO_INCLUDE_DIRS} ${CUSTOM_GEOTRANZ_DIR} + ${CUSTOM_HIDAPI_DIR} ) if(WIN32 OR CYGWIN) @@ -57,6 +60,13 @@ list(APPEND direwolf_SOURCES hdlc_rec2.c hdlc_send.c igate.c + il2p_codec.c + il2p_scramble.c + il2p_rec.c + il2p_payload.c + il2p_init.c + il2p_header.c + il2p_send.c kiss_frame.c kiss.c kissserial.c @@ -96,9 +106,16 @@ if(LINUX) cm108.c ) endif() + if(AVAHI_CLIENT_FOUND) + list(APPEND direwolf_SOURCES + dns_sd_common.c + dns_sd_avahi.c + ) + endif() elseif(WIN32 OR CYGWIN) # windows list(APPEND direwolf_SOURCES audio_win.c + cm108.c # icon # require plain gcc binary or link @@ -107,10 +124,20 @@ if(LINUX) list(REMOVE_ITEM direwolf_SOURCES dwgpsd.c ) - else() # macOS freebsd openbsd + elseif(HAVE_SNDIO) + list(APPEND direwolf_SOURCES + audio.c + ) + else() # macOS freebsd list(APPEND direwolf_SOURCES audio_portaudio.c ) + if(USE_MACOS_DNSSD) + list(APPEND direwolf_SOURCES + dns_sd_common.c + dns_sd_macos.c + ) + endif() endif() add_executable(direwolf @@ -121,19 +148,22 @@ target_link_libraries(direwolf ${GEOTRANZ_LIBRARIES} ${MISC_LIBRARIES} ${REGEX_LIBRARIES} + ${HIDAPI_LIBRARIES} Threads::Threads ${GPSD_LIBRARIES} ${HAMLIB_LIBRARIES} ${ALSA_LIBRARIES} ${UDEV_LIBRARIES} ${PORTAUDIO_LIBRARIES} + ${SNDIO_LIBRARIES} + ${AVAHI_LIBRARIES} ) if(WIN32 OR CYGWIN) set_target_properties(direwolf PROPERTIES COMPILE_FLAGS "-DUSE_REGEX_STATIC" ) - target_link_libraries(direwolf winmm ws2_32) + target_link_libraries(direwolf winmm ws2_32 setupapi) endif() # decode_aprs @@ -266,12 +296,20 @@ target_link_libraries(log2gpx list(APPEND gen_packets_SOURCES gen_packets.c ax25_pad.c + ax25_pad2.c fx25_encode.c + fx25_extract.c fx25_init.c fx25_send.c hdlc_send.c fcs_calc.c gen_tone.c + il2p_codec.c + il2p_scramble.c + il2p_payload.c + il2p_init.c + il2p_header.c + il2p_send.c morse.c dtmf.c textcolor.c @@ -298,14 +336,22 @@ list(APPEND atest_SOURCES demod_9600.c dsp.c fx25_extract.c + fx25_encode.c fx25_init.c fx25_rec.c hdlc_rec.c hdlc_rec2.c + il2p_codec.c + il2p_scramble.c + il2p_rec.c + il2p_payload.c + il2p_init.c + il2p_header.c multi_modem.c rrbb.c fcs_calc.c ax25_pad.c + ax25_pad2.c decode_aprs.c dwgpsnmea.c dwgps.c @@ -398,9 +444,35 @@ if(WIN32 OR CYGWIN) endif() +# TNC interoperability testing for AX.25 connected mode. +# tnctest +list(APPEND tnctest_SOURCES + tnctest.c + textcolor.c + dtime_now.c + serial_port.c + ) + +add_executable(tnctest + ${tnctest_SOURCES} + ) + +target_link_libraries(tnctest + ${MISC_LIBRARIES} + Threads::Threads + ) + +if(WIN32 OR CYGWIN) + target_link_libraries(tnctest ws2_32) +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. + # cm108 -if(UDEV_FOUND) +if(UDEV_FOUND OR WIN32 OR CYGWIN) list(APPEND cm108_SOURCES cm108.c textcolor.c @@ -416,8 +488,21 @@ if(UDEV_FOUND) target_link_libraries(cm108 ${MISC_LIBRARIES} - ${UDEV_LIBRARIES} ) + + if (LINUX) + target_link_libraries(cm108 + ${UDEV_LIBRARIES} + ) + endif() + + if (WIN32 OR CYGWIN) + target_link_libraries(cm108 + ${HIDAPI_LIBRARIES} + ws2_32 + setupapi + ) + endif() endif() @@ -481,7 +566,8 @@ install(TARGETS gen_packets DESTINATION ${INSTALL_BIN_DIR}) install(TARGETS atest DESTINATION ${INSTALL_BIN_DIR}) 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) +if(UDEV_FOUND OR WIN32 OR CYGWIN) install(TARGETS cm108 DESTINATION ${INSTALL_BIN_DIR}) endif() diff --git a/src/agwlib.c b/src/agwlib.c index 09881097..2c03adaa 100644 --- a/src/agwlib.c +++ b/src/agwlib.c @@ -321,7 +321,7 @@ static void * tnc_listen_thread (void *arg) s_tnc_sock = dwsock_connect (s_tnc_host, s_tnc_port, "TNC", 0, 0, tncaddr); if (s_tnc_sock != -1) { - dw_printf ("Succesfully reattached to network TNC.\n"); + dw_printf ("Successfully reattached to network TNC.\n"); // Might need to run TNC initialization again. // For example, a server would register its callsigns. @@ -365,7 +365,7 @@ static void * tnc_listen_thread (void *arg) } /* - * Call to/from fields are 10 bytes but contents must not exceeed 9 characters. + * Call to/from fields are 10 bytes but contents must not exceed 9 characters. * It's not guaranteed that unused bytes will contain 0 so we * don't issue error message in this case. */ @@ -600,7 +600,7 @@ int agwlib_G_ask_port_information (void) * Returns: Number of bytes sent for success, -1 for error. * * Description: This only starts the sequence and does not wait. - * Success or failue will be indicated sometime later by ? + * Success or failure will be indicated sometime later by ? * *--------------------------------------------------------------------*/ @@ -635,7 +635,7 @@ int agwlib_C_connect (int chan, char *call_from, char *call_to) * Returns: Number of bytes sent for success, -1 for error. * * Description: This only starts the sequence and does not wait. - * Success or failue will be indicated sometime later by ? + * Success or failure will be indicated sometime later by ? * *--------------------------------------------------------------------*/ @@ -722,13 +722,13 @@ int agwlib_D_send_connected_data (int chan, int pid, char *call_from, char *call * hand we don't want to get TOO far ahead when transferring a large file. * * Before disconnecting from another station, it would be good to know - * that it actually recevied the last message we sent. For this reason, + * that it actually received the last message we sent. For this reason, * I think it would be good for this to include frames that were - * transmitted but not yet acknowleged. (Even if it was transmitted once, + * transmitted but not yet acknowledged. (Even if it was transmitted once, * it could still be transmitted again, if lost, so you could say it is * still waiting for transmission.) * - * See server.c for a more precise definition of exacly how this is defined. + * See server.c for a more precise definition of exactly how this is defined. * *--------------------------------------------------------------------*/ diff --git a/src/ais.c b/src/ais.c index cadf6482..938fa012 100644 --- a/src/ais.c +++ b/src/ais.c @@ -338,7 +338,7 @@ void ais_to_nmea (unsigned char *ais, int ais_len, char *nmea, int nmea_size) * * Name: ais_parse * - * Purpose: Parse AIS sentence and extract interesing parts. + * Purpose: Parse AIS sentence and extract interesting parts. * * Inputs: sentence NMEA sentence. * @@ -594,7 +594,7 @@ int ais_parse (char *sentence, int quiet, char *descr, int descr_size, char *mss * * Returns: -1 Invalid message type. * 0 Good length. - * 1 Unexpected lenth. + * 1 Unexpected length. * *--------------------------------------------------------------------*/ diff --git a/src/appserver.c b/src/appserver.c index 2badaec8..b0ef7d87 100644 --- a/src/appserver.c +++ b/src/appserver.c @@ -320,7 +320,7 @@ static void poll_timing_test (void) * * data - Should look something like this for incoming: * *** CONNECTED to Station xxx\r - * and ths for my request being accepted: + * and this for my request being accepted: * *** CONNECTED With Station xxx\r * * session_id - Session id to be used in data transfer and @@ -491,15 +491,19 @@ void agw_cb_D_connected_data (int chan, char *call_from, char *call_to, int data // who - list people currently logged in. int n; - char greeting[80]; + char greeting[128]; snprintf (greeting, sizeof(greeting), "Session Channel User Since\r"); agwlib_D_send_connected_data (chan, 0xF0, mycall, call_from, strlen(greeting), greeting); for (n = 0; n < MAX_SESSIONS; n++) { if (session[n].client_addr[0]) { +// I think compiler is confused. It says up to 520 characters can be written. +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wformat-truncation" snprintf (greeting, sizeof(greeting), " %2d %d %-9s [time later]\r", n, session[n].channel, session[n].client_addr); +#pragma GCC diagnostic pop agwlib_D_send_connected_data (chan, 0xF0, mycall, call_from, strlen(greeting), greeting); } } diff --git a/src/aprs_tt.c b/src/aprs_tt.c index 0543f8a7..7b125759 100644 --- a/src/aprs_tt.c +++ b/src/aprs_tt.c @@ -115,6 +115,8 @@ static int find_ttloc_match (char *e, char *xstr, char *ystr, char *zstr, char * static void check_result (void); #endif +static int tt_debug = 0; + /*------------------------------------------------------------------ * @@ -122,7 +124,8 @@ static void check_result (void); * * Purpose: Initialize the APRStt gateway at system startup time. * - * Inputs: Configuration options gathered by config.c. + * Inputs: P - Pointer to configuration options gathered by config.c. + * debug - Debug printing control. * * Global out: Make our own local copy of the structure here. * @@ -164,9 +167,10 @@ static struct ttloc_s test_config[] = { #endif -void aprs_tt_init (struct tt_config_s *p) +void aprs_tt_init (struct tt_config_s *p, int debug) { int c; + tt_debug = debug; #if TT_MAIN /* For unit testing. */ @@ -208,7 +212,7 @@ void aprs_tt_init (struct tt_config_s *p) * The complete message is then processed. * The touch tone decoder sends $ if no activity * for some amount of time, perhaps 5 seconds. - * A partially accumulated messge is discarded if + * A partially accumulated message is discarded if * there is a long gap. * * '.' means no activity during processing period. @@ -483,7 +487,19 @@ static int parse_fields (char *msg) //text_color_set(DW_COLOR_DEBUG); //dw_printf ("parse_fields (%s).\n", msg); - strlcpy (stemp, msg, sizeof(stemp)); +// Make a copy of msg because strtok corrupts the original. +// While we are at it, remove any blanks. +// This should not happen with DTMF reception but could happen +// in manually crafted strings for testing. + + int n = 0; + for (char *m = msg; *m != '\0' && n < sizeof(stemp)-1; m++) { + if (*m != ' ') { + stemp[n++] = *m; + } + } + stemp[n] = '\0'; + e = strtok_r (stemp, "*#", &save); while (e != NULL) { @@ -551,7 +567,7 @@ static int parse_fields (char *msg) default: text_color_set(DW_COLOR_ERROR); - dw_printf ("Field does not start with A, B, C, or digit: \"%s\"\n", msg); + dw_printf ("Field does not start with A, B, C, or digit: \"%s\"\n", e); return (TT_ERROR_D_MSG); } @@ -574,7 +590,7 @@ static int parse_fields (char *msg) * Purpose: Expand compact form "macro" to full format then process. * * Inputs: e - An "entry" extracted from a complete - * APRStt messsage. + * APRStt message. * In this case, it should contain only digits. * * Returns: 0 for success or one of the TT_ERROR_... codes. @@ -689,18 +705,16 @@ static int expand_macro (char *e) * Purpose: Extract traditional format callsign or object name from touch tone sequence. * * Inputs: e - An "entry" extracted from a complete - * APRStt messsage. - * In this case, it should start with "A". + * APRStt message. + * In this case, it should start with "A" then a digit. * * Outputs: m_callsign * * m_symtab_or_overlay - Set to 0-9 or A-Z if specified. * - * m_symbol_code - Always set to 'A'. - * NO! This should be applied only if we - * have the default value at this point. - * The symbol might have been explicitly - * set already and we don't want to overwrite that. + * m_symbol_code - Always set to 'A' (Box, DTMF or RFID) + * If you want a different symbol, use the new + * object name format and separate symbol specification. * * Returns: 0 for success or one of the TT_ERROR_... codes. * @@ -752,6 +766,11 @@ static int parse_callsign (char *e) int len; char tttemp[40], stemp[30]; + if (tt_debug) { + text_color_set(DW_COLOR_DEBUG); + dw_printf ("APRStt parse callsign (starts with A then digit): \"%s\"\n", e); + } + assert (*e == 'A'); len = strlen(e); @@ -762,6 +781,10 @@ static int parse_callsign (char *e) if (len == 4 && isdigit(e[1]) && isdigit(e[2]) && isdigit(e[3])) { strlcpy (m_callsign, e+1, sizeof(m_callsign)); + if (tt_debug) { + text_color_set(DW_COLOR_DEBUG); + dw_printf ("Special case, 3 digit tactical call: \"%s\"\n", m_callsign); + } return (0); } @@ -779,7 +802,7 @@ static int parse_callsign (char *e) return (cs_err); } - strncpy (m_callsign, e+1, 3); + memcpy (m_callsign, e+1, 3); m_callsign[3] = '\0'; if (len == 7) { @@ -789,10 +812,20 @@ static int parse_callsign (char *e) tt_two_key_to_text (tttemp, 0, stemp); m_symbol_code = APRSTT_DEFAULT_SYMBOL; m_symtab_or_overlay = stemp[0]; + if (tt_debug) { + text_color_set(DW_COLOR_DEBUG); + dw_printf ("Three digit abbreviation1: callsign \"%s\", symbol code '%c (Box DTMF)', overlay '%c', checksum %c\n", + m_callsign, m_symbol_code, m_symtab_or_overlay, e[len-1]); + } } else { m_symbol_code = APRSTT_DEFAULT_SYMBOL; m_symtab_or_overlay = e[len-2]; + if (tt_debug) { + text_color_set(DW_COLOR_DEBUG); + dw_printf ("Three digit abbreviation2: callsign \"%s\", symbol code '%c' (Box DTMF), overlay '%c', checksum %c\n", + m_callsign, m_symbol_code, m_symtab_or_overlay, e[len-1]); + } } return (0); } @@ -810,7 +843,7 @@ static int parse_callsign (char *e) } if (isupper(e[len-2])) { - strncpy (tttemp, e+1, len-4); + memcpy (tttemp, e+1, len-4); tttemp[len-4] = '\0'; tt_two_key_to_text (tttemp, 0, m_callsign); @@ -820,14 +853,24 @@ static int parse_callsign (char *e) tt_two_key_to_text (tttemp, 0, stemp); m_symbol_code = APRSTT_DEFAULT_SYMBOL; m_symtab_or_overlay = stemp[0]; + if (tt_debug) { + text_color_set(DW_COLOR_DEBUG); + dw_printf ("Callsign in two key format1: callsign \"%s\", symbol code '%c' (Box DTMF), overlay '%c', checksum %c\n", + m_callsign, m_symbol_code, m_symtab_or_overlay, e[len-1]); + } } else { - strncpy (tttemp, e+1, len-3); + memcpy (tttemp, e+1, len-3); tttemp[len-3] = '\0'; tt_two_key_to_text (tttemp, 0, m_callsign); m_symbol_code = APRSTT_DEFAULT_SYMBOL; m_symtab_or_overlay = e[len-2]; + if (tt_debug) { + text_color_set(DW_COLOR_DEBUG); + dw_printf ("Callsign in two key format2: callsign \"%s\", symbol code '%c' (Box DTMF), overlay '%c', checksum %c\n", + m_callsign, m_symbol_code, m_symtab_or_overlay, e[len-1]); + } } return (0); } @@ -845,7 +888,7 @@ static int parse_callsign (char *e) * Purpose: Extract object name from touch tone sequence. * * Inputs: e - An "entry" extracted from a complete - * APRStt messsage. + * APRStt message. * In this case, it should start with "AA". * * Outputs: m_callsign @@ -864,9 +907,11 @@ static int parse_callsign (char *e) static int parse_object_name (char *e) { int len; - //int c_length; - //char tttemp[40]; - //char stemp[30]; + + if (tt_debug) { + text_color_set(DW_COLOR_DEBUG); + dw_printf ("APRStt parse object name (starts with AA): \"%s\"\n", e); + } assert (e[0] == 'A'); assert (e[1] == 'A'); @@ -882,6 +927,10 @@ static int parse_object_name (char *e) if (tt_two_key_to_text (e+2, 0, m_callsign) == 0) { m_callsign[9] = '\0'; /* truncate to 9 */ m_ssid = 0; /* No ssid for object name */ + if (tt_debug) { + text_color_set(DW_COLOR_DEBUG); + dw_printf ("Object name in two key format: \"%s\"\n", m_callsign); + } return (0); } } @@ -901,7 +950,7 @@ static int parse_object_name (char *e) * Purpose: Extract symbol from touch tone sequence. * * Inputs: e - An "entry" extracted from a complete - * APRStt messsage. + * APRStt message. * In this case, it should start with "AB". * * Outputs: m_symtab_or_overlay @@ -935,6 +984,11 @@ static int parse_symbol (char *e) int nn; char stemp[10]; + if (tt_debug) { + text_color_set(DW_COLOR_DEBUG); + dw_printf ("APRStt parse symbol (starts with AB): \"%s\"\n", e); + } + assert (e[0] == 'A'); assert (e[1] == 'B'); @@ -959,12 +1013,22 @@ static int parse_symbol (char *e) case '1': m_symtab_or_overlay = '/'; m_symbol_code = 32 + nn; + if (tt_debug) { + text_color_set(DW_COLOR_DEBUG); + dw_printf ("symbol code '%c', primary symbol table '%c'\n", + m_symbol_code, m_symtab_or_overlay); + } return (0); break; case '2': m_symtab_or_overlay = '\\'; m_symbol_code = 32 + nn; + if (tt_debug) { + text_color_set(DW_COLOR_DEBUG); + dw_printf ("symbol code '%c', alternate symbol table '%c'\n", + m_symbol_code, m_symtab_or_overlay); + } return (0); break; @@ -973,6 +1037,11 @@ static int parse_symbol (char *e) if (tt_two_key_to_text (e+5, 0, stemp) == 0) { m_symbol_code = 32 + nn; m_symtab_or_overlay = stemp[0]; + if (tt_debug) { + text_color_set(DW_COLOR_DEBUG); + dw_printf ("symbol code '%c', alternate symbol table with overlay '%c'\n", + m_symbol_code, m_symtab_or_overlay); + } return (0); } } @@ -995,7 +1064,7 @@ static int parse_symbol (char *e) * Purpose: Extract QIKcom-2 / APRStt 3 ten digit call or five digit suffix. * * Inputs: e - An "entry" extracted from a complete - * APRStt messsage. + * APRStt message. * In this case, it should start with "AC". * * Outputs: m_callsign @@ -1018,6 +1087,11 @@ static int parse_aprstt3_call (char *e) assert (e[0] == 'A'); assert (e[1] == 'C'); + if (tt_debug) { + text_color_set(DW_COLOR_DEBUG); + dw_printf ("APRStt parse QIKcom-2 / APRStt 3 ten digit call or five digit suffix (starts with AC): \"%s\"\n", e); + } + if (strlen(e) == 2+10) { char call[12]; @@ -1073,7 +1147,7 @@ static int parse_aprstt3_call (char *e) * Purpose: Extract location from touch tone sequence. * * Inputs: e - An "entry" extracted from a complete - * APRStt messsage. + * APRStt message. * In this case, it should start with "B". * * Outputs: m_latitude @@ -1125,6 +1199,11 @@ static int parse_location (char *e) char mh[20]; char stemp[32]; + if (tt_debug) { + text_color_set(DW_COLOR_DEBUG); + dw_printf ("APRStt parse location (starts with B): \"%s\"\n", e); + // TODO: more detail later... + } assert (*e == 'B'); @@ -1204,13 +1283,26 @@ static int parse_location (char *e) lat0 = tt_config.ttloc_ptr[ipat].grid.lat0; lat9 = tt_config.ttloc_ptr[ipat].grid.lat9; + double yrange = lat9 - lat0; y = atof(ystr); - m_latitude = lat0 + y * (lat9-lat0) / (pow(10., strlen(ystr)) - 1.); - + double user_y_max = round(pow(10., strlen(ystr)) - 1.); // e.g. 999 for 3 digits + m_latitude = lat0 + yrange * y / user_y_max; +#if 0 + dw_printf ("TTLOC_GRID LAT min=%f, max=%f, range=%f\n", lat0, lat9, yrange); + dw_printf ("TTLOC_GRID LAT user_y=%f, user_y_max=%f\n", y, user_y_max); + dw_printf ("TTLOC_GRID LAT min + yrange * user_y / user_y_range = %f\n", m_latitude); +#endif lon0 = tt_config.ttloc_ptr[ipat].grid.lon0; lon9 = tt_config.ttloc_ptr[ipat].grid.lon9; + double xrange = lon9 - lon0; x = atof(xstr); - m_longitude = lon0 + x * (lon9-lon0) / (pow(10., strlen(xstr)) - 1.); + double user_x_max = round(pow(10., strlen(xstr)) - 1.); + m_longitude = lon0 + xrange * x / user_x_max; +#if 0 + dw_printf ("TTLOC_GRID LON min=%f, max=%f, range=%f\n", lon0, lon9, xrange); + dw_printf ("TTLOC_GRID LON user_x=%f, user_x_max=%f\n", x, user_x_max); + dw_printf ("TTLOC_GRID LON min + xrange * user_x / user_x_range = %f\n", m_longitude); +#endif m_dao[2] = e[0]; m_dao[3] = e[1]; @@ -1415,7 +1507,7 @@ static int parse_location (char *e) * defined in the configuration file. * * Inputs: e - An "entry" extracted from a complete - * APRStt messsage. + * APRStt message. * In this case, it should start with "B". * * valstrsize - size of the outputs so we can check for buffer overflow. @@ -1566,7 +1658,7 @@ static int find_ttloc_match (char *e, char *xstr, char *ystr, char *zstr, char * * Purpose: Extract comment / status or other special information from touch tone message. * * Inputs: e - An "entry" extracted from a complete - * APRStt messsage. + * APRStt message. * In this case, it should start with "C". * * Outputs: m_comment @@ -1893,6 +1985,7 @@ static const struct { /* Latitude comes out ok, 37.9137 -> 55.82 min. */ /* Longitude -81.1254 -> 8.20 min */ { "B21234*A67979#", "679", "12", "7A", "", "", "12.3400", "56.1200", "!TB2!" }, + { "B533686*A67979#", "679", "12", "7A", "", "", "37.9222", "81.1143", "!TB5!" }, // TODO: should test other coordinate systems. @@ -1985,7 +2078,7 @@ static void check_result (void) int main (int argc, char *argv[]) { - aprs_tt_init (NULL); + aprs_tt_init (NULL, 0); error_count = 0; diff --git a/src/aprs_tt.h b/src/aprs_tt.h index 8cc845d8..4d33f487 100644 --- a/src/aprs_tt.h +++ b/src/aprs_tt.h @@ -123,7 +123,7 @@ struct tt_config_s { int obj_recv_chan; /* Channel to listen for tones. */ int obj_xmit_chan; /* Channel to transmit object report. */ - /* -1 for none. This could happpen if we */ + /* -1 for none. This could happen if we */ /* are only sending to application */ /* and/or IGate. */ @@ -165,7 +165,7 @@ struct tt_config_s { -void aprs_tt_init (struct tt_config_s *p_config); +void aprs_tt_init (struct tt_config_s *p_config, int debug); void aprs_tt_button (int chan, char button); diff --git a/src/atest.c b/src/atest.c index 5c197759..c5f4ec50 100644 --- a/src/atest.c +++ b/src/atest.c @@ -2,7 +2,7 @@ // // This file is part of Dire Wolf, an amateur radio packet TNC. // -// Copyright (C) 2011, 2012, 2013, 2014, 2015, 2016, 2019 John Langner, WB2OSZ +// Copyright (C) 2011, 2012, 2013, 2014, 2015, 2016, 2019, 2021, 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 @@ -23,12 +23,12 @@ * * Name: atest.c * - * Purpose: Test fixture for the AFSK demodulator. + * Purpose: Test fixture for the Dire Wolf demodulators. * - * Inputs: Takes audio from a .WAV file insted of the audio device. + * Inputs: Takes audio from a .WAV file instead of the audio device. * - * Description: This can be used to test the AFSK demodulator under - * controlled and reproducable conditions for tweaking. + * Description: This can be used to test the demodulators under + * controlled and reproducible conditions for tweaking. * * For example * @@ -68,6 +68,7 @@ #include #include #include +#include #define ATEST_C 1 @@ -82,6 +83,7 @@ #include "ptt.h" #include "dtime_now.h" #include "fx25.h" +#include "il2p.h" #include "hdlc_rec.h" @@ -106,7 +108,7 @@ struct wav_header { /* .WAV file header. */ /* 8 bit samples are unsigned bytes */ /* in range of 0 .. 255. */ - /* 16 bit samples are signed short */ + /* 16 bit samples are little endian signed short */ /* in range of -32768 .. +32767. */ static struct { @@ -189,6 +191,7 @@ static int h_opt = 0; // Hexadecimal display of received packet. static char P_opt[16] = ""; // Demodulator profiles. static int d_x_opt = 1; // FX.25 debug. static int d_o_opt = 0; // "-d o" option for DCD output control. */ +static int d_2_opt = 0; // "-d 2" option for IL2P details. */ static int dcd_count = 0; static int dcd_missing_errors = 0; @@ -236,7 +239,7 @@ int main (int argc, char *argv[]) my_audio_config.achan[channel].space_freq = DEFAULT_SPACE_FREQ; my_audio_config.achan[channel].baud = DEFAULT_BAUD; - strlcpy (my_audio_config.achan[channel].profiles, "E", sizeof(my_audio_config.achan[channel].profiles)); + strlcpy (my_audio_config.achan[channel].profiles, "A", sizeof(my_audio_config.achan[channel].profiles)); my_audio_config.achan[channel].num_freq = 1; my_audio_config.achan[channel].offset = 0; @@ -273,12 +276,12 @@ int main (int argc, char *argv[]) case 'B': /* -B for data Bit rate */ /* Also implies modem type based on speed. */ - /* Special case "AIS" rather than number. */ + /* Special cases AIS, EAS rather than number. */ if (strcasecmp(optarg, "AIS") == 0) { - B_opt = 12345; // See special case below. + B_opt = 0xA15A15; // See special case below. } else if (strcasecmp(optarg, "EAS") == 0) { - B_opt = 23456; // See special case below. + B_opt = 0xEA5EA5; // See special case below. } else { B_opt = atoi(optarg); @@ -389,6 +392,7 @@ int main (int argc, char *argv[]) switch (*p) { case 'x': d_x_opt++; break; // FX.25 case 'o': d_o_opt++; break; // DCD output control + case '2': d_2_opt++; break; // IL2P debug out default: break; } } @@ -421,32 +425,26 @@ int main (int argc, char *argv[]) my_audio_config.achan[0].baud = B_opt; - if (my_audio_config.achan[0].baud < MIN_BAUD || my_audio_config.achan[0].baud > MAX_BAUD) { - text_color_set(DW_COLOR_ERROR); - dw_printf ("Use a more reasonable bit rate in range of %d - %d.\n", MIN_BAUD, MAX_BAUD); - exit (EXIT_FAILURE); - } /* We have similar logic in direwolf.c, config.c, gen_packets.c, and atest.c, */ /* that need to be kept in sync. Maybe it could be a common function someday. */ - if (my_audio_config.achan[0].baud == 100) { + if (my_audio_config.achan[0].baud == 100) { // What was this for? my_audio_config.achan[0].modem_type = MODEM_AFSK; my_audio_config.achan[0].mark_freq = 1615; my_audio_config.achan[0].space_freq = 1785; - strlcpy (my_audio_config.achan[0].profiles, "D", sizeof(my_audio_config.achan[0].profiles)); } - else if (my_audio_config.achan[0].baud < 600) { + else if (my_audio_config.achan[0].baud < 600) { // e.g. HF SSB packet my_audio_config.achan[0].modem_type = MODEM_AFSK; my_audio_config.achan[0].mark_freq = 1600; my_audio_config.achan[0].space_freq = 1800; - strlcpy (my_audio_config.achan[0].profiles, "D", sizeof(my_audio_config.achan[0].profiles)); + // Previously we had a "D" which was fine tuned for 300 bps. + // In v1.7, it's not clear if we should use "B" or just stick with "A". } - else if (my_audio_config.achan[0].baud < 1800) { + else if (my_audio_config.achan[0].baud < 1800) { // common 1200 my_audio_config.achan[0].modem_type = MODEM_AFSK; my_audio_config.achan[0].mark_freq = DEFAULT_MARK_FREQ; my_audio_config.achan[0].space_freq = DEFAULT_SPACE_FREQ; - // Should default to E+ or something similar later. } else if (my_audio_config.achan[0].baud < 3600) { my_audio_config.achan[0].modem_type = MODEM_QPSK; @@ -460,20 +458,20 @@ int main (int argc, char *argv[]) my_audio_config.achan[0].space_freq = 0; strlcpy (my_audio_config.achan[0].profiles, "", sizeof(my_audio_config.achan[0].profiles)); } - else if (my_audio_config.achan[0].baud == 12345) { + else if (my_audio_config.achan[0].baud == 0xA15A15) { // Hack for different use of 9600 my_audio_config.achan[0].modem_type = MODEM_AIS; my_audio_config.achan[0].baud = 9600; my_audio_config.achan[0].mark_freq = 0; my_audio_config.achan[0].space_freq = 0; strlcpy (my_audio_config.achan[0].profiles, " ", sizeof(my_audio_config.achan[0].profiles)); // avoid getting default later. } - else if (my_audio_config.achan[0].baud == 23456) { + else if (my_audio_config.achan[0].baud == 0xEA5EA5) { my_audio_config.achan[0].modem_type = MODEM_EAS; my_audio_config.achan[0].baud = 521; // Actually 520.83 but we have an integer field here. // Will make more precise in afsk demod init. my_audio_config.achan[0].mark_freq = 2083; // Actually 2083.3 - logic 1. my_audio_config.achan[0].space_freq = 1563; // Actually 1562.5 - logic 0. - strlcpy (my_audio_config.achan[0].profiles, "D", sizeof(my_audio_config.achan[0].profiles)); + strlcpy (my_audio_config.achan[0].profiles, "A", sizeof(my_audio_config.achan[0].profiles)); } else { my_audio_config.achan[0].modem_type = MODEM_SCRAMBLE; @@ -482,6 +480,12 @@ int main (int argc, char *argv[]) strlcpy (my_audio_config.achan[0].profiles, " ", sizeof(my_audio_config.achan[0].profiles)); // avoid getting default later. } + if (my_audio_config.achan[0].baud < MIN_BAUD || my_audio_config.achan[0].baud > MAX_BAUD) { + text_color_set(DW_COLOR_ERROR); + dw_printf ("Use a more reasonable bit rate in range of %d - %d.\n", MIN_BAUD, MAX_BAUD); + exit (EXIT_FAILURE); + } + /* * -g option means force g3RUH regardless of speed. */ @@ -537,6 +541,7 @@ int main (int argc, char *argv[]) } fx25_init (d_x_opt); + il2p_init (d_2_opt); start_time = dtime_now(); @@ -614,9 +619,9 @@ int main (int argc, char *argv[]) my_audio_config.adev[0].bits_per_sample = format.wbitspersample; my_audio_config.adev[0].num_channels = format.nchannels; - my_audio_config.achan[0].medium = MEDIUM_RADIO; + my_audio_config.chan_medium[0] = MEDIUM_RADIO; if (format.nchannels == 2) { - my_audio_config.achan[1].medium = MEDIUM_RADIO; + my_audio_config.chan_medium[1] = MEDIUM_RADIO; } text_color_set(DW_COLOR_INFO); @@ -703,7 +708,7 @@ int main (int argc, char *argv[]) dw_printf ("%d packets decoded in %.3f seconds. %.1f x realtime\n", packets_decoded_total, elapsed, total_filetime/elapsed); if (d_o_opt) { dw_printf ("DCD count = %d\n", dcd_count); - dw_printf ("DCD missing erors = %d\n", dcd_missing_errors); + dw_printf ("DCD missing errors = %d\n", dcd_missing_errors); } if (error_if_less_than != -1 && packets_decoded_total < error_if_less_than) { @@ -752,14 +757,14 @@ int audio_get (int a) * This is called when we have a good frame. */ -void dlq_rec_frame (int chan, int subchan, int slice, packet_t pp, alevel_t alevel, int is_fx25, retry_t retries, char *spectrum) +void dlq_rec_frame (int chan, int subchan, int slice, packet_t pp, alevel_t alevel, fec_type_t fec_type, retry_t retries, char *spectrum) { char stemp[500]; unsigned char *pinfo; int info_len; int h; - char heard[AX25_MAX_ADDR_LEN]; + char heard[2 * AX25_MAX_ADDR_LEN + 20]; char alevel_text[AX25_ALEVEL_TO_TEXT_SIZE]; packets_decoded_one++; @@ -804,29 +809,48 @@ void dlq_rec_frame (int chan, int subchan, int slice, packet_t pp, alevel_t alev } ax25_alevel_to_text (alevel, alevel_text); - if (my_audio_config.achan[chan].fix_bits == RETRY_NONE && my_audio_config.achan[chan].passall == 0) { - dw_printf ("%s audio level = %s %s\n", heard, alevel_text, spectrum); - } - else if (is_fx25) { - dw_printf ("%s audio level = %s %s\n", heard, alevel_text, spectrum); + /* As suggested by KJ4ERJ, if we are receiving from */ + /* WIDEn-0, it is quite likely (but not guaranteed), that */ + /* we are actually hearing the preceding station in the path. */ + + if (h >= AX25_REPEATER_2 && + strncmp(heard, "WIDE", 4) == 0 && + isdigit(heard[4]) && + heard[5] == '\0') { + + char probably_really[AX25_MAX_ADDR_LEN]; + ax25_get_addr_with_ssid(pp, h-1, probably_really); + + strlcat (heard, " (probably ", sizeof(heard)); + strlcat (heard, probably_really, sizeof(heard)); + strlcat (heard, ")", sizeof(heard)); } - else { - assert (retries >= RETRY_NONE && retries <= RETRY_MAX); - dw_printf ("%s audio level = %s [%s] %s\n", heard, alevel_text, retry_text[(int)retries], spectrum); + + switch (fec_type) { + + case fec_type_fx25: + dw_printf ("%s audio level = %s FX.25 %s\n", heard, alevel_text, spectrum); + break; + + case fec_type_il2p: + dw_printf ("%s audio level = %s IL2P %s\n", heard, alevel_text, spectrum); + break; + + case fec_type_none: + default: + if (my_audio_config.achan[chan].fix_bits == RETRY_NONE && my_audio_config.achan[chan].passall == 0) { + // No fix_bits or passall specified. + dw_printf ("%s audio level = %s %s\n", heard, alevel_text, spectrum); + } + else { + assert (retries >= RETRY_NONE && retries <= RETRY_MAX); // validate array index. + dw_printf ("%s audio level = %s [%s] %s\n", heard, alevel_text, retry_text[(int)retries], spectrum); + } + break; } #endif -//#if defined(EXPERIMENT_G) || defined(EXPERIMENT_H) -// int j; -// -// for (j=0; j #include #include +#include #if USE_ALSA #include -#else -#include -#ifdef __OpenBSD__ -#include +#elif USE_SNDIO +#include +#include #else #include #endif -#endif #include "audio.h" @@ -111,6 +110,9 @@ static struct adev_s { int bytes_per_frame; /* number of bytes for a sample from all channels. */ /* e.g. 4 for stereo 16 bit. */ +#elif USE_SNDIO + struct sio_hdl *sndio_in_handle; + struct sio_hdl *sndio_out_handle; #else int oss_audio_device_fd; /* Single device, both directions. */ @@ -141,6 +143,9 @@ static struct adev_s { #if USE_ALSA static int set_alsa_params (int a, snd_pcm_t *handle, struct audio_s *pa, char *name, char *dir); //static void alsa_select_device (char *pick_dev, int direction, char *result); +#elif USE_SNDIO +static int set_sndio_params (int a, struct sio_hdl *handle, struct audio_s *pa, char *devname, char *inout); +static int poll_sndio (struct sio_hdl *hdl, int events); #else static int set_oss_params (int a, int fd, struct audio_s *pa); #endif @@ -201,7 +206,7 @@ static int calcbufsize(int rate, int chans, int bits) * more restrictive in its capabilities. * It might say, the best I can do is mono, 8 bit, 8000/sec. * - * The sofware modem must use this ACTUAL information + * The software modem must use this ACTUAL information * that the device is supplying, that could be different * than what the user specified. * @@ -212,7 +217,9 @@ static int calcbufsize(int rate, int chans, int bits) int audio_open (struct audio_s *pa) { +#if !USE_SNDIO int err; +#endif int chan; int a; char audio_in_name[30]; @@ -224,7 +231,11 @@ int audio_open (struct audio_s *pa) memset (adev, 0, sizeof(adev)); for (a=0; aadev[a].adevice_in, O_RDWR); @@ -430,6 +463,10 @@ int audio_open (struct audio_s *pa) text_color_set(DW_COLOR_ERROR); dw_printf ("Could not open audio device %s for output\n%s\n", audio_out_name, snd_strerror(err)); + if (err == -EBUSY) { + dw_printf ("This means that some other application is using that device.\n"); + dw_printf ("The solution is to identify that other application and stop it.\n"); + } return (-1); } @@ -439,6 +476,27 @@ int audio_open (struct audio_s *pa) return (-1); } +#elif USE_SNDIO + adev[a].sndio_out_handle = sio_open (audio_out_name, SIO_PLAY, 0); + if (adev[a].sndio_out_handle == NULL) { + text_color_set(DW_COLOR_ERROR); + dw_printf ("Could not open audio device %s for output\n", + audio_out_name); + return (-1); + } + + adev[a].outbuf_size_in_bytes = set_sndio_params (a, adev[a].sndio_out_handle, pa, audio_out_name, "output"); + + if (adev[a].inbuf_size_in_bytes <= 0 || adev[a].outbuf_size_in_bytes <= 0) { + return (-1); + } + + if (!sio_start (adev[a].sndio_out_handle)) { + text_color_set(DW_COLOR_ERROR); + dw_printf ("Could not start audio device %s for output\n", + audio_out_name); + return (-1); + } #endif /* @@ -675,6 +733,112 @@ static int set_alsa_params (int a, snd_pcm_t *handle, struct audio_s *pa, char * } /* end alsa_set_params */ +#elif USE_SNDIO + +/* + * Set parameters for sound card. (sndio) + * + * See /usr/include/sndio.h for details. + */ + +static int set_sndio_params (int a, struct sio_hdl *handle, struct audio_s *pa, char *devname, char *inout) +{ + + struct sio_par q, r; + + /* Signed 16 bit little endian or unsigned 8 bit. */ + sio_initpar (&q); + q.bits = pa->adev[a].bits_per_sample; + q.bps = (q.bits + 7) / 8; + q.sig = (q.bits == 8) ? 0 : 1; + q.le = 1; /* always little endian */ + q.msb = 0; /* LSB aligned */ + q.rchan = q.pchan = pa->adev[a].num_channels; + q.rate = pa->adev[a].samples_per_sec; + q.xrun = SIO_IGNORE; + q.appbufsz = calcbufsize(pa->adev[a].samples_per_sec, pa->adev[a].num_channels, pa->adev[a].bits_per_sample); + + +#if DEBUG + text_color_set(DW_COLOR_DEBUG); + dw_printf ("suggest buffer size %d bytes for %s %s.\n", + q.appbufsz, devname, inout); +#endif + + /* challenge new setting */ + if (!sio_setpar (handle, &q)) { + text_color_set(DW_COLOR_ERROR); + dw_printf ("Could not set hardware parameter for %s %s.\n", + devname, inout); + return (-1); + } + + /* get response */ + if (!sio_getpar (handle, &r)) { + text_color_set(DW_COLOR_ERROR); + dw_printf ("Could not obtain current hardware setting for %s %s.\n", + devname, inout); + return (-1); + } + +#if DEBUG + text_color_set(DW_COLOR_DEBUG); + dw_printf ("audio buffer size %d bytes for %s %s.\n", + r.appbufsz, devname, inout); +#endif + if (q.rate != r.rate) { + text_color_set(DW_COLOR_INFO); + dw_printf ("Asked for %d samples/sec but got %d for %s %s.", + pa->adev[a].samples_per_sec, r.rate, devname, inout); + pa->adev[a].samples_per_sec = r.rate; + } + + /* not supported */ + if (q.bits != r.bits || q.bps != r.bps || q.sig != r.sig || + (q.bits > 8 && q.le != r.le) || + (*inout == 'o' && q.pchan != r.pchan) || + (*inout == 'i' && q.rchan != r.rchan)) { + text_color_set(DW_COLOR_ERROR); + dw_printf ("Unsupported format for %s %s.\n", devname, inout); + return (-1); + } + + return r.appbufsz; + +} /* end set_sndio_params */ + +static int poll_sndio (struct sio_hdl *hdl, int events) +{ + struct pollfd *pfds; + int nfds, revents; + + nfds = sio_nfds (hdl); + pfds = alloca (nfds * sizeof(struct pollfd)); + + do { + nfds = sio_pollfd (hdl, pfds, events); + if (nfds < 1) { + /* no need to wait */ + return (0); + } + if (poll (pfds, nfds, -1) < 0) { + text_color_set(DW_COLOR_ERROR); + dw_printf ("poll %d\n", errno); + return (-1); + } + revents = sio_revents (hdl, pfds); + } while (!(revents & (events | POLLHUP))); + + /* unrecoverable error occurred */ + if (revents & POLLHUP) { + text_color_set(DW_COLOR_ERROR); + dw_printf ("waited for %s, POLLHUP received\n", (events & POLLIN) ? "POLLIN" : "POLLOUT"); + return (-1); + } + + return (0); +} + #else @@ -769,7 +933,7 @@ static int set_oss_params (int a, int fd, struct audio_s *pa) * This was long ago under different conditions. * Should study this again some day. * - * Your milage may vary. + * Your mileage may vary. */ err = ioctl (fd, SNDCTL_DSP_GETBLKSIZE, &ossbuf_size_in_bytes); if (err == -1) { @@ -842,7 +1006,9 @@ __attribute__((hot)) int audio_get (int a) { int n; +#if USE_ALSA int retries = 0; +#endif #if STATISTICS /* Gather numbers for read from audio device. */ @@ -936,6 +1102,8 @@ int audio_get (int a) dw_printf ("This is most likely caused by 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"); + dw_printf ("line option for 9600 bps or -D3 for slower AFSK .\n"); } audio_stats (a, @@ -970,7 +1138,28 @@ int audio_get (int a) } -#else /* end ALSA, begin OSS */ +#elif USE_SNDIO + + while (adev[a].inbuf_next >= adev[a].inbuf_len) { + + assert (adev[a].sndio_in_handle != NULL); + if (poll_sndio (adev[a].sndio_in_handle, POLLIN) < 0) { + adev[a].inbuf_len = 0; + adev[a].inbuf_next = 0; + return (-1); + } + + n = sio_read (adev[a].sndio_in_handle, adev[a].inbuf_ptr, adev[a].inbuf_size_in_bytes); + adev[a].inbuf_len = n; + adev[a].inbuf_next = 0; + + audio_stats (a, + save_audio_config_p->adev[a].num_channels, + n / (save_audio_config_p->adev[a].num_channels * save_audio_config_p->adev[a].bits_per_sample / 8), + save_audio_config_p->statistics_interval); + } + +#else /* begin OSS */ /* Fixed in 1.2. This was formerly outside of the switch */ /* so the OSS version did not process stdin or UDP. */ @@ -1250,6 +1439,37 @@ int audio_flush (int a) adev[a].outbuf_len = 0; return (-1); +#elif USE_SNDIO + + int k; + unsigned char *ptr; + int len; + + ptr = adev[a].outbuf_ptr; + len = adev[a].outbuf_len; + + while (len > 0) { + assert (adev[a].sndio_out_handle != NULL); + if (poll_sndio (adev[a].sndio_out_handle, POLLOUT) < 0) { + text_color_set(DW_COLOR_ERROR); + perror("Can't write to audio device"); + adev[a].outbuf_len = 0; + return (-1); + } + + k = sio_write (adev[a].sndio_out_handle, ptr, len); +#if DEBUGx + text_color_set(DW_COLOR_DEBUG); + dw_printf ("audio_flush(): write %d returns %d\n", len, k); + fflush (stdout); +#endif + ptr += k; + len -= k; + } + + adev[a].outbuf_len = 0; + return (0); + #else /* OSS */ int k; @@ -1320,7 +1540,7 @@ int audio_flush (int a) * (3) Call this function, which might or might not wait long enough. * (4) Add (1) and (2) resulting in when PTT should be turned off. * (5) Take difference between current time and desired PPT off time - * and wait for additoinal time if required. + * and wait for additional time if required. * *----------------------------------------------------------------*/ @@ -1351,6 +1571,10 @@ void audio_wait (int a) * Either way, the caller will now compensate for it. */ +#elif USE_SNDIO + + poll_sndio (adev[a].sndio_out_handle, POLLOUT); + #else assert (adev[a].oss_audio_device_fd > 0); @@ -1396,7 +1620,22 @@ int audio_close (void) snd_pcm_close (adev[a].audio_in_handle); snd_pcm_close (adev[a].audio_out_handle); - + + adev[a].audio_in_handle = adev[a].audio_out_handle = NULL; + +#elif USE_SNDIO + + if (adev[a].sndio_in_handle != NULL && adev[a].sndio_out_handle != NULL) { + + audio_wait (a); + + sio_stop (adev[a].sndio_in_handle); + sio_stop (adev[a].sndio_out_handle); + sio_close (adev[a].sndio_in_handle); + sio_close (adev[a].sndio_out_handle); + + adev[a].sndio_in_handle = adev[a].sndio_out_handle = NULL; + #else if (adev[a].oss_audio_device_fd > 0) { diff --git a/src/audio.h b/src/audio.h index 61dec9d9..cb5ca94e 100644 --- a/src/audio.h +++ b/src/audio.h @@ -72,7 +72,7 @@ struct audio_s { struct adev_param_s { - /* Properites of the sound device. */ + /* Properties of the sound device. */ int defined; /* Was device defined? */ /* First one defaults to yes. */ @@ -102,15 +102,16 @@ struct audio_s { /* This is the probability, in per cent, of randomly corrupting it. */ /* Normally this is 0. 25 would mean corrupt it 25% of the time. */ - int recv_error_rate; /* Similar but the % probablity of dropping a received frame. */ + int recv_error_rate; /* Similar but the % probability of dropping a received frame. */ 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. */ + //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. */ @@ -118,32 +119,50 @@ struct audio_s { /* I put it here, rather than with the rest of the link layer */ /* parameters because it is really a part of the HDLC layer */ /* and is part of the KISS TNC functionality rather than our data link layer. */ + /* Future: not used yet. */ + char timestamp_format[40]; /* -T option */ /* Precede received & transmitted frames with timestamp. */ /* Command line option uses "strftime" format string. */ - /* Properties for each channel, common to receive and transmit. */ - /* Can be different for each radio channel. */ /* 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. */ + // Properties for all channels. - struct achan_param_s { - - // Originally there was a boolean, called "valid", to indicate that the - // channel is valid. This has been replaced with the new "medium" which - // will allow channels to correspond to things other than internal modems. - - enum medium_e medium; // MEDIUM_NONE for invalid. + enum medium_e chan_medium[MAX_TOTAL_CHANS]; + // MEDIUM_NONE for invalid. // MEDIUM_RADIO for internal modem. (only possibility earlier) // MEDIUM_IGATE allows application access to IGate. + // MEDIUM_NETTNC for external TNC via TCP. + + int igate_vchannel; /* Virtual channel mapped to APRS-IS. */ + /* -1 for none. */ + /* Redundant but it makes things quicker and simpler */ + /* than always searching thru above. */ + + /* Properties for each radio channel, common to receive and transmit. */ + /* Can be different for each radio channel. */ + struct achan_param_s { + // Currently, we have a fixed mapping from audio sources to channel. + // + // ADEVICE CHANNEL (mono) (stereo) + // 0 0 0, 1 + // 1 2 2, 3 + // 2 4 4, 5 + // + // A future feauture might allow the user to specify a different audio source. + // This would allow multiple modems (with associated channel) to share an audio source. + // 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. */ @@ -156,9 +175,26 @@ 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; + + // IL2P - New for version 1.7. + // New layer 2 with FEC. Much less overhead than FX.25 but no longer backward compatible. + // Only applies to transmit. + // Listening for FEC sync word should add negligible overhead so + // we leave reception enabled all the time as we do with FX.25. + // TODO: FX.25 should probably be put here rather than global for all channels. + + int fx25_strength; // Strength of FX.25 FEC. + // 16, 23, 64 for specific number of parity symbols. + // 1 for automatic selection based on frame size. + + int il2p_max_fec; // 1 for max FEC length, 0 for automatic based on size. + + int il2p_invert_polarity; // 1 means invert on transmit. Receive handles either automatically. + enum v26_e { V26_UNSPECIFIED=0, V26_A, V26_B } v26_alternative; - // Original implementaion used alternative A for 2400 bbps PSK. + // Original implementation used alternative A for 2400 bbps PSK. // Years later, we discover that MFJ-2400 used alternative B. // It's likely the others did too. it also works a little better. // Default to MFJ compatible and print warning if user did not @@ -240,15 +276,17 @@ struct audio_s { ptt_method_t ptt_method; /* none, serial port, GPIO, LPT, HAMLIB, CM108. */ - char ptt_device[100]; /* Serial device name for PTT. e.g. COM1 or /dev/ttyS0 */ + char ptt_device[128]; /* Serial device name for PTT. e.g. COM1 or /dev/ttyS0 */ /* Also used for HAMLIB. Could be host:port when model is 1. */ /* For years, 20 characters was plenty then we start getting extreme names like this: */ /* /dev/serial/by-id/usb-FTDI_Navigator__CAT___2nd_PTT__00000000-if00-port0 */ /* /dev/serial/by-id/usb-Prolific_Technology_Inc._USB-Serial_Controller_D-if00-port0 */ /* Issue 104, changed to 100 bytes in version 1.5. */ - /* This same field is also used for CM108 GPIO PTT which will */ - /* have a name like /dev/hidraw1. */ + /* This same field is also used for CM108/CM119 GPIO PTT which will */ + /* have a name like /dev/hidraw1 for Linux or */ + /* \\?\hid#vid_0d8c&pid_0008&mi_03#8&39d3555&0&0000#{4d1e55b2-f16f-11cf-88cb-001111000030} */ + /* for Windows. Largest observed was 95 but add some extra to be safe. */ ptt_line_t ptt_line; /* Control line when using serial port. PTT_LINE_RTS, PTT_LINE_DTR. */ ptt_line_t ptt_line2; /* Optional second one: PTT_LINE_NONE when not used. */ @@ -256,12 +294,12 @@ struct audio_s { int out_gpio_num; /* GPIO number. Originally this was only for PTT. */ /* It is now more general. */ /* octrl array is indexed by PTT, DCD, or CONnected indicator. */ - /* For CM108, this should be in range of 1-8. */ + /* For CM108/CM119, this should be in range of 1-8. */ #define MAX_GPIO_NAME_LEN 20 // 12 would cover any case I've seen so this should be safe char out_gpio_name[MAX_GPIO_NAME_LEN]; - /* orginally, gpio number NN was assumed to simply */ + /* originally, gpio number NN was assumed to simply */ /* have the name gpioNN but this turned out not to be */ /* the case for CubieBoard where it was longer. */ /* This is filled in by ptt_init so we don't have to */ @@ -298,7 +336,7 @@ struct audio_s { int in_gpio_num; /* GPIO number */ char in_gpio_name[MAX_GPIO_NAME_LEN]; - /* orginally, gpio number NN was assumed to simply */ + /* originally, gpio number NN was assumed to simply */ /* have the name gpioNN but this turned out not to be */ /* the case for CubieBoard where it was longer. */ /* This is filled in by ptt_init so we don't have to */ @@ -312,7 +350,7 @@ struct audio_s { int dwait; /* First wait extra time for receiver squelch. */ /* Default 0 units of 10 mS each . */ - int slottime; /* Slot time in 10 mS units for persistance algorithm. */ + int slottime; /* Slot time in 10 mS units for persistence algorithm. */ /* Typical value is 10 meaning 100 milliseconds. */ int persist; /* Sets probability for transmitting after each */ @@ -350,8 +388,8 @@ struct audio_s { #define DEFAULT_ADEVICE "" /* Mac OSX: Empty string = default audio device. */ #elif USE_ALSA #define DEFAULT_ADEVICE "default" /* Use default device for ALSA. */ -#elif __OpenBSD__ -#define DEFAULT_ADEVICE "default" /* Use default device for OpenBSD-portaudio. */ +#elif USE_SNDIO +#define DEFAULT_ADEVICE "default" /* Use default device for sndio. */ #else #define DEFAULT_ADEVICE "/dev/dsp" /* First audio device for OSS. (FreeBSD) */ #endif @@ -388,7 +426,8 @@ struct audio_s { #define DEFAULT_BITS_PER_SAMPLE 16 -#define DEFAULT_FIX_BITS RETRY_INVERT_SINGLE +#define DEFAULT_FIX_BITS RETRY_NONE // Interesting research project but even a single bit fix up + // will occasionally let corrupted packets through. /* * Standard for AFSK on VHF FM. @@ -418,11 +457,11 @@ struct audio_s { */ #define DEFAULT_DWAIT 0 -#define DEFAULT_SLOTTIME 10 +#define DEFAULT_SLOTTIME 10 // *10mS = 100mS #define DEFAULT_PERSIST 63 -#define DEFAULT_TXDELAY 30 -#define DEFAULT_TXTAIL 10 -#define DEFAULT_FULLDUP 0 +#define DEFAULT_TXDELAY 30 // *10mS = 300mS +#define DEFAULT_TXTAIL 10 // *10mS = 100mS +#define DEFAULT_FULLDUP 0 // false = half duplex /* * Note that we have two versions of these in audio.c and audio_win.c. diff --git a/src/audio_portaudio.c b/src/audio_portaudio.c index 6d53f6af..cb6ccf10 100644 --- a/src/audio_portaudio.c +++ b/src/audio_portaudio.c @@ -156,7 +156,7 @@ static int calcbufsize(int rate, int chans, int bits) * the same device name for more then one connected device * (ie two SignaLinks). Appending a Portaudio device index to the * the device name ensure we can find the correct one. And if it's not - * available return the first occurence that matches the device name. + * available return the first occurrence that matches the device name. *----------------------------------------------------------------*/ static int searchPADevice(struct adev_s *dev, char *_devName, int reqDeviceNo, int io_flag) { @@ -213,7 +213,21 @@ static int pa_devNN(char *deviceStr, char *_devName, size_t length, int *_devNo) while(*cPtr) { cVal = *cPtr++; if(cVal == ':') break; - if(((cVal >= ' ') && (cVal <= '~')) && (count < length)) { + + // See Issue 417. + // Originally this copied only printable ASCII characters (space thru ~). + // That is a problem for some locales that use UTF-8 characters in the device name. + // original: if(((cVal >= ' ') && (cVal <= '~')) && (count < length)) { + + // At first I was thinking we should keep the test for < ' ' but then I + // remembered that char type can be signed or unsigned depending on implementation. + // If characters are signed then a value above 0x7f would be considered negative. + + // It seems to me that the test for buffer full is off by one. + // count could reach length, leaving no room for a nul terminator. + // Compare has been changed so count is limited to length minus 1. + + if(count < length - 1) { _devName[count++] = cVal; } @@ -513,7 +527,7 @@ static int paOutput16CB( const void *inputBuffer, void *outputBuffer, * more restrictive in its capabilities. * It might say, the best I can do is mono, 8 bit, 8000/sec. * - * The sofware modem must use this ACTUAL information + * The software modem must use this ACTUAL information * that the device is supplying, that could be different * than what the user specified. * @@ -1149,7 +1163,7 @@ int audio_put (int a, int c) static double start = 0, end = 0, diff = 0; if(adev[a].outbuf_len == 0) - start = dtime_now(); + start = dtime_monotonic(); #endif if(c >= 0) { @@ -1178,7 +1192,7 @@ int audio_put (int a, int c) #ifdef __TIMED__ count += frames; if(c < 0) { // When the Ax25 frames are flushed. - end = dtime_now(); + end = dtime_monotonic(); diff = end - start; if(count) dw_printf ("Transfer Time:%3.9f No of Frames:%d Per frame:%3.9f speed:%f\n", @@ -1246,7 +1260,7 @@ int audio_flush (int a) * (3) Call this function, which might or might not wait long enough. * (4) Add (1) and (2) resulting in when PTT should be turned off. * (5) Take difference between current time and desired PPT off time - * and wait for additoinal time if required. + * and wait for additional time if required. * *----------------------------------------------------------------*/ diff --git a/src/audio_stats.c b/src/audio_stats.c index b6549cab..7b94b1ee 100644 --- a/src/audio_stats.c +++ b/src/audio_stats.c @@ -65,7 +65,6 @@ #include "audio_stats.h" #include "textcolor.h" -#include "dtime_now.h" #include "demod.h" /* for alevel_t & demod_get_audio_level() */ diff --git a/src/audio_win.c b/src/audio_win.c index 2183d107..85a1548b 100644 --- a/src/audio_win.c +++ b/src/audio_win.c @@ -84,7 +84,7 @@ static struct audio_s *save_audio_config_p; */ /* - * Originally, we had an abitrary buf time of 40 mS. + * Originally, we had an arbitrary buf time of 40 mS. * * For mono, the buffer size was rounded up from 3528 to 4k so * it was really about 50 mS per buffer or about 20 per second. @@ -209,7 +209,7 @@ static struct adev_s { * more restrictive in its capabilities. * It might say, the best I can do is mono, 8 bit, 8000/sec. * - * The sofware modem must use this ACTUAL information + * The software modem must use this ACTUAL information * that the device is supplying, that could be different * than what the user specified. * @@ -561,6 +561,8 @@ int audio_open (struct audio_s *pa) */ case AUDIO_IN_TYPE_SOUNDCARD: + // Use InitializeCriticalSectionAndSpinCount to avoid exceptions in low memory situations? + InitializeCriticalSection (&(A->in_cs)); err = waveInOpen (&(A->audio_in_handle), in_dev_no[a], &wf, (DWORD_PTR)in_callback, a, CALLBACK_FUNCTION); @@ -921,7 +923,7 @@ int audio_get (int a) * c - One byte in range of 0 - 255. * * - * Global In: out_current - index of output buffer currenly being filled. + * Global In: out_current - index of output buffer currently being filled. * * Returns: Normally non-negative. * -1 for any type of error. @@ -1072,7 +1074,7 @@ int audio_flush (int a) * (3) Call this function, which might or might not wait long enough. * (4) Add (1) and (2) resulting in when PTT should be turned off. * (5) Take difference between current time and desired PPT off time - * and wait for additoinal time if required. + * and wait for additional time if required. * *----------------------------------------------------------------*/ diff --git a/src/ax25_link.c b/src/ax25_link.c index 041066e1..3043a046 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 John Langner, WB2OSZ +// Copyright (C) 2016, 2017, 2018, 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 @@ -26,6 +26,10 @@ * Establish connections and transfer data in the proper * order with retries. * + * Using the term "data link" is rather unfortunate because it causes + * confusion to someone familiar with the OSI networking model. + * This corresponds to the layer 4 transport, not layer 2 data link. + * * Description: * * Typical sequence for establishing a connection @@ -190,14 +194,16 @@ // Debug switches for different types of information. // Should have command line options instead of changing source and recompiling. -static int s_debug_protocol_errors = 1; // Less serious Protocol errors. +static int s_debug_protocol_errors = 0; // Less serious Protocol errors. // Useful for debugging but unnecessarily alarming other times. + // Was it intentially left on for release 1.6? static int s_debug_client_app = 0; // Interaction with client application. // dl_connect_request, dl_data_request, dl_data_indication, etc. static int s_debug_radio = 0; // Received frames and channel busy status. // lm_data_indication, lm_channel_busy + static int s_debug_variables = 0; // Variables, state changes. static int s_debug_retry = 0; // Related to lost I frames, REJ, SREJ, timeout, resending. @@ -246,7 +252,7 @@ typedef struct ax25_dlsm_s { // notifications about state changes. - char addrs[AX25_MAX_REPEATERS][AX25_MAX_ADDR_LEN]; + char addrs[AX25_MAX_ADDRS][AX25_MAX_ADDR_LEN]; // Up to 10 addresses, same order as in frame. int num_addr; // Number of addresses. Should be in range 2 .. 10. @@ -255,7 +261,7 @@ typedef struct ax25_dlsm_s { // addrs[OWNCALL] is owncall for this end of link. // Note that we are acting on behalf of // a client application so the APRS mycall - // might not be relevent. + // might not be relevant. #define PEERCALL AX25_DESTINATION // addrs[PEERCALL] is call for other end. @@ -319,8 +325,8 @@ typedef struct ax25_dlsm_s { int reject_exception; // A REJ frame has been sent to the remote station. (boolean) - // This is used only when receving an I frame, in states 3 & 4, SREJ not enabled. - // When an I frame has an unepected N(S), + // This is used only when receiving an I frame, in states 3 & 4, SREJ not enabled. + // When an I frame has an unexpected N(S), // - if not already set, set it and send REJ. // When an I frame with expected N(S) is received, clear it. // This would prevent us from sending additional REJ while @@ -343,7 +349,7 @@ typedef struct ax25_dlsm_s { // Sometimes the flow chart has SAT instead of SRT. // I think that is a typographical error. - float t1v; // How long to wait for an acknowlegement before resending. + float t1v; // How long to wait for an acknowledgement before resending. // Value used when starting timer T1, in seconds. // "FRACK" parameter in some implementations. // Typically it might be 3 seconds after frame has been @@ -590,6 +596,8 @@ static int AX25MODULO(int n, int m, const char *file, const char *func, int line #define PAUSE_TM201 pause_tm201(S, __func__, __LINE__) #define RESUME_TM201 resume_tm201(S, __func__, __LINE__) +// TODO: add SELECT_T1_VALUE for debugging. + static void dl_data_indication (ax25_dlsm_t *S, int pid, char *data, int len); @@ -824,6 +832,11 @@ static ax25_dlsm_t *get_link_handle (char addrs[AX25_MAX_ADDRS][AX25_MAX_ADDR_LE // Create new data link state machine. p = calloc (sizeof(ax25_dlsm_t), 1); + if (p == NULL) { + text_color_set(DW_COLOR_ERROR); + dw_printf ("FATAL ERROR: Out of memory.\n"); + exit (EXIT_FAILURE); + } p->magic1 = MAGIC1; p->start_time = dtime_now(); p->stream_id = next_stream_id++; @@ -1066,12 +1079,31 @@ void dl_disconnect_request (dlq_item_t *E) case state_1_awaiting_connection: case state_5_awaiting_v22_connection: -// TODO: "requeue." Not sure what to do here. -// If we put it back in the queue we will get it back again probably still in same state. -// Need a way to defer it until the next state change. +// Erratum: The protocol spec says "requeue." If we put disconnect req back in the +// queue we will probably get it back again here while still in same state. +// I don't think we would want to delay it until the next state transition. + +// Suppose someone tried to connect to another station, which is not responding, and decided to cancel +// before all of the SABMe retries were used up. I think we would want to transmit a DISC, send a disc +// notice to the user, and go directly into disconnected state, rather than into awaiting release. + +// New code v1.7 dev, May 6 2023 + text_color_set(DW_COLOR_INFO); + dw_printf ("Stream %d: In progress connection attempt to %s terminated by user.\n", S->stream_id, S->addrs[PEERCALL]); + discard_i_queue (S); + SET_RC(0); + int p1 = 1; + int nopid0 = 0; + packet_t pp15 = ax25_u_frame (S->addrs, S->num_addr, cr_cmd, frame_type_U_DISC, p1, nopid0, NULL, 0); + lm_data_request (S->chan, TQ_PRIO_1_LO, pp15); + + STOP_T1; // started in establish_data_link. + STOP_T3; // probably don't need. + enter_new_state (S, state_0_disconnected, __func__, __LINE__); + server_link_terminated (S->chan, S->client, S->addrs[PEERCALL], S->addrs[OWNCALL], 0); break; - + case state_2_awaiting_release: { // We have previously started the disconnect sequence and are waiting @@ -1157,7 +1189,7 @@ void dl_disconnect_request (dlq_item_t *E) * * Erratum: Not sure how to interpret that. See example below for how it was implemented. * - * Version 1.6: Bug 252. Segmentation was occuring for a V2.0 link. From the spec: + * Version 1.6: Bug 252. Segmentation was occurring for a V2.0 link. From the spec: * "The receipt of an XID response from the other station establishes that both * stations are using AX.25 version 2.2 or higher and enables the use of the * segmenter/reassembler and selective reject." @@ -1493,6 +1525,11 @@ void dl_register_callsign (dlq_item_t *E) } r = calloc(sizeof(reg_callsign_t),1); + if (r == NULL) { + text_color_set(DW_COLOR_ERROR); + dw_printf ("FATAL ERROR: Out of memory.\n"); + exit (EXIT_FAILURE); + } strlcpy (r->callsign, E->addrs[0], sizeof(r->callsign)); r->chan = E->chan; r->client = E->client; @@ -1564,15 +1601,51 @@ void dl_unregister_callsign (dlq_item_t *E) * * Description: This is the sum of: * - Incoming connected data, from application still in the queue. - * - I frames which have been transmitted but not yet acknowleged. + * - I frames which have been transmitted but not yet acknowledged. + * + * Confusion: https://github.com/wb2osz/direwolf/issues/427 + * + * There are different, inconsistent versions of the protocol spec. + * + * One of them simply has: + * + * CallFrom is our call + * CallTo is the call of the other station + * + * A more detailed version has the same thing in the table of fields: + * + * CallFrom 10 bytes Our CallSign + * CallTo 10 bytes Other CallSign + * + * (My first implementation went with that.) + * + * HOWEVER, shortly after that, is contradictory information: + * + * Careful must be exercised to fill correctly both the CallFrom + * and CallTo fields to match the ones of an existing connection, + * otherwise AGWPE won’t return any information at all from this query. + * + * The order of the CallFrom and CallTo is not trivial, it should + * reflect the order used to start the connection, so + * + * * If we started the connection CallFrom=US and CallTo=THEM + * * If the other end started the connection CallFrom=THEM and CallTo=US + * + * This seems to make everything unnecessarily more complicated. + * We should only care about the stream going from the local station to the + * remote station. Why would it matter who reqested the link? The state + * machine doesn't even contain this information so the TNC doesn't know. + * The client app interface needs to behave differently for the two cases. + * + * The new code, below, May 2023, should handle both of those cases. * *------------------------------------------------------------------------------*/ void dl_outstanding_frames_request (dlq_item_t *E) { ax25_dlsm_t *S; - int ok_to_create = 0; // must exist already. - + const int ok_to_create = 0; // must exist already. + int reversed_addrs = 0; if (s_debug_client_app) { text_color_set(DW_COLOR_DEBUG); @@ -1580,12 +1653,28 @@ void dl_outstanding_frames_request (dlq_item_t *E) } S = get_link_handle (E->addrs, E->num_addr, E->chan, E->client, ok_to_create); - - if (S == NULL) { - text_color_set(DW_COLOR_ERROR); - dw_printf ("Can't get outstanding frames for %s -> %s, chan %d\n", E->addrs[OWNCALL], E->addrs[PEERCALL], E->chan); - server_outstanding_frames_reply (E->chan, E->client, E->addrs[OWNCALL], E->addrs[PEERCALL], 0); - return; + if (S != NULL) { + reversed_addrs = 0; + } + else { + // Try swapping the addresses. + // this is communicating with the client app, not over the air, + // so we don't need to worry about digipeaters. + + char swapped[AX25_MAX_ADDRS][AX25_MAX_ADDR_LEN]; + memset (swapped, 0, sizeof(swapped)); + strlcpy (swapped[PEERCALL], E->addrs[OWNCALL], sizeof(swapped[PEERCALL])); + strlcpy (swapped[OWNCALL], E->addrs[PEERCALL], sizeof(swapped[OWNCALL])); + S = get_link_handle (swapped, E->num_addr, E->chan, E->client, ok_to_create); + if (S != NULL) { + reversed_addrs = 1; + } + else { + text_color_set(DW_COLOR_ERROR); + dw_printf ("Can't get outstanding frames for %s -> %s, chan %d\n", E->addrs[OWNCALL], E->addrs[PEERCALL], E->chan); + server_outstanding_frames_reply (E->chan, E->client, E->addrs[OWNCALL], E->addrs[PEERCALL], 0); + return; + } } // Add up these @@ -1614,7 +1703,13 @@ void dl_outstanding_frames_request (dlq_item_t *E) } } - server_outstanding_frames_reply (S->chan, S->client, S->addrs[OWNCALL], S->addrs[PEERCALL], count1 + count2); + if (reversed_addrs) { + // Other end initiated the link. + server_outstanding_frames_reply (S->chan, S->client, S->addrs[PEERCALL], S->addrs[OWNCALL], count1 + count2); + } + else { + server_outstanding_frames_reply (S->chan, S->client, S->addrs[OWNCALL], S->addrs[PEERCALL], count1 + count2); + } } // end dl_outstanding_frames_request @@ -1633,7 +1728,7 @@ void dl_outstanding_frames_request (dlq_item_t *E) * Description: By client application we mean something that attached with the * AGW network protocol. * - * Clean out anything related to the specfied client application. + * Clean out anything related to the specified client application. * This would include state machines and registered callsigns. * *------------------------------------------------------------------------------*/ @@ -2201,7 +2296,7 @@ void lm_data_indication (dlq_item_t *E) break; // Erratum: The AX.25 spec is not clear about whether SREJ should be command, response, or both. -// The underlying X.25 spec clearly says it is reponse only. Let's go with that. +// The underlying X.25 spec clearly says it is response only. Let's go with that. case frame_type_S_SREJ: case frame_type_U_DM: @@ -2224,7 +2319,7 @@ void lm_data_indication (dlq_item_t *E) case frame_type_U_UI: // Don't test at this point in case an APRS frame gets thru. // APRS doesn't specify what to put in the Source and Dest C bits. - // In practice we see all 4 possble combinations. + // In practice we see all 4 possible combinations. // I have an opinion about what would be "correct" (discussed elsewhere) // but in practice no one seems to care. break; @@ -2701,7 +2796,7 @@ static void i_frame_continued (ax25_dlsm_t *S, int p, int ns, int pid, char *inf if (S->rxdata_by_ns[ns] != NULL) { // There is a possibility that we might have another received frame stashed - // away from 8 or 128 (modulo) frames back. Remove it so it doesn't accidently + // away from 8 or 128 (modulo) frames back. Remove it so it doesn't accidentally // show up at some future inopportune time. cdata_delete (S->rxdata_by_ns[ns]); @@ -2786,7 +2881,7 @@ static void i_frame_continued (ax25_dlsm_t *S, int p, int ns, int pid, char *inf // we discard 3,4,5,6, and tell the other end to resend everything starting with 2. // At one time, I had some doubts about when to use command or response for REJ. -// I now believe that reponse, as implied by setting F in the flow chart, is correct. +// I now believe that response, as implied by setting F in the flow chart, is correct. int f = p; int nr = S->vr; // Next expected sequence number. @@ -2796,7 +2891,7 @@ static void i_frame_continued (ax25_dlsm_t *S, int p, int ns, int pid, char *inf S->reject_exception = 1; if (s_debug_retry) { - text_color_set(DW_COLOR_ERROR); // make it more noticable. + text_color_set(DW_COLOR_ERROR); // make it more noticeable. dw_printf ("sending REJ, at %s %d, SREJ not enabled case, V(R)=%d", __func__, __LINE__, S->vr); } @@ -2860,7 +2955,7 @@ static void i_frame_continued (ax25_dlsm_t *S, int p, int ns, int pid, char *inf // In version 1.4: // We end up sending more SREJ than necessary and and get back redundant information. Example: // When we see 113 missing, we ask for a resend. -// When we see 115 & 116 missing, a cummulative SREJ asks for everything. +// When we see 115 & 116 missing, a cumulative SREJ asks for everything. // The other end dutifully sends 113 twice. // // [0.4] DW1>DW0:(SREJ res, n(r)=113, f=0) @@ -2890,7 +2985,7 @@ static void i_frame_continued (ax25_dlsm_t *S, int p, int ns, int pid, char *inf // int allow_f1 = 0; // F=1 from X.25 2.4.6.4 b) 3) int allow_f1 = 1; // F=1 from X.25 2.4.6.4 b) 3) -// send only for this gap, not cummulative from V(R). +// send only for this gap, not cumulative from V(R). int last = AX25MODULO(ns - 1, S->modulo, __FILE__, __func__, __LINE__); int first = last; @@ -2993,7 +3088,7 @@ dw_printf ("%s:%d, %d srej exceptions, V(R)=%d, N(S)=%d\n", __func__, __LINE__, if (first == AX25MODULO(S->vr - 1, S->modulo, __FILE__, __func__, __LINE__)) { // Oops! Went too far. This I frame was already processed. text_color_set(DW_COLOR_ERROR); - dw_printf ("INTERNAL ERROR calulating what to put in SREJ, %s line %d\n", __func__, __LINE__); + dw_printf ("INTERNAL ERROR calculating what to put in SREJ, %s line %d\n", __func__, __LINE__); dw_printf ("V(R)=%d, N(S)=%d, SREJ exception=%d, first=%d, ask_resend_count=%d\n", S->vr, ns, selective_reject_exception(S), first, ask_resend_count); int k; for (k=0; k<128; k++) { @@ -3146,7 +3241,7 @@ static void send_srej_frames (ax25_dlsm_t *S, int *resend, int count, int allow_ if (s_debug_retry) { text_color_set(DW_COLOR_INFO); dw_printf ("%s line %d\n", __func__, __LINE__); - //dw_printf ("state=%d, count=%d, k=%d, V(R)=%d, SREJ exeception=%d\n", S->state, count, S->k_maxframe, S->vr, selective_reject_exception(S)); + //dw_printf ("state=%d, count=%d, k=%d, V(R)=%d, SREJ exception=%d\n", S->state, count, S->k_maxframe, S->vr, selective_reject_exception(S)); dw_printf ("state=%d, count=%d, k=%d, V(R)=%d\n", S->state, count, S->k_maxframe, S->vr); dw_printf ("resend[]="); @@ -3447,7 +3542,7 @@ static void rr_rnr_frame (ax25_dlsm_t *S, int ready, cmdres_t cr, int pf, int nr // we received RR frames with N(R) values indicating that the other side received everything // that we sent. Eventually rc could reach the limit and we would get an error. // If we are in state 4, and other guy ack'ed last I frame we sent, transition to state 3. -// The same thing was done for receving I frames after check_i_frame_ackd. +// The same thing was done for receiving I frames after check_i_frame_ackd. // Thought: Could we simply call check_i_frame_ackd, for consistency, rather than only setting V(A)? @@ -3759,7 +3854,7 @@ static void rej_frame (ax25_dlsm_t *S, cmdres_t cr, int pf, int nr) * * The SREJ command/response initiates more-efficient error recovery by requesting the retransmission of a * single I frame following the detection of a sequence error. This is an advancement over the earlier versions in - * which the requested I frame was retransmitted togther with all additional I frames subsequently transmitted and + * which the requested I frame was retransmitted together with all additional I frames subsequently transmitted and * successfully received. * * When a TNC sends one or more SREJ commands, each with the P bit set to "0" or "1", or one or more SREJ @@ -4409,7 +4504,7 @@ static void disc_frame (ax25_dlsm_t *S, int p) * earliest opportunity. If the TNC is not capable of accepting a SABME command, it responds with a DM frame. * * A TNC that uses a version of AX.25 prior to v2.2 responds with a FRMR. - * ( I think the KPC-3+ has a bug - it replys with DM - WB2OSZ ) + * ( I think the KPC-3+ has a bug - it replies with DM - WB2OSZ ) * * 4.3.3.5. Disconnected Mode (DM) Response * @@ -4636,7 +4731,7 @@ static void ua_frame (ax25_dlsm_t *S, int f) if (f == 1) { if (S->layer_3_initiated) { text_color_set(DW_COLOR_INFO); - // TODO: add via if apppropriate. + // TODO: add via if appropriate. dw_printf ("Stream %d: Connected to %s. (%s)\n", S->stream_id, S->addrs[PEERCALL], S->state == state_5_awaiting_v22_connection ? "v2.2" : "v2.0"); // There is a subtle difference here between connect confirm and indication. // connect *confirm* means "has been made" @@ -5615,7 +5710,7 @@ static void clear_exception_conditions (ax25_dlsm_t *S) * * Other guy gets RR/RNR command P=1. * Same action for either state 3 or 4. - * Whether he has outstanding un-ack'ed sent I frames is irrelevent. + * Whether he has outstanding un-ack'ed sent I frames is irrelevant. * He calls "enquiry response" which sends RR/RNR response F=1. * (Read about detour 1 below and in enquiry_response.) * @@ -6035,7 +6130,7 @@ static void check_need_for_response (ax25_dlsm_t *S, ax25_frame_type_t frame_typ * * Outputs: S->srt New smoothed roundtrip time. * - * S->t1v How long to wait for an acknowlegement before resending. + * S->t1v How long to wait for an acknowledgement before resending. * Value used when starting timer T1, in seconds. * Here it is dynamically adjusted. * @@ -6134,7 +6229,7 @@ static void select_t1_value (ax25_dlsm_t *S) // This goes up exponentially if implemented as documented! // For example, if we were trying to connect to a station which is not there, we - // would retry after 3, the 8, 16, 32, ... and not time out for over an hour. + // would retry after 3, then 8, 16, 32, ... and not time out for over an hour. // That's ridiculous. Let's try increasing it by a quarter second each time. // We now give up after about a minute. @@ -6151,12 +6246,30 @@ static void select_t1_value (ax25_dlsm_t *S) } +// See https://groups.io/g/direwolf/topic/100782658#8542 +// Perhaps the demands of file transfer lead to this problem. + +// "Temporary" hack. +// Automatic fine tuning of t1v generally works well, but on very rare occasions, it gets wildly out of control. +// Until I have more time to properly diagnose this, add some guardrails so it does not go flying off a cliff. + +// The initial value of t1v is frack + frack * 2 (number of digipeateers in path) +// If anything, it should automatically be adjusted down. +// Let's say, something smells fishy if it exceeds twice that initial value. + +// TODO: Add some instrumentation to record where this was called from and all the values in the printf below. + +#if 1 + if (S->t1v < 0.25 || S->t1v > 2 * (g_misc_config_p->frack * (2 * (S->num_addr - 2) + 1)) ) { + INIT_T1V_SRT; + } +#else if (S->t1v < 0.99 || S->t1v > 30) { text_color_set(DW_COLOR_ERROR); dw_printf ("INTERNAL ERROR? Stream %d: select_t1_value, rc = %d, t1 remaining = %.3f, old srt = %.3f, new srt = %.3f, Extreme new t1v = %.3f\n", S->stream_id, S->rc, S->t1_remaining_when_last_stopped, old_srt, S->srt, S->t1v); } - +#endif } /* end select_t1_value */ diff --git a/src/ax25_pad.c b/src/ax25_pad.c index d8af765d..0f075808 100644 --- a/src/ax25_pad.c +++ b/src/ax25_pad.c @@ -49,7 +49,7 @@ * * * APRS uses only UI frames. - * Each starts with 2-10 addressses (14-70 octets): + * Each starts with 2-10 addresses (14-70 octets): * * * Destination Address (note: opposite order in printed format) * @@ -350,8 +350,9 @@ void ax25_delete (packet_t this_p) * strict - True to enforce rules for packets sent over the air. * False to be more lenient for packets from IGate server. * - * Messages from an IGate server can have longer - * addresses after qAC. Up to 9 observed so far. + * Packets from an IGate server can have longer + * addresses after qAC. Up to 9 observed so far. + * 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. @@ -372,7 +373,7 @@ packet_t ax25_from_text (char *monitor, int strict) /* * Tearing it apart is destructive so make our own copy first. */ - char stuff[512]; + char stuff[AX25_MAX_PACKET_LEN+1]; char *pinfo; int ssid_temp, heard_temp; @@ -511,6 +512,13 @@ packet_t ax25_from_text (char *monitor, int strict) // printf ("DEBUG: get digi loop, num addr = %d, address = '%s'\n", k, pa);// FIXME + // Hack for q construct, from APRS-IS, so it does not cause panic later. + + if ( ! strict && pa[0] == 'q' && pa[1] == 'A') { + pa[0] = 'Q'; + pa[2] = toupper(pa[2]); + } + if ( ! ax25_parse_addr (k, pa, strict, atemp, &ssid_temp, &heard_temp)) { text_color_set(DW_COLOR_ERROR); dw_printf ("Failed to create packet from text. Bad digipeater address\n"); @@ -733,6 +741,7 @@ packet_t ax25_dup (packet_t copy_from) * alphanumeric characters for the SSID. * We also get messages like this from a server. * KB1POR>APU25N,TCPIP*,qAC,T2NUENGLD:... + * K1BOS-B>APOSB,TCPIP,WR2X-2*:... * * 2 (extra true) will complain if * is found at end. * @@ -940,7 +949,7 @@ int ax25_check_addresses (packet_t pp) * * Name: ax25_unwrap_third_party * - * Purpose: Unwrap a third party messge from the header. + * Purpose: Unwrap a third party message from the header. * * Inputs: copy_from - Existing packet object. * @@ -1717,6 +1726,19 @@ int ax25_get_info (packet_t this_p, unsigned char **paddr) } /* end ax25_get_info */ +void ax25_set_info (packet_t this_p, unsigned char *new_info_ptr, int new_info_len) +{ + unsigned char *old_info_ptr; + int old_info_len = ax25_get_info (this_p, &old_info_ptr); + this_p->frame_len -= old_info_len; + + if (new_info_len < 0) new_info_len = 0; + if (new_info_len > AX25_MAX_INFO_LEN) new_info_len = AX25_MAX_INFO_LEN; + memcpy (old_info_ptr, new_info_ptr, new_info_len); + this_p->frame_len += new_info_len; +} + + /*------------------------------------------------------------------------------ * * Name: ax25_cut_at_crlf @@ -1844,7 +1866,7 @@ packet_t ax25_get_nextp (packet_t this_p) * * Inputs: this_p - Current packet object. * - * release_time - Time as returned by dtime_now(). + * release_time - Time as returned by dtime_monotonic(). * *------------------------------------------------------------------------------*/ @@ -1892,6 +1914,25 @@ void ax25_set_modulo (packet_t this_p, int modulo) } +/*------------------------------------------------------------------------------ + * + * Name: ax25_get_modulo + * + * Purpose: Get modulo value for I and S frame sequence numbers. + * + * Returns: 8 or 128 if known. + * 0 if unknown. + * + *------------------------------------------------------------------------------*/ + +int ax25_get_modulo (packet_t this_p) +{ + assert (this_p->magic1 == MAGIC); + assert (this_p->magic2 == MAGIC); + + return (this_p->modulo); +} + @@ -1944,6 +1985,7 @@ void ax25_format_addrs (packet_t this_p, char *result) } ax25_get_addr_with_ssid (this_p, AX25_SOURCE, stemp); + // FIXME: For ALL strcat: Pass in sizeof result and use strlcat. strcat (result, stemp); strcat (result, ">"); @@ -2598,6 +2640,15 @@ int ax25_get_frame_len (packet_t this_p) } /* end ax25_get_frame_len */ +unsigned char *ax25_get_frame_data_ptr (packet_t this_p) +{ + assert (this_p->magic1 == MAGIC); + assert (this_p->magic2 == MAGIC); + + return (this_p->frame_data); + +} /* end ax25_get_frame_data_ptr */ + /*------------------------------------------------------------------------------ * @@ -2710,6 +2761,7 @@ unsigned short ax25_m_m_crc (packet_t pp) unsigned char fbuf[AX25_MAX_PACKET_LEN]; int flen; + // TODO: I think this can be more efficient by getting the packet content pointer instead of copying. flen = ax25_pack (pp, fbuf); crc = 0xffff; @@ -2762,7 +2814,8 @@ unsigned short ax25_m_m_crc (packet_t pp) * *------------------------------------------------------------------*/ -#define MAXSAFE 500 +//#define MAXSAFE 500 +#define MAXSAFE AX25_MAX_INFO_LEN void ax25_safe_print (char *pstr, int len, int ascii_only) { @@ -2870,7 +2923,9 @@ int ax25_alevel_to_text (alevel_t alevel, char text[AX25_ALEVEL_TO_TEXT_SIZE]) snprintf (text, AX25_ALEVEL_TO_TEXT_SIZE, "%d(%+d/%+d)", alevel.rec, alevel.mark, alevel.space); } - else if (alevel.mark == -1 && alevel.space == -1) { /* PSK - single number. */ + else if ((alevel.mark == -1 && alevel.space == -1) || /* PSK */ + (alevel.mark == -99 && alevel.space == -99)) { /* v. 1.7 "B" FM demodulator. */ + // ?? Where does -99 come from? snprintf (text, AX25_ALEVEL_TO_TEXT_SIZE, "%d", alevel.rec); } diff --git a/src/ax25_pad.h b/src/ax25_pad.h index 22568f74..cdb84c65 100644 --- a/src/ax25_pad.h +++ b/src/ax25_pad.h @@ -11,7 +11,7 @@ #define AX25_MAX_REPEATERS 8 -#define AX25_MIN_ADDRS 2 /* Destinatin & Source. */ +#define AX25_MIN_ADDRS 2 /* Destination & Source. */ #define AX25_MAX_ADDRS 10 /* Destination, Source, 8 digipeaters. */ #define AX25_DESTINATION 0 /* Address positions in frame. */ @@ -98,7 +98,7 @@ struct packet_s { * * Bits: H R R SSID 0 * - * H for digipeaters set to 0 intially. + * H for digipeaters set to 0 initially. * Changed to 1 when position has been used. * * for source & destination it is called @@ -397,6 +397,7 @@ extern int ax25_get_first_not_repeated(packet_t pp); extern int ax25_get_rr (packet_t this_p, int n); extern int ax25_get_info (packet_t pp, unsigned char **paddr); +extern void ax25_set_info (packet_t pp, unsigned char *info_ptr, int info_len); extern int ax25_cut_at_crlf (packet_t this_p); extern void ax25_set_nextp (packet_t this_p, packet_t next_p); @@ -409,6 +410,7 @@ extern void ax25_set_release_time (packet_t this_p, double release_time); extern double ax25_get_release_time (packet_t this_p); extern void ax25_set_modulo (packet_t this_p, int modulo); +extern int ax25_get_modulo (packet_t this_p); extern void ax25_format_addrs (packet_t pp, char *); extern void ax25_format_via_path (packet_t this_p, char *result, size_t result_size); @@ -428,6 +430,7 @@ extern int ax25_get_c2 (packet_t this_p); extern int ax25_get_pid (packet_t this_p); extern int ax25_get_frame_len (packet_t this_p); +extern unsigned char *ax25_get_frame_data_ptr (packet_t this_p); extern unsigned short ax25_dedupe_crc (packet_t pp); diff --git a/src/ax25_pad2.c b/src/ax25_pad2.c index efba8879..347df4b1 100644 --- a/src/ax25_pad2.c +++ b/src/ax25_pad2.c @@ -102,7 +102,9 @@ * * RR note: It seems that some implementations put a hint * in the "RR" reserved bits. - * http://www.tapr.org/pipermail/ax25-layer2/2005-October/000297.html + * http://www.tapr.org/pipermail/ax25-layer2/2005-October/000297.html (now broken) + * https://elixir.bootlin.com/linux/latest/source/net/ax25/ax25_addr.c#L237 + * * The RR bits can also be used for "DAMA" which is * some sort of channel access coordination scheme. * http://internet.freepage.de/cgi-bin/feets/freepage_ext/41030x030A/rewrite/hennig/afu/afudoc/afudama.html @@ -406,7 +408,7 @@ packet_t ax25_s_frame (char addrs[AX25_MAX_ADDRS][AX25_MAX_ADDR_LEN], int num_ad } // Erratum: The AX.25 spec is not clear about whether SREJ should be command, response, or both. - // The underlying X.25 spec clearly says it is reponse only. Let's go with that. + // The underlying X.25 spec clearly says it is response only. Let's go with that. if (ftype == frame_type_S_SREJ && cr != cr_res) { text_color_set(DW_COLOR_ERROR); diff --git a/src/beacon.c b/src/beacon.c index 3fea7142..69a72701 100644 --- a/src/beacon.c +++ b/src/beacon.c @@ -162,9 +162,10 @@ 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 (g_modem_config_p->achan[chan].medium == MEDIUM_RADIO || - g_modem_config_p->achan[chan].medium == MEDIUM_NETTNC) { + 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 && @@ -614,6 +615,22 @@ static void * beacon_thread (void *arg) /* i.e. Don't take relative to now in case there was some delay. */ bp->next += bp->every; + + // https://github.com/wb2osz/direwolf/pull/301 + // https://github.com/wb2osz/direwolf/pull/301 + // This happens with a portable system with no Internet connection. + // On reboot, the time is in the past. + // After time gets set from GPS, all beacons from that interval are sent. + // FIXME: This will surely break time slotted scheduling. + // TODO: The correct fix will be using monotonic, rather than clock, time. + + /* craigerl: if next beacon is scheduled in the past, then set next beacon relative to now (happens when NTP pushes clock AHEAD) */ + /* fixme: if NTP sets clock BACK an hour, this thread will sleep for that hour */ + if ( bp->next < now ) { + bp->next = now + bp->every; + text_color_set(DW_COLOR_INFO); + dw_printf("\nSystem clock appears to have jumped forward. Beacon schedule updated.\n\n"); + } } } /* if time to send it */ @@ -790,11 +807,17 @@ static void beacon_send (int j, dwgps_info_t *gpsinfo) assert (bp->sendto_chan >= 0); - strlcpy (mycall, g_modem_config_p->achan[bp->sendto_chan].mycall, sizeof(mycall)); + 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)); + } + else { + strlcpy (mycall, g_modem_config_p->achan[bp->sendto_chan].mycall, sizeof(mycall)); + } if (strlen(mycall) == 0 || strcmp(mycall, "NOCALL") == 0) { text_color_set(DW_COLOR_ERROR); - dw_printf ("MYCALL not set for beacon in config file line %d.\n", bp->lineno); + dw_printf ("MYCALL not set for beacon to chan %d in config file line %d.\n", bp->sendto_chan, bp->lineno); return; } @@ -804,7 +827,12 @@ static void beacon_send (int j, dwgps_info_t *gpsinfo) * src > dest [ , via ] */ - strlcpy (beacon_text, mycall, sizeof(beacon_text)); + if (bp->source != NULL) { + strlcpy (beacon_text, bp->source, sizeof(beacon_text)); + } + else { + strlcpy (beacon_text, mycall, sizeof(beacon_text)); + } strlcat (beacon_text, ">", sizeof(beacon_text)); if (bp->dest != NULL) { @@ -1026,7 +1054,7 @@ static void beacon_send (int j, dwgps_info_t *gpsinfo) text_color_set(DW_COLOR_XMIT); dw_printf ("[ig] %s\n", beacon_text); - igate_send_rec_packet (0, pp); + igate_send_rec_packet (-1, pp); // Channel -1 to avoid RF>IS filtering. ax25_delete (pp); break; diff --git a/src/cdigipeater.c b/src/cdigipeater.c index edfa2ddc..06128b20 100644 --- a/src/cdigipeater.c +++ b/src/cdigipeater.c @@ -34,7 +34,7 @@ * are significantly different and I thought it would be * too confusing to munge them together. * - * References: The Ax.25 protcol barely mentions digipeaters and + * References: The Ax.25 protocol barely mentions digipeaters and * and doesn't describe how they should work. * *------------------------------------------------------------------*/ @@ -132,7 +132,7 @@ 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->achan[from_chan].medium != MEDIUM_RADIO) { + if ( from_chan < 0 || from_chan >= MAX_CHANS || save_audio_config_p->chan_medium[from_chan] != MEDIUM_RADIO) { text_color_set(DW_COLOR_ERROR); dw_printf ("cdigipeater: Did not expect to receive on invalid channel %d.\n", from_chan); return; @@ -255,7 +255,7 @@ static packet_t cdigipeat_match (int from_chan, packet_t pp, char *mycall_rec, c * Originally this was the only one. * Should we change it to AFILTER to make it clearer? * CFILTER - Similar for connected moded digipeater. - * IGFILTER - APRS-IS (IGate) server side - completely diffeent. + * IGFILTER - APRS-IS (IGate) server side - completely different. * Confusing with similar name but much different idea. * Maybe this should be renamed to SUBSCRIBE or something like that. * diff --git a/src/cm108.c b/src/cm108.c index ad3b0b8f..ff3ff792 100644 --- a/src/cm108.c +++ b/src/cm108.c @@ -1,7 +1,7 @@ // // This file is part of Dire Wolf, an amateur radio packet TNC. // -// Copyright (C) 2017,2019 John Langner, WB2OSZ +// Copyright (C) 2017,2019,2021 John Langner, WB2OSZ // // Parts of this were adapted from "hamlib" which contains the notice: // @@ -30,9 +30,11 @@ * * Description: * - * There is an incresing demand for using the GPIO pins of USB audio devices for PTT. + * There is an increasing demand for using the GPIO pins of USB audio devices for PTT. * We have a few commercial products: * + * DINAH https://hamprojects.info/dinah/ + * PAUL https://hamprojects.info/paul/ * DMK URI http://www.dmkeng.com/URI_Order_Page.htm * RB-USB RIM http://www.repeater-builder.com/products/usb-rim-lite.html * RA-35 http://www.masterscommunications.com/products/radio-adapter/ra35.html @@ -44,14 +46,15 @@ * http://www.repeater-builder.com/projects/fob/USB-Fob-Construction.pdf * https://irongarment.wordpress.com/2011/03/29/cm108-compatible-chips-with-gpio/ * - * Usually GPIO 3 is used because it is easier to tack solder a wire to a pin on the end. + * Homebrew plans all use GPIO 3 because it is easier to tack solder a wire to a pin on the end. + * All of the products, that I have seen, also use the same pin so this is the default. * * Soundmodem and hamlib paved the way but didn't get too far. * Dire Wolf 1.3 added HAMLIB support (Linux only) which theoretically allows this in a * painful roundabout way. This is documented in the User Guide, section called, * "Hamlib PTT Example 2: Use GPIO of USB audio adapter. (e.g. DMK URI)" * - * It's rather involved and the explantion doesn't cover the case of multiple + * It's rather involved and the explanation doesn't cover the case of multiple * USB-Audio adapters. It is not as straightforward as you might expect. Here we have * an example of 3 C-Media USB adapters, a SignaLink USB, a keyboard, and a mouse. * @@ -87,23 +90,34 @@ * Dire Wolf version 1.5 makes this much more flexible and easier to use by supporting multiple * sound devices and automatically determining the corresponding HID for the PTT signal. * + * In version 1.7, we add a half-backed solution for Windows. It's fine for situations + * with a single USB Audio Adapter, but does not automatically handle the multiple device case. + * Manual configuration needs to be used in this case. + * + * Here is something new and interesting. The All in One cable (AIOC). + * https://github.com/skuep/AIOC/tree/master + * + * A microcontroller is used to emulate a CM108-compatible soundcard + * and a serial port. It fits right on the side of a Bao Feng or similar. + * *---------------------------------------------------------------*/ +#include "direwolf.h" + #ifndef USE_CM108 #ifdef CM108_MAIN -#include "direwolf.h" + #include "textcolor.h" int main (void) { text_color_init (0); // Turn off text color. #if defined(__OpenBSD__) || defined(__FreeBSD__) - dw_printf ("CM108 PTT support is not available for BSD.\n"); + dw_printf ("CM108 PTT support is not available for this operating system.\n"); #else - dw_printf ("CM108 PTT support was disabled in Makefile.linux.\n"); - dw_printf ("It was excluded because /usr/include/libudev.h was missing.\n"); + dw_printf ("CM108 PTT support was excluded because /usr/include/libudev.h was missing.\n"); dw_printf ("Install it with \"sudo apt-get install libudev-dev\" or\n"); dw_printf ("\"sudo yum install libudev-devel\" then rebuild.\n"); #endif @@ -112,11 +126,8 @@ int main (void) #endif -#else // USE_CM108 is defined. - -#include "direwolf.h" +#else // USE_CM108 is defined -#include #include #include #include @@ -124,12 +135,18 @@ int main (void) #include #include +#if __WIN32__ +#include +#include "hidapi.h" +#else +#include #include #include #include // ioctl, _IOR #include #include #include // for HIDIOCGRAWINFO +#endif #include "textcolor.h" #include "cm108.h" @@ -168,6 +185,11 @@ static int cm108_write (char *name, int iomask, int iodata); #define SSS_PID2 0x1607 #define SSS_PID3 0x160b +// https://github.com/skuep/AIOC/blob/master/stm32/aioc-fw/Src/usb_descriptors.h + +#define AIOC_VID 0x1209 +#define AIOC_PID 0x7388 + // Device VID PID Number of GPIO // ------ --- --- -------------- @@ -207,7 +229,9 @@ static int cm108_write (char *name, int iomask, int iodata); || p == CMEDIA_PID_CM119A \ || p == CMEDIA_PID_CM119B )) \ || \ - (v == SSS_VID && (p == SSS_PID1 || p == SSS_PID2 || p == SSS_PID3)) ) + (v == SSS_VID && (p == SSS_PID1 || p == SSS_PID2 || p == SSS_PID3)) \ + || \ + (v == AIOC_VID && p == AIOC_PID) ) // Look out for null source pointer, and avoid buffer overflow on destination. @@ -216,6 +240,8 @@ static int cm108_write (char *name, int iomask, int iodata); // Used to process regular expression matching results. +#ifndef __WIN32__ + static void substr_se (char *dest, const char *src, int start, int endp1) { int len = endp1 - start; @@ -229,6 +255,13 @@ static void substr_se (char *dest, const char *src, int start, int endp1) } /* end substr_se */ +#endif + +// 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. + +#define MAXX_HIDRAW_NAME_LEN 128 /* * Result of taking inventory of USB soundcards and USB HIDs. @@ -237,15 +270,18 @@ static void substr_se (char *dest, const char *src, int start, int endp1) struct thing_s { int vid; // vendor id, displayed as four hexadecimal digits. int pid; // product id, displayed as four hexadecimal digits. - char card_number[8]; // Number. e.g. 2 for plughw:2,0 - char card_name[32]; // Name, assigned by system (e.g. Device_1) or by udev rule. + char card_number[8]; // "Card" Number. e.g. 2 for plughw:2,0 + char card_name[32]; // Audio Card Name, assigned by system (e.g. Device_1) or by udev rule. char product[32]; // product name (e.g. manufacturer, model) char devnode_sound[22]; // e.g. /dev/snd/pcmC0D0p char plughw[72]; // Above in more familiar format e.g. plughw:0,0 // Oversized to silence a compiler warning. char plughw2[72]; // With name rather than number. char devpath[128]; // Kernel dev path. Does not include /sys mount point. - char devnode_hidraw[17]; // e.g. /dev/hidraw3 + char devnode_hidraw[MAXX_HIDRAW_NAME_LEN]; + // e.g. /dev/hidraw3 - for Linux - was length 17 + // The Windows path for a HID looks like this, lengths up to 95 seen. + // \\?\hid#vid_0d8c&pid_000c&mi_03#8&164d11c9&0&0000#{4d1e55b2-f16f-11cf-88cb-001111000030} char devnode_usb[25]; // e.g. /dev/bus/usb/001/012 // This is what we use to match up audio and HID. }; @@ -259,6 +295,13 @@ int cm108_inventory (struct thing_s *things, int max_things); * * Purpose: Useful utility to list USB audio and HID devices. * + * Optional command line arguments: + * + * HID path + * GPIO number (default 3) + * + * When specified the pin will be set high and low until interrupted. + * *------------------------------------------------------------------*/ //#define EXTRA 1 @@ -267,18 +310,111 @@ int cm108_inventory (struct thing_s *things, int max_things); #ifdef CM108_MAIN -int main (void) +static void usage(void) +{ + text_color_set(DW_COLOR_ERROR); + dw_printf ("\n"); + 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__ + dw_printf ("Human Interface Devices (HID) and indicate which ones can be\n"); + dw_printf ("used for GPIO PTT.\n"); +#else + dw_printf ("Audio devices and Human Interface Devices (HID) and indicate\n"); + dw_printf ("which ones can be used for GPIO PTT.\n"); +#endif + dw_printf ("\n"); + dw_printf ("Specify the HID device path to test the PTT function.\n"); + dw_printf ("Its state should change once per second.\n"); +#if __WIN32__ + dw_printf ("You might need to quote the path depending on the command processor.\n"); +#endif + dw_printf ("GPIO 3 is the default. A different number can be optionally specified.\n"); + exit (EXIT_FAILURE); +} + +int main (int argc, char **argv) { struct thing_s things[MAXX_THINGS]; int num_things; int i; text_color_init (0); // Turn off text color. + text_color_set(DW_COLOR_INFO); + + if (argc >=2) { + char path[128]; + strlcpy(path, argv[1], sizeof(path)); + int gpio = 3; + if (argc >= 3) { + gpio = atoi(argv[2]); + } + if (gpio < 1 || gpio > 8) { + dw_printf ("GPIO number must be in range of 1 - 8.\n"); + usage(); + exit (EXIT_FAILURE); + } + int state = 0; + while (1) { + dw_printf ("%d", state); + fflush (stdout); + int err = cm108_set_gpio_pin (path, gpio, state); + if (err != 0) { + dw_printf ("\nWRITE ERROR for USB Audio Adapter GPIO!\n"); + usage(); + exit (EXIT_FAILURE); + } + SLEEP_SEC(1); + state = ! state; + } + } + // Take inventory of USB Audio adapters and other HID devices. num_things = cm108_inventory (things, MAXX_THINGS); +#if __WIN32__ + +///////////////////////////////////////////////////// +// Windows - Remove the sound related columns for now. +///////////////////////////////////////////////////// + + dw_printf (" VID PID %-*s %-*s" + "\n", (int)sizeof(things[0].product), "Product", + 17, "HID [ptt]" + ); + + dw_printf (" --- --- %-*s %-*s" + + "\n", (int)sizeof(things[0].product), "-------", + 17, "---------" + ); + for (i = 0; i < num_things; i++) { + + dw_printf ("%2s %04x %04x %-*s %s" + + "\n", + GOOD_DEVICE(things[i].vid,things[i].pid) ? "**" : " ", + things[i].vid, things[i].pid, + (int)sizeof(things[i].product), things[i].product, + things[i].devnode_hidraw + ); + } + dw_printf ("\n"); + dw_printf ("** = Can use Audio Adapter GPIO for PTT.\n"); + dw_printf ("\n"); + + // T.B.D. - additional text ??? + +#else + +///////////////////////////////////////////// +// Linux +///////////////////////////////////////////// + + dw_printf (" VID PID %-*s %-*s %-*s %-*s %-*s" #if EXTRA " %-*s" @@ -287,7 +423,7 @@ int main (void) (int)sizeof(things[0].devnode_sound), "Sound", (int)sizeof(things[0].plughw)/5, "ADEVICE", (int)sizeof(things[0].plughw2)/4, "ADEVICE", - (int)sizeof(things[0].devnode_hidraw), "HID [ptt]" + 17, "HID [ptt]" #if EXTRA , (int)sizeof(things[0].devnode_usb), "USB" #endif @@ -301,14 +437,14 @@ int main (void) (int)sizeof(things[0].devnode_sound), "-----", (int)sizeof(things[0].plughw)/5, "-------", (int)sizeof(things[0].plughw2)/4, "-------", - (int)sizeof(things[0].devnode_hidraw), "---------" + 17, "---------" #if EXTRA , (int)sizeof(things[0].devnode_usb), "---" #endif ); for (i = 0; i < num_things; i++) { - dw_printf ("%2s %04x %04x %-*s %-*s %-*s %-*s %-*s" + dw_printf ("%2s %04x %04x %-*s %-*s %-*s %-*s %s" #if EXTRA " %-*s" #endif @@ -319,22 +455,22 @@ int main (void) (int)sizeof(things[i].devnode_sound), things[i].devnode_sound, (int)sizeof(things[0].plughw)/5, things[i].plughw, (int)sizeof(things[0].plughw2)/4, things[i].plughw2, - (int)sizeof(things[i].devnode_hidraw), things[i].devnode_hidraw + things[i].devnode_hidraw #if EXTRA , (int)sizeof(things[i].devnode_usb), things[i].devnode_usb #endif ); //dw_printf (" %-*s\n", (int)sizeof(things[i].devpath), things[i].devpath); } + dw_printf ("\n"); + dw_printf ("** = Can use Audio Adapter GPIO for PTT.\n"); + dw_printf ("\n"); - static const char *suggested_names[] = {"Fred", "Wilma", "Pebbles", "Dino", "Barney", "Betty", "Bamm_Bamm" }; + static const char *suggested_names[] = {"Fred", "Wilma", "Pebbles", "Dino", "Barney", "Betty", "Bamm_Bamm", "Chip", "Roxy" }; int iname = 0; // From example in https://alsa.opensrc.org/Udev - dw_printf ("\n"); - dw_printf ("** = Can use Audio Adapter GPIO for PTT.\n"); - dw_printf ("\n"); dw_printf ("Notice that each USB Audio adapter is assigned a number and a name. These are not predictable so you could\n"); dw_printf ("end up using the wrong adapter after adding or removing other USB devices or after rebooting. You can assign a\n"); dw_printf ("name to each USB adapter so you can refer to the same one each time. This can be based on any characteristics\n"); @@ -372,7 +508,7 @@ int main (void) } dw_printf ("LABEL=\"my_usb_audio_end\"\n"); dw_printf ("\n"); - +#endif return (0); } @@ -400,6 +536,47 @@ int main (void) 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__ + + struct hid_device_info *devs, *cur_dev; + + if (hid_init()) { + text_color_set(DW_COLOR_ERROR); + dw_printf("cm108_inventory: hid_init() failed.\n"); + return (-1); + } + + devs = hid_enumerate(0x0, 0x0); + cur_dev = devs; + while (cur_dev) { +#if 0 + printf("Device Found\n type: %04hx %04hx\n path: %s\n serial_number: %ls", cur_dev->vendor_id, cur_dev->product_id, cur_dev->path, cur_dev->serial_number); + printf("\n"); + printf(" Manufacturer: %ls\n", cur_dev->manufacturer_string); + printf(" Product: %ls\n", cur_dev->product_string); + printf(" Release: %hx\n", cur_dev->release_number); + printf(" Interface: %d\n", cur_dev->interface_number); + printf(" Usage (page): 0x%hx (0x%hx)\n", cur_dev->usage, cur_dev->usage_page); + printf("\n"); +#endif + if (num_things < max_things && cur_dev->vendor_id != 0x051d) { // FIXME - remove exception + things[num_things].vid = cur_dev->vendor_id; + things[num_things].pid = cur_dev->product_id; + wcstombs (things[num_things].product, cur_dev->product_string, sizeof(things[num_things].product)); + things[num_things].product[sizeof(things[num_things].product) - 1] = '\0'; + strlcpy (things[num_things].devnode_hidraw, cur_dev->path, sizeof(things[num_things].devnode_hidraw)); + + num_things++; + } + cur_dev = cur_dev->next; + } + hid_free_enumeration(devs); + +#else // Linux, with udev + struct udev *udev; struct udev_enumerate *enumerate; struct udev_list_entry *devices, *dev_list_entry; @@ -410,8 +587,6 @@ int cm108_inventory (struct thing_s *things, int max_things) char const *pattrs_number = NULL; char card_devpath[128] = ""; - int num_things = 0; - memset (things, 0, sizeof(struct thing_s) * max_things); /* * First get a list of the USB audio devices. @@ -561,6 +736,8 @@ int cm108_inventory (struct thing_s *things, int max_things) } } +#endif // end Linux + return (num_things); } /* end cm108_inventory */ @@ -584,7 +761,7 @@ int cm108_inventory (struct thing_s *things, int max_things) * ptt_device_size - Size of result area to avoid buffer overflow. * * Outputs: ptt_device - Device name, something like /dev/hidraw2. - * Will be emptry string if no match found. + * Will be empty string if no match found. * * Returns: none * @@ -594,13 +771,48 @@ void cm108_find_ptt (char *output_audio_device, char *ptt_device, int ptt_devic { struct thing_s things[MAXX_THINGS]; int num_things; - int i; //dw_printf ("DEBUG: cm108_find_ptt('%s')\n", output_audio_device); strlcpy (ptt_device, "", ptt_device_size); + + // Possible improvement: Skip if inventory already taken. num_things = cm108_inventory (things, MAXX_THINGS); +#if __WIN32__ + // 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. + // This is fine for a single USB Audio Adapter, good enough for most people. + // Those with multiple devices will need to manually configure PTT device path. + + // Count how many good devices we have. + + int good_devices = 0; + + for (int i = 0; i < num_things; i++) { + if (GOOD_DEVICE(things[i].vid,things[i].pid) ) { + good_devices++; + //dw_printf ("DEBUG: success! returning '%s'\n", things[i].devnode_hidraw); + strlcpy (ptt_device, things[i].devnode_hidraw, ptt_device_size); + } + } + + if (good_devices == 0) return; // None found - caller will print a message. + + if (good_devices == 1) return; // Success - Only one candidate device. + + text_color_set(DW_COLOR_ERROR); + dw_printf ("There are multiple USB Audio Devices with GPIO capability.\n"); + dw_printf ("Explicitly specify one of them for more predictable results:\n"); + for (int i = 0; i < num_things; i++) { + if (GOOD_DEVICE(things[i].vid,things[i].pid) ) { + dw_printf (" \"%s\"\n", things[i].devnode_hidraw); + } + } + dw_printf ("Run the \"cm108\" utility for more details.\n"); + text_color_set(DW_COLOR_INFO); +#else regex_t sound_re; char emsg[100]; int e = regcomp (&sound_re, ".+:(CARD=)?([A-Za-z0-9_]+)(,.*)?", REG_EXTENDED); @@ -625,7 +837,7 @@ void cm108_find_ptt (char *output_audio_device, char *ptt_device, int ptt_devic return; } - for (i = 0; i < num_things; i++) { + for (int i = 0; i < num_things; i++) { //dw_printf ("DEBUG: i=%d, card_name='%s', card_number='%s'\n", i, things[i].card_name, things[i].card_number); if (strcmp(num_or_name,things[i].card_name) == 0 || strcmp(num_or_name,things[i].card_number) == 0) { //dw_printf ("DEBUG: success! returning '%s'\n", things[i].devnode_hidraw); @@ -638,6 +850,7 @@ void cm108_find_ptt (char *output_audio_device, char *ptt_device, int ptt_devic return; } } +#endif } /* end cm108_find_ptt */ @@ -649,7 +862,8 @@ void cm108_find_ptt (char *output_audio_device, char *ptt_device, int ptt_devic * * Purpose: Set one GPIO pin of the CM108 or similar. * - * Inputs: name - Name of device such as /dev/hidraw2. + * Inputs: name - Name of device such as /dev/hidraw2 or + * \\?\hid#vid_0d8c&pid_0008&mi_03#8&39d3555&0&0000#{4d1e55b2-f16f-11cf-88cb-001111000030} * * num - GPIO number, range 1 thru 8. * @@ -659,9 +873,9 @@ void cm108_find_ptt (char *output_audio_device, char *ptt_device, int ptt_devic * * Errors: A descriptive error message will be printed for any problem. * - * Future: For our initial implementation we are making the simplifying + * Shortcut: For our initial implementation we are making the simplifying * restriction of using only one GPIO pin per device and limit - * configuratin to PTT only. + * configuration to PTT only. * Longer term, we might want to have DCD, and maybe other * controls thru the same chip. * In this case, we would need to retain bit masks for each @@ -669,44 +883,6 @@ void cm108_find_ptt (char *output_audio_device, char *ptt_device, int ptt_devic * *------------------------------------------------------------------*/ -#if TESTCM - -// Switch pin between input, output-low, and output-high. - -// gcc -DTESTCM=1 -DUSE_CM108 cm108.c textcolor.c misc.a -ludev - -int main (int argc, char *argv[]) -{ -#define MODE_IN 0 -#define MODE_OUT 0x04 // GPIO 3 = bit 2 -#define OUT_LOW 0 -#define OUT_HIGH 0x04 - - if (argc != 2) { - text_color_set(DW_COLOR_ERROR); - dw_printf ("Specify HID path on command line.\n"); - exit (1); - } - - while (1) { - text_color_set(DW_COLOR_INFO); - dw_printf ("Input-L\n"); - cm108_write (argv[1], MODE_IN, OUT_LOW); - sleep(5); - dw_printf ("Input-H\n"); - cm108_write (argv[1], MODE_IN, OUT_HIGH); - sleep(5); - dw_printf ("Out-LOW\n"); - cm108_write (argv[1], MODE_OUT, OUT_LOW); - sleep(5); - dw_printf ("out-HIGH\n"); - cm108_write (argv[1], MODE_OUT, OUT_HIGH); - sleep(5); - } -} - -#endif - int cm108_set_gpio_pin (char *name, int num, int state) { @@ -727,7 +903,6 @@ int cm108_set_gpio_pin (char *name, int num, int state) iomask = 1 << (num - 1); // 0=input, 1=output iodata = state << (num - 1); // 0=low, 1=high - return (cm108_write (name, iomask, iodata)); } /* end cm108_set_gpio_pin */ @@ -758,6 +933,36 @@ int cm108_set_gpio_pin (char *name, int num, int state) static int cm108_write (char *name, int iomask, int iodata) { + +#if __WIN32__ + + //text_color_set(DW_COLOR_DEBUG); + //dw_printf ("TEMP DEBUG cm108_write: %s %d %d\n", name, iomask, iodata); + + hid_device *handle = hid_open_path(name); + if (handle == NULL) { + text_color_set(DW_COLOR_ERROR); + dw_printf ("Could not open %s for write\n", name); + return (-1); + } + + unsigned char io[5]; + io[0] = 0; + io[1] = 0; + io[2] = iodata; + io[3] = iomask; + io[4] = 0; + + int res = hid_write(handle, io, sizeof(io)); + if (res < 0) { + text_color_set(DW_COLOR_ERROR); + dw_printf ("Write failed to %s\n", name); + return (-1); + } + + hid_close(handle); + +#else int fd; struct hidraw_devinfo info; char io[5]; @@ -862,6 +1067,8 @@ static int cm108_write (char *name, int iomask, int iodata) } close (fd); + +#endif return (0); } /* end cm108_write */ diff --git a/src/config.c b/src/config.c index 8588a8cc..1ad6c081 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 John Langner, WB2OSZ +// Copyright (C) 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2021 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 @@ -63,7 +63,7 @@ #include "tt_text.h" #include "ax25_link.h" -#ifdef USE_CM108 // Linux only +#if USE_CM108 // Current Linux or Windows only #include "cm108.h" #endif @@ -317,7 +317,7 @@ static double parse_ll (char *str, enum parse_ll_which_e which, int line) * - Negative zone for south. * - Separate North or South. * - * I'm using the first alternatve. + * I'm using the first alternative. * GEOTRANS uses the third. * We will also recognize the second one but I'm not sure if I want to document it. * @@ -585,7 +585,7 @@ static int check_via_path (char *via_path) * *--------------------------------------------------------------------*/ -#define MAXCMDLEN 256 +#define MAXCMDLEN 1200 static char *split (char *string, int rest_of_line) @@ -709,6 +709,14 @@ static char *split (char *string, int rest_of_line) * *--------------------------------------------------------------------*/ +static void rtfm() +{ + text_color_set(DW_COLOR_ERROR); + dw_printf ("See online documentation:\n"); + 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"); +} void config_init (char *fname, struct audio_s *p_audio_config, struct digi_config_s *p_digi_config, @@ -736,6 +744,8 @@ void config_init (char *fname, struct audio_s *p_audio_config, memset (p_audio_config, 0, sizeof(struct audio_s)); + p_audio_config->igate_vchannel = -1; // none. + /* First audio device is always available with defaults. */ /* Others must be explicitly defined before use. */ @@ -755,7 +765,7 @@ void config_init (char *fname, struct audio_s *p_audio_config, for (channel=0; channelachan[channel].medium = MEDIUM_NONE; /* One or both channels will be */ + p_audio_config->chan_medium[channel] = MEDIUM_NONE; /* One or both channels will be */ /* set to radio when corresponding */ /* audio device is defined. */ p_audio_config->achan[channel].modem_type = MODEM_AFSK; @@ -770,6 +780,10 @@ void config_init (char *fname, struct audio_s *p_audio_config, p_audio_config->achan[channel].num_freq = 1; p_audio_config->achan[channel].offset = 0; + p_audio_config->achan[channel].layer2_xmit = LAYER2_AX25; + p_audio_config->achan[channel].il2p_max_fec = 1; + p_audio_config->achan[channel].il2p_invert_polarity = 0; + p_audio_config->achan[channel].fix_bits = DEFAULT_FIX_BITS; p_audio_config->achan[channel].sanity_test = SANITY_APRS; p_audio_config->achan[channel].passall = 0; @@ -804,7 +818,7 @@ void config_init (char *fname, struct audio_s *p_audio_config, /* First channel should always be valid. */ /* If there is no ADEVICE, it uses default device in mono. */ - p_audio_config->achan[0].medium = MEDIUM_RADIO; + p_audio_config->chan_medium[0] = MEDIUM_RADIO; memset (p_digi_config, 0, sizeof(struct digi_config_s)); // APRS digipeater p_digi_config->dedupe_time = DEFAULT_DEDUPE; @@ -852,10 +866,19 @@ void config_init (char *fname, struct audio_s *p_audio_config, memset (p_misc_config, 0, sizeof(struct misc_config_s)); p_misc_config->agwpe_port = DEFAULT_AGWPE_PORT; - p_misc_config->kiss_port = DEFAULT_KISS_PORT; + + for (int i=0; ikiss_port[i] = 0; // entry not used. + p_misc_config->kiss_chan[i] = -1; + } + p_misc_config->kiss_port[0] = DEFAULT_KISS_PORT; + p_misc_config->kiss_chan[0] = -1; // all channels. + p_misc_config->enable_kiss_pt = 0; /* -p option */ p_misc_config->kiss_copy = 0; + p_misc_config->dns_sd_enabled = 1; + /* Defaults from http://info.aprs.net/index.php?title=SmartBeaconing */ p_misc_config->sb_configured = 0; /* TRUE if SmartBeaconing is configured. */ @@ -955,7 +978,8 @@ void config_init (char *fname, struct audio_s *p_audio_config, text_color_set(DW_COLOR_ERROR); dw_printf ("ERROR - Could not open config file %s\n", filepath); dw_printf ("Try using -c command line option for alternate location.\n"); - return; + rtfm(); + exit(EXIT_FAILURE); } dw_printf ("\nReading config file %s\n", filepath); @@ -1012,13 +1036,14 @@ void config_init (char *fname, struct audio_s *p_audio_config, if (t == NULL) { text_color_set(DW_COLOR_ERROR); dw_printf ("Config file: Missing name of audio device for ADEVICE command on line %d.\n", line); - continue; + rtfm(); + exit(EXIT_FAILURE); } p_audio_config->adev[adevice].defined = 1; /* First channel of device is valid. */ - p_audio_config->achan[ADEVFIRSTCHAN(adevice)].medium = MEDIUM_RADIO; + 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)); @@ -1073,7 +1098,7 @@ void config_init (char *fname, struct audio_s *p_audio_config, p_audio_config->adev[adevice].defined = 1; /* First channel of device is valid. */ - p_audio_config->achan[ADEVFIRSTCHAN(adevice)].medium = MEDIUM_RADIO; + 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)); } @@ -1100,7 +1125,7 @@ void config_init (char *fname, struct audio_s *p_audio_config, p_audio_config->adev[adevice].defined = 1; /* First channel of device is valid. */ - p_audio_config->achan[ADEVFIRSTCHAN(adevice)].medium = MEDIUM_RADIO; + p_audio_config->chan_medium[ADEVFIRSTCHAN(adevice)] = MEDIUM_RADIO; strlcpy (p_audio_config->adev[adevice].adevice_out, t, sizeof(p_audio_config->adev[adevice].adevice_out)); } @@ -1147,9 +1172,9 @@ void config_init (char *fname, struct audio_s *p_audio_config, /* Set valid channels depending on mono or stereo. */ - p_audio_config->achan[ADEVFIRSTCHAN(adevice)].medium = MEDIUM_RADIO; + p_audio_config->chan_medium[ADEVFIRSTCHAN(adevice)] = MEDIUM_RADIO; if (n == 2) { - p_audio_config->achan[ADEVFIRSTCHAN(adevice) + 1].medium = MEDIUM_RADIO; + p_audio_config->chan_medium[ADEVFIRSTCHAN(adevice) + 1] = MEDIUM_RADIO; } } else { @@ -1163,7 +1188,7 @@ void config_init (char *fname, struct audio_s *p_audio_config, */ /* - * CHANNEL - Set channel for following commands. + * CHANNEL n - Set channel for channel-specific commands. */ else if (strcasecmp(t, "CHANNEL") == 0) { @@ -1179,7 +1204,7 @@ void config_init (char *fname, struct audio_s *p_audio_config, channel = n; - if (p_audio_config->achan[n].medium != MEDIUM_RADIO) { + if (p_audio_config->chan_medium[n] != MEDIUM_RADIO) { if ( ! p_audio_config->adev[ACHAN2ADEV(n)].defined) { text_color_set(DW_COLOR_ERROR); @@ -1199,6 +1224,44 @@ void config_init (char *fname, struct audio_s *p_audio_config, } } +/* + * ICHANNEL n - Define IGate virtual channel. + * + * This allows a client application to talk to to APRS-IS + * by using a channel number outside the normal range for modems. + * In the future there might be other typs of virtual channels. + * This does not change the current channel number used by MODEM, PTT, etc. + */ + + else if (strcasecmp(t, "ICHANNEL") == 0) { + t = split(NULL,0); + if (t == NULL) { + text_color_set(DW_COLOR_ERROR); + dw_printf ("Line %d: Missing virtual channel number for ICHANNEL command.\n", line); + continue; + } + int ichan = atoi(t); + if (ichan >= MAX_CHANS && ichan < MAX_TOTAL_CHANS) { + + if (p_audio_config->chan_medium[ichan] == MEDIUM_NONE) { + + p_audio_config->chan_medium[ichan] = MEDIUM_IGATE; + + // This is redundant but saves the time of searching through all + // the channels for each packet. + p_audio_config->igate_vchannel = ichan; + } + 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); + } + } + 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); + } + } + /* * MYCALL station */ @@ -1337,7 +1400,7 @@ void config_init (char *fname, struct audio_s *p_audio_config, // Will make more precise in afsk demod init. p_audio_config->achan[channel].mark_freq = 2083; // Actually 2083.3 - logic 1. p_audio_config->achan[channel].space_freq = 1563; // Actually 1562.5 - logic 0. - // ? strlcpy (p_audio_config->achan[channel].profiles, "D", sizeof(p_audio_config->achan[channel].profiles)); + // ? strlcpy (p_audio_config->achan[channel].profiles, "A", sizeof(p_audio_config->achan[channel].profiles)); } else { p_audio_config->achan[channel].modem_type = MODEM_SCRAMBLE; @@ -1641,6 +1704,9 @@ void config_init (char *fname, struct audio_s *p_audio_config, text_color_set(DW_COLOR_INFO); dw_printf ("Line %d: Using a FIX_BITS value greater than %d is not recommended for normal operation.\n", line, DEFAULT_FIX_BITS); + dw_printf ("FIX_BITS > 1 was an interesting experiment but turned out to be a bad idea.\n"); + dw_printf ("Don't be surprised if it takes 100%% CPU, direwolf can't keep up with the audio stream,\n"); + dw_printf ("and you see messages like \"Audio input device 0 error code -32: Broken pipe\"\n"); } t = split(NULL,0); @@ -1808,7 +1874,7 @@ void config_init (char *fname, struct audio_s *p_audio_config, } strlcpy (p_audio_config->achan[channel].octrl[ot].ptt_device, t, sizeof(p_audio_config->achan[channel].octrl[ot].ptt_device)); - // Optional serial port rate for CAT controll PTT. + // Optional serial port rate for CAT control PTT. t = split(NULL,0); if (t != NULL) { @@ -1845,9 +1911,9 @@ void config_init (char *fname, struct audio_s *p_audio_config, } else if (strcasecmp(t, "CM108") == 0) { -/* CM108 - GPIO of USB sound card. case, Linux only. */ +/* CM108 - GPIO of USB sound card. case, Linux and Windows only. */ -#ifdef USE_CM108 +#if USE_CM108 if (ot != OCTYPE_PTT) { // Future project: Allow DCD and CON via the same device. @@ -1884,6 +1950,16 @@ void config_init (char *fname, struct audio_s *p_audio_config, p_audio_config->achan[channel].octrl[ot].out_gpio_num = atoi(t); p_audio_config->achan[channel].octrl[ot].ptt_invert = 0; } +#if __WIN32__ + else if (*t == '\\') { + strlcpy (p_audio_config->achan[channel].octrl[ot].ptt_device, t, sizeof(p_audio_config->achan[channel].octrl[ot].ptt_device)); + } + else { + text_color_set(DW_COLOR_ERROR); + dw_printf ("Config file line %d: Found \"%s\" when expecting GPIO number or device name like \\\\?\\hid#vid_0d8c&... .\n", line, t); + continue; + } +#else else if (*t == '/') { strlcpy (p_audio_config->achan[channel].octrl[ot].ptt_device, t, sizeof(p_audio_config->achan[channel].octrl[ot].ptt_device)); } @@ -1892,6 +1968,7 @@ void config_init (char *fname, struct audio_s *p_audio_config, dw_printf ("Config file line %d: Found \"%s\" when expecting GPIO number or device name like /dev/hidraw1.\n", line, t); continue; } +#endif } if (p_audio_config->achan[channel].octrl[ot].out_gpio_num < 1 || p_audio_config->achan[channel].octrl[ot].out_gpio_num > 8) { text_color_set(DW_COLOR_ERROR); @@ -1903,22 +1980,23 @@ void config_init (char *fname, struct audio_s *p_audio_config, text_color_set(DW_COLOR_ERROR); dw_printf ("Config file line %d: Could not determine USB Audio GPIO PTT device for audio output %s.\n", line, p_audio_config->adev[ACHAN2ADEV(channel)].adevice_out); +#if __WIN32__ + dw_printf ("You must explicitly mention a HID path.\n"); +#else dw_printf ("You must explicitly mention a device name such as /dev/hidraw1.\n"); - dw_printf ("See User Guide for details.\n"); +#endif + dw_printf ("Run \"cm108\" utility to get a list.\n"); + dw_printf ("See Interface Guide for details.\n"); continue; } p_audio_config->achan[channel].octrl[ot].ptt_method = PTT_METHOD_CM108; -#else -#if __WIN32__ - text_color_set(DW_COLOR_ERROR); - dw_printf ("Config file line %d: CM108 USB Audio GPIO PTT is not available for Windows.\n", line); #else text_color_set(DW_COLOR_ERROR); dw_printf ("Config file line %d: %s with CM108 is only available when USB Audio GPIO support is enabled.\n", line, otname); dw_printf ("You must rebuild direwolf with CM108 Audio Adapter GPIO PTT support.\n"); - dw_printf ("See User Guide for details.\n"); -#endif + dw_printf ("See Interface Guide for details.\n"); + rtfm(); exit (EXIT_FAILURE); #endif } @@ -2231,7 +2309,7 @@ void config_init (char *fname, struct audio_s *p_audio_config, * 0 = off, 1 = auto mode, others are suggestions for testing * or special cases. 16, 32, 64 is number of parity bytes to add. * Also set by "-X n" command line option. - * Current a global setting. Could be per channel someday. + * V1.7 changed from global to per-channel setting. */ else if (strcasecmp(t, "FX25TX") == 0) { @@ -2244,13 +2322,15 @@ void config_init (char *fname, struct audio_s *p_audio_config, } n = atoi(t); if (n >= 0 && n < 200) { - p_audio_config->fx25_xmit_enable = n; + p_audio_config->achan[channel].fx25_strength = n; + p_audio_config->achan[channel].layer2_xmit = LAYER2_FX25; } else { - p_audio_config->fx25_xmit_enable = 1; + p_audio_config->achan[channel].fx25_strength = 1; + p_audio_config->achan[channel].layer2_xmit = LAYER2_FX25; text_color_set(DW_COLOR_ERROR); dw_printf ("Line %d: Unreasonable value for FX.25 transmission mode. Using %d.\n", - line, p_audio_config->fx25_xmit_enable); + line, p_audio_config->achan[channel].fx25_strength); } } @@ -2283,12 +2363,57 @@ void config_init (char *fname, struct audio_s *p_audio_config, } } +/* + * IL2PTX [ + - ] [ 0 1 ] - Enable IL2P transmission. Default off. + * "+" means normal polarity. Redundant since it is the default. + * (command line -I for first channel) + * "-" means inverted polarity. Do not use for 1200 bps. + * (command line -i for first channel) + * "0" means weak FEC. Not recommended. + * "1" means stronger FEC. "Max FEC." Default if not specified. + */ + + else if (strcasecmp(t, "IL2PTX") == 0) { + + 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; + + while ((t = split(NULL,0)) != NULL) { + for (char *c = t; *c != '\0'; c++) { + switch (*c) { + case '+': + p_audio_config->achan[channel].il2p_invert_polarity = 0; + break; + case '-': + p_audio_config->achan[channel].il2p_invert_polarity = 1; + break; + case '0': + p_audio_config->achan[channel].il2p_max_fec = 0; + break; + case '1': + p_audio_config->achan[channel].il2p_max_fec = 1; + break; + default: + text_color_set(DW_COLOR_ERROR); + dw_printf ("Line %d: Invalid parameter '%c' fol IL2PTX command.\n", line, *c); + continue; + break; + } + } + } + } + + /* * ==================== APRS Digipeater parameters ==================== */ /* - * DIGIPEAT from-chan to-chan alias-pattern wide-pattern [ OFF|DROP|MARK|TRACE ] + * DIGIPEAT from-chan to-chan alias-pattern wide-pattern [ OFF|DROP|MARK|TRACE | ATGP=alias ] + * + * 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. */ else if (strcasecmp(t, "DIGIPEAT") == 0 || strcasecmp(t, "DIGIPEATER") == 0) { @@ -2319,8 +2444,8 @@ void config_init (char *fname, struct audio_s *p_audio_config, // Channels specified must be radio channels or network TNCs. - if (p_audio_config->achan[from_chan].medium != MEDIUM_RADIO && - p_audio_config->achan[from_chan].medium != MEDIUM_NETTNC) { + if (p_audio_config->chan_medium[from_chan] != MEDIUM_RADIO && + p_audio_config->chan_medium[from_chan] != MEDIUM_NETTNC) { text_color_set(DW_COLOR_ERROR); dw_printf ("Config file, line %d: FROM-channel %d is not valid.\n", line, from_chan); @@ -2347,8 +2472,8 @@ void config_init (char *fname, struct audio_s *p_audio_config, continue; } - if (p_audio_config->achan[to_chan].medium != MEDIUM_RADIO && - p_audio_config->achan[to_chan].medium != MEDIUM_NETTNC) { + if (p_audio_config->chan_medium[to_chan] != MEDIUM_RADIO && + p_audio_config->chan_medium[to_chan] != MEDIUM_NETTNC) { text_color_set(DW_COLOR_ERROR); dw_printf ("Config file, line %d: TO-channel %d is not valid.\n", line, to_chan); @@ -2398,7 +2523,7 @@ void config_init (char *fname, struct audio_s *p_audio_config, text_color_set(DW_COLOR_ERROR); dw_printf ("Config file, line %d: Preemptive digipeating DROP option is discouraged.\n", line); dw_printf ("It can create a via path which is misleading about the actual path taken.\n"); - dw_printf ("TRACE is the best choice for this feature.\n"); + dw_printf ("PREEMPT is the best choice for this feature.\n"); p_digi_config->preempt[from_chan][to_chan] = PREEMPT_DROP; t = split(NULL,0); } @@ -2406,14 +2531,18 @@ void config_init (char *fname, struct audio_s *p_audio_config, text_color_set(DW_COLOR_ERROR); dw_printf ("Config file, line %d: Preemptive digipeating MARK option is discouraged.\n", line); dw_printf ("It can create a via path which is misleading about the actual path taken.\n"); - dw_printf ("TRACE is the best choice for this feature.\n"); + dw_printf ("PREEMPT is the best choice for this feature.\n"); p_digi_config->preempt[from_chan][to_chan] = PREEMPT_MARK; t = split(NULL,0); } - else if (strcasecmp(t, "TRACE") == 0) { + else if ((strcasecmp(t, "TRACE") == 0) || (strncasecmp(t, "PREEMPT", 7) == 0)){ p_digi_config->preempt[from_chan][to_chan] = PREEMPT_TRACE; t = split(NULL,0); } + else if (strncasecmp(t, "ATGP=", 5) == 0) { + strlcpy (p_digi_config->atgp[from_chan][to_chan], t+5, sizeof(p_digi_config->atgp[from_chan][to_chan]));; + t = split(NULL,0); + } } if (t != NULL) { @@ -2476,7 +2605,7 @@ void config_init (char *fname, struct audio_s *p_audio_config, // Only radio channels are valid for regenerate. - if (p_audio_config->achan[from_chan].medium != MEDIUM_RADIO) { + if (p_audio_config->chan_medium[from_chan] != MEDIUM_RADIO) { text_color_set(DW_COLOR_ERROR); dw_printf ("Config file, line %d: FROM-channel %d is not valid.\n", line, from_chan); @@ -2502,7 +2631,7 @@ void config_init (char *fname, struct audio_s *p_audio_config, MAX_CHANS-1, line); continue; } - if (p_audio_config->achan[to_chan].medium != MEDIUM_RADIO) { + if (p_audio_config->chan_medium[to_chan] != MEDIUM_RADIO) { text_color_set(DW_COLOR_ERROR); dw_printf ("Config file, line %d: TO-channel %d is not valid.\n", line, to_chan); @@ -2553,7 +2682,7 @@ void config_init (char *fname, struct audio_s *p_audio_config, // There is discussion about this in the document called // Why-is-9600-only-twice-as-fast-as-1200.pdf - if (p_audio_config->achan[from_chan].medium != MEDIUM_RADIO) { + if (p_audio_config->chan_medium[from_chan] != MEDIUM_RADIO) { text_color_set(DW_COLOR_ERROR); dw_printf ("Config file, line %d: FROM-channel %d is not valid.\n", line, from_chan); @@ -2580,7 +2709,7 @@ void config_init (char *fname, struct audio_s *p_audio_config, MAX_CHANS-1, line); continue; } - if (p_audio_config->achan[to_chan].medium != MEDIUM_RADIO) { + if (p_audio_config->chan_medium[to_chan] != MEDIUM_RADIO) { text_color_set(DW_COLOR_ERROR); dw_printf ("Config file, line %d: TO-channel %d is not valid.\n", line, to_chan); @@ -2642,11 +2771,11 @@ void config_init (char *fname, struct audio_s *p_audio_config, * There is discussion about this in the document called * Why-is-9600-only-twice-as-fast-as-1200.pdf * - * IGFILTER - APRS-IS (IGate) server side - completely diffeent. + * IGFILTER - APRS-IS (IGate) server side - completely different. * I'm not happy with this name because IG sounds like IGate * which is really the client side. More comments later. * Maybe it should be called subscribe or something like that - * because the subscriptions are cummulative. + * because the subscriptions are cumulative. */ else if (strcasecmp(t, "FILTER") == 0) { @@ -2661,6 +2790,12 @@ void config_init (char *fname, struct audio_s *p_audio_config, } if (*t == 'i' || *t == 'I') { from_chan = MAX_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"); + 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! If you insist, be sure to add \" | i/180 \" so you don't break messaging.\n"); } else { from_chan = isdigit(*t) ? atoi(t) : -999; @@ -2671,14 +2806,14 @@ void config_init (char *fname, struct audio_s *p_audio_config, continue; } - if (p_audio_config->achan[from_chan].medium != MEDIUM_RADIO && - p_audio_config->achan[from_chan].medium != MEDIUM_NETTNC) { + if (p_audio_config->chan_medium[from_chan] != MEDIUM_RADIO && + p_audio_config->chan_medium[from_chan] != MEDIUM_NETTNC) { text_color_set(DW_COLOR_ERROR); dw_printf ("Config file, line %d: FROM-channel %d is not valid.\n", line, from_chan); continue; } - if (p_audio_config->achan[from_chan].medium == MEDIUM_IGATE) { + if (p_audio_config->chan_medium[from_chan] == MEDIUM_IGATE) { text_color_set(DW_COLOR_ERROR); dw_printf ("Config file, line %d: Use 'IG' rather than %d for FROM-channel.\n", line, from_chan); @@ -2694,6 +2829,12 @@ void config_init (char *fname, struct audio_s *p_audio_config, } if (*t == 'i' || *t == 'I') { to_chan = MAX_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"); + 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"); } else { to_chan = isdigit(*t) ? atoi(t) : -999; @@ -2703,14 +2844,14 @@ void config_init (char *fname, struct audio_s *p_audio_config, MAX_CHANS-1, line); continue; } - if (p_audio_config->achan[to_chan].medium != MEDIUM_RADIO && - p_audio_config->achan[to_chan].medium != MEDIUM_NETTNC) { + if (p_audio_config->chan_medium[to_chan] != MEDIUM_RADIO && + p_audio_config->chan_medium[to_chan] != MEDIUM_NETTNC) { text_color_set(DW_COLOR_ERROR); dw_printf ("Config file, line %d: TO-channel %d is not valid.\n", line, to_chan); continue; } - if (p_audio_config->achan[to_chan].medium == MEDIUM_IGATE) { + if (p_audio_config->chan_medium[to_chan] == MEDIUM_IGATE) { text_color_set(DW_COLOR_ERROR); dw_printf ("Config file, line %d: Use 'IG' rather than %d for TO-channel.\n", line, to_chan); @@ -2745,6 +2886,9 @@ void config_init (char *fname, struct audio_s *p_audio_config, /* * CFILTER from-chan to-chan filter_specification_expression + * + * Why did I put this here? + * What would be a useful use case? Perhaps block by source or destination? */ else if (strcasecmp(t, "CFILTER") == 0) { @@ -2769,7 +2913,7 @@ void config_init (char *fname, struct audio_s *p_audio_config, // DO NOT allow a network TNC here. // Must be internal modem to have necessary knowledge about channel status. - if (p_audio_config->achan[from_chan].medium != MEDIUM_RADIO) { + if (p_audio_config->chan_medium[from_chan] != MEDIUM_RADIO) { text_color_set(DW_COLOR_ERROR); dw_printf ("Config file, line %d: FROM-channel %d is not valid.\n", line, from_chan); @@ -2790,7 +2934,7 @@ void config_init (char *fname, struct audio_s *p_audio_config, MAX_CHANS-1, line); continue; } - if (p_audio_config->achan[to_chan].medium != MEDIUM_RADIO) { + if (p_audio_config->chan_medium[to_chan] != MEDIUM_RADIO) { text_color_set(DW_COLOR_ERROR); dw_printf ("Config file, line %d: TO-channel %d is not valid.\n", line, to_chan); @@ -3104,7 +3248,7 @@ 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 ("Line %d: Missing latitude for TTGRID command.\n", line); + dw_printf ("Line %d: Missing minimum latitude for TTGRID command.\n", line); continue; } tl->grid.lat0 = parse_ll(t,LAT,line); @@ -3114,7 +3258,7 @@ 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 ("Line %d: Missing longitude for TTGRID command.\n", line); + dw_printf ("Line %d: Missing minimum longitude for TTGRID command.\n", line); continue; } tl->grid.lon0 = parse_ll(t,LON,line); @@ -3124,7 +3268,7 @@ 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 ("Line %d: Missing latitude for TTGRID command.\n", line); + dw_printf ("Line %d: Missing maximum latitude for TTGRID command.\n", line); continue; } tl->grid.lat9 = parse_ll(t,LAT,line); @@ -3134,13 +3278,16 @@ 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 ("Line %d: Missing longitude for TTGRID command.\n", line); + dw_printf ("Line %d: Missing maximum longitude for TTGRID command.\n", line); continue; } - tl->grid.lon0 = parse_ll(t,LON,line); + tl->grid.lon9 = parse_ll(t,LON,line); /* temp debugging */ + // dw_printf ("CONFIG TTGRID min %f %f\n", tl->grid.lat0, tl->grid.lon0); + // dw_printf ("CONFIG TTGRID max %f %f\n", tl->grid.lat9, tl->grid.lon9); + //for (j=0; jttloc_len; j++) { // dw_printf ("debug ttloc %d/%d %s\n", j, p_tt_config->ttloc_size, // p_tt_config->ttloc_ptr[j].pattern); @@ -3677,7 +3824,7 @@ void config_init (char *fname, struct audio_s *p_audio_config, strlcpy(tl->pattern, "", sizeof(tl->pattern)); /* Pattern: Any combination of digits, x, y, and z. */ - /* Also make note of which letters are used in pattern and defintition. */ + /* Also make note of which letters are used in pattern and definition. */ /* Version 1.2: also allow A,B,C,D in the pattern. */ t = split(NULL,0); @@ -3951,7 +4098,7 @@ void config_init (char *fname, struct audio_s *p_audio_config, // I suppose we need internal modem channel here. // otherwise a DTMF decoder would not be available. - if (p_audio_config->achan[r].medium != MEDIUM_RADIO) { + if (p_audio_config->chan_medium[r] != MEDIUM_RADIO) { text_color_set(DW_COLOR_ERROR); dw_printf ("Config file, line %d: TTOBJ DTMF receive channel %d is not valid.\n", line, r); @@ -3977,8 +4124,8 @@ void config_init (char *fname, struct audio_s *p_audio_config, dw_printf ("Config file: Transmit channel must be in range of 0 to %d on line %d.\n", MAX_CHANS-1, line); x = -1; } - else if (p_audio_config->achan[x].medium != MEDIUM_RADIO && - p_audio_config->achan[x].medium != MEDIUM_NETTNC) { + else if (p_audio_config->chan_medium[x] != MEDIUM_RADIO && + p_audio_config->chan_medium[x] != MEDIUM_NETTNC) { text_color_set(DW_COLOR_ERROR); dw_printf ("Config file, line %d: TTOBJ transmit channel %d is not valid.\n", line, x); x = -1; @@ -4463,6 +4610,13 @@ void config_init (char *fname, struct audio_s *p_audio_config, continue; } n = atoi(t); + t = split(NULL,0); + if (t != NULL) { + text_color_set(DW_COLOR_ERROR); + dw_printf ("Line %d: Unexpected \"%s\" after the port number.\n", line, t); + dw_printf ("Perhaps you were trying to use feature available only with KISSPORT.\n"); + continue; + } if ((n >= MIN_IP_PORT_NUMBER && n <= MAX_IP_PORT_NUMBER) || n == 0) { p_misc_config->agwpe_port = n; } @@ -4475,27 +4629,89 @@ void config_init (char *fname, struct audio_s *p_audio_config, } /* - * KISSPORT - Port number for KISS over IP. + * KISSPORT port [ chan ] - Port number for KISS over IP. */ + // Previously we allowed only a single TCP port for KISS. + // An increasing number of people want to run multiple radios. + // Unfortunately, most applications don't know how to deal with multi-radio TNCs. + // They ignore the channel on receive and always transmit to channel 0. + // Running multiple instances of direwolf is a work-around but this leads to + // more complex configuration and we lose the cross-channel digipeating capability. + // In release 1.7 we add a new feature to assign a single radio channel to a TCP port. + // e.g. + // KISSPORT 8001 # default, all channels. Radio channel = KISS channel. + // + // KISSPORT 7000 0 # Only radio channel 0 for receive. + // # Transmit to radio channel 0, ignoring KISS channel. + // + // KISSPORT 7001 1 # Only radio channel 1 for receive. KISS channel set to 0. + // # Transmit to radio channel 1, ignoring KISS channel. + +// FIXME else if (strcasecmp(t, "KISSPORT") == 0) { int n; + int tcp_port = 0; + int chan = -1; // optional. default to all if not specified. t = split(NULL,0); if (t == NULL) { text_color_set(DW_COLOR_ERROR); - dw_printf ("Line %d: Missing port number for KISSPORT command.\n", line); + dw_printf ("Line %d: Missing TCP port number for KISSPORT command.\n", line); continue; } n = atoi(t); if ((n >= MIN_IP_PORT_NUMBER && n <= MAX_IP_PORT_NUMBER) || n == 0) { - p_misc_config->kiss_port = n; + tcp_port = n; } else { - p_misc_config->kiss_port = DEFAULT_KISS_PORT; text_color_set(DW_COLOR_ERROR); - dw_printf ("Line %d: Invalid port number for KISS TCPIP Socket Interface. Using %d.\n", - line, p_misc_config->kiss_port); + dw_printf ("Line %d: Invalid TCP port number for KISS TCPIP Socket Interface.\n", line); + dw_printf ("Use something in the range of %d to %d.\n", MIN_IP_PORT_NUMBER, MAX_IP_PORT_NUMBER); + continue; } + + t = split(NULL,0); + if (t != NULL) { + chan = atoi(t); + if (chan < 0 || chan >= MAX_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); + continue; + } + } + + // "KISSPORT 0" is used to remove the default entry. + + if (tcp_port == 0) { + p_misc_config->kiss_port[0] = 0; // Should all be wiped out? + } + else { + + // Try to find an empty slot. + // A duplicate TCP port number will overwrite the previous value. + + int slot = -1; + for (int i = 0; i < MAX_KISS_TCP_PORTS && slot == -1; i++) { + if (p_misc_config->kiss_port[i] == tcp_port) { + slot = i; + if ( ! (slot == 0 && tcp_port == DEFAULT_KISS_PORT)) { + text_color_set(DW_COLOR_ERROR); + dw_printf ("Line %d: Warning: Duplicate TCP port %d will overwrite previous value.\n", line, tcp_port); + } + } + else if (p_misc_config->kiss_port[i] == 0) { + slot = i; + } + } + if (slot >= 0) { + p_misc_config->kiss_port[slot] = tcp_port; + p_misc_config->kiss_chan[slot] = chan; + } + else { + text_color_set(DW_COLOR_ERROR); + dw_printf ("Line %d: Too many KISSPORT commands.\n", line); + } + } } /* @@ -4557,6 +4773,7 @@ void config_init (char *fname, struct audio_s *p_audio_config, /* * KISSCOPY - Data from network KISS client is copied to all others. + * This does not apply to pseudo terminal KISS. */ else if (strcasecmp(t, "KISSCOPY") == 0) { @@ -4565,7 +4782,44 @@ void config_init (char *fname, struct audio_s *p_audio_config, /* - * GPSNMEA - Device name for reading from GPS receiver. + * DNSSD - Enable or disable (1/0) dns-sd, DNS Service Discovery announcements + * DNSSDNAME - Set DNS-SD service name, defaults to "Dire Wolf on " + */ + + else if (strcasecmp(t, "DNSSD") == 0) { + int n; + t = split(NULL,0); + if (t == NULL) { + text_color_set(DW_COLOR_ERROR); + dw_printf ("Line %d: Missing integer value for DNSSD command.\n", line); + continue; + } + n = atoi(t); + if (n == 0 || n == 1) { + p_misc_config->dns_sd_enabled = n; + } else { + p_misc_config->dns_sd_enabled = 0; + text_color_set(DW_COLOR_ERROR); + dw_printf ("Line %d: Invalid integer value for DNSSD. Disabling dns-sd.\n", line); + } + } + + else if (strcasecmp(t, "DNSSDNAME") == 0) { + t = split(NULL, 1); + if (t == NULL) { + text_color_set(DW_COLOR_ERROR); + dw_printf ("Line %d: Missing service name for DNSSDNAME.\n", line); + continue; + } + else { + strlcpy(p_misc_config->dns_sd_name, t, sizeof(p_misc_config->dns_sd_name)); + } + } + + + +/* + * GPSNMEA serial-device [ speed ] - Direct connection to GPS receiver. */ else if (strcasecmp(t, "gpsnmea") == 0) { t = split(NULL,0); @@ -4574,8 +4828,15 @@ void config_init (char *fname, struct audio_s *p_audio_config, dw_printf ("Config file, line %d: Missing serial port name for GPS receiver.\n", line); continue; } + strlcpy (p_misc_config->gpsnmea_port, t, sizeof(p_misc_config->gpsnmea_port)); + + t = split(NULL,0); + if (t != NULL) { + int n = atoi(t); + p_misc_config->gpsnmea_speed = n; + } else { - strlcpy (p_misc_config->gpsnmea_port, t, sizeof(p_misc_config->gpsnmea_port)); + p_misc_config->gpsnmea_speed = 4800; // The standard at one time. } } @@ -4664,26 +4925,26 @@ void config_init (char *fname, struct audio_s *p_audio_config, strlcpy (p_misc_config->waypoint_serial_port, t, sizeof(p_misc_config->waypoint_serial_port)); } - /* Anthing remaining is the formats to enable. */ + /* Anything remaining is the formats to enable. */ t = split(NULL,1); if (t != NULL) { for ( ; *t != '\0' ; t++ ) { switch (toupper(*t)) { case 'N': - p_misc_config->waypoint_formats |= WPT_FORMAT_NMEA_GENERIC; + p_misc_config->waypoint_formats |= WPL_FORMAT_NMEA_GENERIC; break; case 'G': - p_misc_config->waypoint_formats |= WPT_FORMAT_GARMIN; + p_misc_config->waypoint_formats |= WPL_FORMAT_GARMIN; break; case 'M': - p_misc_config->waypoint_formats |= WPT_FORMAT_MAGELLAN; + p_misc_config->waypoint_formats |= WPL_FORMAT_MAGELLAN; break; case 'K': - p_misc_config->waypoint_formats |= WPT_FORMAT_KENWOOD; + p_misc_config->waypoint_formats |= WPL_FORMAT_KENWOOD; break; case 'A': - p_misc_config->waypoint_formats |= WPT_FORMAT_AIS; + p_misc_config->waypoint_formats |= WPL_FORMAT_AIS; break; case ' ': case ',': @@ -4757,7 +5018,7 @@ void config_init (char *fname, struct audio_s *p_audio_config, text_color_set(DW_COLOR_ERROR); dw_printf ("Config file, line %d: Old style 'BEACON' has been replaced with new commands.\n", line); - dw_printf ("Use PBEACON, OBEACON, or CBEACON instead.\n"); + dw_printf ("Use PBEACON, OBEACON, TBEACON, or CBEACON instead.\n"); } @@ -4772,6 +5033,9 @@ void config_init (char *fname, struct audio_s *p_audio_config, * New style with keywords for options. */ +// TODO: maybe add proportional pathing so multiple beacon timing does not need to be manually constructed? +// http://www.aprs.org/newN/ProportionalPathing.txt + else if (strcasecmp(t, "PBEACON") == 0 || strcasecmp(t, "OBEACON") == 0 || strcasecmp(t, "TBEACON") == 0 || @@ -5031,7 +5295,7 @@ void config_init (char *fname, struct audio_s *p_audio_config, /* * V20 address [ address ... ] - Stations known to support only AX.25 v2.0. * When connecting to these, skip SABME and go right to SABM. - * Possible to have multiple and they are cummulative. + * Possible to have multiple and they are cumulative. */ else if (strcasecmp(t, "V20") == 0) { @@ -5065,9 +5329,9 @@ void config_init (char *fname, struct audio_s *p_audio_config, /* * NOXID address [ address ... ] - Stations known not to understand XID. - * After connecting to these (with v2.2 obviously), don't try using XID commmand. + * After connecting to these (with v2.2 obviously), don't try using XID command. * AX.25 for Linux is the one known case so far. - * Possible to have multiple and they are cummulative. + * Possible to have multiple and they are cumulative. */ else if (strcasecmp(t, "NOXID") == 0) { @@ -5197,7 +5461,7 @@ void config_init (char *fname, struct audio_s *p_audio_config, /* When IGate is enabled, all radio channels must have a callsign associated. */ if (strlen(p_igate_config->t2_login) > 0 && - (p_audio_config->achan[i].medium == MEDIUM_RADIO || p_audio_config->achan[i].medium == MEDIUM_NETTNC)) { + (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) { text_color_set(DW_COLOR_ERROR); @@ -5223,9 +5487,9 @@ void config_init (char *fname, struct audio_s *p_audio_config, if (strlen(p_igate_config->t2_login) > 0) { for (j=0; jachan[j].medium == MEDIUM_RADIO || p_audio_config->achan[j].medium == MEDIUM_NETTNC) { + if (p_audio_config->chan_medium[j] == MEDIUM_RADIO || p_audio_config->chan_medium[j] == MEDIUM_NETTNC) { if (p_digi_config->filter_str[MAX_CHANS][j] == NULL) { - p_digi_config->filter_str[MAX_CHANS][j] = strdup("i/60"); + p_digi_config->filter_str[MAX_CHANS][j] = strdup("i/180"); } } } @@ -5245,6 +5509,10 @@ void config_init (char *fname, struct audio_s *p_audio_config, * Returns 1 for success, 0 for serious error. */ +// FIXME: provide error messages when non applicable option is used for particular beacon type. +// e.g. IBEACON DELAY=1 EVERY=1 SENDTO=IG OVERLAY=R SYMBOL="igate" LAT=37^44.46N LONG=122^27.19W COMMENT="N1KOL-1 IGATE" +// Just ignores overlay, symbol, lat, long, and comment. + static int beacon_options(char *cmd, struct beacon_s *b, int line, struct audio_s *p_audio_config) { char *t; @@ -5273,11 +5541,13 @@ static int beacon_options(char *cmd, struct beacon_s *b, int line, struct audio_ b->freq = G_UNKNOWN; b->tone = G_UNKNOWN; b->offset = G_UNKNOWN; + b->source = NULL; + b->dest = NULL; while ((t = split(NULL,0)) != NULL) { char keyword[20]; - char value[200]; + char value[1000]; char *e; char *p; @@ -5292,6 +5562,44 @@ static int beacon_options(char *cmd, struct beacon_s *b, int line, struct audio_ strlcpy (keyword, t, sizeof(keyword)); strlcpy (value, e+1, sizeof(value)); +// QUICK TEMP EXPERIMENT, maybe permanent new feature. +// Recognize \xnn as hexadecimal value. Handy for UTF-8 in comment. +// Maybe recognize the <0xnn> form that we print. +// +// # Convert between languages here: https://translate.google.com/ then +// # Convert to UTF-8 bytes here: https://codebeautify.org/utf8-converter +// +// pbeacon delay=0:05 every=0:30 sendto=R0 lat=12.5N long=69.97W comment="\xe3\x82\xa2\xe3\x83\x9e\xe3\x83\x81\xe3\x83\xa5\xe3\x82\xa2\xe7\x84\xa1\xe7\xb7\x9a \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" + + char temp[256]; + int tlen = 0; + + for (char *p = value; *p != '\0'; ) { + if (p[0] == '\\' && p[1] == 'x' && strlen(p) >= 4 && isxdigit(p[2]) && isxdigit(p[3])) { + int n = 0; + for (int i = 2; i < 4; i++) { + n = n * 16; + if (islower(p[i])) { + n += p[i] - 'a' + 10; + } + else if (isupper(p[i])) { + n += p[i] - 'A' + 10; + } + else { // must be digit due to isxdigit test above. + n += p[i] - '0'; + } + } + temp[tlen++] = n; + p += 4; + } + else { + temp[tlen++] = *p++; + } + } + temp[tlen] = '\0'; + strlcpy (value, temp, sizeof(value)); + +// end if (strcasecmp(keyword, "DELAY") == 0) { b->delay = parse_interval(value,line); } @@ -5314,7 +5622,8 @@ 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->achan[n].medium == MEDIUM_NONE) { + if (( n < 0 || n >= MAX_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); continue; @@ -5324,7 +5633,8 @@ 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->achan[n].medium == MEDIUM_NONE) { + if (( n < 0 || n >= MAX_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); continue; @@ -5335,7 +5645,8 @@ 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->achan[n].medium == MEDIUM_NONE) { + if (( n < 0 || n >= MAX_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); continue; @@ -5344,6 +5655,17 @@ static int beacon_options(char *cmd, struct beacon_s *b, int line, struct audio_ b->sendto_chan = n; } } + else if (strcasecmp(keyword, "SOURCE") == 0) { + b->source = strdup(value); + for (p = b->source; *p != '\0'; p++) { + if (islower(*p)) { + *p = toupper(*p); /* silently force upper case. */ + } + } + if (strlen(b->source) > 9) { + b->source[9] = '\0'; + } + } else if (strcasecmp(keyword, "DEST") == 0) { b->dest = strdup(value); for (p = b->dest; *p != '\0'; p++) { @@ -5404,7 +5726,29 @@ static int beacon_options(char *cmd, struct beacon_s *b, int line, struct audio_ } } else if (strcasecmp(keyword, "ALT") == 0 || strcasecmp(keyword, "ALTITUDE") == 0) { - b->alt_m = atof(value); + + char *unit = strpbrk(value, "abcedfghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"); + if (unit != NULL) { + float meters = 0; + for (int j=0; jalt_m = atof(value); + } + else { + // valid unit + b->alt_m = atof(value) * meters; + } + } else { + // no unit specified + b->alt_m = atof(value); + } } else if (strcasecmp(keyword, "ZONE") == 0) { strlcpy(zone, value, sizeof(zone)); @@ -5431,8 +5775,9 @@ static int beacon_options(char *cmd, struct beacon_s *b, int line, struct audio_ else if (strcasecmp(keyword, "POWER") == 0) { b->power = atoi(value); } - else if (strcasecmp(keyword, "HEIGHT") == 0) { + else if (strcasecmp(keyword, "HEIGHT") == 0) { // This is in feet. b->height = atoi(value); + // TODO: ability to add units suffix, e.g. 10m } else if (strcasecmp(keyword, "GAIN") == 0) { b->gain = atoi(value); @@ -5480,7 +5825,7 @@ static int beacon_options(char *cmd, struct beacon_s *b, int line, struct audio_ } /* - * Convert UTM coordintes to lat / long. + * Convert UTM coordinates to lat / long. */ if (strlen(zone) > 0 || easting != G_UNKNOWN || northing != G_UNKNOWN) { @@ -5515,6 +5860,10 @@ static int beacon_options(char *cmd, struct beacon_s *b, int line, struct audio_ /* * Process symbol now that we have any later overlay. + * + * FIXME: Someone who used this was surprised to end up with Solar Powser (S-). + * overlay=S symbol="/-" + * We should complain if overlay used with symtab other than \. */ if (strlen(temp_symbol) > 0) { @@ -5547,19 +5896,32 @@ 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->achan[b->sendto_chan].medium == MEDIUM_NONE) { + if (( b->sendto_chan < 0 || b->sendto_chan >= MAX_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); return (0); } - 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 (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 ) { - 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); - return (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 ) { + + 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); + return (0); + } } } diff --git a/src/config.h b/src/config.h index 562d307c..360ac492 100644 --- a/src/config.h +++ b/src/config.h @@ -30,11 +30,31 @@ enum sendto_type_e { SENDTO_XMIT, SENDTO_IGATE, SENDTO_RECV }; #define MAX_BEACONS 30 +#define MAX_KISS_TCP_PORTS (MAX_CHANS+1) struct misc_config_s { - int agwpe_port; /* Port number for the "AGW TCPIP Socket Interface" */ - int kiss_port; /* Port number for the "TCP KISS" protocol. */ + int agwpe_port; /* TCP Port number for the "AGW TCPIP Socket Interface" */ + + // Previously we allowed only a single TCP port for KISS. + // An increasing number of people want to run multiple radios. + // Unfortunately, most applications don't know how to deal with multi-radio TNCs. + // They ignore the channel on receive and always transmit to channel 0. + // Running multiple instances of direwolf is a work-around but this leads to + // more complex configuration and we lose the cross-channel digipeating capability. + // In release 1.7 we add a new feature to assign a single radio channel to a TCP port. + // e.g. + // KISSPORT 8001 # default, all channels. Radio channel = KISS channel. + // + // KISSPORT 7000 0 # Only radio channel 0 for receive. + // # Transmit to radio channel 0, ignoring KISS channel. + // + // KISSPORT 7001 1 # Only radio channel 1 for receive. KISS channel set to 0. + // # Transmit to radio channel 1, ignoring KISS channel. + + int kiss_port[MAX_KISS_TCP_PORTS]; /* TCP Port number for the "TCP KISS" protocol. */ + int kiss_chan[MAX_KISS_TCP_PORTS]; /* Radio Channel number for this port or -1 for all. */ + int kiss_copy; /* Data from network KISS client is copied to all others. */ int enable_kiss_pt; /* Enable pseudo terminal for KISS. */ /* Want this to be off by default because it hangs */ @@ -56,7 +76,8 @@ struct misc_config_s { char gpsnmea_port[20]; /* Serial port name for reading NMEA sentences from GPS. */ /* e.g. COM22, /dev/ttyACM0 */ - /* Currently no option for setting non-standard speed. */ + + int gpsnmea_speed; /* Speed for above, baud, default 4800. */ char gpsd_host[20]; /* Host for gpsd server. */ /* e.g. localhost, 192.168.1.2 */ @@ -77,17 +98,20 @@ struct misc_config_s { int waypoint_formats; /* Which sentence formats should be generated? */ -#define WPT_FORMAT_NMEA_GENERIC 0x01 /* N $GPWPT */ -#define WPT_FORMAT_GARMIN 0x02 /* G $PGRMW */ -#define WPT_FORMAT_MAGELLAN 0x04 /* M $PMGNWPL */ -#define WPT_FORMAT_KENWOOD 0x08 /* K $PKWDWPL */ -#define WPT_FORMAT_AIS 0x10 /* A !AIVDM */ +#define WPL_FORMAT_NMEA_GENERIC 0x01 /* N $GPWPL */ +#define WPL_FORMAT_GARMIN 0x02 /* G $PGRMW */ +#define WPL_FORMAT_MAGELLAN 0x04 /* M $PMGNWPL */ +#define WPL_FORMAT_KENWOOD 0x08 /* K $PKWDWPL */ +#define WPL_FORMAT_AIS 0x10 /* A !AIVDM */ int log_daily_names; /* True to generate new log file each day. */ char log_path[80]; /* Either directory or full file name depending on above. */ + int dns_sd_enabled; /* DNS Service Discovery announcement enabled. */ + char dns_sd_name[64]; /* Name announced on dns-sd; defaults to "Dire Wolf on " */ + int sb_configured; /* TRUE if SmartBeaconing is configured. */ int sb_fast_speed; /* MPH */ int sb_fast_rate; /* seconds */ @@ -160,6 +184,9 @@ struct misc_config_s { time_t next; /* Unix time to transmit next one. */ + char *source; /* NULL or explicit AX.25 source address to use */ + /* instead of the mycall value for the channel. */ + char *dest; /* NULL or explicit AX.25 destination to use */ /* instead of the software version such as APDW11. */ @@ -187,7 +214,7 @@ struct misc_config_s { char symbol; /* Symbol code. */ float power; /* For PHG. */ - float height; + float height; /* HAAT in feet */ float gain; /* Original protocol spec was unclear. */ /* Addendum 1.1 clarifies it is dBi not dBd. */ diff --git a/src/decode_aprs.c b/src/decode_aprs.c index 3afa3773..ab933278 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 John Langner, WB2OSZ +// Copyright (C) 2011, 2012, 2013, 2014, 2015, 2017, 2022 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 @@ -17,6 +17,7 @@ // along with this program. If not, see . // +// TODO: Better error messages for examples here: http://lists.tapr.org/pipermail/aprssig_lists.tapr.org/2023-July/date.html /*------------------------------------------------------------------ * @@ -26,7 +27,7 @@ * * Description: Present the packet contents in human readable format. * This is a fairly complete implementation with error messages - * pointing out various specication violations. + * pointing out various specification violations. * * Assumptions: ax25_from_frame() has been called to * separate the header and information. @@ -116,7 +117,6 @@ static void aprs_morse_code (decode_aprs_t *A, char *, int); static void aprs_positionless_weather_report (decode_aprs_t *A, unsigned char *, int); static void weather_data (decode_aprs_t *A, char *wdata, int wind_prefix); static void aprs_ultimeter (decode_aprs_t *A, char *, int); -static void third_party_header (decode_aprs_t *A, char *, int); static void decode_position (decode_aprs_t *A, position_t *ppos); static void decode_compressed_position (decode_aprs_t *A, compressed_position_t *ppos); static double get_latitude_8 (char *p, int quiet); @@ -141,6 +141,12 @@ static void process_comment (decode_aprs_t *A, char *pstart, int clen); * * quiet - Suppress error messages. * + * third_party_src - Specify when parsing a third party header. + * (decode_aprs is called recursively.) + * This is mostly found when an IGate transmits a message + * that came via APRS-IS. + * NULL when not third party payload. + * * Outputs: A-> g_symbol_table, g_symbol_code, * g_lat, g_lon, * g_speed_mph, g_course, g_altitude_ft, @@ -152,25 +158,29 @@ static void process_comment (decode_aprs_t *A, char *pstart, int clen); * *------------------------------------------------------------------*/ -void decode_aprs (decode_aprs_t *A, packet_t pp, int quiet) +void decode_aprs (decode_aprs_t *A, packet_t pp, int quiet, char *third_party_src) { + //dw_printf ("DEBUG decode_aprs quiet=%d, third_party=%p\n", quiet, third_party_src); - char dest[AX25_MAX_ADDR_LEN]; unsigned char *pinfo; int info_len; info_len = ax25_get_info (pp, &pinfo); - memset (A, 0, sizeof (*A)); + //dw_printf ("DEBUG decode_aprs info=\"%s\"\n", pinfo); + + if (third_party_src == NULL) { + memset (A, 0, sizeof (*A)); + } A->g_quiet = quiet; if (isprint(*pinfo)) { - snprintf (A->g_msg_type, sizeof(A->g_msg_type), "Unknown APRS Data Type Indicator \"%c\"", *pinfo); + snprintf (A->g_data_type_desc, sizeof(A->g_data_type_desc), "ERROR!!! Unknown APRS Data Type Indicator \"%c\"", *pinfo); } else { - snprintf (A->g_msg_type, sizeof(A->g_msg_type), "ERROR!!! Unknown APRS Data Type Indicator: unprintable 0x%02x", *pinfo); + snprintf (A->g_data_type_desc, sizeof(A->g_data_type_desc), "ERROR!!! Unknown APRS Data Type Indicator: unprintable 0x%02x", *pinfo); } A->g_symbol_table = '/'; /* Default to primary table. */ @@ -197,14 +207,61 @@ void decode_aprs (decode_aprs_t *A, packet_t pp, int quiet) 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. +// 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 + +// TODO: Complain if used digi is found after unused. Should never happen. + + +// If third-party header, try to decode just the payload. + + if (*pinfo == '}') { + //dw_printf ("DEBUG decode_aprs recursively process third party header\n"); + + // This must not be strict because the addresses in third party payload doesn't + // need to adhere to the AX.25 address format (i.e. 6 upper case alphanumeric.) + // SSID can be 2 alphanumeric characters. + // Addresses can include lower case, e.g. q construct. + + // e.g. WR2X-2>APRS,WA1PLE-13*:} + // K1BOS-B>APOSB,TCPIP,WR2X-2*:@122015z4221.42ND07111.93W&/A=000000SharkRF openSPOT3 MMDVM446.025 MA/SW + + packet_t pp_payload = ax25_from_text ((char*)pinfo+1, 0); + if (pp_payload != NULL) { + char payload_src[AX25_MAX_ADDR_LEN]; + memset(payload_src, 0, sizeof(payload_src)); + memcpy(payload_src, (char*)pinfo+1, sizeof(payload_src)-1); + char *q = strchr(payload_src, '>'); + if (q != NULL) *q = '\0'; + A->g_has_thirdparty_header = 1; + decode_aprs (A, pp_payload, quiet, payload_src); // 1 means used recursively + ax25_delete (pp_payload); + return; + } + else { + strlcpy (A->g_data_type_desc, "Third Party Header: Unable to parse payload.", sizeof(A->g_data_type_desc)); + ax25_get_addr_with_ssid (pp, AX25_SOURCE, A->g_src); + ax25_get_addr_with_ssid (pp, AX25_DESTINATION, A->g_dest); + } + } /* * Extract source and destination including the SSID. */ - - ax25_get_addr_with_ssid (pp, AX25_SOURCE, A->g_src); - ax25_get_addr_with_ssid (pp, AX25_DESTINATION, dest); + if (third_party_src != NULL) { + strlcpy (A->g_src, third_party_src, sizeof(A->g_src)); + } + else { + ax25_get_addr_with_ssid (pp, AX25_SOURCE, A->g_src); + } + ax25_get_addr_with_ssid (pp, AX25_DESTINATION, A->g_dest); + + //dw_printf ("DEBUG decode_aprs source=%s, dest=%s\n", A->g_src, A->g_dest); + /* * Report error if the information part contains a nul character. @@ -246,7 +303,7 @@ void decode_aprs (decode_aprs_t *A, packet_t pp, int quiet) break; default: - decode_tocall (A, dest); + decode_tocall (A, A->g_dest); break; } @@ -259,16 +316,17 @@ void decode_aprs (decode_aprs_t *A, packet_t pp, int quiet) if (strncmp((char*)pinfo, "!!", 2) == 0) { - aprs_ultimeter (A, (char*)pinfo, info_len); + aprs_ultimeter (A, (char*)pinfo, info_len); // TODO: produce obsolete error. } else { aprs_ll_pos (A, pinfo, info_len); } + A->g_packet_type = packet_type_position; break; - //case '#': /* Peet Bros U-II Weather station */ + //case '#': /* Peet Bros U-II Weather station */ // TODO: produce obsolete error. //case '*': /* Peet Bros U-II Weather station */ //break; @@ -276,11 +334,13 @@ void decode_aprs (decode_aprs_t *A, packet_t pp, int quiet) if (strncmp((char*)pinfo, "$ULTW", 5) == 0) { - aprs_ultimeter (A, (char*)pinfo, info_len); + aprs_ultimeter (A, (char*)pinfo, info_len); // TODO: produce obsolete error. + A->g_packet_type = packet_type_weather; } else { aprs_raw_nmea (A, pinfo, info_len); + A->g_packet_type = packet_type_position; } break; @@ -288,61 +348,98 @@ void decode_aprs (decode_aprs_t *A, packet_t pp, int quiet) case '`': /* Current Mic-E Data (not used in TM-D700) */ aprs_mic_e (A, pp, pinfo, info_len); + A->g_packet_type = packet_type_position; break; case ')': /* Item. */ aprs_item (A, pinfo, info_len); + A->g_packet_type = packet_type_item; break; case '/': /* Position with timestamp (no APRS messaging) */ case '@': /* Position with timestamp (with APRS messaging) */ aprs_ll_pos_time (A, pinfo, info_len); + A->g_packet_type = packet_type_position; break; - case ':': /* Message: for one person, a group, or a bulletin. */ + case ':': /* "Message" (special APRS meaning): for one person, a group, or a bulletin. */ /* Directed Station Query */ /* Telemetry metadata. */ aprs_message (A, pinfo, info_len, quiet); + + switch (A->g_message_subtype) { + case message_subtype_message: + case message_subtype_ack: + case message_subtype_rej: + A->g_packet_type = packet_type_message; + break; + + case message_subtype_nws: + A->g_packet_type = packet_type_nws; + break; + + case message_subtype_bulletin: + default: + break; + + case message_subtype_telem_parm: + case message_subtype_telem_unit: + case message_subtype_telem_eqns: + case message_subtype_telem_bits: + A->g_packet_type = packet_type_telemetry; + break; + + case message_subtype_directed_query: + A->g_packet_type = packet_type_query; + break; + } break; case ';': /* Object */ aprs_object (A, pinfo, info_len); + A->g_packet_type = packet_type_object; break; case '<': /* Station Capabilities */ aprs_station_capabilities (A, (char*)pinfo, info_len); + A->g_packet_type = packet_type_capabilities; break; case '>': /* Status Report */ aprs_status_report (A, (char*)pinfo, info_len); + A->g_packet_type = packet_type_status; break; case '?': /* General Query */ aprs_general_query (A, (char*)pinfo, info_len, quiet); + A->g_packet_type = packet_type_query; break; case 'T': /* Telemetry */ aprs_telemetry (A, (char*)pinfo, info_len, quiet); + A->g_packet_type = packet_type_telemetry; break; case '_': /* Positionless Weather Report */ aprs_positionless_weather_report (A, pinfo, info_len); + A->g_packet_type = packet_type_weather; break; case '{': /* user defined data */ aprs_user_defined (A, (char*)pinfo, info_len); + A->g_packet_type = packet_type_userdefined; break; case 't': /* Raw touch tone data - NOT PART OF STANDARD */ @@ -351,6 +448,7 @@ void decode_aprs (decode_aprs_t *A, packet_t pp, int quiet) /* Might move into user defined data, above. */ aprs_raw_touch_tone (A, (char*)pinfo, info_len); + // no packet type for t/ filter break; case 'm': /* Morse Code data - NOT PART OF STANDARD */ @@ -360,13 +458,12 @@ void decode_aprs (decode_aprs_t *A, packet_t pp, int quiet) /* Might move into user defined data, above. */ aprs_morse_code (A, (char*)pinfo, info_len); + // no packet type for t/ filter break; - case '}': /* third party header */ - - third_party_header (A, (char*)pinfo, info_len); - break; + //case '}': /* third party header */ + // was already caught earlier. //case '\r': /* CR or LF? */ //case '\n': @@ -380,12 +477,27 @@ void decode_aprs (decode_aprs_t *A, packet_t pp, int quiet) /* - * Look in other locations if not found in information field. + * Priority order for determining the symbol is: + * - Information part, where appropriate. Already done above. + * - Destination field starting with GPS, SPC, or SYM. + * - Source SSID - Confusing to most people. Even I forgot about it when + * someone questioned where the symbol came from. It's in the APRS + * protocol spec, end of Chapter 20. */ if (A->g_symbol_table == ' ' || A->g_symbol_code == ' ') { - symbols_from_dest_or_src (*pinfo, A->g_src, dest, &A->g_symbol_table, &A->g_symbol_code); + // A symbol on a "message" makes no sense and confuses people. + // Third party too. Set from the payload. + // Maybe eliminate for a couple others. + + //dw_printf ("DEBUG decode_aprs@end1 third_party=%d, symbol_table=%c, symbol_code=%c, *pinfo=%c\n", third_party, A->g_symbol_table, A->g_symbol_code, *pinfo); + + if (*pinfo != ':' && *pinfo != '}') { + symbols_from_dest_or_src (*pinfo, A->g_src, A->g_dest, &A->g_symbol_table, &A->g_symbol_code); + } + + //dw_printf ("DEBUG decode_aprs@end2 third_party=%d, symbol_table=%c, symbol_code=%c, *pinfo=%c\n", third_party, A->g_symbol_table, A->g_symbol_code, *pinfo); } } /* end decode_aprs */ @@ -393,8 +505,7 @@ void decode_aprs (decode_aprs_t *A, packet_t pp, int quiet) void decode_aprs_print (decode_aprs_t *A) { - char stemp[200]; - //char tmp2[2]; + char stemp[500]; double absll; char news; int deg; @@ -402,18 +513,19 @@ void decode_aprs_print (decode_aprs_t *A) { char s_lat[30]; char s_lon[30]; int n; - char symbol_description[100]; /* * First line has: - * - message type + * - packet type * - object name * - symbol * - manufacturer/application * - mic-e status * - power/height/gain, range */ - strlcpy (stemp, A->g_msg_type, sizeof(stemp)); + strlcpy (stemp, A->g_data_type_desc, sizeof(stemp)); + + //dw_printf ("DEBUG decode_aprs_print stemp1=%s\n", stemp); if (strlen(A->g_name) > 0) { strlcat (stemp, ", \"", sizeof(stemp)); @@ -421,32 +533,59 @@ void decode_aprs_print (decode_aprs_t *A) { strlcat (stemp, "\"", sizeof(stemp)); } + //dw_printf ("DEBUG decode_aprs_print stemp2=%s\n", stemp); + + //dw_printf ("DEBUG decode_aprs_print symbol_code=%c=0x%02x\n", A->g_symbol_code, A->g_symbol_code); + if (A->g_symbol_code != ' ') { - symbols_get_description (A->g_symbol_table, A->g_symbol_code, symbol_description, sizeof(symbol_description)); + char symbol_description[100]; + symbols_get_description (A->g_symbol_table, A->g_symbol_code, symbol_description, sizeof(symbol_description)); + + //dw_printf ("DEBUG decode_aprs_print symbol_description_description=%s\n", symbol_description); + strlcat (stemp, ", ", sizeof(stemp)); strlcat (stemp, symbol_description, sizeof(stemp)); } + //dw_printf ("DEBUG decode_aprs_print stemp3=%s mfr=%s\n", stemp, A->g_mfr); + if (strlen(A->g_mfr) > 0) { - strlcat (stemp, ", ", sizeof(stemp)); - strlcat (stemp, A->g_mfr, sizeof(stemp)); + if (strcmp(A->g_dest, "APRS") == 0 || + strcmp(A->g_dest, "BEACON") == 0 || + strcmp(A->g_dest, "ID") == 0) { + strlcat (stemp, "\nUse of \"", sizeof(stemp)); + strlcat (stemp, A->g_dest, sizeof(stemp)); + strlcat (stemp, "\" in the destination field is obsolete.", sizeof(stemp)); + strlcat (stemp, " You can help to improve the quality of APRS signals.", sizeof(stemp)); + strlcat (stemp, "\nTell the sender (", sizeof(stemp)); + strlcat (stemp, A->g_src, sizeof(stemp)); + strlcat (stemp, ") to use the proper product identifier from", sizeof(stemp)); + strlcat (stemp, " https://github.com/aprsorg/aprs-deviceid ", sizeof(stemp)); + } + else { + strlcat (stemp, ", ", sizeof(stemp)); + strlcat (stemp, A->g_mfr, sizeof(stemp)); + } } + //dw_printf ("DEBUG decode_aprs_print stemp4=%s\n", stemp); + if (strlen(A->g_mic_e_status) > 0) { strlcat (stemp, ", ", sizeof(stemp)); strlcat (stemp, A->g_mic_e_status, sizeof(stemp)); } + //dw_printf ("DEBUG decode_aprs_print stemp5=%s\n", stemp); if (A->g_power > 0) { char phg[100]; - /* Protcol spec doesn't mention whether this is dBd or dBi. */ + /* Protocol spec doesn't mention whether this is dBd or dBi. */ /* Clarified later. */ /* http://eng.usna.navy.mil/~bruninga/aprs/aprs11.html */ /* "The Antenna Gain in the PHG format on page 28 is in dBi." */ - snprintf (phg, sizeof(phg), ", %d W height=%d %ddBi %s", A->g_power, A->g_height, A->g_gain, A->g_directivity); + snprintf (phg, sizeof(phg), ", %d W height(HAAT)=%dft=%.0fm %ddBi %s", A->g_power, A->g_height, DW_FEET_TO_METERS(A->g_height), A->g_gain, A->g_directivity); strlcat (stemp, phg, sizeof(stemp)); } @@ -456,7 +595,13 @@ void decode_aprs_print (decode_aprs_t *A) { snprintf (rng, sizeof(rng), ", range=%.1f", A->g_range); strlcat (stemp, rng, sizeof(stemp)); } - text_color_set(DW_COLOR_DECODED); + + if (strncmp(stemp, "ERROR", 5) == 0) { + text_color_set(DW_COLOR_ERROR); + } + else { + text_color_set(DW_COLOR_DECODED); + } dw_printf("%s\n", stemp); /* @@ -476,7 +621,6 @@ void decode_aprs_print (decode_aprs_t *A) { * Any example was checked for each hemihemisphere using * http://www.amsat.org/cgi-bin/gridconv */ -// FIXME soften language about upper case. if (strlen(A->g_maidenhead) > 0) { @@ -492,7 +636,7 @@ void decode_aprs_print (decode_aprs_t *A) { if (A->g_lat != G_UNKNOWN || A->g_lon != G_UNKNOWN) { -// Have location but it is posible one part is invalid. +// Have location but it is possible one part is invalid. if (A->g_lat != G_UNKNOWN) { @@ -539,10 +683,10 @@ void decode_aprs_print (decode_aprs_t *A) { }; if (A->g_speed_mph != G_UNKNOWN) { - char spd[20]; + char spd[32]; if (strlen(stemp) > 0) strlcat (stemp, ", ", sizeof(stemp)); - snprintf (spd, sizeof(spd), "%.0f MPH", A->g_speed_mph); + snprintf (spd, sizeof(spd), "%.0f km/h (%.0f MPH)", DW_MILES_TO_KM(A->g_speed_mph), A->g_speed_mph); strlcat (stemp, spd, sizeof(stemp)); }; @@ -555,10 +699,10 @@ void decode_aprs_print (decode_aprs_t *A) { }; if (A->g_altitude_ft != G_UNKNOWN) { - char alt[20]; + char alt[32]; if (strlen(stemp) > 0) strlcat (stemp, ", ", sizeof(stemp)); - snprintf (alt, sizeof(alt), "alt %.0f ft", A->g_altitude_ft); + snprintf (alt, sizeof(alt), "alt %.0f m (%.0f ft)", DW_FEET_TO_METERS(A->g_altitude_ft), A->g_altitude_ft); strlcat (stemp, alt, sizeof(stemp)); }; @@ -660,6 +804,26 @@ void decode_aprs_print (decode_aprs_t *A) { * To be part of a valid UTF-8 sequence, it would need to be followed by 10xxxxxx. */ +// For values 00-7F, ASCII, Unicode, and ISO Latin-1 are all the same. +// ISO Latin-1 adds 80-FF range with a few common symbols, such as degree, and +// letters, with diacritical marks, for many European languages. +// Unicode range 80-FF is called "Latin-1 Supplement." Exactly the same as ISO Latin-1. +// For UTF-8, an additional byte is inserted. +// Unicode UTF-8 +// ------- ----- +// 8x C2 8x Insert C2, keep original +// 9x C2 9x " +// Ax C2 Ax " +// Bx C2 Bx " +// Cx C3 8x Insert C3, subtract 40 from original +// Dx C3 9x " +// Ex C3 Ax " +// Fx C3 Bx " +// +// Can we use this knowledge to provide guidance on other ISO Latin-1 characters besides degree? +// Should we? +// Reference: https://www.fileformat.info/info/unicode/utf8test.htm + if ( ! A->g_quiet) { for (j=0; jg_msg_type, "Position", sizeof(A->g_msg_type)); + strlcpy (A->g_data_type_desc, "Position", sizeof(A->g_data_type_desc)); p = (struct aprs_ll_pos_s *)info; q = (struct aprs_compressed_pos_s *)info; @@ -742,8 +906,34 @@ static void aprs_ll_pos (decode_aprs_t *A, unsigned char *info, int ilen) /* In this case, we expect 7 byte "data extension" */ /* for the wind direction and speed. */ - strlcpy (A->g_msg_type, "Weather Report", sizeof(A->g_msg_type)); + strlcpy (A->g_data_type_desc, "Weather Report", sizeof(A->g_data_type_desc)); weather_data (A, p->comment, TRUE); +/* +Here is an interesting case. +The protocol spec states that a position report with symbol _ is a special case +and the information part must contain wxnow.txt format weather data. +But, here we see it being generated like a normal position report. + +N8VIM>BEACON,AB1OC-10*,WIDE2-1:!4240.85N/07133.99W_PHG72604/ Pepperell, MA. WX. 442.9+ PL100<0x0d> +Didn't find wind direction in form c999. +Didn't find wind speed in form s999. +Didn't find wind gust in form g999. +Didn't find temperature in form t999. +Weather Report, WEATHER Station (blue) +N 42 40.8500, W 071 33.9900 +, "PHG72604/ Pepperell, MA. WX. 442.9+ PL100" + +It seems, to me, that this is a violation of the protocol spec. +Then, immediately following, we have a positionless weather report in Ultimeter format. + +N8VIM>APN391,AB1OC-10*,WIDE2-1:$ULTW006F00CA01421C52275800008A00000102FA000F04A6000B002A<0x0d><0x0a> +Ultimeter, Kantronics KPC-3 rom versions +wind 6.9 mph, direction 284, temperature 32.2, barometer 29.75, humidity 76 + +aprs.fi merges these two together. Is that anywhere in the protocol spec or +just a heuristic added after noticing a pair of packets like this? +*/ + } else { /* Regular position report. */ @@ -761,7 +951,7 @@ static void aprs_ll_pos (decode_aprs_t *A, unsigned char *info, int ilen) /* compressed data so we don't expect a 7 byte "data */ /* extension" for them. */ - strlcpy (A->g_msg_type, "Weather Report", sizeof(A->g_msg_type)); + strlcpy (A->g_data_type_desc, "Weather Report", sizeof(A->g_data_type_desc)); weather_data (A, q->comment, FALSE); } else { @@ -831,7 +1021,7 @@ static void aprs_ll_pos_time (decode_aprs_t *A, unsigned char *info, int ilen) } *q; - strlcpy (A->g_msg_type, "Position with time", sizeof(A->g_msg_type)); + strlcpy (A->g_data_type_desc, "Position with time", sizeof(A->g_data_type_desc)); time_t ts = 0; @@ -850,7 +1040,7 @@ static void aprs_ll_pos_time (decode_aprs_t *A, unsigned char *info, int ilen) /* In this case, we expect 7 byte "data extension" */ /* for the wind direction and speed. */ - strlcpy (A->g_msg_type, "Weather Report", sizeof(A->g_msg_type)); + strlcpy (A->g_data_type_desc, "Weather Report", sizeof(A->g_data_type_desc)); weather_data (A, p->comment, TRUE); } else { @@ -871,7 +1061,7 @@ static void aprs_ll_pos_time (decode_aprs_t *A, unsigned char *info, int ilen) /* compressed data so we don't expect a 7 byte "data */ /* extension" for them. */ - strlcpy (A->g_msg_type, "Weather Report", sizeof(A->g_msg_type)); + strlcpy (A->g_data_type_desc, "Weather Report", sizeof(A->g_data_type_desc)); weather_data (A, q->comment, FALSE); } else { @@ -926,7 +1116,7 @@ static void aprs_raw_nmea (decode_aprs_t *A, unsigned char *info, int ilen) (void) dwgpsnmea_gprmc ((char*)info, A->g_quiet, &(A->g_lat), &(A->g_lon), &speed_knots, &(A->g_course)); A->g_speed_mph = DW_KNOTS_TO_MPH(speed_knots); - strlcpy (A->g_msg_type, "Raw GPS data", sizeof(A->g_msg_type)); + strlcpy (A->g_data_type_desc, "Raw GPS data", sizeof(A->g_data_type_desc)); } else if (strncmp((char*)info, "$GPGGA,", 7) == 0 || strncmp((char*)info, "$GNGGA,", 7) == 0) @@ -936,7 +1126,7 @@ static void aprs_raw_nmea (decode_aprs_t *A, unsigned char *info, int ilen) (void) dwgpsnmea_gpgga ((char*)info, A->g_quiet, &(A->g_lat), &(A->g_lon), &alt_meters, &num_sat); A->g_altitude_ft = DW_METERS_TO_FEET(alt_meters); - strlcpy (A->g_msg_type, "Raw GPS data", sizeof(A->g_msg_type)); + strlcpy (A->g_data_type_desc, "Raw GPS data", sizeof(A->g_data_type_desc)); } // TODO (low): add a few other sentence types. @@ -949,7 +1139,9 @@ static void aprs_raw_nmea (decode_aprs_t *A, unsigned char *info, int ilen) * * Function: aprs_mic_e * - * Purpose: Decode MIC-E (also Kenwood D7 & D700) packet. + * Purpose: Decode MIC-E (e.g. Kenwood D7 & D700) packet. + * This format is an overzelous quest to make the packet as short as possible. + * It uses non-printable characters and hacks wrapped in kludges. * * Inputs: info - Pointer to Information field. * ilen - Information field length. @@ -958,31 +1150,120 @@ static void aprs_raw_nmea (decode_aprs_t *A, unsigned char *info, int ilen) * * Description: * - * Destination Address Field - + * AX.25 Destination Address Field - * - * The 7-byte Destination Address field contains + * The 6-byte Destination Address field contains * the following encoded information: * - * * The 6 latitude digits. - * * A 3-bit Mic-E message identifier, specifying one of 7 Standard Mic-E - * Message Codes or one of 7 Custom Message Codes or an Emergency - * Message Code. - * * The North/South and West/East Indicators. - * * The Longitude Offset Indicator. - * * The generic APRS digipeater path code. - * + * Byte 1: Lat digit 1, message bit A + * Byte 2: Lat digit 2, message bit B + * Byte 3: Lat digit 3, message bit C + * Byte 4: Lat digit 4, N/S lat indicator + * Byte 5: Lat digit 5, Longitude offset + * Byte 6: Lat digit 6, W/E Long indicator + * * * "Although the destination address appears to be quite unconventional, it is * still a valid AX.25 address, consisting only of printable 7-bit ASCII values." * - * References: Mic-E TYPE CODES -- http://www.aprs.org/aprs12/mic-e-types.txt + * AX.25 Information Field - Starts with ' or ` + * + * Bytes 1,2,3: Longitude + * Bytes 4,5,6: Speed and Course + * Byte 6: Symbol code + * Byte 7: Symbol Table ID + * + * The rest of it is a complicated comment field which can hold various information + * and must be intrepreted in a particular order. At this point we look for any + * prefix and/or suffix to identify the equipment type. + * + * References: Mic-E TYPE CODES -- http://www.aprs.org/aprs12/mic-e-types.txt + * Mic-E TEST EXAMPLES -- http://www.aprs.org/aprs12/mic-e-examples.txt * - * This is up to date with the 24 Aug 16 version mentioning the TH-D74. + * Next, we have what Addedum 1.2 calls the "type byte." This prefix can be + * space Original MIC-E. + * > Kenwood HT. + * ] Kenwood Mobile. + * none. + * + * We also need to look at the last byte or two + * for a possible suffix to distinguish equipment types. Examples: + * >...... is D7 + * >......= is D72 + * >......^ is D74 + * + * For other brands, it gets worse. There might a 2 character suffix. + * The prefix indicates whether messaging-capable. Examples: + * `....._.% Yaesu FTM-400DR + * `......_) Yaesu FTM-100D + * `......_3 Yaesu FT5D + * + * '......|3 Byonics TinyTrack3 + * '......|4 Byonics TinyTrack4 + * + * Any prefix and suffix must be removed before futher processsing. + * + * Pick one: MIC-E Telemetry Data or "Status Text" (called a comment everywhere else). + * + * If the character after the symbol table id is "," (comma) or 0x1d, we have telemetry. + * (Is this obsoleted by the base-91 telemetry?) + * + * ` Two 2-character hexadecimal numbers. (Channels 1 & 3) + * ' Five 2-character hexadecimal numbers. + * + * Anything left over is a comment which can contain various types of information. + * + * If present, the MIC-E compressed altitude must be first. + * It is three base-91 characters followed by "}". + * Examples: "4T} "4T} ]"4T} + * + * We can also have frequency specification -- http://www.aprs.org/info/freqspec.txt + * + * Warning: Some Kenwood radios add CR at the end, in apparent violation of the spec. + * Watch out so it doesn't get included when looking for equipment type suffix. * * Mic-E TEST EXAMPLES -- http://www.aprs.org/aprs12/mic-e-examples.txt * - * Examples: `b9Z!4y>/>"4N}Paul's_TH-D7 + * Examples: Observed on the air. + * + * KB1HNZ-9>TSSP5P,W1IMD,WIDE1,KQ1L-8,N3LLO-3,WIDE2*:`b6,l}#>/]"48}449.225MHz<0xff><0xff><0xff><0xff><0xff><0xff><0xff><0xff><0xff><0xff><0xff><0xff><0xff><0xff><0xff><0xff><0xff><0xff><0xff><0xff><0xff><0xff><0xff><0xff><0xff><0xff><0xff><0xff>=<0x0d> + * + * ` b6, l}# >/ ] "48} 449.225MHz ...... = <0x0d> + * mic-e long. cs sym prefix alt. freq comment suffix must-ignore + * Kenwood D710 + *--------------- + * + * N1JDU-9>ECCU8Y,W1MHL*,WIDE2-1:'cZ<0x7f>l#H>/]Go fly a kite!<0x0d> + * + * ' cZ<0x7f> l#H >/ ] ..... <0x0d> + * mic-e long. cs sym prefix comment no-suffix must-ignore + * Kenwood D700 + *--------------- * - * TODO: Destination SSID can contain generic digipeater path. + * KC1HHO-7>T2PX5R,WA1PLE-4,WIDE1*,WIDE2-1:`c_snp(k/`"4B}official relay station NTS_(<0x0d> + * + * ` c_s np( k/ ` "4B} ....... _( <0x0d> + * mic-e long. cs sym prefix alt comment suffix must-ignore + * FT2D + *--------------- + * + * N1CMD-12>T3PQ1Y,KA1GJU-3,WIDE1,WA1PLE-4*:`cP#l!Fk/'"7H}|!%&-']|!w`&!|3 + * + * ` cP# l!F k/ ' "7H} |!%&-']| !w`&! |3 + * mic-e long. cs sym prefix alt base91telemetry DAO suffix + * TinyTrack3 + *--------------- + * + * W1STJ-3>T2UR4X,WA1PLE-4,WIDE1*,WIDE2-1:`c@&l#.-/`"5,}146.685MHz T100 -060 146.520 Simplex or Voice Alert_%<0x0d> + * + * ` c@& l#. -/ ` "5,} 146.685MHz T100 -060 .............. _% <0x0d> + * mic-e long. cs sym prefix alt frequency-specification comment suffix must-ignore + * FTM-400DR + *--------------- + * + * + * + * + * TODO: Destination SSID can contain generic digipeater path. (?) * * Bugs: Doesn't handle ambiguous position. "space" treated as zero. * Invalid data results in a message but latitude is not set to unknown. @@ -1022,7 +1303,7 @@ N1ZZN-9>T2SP0W:'c_Vm6hk/`"49}Byonics TinyTrack4|4 # The next group starts with metacharacter "T" which can be any of space > ] ` ' # But space is for original Mic-E, # > and ] are for Kenwood, -# so ` ' would probably be less ambigous choices but any appear to be valid. +# so ` ' would probably be less ambiguous choices but any appear to be valid. N1ZZN-9>T2SP0W:'c_Vm6hk/`"49}Hamhud\9 N1ZZN-9>T2SP0W:'c_Vm6hk/`"49}Argent/9 @@ -1103,7 +1384,7 @@ static void aprs_mic_e (decode_aprs_t *A, packet_t pp, unsigned char *info, int char sym_table_id; } *p; - char dest[10]; + char dest[12]; int ch; int n; int offset; @@ -1113,7 +1394,7 @@ static void aprs_mic_e (decode_aprs_t *A, packet_t pp, unsigned char *info, int 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_msg_type, "MIC-E", sizeof(A->g_msg_type)); + strlcpy (A->g_data_type_desc, "MIC-E", sizeof(A->g_data_type_desc)); p = (struct aprs_mic_e_s *)info; @@ -1130,7 +1411,7 @@ static void aprs_mic_e (decode_aprs_t *A, packet_t pp, unsigned char *info, int mic_e_digit(A, dest[5], 0, &std_msg, &cust_msg)) / 6000.0; -/* 4th character of desination indicates north / south. */ +/* 4th character of destination indicates north / south. */ if ((dest[3] >= '0' && dest[3] <= '9') || dest[3] == 'L') { /* South */ @@ -1172,7 +1453,7 @@ static void aprs_mic_e (decode_aprs_t *A, packet_t pp, unsigned char *info, int /* First character of information field is longitude in degrees. */ /* It is possible for the unprintable DEL character to occur here. */ -/* 5th character of desination indicates longitude offset of +100. */ +/* 5th character of destination indicates longitude offset of +100. */ /* Not quite that simple :-( */ ch = p->lon[0]; @@ -1261,7 +1542,7 @@ static void aprs_mic_e (decode_aprs_t *A, packet_t pp, unsigned char *info, int } } -/* 6th character of destintation indicates east / west. */ +/* 6th character of destination indicates east / west. */ /* * Example of apparently invalid encoding. 6th character missing. @@ -1360,19 +1641,29 @@ static void aprs_mic_e (decode_aprs_t *A, packet_t pp, unsigned char *info, int #define isT(c) ((c) == ' ' || (c) == '>' || (c) == ']' || (c) == '`' || (c) == '\'') -// Last updated Sept. 2016 for TH-D74A +// 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)) { - + +// "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; } @@ -1381,12 +1672,18 @@ static void aprs_mic_e (decode_aprs_t *A, packet_t pp, unsigned char *info, int 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++; } - else if (*pfirst == '`' ) { strlcpy (A->g_mfr, "Mic-Emsg", sizeof(A->g_mfr)); pfirst++; } +// ' should be used for trackers (not message capable). + 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; } else if (*pfirst == '\'' && *(plast-1) == '|' && *plast == '3') { strlcpy (A->g_mfr, "Byonics TinyTrack3", sizeof(A->g_mfr)); pfirst++; plast-=2; } @@ -1395,13 +1692,13 @@ static void aprs_mic_e (decode_aprs_t *A, packet_t pp, unsigned char *info, int 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, "McTrackr", sizeof(A->g_mfr)); pfirst++; } + 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, "OTHER", sizeof(A->g_mfr)); pfirst++; plast-=2; } + else if ( *(plast-1) == '~' ) { strlcpy (A->g_mfr, "Unknown OTHER", sizeof(A->g_mfr)); pfirst++; plast-=2; } } /* @@ -1454,11 +1751,11 @@ static void aprs_mic_e (decode_aprs_t *A, packet_t pp, unsigned char *info, int * Purpose: Decode "Message Format." * The word message is used loosely all over the place, but it has a very specific meaning here. * - * Inputs: info - Pointer to Information field. + * Inputs: info - Pointer to Information field. Be careful not to modify it here! * ilen - Information field length. - * quiet - supress error messages. + * quiet - suppress error messages. * - * Outputs: A->g_msg_type Text description for screen display. + * Outputs: A->g_data_type_desc Text description for screen display. * * A->g_addressee To whom is it addressed. * Could be a specific station, alias, bulletin, etc. @@ -1475,35 +1772,52 @@ static void aprs_mic_e (decode_aprs_t *A, packet_t pp, unsigned char *info, int * It's a lot more complicated with different types of addressees * and replies with acknowledgement or rejection. * - * There is even a special case for telemetry metadata. + * Is it an elegant generalization to lump all of these special cases + * together or was it a big mistake that will cause confusion and incorrect + * implementations? The decision to put telemetry metadata here is baffling. * * - * Cases: :xxxxxxxxx:PARM. Telemetry metadata, parameter name + * Cases: :BLNxxxxxx: ... Bulletin. + * :NWSxxxxxx: ... National Weather Service Bulletin. + * http://www.aprs.org/APRS-docs/WX.TXT + * :SKYxxxxxx: ... Need reference. + * :CWAxxxxxx: ... Need reference. + * :BOMxxxxxx: ... Australian version. + * + * :xxxxxxxxx:PARM. Telemetry metadata, parameter name * :xxxxxxxxx:UNIT. Telemetry metadata, unit/label - * :xxxxxxxxx:EQNS. Telemetry metadata, Equation Coefficents + * :xxxxxxxxx:EQNS. Telemetry metadata, Equation Coefficients * :xxxxxxxxx:BITS. Telemetry metadata, Bit Sense/Project Name * :xxxxxxxxx:? Directed Station Query - * :xxxxxxxxx:ack Message acknowledged (received) - * :xxxxxxxxx:rej Message rejected (unable to accept) + * :xxxxxxxxx:ackNNNN Message acknowledged (received) + * :xxxxxxxxx:rejNNNNN Message rejected (unable to accept) * * :xxxxxxxxx: ... Message with no message number. * (Text may not contain the { character because * it indicates beginning of optional message number.) - * :xxxxxxxxx: ... {num Message with message number. + * :xxxxxxxxx: ... {NNNNN Message with message number, 1 to 5 alphanumeric. + * :xxxxxxxxx: ... {mm} Message with new style message number. + * :xxxxxxxxx: ... {mm}aa Message with new style message number and ack. + * + * + * Reference: http://www.aprs.org/txt/messages101.txt + * http://www.aprs.org/aprs11/replyacks.txt <-- New (1999) adding ack to outgoing message. * *------------------------------------------------------------------*/ -static void aprs_message (decode_aprs_t *A, unsigned char *info, int ilen, int quiet) +static void aprs_message (decode_aprs_t *A, unsigned char *info, int ilen, int quiet) { struct aprs_message_s { char dti; /* : */ char addressee[9]; char colon; /* : */ - char message[73]; /* 0-67 characters for message */ - /* Optional { followed by 1-5 characters for message number */ + char message[256-1-9-1]; /* Officially up to 67 characters for message text. */ + /* Relaxing seemingly arbitrary restriction here; it doesn't need to fit on a punched card. */ + /* Wouldn't surprise me if others did not pay attention to the limit. */ + /* Optional '{' followed by 1-5 alphanumeric characters for message number */ - /* If the first chracter is '?' it is a Directed Station Query. */ + /* If the first character is '?' it is a Directed Station Query. */ } *p; char addressee[AX25_MAX_ADDR_LEN]; @@ -1511,7 +1825,7 @@ static void aprs_message (decode_aprs_t *A, unsigned char *info, int ilen, int q p = (struct aprs_message_s *)info; - strlcpy (A->g_msg_type, "APRS Message", sizeof(A->g_msg_type)); + strlcpy (A->g_data_type_desc, "APRS Message", sizeof(A->g_data_type_desc)); A->g_message_subtype = message_subtype_message; /* until found otherwise */ if (ilen < 11) { @@ -1526,7 +1840,8 @@ static void aprs_message (decode_aprs_t *A, unsigned char *info, int ilen, int q if (p->colon != ':') { if (! quiet) { text_color_set(DW_COLOR_ERROR); - dw_printf("APRS Message must begin with : 9 character addressee :\n"); + dw_printf("APRS Message must begin with ':' 9 character addressee ':'\n"); + dw_printf("Spaces must be added to shorter addressee to make 9 characters.\n"); } A->g_message_subtype = message_subtype_invalid; return; @@ -1541,8 +1856,96 @@ static void aprs_message (decode_aprs_t *A, unsigned char *info, int ilen, int q addressee[i--] = '\0'; } + // Anytone AT-D878UV 2 plus would pad out station name to 6 characters + // before appending the SSID. e.g. "AE7MK -5 " + + // Test cases. First is valid. Others should produce errors: + // + // cbeacon sendto=r0 delay=0:10 info=":AE7MK-5 :test0" + // cbeacon sendto=r0 delay=0:15 info=":AE7MK-5:test1" + // cbeacon sendto=r0 delay=0:20 info=":AE7MK -5 :test2" + // cbeacon sendto=r0 delay=0:25 info=":AE7 -5 :test3" + + static regex_t bad_addressee_re; /* Probably bad addressee. */ + static int first_time = 1; + + if (first_time) { + char emsg[100]; + int e = regcomp (&bad_addressee_re, "[A-Z0-9]+ +-[0-9]", REG_EXTENDED); + if (e) { + regerror (e, &bad_addressee_re, emsg, sizeof(emsg)); + dw_printf("%s:%d: %s\n", __FILE__, __LINE__, emsg); + } + first_time = 0; + } + +#define MAXMATCH_AT 2 + regmatch_t match[MAXMATCH_AT]; + + if (regexec (&bad_addressee_re, addressee, MAXMATCH_AT, match, 0) == 0) { + text_color_set(DW_COLOR_ERROR); + dw_printf("Malformed addressee with space between station name and SSID.\n"); + dw_printf("Please tell message sender this is invalid.\n"); + } + strlcpy (A->g_addressee, addressee, sizeof(A->g_addressee)); +/* + * Addressee starting with BLN or NWS is a bulletin. + */ + if (strlen(addressee) >= 3 && strncmp(addressee,"BLN",3) == 0) { + + // Interpret 3 cases of identifiers. + // BLN9 "general bulletin" has a single digit. + // BLNX "announcement" has a single uppercase letter. + // BLN9xxxxx "group bulletin" has single digit group id and group name up to 5 characters. + + if (strlen(addressee) == 4 && isdigit(addressee[3])) { + snprintf (A->g_data_type_desc, sizeof(A->g_data_type_desc), "General Bulletin with identifier \"%s\"", addressee+3); + } + else if (strlen(addressee) == 4 && isupper(addressee[3])) { + snprintf (A->g_data_type_desc, sizeof(A->g_data_type_desc), "Announcement with identifier \"%s\"", addressee+3); + } + if (strlen(addressee) >=5 && isdigit(addressee[3])) { + snprintf (A->g_data_type_desc, sizeof(A->g_data_type_desc), "Group Bulletin with identifier \"%c\", group name \"%s\"", addressee[3], addressee+4); + } + else { + // Not one of the official formats. + snprintf (A->g_data_type_desc, sizeof(A->g_data_type_desc), "Bulletin with identifier \"%s\"", addressee+3); + } + A->g_message_subtype = message_subtype_bulletin; + strlcpy (A->g_comment, p->message, sizeof(A->g_comment)); + } + + + // Weather bulletins have addressee starting with NWS, SKY, CWA, or BOM. + // The protocol spec and http://www.aprs.org/APRS-docs/WX.TXT state that + // the 3 letter prefix must be followed by a dash. + // However, https://www.aprs-is.net/WX/ also lists the underscore + // alternative for the compressed format. Xastir implements this. + + else if (strlen(addressee) >= 3 && strncmp(addressee,"NWS",3) == 0) { + + if (strlen(addressee) >=4 && addressee[3] == '-') { + snprintf (A->g_data_type_desc, sizeof(A->g_data_type_desc), "Weather bulletin with identifier \"%s\"", addressee+4); + } + else if (strlen(addressee) >=4 && addressee[3] == '_') { + snprintf (A->g_data_type_desc, sizeof(A->g_data_type_desc), "Compressed Weather bulletin with identifier \"%s\"", addressee+4); + } + else { + snprintf (A->g_data_type_desc, sizeof(A->g_data_type_desc), "Weather bulletin is missing - or _ after %.3s", addressee); + } + A->g_message_subtype = message_subtype_nws; + strlcpy (A->g_comment, p->message, sizeof(A->g_comment)); + } + + else if (strlen(addressee) >= 3 && (strncmp(addressee,"SKY",3) == 0 || strncmp(addressee,"CWA",3) == 0 || strncmp(addressee,"BOM",3) == 0)) { + // SKY... or CWA... https://www.aprs-is.net/WX/ + snprintf (A->g_data_type_desc, sizeof(A->g_data_type_desc), "Weather bulletin with identifier \"%s\"", addressee+4); + A->g_message_subtype = message_subtype_nws; + strlcpy (A->g_comment, p->message, sizeof(A->g_comment)); + } + /* * Special message formats contain telemetry metadata. @@ -1555,23 +1958,23 @@ static void aprs_message (decode_aprs_t *A, unsigned char *info, int ilen, int q * Why not use other characters after the "T" for metadata? */ - if (strncmp(p->message,"PARM.",5) == 0) { - snprintf (A->g_msg_type, sizeof(A->g_msg_type), "Telemetry Parameter Name Message for \"%s\"", addressee); + else if (strncmp(p->message,"PARM.",5) == 0) { + snprintf (A->g_data_type_desc, sizeof(A->g_data_type_desc), "Telemetry Parameter Name for \"%s\"", addressee); A->g_message_subtype = message_subtype_telem_parm; telemetry_name_message (addressee, p->message+5); } else if (strncmp(p->message,"UNIT.",5) == 0) { - snprintf (A->g_msg_type, sizeof(A->g_msg_type), "Telemetry Unit/Label Message for \"%s\"", addressee); + snprintf (A->g_data_type_desc, sizeof(A->g_data_type_desc), "Telemetry Unit/Label for \"%s\"", addressee); A->g_message_subtype = message_subtype_telem_unit; telemetry_unit_label_message (addressee, p->message+5); } else if (strncmp(p->message,"EQNS.",5) == 0) { - snprintf (A->g_msg_type, sizeof(A->g_msg_type), "Telemetry Equation Coefficents Message for \"%s\"", addressee); + snprintf (A->g_data_type_desc, sizeof(A->g_data_type_desc), "Telemetry Equation Coefficients for \"%s\"", addressee); A->g_message_subtype = message_subtype_telem_eqns; telemetry_coefficents_message (addressee, p->message+5, quiet); } else if (strncmp(p->message,"BITS.",5) == 0) { - snprintf (A->g_msg_type, sizeof(A->g_msg_type), "Telemetry Bit Sense/Project Name Message for \"%s\"", addressee); + snprintf (A->g_data_type_desc, sizeof(A->g_data_type_desc), "Telemetry Bit Sense/Project Name for \"%s\"", addressee); A->g_message_subtype = message_subtype_telem_bits; telemetry_bit_sense_message (addressee, p->message+5, quiet); } @@ -1582,7 +1985,7 @@ static void aprs_message (decode_aprs_t *A, unsigned char *info, int ilen, int q else if (p->message[0] == '?') { - strlcpy (A->g_msg_type, "Directed Station Query", sizeof(A->g_msg_type)); + strlcpy (A->g_data_type_desc, "Directed Station Query", sizeof(A->g_data_type_desc)); A->g_message_subtype = message_subtype_directed_query; aprs_directed_station_query (A, addressee, p->message+1, quiet); @@ -1590,30 +1993,128 @@ static void aprs_message (decode_aprs_t *A, unsigned char *info, int ilen, int q /* ack or rej? Message number is required for these. */ - else if (strncmp(p->message,"ack",3) == 0) { - strlcpy (A->g_message_number, p->message + 3, sizeof(A->g_message_number)); - snprintf (A->g_msg_type, sizeof(A->g_msg_type), "ACK message %s for \"%s\"", A->g_message_number, addressee); + else if (strncasecmp(p->message,"ack",3) == 0) { + if (strncmp(p->message,"ack",3) != 0) { + text_color_set(DW_COLOR_ERROR); + dw_printf("ERROR: \"%s\" must be lower case \"ack\"\n", p->message); + } + else { + strlcpy (A->g_message_number, p->message + 3, sizeof(A->g_message_number)); + if (strlen(A->g_message_number) == 0) { + text_color_set(DW_COLOR_ERROR); + dw_printf("ERROR: Message number is missing after \"ack\".\n"); + } + } + + // Xastir puts a carriage return on the end. + char *p = strchr(A->g_message_number, '\r'); + if (p != NULL) { + text_color_set(DW_COLOR_ERROR); + dw_printf("The APRS protocol specification says nothing about a possible carriage return after the\n"); + dw_printf("message id. Adding CR might prevent proper interoperability with with other applications.\n"); + *p = '\0'; + } + + if (strlen(A->g_message_number) >= 3 && A->g_message_number[2] == '}') A->g_message_number[2] = '\0'; + snprintf (A->g_data_type_desc, sizeof(A->g_data_type_desc), "\"%s\" ACKnowledged message number \"%s\" from \"%s\"", A->g_src, A->g_message_number, addressee); A->g_message_subtype = message_subtype_ack; } - else if (strncmp(p->message,"rej",3) == 0) { - strlcpy (A->g_message_number, p->message + 3, sizeof(A->g_message_number)); - snprintf (A->g_msg_type, sizeof(A->g_msg_type), "REJ message %s for \"%s\"", A->g_message_number, addressee); + else if (strncasecmp(p->message,"rej",3) == 0) { + if (strncmp(p->message,"rej",3) != 0) { + text_color_set(DW_COLOR_ERROR); + dw_printf("ERROR: \"%s\" must be lower case \"rej\"\n", p->message); + } + else { + strlcpy (A->g_message_number, p->message + 3, sizeof(A->g_message_number)); + if (strlen(A->g_message_number) == 0) { + text_color_set(DW_COLOR_ERROR); + dw_printf("ERROR: Message number is missing after \"rej\".\n"); + } + } + + // Xastir puts a carriage return on the end. + char *p = strchr(A->g_message_number, '\r'); + if (p != NULL) { + text_color_set(DW_COLOR_ERROR); + dw_printf("The APRS protocol specification says nothing about a possible carriage return after the\n"); + dw_printf("message id. Adding CR might prevent proper interoperability with with other applications.\n"); + *p = '\0'; + } + + if (strlen(A->g_message_number) >= 3 && A->g_message_number[2] == '}') A->g_message_number[2] = '\0'; + snprintf (A->g_data_type_desc, sizeof(A->g_data_type_desc), "\"%s\" REJected message number \"%s\" from \"%s\"", A->g_src, A->g_message_number, addressee); A->g_message_subtype = message_subtype_ack; } -/* message number is optional here. */ +// Message to a particular station or a bulletin. +// message number is optional here. +// Test cases. Wrap in third party too. +// A>B::WA1XYX-15:Howdy y'all +// A>B::WA1XYX-15:Howdy y'all{12345 +// A>B::WA1XYX-15:Howdy y'all{12} +// A>B::WA1XYX-15:Howdy y'all{12}34 +// A>B::WA1XYX-15:Howdy y'all{toolong +// X>Y:}A>B::WA1XYX-15:Howdy y'all +// X>Y:}A>B::WA1XYX-15:Howdy y'all{12345 +// X>Y:}A>B::WA1XYX-15:Howdy y'all{12} +// X>Y:}A>B::WA1XYX-15:Howdy y'all{12}34 +// X>Y:}A>B::WA1XYX-15:Howdy y'all{toolong else { + // Normal messaage case. Look for message number. char *pno = strchr(p->message, '{'); if (pno != NULL) { strlcpy (A->g_message_number, pno+1, sizeof(A->g_message_number)); + + // Xastir puts a carriage return on the end. + char *p = strchr(A->g_message_number, '\r'); + if (p != NULL) { + text_color_set(DW_COLOR_ERROR); + dw_printf("The APRS protocol specification says nothing about a possible carriage return after the\n"); + dw_printf("message id. Adding CR might prevent proper interoperability with with other applications.\n"); + *p = '\0'; + } + + int mlen = strlen(A->g_message_number); + if (mlen < 1 || mlen > 5) { + text_color_set(DW_COLOR_ERROR); + dw_printf("Message number \"%s\" has length outside range of 1 to 5.\n", A->g_message_number); + } + + // TODO: Complain if not alphanumeric. + + char ack[8] = ""; + + if (mlen >= 3 && A->g_message_number[2] == '}') { + // New (1999) style. + A->g_message_number[2] = '\0'; + strlcpy (ack, A->g_message_number + 3, sizeof(ack)); + } + + if (strlen(ack) > 0) { + // With ACK. Message number should be 2 characters. + snprintf (A->g_data_type_desc, sizeof(A->g_data_type_desc), "APRS Message, number \"%s\", from \"%s\" to \"%s\", with ACK for \"%s\"", A->g_message_number, A->g_src, addressee, ack); + } + else { + // Message number can be 1-5 characters. + snprintf (A->g_data_type_desc, sizeof(A->g_data_type_desc), "APRS Message, number \"%s\", from \"%s\" to \"%s\"", A->g_message_number, A->g_src, addressee); + } } - snprintf (A->g_msg_type, sizeof(A->g_msg_type), "APRS Message %s for \"%s\"", A->g_message_number, addressee); + else { + // No message number. + snprintf (A->g_data_type_desc, sizeof(A->g_data_type_desc), "APRS Message, with no number, from \"%s\" to \"%s\"", A->g_src, addressee); + } + A->g_message_subtype = message_subtype_message; /* No location so don't use process_comment () */ strlcpy (A->g_comment, p->message, sizeof(A->g_comment)); + // Remove message number when displaying message text. + pno = strchr(A->g_comment, '{'); + if (pno != NULL) { + *pno = '\0'; + } } } @@ -1685,11 +2186,11 @@ static void aprs_object (decode_aprs_t *A, unsigned char *info, int ilen) } if (p->live_killed == '*') - strlcpy (A->g_msg_type, "Object", sizeof(A->g_msg_type)); + strlcpy (A->g_data_type_desc, "Object", sizeof(A->g_data_type_desc)); else if (p->live_killed == '_') - strlcpy (A->g_msg_type, "Killed Object", sizeof(A->g_msg_type)); + strlcpy (A->g_data_type_desc, "Killed Object", sizeof(A->g_data_type_desc)); else - strlcpy (A->g_msg_type, "Object - invalid live/killed", sizeof(A->g_msg_type)); + strlcpy (A->g_data_type_desc, "Object - invalid live/killed", sizeof(A->g_data_type_desc)); ts = get_timestamp (A, p->time_stamp); @@ -1702,7 +2203,7 @@ static void aprs_object (decode_aprs_t *A, unsigned char *info, int ilen) /* In this case, we expect 7 byte "data extension" */ /* for the wind direction and speed. */ - strlcpy (A->g_msg_type, "Weather Report with Object", sizeof(A->g_msg_type)); + strlcpy (A->g_data_type_desc, "Weather Report with Object", sizeof(A->g_data_type_desc)); weather_data (A, p->comment, TRUE); } else { @@ -1721,7 +2222,7 @@ static void aprs_object (decode_aprs_t *A, unsigned char *info, int ilen) /* of weather report and object with compressed */ /* position. */ - strlcpy (A->g_msg_type, "Weather Report with Object", sizeof(A->g_msg_type)); + strlcpy (A->g_data_type_desc, "Weather Report with Object", sizeof(A->g_data_type_desc)); weather_data (A, q->comment, FALSE); } else { @@ -1802,15 +2303,15 @@ static void aprs_item (decode_aprs_t *A, unsigned char *info, int ilen) } if (p->name[i] == '!') - strlcpy (A->g_msg_type, "Item", sizeof(A->g_msg_type)); + strlcpy (A->g_data_type_desc, "Item", sizeof(A->g_data_type_desc)); else if (p->name[i] == '_') - strlcpy (A->g_msg_type, "Killed Item", sizeof(A->g_msg_type)); + strlcpy (A->g_data_type_desc, "Killed Item", sizeof(A->g_data_type_desc)); else { if ( ! A->g_quiet) { text_color_set(DW_COLOR_ERROR); dw_printf("Item name too long or not followed by ! or _.\n"); } - strlcpy (A->g_msg_type, "Object - invalid live/killed", sizeof(A->g_msg_type)); + strlcpy (A->g_data_type_desc, "Object - invalid live/killed", sizeof(A->g_data_type_desc)); } ppos = p->name + i + 1; @@ -1854,7 +2355,7 @@ static void aprs_item (decode_aprs_t *A, unsigned char *info, int ilen) static void aprs_station_capabilities (decode_aprs_t *A, char *info, int ilen) { - strlcpy (A->g_msg_type, "Station Capabilities", sizeof(A->g_msg_type)); + strlcpy (A->g_data_type_desc, "Station Capabilities", sizeof(A->g_data_type_desc)); // process_comment() not applicable here because it // extracts information found in certain formats. @@ -1941,7 +2442,7 @@ static void aprs_status_report (decode_aprs_t *A, char *info, int ilen) } *ps; - strlcpy (A->g_msg_type, "Status Report", sizeof(A->g_msg_type)); + strlcpy (A->g_data_type_desc, "Status Report", sizeof(A->g_data_type_desc)); pt = (struct aprs_status_time_s *)info; pm4 = (struct aprs_status_m4_s *)info; @@ -2113,6 +2614,20 @@ static void aprs_status_report (decode_aprs_t *A, char *info, int ilen) * *------------------------------------------------------------------*/ +/* +https://groups.io/g/direwolf/topic/95961245#7357 + +What APRS queries should DireWolf respond to? Well, it should be configurable whether it responds to queries at all, in case some other application is using DireWolf as a dumb TNC (KISS or AGWPE style) and wants to handle the queries itself. + +Assuming query responding is enabled, the following broadcast queries should be supported (if the corresponding data is configured in DireWolf): + +?APRS (I am an APRS station) +?IGATE (I am operating as a I-gate) +?WX (I am providing local weather data in my beacon) + +*/ + + static void aprs_general_query (decode_aprs_t *A, char *info, int ilen, int quiet) { char *q2; @@ -2122,7 +2637,7 @@ static void aprs_general_query (decode_aprs_t *A, char *info, int ilen, int quie double lat, lon; float radius; - strlcpy (A->g_msg_type, "General Query", sizeof(A->g_msg_type)); + strlcpy (A->g_data_type_desc, "General Query", sizeof(A->g_data_type_desc)); /* * First make a copy because we will modify it while parsing it. @@ -2265,6 +2780,28 @@ static void aprs_general_query (decode_aprs_t *A, char *info, int ilen, int quie * *------------------------------------------------------------------*/ +/* +https://groups.io/g/direwolf/topic/95961245#7357 + +The following directed queries (sent as bodies of APRS text messages) would also be useful (if corresponding data configured): + +?APRSP (force my current beacon) +?APRST and ?PING (trace my path to requestor) +?APRSD (all stations directly heard [no digipeat hops] by local station) +?APRSO (any Objects/Items originated by this station) +?APRSH (how often or how many times the specified 3rd station was heard by the queried station) +?APRSS (immediately send the Status message if configured) (can DireWolf do Status messages?) + +Lynn KJ4ERJ and I have implemented a non-standard query which might be useful: + +?VER (send the human-readable software version of the queried station) + +Hope this is useful. It's just my $.02. + +Andrew, KA2DDO +author of YAAC +*/ + static void aprs_directed_station_query (decode_aprs_t *A, char *addressee, char *query, int quiet) { //char query_type[20]; /* Does the query type always need to be exactly 5 characters? */ @@ -2304,7 +2841,7 @@ static void aprs_directed_station_query (decode_aprs_t *A, char *addressee, char static void aprs_telemetry (decode_aprs_t *A, char *info, int ilen, int quiet) { - strlcpy (A->g_msg_type, "Telemetry", sizeof(A->g_msg_type)); + strlcpy (A->g_data_type_desc, "Telemetry", sizeof(A->g_data_type_desc)); telemetry_data_original (A->g_src, info, quiet, A->g_telemetry, sizeof(A->g_telemetry), A->g_comment, sizeof(A->g_comment)); @@ -2328,10 +2865,12 @@ static void aprs_telemetry (decode_aprs_t *A, char *info, int ilen, int quiet) static void aprs_user_defined (decode_aprs_t *A, char *info, int ilen) { - if (strncmp(info, "{tt", 3) == 0) { // Historical. Should probably use DT. + if (strncmp(info, "{tt", 3) == 0 || // Historical. + strncmp(info, "{DT", 3) == 0) { // Official after registering {D* aprs_raw_touch_tone (A, info, ilen); } - else if (strncmp(info, "{mc", 3) == 0) { // Historical. Should probably use DM. + else if (strncmp(info, "{mc", 3) == 0 || // Historical. + strncmp(info, "{DM", 3) == 0) { // Official after registering {D* aprs_morse_code (A, info, ilen); } else if (info[0] == '{' && info[1] == USER_DEF_USER_ID && info[2] == USER_DEF_TYPE_AIS) { @@ -2339,7 +2878,7 @@ static void aprs_user_defined (decode_aprs_t *A, char *info, int ilen) float knots, course; float alt_meters; - ais_parse (info+3, 0, A->g_msg_type, sizeof(A->g_msg_type), A->g_name, sizeof(A->g_name), + ais_parse (info+3, 0, A->g_data_type_desc, sizeof(A->g_data_type_desc), A->g_name, sizeof(A->g_name), &lat, &lon, &knots, &course, &alt_meters, &(A->g_symbol_table), &(A->g_symbol_code), A->g_comment, sizeof(A->g_comment)); @@ -2351,10 +2890,10 @@ static void aprs_user_defined (decode_aprs_t *A, char *info, int ilen) strcpy (A->g_mfr, ""); } else if (strncmp(info, "{{", 2) == 0) { - snprintf (A->g_msg_type, sizeof(A->g_msg_type), "User-Defined Experimental"); + snprintf (A->g_data_type_desc, sizeof(A->g_data_type_desc), "User-Defined Experimental"); } else { - snprintf (A->g_msg_type, sizeof(A->g_msg_type), "User-Defined Data"); + snprintf (A->g_data_type_desc, sizeof(A->g_data_type_desc), "User-Defined Data"); } } /* end aprs_user_defined */ @@ -2379,7 +2918,7 @@ static void aprs_user_defined (decode_aprs_t *A, char *info, int ilen) static void aprs_raw_touch_tone (decode_aprs_t *A, char *info, int ilen) { - strlcpy (A->g_msg_type, "Raw Touch Tone Data", sizeof(A->g_msg_type)); + strlcpy (A->g_data_type_desc, "Raw Touch Tone Data", sizeof(A->g_data_type_desc)); /* Just copy the info field without the message type. */ @@ -2410,7 +2949,7 @@ static void aprs_raw_touch_tone (decode_aprs_t *A, char *info, int ilen) static void aprs_morse_code (decode_aprs_t *A, char *info, int ilen) { - strlcpy (A->g_msg_type, "Morse Code Data", sizeof(A->g_msg_type)); + strlcpy (A->g_data_type_desc, "Morse Code Data", sizeof(A->g_data_type_desc)); /* Just copy the info field without the message type. */ @@ -2450,7 +2989,7 @@ static void aprs_positionless_weather_report (decode_aprs_t *A, unsigned char *i } *p; - strlcpy (A->g_msg_type, "Positionless Weather Report", sizeof(A->g_msg_type)); + strlcpy (A->g_data_type_desc, "Positionless Weather Report", sizeof(A->g_data_type_desc)); //time_t ts = 0; @@ -2855,7 +3394,7 @@ static void aprs_ultimeter (decode_aprs_t *A, char *info, int ilen) int n; - strlcpy (A->g_msg_type, "Ultimeter", sizeof(A->g_msg_type)); + strlcpy (A->g_data_type_desc, "Ultimeter", sizeof(A->g_data_type_desc)); if (*info == '$') { @@ -2934,31 +3473,6 @@ static void aprs_ultimeter (decode_aprs_t *A, char *info, int ilen) } /* end aprs_ultimeter */ -/*------------------------------------------------------------------ - * - * Function: third_party_header - * - * Purpose: Decode packet from a third party network. - * - * Inputs: info - Pointer to Information field. - * ilen - Information field length. - * - * Outputs: A->g_comment - * - * Description: - * - *------------------------------------------------------------------*/ - -static void third_party_header (decode_aprs_t *A, char *info, int ilen) -{ - - strlcpy (A->g_msg_type, "Third Party Header", sizeof(A->g_msg_type)); - - /* more later? */ - -} /* end third_party_header */ - - /*------------------------------------------------------------------ * @@ -3494,6 +4008,8 @@ time_t get_timestamp (decode_aprs_t *A, char *p) time_t ts; ts = time(NULL); + // FIXME: use gmtime_r instead. + // Besides not being thread safe, gmtime could possibly return null. ptm = gmtime(&ts); pdhm = (void *)p; @@ -3563,9 +4079,14 @@ time_t get_timestamp (decode_aprs_t *A, char *p) * It is composed of: * a pair of letters in range A to R. * a pair of digits in range of 0 to 9. - * a pair of letters in range of A to X. + * an optional pair of letters in range of A to X. * - * The APRS spec says that all letters must be transmitted in upper case. + * The spec says: + * "All letters must be transmitted in upper case. + * Letters may be received in upper case or lower case." + * + * Typically the second set of letters is written in lower case. + * An earlier version incorrectly produced an error if lower case found. * * * Examples from APRS spec: @@ -3586,25 +4107,10 @@ int get_maidenhead (decode_aprs_t *A, char *p) /* We have 4 characters matching the rule. */ - if (islower(p[0]) || islower(p[1])) { - if ( ! A->g_quiet) { - text_color_set(DW_COLOR_ERROR); - dw_printf("Warning: Lower case letter in Maidenhead locator. Specification requires upper case.\n"); - } - } - if (toupper(p[4]) >= 'A' && toupper(p[4]) <= 'X' && toupper(p[5]) >= 'A' && toupper(p[5]) <= 'X') { /* We have 6 characters matching the rule. */ - - if (islower(p[4]) || islower(p[5])) { - if ( ! A->g_quiet) { - text_color_set(DW_COLOR_ERROR); - dw_printf("Warning: Lower case letter in Maidenhead locator. Specification requires upper case.\n"); - } - } - return 6; } @@ -3706,6 +4212,9 @@ static int data_extension_comment (decode_aprs_t *A, char *pdext) strlcpy (A->g_directivity, dir[pdext[6]-'0'], sizeof(A->g_directivity)); } +// TODO: look for another 0-9 A-Z followed by a / +// http://www.aprs.org/aprs12/probes.txt + process_comment (A, pdext+7, -1); return 1; } @@ -3930,11 +4439,8 @@ static void decode_tocall (decode_aprs_t *A, char *dest) * models before getting to the more generic APY. */ -#if defined(__WIN32__) || defined(__OpenBSD__) || defined(__FreeBSD__) || defined(__APPLE__) qsort (tocalls, num_tocalls, sizeof(struct tocalls_s), tocall_cmp); -#else - qsort (tocalls, num_tocalls, sizeof(struct tocalls_s), (__compar_fn_t)tocall_cmp); -#endif + } else { if ( ! A->g_quiet) { @@ -4034,7 +4540,7 @@ static void substr_se (char *dest, const char *src, int start, int endp1) * * !DAO! APRS precision and Datum option. * - * Protocol reference, end of chaper 6. + * Protocol reference, end of chapter 6. * * /A=123456 Altitude * @@ -4074,7 +4580,7 @@ static void substr_se (char *dest, const char *src, int start, int endp1) * This same thing has been observed from others and is intermittent. * * * AGW Tracker can send UTF-16 if an option is selected. This can introduce nul bytes. - * This is wrong. It should be using UTF-8 and I'm not going to accomodate it here. + * This is wrong. It should be using UTF-8 and I'm not going to accommodate it here. * * * The digipeater and IGate functions should pass along anything exactly the @@ -4130,6 +4636,7 @@ static void process_comment (decode_aprs_t *A, char *pstart, int clen) static regex_t base91_tel_re; /* Base 91 compressed telemetry data. */ + int e; char emsg[100]; #define MAXMATCH 4 @@ -4219,9 +4726,7 @@ static void process_comment (decode_aprs_t *A, char *pstart, int clen) dw_printf("%s:%d: %s\n", __FILE__, __LINE__, emsg); } -// TODO: Would like to restrict to even length something like this: ([!-{][!-{]){2,7} - - e = regcomp (&base91_tel_re, "\\|([!-{]{4,14})\\|", REG_EXTENDED); + e = regcomp (&base91_tel_re, "\\|(([!-{][!-{]){2,7})\\|", REG_EXTENDED); if (e) { regerror (e, &base91_tel_re, emsg, sizeof(emsg)); dw_printf("%s:%d: %s\n", __FILE__, __LINE__, emsg); @@ -4241,7 +4746,7 @@ static void process_comment (decode_aprs_t *A, char *pstart, int clen) /* * Watch out for buffer overflow. * KG6AZZ reports that there is a local digipeater that seems to - * malfunction ocassionally. It corrupts the packet, as it is + * malfunction occasionally. It corrupts the packet, as it is * digipeated, causing the comment to be hundreds of characters long. */ @@ -4419,20 +4924,20 @@ static void process_comment (decode_aprs_t *A, char *pstart, int clen) /* * Telemetry data, in base 91 compressed format appears as 2 to 7 pairs - * of base 91 digits, surrounded by | at start and end. + * of base 91 digits, ! thru {, surrounded by | at start and end. */ if (regexec (&base91_tel_re, A->g_comment, MAXMATCH, match, 0) == 0) { - char tdata[30]; /* Should be 4 to 14 characters. */ + char tdata[30]; /* Should be even number of 4 to 14 characters. */ - //dw_printf("compressed telemetry start=%d, end=%d\n", (int)(match[0].rm_so), (int)(match[0].rm_eo)); + //dw_printf("compressed telemetry start=%d, end=%d\n", (int)(match[0].rm_so), (int)(match[0].rm_eo)); substr_se (tdata, A->g_comment, match[1].rm_so, match[1].rm_eo); - //dw_printf("compressed telemetry data = \"%s\"\n", tdata); + //dw_printf("compressed telemetry data = \"%s\"\n", tdata); telemetry_data_base91 (A->g_src, tdata, A->g_telemetry, sizeof(A->g_telemetry)); @@ -4448,8 +4953,10 @@ static void process_comment (decode_aprs_t *A, char *pstart, int clen) * This would not make sense to use this with a compressed location which * already has much greater resolution. * - * It surprized me to see this in a MIC-E message. + * It surprised me to see this in a MIC-E message. * MIC-E has resolution of .01 minute so it would make sense to have it as an option. + * We also find an example in http://www.aprs.org/aprs12/mic-e-examples.txt + * 'abc123R/'123}FFF.FFFMHztext.../A=123456...!DAO! Mv */ if (regexec (&dao_re, A->g_comment, MAXMATCH, match, 0) == 0) @@ -4459,7 +4966,7 @@ static void process_comment (decode_aprs_t *A, char *pstart, int clen) int a = A->g_comment[match[0].rm_so+2]; int o = A->g_comment[match[0].rm_so+3]; - //dw_printf("start=%d, end=%d\n", (int)(match[0].rm_so), (int)(match[0].rm_eo)); + //dw_printf("DAO start=%d, end=%d\n", (int)(match[0].rm_so), (int)(match[0].rm_eo)); /* @@ -4511,7 +5018,7 @@ static void process_comment (decode_aprs_t *A, char *pstart, int clen) */ /* - * Here is an interesting case. + * Here are a couple situations where it is seen. * * W8SAT-1>T2UV0P:`qC<0x1f>l!Xu\'"69}WMNI EDS Response Unit #1|+/%0'n|!w:X!|3 * @@ -4528,13 +5035,26 @@ static void process_comment (decode_aprs_t *A, char *pstart, int clen) * Comment earlier points out that MIC-E format has resolution of 0.01 minute, * same as non-compressed format, so the DAO does work out, after thinking * about it for a while. + * We also find a MIC-E example with !DAO! here: http://www.aprs.org/aprs12/mic-e-examples.txt + * + * Another one: + * + * KS4FUN-12>3X0PRU,W6CX-3,BKELEY,WIDE2*:`2^=l!<0x1c>+/'"48}MT-RTG|%B%p'a|!wqR!|3 + * + * MIC-E, Red Cross, Special + * N 38 00.2588, W 122 06.3354 + * 0 MPH, course 100, alt 108 ft + * MT-RTG comment + * |%B%p'a| Seq=397, A1=443, A2=610 + * !wqR! DAO + * |3 Byonics TinyTrack3 + * */ /* * The spec appears to be wrong. It says '}' is the maximum value when it should be '{'. */ - if (isdigit91(a)) { A->g_lat += (a - B91_MIN) * 1.1 / 600000.0 * sign(A->g_lat); } @@ -4832,7 +5352,9 @@ int main (int argc, char *argv[]) /* Try to process it. */ text_color_set(DW_COLOR_REC); - dw_printf("\n%s\n", stuff); + dw_printf("\n"); + ax25_safe_print (stuff, -1, 0); + dw_printf("\n"); // Do we have monitor format, KISS, or AX.25 frame? @@ -4904,7 +5426,7 @@ int main (int argc, char *argv[]) hex_dump (kiss_frame, kiss_len); // Put FEND at end to keep kiss_unwrap happy. - // Having one at the begining is optional. + // Having one at the beginning is optional. kiss_frame[kiss_len++] = FEND; @@ -4941,7 +5463,7 @@ int main (int argc, char *argv[]) ax25_safe_print ((char *)pinfo, info_len, 1); // Display non-ASCII to hexadecimal. dw_printf ("\n"); - decode_aprs (&A, pp, 0); // Extract information into structure. + decode_aprs (&A, pp, 0, NULL); // Extract information into structure. decode_aprs_print (&A); // Now print it in human readable format. @@ -4962,7 +5484,7 @@ int main (int argc, char *argv[]) if (pp != NULL) { decode_aprs_t A; - decode_aprs (&A, pp, 0); // Extract information into structure. + decode_aprs (&A, pp, 0, NULL); // Extract information into structure. decode_aprs_print (&A); // Now print it in human readable format. diff --git a/src/decode_aprs.h b/src/decode_aprs.h index cdbb1678..94a9fd6b 100644 --- a/src/decode_aprs.h +++ b/src/decode_aprs.h @@ -24,11 +24,12 @@ typedef struct decode_aprs_s { int g_quiet; /* Suppress error messages when decoding. */ - char g_src[AX25_MAX_ADDR_LEN]; + char g_src[AX25_MAX_ADDR_LEN]; // In the case of a packet encapsulated by a 3rd party + // header, this is the encapsulated source. - char g_msg_type[60]; /* APRS data type. Telemetry descriptions get pretty long. */ - /* Putting msg in the name was a poor choice because */ - /* "message" has a specific meaning. Rename it someday. */ + char g_dest[AX25_MAX_ADDR_LEN]; + + char g_data_type_desc[100]; /* APRS data type description. Telemetry descriptions get pretty long. */ char g_symbol_table; /* The Symbol Table Identifier character selects one */ /* of the two Symbol Tables, or it may be used as */ @@ -66,10 +67,30 @@ typedef struct decode_aprs_s { /* Also for Directed Station Query which is a */ /* special case of message. */ + // This is so pfilter.c:filt_t does not need to duplicate the same work. + + int g_has_thirdparty_header; + enum packet_type_e { + packet_type_none=0, + packet_type_position, + packet_type_weather, + packet_type_object, + packet_type_item, + packet_type_message, + packet_type_query, + packet_type_capabilities, + packet_type_status, + packet_type_telemetry, + packet_type_userdefined, + packet_type_nws + } g_packet_type; + enum message_subtype_e { message_subtype_invalid = 0, message_subtype_message, message_subtype_ack, message_subtype_rej, + message_subtype_bulletin, + message_subtype_nws, message_subtype_telem_parm, message_subtype_telem_unit, message_subtype_telem_eqns, @@ -77,23 +98,31 @@ typedef struct decode_aprs_s { message_subtype_directed_query } g_message_subtype; /* Various cases of the overloaded "message." */ - char g_message_number[8]; /* Message number. Should be 1 - 5 characters if used. */ + char g_message_number[12]; /* Message number. Should be 1 - 5 alphanumeric characters if used. */ + /* Addendum 1.1 has new format {mm} or {mm}aa with only two */ + /* characters for message number and an ack riding piggyback. */ float g_speed_mph; /* Speed in MPH. */ + /* The APRS transmission uses knots so watch out for */ + /* conversions when sending and receiving APRS packets. */ float g_course; /* 0 = North, 90 = East, etc. */ int g_power; /* Transmitter power in watts. */ int g_height; /* Antenna height above average terrain, feet. */ + // TODO: rename to g_height_ft - int g_gain; /* Antenna gain in dB. */ + int g_gain; /* Antenna gain in dBi. */ char g_directivity[12]; /* Direction of max signal strength */ float g_range; /* Precomputed radio range in miles. */ float g_altitude_ft; /* Feet above median sea level. */ + /* I used feet here because the APRS specification */ + /* has units of feet for altitude. Meters would be */ + /* more natural to the other 96% of the world. */ char g_mfr[80]; /* Manufacturer or application. */ @@ -135,9 +164,9 @@ typedef struct decode_aprs_s { -extern void decode_aprs (decode_aprs_t *A, packet_t pp, int quiet); +extern void decode_aprs (decode_aprs_t *A, packet_t pp, int quiet, char *third_party_src); extern void decode_aprs_print (decode_aprs_t *A); -#endif \ No newline at end of file +#endif diff --git a/src/demod.c b/src/demod.c index 281367bc..cc522271 100644 --- a/src/demod.c +++ b/src/demod.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, 2019 John Langner, WB2OSZ +// Copyright (C) 2011, 2012, 2013, 2014, 2015, 2016, 2019, 2021 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 @@ -60,10 +60,6 @@ static struct audio_s *save_audio_config_p; -// TODO: temp experiment. - - -static int zerostuff = 1; // temp experiment. // Current state of all the decoders. @@ -106,7 +102,7 @@ int demod_init (struct audio_s *pa) for (chan = 0; chan < MAX_CHANS; chan++) { - if (save_audio_config_p->achan[chan].medium == MEDIUM_RADIO) { + if (save_audio_config_p->chan_medium[chan] == MEDIUM_RADIO) { char *p; char just_letters[16]; @@ -198,46 +194,57 @@ int demod_init (struct audio_s *pa) assert (num_letters == (int)(strlen(just_letters))); /* - * Pick a good default demodulator if none specified. + * Pick a good default demodulator if none specified. + * Previously, we had "D" optimized for 300 bps. + * Gone in 1.7 so it is always "A+". */ if (num_letters == 0) { + strlcpy (just_letters, "A", sizeof(just_letters)); + num_letters = strlen(just_letters); - if (save_audio_config_p->achan[chan].baud < 600) { - - /* This has been optimized for 300 baud. */ - - strlcpy (just_letters, "D", sizeof(just_letters)); + if (have_plus != -1) have_plus = 1; // Add as default for version 1.2 + // If not explicitly turned off. + } - } - else { +/* + * Special case for ARM. + * The higher end ARM chips have loads of power but many people + * are using a single core Pi Zero or similar. + * (I'm still using a model 1 for my digipeater/IGate!) + * Decreasing CPU requirement has a negligible impact on decoding performance. + * + * atest -PA- 01_Track_1.wav --> 1002 packets decoded. + * atest -PA- -D3 01_Track_1.wav --> 997 packets decoded. + * + * Someone concerned about 1/2 of one percent difference can add "-D 1" + */ #if __arm__ - /* We probably don't have a lot of CPU power available. */ - /* Previously we would use F if possible otherwise fall back to A. */ - - /* In version 1.2, new default is E+ /3. */ - strlcpy (just_letters, "E", sizeof(just_letters)); // version 1.2 now E. - if (have_plus != -1) have_plus = 1; // Add as default for version 1.2 - // If not explicitly turned off. - if (save_audio_config_p->achan[chan].decimate == 0) { - if (save_audio_config_p->adev[ACHAN2ADEV(chan)].samples_per_sec > 40000) { - save_audio_config_p->achan[chan].decimate = 3; - } - } -#else - strlcpy (just_letters, "E", sizeof(just_letters)); // version 1.2 changed C to E. - if (have_plus != -1) have_plus = 1; // Add as default for version 1.2 - // If not explicitly turned off. -#endif + if (save_audio_config_p->achan[chan].decimate == 0) { + if (save_audio_config_p->adev[ACHAN2ADEV(chan)].samples_per_sec > 40000) { + save_audio_config_p->achan[chan].decimate = 3; } - num_letters = 1; } +#endif + +/* + * Number of filter taps is proportional to number of audio samples in a "symbol" duration. + * These can get extremely large for low speeds, e.g. 300 baud. + * In this case, increase the decimation ration. Crude approximation. Could be improved. + */ + if (save_audio_config_p->achan[chan].decimate == 0 && + save_audio_config_p->adev[ACHAN2ADEV(chan)].samples_per_sec > 40000 && + save_audio_config_p->achan[chan].baud < 600) { + // Avoid enormous number of filter taps. + + save_audio_config_p->achan[chan].decimate = 3; + } - assert (num_letters == (int)(strlen(just_letters))); /* * Put it back together again. */ + assert (num_letters == (int)(strlen(just_letters))); /* At this point, have_plus can have 3 values: */ /* 1 = turned on, either explicitly or by applied default */ @@ -286,7 +293,7 @@ int demod_init (struct audio_s *pa) if (save_audio_config_p->achan[chan].decimate == 0) { save_audio_config_p->achan[chan].decimate = 1; - if (strchr (just_letters, 'D') != NULL && save_audio_config_p->adev[ACHAN2ADEV(chan)].samples_per_sec > 40000) { + if (strchr (just_letters, 'B') != NULL && save_audio_config_p->adev[ACHAN2ADEV(chan)].samples_per_sec > 40000) { save_audio_config_p->achan[chan].decimate = 3; } } @@ -492,7 +499,7 @@ int demod_init (struct audio_s *pa) // In versions 1.4 and 1.5, V.26 "Alternative A" was used. // years later, I discover that the MFJ-2400 used "Alternative B." // It looks like the other two manufacturers use the same but we - // can't be sure until we find one for compatbility testing. + // can't be sure until we find one for compatibility testing. // In version 1.6 we add a choice for the user. // If neither one was explicitly specified, print a message and take @@ -503,8 +510,8 @@ int demod_init (struct audio_s *pa) text_color_set(DW_COLOR_ERROR); dw_printf ("Two incompatible versions of 2400 bps QPSK are now available.\n"); - dw_printf ("For compatbility with direwolf <= 1.5, use 'V26A' modem option in config file.\n"); - dw_printf ("For compatbility MFJ-2400 use 'V26B' modem option in config file.\n"); + dw_printf ("For compatibility with direwolf <= 1.5, use 'V26A' modem option in config file.\n"); + dw_printf ("For compatibility MFJ-2400 use 'V26B' modem option in config file.\n"); dw_printf ("Command line options -j and -J can be used for channel 0.\n"); dw_printf ("For more information, read the Dire Wolf User Guide and\n"); dw_printf ("2400-4800-PSK-for-APRS-Packet-Radio.pdf.\n"); @@ -665,12 +672,6 @@ int demod_init (struct audio_s *pa) strlcpy (save_audio_config_p->achan[chan].profiles, "+", sizeof(save_audio_config_p->achan[chan].profiles)); } - -#ifdef TUNE_ZEROSTUFF - zerostuff = TUNE_ZEROSTUFF; -#endif - - /* * We need a minimum number of audio samples per bit time for good performance. * Easier to check here because demod_9600_init might have an adjusted sample rate. @@ -685,26 +686,32 @@ int demod_init (struct audio_s *pa) if (save_audio_config_p->achan[chan].upsample == 0) { - if (ratio < 5) { + if (ratio < 4) { - // example: 44100 / 9600 is 4.59 - // Big improvement with x2. - // x4 seems to work the best. - // The other parameters are not as touchy. - // Might reduce on ARM if it takes too much CPU power. + // This is extreme. + // No one should be using a sample rate this low but + // amazingly a recording with 22050 rate can be decoded. + // 3 and 4 are the same. Need more tests. save_audio_config_p->achan[chan].upsample = 4; } + else if (ratio < 5) { + + // example: 44100 / 9600 is 4.59 + // 3 is slightly better than 2 or 4. + + save_audio_config_p->achan[chan].upsample = 3; + } else if (ratio < 10) { - // 48000 / 9600 is 5.00 - // Need more reasearch. Treat like above for now. + // example: 48000 / 9600 = 5 + // 3 is slightly better than 2 or 4. - save_audio_config_p->achan[chan].upsample = 4; + save_audio_config_p->achan[chan].upsample = 3; } else if (ratio < 15) { - // ... + // ... guessing save_audio_config_p->achan[chan].upsample = 2; } @@ -775,7 +782,8 @@ int demod_init (struct audio_s *pa) } demod_9600_init (save_audio_config_p->achan[chan].modem_type, - save_audio_config_p->achan[chan].upsample * save_audio_config_p->adev[ACHAN2ADEV(chan)].samples_per_sec, + save_audio_config_p->adev[ACHAN2ADEV(chan)].samples_per_sec, + save_audio_config_p->achan[chan].upsample, save_audio_config_p->achan[chan].baud, D); if (strchr(save_audio_config_p->achan[chan].profiles, '+') != NULL) { @@ -796,10 +804,23 @@ int demod_init (struct audio_s *pa) } /* switch on modulation type. */ - } /* if channel number is valid */ + } /* if channel medium is radio */ + +// FIXME dw_printf ("-------- end of loop for chn %d \n", chan); } /* for chan ... */ + // Now the virtual channels. FIXME: could be single loop. + + for (chan = MAX_CHANS; chan < MAX_TOTAL_CHANS; chan++) { + +// FIXME dw_printf ("-------- virtual channel loop %d \n", chan); + + if (chan == save_audio_config_p->igate_vchannel) { + text_color_set(DW_COLOR_DEBUG); + dw_printf ("Channel %d: IGate virtual channel.\n", chan); + } + } return (0); @@ -811,7 +832,7 @@ int demod_init (struct audio_s *pa) * * Name: demod_get_sample * - * Purpose: Get one audio sample fromt the specified sound input source. + * Purpose: Get one audio sample from the specified sound input source. * * Inputs: a - Index for audio device. 0 = first. * @@ -821,7 +842,7 @@ int demod_init (struct audio_s *pa) * Global In: save_audio_config_p->adev[ACHAN2ADEV(chan)].bits_per_sample - So we know whether to * read 1 or 2 bytes from audio stream. * - * Description: Grab 1 or two btyes depending on data source. + * Description: Grab 1 or two bytes depending on data source. * * When processing stereo, the caller will call this * at twice the normal rate to obtain alternating left @@ -831,17 +852,15 @@ int demod_init (struct audio_s *pa) #define FSK_READ_ERR (256*256) - __attribute__((hot)) int demod_get_sample (int a) { int x1, x2; - signed short sam; /* short to force sign extention. */ + signed short sam; /* short to force sign extension. */ assert (save_audio_config_p->adev[a].bits_per_sample == 8 || save_audio_config_p->adev[a].bits_per_sample == 16); - if (save_audio_config_p->adev[a].bits_per_sample == 8) { x1 = audio_get(a); @@ -908,12 +927,27 @@ int demod_get_sample (int a) * *--------------------------------------------------------------------*/ +static volatile int mute_input[MAX_CHANS]; + +// New in 1.7. +// A few people have a really bad audio cross talk situation where they receive their own transmissions. +// It usually doesn't cause a problem but it is confusing to look at. +// "half duplex" setting applied only to the transmit logic. i.e. wait for clear channel before sending. +// Receiving was still active. +// I think the simplest solution is to mute/unmute the audio input at this point if not full duplex. +// This is called from ptt_set for half duplex. + +void demod_mute_input (int chan, int mute_during_xmit) +{ + assert (chan >= 0 && chan < MAX_CHANS); + mute_input[chan] = mute_during_xmit; +} __attribute__((hot)) void demod_process_sample (int chan, int subchan, int sam) { float fsam; - int k; + //int k; struct demodulator_state_s *D; @@ -921,6 +955,10 @@ void demod_process_sample (int chan, int subchan, int sam) assert (chan >= 0 && chan < MAX_CHANS); assert (subchan >= 0 && subchan < MAX_SUBCHANS); + if (mute_input[chan]) { + sam = 0; + }; + D = &demodulator_state[chan][subchan]; @@ -1005,47 +1043,7 @@ void demod_process_sample (int chan, int subchan, int sam) case MODEM_AIS: default: - if (zerostuff) { - /* Literature says this is better if followed */ - /* by appropriate low pass filter. */ - /* So far, both are same in tests with different */ - /* optimal low pass filter parameters. */ - - for (k=1; kachan[chan].upsample; k++) { - demod_9600_process_sample (chan, 0, D); - } - demod_9600_process_sample (chan, sam * save_audio_config_p->achan[chan].upsample, D); - } - else { - - /* Linear interpolation. */ - static int prev_sam; - - switch (save_audio_config_p->achan[chan].upsample) { - case 1: - demod_9600_process_sample (chan, sam, D); - break; - case 2: - demod_9600_process_sample (chan, (prev_sam + sam) / 2, D); - demod_9600_process_sample (chan, sam, D); - break; - case 3: - demod_9600_process_sample (chan, (2 * prev_sam + sam) / 3, D); - demod_9600_process_sample (chan, (prev_sam + 2 * sam) / 3, D); - demod_9600_process_sample (chan, sam, D); - break; - case 4: - demod_9600_process_sample (chan, (3 * prev_sam + sam) / 4, D); - demod_9600_process_sample (chan, (prev_sam + sam) / 2, D); - demod_9600_process_sample (chan, (prev_sam + 3 * sam) / 4, D); - demod_9600_process_sample (chan, sam, D); - break; - default: - assert (0); - break; - } - prev_sam = sam; - } + demod_9600_process_sample (chan, sam, save_audio_config_p->achan[chan].upsample, D); break; } /* switch modem_type */ diff --git a/src/demod.h b/src/demod.h index 3233b9ba..f1120cd0 100644 --- a/src/demod.h +++ b/src/demod.h @@ -8,10 +8,13 @@ int demod_init (struct audio_s *pa); +void demod_mute_input (int chan, int mute); + int demod_get_sample (int a); void demod_process_sample (int chan, int subchan, int sam); void demod_print_agc (int chan, int subchan); -alevel_t demod_get_audio_level (int chan, int subchan); \ No newline at end of file +alevel_t demod_get_audio_level (int chan, int subchan); + diff --git a/src/demod_9600.c b/src/demod_9600.c index 2f989830..705d1fa7 100644 --- a/src/demod_9600.c +++ b/src/demod_9600.c @@ -1,7 +1,7 @@ // // This file is part of Dire Wolf, an amateur radio packet TNC. // -// Copyright (C) 2011, 2012, 2013, 2015, 2019 John Langner, WB2OSZ +// Copyright (C) 2011, 2012, 2013, 2015, 2019, 2021 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 @@ -25,7 +25,8 @@ * * Module: demod_9600.c * - * Purpose: Demodulator for scrambled baseband encoding. + * Purpose: Demodulator for baseband signal. + * This is used for AX.25 (with scrambling) and IL2P without. * * Input: Audio samples from either a file or the "sound card." * @@ -45,12 +46,14 @@ #include // Fine tuning for different demodulator types. +// Don't remove this section. It is here for a reason. + +#define DCD_THRESH_ON 32 // Hysteresis: Can miss 0 out of 32 for detecting lock. + // This is best for actual on-the-air signals. + // Still too many brief false matches. +#define DCD_THRESH_OFF 8 // Might want a little more fine tuning. +#define DCD_GOOD_WIDTH 1024 // No more than 1024!!! -#define DCD_THRESH_ON 32 // Hysteresis: Can miss 0 out of 32 for detecting lock. - // This is best for actual on-the-air signals. - // Still too many brief false matches. -#define DCD_THRESH_OFF 8 // Might want a little more fine tuning. -#define DCD_GOOD_WIDTH 1024 // No more than 1024!!! #include "fsk_demod_state.h" // Values above override defaults. #include "tune.h" @@ -125,9 +128,12 @@ static inline float agc (float in, float fast_attack, float slow_decay, float *p * * Inputs: modem_type - Determines whether scrambling is used. * - * samples_per_sec - Number of samples per second. - * Might be upsampled in hopes of - * reducing the PLL jitter. + * samples_per_sec - Number of samples per second for audio. + * + * upsample - Factor to upsample the incoming stream. + * After a lot of experimentation, I discovered that + * it works better if the data is upsampled. + * This reduces the jitter for PLL synchronization. * * baud - Data rate in bits per second. * @@ -137,10 +143,13 @@ static inline float agc (float in, float fast_attack, float slow_decay, float *p * *----------------------------------------------------------------*/ -void demod_9600_init (enum modem_t modem_type, int samples_per_sec, int baud, struct demodulator_state_s *D) +void demod_9600_init (enum modem_t modem_type, int original_sample_rate, int upsample, int baud, struct demodulator_state_s *D) { float fc; int j; + if (upsample < 1) upsample = 1; + if (upsample > 4) upsample = 4; + memset (D, 0, sizeof(struct demodulator_state_s)); D->modem_type = modem_type; @@ -155,12 +164,13 @@ void demod_9600_init (enum modem_t modem_type, int samples_per_sec, int baud, st // case 'L': // upsample x4 with filtering. - D->lp_filter_len_bits = 1.0; + D->lp_filter_len_bits = 1.0; // -U4 = 61 4.59 samples/symbol // Works best with odd number in some tests. Even is better in others. - //D->lp_filter_size = ((int) (0.5f * ( D->lp_filter_len_bits * (float)samples_per_sec / (float)baud ))) * 2 + 1; + //D->lp_filter_size = ((int) (0.5f * ( D->lp_filter_len_bits * (float)original_sample_rate / (float)baud ))) * 2 + 1; - D->lp_filter_size = (int) (( D->lp_filter_len_bits * (float)samples_per_sec / baud) + 0.5f); + // Just round to nearest integer. + D->lp_filter_size = (int) (( D->lp_filter_len_bits * (float)original_sample_rate / baud) + 0.5f); D->lp_window = BP_WINDOW_COSINE; @@ -185,8 +195,11 @@ void demod_9600_init (enum modem_t modem_type, int samples_per_sec, int baud, st dw_printf ("samples per bit = %.1f\n", (double)samples_per_sec / baud); #endif + + // PLL needs to use the upsampled rate. + D->pll_step_per_sample = - (int) round(TICKS_PER_PLL_CYCLE * (double) baud / (double)samples_per_sec); + (int) round(TICKS_PER_PLL_CYCLE * (double) baud / (double)(original_sample_rate * upsample)); #ifdef TUNE_LP_WINDOW @@ -217,13 +230,87 @@ void demod_9600_init (enum modem_t modem_type, int samples_per_sec, int baud, st D->pll_searching_inertia = TUNE_PLL_SEARCHING; #endif - fc = (float)baud * D->lpf_baud / (float)samples_per_sec; + // Initial filter (before scattering) is based on upsampled rate. + + fc = (float)baud * D->lpf_baud / (float)(original_sample_rate * upsample); //dw_printf ("demod_9600_init: call gen_lowpass(fc=%.2f, , size=%d, )\n", fc, D->lp_filter_size); - (void)gen_lowpass (fc, D->lp_filter, D->lp_filter_size, D->lp_window, 0); + gen_lowpass (fc, D->u.bb.lp_filter, D->lp_filter_size * upsample, D->lp_window); + +// New in 1.7 - +// Use a polyphase filter to reduce the CPU load. +// Originally I used zero stuffing to upsample. +// Here is the general idea. +// +// Suppose the input samples are 1 2 3 4 5 6 7 8 9 ... +// Filter coefficients are a b c d e f g h i ... +// +// With original sampling rate, the filtering would involve multiplying and adding: +// +// 1a 2b 3c 4d 5e 6f ... +// +// When upsampling by 3, each of these would need to be evaluated +// for each audio sample: +// +// 1a 0b 0c 2d 0e 0f 3g 0h 0i ... +// 0a 1b 0c 0d 2e 0f 0g 3h 0i ... +// 0a 0b 1c 0d 0e 2f 0g 0h 3i ... +// +// 2/3 of the multiplies are always by a stuffed zero. +// We can do this more efficiently by removing them. +// +// 1a 2d 3g ... +// 1b 2e 3h ... +// 1c 2f 3i ... +// +// We scatter the original filter across multiple shorter filters. +// Each input sample cycles around them to produce the upsampled rate. +// +// a d g ... +// b e h ... +// c f i ... +// +// There are countless sources of information DSP but this one is unique +// in that it is a college course that mentions APRS. +// https://www2.eecs.berkeley.edu/Courses/EE123 +// +// Was the effort worthwhile? Times on an RPi 3. +// +// command: atest -B9600 ~/walkabout9600[abc]-compressed*.wav +// +// These are 3 recordings of a portable system being carried out of +// range and back in again. It is a real world test for weak signals. +// +// options num decoded seconds x realtime +// 1.6 1.7 1.6 1.7 1.6 1.7 +// --- --- --- --- --- --- +// -P- 171 172 23.928 17.967 14.9 19.9 +// -P+ 180 180 54.688 48.772 6.5 7.3 +// -P- -F1 177 178 32.686 26.517 10.9 13.5 +// +// So, it turns out that -P+ doesn't have a dramatic improvement, only +// around 4%, for drastically increased CPU requirements. +// Maybe we should turn that off by default, especially for ARM. +// + + int k = 0; + for (int i = 0; i < D->lp_filter_size; i++) { + D->u.bb.lp_polyphase_1[i] = D->u.bb.lp_filter[k++]; + if (upsample >= 2) { + D->u.bb.lp_polyphase_2[i] = D->u.bb.lp_filter[k++]; + if (upsample >= 3) { + D->u.bb.lp_polyphase_3[i] = D->u.bb.lp_filter[k++]; + if (upsample >= 4) { + D->u.bb.lp_polyphase_4[i] = D->u.bb.lp_filter[k++]; + } + } + } + } + /* Version 1.2: Experiment with different slicing levels. */ + // Really didn't help that much because we should have a symmetrical signal. for (j = 0; j < MAX_SUBCHANS; j++) { slice_point[j] = 0.02f * (j - 0.5f * (MAX_SUBCHANS-1)); @@ -259,7 +346,7 @@ void demod_9600_init (enum modem_t modem_type, int samples_per_sec, int baud, st * been distorted by going thru voice transceivers not * intended to pass this sort of "audio" signal. * - * Data is "scrambled" to reduce the amount of DC bias. + * For G3RUH mode, data is "scrambled" to reduce the amount of DC bias. * The data stream must be unscrambled at the receiving end. * * We also have a digital phase locked loop (PLL) @@ -276,6 +363,9 @@ void demod_9600_init (enum modem_t modem_type, int samples_per_sec, int baud, st * of the function to be called for each bit recovered * from the demodulator. For now, it's simply hard-coded. * + * After experimentation, I found that this works better if + * the original signal is upsampled by 2x or even 4x. + * * References: 9600 Baud Packet Radio Modem Design * http://www.amsat.org/amsat/articles/g3ruh/109.html * @@ -290,63 +380,57 @@ void demod_9600_init (enum modem_t modem_type, int samples_per_sec, int baud, st inline static void nudge_pll (int chan, int subchan, int slice, float demod_out, struct demodulator_state_s *D); +static void process_filtered_sample (int chan, float fsam, struct demodulator_state_s *D); + + __attribute__((hot)) -void demod_9600_process_sample (int chan, int sam, struct demodulator_state_s *D) +void demod_9600_process_sample (int chan, int sam, int upsample, struct demodulator_state_s *D) { - float fsam; - float amp; - float demod_out; #if DEBUG4 static FILE *demod_log_fp = NULL; static int log_file_seq = 0; /* Part of log file name */ #endif - int subchan = 0; - int demod_data; /* Still scrambled. */ - assert (chan >= 0 && chan < MAX_CHANS); assert (subchan >= 0 && subchan < MAX_SUBCHANS); - -/* - * Filters use last 'filter_size' samples. - * - * First push the older samples down. - * - * Finally, put the most recent at the beginning. - * - * Future project? Rather than shifting the samples, - * it might be faster to add another variable to keep - * track of the most recent sample and change the - * indexing in the later loops that multipy and add. - */ - /* Scale to nice number for convenience. */ /* Consistent with the AFSK demodulator, we'd like to use */ /* only half of the dynamic range to have some headroom. */ /* i.e. input range +-16k becomes +-1 here and is */ /* displayed in the heard line as audio level 100. */ - fsam = sam / 16384.0; - -#if defined(TUNE_ZEROSTUFF) && TUNE_ZEROSTUFF == 0 -// experiment - no filtering. - - amp = fsam; + fsam = (float)sam / 16384.0f; + + // Low pass filter + push_sample (fsam, D->u.bb.audio_in, D->lp_filter_size); + + fsam = convolve (D->u.bb.audio_in, D->u.bb.lp_polyphase_1, D->lp_filter_size); + process_filtered_sample (chan, fsam, D); + if (upsample >= 2) { + fsam = convolve (D->u.bb.audio_in, D->u.bb.lp_polyphase_2, D->lp_filter_size); + process_filtered_sample (chan, fsam, D); + if (upsample >= 3) { + fsam = convolve (D->u.bb.audio_in, D->u.bb.lp_polyphase_3, D->lp_filter_size); + process_filtered_sample (chan, fsam, D); + if (upsample >= 4) { + fsam = convolve (D->u.bb.audio_in, D->u.bb.lp_polyphase_4, D->lp_filter_size); + process_filtered_sample (chan, fsam, D); + } + } + } +} -#else - push_sample (fsam, D->raw_cb, D->lp_filter_size); -/* - * Low pass filter to reduce noise yet pass the data. - */ +__attribute__((hot)) +static void process_filtered_sample (int chan, float fsam, struct demodulator_state_s *D) +{ - amp = convolve (D->raw_cb, D->lp_filter, D->lp_filter_size); -#endif + int subchan = 0; /* * Version 1.2: Capture the post-filtering amplitude for display. @@ -359,18 +443,18 @@ void demod_9600_process_sample (int chan, int sam, struct demodulator_state_s *D // TODO: probably no need for this. Just use D->m_peak, D->m_valley - if (amp >= D->alevel_mark_peak) { - D->alevel_mark_peak = amp * D->quick_attack + D->alevel_mark_peak * (1.0f - D->quick_attack); + if (fsam >= D->alevel_mark_peak) { + D->alevel_mark_peak = fsam * D->quick_attack + D->alevel_mark_peak * (1.0f - D->quick_attack); } else { - D->alevel_mark_peak = amp * D->sluggish_decay + D->alevel_mark_peak * (1.0f - D->sluggish_decay); + D->alevel_mark_peak = fsam * D->sluggish_decay + D->alevel_mark_peak * (1.0f - D->sluggish_decay); } - if (amp <= D->alevel_space_peak) { - D->alevel_space_peak = amp * D->quick_attack + D->alevel_space_peak * (1.0f - D->quick_attack); + if (fsam <= D->alevel_space_peak) { + D->alevel_space_peak = fsam * D->quick_attack + D->alevel_space_peak * (1.0f - D->quick_attack); } else { - D->alevel_space_peak = amp * D->sluggish_decay + D->alevel_space_peak * (1.0f - D->sluggish_decay); + D->alevel_space_peak = fsam * D->sluggish_decay + D->alevel_space_peak * (1.0f - D->sluggish_decay); } /* @@ -381,12 +465,14 @@ void demod_9600_process_sample (int chan, int sam, struct demodulator_state_s *D * This works by looking at the minimum and maximum signal peaks * and scaling the results to be roughly in the -1.0 to +1.0 range. */ + float demod_out; + int demod_data; /* Still scrambled. */ - demod_out = agc (amp, D->agc_fast_attack, D->agc_slow_decay, &(D->m_peak), &(D->m_valley)); + demod_out = agc (fsam, D->agc_fast_attack, D->agc_slow_decay, &(D->m_peak), &(D->m_valley)); // TODO: There is potential for multiple decoders with one filter. -//dw_printf ("peak=%.2f valley=%.2f amp=%.2f norm=%.2f\n", D->m_peak, D->m_valley, amp, norm); +//dw_printf ("peak=%.2f valley=%.2f fsam=%.2f norm=%.2f\n", D->m_peak, D->m_valley, fsam, norm); if (D->num_slicers <= 1) { @@ -435,7 +521,7 @@ void demod_9600_process_sample (int chan, int sam, struct demodulator_state_s *D fprintf (demod_log_fp, "%.3f, %.3f, %.3f, %.3f, %.3f, %d, %.2f\n", fsam + 6, - amp + 4, + fsam + 4, D->m_peak + 4, D->m_valley + 4, demod_out + 2, @@ -478,7 +564,7 @@ void demod_9600_process_sample (int chan, int sam, struct demodulator_state_s *D * * Returns: None * - * Descripton: A PLL is used to sample near the centers of the data bits. + * Description: A PLL is used to sample near the centers of the data bits. * * D->data_clock_pll is a SIGNED 32 bit variable. * When it overflows from a large positive value to a negative value, we diff --git a/src/demod_9600.h b/src/demod_9600.h index ac3e7474..51fc15e4 100644 --- a/src/demod_9600.h +++ b/src/demod_9600.h @@ -6,9 +6,9 @@ #include "fsk_demod_state.h" -void demod_9600_init (enum modem_t modem_type, int samples_per_sec, int baud, struct demodulator_state_s *D); +void demod_9600_init (enum modem_t modem_type, int original_sample_rate, int upsample, int baud, struct demodulator_state_s *D); -void demod_9600_process_sample (int chan, int sam, struct demodulator_state_s *D); +void demod_9600_process_sample (int chan, int sam, int upsample, struct demodulator_state_s *D); diff --git a/src/demod_afsk.c b/src/demod_afsk.c index 7a007d1f..b4d6c295 100644 --- a/src/demod_afsk.c +++ b/src/demod_afsk.c @@ -1,7 +1,7 @@ // // This file is part of Dire Wolf, an amateur radio packet TNC. // -// Copyright (C) 2011, 2012, 2013, 2014, 2015 John Langner, WB2OSZ +// Copyright (C) 2011, 2012, 2013, 2014, 2015, 2020 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 @@ -23,8 +23,9 @@ // #define DEBUG3 1 /* print carrier detect changes. */ // #define DEBUG4 1 /* capture AFSK demodulator output to log files */ + /* Can be used to make nice plots. */ -// #define DEBUG5 1 /* capture 9600 output to log files */ +// #define DEBUG5 1 // Write just demodulated bit stream to file. */ /*------------------------------------------------------------------ @@ -62,15 +63,32 @@ #define MIN(a,b) ((a)<(b)?(a):(b)) #define MAX(a,b) ((a)>(b)?(a):(b)) +#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); \ + } } -/* Quick approximation to sqrt(x*x+y*y) */ + +// Cosine table indexed by unsigned byte. +static float fcos256_table[256]; + +#define fcos256(x) (fcos256_table[((x)>>24)&0xff]) +#define fsin256(x) (fcos256_table[(((x)>>24)-64)&0xff]) + +static void nudge_pll (int chan, int subchan, int slice, float demod_out, struct demodulator_state_s *D, float amplitude); + + +/* Quick approximation to sqrt(x*x + y*y) */ /* No benefit for regular PC. */ -/* Should help with microcomputer platform. */ +/* Might help with microcomputer platform??? */ -#if 0 // not using anymore __attribute__((hot)) __attribute__((always_inline)) -static inline float z (float x, float y) +static inline float fast_hypot(float x, float y) { +#if 0 x = fabsf(x); y = fabsf(y); @@ -80,8 +98,11 @@ static inline float z (float x, float y) else { return (y * .941246f + x * .41f); } -} +#else + return (hypotf(x,y)); #endif +} + /* Add sample to buffer and shift the rest down. */ @@ -96,22 +117,31 @@ static inline void push_sample (float val, float *buff, int size) /* FIR filter kernel. */ __attribute__((hot)) __attribute__((always_inline)) -static inline float convolve (const float *__restrict__ data, const float *__restrict__ filter, int filter_size) +static inline float convolve (const float *__restrict__ data, const float *__restrict__ filter, int filter_taps) { float sum = 0.0f; int j; - //#pragma GCC ivdep // ignored until gcc 4.9 - for (j=0; j *ppeak) x = *ppeak; // experiment: clip to envelope? + if (x < *pvalley) x = *pvalley; +#endif if (*ppeak > *pvalley) { - return ((in - 0.5f * (*ppeak + *pvalley)) / (*ppeak - *pvalley)); + + return ((x - 0.5f * (*ppeak + *pvalley)) / (*ppeak - *pvalley)); // my original AGC + + //return (( x - 0.5f * (*ppeak + *pvalley )) * ( *ppeak - *pvalley )); // see note below. + //return (x - 0.5f * (*ppeak + *pvalley)); // not as good either. } return (0.0f); } +// K6JQ pointed me to this wonderful article: +// Improved Automatic Threshold Correction Methods for FSK by Kok Chen, W7AY. +// http://www.w7ay.net/site/Technical/ATC/index.html +// +// The stated problem is a little different, selective fading for HF RTTY, but the +// general idea is the similar: Compensating for imbalance of the two tones. +// +// The stronger tone probably has a better S/N ratio so we apply a larger +// weight to it. Effectively it is comparing power rather than amplitude. +// This is the optimal method from the article referenced. +// +// Interesting idea but it did not work as well as the original AGC in this case. +// For VHF FM we are not dealing with rapid deep selective fading of one tone. +// Instead we have an imbalance which is the same for the whole frame. +// It might be interesting to try this with HF SSB packet which is much like RTTY. +// +// I use the term valley rather than noise floor. +// After a little algebra, it looks remarkably similar to the function above. +// +// return (( x - valley ) * ( peak - valley ) - 0.5f * ( peak - valley ) * ( peak - valley )); +// return (( x - valley ) - 0.5f * ( peak - valley )) * ( peak - valley )); +// return (( x - 0.5f * (peak + valley )) * ( peak - valley )); + + + /* * for multi-slicer experiment. */ @@ -162,11 +226,7 @@ static inline float agc (float in, float fast_attack, float slow_decay, float *p * * D - Pointer to demodulator state for given channel. * - * Outputs: D->ms_filter_size - * D->m_sin_table[] - * D->m_cos_table[] - * D->s_sin_table[] - * D->s_cos_table[] + * Outputs: * * Returns: None. * @@ -180,6 +240,10 @@ void demod_afsk_init (int samples_per_sec, int baud, int mark_freq, { int j; + + for (j = 0; j < 256; j++) { + fcos256_table[j] = cosf((float)j * 2.0f * (float)M_PI / 256.0f); + } memset (D, 0, sizeof(struct demodulator_state_s)); D->num_slicers = 1; @@ -188,128 +252,164 @@ void demod_afsk_init (int samples_per_sec, int baud, int mark_freq, dw_printf ("demod_afsk_init (rate=%d, baud=%d, mark=%d, space=%d, profile=%c\n", samples_per_sec, baud, mark_freq, space_freq, profile); #endif - -#ifdef TUNE_PROFILE - profile = TUNE_PROFILE; -#endif + D->profile = profile; + switch (D->profile) { - D->profile = profile; // so we know whether to take fast path later. + case 'A': // Official name + case 'E': // For compatibility during transition - switch (profile) { + D->profile = 'A'; - case 'D': - - /* Prefilter, Cosine window, FIR lowpass. Tweeked for 300 baud. */ + /* New in version 1.7 */ + /* This is a simpler version of what has been used all along. */ + /* Rather than convolving each sample with a pre-computed mark and */ + /* space filter, we have two free running local oscillators. */ + /* Also see if we can do better with a Root Raised Cosine filter */ + /* which supposedly reduces intersymbol interference. */ D->use_prefilter = 1; /* first, a bandpass filter. */ - D->prefilter_baud = 0.87; - D->pre_filter_len_bits = 1.857; - D->pre_window = BP_WINDOW_COSINE; - D->ms_filter_len_bits = 1.857; /* 91 @ 44100/3, 300 */ - D->ms_window = BP_WINDOW_COSINE; - - //D->bp_window = BP_WINDOW_COSINE; + if (baud > 600) { + D->prefilter_baud = 0.155; + // Low cutoff below mark, high cutoff above space + // as fraction of the symbol rate. + // Intuitively you might expect this to be about + // half the symbol rate, e.g. 600 Hz outside + // the two tones of interest for 1200 baud. + // It turns out that narrower is better. + + D->pre_filter_len_sym = 383 * 1200. / 44100.; // about 8 symbols + D->pre_window = BP_WINDOW_TRUNCATED; + } + else { + D->prefilter_baud = 0.87; // TOTO: fine tune + D->pre_filter_len_sym = 1.857; + D->pre_window = BP_WINDOW_COSINE; + } + + // Local oscillators for Mark and Space tones. + + D->u.afsk.m_osc_phase = 0; + D->u.afsk.m_osc_delta = round ( pow(2., 32.) * (double)mark_freq / (double)samples_per_sec ); + + D->u.afsk.s_osc_phase = 0; + D->u.afsk.s_osc_delta = round ( pow(2., 32.) * (double)space_freq / (double)samples_per_sec ); + + D->u.afsk.use_rrc = 1; + TUNE("TUNE_USE_RRC", D->u.afsk.use_rrc, "use_rrc", "%d") + + if (D->u.afsk.use_rrc) { + D->u.afsk.rrc_width_sym = 2.80; + D->u.afsk.rrc_rolloff = 0.20; + } + else { + D->lpf_baud = 0.14; + D->lp_filter_width_sym = 1.388; + D->lp_window = BP_WINDOW_TRUNCATED; + } + + D->agc_fast_attack = 0.70; + D->agc_slow_decay = 0.000090; - D->lpf_use_fir = 1; - D->lpf_baud = 1.10; - D->lp_filter_len_bits = D->ms_filter_len_bits; - D->lp_window = BP_WINDOW_TRUNCATED; - - D->agc_fast_attack = 0.495; - D->agc_slow_decay = 0.00022; - D->hysteresis = 0.027; - - D->pll_locked_inertia = 0.620; - D->pll_searching_inertia = 0.350; + D->pll_locked_inertia = 0.74; + D->pll_searching_inertia = 0.50; break; - case 'F': // removed obsolete. treat as E for now. - case 'E': + case 'B': // official name + case 'D': // backward compatibility - /* 1200 baud - Started out similar to C but add prefilter. */ - /* Version 1.2 */ - /* Enhancements: */ - /* + Add prefilter. Previously used for 300 baud D, but not 1200. */ - /* + Prefilter length now independent of M/S filters. */ - /* + Lowpass filter length now independent of M/S filters. */ - /* + Allow mixed window types. */ + D->profile = 'B'; - //D->bp_window = BP_WINDOW_COSINE; /* The name says BP but it is used for all of them. */ + // Experiment for version 1.7. + // Up to this point, I've always used separate mark and space + // filters and compared the amplitudes. + // Another technique for an FM demodulator is to mix with + // the center frequency and look for the rate of change of the phase. D->use_prefilter = 1; /* first, a bandpass filter. */ - D->prefilter_baud = 0.23; - D->pre_filter_len_bits = 156 * 1200. / 44100.; - D->pre_window = BP_WINDOW_TRUNCATED; - - D->ms_filter_len_bits = 74 * 1200. / 44100.; - D->ms_window = BP_WINDOW_COSINE; - - D->lpf_use_fir = 1; - D->lpf_baud = 1.18; - D->lp_filter_len_bits = 63 * 1200. / 44100.; - D->lp_window = BP_WINDOW_TRUNCATED; - - //D->agc_fast_attack = 0.300; - //D->agc_slow_decay = 0.000185; - D->agc_fast_attack = 0.820; - D->agc_slow_decay = 0.000214; - D->hysteresis = 0.01; - - //D->pll_locked_inertia = 0.57; - //D->pll_searching_inertia = 0.33; + + if (baud > 600) { + D->prefilter_baud = 0.19; + // Low cutoff below mark, high cutoff above space + // as fraction of the symbol rate. + // Intuitively you might expect this to be about + // half the symbol rate, e.g. 600 Hz outside + // the two tones of interest for 1200 baud. + // It turns out that narrower is better. + + D->pre_filter_len_sym = 8.163; // Filter length in symbol times. + D->pre_window = BP_WINDOW_TRUNCATED; + } + else { + D->prefilter_baud = 0.87; // TOTO: fine tune + D->pre_filter_len_sym = 1.857; + D->pre_window = BP_WINDOW_COSINE; + } + + // Local oscillator for Center frequency. + + D->u.afsk.c_osc_phase = 0; + D->u.afsk.c_osc_delta = round ( pow(2., 32.) * 0.5 * (mark_freq + space_freq) / (double)samples_per_sec ); + + D->u.afsk.use_rrc = 1; + TUNE("TUNE_USE_RRC", D->u.afsk.use_rrc, "use_rrc", "%d") + + if (D->u.afsk.use_rrc) { + D->u.afsk.rrc_width_sym = 2.00; + D->u.afsk.rrc_rolloff = 0.40; + } + else { + D->lpf_baud = 0.5; + D->lp_filter_width_sym = 1.714286; // 63 * 1200. / 44100.; + D->lp_window = BP_WINDOW_TRUNCATED; + } + + // For scaling phase shift into normallized -1 to +1 range for mark and space. + D->u.afsk.normalize_rpsam = 1.0 / (0.5 * abs(mark_freq - space_freq) * 2 * M_PI / samples_per_sec); + + // New "B" demodulator does not use AGC but demod.c needs this to derive "quick" and + // "sluggish" values for overall signal amplitude. That probably should be independent + // of these values. + D->agc_fast_attack = 0.70; + D->agc_slow_decay = 0.000090; + D->pll_locked_inertia = 0.74; D->pll_searching_inertia = 0.50; + + D->alevel_mark_peak = -1; // Disable received signal (m/s) display. + D->alevel_space_peak = -1; break; default: text_color_set(DW_COLOR_ERROR); - dw_printf ("Invalid filter profile = %c\n", profile); + dw_printf ("Invalid AFSK demodulator profile = %c\n", profile); exit (1); } -#ifdef TUNE_PRE_WINDOW - D->pre_window = TUNE_PRE_WINDOW; -#endif -#ifdef TUNE_MS_WINDOW - D->ms_window = TUNE_MS_WINDOW; -#endif -#ifdef TUNE_MS2_WINDOW - D->ms2_window = TUNE_MS2_WINDOW; -#endif -#ifdef TUNE_LP_WINDOW - D->lp_window = TUNE_LP_WINDOW; -#endif + TUNE("TUNE_PRE_BAUD", D->prefilter_baud, "prefilter_baud", "%.3f") + TUNE("TUNE_PRE_WINDOW", D->pre_window, "pre_window", "%d") + + TUNE("TUNE_LPF_BAUD", D->lpf_baud, "lpf_baud", "%.3f") + TUNE("TUNE_LP_WINDOW", D->lp_window, "lp_window", "%d") + + TUNE("TUNE_RRC_ROLLOFF", D->u.afsk.rrc_rolloff, "rrc_rolloff", "%.2f") + TUNE("TUNE_RRC_WIDTH_SYM", D->u.afsk.rrc_width_sym, "rrc_width_sym", "%.2f") + + TUNE("TUNE_AGC_FAST", D->agc_fast_attack, "agc_fast_attack", "%.3f") + TUNE("TUNE_AGC_SLOW", D->agc_slow_decay, "agc_slow_decay", "%.6f") + + TUNE("TUNE_PLL_LOCKED", D->pll_locked_inertia, "pll_locked_inertia", "%.2f") + TUNE("TUNE_PLL_SEARCHING", D->pll_searching_inertia, "pll_searching_inertia", "%.2f") -#if defined(TUNE_AGC_FAST) && defined(TUNE_AGC_SLOW) - D->agc_fast_attack = TUNE_AGC_FAST; - D->agc_slow_decay = TUNE_AGC_SLOW; -#endif -#ifdef TUNE_HYST - D->hysteresis = TUNE_HYST; -#endif -#if defined(TUNE_PLL_LOCKED) && defined(TUNE_PLL_SEARCHING) - D->pll_locked_inertia = TUNE_PLL_LOCKED; - D->pll_searching_inertia = TUNE_PLL_SEARCHING; -#endif -#ifdef TUNE_LPF_BAUD - D->lpf_baud = TUNE_LPF_BAUD; -#endif -#ifdef TUNE_PRE_BAUD - D->prefilter_baud = TUNE_PRE_BAUD; -#endif -#ifdef TUNE_LP_DELAY_FRACT - D->lp_delay_fract = TUNE_LP_DELAY_FRACT; -#endif /* * Calculate constants used for timing. * The audio sample rate must be at least a few times the data rate. * - * Baud is an integer so we hack in a fine ajustment for EAS. + * Baud is an integer so we hack in a fine adjustment for EAS. * Probably makes no difference because the DPLL keeps it in sync. * * A fraction if a Hz would make no difference for the filters. @@ -321,77 +421,34 @@ void demod_afsk_init (int samples_per_sec, int baud, int mark_freq, D->pll_step_per_sample = (int) round((TICKS_PER_PLL_CYCLE * (double)baud) / ((double)samples_per_sec)); } -/* - * Convert number of bit times to number of taps. - */ - - D->pre_filter_size = (int) round( D->pre_filter_len_bits * (float)samples_per_sec / (float)baud ); - D->ms_filter_size = (int) round( D->ms_filter_len_bits * (float)samples_per_sec / (float)baud ); - D->lp_filter_size = (int) round( D->lp_filter_len_bits * (float)samples_per_sec / (float)baud ); - -/* Experiment with other sizes. */ - -#ifdef TUNE_PRE_FILTER_SIZE - D->pre_filter_size = TUNE_PRE_FILTER_SIZE; -#endif -#ifdef TUNE_MS_FILTER_SIZE - D->ms_filter_size = TUNE_MS_FILTER_SIZE; -#endif -#ifdef TUNE_LP_FILTER_SIZE - D->lp_filter_size = TUNE_LP_FILTER_SIZE; -#endif - - //assert (D->pre_filter_size >= 4); - assert (D->ms_filter_size >= 4); - //assert (D->lp_filter_size >= 4); - - if (D->pre_filter_size > MAX_FILTER_SIZE) - { - text_color_set (DW_COLOR_ERROR); - dw_printf ("Calculated filter size of %d is too large.\n", D->pre_filter_size); - dw_printf ("Decrease the audio sample rate or increase the baud rate or\n"); - dw_printf ("recompile the application with MAX_FILTER_SIZE larger than %d.\n", - MAX_FILTER_SIZE); - exit (1); - } - - if (D->ms_filter_size > MAX_FILTER_SIZE) - { - text_color_set (DW_COLOR_ERROR); - dw_printf ("Calculated filter size of %d is too large.\n", D->ms_filter_size); - dw_printf ("Decrease the audio sample rate or increase the baud rate or\n"); - dw_printf ("recompile the application with MAX_FILTER_SIZE larger than %d.\n", - MAX_FILTER_SIZE); - exit (1); - } - - - - if (D->lp_filter_size > MAX_FILTER_SIZE) - { - text_color_set (DW_COLOR_ERROR); - dw_printf ("Calculated filter size of %d is too large.\n", D->pre_filter_size); - dw_printf ("Decrease the audio sample rate or increase the baud rate or\n"); - dw_printf ("recompile the application with MAX_FILTER_SIZE larger than %d.\n", - MAX_FILTER_SIZE); - exit (1); - } - /* * Optionally apply a bandpass ("pre") filter to attenuate * frequencies outside the range of interest. - * This was first used for the "D" profile for 300 baud - * which uses narrow shift. We expect it to have significant - * benefit for a narrow shift. - * In version 1.2, we will also try it with 1200 baud "E" as - * an experiment to see how much it actually helps. */ if (D->use_prefilter) { - float f1, f2; - f1 = MIN(mark_freq,space_freq) - D->prefilter_baud * baud; - f2 = MAX(mark_freq,space_freq) + D->prefilter_baud * baud; + // odd number is a little better + D->pre_filter_taps = ((int)( D->pre_filter_len_sym * (float)samples_per_sec / (float)baud )) | 1; + + TUNE("TUNE_PRE_FILTER_TAPS", D->pre_filter_taps, "pre_filter_taps", "%d") + +// TODO: Size comes out to 417 for 1200 bps with 48000 sample rate. +// The message is upsetting. Can we handle this better? + + if (D->pre_filter_taps > MAX_FILTER_SIZE) { + text_color_set (DW_COLOR_ERROR); + dw_printf ("Warning: Calculated pre filter size of %d is too large.\n", D->pre_filter_taps); + dw_printf ("Decrease the audio sample rate or increase the decimation factor.\n"); + dw_printf ("You can use -D2 or -D3, on the command line, to down-sample the audio rate\n"); + dw_printf ("before demodulating. This greatly decreases the CPU requirements with little\n"); + dw_printf ("impact on the decoding performance. This is useful for a slow ARM processor,\n"); + dw_printf ("such as with a Raspberry Pi model 1.\n"); + D->pre_filter_taps = (MAX_FILTER_SIZE - 1) | 1; + } + + float f1 = MIN(mark_freq,space_freq) - D->prefilter_baud * baud; + float f2 = MAX(mark_freq,space_freq) + D->prefilter_baud * baud; #if 0 text_color_set(DW_COLOR_DEBUG); dw_printf ("Generating prefilter %.0f to %.0f Hz.\n", f1, f2); @@ -399,121 +456,67 @@ void demod_afsk_init (int samples_per_sec, int baud, int mark_freq, f1 = f1 / (float)samples_per_sec; f2 = f2 / (float)samples_per_sec; - gen_bandpass (f1, f2, D->pre_filter, D->pre_filter_size, D->pre_window); + gen_bandpass (f1, f2, D->pre_filter, D->pre_filter_taps, D->pre_window); } -/* - * Filters for detecting mark and space tones. - */ - -#if DEBUG1 - text_color_set(DW_COLOR_DEBUG); - dw_printf ("%s: \n", __FILE__); - dw_printf ("%d baud, %d samples_per_sec\n", baud, samples_per_sec); - dw_printf ("AFSK %d & %d Hz\n", mark_freq, space_freq); - dw_printf ("spll_step_per_sample = %d = 0x%08x\n", D->pll_step_per_sample, D->pll_step_per_sample); - dw_printf ("D->ms_filter_size = %d = 0x%08x\n", D->ms_filter_size, D->ms_filter_size); - dw_printf ("\n"); - dw_printf ("Mark\n"); - dw_printf (" j shape M sin M cos \n"); -#endif - - - gen_ms (mark_freq, samples_per_sec, D->m_sin_table, D->m_cos_table, D->ms_filter_size, D->ms_window); - -#if DEBUG1 - text_color_set(DW_COLOR_DEBUG); - - dw_printf ("Space\n"); - dw_printf (" j shape S sin S cos\n"); -#endif - - gen_ms (space_freq, samples_per_sec, D->s_sin_table, D->s_cos_table, D->ms_filter_size, D->ms_window); - /* * Now the lowpass filter. - * I thought we'd want a cutoff of about 0.5 the baud rate - * but it turns out about 1.1x is better. Still investigating... + * In version 1.7 a Root Raised Cosine filter is added as an alternative + * to the generic low pass filter. + * In both cases, lp_filter and lp_filter_taps are used but the + * contents will be generated differently. Later code does not care. */ + if (D->u.afsk.use_rrc) { - if (D->lpf_use_fir) { - float fc; - fc = baud * D->lpf_baud / (float)samples_per_sec; - D->lp_filter_delay = gen_lowpass (fc, D->lp_filter, D->lp_filter_size, D->lp_window, D->lp_delay_fract); - } - else { - // D->lp_filter_delay = - // Only needed for looking back and I don't expect to use IIR in that case. - } + assert (D->u.afsk.rrc_width_sym >= 1 && D->u.afsk.rrc_width_sym <= 16); + assert (D->u.afsk.rrc_rolloff >= 0. && D->u.afsk.rrc_rolloff <= 1.); -/* - * A non-whole number of cycles results in a DC bias. - * Let's see if it helps to take it out. - * Actually makes things worse: 20 fewer decoded. - * Might want to try again after EXPERIMENTC. - */ + D->lp_filter_taps = ((int) (D->u.afsk.rrc_width_sym * (float)samples_per_sec / baud)) | 1; // odd works better -#if 0 -#ifndef AVOID_FLOATING_POINT + TUNE("TUNE_LP_FILTER_TAPS", D->lp_filter_taps, "lp_filter_taps (RRC)", "%d") -failed experiment + if (D->lp_filter_taps > MAX_FILTER_SIZE) { + text_color_set(DW_COLOR_ERROR); + dw_printf ("Calculated RRC low pass filter size of %d is too large.\n", D->lp_filter_taps); + dw_printf ("Decrease the audio sample rate or increase the decimation factor or\n"); + dw_printf ("recompile the application with MAX_FILTER_SIZE larger than %d.\n", MAX_FILTER_SIZE); + D->lp_filter_taps = (MAX_FILTER_SIZE - 1) | 1; + } - dc_bias = 0; - for (j=0; jms_filter_size; j++) { - dc_bias += D->m_sin_table[j]; - } - for (j=0; jms_filter_size; j++) { - D->m_sin_table[j] -= dc_bias / D->ms_filter_size; + assert (D->lp_filter_taps > 8 && D->lp_filter_taps <= MAX_FILTER_SIZE); + (void)gen_rrc_lowpass (D->lp_filter, D->lp_filter_taps, D->u.afsk.rrc_rolloff, (float)samples_per_sec / baud); } + else { + D->lp_filter_taps = (int) round( D->lp_filter_width_sym * (float)samples_per_sec / (float)baud ); - dc_bias = 0; - for (j=0; jms_filter_size; j++) { - dc_bias += D->m_cos_table[j]; - } - for (j=0; jms_filter_size; j++) { - D->m_cos_table[j] -= dc_bias / D->ms_filter_size; - } + TUNE("TUNE_LP_FILTER_TAPS", D->lp_filter_taps, "lp_filter_taps (FIR)", "%d") + if (D->lp_filter_taps > MAX_FILTER_SIZE) { + text_color_set (DW_COLOR_ERROR); + dw_printf ("Calculated FIR low pass filter size of %d is too large.\n", D->lp_filter_taps); + dw_printf ("Decrease the audio sample rate or increase the decimation factor or\n"); + dw_printf ("recompile the application with MAX_FILTER_SIZE larger than %d.\n", MAX_FILTER_SIZE); + D->lp_filter_taps = (MAX_FILTER_SIZE - 1) | 1; + } - dc_bias = 0; - for (j=0; jms_filter_size; j++) { - dc_bias += D->s_sin_table[j]; - } - for (j=0; jms_filter_size; j++) { - D->s_sin_table[j] -= dc_bias / D->ms_filter_size; - } + assert (D->lp_filter_taps > 8 && D->lp_filter_taps <= MAX_FILTER_SIZE); - dc_bias = 0; - for (j=0; jms_filter_size; j++) { - dc_bias += D->s_cos_table[j]; - } - for (j=0; jms_filter_size; j++) { - D->s_cos_table[j] -= dc_bias / D->ms_filter_size; + float fc = baud * D->lpf_baud / (float)samples_per_sec; + gen_lowpass (fc, D->lp_filter, D->lp_filter_taps, D->lp_window); } -#endif -#endif /* - * In version 1.2 we try another experiment. - * Try using multiple slicing points instead of the traditional AGC. + * Starting with version 1.2 + * try using multiple slicing points instead of the traditional AGC. */ - space_gain[0] = MIN_G; float step = powf(10.0, log10f(MAX_G/MIN_G) / (MAX_SUBCHANS-1)); for (j=1; j= 0 && chan < MAX_CHANS); assert (subchan >= 0 && subchan < MAX_SUBCHANS); /* - * Filters use last 'filter_size' samples. + * Filters use last 'filter_taps' samples. * * First push the older samples down. * @@ -588,187 +622,182 @@ void demod_afsk_process_sample (int chan, int subchan, int sam, struct demodulat * Future project? Can we do better than shifting each time? */ - /* Scale to nice number, TODO: range -1.0 to +1.0, not 2. */ + /* Scale to nice number. */ - fsam = sam / 16384.0f; + float fsam = (float)sam / 16384.0f; - //abs_fsam = fsam >= 0.0f ? fsam : -fsam; + switch (D->profile) { + case 'E': + default: + case 'A': { + /* ========== New in Version 1.7 ========== */ -/* - * Optional bandpass filter before the mark/space discriminator. - */ - -// FIXME: calculate how much we really need. - - int extra = 0; + // Cleaner & simpler than earlier 'A' thru 'E' - if (D->use_prefilter) { - float cleaner; + if (D->use_prefilter) { + push_sample (fsam, D->raw_cb, D->pre_filter_taps); + fsam = convolve (D->raw_cb, D->pre_filter, D->pre_filter_taps); + } - push_sample (fsam, D->raw_cb, D->pre_filter_size); - cleaner = convolve (D->raw_cb, D->pre_filter, D->pre_filter_size); - push_sample (cleaner, D->ms_in_cb, D->ms_filter_size + extra); - } - else { - push_sample (fsam, D->ms_in_cb, D->ms_filter_size + extra); - } + push_sample (fsam * fcos256(D->u.afsk.m_osc_phase), D->u.afsk.m_I_raw, D->lp_filter_taps); + push_sample (fsam * fsin256(D->u.afsk.m_osc_phase), D->u.afsk.m_Q_raw, D->lp_filter_taps); + D->u.afsk.m_osc_phase += D->u.afsk.m_osc_delta; -/* - * Next we have bandpass filters for the mark and space tones. - */ + push_sample (fsam * fcos256(D->u.afsk.s_osc_phase), D->u.afsk.s_I_raw, D->lp_filter_taps); + push_sample (fsam * fsin256(D->u.afsk.s_osc_phase), D->u.afsk.s_Q_raw, D->lp_filter_taps); + D->u.afsk.s_osc_phase += D->u.afsk.s_osc_delta; -/* - * find amplitude of "Mark" tone. - */ - m_sum1 = convolve (D->ms_in_cb, D->m_sin_table, D->ms_filter_size); - m_sum2 = convolve (D->ms_in_cb, D->m_cos_table, D->ms_filter_size); + float m_I = convolve (D->u.afsk.m_I_raw, D->lp_filter, D->lp_filter_taps); + float m_Q = convolve (D->u.afsk.m_Q_raw, D->lp_filter, D->lp_filter_taps); + float m_amp = fast_hypot(m_I, m_Q); - m_amp = sqrtf(m_sum1 * m_sum1 + m_sum2 * m_sum2); + float s_I = convolve (D->u.afsk.s_I_raw, D->lp_filter, D->lp_filter_taps); + float s_Q = convolve (D->u.afsk.s_Q_raw, D->lp_filter, D->lp_filter_taps); + float s_amp = fast_hypot(s_I, s_Q); /* - * Find amplitude of "Space" tone. - */ - s_sum1 = convolve (D->ms_in_cb, D->s_sin_table, D->ms_filter_size); - s_sum2 = convolve (D->ms_in_cb, D->s_cos_table, D->ms_filter_size); - - s_amp = sqrtf(s_sum1 * s_sum1 + s_sum2 * s_sum2); - - -/* - * Apply some low pass filtering BEFORE the AGC to remove - * overshoot, ringing, and other bad stuff. - * - * A simple IIR filter is faster but FIR produces better results. - * - * It is a balancing act between removing high frequency components - * from the tone dectection while letting the data thru. + * Capture the mark and space peak amplitudes for display. + * It uses fast attack and slow decay to get an idea of the + * overall amplitude. */ + if (m_amp >= D->alevel_mark_peak) { + D->alevel_mark_peak = m_amp * D->quick_attack + D->alevel_mark_peak * (1.0f - D->quick_attack); + } + else { + D->alevel_mark_peak = m_amp * D->sluggish_decay + D->alevel_mark_peak * (1.0f - D->sluggish_decay); + } + + if (s_amp >= D->alevel_space_peak) { + D->alevel_space_peak = s_amp * D->quick_attack + D->alevel_space_peak * (1.0f - D->quick_attack); + } + else { + D->alevel_space_peak = s_amp * D->sluggish_decay + D->alevel_space_peak * (1.0f - D->sluggish_decay); + } + + if (D->num_slicers <= 1) { + + // Which tone is stonger? That's simple with an ideal signal. + // However, we don't see too many ideal signals. + // Due to mismatching pre-emphasis and de-emphasis, the two + // tones will often have greatly different amplitudes so we use + // automatic gain control (AGC) to scale each to the same range + // before comparing. + // This is probably over complicated and could be combined with + // the signal amplitude measurement, above. + // It works so let's move along to other topics. + + float m_norm = agc (m_amp, D->agc_fast_attack, D->agc_slow_decay, &(D->m_peak), &(D->m_valley)); + float s_norm = agc (s_amp, D->agc_fast_attack, D->agc_slow_decay, &(D->s_peak), &(D->s_valley)); + + // The normalized values should be around -0.5 to +0.5 so the difference + // should work out to be around -1 to +1. + // This is important because nudge_pll uses the demod_out amplitude to assign + // a quality or confidence score to the symbol. + + float demod_out = m_norm - s_norm; + + // Tested and it looks good. Range of about -1 to +1. + //printf ("JWL DEBUG demod A with agc = %6.2f\n", demod_out); + + nudge_pll (chan, subchan, 0, demod_out, D, 1.0); + + } + else { + // Multiple slice case. + // Rather than trying to find the best threshold location, use multiple + // slicer thresholds in parallel. + // The best slicing point will vary from packet to packet but should + // remain about the same for a given packet. + + // We are not performing the AGC step here but still want the envelope + // for caluculating the confidence level (or quality) of the sample. + + (void) agc (m_amp, D->agc_fast_attack, D->agc_slow_decay, &(D->m_peak), &(D->m_valley)); + (void) agc (s_amp, D->agc_fast_attack, D->agc_slow_decay, &(D->s_peak), &(D->s_valley)); + + for (int slice=0; slicenum_slicers; slice++) { + float demod_out = m_amp - s_amp * space_gain[slice]; + float amp = 0.5f * (D->m_peak - D->m_valley + (D->s_peak - D->s_valley) * space_gain[slice]); + if (amp < 0.0000001f) amp = 1; // avoid divide by zero with no signal. + + // Tested and it looks good. Range of about -1 to +1 relative to amp. + // Biased one way or the other depending on the space gain. + //printf ("JWL DEBUG demod A with slicer %d: %6.2f / %6.2f = %6.2f\n", slice, demod_out, amp, demod_out/amp); + + nudge_pll (chan, subchan, slice, demod_out, D, amp); + } + } + } + break; - if (D->lpf_use_fir) { + case 'D': + case 'B': { + /* ========== Version 1.7 Experiment ========== */ - push_sample (m_amp, D->m_amp_cb, D->lp_filter_size); - m_amp = convolve (D->m_amp_cb, D->lp_filter, D->lp_filter_size); + // New - Convert frequency to a value proportional to frequency. - push_sample (s_amp, D->s_amp_cb, D->lp_filter_size); - s_amp = convolve (D->s_amp_cb, D->lp_filter, D->lp_filter_size); - } - else { - - /* Original, but faster, IIR. */ + if (D->use_prefilter) { + push_sample (fsam, D->raw_cb, D->pre_filter_taps); + fsam = convolve (D->raw_cb, D->pre_filter, D->pre_filter_taps); + } - m_amp = D->lpf_iir * m_amp + (1.0f - D->lpf_iir) * D->m_amp_prev; - D->m_amp_prev = m_amp; + push_sample (fsam * fcos256(D->u.afsk.c_osc_phase), D->u.afsk.c_I_raw, D->lp_filter_taps); + push_sample (fsam * fsin256(D->u.afsk.c_osc_phase), D->u.afsk.c_Q_raw, D->lp_filter_taps); + D->u.afsk.c_osc_phase += D->u.afsk.c_osc_delta; - s_amp = D->lpf_iir * s_amp + (1.0f - D->lpf_iir) * D->s_amp_prev; - D->s_amp_prev = s_amp; - } + float c_I = convolve (D->u.afsk.c_I_raw, D->lp_filter, D->lp_filter_taps); + float c_Q = convolve (D->u.afsk.c_Q_raw, D->lp_filter, D->lp_filter_taps); -/* - * Version 1.2: Try new approach to capturing the amplitude for display. - * This is same as the AGC above without the normalization step. - * We want decay to be substantially slower to get a longer - * range idea of the received audio. - */ + float phase = atan2f (c_Q, c_I); + float rate = phase - D->u.afsk.prev_phase; + if (rate > M_PI) rate -= 2 * M_PI; + else if (rate < -M_PI) rate += 2 * M_PI; + D->u.afsk.prev_phase = phase; - if (m_amp >= D->alevel_mark_peak) { - D->alevel_mark_peak = m_amp * D->quick_attack + D->alevel_mark_peak * (1.0f - D->quick_attack); - } - else { - D->alevel_mark_peak = m_amp * D->sluggish_decay + D->alevel_mark_peak * (1.0f - D->sluggish_decay); - } + // Rate is radians per audio sample interval or something like that. + // Scale scale that into -1 to +1 for expected tones. - if (s_amp >= D->alevel_space_peak) { - D->alevel_space_peak = s_amp * D->quick_attack + D->alevel_space_peak * (1.0f - D->quick_attack); - } - else { - D->alevel_space_peak = s_amp * D->sluggish_decay + D->alevel_space_peak * (1.0f - D->sluggish_decay); - } + float norm_rate = rate * D->u.afsk.normalize_rpsam; + // We really don't have mark and space amplitudes available in this case. -/* - * Which tone is stronger? - * - * In an ideal world, simply compare. In my first naive attempt, that - * worked perfectly with perfect signals. In the real world, we don't - * have too many perfect signals. - * - * Here is an excellent explanation: - * http://www.febo.com/packet/layer-one/transmit.html - * - * Under real conditions, we find that the higher tone usually has a - * considerably smaller amplitude due to the passband characteristics - * of the transmitter and receiver. To make matters worse, it - * varies considerably from one station to another. - * - * The two filters also have different amounts of DC bias. - * - * My solution was to apply automatic gain control (AGC) to the mark and space - * levels. This works by looking at the minimum and maximum outputs - * for each filter and scaling the results to be roughly in the -0.5 to +0.5 range. - * Results were excellent after tweaking the attack and decay times. - * - * 4X6IZ took a different approach. See QEX Jul-Aug 2012. - * - * He ran two different demodulators in parallel. One of them boosted the higher - * frequency tone by 6 dB. Any duplicates were removed. This produced similar results. - * He also used a bandpass filter before the mark/space filters. - * I haven't tried this combination yet for 1200 baud. - * - * First, let's take a look at Track 1 of the TNC test CD. Here the receiver - * has a flat response. We find the mark/space strength ratios very from 0.53 to 1.38 - * with a median of 0.81. This in in line with expections because most - * transmitters add pre-emphasis to boost the higher audio frequencies. - * Track 2 should more closely resemble what comes out of the speaker on a typical - * transceiver. Here we see a ratio from 1.73 to 3.81 with a median of 2.48. - * - * This is similar to my observations of local signals, from the speaker. - * The amplitude ratio varies from 1.48 to 3.41 with a median of 2.70. - * - * Rather than only two filters, let's try slicing the data in more places. - */ + if (D->num_slicers <= 1) { - /* Fast attack and slow decay. */ - /* Numbers were obtained by trial and error from actual */ - /* recorded less-than-optimal signals. */ + float demod_out = norm_rate; + // Tested and it looks good. Range roughly -1 to +1. + //printf ("JWL DEBUG demod B single = %6.2f\n", demod_out); - /* See fsk_demod_agc.h for more information. */ + nudge_pll (chan, subchan, 0, demod_out, D, 1.0); - m_norm = agc (m_amp, D->agc_fast_attack, D->agc_slow_decay, &(D->m_peak), &(D->m_valley)); - s_norm = agc (s_amp, D->agc_fast_attack, D->agc_slow_decay, &(D->s_peak), &(D->s_valley)); + } + else { - if (D->num_slicers <= 1) { + // This would be useful for HF SSB where a tuning error + // would shift the frequency. Multiple slicing points would + // then compensate for differences in transmit/receive frequencies. + // + // Where should we set the thresholds? + // I'm thinking something like: + // -.5 -.375 -.25 -.125 0 .125 .25 .375 .5 + // + // Assuming a 300 Hz shift, this would put slicing thresholds up + // to +-75 Hz from the center. - /* Normal case of one demodulator to one HDLC decoder. */ - /* Demodulator output is difference between response from two filters. */ - /* AGC should generally keep this around -1 to +1 range. */ + for (int slice=0; slicenum_slicers; slice++) { - demod_out = m_norm - s_norm; + float offset = -0.5 + slice * (1. / (D->num_slicers - 1)); + float demod_out = norm_rate + offset; - /* Try adding some Hysteresis. */ - /* (Not to be confused with Hysteria.) */ + //printf ("JWL DEBUG demod B slice %d, offset = %6.3f, demod_out = %6.2f\n", slice, offset, demod_out); - if (demod_out > D->hysteresis) { - demod_data = 1; + nudge_pll (chan, subchan, slice, demod_out, D, 1.0); + } } - else if (demod_out < (- (D->hysteresis))) { - demod_data = 0; - } - else { - demod_data = D->slicer[subchan].prev_demod_data; } - nudge_pll (chan, subchan, 0, demod_data, D); + break; } - else { - int slice; - - for (slice=0; slicenum_slicers; slice++) { - demod_data = m_amp > s_amp * space_gain[slice]; - nudge_pll (chan, subchan, slice, demod_data, D); - } - } - - + #if DEBUG4 if (chan == 0) { @@ -805,9 +834,6 @@ void demod_afsk_process_sample (int chan, int subchan, int sam, struct demodulat } /* end demod_afsk_process_sample */ -__attribute__((hot)) -inline static void nudge_pll (int chan, int subchan, int slice, int demod_data, struct demodulator_state_s *D) -{ /* * Finally, a PLL is used to sample near the centers of the data bits. @@ -831,7 +857,7 @@ inline static void nudge_pll (int chan, int subchan, int slice, int demod_data, * If we adjust it too quickly, the clock will have too much jitter. * If we adjust it too slowly, it will take too long to lock on to a new signal. * - * Be a little more agressive about adjusting the PLL + * Be a little more aggressive about adjusting the PLL * phase when searching for a signal. Don't change it as much when * locked on to a signal. * @@ -839,8 +865,12 @@ inline static void nudge_pll (int chan, int subchan, int slice, int demod_data, * because this happens for each transition from the demodulator. */ +__attribute__((hot)) +static void nudge_pll (int chan, int subchan, int slice, float demod_out, struct demodulator_state_s *D, float amplitude) +{ D->slicer[slice].prev_d_c_pll = D->slicer[slice].data_clock_pll; + // Perform the add as unsigned to avoid signed overflow error. D->slicer[slice].data_clock_pll = (signed)((unsigned)(D->slicer[slice].data_clock_pll) + (unsigned)(D->pll_step_per_sample)); @@ -850,22 +880,57 @@ inline static void nudge_pll (int chan, int subchan, int slice, int demod_data, if (D->slicer[slice].data_clock_pll < 0 && D->slicer[slice].prev_d_c_pll > 0) { /* Overflow - this is where we sample. */ - hdlc_rec_bit (chan, subchan, slice, demod_data, 0, -1); + // Assign it a confidence level or quality, 0 to 100, based on the amplitude. + // Those very close to 0 are suspect. We'll get back to this later. + + int quality = fabsf(demod_out) * 100.0f / amplitude; + if (quality > 100) quality = 100; + +#if DEBUG5 + // Write bit stream to a file. + + static FILE *bsfp = NULL; + static int bcount = 0; + if (chan == 0 && subchan == 0 && slice == 0) { + if (bsfp == NULL) { + bsfp = fopen ("bitstream.txt", "w"); + } + fprintf (bsfp, "%d", demod_out > 0); + bcount++; + if (bcount % 64 == 0) { + fprintf (bsfp, "\n"); + } + } + +#endif + + +#if 1 + hdlc_rec_bit (chan, subchan, slice, demod_out > 0, 0, quality); +#else // TODO: new feature to measure data speed error. +// Maybe hdlc_rec_bit could provide indication when frame starts. + hdlc_rec_bit_new (chan, subchan, slice, demod_out > 0, 0, quality, + &(D->slicer[slice].pll_nudge_total), &(D->slicer[slice].pll_symbol_count)); + D->slicer[slice].pll_symbol_count++; +#endif pll_dcd_each_symbol2 (D, chan, subchan, slice); } // Transitions nudge the DPLL phase toward the incoming signal. + int demod_data = demod_out > 0; if (demod_data != D->slicer[slice].prev_demod_data) { pll_dcd_signal_transition2 (D, slice, D->slicer[slice].data_clock_pll); +// TODO: 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); } else { D->slicer[slice].data_clock_pll = (int)(D->slicer[slice].data_clock_pll * D->pll_searching_inertia); } +// TODO: D->slicer[slice].pll_nudge_total += (int64_t)((signed int)(D->slicer[slice].data_clock_pll)) - (int64_t)before; } /* diff --git a/src/demod_psk.c b/src/demod_psk.c index f01ee217..bc058185 100644 --- a/src/demod_psk.c +++ b/src/demod_psk.c @@ -60,7 +60,7 @@ * "bis" and "ter" are from Latin for second and third. * I used the "ter" version which has phase shifts of 0, 90, 180, and 270 degrees. * - * There are ealier references to an alternative B which uses other phase shifts offset + * There are earlier references to an alternative B which uses other phase shifts offset * by another 45 degrees. * * After getting QPSK working, it was not much more effort to add V.27 with 8 phases. @@ -501,7 +501,7 @@ void demod_psk_init (enum modem_t modem_type, enum v26_e v26_alt, int samples_pe */ float fc = correct_baud * D->u.psk.lpf_baud / (float)samples_per_sec; - gen_lowpass (fc, D->u.psk.lp_filter, D->u.psk.lp_filter_taps, D->u.psk.lp_window, 0); + gen_lowpass (fc, D->u.psk.lp_filter, D->u.psk.lp_filter_taps, D->u.psk.lp_window); /* * No point in having multiple numbers for signal level. @@ -781,7 +781,7 @@ static void nudge_pll (int chan, int subchan, int slice, int demod_bits, struct * If we adjust it too quickly, the clock will have too much jitter. * If we adjust it too slowly, it will take too long to lock on to a new signal. * - * Be a little more agressive about adjusting the PLL + * Be a little more aggressive about adjusting the PLL * phase when searching for a signal. * Don't change it as much when locked on to a signal. */ diff --git a/src/digipeater.c b/src/digipeater.c index 1e4c8147..fbe89370 100644 --- a/src/digipeater.c +++ b/src/digipeater.c @@ -49,7 +49,8 @@ * Preemptive Digipeating (new in version 0.8) * * http://www.aprs.org/aprs12/preemptive-digipeating.txt - * + * I ignored the part about the RR bits. + * *------------------------------------------------------------------*/ #define DIGIPEATER_C @@ -73,7 +74,7 @@ static packet_t digipeat_match (int from_chan, packet_t pp, char *mycall_rec, char *mycall_xmit, - regex_t *uidigi, regex_t *uitrace, int to_chan, enum preempt_e preempt, char *type_filter); + regex_t *uidigi, regex_t *uitrace, int to_chan, enum preempt_e preempt, char *atgp, char *type_filter); /* @@ -154,8 +155,8 @@ 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 || - (save_audio_config_p->achan[from_chan].medium != MEDIUM_RADIO && - save_audio_config_p->achan[from_chan].medium != MEDIUM_NETTNC)) { + (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 ("APRS digipeater: Did not expect to receive on invalid channel %d.\n", from_chan); } @@ -164,7 +165,34 @@ void digipeater (int from_chan, packet_t pp) /* * First pass: Look at packets being digipeated to same channel. * - * We want these to get out quickly. + * We want these to get out quickly, bypassing the usual random wait time. + * + * Some may disagree but I followed what WB4APR had to say about it. + * + * http://www.aprs.org/balloons.html + * + * APRS NETWORK FRATRICIDE: Generally, all APRS digipeaters are supposed to transmit + * immediately and all at the same time. They should NOT wait long enough for each + * one to QRM the channel with the same copy of each packet. NO, APRS digipeaters + * are all supposed to STEP ON EACH OTHER with every packet. This makes sure that + * everyone in range of a digi will hear one and only one copy of each packet. + * and that the packet will digipeat OUTWARD and not backward. The goal is that a + * digipeated packet is cleared out of the local area in ONE packet time and not + * N packet times for every N digipeaters that heard the packet. This means no + * PERSIST times, no DWAIT times and no UIDWAIT times. Notice, this is contrary + * to other packet systems that might want to guarantee delivery (but at the + * expense of throughput). APRS wants to clear the channel quickly to maximize throughput. + * + * http://www.aprs.org/kpc3/kpc3+WIDEn.txt + * + * THIRD: Eliminate the settings that are detrimental to the network. + * + * * UIDWAIT should be OFF. (the default). With it on, your digi is not doing the + * fundamental APRS fratricide that is the primary mechanism for minimizing channel + * loading. All digis that hear the same packet are supposed to DIGI it at the SAME + * time so that all those copies only take up one additional time slot. (but outward + * located digs will hear it without collision (and continue outward propagation) + * */ for (to_chan=0; to_chanachan[to_chan].mycall, &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]); if (result != NULL) { dedupe_remember (pp, to_chan); - tq_append (to_chan, TQ_PRIO_0_HI, result); + tq_append (to_chan, TQ_PRIO_0_HI, result); // High priority queue. digi_count[from_chan][to_chan]++; } } @@ -202,10 +231,11 @@ void digipeater (int from_chan, packet_t pp) 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], 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]); if (result != NULL) { dedupe_remember (pp, to_chan); - tq_append (to_chan, TQ_PRIO_1_LO, result); + tq_append (to_chan, TQ_PRIO_1_LO, result); // Low priority queue. digi_count[from_chan][to_chan]++; } } @@ -244,6 +274,9 @@ void digipeater (int from_chan, packet_t pp) * * preempt - Option for "preemptive" digipeating. * + * atgp - No tracing if this matches alias prefix. + * Hack added for special needs of ATGP. + * * filter_str - Filter expression string or NULL. * * Returns: Packet object for transmission or NULL. @@ -260,32 +293,9 @@ void digipeater (int from_chan, packet_t pp) * *------------------------------------------------------------------------------*/ -#define OBSOLETE14 1 - - -#ifndef OBSOLETE14 -static char *dest_ssid_path[16] = { - "", /* Use VIA path */ - "WIDE1-1", - "WIDE2-2", - "WIDE3-3", - "WIDE4-4", - "WIDE5-5", - "WIDE6-6", - "WIDE7-7", - "WIDE1-1", /* North */ - "WIDE1-1", /* South */ - "WIDE1-1", /* East */ - "WIDE1-1", /* West */ - "WIDE2-2", /* North */ - "WIDE2-2", /* South */ - "WIDE2-2", /* East */ - "WIDE2-2" }; /* West */ -#endif - static packet_t digipeat_match (int from_chan, packet_t pp, char *mycall_rec, char *mycall_xmit, - regex_t *alias, regex_t *wide, int to_chan, enum preempt_e preempt, char *filter_str) + regex_t *alias, regex_t *wide, int to_chan, enum preempt_e preempt, char *atgp, char *filter_str) { char source[AX25_MAX_ADDR_LEN]; int ssid; @@ -323,15 +333,6 @@ static packet_t digipeat_match (int from_chan, packet_t pp, char *mycall_rec, ch * Otherwise we don't want to modify the input because this could be called multiple times. */ -#ifndef OBSOLETE14 // Took it out in 1.4 - - if (ax25_get_num_repeaters(pp) == 0 && (ssid = ax25_get_ssid(pp, AX25_DESTINATION)) > 0) { - ax25_set_addr(pp, AX25_REPEATER_1, dest_ssid_path[ssid]); - ax25_set_ssid(pp, AX25_DESTINATION, 0); - /* Continue with general case, below. */ - } -#endif - /* * Find the first repeater station which doesn't have "has been repeated" set. @@ -440,6 +441,10 @@ static packet_t digipeat_match (int from_chan, packet_t pp, char *mycall_rec, ch /* * If preemptive digipeating is enabled, try matching my call * and aliases against all remaining unused digipeaters. + * + * Bob says: "GENERIC XXXXn-N DIGIPEATING should not do preemptive digipeating." + * + * But consider this case: https://github.com/wb2osz/direwolf/issues/488 */ if (preempt != PREEMPT_OFF) { @@ -465,13 +470,22 @@ static packet_t digipeat_match (int from_chan, packet_t pp, char *mycall_rec, ch switch (preempt) { case PREEMPT_DROP: /* remove all prior */ + // TODO: deprecate this option. Result is misleading. + + text_color_set (DW_COLOR_ERROR); + dw_printf ("The digipeat DROP option will be removed in a future release. Use PREEMPT for preemptive digipeating.\n"); + while (r2 > AX25_REPEATER_1) { ax25_remove_addr (result, r2-1); r2--; } break; - case PREEMPT_MARK: + case PREEMPT_MARK: // TODO: deprecate this option. Result is misleading. + + text_color_set (DW_COLOR_ERROR); + dw_printf ("The digipeat MARK option will be removed in a future release. Use PREEMPT for preemptive digipeating.\n"); + r2--; while (r2 >= AX25_REPEATER_1 && ax25_get_h(result,r2) == 0) { ax25_set_h (result, r2); @@ -479,7 +493,12 @@ static packet_t digipeat_match (int from_chan, packet_t pp, char *mycall_rec, ch } break; - case PREEMPT_TRACE: /* remove prior unused */ + case PREEMPT_TRACE: /* My enhancement - remove prior unused digis. */ + /* this provides an accurate path of where packet traveled. */ + + // Uh oh. It looks like sample config files went out + // with this option. Should it be renamed as + // PREEMPT which is more descriptive? default: while (r2 > AX25_REPEATER_1 && ax25_get_h(result,r2-1) == 0) { ax25_remove_addr (result, r2-1); @@ -510,6 +529,40 @@ static packet_t digipeat_match (int from_chan, packet_t pp, char *mycall_rec, ch err = regexec(wide,repeater,0,NULL,0); if (err == 0) { +// Special hack added for ATGP to behave like some combination of options in some old TNC +// so the via path does not continue to grow and exceed the 8 available positions. +// The strange thing about this is that the used up digipeater is left there but +// removed by the next digipeater. + + if (strlen(atgp) > 0 && strncasecmp(repeater, atgp, strlen(atgp)) == 0) { + + if (ssid >= 1 && ssid <= 7) { + packet_t result; + + result = ax25_dup (pp); + assert (result != NULL); + + // First, remove any already used digipeaters. + + while (ax25_get_num_addr(result) >= 3 && ax25_get_h(result,AX25_REPEATER_1) == 1) { + ax25_remove_addr (result, AX25_REPEATER_1); + r--; + } + + ssid = ssid - 1; + ax25_set_ssid(result, r, ssid); // could be zero. + if (ssid == 0) { + ax25_set_h (result, r); + } + + // Insert own call at beginning and mark it used. + + ax25_insert_addr (result, AX25_REPEATER_1, mycall_xmit); + ax25_set_h (result, AX25_REPEATER_1); + return (result); + } + } + /* * If ssid == 1, we simply replace the repeater with my call and * mark it as being used. @@ -608,7 +661,7 @@ void digi_regen (int from_chan, packet_t pp) * * Name: main * - * Purpose: Standalone test case for this funtionality. + * Purpose: Standalone test case for this functionality. * * Usage: make -f Makefile. dtest * ./dtest @@ -617,7 +670,7 @@ void digi_regen (int from_chan, packet_t pp) #if DIGITEST -static char mycall[] = "WB2OSZ-9"; +static char mycall[12]; static regex_t alias_re; @@ -627,6 +680,7 @@ static int failed; static enum preempt_e preempt = PREEMPT_OFF; +static char config_atgp[AX25_MAX_ADDR_LEN] = "HOP"; static void test (char *in, char *out) @@ -640,6 +694,7 @@ static void test (char *in, char *out) int frame_len; alevel_t alevel; + dw_printf ("\n"); /* @@ -689,9 +744,9 @@ static void test (char *in, char *out) text_color_set(DW_COLOR_REC); dw_printf ("Rec\t%s\n", rec); -//TODO: Add filtering to test. -// V - result = digipeat_match (0, pp, mycall, mycall, &alias_re, &wide_re, 0, preempt, NULL); +//TODO: Add filtering to test. +// V + result = digipeat_match (0, pp, mycall, mycall, &alias_re, &wide_re, 0, preempt, config_atgp, NULL); if (result != NULL) { @@ -726,6 +781,7 @@ int main (int argc, char *argv[]) int e; failed = 0; char message[256]; + strlcpy(mycall, "WB2OSZ-9", sizeof(mycall)); dedupe_init (4); @@ -740,7 +796,7 @@ int main (int argc, char *argv[]) exit (1); } - e = regcomp (&wide_re, "^WIDE[1-7]-[1-7]$|^TRACE[1-7]-[1-7]$|^MA[1-7]-[1-7]$", REG_EXTENDED|REG_NOSUB); + e = regcomp (&wide_re, "^WIDE[1-7]-[1-7]$|^TRACE[1-7]-[1-7]$|^MA[1-7]-[1-7]$|^HOP[1-7]-[1-7]$", REG_EXTENDED|REG_NOSUB); if (e != 0) { regerror (e, &wide_re, message, sizeof(message)); text_color_set(DW_COLOR_ERROR); @@ -830,11 +886,8 @@ int main (int argc, char *argv[]) */ test ( "W1ABC>TEST-3:", -#ifndef OBSOLETE14 - "W1ABC>TEST,WB2OSZ-9*,WIDE3-2:"); -#else ""); -#endif + test ( "W1DEF>TEST-3,WIDE2-2:", "W1DEF>TEST-3,WB2OSZ-9*,WIDE2-1:"); @@ -844,17 +897,17 @@ int main (int argc, char *argv[]) * The 4th case might be controversial. */ - test ( "W1XYZ>TEST,R1*,WIDE3-2:info1", - "W1XYZ>TEST,R1,WB2OSZ-9*,WIDE3-1:info1"); + test ( "W1XYZ>TESTD,R1*,WIDE3-2:info1", + "W1XYZ>TESTD,R1,WB2OSZ-9*,WIDE3-1:info1"); - test ( "W1XYZ>TEST,R2*,WIDE3-2:info1", + test ( "W1XYZ>TESTD,R2*,WIDE3-2:info1", ""); - test ( "W1XYZ>TEST,R3*,WIDE3-2:info1", + test ( "W1XYZ>TESTD,R3*,WIDE3-2:info1", ""); - test ( "W1XYZ>TEST,R1*,WB2OSZ-9:has explicit routing", - "W1XYZ>TEST,R1,WB2OSZ-9*:has explicit routing"); + test ( "W1XYZ>TESTD,R1*,WB2OSZ-9:has explicit routing", + "W1XYZ>TESTD,R1,WB2OSZ-9*:has explicit routing"); /* @@ -931,6 +984,54 @@ int main (int argc, char *argv[]) test ( "WB2OSZ-15>TEST14,WIDE1-1,WIDE1-1:stuff", "WB2OSZ-15>TEST14,WB2OSZ-9*,WIDE1-1:stuff"); +// New in 1.7 - ATGP Hack + + preempt = PREEMPT_OFF; // Shouldn't make a difference here. + + test ( "W1ABC>TEST51,HOP7-7,HOP7-7:stuff1", + "W1ABC>TEST51,WB2OSZ-9*,HOP7-6,HOP7-7:stuff1"); + + test ( "W1ABC>TEST52,ABCD*,HOP7-1,HOP7-7:stuff2", + "W1ABC>TEST52,WB2OSZ-9,HOP7*,HOP7-7:stuff2"); // Used up address remains. + + test ( "W1ABC>TEST53,HOP7*,HOP7-7:stuff3", + "W1ABC>TEST53,WB2OSZ-9*,HOP7-6:stuff3"); // But it gets removed here. + + test ( "W1ABC>TEST54,HOP7*,HOP7-1:stuff4", + "W1ABC>TEST54,WB2OSZ-9,HOP7*:stuff4"); // Remains again here. + + test ( "W1ABC>TEST55,HOP7,HOP7*:stuff5", + ""); + +// Examples given for desired result. + + strlcpy (mycall, "CLNGMN-1", sizeof(mycall)); + test ( "W1ABC>TEST60,HOP7-7,HOP7-7:", + "W1ABC>TEST60,CLNGMN-1*,HOP7-6,HOP7-7:"); + test ( "W1ABC>TEST61,ROAN-3*,HOP7-6,HOP7-7:", + "W1ABC>TEST61,CLNGMN-1*,HOP7-5,HOP7-7:"); + + strlcpy (mycall, "GDHILL-8", sizeof(mycall)); + test ( "W1ABC>TEST62,MDMTNS-7*,HOP7-1,HOP7-7:", + "W1ABC>TEST62,GDHILL-8,HOP7*,HOP7-7:"); + test ( "W1ABC>TEST63,CAMLBK-9*,HOP7-1,HOP7-7:", + "W1ABC>TEST63,GDHILL-8,HOP7*,HOP7-7:"); + + strlcpy (mycall, "MDMTNS-7", sizeof(mycall)); + test ( "W1ABC>TEST64,GDHILL-8*,HOP7*,HOP7-7:", + "W1ABC>TEST64,MDMTNS-7*,HOP7-6:"); + + strlcpy (mycall, "CAMLBK-9", sizeof(mycall)); + test ( "W1ABC>TEST65,GDHILL-8,HOP7*,HOP7-7:", + "W1ABC>TEST65,CAMLBK-9*,HOP7-6:"); + + strlcpy (mycall, "KATHDN-15", sizeof(mycall)); + test ( "W1ABC>TEST66,MTWASH-14*,HOP7-1:", + "W1ABC>TEST66,KATHDN-15,HOP7*:"); + + strlcpy (mycall, "SPRNGR-1", sizeof(mycall)); + test ( "W1ABC>TEST67,CLNGMN-1*,HOP7-1:", + "W1ABC>TEST67,SPRNGR-1,HOP7*:"); if (failed == 0) { diff --git a/src/digipeater.h b/src/digipeater.h index 9885859e..5c849769 100644 --- a/src/digipeater.h +++ b/src/digipeater.h @@ -1,5 +1,4 @@ - #ifndef DIGIPEATER_H #define DIGIPEATER_H 1 @@ -38,6 +37,11 @@ struct digi_config_s { enum preempt_e { PREEMPT_OFF, PREEMPT_DROP, PREEMPT_MARK, PREEMPT_TRACE } preempt[MAX_CHANS][MAX_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 *filter_str[MAX_CHANS+1][MAX_CHANS+1]; // NULL or optional Packet Filter strings such as "t/m". // Notice the size of arrays is one larger than normal. diff --git a/src/direwolf.c b/src/direwolf.c index 456b16f0..e23aecb4 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 John Langner, WB2OSZ +// Copyright (C) 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2019, 2020, 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 @@ -33,7 +33,9 @@ * Internet Gateway (IGate) * Ham Radio of Things - IoT with Ham Radio * FX.25 Forward Error Correction. - * + * IL2P Forward Error Correction. + * Emergency Alert System (EAS) Specific Area Message Encoding (SAME) receiver. + * AIS receiver for tracking ships. * *---------------------------------------------------------------*/ @@ -67,9 +69,8 @@ #include #include #include -#ifdef __OpenBSD__ -#include -#elif __APPLE__ +#if USE_SNDIO || __APPLE__ +// no need to include #else #include #endif @@ -124,7 +125,10 @@ #include "ax25_link.h" #include "dtime_now.h" #include "fx25.h" +#include "il2p.h" #include "dwsock.h" +#include "dns_sd_dw.h" +#include "dlq.h" // for fec_type_t definition. //static int idx_decoded = 0; @@ -135,7 +139,7 @@ static BOOL cleanup_win (int); static void cleanup_linux (int); #endif -static void usage (char **argv); +static void usage (); #if defined(__SSE__) && !defined(__APPLE__) @@ -193,7 +197,6 @@ int main (int argc, char *argv[]) //int eof; int j; char config_file[100]; - int xmit_calibrate_option = 0; int enable_pseudo_terminal = 0; struct digi_config_s digi_config; struct cdigi_config_s cdigi_config; @@ -223,13 +226,22 @@ int main (int argc, char *argv[]) int d_h_opt = 0; /* "-d h" option for hamlib debugging. Repeat for more detail */ #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 E_tx_opt = 0; /* "-E n" Error rate % for clobbering trasmit frames. */ + int aprstt_debug = 0; /* "-d d" option for APRStt (think Dtmf) debug. */ + + int E_tx_opt = 0; /* "-E n" Error rate % for clobbering transmit frames. */ int E_rx_opt = 0; /* "-E Rn" Error rate % for clobbering receive frames. */ float e_recv_ber = 0.0; /* Receive Bit Error Rate (BER). */ int X_fx25_xmit_enable = 0; /* FX.25 transmit enable. */ + int I_opt = -1; /* IL2P transmit, normal polarity, arg is max_fec. */ + int i_opt = -1; /* IL2P transmit, inverted polarity, arg is max_fec. */ + + char x_opt_mode = ' '; /* "-x N" option for transmitting calibration tones. */ + int x_opt_chan = 0; /* Split into 2 parts. Mode e.g. m, a, and optional channel. */ + strlcpy(l_opt_logdir, "", sizeof(l_opt_logdir)); strlcpy(L_opt_logfile, "", sizeof(L_opt_logfile)); strlcpy(P_opt, "", sizeof(P_opt)); @@ -272,6 +284,8 @@ int main (int argc, char *argv[]) /* 1 = normal, 0 = no text colors. */ /* 2, 3, ... alternate escape sequences for different terminals. */ +// FIXME: consider case of no space between t and number. + for (j=1; j= MAX_CHANS) { + text_color_set(DW_COLOR_ERROR); + dw_printf ("Invalid channel %d for -x. \n", x_opt_chan); + text_color_set(DW_COLOR_INFO); + exit (EXIT_FAILURE); + } break; case 'r': /* -r audio samples/sec. e.g. 44100 */ @@ -531,7 +599,7 @@ int main (int argc, char *argv[]) case '?': /* For '?' unknown option message was already printed. */ - usage (argv); + usage (); break; case 'd': /* Set debug option. */ @@ -566,6 +634,8 @@ int main (int argc, char *argv[]) case 'h': d_h_opt++; break; // Hamlib verbose level. #endif case 'x': d_x_opt++; break; // FX.25 + case '2': d_2_opt++; break; // IL2P + case 'd': aprstt_debug++; break; // APRStt (mnemonic Dtmf) default: break; } } @@ -653,6 +723,16 @@ int main (int argc, char *argv[]) X_fx25_xmit_enable = atoi(optarg); break; + case 'I': // IL2P, normal polarity + + I_opt = atoi(optarg); + break; + + case 'i': // IL2P, inverted polarity + + i_opt = atoi(optarg); + break; + case 'A': // -A convert AIS to APRS object A_opt_ais_to_obj = 1; @@ -663,7 +743,7 @@ int main (int argc, char *argv[]) /* Should not be here. */ text_color_set(DW_COLOR_DEBUG); dw_printf("?? getopt returned character code 0%o ??\n", c); - usage (argv); + usage (); } } /* end while(1) for options */ @@ -703,7 +783,7 @@ int main (int argc, char *argv[]) if (n_opt != 0) { audio_config.adev[0].num_channels = n_opt; if (n_opt == 2) { - audio_config.achan[1].medium = MEDIUM_RADIO; + audio_config.chan_medium[1] = MEDIUM_RADIO; } } @@ -758,7 +838,7 @@ int main (int argc, char *argv[]) // Will make more precise in afsk demod init. audio_config.achan[0].mark_freq = 2083; // Actually 2083.3 - logic 1. audio_config.achan[0].space_freq = 1563; // Actually 1562.5 - logic 0. - strlcpy (audio_config.achan[0].profiles, "D", sizeof(audio_config.achan[0].profiles)); + strlcpy (audio_config.achan[0].profiles, "A", sizeof(audio_config.achan[0].profiles)); } else { audio_config.achan[0].modem_type = MODEM_SCRAMBLE; @@ -853,7 +933,45 @@ int main (int argc, char *argv[]) audio_config.recv_ber = e_recv_ber; - audio_config.fx25_xmit_enable = X_fx25_xmit_enable; + if (X_fx25_xmit_enable > 0) { + if (I_opt != -1 || i_opt != -1) { + text_color_set(DW_COLOR_ERROR); + dw_printf ("Can't mix -X with -I or -i.\n"); + exit (EXIT_FAILURE); + } + audio_config.achan[0].fx25_strength = X_fx25_xmit_enable; + audio_config.achan[0].layer2_xmit = LAYER2_FX25; + } + + if (I_opt != -1 && i_opt != -1) { + text_color_set(DW_COLOR_ERROR); + dw_printf ("Can't use both -I and -i at the same time.\n"); + exit (EXIT_FAILURE); + } + + if (I_opt >= 0) { + audio_config.achan[0].layer2_xmit = LAYER2_IL2P; + audio_config.achan[0].il2p_max_fec = (I_opt > 0); + if (audio_config.achan[0].il2p_max_fec == 0) { + text_color_set(DW_COLOR_ERROR); + dw_printf ("It is highly recommended that 1, rather than 0, is used with -I for best results.\n"); + } + audio_config.achan[0].il2p_invert_polarity = 0; // normal + } + + if (i_opt >= 0) { + audio_config.achan[0].layer2_xmit = LAYER2_IL2P; + audio_config.achan[0].il2p_max_fec = (i_opt > 0); + if (audio_config.achan[0].il2p_max_fec == 0) { + text_color_set(DW_COLOR_ERROR); + dw_printf ("It is highly recommended that 1, rather than 0, is used with -i for best results.\n"); + } + audio_config.achan[0].il2p_invert_polarity = 1; // invert for transmit + if (audio_config.achan[0].baud == 1200) { + text_color_set(DW_COLOR_ERROR); + dw_printf ("Using -i with 1200 bps is a bad idea. Use -I instead.\n"); + } + } /* @@ -870,20 +988,22 @@ int main (int argc, char *argv[]) text_color_set(DW_COLOR_ERROR); dw_printf ("Pointless to continue without audio device.\n"); SLEEP_SEC(5); + usage (); exit (1); } /* - * Initialize the demodulator(s) and HDLC decoder. + * Initialize the demodulator(s) and layer 2 decoder (HDLC, IL2P). */ multi_modem_init (&audio_config); fx25_init (d_x_opt); + il2p_init (d_2_opt); /* * Initialize the touch tone decoder & APRStt gateway. */ dtmf_init (&audio_config, audio_amplitude); - aprs_tt_init (&tt_config); + aprs_tt_init (&tt_config, aprstt_debug); tt_user_init (&audio_config, &tt_config); /* @@ -905,30 +1025,77 @@ int main (int argc, char *argv[]) xmit_init (&audio_config, d_p_opt); /* - * If -x option specified, transmit alternating tones for transmitter + * If -x N option specified, transmit calibration tones for transmitter * audio level adjustment, up to 1 minute then quit. - * TODO: enhance for more than one channel. + * a: Alternating mark/space tones + * m: Mark tone (e.g. 1200Hz) + * s: Space tone (e.g. 2200Hz) + * p: Set PTT only. + * A leading or trailing number is the channel. */ - if (xmit_calibrate_option) { - - int max_duration = 60; /* seconds */ - int n = audio_config.achan[0].baud * max_duration; - int chan = 0; - - text_color_set(DW_COLOR_INFO); - dw_printf ("\nSending transmit calibration tones. Press control-C to terminate.\n"); - - ptt_set (OCTYPE_PTT, chan, 1); - while (n-- > 0) { - - tone_gen_put_bit (chan, n & 1); - - } - ptt_set (OCTYPE_PTT, chan, 0); - exit (0); + if (x_opt_mode != ' ') { + if (audio_config.chan_medium[x_opt_chan] == MEDIUM_RADIO) { + if (audio_config.achan[x_opt_chan].mark_freq + && audio_config.achan[x_opt_chan].space_freq) { + int max_duration = 60; + int n = audio_config.achan[x_opt_chan].baud * max_duration; + + text_color_set(DW_COLOR_INFO); + ptt_set(OCTYPE_PTT, x_opt_chan, 1); + + switch (x_opt_mode) { + default: + case 'a': // Alternating tones: -x a + dw_printf("\nSending alternating mark/space calibration tones (%d/%dHz) on channel %d.\nPress control-C to terminate.\n", + audio_config.achan[x_opt_chan].mark_freq, + audio_config.achan[x_opt_chan].space_freq, + x_opt_chan); + while (n-- > 0) { + tone_gen_put_bit(x_opt_chan, n & 1); + } + break; + case 'm': // "Mark" tone: -x m + dw_printf("\nSending mark calibration tone (%dHz) on channel %d.\nPress control-C to terminate.\n", + audio_config.achan[x_opt_chan].mark_freq, + x_opt_chan); + while (n-- > 0) { + tone_gen_put_bit(x_opt_chan, 1); + } + break; + case 's': // "Space" tone: -x s + dw_printf("\nSending space calibration tone (%dHz) on channel %d.\nPress control-C to terminate.\n", + audio_config.achan[x_opt_chan].space_freq, + x_opt_chan); + while (n-- > 0) { + tone_gen_put_bit(x_opt_chan, 0); + } + break; + case 'p': // Silence - set PTT only: -x p + dw_printf("\nSending silence (Set PTT only) on channel %d.\nPress control-C to terminate.\n", x_opt_chan); + SLEEP_SEC(max_duration); + break; + } + + ptt_set(OCTYPE_PTT, x_opt_chan, 0); + text_color_set(DW_COLOR_INFO); + exit(EXIT_SUCCESS); + + } else { + text_color_set(DW_COLOR_ERROR); + dw_printf("\nMark/Space frequencies not defined for channel %d. Cannot calibrate using this modem type.\n", x_opt_chan); + text_color_set(DW_COLOR_INFO); + exit(EXIT_FAILURE); + } + } else { + text_color_set(DW_COLOR_ERROR); + dw_printf("\nChannel %d is not configured as a radio channel.\n", x_opt_chan); + text_color_set(DW_COLOR_INFO); + exit(EXIT_FAILURE); + } } + /* * Initialize the digipeater and IGate functions. */ @@ -944,6 +1111,11 @@ int main (int argc, char *argv[]) server_init (&audio_config, &misc_config); kissnet_init (&misc_config); +#if (USE_AVAHI_CLIENT|USE_MACOS_DNSSD) + if (misc_config.kiss_port > 0 && misc_config.dns_sd_enabled) + dns_sd_announce(&misc_config); +#endif + /* * Create a pseudo terminal and KISS TNC emulator. */ @@ -1008,7 +1180,7 @@ int main (int argc, char *argv[]) // TODO: Use only one printf per line so output doesn't get jumbled up with stuff from other threads. -void app_process_rec_packet (int chan, int subchan, int slice, packet_t pp, alevel_t alevel, int is_fx25, retry_t retries, char *spectrum) +void app_process_rec_packet (int chan, int subchan, int slice, packet_t pp, alevel_t alevel, fec_type_t fec_type, retry_t retries, char *spectrum) { char stemp[500]; @@ -1017,21 +1189,31 @@ void app_process_rec_packet (int chan, int subchan, int slice, packet_t pp, alev char heard[AX25_MAX_ADDR_LEN]; //int j; int h; - char display_retries[32]; + char display_retries[32]; // Extra stuff before slice indicators. + // Can indicate FX.25/IL2P or fix_bits. - assert (chan >= 0 && chan < MAX_CHANS); - assert (subchan >= -1 && subchan < MAX_SUBCHANS); + assert (chan >= 0 && chan < MAX_TOTAL_CHANS); // TOTAL for virtual channels + assert (subchan >= -2 && subchan < MAX_SUBCHANS); assert (slice >= 0 && slice < MAX_SLICERS); assert (pp != NULL); // 1.1J+ strlcpy (display_retries, "", sizeof(display_retries)); - if (is_fx25) { - ; - } - else if (audio_config.achan[chan].fix_bits != RETRY_NONE || audio_config.achan[chan].passall) { - assert (retries >= RETRY_NONE && retries <= RETRY_MAX); - snprintf (display_retries, sizeof(display_retries), " [%s] ", retry_text[(int)retries]); + switch (fec_type) { + case fec_type_fx25: + strlcpy (display_retries, " FX.25 ", sizeof(display_retries)); + break; + case fec_type_il2p: + strlcpy (display_retries, " IL2P ", sizeof(display_retries)); + break; + case fec_type_none: + default: + // Possible fix_bits indication. + if (audio_config.achan[chan].fix_bits != RETRY_NONE || audio_config.achan[chan].passall) { + assert (retries >= RETRY_NONE && retries <= RETRY_MAX); + snprintf (display_retries, sizeof(display_retries), " [%s] ", retry_text[(int)retries]); + } + break; } ax25_format_addrs (pp, stemp); @@ -1056,8 +1238,11 @@ void app_process_rec_packet (int chan, int subchan, int slice, packet_t pp, alev text_color_set(DW_COLOR_DEBUG); dw_printf ("\n"); - if (( ! q_h_opt ) && alevel.rec >= 0) { /* suppress if "-q h" option */ +// The HEARD line. + if (( ! q_h_opt ) && alevel.rec >= 0) { /* suppress if "-q h" option */ +// FIXME: rather than checking for ichannel, how about checking medium==radio + if (chan != audio_config.igate_vchannel) { // suppress if from ICHANNEL if (h != -1 && h != AX25_SOURCE) { dw_printf ("Digipeater "); } @@ -1100,6 +1285,7 @@ void app_process_rec_packet (int chan, int subchan, int slice, packet_t pp, alev dw_printf ("%s audio level = %s %s %s\n", heard, alevel_text, display_retries, spectrum); } + } } /* Version 1.2: Cranking the input level way up produces 199. */ @@ -1113,6 +1299,12 @@ void app_process_rec_packet (int chan, int subchan, int slice, packet_t pp, alev text_color_set(DW_COLOR_ERROR); 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) { + + text_color_set(DW_COLOR_ERROR); + dw_printf ("Audio input level is too low. Increase so most stations are around 50.\n"); + } // Display non-APRS packets in a different color. @@ -1137,6 +1329,10 @@ void app_process_rec_packet (int chan, int subchan, int slice, packet_t pp, alev text_color_set(DW_COLOR_REC); dw_printf ("[%d.dtmf%s] ", chan, ts); } + else if (subchan == -2) { + text_color_set(DW_COLOR_REC); + dw_printf ("[%d.is%s] ", chan, ts); + } else { if (ax25_is_aprs(pp)) { text_color_set(DW_COLOR_REC); @@ -1247,7 +1443,7 @@ void app_process_rec_packet (int chan, int subchan, int slice, packet_t pp, alev // we still want to decode it for logging and other processing. // Just be quiet about errors if "-qd" is set. - decode_aprs (&A, pp, q_d_opt); + decode_aprs (&A, pp, q_d_opt, NULL); if ( ! q_d_opt ) { @@ -1325,10 +1521,10 @@ void app_process_rec_packet (int chan, int subchan, int slice, packet_t pp, alev flen = ax25_pack(pp, fbuf); - server_send_rec_packet (chan, pp, fbuf, flen); // AGW net protocol - kissnet_send_rec_packet (chan, KISS_CMD_DATA_FRAME, fbuf, flen, -1); // KISS TCP - kissserial_send_rec_packet (chan, KISS_CMD_DATA_FRAME, fbuf, flen, -1); // KISS serial port - kisspt_send_rec_packet (chan, KISS_CMD_DATA_FRAME, fbuf, flen, -1); // KISS pseudo terminal + server_send_rec_packet (chan, pp, fbuf, flen); // AGW net protocol + kissnet_send_rec_packet (chan, KISS_CMD_DATA_FRAME, fbuf, flen, NULL, -1); // KISS TCP + kissserial_send_rec_packet (chan, KISS_CMD_DATA_FRAME, fbuf, flen, NULL, -1); // KISS serial port + kisspt_send_rec_packet (chan, KISS_CMD_DATA_FRAME, fbuf, flen, NULL, -1); // KISS pseudo terminal if (A_opt_ais_to_obj && strlen(ais_obj_packet) != 0) { packet_t ao_pp = ax25_from_text (ais_obj_packet, 1); @@ -1337,32 +1533,52 @@ void app_process_rec_packet (int chan, int subchan, int slice, packet_t pp, alev int ao_flen = ax25_pack(ao_pp, ao_fbuf); server_send_rec_packet (chan, ao_pp, ao_fbuf, ao_flen); - kissnet_send_rec_packet (chan, KISS_CMD_DATA_FRAME, ao_fbuf, ao_flen, -1); - kissserial_send_rec_packet (chan, KISS_CMD_DATA_FRAME, ao_fbuf, ao_flen, -1); - kisspt_send_rec_packet (chan, KISS_CMD_DATA_FRAME, ao_fbuf, ao_flen, -1); + kissnet_send_rec_packet (chan, KISS_CMD_DATA_FRAME, ao_fbuf, ao_flen, NULL, -1); + kissserial_send_rec_packet (chan, KISS_CMD_DATA_FRAME, ao_fbuf, ao_flen, NULL, -1); + kisspt_send_rec_packet (chan, KISS_CMD_DATA_FRAME, ao_fbuf, ao_flen, NULL, -1); ax25_delete (ao_pp); } } +/* + * If it is from the ICHANNEL, we are done. + * Don't digipeat. Don't IGate. + * Don't do anything with it after printing and sending to client apps. + */ + + if (chan == audio_config.igate_vchannel) { + return; + } + /* - * If it came from DTMF decoder, send it to APRStt gateway. + * If it came from DTMF decoder (subchan == -1), send it to APRStt gateway. * Otherwise, it is a candidate for IGate and digipeater. * - * TODO: It would be useful to have some way to simulate touch tone + * It is also useful to have some way to simulate touch tone * sequences with BEACON sendto=R0 for testing. */ - if (subchan == -1) { + if (subchan == -1) { // from DTMF decoder if (tt_config.gateway_enabled && info_len >= 2) { aprs_tt_sequence (chan, (char*)(pinfo+1)); } } + else if (*pinfo == 't' && info_len >= 2 && tt_config.gateway_enabled) { + // For testing. + // Would be nice to verify it was generated locally, + // not received over the air. + aprs_tt_sequence (chan, (char*)(pinfo+1)); + } else { -/* Send to Internet server if option is enabled. */ -/* Consider only those with correct CRC. */ - - if (ax25_is_aprs(pp) && retries == RETRY_NONE) { +/* + * Send to the IGate processing. + * Use only those with correct CRC; We don't want to spread corrupted data! + * Our earlier "fix bits" hack could allow corrupted information to get thru. + * However, if it used FEC mode (FX.25. IL2P), we have much higher level of + * confidence that it is correct. + */ + if (ax25_is_aprs(pp) && ( retries == RETRY_NONE || fec_type == fec_type_fx25 || fec_type == fec_type_il2p) ) { igate_send_rec_packet (chan, pp); } @@ -1377,24 +1593,23 @@ void app_process_rec_packet (int chan, int subchan, int slice, packet_t pp, alev /* - * APRS digipeater. + * Send to APRS digipeater. * Use only those with correct CRC; We don't want to spread corrupted data! + * Our earlier "fix bits" hack could allow corrupted information to get thru. + * However, if it used FEC mode (FX.25. IL2P), we have much higher level of + * confidence that it is correct. */ - -// TODO: Should also use anything received with FX.25 because it is known to be good. -// Our earlier "fix bits" hack could allow corrupted information to get thru. - - if (ax25_is_aprs(pp) && retries == RETRY_NONE) { + if (ax25_is_aprs(pp) && ( retries == RETRY_NONE || fec_type == fec_type_fx25 || fec_type == fec_type_il2p) ) { digipeater (chan, pp); } /* * Connected mode digipeater. - * Use only those with correct CRC. + * Use only those with correct CRC (or using FEC.) */ - if (retries == RETRY_NONE) { + if (retries == RETRY_NONE || fec_type == fec_type_fx25 || fec_type == fec_type_il2p) { cdigipeater (chan, pp); } @@ -1469,7 +1684,9 @@ static void usage (char **argv) dw_printf (" -P xxx Modem Profiles.\n"); dw_printf (" -A Convert AIS positions to APRS Object Reports.\n"); dw_printf (" -D n Divide audio sample rate by n for channel 0.\n"); - dw_printf (" -X n 1 to enable FX.25 transmit.\n"); + dw_printf (" -X n 1 to enable FX.25 transmit. 16, 32, 64 for specific number of check bytes.\n"); + dw_printf (" -I n Enable IL2P transmit. n=1 is recommended. 0 uses weaker FEC.\n"); + dw_printf (" -i n Enable IL2P transmit, inverted polarity. n=1 is recommended. 0 uses weaker FEC.\n"); dw_printf (" -d Debug options:\n"); dw_printf (" a a = AGWPE network protocol client.\n"); dw_printf (" k k = KISS serial port or pseudo terminal client.\n"); @@ -1487,6 +1704,8 @@ static void usage (char **argv) dw_printf (" h h = hamlib increase verbose level.\n"); #endif 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"); 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"); @@ -1499,6 +1718,11 @@ static void usage (char **argv) dw_printf (" -p Enable pseudo terminal for KISS protocol.\n"); #endif dw_printf (" -x Send Xmit level calibration tones.\n"); + dw_printf (" a a = Alternating mark/space tones.\n"); + dw_printf (" m m = Steady mark tone (e.g. 1200Hz).\n"); + dw_printf (" s s = Steady space tone (e.g. 2200Hz).\n"); + dw_printf (" p p = Silence (Set PTT only).\n"); + dw_printf (" chan Optionally add a number to specify radio channel.\n"); dw_printf (" -u Print UTF-8 test string and exit.\n"); dw_printf (" -S Print symbol tables and exit.\n"); dw_printf (" -T fmt Time stamp format for sent and received frames.\n"); @@ -1506,19 +1730,20 @@ static void usage (char **argv) dw_printf ("\n"); dw_printf ("After any options, there can be a single command line argument for the source of\n"); - dw_printf ("received audio. This can overrides the audio input specified in the configuration file.\n"); + dw_printf ("received audio. This can override the audio input specified in the configuration file.\n"); dw_printf ("\n"); #if __WIN32__ - dw_printf ("Complete documentation can be found in the 'doc' folder\n"); + dw_printf ("Documentation can be found in the 'doc' folder\n"); #else // TODO: Could vary by platform and build options. - dw_printf ("Complete documentation can be found in /usr/local/share/doc/direwolf\n"); + dw_printf ("Documentation can be found in /usr/local/share/doc/direwolf\n"); #endif dw_printf ("or online at https://github.com/wb2osz/direwolf/tree/master/doc\n"); + dw_printf ("additional topics: https://github.com/wb2osz/direwolf-doc\n"); + text_color_set(DW_COLOR_INFO); exit (EXIT_FAILURE); } - /* end direwolf.c */ diff --git a/src/direwolf.h b/src/direwolf.h index efc329ba..69b09529 100644 --- a/src/direwolf.h +++ b/src/direwolf.h @@ -37,11 +37,8 @@ #endif - /* - * Previously, we could handle only a single audio device. - * This meant we could have only two radio channels. - * In version 1.2, we relax this restriction and allow more audio devices. + * Maximum number of audio devices. * Three is probably adequate for standard version. * Larger reasonable numbers should also be fine. * @@ -65,7 +62,15 @@ * and make sure they handle undefined channels correctly. */ -#define MAX_CHANS ((MAX_ADEVS) * 2) +#define MAX_RADIO_CHANS ((MAX_ADEVS) * 2) + +#define MAX_CHANS MAX_RADIO_CHANS // TODO: Replace all former with latter to avoid confusion with following. + +#define MAX_TOTAL_CHANS 16 // v1.7 allows additional virtual channels which are connected + // to something other than radio modems. + // Total maximum channels is based on the 4 bit KISS field. + // Someone with very unusual requirements could increase this and + // use only the AGW network protocol. /* * Maximum number of rigs. @@ -97,7 +102,7 @@ * Each one of these can have multiple slicers, at * different levels, to compensate for different * amplitudes of the AFSK tones. - * Intially used same number as subchannels but + * Initially used same number as subchannels but * we could probably trim this down a little * without impacting performance. */ @@ -176,6 +181,7 @@ #define DW_METERS_TO_FEET(x) ((x) == G_UNKNOWN ? G_UNKNOWN : (x) * 3.2808399) #define DW_FEET_TO_METERS(x) ((x) == G_UNKNOWN ? G_UNKNOWN : (x) * 0.3048) #define DW_KM_TO_MILES(x) ((x) == G_UNKNOWN ? G_UNKNOWN : (x) * 0.621371192) +#define DW_MILES_TO_KM(x) ((x) == G_UNKNOWN ? G_UNKNOWN : (x) * 1.609344) #define DW_KNOTS_TO_MPH(x) ((x) == G_UNKNOWN ? G_UNKNOWN : (x) * 1.15077945) #define DW_KNOTS_TO_METERS_PER_SEC(x) ((x) == G_UNKNOWN ? G_UNKNOWN : (x) * 0.51444444444) @@ -272,49 +278,78 @@ typedef pthread_mutex_t dw_mutex_t; /* Platform differences for string functions. */ +// Windows is missing a few which are available on Unix/Linux platforms. +// We provide our own copies when building on Windows. #if __WIN32__ char *strsep(char **stringp, const char *delim); char *strtok_r(char *str, const char *delim, char **saveptr); #endif -// Don't recall why for everyone. +// Don't recall why I added this for everyone rather than only for Windows. +// Potential problem if some C library declares it a little differently. char *strcasestr(const char *S, const char *FIND); -#if defined(__OpenBSD__) || defined(__FreeBSD__) || defined(__APPLE__) - -// strlcpy and strlcat should be in string.h and the C library. - -#else // Use our own copy - - -// These prevent /usr/include/gps.h from providing its own definition. -#define HAVE_STRLCAT 1 +// cmake tries to determine whether strlcpy and strlcat are provided by the C runtime library. +// +// ../CMakeLists.txt:check_symbol_exists(strlcpy string.h HAVE_STRLCPY) +// +// It sets HAVE_STRLCPY and HAVE_STRLCAT if the corresponding functions are declared. +// Unfortunately this does not work right for glibc 2.38 which declares the functions +// like this: +// +// extern __typeof (strlcpy) __strlcpy; +// libc_hidden_proto (__strlcpy) +// extern __typeof (strlcat) __strlcat; +// libc_hidden_proto (__strlcat) +// +// Rather than the normal way found in earlier versions: +// +// extern char *strcpy (char *__restrict __dest, const char *__restrict __src) +// +// Perhaps a later version of cmake will recognize this form but the version I'm +// using does not. +// So, our work around is to assume these functions are available for glibc >= 2.38. +// +// In theory, cmake should be able to find the version of the C runtime library, +// but I could not get it to work. So we have the test here. We will still build +// own library with the strl... functions but this does not cause a problem +// because they have special debug names which will not cause a conflict. + +#ifdef __GLIBC__ +#if (__GLIBC__ > 2) || ((__GLIBC__ == 2) && (__GLIBC_MINOR__ >= 38)) +// These functions first added in 2.38. +//#warning "DEBUG - glibc >= 2.38" #define HAVE_STRLCPY 1 +#define HAVE_STRLCAT 1 +#else +//#warning "DEBUG - glibc < 2.38" +#endif +#endif +#define DEBUG_STRL 1 // Extra Debug version when using our own strlcpy, strlcat. + // Should be ignored if not supplying our own. -#define DEBUG_STRL 1 - +#ifndef HAVE_STRLCPY // Need to supply our own. #if DEBUG_STRL - #define strlcpy(dst,src,siz) strlcpy_debug(dst,src,siz,__FILE__,__func__,__LINE__) -#define strlcat(dst,src,siz) strlcat_debug(dst,src,siz,__FILE__,__func__,__LINE__) - size_t strlcpy_debug(char *__restrict__ dst, const char *__restrict__ src, size_t siz, const char *file, const char *func, int line); -size_t strlcat_debug(char *__restrict__ dst, const char *__restrict__ src, size_t siz, const char *file, const char *func, int line); - #else - #define strlcpy(dst,src,siz) strlcpy_debug(dst,src,siz) -#define strlcat(dst,src,siz) strlcat_debug(dst,src,siz) - size_t strlcpy_debug(char *__restrict__ dst, const char *__restrict__ src, size_t siz); -size_t strlcat_debug(char *__restrict__ dst, const char *__restrict__ src, size_t siz); - #endif /* DEBUG_STRL */ +#endif -#endif /* BSD or Apple */ +#ifndef HAVE_STRLCAT // Need to supply our own. +#if DEBUG_STRL +#define strlcat(dst,src,siz) strlcat_debug(dst,src,siz,__FILE__,__func__,__LINE__) +size_t strlcat_debug(char *__restrict__ dst, const char *__restrict__ src, size_t siz, const char *file, const char *func, int line); +#else +#define strlcat(dst,src,siz) strlcat_debug(dst,src,siz) +size_t strlcat_debug(char *__restrict__ dst, const char *__restrict__ src, size_t siz); +#endif /* DEBUG_STRL */ +#endif #endif /* ifndef DIREWOLF_H */ diff --git a/src/dlq.c b/src/dlq.c index 2f21f6d0..f56b8649 100644 --- a/src/dlq.c +++ b/src/dlq.c @@ -130,12 +130,18 @@ void dlq_init (void) #else int err; err = pthread_mutex_init (&wake_up_mutex, NULL); + if (err != 0) { + text_color_set(DW_COLOR_ERROR); + dw_printf ("dlq_init: pthread_mutex_init err=%d", err); + perror (""); + exit (EXIT_FAILURE); + } err = pthread_mutex_init (&dlq_mutex, NULL); if (err != 0) { text_color_set(DW_COLOR_ERROR); dw_printf ("dlq_init: pthread_mutex_init err=%d", err); perror (""); - exit (1); + exit (EXIT_FAILURE); } #endif @@ -209,10 +215,10 @@ void dlq_init (void) * display of audio level line. * Use -2 to indicate DTMF message.) * - * is_fx25 - Was it from FX.25? Need to know because + * fec_type - Was it from FX.25 or IL2P? Need to know because * meaning of retries is different. * - * retries - Level of bit correction used. + * retries - Level of correction used. * * spectrum - Display of how well multiple decoders did. * @@ -222,7 +228,7 @@ void dlq_init (void) * *--------------------------------------------------------------------*/ -void dlq_rec_frame (int chan, int subchan, int slice, packet_t pp, alevel_t alevel, int is_fx25, retry_t retries, char *spectrum) +void dlq_rec_frame (int chan, int subchan, int slice, packet_t pp, alevel_t alevel, fec_type_t fec_type, retry_t retries, char *spectrum) { struct dlq_item_s *pnew; @@ -233,7 +239,7 @@ void dlq_rec_frame (int chan, int subchan, int slice, packet_t pp, alevel_t alev dw_printf ("dlq_rec_frame (chan=%d, pp=%p, ...)\n", chan, pp); #endif - assert (chan >= 0 && chan < MAX_CHANS); + assert (chan >= 0 && chan < MAX_TOTAL_CHANS); // TOTAL to include virtual channels. if (pp == NULL) { text_color_set(DW_COLOR_ERROR); @@ -253,6 +259,11 @@ void dlq_rec_frame (int chan, int subchan, int slice, packet_t pp, alevel_t alev /* Allocate a new queue item. */ pnew = (struct dlq_item_s *) calloc (sizeof(struct dlq_item_s), 1); + if (pnew == NULL) { + text_color_set(DW_COLOR_ERROR); + dw_printf ("FATAL ERROR: Out of memory.\n"); + exit (EXIT_FAILURE); + } s_new_count++; if (s_new_count > s_delete_count + 50) { @@ -267,7 +278,7 @@ void dlq_rec_frame (int chan, int subchan, int slice, packet_t pp, alevel_t alev pnew->subchan = subchan; pnew->pp = pp; pnew->alevel = alevel; - pnew->is_fx25 = is_fx25; + pnew->fec_type = fec_type; pnew->retries = retries; if (spectrum == NULL) strlcpy(pnew->spectrum, "", sizeof(pnew->spectrum)); @@ -492,6 +503,11 @@ void dlq_connect_request (char addrs[AX25_MAX_ADDRS][AX25_MAX_ADDR_LEN], int num /* Allocate a new queue item. */ pnew = (struct dlq_item_s *) calloc (sizeof(struct dlq_item_s), 1); + if (pnew == NULL) { + text_color_set(DW_COLOR_ERROR); + dw_printf ("FATAL ERROR: Out of memory.\n"); + exit (EXIT_FAILURE); + } s_new_count++; pnew->type = DLQ_CONNECT_REQUEST; @@ -545,6 +561,11 @@ void dlq_disconnect_request (char addrs[AX25_MAX_ADDRS][AX25_MAX_ADDR_LEN], int /* Allocate a new queue item. */ pnew = (struct dlq_item_s *) calloc (sizeof(struct dlq_item_s), 1); + if (pnew == NULL) { + text_color_set(DW_COLOR_ERROR); + dw_printf ("FATAL ERROR: Out of memory.\n"); + exit (EXIT_FAILURE); + } s_new_count++; pnew->type = DLQ_DISCONNECT_REQUEST; @@ -603,6 +624,11 @@ void dlq_outstanding_frames_request (char addrs[AX25_MAX_ADDRS][AX25_MAX_ADDR_LE /* Allocate a new queue item. */ pnew = (struct dlq_item_s *) calloc (sizeof(struct dlq_item_s), 1); + if (pnew == NULL) { + text_color_set(DW_COLOR_ERROR); + dw_printf ("FATAL ERROR: Out of memory.\n"); + exit (EXIT_FAILURE); + } s_new_count++; pnew->type = DLQ_OUTSTANDING_FRAMES_REQUEST; @@ -670,6 +696,11 @@ void dlq_xmit_data_request (char addrs[AX25_MAX_ADDRS][AX25_MAX_ADDR_LEN], int n /* Allocate a new queue item. */ pnew = (struct dlq_item_s *) calloc (sizeof(struct dlq_item_s), 1); + if (pnew == NULL) { + text_color_set(DW_COLOR_ERROR); + dw_printf ("FATAL ERROR: Out of memory.\n"); + exit (EXIT_FAILURE); + } s_new_count++; pnew->type = DLQ_XMIT_DATA_REQUEST; @@ -697,8 +728,7 @@ void dlq_xmit_data_request (char addrs[AX25_MAX_ADDRS][AX25_MAX_ADDR_LEN], int n * * Purpose: Register callsigns that we will recognize for incoming connection requests. * - * Inputs: addrs - Source (owncall), destination (peercall), - * and possibly digipeaters. + * Inputs: addr - Callsign to [un]register. * * chan - Channel, 0 is first. * @@ -718,7 +748,7 @@ void dlq_xmit_data_request (char addrs[AX25_MAX_ADDRS][AX25_MAX_ADDR_LEN], int n *--------------------------------------------------------------------*/ -void dlq_register_callsign (char addr[AX25_MAX_ADDR_LEN], int chan, int client) +void dlq_register_callsign (char *addr, int chan, int client) { struct dlq_item_s *pnew; @@ -733,11 +763,16 @@ void dlq_register_callsign (char addr[AX25_MAX_ADDR_LEN], int chan, int client) /* Allocate a new queue item. */ pnew = (struct dlq_item_s *) calloc (sizeof(struct dlq_item_s), 1); + if (pnew == NULL) { + text_color_set(DW_COLOR_ERROR); + dw_printf ("FATAL ERROR: Out of memory.\n"); + exit (EXIT_FAILURE); + } s_new_count++; pnew->type = DLQ_REGISTER_CALLSIGN; pnew->chan = chan; - strlcpy (pnew->addrs[0], addr, AX25_MAX_ADDR_LEN); + strlcpy (pnew->addrs[0], addr, sizeof(pnew->addrs[0])); pnew->num_addr = 1; pnew->client = client; @@ -748,7 +783,7 @@ void dlq_register_callsign (char addr[AX25_MAX_ADDR_LEN], int chan, int client) } /* end dlq_register_callsign */ -void dlq_unregister_callsign (char addr[AX25_MAX_ADDR_LEN], int chan, int client) +void dlq_unregister_callsign (char *addr, int chan, int client) { struct dlq_item_s *pnew; @@ -763,11 +798,16 @@ void dlq_unregister_callsign (char addr[AX25_MAX_ADDR_LEN], int chan, int client /* Allocate a new queue item. */ pnew = (struct dlq_item_s *) calloc (sizeof(struct dlq_item_s), 1); + if (pnew == NULL) { + text_color_set(DW_COLOR_ERROR); + dw_printf ("FATAL ERROR: Out of memory.\n"); + exit (EXIT_FAILURE); + } s_new_count++; pnew->type = DLQ_UNREGISTER_CALLSIGN; pnew->chan = chan; - strlcpy (pnew->addrs[0], addr, AX25_MAX_ADDR_LEN); + strlcpy (pnew->addrs[0], addr, sizeof(pnew->addrs[0])); pnew->num_addr = 1; pnew->client = client; @@ -817,6 +857,11 @@ void dlq_channel_busy (int chan, int activity, int status) /* Allocate a new queue item. */ pnew = (struct dlq_item_s *) calloc (sizeof(struct dlq_item_s), 1); + if (pnew == NULL) { + text_color_set(DW_COLOR_ERROR); + dw_printf ("FATAL ERROR: Out of memory.\n"); + exit (EXIT_FAILURE); + } s_new_count++; pnew->type = DLQ_CHANNEL_BUSY; @@ -840,7 +885,7 @@ void dlq_channel_busy (int chan, int activity, int status) * Name: dlq_seize_confirm * * Purpose: Inform data link state machine that the transmitter is on. - * This is in reponse to lm_seize_request. + * This is in response to lm_seize_request. * * Inputs: chan - Radio channel number. * @@ -865,6 +910,11 @@ void dlq_seize_confirm (int chan) /* Allocate a new queue item. */ pnew = (struct dlq_item_s *) calloc (sizeof(struct dlq_item_s), 1); + if (pnew == NULL) { + text_color_set(DW_COLOR_ERROR); + dw_printf ("FATAL ERROR: Out of memory.\n"); + exit (EXIT_FAILURE); + } s_new_count++; pnew->type = DLQ_SEIZE_CONFIRM; @@ -910,6 +960,11 @@ void dlq_client_cleanup (int client) /* Allocate a new queue item. */ pnew = (struct dlq_item_s *) calloc (sizeof(struct dlq_item_s), 1); + if (pnew == NULL) { + text_color_set(DW_COLOR_ERROR); + dw_printf ("FATAL ERROR: Out of memory.\n"); + exit (EXIT_FAILURE); + } s_new_count++; // All we care about is the client number. @@ -1192,6 +1247,11 @@ cdata_t *cdata_new (int pid, char *data, int len) size = ( len + 127 ) & ~0x7f; cdata = malloc ( sizeof(cdata_t) + size ); + if (cdata == NULL) { + text_color_set(DW_COLOR_ERROR); + dw_printf ("FATAL ERROR: Out of memory.\n"); + exit (EXIT_FAILURE); + } cdata->magic = TXDATA_MAGIC; cdata->next = NULL; diff --git a/src/dlq.h b/src/dlq.h index 87716362..fdac1c0c 100644 --- a/src/dlq.h +++ b/src/dlq.h @@ -33,10 +33,13 @@ typedef struct cdata_s { + /* Types of things that can be in queue. */ typedef enum dlq_type_e {DLQ_REC_FRAME, DLQ_CONNECT_REQUEST, DLQ_DISCONNECT_REQUEST, DLQ_XMIT_DATA_REQUEST, DLQ_REGISTER_CALLSIGN, DLQ_UNREGISTER_CALLSIGN, DLQ_OUTSTANDING_FRAMES_REQUEST, DLQ_CHANNEL_BUSY, DLQ_SEIZE_CONFIRM, DLQ_CLIENT_CLEANUP} dlq_type_t; +typedef enum fec_type_e {fec_type_none=0, fec_type_fx25=1, fec_type_il2p=2} fec_type_t; + /* A queue item. */ @@ -68,7 +71,7 @@ typedef struct dlq_item_s { alevel_t alevel; /* Audio level. */ - int is_fx25; /* Was it from FX.25? */ + fec_type_t fec_type; // Type of FEC for received signal: none, FX.25, or IL2P. retry_t retries; /* Effort expended to get a valid CRC. */ /* Bits changed for regular AX.25. */ @@ -106,7 +109,7 @@ void dlq_init (void); -void dlq_rec_frame (int chan, int subchan, int slice, packet_t pp, alevel_t alevel, int is_fx25, retry_t retries, char *spectrum); +void dlq_rec_frame (int chan, int subchan, int slice, packet_t pp, alevel_t alevel, fec_type_t fec_type, retry_t retries, char *spectrum); void dlq_connect_request (char addrs[AX25_MAX_ADDRS][AX25_MAX_ADDR_LEN], int num_addr, int chan, int client, int pid); @@ -116,9 +119,9 @@ void dlq_outstanding_frames_request (char addrs[AX25_MAX_ADDRS][AX25_MAX_ADDR_LE void dlq_xmit_data_request (char addrs[AX25_MAX_ADDRS][AX25_MAX_ADDR_LEN], int num_addr, int chan, int client, int pid, char *xdata_ptr, int xdata_len); -void dlq_register_callsign (char addr[AX25_MAX_ADDR_LEN], int chan, int client); +void dlq_register_callsign (char *addr, int chan, int client); -void dlq_unregister_callsign (char addr[AX25_MAX_ADDR_LEN], int chan, int client); +void dlq_unregister_callsign (char *addr, int chan, int client); void dlq_channel_busy (int chan, int activity, int status); diff --git a/src/dns_sd_avahi.c b/src/dns_sd_avahi.c new file mode 100644 index 00000000..63ce0b66 --- /dev/null +++ b/src/dns_sd_avahi.c @@ -0,0 +1,260 @@ +// +// This file is part of Dire Wolf, an amateur radio packet TNC. +// +// Copyright (C) 2020 Heikki Hannikainen, OH7LZB +// +// +// 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: dns_sd_avahi.c + * + * Purpose: Announce the KISS over TCP service using DNS-SD via Avahi + * + * Description: + * + * Most people have typed in enough IP addresses and ports by now, and + * would rather just select an available TNC that is automatically + * discovered on the local network. Even more so on a mobile device + * such an Android or iOS phone or tablet. + * + * On Linux, the announcement can be made through Avahi, the mDNS + * framework commonly deployed on Linux systems. + * + * This is largely based on the publishing example of the Avahi library. + */ + +#ifdef USE_AVAHI_CLIENT + +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include "dns_sd_dw.h" +#include "dns_sd_common.h" +#include "textcolor.h" + +static AvahiEntryGroup *group = NULL; +static AvahiSimplePoll *simple_poll = NULL; +static AvahiClient *client = NULL; +static char *name = NULL; +static int kiss_port = 0; + +pthread_t avahi_thread; + +static void create_services(AvahiClient *c); + +#define PRINT_PREFIX "DNS-SD: Avahi: " + +static void entry_group_callback(AvahiEntryGroup *g, AvahiEntryGroupState state, AVAHI_GCC_UNUSED void *userdata) +{ + assert(g == group || group == NULL); + group = g; + + /* Called whenever the entry group state changes */ + switch (state) { + case AVAHI_ENTRY_GROUP_ESTABLISHED : + /* The entry group has been established successfully */ + text_color_set(DW_COLOR_INFO); + dw_printf(PRINT_PREFIX "Service '%s' successfully registered.\n", name); + break; + case AVAHI_ENTRY_GROUP_COLLISION: { + char *n; + /* A service name collision with a remote service + * happened. Let's pick a new name. */ + n = avahi_alternative_service_name(name); + avahi_free(name); + name = n; + text_color_set(DW_COLOR_INFO); + dw_printf(PRINT_PREFIX "Service name collision, renaming service to '%s'\n", name); + /* And recreate the services */ + create_services(avahi_entry_group_get_client(g)); + break; + } + case AVAHI_ENTRY_GROUP_FAILURE: + text_color_set(DW_COLOR_ERROR); + dw_printf(PRINT_PREFIX "Entry group failure: %s\n", avahi_strerror(avahi_client_errno(avahi_entry_group_get_client(g)))); + /* Some kind of failure happened while we were registering our services */ + avahi_simple_poll_quit(simple_poll); + break; + case AVAHI_ENTRY_GROUP_UNCOMMITED: + case AVAHI_ENTRY_GROUP_REGISTERING: + ; + } +} + +static void create_services(AvahiClient *c) +{ + char *n; + int ret; + assert(c); + /* If this is the first time we're called, let's create a new + * entry group if necessary */ + if (!group) { + if (!(group = avahi_entry_group_new(c, entry_group_callback, NULL))) { + text_color_set(DW_COLOR_ERROR); + dw_printf(PRINT_PREFIX "avahi_entry_group_new() failed: %s\n", avahi_strerror(avahi_client_errno(c))); + goto fail; + } + } else { + avahi_entry_group_reset(group); + } + + /* If the group is empty (either because it was just created, or + * because it was reset previously, add our entries. */ + if (avahi_entry_group_is_empty(group)) { + text_color_set(DW_COLOR_INFO); + dw_printf(PRINT_PREFIX "Announcing KISS TCP on port %d as '%s'\n", kiss_port, name); + + /* Announce with AVAHI_PROTO_INET instead of AVAHI_PROTO_UNSPEC, since Dire Wolf currently + * only listens on IPv4. + */ + + if ((ret = avahi_entry_group_add_service(group, AVAHI_IF_UNSPEC, AVAHI_PROTO_INET, 0, name, DNS_SD_SERVICE, NULL, NULL, kiss_port, NULL)) < 0) { + if (ret == AVAHI_ERR_COLLISION) + goto collision; + text_color_set(DW_COLOR_ERROR); + dw_printf(PRINT_PREFIX "Failed to add _kiss-tnc._tcp service: %s\n", avahi_strerror(ret)); + goto fail; + } + + /* Tell the server to register the service */ + if ((ret = avahi_entry_group_commit(group)) < 0) { + text_color_set(DW_COLOR_ERROR); + dw_printf(PRINT_PREFIX "Failed to commit entry group: %s\n", avahi_strerror(ret)); + goto fail; + } + } + return; + +collision: + /* A service name collision with a local service happened. Let's + * pick a new name */ + n = avahi_alternative_service_name(name); + avahi_free(name); + name = n; + text_color_set(DW_COLOR_INFO); + dw_printf(PRINT_PREFIX "Service name collision, renaming service to '%s'\n", name); + avahi_entry_group_reset(group); + create_services(c); + return; + +fail: + avahi_simple_poll_quit(simple_poll); +} + +static void client_callback(AvahiClient *c, AvahiClientState state, AVAHI_GCC_UNUSED void * userdata) +{ + assert(c); + /* Called whenever the client or server state changes */ + switch (state) { + case AVAHI_CLIENT_S_RUNNING: + /* The server has startup successfully and registered its host + * name on the network, so it's time to create our services */ + create_services(c); + break; + case AVAHI_CLIENT_FAILURE: + text_color_set(DW_COLOR_ERROR); + dw_printf(PRINT_PREFIX "Client failure: %s\n", avahi_strerror(avahi_client_errno(c))); + avahi_simple_poll_quit(simple_poll); + break; + case AVAHI_CLIENT_S_COLLISION: + /* Let's drop our registered services. When the server is back + * in AVAHI_SERVER_RUNNING state we will register them + * again with the new host name. */ + case AVAHI_CLIENT_S_REGISTERING: + /* The server records are now being established. This + * might be caused by a host name change. We need to wait + * for our own records to register until the host name is + * properly esatblished. */ + if (group) + avahi_entry_group_reset(group); + break; + case AVAHI_CLIENT_CONNECTING: + ; + } +} + +static void cleanup(void) +{ + /* Cleanup things */ + if (client) + avahi_client_free(client); + + if (simple_poll) + avahi_simple_poll_free(simple_poll); + + avahi_free(name); +} + + +static void *avahi_mainloop(void *arg) +{ + /* Run the main loop */ + avahi_simple_poll_loop(simple_poll); + + cleanup(); + + return NULL; +} + +void dns_sd_announce (struct misc_config_s *mc) +{ + text_color_set(DW_COLOR_DEBUG); + //kiss_port = mc->kiss_port; // now an array. + kiss_port = mc->kiss_port[0]; // FIXME: Quick hack until I can handle multiple TCP ports properly. + + int error; + + /* Allocate main loop object */ + if (!(simple_poll = avahi_simple_poll_new())) { + text_color_set(DW_COLOR_ERROR); + dw_printf(PRINT_PREFIX "Failed to create Avahi simple poll object.\n"); + goto fail; + } + + if (mc->dns_sd_name[0]) { + name = avahi_strdup(mc->dns_sd_name); + } else { + name = dns_sd_default_service_name(); + } + + /* Allocate a new client */ + client = avahi_client_new(avahi_simple_poll_get(simple_poll), 0, client_callback, NULL, &error); + + /* Check whether creating the client object succeeded */ + if (!client) { + text_color_set(DW_COLOR_ERROR); + dw_printf(PRINT_PREFIX "Failed to create Avahi client: %s\n", avahi_strerror(error)); + goto fail; + } + + pthread_create(&avahi_thread, NULL, &avahi_mainloop, NULL); + + return; + +fail: + cleanup(); +} + +#endif // USE_AVAHI_CLIENT + diff --git a/src/dns_sd_common.c b/src/dns_sd_common.c new file mode 100644 index 00000000..65a1cbf7 --- /dev/null +++ b/src/dns_sd_common.c @@ -0,0 +1,65 @@ +// +// This file is part of Dire Wolf, an amateur radio packet TNC. +// +// Copyright (C) 2020 Heikki Hannikainen, OH7LZB +// +// +// 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: dns_sd_common.c + * + * Purpose: Announce the KISS over TCP service using DNS-SD, common functions + * + * Description: + * + * Most people have typed in enough IP addresses and ports by now, and + * would rather just select an available TNC that is automatically + * discovered on the local network. Even more so on a mobile device + * such an Android or iOS phone or tablet. + * + * This module contains common functions needed on Linux and MacOS. + */ + + +#include +#include +#include + +/* Get a default service name to publish. By default, + * "Dire Wolf on ", or just "Dire Wolf" if hostname cannot + * be obtained. + */ +char *dns_sd_default_service_name(void) +{ + char hostname[51]; + char sname[64]; + + int i = gethostname(hostname, sizeof(hostname)); + if (i == 0) { + hostname[sizeof(hostname)-1] = 0; + + // on some systems, an FQDN is returned; remove domain part + char *dot = strchr(hostname, '.'); + if (dot) + *dot = 0; + + snprintf(sname, sizeof(sname), "Dire Wolf on %s", hostname); + return strdup(sname); + } + + return strdup("Dire Wolf"); +} + diff --git a/src/dns_sd_common.h b/src/dns_sd_common.h new file mode 100644 index 00000000..f104bf83 --- /dev/null +++ b/src/dns_sd_common.h @@ -0,0 +1,7 @@ + +#if (USE_AVAHI_CLIENT|USE_MACOS_DNSSD) + +char *dns_sd_default_service_name(void); + +#endif + diff --git a/src/dns_sd_dw.h b/src/dns_sd_dw.h new file mode 100644 index 00000000..79f4b868 --- /dev/null +++ b/src/dns_sd_dw.h @@ -0,0 +1,10 @@ + +#if (USE_AVAHI_CLIENT|USE_MACOS_DNSSD) + +#include "config.h" + +#define DNS_SD_SERVICE "_kiss-tnc._tcp" + +void dns_sd_announce (struct misc_config_s *mc); + +#endif // USE_AVAHI_CLIENT diff --git a/src/dns_sd_macos.c b/src/dns_sd_macos.c new file mode 100644 index 00000000..d733a412 --- /dev/null +++ b/src/dns_sd_macos.c @@ -0,0 +1,89 @@ +// +// This file is part of Dire Wolf, an amateur radio packet TNC. +// +// Copyright (C) 2020 Heikki Hannikainen, OH7LZB +// +// +// 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: dns_sd_macos.c + * + * Purpose: Announce the KISS over TCP service using MacOS dns-sd + * + * Description: + * + * Most people have typed in enough IP addresses and ports by now, and + * would rather just select an available TNC that is automatically + * discovered on the local network. Even more so on a mobile device + * such an Android or iOS phone or tablet. + * + * On MacOs, the announcement can be made through dns-sd. + */ + +#ifdef USE_MACOS_DNSSD + +#include +#include +#include + +#include "dns_sd_dw.h" +#include "dns_sd_common.h" +#include "textcolor.h" + +static char *name = NULL; + +static void registerServiceCallBack(DNSServiceRef sdRef, DNSServiceFlags flags, DNSServiceErrorType errorCode, + const char* name, const char* regType, const char* domain, void* context) +{ + if (errorCode == kDNSServiceErr_NoError) { + text_color_set(DW_COLOR_INFO); + dw_printf("DNS-SD: Successfully registered '%s'\n", name); + } else { + text_color_set(DW_COLOR_ERROR); + dw_printf("DNS-SD: Failed to register '%s': %d\n", name, errorCode); + } +} + +void dns_sd_announce (struct misc_config_s *mc) +{ + //int kiss_port = mc->kiss_port; // now an array. + int kiss_port = mc->kiss_port[0]; // FIXME: Quick hack until I can handle multiple TCP ports properly. + + if (mc->dns_sd_name[0]) { + name = strdup(mc->dns_sd_name); + } else { + name = dns_sd_default_service_name(); + } + + uint16_t port_nw = htons(kiss_port); + + DNSServiceRef registerRef; + DNSServiceErrorType err = DNSServiceRegister( + ®isterRef, 0, 0, name, DNS_SD_SERVICE, NULL, NULL, + port_nw, 0, NULL, registerServiceCallBack, NULL); + + if (err == kDNSServiceErr_NoError) { + text_color_set(DW_COLOR_INFO); + dw_printf("DNS-SD: Announcing KISS TCP on port %d as '%s'\n", kiss_port, name); + } else { + text_color_set(DW_COLOR_ERROR); + dw_printf("DNS-SD: Failed to announce '%s': %d\n", name, err); + } +} + +#endif // USE_MACOS_DNSSD + + diff --git a/src/dsp.c b/src/dsp.c index 6ba70949..4a5f4a88 100644 --- a/src/dsp.c +++ b/src/dsp.c @@ -43,7 +43,6 @@ #include "dsp.h" -//#include "fsk_demod_agc.h" /* for M_FILTER_SIZE, etc. */ #define MIN(a,b) ((a)<(b)?(a):(b)) #define MAX(a,b) ((a)>(b)?(a):(b)) @@ -127,7 +126,7 @@ float window (bp_window_t type, int size, int j) *----------------------------------------------------------------*/ -int gen_lowpass (float fc, float *lp_filter, int filter_size, bp_window_t wtype, float lp_delay_fract) +void gen_lowpass (float fc, float *lp_filter, int filter_size, bp_window_t wtype) { int j; float G; @@ -175,54 +174,7 @@ int gen_lowpass (float fc, float *lp_filter, int filter_size, bp_window_t wtype, lp_filter[j] = lp_filter[j] / G; } - -// Calculate the signal delay. -// If a signal at level 0 steps to level 1, this is the time that it would -// take for the output to reach 0.5. -// -// Examples: -// -// Filter has one tap with value of 1.0. -// Output is immediate so I would call this delay of 0. -// -// Filter coefficients: 0.2, 0.2, 0.2, 0.2, 0.2 -// "1" inputs Out -// 1 0.2 -// 2 0.4 -// 3 0.6 -// -// In this case, the output does not change immediately. -// It takes two more samples to reach the half way point -// so it has a delay of 2. - - float sum = 0; - int delay = 0; - - if (lp_delay_fract == 0) lp_delay_fract = 0.5; - - for (j=0; j lp_delay_fract) { - delay = j; - break; - } - } - -#if DEBUG1 - dw_printf ("Low Pass Delay = %d samples\n", delay) ; -#endif - -// Hmmm. This might have been wasted effort. The result is always half the number of taps. - - if (delay < 2 || delay > filter_size - 2) { - text_color_set(DW_COLOR_ERROR); - dw_printf ("Internal error, %s %d, delay %d for size %d\n", __func__, __LINE__, delay, filter_size); - } - - return (delay); + return; } /* end gen_lowpass */ @@ -369,4 +321,86 @@ void gen_ms (int fc, int sps, float *sin_table, float *cos_table, int filter_siz } /* end gen_ms */ + + + +/*------------------------------------------------------------------ + * + * Name: rrc + * + * Purpose: Root Raised Cosine function. + * Why do they call it that? + * It's mostly the sinc function with cos windowing to taper off edges faster. + * + * Inputs: t - Time in units of symbol duration. + * i.e. The centers of two adjacent symbols would differ by 1. + * + * a - Roll off factor, between 0 and 1. + * + * Returns: Basically the sinc (sin(x)/x) function with edges decreasing faster. + * Should be 1 for t = 0 and 0 at all other integer values of t. + * + *----------------------------------------------------------------*/ + +__attribute__((const)) +float rrc (float t, float a) +{ + float sinc, window, result; + + if (t > -0.001 && t < 0.001) { + sinc = 1; + } + else { + sinc = sinf(M_PI * t) / (M_PI * t); + } + + if (fabsf(a * t) > 0.499 && fabsf(a * t) < 0.501) { + window = M_PI / 4; + } + else { + window = cos(M_PI * a * t) / ( 1 - powf(2 * a * t, 2)); + // This made nicer looking waveforms for generating signal. + //window = cos(M_PI * a * t); + // Do we want to let it go negative? + // I think this would happen when a > 0.5 / (filter width in symbol times) + if (window < 0) { + //printf ("'a' is too large for range of 't'.\n"); + //window = 0; + } + } + + result = sinc * window; + +#if DEBUGRRC + // t should vary from - to + half of filter size in symbols. + // Result should be 1 at t=0 and 0 at all other integer values of t. + + printf ("%.3f, %.3f, %.3f, %.3f\n", t, sinc, window, result); +#endif + return (result); +} + +// The Root Raised Cosine (RRC) low pass filter is suppposed to minimize Intersymbol Interference (ISI). + +void gen_rrc_lowpass (float *pfilter, int filter_taps, float rolloff, float samples_per_symbol) +{ + int k; + float t; + + for (k = 0; k < filter_taps; k++) { + t = (k - ((filter_taps - 1.0) / 2.0)) / samples_per_symbol; + pfilter[k] = rrc (t, rolloff); + } + + // Scale it for unity gain. + + t = 0; + for (k = 0; k < filter_taps; k++) { + t += pfilter[k]; + } + for (k = 0; k < filter_taps; k++) { + pfilter[k] = pfilter[k] / t; + } +} + /* end dsp.c */ diff --git a/src/dsp.h b/src/dsp.h index 5d5b882d..e0dbd248 100644 --- a/src/dsp.h +++ b/src/dsp.h @@ -5,9 +5,13 @@ float window (bp_window_t type, int size, int j); -int gen_lowpass (float fc, float *lp_filter, int filter_size, bp_window_t wtype, float lp_delay_fract); +void gen_lowpass (float fc, float *lp_filter, int filter_size, bp_window_t wtype); void gen_bandpass (float f1, float f2, float *bp_filter, int filter_size, bp_window_t wtype); void gen_ms (int fc, int samples_per_sec, float *sin_table, float *cos_table, int filter_size, int wtype); + +__attribute__((const)) float rrc (float t, float a); + +void gen_rrc_lowpass (float *pfilter, int filter_taps, float rolloff, float samples_per_symbol); diff --git a/src/dtime_now.c b/src/dtime_now.c index af7b98a2..e5b40c6c 100644 --- a/src/dtime_now.c +++ b/src/dtime_now.c @@ -25,9 +25,9 @@ /*------------------------------------------------------------------ * - * Name: dtime_now + * Name: dtime_realtime * - * Purpose: Return current time as double precision. + * Purpose: Return current wall clock time as double precision. * * Input: none * @@ -40,15 +40,99 @@ * part of a second, and having extra calculations everywhere, * simply use double precision floating point to make usage * easier. - * + * + * NOTE: This is not a good way to calculate elapsed time because + * it can jump forward or backware via NTP or other manual setting. + * + * Use the monotonic version for measuring elapsed time. + * + * History: Originally I called this dtime_now. We ran into issues where + * we really cared about elapsed time, rather than wall clock time. + * The wall clock time could be wrong at start up time if there + * is no realtime clock or Internet access. It can then jump + * when GPS time or Internet access becomes available. + * All instances of dtime_now should be replaced by dtime_realtime + * if we want wall clock time, or dtime_monotonic if it is to be + * used for measuring elapsed time, such as between becons. + * *---------------------------------------------------------------*/ +double dtime_realtime (void) +{ + double result; + +#if __WIN32__ + /* 64 bit integer is number of 100 nanosecond intervals from Jan 1, 1601. */ + + FILETIME ft; + + GetSystemTimeAsFileTime (&ft); + + result = ((( (double)ft.dwHighDateTime * (256. * 256. * 256. * 256.) + + (double)ft.dwLowDateTime ) / 10000000.) - 11644473600.); +#else + /* tv_sec is seconds from Jan 1, 1970. */ + + struct timespec ts; + +#ifdef __APPLE__ + +// Why didn't I use clock_gettime? +// Not available before Max OSX 10.12? https://github.com/gambit/gambit/issues/293 + + struct timeval tp; + gettimeofday(&tp, NULL); + ts.tv_nsec = tp.tv_usec * 1000; + ts.tv_sec = tp.tv_sec; +#else + clock_gettime (CLOCK_REALTIME, &ts); +#endif + + result = ((double)(ts.tv_sec) + (double)(ts.tv_nsec) * 0.000000001); + +#endif + +#if DEBUG + text_color_set(DW_COLOR_DEBUG); + dw_printf ("dtime_realtime() returns %.3f\n", result ); +#endif + + return (result); +} + -double dtime_now (void) +/*------------------------------------------------------------------ + * + * Name: dtime_monotonic + * + * Purpose: Return montonically increasing time, which is not influenced + * by the wall clock changing. e.g. leap seconds, NTP adjustments. + * + * Input: none + * + * Returns: Time as double precision, so we can get resolution + * finer than one second. + * + * Description: Use this when calculating elapsed time. + * + *---------------------------------------------------------------*/ + +double dtime_monotonic (void) { double result; #if __WIN32__ + +// FIXME: +// This is still returning wall clock time. +// https://docs.microsoft.com/en-us/windows/win32/api/sysinfoapi/nf-sysinfoapi-gettickcount64 +// GetTickCount64 would be ideal but it requires Vista or Server 2008. +// As far as I know, the current version of direwolf still works on XP. +// +// As a work-around, GetTickCount could be used if we add extra code to deal +// with the wrap around after about 49.7 days. +// Resolution is only about 10 or 16 milliseconds. Is that good enough? + /* 64 bit integer is number of 100 nanosecond intervals from Jan 1, 1601. */ FILETIME ft; @@ -63,12 +147,22 @@ double dtime_now (void) struct timespec ts; #ifdef __APPLE__ + +// FIXME: Does MacOS have a monotonically increasing time? +// https://stackoverflow.com/questions/41509505/clock-gettime-on-macos + struct timeval tp; gettimeofday(&tp, NULL); ts.tv_nsec = tp.tv_usec * 1000; ts.tv_sec = tp.tv_sec; #else - clock_gettime (CLOCK_REALTIME, &ts); + +// This is the only case handled properly. +// Probably the only one that matters. +// It is common to have a Raspberry Pi, without Internet, +// starting up direwolf before GPS/NTP adjusts the time. + + clock_gettime (CLOCK_MONOTONIC, &ts); #endif result = ((double)(ts.tv_sec) + (double)(ts.tv_nsec) * 0.000000001); @@ -84,6 +178,7 @@ double dtime_now (void) } + /*------------------------------------------------------------------ * * Name: timestamp_now @@ -104,7 +199,7 @@ double dtime_now (void) void timestamp_now (char *result, int result_size, int show_ms) { - double now = dtime_now(); + double now = dtime_realtime(); time_t t = (int)now; struct tm tm; @@ -150,7 +245,7 @@ void timestamp_now (char *result, int result_size, int show_ms) void timestamp_user_format (char *result, int result_size, char *user_format) { - double now = dtime_now(); + double now = dtime_realtime(); time_t t = (int)now; struct tm tm; @@ -191,7 +286,7 @@ void timestamp_user_format (char *result, int result_size, char *user_format) void timestamp_filename (char *result, int result_size) { - double now = dtime_now(); + double now = dtime_realtime(); time_t t = (int)now; struct tm tm; diff --git a/src/dtime_now.h b/src/dtime_now.h index ac22f7d5..411534bf 100644 --- a/src/dtime_now.h +++ b/src/dtime_now.h @@ -1,9 +1,18 @@ -extern double dtime_now (void); +extern double dtime_realtime (void); + +extern double dtime_monotonic (void); + void timestamp_now (char *result, int result_size, int show_ms); void timestamp_user_format (char *result, int result_size, char *user_format); -void timestamp_filename (char *result, int result_size); \ No newline at end of file +void timestamp_filename (char *result, int result_size); + + +// FIXME: remove temp workaround. +// Needs many scattered updates. + +#define dtime_now dtime_realtime diff --git a/src/dtmf.c b/src/dtmf.c index 788be18b..953b0f70 100644 --- a/src/dtmf.c +++ b/src/dtmf.c @@ -245,7 +245,7 @@ char dtmf_sample (int c, float input) * others in the same group multiplied by some factor. * * For perfect synthetic signals this needs to be in - * the range of about 1.33 (very senstive) to 2.15 (very fussy). + * the range of about 1.33 (very sensitive) to 2.15 (very fussy). * * Too low will cause false triggers on random noise. * Too high will won't decode less than perfect signals. @@ -529,7 +529,7 @@ int main () memset (&my_audio_config, 0, sizeof(my_audio_config)); my_audio_config.adev[ACHAN2ADEV(c)].defined = 1; my_audio_config.adev[ACHAN2ADEV(c)].samples_per_sec = 44100; - my_audio_config.achan[c].medium = MEDIUM_RADIO; + my_audio_config.chan_medium[c] = MEDIUM_RADIO; my_audio_config.achan[c].dtmf_decode = DTMF_DECODE_ON; dtmf_init(&my_audio_config, 50); diff --git a/src/dwgps.c b/src/dwgps.c index 83e92444..ccf24b0b 100644 --- a/src/dwgps.c +++ b/src/dwgps.c @@ -90,7 +90,7 @@ static dw_mutex_t s_gps_mutex; * * Name: dwgps_init * - * Purpose: Intialize the GPS interface. + * Purpose: Initialize the GPS interface. * * Inputs: pconfig Configuration settings. This might include * serial port name for direct connect and host diff --git a/src/dwgpsd.c b/src/dwgpsd.c index 70b650bd..1bc9d47a 100644 --- a/src/dwgpsd.c +++ b/src/dwgpsd.c @@ -1,7 +1,7 @@ // // This file is part of Dire Wolf, an amateur radio packet TNC. // -// Copyright (C) 2013, 2014, 2015, 2020 John Langner, WB2OSZ +// Copyright (C) 2013, 2014, 2015, 2020, 2022 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 @@ -20,7 +20,7 @@ /*------------------------------------------------------------------ * - * Module: dwgps.c + * Module: dwgpsd.c * * Purpose: Interface to location data, i.e. GPS receiver. * @@ -57,13 +57,36 @@ -// An incompatibility was introduced with version 7 -// and again with 9 and again with 10. +// An API incompatibility was introduced with API version 7. +// and again with 9. +// and again with 10. +// We deal with it by using a bunch of conditional code such as: +// #if GPSD_API_MAJOR_VERSION >= 9 -#if GPSD_API_MAJOR_VERSION < 5 || GPSD_API_MAJOR_VERSION > 11 -#error libgps API version might be incompatible. + +// release lib version API Raspberry Pi OS Testing status +// 3.22 28 11 bullseye OK. +// 3.23 29 12 OK. +// 3.25 30 14 OK, Jan. 2023 + + +// 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 +// often but more recent versions have not broken backward compatibility. + +#define MAX_TESTED_VERSION 14 + +#if (GPSD_API_MAJOR_VERSION < 5) || (GPSD_API_MAJOR_VERSION > MAX_TESTED_VERSION) +#pragma message "Your version of gpsd might be incompatible with this application." +#pragma message "The libgps application program interface (API) often" +#pragma message "changes to be incompatible with earlier versions." +// I could not figure out how to do value substitution here. +#pragma message "You have libgpsd API version GPSD_API_MAJOR_VERSION." +#pragma message "The last that has been tested is MAX_TESTED_VERSION." +#pragma message "Even if this builds successfully, it might not run properly." #endif + /* * Information for interface to gpsd daemon. */ @@ -94,7 +117,7 @@ static void * read_gpsd_thread (void *arg); * * Name: dwgpsd_init * - * Purpose: Intialize the GPS interface. + * Purpose: Initialize the GPS interface. * * Inputs: pconfig Configuration settings. This includes * host name or address for network connection. @@ -163,6 +186,22 @@ static void * read_gpsd_thread (void *arg); * can't find it there. Solution is to define environment variable: * * export LD_LIBRARY_PATH=/use/local/lib + * + * January 2023: Now using 64 bit Raspberry Pi OS, bullseye. + * See https://gitlab.com/gpsd/gpsd/-/blob/master/build.adoc + * Try to install in proper library place so we don't have to mess with LD_LIBRARY_PATH. + * + * (Remove any existing gpsd first so we are not mixing mismatched pieces.) + * + * sudo apt-get install libncurses5-dev + * sudo apt-get install gtk+-3.0 + * + * git clone https://gitlab.com/gpsd/gpsd.git gpsd-gitlab + * cd gpsd-gitlab + * scons prefix=/usr libdir=lib/aarch64-linux-gnu + * [ scons check ] + * sudo scons udev-install + * */ @@ -191,7 +230,7 @@ int dwgpsd_init (struct misc_config_s *pconfig, int debug) if (strlen(pconfig->gpsd_host) == 0) { - /* Nothing to do. Leave initial fix value of errror. */ + /* Nothing to do. Leave initial fix value of error. */ return (0); } @@ -364,6 +403,10 @@ static void * read_gpsd_thread (void *arg) default: case MODE_NOT_SEEN: case MODE_NO_FIX: + if (info.fix <= DWFIX_NOT_SEEN) { + text_color_set(DW_COLOR_INFO); + dw_printf ("GPSD: No location fix.\n"); + } if (info.fix >= DWFIX_2D) { text_color_set(DW_COLOR_INFO); dw_printf ("GPSD: Lost location fix.\n"); @@ -393,13 +436,15 @@ static void * read_gpsd_thread (void *arg) if (/*gpsdata.stupid_status >= STATUS_FIX &&*/ gpsdata.fix.mode >= MODE_2D) { - info.dlat = isfinite(gpsdata.fix.latitude) ? gpsdata.fix.latitude : G_UNKNOWN; - info.dlon = isfinite(gpsdata.fix.longitude) ? gpsdata.fix.longitude : G_UNKNOWN; +#define GOOD(x) (isfinite(x) && ! isnan(x)) + + info.dlat = GOOD(gpsdata.fix.latitude) ? gpsdata.fix.latitude : G_UNKNOWN; + info.dlon = GOOD(gpsdata.fix.longitude) ? gpsdata.fix.longitude : G_UNKNOWN; // When stationary, track is NaN which is not finite. - info.track = isfinite(gpsdata.fix.track) ? gpsdata.fix.track : G_UNKNOWN; - info.speed_knots = isfinite(gpsdata.fix.speed) ? (MPS_TO_KNOTS * gpsdata.fix.speed) : G_UNKNOWN; + info.track = GOOD(gpsdata.fix.track) ? gpsdata.fix.track : G_UNKNOWN; + info.speed_knots = GOOD(gpsdata.fix.speed) ? (MPS_TO_KNOTS * gpsdata.fix.speed) : G_UNKNOWN; if (gpsdata.fix.mode >= MODE_3D) { - info.altitude = isfinite(gpsdata.fix.stupid_altitude) ? gpsdata.fix.stupid_altitude : G_UNKNOWN; + info.altitude = GOOD(gpsdata.fix.stupid_altitude) ? gpsdata.fix.stupid_altitude : G_UNKNOWN; } // Otherwise keep last known altitude when we downgrade from 3D to 2D fix. // Caller knows altitude is outdated if info.fix == DWFIX_2D. diff --git a/src/dwgpsnmea.c b/src/dwgpsnmea.c index 840ab652..14cda77e 100644 --- a/src/dwgpsnmea.c +++ b/src/dwgpsnmea.c @@ -144,11 +144,9 @@ int dwgpsnmea_init (struct misc_config_s *pconfig, int debug) /* * Open serial port connection. - * 4800 baud is standard for GPS. - * Should add an option to allow changing someday. */ - s_gpsnmea_port_fd = serial_port_open (pconfig->gpsnmea_port, 4800); + s_gpsnmea_port_fd = serial_port_open (pconfig->gpsnmea_port, pconfig->gpsnmea_speed); if (s_gpsnmea_port_fd != MYFDERROR) { #if __WIN32__ @@ -182,12 +180,10 @@ int dwgpsnmea_init (struct misc_config_s *pconfig, int debug) /* Return fd to share if waypoint wants same device. */ -/* Currently both are fixed speed at 4800. */ -/* If that ever becomes configurable, that needs to be compared too. */ MYFDTYPE dwgpsnmea_get_fd(char *wp_port_name, int speed) { - if (strcmp(s_save_configp->gpsnmea_port, wp_port_name) == 0 && speed == 4800) { + if (strcmp(s_save_configp->gpsnmea_port, wp_port_name) == 0 && speed == s_save_configp->gpsnmea_speed) { return (s_gpsnmea_port_fd); } return (MYFDERROR); @@ -266,9 +262,12 @@ static void * read_gpsnmea_thread (void *arg) } dwgps_set_data (&info); - // TODO: doesn't exist yet - serial_port_close(fd); + serial_port_close(s_gpsnmea_port_fd); s_gpsnmea_port_fd = MYFDERROR; + // TODO: If the open() was in this thread, we could wait a while and + // try to open again. That would allow recovery if the USB GPS device + // is unplugged and plugged in again. break; /* terminate thread. */ } @@ -289,73 +288,53 @@ static void * read_gpsnmea_thread (void *arg) } /* Process sentence. */ +// TODO: More general: Ignore the second letter rather than recognizing only GP... and GN... if (strncmp(gps_msg, "$GPRMC", 6) == 0 || strncmp(gps_msg, "$GNRMC", 6) == 0) { - f = dwgpsnmea_gprmc (gps_msg, 0, &info.dlat, &info.dlon, &info.speed_knots, &info.track); + // Here we just tuck away the course and speed. + // Fix and location will be updated by GxGGA. - if (f == DWFIX_ERROR) { + double ignore_dlat; + double ignore_dlon; + + f = dwgpsnmea_gprmc (gps_msg, 0, &ignore_dlat, &ignore_dlon, &info.speed_knots, &info.track); - /* Parse error. Shouldn't happen. Better luck next time. */ - text_color_set(DW_COLOR_INFO); + if (f == DWFIX_ERROR) { + /* Parse error. Shouldn't happen. Better luck next time. */ + text_color_set(DW_COLOR_ERROR); dw_printf ("GPSNMEA: Error parsing $GPRMC sentence.\n"); dw_printf ("%s\n", gps_msg); } - else if (f == DWFIX_2D) { - - if (info.fix != DWFIX_2D && info.fix != DWFIX_3D) { - - text_color_set(DW_COLOR_INFO); - dw_printf ("GPSNMEA: Location fix is now available.\n"); - - info.fix = DWFIX_2D; // Don't know if 2D or 3D. Take minimum. - } - info.timestamp = time(NULL); - if (s_debug >= 2) { - text_color_set(DW_COLOR_DEBUG); - dwgps_print ("GPSNMEA: ", &info); - } - dwgps_set_data (&info); - } - else { - - if (info.fix == DWFIX_2D || info.fix == DWFIX_3D) { - - text_color_set(DW_COLOR_INFO); - dw_printf ("GPSNMEA: Lost location fix.\n"); - } - info.fix = f; /* lost it. */ - info.timestamp = time(NULL); - if (s_debug >= 2) { - text_color_set(DW_COLOR_DEBUG); - dwgps_print ("GPSNMEA: ", &info); - } - dwgps_set_data (&info); - } - } + else if (strncmp(gps_msg, "$GPGGA", 6) == 0 || strncmp(gps_msg, "$GNGGA", 6) == 0) { int nsat; f = dwgpsnmea_gpgga (gps_msg, 0, &info.dlat, &info.dlon, &info.altitude, &nsat); - /* Only switch between 2D & 3D. */ - /* Let GPRMC handle other changes in fix state and data transfer. */ - if (f == DWFIX_ERROR) { - /* Parse error. Shouldn't happen. Better luck next time. */ - text_color_set(DW_COLOR_INFO); + text_color_set(DW_COLOR_ERROR); dw_printf ("GPSNMEA: Error parsing $GPGGA sentence.\n"); dw_printf ("%s\n", gps_msg); } - else if ((f == DWFIX_3D && info.fix == DWFIX_2D) || - (f == DWFIX_2D && info.fix == DWFIX_3D)) { - text_color_set(DW_COLOR_INFO); - dw_printf ("GPSNMEA: Location fix is now %dD.\n", (int)f); - info.fix = f; + else { + if (f != info.fix) { // Print change in location fix. + text_color_set(DW_COLOR_INFO); + if (f == DWFIX_NO_FIX) dw_printf ("GPSNMEA: Location fix has been lost.\n"); + if (f == DWFIX_2D) dw_printf ("GPSNMEA: Location fix is now 2D.\n"); + if (f == DWFIX_3D) dw_printf ("GPSNMEA: Location fix is now 3D.\n"); + info.fix = f; + } + info.timestamp = time(NULL); + if (s_debug >= 2) { + text_color_set(DW_COLOR_DEBUG); + dwgps_print ("GPSNMEA: ", &info); + } + dwgps_set_data (&info); } } } @@ -436,7 +415,7 @@ static int remove_checksum (char *sent, int quiet) * * Name: dwgpsnmea_gprmc * - * Purpose: Parse $GPRMC sentence and extract interesing parts. + * Purpose: Parse $GPRMC sentence and extract interesting parts. * * Inputs: sentence NMEA sentence. * @@ -446,9 +425,11 @@ static int remove_checksum (char *sent, int quiet) * odlon longitude * oknots speed * ocourse direction of travel. - * + * * Left undefined if not valid. * + * Note: RMC does not contain altitude. + * * Returns: DWFIX_ERROR Parse error. * DWFIX_NO_FIX GPS is there but Position unknown. Could be temporary. * DWFIX_2D Valid position. We don't know if it is really 2D or 3D. @@ -585,7 +566,7 @@ dwfix_t dwgpsnmea_gprmc (char *sentence, int quiet, double *odlat, double *odlon * * Name: dwgpsnmea_gpgga * - * Purpose: Parse $GPGGA sentence and extract interesing parts. + * Purpose: Parse $GPGGA sentence and extract interesting parts. * * Inputs: sentence NMEA sentence. * @@ -598,10 +579,13 @@ dwfix_t dwgpsnmea_gprmc (char *sentence, int quiet, double *odlat, double *odlon * * Left undefined if not valid. * + * Note: GGA has altitude but not course and speed so we need to use both. + * * Returns: DWFIX_ERROR Parse error. * DWFIX_NO_FIX GPS is there but Position unknown. Could be temporary. * DWFIX_2D Valid position. We don't know if it is really 2D or 3D. * Take more cautious value so we don't try using altitude. + * DWFIX_3D Valid 3D position. * * Examples: $GPGGA,001429.00,,,,,0,00,99.99,,,,,,*68 * $GPGGA,212407.000,4237.1505,N,07120.8602,W,0,00,,,M,,M,,*58 @@ -610,9 +594,6 @@ dwfix_t dwgpsnmea_gprmc (char *sentence, int quiet, double *odlat, double *odlon * *--------------------------------------------------------------------*/ - -// TODO: in progress... - dwfix_t dwgpsnmea_gpgga (char *sentence, int quiet, double *odlat, double *odlon, float *oalt, int *onsat) { char stemp[NMEA_MAX_LEN]; /* Make copy because parsing is destructive. */ @@ -709,8 +690,7 @@ dwfix_t dwgpsnmea_gpgga (char *sentence, int quiet, double *odlat, double *odlon return (DWFIX_ERROR); } - - // TODO: num sat... + // TODO: num sat... Why would we care? /* * We can distinguish between 2D & 3D fix by presence diff --git a/src/dwsock.h b/src/dwsock.h index 23e63e11..986f6a29 100644 --- a/src/dwsock.h +++ b/src/dwsock.h @@ -11,7 +11,7 @@ int dwsock_init (void); -int dwsock_connect (char *hostname, char *port, char *description, int allow_ipv6, int debug, char *ipaddr_str); +int dwsock_connect (char *hostname, char *port, char *description, int allow_ipv6, int debug, char ipaddr_str[DWSOCK_IPADDR_LEN]); /* ipaddr_str needs to be at least SOCK_IPADDR_LEN bytes */ char *dwsock_ia_to_text (int Family, void * pAddr, char * pStringBuf, size_t StringBufSize); diff --git a/src/encode_aprs.c b/src/encode_aprs.c index 225cb080..20992bf7 100644 --- a/src/encode_aprs.c +++ b/src/encode_aprs.c @@ -81,8 +81,14 @@ typedef struct position_s { static int set_norm_position (char symtab, char symbol, double dlat, double dlong, int ambiguity, position_t *presult) { + // An over zealous compiler might complain about l*itude_to_str writing + // N characters plus nul to an N character field so we stick it into a + // larger temp then copy the desired number of bytes. (Issue 296) - latitude_to_str (dlat, ambiguity, presult->lat); + char stemp[16]; + + latitude_to_str (dlat, ambiguity, stemp); + memcpy (presult->lat, stemp, sizeof(presult->lat)); if (symtab != '/' && symtab != '\\' && ! isdigit(symtab) && ! isupper(symtab)) { text_color_set(DW_COLOR_ERROR); @@ -90,7 +96,8 @@ static int set_norm_position (char symtab, char symbol, double dlat, double dlon } presult->sym_table_id = symtab; - longitude_to_str (dlong, ambiguity, presult->lon); + longitude_to_str (dlong, ambiguity, stemp); + memcpy (presult->lon, stemp, sizeof(presult->lon)); if (symbol < '!' || symbol > '~') { text_color_set(DW_COLOR_ERROR); @@ -118,7 +125,7 @@ static int set_norm_position (char symtab, char symbol, double dlat, double dlon * height - Feet. * gain - dBi. * - * course - Degress, 0 - 360 (360 equiv. to 0). + * course - Degrees, 0 - 360 (360 equiv. to 0). * Use G_UNKNOWN for none or unknown. * speed - knots. * @@ -336,7 +343,7 @@ static int phg_data_extension (int power, int height, int gain, char *dir, char * * Purpose: Fill in parts of the course & speed data extension. * - * Inputs: course - Degress, 0 - 360 (360 equiv. to 0). + * Inputs: course - Degrees, 0 - 360 (360 equiv. to 0). * Use G_UNKNOWN for none or unknown. * * speed - knots. @@ -487,7 +494,7 @@ static int frequency_spec (float freq, float tone, float offset, char *presult) * gain - dB. Not clear if it is dBi or dBd. * dir - Directivity: N, NE, etc., omni. * - * course - Degress, 0 - 360 (360 equiv. to 0). + * course - Degrees, 0 - 360 (360 equiv. to 0). * Use G_UNKNOWN for none or unknown. * speed - knots. // TODO: should distinguish unknown(not revevant) vs. known zero. * @@ -497,7 +504,7 @@ static int frequency_spec (float freq, float tone, float offset, char *presult) * * comment - Additional comment text. * - * result_size - Ammount of space for result, provideed by + * result_size - Amount of space for result, provided by * caller, to avoid buffer overflow. * * Outputs: presult - Stored here. Should be at least ??? bytes. @@ -512,7 +519,7 @@ static int frequency_spec (float freq, float tone, float offset, char *presult) * Power/height/gain/directivity or * Course/speed. * - * Afer that, + * After that, * *----------------------------------------------------------------*/ @@ -544,6 +551,15 @@ int encode_position (int messaging, int compressed, double lat, double lon, int int result_len = 0; if (compressed) { + +// Thought: +// https://groups.io/g/direwolf/topic/92718535#6886 +// When speed is zero, we could put the altitude in the compressed +// position rather than having /A=999999. +// However, the resolution would be decreased and that could be important +// when hiking in hilly terrain. It would also be confusing to +// flip back and forth between two different representations. + aprs_compressed_pos_t *p = (aprs_compressed_pos_t *)presult; p->dti = messaging ? '=' : '!'; @@ -629,7 +645,7 @@ int encode_position (int messaging, int compressed, double lat, double lon, int * gain - dB. Not clear if it is dBi or dBd. * dir - Direction: N, NE, etc., omni. * - * course - Degress, 0 - 360 (360 equiv. to 0). + * course - Degrees, 0 - 360 (360 equiv. to 0). * Use G_UNKNOWN for none or unknown. * speed - knots. * @@ -639,7 +655,7 @@ int encode_position (int messaging, int compressed, double lat, double lon, int * * comment - Additional comment text. * - * result_size - Ammount of space for result, provideed by + * result_size - Amount of space for result, provided by * caller, to avoid buffer overflow. * * Outputs: presult - Stored here. Should be at least ??? bytes. @@ -769,7 +785,7 @@ int encode_object (char *name, int compressed, time_t thyme, double lat, double * Inputs: addressee - Addressed to, up to 9 characters. * text - Text part of the message. * id - Identifier, 0 to 5 characters. - * result_size - Ammount of space for result, provided by + * result_size - Amount of space for result, provided by * caller, to avoid buffer overflow. * * Outputs: presult - Stored here. @@ -853,7 +869,7 @@ int main (int argc, char *argv[]) dw_printf ("%s\n", result); if (strcmp(result, "!4234.61ND07126.47W&PHG7368") != 0) { dw_printf ("ERROR! line %d\n", __LINE__); errors++; } -/* with freq & tone. minus offset, no offset, explict simplex. */ +/* with freq & tone. minus offset, no offset, explicit simplex. */ encode_position (0, 0, 42+34.61/60, -(71+26.47/60), 0, G_UNKNOWN, 'D', '&', 0, 0, 0, NULL, G_UNKNOWN, 0, 146.955, 74.4, -0.6, NULL, result, sizeof(result)); diff --git a/src/fsk_demod_agc.h b/src/fsk_demod_agc.h deleted file mode 100644 index 95c80794..00000000 --- a/src/fsk_demod_agc.h +++ /dev/null @@ -1,2 +0,0 @@ -#define TUNE_MS_FILTER_SIZE 140 -#define TUNE_PRE_BAUD 1.080 diff --git a/src/fsk_demod_state.h b/src/fsk_demod_state.h index f7b56502..c9b26c23 100644 --- a/src/fsk_demod_state.h +++ b/src/fsk_demod_state.h @@ -2,6 +2,8 @@ #ifndef FSK_DEMOD_STATE_H +#include // int64_t + #include "rpack.h" #include "audio.h" // for enum modem_t @@ -20,6 +22,32 @@ typedef enum bp_window_e { BP_WINDOW_TRUNCATED, BP_WINDOW_BLACKMAN, BP_WINDOW_FLATTOP } bp_window_t; +// Experimental low pass filter to detect DC bias or low frequency changes. +// IIR behaves like an analog R-C filter. +// Intuitively, it seems like FIR would be better because it is based on a finite history. +// However, it would require MANY taps and a LOT of computation for a low frequency. +// We can use a little trick here to keep a running average. +// This would be equivalent to convolving with an array of all 1 values. +// That would eliminate the need to multiply. +// We can also eliminate the need to add them all up each time by keeping a running total. +// Add a sample to the total when putting it in our array of recent samples. +// Subtract it from the total when it gets pushed off the end. +// We can also eliminate the need to shift them all down by using a circular buffer. + +#define CIC_LEN_MAX 4000 + +typedef struct cic_s { + int len; // Number of elements used. + // Might want to dynamically allocate. + short in[CIC_LEN_MAX]; // Samples coming in. + int sum; // Running sum. + int inext; // Next position to fill. +} cic_t; + + +#define MAX_FILTER_SIZE 480 /* 401 is needed for profile A, 300 baud & 44100. Revisit someday. */ + // Size comes out to 417 for 1200 bps with 48000 sample rate + // v1.7 - Was 404. Bump up to 480. struct demodulator_state_s { @@ -39,30 +67,12 @@ struct demodulator_state_s // Data is sampled when it overflows. - int ms_filter_size; /* Size of mark & space filters, in audio samples. */ - /* Started off as a guess of one bit length */ - /* but about 2 bit times turned out to be better. */ - /* Currently using same size for any prefilter. */ - - -#define MAX_FILTER_SIZE 320 /* 304 is needed for profile C, 300 baud & 44100. */ - -/* - * Filter length for Mark & Space in bit times. - * e.g. 1 means 1/1200 second for 1200 baud. - */ - float ms_filter_len_bits; - float lp_delay_fract; - /* * Window type for the various filters. */ - bp_window_t pre_window; - bp_window_t ms_window; bp_window_t lp_window; - /* * Alternate Low pass filters. * First is arbitrary number for quick IIR. @@ -78,16 +88,13 @@ struct demodulator_state_s /* In practice, it turned out a little larger */ /* for profiles B, C, D. */ - float lp_filter_len_bits; /* Length in number of bit times. */ + float lp_filter_width_sym; /* Length in number of symbol times. */ - int lp_filter_size; /* Size of Low Pass filter, in audio samples. */ - /* Previously it was always the same as the M/S */ - /* filters but in version 1.2 it's now independent. */ +#define lp_filter_len_bits lp_filter_width_sym // FIXME: temp hack - int lp_filter_delay; /* Number of samples that the low pass filter */ - /* delays the signal. */ - - /* New in 1.6. */ + int lp_filter_taps; /* Size of Low Pass filter, in audio samples. */ + +#define lp_filter_size lp_filter_taps // FIXME: temp hack /* @@ -111,6 +118,7 @@ struct demodulator_state_s /* * Phase Locked Loop (PLL) inertia. * Larger number means less influence by signal transitions. + * It is more resistant to change when locked on to a signal. */ float pll_locked_inertia; float pll_searching_inertia; @@ -129,23 +137,17 @@ struct demodulator_state_s /* lower = min(1600,1800) - 0.5 * 300 = 1450 */ /* upper = max(1600,1800) + 0.5 * 300 = 1950 */ - float pre_filter_len_bits; /* Length in number of bit times. */ - - int pre_filter_size; /* Size of pre filter, in audio samples. */ + float pre_filter_len_sym; // Length in number of symbol times. +#define pre_filter_len_bits pre_filter_len_sym // temp until all references changed. - float pre_filter[MAX_FILTER_SIZE] __attribute__((aligned(16))); + bp_window_t pre_window; // Window type for filter shaping. + int pre_filter_taps; // Calculated number of filter taps. +#define pre_filter_size pre_filter_taps // temp until all references changed. -/* - * Kernel for the mark and space detection filters. - */ - - float m_sin_table[MAX_FILTER_SIZE] __attribute__((aligned(16))); - float m_cos_table[MAX_FILTER_SIZE] __attribute__((aligned(16))); - - float s_sin_table[MAX_FILTER_SIZE] __attribute__((aligned(16))); - float s_cos_table[MAX_FILTER_SIZE] __attribute__((aligned(16))); + float pre_filter[MAX_FILTER_SIZE] __attribute__((aligned(16))); + float raw_cb[MAX_FILTER_SIZE] __attribute__((aligned(16))); // audio in, need better name. /* * The rest are continuously updated. @@ -154,11 +156,6 @@ struct demodulator_state_s unsigned int lo_phase; /* Local oscillator for PSK. */ -/* - * Most recent raw audio samples, before/after prefiltering. - */ - float raw_cb[MAX_FILTER_SIZE] __attribute__((aligned(16))); - /* * Use half of the AGC code to get a measure of input audio amplitude. * These use "quick" attack and "sluggish" decay while the @@ -170,24 +167,14 @@ struct demodulator_state_s float alevel_mark_peak; float alevel_space_peak; -/* - * Input to the mark/space detector. - * Could be prefiltered or raw audio. - */ - float ms_in_cb[MAX_FILTER_SIZE] __attribute__((aligned(16))); - /* * Outputs from the mark and space amplitude detection, * used as inputs to the FIR lowpass filters. * Kernel for the lowpass filters. */ - float m_amp_cb[MAX_FILTER_SIZE] __attribute__((aligned(16))); - float s_amp_cb[MAX_FILTER_SIZE] __attribute__((aligned(16))); - float lp_filter[MAX_FILTER_SIZE] __attribute__((aligned(16))); - float m_peak, s_peak; float m_valley, s_valley; float m_amp_prev, s_amp_prev; @@ -232,6 +219,12 @@ struct demodulator_state_s signed int prev_d_c_pll; // Previous value of above, before // incrementing, to detect overflows. + int pll_symbol_count; // Number symbols during time nudge_total is accumulated. + int64_t pll_nudge_total; // Sum of DPLL nudge amounts. + // Both of these are cleared at start of frame. + // At end of frame, we can see if incoming + // baud rate is a little off. + int prev_demod_data; // Previous data bit detected. // Used to look for transitions. float prev_demod_out_f; @@ -266,6 +259,122 @@ struct demodulator_state_s union { +////////////////////////////////////////////////////////////////////////////////// +// // +// AFSK only - new method in 1.7 // +// // +////////////////////////////////////////////////////////////////////////////////// + + + struct afsk_only_s { + + unsigned int m_osc_phase; // Phase for Mark local oscillator. + unsigned int m_osc_delta; // How much to change for each audio sample. + + unsigned int s_osc_phase; // Phase for Space local oscillator. + unsigned int s_osc_delta; // How much to change for each audio sample. + + unsigned int c_osc_phase; // Phase for Center frequency local oscillator. + unsigned int c_osc_delta; // How much to change for each audio sample. + + // Need two mixers for profile "A". + + float m_I_raw[MAX_FILTER_SIZE] __attribute__((aligned(16))); + float m_Q_raw[MAX_FILTER_SIZE] __attribute__((aligned(16))); + + float s_I_raw[MAX_FILTER_SIZE] __attribute__((aligned(16))); + float s_Q_raw[MAX_FILTER_SIZE] __attribute__((aligned(16))); + + // Only need one mixer for profile "B". Reuse the same storage? + +//#define c_I_raw m_I_raw +//#define c_Q_raw m_Q_raw + float c_I_raw[MAX_FILTER_SIZE] __attribute__((aligned(16))); + float c_Q_raw[MAX_FILTER_SIZE] __attribute__((aligned(16))); + + int use_rrc; // Use RRC rather than generic low pass. + + float rrc_width_sym; /* Width of RRC filter in number of symbols. */ + + float rrc_rolloff; /* Rolloff factor for RRC. Between 0 and 1. */ + + float prev_phase; // To see phase shift between samples for FM demod. + + float normalize_rpsam; // Normalize to -1 to +1 for expected tones. + + } afsk; + +////////////////////////////////////////////////////////////////////////////////// +// // +// Baseband only, AKA G3RUH // +// // +////////////////////////////////////////////////////////////////////////////////// + +// TODO: Continue experiments with root raised cosine filter. +// Either switch to that or take out all the related stuff. + + struct bb_only_s { + + float rrc_width_sym; /* Width of RRC filter in number of symbols. */ + + float rrc_rolloff; /* Rolloff factor for RRC. Between 0 and 1. */ + + int rrc_filter_taps; // Number of elements used in the next two. + +// FIXME: TODO: reevaluate max size needed. + + float audio_in[MAX_FILTER_SIZE] __attribute__((aligned(16))); // Audio samples in. + + + float lp_filter[MAX_FILTER_SIZE] __attribute__((aligned(16))); // Low pass filter. + + // New in 1.7 - Polyphase filter to reduce CPU requirements. + + float lp_polyphase_1[MAX_FILTER_SIZE] __attribute__((aligned(16))); + float lp_polyphase_2[MAX_FILTER_SIZE] __attribute__((aligned(16))); + float lp_polyphase_3[MAX_FILTER_SIZE] __attribute__((aligned(16))); + float lp_polyphase_4[MAX_FILTER_SIZE] __attribute__((aligned(16))); + + float lp_1_iir_param; // very low pass filters to get DC offset. + float lp_1_out; + + float lp_2_iir_param; + float lp_2_out; + + float agc_1_fast_attack; // Signal envelope detection. + float agc_1_slow_decay; + float agc_1_peak; + float agc_1_valley; + + float agc_2_fast_attack; + float agc_2_slow_decay; + float agc_2_peak; + float agc_2_valley; + + float agc_3_fast_attack; + float agc_3_slow_decay; + float agc_3_peak; + float agc_3_valley; + + // CIC low pass filters to detect DC bias or low frequency changes. + // IIR behaves like an analog R-C filter. + // Intuitively, it seems like FIR would be better because it is based on a finite history. + // However, it would require MANY taps and a LOT of computation for a low frequency. + // We can use a little trick here to keep a running average. + // This would be equivalent to convolving with an array of all 1 values. + // That would eliminate the need to multiply. + // We can also eliminate the need to add them all up each time by keeping a running total. + // Add a sample to the total when putting it in our array of recent samples. + // Subtract it from the total when it gets pushed off the end. + // We can also eliminate the need to shift them all down by using a circular buffer. + // This only works with integers because float would have cumulated round off errors. + + cic_t cic_center1; + cic_t cic_above; + cic_t cic_below; + + } bb; + ////////////////////////////////////////////////////////////////////////////////// // // // PSK only. // diff --git a/src/fx25_init.c b/src/fx25_init.c index 9031a6de..47423254 100644 --- a/src/fx25_init.c +++ b/src/fx25_init.c @@ -371,6 +371,7 @@ int fx25_pick_mode (int fx_mode, int dlen) // The PRUG FX.25 TNC has additional modes that will handle larger frames // by using multiple RS blocks. This is a future possibility but needs // to be coordinated with other FX.25 developers so we maintain compatibility. +// See https://web.tapr.org/meetings/DCC_2020/JE1WAZ/DCC-2020-PRUG-FINAL.pptx static const int prefer[6] = { 0x04, 0x03, 0x06, 0x09, 0x05, 0x01 }; for (int k = 0; k < 6; k++) { @@ -410,19 +411,25 @@ struct rs *INIT_RS(unsigned int symsize,unsigned int gfpoly,unsigned fcr,unsigne return NULL; /* Can't have more roots than symbol values! */ rs = (struct rs *)calloc(1,sizeof(struct rs)); + if (rs == NULL) { + text_color_set(DW_COLOR_ERROR); + dw_printf ("FATAL ERROR: Out of memory.\n"); + exit (EXIT_FAILURE); + } rs->mm = symsize; rs->nn = (1<alpha_to = (DTYPE *)malloc(sizeof(DTYPE)*(rs->nn+1)); + rs->alpha_to = (DTYPE *)calloc((rs->nn+1),sizeof(DTYPE)); if(rs->alpha_to == NULL){ - free(rs); - return NULL; + text_color_set(DW_COLOR_ERROR); + dw_printf ("FATAL ERROR: Out of memory.\n"); + exit (EXIT_FAILURE); } - rs->index_of = (DTYPE *)malloc(sizeof(DTYPE)*(rs->nn+1)); + rs->index_of = (DTYPE *)calloc((rs->nn+1),sizeof(DTYPE)); if(rs->index_of == NULL){ - free(rs->alpha_to); - free(rs); - return NULL; + text_color_set(DW_COLOR_ERROR); + dw_printf ("FATAL ERROR: Out of memory.\n"); + exit (EXIT_FAILURE); } /* Generate Galois field lookup tables */ @@ -446,14 +453,13 @@ struct rs *INIT_RS(unsigned int symsize,unsigned int gfpoly,unsigned fcr,unsigne } /* Form RS code generator polynomial from its roots */ - rs->genpoly = (DTYPE *)malloc(sizeof(DTYPE)*(nroots+1)); + rs->genpoly = (DTYPE *)calloc((nroots+1),sizeof(DTYPE)); if(rs->genpoly == NULL){ - free(rs->alpha_to); - free(rs->index_of); - free(rs); - return NULL; + text_color_set(DW_COLOR_ERROR); + dw_printf ("FATAL ERROR: Out of memory.\n"); + exit (EXIT_FAILURE); } - rs->fcr = fcr; + rs->fcr = fcr; rs->prim = prim; rs->nroots = nroots; @@ -482,7 +488,7 @@ struct rs *INIT_RS(unsigned int symsize,unsigned int gfpoly,unsigned fcr,unsigne } // diagnostic prints -/* +#if 0 printf("Alpha To:\n\r"); for (i=0; i < sizeof(DTYPE)*(rs->nn+1); i++) printf("0x%2x,", rs->alpha_to[i]); @@ -497,7 +503,7 @@ struct rs *INIT_RS(unsigned int symsize,unsigned int gfpoly,unsigned fcr,unsigne for (i = 0; i <= nroots; i++) printf("0x%2x,", rs->genpoly[i]); printf("\n\r"); -*/ +#endif return rs; } diff --git a/src/fx25_rec.c b/src/fx25_rec.c index 3ab78518..9cb5c4d9 100644 --- a/src/fx25_rec.c +++ b/src/fx25_rec.c @@ -258,7 +258,7 @@ int fx25_rec_busy (int chan) { assert (chan >= 0 && chan < MAX_CHANS); - // This could be a litle faster if we knew number of + // This could be a little faster if we knew number of // subchannels and slicers but it is probably insignificant. for (int i = 0; i < MAX_SUBCHANS; i++) { diff --git a/src/gen_packets.c b/src/gen_packets.c index b0977906..57b2741c 100644 --- a/src/gen_packets.c +++ b/src/gen_packets.c @@ -1,7 +1,7 @@ // // This file is part of Dire Wolf, an amateur radio packet TNC. // -// Copyright (C) 2011, 2013, 2014, 2015, 2016, 2019 John Langner, WB2OSZ +// Copyright (C) 2011, 2013, 2014, 2015, 2016, 2019, 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 @@ -56,7 +56,15 @@ * gen_packets -n 100 -o z2.wav * atest z2.wav * - * + * Variable speed. e.g. 95% to 105% of normal speed. + * Required parameter is max % below and above normal. + * Optionally specify step other than 0.1%. + * Used to test how tolerant TNCs are to senders not + * not using exactly the right baud rate. + * + * gen_packets -v 5 + * gen_packets -v 5,0.5 + * *------------------------------------------------------------------*/ @@ -67,6 +75,7 @@ #include #include #include +#include #include "audio.h" #include "ax25_pad.h" @@ -76,6 +85,7 @@ #include "morse.h" #include "dtmf.h" #include "fx25.h" +#include "il2p.h" /* Own random number generator so we can get */ @@ -99,6 +109,7 @@ static float g_noise_level = 0; static int g_morse_wpm = 0; /* Send morse code at this speed. */ + static struct audio_s modem; @@ -107,13 +118,48 @@ static void send_packet (char *str) packet_t pp; unsigned char fbuf[AX25_MAX_PACKET_LEN+2]; int flen; - int c; + int c = 0; // channel number. if (g_morse_wpm > 0) { - // TODO: Why not use the destination field instead of command line option? + // Why not use the destination field instead of command line option? + // For one thing, this is not in TNC-2 monitor format. + + morse_send (c, str, g_morse_wpm, 100, 100); + } + else if (modem.achan[0].modem_type == MODEM_EAS) { + +// Generate EAS SAME signal FOR RESEARCH AND TESTING ONLY!!! +// There could be legal consequences for sending unauhorized SAME +// over the radio so don't do it! + + // I'm expecting to see TNC 2 monitor format. + // The source and destination are ignored. + // The optional destination SSID is the number of times to repeat. + // The user defined data type indicator can optionally be used + // for compatibility with how it is received and presented to client apps. + // Examples: + // X>X-3:{DEZCZC-WXR-RWT-033019-033017-033015-033013-033011-025011-025017-033007-033005-033003-033001-025009-025027-033009+0015-1691525-KGYX/NWS- + // X>X:NNNN + + pp = ax25_from_text (str, 1); + if (pp == NULL) { + text_color_set(DW_COLOR_ERROR); + dw_printf ("\"%s\" is not valid TNC2 monitoring format.\n", str); + return; + } + unsigned char *pinfo; + int info_len = ax25_get_info (pp, &pinfo); + if (info_len >= 3 && strncmp((char*)pinfo, "{DE", 3) == 0) { + pinfo += 3; + info_len -= 3; + } + + int repeat = ax25_get_ssid (pp, AX25_DESTINATION); + if (repeat == 0) repeat = 1; - morse_send (0, str, g_morse_wpm, 100, 100); + eas_send (c, pinfo, repeat, 500, 500); + ax25_delete (pp); } else { pp = ax25_from_text (str, 1); @@ -123,6 +169,10 @@ static void send_packet (char *str) return; } flen = ax25_pack (pp, fbuf); + (void)flen; + + // If stereo, put same thing in each channel. + for (c=0; c MAX_BAUD)) { - text_color_set(DW_COLOR_ERROR); - dw_printf ("Use a more reasonable bit rate in range of %d - %d.\n", MIN_BAUD, MAX_BAUD); - exit (EXIT_FAILURE); - } /* We have similar logic in direwolf.c, config.c, gen_packets.c, and atest.c, */ /* that need to be kept in sync. Maybe it could be a common function someday. */ - if (modem.achan[0].baud == 100) { + if (modem.achan[0].baud == 100) { // What was this for? modem.achan[0].modem_type = MODEM_AFSK; modem.achan[0].mark_freq = 1615; modem.achan[0].space_freq = 1785; } + else if (modem.achan[0].baud == 0xEA5EA5) { + modem.achan[0].baud = 521; // Fine tuned later. 520.83333 + // Proper fix is to make this float. + modem.achan[0].modem_type = MODEM_EAS; + modem.achan[0].mark_freq = 2083.3333; // Ideally these should be floating point. + modem.achan[0].space_freq = 1562.5000 ; + } else if (modem.achan[0].baud < 600) { modem.achan[0].modem_type = MODEM_AFSK; modem.achan[0].mark_freq = 1600; // Typical for HF SSB @@ -318,6 +380,11 @@ int main(int argc, char **argv) text_color_set(DW_COLOR_INFO); dw_printf ("Using scrambled baseband signal rather than AFSK.\n"); } + if (modem.achan[0].baud != 100 && (modem.achan[0].baud < MIN_BAUD || modem.achan[0].baud > MAX_BAUD)) { + text_color_set(DW_COLOR_ERROR); + dw_printf ("Use a more reasonable bit rate in range of %d - %d.\n", MIN_BAUD, MAX_BAUD); + exit (EXIT_FAILURE); + } break; case 'g': /* -g for g3ruh scrambling */ @@ -424,7 +491,7 @@ int main(int argc, char **argv) case '2': /* -2 for 2 channels of sound */ modem.adev[0].num_channels = 2; - modem.achan[1].medium = MEDIUM_RADIO; + modem.chan_medium[1] = MEDIUM_RADIO; text_color_set(DW_COLOR_INFO); dw_printf("2 channels of sound rather than 1.\n"); break; @@ -453,9 +520,29 @@ int main(int argc, char **argv) case 'X': - modem.fx25_xmit_enable = atoi(optarg); + X_opt = atoi(optarg); break; + case 'I': // IL2P, normal polarity + + I_opt = atoi(optarg); + break; + + case 'i': // IL2P, inverted polarity + + i_opt = atoi(optarg); + break; + + case 'v': // Variable speed data + an - this percentage + // optional comma and increment. + + variable_speed_max_error = fabs(atof(optarg)); + char *q = strchr(optarg, ','); + if (q != NULL) { + variable_speed_increment = fabs(atof(q+1)); + } + break; + case '?': /* Unknown option message was already printed. */ @@ -466,7 +553,7 @@ int main(int argc, char **argv) /* Should not be here. */ text_color_set(DW_COLOR_ERROR); - dw_printf("?? getopt returned character code 0%o ??\n", c); + dw_printf("?? getopt returned character code 0%o ??\n", (unsigned)c); usage (argv); } } @@ -507,6 +594,43 @@ int main(int argc, char **argv) exit (1); } + if (X_opt > 0) { + if (I_opt != -1 || i_opt != -1) { + text_color_set(DW_COLOR_ERROR); + dw_printf ("Can't mix -X with -I or -i.\n"); + exit (EXIT_FAILURE); + } + modem.achan[0].fx25_strength = X_opt; + modem.achan[0].layer2_xmit = LAYER2_FX25; + } + + if (I_opt != -1 && i_opt != -1) { + text_color_set(DW_COLOR_ERROR); + dw_printf ("Can't use both -I and -i at the same time.\n"); + exit (EXIT_FAILURE); + } + + if (I_opt >= 0) { + text_color_set(DW_COLOR_INFO); + dw_printf ("Using IL2P normal polarity.\n"); + modem.achan[0].layer2_xmit = LAYER2_IL2P; + modem.achan[0].il2p_max_fec = (I_opt > 0); + modem.achan[0].il2p_invert_polarity = 0; // normal + } + + if (i_opt >= 0) { + text_color_set(DW_COLOR_INFO); + dw_printf ("Using IL2P inverted polarity.\n"); + modem.achan[0].layer2_xmit = LAYER2_IL2P; + modem.achan[0].il2p_max_fec = (i_opt > 0); + modem.achan[0].il2p_invert_polarity = 1; // invert for transmit + if (modem.achan[0].baud == 1200) { + text_color_set(DW_COLOR_ERROR); + dw_printf ("Using -i with 1200 bps is a bad idea. Use -I instead.\n"); + } + } + + /* * Open the output file. */ @@ -536,6 +660,7 @@ int main(int argc, char **argv) // Just use the default of minimal information. fx25_init (1); + il2p_init (0); // There are no "-d" options so far but it could be handy here. assert (modem.adev[0].bits_per_sample == 8 || modem.adev[0].bits_per_sample == 16); assert (modem.adev[0].num_channels == 1 || modem.adev[0].num_channels == 2); @@ -596,9 +721,35 @@ int main(int argc, char **argv) */ text_color_set(DW_COLOR_INFO); dw_printf ("built in message...\n"); - - if (packet_count > 0) { +// +// Generate packets with variable speed. +// This overrides any other number of packets or adding noise. +// + + + if (variable_speed_max_error != 0) { + + int normal_speed = modem.achan[0].baud; + + text_color_set(DW_COLOR_INFO); + dw_printf ("Variable speed.\n"); + + for (double speed_error = - variable_speed_max_error; + speed_error <= variable_speed_max_error + 0.001; + speed_error += variable_speed_increment) { + + // Baud is int so we get some roundoff. Make it real? + modem.achan[0].baud = (int)round(normal_speed * (1. + speed_error / 100.)); + gen_tone_init (&modem, amplitude/2, 1); + + char stemp[256]; + snprintf (stemp, sizeof(stemp), "WB2OSZ-15>TEST:, speed %+0.1f%% The quick brown fox jumps over the lazy dog!", speed_error); + send_packet (stemp); + } + } + + else if (packet_count > 0) { /* * Generate packets with increasing noise level. @@ -640,14 +791,23 @@ int main(int argc, char **argv) } else { + // This should send a total of 6. + // Note that sticking in the user defined type {DE is optional. + + if (modem.achan[0].modem_type == MODEM_EAS) { + send_packet ("X>X-3:{DEZCZC-WXR-RWT-033019-033017-033015-033013-033011-025011-025017-033007-033005-033003-033001-025009-025027-033009+0015-1691525-KGYX/NWS-"); + send_packet ("X>X-2:{DENNNN"); + send_packet ("X>X:NNNN"); + } + else { /* * Builtin default 4 packets. */ - - send_packet ("WB2OSZ-15>TEST:,The quick brown fox jumps over the lazy dog! 1 of 4"); - send_packet ("WB2OSZ-15>TEST:,The quick brown fox jumps over the lazy dog! 2 of 4"); - send_packet ("WB2OSZ-15>TEST:,The quick brown fox jumps over the lazy dog! 3 of 4"); - send_packet ("WB2OSZ-15>TEST:,The quick brown fox jumps over the lazy dog! 4 of 4"); + send_packet ("WB2OSZ-15>TEST:,The quick brown fox jumps over the lazy dog! 1 of 4"); + send_packet ("WB2OSZ-15>TEST:,The quick brown fox jumps over the lazy dog! 2 of 4"); + send_packet ("WB2OSZ-15>TEST:,The quick brown fox jumps over the lazy dog! 3 of 4"); + send_packet ("WB2OSZ-15>TEST:,The quick brown fox jumps over the lazy dog! 4 of 4"); + } } audio_file_close(); @@ -665,11 +825,13 @@ static void usage (char **argv) dw_printf ("Options:\n"); dw_printf (" -a Signal amplitude in range of 0 - 200%%. Default 50.\n"); dw_printf (" -b Bits / second for data. Default is %d.\n", DEFAULT_BAUD); - dw_printf (" -B Bits / second for data. Proper modem selected for 300, 1200, 2400, 4800, 9600.\n"); + dw_printf (" -B Bits / second for data. Proper modem selected for 300, 1200, 2400, 4800, 9600, EAS.\n"); dw_printf (" -g Scrambled baseband rather than AFSK.\n"); dw_printf (" -j 2400 bps QPSK compatible with direwolf <= 1.5.\n"); dw_printf (" -J 2400 bps QPSK compatible with MFJ-2400.\n"); - dw_printf (" -X n Generate FX.25 frames. Specify number of check bytes: 16, 32, or 64.\n"); + dw_printf (" -X n 1 to enable FX.25 transmit. 16, 32, 64 for specific number of check bytes.\n"); + dw_printf (" -I n Enable IL2P transmit. n=1 is recommended. 0 uses weaker FEC.\n"); + dw_printf (" -i n Enable IL2P transmit, inverted polarity. n=1 is recommended. 0 uses weaker FEC.\n"); dw_printf (" -m Mark frequency. Default is %d.\n", DEFAULT_MARK_FREQ); dw_printf (" -s Space frequency. Default is %d.\n", DEFAULT_SPACE_FREQ); dw_printf (" -r Audio sample Rate. Default is %d.\n", DEFAULT_SAMPLES_PER_SEC); @@ -677,6 +839,7 @@ static void usage (char **argv) dw_printf (" -o Send output to .wav file.\n"); dw_printf (" -8 8 bit audio rather than 16.\n"); dw_printf (" -2 2 channels (stereo) audio rather than one channel.\n"); + dw_printf (" -v max[,incr] Variable speed with specified maximum error and increment.\n"); // dw_printf (" -z Number of leading zero bits before frame.\n"); // dw_printf (" Default is 12 which is .01 seconds at 1200 bits/sec.\n"); @@ -685,6 +848,7 @@ static void usage (char **argv) dw_printf ("the default built-in message. The format should correspond to\n"); dw_printf ("the standard packet monitoring representation such as,\n\n"); dw_printf (" WB2OSZ-1>APDW12,WIDE2-2:!4237.14NS07120.83W#\n"); + dw_printf ("User defined content can't be used with -n option.\n"); dw_printf ("\n"); dw_printf ("Example: gen_packets -o x.wav \n"); dw_printf ("\n"); diff --git a/src/gen_tone.c b/src/gen_tone.c index 68f72bc0..6a816556 100644 --- a/src/gen_tone.c +++ b/src/gen_tone.c @@ -1,7 +1,7 @@ // // This file is part of Dire Wolf, an amateur radio packet TNC. // -// Copyright (C) 2011, 2014, 2015, 2016, 2019 John Langner, WB2OSZ +// Copyright (C) 2011, 2014, 2015, 2016, 2019, 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 @@ -70,6 +70,7 @@ static int ticks_per_sample[MAX_CHANS]; /* Same for both channels of same soundc 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 short sine_table[256]; @@ -164,7 +165,7 @@ int gen_tone_init (struct audio_s *audio_config_p, int amp, int gen_packets) for (chan = 0; chan < MAX_CHANS; chan++) { - if (audio_config_p->achan[chan].medium == MEDIUM_RADIO) { + if (audio_config_p->chan_medium[chan] == MEDIUM_RADIO) { int a = ACHAN2ADEV(chan); @@ -198,8 +199,11 @@ int gen_tone_init (struct audio_s *audio_config_p, int amp, int gen_packets) ticks_per_bit[chan] = (int) ((TICKS_PER_CYCLE / ((double)audio_config_p->achan[chan].baud * 0.5)) + 0.5); f1_change_per_sample[chan] = (int) (((double)audio_config_p->achan[chan].mark_freq * TICKS_PER_CYCLE / (double)audio_config_p->adev[a].samples_per_sec ) + 0.5); f2_change_per_sample[chan] = f1_change_per_sample[chan]; // Not used. + samples_per_symbol[chan] = 2. * (float)audio_config_p->adev[a].samples_per_sec / (float)audio_config_p->achan[chan].baud; tone_phase[chan] = PHASE_SHIFT_45; // Just to mimic first attempt. + // ??? Why? We are only concerned with the difference + // from one symbol to the next. break; case MODEM_8PSK: @@ -211,6 +215,7 @@ int gen_tone_init (struct audio_s *audio_config_p, int amp, int gen_packets) ticks_per_bit[chan] = (int) ((TICKS_PER_CYCLE / ((double)audio_config_p->achan[chan].baud / 3.)) + 0.5); f1_change_per_sample[chan] = (int) (((double)audio_config_p->achan[chan].mark_freq * TICKS_PER_CYCLE / (double)audio_config_p->adev[a].samples_per_sec ) + 0.5); f2_change_per_sample[chan] = f1_change_per_sample[chan]; // Not used. + samples_per_symbol[chan] = 3. * (float)audio_config_p->adev[a].samples_per_sec / (float)audio_config_p->achan[chan].baud; break; case MODEM_BASEBAND: @@ -220,11 +225,23 @@ int gen_tone_init (struct audio_s *audio_config_p, int amp, int gen_packets) // Tone is half baud. ticks_per_bit[chan] = (int) ((TICKS_PER_CYCLE / (double)audio_config_p->achan[chan].baud ) + 0.5); f1_change_per_sample[chan] = (int) (((double)audio_config_p->achan[chan].baud * 0.5 * TICKS_PER_CYCLE / (double)audio_config_p->adev[a].samples_per_sec ) + 0.5); + samples_per_symbol[chan] = (float)audio_config_p->adev[a].samples_per_sec / (float)audio_config_p->achan[chan].baud; + break; + + case MODEM_EAS: // EAS. + + // TODO: Proper fix would be to use float for baud, mark, space. + + ticks_per_bit[chan] = (int) ((TICKS_PER_CYCLE / 520.833333333333 ) + 0.5); + samples_per_symbol[chan] = (int)((audio_config_p->adev[a].samples_per_sec / 520.83333) + 0.5); + f1_change_per_sample[chan] = (int) ((2083.33333333333 * TICKS_PER_CYCLE / (double)audio_config_p->adev[a].samples_per_sec ) + 0.5); + f2_change_per_sample[chan] = (int) ((1562.5000000 * TICKS_PER_CYCLE / (double)audio_config_p->adev[a].samples_per_sec ) + 0.5); break; default: // AFSK ticks_per_bit[chan] = (int) ((TICKS_PER_CYCLE / (double)audio_config_p->achan[chan].baud ) + 0.5); + samples_per_symbol[chan] = (float)audio_config_p->adev[a].samples_per_sec / (float)audio_config_p->achan[chan].baud; f1_change_per_sample[chan] = (int) (((double)audio_config_p->achan[chan].mark_freq * TICKS_PER_CYCLE / (double)audio_config_p->adev[a].samples_per_sec ) + 0.5); f2_change_per_sample[chan] = (int) (((double)audio_config_p->achan[chan].space_freq * TICKS_PER_CYCLE / (double)audio_config_p->adev[a].samples_per_sec ) + 0.5); break; @@ -285,9 +302,64 @@ int gen_tone_init (struct audio_s *audio_config_p, int amp, int gen_packets) * *--------------------------------------------------------------------*/ +// Interpolate between two values. +// My original approximation simply jumped between phases, producing a discontinuity, +// and increasing bandwidth. +// According to multiple sources, we should transition more gently. +// Below see see a rough approximation of: +// * A step function, immediately going to new value. +// * Linear interpoation. +// * Raised cosine. Square root of cosine is also mentioned. +// +// new - / -- +// | / / +// | / | +// | / / +// old ------- / -- +// step linear raised cosine +// +// Inputs are the old (previous value), new value, and a blending control +// 0 -> take old value +// 1 -> take new value. +// inbetween some sort of weighted average. + +static inline float interpol8 (float oldv, float newv, float bc) +{ + // Step function. + //return (newv); // 78 on 11/7 + + assert (bc >= 0); + assert (bc <= 1.1); + + if (bc < 0) return (oldv); + if (bc > 1) return (newv); + + // Linear interpolation, just for comparison. + //return (bc * newv + (1.0f - bc) * oldv); // 39 on 11/7 + + float rc = 0.5f * (cosf(bc * M_PI - M_PI) + 1.0f); + float rrc = bc >= 0.5f + ? 0.5f * (sqrtf(fabsf(cosf(bc * M_PI - M_PI))) + 1.0f) + : 0.5f * (-sqrtf(fabsf(cosf(bc * M_PI - M_PI))) + 1.0f); + + (void)rrc; + return (rc * newv + (1.0f - bc) * oldv); // 49 on 11/7 + //return (rrc * newv + (1.0f - bc) * oldv); // 55 on 11/7 +} + static const int gray2phase_v26[4] = {0, 1, 3, 2}; 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. + +// For PSK, we generate the final signal by combining fixed frequency cosine and +// sine by the following weights. +static const float ci[8] = { 1, .7071, 0, -.7071, -1, -.7071, 0, .7071 }; +static const float sq[8] = { 0, .7071, 1, .7071, 0, -.7071, -1, -.7071 }; +#endif void tone_gen_put_bit (int chan, int dat) { @@ -295,7 +367,7 @@ void tone_gen_put_bit (int chan, int dat) assert (save_audio_config_p != NULL); - if (save_audio_config_p->achan[chan].medium != MEDIUM_RADIO) { + if (save_audio_config_p->chan_medium[chan] != MEDIUM_RADIO) { text_color_set(DW_COLOR_ERROR); dw_printf ("Invalid channel %d for tone generation.\n", chan); return; @@ -324,14 +396,28 @@ void tone_gen_put_bit (int chan, int dat) // All zero bits should give us steady 1800 Hz. // All one bits should flip phase by 180 degrees each time. + // For V.26B, add another 45 degrees. + // This seems to work a little better. dibit = (save_bit[chan] << 1) | dat; - symbol = gray2phase_v26[dibit]; + symbol = gray2phase_v26[dibit]; // 0 .. 3 for QPSK. +#if PSKIQ + // One phase shift unit is 45 degrees. + // Remember what it was last time and calculate new. + // values 0 .. 7. + xmit_prev_octant[chan] = xmit_octant[chan]; + xmit_octant[chan] += symbol * 2; + if (save_audio_config_p->achan[chan].v26_alternative == V26_B) { + xmit_octant[chan] += 1; + } + xmit_octant[chan] &= 0x7; +#else tone_phase[chan] += symbol * PHASE_SHIFT_90; if (save_audio_config_p->achan[chan].v26_alternative == V26_B) { tone_phase[chan] += PHASE_SHIFT_45; } +#endif bit_count[chan]++; } @@ -360,14 +446,19 @@ void tone_gen_put_bit (int chan, int dat) bit_count[chan] = 0; } - if (save_audio_config_p->achan[chan].modem_type == MODEM_SCRAMBLE) { + // Would be logical to have MODEM_BASEBAND for IL2P rather than checking here. But... + // That would mean putting in at least 3 places and testing all rather than just one. + if (save_audio_config_p->achan[chan].modem_type == MODEM_SCRAMBLE && + save_audio_config_p->achan[chan].layer2_xmit != LAYER2_IL2P) { int x; x = (dat ^ (lfsr[chan] >> 16) ^ (lfsr[chan] >> 11)) & 1; lfsr[chan] = (lfsr[chan] << 1) | (x & 1); dat = x; } - +#if PSKIQ + int blend = 1; +#endif do { /* until enough audio samples for this symbol. */ int sam; @@ -380,14 +471,70 @@ void tone_gen_put_bit (int chan, int dat) text_color_set(DW_COLOR_DEBUG); dw_printf ("tone_gen_put_bit %d AFSK\n", __LINE__); #endif - tone_phase[chan] += dat ? f2_change_per_sample[chan] : f1_change_per_sample[chan]; + + // v1.7 reversed. + // Previously a data '1' selected the second (usually higher) tone. + // It never really mattered before because we were using NRZI. + // With the addition of IL2P, we need to be more careful. + // A data '1' should be the mark tone. + + tone_phase[chan] += dat ? f1_change_per_sample[chan] : f2_change_per_sample[chan]; + sam = sine_table[(tone_phase[chan] >> 24) & 0xff]; + gen_tone_put_sample (chan, a, sam); + break; + + case MODEM_EAS: + + tone_phase[chan] += dat ? f1_change_per_sample[chan] : f2_change_per_sample[chan]; sam = sine_table[(tone_phase[chan] >> 24) & 0xff]; gen_tone_put_sample (chan, a, sam); break; case MODEM_QPSK: - case MODEM_8PSK: +#if DEBUG2 + text_color_set(DW_COLOR_DEBUG); + dw_printf ("tone_gen_put_bit %d PSK\n", __LINE__); +#endif + tone_phase[chan] += f1_change_per_sample[chan]; +#if PSKIQ +#if 1 // blend JWL + // remove loop invariant + float old_i = ci[xmit_prev_octant[chan]]; + float old_q = sq[xmit_prev_octant[chan]]; + + float new_i = ci[xmit_octant[chan]]; + float new_q = sq[xmit_octant[chan]]; + + float b = blend / samples_per_symbol[chan]; // roughly 0 to 1 + blend++; + // b = (b - 0.5) * 20 + 0.5; + // if (b < 0) b = 0; + // if (b > 1) b = 1; + // b = b > 0.5; + //b = 1; // 78 decoded with this. + // only 39 without. + + + //float blended_i = new_i * b + old_i * (1.0f - b); + //float blended_q = new_q * b + old_q * (1.0f - b); + + float blended_i = interpol8 (old_i, new_i, b); + float blended_q = interpol8 (old_q, new_q, b); + + sam = blended_i * sine_table[((tone_phase[chan] - PHASE_SHIFT_90) >> 24) & 0xff] + + blended_q * sine_table[(tone_phase[chan] >> 24) & 0xff]; +#else // jump + sam = ci[xmit_octant[chan]] * sine_table[((tone_phase[chan] - PHASE_SHIFT_90) >> 24) & 0xff] + + sq[xmit_octant[chan]] * sine_table[(tone_phase[chan] >> 24) & 0xff]; +#endif +#else + sam = sine_table[(tone_phase[chan] >> 24) & 0xff]; +#endif + gen_tone_put_sample (chan, a, sam); + break; + + case MODEM_8PSK: #if DEBUG2 text_color_set(DW_COLOR_DEBUG); dw_printf ("tone_gen_put_bit %d PSK\n", __LINE__); @@ -511,6 +658,20 @@ void gen_tone_put_sample (int chan, int a, int sam) { } } +void gen_tone_put_quiet_ms (int chan, int time_ms) { + + int a = ACHAN2ADEV(chan); /* device for channel. */ + int sam = 0; + + int nsamples = (int) ((time_ms * (float)save_audio_config_p->adev[a].samples_per_sec / 1000.) + 0.5); + + for (int j=0; j 30 OVERLAYED CAR + sym_car, // > 30 OVERLAID CAR sym_default, // ? 31 INFO Kiosk (Blue box with ?) - sym_default, // @ 32 HURICANE/Trop-Storm + sym_default, // @ 32 HURRICANE/Trop-Storm sym_default, // A 33 overlayBOX DTMF & RFID & XO sym_default, // B 34 Blwng Snow (& future codes) sym_coast_guard, // C 35 Coast Guard @@ -445,7 +445,7 @@ static const symbol_type_t grm_alternate_symtab[SYMTAB_SIZE] = { sym_default, // G 39 Snow Shwr (& future ovrlys) sym_default, // H 40 Haze (& Overlay Hazards) sym_default, // I 41 Rain Shower - sym_default, // J 42 Lightening (& future ovrlys) + sym_default, // J 42 Lightning (& future ovrlys) sym_rbcn, // K 43 Kenwood HT (W) sym_light, // L 44 Lighthouse sym_default, // M 45 MARS (A=Army,N=Navy,F=AF) @@ -488,12 +488,12 @@ static const symbol_type_t grm_alternate_symtab[SYMTAB_SIZE] = { sym_restrooms, // r 82 Restrooms sym_default, // s 83 OVERLAY SHIP/boat (top view) sym_default, // t 84 Tornado - sym_car, // u 85 OVERLAYED TRUCK - sym_car, // v 86 OVERLAYED Van + sym_car, // u 85 OVERLAID TRUCK + sym_car, // v 86 OVERLAID Van sym_default, // w 87 Flooding sym_wreck, // x 88 Wreck or Obstruction ->X<- sym_default, // y 89 Skywarn - sym_default, // z 90 OVERLAYED Shelter + sym_default, // z 90 OVERLAID Shelter sym_default, // { 91 Fog (& future ovrly codes) sym_default, // | 92 TNC Stream Switch sym_default, // } 93 diff --git a/src/hdlc_rec.c b/src/hdlc_rec.c index 6b395be6..d87a1b50 100644 --- a/src/hdlc_rec.c +++ b/src/hdlc_rec.c @@ -46,6 +46,7 @@ #include "demod_9600.h" /* for descramble() */ #include "ptt.h" #include "fx25.h" +#include "il2p.h" //#define TEST 1 /* Define for unit testing. */ @@ -151,7 +152,7 @@ void hdlc_rec_init (struct audio_s *pa) for (ch = 0; ch < MAX_CHANS; ch++) { - if (pa->achan[ch].medium == MEDIUM_RADIO) { + if (pa->chan_medium[ch] == MEDIUM_RADIO) { num_subchan[ch] = pa->achan[ch].num_subchan; @@ -496,6 +497,7 @@ void hdlc_rec_bit (int chan, int subchan, int slice, int raw, int is_scrambled, if (g_audio_p->achan[chan].modem_type != MODEM_AIS) { fx25_rec_bit (chan, subchan, slice, dbit); + il2p_rec_bit (chan, subchan, slice, raw); // Note: skip NRZI. } /* @@ -759,7 +761,7 @@ void dcd_change (int chan, int subchan, int slice, int state) * * Name: hdlc_rec_data_detect_any * - * Purpose: Determine if the radio channel is curently busy + * Purpose: Determine if the radio channel is currently busy * with packet data. * This version doesn't care about voice or other sounds. * This is used by the transmit logic to transmit only @@ -774,7 +776,7 @@ void dcd_change (int chan, int subchan, int slice, int state) * Description: We have two different versions here. * * hdlc_rec_data_detect_any sees if ANY of the decoders - * for this channel are receving a signal. This is + * for this channel are receiving a signal. This is * used to determine whether the channel is clear and * we can transmit. This would apply to the 300 baud * HF SSB case where we have multiple decoders running diff --git a/src/hdlc_rec2.c b/src/hdlc_rec2.c index e23aaee7..b817018f 100644 --- a/src/hdlc_rec2.c +++ b/src/hdlc_rec2.c @@ -273,7 +273,7 @@ void hdlc_rec2_block (rrbb_t block) } /* - * Not successful with frame in orginal form. + * Not successful with frame in original form. * See if we can "fix" it. */ if (try_to_fix_quick_now (block, chan, subchan, slice, alevel)) { diff --git a/src/hdlc_rec2.h b/src/hdlc_rec2.h index 5141f3ac..01ef3238 100644 --- a/src/hdlc_rec2.h +++ b/src/hdlc_rec2.h @@ -6,6 +6,7 @@ #include "ax25_pad.h" /* for packet_t, alevel_t */ #include "rrbb.h" #include "audio.h" /* for struct audio_s */ +#include "dlq.h" // for fec_type_t definition. @@ -62,6 +63,6 @@ int hdlc_rec2_try_to_fix_later (rrbb_t block, int chan, int subchan, int slice, /* Provided by the top level application to process a complete frame. */ -void app_process_rec_packet (int chan, int subchan, int slice, packet_t pp, alevel_t level, int is_fx25, retry_t retries, char *spectrum); +void app_process_rec_packet (int chan, int subchan, int slice, packet_t pp, alevel_t level, fec_type_t fec_type, retry_t retries, char *spectrum); #endif diff --git a/src/hdlc_send.c b/src/hdlc_send.c index 2d170f67..8a1cdc6d 100644 --- a/src/hdlc_send.c +++ b/src/hdlc_send.c @@ -2,7 +2,7 @@ // // This file is part of Dire Wolf, an amateur radio packet TNC. // -// Copyright (C) 2011, 2013, 2014, 2019 John Langner, WB2OSZ +// Copyright (C) 2011, 2013, 2014, 2019, 2021 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 @@ -27,37 +27,37 @@ #include "gen_tone.h" #include "textcolor.h" #include "fcs_calc.h" +#include "ax25_pad.h" #include "fx25.h" +#include "il2p.h" -static void send_control (int, int); -static void send_data (int, int); -static void send_bit (int, int); +static void send_byte_msb_first (int chan, int x, int polarity); +static void send_control_nrzi (int, int); +static void send_data_nrzi (int, int); +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_CHANS]; // Count number of bits sent by "hdlc_send_frame" or "hdlc_send_flags" /*------------------------------------------------------------- * - * Name: hdlc_send + * Name: layer2_send_frame * - * Purpose: Convert HDLC frames to a stream of bits. + * Purpose: Convert frames to a stream of bits. + * Originally this was for AX.25 only, hence the file name. + * Over time, FX.25 and IL2P were shoehorned in. * * Inputs: chan - Audio channel number, 0 = first. * - * fbuf - Frame buffer address. - * - * flen - Frame length, not including the FCS. + * pp - Packet object. * * bad_fcs - Append an invalid FCS for testing purposes. * Applies only to regular AX.25. * - * fx25_xmit_enable - Just like the name says. - * * Outputs: Bits are shipped out by calling tone_gen_put_bit(). * * Returns: Number of bits sent including "flags" and the @@ -65,12 +65,12 @@ static int number_of_bits_sent[MAX_CHANS]; // Count number of bits sent by "hdl * The required time can be calculated by dividing this * number by the transmit rate of bits/sec. * - * Description: Convert to stream of bits including: + * Description: For AX.25, send: * start flag * bit stuffed data * calculated FCS * end flag - * NRZI encoding + * NRZI encoding for all but the "flags." * * * Assumptions: It is assumed that the tone_gen module has been @@ -81,23 +81,40 @@ static int number_of_bits_sent[MAX_CHANS]; // Count number of bits sent by "hdl static int ax25_only_hdlc_send_frame (int chan, unsigned char *fbuf, int flen, int bad_fcs); -// New in 1.6: Option to encapsulate in FX.25. -int hdlc_send_frame (int chan, unsigned char *fbuf, int flen, int bad_fcs, int fx25_xmit_enable) +int layer2_send_frame (int chan, packet_t pp, int bad_fcs, struct audio_s *audio_config_p) { - if (fx25_xmit_enable) { - int n = fx25_send_frame (chan, fbuf, flen, fx25_xmit_enable); + if (audio_config_p->achan[chan].layer2_xmit == LAYER2_IL2P) { + + int n = il2p_send_frame (chan, pp, audio_config_p->achan[chan].il2p_max_fec, + audio_config_p->achan[chan].il2p_invert_polarity); + if (n > 0) { + return (n); + } + text_color_set(DW_COLOR_ERROR); + dw_printf ("Unable to send IL2p frame. Falling back to regular AX.25.\n"); + // Not sure if we should fall back to AX.25 or not here. + } + else if (audio_config_p->achan[chan].layer2_xmit == LAYER2_FX25) { + unsigned char fbuf[AX25_MAX_PACKET_LEN+2]; + int flen = ax25_pack (pp, fbuf); + int n = fx25_send_frame (chan, fbuf, flen, audio_config_p->achan[chan].fx25_strength); if (n > 0) { return (n); } text_color_set(DW_COLOR_ERROR); dw_printf ("Unable to send FX.25. Falling back to regular AX.25.\n"); + // Definitely need to fall back to AX.25 here because + // the FX.25 frame length is so limited. } + unsigned char fbuf[AX25_MAX_PACKET_LEN+2]; + int flen = ax25_pack (pp, fbuf); return (ax25_only_hdlc_send_frame (chan, fbuf, flen, bad_fcs)); } + static int ax25_only_hdlc_send_frame (int chan, unsigned char *fbuf, int flen, int bad_fcs) { int j, fcs; @@ -105,33 +122,31 @@ static int ax25_only_hdlc_send_frame (int chan, unsigned char *fbuf, int flen, i number_of_bits_sent[chan] = 0; - #if DEBUG text_color_set(DW_COLOR_DEBUG); dw_printf ("hdlc_send_frame ( chan = %d, fbuf = %p, flen = %d, bad_fcs = %d)\n", chan, fbuf, flen, bad_fcs); fflush (stdout); #endif - - send_control (chan, 0x7e); /* Start frame */ + send_control_nrzi (chan, 0x7e); /* Start frame */ for (j=0; j> 8) & 0xff); + send_data_nrzi (chan, (~fcs) & 0xff); + send_data_nrzi (chan, ((~fcs) >> 8) & 0xff); } else { - send_data (chan, fcs & 0xff); - send_data (chan, (fcs >> 8) & 0xff); + send_data_nrzi (chan, fcs & 0xff); + send_data_nrzi (chan, (fcs >> 8) & 0xff); } - send_control (chan, 0x7e); /* End frame */ + send_control_nrzi (chan, 0x7e); /* End frame */ return (number_of_bits_sent[chan]); } @@ -139,22 +154,25 @@ static int ax25_only_hdlc_send_frame (int chan, unsigned char *fbuf, int flen, i /*------------------------------------------------------------- * - * Name: hdlc_send_flags + * Name: layer2_preamble_postamble * - * Purpose: Send HDLC flags before and after the frame. + * Purpose: Send filler pattern before and after the frame. + * For HDLC it is 01111110, for IL2P 01010101. * * Inputs: chan - Audio channel number, 0 = first. * - * nflags - Number of flag patterns to send. + * nbytes - Number of bytes to send. * * finish - True for end of transmission. * This causes the last audio buffer to be flushed. * + * audio_config_p - Configuration for audio and modems. + * * Outputs: Bits are shipped out by calling tone_gen_put_bit(). * * Returns: Number of bits sent. * There is no bit-stuffing so we would expect this to - * be 8 * nflags. + * be 8 * nbytes. * The required time can be calculated by dividing this * number by the transmit rate of bits/sec. * @@ -164,25 +182,30 @@ static int ax25_only_hdlc_send_frame (int chan, unsigned char *fbuf, int flen, i * *--------------------------------------------------------------*/ -int hdlc_send_flags (int chan, int nflags, int finish) +int layer2_preamble_postamble (int chan, int nbytes, int finish, struct audio_s *audio_config_p) { int j; - number_of_bits_sent[chan] = 0; - #if DEBUG text_color_set(DW_COLOR_DEBUG); dw_printf ("hdlc_send_flags ( chan = %d, nflags = %d, finish = %d )\n", chan, nflags, finish); fflush (stdout); #endif - /* The AX.25 spec states that when the transmitter is on but not sending data */ - /* it should send a continuous stream of "flags." */ + // When the transmitter is on but not sending data, it should be sending + // a stream of a filler pattern. + // For AX.25, it is the 01111110 "flag" pattern with NRZI and no bit stuffing. + // For IL2P, it is 01010101 without NRZI. - for (j=0; jachan[chan].layer2_xmit == LAYER2_IL2P) { + send_byte_msb_first (chan, IL2P_PREAMBLE, audio_config_p->achan[chan].il2p_invert_polarity); + } + else { + send_control_nrzi (chan, 0x7e); + } } /* Push out the final partial buffer! */ @@ -196,33 +219,54 @@ int hdlc_send_flags (int chan, int nflags, int finish) +// The next one is only for IL2P. No NRZI. +// MSB first, opposite of AX.25. + +static void send_byte_msb_first (int chan, int x, int polarity) +{ + int i; + + for (i=0; i<8; i++) { + int dbit = (x & 0x80) != 0; + tone_gen_put_bit (chan, (dbit ^ polarity) & 1); + x <<= 1; + number_of_bits_sent[chan]++; + } +} + + +// The following are only for HDLC. +// All bits are sent NRZI. +// Data (non flags) use bit stuffing. + + static int stuff[MAX_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. -static void send_control (int chan, int x) +static void send_control_nrzi (int chan, int x) { int i; for (i=0; i<8; i++) { - send_bit (chan, x & 1); + send_bit_nrzi (chan, x & 1); x >>= 1; } stuff[chan] = 0; } -static void send_data (int chan, int x) +static void send_data_nrzi (int chan, int x) { int i; for (i=0; i<8; i++) { - send_bit (chan, x & 1); + send_bit_nrzi (chan, x & 1); if (x & 1) { stuff[chan]++; if (stuff[chan] == 5) { - send_bit (chan, 0); + send_bit_nrzi (chan, 0); stuff[chan] = 0; } } else { @@ -238,7 +282,7 @@ static void send_data (int chan, int x) * data 0 bit -> invert signal. */ -static void send_bit (int chan, int b) +static void send_bit_nrzi (int chan, int b) { static int output[MAX_CHANS]; @@ -251,4 +295,82 @@ static void send_bit (int chan, int b) number_of_bits_sent[chan]++; } -/* end hdlc_send.c */ \ No newline at end of file + +// The rest of this is for EAS SAME. +// This is sort of a logical place because it serializes a frame, but not in HDLC. +// We have a parallel where SAME deserialization is in hdlc_rec. +// Maybe both should be pulled out and moved to a same.c. + + +/*------------------------------------------------------------------- + * + * Name: eas_send + * + * Purpose: Serialize EAS SAME for transmission. + * + * Inputs: chan - Radio channel number. + * str - Character string to send. + * repeat - Number of times to repeat with 1 sec quiet between. + * txdelay - Delay (ms) from PTT to first preamble bit. + * txtail - Delay (ms) from last data bit to PTT off. + * + * + * Returns: Total number of milliseconds to activate PTT. + * This includes delays before the first character + * and after the last to avoid chopping off part of it. + * + * Description: xmit_thread calls this instead of the usual hdlc_send + * when we have a special packet that means send EAS SAME + * code. + * + *--------------------------------------------------------------------*/ + +static inline void eas_put_byte (int chan, unsigned char b) +{ + for (int n=0; n<8; n++) { + tone_gen_put_bit (chan, (b & 1)); + b >>= 1; + } +} + +int eas_send (int chan, unsigned char *str, int repeat, int txdelay, int txtail) +{ + int bytes_sent = 0; + const int gap = 1000; + int gaps_sent = 0; + + gen_tone_put_quiet_ms (chan, txdelay); + + for (int r=0; rIS filtering. + * Beaconing (sendto=ig, chan=-1) and a client app sending + * to ICHANNEL should bypass the filtering. * * recv_pp - Pointer to packet object. * *** CALLER IS RESPONSIBLE FOR DELETING IT! ** @@ -898,7 +905,12 @@ void igate_send_rec_packet (int chan, packet_t recv_pp) * In that case, the payload will have TCPIP in the path and it will be dropped. */ - if (save_digi_config_p->filter_str[chan][MAX_CHANS] != NULL) { +// Apply RF>IS filtering only if it same from a radio channel. +// 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 (pfilter(chan, MAX_CHANS, save_digi_config_p->filter_str[chan][MAX_CHANS], recv_pp, 1) != 1) { @@ -1217,7 +1229,7 @@ static void send_packet_to_server (packet_t pp, int chan) * Name: send_msg_to_server * * Purpose: Send something to the IGate server. - * This one function should be used for login, hearbeats, + * This one function should be used for login, heartbeats, * and packets. * * Inputs: imsg - Message. We will add CR/LF here. @@ -1469,7 +1481,7 @@ static void * igate_recv_thread (void *arg) * * Future: might have ability to configure multiple transmit * channels, each with own client side filtering and via path. - * Loop here over all configured channels. + * If so, loop here over all configured channels. */ text_color_set(DW_COLOR_REC); dw_printf ("\n[ig>tx] "); // formerly just [ig] @@ -1504,6 +1516,64 @@ static void * igate_recv_thread (void *arg) if (to_chan >= 0) { maybe_xmit_packet_from_igate ((char*)message, to_chan); } + + +/* + * New in 1.7: If ICHANNEL was specified, send packet to client app as specified channel. + */ + if (save_audio_config_p->igate_vchannel >= 0) { + + int ichan = save_audio_config_p->igate_vchannel; + + // My original poorly thoughtout idea was to parse it into a packet object, + // using the non-strict option, and send to the client app. + // + // A lot of things can go wrong with that approach. + + // (1) Up to 8 digipeaters are allowed in radio format. + // There is a potential of finding a larger number here. + // + // (2) The via path can have names that are not valid in the radio format. + // e.g. qAC, T2HAKATA, N5JXS-F1. + // Non-strict parsing would force uppercase, truncate names too long, + // and drop unacceptable SSIDs. + // + // (3) The source address could be invalid for the RF address format. + // e.g. WHO-IS>APJIW4,TCPIP*,qAC,AE5PL-JF::ZL1JSH-9 :Charles Beadfield/New Zealand{583 + // That is essential information that we absolutely need to preserve. + // + // I think the only correct solution is to apply a third party header + // wrapper so the original contents are preserved. This will be a little + // more work for the application developer. Search for ":}" and use only + // the part after that. At this point, I don't see any value in encoding + // information in the source/destination so I will just use "X>X:}" as a prefix + + char stemp[AX25_MAX_INFO_LEN]; + strlcpy (stemp, "X>X:}", sizeof(stemp)); + strlcat (stemp, (char*)message, sizeof(stemp)); + + packet_t pp3 = ax25_from_text(stemp, 0); // 0 means not strict + if (pp3 != NULL) { + + alevel_t alevel; + memset (&alevel, 0, sizeof(alevel)); + alevel.mark = -2; // FIXME: Do we want some other special case? + alevel.space = -2; + + int subchan = -2; // FIXME: -1 is special case for APRStt. + // See what happens with -2 and follow up on this. + // Do we need something else here? + int slice = 0; + fec_type_t fec_type = fec_type_none; + char spectrum[] = "APRS-IS"; + dlq_rec_frame (ichan, subchan, slice, pp3, alevel, fec_type, RETRY_NONE, spectrum); + } + else { + text_color_set(DW_COLOR_ERROR); + dw_printf ("ICHANNEL %d: Could not parse message from APRS-IS server.\n", ichan); + dw_printf ("%s\n", message); + } + } // end ICHANNEL option } } /* while (1) */ @@ -1538,9 +1608,14 @@ static void * igate_recv_thread (void *arg) * Duplicate removal will drop the original if there is no * corresponding digipeated version. * + * + * This was an idea that came up in one of the discussion forums. + * I rushed in without thinking about it very much. + * * In retrospect, I don't think this was such a good idea. * It would be of value only if there is no other IGate nearby * that would report on the original transmission. + * I wonder if anyone would notice if this silently disappeared. * *--------------------------------------------------------------------*/ @@ -1652,7 +1727,14 @@ static void * satgate_delay_thread (void *arg) * K1RI-2>APWW10,WIDE1-1,WIDE2-1,qAS,K1RI:/221700h/9AmAT3PQ3S,WIDE1-1,WIDE2-1,qAR,W1TG-1:`c)@qh\>/"50}TinyTrak4 Mobile * - * Notice how the final address in the header might not + * This is interesting because the source is not a valid AX.25 address. + * Non-RF stations can have 2 alphanumeric characters for SSID. + * In this example, the WHO-IS server is responding to a message. + * + * WHO-IS>APJIW4,TCPIP*,qAC,AE5PL-JF::ZL1JSH-9 :Charles Beadfield/New Zealand{583 + * + * + * Notice how the final digipeater address, in the header, might not * be a valid AX.25 address. We see a 9 character address * (with no ssid) and an ssid of two letters. * We don't care because we end up discarding them before @@ -1667,29 +1749,61 @@ static void * satgate_delay_thread (void *arg) * *--------------------------------------------------------------------*/ -static void maybe_xmit_packet_from_igate (char *message, int to_chan) + +// It is unforunate that the : data type indicator (DTI) was overloaded with +// so many different meanings. Simply looking at the DTI is not adequate for +// determining whether a packet is a message. +// We need to exclude the other special cases of telemetry metadata, +// bulletins, and weather bulletins. + +static int is_message_message (char *infop) { - packet_t pp3; - char payload[AX25_MAX_PACKET_LEN]; /* what is max len? */ - char src[AX25_MAX_ADDR_LEN]; /* Source address. */ + if (*infop != ':') return (0); + if (strlen(infop) < 11) return (0); // too short for : addressee : + if (strlen(infop) >= 16) { + if (strncmp(infop+10, ":PARM.", 6) == 0) return (0); + if (strncmp(infop+10, ":UNIT.", 6) == 0) return (0); + if (strncmp(infop+10, ":EQNS.", 6) == 0) return (0); + if (strncmp(infop+10, ":BITS.", 6) == 0) return (0); + } + if (strlen(infop) >= 4) { + if (strncmp(infop+1, "BLN", 3) == 0) return (0); + if (strncmp(infop+1, "NWS", 3) == 0) return (0); + if (strncmp(infop+1, "SKY", 3) == 0) return (0); + if (strncmp(infop+1, "CWA", 3) == 0) return (0); + if (strncmp(infop+1, "BOM", 3) == 0) return (0); + } + return (1); // message, including ack, rej +} - char *pinfo = NULL; - int info_len; + +static void maybe_xmit_packet_from_igate (char *message, int to_chan) +{ int n; assert (to_chan >= 0 && to_chan < MAX_CHANS); - /* - * Try to parse it into a packet object. - * This will contain "q constructs" and we might see an address - * with two alphnumeric characters in the SSID so we must use - * the non-strict parsing. + * Try to parse it into a packet object; we need this for the packet filtering. * - * Bug: Up to 8 digipeaters are allowed in radio format. - * There is a potential of finding a larger number here. + * We use the non-strict option because there the via path can have: + * - station names longer than 6. + * - alphanumeric SSID. + * - lower case for "q constructs. + * We don't care about any of those because the via path will be discarded anyhow. + * + * The other issue, that I did not think of originally, is that the "source" + * address might not conform to AX.25 restrictions when it originally came + * from a non-RF source. For example an APRS "message" might be sent to the + * "WHO-IS" server, and the reply message would have that for the source address. + * + * Originally, I used the source address from the packet object but that was + * missing the alphanumeric SSID. This needs to be done differently. + * + * Potential Bug: Up to 8 digipeaters are allowed in radio format. + * Is there a possibility of finding a larger number here? */ - pp3 = ax25_from_text(message, 0); + packet_t pp3 = ax25_from_text(message, 0); if (pp3 == NULL) { text_color_set(DW_COLOR_ERROR); dw_printf ("Tx IGate: Could not parse message from server.\n"); @@ -1697,7 +1811,18 @@ static void maybe_xmit_packet_from_igate (char *message, int to_chan) return; } - ax25_get_addr_with_ssid (pp3, AX25_SOURCE, src); +// Issue 408: The source address might not be valid AX.25 because it +// came from a non-RF station. e.g. some server responding to a message. +// We need to take source address from original rather than extracting it +// from the packet object. + + char src[AX25_MAX_ADDR_LEN]; /* Source address. */ + memset (src, 0, sizeof(src)); + memcpy (src, message, sizeof(src)-1); + char *gt = strchr(src, '>'); + if (gt != NULL) { + *gt = '\0'; + } /* * Drop if path contains: @@ -1709,8 +1834,8 @@ static void maybe_xmit_packet_from_igate (char *message, int to_chan) ax25_get_addr_with_ssid (pp3, n + AX25_REPEATER_1, via); - if (strcmp(via, "qAX") == 0 || - strcmp(via, "TCPXX") == 0 || + if (strcmp(via, "qAX") == 0 || // qAX deprecated. http://www.aprs-is.net/q.aspx + strcmp(via, "TCPXX") == 0 || // TCPXX deprecated. strcmp(via, "RFONLY") == 0 || strcmp(via, "NOGATE") == 0) { @@ -1739,14 +1864,26 @@ static void maybe_xmit_packet_from_igate (char *message, int to_chan) * If we recently transmitted a 'message' from some station, * send the position of the message sender when it comes along later. * + * Some refer to this as a "courtesy posit report" but I don't + * think that is an official term. + * * If we have a position report, look up the sender and see if we should * bypass the normal filtering. + * + * Reference: https://www.aprs-is.net/IGating.aspx + * + * "Passing all message packets also includes passing the sending station's position + * along with the message. When APRS-IS was small, we did this using historical position + * packets. This has become problematic as it introduces historical data on to RF. + * The IGate should note the station(s) it has gated messages to RF for and pass + * the next position packet seen for that station(s) to RF." */ // TODO: Not quite this simple. Should have a function to check for position. // $ raw gps could be a position. @ could be weather data depending on symbol. - info_len = ax25_get_info (pp3, (unsigned char **)(&pinfo)); + char *pinfo = NULL; + int info_len = ax25_get_info (pp3, (unsigned char **)(&pinfo)); int msp_special_case = 0; @@ -1776,12 +1913,6 @@ static void maybe_xmit_packet_from_igate (char *message, int to_chan) // 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. - // TODO: clean up - remove these lines. - //if (s_debug >= 1) { - // text_color_set(DW_COLOR_INFO); - // dw_printf ("Packet from IGate to channel %d was rejected by filter: %s\n", to_chan, save_digi_config_p->filter_str[MAX_CHANS][to_chan]); - //} - ax25_delete (pp3); return; } @@ -1790,13 +1921,15 @@ static void maybe_xmit_packet_from_igate (char *message, int to_chan) /* - * Remove the VIA path. + * We want to discard the via path, as received from the APRS-IS, then + * replace it with TCPIP and our own call, marked as used. + * * * For example, we might get something like this from the server. - * K1USN-1>APWW10,TCPIP*,qAC,N5JXS-F1:T#479,100,048,002,500,000,10000000<0x0d><0x0a> + * K1USN-1>APWW10,TCPIP*,qAC,N5JXS-F1:T#479,100,048,002,500,000,10000000 * - * We want to reduce it to this before wrapping it as third party traffic. - * K1USN-1>APWW10:T#479,100,048,002,500,000,10000000<0x0d><0x0a> + * We want to transform it to this before wrapping it as third party traffic. + * K1USN-1>APWW10,TCPIP,mycall*:T#479,100,048,002,500,000,10000000 */ /* @@ -1824,36 +1957,23 @@ static void maybe_xmit_packet_from_igate (char *message, int to_chan) * * What is the ",I" construct? * Do we care here? - * Is is something new and improved that we should be using in the other direction? + * Is it something new and improved that we should be using in the other direction? */ - while (ax25_get_num_repeaters(pp3) > 0) { - ax25_remove_addr (pp3, AX25_REPEATER_1); - } + char payload[AX25_MAX_PACKET_LEN]; + 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); -/* - * Replace the VIA path with TCPIP and my call. - * Mark my call as having been used. - */ - ax25_set_addr (pp3, AX25_REPEATER_1, "TCPIP"); - ax25_set_h (pp3, AX25_REPEATER_1); - ax25_set_addr (pp3, AX25_REPEATER_2, save_audio_config_p->achan[to_chan].mycall); - ax25_set_h (pp3, AX25_REPEATER_2); -/* - * Convert to text representation. - */ - memset (payload, 0, sizeof(payload)); - - ax25_format_addrs (pp3, payload); - info_len = ax25_get_info (pp3, (unsigned char **)(&pinfo)); - (void)(info_len); - strlcat (payload, pinfo, sizeof(payload)); #if DEBUGx text_color_set(DW_COLOR_DEBUG); - dw_printf ("Tx IGate: payload=%s\n", payload); + dw_printf ("Tx IGate: DEBUG payload=%s\n", payload); #endif + + /* * Encapsulate for sending over radio if no reason to drop it. @@ -1870,19 +1990,13 @@ static void maybe_xmit_packet_from_igate (char *message, int to_chan) */ if (ig_to_tx_allow (pp3, to_chan)) { char radio [2400]; - packet_t pradio; - snprintf (radio, sizeof(radio), "%s>%s%d%d%s:}%s", save_audio_config_p->achan[to_chan].mycall, APP_TOCALL, MAJOR_VERSION, MINOR_VERSION, save_igate_config_p->tx_via, payload); - pradio = ax25_from_text (radio, 1); - - /* Oops. Didn't have a check for NULL here. */ - /* Could this be the cause of rare and elusive crashes in 1.2? */ - + packet_t pradio = ax25_from_text (radio, 1); if (pradio != NULL) { #if ITEST @@ -1895,10 +2009,7 @@ static void maybe_xmit_packet_from_igate (char *message, int to_chan) #endif stats_rf_xmit_packets++; // Any type of packet. - // TEMP TEST: metadata temporarily allowed during testing. - - if (*pinfo == ':' && ! is_telem_metadata(pinfo)) { - // temp test // if (*pinfo == ':') { + if (is_message_message(pinfo)) { // We transmitted a "message." Telemetry metadata is excluded. // Remember to pass along address of the sender later. @@ -2211,7 +2322,7 @@ static int rx_to_ig_allow (packet_t pp) * * Future: * Should the digipeater function avoid transmitting something if it - * was recently transmitted by the IGate funtion? + * was recently transmitted by the IGate function? * This code is pretty much the same as dedupe.c. Maybe it could all * be combined into one. Need to ponder this some more. * @@ -2356,6 +2467,8 @@ void ig_to_tx_remember (packet_t pp, int chan, int bydigi) } } + + static int ig_to_tx_allow (packet_t pp, int chan) { unsigned short crc = ax25_dedupe_crc(pp); @@ -2388,7 +2501,7 @@ static int ig_to_tx_allow (packet_t pp, int chan) /* We have a duplicate within some time period. */ - if (*pinfo == ':' && ! is_telem_metadata((char*)pinfo)) { + if (is_message_message((char*)pinfo)) { /* I think I want to avoid the duplicate suppression for "messages." */ /* Suppose we transmit a message from station X and it doesn't get an ack back. */ @@ -2437,7 +2550,7 @@ static int ig_to_tx_allow (packet_t pp, int chan) /* the normal limit for them. */ increase_limit = 1; - if (*pinfo == ':' && ! is_telem_metadata((char*)pinfo)) { + if (is_message_message((char*)pinfo)) { increase_limit = 3; } diff --git a/src/il2p.h b/src/il2p.h new file mode 100644 index 00000000..d18e2d05 --- /dev/null +++ b/src/il2p.h @@ -0,0 +1,145 @@ + + +#ifndef IL2P_H +#define IL2P_H 1 + + +#define IL2P_PREAMBLE 0x55 + +#define IL2P_SYNC_WORD 0xF15E48 + +#define IL2P_SYNC_WORD_SIZE 3 +#define IL2P_HEADER_SIZE 13 // Does not include 2 parity. +#define IL2P_HEADER_PARITY 2 + +#define IL2P_MAX_PAYLOAD_SIZE 1023 +#define IL2P_MAX_PAYLOAD_BLOCKS 5 +#define IL2P_MAX_PARITY_SYMBOLS 16 // For payload only. +#define IL2P_MAX_ENCODED_PAYLOAD_SIZE (IL2P_MAX_PAYLOAD_SIZE + IL2P_MAX_PAYLOAD_BLOCKS * IL2P_MAX_PARITY_SYMBOLS) + +#define IL2P_MAX_PACKET_SIZE (IL2P_SYNC_WORD_SIZE + IL2P_HEADER_SIZE + IL2P_HEADER_PARITY + IL2P_MAX_ENCODED_PAYLOAD_SIZE) + + +/////////////////////////////////////////////////////////////////////////////// +// +// il2p_init.c +// +/////////////////////////////////////////////////////////////////////////////// + + +// Init must be called at start of application. + +extern void il2p_init (int debug); + +#include "fx25.h" // For Reed Solomon stuff. e.g. struct rs + // Maybe rearrange someday because RS now used another place. + +extern struct rs *il2p_find_rs(int nparity); // Internal later? + +extern void il2p_encode_rs (unsigned char *tx_data, int data_size, int num_parity, unsigned char *parity_out); + +extern int il2p_decode_rs (unsigned char *rec_block, int data_size, int num_parity, unsigned char *out); + +extern int il2p_get_debug(void); +extern void il2p_set_debug(int debug); + + +/////////////////////////////////////////////////////////////////////////////// +// +// il2p_rec.c +// +/////////////////////////////////////////////////////////////////////////////// + +// Receives a bit stream from demodulator. + +extern void il2p_rec_bit (int chan, int subchan, int slice, int dbit); + + + + +/////////////////////////////////////////////////////////////////////////////// +// +// il2p_send.c +// +/////////////////////////////////////////////////////////////////////////////// + +#include "ax25_pad.h" // For packet object. + +// Send bit stream to modulator. + +int il2p_send_frame (int chan, packet_t pp, int max_fec, int polarity); + + + +/////////////////////////////////////////////////////////////////////////////// +// +// il2p_codec.c +// +/////////////////////////////////////////////////////////////////////////////// + +#include "ax25_pad.h" + +extern int il2p_encode_frame (packet_t pp, int max_fec, unsigned char *iout); + +packet_t il2p_decode_frame (unsigned char *irec); + +packet_t il2p_decode_header_payload (unsigned char* uhdr, unsigned char *epayload, int *symbols_corrected); + + + + +/////////////////////////////////////////////////////////////////////////////// +// +// il2p_header.c +// +/////////////////////////////////////////////////////////////////////////////// + + +extern int il2p_type_1_header (packet_t pp, int max_fec, unsigned char *hdr); + +extern packet_t il2p_decode_header_type_1 (unsigned char *hdr, int num_sym_changed); + + +extern int il2p_type_0_header (packet_t pp, int max_fec, unsigned char *hdr); + +extern int il2p_clarify_header(unsigned char *rec_hdr, unsigned char *corrected_descrambled_hdr); + + + +/////////////////////////////////////////////////////////////////////////////// +// +// il2p_scramble.c +// +/////////////////////////////////////////////////////////////////////////////// + +extern void il2p_scramble_block (unsigned char *in, unsigned char *out, int len); + +extern void il2p_descramble_block (unsigned char *in, unsigned char *out, int len); + + +/////////////////////////////////////////////////////////////////////////////// +// +// il2p_payload.c +// +/////////////////////////////////////////////////////////////////////////////// + + +typedef struct { + int payload_byte_count; // Total size, 0 thru 1023 + int payload_block_count; + int small_block_size; + int large_block_size; + int large_block_count; + int small_block_count; + int parity_symbols_per_block; // 2, 4, 6, 8, 16 +} il2p_payload_properties_t; + +extern int il2p_payload_compute (il2p_payload_properties_t *p, int payload_size, int max_fec); + +extern int il2p_encode_payload (unsigned char *payload, int payload_size, int max_fec, unsigned char *enc); + +extern int il2p_decode_payload (unsigned char *received, int payload_size, int max_fec, unsigned char *payload_out, int *symbols_corrected); + +extern int il2p_get_header_attributes (unsigned char *hdr, int *hdr_type, int *max_fec); + +#endif diff --git a/src/il2p_codec.c b/src/il2p_codec.c new file mode 100644 index 00000000..a20aed65 --- /dev/null +++ b/src/il2p_codec.c @@ -0,0 +1,263 @@ +// +// This file is part of Dire Wolf, an amateur radio packet TNC. +// +// Copyright (C) 2021 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 . +// + +#include "direwolf.h" + +#include +#include +#include +#include + +#include "il2p.h" +#include "textcolor.h" +#include "demod.h" + + +/*------------------------------------------------------------- + * + * File: il2p_codec.c + * + * Purpose: Convert IL2P encoded format from and to direwolf internal packet format. + * + *--------------------------------------------------------------*/ + + +/*------------------------------------------------------------- + * + * Name: il2p_encode_frame + * + * Purpose: Convert AX.25 frame to IL2P encoding. + * + * Inputs: chan - Audio channel number, 0 = first. + * + * pp - Packet object pointer. + * + * max_fec - 1 to send maximum FEC size rather than automatic. + * + * Outputs: iout - Encoded result, excluding the 3 byte sync word. + * Caller should provide IL2P_MAX_PACKET_SIZE bytes. + * + * Returns: Number of bytes for transmission. + * -1 is returned for failure. + * + * Description: Encode into IL2P format. + * + * Errors: If something goes wrong, return -1. + * + * Most likely reason is that the frame is too large. + * IL2P has a max payload size of 1023 bytes. + * For a type 1 header, this is the maximum AX.25 Information part size. + * For a type 0 header, this is the entire AX.25 frame. + * + *--------------------------------------------------------------*/ + +int il2p_encode_frame (packet_t pp, int max_fec, unsigned char *iout) +{ + +// Can a type 1 header be used? + + unsigned char hdr[IL2P_HEADER_SIZE + IL2P_HEADER_PARITY]; + int e; + int out_len = 0; + + e = il2p_type_1_header (pp, max_fec, hdr); + if (e >= 0) { + il2p_scramble_block (hdr, iout, IL2P_HEADER_SIZE); + il2p_encode_rs (iout, IL2P_HEADER_SIZE, IL2P_HEADER_PARITY, iout+IL2P_HEADER_SIZE); + out_len = IL2P_HEADER_SIZE + IL2P_HEADER_PARITY; + + if (e == 0) { + // Success. No info part. + return (out_len); + } + + // Payload is AX.25 info part. + unsigned char *pinfo; + int info_len; + info_len = ax25_get_info (pp, &pinfo); + + int k = il2p_encode_payload (pinfo, info_len, max_fec, iout+out_len); + if (k > 0) { + out_len += k; + // Success. Info part was <= 1023 bytes. + return (out_len); + } + + // Something went wrong with the payload encoding. + return (-1); + } + else if (e == -1) { + +// Could not use type 1 header for some reason. +// e.g. More than 2 addresses, extended (mod 128) sequence numbers, etc. + + e = il2p_type_0_header (pp, max_fec, hdr); + if (e > 0) { + + il2p_scramble_block (hdr, iout, IL2P_HEADER_SIZE); + il2p_encode_rs (iout, IL2P_HEADER_SIZE, IL2P_HEADER_PARITY, iout+IL2P_HEADER_SIZE); + out_len = IL2P_HEADER_SIZE + IL2P_HEADER_PARITY; + + // Payload is entire AX.25 frame. + + unsigned char *frame_data_ptr = ax25_get_frame_data_ptr (pp); + int frame_len = ax25_get_frame_len (pp); + int k = il2p_encode_payload (frame_data_ptr, frame_len, max_fec, iout+out_len); + if (k > 0) { + out_len += k; + // Success. Entire AX.25 frame <= 1023 bytes. + return (out_len); + } + // Something went wrong with the payload encoding. + return (-1); + } + else if (e == 0) { + // Impossible condition. Type 0 header must have payload. + return (-1); + } + else { + // AX.25 frame is too large. + return (-1); + } + } + + // AX.25 Information part is too large. + return (-1); +} + + + +/*------------------------------------------------------------- + * + * Name: il2p_decode_frame + * + * Purpose: Convert IL2P encoding to AX.25 frame. + * This is only used during testing, with a whole encoded frame. + * During reception, the header would have FEC and descrambling + * applied first so we would know how much to collect for the payload. + * + * Inputs: irec - Received IL2P frame excluding the 3 byte sync word. + * + * Future Out: Number of symbols corrected. + * + * Returns: Packet pointer or NULL for error. + * + *--------------------------------------------------------------*/ + +packet_t il2p_decode_frame (unsigned char *irec) +{ + unsigned char uhdr[IL2P_HEADER_SIZE]; // After FEC and descrambling. + int e = il2p_clarify_header (irec, uhdr); + + // TODO?: for symmetry we might want to clarify the payload before combining. + + return (il2p_decode_header_payload(uhdr, irec + IL2P_HEADER_SIZE + IL2P_HEADER_PARITY, &e)); +} + + +/*------------------------------------------------------------- + * + * Name: il2p_decode_header_payload + * + * Purpose: Convert IL2P encoding to AX.25 frame + * + * Inputs: uhdr - Received header after FEC and descrambling. + * epayload - Encoded payload. + * + * In/Out: symbols_corrected - Symbols (bytes) corrected in the header. + * Should be 0 or 1 because it has 2 parity symbols. + * Here we add number of corrections for the payload. + * + * Returns: Packet pointer or NULL for error. + * + *--------------------------------------------------------------*/ + +packet_t il2p_decode_header_payload (unsigned char* uhdr, unsigned char *epayload, int *symbols_corrected) +{ + int hdr_type; + int max_fec; + int payload_len = il2p_get_header_attributes (uhdr, &hdr_type, &max_fec); + + packet_t pp = NULL; + + if (hdr_type == 1) { + +// Header type 1. Any payload is the AX.25 Information part. + + pp = il2p_decode_header_type_1 (uhdr, *symbols_corrected); + if (pp == NULL) { + // Failed for some reason. + return (NULL); + } + + if (payload_len > 0) { + // This is the AX.25 Information part. + + unsigned char extracted[IL2P_MAX_PAYLOAD_SIZE]; + int e = il2p_decode_payload (epayload, payload_len, max_fec, extracted, symbols_corrected); + + // It would be possible to have a good header but too many errors in the payload. + + if (e <= 0) { + ax25_delete (pp); + pp = NULL; + return (pp); + } + + if (e != payload_len) { + text_color_set(DW_COLOR_ERROR); + dw_printf ("IL2P Internal Error: %s(): hdr_type=%d, max_fec=%d, payload_len=%d, e=%d.\n", __func__, hdr_type, max_fec, payload_len, e); + } + + ax25_set_info (pp, extracted, payload_len); + } + return (pp); + } + else { + +// Header type 0. The payload is the entire AX.25 frame. + + unsigned char extracted[IL2P_MAX_PAYLOAD_SIZE]; + int e = il2p_decode_payload (epayload, payload_len, max_fec, extracted, symbols_corrected); + + if (e <= 0) { // Payload was not received correctly. + return (NULL); + } + + if (e != payload_len) { + text_color_set(DW_COLOR_ERROR); + dw_printf ("IL2P Internal Error: %s(): hdr_type=%d, e=%d, payload_len=%d\n", __func__, hdr_type, e, payload_len); + return (NULL); + } + + alevel_t alevel; + memset (&alevel, 0, sizeof(alevel)); + //alevel = demod_get_audio_level (chan, subchan); // What TODO? We don't know channel here. + // I think alevel gets filled in somewhere later making + // this redundant. + + pp = ax25_from_frame (extracted, payload_len, alevel); + return (pp); + } + +} // end il2p_decode_header_payload + +// end il2p_codec.c + + diff --git a/src/il2p_header.c b/src/il2p_header.c new file mode 100644 index 00000000..94fd25ba --- /dev/null +++ b/src/il2p_header.c @@ -0,0 +1,679 @@ +// +// This file is part of Dire Wolf, an amateur radio packet TNC. +// +// Copyright (C) 2021 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 . +// + +#include "direwolf.h" + +#include +#include +#include +#include + +#include "textcolor.h" +#include "ax25_pad.h" +#include "ax25_pad2.h" +#include "il2p.h" + + + +/*-------------------------------------------------------------------------------- + * + * File: il2p_header.c + * + * Purpose: Functions to deal with the IL2P header. + * + * Reference: http://tarpn.net/t/il2p/il2p-specification0-4.pdf + * + *--------------------------------------------------------------------------------*/ + + + +// Convert ASCII to/from DEC SIXBIT as defined here: +// https://en.wikipedia.org/wiki/Six-bit_character_code#DEC_six-bit_code + +static inline int ascii_to_sixbit (int a) +{ + if (a >= ' ' && a <= '_') return (a - ' '); + return (31); // '?' for any invalid. +} + +static inline int sixbit_to_ascii (int s) +{ + return (s + ' '); +} + +// Functions for setting the various header fields. +// It is assumed that it was zeroed first so only the '1' bits are set. + +static void set_field (unsigned char *hdr, int bit_num, int lsb_index, int width, int value) +{ + while (width > 0 && value != 0) { + assert (lsb_index >= 0 && lsb_index <= 11); + if (value & 1) { + hdr[lsb_index] |= 1 << bit_num; + } + value >>= 1; + lsb_index--; + width--; + } + assert (value == 0); +} + +#define SET_UI(hdr,val) set_field(hdr, 6, 0, 1, val) + +#define SET_PID(hdr,val) set_field(hdr, 6, 4, 4, val) + +#define SET_CONTROL(hdr,val) set_field(hdr, 6, 11, 7, val) + + +#define SET_FEC_LEVEL(hdr,val) set_field(hdr, 7, 0, 1, val) + +#define SET_HDR_TYPE(hdr,val) set_field(hdr, 7, 1, 1, val) + +#define SET_PAYLOAD_BYTE_COUNT(hdr,val) set_field(hdr, 7, 11, 10, val) + + +// Extracting the fields. + +static int get_field (unsigned char *hdr, int bit_num, int lsb_index, int width) +{ + int result = 0; + lsb_index -= width - 1; + while (width > 0) { + result <<= 1; + assert (lsb_index >= 0 && lsb_index <= 11); + if (hdr[lsb_index] & (1 << bit_num)) { + result |= 1; + } + lsb_index++; + width--; + } + return (result); +} + +#define GET_UI(hdr) get_field(hdr, 6, 0, 1) + +#define GET_PID(hdr) get_field(hdr, 6, 4, 4) + +#define GET_CONTROL(hdr) get_field(hdr, 6, 11, 7) + + +#define GET_FEC_LEVEL(hdr) get_field(hdr, 7, 0, 1) + +#define GET_HDR_TYPE(hdr) get_field(hdr, 7, 1, 1) + +#define GET_PAYLOAD_BYTE_COUNT(hdr) get_field(hdr, 7, 11, 10) + + + +// AX.25 'I' and 'UI' frames have a protocol ID which determines how the +// information part should be interpreted. +// Here we squeeze the most common cases down to 4 bits. +// Return -1 if translation is not possible. Fall back to type 0 header in this case. + +static int encode_pid (packet_t pp) +{ + int pid = ax25_get_pid(pp); + + if ((pid & 0x30) == 0x20) return (0x2); // AX.25 Layer 3 + if ((pid & 0x30) == 0x10) return (0x2); // AX.25 Layer 3 + if (pid == 0x01) return (0x3); // ISO 8208 / CCIT X.25 PLP + if (pid == 0x06) return (0x4); // Compressed TCP/IP + if (pid == 0x07) return (0x5); // Uncompressed TCP/IP + if (pid == 0x08) return (0x6); // Segmentation fragmen + if (pid == 0xcc) return (0xb); // ARPA Internet Protocol + if (pid == 0xcd) return (0xc); // ARPA Address Resolution + if (pid == 0xce) return (0xd); // FlexNet + if (pid == 0xcf) return (0xe); // TheNET + if (pid == 0xf0) return (0xf); // No L3 + return (-1); +} + +// Convert IL2P 4 bit PID to AX.25 8 bit PID. + + +static int decode_pid (int pid) +{ + static const unsigned char axpid[16] = { + 0xf0, // Should not happen. 0 is for 'S' frames. + 0xf0, // Should not happen. 1 is for 'U' frames (but not UI). + 0x20, // AX.25 Layer 3 + 0x01, // ISO 8208 / CCIT X.25 PLP + 0x06, // Compressed TCP/IP + 0x07, // Uncompressed TCP/IP + 0x08, // Segmentation fragment + 0xf0, // Future + 0xf0, // Future + 0xf0, // Future + 0xf0, // Future + 0xcc, // ARPA Internet Protocol + 0xcd, // ARPA Address Resolution + 0xce, // FlexNet + 0xcf, // TheNET + 0xf0 }; // No L3 + + assert (pid >= 0 && pid <= 15); + return (axpid[pid]); +} + + + +/*-------------------------------------------------------------------------------- + * + * Function: il2p_type_1_header + * + * Purpose: Attempt to create type 1 header from packet object. + * + * Inputs: pp - Packet object. + * + * max_fec - 1 to use maximum FEC symbols , 0 for automatic. + * + * Outputs: hdr - IL2P header with no scrambling or parity symbols. + * Must be large enough to hold IL2P_HEADER_SIZE unsigned bytes. + * + * Returns: Number of bytes for information part or -1 for failure. + * In case of failure, fall back to type 0 transparent encapsulation. + * + * Description: Type 1 Headers do not support AX.25 repeater callsign addressing, + * Modulo-128 extended mode window sequence numbers, nor any callsign + * characters that cannot translate to DEC SIXBIT. + * If these cases are encountered during IL2P packet encoding, + * the encoder switches to Type 0 Transparent Encapsulation. + * SABME can't be handled by type 1. + * + *--------------------------------------------------------------------------------*/ + +int il2p_type_1_header (packet_t pp, int max_fec, unsigned char *hdr) +{ + memset (hdr, 0, IL2P_HEADER_SIZE); + + if (ax25_get_num_addr(pp) != 2) { + // Only two addresses are allowed for type 1 header. + return (-1); + } + + // Check does not apply for 'U' frames but put in one place rather than two. + + if (ax25_get_modulo(pp) == 128) return(-1); + +// Destination and source addresses go into low bits 0-5 for bytes 0-11. + + char dst_addr[AX25_MAX_ADDR_LEN]; + char src_addr[AX25_MAX_ADDR_LEN]; + + ax25_get_addr_no_ssid (pp, AX25_DESTINATION, dst_addr); + int dst_ssid = ax25_get_ssid (pp, AX25_DESTINATION); + + ax25_get_addr_no_ssid (pp, AX25_SOURCE, src_addr); + int src_ssid = ax25_get_ssid (pp, AX25_SOURCE); + + unsigned char *a = (unsigned char *)dst_addr; + for (int i = 0; *a != '\0'; i++, a++) { + if (*a < ' ' || *a > '_') { + // Shouldn't happen but follow the rule. + return (-1); + } + hdr[i] = ascii_to_sixbit(*a); + } + + a = (unsigned char *)src_addr; + for (int i = 6; *a != '\0'; i++, a++) { + if (*a < ' ' || *a > '_') { + // Shouldn't happen but follow the rule. + return (-1); + } + hdr[i] = ascii_to_sixbit(*a); + } + + // Byte 12 has DEST SSID in upper nybble and SRC SSID in lower nybble and + hdr[12] = (dst_ssid << 4) | src_ssid; + + 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 (pp, &cr, description, &pf, &nr, &ns); + + //dw_printf ("%s(): %s-%d>%s-%d: %s\n", __func__, src_addr, src_ssid, dst_addr, dst_ssid, description); + + switch (frame_type) { + + case frame_type_S_RR: // Receive Ready - System Ready To Receive + case frame_type_S_RNR: // Receive Not Ready - TNC Buffer Full + case frame_type_S_REJ: // Reject Frame - Out of Sequence or Duplicate + case frame_type_S_SREJ: // Selective Reject - Request single frame repeat + + // S frames (RR, RNR, REJ, SREJ), mod 8, have control N(R) P/F S S 0 1 + // These are mapped into P/F N(R) C S S + // Bit 6 is not mentioned in documentation but it is used for P/F for the other frame types. + // C is copied from the C bit in the destination addr. + // C from source is not used here. Reception assumes it is the opposite. + // PID is set to 0, meaning none, for S frames. + + SET_UI(hdr, 0); + SET_PID(hdr, 0); + SET_CONTROL(hdr, (pf<<6) | (nr<<3) | (((cr == cr_cmd) | (cr == cr_11))<<2)); + + // This gets OR'ed into the above. + switch (frame_type) { + case frame_type_S_RR: SET_CONTROL(hdr, 0); break; + case frame_type_S_RNR: SET_CONTROL(hdr, 1); break; + case frame_type_S_REJ: SET_CONTROL(hdr, 2); break; + case frame_type_S_SREJ: SET_CONTROL(hdr, 3); break; + default: break; + } + + break; + + case frame_type_U_SABM: // Set Async Balanced Mode + case frame_type_U_DISC: // Disconnect + case frame_type_U_DM: // Disconnect Mode + case frame_type_U_UA: // Unnumbered Acknowledge + case frame_type_U_FRMR: // Frame Reject + case frame_type_U_UI: // Unnumbered Information + case frame_type_U_XID: // Exchange Identification + case frame_type_U_TEST: // Test + + // The encoding allows only 3 bits for frame type and SABME got left out. + // Control format: P/F opcode[3] C n/a n/a + // The grayed out n/a bits are observed as 00 in the example. + // The header UI field must also be set for UI frames. + // PID is set to 1 for all U frames other than UI. + + if (frame_type == frame_type_U_UI) { + SET_UI(hdr, 1); // I guess this is how we distinguish 'I' and 'UI' + // on the receiving end. + int pid = encode_pid(pp); + if (pid < 0) return (-1); + SET_PID(hdr, pid); + } + else { + SET_PID(hdr, 1); // 1 for 'U' other than 'UI'. + } + + // Each of the destination and source addresses has a "C" bit. + // They should normally have the opposite setting. + // IL2P has only a single bit to represent 4 possbilities. + // + // dst src il2p meaning + // --- --- ---- ------- + // 0 0 0 Not valid (earlier protocol version) + // 1 0 1 Command (v2) + // 0 1 0 Response (v2) + // 1 1 1 Not valid (earlier protocol version) + // + // APRS does not mention how to set these bits and all 4 combinations + // are seen in the wild. Apparently these are ignored on receive and no + // one cares. Here we copy from the C bit in the destination address. + // It should be noted that the case of both C bits being the same can't + // be represented so the il2p encode/decode bit not produce exactly the + // same bits. We see this in the second example in the protocol spec. + // The original UI frame has both C bits of 0 so it is received as a response. + + SET_CONTROL(hdr, (pf<<6) | (((cr == cr_cmd) | (cr == cr_11))<<2)); + + // This gets OR'ed into the above. + switch (frame_type) { + case frame_type_U_SABM: SET_CONTROL(hdr, 0<<3); break; + case frame_type_U_DISC: SET_CONTROL(hdr, 1<<3); break; + case frame_type_U_DM: SET_CONTROL(hdr, 2<<3); break; + case frame_type_U_UA: SET_CONTROL(hdr, 3<<3); break; + case frame_type_U_FRMR: SET_CONTROL(hdr, 4<<3); break; + case frame_type_U_UI: SET_CONTROL(hdr, 5<<3); break; + case frame_type_U_XID: SET_CONTROL(hdr, 6<<3); break; + case frame_type_U_TEST: SET_CONTROL(hdr, 7<<3); break; + default: break; + } + break; + + case frame_type_I: // Information + + // I frames (mod 8 only) + // encoded control: P/F N(R) N(S) + + SET_UI(hdr, 0); + + int pid2 = encode_pid(pp); + if (pid2 < 0) return (-1); + SET_PID(hdr, pid2); + + SET_CONTROL(hdr, (pf<<6) | (nr<<3) | ns); + break; + + case frame_type_U_SABME: // Set Async Balanced Mode, Extended + case frame_type_U: // other Unnumbered, not used by AX.25. + case frame_not_AX25: // Could not get control byte from frame. + default: + + // Fall back to the header type 0 for these. + return (-1); + } + +// Common for all header type 1. + + // Bit 7 has [FEC Level:1], [HDR Type:1], [Payload byte Count:10] + + SET_FEC_LEVEL(hdr, max_fec); + SET_HDR_TYPE(hdr, 1); + + unsigned char *pinfo; + int info_len; + + info_len = ax25_get_info (pp, &pinfo); + if (info_len < 0 || info_len > IL2P_MAX_PAYLOAD_SIZE) { + return (-2); + } + + SET_PAYLOAD_BYTE_COUNT(hdr, info_len); + return (info_len); +} + + +// This should create a packet from the IL2P header. +// The information part will not be filled in. + +static void trim (char *stuff) +{ + char *p = stuff + strlen(stuff) - 1; + while (strlen(stuff) > 0 && (*p == ' ')) { + *p = '\0'; + p--; + } +} + + + +/*-------------------------------------------------------------------------------- + * + * Function: il2p_decode_header_type_1 + * + * Purpose: Attempt to convert type 1 header to a packet object. + * + * Inputs: hdr - IL2P header with no scrambling or parity symbols. + * + * num_sym_changed - Number of symbols changed by FEC in the header. + * Should be 0 or 1. + * + * Returns: Packet Object or NULL for failure. + * + * Description: A later step will process the payload for the information part. + * + *--------------------------------------------------------------------------------*/ + +packet_t il2p_decode_header_type_1 (unsigned char *hdr, int num_sym_changed) +{ + + if (GET_HDR_TYPE(hdr) != 1 ) { + text_color_set(DW_COLOR_ERROR); + dw_printf ("IL2P Internal error. Should not be here: %s, when header type is 0.\n", __func__); + return (NULL); + } + +// First get the addresses including SSID. + + char addrs[AX25_MAX_ADDRS][AX25_MAX_ADDR_LEN]; + int num_addr = 2; + memset (addrs, 0, 2*AX25_MAX_ADDR_LEN); + +// The IL2P header uses 2 parity symbols which means a single corrupted symbol (byte) +// can always be corrected. +// However, I have seen cases, where the error rate is very high, where the RS decoder +// thinks it found a valid code block by changing one symbol but it was the wrong one. +// The result is trash. This shows up as address fields like 'R&G4"A' and 'TEW\ !'. +// I added a sanity check here to catch characters other than upper case letters and digits. +// The frame should be rejected in this case. The question is whether to discard it +// silently or print a message so the user can see that something strange is happening? +// My current thinking is that it should be silently ignored if the header has been +// modified (correctee or more likely, made worse in this cases). +// If no changes were made, something weird is happening. We should mention it for +// troubleshooting rather than sweeping it under the rug. + +// The same thing has been observed with the payload, under very high error conditions, +// and max_fec==0. Here I don't see a good solution. AX.25 information can contain +// "binary" data so I'm not sure what sort of sanity check could be added. +// This was not observed with max_fec==1. If we make that the default, same as Nino TNC, +// it would be extremely extremely unlikely unless someone explicitly selects weaker FEC. + +// TODO: We could do something similar for header type 0. +// The address fields should be all binary zero values. +// Someone overly ambitious might check the addresses found in the first payload block. + + for (int i = 0; i <= 5; i++) { + addrs[AX25_DESTINATION][i] = sixbit_to_ascii(hdr[i] & 0x3f); + } + trim (addrs[AX25_DESTINATION]); + for (int i = 0; i < strlen(addrs[AX25_DESTINATION]); i++) { + if (! isupper(addrs[AX25_DESTINATION][i]) && ! isdigit(addrs[AX25_DESTINATION][i])) { + if (num_sym_changed == 0) { + // This can pop up sporadically when receiving random noise. + // Would be better to show only when debug is enabled but variable not available here. + // TODO: For now we will just suppress it. + //text_color_set(DW_COLOR_ERROR); + //dw_printf ("IL2P: Invalid character '%c' in destination address '%s'\n", addrs[AX25_DESTINATION][i], addrs[AX25_DESTINATION]); + } + return (NULL); + } + } + snprintf (addrs[AX25_DESTINATION]+strlen(addrs[AX25_DESTINATION]), 4, "-%d", (hdr[12] >> 4) &0xf); + + for (int i = 0; i <= 5; i++) { + addrs[AX25_SOURCE][i] = sixbit_to_ascii(hdr[i+6] & 0x3f); + } + trim (addrs[AX25_SOURCE]); + for (int i = 0; i < strlen(addrs[AX25_SOURCE]); i++) { + if (! isupper(addrs[AX25_SOURCE][i]) && ! isdigit(addrs[AX25_SOURCE][i])) { + if (num_sym_changed == 0) { + // This can pop up sporadically when receiving random noise. + // Would be better to show only when debug is enabled but variable not available here. + // TODO: For now we will just suppress it. + //text_color_set(DW_COLOR_ERROR); + //dw_printf ("IL2P: Invalid character '%c' in source address '%s'\n", addrs[AX25_SOURCE][i], addrs[AX25_SOURCE]); + } + return (NULL); + } + } + snprintf (addrs[AX25_SOURCE]+strlen(addrs[AX25_SOURCE]), 4, "-%d", hdr[12] &0xf); + +// The PID field gives us the general type. +// 0 = 'S' frame. +// 1 = 'U' frame other than UI. +// others are either 'UI' or 'I' depending on the UI field. + + int pid = GET_PID(hdr); + int ui = GET_UI(hdr); + + if (pid == 0) { + +// 'S' frame. +// The control field contains: P/F N(R) C S S + + int control = GET_CONTROL(hdr); + cmdres_t cr = (control & 0x04) ? cr_cmd : cr_res; + ax25_frame_type_t ftype; + switch (control & 0x03) { + case 0: ftype = frame_type_S_RR; break; + case 1: ftype = frame_type_S_RNR; break; + case 2: ftype = frame_type_S_REJ; break; + default: ftype = frame_type_S_SREJ; break; + } + int modulo = 8; + int nr = (control >> 3) & 0x07; + int pf = (control >> 6) & 0x01; + unsigned char *pinfo = NULL; // Any info for SREJ will be added later. + int info_len = 0; + return (ax25_s_frame (addrs, num_addr, cr, ftype, modulo, nr, pf, pinfo, info_len)); + } + else if (pid == 1) { + +// 'U' frame other than 'UI'. +// The control field contains: P/F OPCODE{3) C x x + + int control = GET_CONTROL(hdr); + cmdres_t cr = (control & 0x04) ? cr_cmd : cr_res; + int axpid = 0; // unused for U other than UI. + ax25_frame_type_t ftype; + switch ((control >> 3) & 0x7) { + case 0: ftype = frame_type_U_SABM; break; + case 1: ftype = frame_type_U_DISC; break; + case 2: ftype = frame_type_U_DM; break; + case 3: ftype = frame_type_U_UA; break; + case 4: ftype = frame_type_U_FRMR; break; + case 5: ftype = frame_type_U_UI; axpid = 0xf0; break; // Should not happen with IL2P pid == 1. + case 6: ftype = frame_type_U_XID; break; + default: ftype = frame_type_U_TEST; break; + } + int pf = (control >> 6) & 0x01; + unsigned char *pinfo = NULL; // Any info for UI, XID, TEST will be added later. + int info_len = 0; + return (ax25_u_frame (addrs, num_addr, cr, ftype, pf, axpid, pinfo, info_len)); + } + else if (ui) { + +// 'UI' frame. +// The control field contains: P/F OPCODE{3) C x x + + int control = GET_CONTROL(hdr); + cmdres_t cr = (control & 0x04) ? cr_cmd : cr_res; + ax25_frame_type_t ftype = frame_type_U_UI; + int pf = (control >> 6) & 0x01; + int axpid = decode_pid(GET_PID(hdr)); + unsigned char *pinfo = NULL; // Any info for UI, XID, TEST will be added later. + int info_len = 0; + return (ax25_u_frame (addrs, num_addr, cr, ftype, pf, axpid, pinfo, info_len)); + } + else { + +// 'I' frame. +// The control field contains: P/F N(R) N(S) + + int control = GET_CONTROL(hdr); + cmdres_t cr = cr_cmd; // Always command. + int pf = (control >> 6) & 0x01; + int nr = (control >> 3) & 0x7; + int ns = control & 0x7; + int modulo = 8; + int axpid = decode_pid(GET_PID(hdr)); + unsigned char *pinfo = NULL; // Any info for UI, XID, TEST will be added later. + int info_len = 0; + return (ax25_i_frame (addrs, num_addr, cr, modulo, nr, ns, pf, axpid, pinfo, info_len)); + } + return (NULL); // unreachable but avoid warning. + +} // end + + +/*-------------------------------------------------------------------------------- + * + * Function: il2p_type_0_header + * + * Purpose: Attempt to create type 0 header from packet object. + * + * Inputs: pp - Packet object. + * + * max_fec - 1 to use maximum FEC symbols, 0 for automatic. + * + * Outputs: hdr - IL2P header with no scrambling or parity symbols. + * Must be large enough to hold IL2P_HEADER_SIZE unsigned bytes. + * + * Returns: Number of bytes for information part or -1 for failure. + * In case of failure, fall back to type 0 transparent encapsulation. + * + * Description: The type 0 header is used when it is not one of the restricted cases + * covered by the type 1 header. + * The AX.25 frame is put in the payload. + * This will cover: more than one address, mod 128 sequences, etc. + * + *--------------------------------------------------------------------------------*/ + +int il2p_type_0_header (packet_t pp, int max_fec, unsigned char *hdr) +{ + memset (hdr, 0, IL2P_HEADER_SIZE); + + // Bit 7 has [FEC Level:1], [HDR Type:1], [Payload byte Count:10] + + SET_FEC_LEVEL(hdr, max_fec); + SET_HDR_TYPE(hdr, 0); + + int frame_len = ax25_get_frame_len (pp); + + if (frame_len < 14 || frame_len > IL2P_MAX_PAYLOAD_SIZE) { + return (-2); + } + + SET_PAYLOAD_BYTE_COUNT(hdr, frame_len); + return (frame_len); +} + + +/*********************************************************************************** + * + * Name: il2p_get_header_attributes + * + * Purpose: Extract a few attributes from an IL2p header. + * + * Inputs: hdr - IL2P header structure. + * + * Outputs: hdr_type - 0 or 1. + * + * max_fec - 0 for automatic or 1 for fixed maximum size. + * + * Returns: Payload byte count. (actual payload size, not the larger encoded format) + * + ***********************************************************************************/ + + +int il2p_get_header_attributes (unsigned char *hdr, int *hdr_type, int *max_fec) +{ + *hdr_type = GET_HDR_TYPE(hdr); + *max_fec = GET_FEC_LEVEL(hdr); + return(GET_PAYLOAD_BYTE_COUNT(hdr)); +} + + +/*********************************************************************************** + * + * Name: il2p_clarify_header + * + * Purpose: Convert received header to usable form. + * This involves RS FEC then descrambling. + * + * Inputs: rec_hdr - Header as received over the radio. + * + * Outputs: corrected_descrambled_hdr - After RS FEC and unscrambling. + * + * Returns: Number of symbols that were corrected: + * 0 = No errors + * 1 = Single symbol corrected. + * <0 = Unable to obtain good header. + * + ***********************************************************************************/ + +int il2p_clarify_header(unsigned char *rec_hdr, unsigned char *corrected_descrambled_hdr) +{ + unsigned char corrected[IL2P_HEADER_SIZE+IL2P_HEADER_PARITY]; + + int e = il2p_decode_rs (rec_hdr, IL2P_HEADER_SIZE, IL2P_HEADER_PARITY, corrected); + + il2p_descramble_block (corrected, corrected_descrambled_hdr, IL2P_HEADER_SIZE); + + return (e); +} + +// end il2p_header.c \ No newline at end of file diff --git a/src/il2p_init.c b/src/il2p_init.c new file mode 100644 index 00000000..533a2213 --- /dev/null +++ b/src/il2p_init.c @@ -0,0 +1,226 @@ +// +// This file is part of Dire Wolf, an amateur radio packet TNC. +// +// Copyright (C) 2021 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 . +// + +#include "direwolf.h" + +#include +#include +#include +#include + +#include "textcolor.h" +#include "fx25.h" // For Reed Solomon stuff. +#include "il2p.h" + +// Interesting related stuff: +// https://www.kernel.org/doc/html/v4.15/core-api/librs.html +// https://berthub.eu/articles/posts/reed-solomon-for-programmers/ + + +#define MAX_NROOTS 16 + +#define NTAB 5 + +static struct { + int symsize; // Symbol size, bits (1-8). Always 8 for this application. + int genpoly; // Field generator polynomial coefficients. + int fcs; // First root of RS code generator polynomial, index form. + // FX.25 uses 1 but IL2P uses 0. + int prim; // Primitive element to generate polynomial roots. + int nroots; // RS code generator polynomial degree (number of roots). + // Same as number of check bytes added. + struct rs *rs; // Pointer to RS codec control block. Filled in at init time. +} Tab[NTAB] = { + {8, 0x11d, 0, 1, 2, NULL }, // 2 parity + {8, 0x11d, 0, 1, 4, NULL }, // 4 parity + {8, 0x11d, 0, 1, 6, NULL }, // 6 parity + {8, 0x11d, 0, 1, 8, NULL }, // 8 parity + {8, 0x11d, 0, 1, 16, NULL }, // 16 parity +}; + + + +static int g_il2p_debug = 0; + + +/*------------------------------------------------------------- + * + * Name: il2p_init + * + * Purpose: This must be called at application start up time. + * It sets up tables for the Reed-Solomon functions. + * + * Inputs: debug - Enable debug output. + * + *--------------------------------------------------------------*/ + +void il2p_init (int il2p_debug) +{ + g_il2p_debug = il2p_debug; + + for (int i = 0 ; i < NTAB ; i++) { + assert (Tab[i].nroots <= MAX_NROOTS); + Tab[i].rs = INIT_RS(Tab[i].symsize, Tab[i].genpoly, Tab[i].fcs, Tab[i].prim, Tab[i].nroots); + if (Tab[i].rs == NULL) { + text_color_set(DW_COLOR_ERROR); + dw_printf("IL2P internal error: init_rs_char failed!\n"); + exit(EXIT_FAILURE); + } + } + +} // end il2p_init + + +int il2p_get_debug(void) +{ + return (g_il2p_debug); +} +void il2p_set_debug(int debug) +{ + g_il2p_debug = debug; +} + + +// Find RS codec control block for specified number of parity symbols. + +struct rs *il2p_find_rs(int nparity) +{ + for (int n = 0; n < NTAB; n++) { + if (Tab[n].nroots == nparity) { + return (Tab[n].rs); + } + } + text_color_set(DW_COLOR_ERROR); + dw_printf ("IL2P INTERNAL ERROR: il2p_find_rs: control block not found for nparity = %d.\n", nparity); + return (Tab[0].rs); +} + + +/*------------------------------------------------------------- + * + * Name: void il2p_encode_rs + * + * Purpose: Add parity symbols to a block of data. + * + * Inputs: tx_data Header or other data to transmit. + * data_size Number of data bytes in above. + * num_parity Number of parity symbols to add. + * Maximum of IL2P_MAX_PARITY_SYMBOLS. + * + * Outputs: parity_out Specified number of parity symbols + * + * Restriction: data_size + num_parity <= 255 which is the RS block size. + * The caller must ensure this. + * + *--------------------------------------------------------------*/ + +void il2p_encode_rs (unsigned char *tx_data, int data_size, int num_parity, unsigned char *parity_out) +{ + assert (data_size >= 1); + assert (num_parity == 2 || num_parity == 4 || num_parity == 6 || num_parity == 8 || num_parity == 16); + assert (data_size + num_parity <= 255); + + unsigned char rs_block[FX25_BLOCK_SIZE]; + memset (rs_block, 0, sizeof(rs_block)); + memcpy (rs_block + sizeof(rs_block) - data_size - num_parity, tx_data, data_size); + ENCODE_RS (il2p_find_rs(num_parity), rs_block, parity_out); +} + +/*------------------------------------------------------------- + * + * Name: void il2p_decode_rs + * + * Purpose: Check and attempt to fix block with FEC. + * + * Inputs: rec_block Received block composed of data and parity. + * Total size is sum of following two parameters. + * data_size Number of data bytes in above. + * num_parity Number of parity symbols (bytes) in above. + * + * Outputs: out Original with possible corrections applied. + * data_size bytes. + * + * Returns: -1 for unrecoverable. + * >= 0 for success. Number of symbols corrected. + * + *--------------------------------------------------------------*/ + +int il2p_decode_rs (unsigned char *rec_block, int data_size, int num_parity, unsigned char *out) +{ + + // Use zero padding in front if data size is too small. + + int n = data_size + num_parity; // total size in. + + unsigned char rs_block[FX25_BLOCK_SIZE]; + + // We could probably do this more efficiently by skipping the + // processing of the bytes known to be zero. Good enough for now. + + memset (rs_block, 0, sizeof(rs_block) - n); + memcpy (rs_block + sizeof(rs_block) - n, rec_block, n); + + if (il2p_get_debug() >= 3) { + text_color_set(DW_COLOR_DEBUG); + dw_printf ("============================== il2p_decode_rs ==============================\n"); + dw_printf ("%d filler zeros, %d data, %d parity\n", (int)(sizeof(rs_block) - n), data_size, num_parity); + fx_hex_dump (rs_block, sizeof (rs_block)); + } + + int derrlocs[FX25_MAX_CHECK]; // Half would probably be OK. + + int derrors = DECODE_RS(il2p_find_rs(num_parity), rs_block, derrlocs, 0); + memcpy (out, rs_block + sizeof(rs_block) - n, data_size); + + if (il2p_get_debug() >= 3) { + if (derrors == 0) { + dw_printf ("No errors reported for RS block.\n"); + } + else if (derrors > 0) { + dw_printf ("%d errors fixed in positions:\n", derrors); + for (int j = 0; j < derrors; j++) { + dw_printf (" %3d (0x%02x)\n", derrlocs[j] , derrlocs[j]); + } + fx_hex_dump (rs_block, sizeof (rs_block)); + } + } + + // It is possible to have a situation where too many errors are + // present but the algorithm could get a good code block by "fixing" + // one of the padding bytes that should be 0. + + for (int i = 0; i < derrors; i++) { + if (derrlocs[i] < sizeof(rs_block) - n) { + if (il2p_get_debug() >= 3) { + text_color_set(DW_COLOR_DEBUG); + dw_printf ("RS DECODE ERROR! Padding position %d should be 0 but it was set to %02x.\n", derrlocs[i], rs_block[derrlocs[i]]); + } + derrors = -1; + break; + } + } + + if (il2p_get_debug() >= 3) { + text_color_set(DW_COLOR_DEBUG); + dw_printf ("============================== il2p_decode_rs returns %d ==============================\n", derrors); + } + return (derrors); +} + +// end il2p_init.c \ No newline at end of file diff --git a/src/il2p_payload.c b/src/il2p_payload.c new file mode 100644 index 00000000..d5fb4887 --- /dev/null +++ b/src/il2p_payload.c @@ -0,0 +1,298 @@ +// +// This file is part of Dire Wolf, an amateur radio packet TNC. +// +// Copyright (C) 2021 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 . +// + +#include "direwolf.h" + +#include +#include +#include +#include + +#include "textcolor.h" +#include "il2p.h" + + +/*-------------------------------------------------------------------------------- + * + * File: il2p_payload.c + * + * Purpose: Functions dealing with the payload. + * + *--------------------------------------------------------------------------------*/ + + +/*-------------------------------------------------------------------------------- + * + * Function: il2p_payload_compute + * + * Purpose: Compute number and sizes of data blocks based on total size. + * + * Inputs: payload_size 0 to 1023. (IL2P_MAX_PAYLOAD_SIZE) + * max_fec true for 16 parity symbols, false for automatic. + * + * Outputs: *p Payload block sizes and counts. + * Number of parity symbols per block. + * + * Returns: Number of bytes in the encoded format. + * Could be 0 for no payload blocks. + * -1 for error (i.e. invalid unencoded size: <0 or >1023) + * + *--------------------------------------------------------------------------------*/ + +int il2p_payload_compute (il2p_payload_properties_t *p, int payload_size, int max_fec) +{ + memset (p, 0, sizeof(il2p_payload_properties_t)); + + if (payload_size < 0 || payload_size > IL2P_MAX_PAYLOAD_SIZE) { + return (-1); + } + if (payload_size == 0) { + return (0); + } + + if (max_fec) { + p->payload_byte_count = payload_size; + p->payload_block_count = (p->payload_byte_count + 238) / 239; + p->small_block_size = p->payload_byte_count / p->payload_block_count; + p->large_block_size = p->small_block_size + 1; + p->large_block_count = p->payload_byte_count - (p->payload_block_count * p->small_block_size); + p->small_block_count = p->payload_block_count - p->large_block_count; + p->parity_symbols_per_block = 16; + } + else { + p->payload_byte_count = payload_size; + p->payload_block_count = (p->payload_byte_count + 246) / 247; + p->small_block_size = p->payload_byte_count / p->payload_block_count; + p->large_block_size = p->small_block_size + 1; + p->large_block_count = p->payload_byte_count - (p->payload_block_count * p->small_block_size); + p->small_block_count = p->payload_block_count - p->large_block_count; + //p->parity_symbols_per_block = (p->small_block_size / 32) + 2; // Looks like error in documentation + + // It would work if the number of parity symbols was based on large block size. + + if (p->small_block_size <= 61) p->parity_symbols_per_block = 2; + else if (p->small_block_size <= 123) p->parity_symbols_per_block = 4; + else if (p->small_block_size <= 185) p->parity_symbols_per_block = 6; + else if (p->small_block_size <= 247) p->parity_symbols_per_block = 8; + else { + // Should not happen. But just in case... + text_color_set(DW_COLOR_ERROR); + dw_printf ("IL2P parity symbol per payload block error. small_block_size = %d\n", p->small_block_size); + return (-1); + } + } + + // Return the total size for the encoded format. + + return (p->small_block_count * (p->small_block_size + p->parity_symbols_per_block) + + p->large_block_count * (p->large_block_size + p->parity_symbols_per_block)); + +} // end il2p_payload_compute + + + +/*-------------------------------------------------------------------------------- + * + * Function: il2p_encode_payload + * + * Purpose: Split payload into multiple blocks such that each set + * of data and parity symbols fit into a 255 byte RS block. + * + * Inputs: *payload Array of bytes. + * payload_size 0 to 1023. (IL2P_MAX_PAYLOAD_SIZE) + * max_fec true for 16 parity symbols, false for automatic. + * + * Outputs: *enc Encoded payload for transmission. + * Up to IL2P_MAX_ENCODED_SIZE bytes. + * + * Returns: -1 for error (i.e. invalid size) + * 0 for no blocks. (i.e. size zero) + * Number of bytes generated. Maximum IL2P_MAX_ENCODED_SIZE. + * + * Note: I interpreted the protocol spec as saying the LFSR state is retained + * between data blocks. During interoperability testing, I found that + * was not the case. It is reset for each data block. + * + *--------------------------------------------------------------------------------*/ + + +int il2p_encode_payload (unsigned char *payload, int payload_size, int max_fec, unsigned char *enc) +{ + if (payload_size > IL2P_MAX_PAYLOAD_SIZE) return (-1); + if (payload_size == 0) return (0); + +// Determine number of blocks and sizes. + + il2p_payload_properties_t ipp; + int e; + e = il2p_payload_compute (&ipp, payload_size, max_fec); + if (e <= 0) { + return (e); + } + + unsigned char *pin = payload; + unsigned char *pout = enc; + int encoded_length = 0; + unsigned char scram[256]; + unsigned char parity[IL2P_MAX_PARITY_SYMBOLS]; + +// First the large blocks. + + for (int b = 0; b < ipp.large_block_count; b++) { + + il2p_scramble_block (pin, scram, ipp.large_block_size); + memcpy (pout, scram, ipp.large_block_size); + pin += ipp.large_block_size; + pout += ipp.large_block_size; + encoded_length += ipp.large_block_size; + il2p_encode_rs (scram, ipp.large_block_size, ipp.parity_symbols_per_block, parity); + memcpy (pout, parity, ipp.parity_symbols_per_block); + pout += ipp.parity_symbols_per_block; + encoded_length += ipp.parity_symbols_per_block; + } + +// Then the small blocks. + + for (int b = 0; b < ipp.small_block_count; b++) { + + il2p_scramble_block (pin, scram, ipp.small_block_size); + memcpy (pout, scram, ipp.small_block_size); + pin += ipp.small_block_size; + pout += ipp.small_block_size; + encoded_length += ipp.small_block_size; + il2p_encode_rs (scram, ipp.small_block_size, ipp.parity_symbols_per_block, parity); + memcpy (pout, parity, ipp.parity_symbols_per_block); + pout += ipp.parity_symbols_per_block; + encoded_length += ipp.parity_symbols_per_block; + } + + return (encoded_length); + +} // end il2p_encode_payload + + +/*-------------------------------------------------------------------------------- + * + * Function: il2p_decode_payload + * + * Purpose: Extract original data from encoded payload. + * + * Inputs: received Array of bytes. Size is unknown but in practice it + * must not exceed IL2P_MAX_ENCODED_SIZE. + * payload_size 0 to 1023. (IL2P_MAX_PAYLOAD_SIZE) + * Expected result size based on header. + * max_fec true for 16 parity symbols, false for automatic. + * + * Outputs: payload_out Recovered payload. + * + * In/Out: symbols_corrected Number of symbols corrected. + * + * + * Returns: Number of bytes extracted. Should be same as payload_size going in. + * -3 for unexpected internal inconsistency. + * -2 for unable to recover from signal corruption. + * -1 for invalid size. + * 0 for no blocks. (i.e. size zero) + * + * Description: Each block is scrambled separately but the LSFR state is carried + * from the first payload block to the next. + * + *--------------------------------------------------------------------------------*/ + +int il2p_decode_payload (unsigned char *received, int payload_size, int max_fec, unsigned char *payload_out, int *symbols_corrected) +{ +// Determine number of blocks and sizes. + + il2p_payload_properties_t ipp; + int e; + e = il2p_payload_compute (&ipp, payload_size, max_fec); + if (e <= 0) { + return (e); + } + + unsigned char *pin = received; + unsigned char *pout = payload_out; + int decoded_length = 0; + int failed = 0; + +// First the large blocks. + + for (int b = 0; b < ipp.large_block_count; b++) { + unsigned char corrected_block[255]; + int e = il2p_decode_rs (pin, ipp.large_block_size, ipp.parity_symbols_per_block, corrected_block); + + // dw_printf ("%s:%d: large block decode_rs returned status = %d\n", __FILE__, __LINE__, e); + + if (e < 0) failed = 1; + *symbols_corrected += e; + + il2p_descramble_block (corrected_block, pout, ipp.large_block_size); + + if (il2p_get_debug() >= 2) { + text_color_set(DW_COLOR_DEBUG); + dw_printf ("Descrambled large payload block, %d bytes:\n", ipp.large_block_size); + fx_hex_dump(pout, ipp.large_block_size); + } + + pin += ipp.large_block_size + ipp.parity_symbols_per_block; + pout += ipp.large_block_size; + decoded_length += ipp.large_block_size; + } + +// Then the small blocks. + + for (int b = 0; b < ipp.small_block_count; b++) { + unsigned char corrected_block[255]; + int e = il2p_decode_rs (pin, ipp.small_block_size, ipp.parity_symbols_per_block, corrected_block); + + // dw_printf ("%s:%d: small block decode_rs returned status = %d\n", __FILE__, __LINE__, e); + + if (e < 0) failed = 1; + *symbols_corrected += e; + + il2p_descramble_block (corrected_block, pout, ipp.small_block_size); + + if (il2p_get_debug() >= 2) { + text_color_set(DW_COLOR_DEBUG); + dw_printf ("Descrambled small payload block, %d bytes:\n", ipp.small_block_size); + fx_hex_dump(pout, ipp.small_block_size); + } + + pin += ipp.small_block_size + ipp.parity_symbols_per_block; + pout += ipp.small_block_size; + decoded_length += ipp.small_block_size; + } + + if (failed) { + //dw_printf ("%s:%d: failed = %0x\n", __FILE__, __LINE__, failed); + return (-2); + } + + if (decoded_length != payload_size) { + text_color_set(DW_COLOR_ERROR); + dw_printf ("IL2P Internal error: decoded_length = %d, payload_size = %d\n", decoded_length, payload_size); + return (-3); + } + + return (decoded_length); + +} // end il2p_decode_payload + +// end il2p_payload.c + diff --git a/src/il2p_rec.c b/src/il2p_rec.c new file mode 100644 index 00000000..5ad457a0 --- /dev/null +++ b/src/il2p_rec.c @@ -0,0 +1,276 @@ +// +// This file is part of Dire Wolf, an amateur radio packet TNC. +// +// Copyright (C) 2021 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: il2p_rec.c + * + * Purpose: Extract IL2P frames from a stream of bits and process them. + * + * References: http://tarpn.net/t/il2p/il2p-specification0-4.pdf + * + *******************************************************************************/ + +#include "direwolf.h" + +#include +#include +#include +#include + +#include "textcolor.h" +#include "il2p.h" +#include "multi_modem.h" +#include "demod.h" + + +struct il2p_context_s { + + enum { IL2P_SEARCHING=0, IL2P_HEADER, IL2P_PAYLOAD, IL2P_DECODE } state; + + unsigned int acc; // Accumulate most recent 24 bits for sync word matching. + // Lower 8 bits are also used for accumulating bytes for + // the header and payload. + + int bc; // Bit counter so we know when a complete byte has been accumulated. + + int polarity; // 1 if opposite of expected polarity. + + unsigned char shdr[IL2P_HEADER_SIZE+IL2P_HEADER_PARITY]; + // Scrambled header as received over the radio. Includes parity. + int hc; // Number if bytes placed in above. + + unsigned char uhdr[IL2P_HEADER_SIZE]; // Header after FEC and unscrambling. + + int eplen; // Encoded payload length. This is not the nuumber from + // from the header but rather the number of encoded bytes to gather. + + unsigned char spayload[IL2P_MAX_ENCODED_PAYLOAD_SIZE]; + // Scrambled and encoded payload as received over the radio. + int pc; // Number of bytes placed in above. + + int corrected; // Number of symbols corrected by RS FEC. +}; + +static struct il2p_context_s *il2p_context[MAX_CHANS][MAX_SUBCHANS][MAX_SLICERS]; + + + +/*********************************************************************************** + * + * Name: il2p_rec_bit + * + * Purpose: Extract FX.25 packets from a stream of bits. + * + * Inputs: chan - Channel number. + * + * subchan - This allows multiple demodulators per channel. + * + * slice - Allows multiple slicers per demodulator (subchannel). + * + * dbit - One bit from the received data stream. + * + * Description: This is called once for each received bit. + * For each valid packet, process_rec_frame() is called for further processing. + * It can gather multiple candidates from different parallel demodulators + * ("subchannels") and slicers, then decide which one is the best. + * + ***********************************************************************************/ + +void il2p_rec_bit (int chan, int subchan, int slice, int dbit) +{ + +// Allocate context blocks only as needed. + + struct il2p_context_s *F = il2p_context[chan][subchan][slice]; + if (F == NULL) { + assert (chan >= 0 && chan < MAX_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)); + assert (F != NULL); + memset (F, 0, sizeof(struct il2p_context_s)); + } + +// Accumulate most recent 24 bits received. Most recent is LSB. + + F->acc = ((F->acc << 1) | (dbit & 1)) & 0x00ffffff; + +// State machine to look for sync word then gather appropriate number of header and payload bytes. + + switch (F->state) { + + case IL2P_SEARCHING: // Searching for the sync word. + + if (__builtin_popcount (F->acc ^ IL2P_SYNC_WORD) <= 1) { // allow single bit mismatch + //text_color_set (DW_COLOR_INFO); + //dw_printf ("IL2P header has normal polarity\n"); + F->polarity = 0; + F->state = IL2P_HEADER; + F->bc = 0; + F->hc = 0; + } + else if (__builtin_popcount ((~F->acc & 0x00ffffff) ^ IL2P_SYNC_WORD) <= 1) { + text_color_set (DW_COLOR_INFO); + // FIXME - this pops up occasionally with random noise. Find better way to convey information. + // This also happens for each slicer - to noisy. + //dw_printf ("IL2P header has reverse polarity\n"); + F->polarity = 1; + F->state = IL2P_HEADER; + F->bc = 0; + F->hc = 0; + } + break; + + case IL2P_HEADER: // Gathering the header. + + F->bc++; + if (F->bc == 8) { // full byte has been collected. + F->bc = 0; + if ( ! F->polarity) { + F->shdr[F->hc++] = F->acc & 0xff; + } + else { + F->shdr[F->hc++] = (~ F->acc) & 0xff; + } + if (F->hc == IL2P_HEADER_SIZE+IL2P_HEADER_PARITY) { // Have all of header + + if (il2p_get_debug() >= 1) { + text_color_set (DW_COLOR_DEBUG); + dw_printf ("IL2P header as received [%d.%d.%d]:\n", chan, subchan, slice); + fx_hex_dump (F->shdr, IL2P_HEADER_SIZE+IL2P_HEADER_PARITY); + } + + // Fix any errors and descramble. + F->corrected = il2p_clarify_header(F->shdr, F->uhdr); + + if (F->corrected >= 0) { // Good header. + // How much payload is expected? + il2p_payload_properties_t plprop; + int hdr_type, max_fec; + int len = il2p_get_header_attributes (F->uhdr, &hdr_type, &max_fec); + + F->eplen = il2p_payload_compute (&plprop, len, max_fec); + + if (il2p_get_debug() >= 1) { + text_color_set(DW_COLOR_DEBUG); + dw_printf ("IL2P header after correcting %d symbols and unscrambling [%d.%d.%d]:\n", F->corrected, chan, subchan, slice); + fx_hex_dump (F->uhdr, IL2P_HEADER_SIZE); + dw_printf ("Header type %d, max fec = %d\n", hdr_type, max_fec); + dw_printf ("Need to collect %d encoded bytes for %d byte payload.\n", F->eplen, len); + dw_printf ("%d small blocks of %d and %d large blocks of %d. %d parity symbols per block\n", + plprop.small_block_count, plprop.small_block_size, + plprop.large_block_count, plprop.large_block_size, plprop.parity_symbols_per_block); + } + + if (F->eplen >= 1) { // Need to gather payload. + F->pc = 0; + F->state = IL2P_PAYLOAD; + } + else if (F->eplen == 0) { // No payload. + F->pc = 0; + F->state = IL2P_DECODE; + } + else { // Error. + + if (il2p_get_debug() >= 1) { + text_color_set (DW_COLOR_ERROR); + dw_printf ("IL2P header INVALID.\n"); + } + + F->state = IL2P_SEARCHING; + } + } // good header after FEC. + else { + F->state = IL2P_SEARCHING; // Header failed FEC check. + } + } // entire header has been collected. + } // full byte collected. + break; + + case IL2P_PAYLOAD: // Gathering the payload, if any. + + F->bc++; + if (F->bc == 8) { // full byte has been collected. + F->bc = 0; + if ( ! F->polarity) { + F->spayload[F->pc++] = F->acc & 0xff; + } + else { + F->spayload[F->pc++] = (~ F->acc) & 0xff; + } + if (F->pc == F->eplen) { + + // TODO?: for symmetry it seems like we should clarify the payload before combining. + + F->state = IL2P_DECODE; + } + } + break; + + case IL2P_DECODE: + // We get here after a good header and any payload has been collected. + // Processing is delayed by one bit but I think it makes the logic cleaner. + // During unit testing be sure to send an extra bit to flush it out at the end. + + // in uhdr[IL2P_HEADER_SIZE]; // Header after FEC and descrambling. + + // TODO?: for symmetry, we might decode the payload here and later build the frame. + + { + packet_t pp = il2p_decode_header_payload (F->uhdr, F->spayload, &(F->corrected)); + + if (il2p_get_debug() >= 1) { + if (pp != NULL) { + ax25_hex_dump (pp); + } + else { + // Most likely too many FEC errors. + text_color_set(DW_COLOR_ERROR); + dw_printf ("FAILED to construct frame in %s.\n", __func__); + } + } + + 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. + + // 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); + } + } // end block for local variables. + + if (il2p_get_debug() >= 1) { + text_color_set(DW_COLOR_DEBUG); + dw_printf ("-----\n"); + } + + F->state = IL2P_SEARCHING; + break; + + } // end of switch + +} // end il2p_rec_bit + + +// end il2p_rec.c diff --git a/src/il2p_scramble.c b/src/il2p_scramble.c new file mode 100644 index 00000000..6219cddb --- /dev/null +++ b/src/il2p_scramble.c @@ -0,0 +1,157 @@ +// +// This file is part of Dire Wolf, an amateur radio packet TNC. +// +// Copyright (C) 2021 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: il2p_scramble.c + * + * Purpose: Scramble / descramble data as specified in the IL2P protocol specification. + * + *--------------------------------------------------------------------------------*/ + + +#include "direwolf.h" + +#include +#include +#include +#include + +#include "il2p.h" + + +// Scramble bits for il2p transmit. + +// Note that there is a delay of 5 until the first bit comes out. +// So we need to need to ignore the first 5 out and stick in +// an extra 5 filler bits to flush at the end. + +#define INIT_TX_LSFR 0x00f + +static inline int scramble_bit (int in, int *state) +{ + int out = ((*state >> 4) ^ *state) & 1; + *state = ( (((in ^ *state) & 1) << 9) | (*state ^ ((*state & 1) << 4)) ) >> 1; + return (out); +} + + +// Undo data scrambling for il2p receive. + +#define INIT_RX_LSFR 0x1f0 + +static inline int descramble_bit (int in, int *state) +{ + int out = (in ^ *state) & 1; + *state = ((*state >> 1) | ((in & 1) << 8)) ^ ((in & 1) << 3); + return (out); +} + + +/*-------------------------------------------------------------------------------- + * + * Function: il2p_scramble_block + * + * Purpose: Scramble a block before adding RS parity. + * + * Inputs: in Array of bytes. + * len Number of bytes both in and out. + * + * Outputs: out Array of bytes. + * + *--------------------------------------------------------------------------------*/ + +void il2p_scramble_block (unsigned char *in, unsigned char *out, int len) +{ + int tx_lfsr_state = INIT_TX_LSFR; + + memset (out, 0, len); + + int skipping = 1; // Discard the first 5 out. + int ob = 0; // Index to output byte. + int om = 0x80; // Output bit mask; + for (int ib = 0; ib < len; ib++) { + for (int im = 0x80; im != 0; im >>= 1) { + int s = scramble_bit((in[ib] & im) != 0, &tx_lfsr_state); + if (ib == 0 && im == 0x04) skipping = 0; + if ( ! skipping) { + if (s) { + out[ob] |= om; + } + om >>= 1; + if (om == 0) { + om = 0x80; + ob++; + } + } + } + } + // Flush it. + + // This is a relic from when I thought the state would need to + // be passed along for the next block. + // Preserve the LSFR state from before flushing. + // This might be needed as the initial state for later payload blocks. + int x = tx_lfsr_state; + for (int n = 0; n < 5; n++) { + int s = scramble_bit(0, &x); + if (s) { + out[ob] |= om; + } + om >>=1; + if (om == 0) { + om = 0x80; + ob++; + } + } + +} // end il2p_scramble_block + + + +/*-------------------------------------------------------------------------------- + * + * Function: il2p_descramble_block + * + * Purpose: Descramble a block after removing RS parity. + * + * Inputs: in Array of bytes. + * len Number of bytes both in and out. + * + * Outputs: out Array of bytes. + * + *--------------------------------------------------------------------------------*/ + +void il2p_descramble_block (unsigned char *in, unsigned char *out, int len) +{ + int rx_lfsr_state = INIT_RX_LSFR; + + memset (out, 0, len); + + for (int b = 0; b < len; b++) { + for (int m = 0x80; m != 0; m >>= 1) { + int d = descramble_bit((in[b] & m) != 0, &rx_lfsr_state); + if (d) { + out[b] |= m; + } + } + } +} + +// end il2p_scramble.c diff --git a/src/il2p_send.c b/src/il2p_send.c new file mode 100644 index 00000000..3c4554e0 --- /dev/null +++ b/src/il2p_send.c @@ -0,0 +1,147 @@ +// +// This file is part of Dire Wolf, an amateur radio packet TNC. +// +// Copyright (C) 2021 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 . +// + +#include "direwolf.h" + +#include +#include +#include +#include + +#include "il2p.h" +#include "textcolor.h" +#include "audio.h" +#include "gen_tone.h" + + +static int number_of_bits_sent[MAX_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); + + + +/*------------------------------------------------------------- + * + * Name: il2p_send_frame + * + * Purpose: Convert frames to a stream of bits in IL2P format. + * + * Inputs: chan - Audio channel number, 0 = first. + * + * pp - Pointer to packet object. + * + * max_fec - 1 to force 16 parity symbols for each payload block. + * 0 for automatic depending on block size. + * + * polarity - 0 for normal. 1 to invert signal. + * 2 special case for testing - introduce some errors to test FEC. + * + * Outputs: Bits are shipped out by calling tone_gen_put_bit(). + * + * Returns: Number of bits sent including + * - Preamble (01010101...) + * - 3 byte Sync Word. + * - 15 bytes for Header. + * - Optional payload. + * The required time can be calculated by dividing this + * number by the transmit rate of bits/sec. + * -1 is returned for failure. + * + * Description: Generate an IL2P encoded frame. + * + * Assumptions: It is assumed that the tone_gen module has been + * properly initialized so that bits sent with + * tone_gen_put_bit() are processed correctly. + * + * Errors: Return -1 for error. Probably frame too large. + * + * Note: Inconsistency here. ax25 version has just a byte array + * and length going in. Here we need the full packet object. + * + *--------------------------------------------------------------*/ + +int il2p_send_frame (int chan, packet_t pp, int max_fec, int polarity) +{ + unsigned char encoded[IL2P_MAX_PACKET_SIZE]; + + encoded[0] = ( IL2P_SYNC_WORD >> 16 ) & 0xff; + encoded[1] = ( IL2P_SYNC_WORD >> 8 ) & 0xff; + encoded[2] = ( IL2P_SYNC_WORD ) & 0xff; + + int elen = il2p_encode_frame (pp, max_fec, encoded + IL2P_SYNC_WORD_SIZE); + if (elen <= 0) { + text_color_set(DW_COLOR_ERROR); + dw_printf ("IL2P: Unable to encode frame into IL2P.\n"); + return (-1); + } + + elen += IL2P_SYNC_WORD_SIZE; + + number_of_bits_sent[chan] = 0; + + if (il2p_get_debug() >= 1) { + text_color_set(DW_COLOR_DEBUG); + dw_printf ("IL2P frame, max_fec = %d, %d encoded bytes total\n", max_fec, elen); + fx_hex_dump (encoded, elen); + } + + // Clobber some bytes for testing. + if (polarity >= 2) { + for (int j = 10; j < elen; j+=100) { + encoded[j] = ~ encoded[j]; + } + } + + // Send bits to modulator. + + static unsigned char preamble = IL2P_PREAMBLE; + + send_bytes (chan, &preamble, 1, polarity); + send_bytes (chan, encoded, elen, polarity); + + return (number_of_bits_sent[chan]); +} + + +static void send_bytes (int chan, unsigned char *b, int count, int polarity) +{ + for (int j = 0; j < count; j++) { + unsigned int x = b[j]; + for (int k = 0; k < 8; k++) { + send_bit (chan, (x & 0x80) != 0, polarity); + x <<= 1; + } + } +} + +// NRZI would be applied for AX.25 but IL2P does not use it. +// However we do have an option to invert the signal. +// The direwolf receive implementation will automatically compensate +// for either polarity but other implementations might not. + +static void send_bit (int chan, int b, int polarity) +{ + tone_gen_put_bit (chan, (b ^ polarity) & 1); + number_of_bits_sent[chan]++; +} + + + +// end il2p_send.c \ No newline at end of file diff --git a/src/il2p_test.c b/src/il2p_test.c new file mode 100644 index 00000000..17995259 --- /dev/null +++ b/src/il2p_test.c @@ -0,0 +1,977 @@ +// +// This file is part of Dire Wolf, an amateur radio packet TNC. +// +// Copyright (C) 2021 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 . +// + +#include "direwolf.h" + +#include +#include +#include +#include + +#include "textcolor.h" +#include "il2p.h" +#include "ax25_pad.h" +#include "ax25_pad2.h" +#include "multi_modem.h" + + +static void test_scramble(void); +static void test_rs(void); +static void test_payload(void); +static void test_example_headers(void); +static void all_frame_types(void); +static void test_serdes(void); +static void decode_bitstream(void); + +/*------------------------------------------------------------- + * + * Name: il2p_test.c + * + * Purpose: Unit tests for IL2P protocol functions. + * + * Errors: Die if anything goes wrong. + * + *--------------------------------------------------------------*/ + + +int main () +{ + int enable_color = 1; + text_color_init (enable_color); + + int enable_debug_out = 0; + il2p_init(enable_debug_out); + + text_color_set(DW_COLOR_INFO); + dw_printf ("Begin IL2P unit tests.\n"); + +// These start simple and later complex cases build upon earlier successes. + +// Test scramble and descramble. + + test_scramble(); + +// Test Reed Solomon error correction. + + test_rs(); + +// Test payload functions. + + test_payload(); + +// Try encoding the example headers in the protocol spec. + + test_example_headers(); + +// Convert all of the AX.25 frame types to IL2P and back again. + + all_frame_types(); + +// Use same serialize / deserialize functions used on the air. + + test_serdes (); + +// Decode bitstream from demodulator if data file is available. +// TODO: Very large info parts. Appropriate error if too long. +// TODO: More than 2 addresses. + + decode_bitstream(); + + text_color_set(DW_COLOR_REC); + dw_printf ("\n----------\n\n"); + dw_printf ("\nSUCCESS!\n"); + + return (EXIT_SUCCESS); +} + + + +///////////////////////////////////////////////////////////////////////////////////////////// +// +// Test scrambling and descrambling. +// +///////////////////////////////////////////////////////////////////////////////////////////// + +static void test_scramble(void) +{ + text_color_set(DW_COLOR_INFO); + dw_printf ("Test scrambling...\n"); + +// First an example from the protocol specification to make sure I'm compatible. + + static unsigned char scramin1[] = { 0x63, 0xf1, 0x40, 0x40, 0x40, 0x00, 0x6b, 0x2b, 0x54, 0x28, 0x25, 0x2a, 0x0f }; + static unsigned char scramout1[] = { 0x6a, 0xea, 0x9c, 0xc2, 0x01, 0x11, 0xfc, 0x14, 0x1f, 0xda, 0x6e, 0xf2, 0x53 }; + unsigned char scramout[sizeof(scramin1)]; + + il2p_scramble_block (scramin1, scramout, sizeof(scramin1)); + assert (memcmp(scramout, scramout, sizeof(scramout1)) == 0); + +} // end test_scramble. + + + + +///////////////////////////////////////////////////////////////////////////////////////////// +// +// Test Reed Solomon encode/decode examples found in the protocol spec. +// The data part is scrambled but that does not matter here because. +// We are only concerned abound adding the parity and verifying. +// +///////////////////////////////////////////////////////////////////////////////////////////// + + +static void test_rs() +{ + text_color_set(DW_COLOR_INFO); + dw_printf ("Test Reed Solomon functions...\n"); + + static unsigned char example_s[] = { 0x26, 0x57, 0x4d, 0x57, 0xf1, 0x96, 0xcc, 0x85, 0x42, 0xe7, 0x24, 0xf7, 0x2e, + 0x8a, 0x97 }; + unsigned char parity_out[2]; + il2p_encode_rs (example_s, 13, 2, parity_out); + //dw_printf ("DEBUG RS encode %02x %02x\n", parity_out[0], parity_out[1]); + assert (memcmp(parity_out, example_s + 13, 2) == 0); + + + static unsigned char example_u[] = { 0x6a, 0xea, 0x9c, 0xc2, 0x01, 0x11, 0xfc, 0x14, 0x1f, 0xda, 0x6e, 0xf2, 0x53, + 0x91, 0xbd }; + il2p_encode_rs (example_u, 13, 2, parity_out); + //dw_printf ("DEBUG RS encode %02x %02x\n", parity_out[0], parity_out[1]); + assert (memcmp(parity_out, example_u + 13, 2) == 0); + + // See if we can go the other way. + + unsigned char received[15]; + unsigned char corrected[15]; + int e; + + e = il2p_decode_rs (example_s, 13, 2, corrected); + assert (e == 0); + assert (memcmp(example_s, corrected, 13) == 0); + + memcpy (received, example_s, 15); + received[0] = '?'; + e = il2p_decode_rs (received, 13, 2, corrected); + assert (e == 1); + assert (memcmp(example_s, corrected, 13) == 0); + + e = il2p_decode_rs (example_u, 13, 2, corrected); + assert (e == 0); + assert (memcmp(example_u, corrected, 13) == 0); + + memcpy (received, example_u, 15); + received[12] = '?'; + e = il2p_decode_rs (received, 13, 2, corrected); + assert (e == 1); + assert (memcmp(example_u, corrected, 13) == 0); + + received[1] = '?'; + received[2] = '?'; + e = il2p_decode_rs (received, 13, 2, corrected); + assert (e == -1); +} + + + +///////////////////////////////////////////////////////////////////////////////////////////// +// +// Test payload functions. +// +///////////////////////////////////////////////////////////////////////////////////////////// + +static void test_payload(void) +{ + text_color_set(DW_COLOR_INFO); + dw_printf ("Test payload functions...\n"); + + il2p_payload_properties_t ipp; + int e; + +// Examples in specification. + + e = il2p_payload_compute (&ipp, 100, 0); + assert (ipp.small_block_size == 100); + assert (ipp.large_block_size == 101); + assert (ipp.large_block_count == 0); + assert (ipp.small_block_count == 1); + assert (ipp.parity_symbols_per_block == 4); + + e = il2p_payload_compute (&ipp, 236, 0); + assert (ipp.small_block_size == 236); + assert (ipp.large_block_size == 237); + assert (ipp.large_block_count == 0); + assert (ipp.small_block_count == 1); + assert (ipp.parity_symbols_per_block == 8); + + e = il2p_payload_compute (&ipp, 512, 0); + assert (ipp.small_block_size == 170); + assert (ipp.large_block_size == 171); + assert (ipp.large_block_count == 2); + assert (ipp.small_block_count == 1); + assert (ipp.parity_symbols_per_block == 6); + + e = il2p_payload_compute (&ipp, 1023, 0); + assert (ipp.small_block_size == 204); + assert (ipp.large_block_size == 205); + assert (ipp.large_block_count == 3); + assert (ipp.small_block_count == 2); + assert (ipp.parity_symbols_per_block == 8); + +// Now try all possible sizes for Baseline FEC Parity. + + for (int n = 1; n <= IL2P_MAX_PAYLOAD_SIZE; n++) { + e = il2p_payload_compute (&ipp, n, 0); + //dw_printf ("bytecount=%d, smallsize=%d, largesize=%d, largecount=%d, smallcount=%d\n", n, + // ipp.small_block_size, ipp.large_block_size, + // ipp.large_block_count, ipp.small_block_count); + //fflush (stdout); + + assert (ipp.payload_block_count >= 1 && ipp.payload_block_count <= IL2P_MAX_PAYLOAD_BLOCKS); + assert (ipp.payload_block_count == ipp.small_block_count + ipp.large_block_count); + assert (ipp.small_block_count * ipp.small_block_size + + ipp.large_block_count * ipp.large_block_size == n); + assert (ipp.parity_symbols_per_block == 2 || + ipp.parity_symbols_per_block == 4 || + ipp.parity_symbols_per_block == 6 || + ipp.parity_symbols_per_block == 8); + + // Data and parity must fit in RS block size of 255. + // Size test does not apply if block count is 0. + assert (ipp.small_block_count == 0 || ipp.small_block_size + ipp.parity_symbols_per_block <= 255); + assert (ipp.large_block_count == 0 || ipp.large_block_size + ipp.parity_symbols_per_block <= 255); + } + +// All sizes for MAX FEC. + + for (int n = 1; n <= IL2P_MAX_PAYLOAD_SIZE; n++) { + e = il2p_payload_compute (&ipp, n, 1); // 1 for max fec. + //dw_printf ("bytecount=%d, smallsize=%d, largesize=%d, largecount=%d, smallcount=%d\n", n, + // ipp.small_block_size, ipp.large_block_size, + // ipp.large_block_count, ipp.small_block_count); + //fflush (stdout); + + assert (ipp.payload_block_count >= 1 && ipp.payload_block_count <= IL2P_MAX_PAYLOAD_BLOCKS); + assert (ipp.payload_block_count == ipp.small_block_count + ipp.large_block_count); + assert (ipp.small_block_count * ipp.small_block_size + + ipp.large_block_count * ipp.large_block_size == n); + assert (ipp.parity_symbols_per_block == 16); + + // Data and parity must fit in RS block size of 255. + // Size test does not apply if block count is 0. + assert (ipp.small_block_count == 0 || ipp.small_block_size + ipp.parity_symbols_per_block <= 255); + assert (ipp.large_block_count == 0 || ipp.large_block_size + ipp.parity_symbols_per_block <= 255); + } + +// Now let's try encoding payloads and extracting original again. +// This will also provide exercise for scrambling and Reed Solomon under more conditions. + + unsigned char original_payload[IL2P_MAX_PAYLOAD_SIZE]; + for (int n = 0; n < IL2P_MAX_PAYLOAD_SIZE; n++) { + original_payload[n] = n & 0xff; + } + for (int max_fec = 0; max_fec <= 1; max_fec++) { + for (int payload_length = 1; payload_length <= IL2P_MAX_PAYLOAD_SIZE; payload_length++) { + //dw_printf ("\n--------- max_fec = %d, payload_length = %d\n", max_fec, payload_length); + unsigned char encoded[IL2P_MAX_ENCODED_PAYLOAD_SIZE]; + int k = il2p_encode_payload (original_payload, payload_length, max_fec, encoded); + + //dw_printf ("payload length %d %s -> %d\n", payload_length, max_fec ? "M" : "", k); + assert (k > payload_length && k <= IL2P_MAX_ENCODED_PAYLOAD_SIZE); + + // Now extract. + + unsigned char extracted[IL2P_MAX_PAYLOAD_SIZE]; + int symbols_corrected = 0; + int e = il2p_decode_payload (encoded, payload_length, max_fec, extracted, &symbols_corrected); + //dw_printf ("e = %d, payload_length = %d\n", e, payload_length); + assert (e == payload_length); + + // if (memcmp (original_payload, extracted, payload_length) != 0) { + // dw_printf ("********** Received message not as expected. **********\n"); + // fx_hex_dump(extracted, payload_length); + // } + assert (memcmp (original_payload, extracted, payload_length) == 0); + } + } + (void)e; +} // end test_payload + + + +///////////////////////////////////////////////////////////////////////////////////////////// +// +// Test header examples found in protocol specification. +// +///////////////////////////////////////////////////////////////////////////////////////////// + +static void test_example_headers() +{ + +//----------- Example 1: AX.25 S-Frame -------------- + +// This frame sample only includes a 15 byte header, without PID field. +// Destination Callsign: ?KA2DEW-2 +// Source Callsign: ?KK4HEJ-7 +// N(R): 5 +// P/F: 1 +// C: 1 +// Control Opcode: 00 (Receive Ready) +// +// AX.25 data: +// 96 82 64 88 8a ae e4 96 96 68 90 8a 94 6f b1 +// +// IL2P Data Prior to Scrambling and RS Encoding: +// 2b a1 12 24 25 77 6b 2b 54 68 25 2a 27 +// +// IL2P Data After Scrambling and RS Encoding: +// 26 57 4d 57 f1 96 cc 85 42 e7 24 f7 2e 8a 97 + + text_color_set(DW_COLOR_INFO); + dw_printf ("Example 1: AX.25 S-Frame...\n"); + + static unsigned char example1[] = {0x96, 0x82, 0x64, 0x88, 0x8a, 0xae, 0xe4, 0x96, 0x96, 0x68, 0x90, 0x8a, 0x94, 0x6f, 0xb1}; + static unsigned char header1[] = {0x2b, 0xa1, 0x12, 0x24, 0x25, 0x77, 0x6b, 0x2b, 0x54, 0x68, 0x25, 0x2a, 0x27 }; + unsigned char header[IL2P_HEADER_SIZE]; + unsigned char sresult[32]; + memset (header, 0, sizeof(header)); + memset (sresult, 0, sizeof(sresult)); + unsigned char check[2]; + alevel_t alevel; + memset(&alevel, 0, sizeof(alevel)); + + packet_t pp = ax25_from_frame (example1, sizeof(example1), alevel); + assert (pp != NULL); + int e; + e = il2p_type_1_header (pp, 0, header); + assert (e == 0); + ax25_delete(pp); + + //dw_printf ("Example 1 header:\n"); + //for (int i = 0 ; i < sizeof(header); i++) { + // dw_printf (" %02x", header[i]); + //} + ///dw_printf ("\n"); + + assert (memcmp(header, header1, sizeof(header)) == 0); + + il2p_scramble_block (header, sresult, 13); + //dw_printf ("Expect scrambled 26 57 4d 57 f1 96 cc 85 42 e7 24 f7 2e\n"); + //for (int i = 0 ; i < sizeof(sresult); i++) { + // dw_printf (" %02x", sresult[i]); + //} + //dw_printf ("\n"); + + il2p_encode_rs (sresult, 13, 2, check); + + //dw_printf ("expect checksum = 8a 97\n"); + //dw_printf ("check = "); + //for (int i = 0 ; i < sizeof(check); i++) { + // dw_printf (" %02x", check[i]); + //} + //dw_printf ("\n"); + assert (check[0] == 0x8a); + assert (check[1] == 0x97); + +// Can we go from IL2P back to AX.25? + + pp = il2p_decode_header_type_1 (header, 0); + assert (pp != NULL); + + char dst_addr[AX25_MAX_ADDR_LEN]; + char src_addr[AX25_MAX_ADDR_LEN]; + + ax25_get_addr_with_ssid (pp, AX25_DESTINATION, dst_addr); + ax25_get_addr_with_ssid (pp, AX25_SOURCE, src_addr); + + 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 (pp, &cr, description, &pf, &nr, &ns); + (void)frame_type; +#if 1 + dw_printf ("%s(): %s>%s: %s\n", __func__, src_addr, dst_addr, description); +#endif +// TODO: compare binary. + ax25_delete (pp); + + dw_printf ("Example 1 header OK\n"); + + +// -------------- Example 2 - UI frame, no info part ------------------ + +// This is an AX.25 Unnumbered Information frame, such as APRS. +// Destination Callsign: ?CQ -0 +// Source Callsign: ?KK4HEJ-15 +// P/F: 0 +// C: 0 +// Control Opcode: 3 Unnumbered Information +// PID: 0xF0 No L3 +// +// AX.25 Data: +// 86 a2 40 40 40 40 60 96 96 68 90 8a 94 7f 03 f0 +// +// IL2P Data Prior to Scrambling and RS Encoding: +// 63 f1 40 40 40 00 6b 2b 54 28 25 2a 0f +// +// IL2P Data After Scrambling and RS Encoding: +// 6a ea 9c c2 01 11 fc 14 1f da 6e f2 53 91 bd + + + //dw_printf ("---------- example 2 ------------\n"); + static unsigned char example2[] = { 0x86, 0xa2, 0x40, 0x40, 0x40, 0x40, 0x60, 0x96, 0x96, 0x68, 0x90, 0x8a, 0x94, 0x7f, 0x03, 0xf0 }; + static unsigned char header2[] = { 0x63, 0xf1, 0x40, 0x40, 0x40, 0x00, 0x6b, 0x2b, 0x54, 0x28, 0x25, 0x2a, 0x0f }; + memset (header, 0, sizeof(header)); + memset (sresult, 0, sizeof(sresult)); + memset(&alevel, 0, sizeof(alevel)); + + pp = ax25_from_frame (example2, sizeof(example2), alevel); + assert (pp != NULL); + e = il2p_type_1_header (pp, 0, header); + assert (e == 0); + ax25_delete(pp); + + //dw_printf ("Example 2 header:\n"); + //for (int i = 0 ; i < sizeof(header); i++) { + // dw_printf (" %02x", header[i]); + //} + //dw_printf ("\n"); + + assert (memcmp(header, header2, sizeof(header2)) == 0); + + il2p_scramble_block (header, sresult, 13); + //dw_printf ("Expect scrambled 6a ea 9c c2 01 11 fc 14 1f da 6e f2 53\n"); + //for (int i = 0 ; i < sizeof(sresult); i++) { + // dw_printf (" %02x", sresult[i]); + //} + //dw_printf ("\n"); + + il2p_encode_rs (sresult, 13, 2, check); + + //dw_printf ("expect checksum = 91 bd\n"); + //dw_printf ("check = "); + //for (int i = 0 ; i < sizeof(check); i++) { + // dw_printf (" %02x", check[i]); + //} + //dw_printf ("\n"); + assert (check[0] == 0x91); + assert (check[1] == 0xbd); + +// Can we go from IL2P back to AX.25? + + pp = il2p_decode_header_type_1 (header, 0); + assert (pp != NULL); + + ax25_get_addr_with_ssid (pp, AX25_DESTINATION, dst_addr); + ax25_get_addr_with_ssid (pp, AX25_SOURCE, src_addr); + + frame_type = ax25_frame_type (pp, &cr, description, &pf, &nr, &ns); + (void)frame_type; +#if 1 + dw_printf ("%s(): %s>%s: %s\n", __func__, src_addr, dst_addr, description); +#endif +// TODO: compare binary. + + ax25_delete (pp); +// TODO: more examples + + dw_printf ("Example 2 header OK\n"); + + +// -------------- Example 3 - I Frame ------------------ + +// This is an AX.25 I-Frame with 9 bytes of information after the 16 byte header. +// +// Destination Callsign: ?KA2DEW-2 +// Source Callsign: ?KK4HEJ-2 +// P/F: 1 +// C: 1 +// N(R): 5 +// N(S): 4 +// AX.25 PID: 0xCF TheNET +// IL2P Payload Byte Count: 9 +// +// AX.25 Data: +// 96 82 64 88 8a ae e4 96 96 68 90 8a 94 65 b8 cf 30 31 32 33 34 35 36 37 38 +// +// IL2P Scrambled and Encoded Data: +// 26 13 6d 02 8c fe fb e8 aa 94 2d 6a 34 43 35 3c 69 9f 0c 75 5a 38 a1 7f f3 fc + + + //dw_printf ("---------- example 3 ------------\n"); + static unsigned char example3[] = { 0x96, 0x82, 0x64, 0x88, 0x8a, 0xae, 0xe4, 0x96, 0x96, 0x68, 0x90, 0x8a, 0x94, 0x65, 0xb8, 0xcf, 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38 }; + static unsigned char header3[] = { 0x2b, 0xe1, 0x52, 0x64, 0x25, 0x77, 0x6b, 0x2b, 0xd4, 0x68, 0x25, 0xaa, 0x22 }; + static unsigned char complete3[] = { 0x26, 0x13, 0x6d, 0x02, 0x8c, 0xfe, 0xfb, 0xe8, 0xaa, 0x94, 0x2d, 0x6a, 0x34, 0x43, 0x35, 0x3c, 0x69, 0x9f, 0x0c, 0x75, 0x5a, 0x38, 0xa1, 0x7f, 0xf3, 0xfc }; + memset (header, 0, sizeof(header)); + memset (sresult, 0, sizeof(sresult)); + memset(&alevel, 0, sizeof(alevel)); + + pp = ax25_from_frame (example3, sizeof(example3), alevel); + assert (pp != NULL); + e = il2p_type_1_header (pp, 0, header); + assert (e == 9); + ax25_delete(pp); + + //dw_printf ("Example 3 header:\n"); + //for (int i = 0 ; i < sizeof(header); i++) { + // dw_printf (" %02x", header[i]); + //} + //dw_printf ("\n"); + + assert (memcmp(header, header3, sizeof(header)) == 0); + + il2p_scramble_block (header, sresult, 13); + //dw_printf ("Expect scrambled 26 13 6d 02 8c fe fb e8 aa 94 2d 6a 34\n"); + //for (int i = 0 ; i < sizeof(sresult); i++) { + // dw_printf (" %02x", sresult[i]); + //} + //dw_printf ("\n"); + + il2p_encode_rs (sresult, 13, 2, check); + + //dw_printf ("expect checksum = 43 35\n"); + //dw_printf ("check = "); + //for (int i = 0 ; i < sizeof(check); i++) { + // dw_printf (" %02x", check[i]); + //} + //dw_printf ("\n"); + + assert (check[0] == 0x43); + assert (check[1] == 0x35); + + // That was only the header. We will get to the info part in a later test. + +// Can we go from IL2P back to AX.25? + + pp = il2p_decode_header_type_1 (header, 0); + assert (pp != NULL); + + ax25_get_addr_with_ssid (pp, AX25_DESTINATION, dst_addr); + ax25_get_addr_with_ssid (pp, AX25_SOURCE, src_addr); + + frame_type = ax25_frame_type (pp, &cr, description, &pf, &nr, &ns); + (void)frame_type; +#if 1 + dw_printf ("%s(): %s>%s: %s\n", __func__, src_addr, dst_addr, description); +#endif +// TODO: compare binary. + + ax25_delete (pp); + dw_printf ("Example 3 header OK\n"); + +// Example 3 again, this time the Information part is included. + + pp = ax25_from_frame (example3, sizeof(example3), alevel); + assert (pp != NULL); + + int max_fec = 0; + unsigned char iout[IL2P_MAX_PACKET_SIZE]; + e = il2p_encode_frame (pp, max_fec, iout); + + //dw_printf ("expected for example 3:\n"); + //fx_hex_dump(complete3, sizeof(complete3)); + //dw_printf ("actual result for example 3:\n"); + //fx_hex_dump(iout, e); + // Does it match the example in the protocol spec? + assert (e == sizeof(complete3)); + assert (memcmp(iout, complete3, sizeof(complete3)) == 0); + ax25_delete (pp); + + dw_printf ("Example 3 with info OK\n"); + +} // end test_example_headers + + + +///////////////////////////////////////////////////////////////////////////////////////////// +// +// Test all of the frame types. +// +// Encode to IL2P format, decode, and verify that the result is the same as the original. +// +///////////////////////////////////////////////////////////////////////////////////////////// + + +static void enc_dec_compare (packet_t pp1) +{ + for (int max_fec = 0; max_fec <= 1; max_fec++) { + + unsigned char encoded[IL2P_MAX_PACKET_SIZE]; + int enc_len; + enc_len = il2p_encode_frame (pp1, max_fec, encoded); + assert (enc_len >= 0); + + packet_t pp2; + pp2 = il2p_decode_frame (encoded); + assert (pp2 != NULL); + +// Is it the same after encoding to IL2P and then decoding? + + int len1 = ax25_get_frame_len (pp1); + unsigned char *data1 = ax25_get_frame_data_ptr (pp1); + + int len2 = ax25_get_frame_len (pp2); + unsigned char *data2 = ax25_get_frame_data_ptr (pp2); + + if (len1 != len2 || memcmp(data1, data2, len1) != 0) { + + dw_printf ("\nEncode/Decode Error. Original:\n"); + ax25_hex_dump (pp1); + + dw_printf ("IL2P encoded as:\n"); + fx_hex_dump(encoded, enc_len); + + dw_printf ("Got turned into this:\n"); + ax25_hex_dump (pp2); + } + + assert (len1 == len2 && memcmp(data1, data2, len1) == 0); + + ax25_delete (pp2); + } +} + +static void all_frame_types(void) +{ + char addrs[AX25_MAX_ADDRS][AX25_MAX_ADDR_LEN]; + int num_addr = 2; + cmdres_t cr; + ax25_frame_type_t ftype; + int pf = 0; + int pid = 0xf0; + int modulo; + int nr, ns; + unsigned char *pinfo = NULL; + int info_len = 0; + packet_t pp; + + strcpy (addrs[0], "W2UB"); + strcpy (addrs[1], "WB2OSZ-12"); + num_addr = 2; + + text_color_set(DW_COLOR_INFO); + dw_printf ("Testing all frame types.\n"); + +/* U frame */ + + dw_printf ("\nU frames...\n"); + + for (ftype = frame_type_U_SABME; ftype <= frame_type_U_TEST; ftype++) { + + for (pf = 0; pf <= 1; pf++) { + + int cmin = 0, cmax = 1; + + switch (ftype) { + // 0 = response, 1 = command + case frame_type_U_SABME: cmin = 1; cmax = 1; break; + case frame_type_U_SABM: cmin = 1; cmax = 1; break; + case frame_type_U_DISC: cmin = 1; cmax = 1; break; + case frame_type_U_DM: cmin = 0; cmax = 0; break; + case frame_type_U_UA: cmin = 0; cmax = 0; break; + case frame_type_U_FRMR: cmin = 0; cmax = 0; break; + case frame_type_U_UI: cmin = 0; cmax = 1; break; + case frame_type_U_XID: cmin = 0; cmax = 1; break; + case frame_type_U_TEST: cmin = 0; cmax = 1; break; + default: break; // avoid compiler warning. + } + + for (cr = cmin; cr <= cmax; cr++) { + + text_color_set(DW_COLOR_INFO); + dw_printf ("\nConstruct U frame, cr=%d, ftype=%d, pid=0x%02x\n", cr, ftype, pid); + + pp = ax25_u_frame (addrs, num_addr, cr, ftype, pf, pid, pinfo, info_len); + ax25_hex_dump (pp); + enc_dec_compare (pp); + ax25_delete (pp); + } + } + } + + +/* S frame */ + + //strcpy (addrs[2], "DIGI1-1"); + //num_addr = 3; + + dw_printf ("\nS frames...\n"); + + for (ftype = frame_type_S_RR; ftype <= frame_type_S_SREJ; ftype++) { + + for (pf = 0; pf <= 1; pf++) { + + modulo = 8; + nr = modulo / 2 + 1; + + // SREJ can only be response. + + for (cr = 0; cr <= (int)(ftype!=frame_type_S_SREJ); cr++) { + + text_color_set(DW_COLOR_INFO); + dw_printf ("\nConstruct S frame, cmd=%d, ftype=%d, pid=0x%02x\n", cr, ftype, pid); + + pp = ax25_s_frame (addrs, num_addr, cr, ftype, modulo, nr, pf, NULL, 0); + + ax25_hex_dump (pp); + enc_dec_compare (pp); + ax25_delete (pp); + } + + modulo = 128; + nr = modulo / 2 + 1; + + for (cr = 0; cr <= (int)(ftype!=frame_type_S_SREJ); cr++) { + + text_color_set(DW_COLOR_INFO); + dw_printf ("\nConstruct S frame, cmd=%d, ftype=%d, pid=0x%02x\n", cr, ftype, pid); + + pp = ax25_s_frame (addrs, num_addr, cr, ftype, modulo, nr, pf, NULL, 0); + + ax25_hex_dump (pp); + enc_dec_compare (pp); + ax25_delete (pp); + } + } + } + +/* SREJ is only S frame which can have information part. */ + + static unsigned char srej_info[] = { 1<<1, 2<<1, 3<<1, 4<<1 }; + + ftype = frame_type_S_SREJ; + for (pf = 0; pf <= 1; pf++) { + + modulo = 128; + nr = 127; + cr = cr_res; + + text_color_set(DW_COLOR_INFO); + dw_printf ("\nConstruct Multi-SREJ S frame, cmd=%d, ftype=%d, pid=0x%02x\n", cr, ftype, pid); + + pp = ax25_s_frame (addrs, num_addr, cr, ftype, modulo, nr, pf, srej_info, (int)(sizeof(srej_info))); + + ax25_hex_dump (pp); + enc_dec_compare (pp); + ax25_delete (pp); + } + + +/* I frame */ + + dw_printf ("\nI frames...\n"); + + pinfo = (unsigned char*)"The rain in Spain stays mainly on the plain."; + info_len = strlen((char*)pinfo); + + for (pf = 0; pf <= 1; pf++) { + + modulo = 8; + nr = 0x55 & (modulo - 1); + ns = 0xaa & (modulo - 1); + + for (cr = 1; cr <= 1; cr++) { // can only be command + + text_color_set(DW_COLOR_INFO); + dw_printf ("\nConstruct I frame, cmd=%d, ftype=%d, pid=0x%02x\n", cr, ftype, pid); + + pp = ax25_i_frame (addrs, num_addr, cr, modulo, nr, ns, pf, pid, pinfo, info_len); + + ax25_hex_dump (pp); + enc_dec_compare (pp); + ax25_delete (pp); + } + + modulo = 128; + nr = 0x55 & (modulo - 1); + ns = 0xaa & (modulo - 1); + + for (cr = 1; cr <= 1; cr++) { + + text_color_set(DW_COLOR_INFO); + dw_printf ("\nConstruct I frame, cmd=%d, ftype=%d, pid=0x%02x\n", cr, ftype, pid); + + pp = ax25_i_frame (addrs, num_addr, cr, modulo, nr, ns, pf, pid, pinfo, info_len); + + ax25_hex_dump (pp); + enc_dec_compare (pp); + ax25_delete (pp); + } + } + +} // end all_frame_types + + +///////////////////////////////////////////////////////////////////////////////////////////// +// +// Test bitstream tapped off from demodulator. +// +// 5 frames were sent to Nino TNC and a recording was made. +// This was demodulated and the resulting bit stream saved to a file. +// +// No automatic test here - must be done manually with audio recording. +// +///////////////////////////////////////////////////////////////////////////////////////////// + +static int decoding_bitstream = 0; + +static void decode_bitstream(void) +{ + dw_printf("-----\nReading il2p-bitstream.txt if available...\n"); + + FILE *fp = fopen ("il2p-bitstream.txt", "r"); + if (fp == NULL) { + dw_printf ("Bitstream test file not available.\n"); + return; + } + + decoding_bitstream = 1; + int save_previous = il2p_get_debug(); + il2p_set_debug (1); + + int ch; + while ( (ch = fgetc(fp)) != EOF) { + + if (ch == '0' || ch == '1') { + il2p_rec_bit (0, 0, 0, ch - '0'); + } + } + fclose(fp); + il2p_set_debug (save_previous); + decoding_bitstream = 0; + +} // end decode_bitstream + + + + +///////////////////////////////////////////////////////////////////////////////////////////// +// +// Test serialize / deserialize. +// +// This uses same functions used on the air. +// +///////////////////////////////////////////////////////////////////////////////////////////// + +static char addrs2[] = "AA1AAA-1>ZZ9ZZZ-9"; +static char addrs3[] = "AA1AAA-1>ZZ9ZZZ-9,DIGI*"; +static char text[] = + "'... As I was saying, that seems to be done right - though I haven't time to look it over thoroughly just now - and that shows that there are three hundred and sixty-four days when you might get un-birthday presents -'" + "\n" + "'Certainly,' said Alice." + "\n" + "'And only one for birthday presents, you know. There's glory for you!'" + "\n" + "'I don't know what you mean by \"glory\",' Alice said." + "\n" + "Humpty Dumpty smiled contemptuously. 'Of course you don't - till I tell you. I meant \"there's a nice knock-down argument for you!\"'" + "\n" + "'But \"glory\" doesn't mean \"a nice knock-down argument\",' Alice objected." + "\n" + "'When I use a word,' Humpty Dumpty said, in rather a scornful tone, 'it means just what I choose it to mean - neither more nor less.'" + "\n" + "'The question is,' said Alice, 'whether you can make words mean so many different things.'" + "\n" + "'The question is,' said Humpty Dumpty, 'which is to be master - that's all.'" + "\n" ; + + +static int rec_count = -1; // disable deserialized packet test. +static int polarity = 0; + +static void test_serdes (void) +{ + text_color_set(DW_COLOR_INFO); + dw_printf ("\nTest serialize / deserialize...\n"); + rec_count = 0; + + int max_fec = 1; + + // try combinations of header type, max_fec, polarity, errors. + + for (int hdr_type = 0; hdr_type <= 1; hdr_type++) { + char packet[1024]; + snprintf (packet, sizeof(packet), "%s:%s", hdr_type ? addrs2 : addrs3, text); + packet_t pp = ax25_from_text (packet, 1); + assert (pp != NULL); + + int chan = 0; + + + for (max_fec = 0; max_fec <= 1; max_fec++) { + for (polarity = 0; polarity <= 2; polarity++) { // 2 means throw in some errors. + int num_bits_sent = il2p_send_frame (chan, pp, max_fec, polarity); + dw_printf ("%d bits sent.\n", num_bits_sent); + + // Need extra bit at end to flush out state machine. + il2p_rec_bit (0, 0, 0, 0); + } + } + ax25_delete(pp); + } + + dw_printf ("Serdes receive count = %d\n", rec_count); + assert (rec_count == 12); + rec_count = -1; // disable deserialized packet test. +} + + +// Serializing calls this which then simulates the demodulator output. + +void tone_gen_put_bit (int chan, int data) +{ + il2p_rec_bit (chan, 0, 0, data); +} + +// This is called when a complete frame has been deserialized. + +void multi_modem_process_rec_packet (int chan, int subchan, int slice, packet_t pp, alevel_t alevel, retry_t retries, fec_type_t fec_type) +{ + if (rec_count < 0) return; // Skip check before serdes test. + + rec_count++; + + // Does it have the the expected content? + + unsigned char *pinfo; + int len = ax25_get_info(pp, &pinfo); + assert (len == strlen(text)); + assert (strcmp(text, (char*)pinfo) == 0); + + dw_printf ("Number of symbols corrected: %d\n", retries); + if (polarity == 2) { // expecting errors corrected. + assert (retries == 10); + } + else { // should be no errors. + assert (retries == 0); + } + + ax25_delete (pp); +} + +alevel_t demod_get_audio_level (int chan, int subchan) +{ + alevel_t alevel; + memset (&alevel, 0, sizeof(alevel)); + return (alevel); +} + +// end il2p_test.c \ No newline at end of file diff --git a/src/kiss.c b/src/kiss.c index 76cb322d..f93cb94c 100644 --- a/src/kiss.c +++ b/src/kiss.c @@ -94,7 +94,7 @@ void kisspt_set_debug (int n) return; } -void kisspt_send_rec_packet (int chan, int kiss_cmd, unsigned char *fbuf, int flen, int client) +void kisspt_send_rec_packet (int chan, int kiss_cmd, unsigned char *fbuf, int flen, struct kissport_status_s *kps, int client) { return; } @@ -374,7 +374,7 @@ static int kisspt_open_pt (void) * flen - Length of raw received frame not including the FCS * or -1 for a text string. * - * client - Not used for pseudo terminal. + * kps, client - Not used for pseudo terminal. * Here so that 3 related functions all have * the same parameter list. * @@ -385,7 +385,7 @@ static int kisspt_open_pt (void) *--------------------------------------------------------------------*/ -void kisspt_send_rec_packet (int chan, int kiss_cmd, unsigned char *fbuf, int flen, int client) +void kisspt_send_rec_packet (int chan, int kiss_cmd, unsigned char *fbuf, int flen, struct kissport_status_s *kps, int client) { unsigned char kiss_buff[2 * AX25_MAX_PACKET_LEN + 2]; int kiss_len; @@ -465,11 +465,11 @@ void kisspt_send_rec_packet (int chan, int kiss_cmd, unsigned char *fbuf, int f * * Returns: one byte (value 0 - 255) or terminate thread on error. * - * Description: There is room for improvment here. Reading one byte + * Description: There is room for improvement here. Reading one byte * at a time is inefficient. We could read a large block * into a local buffer and return a byte from that most of the time. * Is it worth the effort? I don't know. With GHz processors and - * the low data rate here it might not make a noticable difference. + * the low data rate here it might not make a noticeable difference. * *--------------------------------------------------------------------*/ @@ -591,7 +591,7 @@ static void * kisspt_listen_thread (void *arg) while (1) { ch = kisspt_get(); - kiss_rec_byte (&kf, ch, kisspt_debug, -1, kisspt_send_rec_packet); + kiss_rec_byte (&kf, ch, kisspt_debug, NULL, -1, kisspt_send_rec_packet); } return (void *) 0; /* Unreachable but avoids compiler warning. */ diff --git a/src/kiss.h b/src/kiss.h index 47874405..1dc40daf 100644 --- a/src/kiss.h +++ b/src/kiss.h @@ -10,12 +10,13 @@ #include "config.h" - +#include "kiss_frame.h" // for struct kissport_status_s void kisspt_init (struct misc_config_s *misc_config); -void kisspt_send_rec_packet (int chan, int kiss_cmd, unsigned char *fbuf, int flen, int client); +void kisspt_send_rec_packet (int chan, int kiss_cmd, unsigned char *fbuf, int flen, + struct kissport_status_s *notused1, int notused2); void kisspt_set_debug (int n); diff --git a/src/kiss_frame.c b/src/kiss_frame.c index c0876357..aa581dd2 100644 --- a/src/kiss_frame.c +++ b/src/kiss_frame.c @@ -1,7 +1,7 @@ // // This file is part of Dire Wolf, an amateur radio packet TNC. // -// Copyright (C) 2013, 2014, 2017 John Langner, WB2OSZ +// Copyright (C) 2013, 2014, 2017, 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 @@ -40,7 +40,8 @@ * * The first byte of the frame contains: * - * * port number (radio channel) in upper nybble. + * * radio channel in upper nybble. + * (KISS doc uses "port" but I don't like that because it has too many meanings.) * * command in lower nybble. * * @@ -64,7 +65,7 @@ * _6 SetHardware TNC specific. * * _C XKISS extension - not supported. - * _E XKISS extention - not supported. + * _E XKISS extension - not supported. * * FF Return Exit KISS mode. Ignored. * @@ -107,6 +108,7 @@ void hex_dump (unsigned char *p, int len) offset = 0; while (len > 0) { n = len < 16 ? len : 16; + // FIXME: Is there some reason not to use dw_printf here? printf (" %03x: ", offset); for (i=0; inoise)) == 0 || strcasecmp("reset\r", (char*)(kf->noise)) == 0) { // first 2 parameters don't matter when length is -1 indicating text. - (*sendfun) (0, 0, (unsigned char *)"\xc0\xc0", -1, client); + (*sendfun) (0, 0, (unsigned char *)"\xc0\xc0", -1, kps, client); } else { - (*sendfun) (0, 0, (unsigned char *)"\r\ncmd:", -1, client); + (*sendfun) (0, 0, (unsigned char *)"\r\ncmd:", -1, kps, client); } #endif kf->noise_len = 0; @@ -469,7 +476,7 @@ void kiss_rec_byte (kiss_frame_t *kf, unsigned char ch, int debug, int client, v hex_dump (unwrapped+1, ulen-1); } - kiss_process_msg (unwrapped, ulen, debug, client, sendfun); + kiss_process_msg (unwrapped, ulen, debug, kps, client, sendfun); kf->state = KS_SEARCHING; return; @@ -506,6 +513,9 @@ void kiss_rec_byte (kiss_frame_t *kf, unsigned char ch, int debug, int client, v * * debug - Debug option is selected. * + * kps - Used only for TCP KISS. + * Should be NULL for pseudo terminal and serial port. + * * client - Client app number for TCP KISS. * Should be -1 for pseudo termal and serial port. * @@ -519,28 +529,40 @@ void kiss_rec_byte (kiss_frame_t *kf, unsigned char ch, int debug, int client, v // Some functions are only for the TNC end. // Other functions are suitble for both TNC and client app. -// This is used only by the TNC sided. +// This is used only by the TNC side. -void kiss_process_msg (unsigned char *kiss_msg, int kiss_len, int debug, int client, void (*sendfun)(int,int,unsigned char*,int,int)) +void kiss_process_msg (unsigned char *kiss_msg, int kiss_len, int debug, struct kissport_status_s *kps, int client, + void (*sendfun)(int chan, int kiss_cmd, unsigned char *fbuf, int flen, struct kissport_status_s *kps, int client)) { - int port; // Should rename to chan because that's what we use everywhere else. + int chan; int cmd; - packet_t pp; alevel_t alevel; - port = (kiss_msg[0] >> 4) & 0xf; +// New in 1.7: +// We can have KISS TCP ports which convey only a single radio channel. +// This is to allow operation by applications which only know how to talk to single radio TNCs. + + if (kps != NULL && kps->chan != -1) { + // Ignore channel from KISS and substitute radio channel for that KISS TCP port. + chan = kps->chan; + } + else { + // Normal case of getting radio channel from the KISS frame. + chan = (kiss_msg[0] >> 4) & 0xf; + } cmd = kiss_msg[0] & 0xf; switch (cmd) { case KISS_CMD_DATA_FRAME: /* 0 = Data Frame */ - if (client >= 0) { - kissnet_copy (kiss_msg, kiss_len, port, cmd, client); - } + // kissnet_copy clobbers first byte but we don't care + // because we have already determined channel and command. + + kissnet_copy (kiss_msg, kiss_len, chan, cmd, kps, client); /* Note July 2017: There is a variant of of KISS, called SMACK, that assumes */ - /* a TNC can never have more than 8 ports. http://symek.de/g/smack.html */ + /* a TNC can never have more than 8 channels. http://symek.de/g/smack.html */ /* It uses the MSB to indicate that a checksum is added. I wonder if this */ /* is why we sometimes hear about a request to transmit on channel 8. */ /* Should we have a message that asks the user if SMACK is being used, */ @@ -580,20 +602,23 @@ void kiss_process_msg (unsigned char *kiss_msg, int kiss_len, int debug, int cli // Would it make sense to implement SMACK? I don't think so. // Adding a checksum to the KISS data offers no benefit because it is very reliable. -// It violates the original protocol specification which states that 16 ports (radio channels) are possible. +// It violates the original protocol specification which states that 16 radio channels are possible. +// (Some times the term 'port' is used but I try to use 'channel' all the time because 'port' +// has too many other meanings. Serial port, TCP port, ...) // SMACK imposes a limit of 8. That limit might have been OK back in 1991 but not now. // There are people using more than 8 radio channels (using SDR not traditional radios) with direwolf. - /* Verify that the port (channel) number is valid. */ + /* Verify that the radio channel number is valid. */ /* Any sort of medium should be OK here. */ - if (port < 0 || port >= MAX_CHANS || save_audio_config_p->achan[port].medium == MEDIUM_NONE) { + if ((chan < 0 || chan >= MAX_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", port); + dw_printf ("Invalid transmit channel %d from KISS client app.\n", chan); dw_printf ("\n"); dw_printf ("Are you using AX.25 for Linux? It might be trying to use a modified\n"); - dw_printf ("version of KISS which uses the port (channel) field differently than the\n"); + dw_printf ("version of KISS which uses the channel field differently than the\n"); dw_printf ("original KISS protocol specification. The solution might be to use\n"); dw_printf ("a command like \"kissparms -c 1 -p radio\" to set CRC none mode.\n"); dw_printf ("Another way of doing this is pre-loading the \"kiss\" kernel module with CRC disabled:\n"); @@ -606,7 +631,7 @@ void kiss_process_msg (unsigned char *kiss_msg, int kiss_len, int debug, int cli } memset (&alevel, 0xff, sizeof(alevel)); - pp = ax25_from_frame (kiss_msg+1, kiss_len-1, alevel); + packet_t pp = ax25_from_frame (kiss_msg+1, kiss_len-1, alevel); if (pp == NULL) { text_color_set(DW_COLOR_ERROR); dw_printf ("ERROR - Invalid KISS data frame from client app.\n"); @@ -621,10 +646,10 @@ void kiss_process_msg (unsigned char *kiss_msg, int kiss_len, int debug, int cli if (ax25_get_num_repeaters(pp) >= 1 && ax25_get_h(pp,AX25_REPEATER_1)) { - tq_append (port, TQ_PRIO_0_HI, pp); + tq_append (chan, TQ_PRIO_0_HI, pp); } else { - tq_append (port, TQ_PRIO_1_LO, pp); + tq_append (chan, TQ_PRIO_1_LO, pp); } } break; @@ -637,13 +662,13 @@ void kiss_process_msg (unsigned char *kiss_msg, int kiss_len, int debug, int cli return; } text_color_set(DW_COLOR_INFO); - dw_printf ("KISS protocol set TXDELAY = %d (*10mS units = %d mS), port %d\n", kiss_msg[1], kiss_msg[1] * 10, port); + 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) { 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"); } - xmit_set_txdelay (port, kiss_msg[1]); + xmit_set_txdelay (chan, kiss_msg[1]); break; case KISS_CMD_PERSISTENCE: /* 2 = Persistence */ @@ -654,13 +679,13 @@ void kiss_process_msg (unsigned char *kiss_msg, int kiss_len, int debug, int cli return; } text_color_set(DW_COLOR_INFO); - dw_printf ("KISS protocol set Persistence = %d, port %d\n", kiss_msg[1], port); + dw_printf ("KISS protocol set Persistence = %d, chan %d\n", kiss_msg[1], chan); 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"); } - xmit_set_persist (port, kiss_msg[1]); + xmit_set_persist (chan, kiss_msg[1]); break; case KISS_CMD_SLOTTIME: /* 3 = SlotTime */ @@ -671,13 +696,13 @@ void kiss_process_msg (unsigned char *kiss_msg, int kiss_len, int debug, int cli return; } text_color_set(DW_COLOR_INFO); - dw_printf ("KISS protocol set SlotTime = %d (*10mS units = %d mS), port %d\n", kiss_msg[1], kiss_msg[1] * 10, port); + dw_printf ("KISS protocol set SlotTime = %d (*10mS units = %d mS), chan %d\n", kiss_msg[1], kiss_msg[1] * 10, chan); 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"); } - xmit_set_slottime (port, kiss_msg[1]); + xmit_set_slottime (chan, kiss_msg[1]); break; case KISS_CMD_TXTAIL: /* 4 = TXtail */ @@ -688,13 +713,13 @@ void kiss_process_msg (unsigned char *kiss_msg, int kiss_len, int debug, int cli return; } text_color_set(DW_COLOR_INFO); - dw_printf ("KISS protocol set TXtail = %d (*10mS units = %d mS), port %d\n", kiss_msg[1], kiss_msg[1] * 10, port); + 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) { 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"); } - xmit_set_txtail (port, kiss_msg[1]); + xmit_set_txtail (chan, kiss_msg[1]); break; case KISS_CMD_FULLDUPLEX: /* 5 = FullDuplex */ @@ -705,8 +730,8 @@ void kiss_process_msg (unsigned char *kiss_msg, int kiss_len, int debug, int cli return; } text_color_set(DW_COLOR_INFO); - dw_printf ("KISS protocol set FullDuplex = %d, port %d\n", kiss_msg[1], port); - xmit_set_fulldup (port, kiss_msg[1]); + dw_printf ("KISS protocol set FullDuplex = %d, chan %d\n", kiss_msg[1], chan); + xmit_set_fulldup (chan, kiss_msg[1]); break; case KISS_CMD_SET_HARDWARE: /* 6 = TNC specific */ @@ -718,11 +743,11 @@ void kiss_process_msg (unsigned char *kiss_msg, int kiss_len, int debug, int cli } kiss_msg[kiss_len] = '\0'; text_color_set(DW_COLOR_INFO); - dw_printf ("KISS protocol set hardware \"%s\", port %d\n", (char*)(kiss_msg+1), port); - kiss_set_hardware (port, (char*)(kiss_msg+1), debug, client, sendfun); + dw_printf ("KISS protocol set hardware \"%s\", chan %d\n", (char*)(kiss_msg+1), chan); + kiss_set_hardware (chan, (char*)(kiss_msg+1), debug, kps, client, sendfun); break; - case KISS_CMD_END_KISS: /* 15 = End KISS mode, port should be 15. */ + case KISS_CMD_END_KISS: /* 15 = End KISS mode, channel should be 15. */ /* Ignore it. */ text_color_set(DW_COLOR_INFO); dw_printf ("KISS protocol end KISS mode - Ignored.\n"); @@ -842,7 +867,8 @@ void kiss_process_msg (unsigned char *kiss_msg, int kiss_len, int debug, int cli #ifndef KISSUTIL -static void kiss_set_hardware (int chan, char *command, int debug, int client, void (*sendfun)(int,int,unsigned char*,int,int)) +static void kiss_set_hardware (int chan, char *command, int debug, struct kissport_status_s *kps, int client, + void (*sendfun)(int chan, int kiss_cmd, unsigned char *fbuf, int flen, struct kissport_status_s *onlykps, int onlyclient)) { char *param; char response[100]; @@ -860,7 +886,7 @@ static void kiss_set_hardware (int chan, char *command, int debug, int client, v } snprintf (response, sizeof(response), "DIREWOLF %d.%d", MAJOR_VERSION, MINOR_VERSION); - (*sendfun) (chan, KISS_CMD_SET_HARDWARE, (unsigned char *)response, strlen(response), client); + (*sendfun) (chan, KISS_CMD_SET_HARDWARE, (unsigned char *)response, strlen(response), kps, client); } else if (strcmp(command, "TXBUF") == 0) { /* TXBUF - Number of bytes in transmit queue. */ @@ -872,7 +898,7 @@ static void kiss_set_hardware (int chan, char *command, int debug, int client, v int n = tq_count (chan, -1, "", "", 1); snprintf (response, sizeof(response), "TXBUF:%d", n); - (*sendfun) (chan, KISS_CMD_SET_HARDWARE, (unsigned char *)response, strlen(response), client); + (*sendfun) (chan, KISS_CMD_SET_HARDWARE, (unsigned char *)response, strlen(response), kps, client); } else { @@ -929,7 +955,7 @@ void kiss_debug_print (fromto_t fromto, char *special, unsigned char *pmsg, int p = pmsg; if (*p == FEND) p++; - dw_printf ("%s %s %s KISS client application, port %d, total length = %d\n", + dw_printf ("%s %s %s KISS client application, channel %d, total length = %d\n", prefix[(int)fromto], function[p[0] & 0xf], direction[(int)fromto], (p[0] >> 4) & 0xf, msg_len); } diff --git a/src/kiss_frame.h b/src/kiss_frame.h index 0bddf3ec..941f5c01 100644 --- a/src/kiss_frame.h +++ b/src/kiss_frame.h @@ -1,6 +1,10 @@ /* kiss_frame.h */ +#ifndef KISS_FRAME_H +#define KISS_FRAME_H + + #include "audio.h" /* for struct audio_s */ @@ -41,7 +45,7 @@ enum kiss_state_e { #define MAX_KISS_LEN 2048 /* Spec calls for at least 1024. */ - /* Might want to make it longer to accomodate */ + /* Might want to make it longer to accommodate */ /* maximum packet length. */ #define MAX_NOISE_LEN 100 @@ -61,6 +65,42 @@ typedef struct kiss_frame_s { } kiss_frame_t; +// This is used only for TCPKISS but it put in kissnet.h, +// there would be a circular dependency between the two header files. +// Each KISS TCP port has its own status block. + +struct kissport_status_s { + + struct kissport_status_s *pnext; // To next in list. + + volatile int arg2; // temp for passing second arg into + // kissnet_listen_thread + + int tcp_port; // default 8001 + + int chan; // Radio channel for this tcp port. + // -1 for all. + + // The default is a limit of 3 client applications at the same time. + // You can increase the limit by changing the line below. + // A larger number consumes more resources so don't go crazy by making it larger than needed. + // TODO: Should this be moved to direwolf.h so max number of audio devices + // client apps are in the same place? + +#define MAX_NET_CLIENTS 3 + + int client_sock[MAX_NET_CLIENTS]; + /* File descriptor for socket for */ + /* communication with client application. */ + /* Set to -1 if not connected. */ + /* (Don't use SOCKET type because it is unsigned.) */ + + kiss_frame_t kf[MAX_NET_CLIENTS]; + /* Accumulated KISS frame and state of decoder. */ +}; + + + #ifndef KISSUTIL void kiss_frame_init (struct audio_s *pa); #endif @@ -69,12 +109,18 @@ int kiss_encapsulate (unsigned char *in, int ilen, unsigned char *out); int kiss_unwrap (unsigned char *in, int ilen, unsigned char *out); -void kiss_rec_byte (kiss_frame_t *kf, unsigned char ch, int debug, int client, void (*sendfun)(int,int,unsigned char*,int,int)); +void kiss_rec_byte (kiss_frame_t *kf, unsigned char ch, int debug, struct kissport_status_s *kps, int client, + void (*sendfun)(int chan, int kiss_cmd, unsigned char *fbuf, int flen, struct kissport_status_s *onlykps, int onlyclient)); typedef enum fromto_e { FROM_CLIENT=0, TO_CLIENT=1 } fromto_t; -void kiss_process_msg (unsigned char *kiss_msg, int kiss_len, int debug, int client, void (*sendfun)(int,int,unsigned char*,int,int)); +void kiss_process_msg (unsigned char *kiss_msg, int kiss_len, int debug, struct kissport_status_s *kps, int client, + void (*sendfun)(int chan, int kiss_cmd, unsigned char *fbuf, int flen, struct kissport_status_s *onlykps, int onlyclient)); void kiss_debug_print (fromto_t fromto, char *special, unsigned char *pmsg, int msg_len); + +#endif // KISS_FRAME_H + + /* end kiss_frame.h */ diff --git a/src/kissnet.c b/src/kissnet.c index 7b4f1c4f..97094a08 100644 --- a/src/kissnet.c +++ b/src/kissnet.c @@ -1,7 +1,6 @@ // // This file is part of Dire Wolf, an amateur radio packet TNC. -// -// Copyright (C) 2011-2014, 2015, 2017 John Langner, WB2OSZ +// Copyright (C) 2011-2014, 2015, 2017, 2021 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 @@ -87,6 +86,83 @@ * *---------------------------------------------------------------*/ +/* + Separate TCP ports per radio: + +An increasing number of people are using multiple radios. +direwolf is capable of handling many radio channels and +provides cross-band repeating, etc. +Maybe a single stereo audio interface is used for 2 radios. + + +------------+ tcp 8001, all channels +Radio A -------- | | -------------------------- Application A + | direwolf | +Radio B -------- | | -------------------------- Application B + +------------+ tcp 8001, all channels + +The KISS protocol has a 4 bit field for the TNC port (which I prefer to +call channel because port has too many different meanings). +direwolf handles this fine. However, most applications were written assuming +that a TNC could only talk to a single radio. On reception, they ignore the +channel in the KISS frame. For transmit, the channel is always set to 0. + +Many people are using the work-around of two separate instances of direwolf. + + +------------+ tcp 8001, KISS ch 0 +Radio A -------- | direwolf | -------------------------- Application A + +------------+ + + +------------+ tcp 8002, KISS ch 0 +Radio B -------- | direwolf | -------------------------- Application B + +------------+ + + +Or they might be using a single application that knows how to talk to multiple +single port TNCs. But they don't know how to multiplex multiple channels +thru a single KISS stream. + + +------------+ tcp 8001, KISS ch 0 +Radio A -------- | direwolf | ------------------------ + +------------+ \ + -- Application + +------------+ tcp 8002, KISS ch 0 / +Radio B -------- | direwolf | ------------------------ + +------------+ + +Using two different instances of direwolf means more complex configuration +and loss of cross-channel digipeating. It is possible to use a stereo +audio interface but some ALSA magic is required to make it look like two +independent virtual mono interfaces. + +In version 1.7, we add the capability of multiple KISS TCP ports, each for +a single radio channel. e.g. + +KISSPORT 8001 1 +KISSPORT 8002 2 + +Now can use a single instance of direwolf. + + + +------------+ tcp 8001, KISS ch 0 +Radio A -------- | | -------------------------- Application A + | direwolf | +Radio B -------- | | -------------------------- Application B + +------------+ tcp 8002, KISS ch 0 + +When receiving, the KISS channel is set to 0. + - only radio channel 1 would be sent over tcp port 8001. + - only radio channel 2 would be sent over tcp port 8001. + +When transmitting, the KISS channel is ignored. + - frames from tcp port 8001 are transmitted on radio channel 1. + - frames from tcp port 8002 are transmitted on radio channel 2. + +Of course, you could also use an application, capable of connecting to +multiple single radio TNCs. Separate TCP ports actually go to the +same direwolf instance. + +*/ + /* * Native Windows: Use the Winsock interface. @@ -127,25 +203,6 @@ void hex_dump (unsigned char *p, int len); // This should be in a .h file. -/* - * Early on we allowed one AGW connection and one KISS TCP connection at a time. - * In version 1.1, we allowed multiple concurrent client apps to attach with the AGW network protocol. - * In Version 1.5, we do essentially the same here to allow multiple concurrent KISS TCP clients. - * The default is a limit of 3 client applications at the same time. - * You can increase the limit by changing the line below. - * A larger number consumes more resources so don't go crazy by making it larger than needed. - */ - -#define MAX_NET_CLIENTS 3 - -static int client_sock[MAX_NET_CLIENTS]; - /* File descriptor for socket for */ - /* communication with client application. */ - /* Set to -1 if not connected. */ - /* (Don't use SOCKET type because it is unsigned.) */ - -static kiss_frame_t kf[MAX_NET_CLIENTS]; - /* Accumulated KISS frame and state of decoder. */ @@ -162,6 +219,12 @@ static THREAD_F kissnet_listen_thread (void *arg); static struct misc_config_s *s_misc_config_p; + +// Each TCP port has its own status block. +// There is a variable number so use a linked list. + +static struct kissport_status_s *all_ports = NULL; + static int kiss_debug = 0; /* Print information flowing from and to client. */ void kiss_net_set_debug (int n) @@ -177,11 +240,9 @@ void kiss_net_set_debug (int n) * * Purpose: Set up a server to listen for connection requests from * an application such as Xastir or APRSIS32. + * This is called once from the main program. * * Inputs: mc->kiss_port - TCP port for server. - * Main program has default of 8000 but allows - * an alternative to be specified on the command line - * * 0 means disable. New in version 1.2. * * Outputs: @@ -193,8 +254,34 @@ void kiss_net_set_debug (int n) * *--------------------------------------------------------------------*/ +static void kissnet_init_one (struct kissport_status_s *kps); void kissnet_init (struct misc_config_s *mc) +{ + s_misc_config_p = mc; + + for (int i = 0; i < MAX_KISS_TCP_PORTS; i++) { + if (mc->kiss_port[i] != 0) { + struct kissport_status_s *kps = calloc(sizeof(struct kissport_status_s), 1); + if (kps == NULL) { + text_color_set(DW_COLOR_ERROR); + dw_printf ("FATAL ERROR: Out of memory.\n"); + exit (EXIT_FAILURE); + } + + kps->tcp_port = mc->kiss_port[i]; + kps->chan = mc->kiss_chan[i]; + kissnet_init_one (kps); + + // Add to list. + kps->pnext = all_ports; + all_ports = kps; + } + } +} + + +static void kissnet_init_one (struct kissport_status_s *kps) { int client; @@ -206,23 +293,22 @@ void kissnet_init (struct misc_config_s *mc) pthread_t cmd_listen_tid[MAX_NET_CLIENTS]; int e; #endif - s_misc_config_p = mc; - int kiss_port = mc->kiss_port; /* default 8001 but easily changed. */ + #if DEBUG text_color_set(DW_COLOR_DEBUG); - dw_printf ("kissnet_init ( %d )\n", kiss_port); + dw_printf ("kissnet_init ( tcp port %d, radio chan = %d )\n", kps->tcp_port, kps->chan); #endif for (client=0; clientclient_sock[client] = -1; + memset (&(kps->kf[client]), 0, sizeof(kps->kf[client])); } - if (kiss_port == 0) { + if (kps->tcp_port == 0) { text_color_set(DW_COLOR_INFO); dw_printf ("Disabled KISS network client port.\n"); return; @@ -232,17 +318,18 @@ void kissnet_init (struct misc_config_s *mc) * This waits for a client to connect and sets client_sock[n]. */ #if __WIN32__ - connect_listen_th = (HANDLE)_beginthreadex (NULL, 0, connect_listen_thread, (void *)(ptrdiff_t)kiss_port, 0, NULL); + connect_listen_th = (HANDLE)_beginthreadex (NULL, 0, connect_listen_thread, (void *)kps, 0, NULL); if (connect_listen_th == NULL) { text_color_set(DW_COLOR_ERROR); - dw_printf ("Could not create KISS socket connect listening thread\n"); + dw_printf ("Could not create KISS socket connect listening thread for tcp port %d, radio chan %d\n", kps->tcp_port, kps->chan); return; } #else - e = pthread_create (&connect_listen_tid, NULL, connect_listen_thread, (void *)(ptrdiff_t)kiss_port); + e = pthread_create (&connect_listen_tid, NULL, connect_listen_thread, (void *)kps); if (e != 0) { text_color_set(DW_COLOR_ERROR); perror("Could not create KISS socket connect listening thread"); + dw_printf ("for tcp port %d, radio chan %d\n", kps->tcp_port, kps->chan); return; } #endif @@ -254,15 +341,17 @@ void kissnet_init (struct misc_config_s *mc) */ for (client = 0; client < MAX_NET_CLIENTS; client++) { + kps->arg2 = client; + #if __WIN32__ - cmd_listen_th[client] = (HANDLE)_beginthreadex (NULL, 0, kissnet_listen_thread, (void*)(ptrdiff_t)client, 0, NULL); + cmd_listen_th[client] = (HANDLE)_beginthreadex (NULL, 0, kissnet_listen_thread, (void*)kps, 0, NULL); if (cmd_listen_th[client] == NULL) { text_color_set(DW_COLOR_ERROR); dw_printf ("Could not create KISS command listening thread for client %d\n", client); return; } #else - e = pthread_create (&(cmd_listen_tid[client]), NULL, kissnet_listen_thread, (void *)(ptrdiff_t)client); + e = pthread_create (&(cmd_listen_tid[client]), NULL, kissnet_listen_thread, (void *)kps); if (e != 0) { text_color_set(DW_COLOR_ERROR); dw_printf ("Could not create KISS command listening thread for client %d\n", client); @@ -271,6 +360,18 @@ void kissnet_init (struct misc_config_s *mc) return; } #endif + // Wait for new thread to get content of arg2 before reusing it for the next thread create. + + int timer = 0; + while (kps->arg2 >= 0) { + SLEEP_MS(10); + timer++; + if (timer > 100) { // 1 second - thread did not start + text_color_set(DW_COLOR_ERROR); + dw_printf ("KISS data listening thread did not start for tcp port %d, client slot %d\n", kps->tcp_port, client); + kps->arg2 = -1; // Keep moving along. + } + } } } @@ -281,9 +382,7 @@ void kissnet_init (struct misc_config_s *mc) * * Purpose: Wait for a connection request from an application. * - * Inputs: arg - TCP port for server. - * Main program has default of 8001 but allows - * an alternative to be specified on the command line + * Inputs: arg - KISS port status block. * * Outputs: client_sock - File descriptor for communicating with client app. * @@ -296,20 +395,22 @@ void kissnet_init (struct misc_config_s *mc) static THREAD_F connect_listen_thread (void *arg) { + struct kissport_status_s *kps = arg; + #if __WIN32__ struct addrinfo hints; struct addrinfo *ai = NULL; int err; - char kiss_port_str[12]; + char tcp_port_str[12]; SOCKET listen_sock; WSADATA wsadata; - snprintf (kiss_port_str, sizeof(kiss_port_str), "%d", (int)(ptrdiff_t)arg); + snprintf (tcp_port_str, sizeof(tcp_port_str), "%d", kps->tcp_port); #if DEBUG text_color_set(DW_COLOR_DEBUG); - dw_printf ("DEBUG: kissnet port = %d = '%s'\n", (int)(ptrdiff_t)arg, kiss_port_str); + dw_printf ("DEBUG: kissnet port = %d = '%s'\n", (int)(ptrdiff_t)arg, tcp_port_str); #endif err = WSAStartup (MAKEWORD(2,2), &wsadata); if (err != 0) { @@ -332,7 +433,7 @@ static THREAD_F connect_listen_thread (void *arg) hints.ai_protocol = IPPROTO_TCP; hints.ai_flags = AI_PASSIVE; - err = getaddrinfo(NULL, kiss_port_str, &hints, &ai); + err = getaddrinfo(NULL, tcp_port_str, &hints, &ai); if (err != 0) { text_color_set(DW_COLOR_ERROR); dw_printf("getaddrinfo failed: %d\n", err); @@ -350,14 +451,14 @@ static THREAD_F connect_listen_thread (void *arg) #if DEBUG text_color_set(DW_COLOR_DEBUG); - dw_printf("Binding to port %s ... \n", kiss_port_str); + dw_printf("Binding to port %s ... \n", tcp_port_str); #endif err = bind( listen_sock, ai->ai_addr, (int)ai->ai_addrlen); if (err == SOCKET_ERROR) { text_color_set(DW_COLOR_ERROR); dw_printf("Bind failed with error: %d\n", WSAGetLastError()); // TODO: provide corresponding text. - dw_printf("Some other application is probably already using port %s.\n", kiss_port_str); + dw_printf("Some other application is probably already using port %s.\n", tcp_port_str); dw_printf("Try using a different port number with KISSPORT in the configuration file.\n"); freeaddrinfo(ai); closesocket(listen_sock); @@ -369,7 +470,7 @@ static THREAD_F connect_listen_thread (void *arg) #if DEBUG text_color_set(DW_COLOR_DEBUG); - dw_printf("opened KISS socket as fd (%d) on port (%s) for stream i/o\n", listen_sock, kiss_port_str ); + dw_printf("opened KISS socket as fd (%d) on port (%s) for stream i/o\n", listen_sock, tcp_port_str ); #endif while (1) { @@ -379,7 +480,7 @@ static THREAD_F connect_listen_thread (void *arg) client = -1; for (c = 0; c < MAX_NET_CLIENTS && client < 0; c++) { - if (client_sock[c] <= 0) { + if (kps->client_sock[c] <= 0) { client = c; } } @@ -397,11 +498,16 @@ static THREAD_F connect_listen_thread (void *arg) } text_color_set(DW_COLOR_INFO); - dw_printf("Ready to accept KISS TCP client application %d on port %s ...\n", client, kiss_port_str); - - client_sock[client] = accept(listen_sock, NULL, NULL); + if (kps->chan == -1) { + dw_printf("Ready to accept KISS TCP client application %d on port %s ...\n", client, tcp_port_str); + } + else { + dw_printf("Ready to accept KISS TCP client application %d on port %s (radio channel %d) ...\n", client, tcp_port_str, kps->chan); + } - if (client_sock[client] == -1) { + kps->client_sock[client] = accept(listen_sock, NULL, NULL); + + if (kps->client_sock[client] == -1) { text_color_set(DW_COLOR_ERROR); dw_printf("Accept failed with error: %d\n", WSAGetLastError()); closesocket(listen_sock); @@ -410,10 +516,15 @@ static THREAD_F connect_listen_thread (void *arg) } text_color_set(DW_COLOR_INFO); - dw_printf("\nAttached to KISS TCP client application %d ...\n\n", client); + if (kps->chan == -1) { + dw_printf("\nAttached to KISS TCP client application %d on port %s ...\n\n", client, tcp_port_str); + } + else { + dw_printf("\nAttached to KISS TCP client application %d on port %s (radio channel %d) ...\n\n", client, tcp_port_str, kps->chan); + } // Reset the state and buffer. - memset (&(kf[client]), 0, sizeof(kf[client])); + memset (&(kps->kf[client]), 0, sizeof(kps->kf[client])); } else { SLEEP_SEC(1); /* wait then check again if more clients allowed. */ @@ -421,12 +532,11 @@ static THREAD_F connect_listen_thread (void *arg) } -#else /* End of Windows case, now Linux. */ +#else /* End of Windows case, now Linux / Unix / Mac OSX. */ - struct sockaddr_in sockaddr; /* Internet socket address stuct */ + struct sockaddr_in sockaddr; /* Internet socket address struct */ socklen_t sockaddr_size = sizeof(struct sockaddr_in); - int kiss_port = (int)(ptrdiff_t)arg; int listen_sock; int bcopt = 1; @@ -446,19 +556,19 @@ static THREAD_F connect_listen_thread (void *arg) setsockopt (listen_sock, SOL_SOCKET, SO_REUSEADDR, (const char *)&bcopt, 4); sockaddr.sin_addr.s_addr = INADDR_ANY; - sockaddr.sin_port = htons(kiss_port); + sockaddr.sin_port = htons(kps->tcp_port); sockaddr.sin_family = AF_INET; #if DEBUG text_color_set(DW_COLOR_DEBUG); - dw_printf("Binding to port %d ... \n", kiss_port); + dw_printf("Binding to port %d ... \n", kps->tcp_port); #endif if (bind(listen_sock,(struct sockaddr*)&sockaddr,sizeof(sockaddr)) == -1) { text_color_set(DW_COLOR_ERROR); dw_printf("Bind failed with error: %d\n", errno); dw_printf("%s\n", strerror(errno)); - dw_printf("Some other application is probably already using port %d.\n", kiss_port); + dw_printf("Some other application is probably already using port %d.\n", kps->tcp_port); dw_printf("Try using a different port number with KISSPORT in the configuration file.\n"); return (NULL); } @@ -477,7 +587,7 @@ static THREAD_F connect_listen_thread (void *arg) client = -1; for (c = 0; c < MAX_NET_CLIENTS && client < 0; c++) { - if (client_sock[c] <= 0) { + if (kps->client_sock[c] <= 0) { client = c; } } @@ -492,15 +602,25 @@ static THREAD_F connect_listen_thread (void *arg) } text_color_set(DW_COLOR_INFO); - dw_printf("Ready to accept KISS TCP client application %d on port %d ...\n", client, kiss_port); - - client_sock[client] = accept(listen_sock, (struct sockaddr*)(&sockaddr),&sockaddr_size); + if (kps->chan == -1) { + dw_printf("Ready to accept KISS TCP client application %d on port %d ...\n", client, kps->tcp_port); + } + else { + dw_printf("Ready to accept KISS TCP client application %d on port %d (radio channel %d) ...\n", client, kps->tcp_port, kps->chan); + } + + kps->client_sock[client] = accept(listen_sock, (struct sockaddr*)(&sockaddr),&sockaddr_size); text_color_set(DW_COLOR_INFO); - dw_printf("\nAttached to KISS TCP client application %d...\n\n", client); + if (kps->chan == -1) { + dw_printf("\nAttached to KISS TCP client application %d on port %d ...\n\n", client, kps->tcp_port); + } + else { + dw_printf("\nAttached to KISS TCP client application %d on port %d (radio channel %d) ...\n\n", client, kps->tcp_port, kps->chan); + } // Reset the state and buffer. - memset (&(kf[client]), 0, sizeof(kf[client])); + memset (&(kps->kf[client]), 0, sizeof(kps->kf[client])); } else { SLEEP_SEC(1); /* wait then check again if more clients allowed. */ @@ -517,7 +637,7 @@ static THREAD_F connect_listen_thread (void *arg) * * Name: kissnet_send_rec_packet * - * Purpose: Send a received packet to the client app. + * Purpose: Send a packet, received over the radio, to the client app. * * Inputs: chan - Channel number where packet was received. * 0 = first, 1 = second if any. @@ -537,119 +657,131 @@ static THREAD_F connect_listen_thread (void *arg) * it is using a traditional TNC and tries to put it * into KISS mode. * - * tcpclient - It is possible to have more than client attached + * onlykps - KISS TCP status block pointer or NULL. + * + * onlyclient - It is possible to have more than client attached * at the same time with TCP KISS. - * When a frame is received from the radio we want it - * to go to all of the clients. In this case specify -1. + * Starting with version 1.7 we can have multiple TCP ports. + * When a frame is received from the radio we normally want it + * to go to all of the clients. + * In this case specify NULL for onlykps and -1 tcp client. * When responding to a command from the client, we want * to send only to that one client app. In this case - * use the value 0 .. MAX_NET_CLIENTS-1. + * a non NULL kps and onlyclient >= 0. * * Description: Send message to client(s) if connected. * Disconnect from client, and notify user, if any error. * *--------------------------------------------------------------------*/ - -void kissnet_send_rec_packet (int chan, int kiss_cmd, unsigned char *fbuf, int flen, int tcpclient) +void kissnet_send_rec_packet (int chan, int kiss_cmd, unsigned char *fbuf, int flen, + struct kissport_status_s *onlykps, int onlyclient) { unsigned char kiss_buff[2 * AX25_MAX_PACKET_LEN]; int kiss_len; int err; - int first, last, client; -// Something received over the radio would be sent to all attached clients. +// Something received over the radio would normally be sent to all attached clients. // However, there are times we want to send a response only to a particular client. // In the case of a serial port or pseudo terminal, there is only one potential client. // so the response would be sent to only one place. A new parameter has been added for this. - if (tcpclient >= 0 && tcpclient < MAX_NET_CLIENTS) { - first = tcpclient; - last = tcpclient; - } - else if (tcpclient == -1) { - first = 0; - last = MAX_NET_CLIENTS - 1; - } - else { - text_color_set(DW_COLOR_ERROR); - dw_printf ("KISS TCP: Internal error, kissnet_send_rec_packet, tcpclient = %d.\n", tcpclient); - return; - } + for (struct kissport_status_s *kps = all_ports; kps != NULL; kps = kps->pnext) { + if (onlykps == NULL || kps == onlykps) { - for (client = first; client <= last; client++) { + for (int client = 0; client < MAX_NET_CLIENTS; client++) { - if (client_sock[client] != -1) { + if (onlyclient == -1 || client == onlyclient) { - if (flen < 0) { + if (kps->client_sock[client] != -1) { + + if (flen < 0) { // A client app might think it is attached to a traditional TNC. // It might try sending commands over and over again trying to get the TNC into KISS mode. // We recognize this attempt and send it something to keep it happy. - text_color_set(DW_COLOR_ERROR); - dw_printf ("KISS TCP: Something unexpected from client application.\n"); - dw_printf ("Is client app treating this like an old TNC with command mode?\n"); - dw_printf ("This can be caused by the application sending commands to put a\n"); - dw_printf ("traditional TNC into KISS mode. It is usually a harmless warning.\n"); - dw_printf ("For best results, configure for a KISS-only TNC to avoid this.\n"); - dw_printf ("In the case of APRSISCE/32, use \"Simply(KISS)\" rather than \"KISS.\"\n"); - - flen = strlen((char*)fbuf); - if (kiss_debug) { - kiss_debug_print (TO_CLIENT, "Fake command prompt", fbuf, flen); - } - strlcpy ((char *)kiss_buff, (char *)fbuf, sizeof(kiss_buff)); - kiss_len = strlen((char *)kiss_buff); - } - else { - unsigned char stemp[AX25_MAX_PACKET_LEN + 1]; - - assert (flen < (int)(sizeof(stemp))); - - stemp[0] = (chan << 4) | kiss_cmd; - memcpy (stemp+1, fbuf, flen); - - if (kiss_debug >= 2) { - /* AX.25 frame with the CRC removed. */ - text_color_set(DW_COLOR_DEBUG); - dw_printf ("\n"); - dw_printf ("Packet content before adding KISS framing and any escapes:\n"); - hex_dump (fbuf, flen); - } - - kiss_len = kiss_encapsulate (stemp, flen+1, kiss_buff); - - /* This has the escapes and the surrounding FENDs. */ - - if (kiss_debug) { - kiss_debug_print (TO_CLIENT, NULL, kiss_buff, kiss_len); - } - } + text_color_set(DW_COLOR_ERROR); + dw_printf ("KISS TCP: Something unexpected from client application.\n"); + dw_printf ("Is client app treating this like an old TNC with command mode?\n"); + dw_printf ("This can be caused by the application sending commands to put a\n"); + dw_printf ("traditional TNC into KISS mode. It is usually a harmless warning.\n"); + dw_printf ("For best results, configure for a KISS-only TNC to avoid this.\n"); + dw_printf ("In the case of APRSISCE/32, use \"Simply(KISS)\" rather than \"KISS.\"\n"); + + flen = strlen((char*)fbuf); + if (kiss_debug) { + kiss_debug_print (TO_CLIENT, "Fake command prompt", fbuf, flen); + } + strlcpy ((char *)kiss_buff, (char *)fbuf, sizeof(kiss_buff)); + kiss_len = strlen((char *)kiss_buff); + } + else { + unsigned char stemp[AX25_MAX_PACKET_LEN + 1]; + + assert (flen < (int)(sizeof(stemp))); + + // New in 1.7. + // Previously all channels were sent to everyone. + // We now have tcp ports which carry only a single radio channel. + // The application will see KISS channel 0 regardless of the radio channel. + + if (kps->chan == -1) { + // Normal case, all channels. + stemp[0] = (chan << 4) | kiss_cmd; + } + else if (kps->chan == chan) { + // Single radio channel for this port. Application sees 0. + stemp[0] = (0 << 4) | kiss_cmd; + } + else { + // Skip it. + continue; + } + + memcpy (stemp+1, fbuf, flen); + + if (kiss_debug >= 2) { + /* AX.25 frame with the CRC removed. */ + text_color_set(DW_COLOR_DEBUG); + dw_printf ("\n"); + dw_printf ("Packet content before adding KISS framing and any escapes:\n"); + hex_dump (fbuf, flen); + } + + kiss_len = kiss_encapsulate (stemp, flen+1, kiss_buff); + + /* This has the escapes and the surrounding FENDs. */ + + if (kiss_debug) { + kiss_debug_print (TO_CLIENT, NULL, kiss_buff, kiss_len); + } + } #if __WIN32__ - err = SOCK_SEND(client_sock[client], (char*)kiss_buff, kiss_len); - if (err == SOCKET_ERROR) - { - text_color_set(DW_COLOR_ERROR); - dw_printf ("\nError %d sending message to KISS client %d application. Closing connection.\n\n", WSAGetLastError(), client); - closesocket (client_sock[client]); - client_sock[client] = -1; - WSACleanup(); - } + err = SOCK_SEND(kps->client_sock[client], (char*)kiss_buff, kiss_len); + if (err == SOCKET_ERROR) { + text_color_set(DW_COLOR_ERROR); + dw_printf ("\nError %d sending message to KISS client application %d on port %d. Closing connection.\n\n", WSAGetLastError(), client, kps->tcp_port); + closesocket (kps->client_sock[client]); + kps->client_sock[client] = -1; + WSACleanup(); + } #else - err = SOCK_SEND (client_sock[client], kiss_buff, kiss_len); - if (err <= 0) - { - text_color_set(DW_COLOR_ERROR); - dw_printf ("\nError sending message to KISS client %d application. Closing connection.\n\n", client); - close (client_sock[client]); - client_sock[client] = -1; - } + err = SOCK_SEND (kps->client_sock[client], kiss_buff, kiss_len); + if (err <= 0) { + text_color_set(DW_COLOR_ERROR); + dw_printf ("\nError %d sending message to KISS client application %d on port %d. Closing connection.\n\n", err, client, kps->tcp_port); + close (kps->client_sock[client]); + kps->client_sock[client] = -1; + } #endif - } - } + } // frame length >= 0 + } // if all clients or the one specifie + } // for each client on the tcp port + } // if all ports or the one specified + } // for each tcp port } /* end kissnet_send_rec_packet */ @@ -661,14 +793,14 @@ void kissnet_send_rec_packet (int chan, int kiss_cmd, unsigned char *fbuf, int f * Purpose: Send data from one network KISS client to all others. * * Inputs: in_msg - KISS frame data without the framing or escapes. - * The first byte is channel (port) and command (should be data). + * The first byte is channel and command (should be data). + * Caller no longer cares this byte. We will clobber it here. * * in_len - Number of bytes in above. * - * chan - Channel. Redundant because it is also in first byte of kiss_msg. - * Not currently used. + * chan - Channel. Use this instead of first byte of in_msg. * - * cmd - KISS command nybble. Redundant because it is in first byte. + * cmd - KISS command nybble. * Should be 0 because I'm expecting this only for data. * * from_client - Number of network (TCP) client instance. @@ -687,52 +819,66 @@ void kissnet_send_rec_packet (int chan, int kiss_cmd, unsigned char *fbuf, int f *--------------------------------------------------------------------*/ -void kissnet_copy (unsigned char *in_msg, int in_len, int chan, int cmd, int from_client) +void kissnet_copy (unsigned char *in_msg, int in_len, int chan, int cmd, struct kissport_status_s *from_kps, int from_client) { unsigned char kiss_buff[2 * AX25_MAX_PACKET_LEN]; - int kiss_len; int err; - int send_to; - (void) chan; - (void) cmd; if (s_misc_config_p->kiss_copy) { - for (send_to = 0; send_to < MAX_NET_CLIENTS; send_to++) { + for (struct kissport_status_s *kps = all_ports; kps != NULL; kps = kps->pnext) { + + for (int client = 0; client < MAX_NET_CLIENTS; client++) { + + if ( ! ( kps == from_kps && client == from_client ) ) { // To all but origin. - if (send_to != from_client && client_sock[send_to] != -1) { + if (kps->client_sock[client] != -1) { - kiss_len = kiss_encapsulate (in_msg, in_len, kiss_buff); + if (kps-> chan == -1 || kps->chan == chan) { - /* This has the escapes and the surrounding FENDs. */ + // Two different cases here: + // - The TCP port allows all channels, or + // - The TCP port allows only one channel. In this case set KISS channel to 0. - if (kiss_debug) { - kiss_debug_print (TO_CLIENT, NULL, kiss_buff, kiss_len); - } + if (kps->chan == -1) { + in_msg[0] = (chan << 4) | cmd; + } + else { + in_msg[0] = 0 | cmd; // set channel to zero. + } + + int kiss_len = kiss_encapsulate (in_msg, in_len, kiss_buff); + + /* This has the escapes and the surrounding FENDs. */ + + if (kiss_debug) { + kiss_debug_print (TO_CLIENT, NULL, kiss_buff, kiss_len); + } #if __WIN32__ - err = SOCK_SEND(client_sock[send_to], (char*)kiss_buff, kiss_len); - if (err == SOCKET_ERROR) - { - text_color_set(DW_COLOR_ERROR); - dw_printf ("\nError %d copying message to KISS client %d application. Closing connection.\n\n", WSAGetLastError(), send_to); - closesocket (client_sock[send_to]); - client_sock[send_to] = -1; - WSACleanup(); - } + err = SOCK_SEND(kps->client_sock[client], (char*)kiss_buff, kiss_len); + if (err == SOCKET_ERROR) { + text_color_set(DW_COLOR_ERROR); + dw_printf ("\nError %d copying message to KISS TCP port %d client %d application. Closing connection.\n\n", WSAGetLastError(), kps->tcp_port, client); + closesocket (kps->client_sock[client]); + kps->client_sock[client] = -1; + WSACleanup(); + } #else - err = SOCK_SEND (client_sock[send_to], kiss_buff, kiss_len); - if (err <= 0) - { - text_color_set(DW_COLOR_ERROR); - dw_printf ("\nError copying message to KISS client %d application. Closing connection.\n\n", send_to); - close (client_sock[send_to]); - client_sock[send_to] = -1; - } + err = SOCK_SEND (kps->client_sock[client], kiss_buff, kiss_len); + if (err <= 0) { + text_color_set(DW_COLOR_ERROR); + dw_printf ("\nError copying message to KISS TCP port %d client %d application. Closing connection.\n\n", kps->tcp_port, client); + close (kps->client_sock[client]); + kps->client_sock[client] = -1; + } #endif - } // if origin and destination different. - } // loop over all KISS network clients. + } // Channel is allowed on this port. + } // socket is open + } // if origin and destination different. + } // loop over all KISS network clients for one port. + } // loop over all KISS TCP ports } // Feature enabled. } /* end kissnet_copy */ @@ -759,20 +905,19 @@ void kissnet_copy (unsigned char *in_msg, int in_len, int chan, int cmd, int fro /* Return one byte (value 0 - 255) */ -static int kiss_get (int client) +static int kiss_get (struct kissport_status_s *kps, int client) { - unsigned char ch; - int n; while (1) { - while (client_sock[client] <= 0) { + while (kps->client_sock[client] <= 0) { SLEEP_SEC(1); /* Not connected. Try again later. */ } /* Just get one byte at a time. */ - n = SOCK_RECV (client_sock[client], (char *)(&ch), 1); + unsigned char ch; + int n = SOCK_RECV (kps->client_sock[client], (char *)(&ch), 1); if (n == 1) { #if DEBUG9 @@ -792,13 +937,13 @@ static int kiss_get (int client) } text_color_set(DW_COLOR_ERROR); - dw_printf ("\nKISS client application %d has gone away.\n\n", client); + dw_printf ("\nKISS client application %d on TCP port %d has gone away.\n\n", client, kps->tcp_port); #if __WIN32__ - closesocket (client_sock[client]); + closesocket (kps->client_sock[client]); #else - close (client_sock[client]); + close (kps->client_sock[client]); #endif - client_sock[client] = -1; + kps->client_sock[client] = -1; } } @@ -806,17 +951,19 @@ static int kiss_get (int client) static THREAD_F kissnet_listen_thread (void *arg) { - unsigned char ch; - + struct kissport_status_s *kps = arg; - int client = (int)(ptrdiff_t)arg; + int client = kps->arg2; + assert (client >= 0 && client < MAX_NET_CLIENTS); + + kps->arg2 = -1; // Indicates thread is running so + // arg2 can be reused for the next one. #if DEBUG text_color_set(DW_COLOR_DEBUG); - dw_printf ("kissnet_listen_thread ( client = %d, socket fd = %d )\n", client, client_sock[client]); + dw_printf ("kissnet_listen_thread ( tcp_port = %d, client = %d, socket fd = %d )\n", kps->tcp_port, client, kps->client_sock[client]); #endif - assert (client >= 0 && client < MAX_NET_CLIENTS); // So why is kissnet_send_rec_packet mentioned here for incoming from the client app? @@ -832,8 +979,8 @@ static THREAD_F kissnet_listen_thread (void *arg) while (1) { - ch = kiss_get(client); - kiss_rec_byte (&(kf[client]), ch, kiss_debug, client, kissnet_send_rec_packet); + unsigned char ch = kiss_get(kps, client); + kiss_rec_byte (&(kps->kf[client]), ch, kiss_debug, kps, client, kissnet_send_rec_packet); } #if __WIN32__ diff --git a/src/kissnet.h b/src/kissnet.h index ac00752e..469e4e63 100644 --- a/src/kissnet.h +++ b/src/kissnet.h @@ -3,21 +3,27 @@ * Name: kissnet.h */ +#ifndef KISSNET_H +#define KISSNET_H #include "ax25_pad.h" /* for packet_t */ #include "config.h" +#include "kiss_frame.h" void kissnet_init (struct misc_config_s *misc_config); -void kissnet_send_rec_packet (int chan, int kiss_cmd, unsigned char *fbuf, int flen, int client); +void kissnet_send_rec_packet (int chan, int kiss_cmd, unsigned char *fbuf, int flen, + struct kissport_status_s *onlykps, int onlyclient); void kiss_net_set_debug (int n); -void kissnet_copy (unsigned char *kiss_msg, int kiss_len, int chan, int cmd, int from_client); +void kissnet_copy (unsigned char *kiss_msg, int kiss_len, int chan, int cmd, struct kissport_status_s *from_kps, int from_client); +#endif // KISSNET_H + /* end kissnet.h */ diff --git a/src/kissserial.c b/src/kissserial.c index 9f185ef1..1ee5356a 100644 --- a/src/kissserial.c +++ b/src/kissserial.c @@ -261,6 +261,7 @@ void kissserial_init (struct misc_config_s *mc) * flen - Length of raw received frame not including the FCS * or -1 for a text string. * + * kps * client - Not used for serial port version. * Here so that 3 related functions all have * the same parameter list. @@ -272,7 +273,8 @@ void kissserial_init (struct misc_config_s *mc) *--------------------------------------------------------------------*/ -void kissserial_send_rec_packet (int chan, int kiss_cmd, unsigned char *fbuf, int flen, int client) +void kissserial_send_rec_packet (int chan, int kiss_cmd, unsigned char *fbuf, int flen, + struct kissport_status_s *notused1, int notused2) { unsigned char kiss_buff[2 * AX25_MAX_PACKET_LEN + 2]; int kiss_len; @@ -370,11 +372,11 @@ void kissserial_send_rec_packet (int chan, int kiss_cmd, unsigned char *fbuf, i * * Returns: one byte (value 0 - 255) or terminate thread on error. * - * Description: There is room for improvment here. Reading one byte + * Description: There is room for improvement here. Reading one byte * at a time is inefficient. We could read a large block * into a local buffer and return a byte from that most of the time. * Is it worth the effort? I don't know. With GHz processors and - * the low data rate here it might not make a noticable difference. + * the low data rate here it might not make a noticeable difference. * *--------------------------------------------------------------------*/ @@ -488,7 +490,7 @@ static THREAD_F kissserial_listen_thread (void *arg) while (1) { ch = kissserial_get(); - kiss_rec_byte (&kf, ch, kissserial_debug, -1, kissserial_send_rec_packet); + kiss_rec_byte (&kf, ch, kissserial_debug, NULL, -1, kissserial_send_rec_packet); } #if __WIN32__ diff --git a/src/kissserial.h b/src/kissserial.h index a6775291..44fb3c3f 100644 --- a/src/kissserial.h +++ b/src/kissserial.h @@ -8,12 +8,14 @@ #include "config.h" - +#include "kiss_frame.h" void kissserial_init (struct misc_config_s *misc_config); -void kissserial_send_rec_packet (int chan, int kiss_cmd, unsigned char *fbuf, int flen, int client); +void kissserial_send_rec_packet (int chan, int kiss_cmd, unsigned char *fbuf, int flen, + struct kissport_status_s *notused1, int notused2); + void kissserial_set_debug (int n); diff --git a/src/kissutil.c b/src/kissutil.c index 026a6ea3..fcd86088 100644 --- a/src/kissutil.c +++ b/src/kissutil.c @@ -67,7 +67,6 @@ #include "serial_port.h" #include "kiss_frame.h" #include "dwsock.h" -#include "dtime_now.h" #include "audio.h" // for DEFAULT_TXDELAY, etc. #include "dtime_now.h" @@ -308,10 +307,15 @@ int main (int argc, char *argv[]) } #endif +// Give the threads a little while to open the TNC connection before trying to use it. +// This was a problem when the transmit queue already existed when starting up. + + SLEEP_MS (500); + /* * Process keyboard or other input source. */ - char stuff[1000]; + char stuff[AX25_MAX_PACKET_LEN]; if (strlen(transmit_from) > 0) { /* @@ -544,8 +548,8 @@ static void process_input (char *stuff) static void send_to_kiss_tnc (int chan, int cmd, char *data, int dlen) { - unsigned char temp[1000]; - unsigned char kissed[2000]; + unsigned char temp[AX25_MAX_PACKET_LEN]; // We don't limit to 256 info bytes. + unsigned char kissed[AX25_MAX_PACKET_LEN*2]; int klen; if (chan < 0 || chan > 15) { @@ -587,6 +591,7 @@ static void send_to_kiss_tnc (int chan, int cmd, char *data, int dlen) if (rc != klen) { text_color_set(DW_COLOR_ERROR); dw_printf ("ERROR writing KISS frame to serial port.\n"); + //dw_printf ("DEBUG wanted %d, got %d\n", klen, rc); } } @@ -663,7 +668,7 @@ static THREAD_F tnc_listen_net (void *arg) // on the assumption it was being used in only one direction. // Not worried enough about it to do anything at this time. - kiss_rec_byte (&kstate, data[j], verbose, client, NULL); + kiss_rec_byte (&kstate, data[j], verbose, NULL, client, NULL); } } @@ -726,7 +731,7 @@ static THREAD_F tnc_listen_serial (void *arg) // Feed in one byte at a time. // kiss_process_msg is called when a complete frame has been accumulated. - kiss_rec_byte (&kstate, ch, verbose, client, NULL); + kiss_rec_byte (&kstate, ch, verbose, NULL, client, NULL); } } /* end tnc_listen_serial */ @@ -754,7 +759,8 @@ static THREAD_F tnc_listen_serial (void *arg) * *-----------------------------------------------------------------*/ -void kiss_process_msg (unsigned char *kiss_msg, int kiss_len, int debug, int client, void (*sendfun)(int,int,unsigned char*,int,int)) +void kiss_process_msg (unsigned char *kiss_msg, int kiss_len, int debug, struct kissport_status_s *kps, int client, + void (*sendfun)(int chan, int kiss_cmd, unsigned char *fbuf, int flen, struct kissport_status_s *onlykps, int onlyclient)) { int chan; int cmd; @@ -915,7 +921,7 @@ static void usage(void) dw_printf (" a serial port. e.g. /dev/ttyAMA0 or COM3.\n"); dw_printf (" -s Serial port speed, default 9600.\n"); dw_printf (" -v Verbose. Show the KISS frame contents.\n"); - dw_printf (" -f Transmit files directory. Processs and delete files here.\n"); + dw_printf (" -f Transmit files directory. Process and delete files here.\n"); dw_printf (" -o Receive output queue directory. Store received frames here.\n"); dw_printf (" -T Precede received frames with 'strftime' format time stamp.\n"); usage2(); @@ -929,7 +935,7 @@ static void usage2 (void) dw_printf ("Input, starting with upper case letter or digit, is assumed\n"); dw_printf ("to be an AX.25 frame in the usual TNC2 monitoring format.\n"); dw_printf ("\n"); - dw_printf ("Input, starting with a lower case letter is a commmand.\n"); + dw_printf ("Input, starting with a lower case letter is a command.\n"); dw_printf ("Whitespace, as shown in examples, is optional.\n"); dw_printf ("\n"); dw_printf (" letter meaning example\n"); diff --git a/src/latlong.c b/src/latlong.c index b3eadcc8..d5f23365 100644 --- a/src/latlong.c +++ b/src/latlong.c @@ -55,7 +55,11 @@ * ambiguity - If 1, 2, 3, or 4, blank out that many trailing digits. * * Outputs: slat - String in format ddmm.mm[NS] - * Should always be exactly 8 characters + NUL. + * Must always be exactly 8 characters + NUL. + * Put in leading zeros if necessary. + * We must have exactly ddmm.mm and hemisphere because + * the APRS position report has fixed width fields. + * Trailing digits can be blanked for position ambiguity. * * Returns: None * @@ -101,6 +105,12 @@ void latitude_to_str (double dlat, int ambiguity, char *slat) ideg = (int)dlat; dmin = (dlat - ideg) * 60.; + // dmin is known to be in range of 0 <= dmin < 60. + + // Minutes must be exactly like 99.99 with leading zeros, + // if needed, to make it fixed width. + // Two digits, decimal point, two digits, nul terminator. + snprintf (smin, sizeof(smin), "%05.2f", dmin); /* Due to roundoff, 59.9999 could come out as "60.00" */ if (smin[0] == '6') { @@ -108,6 +118,10 @@ void latitude_to_str (double dlat, int ambiguity, char *slat) ideg++; } + // Assumes slat can hold 8 characters + nul. + // Degrees must be exactly 2 digits, with leading zero, if needed. + + // FIXME: Should pass in sizeof slat and use snprintf sprintf (slat, "%02d%s%c", ideg, smin, hemi); if (ambiguity >= 1) { @@ -135,9 +149,12 @@ void latitude_to_str (double dlat, int ambiguity, char *slat) * Inputs: dlong - Floating point degrees. * ambiguity - If 1, 2, 3, or 4, blank out that many trailing digits. * - * Outputs: slat - String in format dddmm.mm[NS] - * Should always be exactly 9 characters + NUL. - * + * Outputs: slong - String in format dddmm.mm[NS] + * Must always be exactly 9 characters + NUL. + * Put in leading zeros if necessary. + * We must have exactly dddmm.mm and hemisphere because + * the APRS position report has fixed width fields. + * Trailing digits can be blanked for position ambiguity. * Returns: None * *----------------------------------------------------------------*/ @@ -178,7 +195,12 @@ void longitude_to_str (double dlong, int ambiguity, char *slong) ideg++; } + // Assumes slong can hold 9 characters + nul. + // Degrees must be exactly 3 digits, with leading zero, if needed. + + // FIXME: Should pass in sizeof slong and use snprintf sprintf (slong, "%03d%s%c", ideg, smin, hemi); + /* * The spec says position ambiguity in latitude also * applies to longitude automatically. @@ -352,6 +374,7 @@ void latitude_to_nmea (double dlat, char *slat, char *hemi) ideg++; } + // FIXME: Should pass in sizeof slat and use snprintf sprintf (slat, "%02d%s", ideg, smin); } /* end latitude_to_str */ @@ -413,6 +436,7 @@ void longitude_to_nmea (double dlong, char *slong, char *hemi) ideg++; } + // FIXME: Should pass in sizeof slong and use snprintf sprintf (slong, "%03d%s", ideg, smin); } /* end longitude_to_nmea */ @@ -903,6 +927,14 @@ int main (int argc, char *argv[]) latitude_to_str (45.999830, 4, result); if (strcmp(result, "45 . N") != 0) { errors++; dw_printf ("Error 1.8: Did not expect \"%s\"\n", result); } + // Test for leading zeros for small values. Result must be fixed width. + + latitude_to_str (0.016666666, 0, result); + if (strcmp(result, "0001.00N") != 0) { errors++; dw_printf ("Error 1.9: Did not expect \"%s\"\n", result); } + + latitude_to_str (-1.999999, 0, result); + if (strcmp(result, "0200.00S") != 0) { errors++; dw_printf ("Error 1.10: Did not expect \"%s\"\n", result); } + /* Longitude to APRS format. */ longitude_to_str (45.25, 0, result); @@ -931,6 +963,15 @@ int main (int argc, char *argv[]) longitude_to_str (45.999830, 4, result); if (strcmp(result, "045 . E") != 0) { errors++; dw_printf ("Error 2.8: Did not expect \"%s\"\n", result); } + // Test for leading zeros for small values. Result must be fixed width. + + longitude_to_str (0.016666666, 0, result); + if (strcmp(result, "00001.00E") != 0) { errors++; dw_printf ("Error 2.9: Did not expect \"%s\"\n", result); } + + longitude_to_str (-1.999999, 0, result); + if (strcmp(result, "00200.00W") != 0) { errors++; dw_printf ("Error 2.10: Did not expect \"%s\"\n", result); } + + /* Compressed format. */ /* Protocol spec example has <*e7 but I got <*e8 due to rounding rather than truncation to integer. */ diff --git a/src/log.c b/src/log.c index 02aab2ab..d7ef544b 100644 --- a/src/log.c +++ b/src/log.c @@ -224,7 +224,7 @@ void log_write (int chan, decode_aprs_t *A, packet_t pp, alevel_t alevel, retry_ now = time(NULL); // Get current time. (void)gmtime_r (&now, &tm); - +// FIXME: https://github.com/wb2osz/direwolf/issues/473 if (g_daily_names) { @@ -345,7 +345,7 @@ void log_write (int chan, decode_aprs_t *A, packet_t pp, alevel_t alevel, retry_ char sstatus[40]; char stelemetry[200]; char scomment[256]; - char alevel_text[32]; + char alevel_text[40]; diff --git a/src/log2gpx.c b/src/log2gpx.c index b13d80ea..15d389f4 100644 --- a/src/log2gpx.c +++ b/src/log2gpx.c @@ -258,8 +258,6 @@ static void read_csv(FILE *fp) float speed = UNKNOWN_VALUE; float course = UNKNOWN_VALUE; float alt = UNKNOWN_VALUE; - double freq = UNKNOWN_VALUE; - int offset = UNKNOWN_VALUE; char stemp[16], desc[32], comment[256]; if (pspeed != NULL && strlen(pspeed) > 0) { @@ -275,7 +273,7 @@ static void read_csv(FILE *fp) /* combine freq/offset/tone into one description string. */ if (pfreq != NULL && strlen(pfreq) > 0) { - freq = atof(pfreq); + double freq = atof(pfreq); snprintf (desc, sizeof(desc), "%.3f MHz", freq); } else { @@ -283,7 +281,7 @@ static void read_csv(FILE *fp) } if (poffset != NULL && strlen(poffset) > 0) { - offset = atoi(poffset); + int offset = atoi(poffset); if (offset != 0 && offset % 1000 == 0) { snprintf (stemp, sizeof(stemp), "%+dM", offset / 1000); } diff --git a/src/mgn_icon.h b/src/mgn_icon.h index 4563cce1..c870bc0e 100644 --- a/src/mgn_icon.h +++ b/src/mgn_icon.h @@ -208,9 +208,9 @@ static const char mgn_alternate_symtab[SYMTAB_SIZE][3] = { MGN_park, // ; 27 Park/Picnic area MGN_default, // < 28 ADVISORY (one WX flag) MGN_default, // = 29 APRStt Touchtone (DTMF users) - MGN_default, // > 30 OVERLAYED CAR + MGN_default, // > 30 OVERLAID CAR MGN_tourist_info, // ? 31 INFO Kiosk (Blue box with ?) - MGN_default, // @ 32 HURICANE/Trop-Storm + MGN_default, // @ 32 HURRICANE/Trop-Storm MGN_box, // A 33 overlayBOX DTMF & RFID & XO MGN_default, // B 34 Blwng Snow (& future codes) MGN_boating, // C 35 Coast Guard @@ -220,7 +220,7 @@ static const char mgn_alternate_symtab[SYMTAB_SIZE][3] = { MGN_default, // G 39 Snow Shwr (& future ovrlys) MGN_default, // H 40 Haze (& Overlay Hazards) MGN_default, // I 41 Rain Shower - MGN_default, // J 42 Lightening (& future ovrlys) + MGN_default, // J 42 Lightning (& future ovrlys) MGN_default, // K 43 Kenwood HT (W) MGN_lighthouse, // L 44 Lighthouse MGN_default, // M 45 MARS (A=Army,N=Navy,F=AF) @@ -263,12 +263,12 @@ static const char mgn_alternate_symtab[SYMTAB_SIZE][3] = { MGN_default, // r 82 Restrooms MGN_default, // s 83 OVERLAY SHIP/boat (top view) MGN_default, // t 84 Tornado - MGN_default, // u 85 OVERLAYED TRUCK - MGN_default, // v 86 OVERLAYED Van + MGN_default, // u 85 OVERLAID TRUCK + MGN_default, // v 86 OVERLAID Van MGN_default, // w 87 Flooding MGN_wreck, // x 88 Wreck or Obstruction ->X<- MGN_default, // y 89 Skywarn - MGN_default, // z 90 OVERLAYED Shelter + MGN_default, // z 90 OVERLAID Shelter MGN_default, // { 91 Fog (& future ovrly codes) MGN_default, // | 92 TNC Stream Switch MGN_default, // } 93 diff --git a/src/mheard.c b/src/mheard.c index eff4709a..f11c68f0 100644 --- a/src/mheard.c +++ b/src/mheard.c @@ -102,7 +102,7 @@ typedef struct mheard_s { double dlat, dlon; // Last position. G_UNKNOWN for unknown. - int msp; // Allow message sender positon report. + int msp; // Allow message sender position report. // When non zero, an IS>RF position report is allowed. // Then decremented. @@ -336,6 +336,52 @@ void mheard_save_rf (int chan, decode_aprs_t *A, packet_t pp, alevel_t alevel, r */ hops = ax25_get_heard(pp) - AX25_SOURCE; +/* + * Consider the following scenario: + * + * (1) We hear AA1PR-9 by a path of 4 digipeaters. + * Looking closer, it's probably only two because there are left over WIDE1-0 and WIDE2-0. + * + * Digipeater WIDE2 (probably N3LLO-3) audio level = 72(19/15) [NONE] _|||||___ + * [0.3] AA1PR-9>APY300,K1EQX-7,WIDE1,N3LLO-3,WIDE2*,ARISS::ANSRVR :cq hotg vt aprsthursday{01<0x0d> + * ----- ----- + * + * (2) APRS-IS sends a response to us. + * + * [ig>tx] ANSRVR>APWW11,KJ4ERJ-15*,TCPIP*,qAS,KJ4ERJ-15::AA1PR-9 :N:HOTG 161 Messages Sent{JL} + * + * (3) Here is our analysis of whether it should be sent to RF. + * + * Was message addressee AA1PR-9 heard in the past 180 minutes, with 2 or fewer digipeater hops? + * No, AA1PR-9 was last heard over the radio with 4 digipeater hops 0 minutes ago. + * + * The wrong hop count caused us to drop a packet that should have been transmitted. + * We could put in a hack to not count the "WIDEn-0" addresses. + * That is not correct because other prefixes could be used and we don't know + * what they are for other digipeaters. + * I think the best solution is to simply ignore the hop count. + * Maybe next release will have a major cleanup. + */ + + // HACK - Reduce hop count by number of used WIDEn-0 addresses. + + if (hops > 1) { + for (int k = 0; k < ax25_get_num_repeaters(pp); k++) { + char digi[AX25_MAX_ADDR_LEN]; + ax25_get_addr_no_ssid (pp, AX25_REPEATER_1 + k, digi); + int ssid = ax25_get_ssid (pp, AX25_REPEATER_1 + k); + int used = ax25_get_h (pp, AX25_REPEATER_1 + k); + + //text_color_set(DW_COLOR_DEBUG); + //dw_printf ("Examining %s-%d used=%d.\n", digi, ssid, used); + + if (used && strlen(digi) == 5 && strncmp(digi, "WIDE", 4) == 0 && isdigit(digi[4]) && ssid == 0) { + hops--; + //text_color_set(DW_COLOR_DEBUG); + //dw_printf ("Decrease hop count to %d for problematic %s.\n", hops, digi); + } + } + } mptr = mheard_ptr(source); if (mptr == NULL) { @@ -350,6 +396,11 @@ void mheard_save_rf (int chan, decode_aprs_t *A, packet_t pp, alevel_t alevel, r } mptr = calloc(sizeof(mheard_t),1); + if (mptr == NULL) { + text_color_set(DW_COLOR_ERROR); + dw_printf ("FATAL ERROR: Out of memory.\n"); + exit (EXIT_FAILURE); + } strlcpy (mptr->callsign, source, sizeof(mptr->callsign)); mptr->count = 1; mptr->chan = chan; @@ -429,6 +480,7 @@ void mheard_save_rf (int chan, decode_aprs_t *A, packet_t pp, alevel_t alevel, r * N1HKO-10>APJI40,TCPIP*,qAC,N1HKO-JS:APWW10,WIDE1-1,WIDE2-1,qAS,K1RI:/221700h/9AmAT3PQ3S,WIDE1-1,WIDE2-1,qAR,W1TG-1:`c)@qh\>/"50}TinyTrak4 Mobile + * WHO-IS>APJIW4,TCPIP*,qAC,AE5PL-JF::WB2OSZ :C/Billerica Amateur Radio Society/MA/United States{XF}WO * * Notice how the final address in the header might not * be a valid AX.25 address. We see a 9 character address @@ -438,6 +490,7 @@ void mheard_save_rf (int chan, decode_aprs_t *A, packet_t pp, alevel_t alevel, r * a clue about the journey taken but I don't think we care here. * * All we should care about here is the the source address. + * Note that the source address might not adhere to the AX.25 format. * * Description: * @@ -445,21 +498,25 @@ void mheard_save_rf (int chan, decode_aprs_t *A, packet_t pp, alevel_t alevel, r void mheard_save_is (char *ptext) { - packet_t pp; time_t now = time(NULL); char source[AX25_MAX_ADDR_LEN]; - mheard_t *mptr; + +#if 1 +// It is possible that source won't adhere to the AX.25 restrictions. +// So we simply extract the source address, as text, from the beginning rather than +// using ax25_from_text() and ax25_get_addr_with_ssid(). + + memset (source, 0, sizeof(source)); + memcpy (source, ptext, sizeof(source)-1); + char *g = strchr(source, '>'); + if (g != NULL) *g = '\0'; + +#else /* - * Try to parse it into a packet object. - * This will contain "q constructs" and we might see an address - * with two alphnumeric characters in the SSID so we must use - * the non-strict parsing. - * - * Bug: Up to 8 digipeaters are allowed in radio format. - * There is a potential of finding a larger number here. + * Keep this here in case I want to revive it to get location. */ - pp = ax25_from_text(ptext, 0); + packet_t pp = ax25_from_text(ptext, 0); if (pp == NULL) { if (mheard_debug) { @@ -470,13 +527,17 @@ void mheard_save_is (char *ptext) return; } - ax25_get_addr_with_ssid (pp, AX25_SOURCE, source); + //////ax25_get_addr_with_ssid (pp, AX25_SOURCE, source); +#endif - mptr = mheard_ptr(source); + mheard_t *mptr = mheard_ptr(source); if (mptr == NULL) { int i; /* * Not heard before. Add it. + * Observation years later: + * Hmmmm. I wonder why I did not store the location if available. + * An earlier example has an APRSdroid station reporting location without using [ham] RF. */ if (mheard_debug) { @@ -485,6 +546,11 @@ void mheard_save_is (char *ptext) } mptr = calloc(sizeof(mheard_t),1); + if (mptr == NULL) { + text_color_set(DW_COLOR_ERROR); + dw_printf ("FATAL ERROR: Out of memory.\n"); + exit (EXIT_FAILURE); + } strlcpy (mptr->callsign, source, sizeof(mptr->callsign)); mptr->count = 1; mptr->last_heard_is = now; @@ -527,7 +593,9 @@ void mheard_save_is (char *ptext) mheard_dump (); } +#if 0 ax25_delete (pp); +#endif } /* end mheard_save_is */ @@ -549,7 +617,7 @@ void mheard_save_is (char *ptext) * 8 for RF_CNT. * * time_limit - Include only stations heard within this many minutes. - * Typically 30 or 60. + * Typically 180. * * Returns: Number to be used in the statistics report. * @@ -627,7 +695,7 @@ int mheard_count (int max_hops, int time_limit) * callsign - Callsign for station. * * time_limit - Include only stations heard within this many minutes. - * Typically 30 or 60. + * Typically 180. * * max_hops - Include only stations heard with this number of * digipeater hops or less. For reporting, we might use: @@ -768,7 +836,7 @@ void mheard_set_msp (char *callsign, int num) * * Inputs: callsign - Callsign for station which sent the "message." * - * Returns: The cound for the specified station. + * Returns: The count for the specified station. * 0 if not found. * *------------------------------------------------------------------*/ diff --git a/src/morse.c b/src/morse.c index 8e709fd9..b61e75cb 100644 --- a/src/morse.c +++ b/src/morse.c @@ -70,7 +70,7 @@ static const struct morse_s { { 'F', "..-." }, { 'G', "--." }, { 'H', "...." }, - { 'I', "." }, + { 'I', ".." }, { 'J', ".---" }, { 'K', "-.-" }, { 'L', ".-.." }, @@ -313,7 +313,7 @@ static void morse_tone (int chan, int tu, int wpm) { int f1_change_per_sample; // How much to advance phase for each audio sample. - if (save_audio_config_p->achan[chan].medium != MEDIUM_RADIO) { + if (save_audio_config_p->chan_medium[chan] != MEDIUM_RADIO) { text_color_set(DW_COLOR_ERROR); dw_printf ("Invalid channel %d for sending Morse Code.\n", chan); return; @@ -365,7 +365,7 @@ static void morse_quiet (int chan, int tu, int wpm) { int nsamples; int j; - if (save_audio_config_p->achan[chan].medium != MEDIUM_RADIO) { + if (save_audio_config_p->chan_medium[chan] != MEDIUM_RADIO) { text_color_set(DW_COLOR_ERROR); dw_printf ("Invalid channel %d for sending Morse Code.\n", chan); return; @@ -404,7 +404,7 @@ static void morse_quiet_ms (int chan, int ms) { int nsamples; int j; - if (save_audio_config_p->achan[chan].medium != MEDIUM_RADIO) { + if (save_audio_config_p->chan_medium[chan] != MEDIUM_RADIO) { text_color_set(DW_COLOR_ERROR); dw_printf ("Invalid channel %d for sending Morse Code.\n", chan); return; diff --git a/src/multi_modem.c b/src/multi_modem.c index c59af071..d2382f1a 100644 --- a/src/multi_modem.c +++ b/src/multi_modem.c @@ -83,7 +83,7 @@ //#define DEBUG 1 -#define DIGIPEATER_C +#define DIGIPEATER_C // Why? #include "direwolf.h" @@ -116,7 +116,8 @@ static struct audio_s *save_audio_config_p; static struct { packet_t packet_p; alevel_t alevel; - int is_fx25; // 1 for FX.25, 0 for regular AX.25. + float speed_error; + fec_type_t fec_type; // Type of FEC: none(0), fx25, il2p retry_t retries; // For the old "fix bits" strategy, this is the // number of bits that were modified to get a good CRC. // It would be 0 to something around 4. @@ -172,7 +173,7 @@ void multi_modem_init (struct audio_s *pa) hdlc_rec_init (save_audio_config_p); for (chan=0; chanachan[chan].medium == MEDIUM_RADIO) { + if (save_audio_config_p->chan_medium[chan] == MEDIUM_RADIO) { if (save_audio_config_p->achan[chan].baud <= 0) { text_color_set(DW_COLOR_ERROR); dw_printf("Internal error, chan=%d, %s, %d\n", chan, __FILE__, __LINE__); @@ -306,14 +307,14 @@ void multi_modem_process_sample (int chan, int audio_sample) * display of audio level line. * Use -2 to indicate DTMF message.) * retries - Level of correction used. - * is_fx25 - 1 for FX.25, 0 for normal AX.25. + * fec_type - none(0), fx25, il2p * * Description: Add to list of candidates. Best one will be picked later. * *--------------------------------------------------------------------*/ -void multi_modem_process_rec_frame (int chan, int subchan, int slice, unsigned char *fbuf, int flen, alevel_t alevel, retry_t retries, int is_fx25) +void multi_modem_process_rec_frame (int chan, int subchan, int slice, unsigned char *fbuf, int flen, alevel_t alevel, retry_t retries, fec_type_t fec_type) { packet_t pp; @@ -345,13 +346,20 @@ void multi_modem_process_rec_frame (int chan, int subchan, int slice, unsigned c else { pp = ax25_from_frame (fbuf, flen, alevel); } + + multi_modem_process_rec_packet (chan, subchan, slice, pp, alevel, retries, fec_type); +} + +// TODO: Eliminate function above and move code elsewhere? + +void multi_modem_process_rec_packet (int chan, int subchan, int slice, packet_t pp, alevel_t alevel, retry_t retries, fec_type_t fec_type) +{ if (pp == NULL) { text_color_set(DW_COLOR_ERROR); dw_printf ("Unexpected internal problem, %s %d\n", __FILE__, __LINE__); return; /* oops! why would it fail? */ } - /* * If only one demodulator/slicer, and no FX.25 in progress, * push it thru and forget about all this foolishness. @@ -379,7 +387,7 @@ void multi_modem_process_rec_frame (int chan, int subchan, int slice, unsigned c ax25_delete (pp); } else { - dlq_rec_frame (chan, subchan, slice, pp, alevel, is_fx25, retries, ""); + dlq_rec_frame (chan, subchan, slice, pp, alevel, fec_type, retries, ""); } return; } @@ -399,7 +407,7 @@ void multi_modem_process_rec_frame (int chan, int subchan, int slice, unsigned c candidate[chan][subchan][slice].packet_p = pp; candidate[chan][subchan][slice].alevel = alevel; - candidate[chan][subchan][slice].is_fx25 = is_fx25; + candidate[chan][subchan][slice].fec_type = fec_type; candidate[chan][subchan][slice].retries = retries; candidate[chan][subchan][slice].age = 0; candidate[chan][subchan][slice].crc = ax25_m_m_crc(pp); @@ -436,6 +444,9 @@ static void pick_best_candidate (int chan) int best_n, best_score; char spectrum[MAX_SUBCHANS*MAX_SLICERS+1]; int n, j, k; + if (save_audio_config_p->achan[chan].num_slicers < 1) { + save_audio_config_p->achan[chan].num_slicers = 1; + } int num_bars = save_audio_config_p->achan[chan].num_slicers * save_audio_config_p->achan[chan].num_subchan; memset (spectrum, 0, sizeof(spectrum)); @@ -449,7 +460,7 @@ static void pick_best_candidate (int chan) if (candidate[chan][j][k].packet_p == NULL) { spectrum[n] = '_'; } - else if (candidate[chan][j][k].is_fx25) { + else if (candidate[chan][j][k].fec_type != fec_type_none) { // FX.25 or IL2P // FIXME: using retries both as an enum and later int too. if ((int)(candidate[chan][j][k].retries) <= 9) { spectrum[n] = '0' + candidate[chan][j][k].retries; @@ -457,7 +468,7 @@ static void pick_best_candidate (int chan) else { spectrum[n] = '+'; } - } + } // AX.25 below else if (candidate[chan][j][k].retries == RETRY_NONE) { spectrum[n] = '|'; } @@ -468,14 +479,14 @@ static void pick_best_candidate (int chan) spectrum[n] = '.'; } - /* Begining score depends on effort to get a valid frame CRC. */ + /* Beginning score depends on effort to get a valid frame CRC. */ if (candidate[chan][j][k].packet_p == NULL) { candidate[chan][j][k].score = 0; } else { - if (candidate[chan][j][k].is_fx25) { - candidate[chan][j][k].score = 9000 - 100 * candidate[chan][j][k].retries; + if (candidate[chan][j][k].fec_type != fec_type_none) { + candidate[chan][j][k].score = 9000 - 100 * candidate[chan][j][k].retries; // has FEC } else { /* Originally, this produced 0 for the PASSALL case. */ @@ -488,6 +499,9 @@ static void pick_best_candidate (int chan) } } + // FIXME: IL2p & FX.25 don't have CRC calculated. Must fill it in first. + + /* Bump it up slightly if others nearby have the same CRC. */ for (n = 0; n < num_bars; n++) { @@ -540,9 +554,9 @@ static void pick_best_candidate (int chan) candidate[chan][j][k].packet_p); } else { - dw_printf ("%d.%d.%d: ptr=%p, is_fx25=%d, retry=%d, age=%3d, crc=%04x, score=%d %s\n", chan, j, k, + dw_printf ("%d.%d.%d: ptr=%p, fec_type=%d, retry=%d, age=%3d, crc=%04x, score=%d %s\n", chan, j, k, candidate[chan][j][k].packet_p, - candidate[chan][j][k].is_fx25, + (int)(candidate[chan][j][k].fec_type), (int)(candidate[chan][j][k].retries), candidate[chan][j][k].age, candidate[chan][j][k].crc, @@ -601,7 +615,7 @@ static void pick_best_candidate (int chan) dlq_rec_frame (chan, j, k, candidate[chan][j][k].packet_p, candidate[chan][j][k].alevel, - candidate[chan][j][k].is_fx25, + candidate[chan][j][k].fec_type, (int)(candidate[chan][j][k].retries), spectrum); diff --git a/src/multi_modem.h b/src/multi_modem.h index 0734492b..51c3cde5 100644 --- a/src/multi_modem.h +++ b/src/multi_modem.h @@ -16,6 +16,9 @@ void multi_modem_process_sample (int c, int audio_sample); int multi_modem_get_dc_average (int chan); -void multi_modem_process_rec_frame (int chan, int subchan, int slice, unsigned char *fbuf, int flen, alevel_t alevel, retry_t retries, int is_fx25); +// Deprecated. Replace with ...packet +void multi_modem_process_rec_frame (int chan, int subchan, int slice, unsigned char *fbuf, int flen, alevel_t alevel, retry_t retries, fec_type_t fec_type); + +void multi_modem_process_rec_packet (int chan, int subchan, int slice, packet_t pp, alevel_t alevel, retry_t retries, fec_type_t fec_type); #endif diff --git a/src/pfilter.c b/src/pfilter.c index 626f0712..35767a67 100644 --- a/src/pfilter.c +++ b/src/pfilter.c @@ -1,7 +1,7 @@ // // This file is part of Dire Wolf, an amateur radio packet TNC. // -// Copyright (C) 2015, 2016 John Langner, WB2OSZ +// Copyright (C) 2015, 2016, 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 @@ -116,7 +116,7 @@ typedef struct pfstate_s { /* * Are we processing APRS or connected mode? - * This determines whch types of filters are available. + * This determines which types of filters are available. */ int is_aprs; @@ -220,7 +220,7 @@ int pfilter (int from_chan, int to_chan, char *filter, packet_t pp, int is_aprs) pfstate.from_chan = from_chan; pfstate.to_chan = to_chan; - /* Copy filter string, changing any control characers to spaces. */ + /* Copy filter string, changing any control characters to spaces. */ strlcpy (pfstate.filter_str, filter, sizeof(pfstate.filter_str)); @@ -235,7 +235,7 @@ int pfilter (int from_chan, int to_chan, char *filter, packet_t pp, int is_aprs) pfstate.is_aprs = is_aprs; if (is_aprs) { - decode_aprs (&pfstate.decoded, pp, 1); + decode_aprs (&pfstate.decoded, pp, 1, NULL); } next_token(&pfstate); @@ -546,7 +546,8 @@ static int parse_filter_spec (pfstate_t *pf) /* b - budlist */ else if (pf->token_str[0] == 'b' && ispunct(pf->token_str[1])) { - /* Budlist - source address */ + /* Budlist - AX.25 source address */ + /* Could be different than source encapsulated by 3rd party header. */ char addr[AX25_MAX_ADDR_LEN]; ax25_get_addr_with_ssid (pf->pp, AX25_SOURCE, addr); result = filt_bodgu (pf, addr); @@ -572,7 +573,7 @@ static int parse_filter_spec (pfstate_t *pf) else if (pf->token_str[0] == 'd' && ispunct(pf->token_str[1])) { int n; - // loop on all digipeaters + // Loop on all AX.25 digipeaters. result = 0; for (n = AX25_REPEATER_1; result == 0 && n < ax25_get_num_addr (pf->pp); n++) { // Consider only those with the H (has-been-used) bit set. @@ -599,7 +600,7 @@ static int parse_filter_spec (pfstate_t *pf) else if (pf->token_str[0] == 'v' && ispunct(pf->token_str[1])) { int n; - // loop on all digipeaters (mnemonic Via) + // loop on all AX.25 digipeaters (mnemonic Via) result = 0; for (n = AX25_REPEATER_1; result == 0 && n < ax25_get_num_addr (pf->pp); n++) { // This is different than the previous "d" filter. @@ -623,10 +624,15 @@ static int parse_filter_spec (pfstate_t *pf) } } -/* g - Addressee of message. */ +/* g - Addressee of message. e.g. "BLN*" for bulletins. */ else if (pf->token_str[0] == 'g' && ispunct(pf->token_str[1])) { - if (ax25_get_dti(pf->pp) == ':') { + if (pf->decoded.g_message_subtype == message_subtype_message || + pf->decoded.g_message_subtype == message_subtype_ack || + pf->decoded.g_message_subtype == message_subtype_rej || + pf->decoded.g_message_subtype == message_subtype_bulletin || + pf->decoded.g_message_subtype == message_subtype_nws || + pf->decoded.g_message_subtype == message_subtype_directed_query) { result = filt_bodgu (pf, pf->decoded.g_addressee); if (s_debug >= 2) { @@ -643,7 +649,7 @@ static int parse_filter_spec (pfstate_t *pf) } } -/* u - unproto (destination) */ +/* u - unproto (AX.25 destination) */ else if (pf->token_str[0] == 'u' && ispunct(pf->token_str[1])) { /* Probably want to exclude mic-e types */ @@ -668,7 +674,7 @@ static int parse_filter_spec (pfstate_t *pf) } } -/* t - type: position, weather, etc. */ +/* t - packet type: position, weather, telemetry, etc. */ else if (pf->token_str[0] == 't' && ispunct(pf->token_str[1])) { @@ -728,7 +734,7 @@ static int parse_filter_spec (pfstate_t *pf) (void) ax25_get_info (pf->pp, (unsigned char **)(&infop)); text_color_set(DW_COLOR_DEBUG); - if (*infop == ':' && ! is_telem_metadata(infop)) { + if (pf->decoded.g_packet_type == packet_type_message) { dw_printf (" %s returns %s for message to %s\n", pf->token_str, bool2text(result), pf->decoded.g_addressee); } else { @@ -776,7 +782,7 @@ static int parse_filter_spec (pfstate_t *pf) * -1 = error detected * * Description: Same function is used for all of these because they are so similar. - * Look for exact match to any of the specifed strings. + * Look for exact match to any of the specified strings. * All of them allow wildcarding with single * at the end. * *------------------------------------------------------------------------------*/ @@ -832,31 +838,17 @@ static int filt_bodgu (pfstate_t *pf, char *arg) * 0 = no * -1 = error detected * - * Description: The filter is based the type filtering described here: + * Description: The filter is loosely based the type filtering described here: * http://www.aprs-is.net/javAPRSFilter.aspx * - * Most of these simply check the first byte of the information part. - * Trying to detect NWS information is a little trickier. + * Mostly use g_packet_type and g_message_subtype from decode_aprs. + * + * References: * http://www.aprs-is.net/WX/ - * http://wxsvr.aprs.net.au/protocol-new.html + * http://wxsvr.aprs.net.au/protocol-new.html (has disappeared) * *------------------------------------------------------------------------------*/ -/* Telemetry metadata is a special case of message. */ -/* We want to categorize it as telemetry rather than message. */ - -int is_telem_metadata (char *infop) -{ - if (*infop != ':') return (0); - if (strlen(infop) < 16) return (0); - if (strncmp(infop+10, ":PARM.", 6) == 0) return (1); - if (strncmp(infop+10, ":UNIT.", 6) == 0) return (1); - if (strncmp(infop+10, ":EQNS.", 6) == 0) return (1); - if (strncmp(infop+10, ":BITS.", 6) == 0) return (1); - return (0); -} - - static int filt_t (pfstate_t *pf) { char src[AX25_MAX_ADDR_LEN]; @@ -873,108 +865,60 @@ static int filt_t (pfstate_t *pf) switch (*f) { case 'p': /* Position */ - if (*infop == '!') return (1); - if (*infop == '/') return (1); - if (*infop == '=') return (1); - if (*infop == '@') return (1); - if (*infop == '\'') return (1); // MIC-E - if (*infop == '`') return (1); // MIC-E - - // What if we have "_" symbol code for weather? - // Still consider as position. - // The same packet can match more than one type here. + if (pf->decoded.g_packet_type == packet_type_position) return(1); break; case 'o': /* Object */ - if (*infop == ';') return (1); + if (pf->decoded.g_packet_type == packet_type_object) return(1); break; case 'i': /* Item */ - if (*infop == ')') return (1); + if (pf->decoded.g_packet_type == packet_type_item) return(1); break; - case 'm': /* Message */ - if (*infop == ':' && ! is_telem_metadata(infop)) return (1); + case 'm': // Any "message." + if (pf->decoded.g_packet_type == packet_type_message) return(1); break; case 'q': /* Query */ - if (*infop == '?') return (1); + if (pf->decoded.g_packet_type == packet_type_query) return(1); break; case 'c': /* station Capabilities - my extension */ /* Most often used for IGate statistics. */ - if (*infop == '<') return (1); + if (pf->decoded.g_packet_type == packet_type_capabilities) return(1); break; case 's': /* Status */ - if (*infop == '>') return (1); + if (pf->decoded.g_packet_type == packet_type_status) return(1); break; - case 't': /* Telemetry */ - if (*infop == 'T') return (1); - if (is_telem_metadata(infop)) return (1); + case 't': /* Telemetry data or metadata */ + if (pf->decoded.g_packet_type == packet_type_telemetry) return(1); break; case 'u': /* User-defined */ - if (*infop == '{') return (1); + if (pf->decoded.g_packet_type == packet_type_userdefined) return(1); break; - case 'h': /* third party Header - my extension */ - if (*infop == '}') return (1); + case 'h': /* has third party Header - my extension */ + if (pf->decoded.g_has_thirdparty_header) return (1); break; case 'w': /* Weather */ - if (*infop == '*') return (1); // Peet Bros - if (*infop == '_') return (1); // Weather report, no position. - if (strncmp(infop, "!!", 2) == 0) return(1); // Ultimeter 2000. - - /* '$' is normally raw GPS. Check for special case. */ - - if (strncmp(infop, "$ULTW", 5) == 0) return (1); - - /* Positions !=/@ with symbol code _ are weather. */ - - if (strchr("!=/@", *infop) != NULL && - pf->decoded.g_symbol_code == '_') return (1); + if (pf->decoded.g_packet_type == packet_type_weather) return(1); + /* Positions !=/@ with symbol code _ are weather. */ /* Object with _ symbol is also weather. APRS protocol spec page 66. */ + // Can't use *infop because it would not work with 3rd party header. - if (*infop == ';' && - pf->decoded.g_symbol_code == '_') return (1); - -// TODO: need more test cases at end for new weather cases. - + if ((pf->decoded.g_packet_type == packet_type_position || + pf->decoded.g_packet_type == packet_type_object) && pf->decoded.g_symbol_code == '_') return (1); break; case 'n': /* NWS format */ -/* - * This is the interesting case. - * The source must be exactly 6 upper case letters, no SSID. - */ - if (strlen(src) != 6) break; - if (! isupper(src[0])) break; - if (! isupper(src[1])) break; - if (! isupper(src[2])) break; - if (! isupper(src[3])) break; - if (! isupper(src[4])) break; - if (! isupper(src[5])) break; -/* - * We can have a "message" with addressee starting with NWS, SKY, or BOM (Australian version.) - */ - if (strncmp(infop, ":NWS", 4) == 0) return (1); - if (strncmp(infop, ":SKY", 4) == 0) return (1); - if (strncmp(infop, ":BOM", 4) == 0) return (1); -/* - * Or we can have an object. - * It's not exactly clear how to distiguish this from other objects. - * It looks like the first 3 characters of the source should be the same - * as the first 3 characters of the addressee. - */ - if (infop[0] == ';' && - infop[1] == src[0] && - infop[2] == src[1] && - infop[3] == src[2]) return (1); + if (pf->decoded.g_packet_type == packet_type_nws) return(1); break; default: @@ -990,6 +934,7 @@ static int filt_t (pfstate_t *pf) + /*------------------------------------------------------------------------------ * * Name: filt_r @@ -1278,7 +1223,8 @@ static int filt_s (pfstate_t *pf) * * Name: filt_i * - * Purpose: IGate messaging default behavior. + * Purpose: IGate messaging filter. + * This would make sense only for IS>RF direction. * * Inputs: pf - Pointer to current state information. * token_str should contain something of format: @@ -1290,7 +1236,7 @@ static int filt_s (pfstate_t *pf) * -1 = error detected * * Description: Selection is based on time since last heard on RF, and distance - * in terms of digipeater hops and/or phyiscal location. + * in terms of digipeater hops and/or physical location. * * i/time * i/time/hops @@ -1307,7 +1253,7 @@ static int filt_s (pfstate_t *pf) * The rest is distanced, in kilometers, from given point. * * Examples: - * i/60/0 Heard in past 60 minutes directly. + * i/180/0 Heard in past 3 hours directly. * i/45 Past 45 minutes, default max digi hops. * i/180/3 Default time (3 hours), max 3 digi hops. * i/180/8/42.6/-71.3/50. @@ -1317,13 +1263,50 @@ static int filt_s (pfstate_t *pf) * The basic idea is that we want to transmit a "message" only if the * addressee has been heard recently and is not too far away. * + * That is so we can distinguish messages addressed to a specific + * station, and other sundry uses of the addressee field. + * * After passing along a "message" we will also allow the next * position report from the sender of the "message." * That is done somewhere else. We are not concerned with it here. * * IMHO, the rules here are too restrictive. * - * FIXME -explain + * The APRS-IS would send a "message" to my IGate only if the addressee + * has been heard nearby recently. 180 minutes, I believe. + * Why would I not want to transmit it? + * + * Discussion: In retrospect, I think this is far too complicated. + * In a future release, I think at options other than time should be removed. + * Messages have more value than most packets. Why reduce the chance of successful delivery? + * + * Consider the following scenario: + * + * (1) We hear AA1PR-9 by a path of 4 digipeaters. + * Looking closer, it's probably only two because there are left over WIDE1-0 and WIDE2-0. + * + * Digipeater WIDE2 (probably N3LLO-3) audio level = 72(19/15) [NONE] _|||||___ + * [0.3] AA1PR-9>APY300,K1EQX-7,WIDE1,N3LLO-3,WIDE2*,ARISS::ANSRVR :cq hotg vt aprsthursday{01<0x0d> + * + * (2) APRS-IS sends a response to us. + * + * [ig>tx] ANSRVR>APWW11,KJ4ERJ-15*,TCPIP*,qAS,KJ4ERJ-15::AA1PR-9 :N:HOTG 161 Messages Sent{JL} + * + * (3) Here is our analysis of whether it should be sent to RF. + * + * Was message addressee AA1PR-9 heard in the past 180 minutes, with 2 or fewer digipeater hops? + * No, AA1PR-9 was last heard over the radio with 4 digipeater hops 0 minutes ago. + * + * The wrong hop count caused us to drop a packet that should have been transmitted. + * We could put in a hack to not count the "WIDE*-0" addresses. + * That is not correct because other prefixes could be used and we don't know + * what they are for other digipeaters. + * I think the best solution is to simply ignore the hop count. + * + * Release 1.7: I got overly ambitious and now realize this is just giving people too much + * "rope to hang themselves," drop messages unexpectedly, and accidentally break messaging. + * Change documentation to mention only the time limit. + * The other functionality will be undocumented and maybe disappear over time. * *------------------------------------------------------------------------------*/ @@ -1352,9 +1335,9 @@ static int filt_i (pfstate_t *pf) double km = G_UNKNOWN; - char src[AX25_MAX_ADDR_LEN]; - char *infop = NULL; - int info_len; + //char src[AX25_MAX_ADDR_LEN]; + //char *infop = NULL; + //int info_len; //char *f; //char addressee[AX25_MAX_ADDR_LEN]; @@ -1428,20 +1411,7 @@ static int filt_i (pfstate_t *pf) * Get source address and info part. * Addressee has already been extracted into pf->decoded.g_addressee. */ - - memset (src, 0, sizeof(src)); - ax25_get_addr_with_ssid (pf->pp, AX25_SOURCE, src); - info_len = ax25_get_info (pf->pp, (unsigned char **)(&infop)); - - if (infop == NULL) return (0); - if (info_len < 1) return (0); - -// Determine packet type. We are interested only in "message." -// Telemetry metadata is not considered a message. - - if (*infop != ':') return (0); - if (is_telem_metadata(infop)) return (0); - + if (pf->decoded.g_packet_type != packet_type_message) return(0); #if defined(PFTEST) || defined(DIGITEST) // TODO: test functionality too, not just syntax. @@ -1481,7 +1451,7 @@ static int filt_i (pfstate_t *pf) * the past minute, rather than the usual 180 minutes for the addressee. */ - was_heard = mheard_was_recently_nearby ("source", src, 1, 0, G_UNKNOWN, G_UNKNOWN, G_UNKNOWN); + was_heard = mheard_was_recently_nearby ("source", pf->decoded.g_src, 1, 0, G_UNKNOWN, G_UNKNOWN, G_UNKNOWN); if (was_heard) return (0); @@ -1635,24 +1605,29 @@ int main () pftest (112, "t/t", "WM1X>APU25N:@210147z4235.39N/07106.58W_359/000g000t027r000P000p000h89b10234/WX REPORT {UIV32N}<0x0d>", 0); pftest (113, "t/w", "WM1X>APU25N:@210147z4235.39N/07106.58W_359/000g000t027r000P000p000h89b10234/WX REPORT {UIV32N}<0x0d>", 1); - /* Telemetry metadata is a special case of message. */ + /* Telemetry metadata should not be classified as message. */ pftest (114, "t/t", "KJ4SNT>APMI04::KJ4SNT :PARM.Vin,Rx1h,Dg1h,Eff1h,Rx10m,O1,O2,O3,O4,I1,I2,I3,I4", 1); pftest (115, "t/m", "KJ4SNT>APMI04::KJ4SNT :PARM.Vin,Rx1h,Dg1h,Eff1h,Rx10m,O1,O2,O3,O4,I1,I2,I3,I4", 0); pftest (116, "t/t", "KB1GKN-10>APRX27,UNCAN,WIDE1*:T#491,4.9,0.3,25.0,0.0,1.0,00000000", 1); + /* Bulletins should not be considered to be messages. Was bug in 1.6. */ + pftest (117, "t/m", "A>B::W1AW :test", 1); + pftest (118, "t/m", "A>B::BLN :test", 0); + pftest (119, "t/m", "A>B::NWS :test", 0); - pftest (120, "t/p", "CWAPID>APRS::NWS-TTTTT:DDHHMMz,ADVISETYPE,zcs{seq#", 0); + // https://www.aprs-is.net/WX/ + pftest (121, "t/p", "CWAPID>APRS::NWS-TTTTT:DDHHMMz,ADVISETYPE,zcs{seq#", 0); pftest (122, "t/p", "CWAPID>APRS::SKYCWA :DDHHMMz,ADVISETYPE,zcs{seq#", 0); pftest (123, "t/p", "CWAPID>APRS:;CWAttttz *DDHHMMzLATLONICONADVISETYPE{seq#", 0); pftest (124, "t/n", "CWAPID>APRS::NWS-TTTTT:DDHHMMz,ADVISETYPE,zcs{seq#", 1); pftest (125, "t/n", "CWAPID>APRS::SKYCWA :DDHHMMz,ADVISETYPE,zcs{seq#", 1); - pftest (126, "t/n", "CWAPID>APRS:;CWAttttz *DDHHMMzLATLONICONADVISETYPE{seq#", 1); + //pftest (126, "t/n", "CWAPID>APRS:;CWAttttz *DDHHMMzLATLONICONADVISETYPE{seq#", 1); pftest (127, "t/", "CWAPID>APRS:;CWAttttz *DDHHMMzLATLONICONADVISETYPE{seq#", 0); pftest (128, "t/c", "S0RCE>DEST:DEST:DEST:}thirdpartyheaderwhatever", 1); - pftest (131, "t/c", "S0RCE>DEST:}thirdpartyheaderwhatever", 0); + pftest (130, "t/h", "S0RCE>DEST:}WB2OSZ-5>APDW12,DIGI1,DIGI2*,DIGI3,DIGI4:!4237.14NS07120.83W#PHG7140Chelmsford MA", 1); + pftest (131, "t/c", "S0RCE>DEST:}WB2OSZ-5>APDW12,DIGI1,DIGI2*,DIGI3,DIGI4:!4237.14NS07120.83W#PHG7140Chelmsford MA", 0); pftest (140, "r/42.6/-71.3/10", "WB2OSZ-5>APDW12,WIDE1-1,WIDE2-1:!4237.14NS07120.83W#PHG7140Chelmsford MA", 1); pftest (141, "r/42.6/-71.3/10", "WA1PLE-5>APWW10,W1MHL,N8VIM,WIDE2*:@022301h4208.75N/07115.16WoAPRS-IS for Win32", 0); @@ -1707,13 +1682,13 @@ int main () pftest (203, "t/w t/w", "CWAPID>APRS:;CWAttttz *DDHHMMzLATLONICONADVISETYPE{seq#", -1); pftest (204, "r/42.6/-71.3", "WA1PLE-5>APWW10,W1MHL,N8VIM,WIDE2*:@022301h4208.75N/07115.16WoAPRS-IS for Win32", -1); - pftest (220, "i/30/8/42.6/-71.3/50", "WB2OSZ-5>APDW14::W2UB :Happy Birthday{001", 1); - pftest (222, "i/30/8/42.6/-71.3/", "WB2OSZ-5>APDW14::W2UB :Happy Birthday{001", -1); - pftest (223, "i/30/8/42.6/-71.3", "WB2OSZ-5>APDW14::W2UB :Happy Birthday{001", -1); - pftest (224, "i/30/8/42.6/", "WB2OSZ-5>APDW14::W2UB :Happy Birthday{001", -1); - pftest (225, "i/30/8/42.6", "WB2OSZ-5>APDW14::W2UB :Happy Birthday{001", -1); - pftest (226, "i/30/8/", "WB2OSZ-5>APDW14::W2UB :Happy Birthday{001", 1); - pftest (227, "i/30/8", "WB2OSZ-5>APDW14::W2UB :Happy Birthday{001", 1); + pftest (210, "i/30/8/42.6/-71.3/50", "WB2OSZ-5>APDW14::W2UB :Happy Birthday{001", 1); + pftest (212, "i/30/8/42.6/-71.3/", "WB2OSZ-5>APDW14::W2UB :Happy Birthday{001", -1); + pftest (213, "i/30/8/42.6/-71.3", "WB2OSZ-5>APDW14::W2UB :Happy Birthday{001", -1); + pftest (214, "i/30/8/42.6/", "WB2OSZ-5>APDW14::W2UB :Happy Birthday{001", -1); + pftest (215, "i/30/8/42.6", "WB2OSZ-5>APDW14::W2UB :Happy Birthday{001", -1); + pftest (216, "i/30/8/", "WB2OSZ-5>APDW14::W2UB :Happy Birthday{001", 1); + pftest (217, "i/30/8", "WB2OSZ-5>APDW14::W2UB :Happy Birthday{001", 1); // FIXME: behaves differently on Windows and Linux. Why? // On Windows we have our own version of strsep because it's not in the MS library. @@ -1721,7 +1696,12 @@ int main () //pftest (228, "i/30/", "WB2OSZ-5>APDW14::W2UB :Happy Birthday{001", 1); pftest (229, "i/30", "WB2OSZ-5>APDW14::W2UB :Happy Birthday{001", 1); - pftest (230, "i/", "WB2OSZ-5>APDW14::W2UB :Happy Birthday{001", -1); + pftest (230, "i/30", "X>X:}WB2OSZ-5>APDW14::W2UB :Happy Birthday{001", 1); + pftest (231, "i/", "WB2OSZ-5>APDW14::W2UB :Happy Birthday{001", -1); + + // Besure bulletins and telemetry metadata don't get included. + pftest (234, "i/30", "KJ4SNT>APMI04::KJ4SNT :PARM.Vin,Rx1h,Dg1h,Eff1h,Rx10m,O1,O2,O3,O4,I1,I2,I3,I4", 0); + pftest (235, "i/30", "A>B::BLN :test", 0); pftest (240, "s/", "WB2OSZ-5>APDW12:!4237.14N/07120.83WOPHG7140Chelmsford MA", -1); pftest (241, "s/'/O/-/#/_", "WB2OSZ-5>APDW12:!4237.14N/07120.83WOPHG7140Chelmsford MA", -1); @@ -1731,12 +1711,36 @@ int main () pftest (245, "s//", "WB2OSZ-5>APDW12:!4237.14N/07120.83WOPHG7140Chelmsford MA", -1); pftest (246, "s///", "WB2OSZ-5>APDW12:!4237.14N/07120.83WOPHG7140Chelmsford MA", -1); + // Third party header - done properly in 1.7. + // Packet filter t/h is no longer a mutually exclusive packet type. + // Now it is an independent attribute and the encapsulated part is evaluated. + + pftest (250, "o/home", "A>B:}WB2OSZ>APDW12,WIDE1-1,WIDE2-1:;home *111111z4237.14N/07120.83W-Chelmsford MA", 1); + pftest (251, "t/p", "A>B:}W1WRA-7>TRSY3T,WIDE1-1,WIDE2-1:`c-:l!hK\\>\"4b}=<0x0d>", 1); + pftest (252, "i/180", "A>B:}WB2OSZ-5>APDW14::W2UB :Happy Birthday{001", 1); + pftest (253, "t/m", "A>B:}WB2OSZ-5>APDW14::W2UB :Happy Birthday{001", 1); + pftest (254, "r/42.6/-71.3/10", "A>B:}WB2OSZ-5>APDW12,WIDE1-1,WIDE2-1:!4237.14NS07120.83W#PHG7140Chelmsford MA", 1); + pftest (254, "r/42.6/-71.3/10", "A>B:}WA1PLE-5>APWW10,W1MHL,N8VIM,WIDE2*:@022301h4208.75N/07115.16WoAPRS-IS for Win32", 0); + pftest (255, "t/h", "KB1GKN-10>APRX27,UNCAN,WIDE1*:T#491,4.9,0.3,25.0,0.0,1.0,00000000", 0); + pftest (256, "t/h", "A>B:}KB1GKN-10>APRX27,UNCAN,WIDE1*:T#491,4.9,0.3,25.0,0.0,1.0,00000000", 1); + pftest (258, "t/t", "A>B:}KB1GKN-10>APRX27,UNCAN,WIDE1*:T#491,4.9,0.3,25.0,0.0,1.0,00000000", 1); + pftest (259, "t/t", "A>B:}KJ4SNT>APMI04::KJ4SNT :PARM.Vin,Rx1h,Dg1h,Eff1h,Rx10m,O1,O2,O3,O4,I1,I2,I3,I4", 1); + + pftest (270, "g/BLN*", "WB2OSZ>APDW17::BLN1xxxxx:bulletin text", 1); + pftest (271, "g/BLN*", "A>B:}WB2OSZ>APDW17::BLN1xxxxx:bulletin text", 1); + pftest (272, "g/BLN*", "A>B:}WB2OSZ>APDW17::W1AW :xxxx", 0); + + pftest (273, "g/NWS*", "WB2OSZ>APDW17::NWS-xxxxx:weather bulletin", 1); + pftest (274, "g/NWS*", "A>B:}WB2OSZ>APDW17::NWS-xxxxx:weather bulletin", 1); + pftest (275, "g/NWS*", "A>B:}WB2OSZ>APDW17::W1AW :xxxx", 0); + +// TODO: add b/ with 3rd party header. -// TODO: to be continued... +// TODO: to be continued... directed query ... if (error_count > 0) { text_color_set (DW_COLOR_ERROR); - dw_printf ("\nPacket Filtering Test - FAILED!\n"); + dw_printf ("\nPacket Filtering Test - FAILED! %d errors\n", error_count); exit (EXIT_FAILURE); } text_color_set (DW_COLOR_REC); diff --git a/src/ptt.c b/src/ptt.c index 2a943006..5187f1df 100644 --- a/src/ptt.c +++ b/src/ptt.c @@ -1,7 +1,7 @@ // // This file is part of Dire Wolf, an amateur radio packet TNC. // -// Copyright (C) 2011, 2013, 2014, 2015, 2016, 2017 John Langner, WB2OSZ +// Copyright (C) 2011, 2013, 2014, 2015, 2016, 2017, 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 @@ -126,7 +126,7 @@ This is documented in the User Guide, section called, "Hamlib PTT Example 2: Use GPIO of USB audio adapter. (e.g. DMK URI)" - It's rather involved and the explantion doesn't cover the case of multiple + It's rather involved and the explanation doesn't cover the case of multiple USB-Audio adapters. It would be nice to have a little script which lists all of the USB-Audio adapters and the corresponding /dev/hidraw device. ( We now have it. The included "cm108" application. ) @@ -162,20 +162,21 @@ #include #endif -#ifdef USE_CM108 -#include "cm108.h" -#endif - /* So we can have more common code for fd. */ typedef int HANDLE; #define INVALID_HANDLE_VALUE (-1) -#endif +#endif /* __WIN32__ */ + +#ifdef USE_CM108 +#include "cm108.h" +#endif /* USE_CM108 */ #include "textcolor.h" #include "audio.h" #include "ptt.h" #include "dlq.h" +#include "demod.h" // to mute recv audio during xmit if half duplex. #if __WIN32__ @@ -357,6 +358,9 @@ static void get_access_to_gpio (const char *path) * We don't have permission. * Try a hack which requires that the user be set up to use sudo without a password. */ +// FIXME: I think this was a horrible work around for some early release that +// did not give gpio permission to the pi user. This should go. +// Provide recovery instructions when there is a permission failure. if (ptt_debug_level >= 2) { text_color_set(DW_COLOR_ERROR); // debug message but different color so it stands out. @@ -494,6 +498,13 @@ void export_gpio(int ch, int ot, int invert, int direction) * matching the pattern "gpio61_*". * * We are finally implementing the third choice. + */ + +/* + * Then we have the Odroid board with GPIO numbers starting around 480. + * Can we simply use those numbers? + * Apparently, the export names look like GPIOX.17 + * https://wiki.odroid.com/odroid-c4/hardware/expansion_connectors#gpio_map_for_wiringpi_library */ struct dirent **file_list; @@ -737,7 +748,7 @@ void ptt_init (struct audio_s *audio_config_p) for (ch = 0; ch < MAX_CHANS; ch++) { - if (audio_config_p->achan[ch].medium == MEDIUM_RADIO) { + if (audio_config_p->chan_medium[ch] == MEDIUM_RADIO) { int ot; for (ot = 0; ot < NUM_OCTYPES; ot++) { @@ -768,7 +779,7 @@ void ptt_init (struct audio_s *audio_config_p) int j, k; for (j = ch; j >= 0; j--) { - if (audio_config_p->achan[j].medium == MEDIUM_RADIO) { + if (audio_config_p->chan_medium[j] == MEDIUM_RADIO) { for (k = ((j==ch) ? (ot - 1) : (NUM_OCTYPES-1)); k >= 0; k--) { if (strcmp(audio_config_p->achan[ch].octrl[ot].ptt_device,audio_config_p->achan[j].octrl[k].ptt_device) == 0) { fd = ptt_fd[j][k]; @@ -851,7 +862,7 @@ void ptt_init (struct audio_s *audio_config_p) using_gpio = 0; for (ch=0; chachan[ch].medium == MEDIUM_RADIO) { + if (save_audio_config_p->chan_medium[ch] == MEDIUM_RADIO) { int ot; for (ot = 0; ot < NUM_OCTYPES; ot++) { if (audio_config_p->achan[ch].octrl[ot].ptt_method == PTT_METHOD_GPIO) { @@ -876,7 +887,7 @@ void ptt_init (struct audio_s *audio_config_p) */ 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) { int ot; // output control type, PTT, DCD, CON, ... int it; // input control type @@ -908,13 +919,13 @@ 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++) { - if (save_audio_config_p->achan[ch].medium == MEDIUM_RADIO) { + if (save_audio_config_p->chan_medium[ch] == MEDIUM_RADIO) { int ot; for (ot = 0; ot < NUM_OCTYPES; ot++) { if (audio_config_p->achan[ch].octrl[ot].ptt_method == PTT_METHOD_LPT) { /* Can't open the same device more than once so we */ - /* need more logic to look for the case of mutiple radio */ + /* need more logic to look for the case of multiple radio */ /* channels using different pins of the LPT port. */ /* Did some earlier channel use the same ptt device name? */ @@ -923,7 +934,7 @@ void ptt_init (struct audio_s *audio_config_p) int j, k; for (j = ch; j >= 0; j--) { - if (audio_config_p->achan[j].medium == MEDIUM_RADIO) { + if (audio_config_p->chan_medium[j] == MEDIUM_RADIO) { for (k = ((j==ch) ? (ot - 1) : (NUM_OCTYPES-1)); k >= 0; k--) { if (strcmp(audio_config_p->achan[ch].octrl[ot].ptt_device,audio_config_p->achan[j].octrl[k].ptt_device) == 0) { fd = ptt_fd[j][k]; @@ -975,11 +986,13 @@ void ptt_init (struct audio_s *audio_config_p) #ifdef USE_HAMLIB 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) { int ot; for (ot = 0; ot < NUM_OCTYPES; ot++) { if (audio_config_p->achan[ch].octrl[ot].ptt_method == PTT_METHOD_HAMLIB) { if (ot == OCTYPE_PTT) { + int err = -1; + int tries = 0; /* For "AUTO" model, try to guess what is out there. */ @@ -1044,13 +1057,24 @@ void ptt_init (struct audio_s *audio_config_p) rig[ch][ot]->state.rigport.parm.serial.parity = RIG_PARITY_NONE; rig[ch][ot]->state.rigport.parm.serial.handshake = RIG_HANDSHAKE_NONE; } - int err = rig_open(rig[ch][ot]); + tries = 0; + do { + // Try up to 5 times, Hamlib can take a moment to finish init + err = rig_open(rig[ch][ot]); + if (++tries > 5) { + break; + } else if (err != RIG_OK) { + text_color_set(DW_COLOR_INFO); + dw_printf ("Retrying Hamlib Rig open...\n"); + sleep (5); + } + } while (err != RIG_OK); if (err != RIG_OK) { text_color_set(DW_COLOR_ERROR); dw_printf ("Hamlib Rig open error %d: %s\n", err, rigerror(err)); rig_cleanup (rig[ch][ot]); rig[ch][ot] = NULL; - continue; + exit (1); } /* Successful. Later code should check for rig[ch][ot] not NULL. */ @@ -1075,7 +1099,7 @@ void ptt_init (struct audio_s *audio_config_p) for (ch = 0; ch < MAX_CHANS; ch++) { - if (audio_config_p->achan[ch].medium == MEDIUM_RADIO) { + if (audio_config_p->chan_medium[ch] == MEDIUM_RADIO) { int ot; for (ot = 0; ot < NUM_OCTYPES; ot++) { if (audio_config_p->achan[ch].octrl[ot].ptt_method == PTT_METHOD_CM108) { @@ -1096,7 +1120,7 @@ void ptt_init (struct audio_s *audio_config_p) /* Why doesn't it transmit? Probably forgot to specify PTT option. */ for (ch=0; chachan[ch].medium == MEDIUM_RADIO) { + 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); @@ -1131,6 +1155,8 @@ void ptt_init (struct audio_s *audio_config_p) * *--------------------------------------------------------------------*/ +// JWL - save status and new get_ptt function. + void ptt_set (int ot, int chan, int ptt_signal) { @@ -1148,12 +1174,25 @@ void ptt_set (int ot, int chan, int ptt_signal) assert (chan >= 0 && chan < MAX_CHANS); - if ( save_audio_config_p->achan[chan].medium != MEDIUM_RADIO) { + if ( save_audio_config_p->chan_medium[chan] != MEDIUM_RADIO) { text_color_set(DW_COLOR_ERROR); dw_printf ("Internal error, ptt_set ( %s, %d, %d ), did not expect invalid channel.\n", otnames[ot], chan, ptt); return; } +// New in 1.7. +// A few people have a really bad audio cross talk situation where they receive their own transmissions. +// It usually doesn't cause a problem but it is confusing to look at. +// "half duplex" setting applied only to the transmit logic. i.e. wait for clear channel before sending. +// Receiving was still active. +// I think the simplest solution is to mute/unmute the audio input at this point if not full duplex. + +#ifndef TEST + if ( ot == OCTYPE_PTT && ! save_audio_config_p->achan[chan].fulldup) { + demod_mute_input (chan, ptt_signal); + } +#endif + /* * The data link state machine has an interest in activity on the radio channel. * This is a very convenient place to get that information. @@ -1359,7 +1398,7 @@ int get_input (int it, int chan) assert (it >= 0 && it < NUM_ICTYPES); assert (chan >= 0 && chan < MAX_CHANS); - if ( save_audio_config_p->achan[chan].medium != MEDIUM_RADIO) { + if ( save_audio_config_p->chan_medium[chan] != MEDIUM_RADIO) { text_color_set(DW_COLOR_ERROR); dw_printf ("Internal error, get_input ( %d, %d ), did not expect invalid channel.\n", it, chan); return -1; @@ -1423,7 +1462,7 @@ void ptt_term (void) int n; for (n = 0; n < MAX_CHANS; n++) { - if (save_audio_config_p->achan[n].medium == MEDIUM_RADIO) { + if (save_audio_config_p->chan_medium[n] == MEDIUM_RADIO) { int ot; for (ot = 0; ot < NUM_OCTYPES; ot++) { ptt_set (ot, n, 0); @@ -1432,7 +1471,7 @@ void ptt_term (void) } for (n = 0; n < MAX_CHANS; n++) { - if (save_audio_config_p->achan[n].medium == MEDIUM_RADIO) { + if (save_audio_config_p->chan_medium[n] == MEDIUM_RADIO) { int ot; for (ot = 0; ot < NUM_OCTYPES; ot++) { if (ptt_fd[n][ot] != INVALID_HANDLE_VALUE) { @@ -1450,7 +1489,7 @@ void ptt_term (void) #ifdef USE_HAMLIB for (n = 0; n < MAX_CHANS; n++) { - if (save_audio_config_p->achan[n].medium == MEDIUM_RADIO) { + if (save_audio_config_p->chan_medium[n] == MEDIUM_RADIO) { int ot; for (ot = 0; ot < NUM_OCTYPES; ot++) { if (rig[n][ot] != NULL) { @@ -1489,14 +1528,14 @@ int main () my_audio_config.adev[0].num_channels = 2; - my_audio_config.achan[0].medium = MEDIUM_RADIO; + my_audio_config.chan_medium[0] = MEDIUM_RADIO; my_audio_config.achan[0].octrl[OCTYPE_PTT].ptt_method = PTT_METHOD_SERIAL; // TODO: device should be command line argument. strlcpy (my_audio_config.achan[0].octrl[OCTYPE_PTT].ptt_device, "COM3", sizeof(my_audio_config.achan[0].octrl[OCTYPE_PTT].ptt_device)); //strlcpy (my_audio_config.achan[0].octrl[OCTYPE_PTT].ptt_device, "/dev/ttyUSB0", sizeof(my_audio_config.achan[0].octrl[OCTYPE_PTT].ptt_device)); my_audio_config.achan[0].octrl[OCTYPE_PTT].ptt_line = PTT_LINE_RTS; - my_audio_config.achan[1].medium = MEDIUM_RADIO; + my_audio_config.chan_medium[1] = MEDIUM_RADIO; my_audio_config.achan[1].octrl[OCTYPE_PTT].ptt_method = PTT_METHOD_SERIAL; strlcpy (my_audio_config.achan[1].octrl[OCTYPE_PTT].ptt_device, "COM3", sizeof(my_audio_config.achan[1].octrl[OCTYPE_PTT].ptt_device)); //strlcpy (my_audio_config.achan[1].octrl[OCTYPE_PTT].ptt_device, "/dev/ttyUSB0", sizeof(my_audio_config.achan[1].octrl[OCTYPE_PTT].ptt_device)); @@ -1570,7 +1609,7 @@ int main () memset (&my_audio_config, 0, sizeof(my_audio_config)); my_audio_config.adev[0].num_channels = 1; - my_audio_config.achan[0].medium = MEDIUM_RADIO; + my_audio_config.chan_medium[0] = MEDIUM_RADIO; my_audio_config.adev[0].octrl[OCTYPE_PTT].ptt_method = PTT_METHOD_GPIO; my_audio_config.adev[0].octrl[OCTYPE_PTT].out_gpio_num = 25; @@ -1601,10 +1640,10 @@ int main () #if 0 memset (&my_audio_config, 0, sizeof(my_audio_config)); my_audio_config.num_channels = 2; - my_audio_config.achan[0].medium = MEDIUM_RADIO; + my_audio_config.chan_medium[0] = MEDIUM_RADIO; my_audio_config.adev[0].octrl[OCTYPE_PTT].ptt_method = PTT_METHOD_LPT; my_audio_config.adev[0].octrl[OCTYPE_PTT].ptt_lpt_bit = 0; - my_audio_config.achan[1].medium = MEDIUM_RADIO; + my_audio_config.chan_medium[1] = MEDIUM_RADIO; my_audio_config.adev[1].octrl[OCTYPE_PTT].ptt_method = PTT_METHOD_LPT; my_audio_config.adev[1].octrl[OCTYPE_PTT].ptt_lpt_bit = 1; diff --git a/src/recv.c b/src/recv.c index f5c78167..49040e55 100644 --- a/src/recv.c +++ b/src/recv.c @@ -42,7 +42,7 @@ * multi_modem_process_sample(s) * * - * When a packet is succesfully decoded, somebody calls + * When a packet is successfully decoded, somebody calls * app_process_rec_frame, also in direwolf.c * * @@ -107,7 +107,6 @@ #include "recv.h" #include "dtmf.h" #include "aprs_tt.h" -#include "dtime_now.h" #include "ax25_link.h" @@ -208,7 +207,7 @@ static void * recv_adev_thread (void *arg) int eof; /* This audio device can have one (mono) or two (stereo) channels. */ - /* Find number of the first channel. */ + /* Find number of the first channel and number of channels. */ int first_chan = ADEVFIRSTCHAN(a); int num_chan = save_pa->adev[a].num_channels; @@ -235,6 +234,8 @@ static void * recv_adev_thread (void *arg) if (audio_sample >= 256 * 256) eof = 1; + // Future? provide more flexible mapping. + // i.e. for each valid channel where audio_source[] is first_chan+c. multi_modem_process_sample(first_chan + c, audio_sample); @@ -263,14 +264,14 @@ static void * recv_adev_thread (void *arg) aprs_tt_button (first_chan + c, tt); } } - } + } // for c is just 0 or 0 then 1 /* When a complete frame is accumulated, */ /* dlq_rec_frame, is called. */ /* recv_process, below, drains the queue. */ - } + } // while !eof on audio stream // What should we do now? // Seimply terminate the application? @@ -338,7 +339,7 @@ void recv_process (void) * - Digipeater. */ - app_process_rec_packet (pitem->chan, pitem->subchan, pitem->slice, pitem->pp, pitem->alevel, pitem->is_fx25, pitem->retries, pitem->spectrum); + app_process_rec_packet (pitem->chan, pitem->subchan, pitem->slice, pitem->pp, pitem->alevel, pitem->fec_type, pitem->retries, pitem->spectrum); /* diff --git a/src/rrbb.c b/src/rrbb.c index 82d8aaea..e787dae5 100644 --- a/src/rrbb.c +++ b/src/rrbb.c @@ -51,8 +51,8 @@ #define MAGIC2 0x56788765 -static int new_count = 0; -static int delete_count = 0; +volatile static int new_count = 0; +volatile static int delete_count = 0; /*********************************************************************************** @@ -88,7 +88,11 @@ rrbb_t rrbb_new (int chan, int subchan, int slice, int is_scrambled, int descram assert (slice >= 0 && slice < MAX_SLICERS); result = malloc(sizeof(struct rrbb_s)); - + if (result == NULL) { + text_color_set(DW_COLOR_ERROR); + dw_printf ("FATAL ERROR: Out of memory.\n"); + exit (EXIT_FAILURE); + } result->magic1 = MAGIC1; result->chan = chan; result->subchan = subchan; @@ -421,6 +425,50 @@ alevel_t rrbb_get_audio_level (rrbb_t b) +/*********************************************************************************** + * + * Name: rrbb_set_speed_error + * + * Purpose: Set speed error of the received frame. + * + * Inputs: b Handle for bit array. + * speed_error In percentage. + * + ***********************************************************************************/ + +void rrbb_set_speed_error (rrbb_t b, float speed_error) +{ + assert (b != NULL); + assert (b->magic1 == MAGIC1); + assert (b->magic2 == MAGIC2); + + b->speed_error = speed_error; +} + + +/*********************************************************************************** + * + * Name: rrbb_get_speed_error + * + * Purpose: Get speed error of the received frame. + * + * Inputs: b Handle for bit array. + * + * Returns: speed error in percentage. + * + ***********************************************************************************/ + +float rrbb_get_speed_error (rrbb_t b) +{ + assert (b != NULL); + assert (b->magic1 == MAGIC1); + assert (b->magic2 == MAGIC2); + + return (b->speed_error); +} + + + /*********************************************************************************** * * Name: rrbb_get_is_scrambled @@ -484,6 +532,7 @@ int rrbb_get_prev_descram (rrbb_t b) } + /* end rrbb.c */ diff --git a/src/rrbb.h b/src/rrbb.h index 4b283726..894a448f 100644 --- a/src/rrbb.h +++ b/src/rrbb.h @@ -33,6 +33,7 @@ typedef struct rrbb_s { int slice; /* Which slicer. */ alevel_t alevel; /* Received audio level at time of frame capture. */ + float speed_error; /* Received data speed error as percentage. */ unsigned int len; /* Current number of samples in array. */ int is_scrambled; /* Is data scrambled G3RUH / K9NG style? */ @@ -84,6 +85,9 @@ int rrbb_get_slice (rrbb_t b); void rrbb_set_audio_level (rrbb_t b, alevel_t alevel); alevel_t rrbb_get_audio_level (rrbb_t b); +void rrbb_set_speed_error (rrbb_t b, float speed_error); +float rrbb_get_speed_error (rrbb_t b); + int rrbb_get_is_scrambled (rrbb_t b); int rrbb_get_descram_state (rrbb_t b); int rrbb_get_prev_descram (rrbb_t b); diff --git a/src/serial_port.c b/src/serial_port.c index 7dff33a4..c57ee202 100644 --- a/src/serial_port.c +++ b/src/serial_port.c @@ -237,12 +237,14 @@ MYFDTYPE serial_port_open (char *devicename, int baud) case 9600: cfsetispeed (&ts, B9600); cfsetospeed (&ts, B9600); break; case 19200: cfsetispeed (&ts, B19200); cfsetospeed (&ts, B19200); break; case 38400: cfsetispeed (&ts, B38400); cfsetospeed (&ts, B38400); break; -#ifndef __APPLE__ +// This does not seem to be a problem anymore. +// Leaving traces behind, as clue, in case failure is encountered in some older version. +//#ifndef __APPLE__ // Not defined for Mac OSX. // https://groups.yahoo.com/neo/groups/direwolf_packet/conversations/messages/2072 case 57600: cfsetispeed (&ts, B57600); cfsetospeed (&ts, B57600); break; case 115200: cfsetispeed (&ts, B115200); cfsetospeed (&ts, B115200); break; -#endif +//#endif default: text_color_set(DW_COLOR_ERROR); dw_printf ("serial_port_open: Unsupported speed %d. Using 4800.\n", baud); cfsetispeed (&ts, B4800); cfsetospeed (&ts, B4800); @@ -302,17 +304,12 @@ int serial_port_write (MYFDTYPE fd, char *str, int len) { text_color_set(DW_COLOR_ERROR); dw_printf ("Error writing to serial port. Error %d.\n\n", err); + return (-1); } } - else if ((int)nwritten != len) - { - // Do we want this message here? - // Or rely on caller to check and provide something more meaningful for the usage? - //text_color_set(DW_COLOR_ERROR); - //dw_printf ("Error writing to serial port. Only %d of %d written.\n\n", (int)nwritten, len); - } - return (nwritten); + // nwritten is 0 for asynchronous write, at this point, so just return the requested len. + return (len); #else int written; diff --git a/src/server.c b/src/server.c index 8c52b199..6b41af25 100644 --- a/src/server.c +++ b/src/server.c @@ -116,6 +116,10 @@ * References: AGWPE TCP/IP API Tutorial * http://uz7ho.org.ua/includes/agwpeapi.htm * + * It has disappeared from the original location but you can find it here: + * https://web.archive.org/web/20130807113413/http:/uz7ho.org.ua/includes/agwpeapi.htm + * https://www.on7lds.net/42/sites/default/files/AGWPEAPI.HTM + * * Getting Started with Winsock * http://msdn.microsoft.com/en-us/library/windows/desktop/bb530742(v=vs.85).aspx * @@ -172,6 +176,7 @@ * You can increase the limit by changing the line below. * A larger number consumes more resources so don't go crazy by making it larger than needed. */ +// FIXME: Put in direwolf.h rather than in .c file. Change name to reflect AGW vs KISS. Update user guide 5.7. #define MAX_NET_CLIENTS 3 @@ -397,7 +402,7 @@ static void debug_print (fromto_t fromto, int client, struct agwpe_s *pmsg, int if (msg_len < 36) { text_color_set (DW_COLOR_ERROR); - dw_printf ("AGWPE message length, %d, is shorter than minumum 36.\n", msg_len); + dw_printf ("AGWPE message length, %d, is shorter than minimum 36.\n", msg_len); } if (msg_len != netle2host(pmsg->data_len_NETLE) + 36) { text_color_set (DW_COLOR_ERROR); @@ -666,7 +671,7 @@ static THREAD_F connect_listen_thread (void *arg) #else /* End of Windows case, now Linux */ - struct sockaddr_in sockaddr; /* Internet socket address stuct */ + struct sockaddr_in sockaddr; /* Internet socket address struct */ socklen_t sockaddr_size = sizeof(struct sockaddr_in); int server_port = (int)(ptrdiff_t)arg; int listen_sock; @@ -864,7 +869,7 @@ void server_send_monitored (int chan, packet_t pp, int own_xmit) */ struct { struct agwpe_s hdr; - char data[1+AX25_MAX_PACKET_LEN]; + char data[128+AX25_MAX_PACKET_LEN]; // Add plenty of room for header prefix. } agwpe_msg; int err; @@ -902,7 +907,7 @@ void server_send_monitored (int chan, packet_t pp, int own_xmit) // Add the description with <... > - char desc[80]; + char desc[120]; agwpe_msg.hdr.datakind = mon_desc (pp, desc, sizeof(desc)); if (own_xmit) { agwpe_msg.hdr.datakind = 'T'; @@ -917,16 +922,22 @@ void server_send_monitored (int chan, packet_t pp, int own_xmit) snprintf (ts, sizeof(ts), "[%02d:%02d:%02d]\r", tm->tm_hour, tm->tm_min, tm->tm_sec); strlcat ((char*)(agwpe_msg.data), ts, sizeof(agwpe_msg.data)); - // Information if any with \r\r. + // Information if any with \r. unsigned char *pinfo = NULL; int info_len = ax25_get_info (pp, &pinfo); + int msg_data_len = strlen((char*)(agwpe_msg.data)); // result length so far + if (info_len > 0 && pinfo != NULL) { - strlcat ((char*)(agwpe_msg.data), (char*)pinfo, sizeof(agwpe_msg.data)); - strlcat ((char*)(agwpe_msg.data), "\r", sizeof(agwpe_msg.data)); + // Issue 367: Use of strlcat truncated information part at any nul character. + // Use memcpy instead to preserve binary data, e.g. NET/ROM. + memcpy (agwpe_msg.data + msg_data_len, pinfo, info_len); + msg_data_len += info_len; + agwpe_msg.data[msg_data_len++] = '\r'; } - agwpe_msg.hdr.data_len_NETLE = host2netle(strlen(agwpe_msg.data) + 1) /* +1 to include terminating null */ ; + agwpe_msg.data[msg_data_len++] = '\0'; // add nul at end, included in length. + agwpe_msg.hdr.data_len_NETLE = host2netle(msg_data_len); if (debug_client) { debug_print (TO_CLIENT, client, &agwpe_msg.hdr, sizeof(agwpe_msg.hdr) + netle2host(agwpe_msg.hdr.data_len_NETLE)); @@ -1032,7 +1043,7 @@ static char mon_desc (packet_t pp, char *result, int result_size) switch (ftype) { - case frame_type_I: snprintf (result, result_size, "", ns, nr, ax25_get_pid(pp), info_len, pf_text, pf); return ('I'); + case frame_type_I: snprintf (result, result_size, "", ns, nr, ax25_get_pid(pp), info_len, pf_text, pf); return ('I'); case frame_type_U_UI: snprintf (result, result_size, "", ax25_get_pid(pp), info_len, pf_text, pf); return ('U'); break; @@ -1410,7 +1421,7 @@ static THREAD_F cmd_listen_thread (void *arg) } /* - * Call to/from fields are 10 bytes but contents must not exceeed 9 characters. + * Call to/from fields are 10 bytes but contents must not exceed 9 characters. * It's not guaranteed that unused bytes will contain 0 so we * don't issue error message in this case. */ @@ -1534,9 +1545,9 @@ static THREAD_F cmd_listen_thread (void *arg) count = 0; for (j=0; jachan[j].medium == MEDIUM_RADIO || - save_audio_config_p->achan[j].medium == MEDIUM_IGATE || - save_audio_config_p->achan[j].medium == MEDIUM_NETTNC) { + if (save_audio_config_p->chan_medium[j] == MEDIUM_RADIO || + save_audio_config_p->chan_medium[j] == MEDIUM_IGATE || + save_audio_config_p->chan_medium[j] == MEDIUM_NETTNC) { count++; } } @@ -1544,7 +1555,7 @@ static THREAD_F cmd_listen_thread (void *arg) for (j=0; jachan[j].medium) { + switch (save_audio_config_p->chan_medium[j]) { case MEDIUM_RADIO: { @@ -1805,6 +1816,11 @@ static THREAD_F cmd_listen_thread (void *arg) break; + case 'P': /* Application Login */ + + // Silently ignore it. + break; + case 'X': /* Register CallSign */ { @@ -1823,7 +1839,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->achan[chan].medium == MEDIUM_RADIO) { + if (chan >= 0 && chan < MAX_CHANS && save_audio_config_p->chan_medium[chan] == MEDIUM_RADIO) { ok = 1; dlq_register_callsign (cmd.hdr.call_from, chan, client); } @@ -1852,7 +1868,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->achan[chan].medium == MEDIUM_RADIO) { + if (chan >= 0 && chan < MAX_CHANS && save_audio_config_p->chan_medium[chan] == MEDIUM_RADIO) { dlq_unregister_callsign (cmd.hdr.call_from, chan, client); } else { @@ -1860,7 +1876,7 @@ static THREAD_F cmd_listen_thread (void *arg) dw_printf ("AGW protocol error. Unregister callsign for invalid channel %d.\n", chan); } } - /* No reponse is expected. */ + /* No response is expected. */ break; case 'C': /* Connect, Start an AX.25 Connection */ @@ -1926,8 +1942,10 @@ static THREAD_F cmd_listen_thread (void *arg) case 'D': /* Send Connected Data */ { - char callsigns[2][AX25_MAX_ADDR_LEN]; - const int num_calls = 2; + char callsigns[AX25_MAX_ADDRS][AX25_MAX_ADDR_LEN]; + memset (callsigns, 0, sizeof(callsigns)); + const int num_calls = 2; // only first 2 used. Digipeater path + // must be remembered from connect request. strlcpy (callsigns[AX25_SOURCE], cmd.hdr.call_from, sizeof(callsigns[AX25_SOURCE])); strlcpy (callsigns[AX25_DESTINATION], cmd.hdr.call_to, sizeof(callsigns[AX25_SOURCE])); @@ -1940,8 +1958,9 @@ static THREAD_F cmd_listen_thread (void *arg) case 'd': /* Disconnect, Terminate an AX.25 Connection */ { - char callsigns[2][AX25_MAX_ADDR_LEN]; - const int num_calls = 2; + char callsigns[AX25_MAX_ADDRS][AX25_MAX_ADDR_LEN]; + memset (callsigns, 0, sizeof(callsigns)); + const int num_calls = 2; // only first 2 used. strlcpy (callsigns[AX25_SOURCE], cmd.hdr.call_from, sizeof(callsigns[AX25_SOURCE])); strlcpy (callsigns[AX25_DESTINATION], cmd.hdr.call_to, sizeof(callsigns[AX25_SOURCE])); @@ -2065,7 +2084,7 @@ static THREAD_F cmd_listen_thread (void *arg) // Before disconnecting from another station, it would be good to know // that it actually received the last message we sent. For this reason, // I think it would be good for this to include information frames that were - // transmitted but not yet acknowleged. + // transmitted but not yet acknowledged. // You could say that a particular frame is still waiting to be sent even // if was already sent because it could be sent again if lost previously. @@ -2091,15 +2110,14 @@ static THREAD_F cmd_listen_thread (void *arg) { - char callsigns[2][AX25_MAX_ADDR_LEN]; - const int num_calls = 2; + char callsigns[AX25_MAX_ADDRS][AX25_MAX_ADDR_LEN]; + memset (callsigns, 0, sizeof(callsigns)); + const int num_calls = 2; // only first 2 used. strlcpy (callsigns[AX25_SOURCE], cmd.hdr.call_from, sizeof(callsigns[AX25_SOURCE])); strlcpy (callsigns[AX25_DESTINATION], cmd.hdr.call_to, sizeof(callsigns[AX25_SOURCE])); - // Issue 169. Proper implementation for 'Y'. dlq_outstanding_frames_request (callsigns, num_calls, cmd.hdr.portx, client); - } break; diff --git a/src/symbols.c b/src/symbols.c index bb29e4f2..c9f07e6e 100644 --- a/src/symbols.c +++ b/src/symbols.c @@ -1,7 +1,7 @@ // // This file is part of Dire Wolf, an amateur radio packet TNC. // -// Copyright (C) 2011, 2012, 2013, 2014, 2015 John Langner, WB2OSZ +// Copyright (C) 2011, 2012, 2013, 2014, 2015, 2022 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 @@ -193,9 +193,9 @@ static const struct { /* ; 27 */ { "NS", "Park/Picnic area" }, /* < 28 */ { "NT", "ADVISORY (one WX flag)" }, /* = 29 */ { "NU", "APRStt Touchtone (DTMF users)" }, - /* > 30 */ { "NV", "OVERLAYED CAR" }, + /* > 30 */ { "NV", "OVERLAID CAR" }, /* ? 31 */ { "NW", "INFO Kiosk (Blue box with ?)" }, - /* @ 32 */ { "NX", "HURICANE/Trop-Storm" }, + /* @ 32 */ { "NX", "HURRICANE/Trop-Storm" }, /* A 33 */ { "AA", "overlayBOX DTMF & RFID & XO" }, /* B 34 */ { "AB", "Blwng Snow (& future codes)" }, /* C 35 */ { "AC", "Coast Guard" }, @@ -205,7 +205,7 @@ static const struct { /* G 39 */ { "AG", "Snow Shwr (& future ovrlys)" }, /* H 40 */ { "AH", "Haze (& Overlay Hazards)" }, /* I 41 */ { "AI", "Rain Shower" }, - /* J 42 */ { "AJ", "Lightening (& future ovrlys)" }, + /* J 42 */ { "AJ", "Lightning (& future ovrlys)" }, /* K 43 */ { "AK", "Kenwood HT (W)" }, /* L 44 */ { "AL", "Lighthouse" }, /* M 45 */ { "AM", "MARS (A=Army,N=Navy,F=AF)" }, @@ -248,12 +248,12 @@ static const struct { /* r 82 */ { "SR", "Restrooms" }, /* s 83 */ { "SS", "OVERLAY SHIP/boat (top view)" }, /* t 84 */ { "ST", "Tornado" }, - /* u 85 */ { "SU", "OVERLAYED TRUCK" }, - /* v 86 */ { "SV", "OVERLAYED Van" }, + /* u 85 */ { "SU", "OVERLAID TRUCK" }, + /* v 86 */ { "SV", "OVERLAID Van" }, /* w 87 */ { "SW", "Flooding" }, /* x 88 */ { "SX", "Wreck or Obstruction ->X<-" }, /* y 89 */ { "SY", "Skywarn" }, - /* z 90 */ { "SZ", "OVERLAYED Shelter" }, + /* z 90 */ { "SZ", "OVERLAID Shelter" }, /* { 91 */ { "Q1", "Fog (& future ovrly codes)" }, /* | 92 */ { "Q2", "TNC Stream Switch" }, /* } 93 */ { "Q3", "" }, @@ -298,7 +298,7 @@ static const char *search_locations[] = { * Description: The primary and alternate symbol tables are constant * so they are hardcoded. * However the "new" sysmbols, which give new meanings to - * overlayed symbols, are always evolving. + * OVERLAID symbols, are always evolving. * For maximum flexibility, we will read the * data file at run time rather than compiling it in. * @@ -319,7 +319,7 @@ static const char *search_locations[] = { typedef struct new_sym_s { char overlay; char symbol; - char description[NEW_SYM_DESC_LEN+1]; + char *description; } new_sym_t; static new_sym_t *new_sym_ptr = NULL; /* Dynamically allocated array. */ @@ -352,10 +352,22 @@ void symbols_init (void) char stuff[200]; int j; +// Feb. 2022 - Noticed that some lines have - rather than =. + +// LD = LIght Rail or Subway (new Aug 2014) +// SD = Seaport Depot (new Aug 2014) +// DIGIPEATERS +// /# - Generic digipeater +// 1# - WIDE1-1 digipeater + + #define GOOD_LINE(x) (strlen(x) > 6 && \ (x[COL1_OVERLAY] == '/' || x[COL1_OVERLAY] == '\\' || isupper(x[COL1_OVERLAY]) || isdigit(x[COL1_OVERLAY])) \ && x[COL2_SYMBOL] >= '!' && x[COL2_SYMBOL] <= '~' \ - && x[COL3_SP] == ' ' && x[COL4_EQUAL] == '=' && x[COL5_SP] == ' ' && x[COL6_DESC] != ' ') + && x[COL3_SP] == ' ' \ + && (x[COL4_EQUAL] == '=' || x[COL4_EQUAL] == '-') \ + && x[COL5_SP] == ' ' \ + && x[COL6_DESC] != ' ') if (new_sym_ptr != NULL) { return; /* was called already. */ @@ -374,7 +386,7 @@ void symbols_init (void) text_color_set(DW_COLOR_ERROR); dw_printf ("Warning: Could not open 'symbols-new.txt'.\n"); - dw_printf ("The \"new\" overlayed character information will not be available.\n"); + dw_printf ("The \"new\" OVERLAID character information will not be available.\n"); new_sym_size = 1; new_sym_ptr = calloc(new_sym_size, sizeof(new_sym_t)); /* Don't try again. */ @@ -406,7 +418,7 @@ void symbols_init (void) } new_sym_ptr[new_sym_len].overlay = stuff[COL1_OVERLAY]; new_sym_ptr[new_sym_len].symbol = stuff[COL2_SYMBOL]; - strncpy(new_sym_ptr[new_sym_len].description, stuff+COL6_DESC, NEW_SYM_DESC_LEN); + new_sym_ptr[new_sym_len].description = strdup(stuff+COL6_DESC); new_sym_len++; } } @@ -556,6 +568,7 @@ static const char ssid_to_sym[16] = { 'v' /* 15 - Van */ }; + void symbols_from_dest_or_src (char dti, char *src, char *dest, char *symtab, char *symbol) { char *p; @@ -659,18 +672,34 @@ void symbols_from_dest_or_src (char dti, char *src, char *dest, char *symtab, ch /* * When all else fails, use source SSID. + * This is totally non-obvious and confusing, but it is in the APRS protocol spec. + * Chapter 20, "Symbol in the Source Address SSID" */ - p = strchr (src, '-'); - if (p != NULL) - { - int ssid; +// January 2022 - Every time this shows up, it confuses people terribly. +// e.g. An APRS "message" shows up with Bus or Motorcycle in the description. - ssid = atoi(p+1); - if (ssid >= 1 && ssid <= 15) { - *symtab = '/'; /* All in Primary table. */ - *symbol = ssid_to_sym[ssid]; - return; +// The position and object formats all contain a proper symbol and table. +// There doesn't seem to be much reason to have a symbol for something without +// a position because it would not show up on a map. +// This just seems to be a remnant of something used long ago and no longer needed. +// The protocol spec mentions a "MIM tracker" but I can't find any references to it. + +// If this was completely removed, no one would probably ever notice. +// The only possible useful case I can think of would be someone sending a +// NMEA string directly from a GPS receiver and wanting to keep the destination field +// for the system type. + + if (dti == '$') { + + p = strchr (src, '-'); + if (p != NULL) { + int ssid = atoi(p+1); + if (ssid >= 1 && ssid <= 15) { + *symtab = '/'; /* All in Primary table. */ + *symbol = ssid_to_sym[ssid]; + return; + } } } diff --git a/src/telemetry.c b/src/telemetry.c index b71bc8d9..2a6c690c 100644 --- a/src/telemetry.c +++ b/src/telemetry.c @@ -69,7 +69,7 @@ #define T_NUM_ANALOG 5 /* Number of analog channels. */ -#define T_NUM_DIGITAL 8 /* Number of digital channnels. */ +#define T_NUM_DIGITAL 8 /* Number of digital channels. */ #define T_STR_LEN 16 /* Max len for labels and units. */ @@ -707,7 +707,7 @@ void telemetry_unit_label_message (char *station, char *msg) * * Name: telemetry_coefficents_message * - * Purpose: Interpret message with scaling coefficents for analog channels. + * Purpose: Interpret message with scaling coefficients for analog channels. * * Inputs: station - Name of station reporting telemetry. * In this case it is the destination for the message, @@ -771,7 +771,7 @@ void telemetry_coefficents_message (char *station, char *msg, int quiet) else { if ( ! quiet) { text_color_set(DW_COLOR_ERROR); - dw_printf ("Equation coefficent position A%d%c is empty.\n", n/3+1, n%3+'a'); + dw_printf ("Equation coefficient position A%d%c is empty.\n", n/3+1, n%3+'a'); dw_printf ("Some applications might not handle this correctly.\n"); } } @@ -782,7 +782,7 @@ void telemetry_coefficents_message (char *station, char *msg, int quiet) if (n != T_NUM_ANALOG * 3) { if ( ! quiet) { text_color_set(DW_COLOR_ERROR); - dw_printf ("Found %d equation coefficents when 15 were expected.\n", n); + dw_printf ("Found %d equation coefficients when 15 were expected.\n", n); dw_printf ("Some applications might not handle this correctly.\n"); } } @@ -807,7 +807,7 @@ void telemetry_coefficents_message (char *station, char *msg, int quiet) * * Name: telemetry_bit_sense_message * - * Purpose: Interpret message with scaling coefficents for analog channels. + * Purpose: Interpret message with scaling coefficients for analog channels. * * Inputs: station - Name of station reporting telemetry. * In this case it is the destination for the message, @@ -910,7 +910,7 @@ void telemetry_bit_sense_message (char *station, char *msg, int quiet) * seq - Sequence number. * araw - 5 analog raw values. * ndp - Number of decimal points for each. - * draw - 8 digial raw vales. + * draw - 8 digital raw vales. * * Outputs: output - Decoded telemetry in human readable format. * diff --git a/src/textcolor.c b/src/textcolor.c index a515e2eb..dea90f09 100644 --- a/src/textcolor.c +++ b/src/textcolor.c @@ -349,7 +349,7 @@ void text_color_set ( enum dw_color_e c ) * output to stdout or other desired destination. * * Inputs: fmt - C language format. - * ... - Addtional arguments, just like printf. + * ... - Additional arguments, just like printf. * * * Returns: Number of characters in result. diff --git a/src/tnctest.c b/src/tnctest.c new file mode 100644 index 00000000..0d4c26b4 --- /dev/null +++ b/src/tnctest.c @@ -0,0 +1,1284 @@ +// +// This file is part of Dire Wolf, an amateur radio packet TNC. +// +// Copyright (C) 2016 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: tnctest.c + * + * Purpose: Test AX.25 connected mode between two TNCs. + * + * Description: The first TNC will connect to the second TNC and send a bunch of data. + * Proper transfer of data will be verified. + * + * Usage: tnctest [options] port0=name0 port1=name1 + * + * Example: tnctest localhost:8000=direwolf COM1=KPC-3+ + * + * Each port can have the following forms: + * + * * host-name:tcp-port + * * ip-addr:tcp-port + * * tcp-port + * * serial port name (e.g. COM1, /dev/ttyS0) + * + *---------------------------------------------------------------*/ + + + +/* + * Native Windows: Use the Winsock interface. + * Linux: Use the BSD socket interface. + */ + +#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 +#include +#include +#include +#include +//#include +#include +#endif + +#include +#include +#include +#include +#include +#include + + +//#include "ax25_pad.h" +#include "textcolor.h" +#include "dtime_now.h" +#include "serial_port.h" + + +/* We don't deal with big-endian processors here. */ +/* TODO: Use agwlib (which did not exist when this was written) */ +/* rather than duplicating the effort here. */ + +struct agwpe_s { + +#if 1 + + unsigned char portx; /* 0 for first, 1 for second, etc. */ + unsigned char reserved1; + unsigned char reserved2; + unsigned char reserved3; + + unsigned char datakind; /* message type, usually written as a letter. */ + unsigned char reserved4; + unsigned char pid; + unsigned char reserved5; + +#else + short portx; /* 0 for first, 1 for second, etc. */ + short port_hi_reserved; + short kind_lo; /* message type */ + short kind_hi; +#endif + char call_from[10]; + char call_to[10]; + int data_len; /* Number of data bytes following. */ + int user_reserved; +}; + + +#if __WIN32__ +static unsigned __stdcall tnc_thread_net (void *arg); +static unsigned __stdcall tnc_thread_serial (void *arg); +#else +static void * tnc_thread_net (void *arg); +static void * tnc_thread_serial (void *arg); +#endif + + +static void tnc_connect (int from, int to); +static void tnc_disconnect (int from, int to); +static void tnc_send_data (int from, int to, char * data); +static void tnc_reset (int from, int to); + +/* + * Convert Internet address to text. + * Can't use InetNtop because it is supported only on Windows Vista and later. + */ + +static char * ia_to_text (int Family, void * pAddr, char * pStringBuf, size_t StringBufSize) +{ + struct sockaddr_in *sa4; + struct sockaddr_in6 *sa6; + + switch (Family) { + case AF_INET: + sa4 = (struct sockaddr_in *)pAddr; +#if __WIN32__ + snprintf (pStringBuf, StringBufSize, "%d.%d.%d.%d", sa4->sin_addr.S_un.S_un_b.s_b1, + sa4->sin_addr.S_un.S_un_b.s_b2, + sa4->sin_addr.S_un.S_un_b.s_b3, + sa4->sin_addr.S_un.S_un_b.s_b4); +#else + inet_ntop (AF_INET, &(sa4->sin_addr), pStringBuf, StringBufSize); +#endif + break; + case AF_INET6: + sa6 = (struct sockaddr_in6 *)pAddr; +#if __WIN32__ + snprintf (pStringBuf, StringBufSize, "%x:%x:%x:%x:%x:%x:%x:%x", + ntohs(((unsigned short *)(&(sa6->sin6_addr)))[0]), + ntohs(((unsigned short *)(&(sa6->sin6_addr)))[1]), + ntohs(((unsigned short *)(&(sa6->sin6_addr)))[2]), + ntohs(((unsigned short *)(&(sa6->sin6_addr)))[3]), + ntohs(((unsigned short *)(&(sa6->sin6_addr)))[4]), + ntohs(((unsigned short *)(&(sa6->sin6_addr)))[5]), + ntohs(((unsigned short *)(&(sa6->sin6_addr)))[6]), + ntohs(((unsigned short *)(&(sa6->sin6_addr)))[7])); +#else + inet_ntop (AF_INET6, &(sa6->sin6_addr), pStringBuf, StringBufSize); +#endif + break; + default: + snprintf (pStringBuf, StringBufSize, "Invalid address family!"); + } + assert (strlen(pStringBuf) < StringBufSize); + return pStringBuf; +} + + + +/*------------------------------------------------------------------ + * + * Name: main + * + * Purpose: Basic test for connected AX.25 data mode between TNCs. + * + * Usage: Described above. + * + *---------------------------------------------------------------*/ + +#define MAX_TNC 2 // Just 2 for now. + // Could be more later for multiple concurrent connections. + +/* Obtained from the command line. */ + +static int num_tnc; /* How many TNCs for this test? */ + /* Initially only 2 but long term we might */ + /* enhance it to allow multiple concurrent connections. */ + +static char hostname[MAX_TNC][50]; /* DNS host name or IPv4 address. */ + /* Some of the code is there for IPv6 but */ + /* needs more work. */ + /* Defaults to "localhost" if not specified. */ + +static char port[MAX_TNC][30]; /* If it begins with a digit, it is considered */ + /* a TCP port number at the hostname. */ + /* Otherwise, we treat it as a serial port name. */ + +static char description[MAX_TNC][50]; /* Name used in the output. */ + +static int using_tcp[MAX_TNC]; /* Are we using TCP or serial port for each TNC? */ + /* Use corresponding one of the next two. */ + +static int server_sock[MAX_TNC]; /* File descriptor for AGW socket interface. */ + /* Set to -1 if not used. */ + /* (Don't use SOCKET type because it is unsigned.) */ + +static MYFDTYPE serial_fd[MAX_TNC]; /* Serial port handle. */ + +static volatile int busy[MAX_TNC]; /* True when TNC busy and can't accept more data. */ + /* For serial port, this is set by XON / XOFF characters. */ + +#define XOFF 0x13 +#define XON 0x11 + +static char tnc_address[MAX_TNC][12]; /* Name of the TNC used in the frames. Originally, this */ + /* was simply TNC0 and TNC1 but that can get hard to read */ + /* and confusing. Later used DW0, DW1, for direwolf */ + /* so the direction of flow is easier to grasp. */ + +#if __WIN32__ + static HANDLE tnc_th[MAX_TNC]; +#else + static pthread_t tnc_tid[MAX_TNC]; +#endif + +#define LINE_WIDTH 80 +//#define LINE_WIDTH 120 /* If I was more ambitious I might try to get */ + /* this from the terminal properties. */ + +static int column_width; /* Line width divided by number of TNCs. */ + + +/* + * Current state for each TNC. + */ + +static int is_connected[MAX_TNC]; /* -1 = not yet available. */ + /* 0 = not connected. */ + /* 1 = not connected. */ + +static int have_cmd_prompt[MAX_TNC]; /* Set if "cmd:" was the last thing seen. */ + +static int last_rec_seq[MAX_TNC]; /* Each data packet will contain a sequence number. */ + /* This is used to verify that all have been */ + /* received in the correct order. */ + + + +/* + * Start time so we can print relative elapsed time. + */ + +static double start_dtime; + + +static int max_count; + +int main (int argc, char *argv[]) +{ + int j; + int timeout; + int send_count = 0; + int burst_size = 1; + int errors = 0; + + //max_count = 20; + max_count = 200; + //max_count = 6; + max_count = 1000; + max_count = 9999; + +#if __WIN32__ +#else + int e; + + setlinebuf (stdout); +#endif + + start_dtime = dtime_now(); + +/* + * Extract command line args. + */ + num_tnc = argc - 1; + + if (num_tnc < 2 || num_tnc > MAX_TNC) { + printf ("Specify minimum 2, maximum %d TNCs on the command line.\n", MAX_TNC); + exit (EXIT_FAILURE); + } + + column_width = LINE_WIDTH / num_tnc; + + for (j=0; j 0) { + + SLEEP_MS(100); + timeout--; + ready = 1; + for (j=0; j last0) { + last0 = last_rec_seq[0]; + no_activity = 0; + } + if (last_rec_seq[1] > last1) { + last1 = last_rec_seq[1]; + no_activity = 0; + } + } + + if (last_rec_seq[0] == max_count) { + printf ("Got last expected reply.\n"); + } + else { + printf ("ERROR: Timeout - No incoming activity for %d seconds.\n", no_activity); + errors++; + } + +/* + * Did we get all expected replies? + */ + if (last_rec_seq[0] != max_count) { + printf ("ERROR: Last received reply was %d when we were expecting %d.\n", last_rec_seq[0], max_count); + errors++; + } + +/* + * Ask for disconnect. Wait until complete. + */ + + tnc_disconnect (0, 1); + + timeout = 200; // 20 sec should be generous. + ready = 0; + while ( ! ready && timeout > 0) { + + SLEEP_MS(100); + timeout--; + ready = 1; + for (j=0; j1 for the other end which answers. + * + * data - Should look something like this: + * 9999 send data + * 9999 reply + * + * Global In/Out: last_rec_seq[my_index] + * + * Description: Look for expected format. + * Extract the sequence number. + * Verify that it is the next expected one. + * Update it. + * + *--------------------------------------------------------------------*/ + +void process_rec_data (int my_index, char *data) +{ + int n; + + if (isdigit(*data) && strncmp(data+4, " send", 5) == 0) { + if (my_index > 0) { + last_rec_seq[my_index]++; + + n = atoi(data); + if (n != last_rec_seq[my_index]) { + printf ("%*s%s: Received %d when %d was expected.\n", my_index*column_width, "", tnc_address[my_index], n, last_rec_seq[my_index]); + SLEEP_MS(10000); + printf ("TEST FAILED!\n"); + exit (EXIT_FAILURE); + } + } + } + + else if (isdigit(*data) && strncmp(data+4, " reply", 6) == 0) { + if (my_index == 0) { + last_rec_seq[my_index]++; + n = atoi(data); + if (n != last_rec_seq[my_index]) { + printf ("%*s%s: Received %d when %d was expected.\n", my_index*column_width, "", tnc_address[my_index], n, last_rec_seq[my_index]); + SLEEP_MS(10000); + printf ("TEST FAILED!\n"); + exit (EXIT_FAILURE); + } + } + } + + else if (data[0] == 'A') { + + if (strncmp(data, "ABCDEFGHIJKLMNOPQRSTUVWXYZ", strlen(data)-1) != 0) { + printf ("%*s%s: Segmentation is broken.\n", my_index*column_width, "", tnc_address[my_index]); + SLEEP_MS(10000); + printf ("TEST FAILED!\n"); + exit (EXIT_FAILURE); + } + } +} + + + +/*------------------------------------------------------------------- + * + * Name: tnc_thread_net + * + * Purpose: Establish connection with a TNC via network. + * + * Inputs: arg - My instance index, 0 thru MAX_TNC-1. + * + * Outputs: packets - Received packets are put in the corresponding column + * 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. + * + * Description: Perform any necessary configuration for the TNC then wait + * for responses and process them. + * + *--------------------------------------------------------------------*/ + +#define MAX_HOSTS 30 + +#if __WIN32__ +static unsigned __stdcall tnc_thread_net (void *arg) +#else +static void * tnc_thread_net (void *arg) +#endif +{ + int my_index; + struct addrinfo hints; + struct addrinfo *ai_head = NULL; + struct addrinfo *ai; + struct addrinfo *hosts[MAX_HOSTS]; + int num_hosts, n; + int err; + char ipaddr_str[46]; /* text form of IP address */ +#if __WIN32__ + WSADATA wsadata; +#endif + + struct agwpe_s mon_cmd; + char data[4096]; + double dnow; + + + my_index = (int)(ptrdiff_t)arg; + +#if DEBUGx + printf ("DEBUG: tnc_thread_net %d start, port = '%s'\n", my_index, port[my_index]); +#endif + +#if __WIN32__ + err = WSAStartup (MAKEWORD(2,2), &wsadata); + if (err != 0) { + printf("WSAStartup failed: %d\n", err); + return (0); + } + + if (LOBYTE(wsadata.wVersion) != 2 || HIBYTE(wsadata.wVersion) != 2) { + printf("Could not find a usable version of Winsock.dll\n"); + WSACleanup(); + //sleep (1); + return (0); + } +#endif + + memset (&hints, 0, sizeof(hints)); + + hints.ai_family = AF_UNSPEC; /* Allow either IPv4 or IPv6. */ + // hints.ai_family = AF_INET; /* IPv4 only. */ + hints.ai_socktype = SOCK_STREAM; + hints.ai_protocol = IPPROTO_TCP; + +/* + * Connect to TNC server. + */ + + ai_head = NULL; + err = getaddrinfo(hostname[my_index], port[my_index], &hints, &ai_head); + if (err != 0) { +#if __WIN32__ + printf ("Can't get address for server %s, err=%d\n", + hostname[my_index], WSAGetLastError()); +#else + printf ("Can't get address for server %s, %s\n", + hostname[my_index], gai_strerror(err)); +#endif + freeaddrinfo(ai_head); + exit (1); + } + +#if DEBUG_DNS + printf ("getaddrinfo returns:\n"); +#endif + num_hosts = 0; + for (ai = ai_head; ai != NULL; ai = ai->ai_next) { +#if DEBUG_DNS + ia_to_text (ai->ai_family, ai->ai_addr, ipaddr_str, sizeof(ipaddr_str)); + printf (" %s\n", ipaddr_str); +#endif + hosts[num_hosts] = ai; + if (num_hosts < MAX_HOSTS) num_hosts++; + } + +#if DEBUG_DNS + printf ("addresses for hostname:\n"); + for (n=0; nai_family, hosts[n]->ai_addr, ipaddr_str, sizeof(ipaddr_str)); + printf (" %s\n", ipaddr_str); + } +#endif + + // Try each address until we find one that is successful. + + for (n=0; nai_family, ai->ai_addr, ipaddr_str, sizeof(ipaddr_str)); + is = socket(ai->ai_family, ai->ai_socktype, ai->ai_protocol); +#if __WIN32__ + if (is == INVALID_SOCKET) { + printf ("Socket creation failed, err=%d", WSAGetLastError()); + WSACleanup(); + is = -1; + continue; + } +#else + if (err != 0) { + printf ("Socket creation failed, err=%s", gai_strerror(err)); + (void) close (is); + is = -1; + continue; + } +#endif + +#ifndef DEBUG_DNS + err = connect(is, ai->ai_addr, (int)ai->ai_addrlen); +#if __WIN32__ + if (err == SOCKET_ERROR) { +#if DEBUGx + printf("Connect to %s on %s (%s), port %s failed.\n", + description[my_index], hostname[my_index], ipaddr_str, port[my_index]); +#endif + closesocket (is); + is = -1; + continue; + } +#else + if (err != 0) { +#if DEBUGx + printf("Connect to %s on %s (%s), port %s failed.\n", + description[my_index], hostname[my_index], ipaddr_str, port[my_index]); +#endif + (void) close (is); + is = -1; + continue; + } + int flag = 1; + err = setsockopt (is, IPPROTO_TCP, TCP_NODELAY, (void*)(long)(&flag), (socklen_t)sizeof(flag)); + if (err < 0) { + printf("setsockopt TCP_NODELAY failed.\n"); + } +#endif + +/* Success. */ + + + server_sock[my_index] = is; +#endif + break; + } + + freeaddrinfo(ai_head); + + if (server_sock[my_index] == -1) { + + printf("TNC %d unable to connect to %s on %s (%s), port %s\n", + my_index, description[my_index], hostname[my_index], ipaddr_str, port[my_index] ); + exit (1); + } + + +#if 1 // Temp test just to get something. + +/* + * Send command to toggle reception of frames in raw format. + */ + memset (&mon_cmd, 0, sizeof(mon_cmd)); + + mon_cmd.datakind = 'k'; + + SOCK_SEND(server_sock[my_index], (char*)(&mon_cmd), sizeof(mon_cmd)); + +#endif + +/* + * Send command to register my callsign for incoming connect request. + * Not really needed when we initiate the connection. + */ + + memset (&mon_cmd, 0, sizeof(mon_cmd)); + + mon_cmd.datakind = 'X'; + strlcpy (mon_cmd.call_from, tnc_address[my_index], sizeof(mon_cmd.call_from)); + + SOCK_SEND(server_sock[my_index], (char*)(&mon_cmd), sizeof(mon_cmd)); + + + +/* + * Inform main program and observer that we are ready to go. + */ + printf("TNC %d now available. %s on %s (%s), port %s\n", + my_index, description[my_index], hostname[my_index], ipaddr_str, port[my_index] ); + is_connected[my_index] = 0; + +/* + * Print what we get from TNC. + */ + + while (1) { + int n; + + n = SOCK_RECV (server_sock[my_index], (char*)(&mon_cmd), sizeof(mon_cmd)); + + if (n != sizeof(mon_cmd)) { + printf ("Read error, TNC %d received %d command bytes.\n", my_index, n); + exit (1); + } + + +#if DEBUGx + printf ("TNC %d received '%c' data, data_len = %d\n", + my_index, mon_cmd.datakind, mon_cmd.data_len); +#endif + assert (mon_cmd.data_len >= 0 && mon_cmd.data_len < (int)(sizeof(data))); + + if (mon_cmd.data_len > 0) { + + n = SOCK_RECV (server_sock[my_index], data, mon_cmd.data_len); + + if (n != mon_cmd.data_len) { + printf ("Read error, TNC %d received %d data bytes.\n", my_index, n); + exit (1); + } + data[mon_cmd.data_len] = '\0'; + } + +/* + * What did we get? + */ + + dnow = dtime_now(); + + switch (mon_cmd.datakind) { + + case 'C': // AX.25 Connection Received + + printf("%*s[R %.3f] *** Connected to %s ***\n", my_index*column_width, "", dnow-start_dtime, mon_cmd.call_from); + is_connected[my_index] = 1; + + break; + + case 'D': // Connected AX.25 Data + + printf("%*s[R %.3f] %s\n", my_index*column_width, "", dnow-start_dtime, data); + + process_rec_data (my_index, data); + + + if (isdigit(data[0]) && isdigit(data[1]) && isdigit(data[2]) && isdigit(data[3]) && + strncmp(data+4, " send", 5) == 0) { + // Expected message. Make sure it is expected sequence and send reply. + int n = atoi(data); + char reply[80]; + snprintf (reply, sizeof(reply), "%04d reply\r", n); + tnc_send_data (my_index, 1 - my_index, reply); + + // HACK! + // It gets very confusing because N(S) and N(R) are very close. + // Send a couple dozen I frames so they will be easier to distinguish visually. + // Currently don't have the same in serial port version. + + // We change the length each time to test segmentation. + // Set PACLEN to some very small number like 5. + + if (n == 1 && max_count > 1) { + int j; + for (j = 1; j <= 26; j++) { + snprintf (reply, sizeof(reply), "%.*s\r", j, "ABCDEFGHIJKLMNOPQRSTUVWXYZ"); + tnc_send_data (my_index, 1 - my_index, reply); + } + } + } + + break; + + case 'd': // Disconnected + + printf("%*s[R %.3f] *** Disconnected from %s ***\n", my_index*column_width, "", dnow-start_dtime, mon_cmd.call_from); + is_connected[my_index] = 0; + + break; + + case 'y': // Outstanding frames waiting on a Port + + printf("%*s[R %.3f] *** Outstanding frames waiting %d ***\n", my_index*column_width, "", dnow-start_dtime, 123); // TODO + + break; + + default: + + //printf("%*s[R %.3f] --- Ignoring cmd kind '%c' ---\n", my_index*column_width, "", dnow-start_dtime, mon_cmd.datakind); + + break; + } + } + +} /* end tnc_thread_net */ + + + + + +/*------------------------------------------------------------------- + * + * Name: tnc_thread_serial + * + * Purpose: Establish connection with a TNC via serial port. + * + * Inputs: arg - My instance index, 0 thru MAX_TNC-1. + * + * Outputs: packets - Received packets are put in the corresponding column + * 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. + * + * Description: Perform any necessary configuration for the TNC then wait + * for responses and process them. + * + *--------------------------------------------------------------------*/ + + +#if __WIN32__ +typedef HANDLE MYFDTYPE; +#define MYFDERROR INVALID_HANDLE_VALUE +#else +typedef int MYFDTYPE; +#define MYFDERROR (-1) +#endif + + +#if __WIN32__ +static unsigned __stdcall tnc_thread_serial (void *arg) +#else +static void * tnc_thread_serial (void *arg) +#endif +{ + int my_index = (int)(ptrdiff_t)arg; + char cmd[80]; + + serial_fd[my_index] = serial_port_open (port[my_index], 9600); + + if (serial_fd[my_index] == MYFDERROR) { + printf("TNC %d unable to connect to %s on %s.\n", + my_index, description[my_index], port[my_index] ); + exit (1); + } + + +/* + * Make sure we are in command mode. + */ + + strcpy (cmd, "\003\rreset\r"); + serial_port_write (serial_fd[my_index], cmd, strlen(cmd)); + SLEEP_MS (3000); + + strcpy (cmd, "echo on\r"); + serial_port_write (serial_fd[my_index], cmd, strlen(cmd)); + SLEEP_MS (200); + +// do any necessary set up here. such as setting mycall + + snprintf (cmd, sizeof(cmd), "mycall %s\r", tnc_address[my_index]); + serial_port_write (serial_fd[my_index], cmd, strlen(cmd)); + SLEEP_MS (200); + +// Don't want to stop tty output when typing begins. + + strcpy (cmd, "flow off\r"); + serial_port_write (serial_fd[my_index], cmd, strlen(cmd)); + + strcpy (cmd, "echo off\r"); + serial_port_write (serial_fd[my_index], cmd, strlen(cmd)); + +/* Success. */ + + printf("TNC %d now available. %s on %s\n", + my_index, description[my_index], port[my_index] ); + is_connected[my_index] = 0; + + +/* + * Read and print. + */ + + while (1) { + int ch; + char result[500]; + int len; + int done; + + len = 0; + result[len] = '\0'; + done = 0; + + while ( ! done) { + + ch = serial_port_get1(serial_fd[my_index]); + + if (ch < 0) { + printf("TNC %d fatal read error.\n", my_index); + exit (1); + } + + if (ch == '\r' || ch == '\n') { + done = 1; + } + else if (ch == XOFF) { + double dnow = dtime_now(); + printf("%*s[R %.3f] \n", my_index*column_width, "", dnow-start_dtime); + busy[my_index] = 1; + } + else if (ch == XON) { + double dnow = dtime_now(); + printf("%*s[R %.3f] \n", my_index*column_width, "", dnow-start_dtime); + busy[my_index] = 0; + } + else if (isprint(ch)) { + result[len] = ch; + len++; + result[len] = '\0'; + } + else { + char hex[12]; + + snprintf (hex, sizeof(hex), "", ch); + strlcat (result, hex, sizeof(result)); + len = strlen(result); + } + if (strcmp(result, "cmd:") == 0) { + done = 1; + have_cmd_prompt[my_index] = 1; + } + else { + have_cmd_prompt[my_index] = 0; + } + } + + if (len > 0) { + + double dnow = dtime_now(); + + printf("%*s[R %.3f] %s\n", my_index*column_width, "", dnow-start_dtime, result); + + if (strncmp(result, "*** CONNECTED", 13) == 0) { + is_connected[my_index] = 1; + } + + if (strncmp(result, "*** DISCONNECTED", 16) == 0) { + is_connected[my_index] = 0; + } + + if (strncmp(result, "Not while connected", 19) == 0) { + // Not expecting this. + // What to do? + } + + process_rec_data (my_index, result); + + if (isdigit(result[0]) && isdigit(result[1]) && isdigit(result[2]) && isdigit(result[3]) && + strncmp(result+4, " send", 5) == 0) { + // Expected message. Make sure it is expected sequence and send reply. + int n = atoi(result); + char reply[80]; + snprintf (reply, sizeof(reply), "%04d reply\r", n); + tnc_send_data (my_index, 1 - my_index, reply); + } + + } + } + +} /* end tnc_thread_serial */ + + + + +static void tnc_connect (int from, int to) +{ + + double dnow = dtime_now(); + + printf("%*s[T %.3f] *** Send connect request ***\n", from*column_width, "", dnow-start_dtime); + + if (using_tcp[from]) { + +//struct agwpe_s { +// short portx; /* 0 for first, 1 for second, etc. */ +// short port_hi_reserved; +// short datakind; /* message type */ +// short kind_hi; +// char call_from[10]; +// char call_to[10]; +// int data_len; /* Number of data bytes following. */ +// int user_reserved; +//}; + struct agwpe_s cmd; + + memset (&cmd, 0, sizeof(cmd)); + + cmd.datakind = 'C'; + strlcpy (cmd.call_from, tnc_address[from], sizeof(cmd.call_from)); + strlcpy (cmd.call_to, tnc_address[to], sizeof(cmd.call_to)); + + SOCK_SEND(server_sock[from], (char*)(&cmd), sizeof(cmd)); + } + else { + + char cmd[80]; + + if (! have_cmd_prompt[from]) { + + SLEEP_MS (1500); + strcpy (cmd, "\003\003\003"); + serial_port_write (serial_fd[from], cmd, strlen(cmd)); + SLEEP_MS (1500); + + strcpy (cmd, "\r"); + serial_port_write (serial_fd[from], cmd, strlen(cmd)); + SLEEP_MS (200); + } + + snprintf (cmd, sizeof(cmd), "connect %s\r", tnc_address[to]); + serial_port_write (serial_fd[from], cmd, strlen(cmd)); + } + +} /* end tnc_connect */ + + +static void tnc_disconnect (int from, int to) +{ + double dnow = dtime_now(); + + printf("%*s[T %.3f] *** Send disconnect request ***\n", from*column_width, "", dnow-start_dtime); + + if (using_tcp[from]) { + + struct agwpe_s cmd; + + memset (&cmd, 0, sizeof(cmd)); + + cmd.datakind = 'd'; + strlcpy (cmd.call_from, tnc_address[from], sizeof(cmd.call_from)); + strlcpy (cmd.call_to, tnc_address[to], sizeof(cmd.call_to)); + + SOCK_SEND(server_sock[from], (char*)(&cmd), sizeof(cmd)); + } + else { + + char cmd[80]; + + if (! have_cmd_prompt[from]) { + + SLEEP_MS (1500); + strcpy (cmd, "\003\003\003"); + serial_port_write (serial_fd[from], cmd, strlen(cmd)); + SLEEP_MS (1500); + + strcpy (cmd, "\r"); + serial_port_write (serial_fd[from], cmd, strlen(cmd)); + SLEEP_MS (200); + } + + strcpy (cmd, "disconnect\r"); + serial_port_write (serial_fd[from], cmd, strlen(cmd)); + } + +} /* end tnc_disconnect */ + + +static void tnc_reset (int from, int to) +{ + double dnow = dtime_now(); + + printf("%*s[T %.3f] *** Send reset ***\n", from*column_width, "", dnow-start_dtime); + + if (using_tcp[from]) { + + + } + else { + + char cmd[80]; + + SLEEP_MS (1500); + strcpy (cmd, "\003\003\003"); + serial_port_write (serial_fd[from], cmd, strlen(cmd)); + SLEEP_MS (1500); + + strcpy (cmd, "\r"); + serial_port_write (serial_fd[from], cmd, strlen(cmd)); + SLEEP_MS (200); + + strcpy (cmd, "reset\r"); + serial_port_write (serial_fd[from], cmd, strlen(cmd)); + } + +} /* end tnc_disconnect */ + + + +static void tnc_send_data (int from, int to, char * data) +{ + double dnow = dtime_now(); + + printf("%*s[T %.3f] %s\n", from*column_width, "", dnow-start_dtime, data); + + if (using_tcp[from]) { + + struct { + struct agwpe_s hdr; + char data[256]; + } cmd; + + memset (&cmd.hdr, 0, sizeof(cmd.hdr)); + + cmd.hdr.datakind = 'D'; + cmd.hdr.pid = 0xf0; + snprintf (cmd.hdr.call_from, sizeof(cmd.hdr.call_from), "%s", tnc_address[from]); + snprintf (cmd.hdr.call_to, sizeof(cmd.hdr.call_to), "%s", tnc_address[to]); + cmd.hdr.data_len = strlen(data); + strlcpy (cmd.data, data, sizeof(cmd.data)); + + SOCK_SEND(server_sock[from], (char*)(&cmd), sizeof(cmd.hdr) + strlen(data)); + } + else { + + // The assumption is that we are in CONVERS mode. + // The data should be terminated by carriage return. + + int timeout = 600; // 60 sec. I've seen it take more than 20. + while (timeout > 0 && busy[from]) { + SLEEP_MS(100); + timeout--; + } + if (timeout == 0) { + printf ("ERROR: Gave up waiting while TNC busy.\n"); + tnc_disconnect (0,1); + SLEEP_MS(5000); + printf ("TEST FAILED!\n"); + exit (EXIT_FAILURE); + } + else { + serial_port_write (serial_fd[from], data, strlen(data)); + } + } + +} /* end tnc_disconnect */ + + + + +/* end tnctest.c */ diff --git a/src/tq.c b/src/tq.c index 0cc4bec0..c656af54 100644 --- a/src/tq.c +++ b/src/tq.c @@ -1,7 +1,7 @@ // // This file is part of Dire Wolf, an amateur radio packet TNC. // -// Copyright (C) 2011, 2012, 2014, 2015, 2016 John Langner, WB2OSZ +// Copyright (C) 2011, 2012, 2014, 2015, 2016, 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 @@ -50,7 +50,8 @@ #include "audio.h" #include "tq.h" #include "dedupe.h" - +#include "igate.h" +#include "dtime_now.h" @@ -148,7 +149,7 @@ void tq_init (struct audio_s *audio_config_p) for (c = 0; c < MAX_CHANS; c++) { - if (audio_config_p->achan[c].medium == MEDIUM_RADIO) { + if (audio_config_p->chan_medium[c] == MEDIUM_RADIO) { wake_up_event[c] = CreateEvent (NULL, 0, 0, NULL); @@ -167,7 +168,7 @@ void tq_init (struct audio_s *audio_config_p) xmit_thread_is_waiting[c] = 0; - if (audio_config_p->achan[c].medium == MEDIUM_RADIO) { + if (audio_config_p->chan_medium[c] == MEDIUM_RADIO) { err = pthread_cond_init (&(wake_up_cond[c]), NULL); if (err != 0) { text_color_set(DW_COLOR_ERROR); @@ -195,6 +196,9 @@ void tq_init (struct audio_s *audio_config_p) * * Inputs: chan - Channel, 0 is first. * + * New in 1.7: + * Channel can be assigned to IGate rather than a radio. + * * prio - Priority, use TQ_PRIO_0_HI for digipeated or * TQ_PRIO_1_LO for normal. * @@ -247,7 +251,44 @@ void tq_append (int chan, int prio, packet_t pp) } #endif - if (chan < 0 || chan >= MAX_CHANS || save_audio_config_p->achan[chan].medium == MEDIUM_NONE) { +// New in 1.7 - A channel can be assigned to the IGate rather than a radio. + +#ifndef DIGITEST // avoid dtest link error + + if (save_audio_config_p->chan_medium[chan] == MEDIUM_IGATE) { + + char ts[100]; // optional time stamp. + + if (strlen(save_audio_config_p->timestamp_format) > 0) { + char tstmp[100]; + timestamp_user_format (tstmp, sizeof(tstmp), save_audio_config_p->timestamp_format); + strlcpy (ts, " ", sizeof(ts)); // space after channel. + strlcat (ts, tstmp, sizeof(ts)); + } + else { + strlcpy (ts, "", sizeof(ts)); + } + + char stemp[256]; // Formated addresses. + ax25_format_addrs (pp, stemp); + 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); + 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) { 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"); @@ -281,8 +322,6 @@ void tq_append (int chan, int prio, packet_t pp) * The check would allow an unlimited number of other types. * * Limit was 20. Changed to 100 in version 1.2 as a workaround. - * - * Implementing the 6PACK protocol is probably the proper solution. */ if (ax25_is_aprs(pp) && tq_count(chan,prio,"","",0) > 100) { @@ -451,7 +490,7 @@ void lm_data_request (int chan, int prio, packet_t pp) } #endif - if (chan < 0 || chan >= MAX_CHANS || save_audio_config_p->achan[chan].medium != MEDIUM_RADIO) { + if (chan < 0 || chan >= MAX_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); @@ -609,7 +648,7 @@ void lm_seize_request (int chan) #endif - if (chan < 0 || chan >= MAX_CHANS || save_audio_config_p->achan[chan].medium != MEDIUM_RADIO) { + if (chan < 0 || chan >= MAX_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); diff --git a/src/tt_text.c b/src/tt_text.c index 9ca7f466..112adfe5 100644 --- a/src/tt_text.c +++ b/src/tt_text.c @@ -382,7 +382,7 @@ int tt_text_to_two_key (const char *text, int quiet, char *buttons) * Outputs: buttons - Sequence of two buttons to press. * "00" for error because this is probably * being used to build up a fixed length - * string where positions are signficant. + * string where positions are significant. * Must be at least 3 bytes. * * Returns: Number of errors detected. @@ -470,7 +470,7 @@ int tt_text_to_call10 (const char *text, int quiet, char *buttons) char padded[8]; char stemp[11]; - + // FIXME: Add parameter for sizeof buttons and use strlcpy strcpy (buttons, ""); /* Quick validity check. */ @@ -540,6 +540,7 @@ int tt_text_to_call10 (const char *text, int quiet, char *buttons) /* Binary to decimal for the columns. */ snprintf (stemp, sizeof(stemp), "%04d", packed); + // FIXME: add parameter for sizeof buttons and use strlcat strcat (buttons, stemp); return (errors); @@ -1435,6 +1436,7 @@ int tt_satsq_to_text (const char *buttons, int quiet, char *text) row = buttons[0] - '0'; col = buttons[1] - '0'; + // FIXME: Add parameter for sizeof text and use strlcpy, strlcat. strcpy (text, grid[row][col]); strcat (text, buttons+2); @@ -1603,11 +1605,11 @@ int main (int argc, char *argv[]) exit (1); } - strcpy (text, argv[1]); + strlcpy (text, argv[1], sizeof(text)); for (n = 2; n < argc; n++) { - strcat (text, " "); - strcat (text, argv[n]); + strlcat (text, " ", sizeof(text)); + strlcat (text, argv[n], sizeof(text)); } dw_printf ("Push buttons for multi-press method:\n"); @@ -1670,7 +1672,7 @@ int main (int argc, char *argv[]) exit (1); } - strcpy (buttons, argv[1]); + strlcpy (buttons, argv[1], sizeof(buttons)); for (n = 2; n < argc; n++) { strlcat (buttons, argv[n], sizeof(buttons)); diff --git a/src/tt_user.c b/src/tt_user.c index c0713264..a73d6a46 100644 --- a/src/tt_user.c +++ b/src/tt_user.c @@ -230,7 +230,7 @@ void tt_user_init (struct audio_s *p_audio_config, struct tt_config_s *p_tt_conf * Inputs: callsign - full or a old style 3 DIGIT suffix abbreviation * overlay * - * Returns: Handle for refering to table position or -1 if not found. + * Returns: Handle for referring to table position or -1 if not found. * This happens to be an index into an array but * the implementation could change so the caller should * not make any assumptions. @@ -290,7 +290,7 @@ int tt_user_search (char *callsign, char overlay) * * Outputs: callsign - corresponding full callsign or empty string. * - * Returns: Handle for refering to table position (>= 0) or -1 if not found. + * Returns: Handle for referring to table position (>= 0) or -1 if not found. * This happens to be an index into an array but * the implementation could change so the caller should * not make any assumptions. @@ -351,7 +351,7 @@ static void clear_user(int i) * * Inputs: none * - * Returns: Handle for refering to table position. + * Returns: Handle for referring to table position. * * Description: If table is already full, this should delete the * least recently heard user to make room. @@ -453,7 +453,7 @@ static void digit_suffix (char *callsign, char *suffix) * * Name: tt_user_heard * - * Purpose: Record information from an APRStt trasmission. + * Purpose: Record information from an APRStt transmission. * * Inputs: callsign - full or an abbreviation * ssid @@ -881,8 +881,8 @@ static void xmit_object_report (int i, int first_time) * Any attached application(s). * IGate. * - * When transmitting over the radio, it gets sent multipe times, to help - * probablity of being heard, with increasing delays between. + * When transmitting over the radio, it gets sent multiple times, to help + * probability of being heard, with increasing delays between. * * The other methods are reliable so we only want to send it once. */ @@ -897,9 +897,9 @@ static void xmit_object_report (int i, int first_time) flen = ax25_pack(pp, fbuf); server_send_rec_packet (save_tt_config_p->obj_recv_chan, pp, fbuf, flen); - kissnet_send_rec_packet (save_tt_config_p->obj_recv_chan, KISS_CMD_DATA_FRAME, fbuf, flen, -1); - kissserial_send_rec_packet (save_tt_config_p->obj_recv_chan, KISS_CMD_DATA_FRAME, fbuf, flen, -1); - kisspt_send_rec_packet (save_tt_config_p->obj_recv_chan, KISS_CMD_DATA_FRAME, fbuf, flen, -1); + kissnet_send_rec_packet (save_tt_config_p->obj_recv_chan, KISS_CMD_DATA_FRAME, fbuf, flen, NULL, -1); + kissserial_send_rec_packet (save_tt_config_p->obj_recv_chan, KISS_CMD_DATA_FRAME, fbuf, flen, NULL, -1); + kisspt_send_rec_packet (save_tt_config_p->obj_recv_chan, KISS_CMD_DATA_FRAME, fbuf, flen, NULL, -1); } if (first_time && save_tt_config_p->obj_send_to_ig) { diff --git a/src/ttcalc.c b/src/ttcalc.c index 51952b92..a031ec0a 100644 --- a/src/ttcalc.c +++ b/src/ttcalc.c @@ -187,7 +187,7 @@ int main (int argc, char *argv[]) ax25_format_addrs (pp, result); info_len = ax25_get_info (pp, (unsigned char **)(&pinfo)); pinfo[info_len] = '\0'; - strcat (result, pinfo); + strlcat (result, pinfo, sizeof(result)); for (p=result; *p!='\0'; p++) { if (! isprint(*p)) *p = ' '; } diff --git a/src/waypoint.c b/src/waypoint.c index c06b362f..20c1cdbc 100644 --- a/src/waypoint.c +++ b/src/waypoint.c @@ -187,10 +187,10 @@ void waypoint_init (struct misc_config_s *mc) s_waypoint_formats = mc->waypoint_formats; if (s_waypoint_formats == 0) { - s_waypoint_formats = WPT_FORMAT_NMEA_GENERIC | WPT_FORMAT_KENWOOD; + s_waypoint_formats = WPL_FORMAT_NMEA_GENERIC | WPL_FORMAT_KENWOOD; } - if (s_waypoint_formats & WPT_FORMAT_GARMIN) { - s_waypoint_formats |= WPT_FORMAT_NMEA_GENERIC; /* See explanation below. */ + if (s_waypoint_formats & WPL_FORMAT_GARMIN) { + s_waypoint_formats |= WPL_FORMAT_NMEA_GENERIC; /* See explanation below. */ } #if DEBUG @@ -298,7 +298,7 @@ void waypoint_send_sentence (char *name_in, double dlat, double dlong, char symt dw_printf ("waypoint_send_sentence (\"%s\", \"%c%c\")\n", name_in, symtab, symbol); #endif -// Don't waste time if no destintations specified. +// Don't waste time if no destinations specified. if (s_waypoint_serial_port_fd == MYFDERROR && s_waypoint_udp_sock_fd == -1) { @@ -376,7 +376,7 @@ void waypoint_send_sentence (char *name_in, double dlat, double dlong, char symt * *99 is checksum */ - if (s_waypoint_formats & WPT_FORMAT_NMEA_GENERIC) { + if (s_waypoint_formats & WPL_FORMAT_NMEA_GENERIC) { snprintf (sentence, sizeof(sentence), "$GPWPL,%s,%s,%s,%s,%s", slat, slat_ns, slong, slong_ew, wname); append_checksum (sentence); @@ -406,7 +406,7 @@ void waypoint_send_sentence (char *name_in, double dlat, double dlong, char symt * *99 is checksum */ - if (s_waypoint_formats & WPT_FORMAT_GARMIN) { + if (s_waypoint_formats & WPL_FORMAT_GARMIN) { int i = symbol - ' '; int grm_sym; /* Garmin symbol code. */ @@ -455,7 +455,7 @@ void waypoint_send_sentence (char *name_in, double dlat, double dlong, char symt * to delete that specific waypoint. */ - if (s_waypoint_formats & WPT_FORMAT_MAGELLAN) { + if (s_waypoint_formats & WPL_FORMAT_MAGELLAN) { int i = symbol - ' '; char sicon[3]; /* Magellan icon string. Currently 1 or 2 characters. */ @@ -567,7 +567,7 @@ void waypoint_send_sentence (char *name_in, double dlat, double dlong, char symt - if (s_waypoint_formats & WPT_FORMAT_KENWOOD) { + if (s_waypoint_formats & WPL_FORMAT_KENWOOD) { time_t now; struct tm tm; @@ -654,7 +654,7 @@ void waypoint_send_ais (char *sentence) return; } - if (s_waypoint_formats & WPT_FORMAT_AIS) { + if (s_waypoint_formats & WPL_FORMAT_AIS) { send_sentence (sentence); } } diff --git a/src/xid.c b/src/xid.c index 617720c7..14e67e8d 100644 --- a/src/xid.c +++ b/src/xid.c @@ -92,7 +92,7 @@ // The first byte must be of the form 1000 0xx0 // The second byte must be of the form 1010 xx00 // The third byte must be of the form 0000 0010 -// If we process the three byte "HDLC Optional Parmeters" like +// If we process the three byte "HDLC Optional Parameters" like // the other multibyte numeric fields, with the most significant // byte first, we end up with bit masks like this. // The bit order would be 8 7 6 5 4 3 2 1 16 15 14 13 12 11 10 9 24 23 22 21 20 19 18 17 diff --git a/src/xmit.c b/src/xmit.c index 6a725a7d..13bbaecb 100644 --- a/src/xmit.c +++ b/src/xmit.c @@ -88,7 +88,7 @@ */ -static int xmit_slottime[MAX_CHANS]; /* Slot time in 10 mS units for persistance algorithm. */ +static int xmit_slottime[MAX_CHANS]; /* Slot time in 10 mS units for persistence algorithm. */ static int xmit_persist[MAX_CHANS]; /* Sets probability for transmitting after each */ /* slot time delay. Transmit if a random number */ @@ -278,7 +278,7 @@ void xmit_init (struct audio_s *p_modem, int debug_xmit_packet) for (j=0; jachan[j].medium == MEDIUM_RADIO) { + if (p_modem->chan_medium[j] == MEDIUM_RADIO) { #if __WIN32__ xmit_th[j] = (HANDLE)_beginthreadex (NULL, 0, xmit_thread, (void*)(ptrdiff_t)j, 0, NULL); if (xmit_th[j] == NULL) { @@ -597,6 +597,11 @@ static void * xmit_thread (void *arg) case FLAVOR_APRS_DIGI: xmit_ax25_frames (chan, prio, pp, 1); /* 1 means don't bundle */ + // I don't know if this in some official specification + // somewhere, but it is generally agreed that APRS digipeaters + // should send only one frame at a time rather than + // bundling multiple frames into a single transmission. + // Discussion here: http://lists.tapr.org/pipermail/aprssig_lists.tapr.org/2021-September/049034.html break; case FLAVOR_APRS_NEW: @@ -761,7 +766,7 @@ static void xmit_ax25_frames (int chan, int prio, packet_t pp, int max_bundle) // machine, that the transmission opportunity has arrived." pre_flags = MS_TO_BITS(xmit_txdelay[chan] * 10, chan) / 8; - num_bits = hdlc_send_flags (chan, pre_flags, 0); + num_bits = layer2_preamble_postamble (chan, pre_flags, 0, save_audio_config_p); #if DEBUG text_color_set(DW_COLOR_DEBUG); dw_printf ("xmit_thread: t=%.3f, txdelay=%d [*10], pre_flags=%d, num_bits=%d\n", dtime_now()-time_ptt, xmit_txdelay[chan], pre_flags, num_bits); @@ -862,7 +867,7 @@ static void xmit_ax25_frames (int chan, int prio, packet_t pp, int max_bundle) */ post_flags = MS_TO_BITS(xmit_txtail[chan] * 10, chan) / 8; - nb = hdlc_send_flags (chan, post_flags, 1); + nb = layer2_preamble_postamble (chan, post_flags, 1, save_audio_config_p); num_bits += nb; #if DEBUG text_color_set(DW_COLOR_DEBUG); @@ -957,8 +962,6 @@ static void xmit_ax25_frames (int chan, int prio, packet_t pp, int max_bundle) static int send_one_frame (int c, int p, packet_t pp) { - unsigned char fbuf[AX25_MAX_PACKET_LEN+2]; - int flen; char stemp[1024]; /* max size needed? */ int info_len; unsigned char *pinfo; @@ -1002,10 +1005,10 @@ static int send_one_frame (int c, int p, packet_t pp) ax25_format_addrs (pp, stemp); info_len = ax25_get_info (pp, &pinfo); text_color_set(DW_COLOR_XMIT); -#if 0 +#if 0 // FIXME - enable this? dw_printf ("[%d%c%s%s] ", c, p==TQ_PRIO_0_HI ? 'H' : 'L', - save_audio_config_p->fx25_xmit_enable ? "F" : "", + save_audio_config_p->achan[c].fx25_strength ? "F" : "", ts); #else dw_printf ("[%d%c%s] ", c, p==TQ_PRIO_0_HI ? 'H' : 'L', ts); @@ -1059,9 +1062,6 @@ static int send_one_frame (int c, int p, packet_t pp) /* * Transmit the frame. */ - flen = ax25_pack (pp, fbuf); - assert (flen >= 1 && flen <= (int)(sizeof(fbuf))); - int send_invalid_fcs2 = 0; if (save_audio_config_p->xmit_error_rate != 0) { @@ -1074,7 +1074,7 @@ static int send_one_frame (int c, int p, packet_t pp) } } - nb = hdlc_send_frame (c, fbuf, flen, send_invalid_fcs2, save_audio_config_p->fx25_xmit_enable); + nb = layer2_send_frame (c, pp, send_invalid_fcs2, save_audio_config_p); // Optionally send confirmation to AGW client app if monitoring enabled. @@ -1384,7 +1384,7 @@ static void xmit_dtmf (int c, packet_t pp, int speed) * New in version 1.5: full duplex. * Just start transmitting rather than waiting for clear channel. * This would only be appropriate when transmit and receive are - * using different radio freqencies. e.g. VHF up, UHF down satellite. + * using different radio frequencies. e.g. VHF up, UHF down satellite. * * Transmit delay algorithm: * diff --git a/systemd/direwolf.service b/systemd/direwolf.service index d4109a04..c3380fac 100644 --- a/systemd/direwolf.service +++ b/systemd/direwolf.service @@ -1,6 +1,7 @@ [Unit] Description=Direwolf Sound Card-based AX.25 TNC After=sound.target +After=network.target [Service] EnvironmentFile=/etc/sysconfig/direwolf @@ -22,3 +23,5 @@ ReadWritePaths=/var/log/direwolf [Install] WantedBy=multi-user.target DefaultInstance=1 + +# alternate version: https://www.f4fxl.org/start-direwolf-at-boot-the-systemd-way/ diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 6d4336e1..91e06a2c 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -6,6 +6,7 @@ set(GEN_PACKETS_BIN "${CMAKE_BINARY_DIR}/src/gen_packets${CMAKE_EXECUTABLE_SUFFI set(ATEST_BIN "${CMAKE_BINARY_DIR}/src/atest${CMAKE_EXECUTABLE_SUFFIX}") set(FXSEND_BIN "${CMAKE_BINARY_DIR}/test/fxsend${CMAKE_EXECUTABLE_SUFFIX}") set(FXREC_BIN "${CMAKE_BINARY_DIR}/test/fxrec${CMAKE_EXECUTABLE_SUFFIX}") +set(IL2P_TEST_BIN "${CMAKE_BINARY_DIR}/test/il2p_test${CMAKE_EXECUTABLE_SUFFIX}") if(WIN32) set(CUSTOM_SCRIPT_SUFFIX ".bat") @@ -14,14 +15,18 @@ else() endif() set(TEST_CHECK-FX25_FILE "check-fx25") +set(TEST_CHECK-IL2P_FILE "check-il2p") set(TEST_CHECK-MODEM1200_FILE "check-modem1200") +set(TEST_CHECK-MODEM1200_IL2P_FILE "check-modem1200-i") set(TEST_CHECK-MODEM300_FILE "check-modem300") set(TEST_CHECK-MODEM9600_FILE "check-modem9600") +set(TEST_CHECK-MODEM9600_IL2P_FILE "check-modem9600-i") set(TEST_CHECK-MODEM19200_FILE "check-modem19200") set(TEST_CHECK-MODEM2400-a_FILE "check-modem2400-a") set(TEST_CHECK-MODEM2400-b_FILE "check-modem2400-b") set(TEST_CHECK-MODEM2400-g_FILE "check-modem2400-g") set(TEST_CHECK-MODEM4800_FILE "check-modem4800") +set(TEST_CHECK-MODEMEAS_FILE "check-modemeas") # generate the scripts that run the tests @@ -31,12 +36,24 @@ configure_file( @ONLY ) +configure_file( + "${CUSTOM_TEST_SCRIPTS_DIR}/${TEST_CHECK-IL2P_FILE}" + "${CUSTOM_TEST_BINARY_DIR}/${TEST_CHECK-IL2P_FILE}${CUSTOM_SCRIPT_SUFFIX}" + @ONLY + ) + configure_file( "${CUSTOM_TEST_SCRIPTS_DIR}/${TEST_CHECK-MODEM1200_FILE}" "${CUSTOM_TEST_BINARY_DIR}/${TEST_CHECK-MODEM1200_FILE}${CUSTOM_SCRIPT_SUFFIX}" @ONLY ) +configure_file( + "${CUSTOM_TEST_SCRIPTS_DIR}/${TEST_CHECK-MODEM1200_IL2P_FILE}" + "${CUSTOM_TEST_BINARY_DIR}/${TEST_CHECK-MODEM1200_IL2P_FILE}${CUSTOM_SCRIPT_SUFFIX}" + @ONLY + ) + configure_file( "${CUSTOM_TEST_SCRIPTS_DIR}/${TEST_CHECK-MODEM300_FILE}" "${CUSTOM_TEST_BINARY_DIR}${CUSTOM_SCRIPT_SUFFIX}" @@ -49,6 +66,12 @@ configure_file( @ONLY ) +configure_file( + "${CUSTOM_TEST_SCRIPTS_DIR}/${TEST_CHECK-MODEM9600_IL2P_FILE}" + "${CUSTOM_TEST_BINARY_DIR}/${TEST_CHECK-MODEM9600_IL2P_FILE}${CUSTOM_SCRIPT_SUFFIX}" + @ONLY + ) + configure_file( "${CUSTOM_TEST_SCRIPTS_DIR}/${TEST_CHECK-MODEM19200_FILE}" "${CUSTOM_TEST_BINARY_DIR}/${TEST_CHECK-MODEM19200_FILE}${CUSTOM_SCRIPT_SUFFIX}" @@ -79,6 +102,12 @@ configure_file( @ONLY ) +configure_file( + "${CUSTOM_TEST_SCRIPTS_DIR}/${TEST_CHECK-MODEMEAS_FILE}" + "${CUSTOM_TEST_BINARY_DIR}/${TEST_CHECK-MODEMEAS_FILE}${CUSTOM_SCRIPT_SUFFIX}" + @ONLY + ) + # global includes # not ideal but not so slow @@ -100,63 +129,12 @@ if(WIN32 OR CYGWIN) ) endif() -# Why is there a special atest9 instead of using the normal one? - -# Unit test for demodulators -list(APPEND atest9_SOURCES - ${CUSTOM_SRC_DIR}/atest.c - ${CUSTOM_SRC_DIR}/ais.c - ${CUSTOM_SRC_DIR}/demod.c - ${CUSTOM_SRC_DIR}/dsp.c - ${CUSTOM_SRC_DIR}/demod_afsk.c - ${CUSTOM_SRC_DIR}/demod_psk.c - ${CUSTOM_SRC_DIR}/demod_9600.c - ${CUSTOM_SRC_DIR}/fx25_extract.c - ${CUSTOM_SRC_DIR}/fx25_init.c - ${CUSTOM_SRC_DIR}/fx25_rec.c - ${CUSTOM_SRC_DIR}/hdlc_rec.c - ${CUSTOM_SRC_DIR}/hdlc_rec2.c - ${CUSTOM_SRC_DIR}/multi_modem.c - ${CUSTOM_SRC_DIR}/rrbb.c - ${CUSTOM_SRC_DIR}/fcs_calc.c - ${CUSTOM_SRC_DIR}/ax25_pad.c - ${CUSTOM_SRC_DIR}/decode_aprs.c - ${CUSTOM_SRC_DIR}/dwgpsnmea.c - ${CUSTOM_SRC_DIR}/dwgps.c - ${CUSTOM_SRC_DIR}/dwgpsd.c - ${CUSTOM_SRC_DIR}/serial_port.c - ${CUSTOM_SRC_DIR}/latlong.c - ${CUSTOM_SRC_DIR}/symbols.c - ${CUSTOM_SRC_DIR}/textcolor.c - ${CUSTOM_SRC_DIR}/telemetry.c - ${CUSTOM_SRC_DIR}/dtime_now.c - ${CUSTOM_SRC_DIR}/tt_text.c - ) - if(WIN32 OR CYGWIN) list(REMOVE_ITEM atest9_SOURCES ${CUSTOM_SRC_DIR}/dwgpsd.c ) endif() -add_executable(atest9 - ${atest9_SOURCES} - ) - -target_link_libraries(atest9 - ${MISC_LIBRARIES} - ${REGEX_LIBRARIES} - ${GPSD_LIBRARIES} - Threads::Threads - ) - -if(WIN32 OR CYGWIN) - set_target_properties(atest9 - PROPERTIES COMPILE_FLAGS "-DUSE_REGEX_STATIC" - ) - target_link_libraries(atest9 ws2_32) -endif() - # Unit test for inner digipeater algorithm list(APPEND dtest_SOURCES @@ -205,7 +183,7 @@ if(WIN32 OR CYGWIN) endif() -# Unit test for APRStt tone seqence parsing. +# Unit test for APRStt tone sequence parsing. list(APPEND ttest_SOURCES ${CUSTOM_SRC_DIR}/aprs_tt.c ${CUSTOM_SRC_DIR}/tt_text.c @@ -468,6 +446,39 @@ set_target_properties(fxrec ) +# Unit Test IL2P with out modems. + +list(APPEND il2p_test_SOURCES + ${CUSTOM_SRC_DIR}/il2p_test.c + ${CUSTOM_SRC_DIR}/il2p_init.c + ${CUSTOM_SRC_DIR}/il2p_rec.c + ${CUSTOM_SRC_DIR}/il2p_send.c + ${CUSTOM_SRC_DIR}/il2p_codec.c + ${CUSTOM_SRC_DIR}/il2p_payload.c + ${CUSTOM_SRC_DIR}/il2p_header.c + ${CUSTOM_SRC_DIR}/il2p_scramble.c + ${CUSTOM_SRC_DIR}/ax25_pad.c + ${CUSTOM_SRC_DIR}/ax25_pad2.c + ${CUSTOM_SRC_DIR}/fx25_encode.c + ${CUSTOM_SRC_DIR}/fx25_extract.c + ${CUSTOM_SRC_DIR}/fx25_init.c + ${CUSTOM_SRC_DIR}/fcs_calc.c + ${CUSTOM_SRC_DIR}/textcolor.c + ) + +add_executable(il2p_test + ${il2p_test_SOURCES} + ) + +#FIXME - remove if not needed. +#set_target_properties(il2p_test +# PROPERTIES COMPILE_FLAGS "-DXXXXX" +# ) + +target_link_libraries(il2p_test + ${MISC_LIBRARIES} + ) + # doing ctest on previous programs add_test(dtest dtest) @@ -483,17 +494,19 @@ add_test(xidtest xidtest) add_test(dtmftest dtmftest) add_test(check-fx25 "${CUSTOM_TEST_BINARY_DIR}/${TEST_CHECK-FX25_FILE}${CUSTOM_SCRIPT_SUFFIX}") +add_test(check-il2p "${CUSTOM_TEST_BINARY_DIR}/${TEST_CHECK-IL2P_FILE}${CUSTOM_SCRIPT_SUFFIX}") add_test(check-modem1200 "${CUSTOM_TEST_BINARY_DIR}/${TEST_CHECK-MODEM1200_FILE}${CUSTOM_SCRIPT_SUFFIX}") +add_test(check-modem1200-i "${CUSTOM_TEST_BINARY_DIR}/${TEST_CHECK-MODEM1200_IL2P_FILE}${CUSTOM_SCRIPT_SUFFIX}") add_test(check-modem300 "${CUSTOM_TEST_BINARY_DIR}/${TEST_CHECK-MODEM300_FILE}${CUSTOM_SCRIPT_SUFFIX}") add_test(check-modem9600 "${CUSTOM_TEST_BINARY_DIR}/${TEST_CHECK-MODEM9600_FILE}${CUSTOM_SCRIPT_SUFFIX}") +add_test(check-modem9600-i "${CUSTOM_TEST_BINARY_DIR}/${TEST_CHECK-MODEM9600_IL2P_FILE}${CUSTOM_SCRIPT_SUFFIX}") add_test(check-modem19200 "${CUSTOM_TEST_BINARY_DIR}/${TEST_CHECK-MODEM19200_FILE}${CUSTOM_SCRIPT_SUFFIX}") add_test(check-modem2400-a "${CUSTOM_TEST_BINARY_DIR}/${TEST_CHECK-MODEM2400-a_FILE}${CUSTOM_SCRIPT_SUFFIX}") add_test(check-modem2400-b "${CUSTOM_TEST_BINARY_DIR}/${TEST_CHECK-MODEM2400-b_FILE}${CUSTOM_SCRIPT_SUFFIX}") add_test(check-modem2400-g "${CUSTOM_TEST_BINARY_DIR}/${TEST_CHECK-MODEM2400-g_FILE}${CUSTOM_SCRIPT_SUFFIX}") add_test(check-modem4800 "${CUSTOM_TEST_BINARY_DIR}/${TEST_CHECK-MODEM4800_FILE}${CUSTOM_SCRIPT_SUFFIX}") +add_test(check-modemeas "${CUSTOM_TEST_BINARY_DIR}/${TEST_CHECK-MODEMEAS_FILE}${CUSTOM_SCRIPT_SUFFIX}") -# TODO miss the audio file -# ./atest9 -B 9600 ../walkabout9600.wav | grep "packets decoded in" >atest.out # ----------------------------- Manual tests and experiments --------------------------- diff --git a/test/scripts/check-fx25 b/test/scripts/check-fx25 index 37df174f..d5003337 100755 --- a/test/scripts/check-fx25 +++ b/test/scripts/check-fx25 @@ -3,3 +3,14 @@ @FXSEND_BIN@ @FXREC_BIN@ +@GEN_PACKETS_BIN@ -B9600 -n 100 -X 0 -o test96f0.wav +@ATEST_BIN@ -B9600 -F0 -L60 -G64 test96f0.wav + +@GEN_PACKETS_BIN@ -B9600 -n 100 -X 16 -o test96f16.wav +@ATEST_BIN@ -B9600 -F0 -L63 -G67 test96f16.wav + +@GEN_PACKETS_BIN@ -B9600 -n 100 -X 32 -o test96f32.wav +@ATEST_BIN@ -B9600 -F0 -L64 -G68 test96f32.wav + +@GEN_PACKETS_BIN@ -B9600 -n 100 -X 64 -o test96f64.wav +@ATEST_BIN@ -B9600 -F0 -L71 -G75 test96f64.wav diff --git a/test/scripts/check-il2p b/test/scripts/check-il2p new file mode 100755 index 00000000..da404720 --- /dev/null +++ b/test/scripts/check-il2p @@ -0,0 +1,4 @@ +@CUSTOM_SHELL_SHABANG@ + +@IL2P_TEST_BIN@ + diff --git a/test/scripts/check-modem1200 b/test/scripts/check-modem1200 index 2b975871..d3022796 100755 --- a/test/scripts/check-modem1200 +++ b/test/scripts/check-modem1200 @@ -1,5 +1,7 @@ @CUSTOM_SHELL_SHABANG@ @GEN_PACKETS_BIN@ -n 100 -o test12.wav -@ATEST_BIN@ -F0 -PE -L64 -G72 test12.wav -@ATEST_BIN@ -F1 -PE -L70 -G75 test12.wav +@ATEST_BIN@ -F0 -PA -D1 -L66 -G72 test12.wav +@ATEST_BIN@ -F1 -PA -D1 -L72 -G78 test12.wav +@ATEST_BIN@ -F0 -PB -D1 -L66 -G74 test12.wav +@ATEST_BIN@ -F1 -PB -D1 -L70 -G82 test12.wav diff --git a/test/scripts/check-modem1200-i b/test/scripts/check-modem1200-i new file mode 100755 index 00000000..9ef01bab --- /dev/null +++ b/test/scripts/check-modem1200-i @@ -0,0 +1,4 @@ +@CUSTOM_SHELL_SHABANG@ + +@GEN_PACKETS_BIN@ -I1 -n 100 -o test12-il2p.wav +@ATEST_BIN@ -P+ -D1 -L92 -G94 test12-il2p.wav diff --git a/test/scripts/check-modem300 b/test/scripts/check-modem300 index da37dd2b..6d6e6432 100755 --- a/test/scripts/check-modem300 +++ b/test/scripts/check-modem300 @@ -1,5 +1,7 @@ @CUSTOM_SHELL_SHABANG@ @GEN_PACKETS_BIN@ -B300 -n 100 -o test3.wav -@ATEST_BIN@ -B300 -F0 -L68 -G69 test3.wav -@ATEST_BIN@ -B300 -F1 -L71 -G75 test3.wav +@ATEST_BIN@ -B300 -PA -F0 -L65 -G71 test3.wav +@ATEST_BIN@ -B300 -PA -F1 -L69 -G75 test3.wav +@ATEST_BIN@ -B300 -PB -F0 -L69 -G75 test3.wav +@ATEST_BIN@ -B300 -PB -F1 -L73 -G79 test3.wav diff --git a/test/scripts/check-modem9600-i b/test/scripts/check-modem9600-i new file mode 100755 index 00000000..0ba01bea --- /dev/null +++ b/test/scripts/check-modem9600-i @@ -0,0 +1,18 @@ +@CUSTOM_SHELL_SHABANG@ + +@GEN_PACKETS_BIN@ -B9600 -I1 -n 100 -o test96-il2p-I1.wav +@ATEST_BIN@ -B9600 -L72 -G76 test96-il2p-I1.wav +@ATEST_BIN@ -B9600 -P+ -L76 -G80 test96-il2p-I1.wav + +@GEN_PACKETS_BIN@ -B9600 -I0 -n 100 -o test96-il2p-I0.wav +@ATEST_BIN@ -B9600 -L64 -G68 test96-il2p-I0.wav + + +@GEN_PACKETS_BIN@ -B9600 -i1 -n 100 -o test96-il2p-i1.wav +@ATEST_BIN@ -B9600 -L70 -G74 test96-il2p-i1.wav +@ATEST_BIN@ -B9600 -P+ -L73 -G77 test96-il2p-i1.wav + +@GEN_PACKETS_BIN@ -B9600 -i0 -n 100 -o test96-il2p-i0.wav +@ATEST_BIN@ -B9600 -L67 -G71 test96-il2p-i0.wav + + diff --git a/test/scripts/check-modemeas b/test/scripts/check-modemeas new file mode 100755 index 00000000..c2a88539 --- /dev/null +++ b/test/scripts/check-modemeas @@ -0,0 +1,4 @@ +@CUSTOM_SHELL_SHABANG@ + +@GEN_PACKETS_BIN@ -B EAS -o testeas.wav +@ATEST_BIN@ -B EAS -L 6 -G 6 testeas.wav