Merge branch 'dev' into bugfix/ThirsPartyType

This commit is contained in:
Geoffrey Merck 2023-02-20 18:07:25 +01:00
commit 33cda1f447
33 changed files with 655 additions and 103 deletions

170
.github/workflows/ci.yml vendored Normal file
View File

@ -0,0 +1,170 @@
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: ''
}
- {
name: 'Ubuntu 18.04',
os: ubuntu-18.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

73
.github/workflows/codeql-analysis.yml vendored Normal file
View File

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

View File

@ -7,6 +7,9 @@
### New Features: ###
- Additional documentation location to slow down growth of main repository. [https://github.com/wb2osz/direwolf-doc](https://github.com/wb2osz/direwolf-doc)
- 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.

View File

@ -167,15 +167,16 @@ elseif(APPLE)
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")
endif()
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()
endif()
if (C_CLANG OR C_GCC)

View File

@ -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)

View File

@ -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.
*/

View File

@ -68,6 +68,7 @@
#include <string.h>
#include <time.h>
#include <getopt.h>
#include <ctype.h>
#define ATEST_C 1

View File

@ -1532,7 +1532,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.
*
*----------------------------------------------------------------*/

View File

@ -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,7 +102,7 @@ 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. */

View File

@ -1260,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.
*
*----------------------------------------------------------------*/

View File

@ -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.
@ -1074,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.
*
*----------------------------------------------------------------*/

View File

@ -347,7 +347,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
@ -6049,7 +6049,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.
*

View File

@ -1866,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().
*
*------------------------------------------------------------------------------*/
@ -2923,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);
}

View File

@ -162,6 +162,7 @@ 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->chan_medium[chan] == MEDIUM_RADIO ||
g_modem_config_p->chan_medium[chan] == MEDIUM_NETTNC) {
@ -621,6 +622,7 @@ static void * beacon_thread (void *arg)
// 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 */
@ -805,11 +807,17 @@ static void beacon_send (int j, dwgps_info_t *gpsinfo)
assert (bp->sendto_chan >= 0);
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;
}
@ -1046,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;

View File

@ -5594,7 +5594,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->chan_medium[n] == 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;
@ -5604,7 +5605,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->chan_medium[n] == 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;
@ -5615,7 +5617,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->chan_medium[n] == 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;
@ -5829,7 +5832,7 @@ 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 surprized to end up with Solar Powser (S-).
* 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 \.
*/
@ -5864,12 +5867,24 @@ static int beacon_options(char *cmd, struct beacon_s *b, int line, struct audio_
if (b->sendto_type == SENDTO_XMIT) {
if ( b->sendto_chan < 0 || b->sendto_chan >= MAX_CHANS || p_audio_config->chan_medium[b->sendto_chan] == MEDIUM_NONE) {
if (( b->sendto_chan < 0 || b->sendto_chan >= MAX_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 (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", 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 ) {
@ -5879,6 +5894,7 @@ static int beacon_options(char *cmd, struct beacon_s *b, int line, struct audio_
return (0);
}
}
}
return (1);
}

View File

@ -858,6 +858,32 @@ static void aprs_ll_pos (decode_aprs_t *A, unsigned char *info, int ilen)
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. */
@ -1375,7 +1401,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.
@ -1579,7 +1605,7 @@ 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. Be carefull not to modify it here!
* Inputs: info - Pointer to Information field. Be careful not to modify it here!
* ilen - Information field length.
* quiet - suppress error messages.
*
@ -2372,6 +2398,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;
@ -2524,6 +2564,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? */

View File

@ -832,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.
*

View File

@ -309,10 +309,6 @@ void demod_afsk_init (int samples_per_sec, int baud, int mark_freq,
D->lp_window = BP_WINDOW_TRUNCATED;
}
D->agc_fast_attack = 0.820;
D->agc_slow_decay = 0.000214;
D->agc_fast_attack = 0.45;
D->agc_slow_decay = 0.000195;
D->agc_fast_attack = 0.70;
D->agc_slow_decay = 0.000090;
@ -372,10 +368,16 @@ void demod_afsk_init (int samples_per_sec, int baud, int mark_freq,
// 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; // FIXME: disable display
D->alevel_mark_peak = -1; // Disable received signal (m/s) display.
D->alevel_space_peak = -1;
break;
@ -868,6 +870,7 @@ static void nudge_pll (int chan, int subchan, int slice, float demod_out, struct
{
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));
@ -901,7 +904,15 @@ static void nudge_pll (int chan, int subchan, int slice, float demod_out, struct
#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);
}
@ -912,12 +923,14 @@ static void nudge_pll (int chan, int subchan, int slice, float demod_out, struct
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;
}
/*

View File

@ -1053,7 +1053,7 @@ int main (int argc, char *argv[])
audio_config.achan[x_opt_chan].mark_freq,
x_opt_chan);
while (n-- > 0) {
tone_gen_put_bit(x_opt_chan, 0);
tone_gen_put_bit(x_opt_chan, 1);
}
break;
case 's': // "Space" tone: -x s
@ -1061,7 +1061,7 @@ int main (int argc, char *argv[])
audio_config.achan[x_opt_chan].space_freq,
x_opt_chan);
while (n-- > 0) {
tone_gen_put_bit(x_opt_chan, 1);
tone_gen_put_bit(x_opt_chan, 0);
}
break;
case 'p': // Silence - set PTT only: -x p

View File

@ -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
*
@ -41,10 +41,23 @@
* 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_now (void)
double dtime_realtime (void)
{
double result;
@ -63,6 +76,10 @@ double dtime_now (void)
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;
@ -75,6 +92,83 @@ double dtime_now (void)
#endif
#if DEBUG
text_color_set(DW_COLOR_DEBUG);
dw_printf ("dtime_realtime() returns %.3f\n", result );
#endif
return (result);
}
/*------------------------------------------------------------------
*
* 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;
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__
// 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
// 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);
#endif
#if DEBUG
text_color_set(DW_COLOR_DEBUG);
dw_printf ("dtime_now() returns %.3f\n", result );
@ -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;

View File

@ -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);
// FIXME: remove temp workaround.
// Needs many scattered updates.
#define dtime_now dtime_realtime

View File

@ -57,18 +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
// release lib version API Raspberry Pi OS
// 3.22 28 11 bullseye
// 3.23 29 12
// 3.24 14 Not tested yet.
#if GPSD_API_MAJOR_VERSION < 5 || GPSD_API_MAJOR_VERSION > 12
#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.
*/
@ -168,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
*
*/

View File

@ -367,7 +367,7 @@ struct demodulator_state_s
// 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 cummulated round off errors.
// This only works with integers because float would have cumulated round off errors.
cic_t cic_center1;
cic_t cic_above;

View File

@ -1,7 +1,7 @@
//
// This file is part of Dire Wolf, an amateur radio packet TNC.
//
// Copyright (C) 2013, 2014, 2015, 2016 John Langner, WB2OSZ
// Copyright (C) 2013, 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
@ -328,7 +328,7 @@ static int stats_uplink_packets; /* Number of packets passed along to the IGate
/* server after filtering. */
static int stats_uplink_bytes; /* Total number of bytes sent to IGate server */
/* including login, packets, and hearbeats. */
/* including login, packets, and heartbeats. */
static int stats_downlink_bytes; /* Total number of bytes from IGate server including */
/* packets, heartbeats, other messages. */
@ -855,6 +855,9 @@ static void * connnect_thread (void *arg)
* Purpose: Send a packet to the IGate server
*
* Inputs: chan - Radio channel it was received on.
* This is required for the RF>IS 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! **
@ -902,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) {
@ -1221,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.
@ -1517,32 +1525,36 @@ static void * igate_recv_thread (void *arg)
int ichan = save_audio_config_p->igate_vchannel;
// 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.
// 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.
// Possible problem: Up to 8 digipeaters are allowed in radio format.
// (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
packet_t pp3 = ax25_from_text((char*)message, 0); // 0 means not strict
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) {
// Should we remove the VIA path?
// For example, we might get something like this from the server.
// Lower case 'q' and non-numeric SSID are not valid for AX.25 over the air.
// K1USN-1>APWW10,TCPIP*,qAC,N5JXS-F1:T#479,100,048,002,500,000,10000000
// Should we try to retain all information and pass that along, to the best of our ability,
// to the client app, or should we remove the via path so it looks like this?
// K1USN-1>APWW10:T#479,100,048,002,500,000,10000000
// For now, keep it intact and see if it causes problems. Easy to remove like this:
// while (ax25_get_num_repeaters(pp3) > 0) {
// ax25_remove_addr (pp3, AX25_REPEATER_1);
// }
alevel_t alevel;
memset (&alevel, 0, sizeof(alevel));
alevel.mark = -2; // FIXME: Do we want some other special case?
@ -1831,8 +1843,19 @@ 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.

View File

@ -437,7 +437,7 @@ packet_t il2p_decode_header_type_1 (unsigned char *hdr, int num_sym_changed)
// 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 uppper case letters and digits.
// 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

View File

@ -194,7 +194,7 @@ int il2p_encode_payload (unsigned char *payload, int payload_size, int max_fec,
* Purpose: Extract original data from encoded payload.
*
* Inputs: received Array of bytes. Size is unknown but in practice it
* must not exceeed IL2P_MAX_ENCODED_SIZE.
* 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.

View File

@ -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
@ -611,7 +611,8 @@ void kiss_process_msg (unsigned char *kiss_msg, int kiss_len, int debug, struct
/* Verify that the radio channel number is valid. */
/* Any sort of medium should be OK here. */
if (chan < 0 || chan >= MAX_CHANS || save_audio_config_p->chan_medium[chan] == MEDIUM_NONE) {
if ((chan < 0 || chan >= MAX_CHANS || save_audio_config_p->chan_medium[chan] == MEDIUM_NONE)
&& save_audio_config_p->chan_medium[chan] != MEDIUM_IGATE) {
text_color_set(DW_COLOR_ERROR);
dw_printf ("Invalid transmit channel %d from KISS client app.\n", chan);
dw_printf ("\n");

View File

@ -1421,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.
*/

View File

@ -681,7 +681,7 @@ void symbols_from_dest_or_src (char dti, char *src, char *dest, char *symtab, ch
// 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 postion because it would not show up on a map.
// 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.

View File

@ -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"
@ -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,6 +251,43 @@ void tq_append (int chan, int prio, packet_t pp)
}
#endif
// New in 1.7 - A channel can be assigned to the IGate rather than a radio.
#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);
@ -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) {

View File

@ -882,7 +882,7 @@ static void xmit_object_report (int i, int first_time)
* IGate.
*
* When transmitting over the radio, it gets sent multiple times, to help
* probablity of being heard, with increasing delays between.
* probability of being heard, with increasing delays between.
*
* The other methods are reliable so we only want to send it once.
*/

View File

@ -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) {

View File

@ -600,7 +600,7 @@ static void * xmit_thread (void *arg)
// 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
// bunding multiple frames into a single transmission.
// bundling multiple frames into a single transmission.
// Discussion here: http://lists.tapr.org/pipermail/aprssig_lists.tapr.org/2021-September/049034.html
break;