R04H30 Merge to Master (#88)

* implement support for LC_GROUP_UPDT (this includes the RPC between the CC and VC's to announce active TG lists); completely and entirely refactor how packet handling threads are done, use a new method introduced for this, thread pool resources. this will ultimately be more resource consuming depending on configuration as the worker threads for packet processing stay alive along side the main process. this should be more performant (because we're not constantly creating and destroying threads) and will prevent error conditions that can cause an extreme number of threads to spawn;

* reduce influx and V.24 thread pool sizes; add calculation of how long between when a packet was Rx to when it began proper processing;

* don't use void* for the task routines;

* rename ThreadPoolCallback to ThreadPoolTask; add some checking around task validness; correct a valgrind issue with RawFrameQueue() write not deleting the buffer before return;

* valgrind cleanups;

* gate active TG from CC to VC updates at 5s (prevent API spam);

* update package version;

* don't attempt to send active TG updates to 0.0.0.0;

* make notification of active TGs CC -> VC optional;

* correct ThreadPool issue on Win32; split UDP PCM audio processing into its own thread; implement user control of inter-audio frame delay and jitter buffer (if using inter-audio frame delay);

* lock queue;

* add custom classes for STL containers that support mutex locking for thread safe operation; modify ChannelLookup and AffiliationLookup to use concurrent containers; modify FNE to use concurrent containers for internal lists;

* remove mutex used for protecting udp packet deque;

* more concurrency solidification;

* further concurrency class usage; bump version number to R04H30;

* add --boot commandline argument to reboot modem into bootloader without any interactive interface;

* cleanup program -h usage display;

* incorrect opcode define;

* update README.md;

* correct incorrect string format for non-useAlternatePortForDiagnostics;

* update README.md;

* simplify influxdb worker task function;

* refactor PL_ACT_PEER_LIST opcode entirely, use zlib and compress list sent and properly block data sent;

* don't waste cycles on building the peer list repeatedly, build it once for the cycle;

* deduplicate compress/decompress code into a static C++ class;

* stylecop file formatting;

* add table locking and remove at find;

* deduplicate implementation;

* cleanup unused label; change FrameQueue's unordered_map to a concurrent one; fix incorrect setting of __lock() for concurrent containers;

* add more timestream map locking;

* fix incorrect behavior when deriving initial timestamp for a call stream; correct incorrect behavior inserting new stream in to timestamps table for frame queue; fix incorrect behavior deriving timestamp for bridge RTP;

* fix issue with naive approach to handling PL_ACT_PEER_LIST data fragementation;

* disable accidental debug code;

* instead of asserting, throw a log error message and discard network packet;

* lock the talkgroup tables when a find is in progress (this will prevent some weird concurrency behaviors because talkgroup rules does not use the concurrent vector);

* if filter headers or terminators is enabled, and the target peer is in the exclusion list, do not send headers or terminators;

* exclusion check should happen before the rewrite check;

* implement dvmpatch, this is a new utility that allows simple TG to TG patching;

* ensure FNE downstream peers that report as peer link have implicit always rules applied to them (i.e. they will *always* receive *all* traffic); correct order of operations when deleting a peer entry (delete connection *AFTER* removing from peers table);

* document concern over possible null ref concurrency issue;

* don't be overly aggressive with FNE process niceness, nice of -10 is more then sufficient;
4.30h_maint 2025-05-01
Bryan Biedenkapp 9 months ago committed by GitHub
parent c10087e6cd
commit af2e7c9c8d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -4,7 +4,7 @@
# * GPLv2 Open Source. Use is subject to license terms.
# * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
# *
# * Copyright (C) 2022,2024 Bryan Biedenkapp, N2PLL
# * Copyright (C) 2022,2024,2025 Bryan Biedenkapp, N2PLL
# * Copyright (C) 2022 Natalie Moore
# *
# */
@ -267,7 +267,8 @@ install(TARGETS sysview DESTINATION ${CMAKE_INSTALL_PREFIX}/bin)
install(TARGETS tged DESTINATION ${CMAKE_INSTALL_PREFIX}/bin)
endif (ENABLE_TUI_SUPPORT AND (NOT DISABLE_TUI_APPS))
install(TARGETS dvmbridge DESTINATION ${CMAKE_INSTALL_PREFIX}/bin)
install(FILES configs/config.example.yml configs/fne-config.example.yml configs/fne-sysview.example.yml configs/monitor-config.example.yml configs/iden_table.example.dat configs/RSSI.dat configs/rid_acl.example.dat configs/talkgroup_rules.example.yml configs/bridge-config.example.yml DESTINATION ${CMAKE_INSTALL_PREFIX}/etc)
install(TARGETS dvmpatch DESTINATION ${CMAKE_INSTALL_PREFIX}/bin)
install(FILES configs/config.example.yml configs/fne-config.example.yml configs/fne-sysview.example.yml configs/monitor-config.example.yml configs/iden_table.example.dat configs/RSSI.dat configs/rid_acl.example.dat configs/talkgroup_rules.example.yml configs/bridge-config.example.yml configs/patch-config.example.yml DESTINATION ${CMAKE_INSTALL_PREFIX}/etc)
install(PROGRAMS tools/start-dvm.sh tools/stop-dvm.sh tools/dvm-watchdog.sh tools/stop-watchdog.sh tools/fne-watchdog.sh tools/start-dvm-fne.sh DESTINATION ${CMAKE_INSTALL_PREFIX}/bin)
install(CODE "execute_process(COMMAND bash \"-c\" \"sed -i 's/filePath: ./filePath: \\\\/var\\\\/log\\\\//' /usr/local/etc/config.example.yml\")")
install(CODE "execute_process(COMMAND bash \"-c\" \"sed -i 's/activityFilePath: ./activityFilePath: \\\\/var\\\\/log\\\\//' /usr/local/etc/config.example.yml\")")
@ -289,13 +290,15 @@ if (NOT TARGET strip)
COMMAND arm-linux-gnueabihf-strip -s sysview
COMMAND arm-linux-gnueabihf-strip -s tged
COMMAND arm-linux-gnueabihf-strip -s peered
COMMAND arm-linux-gnueabihf-strip -s dvmbridge)
COMMAND arm-linux-gnueabihf-strip -s dvmbridge
COMMAND arm-linux-gnueabihf-strip -s dvmpatch)
else()
add_custom_target(strip
COMMAND arm-linux-gnueabihf-strip -s dvmhost
COMMAND arm-linux-gnueabihf-strip -s dvmfne
COMMAND arm-linux-gnueabihf-strip -s dvmcmd
COMMAND arm-linux-gnueabihf-strip -s dvmbridge)
COMMAND arm-linux-gnueabihf-strip -s dvmbridge
COMMAND arm-linux-gnueabihf-strip -s dvmpatch)
endif (ENABLE_TUI_SUPPORT AND (NOT DISABLE_TUI_APPS))
elseif (CROSS_COMPILE_AARCH64)
if (ENABLE_TUI_SUPPORT AND (NOT DISABLE_TUI_APPS))
@ -307,13 +310,15 @@ if (NOT TARGET strip)
COMMAND aarch64-linux-gnu-strip -s sysview
COMMAND aarch64-linux-gnu-strip -s tged
COMMAND aarch64-linux-gnu-strip -s peered
COMMAND aarch64-linux-gnu-strip -s dvmbridge)
COMMAND aarch64-linux-gnu-strip -s dvmbridge
COMMAND aarch64-linux-gnu-strip -s dvmpatch)
else()
add_custom_target(strip
COMMAND aarch64-linux-gnu-strip -s dvmhost
COMMAND aarch64-linux-gnu-strip -s dvmfne
COMMAND aarch64-linux-gnu-strip -s dvmcmd
COMMAND aarch64-linux-gnu-strip -s dvmbridge)
COMMAND aarch64-linux-gnu-strip -s dvmbridge
COMMAND aarch64-linux-gnu-strip -s dvmpatch)
endif (ENABLE_TUI_SUPPORT AND (NOT DISABLE_TUI_APPS))
elseif (CROSS_COMPILE_RPI_ARM)
if (NOT WITH_RPI_ARM_TOOLS)
@ -321,13 +326,15 @@ if (NOT TARGET strip)
COMMAND ${CMAKE_CURRENT_BINARY_DIR}/_deps/rpitools-src/arm-bcm2708/arm-linux-gnueabihf/bin/arm-linux-gnueabihf-strip -s dvmhost
COMMAND ${CMAKE_CURRENT_BINARY_DIR}/_deps/rpitools-src/arm-bcm2708/arm-linux-gnueabihf/bin/arm-linux-gnueabihf-strip -s dvmfne
COMMAND ${CMAKE_CURRENT_BINARY_DIR}/_deps/rpitools-src/arm-bcm2708/arm-linux-gnueabihf/bin/arm-linux-gnueabihf-strip -s dvmcmd
COMMAND ${CMAKE_CURRENT_BINARY_DIR}/_deps/rpitools-src/arm-bcm2708/arm-linux-gnueabihf/bin/arm-linux-gnueabihf-strip -s dvmbridge)
COMMAND ${CMAKE_CURRENT_BINARY_DIR}/_deps/rpitools-src/arm-bcm2708/arm-linux-gnueabihf/bin/arm-linux-gnueabihf-strip -s dvmbridge
COMMAND ${CMAKE_CURRENT_BINARY_DIR}/_deps/rpitools-src/arm-bcm2708/arm-linux-gnueabihf/bin/arm-linux-gnueabihf-strip -s dvmpatch)
else()
add_custom_target(strip
COMMAND ${RPI_ARM_TOOLS}/arm-bcm2708/arm-linux-gnueabihf/bin/arm-linux-gnueabihf-strip -s dvmhost
COMMAND ${RPI_ARM_TOOLS}/arm-bcm2708/arm-linux-gnueabihf/bin/arm-linux-gnueabihf-strip -s dvmfne
COMMAND ${RPI_ARM_TOOLS}/arm-bcm2708/arm-linux-gnueabihf/bin/arm-linux-gnueabihf-strip -s dvmcmd
COMMAND ${RPI_ARM_TOOLS}/arm-bcm2708/arm-linux-gnueabihf/bin/arm-linux-gnueabihf-strip -s dvmbridge)
COMMAND ${RPI_ARM_TOOLS}/arm-bcm2708/arm-linux-gnueabihf/bin/arm-linux-gnueabihf-strip -s dvmbridge
COMMAND ${RPI_ARM_TOOLS}/arm-bcm2708/arm-linux-gnueabihf/bin/arm-linux-gnueabihf-strip -s dvmpatch)
endif ()
else()
if (ENABLE_TUI_SUPPORT AND (NOT DISABLE_TUI_APPS))
@ -339,13 +346,15 @@ if (NOT TARGET strip)
COMMAND strip -s sysview
COMMAND strip -s tged
COMMAND strip -s peered
COMMAND strip -s dvmbridge)
COMMAND strip -s dvmbridge
COMMAND strip -s dvmpatch)
else()
add_custom_target(strip
COMMAND strip -s dvmhost
COMMAND strip -s dvmfne
COMMAND strip -s dvmcmd
COMMAND strip -s dvmbridge)
COMMAND strip -s dvmbridge
COMMAND strip -s dvmpatch)
endif (ENABLE_TUI_SUPPORT AND (NOT DISABLE_TUI_APPS))
endif (CROSS_COMPILE_ARM)
endif (NOT TARGET strip)
@ -375,6 +384,7 @@ if (NOT TARGET tarball)
COMMAND cp -v peered ${CMAKE_INSTALL_PREFIX_TARBALL}/dvm/bin
COMMAND cp -v dvmfne ${CMAKE_INSTALL_PREFIX_TARBALL}/dvm/bin
COMMAND cp -v dvmbridge ${CMAKE_INSTALL_PREFIX_TARBALL}/dvm/bin
COMMAND cp -v dvmpatch ${CMAKE_INSTALL_PREFIX_TARBALL}/dvm/bin
COMMAND cp ${CMAKE_SOURCE_DIR}/tools/*.sh ${CMAKE_INSTALL_PREFIX_TARBALL}/dvm
COMMAND chmod +x ${CMAKE_INSTALL_PREFIX_TARBALL}/dvm/*.sh
COMMAND cp -v ${CMAKE_SOURCE_DIR}/configs/*.yml ${CMAKE_INSTALL_PREFIX_TARBALL}/dvm
@ -408,6 +418,7 @@ if (NOT TARGET tarball)
COMMAND cp -v dvmcmd ${CMAKE_INSTALL_PREFIX_TARBALL}/dvm/bin
COMMAND cp -v dvmfne ${CMAKE_INSTALL_PREFIX_TARBALL}/dvm/bin
COMMAND cp -v dvmbridge ${CMAKE_INSTALL_PREFIX_TARBALL}/dvm/bin
COMMAND cp -v dvmpatch ${CMAKE_INSTALL_PREFIX_TARBALL}/dvm/bin
COMMAND cp ${CMAKE_SOURCE_DIR}/tools/*.sh ${CMAKE_INSTALL_PREFIX_TARBALL}/dvm
COMMAND chmod +x ${CMAKE_INSTALL_PREFIX_TARBALL}/dvm/*.sh
COMMAND cp -v ${CMAKE_SOURCE_DIR}/configs/*.yml ${CMAKE_INSTALL_PREFIX_TARBALL}/dvm
@ -454,6 +465,7 @@ if (NOT TARGET tarball_notools)
COMMAND cp -v peered ${CMAKE_INSTALL_PREFIX_TARBALL}/dvm/bin
COMMAND cp -v dvmfne ${CMAKE_INSTALL_PREFIX_TARBALL}/dvm/bin
COMMAND cp -v dvmbridge ${CMAKE_INSTALL_PREFIX_TARBALL}/dvm/bin
COMMAND cp -v dvmpatch ${CMAKE_INSTALL_PREFIX_TARBALL}/dvm/bin
COMMAND cp -v ${CMAKE_SOURCE_DIR}/configs/*.yml ${CMAKE_INSTALL_PREFIX_TARBALL}/dvm
COMMAND mkdir -p ${CMAKE_INSTALL_PREFIX_TARBALL}/dvm/schema
COMMAND cp -v ${CMAKE_SOURCE_DIR}/configs/schema/*.json ${CMAKE_INSTALL_PREFIX_TARBALL}/dvm/schema
@ -485,6 +497,7 @@ if (NOT TARGET tarball_notools)
COMMAND cp -v dvmcmd ${CMAKE_INSTALL_PREFIX_TARBALL}/dvm/bin
COMMAND cp -v dvmfne ${CMAKE_INSTALL_PREFIX_TARBALL}/dvm/bin
COMMAND cp -v dvmbridge ${CMAKE_INSTALL_PREFIX_TARBALL}/dvm/bin
COMMAND cp -v dvmpatch ${CMAKE_INSTALL_PREFIX_TARBALL}/dvm/bin
COMMAND cp -v ${CMAKE_SOURCE_DIR}/configs/*.yml ${CMAKE_INSTALL_PREFIX_TARBALL}/dvm
COMMAND mkdir -p ${CMAKE_INSTALL_PREFIX_TARBALL}/dvm/schema
COMMAND cp -v ${CMAKE_SOURCE_DIR}/configs/schema/*.json ${CMAKE_INSTALL_PREFIX_TARBALL}/dvm/schema
@ -530,6 +543,7 @@ add_custom_target(old_install
COMMAND install -m 755 peered ${CMAKE_LEGACY_INSTALL_PREFIX}/bin
COMMAND install -m 755 dvmfne ${CMAKE_LEGACY_INSTALL_PREFIX}/bin
COMMAND install -m 755 dvmbridge ${CMAKE_LEGACY_INSTALL_PREFIX}/bin
COMMAND install -m 755 dvmpatch ${CMAKE_LEGACY_INSTALL_PREFIX}/bin
COMMAND install -m 644 ${CMAKE_SOURCE_DIR}/configs/config.example.yml ${CMAKE_LEGACY_INSTALL_PREFIX}/config.example.yml
COMMAND install -m 644 ${CMAKE_SOURCE_DIR}/configs/fne-config.example.yml ${CMAKE_LEGACY_INSTALL_PREFIX}/fne-config.example.yml
COMMAND install -m 644 ${CMAKE_SOURCE_DIR}/configs/fne-sysview.example.yml ${CMAKE_LEGACY_INSTALL_PREFIX}/fne-sysview.example.yml
@ -539,6 +553,7 @@ add_custom_target(old_install
COMMAND install -m 644 ${CMAKE_SOURCE_DIR}/configs/rid_acl.example.dat ${CMAKE_LEGACY_INSTALL_PREFIX}/rid_acl.dat
COMMAND install -m 644 ${CMAKE_SOURCE_DIR}/configs/talkgroup_rules.example.yml ${CMAKE_LEGACY_INSTALL_PREFIX}/talkgroup_rules.example.yml
COMMAND install -m 644 ${CMAKE_SOURCE_DIR}/configs/bridge-config.example.yml ${CMAKE_LEGACY_INSTALL_PREFIX}/bridge-config.example.yml
COMMAND install -m 644 ${CMAKE_SOURCE_DIR}/configs/patch-config.example.yml ${CMAKE_LEGACY_INSTALL_PREFIX}/patch-config.example.yml
COMMAND install -m 644 ${CMAKE_SOURCE_DIR}/configs/schema/talkgroup_rules.yaml_schema.json ${CMAKE_LEGACY_INSTALL_PREFIX}/schema/talkgroup_rules.yaml_schema.json
COMMAND install -m 755 ${CMAKE_SOURCE_DIR}/tools/start-dvm.sh ${CMAKE_LEGACY_INSTALL_PREFIX}
COMMAND install -m 755 ${CMAKE_SOURCE_DIR}/tools/stop-dvm.sh ${CMAKE_LEGACY_INSTALL_PREFIX}
@ -581,6 +596,7 @@ add_custom_target(old_install-service
COMMAND chown dvmhost:dvmhost ${CMAKE_LEGACY_INSTALL_PREFIX}/rid_acl.example.dat
COMMAND chown dvmhost:dvmhost ${CMAKE_LEGACY_INSTALL_PREFIX}/talkgroup_rules.example.yml
COMMAND chown dvmhost:dvmhost ${CMAKE_LEGACY_INSTALL_PREFIX}/bridge-config.example.yml
COMMAND chown dvmhost:dvmhost ${CMAKE_LEGACY_INSTALL_PREFIX}/patch-config.example.yml
COMMAND chown dvmhost:dvmhost ${CMAKE_LEGACY_INSTALL_PREFIX}/log
COMMAND cp ../linux/dvmhost.service /lib/systemd/system/
COMMAND bash \"-c\" \"sed -i 's/\\\\/usr\\\\/local\\\\/bin/\\\\/opt\\\\/dvm\\\\/bin/' /lib/systemd/system/dvmhost.service\"

@ -15,6 +15,7 @@ This project suite generates a few executables:
- `dvmhost` host software that connects to the DVM modems (both air interface for repeater and hotspot or P25 DFSI for commerical P25 hardware) and is the primary data processing application for digital modes. [See configuration](#dvmhost-configuration) to configure and calibrate.
- `dvmfne` a network "core", this provides a central server for `dvmhost` instances to connect to and be networked with, allowing relay of traffic and other data between `dvmhost` instances and other `dvmfne` instances. [See configuration](#dvmfne-configuration) to configure.
- `dvmbridge` a analog/PCM audio bridge, this provides the capability for analog or PCM audio resources to be connected to a `dvmfne` instance, allowing realtime vocoding of traffic. [See configuration](#dvmbridge-configuration) to configure.
- `dvmpatch` a talkgroup patching utility, this provides the capability to manually patch talkgroups of the same digital mode together. [See configuration](#dvmpatch-configuration) to configure.
- `dvmcmd` a simple command-line utility to send remote control commands to a `dvmhost` or `dvmfne` instance with REST API configured.
### Supplementary Support Applications
@ -215,12 +216,18 @@ using the command line parameter `-wasapi` will force `dvmbridge` to utilize WAS
There is no other real configuration for a `dvmbridge` instance other then setting the appropriate parameters within the configuration files.
## dvmpatch Configuration
This source repository contains configuration example files within the configs folder, please review `patch-config.example.yml` for the `dvmpatch` for details on various configurable options.
There is no other real configuration for a `dvmpatch` instance other then setting the appropriate parameters within the configuration files.
## Command Line Parameters
### dvmhost Command Line Parameters
```
usage: ./dvmhost [-vhdf][--syslog][--setup][-c <configuration file>][--remote [-a <address>] [-p <port>]]
usage: ./dvmhost [-vhdf] [--syslog] [--setup] [--cal][--boot] [-c <configuration file>] [--remote [-a <address>] [-p <port>]]
-v show version information
-h show this screen
@ -229,13 +236,17 @@ usage: ./dvmhost [-vhdf][--syslog][--setup][-c <configuration file>][--remote [-
--syslog force logging to syslog
--setup setup and calibration mode
--setup TUI setup and calibration mode
--cal simple calibration mode
--boot connects to modem and reboots into bootloader mode
-c <file> specifies the configuration file to use
--remote remote modem mode
-a remote modem command address
-p remote modem command port
-P remote modem command port (local listening port)
-- stop handling options
```
@ -281,6 +292,20 @@ Audio Output Devices:
... <list of audio output devices> ...
```
### dvmpatch Command Line Parameters
```
usage: ./dvmpatch [-vhf][-c <configuration file>]
-v show version information
-h show this screen
-f foreground mode
-c <file> specifies the configuration file to use
-- stop handling options
```
### dvmcmd Command Line Parameters
```

@ -80,6 +80,17 @@ network:
# Flag indicating UDP audio should follow the USRP format.
udpUsrp: false
# Delay in-between UDP audio frames (in ms).
# (Some applications will send RTP/PCM audio too fast, requiring a delay in-between packets to
# be added for appropriate IMBE audio pacing. For most cases a 20ms delay will properly pace
# audio frames. If set to 0, no frame pacing or jitter buffer will be applied and audio will be
# encoded as fast as it is received.)
udpInterFrameDelay: 0
# Jitter Buffer Length (in ms).
# (This is only applied if utilizing inter frame delay, otherwise packet timing is assumed to be
# properly handled by the source.)
udpJitter: 200
# Flag indicating the UDP audio will be padded with silence during hang time before end of call.
udpHangSilence: true

@ -216,6 +216,8 @@ protocols:
interval: 300
# Amount of time to transmit non-dedicated CC broadcasts. (seconds)
duration: 1
# Flag indicating this CC will notify VCs of active TGIDs.
notifyActiveTG: false
# Flag to disable TSDU triple-block transmissions and instead transmit single-block TSDUs.
disableTSDUMBF: false
# Flag to enable optional TIME_DATE_ANNC TSBK during a CC broadcast.

@ -46,6 +46,9 @@ master:
# Flag indicating whether or not verbose debug logging is enabled.
debug: false
# Maximum number of concurrent packet processing workers.
workers: 16
# Maximum permitted connections (hard maximum is 250 peers).
connectionLimit: 100

@ -0,0 +1,81 @@
#
# Digital Voice Modem - TG Patch
#
# Flag indicating whether the host will run as a background or foreground task.
daemon: true
#
# Logging Configuration
#
# Logging Levels:
# 1 - Debug
# 2 - Message
# 3 - Informational
# 4 - Warning
# 5 - Error
# 6 - Fatal
#
log:
# Console display logging level (used when in foreground).
displayLevel: 1
# File logging level.
fileLevel: 1
# Flag indicating file logs should be sent to syslog instead of a file.
useSyslog: false
# Full path for the directory to store the log files.
filePath: .
# Full path for the directory to store the activity log files.
activityFilePath: .
# Log filename prefix.
fileRoot: dvmpatch
#
# Network Configuration
#
network:
# Network Peer ID
id: 9000123
# Hostname/IP address of FNE master to connect to.
address: 127.0.0.1
# Port number to connect to.
port: 62031
# FNE access password.
password: RPT1234
# Flag indicating whether or not host endpoint networking is encrypted.
encrypted: false
# AES-256 32-byte Preshared Key
# (This field *must* be 32 hex bytes in length or 64 characters
# 0 - 9, A - F.)
presharedKey: "000102030405060708090A0B0C0D0E0F000102030405060708090A0B0C0D0E0F"
# Flag indicating whether or not the host diagnostic log will be sent to the network.
allowDiagnosticTransfer: true
# Flag indicating whether or not verbose debug logging is enabled.
debug: false
# Source Talkgroup ID for transmitted/received audio frames.
sourceTGID: 1
# Source Slot for received/transmitted audio frames.
sourceSlot: 1
# Destination Talkgroup ID for transmitted/received audio frames.
destinationTGID: 1
# Destination Slot for received/transmitted audio frames.
destinationSlot: 1
# Flag indicating whether or not the patch is two-way.
twoWay: false
system:
# Textual Name
identity: PATCH
# Digital mode (1 - DMR, 2 - P25).
digiMode: 1
# Flag indicating whether or not trace logging is enabled.
trace: false
# Flag indicating whether or not debug logging is enabled.
debug: false

@ -60,7 +60,7 @@ set(CPACK_PACKAGE_VENDOR "DVMProject")
set(CPACK_DEBIAN_PACKAGE_DESCRIPTION "The DVM Host software provides the host computer implementation of a mixed-mode DMR, P25 and/or NXDN or dedicated-mode DMR, P25 or NXDN repeater system that talks to the actual modem hardware. The host software; is the portion of a complete Over-The-Air modem implementation that performs the data processing, decision making and FEC correction for a digital repeater.")
set(CPACK_DEBIAN_PACKAGE_MAINTAINER "DVMProject Authors")
set(CPACK_DEBIAN_PACKAGE_VERSION "R04Axx")
set(CPACK_DEBIAN_PACKAGE_VERSION "R04Hxx")
set(CPACK_DEBIAN_PACKAGE_RELEASE "0")
set(CPACK_DEBIAN_PACKAGE_HOMEPAGE "https://github.com/dvmproject")
@ -142,3 +142,15 @@ else ()
target_link_libraries(dvmbridge PRIVATE common vocoder ${OPENSSL_LIBRARIES} dl asio::asio Threads::Threads)
endif (COMPILE_WIN32)
target_include_directories(dvmbridge PRIVATE ${OPENSSL_INCLUDE_DIR} src src/bridge)
#
## dvmpatch
#
include(src/patch/CMakeLists.txt)
add_executable(dvmpatch ${common_INCLUDE} ${patch_SRC})
if (COMPILE_WIN32)
target_link_libraries(dvmpatch PRIVATE common ${OPENSSL_LIBRARIES} asio::asio Threads::Threads)
else ()
target_link_libraries(dvmpatch PRIVATE common ${OPENSSL_LIBRARIES} dl asio::asio Threads::Threads)
endif (COMPILE_WIN32)
target_include_directories(dvmpatch PRIVATE ${OPENSSL_INCLUDE_DIR} src src/patch)

@ -46,6 +46,7 @@ option(DEBUG_P25_DFSI "" off)
option(DEBUG_RINGBUFFER "" off)
option(DEBUG_HTTP_PAYLOAD "" off)
option(DEBUG_TRELLIS "" off)
option(DEBUG_COMPRESS "" off)
if (DEBUG_DMR_PDU_DATA)
message(CHECK_START "DMR PDU Data Debug")
@ -139,6 +140,10 @@ if (DEBUG_TRELLIS)
message(CHECK_START "Trellis Encoding Debug")
add_definitions(-DDEBUG_TRELLIS)
endif (DEBUG_TRELLIS)
if (DEBUG_COMPRESS)
message(CHECK_START "zlib Compression Debug")
add_definitions(-DDEBUG_COMPRESS)
endif (DEBUG_COMPRESS)
set(CMAKE_MODULE_PATH "${PROJECT_SOURCE_DIR}/cmake")
find_package(Threads REQUIRED)

@ -22,9 +22,10 @@
#include "common/p25/P25Utils.h"
#include "common/network/RTPHeader.h"
#include "common/network/udp/Socket.h"
#include "common/Log.h"
#include "common/Clock.h"
#include "common/StopWatch.h"
#include "common/Thread.h"
#include "common/Log.h"
#include "common/Utils.h"
#include "bridge/ActivityLog.h"
#include "HostBridge.h"
@ -298,7 +299,10 @@ HostBridge::HostBridge(const std::string& confFile) :
m_udpUseULaw(false),
m_udpRTPFrames(false),
m_udpUsrp(false),
m_udpInterFrameDelay(0U),
m_udpJitter(200U),
m_udpSilenceDuringHang(true),
m_lastUdpFrameTime(0U),
m_tekAlgoId(p25::defines::ALGO_UNENCRYPT),
m_tekKeyId(0U),
m_requestedTek(false),
@ -336,6 +340,7 @@ HostBridge::HostBridge(const std::string& confFile) :
m_maDevice(),
m_inputAudio(MBE_SAMPLES_LENGTH * NUMBER_OF_BUFFERS, "Input Audio Buffer"),
m_outputAudio(MBE_SAMPLES_LENGTH * NUMBER_OF_BUFFERS, "Output Audio Buffer"),
m_udpPackets(),
m_decoder(nullptr),
m_encoder(nullptr),
m_mdcDecoder(nullptr),
@ -646,6 +651,11 @@ int HostBridge::run()
}
}
if (m_udpAudio) {
if (!Thread::runAsThread(this, threadUDPAudioProcess))
return EXIT_FAILURE;
}
::LogInfoEx(LOG_HOST, "Bridge is up and running");
m_running = true;
@ -1068,6 +1078,8 @@ bool HostBridge::createNetwork()
m_udpReceiveAddress = networkConf["udpReceiveAddress"].as<std::string>();
m_udpUseULaw = networkConf["udpUseULaw"].as<bool>(false);
m_udpUsrp = networkConf["udpUsrp"].as<bool>(false);
m_udpInterFrameDelay = (uint8_t)networkConf["udpInterFrameDelay"].as<uint32_t>(0U);
m_udpJitter = (uint16_t)networkConf["udpJitter"].as<uint32_t>(200U);
m_udpSilenceDuringHang = networkConf["udpHangSilence"].as<bool>(true);
if (m_udpUsrp) {
@ -1220,6 +1232,8 @@ bool HostBridge::createNetwork()
LogInfo(" UDP Audio RTP Framed: %s", m_udpRTPFrames ? "yes" : "no");
}
LogInfo(" UDP Audio USRP: %s", m_udpUsrp ? "yes" : "no");
LogInfo(" UDP Inter Audio Frame Delay: %ums", m_udpInterFrameDelay);
LogInfo(" UDP Jitter: %ums", m_udpJitter);
LogInfo(" UDP Silence During Hangtime: %s", m_udpSilenceDuringHang ? "yes" : "no");
}
@ -1354,110 +1368,46 @@ void HostBridge::processUDPAudio()
// Utils::dump(1U, "PCM RECV BYTE BUFFER", pcm, pcmLength);
m_udpDstId = m_dstId;
if (m_udpMetadata) {
if (m_overrideSrcIdFromUDP) {
uint32_t udpSrcId = __GET_UINT32(buffer, pcmLength + 8U);
if (udpSrcId != 0U) {
// if the UDP source ID now doesn't match the current call ID, reset call states
if (m_resetCallForSourceIdChange && (udpSrcId != m_udpSrcId)) {
callEnd(m_udpSrcId, m_dstId);
m_udpDstId = m_dstId;
}
m_udpSrcId = udpSrcId;
} else {
if (m_udpSrcId == 0U) {
m_udpSrcId = m_srcId;
}
}
} else {
m_udpSrcId = m_srcId;
}
} else {
m_udpSrcId = m_srcId;
}
NetPacketRequest* req = new NetPacketRequest();
req->pcm = new uint8_t[pcmLength];
::memset(req->pcm, 0x00U, pcmLength);
::memcpy(req->pcm, pcm, pcmLength);
std::lock_guard<std::mutex> lock(m_audioMutex);
req->pcmLength = pcmLength;
int smpIdx = 0;
short samples[MBE_SAMPLES_LENGTH];
if (m_udpUseULaw) {
if (m_trace)
Utils::dump(1U, "HostBridge()::processUDPAudio() uLaw Audio", pcm, MBE_SAMPLES_LENGTH * 2U);
req->srcId = m_srcId;
req->dstId = m_dstId;
for (uint32_t pcmIdx = 0; pcmIdx < MBE_SAMPLES_LENGTH; pcmIdx++) {
samples[smpIdx] = decodeMuLaw(pcm[pcmIdx]);
smpIdx++;
}
uint64_t now = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::system_clock::now().time_since_epoch()).count();
if (m_udpInterFrameDelay > 0U) {
uint64_t pktTime = 0U;
int pcmIdx = 0;
for (uint32_t smpIdx = 0; smpIdx < MBE_SAMPLES_LENGTH; smpIdx++) {
pcm[pcmIdx + 0] = (uint8_t)(samples[smpIdx] & 0xFF);
pcm[pcmIdx + 1] = (uint8_t)((samples[smpIdx] >> 8) & 0xFF);
pcmIdx += 2;
// if this is our first message, timestamp is just now + the jitter buffer offset in ms
if (m_lastUdpFrameTime == 0U) {
pktTime = now + m_udpJitter;
}
else {
// if the last message occurred longer than our jitter buffer delay, we restart the sequence and calculate the same as above
if ((int64_t)(now - m_lastUdpFrameTime) > m_udpJitter) {
pktTime = now + m_udpJitter;
}
// otherwise, we time out messages as required by the message type
else {
for (uint32_t pcmIdx = 0; pcmIdx < pcmLength; pcmIdx += 2) {
samples[smpIdx] = (short)((pcm[pcmIdx + 1] << 8) + pcm[pcmIdx + 0]);
smpIdx++;
pktTime = m_lastUdpFrameTime + 20U;
}
}
m_inputAudio.addData(samples, MBE_SAMPLES_LENGTH);
m_trafficFromUDP = true;
// force start a call if one isn't already in progress
if (!m_audioDetect && !m_callInProgress) {
m_audioDetect = true;
if (m_txStreamId == 0U) {
m_txStreamId = 1U; // prevent further false starts -- this isn't the right way to handle this...
LogMessage(LOG_HOST, "%s, call start, srcId = %u, dstId = %u", UDP_CALL, m_udpSrcId, m_udpDstId);
if (m_grantDemand) {
switch (m_txMode) {
case TX_MODE_P25:
{
p25::lc::LC lc = p25::lc::LC();
lc.setLCO(p25::defines::LCO::GROUP);
lc.setDstId(m_udpDstId);
lc.setSrcId(m_udpSrcId);
p25::data::LowSpeedData lsd = p25::data::LowSpeedData();
uint8_t controlByte = 0x80U;
m_network->writeP25TDU(lc, lsd, controlByte);
}
break;
}
}
req->pktRxTime = pktTime;
m_lastUdpFrameTime = pktTime;
} else {
req->pktRxTime = now;
}
m_udpCallClock.stop();
m_udpDropTime.stop();
if (!m_udpDropTime.isRunning())
m_udpDropTime.start();
m_udpCallClock.start();
if (m_udpMetadata) {
req->srcId = __GET_UINT32(buffer, pcmLength + 8U);
}
// If audio detection is active and no call is in progress, encode and transmit the audio
if (m_audioDetect && !m_callInProgress) {
m_udpDropTime.start();
m_udpCallClock.start();
switch (m_txMode) {
case TX_MODE_DMR:
encodeDMRAudioFrame(pcm, m_udpSrcId);
break;
case TX_MODE_P25:
encodeP25AudioFrame(pcm, m_udpSrcId);
break;
}
}
m_udpPackets.push_back(req);
}
}
@ -2759,14 +2709,18 @@ uint8_t* HostBridge::generateRTPHeaders(uint8_t msgLen, uint16_t& rtpSeq)
uint8_t* buffer = new uint8_t[RTP_HEADER_LENGTH_BYTES + msgLen];
::memset(buffer, 0x00U, RTP_HEADER_LENGTH_BYTES + msgLen);
header.encode(buffer);
if (timestamp == INVALID_TS) {
if (m_debug)
LogDebugEx(LOG_NET, "HostBridge::generateRTPHeaders()", "RTP, initial TS = %u, rtpSeq = %u", header.getTimestamp(), rtpSeq);
timestamp = (uint32_t)system_clock::ntp::now();
header.setTimestamp(timestamp);
m_rtpTimestamp = header.getTimestamp();
}
header.encode(buffer);
return buffer;
}
@ -2845,6 +2799,7 @@ void HostBridge::callEnd(uint32_t srcId, uint32_t dstId)
m_udpSrcId = 0;
m_udpDstId = 0;
m_lastUdpFrameTime = 0U;
m_trafficFromUDP = false;
m_dmrSeqNo = 0U;
@ -2894,7 +2849,7 @@ void* HostBridge::threadAudioProcess(void* arg)
::pthread_detach(th->thread);
#endif // defined(_WIN32)
std::string threadName("bridge:audio-process");
std::string threadName("bridge:local-audio");
HostBridge* bridge = static_cast<HostBridge*>(th->obj);
if (bridge == nullptr) {
g_killed = true;
@ -3034,6 +2989,186 @@ void* HostBridge::threadAudioProcess(void* arg)
return nullptr;
}
/* Entry point to UDP audio processing thread. */
void* HostBridge::threadUDPAudioProcess(void* arg)
{
thread_t* th = (thread_t*)arg;
if (th != nullptr) {
#if defined(_WIN32)
::CloseHandle(th->thread);
#else
::pthread_detach(th->thread);
#endif // defined(_WIN32)
std::string threadName("bridge:udp-audio");
HostBridge* bridge = static_cast<HostBridge*>(th->obj);
if (bridge == nullptr) {
g_killed = true;
LogError(LOG_HOST, "[FAIL] %s", threadName.c_str());
}
if (g_killed) {
delete th;
return nullptr;
}
LogMessage(LOG_HOST, "[ OK ] %s", threadName.c_str());
#ifdef _GNU_SOURCE
::pthread_setname_np(th->thread, threadName.c_str());
#endif // _GNU_SOURCE
while (!g_killed) {
if (!bridge->m_running) {
Thread::sleep(1U);
continue;
}
if (bridge->m_udpPackets.empty())
Thread::sleep(1U);
else {
NetPacketRequest* req = bridge->m_udpPackets[0];
if (req != nullptr) {
if (bridge->m_udpInterFrameDelay > 0U) {
int64_t now = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::system_clock::now().time_since_epoch()).count();
if (req->pktRxTime > now) {
Thread::sleep(1U);
continue;
}
}
bridge->m_udpPackets.pop_front();
bridge->m_udpDstId = bridge->m_dstId;
if (bridge->m_udpMetadata) {
if (bridge->m_overrideSrcIdFromUDP) {
if (req->srcId != 0U) {
// if the UDP source ID now doesn't match the current call ID, reset call states
if (bridge->m_resetCallForSourceIdChange && (req->srcId != bridge->m_udpSrcId)) {
bridge->callEnd(bridge->m_udpSrcId, bridge->m_dstId);
bridge->m_udpDstId = bridge->m_dstId;
}
bridge->m_udpSrcId = req->srcId;
}
else {
if (bridge->m_udpSrcId == 0U) {
bridge->m_udpSrcId = req->srcId;
}
}
}
else {
bridge->m_udpSrcId = bridge->m_srcId;
}
}
else {
bridge->m_udpSrcId = bridge->m_srcId;
}
std::lock_guard<std::mutex> lock(m_audioMutex);
int smpIdx = 0;
short samples[MBE_SAMPLES_LENGTH];
if (bridge->m_udpUseULaw) {
if (bridge->m_trace)
Utils::dump(1U, "HostBridge()::threadUDPAudioProcess() uLaw Audio", req->pcm, MBE_SAMPLES_LENGTH * 2U);
for (uint32_t pcmIdx = 0; pcmIdx < MBE_SAMPLES_LENGTH; pcmIdx++) {
samples[smpIdx] = decodeMuLaw(req->pcm[pcmIdx]);
smpIdx++;
}
int pcmIdx = 0;
for (uint32_t smpIdx = 0; smpIdx < MBE_SAMPLES_LENGTH; smpIdx++) {
req->pcm[pcmIdx + 0] = (uint8_t)(samples[smpIdx] & 0xFF);
req->pcm[pcmIdx + 1] = (uint8_t)((samples[smpIdx] >> 8) & 0xFF);
pcmIdx += 2;
}
}
else {
for (uint32_t pcmIdx = 0; pcmIdx < req->pcmLength; pcmIdx += 2) {
samples[smpIdx] = (short)((req->pcm[pcmIdx + 1] << 8) + req->pcm[pcmIdx + 0]);
smpIdx++;
}
}
bridge->m_inputAudio.addData(samples, MBE_SAMPLES_LENGTH);
bridge->m_trafficFromUDP = true;
// force start a call if one isn't already in progress
if (!bridge->m_audioDetect && !bridge->m_callInProgress) {
bridge->m_audioDetect = true;
if (bridge->m_txStreamId == 0U) {
bridge->m_txStreamId = 1U; // prevent further false starts -- this isn't the right way to handle this...
LogMessage(LOG_HOST, "%s, call start, srcId = %u, dstId = %u", UDP_CALL, bridge->m_udpSrcId, bridge->m_udpDstId);
if (bridge->m_grantDemand) {
switch (bridge->m_txMode) {
case TX_MODE_P25:
{
p25::lc::LC lc = p25::lc::LC();
lc.setLCO(p25::defines::LCO::GROUP);
lc.setDstId(bridge->m_udpDstId);
lc.setSrcId(bridge->m_udpSrcId);
p25::data::LowSpeedData lsd = p25::data::LowSpeedData();
uint8_t controlByte = 0x80U;
bridge->m_network->writeP25TDU(lc, lsd, controlByte);
}
break;
}
}
}
bridge->m_udpCallClock.stop();
bridge->m_udpDropTime.stop();
if (!bridge->m_udpDropTime.isRunning())
bridge->m_udpDropTime.start();
bridge->m_udpCallClock.start();
}
// If audio detection is active and no call is in progress, encode and transmit the audio
if (bridge->m_audioDetect && !bridge->m_callInProgress) {
bridge->m_udpDropTime.start();
bridge->m_udpCallClock.start();
switch (bridge->m_txMode) {
case TX_MODE_DMR:
bridge->encodeDMRAudioFrame(req->pcm, bridge->m_udpSrcId);
break;
case TX_MODE_P25:
bridge->encodeP25AudioFrame(req->pcm, bridge->m_udpSrcId);
break;
}
}
delete[] req->pcm;
delete req;
if (bridge->m_udpInterFrameDelay > 0U) {
Thread::sleep(bridge->m_udpInterFrameDelay);
}
else {
Thread::sleep(1U);
}
} else {
bridge->m_udpPackets.pop_front();
Thread::sleep(1U);
}
}
}
LogMessage(LOG_HOST, "[STOP] %s", threadName.c_str());
delete th;
}
return nullptr;
}
/* Entry point to network processing thread. */
void* HostBridge::threadNetworkProcess(void* arg)

@ -17,6 +17,7 @@
#define __HOST_BRIDGE_H__
#include "Defines.h"
#include "common/concurrent/deque.h"
#include "common/dmr/data/EmbeddedData.h"
#include "common/dmr/lc/LC.h"
#include "common/dmr/lc/PrivacyLC.h"
@ -33,8 +34,6 @@
#include "network/PeerNetwork.h"
#include <string>
#include <unordered_map>
#include <vector>
#include <mutex>
#if defined(_WIN32)
@ -119,6 +118,24 @@ uint8_t encodeMuLaw(short pcm);
*/
short decodeMuLaw(uint8_t ulaw);
// ---------------------------------------------------------------------------
// Structure Declaration
// ---------------------------------------------------------------------------
/**
* @brief Represents the data required for a network packet handler thread.
* @ingroup bridge
*/
struct NetPacketRequest {
uint32_t srcId;
uint32_t dstId;
int pcmLength = 0U; //! Length of PCM data buffer
uint8_t* pcm = nullptr; //! Raw PCM buffer
uint64_t pktRxTime; //! Packet receive time
};
// ---------------------------------------------------------------------------
// Class Declaration
// ---------------------------------------------------------------------------
@ -166,7 +183,10 @@ private:
bool m_udpUseULaw;
bool m_udpRTPFrames;
bool m_udpUsrp;
uint8_t m_udpInterFrameDelay;
uint16_t m_udpJitter;
bool m_udpSilenceDuringHang;
uint64_t m_lastUdpFrameTime;
uint8_t m_tekAlgoId;
uint16_t m_tekKeyId;
@ -219,6 +239,7 @@ private:
RingBuffer<short> m_inputAudio;
RingBuffer<short> m_outputAudio;
concurrent::deque<NetPacketRequest*> m_udpPackets;
vocoder::MBEDecoder* m_decoder;
vocoder::MBEEncoder* m_encoder;
@ -503,6 +524,13 @@ private:
*/
static void* threadAudioProcess(void* arg);
/**
* @brief Entry point to UDP audio processing thread.
* @param arg Instance of the thread_t structure.
* @returns void* (Ignore)
*/
static void* threadUDPAudioProcess(void* arg);
/**
* @brief Entry point to network processing thread.
* @param arg Instance of the thread_t structure.

@ -50,6 +50,7 @@ file(GLOB common_SRC
"src/common/network/udp/*.cpp"
"src/common/network/viface/*.cpp"
"src/common/yaml/*.cpp"
"src/common/zlib/*.cpp"
"src/common/zlib/*.c"
"src/common/*.cpp"
)
@ -87,6 +88,7 @@ file(GLOB common_INCLUDE
"src/common/nxdn/lc/rcch/*.h"
# Core
"src/common/concurrent/*.h"
"src/common/edac/*.h"
"src/common/edac/rs/*.h"
"src/common/lookups/*.h"

@ -108,8 +108,8 @@ typedef unsigned long long ulong64_t;
#define __EXE_NAME__ ""
#define VERSION_MAJOR "04"
#define VERSION_MINOR "21"
#define VERSION_REV "G"
#define VERSION_MINOR "30"
#define VERSION_REV "H"
#define __NETVER__ "DVM_R" VERSION_MAJOR VERSION_REV VERSION_MINOR
#define __VER__ VERSION_MAJOR "." VERSION_MINOR VERSION_REV " (R" VERSION_MAJOR VERSION_REV VERSION_MINOR " " __GIT_VER__ ")"

@ -101,7 +101,7 @@ public:
/**
* @brief Executes the specified start routine to run as a thread.
* On POSIX, if the thread fails to spawn for any reason, the caller should clean up the passed thread_t
* argument.
* argument. This shouldn't be used for spawning threads used for things like packet processing.
* @param obj Instance of a object to pass to the threaded function.
* @param startRoutine Represents the function that executes on a thread.
* @param[out] thread Instance of the thread data.

@ -0,0 +1,236 @@
// SPDX-License-Identifier: GPL-2.0-only
/*
* Digital Voice Modem - Common Library
* GPLv2 Open Source. Use is subject to license terms.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* Copyright (C) 2025 Bryan Biedenkapp, N2PLL
*
*/
#include "ThreadPool.h"
#include "Log.h"
#include <cerrno>
#include <signal.h>
#if !defined(_WIN32)
#include <unistd.h>
#endif // !defined(_WIN32)
// ---------------------------------------------------------------------------
// Constants
// ---------------------------------------------------------------------------
#define MIN_WORKER_CNT 4U
// ---------------------------------------------------------------------------
// Public Class Members
// ---------------------------------------------------------------------------
/* Initializes a new instance of the ThreadPool class. */
ThreadPool::ThreadPool(uint16_t workerCnt, std::string name) :
m_maxWorkerCnt(workerCnt),
m_maxQueuedTasks(0U),
m_poolState(STOP),
m_workers(),
m_tasks(),
m_workerMutex(),
m_queueMutex(),
m_cond(),
m_name(name)
{
if (m_maxWorkerCnt < MIN_WORKER_CNT)
m_maxWorkerCnt = MIN_WORKER_CNT;
}
/* Finalizes a instance of the ThreadPool class. */
ThreadPool::~ThreadPool() = default;
/* Enqueue a thread pool task. */
bool ThreadPool::enqueue(ThreadPoolTask* task)
{
// scope is intentional
{
std::unique_lock<std::mutex> lock(m_workerMutex);
if (m_poolState == RUNNING && m_workers.size() < m_maxWorkerCnt) {
thread_t* thread = new thread_t();
thread->obj = this;
#if defined(_WIN32)
HANDLE hnd = ::CreateThread(NULL, 0, worker, thread, CREATE_SUSPENDED, NULL);
if (hnd == NULL) {
LogError(LOG_HOST, "Error returned from CreateThread, err: %lu", ::GetLastError());
return false;
}
thread->thread = hnd;
::ResumeThread(hnd);
#else
if (::pthread_create(&thread->thread, NULL, worker, thread) != 0) {
LogError(LOG_HOST, "Error returned from pthread_create, err: %d", errno);
return false;
}
#endif // defined(_WIN32)
m_workers.emplace_back(thread->thread);
}
}
// scope is intentional
{
std::unique_lock<std::mutex> lock(m_queueMutex);
if (m_poolState == STOP) {
LogError(LOG_HOST, "Cannot enqueue task on a stopped thread pool!");
return false;
}
if (m_maxQueuedTasks > 0U && m_tasks.size() >= m_maxQueuedTasks) {
LogError(LOG_HOST, "Cannot enqueue task, thread pool queue is full!");
return false;
}
m_tasks.emplace(std::unique_ptr<ThreadPoolTask>(task));
}
m_cond.notify_one();
return true;
}
/* Starts the thread pool. */
void ThreadPool::start()
{
// scope is intentional
{
std::unique_lock<std::mutex> lock(m_queueMutex);
m_poolState = RUNNING;
for (uint32_t i = m_workers.size(); i < m_maxWorkerCnt; i++) {
thread_t* thread = new thread_t();
thread->obj = this;
#if defined(_WIN32)
HANDLE hnd = ::CreateThread(NULL, 0, worker, thread, CREATE_SUSPENDED, NULL);
if (hnd == NULL) {
LogError(LOG_HOST, "Error returned from CreateThread, err: %lu", ::GetLastError());
continue;
}
thread->thread = hnd;
::ResumeThread(hnd);
#else
if (::pthread_create(&thread->thread, NULL, worker, thread) != 0) {
LogError(LOG_HOST, "Error returned from pthread_create, err: %d", errno);
continue;
}
#endif // defined(_WIN32)
m_workers.emplace_back(thread->thread);
}
}
m_cond.notify_all();
}
/* Stops the thread pool. */
void ThreadPool::stop()
{
// scope is intentional
{
std::unique_lock<std::mutex> lock(m_queueMutex);
m_poolState = STOP;
}
m_cond.notify_all();
}
/* Make calling thread wait for termination of any remaining thread pool tasks. */
void ThreadPool::wait()
{
m_cond.notify_all();
for (pthread_t worker : m_workers) {
#if defined(_WIN32)
::WaitForSingleObject(worker, INFINITE);
::CloseHandle(worker);
#else
::pthread_join(worker, NULL);
#endif // defined(_WIN32)
}
}
// ---------------------------------------------------------------------------
// Private Class Members
// ---------------------------------------------------------------------------
/* Internal worker thread that is used to execute task functions. */
#if defined(_WIN32)
DWORD ThreadPool::worker(LPVOID arg)
#else
void* ThreadPool::worker(void* arg)
#endif // defined(_WIN32)
{
thread_t* thread = (thread_t*)arg;
if (thread == nullptr) {
LogError(LOG_HOST, "Fatal error starting thread pool worker! No thread!");
#if defined(_WIN32)
return 0UL;
#else
return nullptr;
#endif // defined(_WIN32)
}
ThreadPool* threadPool = (ThreadPool*)thread->obj;
if (threadPool == nullptr) {
LogError(LOG_HOST, "Fatal error starting thread pool worker! No thread pool owner!");
delete thread;
#if defined(_WIN32)
return 0UL;
#else
return nullptr;
#endif // defined(_WIN32)
}
std::stringstream threadName;
threadName << threadPool->m_name << ":worker";
#ifdef _GNU_SOURCE
::pthread_setname_np(thread->thread, threadName.str().c_str());
#endif // _GNU_SOURCE
ThreadPoolTask* task = nullptr;
while (threadPool->m_poolState != STOP) {
// scope is intentional
{
std::unique_lock<std::mutex> lock(threadPool->m_queueMutex);
threadPool->m_cond.wait(lock, [=] { return threadPool->m_poolState == STOP || !threadPool->m_tasks.empty(); });
if (threadPool->m_poolState == STOP && threadPool->m_tasks.empty()) {
delete thread;
#if defined(_WIN32)
return 0UL;
#else
return nullptr;
#endif // defined(_WIN32)
}
task = (threadPool->m_tasks.front()).release();
threadPool->m_tasks.pop();
}
if (task == nullptr)
continue;
task->run();
delete task;
}
#if defined(_WIN32)
return 0UL;
#else
return nullptr;
#endif // defined(_WIN32)
}

@ -0,0 +1,158 @@
// SPDX-License-Identifier: GPL-2.0-only
/*
* Digital Voice Modem - Common Library
* GPLv2 Open Source. Use is subject to license terms.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* Copyright (C) 2025 Bryan Biedenkapp, N2PLL
*
*/
/**
* @file ThreadPool.h
* @ingroup threading
* @file ThreadPool.cpp
* @ingroup threading
*/
#if !defined(__THREAD_POOL_H__)
#define __THREAD_POOL_H__
#include "common/Defines.h"
#include "common/Thread.h"
#include <vector>
#include <queue>
#include <mutex>
#include <condition_variable>
#include <functional>
// ---------------------------------------------------------------------------
// Class Declaration
// ---------------------------------------------------------------------------
/**
* @brief Represents a task run by a thread pool worker thread.
* @ingroup threading
*/
class HOST_SW_API ThreadPoolTask {
public:
/**
* @brief Initializes a new instance of the ThreadPoolTask class.
* @tparam F
* @tparam Args
* @param f
* @param args
*/
template<class F, class... Args>
ThreadPoolTask(F&& f, Args&&... args)
{
task = std::bind(std::forward<F>(f), std::forward<Args>(args)...);
}
virtual ~ThreadPoolTask() = default;
/**
* @brief Starts the task function.
*/
void run() { task(); }
private:
std::function<void()> task;
};
/**
* @brief
* @tparam F
* @tparam Args
* @param f
* @param args
* @return ThreadPoolTask*
*/
template<class F, class... Args>
ThreadPoolTask* new_pooltask(F&& f, Args&&... args) { return new ThreadPoolTask(f, args...); }
// ---------------------------------------------------------------------------
// Class Declaration
// ---------------------------------------------------------------------------
/**
* @brief Creates and controls a thread pool.
* @ingroup threading
*/
class HOST_SW_API ThreadPool {
public:
/**
* @brief Initializes a new instance of the Thread class.
* @param workerCnt Number of worker threads to create.
* @param poolName Name of the thread pool.
*/
ThreadPool(uint16_t workerCnt = 4U, std::string poolName = "pool");
/**
* @brief Finalizes a instance of the Thread class.
*/
virtual ~ThreadPool();
/**
* @brief Enqueues a thread pool task.
* @param task Task to enqueue.
* @returns bool True, if task enqueued otherwise false.
*/
bool enqueue(ThreadPoolTask* task);
/**
* @brief Starts the thread pool.
*/
void start();
/**
* @brief Stops the thread pool.
*/
void stop();
/**
* @brief Make calling thread wait for termination of any remaining thread pool tasks.
*/
void wait();
public:
/**
* @brief Maximum number of worker threads.
*/
__PROPERTY(uint16_t, maxWorkerCnt, MaxWorkerCnt);
/**
* @brief Maximum number of queued tasks.
*/
__PROPERTY(uint16_t, maxQueuedTasks, MaxQueuedTasks);
private:
/**
* @brief Thread pool state.
*/
enum PoolState {
STOP = 0,
IDLE,
RUNNING
};
PoolState m_poolState;
std::vector<pthread_t> m_workers;
std::queue<std::unique_ptr<ThreadPoolTask>> m_tasks;
std::mutex m_workerMutex;
std::mutex m_queueMutex;
std::condition_variable m_cond;
std::string m_name;
/**
* @brief Internal worker thats used as the entry point for the worker threads.
* @param arg
* @returns void*
*/
#if defined(_WIN32)
static DWORD __stdcall worker(LPVOID arg);
#else
static void* worker(void* arg);
#endif // defined(_WIN32)
};
#endif // __THREAD_POOL_H__

@ -0,0 +1,101 @@
// SPDX-License-Identifier: GPL-2.0-only
/*
* Digital Voice Modem - Common Library
* GPLv2 Open Source. Use is subject to license terms.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* Copyright (C) 2025 Bryan Biedenkapp, N2PLL
*
*/
/**
* @file concurrent_lock.h
* @ingroup concurrency
*/
#if !defined(__CONCURRENCY_CONCURRENT_LOCK_H__)
#define __CONCURRENCY_CONCURRENT_LOCK_H__
#include "common/Thread.h"
#include <mutex>
#include <condition_variable>
namespace concurrent
{
// ---------------------------------------------------------------------------
// Class Declaration
// ---------------------------------------------------------------------------
/**
* @brief Base class for a concurrently locked container.
* @ingroup concurrency
*/
class concurrent_lock
{
public:
/**
* @brief Initializes a new instance of the concurrent_lock class.
*/
concurrent_lock() :
m_mutex(),
m_locked(false)
{
/* stub */
}
/**
* @brief Locks the object.
* @param readLock Flag indicating whether or not to use read locking.
*/
void lock(bool readLock = true) const { __lock(readLock); }
/**
* @brief Unlocks the object.
*/
void unlock() const { __unlock(); }
/**
* @brief Flag indicating whether or not the object is read locked.
* @return bool True if the object is read locked, false otherwise.
*/
bool isReadLocked() const { return m_locked; }
/**
* @brief Spins until the object is unlocked.
*/
void spinlock() const { __spinlock(); }
protected:
mutable std::mutex m_mutex; //! Mutex used for change locking.
mutable bool m_locked = false; //! Flag used for read locking (prevents find lookups), should be used when atomic operations (add/erase/etc) are being used.
/**
* @brief Lock the object.
* @param readLock Flag indicating whether or not to use read locking.
*/
inline void __lock(bool readLock = true) const
{
m_mutex.lock();
if (readLock)
m_locked = true;
}
/**
* @brief Unlock the object.
*/
inline void __unlock() const
{
m_mutex.unlock();
m_locked = false;
}
/**
* @brief Spins until the object is read unlocked.
*/
inline void __spinlock() const
{
if (m_locked) {
while (m_locked)
Thread::sleep(1U);
}
}
};
} // namespace concurrent
#endif // __CONCURRENCY_CONCURRENT_LOCK_H__

@ -0,0 +1,427 @@
// SPDX-License-Identifier: GPL-2.0-only
/*
* Digital Voice Modem - Common Library
* GPLv2 Open Source. Use is subject to license terms.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* Copyright (C) 2025 Bryan Biedenkapp, N2PLL
*
*/
/**
* @file deque.h
* @ingroup concurrency
*/
#if !defined(__CONCURRENCY_DEQUE_H__)
#define __CONCURRENCY_DEQUE_H__
#include "common/concurrent/concurrent_lock.h"
#include "common/Thread.h"
#include <deque>
#include <mutex>
namespace concurrent
{
// ---------------------------------------------------------------------------
// Class Declaration
// ---------------------------------------------------------------------------
/**
* @brief Thread-safe std::deque.
* @ingroup concurrency
*/
template <typename T>
class deque : public concurrent_lock
{
using __std = std::deque<T>;
public:
using iterator = typename __std::iterator;
using const_iterator = typename __std::const_iterator;
/**
* @brief Initializes a new instance of the deque class.
*/
deque() : concurrent_lock(),
m_deque()
{
/* stub */
}
/**
* @brief Initializes a new instance of the deque class.
* @param size Initial size of the deque.
*/
deque(size_t size) : concurrent_lock(),
m_deque(size)
{
/* stub */
}
/**
* @brief Finalizes a instance of the deque class.
*/
virtual ~deque()
{
m_deque.clear();
}
/**
* @brief Deque assignment operator.
* @param other A deque of identical element and allocator types.
*/
deque& operator=(const deque& other)
{
__lock();
m_deque = other.m_deque;
__unlock();
return *this;
}
/**
* @brief Deque assignment operator.
* @param other A deque of identical element and allocator types.
*/
deque& operator=(const std::deque<T>& other)
{
__lock();
m_deque = other;
__unlock();
return *this;
}
/**
* @brief Deque assignment operator.
* @param other A deque of identical element and allocator types.
*/
deque& operator=(deque& other)
{
__lock();
m_deque = other.m_deque;
__unlock();
return *this;
}
/**
* @brief Deque assignment operator.
* @param other A deque of identical element and allocator types.
*/
deque& operator=(std::deque<T>& other)
{
__lock();
m_deque = other;
__unlock();
return *this;
}
/**
* @brief Assigns a given value to a deque.
* @param size Number of elements to be assigned.
* @param value Value to be assigned.
*/
void assign(size_t size, const T& value)
{
__lock();
m_deque.assign(size, value);
__unlock();
}
/**
* @brief Returns a read/write iterator that points to the first
* element in the deque. Iteration is done in ordinary
* element order.
* @returns iterator
*/
iterator begin()
{
__spinlock();
return m_deque.begin();
}
/**
* @brief Returns a read-only (constant) iterator that points to the
* first element in the deque. Iteration is done in ordinary
* element order.
* @returns const_iterator
*/
const_iterator begin() const
{
__spinlock();
return m_deque.begin();
}
/**
* @brief Returns a read/write iterator that points one past the last
* element in the deque. Iteration is done in ordinary
* element order.
* @returns iterator
*/
iterator end()
{
__spinlock();
return m_deque.end();
}
/**
* @brief Returns a read-only (constant) iterator that points one past
* the last element in the deque. Iteration is done in ordinary
* element order.
* @returns const_iterator
*/
const_iterator end() const
{
__spinlock();
return m_deque.end();
}
/**
* @brief Returns a read-only (constant) iterator that points to the
* first element in the deque. Iteration is done in ordinary
* element order.
* @returns const_iterator
*/
const_iterator cbegin() const
{
__spinlock();
return m_deque.cbegin();
}
/**
* @brief Returns a read-only (constant) iterator that points one past
* the last element in the deque. Iteration is done in ordinary
* element order.
* @returns const_iterator
*/
const_iterator cend() const
{
__spinlock();
return m_deque.cend();
}
/**
* @brief Gets the total number of elements in the deque.
* @returns size_t Total number of elements in the deque.
*/
size_t size() const
{
__spinlock();
return m_deque.size();
}
/**
* @brief Resizes the deque to contain the specified number of elements.
* @param size Number of elements the deque should contain.
*/
void resize(size_t size)
{
__lock();
m_deque.resize(size);
__unlock();
}
/**
* @brief Returns the total number of elements that the deque can
* hold before needing to allocate more memory.
* @returns size_t
*/
size_t capacity() const
{
__spinlock();
return m_deque.capacity();
}
/**
* @brief Checks if the deque is empty.
* @returns bool True if the deque is empty, false otherwise.
*/
bool empty() const
{
__spinlock();
return m_deque.empty();
}
/**
* @brief Gets the element at the specified index.
* @param index Index of the element to get.
* @returns T& Element at the specified index.
*/
T& operator[](size_t index)
{
__spinlock();
return m_deque[index];
}
/**
* @brief Gets the element at the specified index.
* @param index Index of the element to get.
* @returns const T& Element at the specified index.
*/
const T& operator[](size_t index) const
{
__spinlock();
return m_deque[index];
}
/**
* @brief Gets the element at the specified index.
* @param index Index of the element to get.
* @returns T& Element at the specified index.
*/
T& at(size_t index)
{
__spinlock();
return m_deque.at(index);
}
/**
* @brief Gets the element at the specified index.
* @param index Index of the element to get.
* @returns const T& Element at the specified index.
*/
const T& at(size_t index) const
{
__spinlock();
return m_deque.at(index);
}
/**
* @brief Adds an element to the end of the deque.
* @param value Value to be added.
*/
void push_back(const T& value)
{
__lock();
m_deque.push_back(value);
__unlock();
}
/**
* @brief Adds an element to the end of the deque.
* @param value Value to be added.
*/
void push_front(const T& value)
{
__lock();
m_deque.push_front(value);
__unlock();
}
/**
* @brief Removes the last element of the deque.
*/
void pop_back()
{
__lock();
m_deque.pop_back();
__unlock();
}
/**
* @brief Removes the first element of the deque.
*/
void pop_front()
{
__lock();
m_deque.pop_front();
__unlock();
}
/**
* @brief Gets the first element of the deque.
* @returns T& First element of the deque.
*/
T& front()
{
__spinlock();
return m_deque.front();
}
/**
* @brief Gets the first element of the deque.
* @returns const T& First element of the deque.
*/
const T& front() const
{
__spinlock();
return m_deque.front();
}
/**
* @brief Gets the last element of the deque.
* @returns T& Last element of the deque.
*/
T& back()
{
__spinlock();
return m_deque.back();
}
/**
* @brief Gets the last element of the deque.
* @returns const T& Last element of the deque.
*/
const T& back() const
{
__spinlock();
return m_deque.back();
}
/**
* @brief Removes the element at the specified index.
* @param index Index of the element to remove.
*/
void erase(size_t index)
{
__lock();
m_deque.erase(m_deque.begin() + index);
__unlock();
}
/**
* @brief Removes the element at the specified iterator.
* @param position Iterator of the element to remove.
*/
void erase(const_iterator position)
{
__lock();
m_deque.erase(position);
__unlock();
}
/**
* @brief Removes the elements in the specified range.
* @param first Iterator of the first element to remove.
* @param last Iterator of the last element to remove.
*/
void erase(const_iterator first, const_iterator last)
{
__lock();
m_deque.erase(first, last);
__unlock();
}
/**
* @brief Swaps data with another deque.
* @param other A deque of the same element and allocator types.
*/
void swap(deque& other)
{
__lock();
m_deque.swap(other.m_deque);
__unlock();
}
/**
* @brief Clears the deque.
*/
void clear()
{
__lock();
m_deque.clear();
__unlock();
}
/**
* @brief Gets the underlying deque.
* @returns std::deque<T>& Underlying deque.
*/
std::deque<T>& get()
{
__spinlock();
return m_deque;
}
/**
* @brief Gets the underlying deque.
* @returns const std::deque<T>& Underlying deque.
*/
const std::deque<T>& get() const
{
__spinlock();
return m_deque;
}
private:
std::deque<T> m_deque;
};
} // namespace concurrent
#endif // __CONCURRENCY_DEQUE_H__

@ -0,0 +1,376 @@
// SPDX-License-Identifier: GPL-2.0-only
/*
* Digital Voice Modem - Common Library
* GPLv2 Open Source. Use is subject to license terms.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* Copyright (C) 2025 Bryan Biedenkapp, N2PLL
*
*/
/**
* @file map.h
* @ingroup concurrency
*/
#if !defined(__CONCURRENCY_MAP_H__)
#define __CONCURRENCY_MAP_H__
#include "common/concurrent/concurrent_lock.h"
#include "common/Thread.h"
#include <map>
#include <mutex>
namespace concurrent
{
// ---------------------------------------------------------------------------
// Class Declaration
// ---------------------------------------------------------------------------
/**
* @brief Thread-safe std::map.
* @ingroup concurrency
*/
template <typename Key, typename T>
class map : public concurrent_lock
{
using __std = std::map<Key, T>;
public:
using iterator = typename __std::iterator;
using const_iterator = typename __std::const_iterator;
/**
* @brief Initializes a new instance of the map class.
*/
map() : concurrent_lock(),
m_map()
{
/* stub */
}
/**
* @brief Initializes a new instance of the map class.
* @param size Initial size of the map.
*/
map(size_t size) : concurrent_lock(),
m_map(size)
{
/* stub */
}
/**
* @brief Finalizes a instance of the map class.
*/
virtual ~map()
{
m_map.clear();
}
/**
* @brief map assignment operator.
* @param other A map of identical element and allocator types.
*/
map& operator=(const map& other)
{
__lock();
m_map = other.m_map;
__unlock();
return *this;
}
/**
* @brief Map assignment operator.
* @param other A map of identical element and allocator types.
*/
map& operator=(const std::map<Key, T>& other)
{
__lock();
m_map = other;
__unlock();
return *this;
}
/**
* @brief Map assignment operator.
* @param other A map of identical element and allocator types.
*/
map& operator=(map& other)
{
__lock();
m_map = other.m_map;
__unlock();
return *this;
}
/**
* @brief Map assignment operator.
* @param other A map of identical element and allocator types.
*/
map& operator=(std::map<Key, T>& other)
{
__lock();
m_map = other;
__unlock();
return *this;
}
/**
* @brief Assigns a given value to a map.
* @param size Number of elements to be assigned.
* @param value Value to be assigned.
*/
void assign(size_t size, const T& value)
{
__lock();
m_map.assign(size, value);
__unlock();
}
/**
* @brief Returns a read/write iterator that points to the first
* element in the map. Iteration is done in ordinary
* element order.
* @returns iterator
*/
iterator begin()
{
__spinlock();
return m_map.begin();
}
/**
* @brief Returns a read-only (constant) iterator that points to the
* first element in the map. Iteration is done in ordinary
* element order.
* @returns const_iterator
*/
const_iterator begin() const
{
__spinlock();
return m_map.begin();
}
/**
* @brief Returns a read/write iterator that points one past the last
* element in the map. Iteration is done in ordinary
* element order.
* @returns iterator
*/
iterator end()
{
__spinlock();
return m_map.end();
}
/**
* @brief Returns a read-only (constant) iterator that points one past
* the last element in the map. Iteration is done in ordinary
* element order.
* @returns const_iterator
*/
const_iterator end() const
{
__spinlock();
return m_map.end();
}
/**
* @brief Returns a read-only (constant) iterator that points to the
* first element in the map. Iteration is done in ordinary
* element order.
* @returns const_iterator
*/
const_iterator cbegin() const
{
__spinlock();
return m_map.cbegin();
}
/**
* @brief Returns a read-only (constant) iterator that points one past
* the last element in the map. Iteration is done in ordinary
* element order.
* @returns const_iterator
*/
const_iterator cend() const
{
__spinlock();
return m_map.cend();
}
/**
* @brief Gets the element at the specified key.
* @param key Key of the element to get.
* @returns T& Element at the specified key.
*/
T& operator[](const Key& key)
{
__spinlock();
return m_map[key];
}
/**
* @brief Gets the element at the specified key.
* @param key Key of the element to get.
* @returns const T& Element at the specified key.
*/
const T& operator[](const Key& key) const
{
__spinlock();
return m_map[key];
}
/**
* @brief Gets the element at the specified key.
* @param key Key of the element to get.
* @returns T& Element at the specified key.
*/
T& at(const Key& key)
{
__spinlock();
return m_map.at(key);
}
/**
* @brief Gets the element at the specified key.
* @param key Key of the element to get.
* @returns const T& Element at the specified key.
*/
const T& at(const Key& key) const
{
__spinlock();
return m_map.at(key);
}
/**
* @brief Gets the total number of elements in the map.
* @returns size_t Total number of elements in the map.
*/
size_t size() const
{
__spinlock();
return m_map.size();
}
/**
* @brief Checks if the map is empty.
* @returns bool True if the map is empty, false otherwise.
*/
bool empty() const
{
__spinlock();
return m_map.empty();
}
/**
* @brief Checks if the map contains the specified key.
* @param key Key to check.
* @returns bool True if the map contains the specified key, false otherwise.
*/
bool contains(const Key& key) const
{
__spinlock();
return m_map.contains(key);
}
/**
* @brief Inserts a new element into the map.
* @param key Key of the element to insert.
* @param value Value of the element to insert.
*/
void insert(const Key& key, const T& value)
{
__lock();
m_map.insert({key, value});
__unlock();
}
/**
* @brief Removes the element at the specified key.
* @param key Key of the element to remove.
*/
void erase(const Key& key)
{
__lock();
m_map.erase(key);
__unlock();
}
/**
* @brief Removes the element at the specified iterator.
* @param position Iterator of the element to remove.
*/
void erase(const_iterator position)
{
__lock();
m_map.erase(position);
__unlock();
}
/**
* @brief Removes the elements in the specified range.
* @param first Iterator of the first element to remove.
* @param last Iterator of the last element to remove.
*/
void erase(const_iterator first, const_iterator last)
{
__lock();
m_map.erase(first, last);
__unlock();
}
/**
* @brief Clears the map.
*/
void clear()
{
__lock();
m_map.clear();
__unlock();
}
/**
* @brief Tries to locate an element in an map.
* @param key Key to be located.
* @return iterator Iterator pointing to sought-after element, or end() if not
* found.
*/
iterator find(const Key& key)
{
__spinlock();
return m_map.find(key);
}
/**
* @brief Tries to locate an element in an map.
* @param key Key to be located.
* @return const_iterator Iterator pointing to sought-after element, or end() if not
* found.
*/
const_iterator find(const Key& key) const
{
__spinlock();
return m_map.find(key);
}
/**
* @brief Finds the number of elements.
* @param key Key to count.
* @return size_t Number of elements with specified key.
*/
size_t count(const Key& key) const
{
__spinlock();
return m_map.count(key);
}
/**
* @brief Gets the underlying map.
* @returns std::map<Key, T>& Underlying map.
*/
std::map<Key, T>& get()
{
__spinlock();
return m_map;
}
/**
* @brief Gets the underlying map.
* @returns const std::map<Key, T>& Underlying map.
*/
const std::map<Key, T>& get() const
{
__spinlock();
return m_map;
}
private:
std::map<Key, T> m_map;
};
} // namespace concurrent
#endif // __CONCURRENCY_MAP_H__

@ -0,0 +1,376 @@
// SPDX-License-Identifier: GPL-2.0-only
/*
* Digital Voice Modem - Common Library
* GPLv2 Open Source. Use is subject to license terms.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* Copyright (C) 2025 Bryan Biedenkapp, N2PLL
*
*/
/**
* @file unordered_map.h
* @ingroup concurrency
*/
#if !defined(__CONCURRENCY_UNORDERED_MAP_H__)
#define __CONCURRENCY_UNORDERED_MAP_H__
#include "common/concurrent/concurrent_lock.h"
#include "common/Thread.h"
#include <unordered_map>
#include <mutex>
namespace concurrent
{
// ---------------------------------------------------------------------------
// Class Declaration
// ---------------------------------------------------------------------------
/**
* @brief Thread-safe std::unordered_map.
* @ingroup concurrency
*/
template <typename Key, typename T>
class unordered_map : public concurrent_lock
{
using __std = std::unordered_map<Key, T>;
public:
using iterator = typename __std::iterator;
using const_iterator = typename __std::const_iterator;
/**
* @brief Initializes a new instance of the unordered_map class.
*/
unordered_map() : concurrent_lock(),
m_map()
{
/* stub */
}
/**
* @brief Initializes a new instance of the unordered_map class.
* @param size Initial size of the unordered_map.
*/
unordered_map(size_t size) : concurrent_lock(),
m_map(size)
{
/* stub */
}
/**
* @brief Finalizes a instance of the unordered_map class.
*/
virtual ~unordered_map()
{
m_map.clear();
}
/**
* @brief Unordered map assignment operator.
* @param other A map of identical element and allocator types.
*/
unordered_map& operator=(const unordered_map& other)
{
__lock();
m_map = other.m_map;
__unlock();
return *this;
}
/**
* @brief Unordered map assignment operator.
* @param other A map of identical element and allocator types.
*/
unordered_map& operator=(const std::unordered_map<Key, T>& other)
{
__lock();
m_map = other;
__unlock();
return *this;
}
/**
* @brief Unordered map assignment operator.
* @param other A map of identical element and allocator types.
*/
unordered_map& operator=(unordered_map& other)
{
__lock();
m_map = other.m_map;
__unlock();
return *this;
}
/**
* @brief Unordered map assignment operator.
* @param other A map of identical element and allocator types.
*/
unordered_map& operator=(std::unordered_map<Key, T>& other)
{
__lock();
m_map = other;
__unlock();
return *this;
}
/**
* @brief Assigns a given value to a unordered_map.
* @param size Number of elements to be assigned.
* @param value Value to be assigned.
*/
void assign(size_t size, const T& value)
{
__lock();
m_map.assign(size, value);
__unlock();
}
/**
* @brief Returns a read/write iterator that points to the first
* element in the unordered_map. Iteration is done in ordinary
* element order.
* @returns iterator
*/
iterator begin()
{
__spinlock();
return m_map.begin();
}
/**
* @brief Returns a read-only (constant) iterator that points to the
* first element in the unordered_map. Iteration is done in ordinary
* element order.
* @returns const_iterator
*/
const_iterator begin() const
{
__spinlock();
return m_map.begin();
}
/**
* @brief Returns a read/write iterator that points one past the last
* element in the unordered_map. Iteration is done in ordinary
* element order.
* @returns iterator
*/
iterator end()
{
__spinlock();
return m_map.end();
}
/**
* @brief Returns a read-only (constant) iterator that points one past
* the last element in the unordered_map. Iteration is done in ordinary
* element order.
* @returns const_iterator
*/
const_iterator end() const
{
__spinlock();
return m_map.end();
}
/**
* @brief Returns a read-only (constant) iterator that points to the
* first element in the unordered_map. Iteration is done in ordinary
* element order.
* @returns const_iterator
*/
const_iterator cbegin() const
{
__spinlock();
return m_map.cbegin();
}
/**
* @brief Returns a read-only (constant) iterator that points one past
* the last element in the unordered_map. Iteration is done in ordinary
* element order.
* @returns const_iterator
*/
const_iterator cend() const
{
__spinlock();
return m_map.cend();
}
/**
* @brief Gets the element at the specified key.
* @param key Key of the element to get.
* @returns T& Element at the specified key.
*/
T& operator[](const Key& key)
{
__spinlock();
return m_map[key];
}
/**
* @brief Gets the element at the specified key.
* @param key Key of the element to get.
* @returns const T& Element at the specified key.
*/
const T& operator[](const Key& key) const
{
__spinlock();
return m_map[key];
}
/**
* @brief Gets the element at the specified key.
* @param key Key of the element to get.
* @returns T& Element at the specified key.
*/
T& at(const Key& key)
{
__spinlock();
return m_map.at(key);
}
/**
* @brief Gets the element at the specified key.
* @param key Key of the element to get.
* @returns const T& Element at the specified key.
*/
const T& at(const Key& key) const
{
__spinlock();
return m_map.at(key);
}
/**
* @brief Gets the total number of elements in the unordered_map.
* @returns size_t Total number of elements in the unordered_map.
*/
size_t size() const
{
__spinlock();
return m_map.size();
}
/**
* @brief Checks if the unordered_map is empty.
* @returns bool True if the unordered_map is empty, false otherwise.
*/
bool empty() const
{
__spinlock();
return m_map.empty();
}
/**
* @brief Checks if the unordered_map contains the specified key.
* @param key Key to check.
* @returns bool True if the unordered_map contains the specified key, false otherwise.
*/
bool contains(const Key& key) const
{
__spinlock();
return m_map.contains(key);
}
/**
* @brief Inserts a new element into the unordered_map.
* @param key Key of the element to insert.
* @param value Value of the element to insert.
*/
void insert(const Key& key, const T& value)
{
__lock();
m_map.insert({key, value});
__unlock();
}
/**
* @brief Removes the element at the specified key.
* @param key Key of the element to remove.
*/
void erase(const Key& key)
{
__lock();
m_map.erase(key);
__unlock();
}
/**
* @brief Removes the element at the specified iterator.
* @param position Iterator of the element to remove.
*/
void erase(const_iterator position)
{
__lock();
m_map.erase(position);
__unlock();
}
/**
* @brief Removes the elements in the specified range.
* @param first Iterator of the first element to remove.
* @param last Iterator of the last element to remove.
*/
void erase(const_iterator first, const_iterator last)
{
__lock();
m_map.erase(first, last);
__unlock();
}
/**
* @brief Clears the unordered_map.
*/
void clear()
{
__lock();
m_map.clear();
__unlock();
}
/**
* @brief Tries to locate an element in an unordered_map.
* @param key Key to be located.
* @return iterator Iterator pointing to sought-after element, or end() if not
* found.
*/
iterator find(const Key& key)
{
__spinlock();
return m_map.find(key);
}
/**
* @brief Tries to locate an element in an unordered_map.
* @param key Key to be located.
* @return const_iterator Iterator pointing to sought-after element, or end() if not
* found.
*/
const_iterator find(const Key& key) const
{
__spinlock();
return m_map.find(key);
}
/**
* @brief Finds the number of elements.
* @param key Key to count.
* @return size_t Number of elements with specified key.
*/
size_t count(const Key& key) const
{
__spinlock();
return m_map.count(key);
}
/**
* @brief Gets the underlying unordered_map.
* @returns std::unordered_map<Key, T>& Underlying unordered_map.
*/
std::unordered_map<Key, T>& get()
{
__spinlock();
return m_map;
}
/**
* @brief Gets the underlying unordered_map.
* @returns const std::unordered_map<Key, T>& Underlying unordered_map.
*/
const std::unordered_map<Key, T>& get() const
{
__spinlock();
return m_map;
}
private:
std::unordered_map<Key, T> m_map;
};
} // namespace concurrent
#endif // __CONCURRENCY_UNORDERED_MAP_H__

@ -0,0 +1,438 @@
// SPDX-License-Identifier: GPL-2.0-only
/*
* Digital Voice Modem - Common Library
* GPLv2 Open Source. Use is subject to license terms.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* Copyright (C) 2025 Bryan Biedenkapp, N2PLL
*
*/
/**
* @defgroup concurrency Concurrent STL.
* @brief Defines and implements concurrency support using standard STL containers.
* @ingroup common
*
* @file vector.h
* @ingroup concurrency
*/
#if !defined(__CONCURRENCY_VECTOR_H__)
#define __CONCURRENCY_VECTOR_H__
#include "common/concurrent/concurrent_lock.h"
#include "common/Thread.h"
#include <vector>
#include <mutex>
namespace concurrent
{
// ---------------------------------------------------------------------------
// Class Declaration
// ---------------------------------------------------------------------------
/**
* @brief Thread-safe std::vector.
* @ingroup concurrency
*/
template <typename T>
class vector : public concurrent_lock
{
using __std = std::vector<T>;
public:
using iterator = typename __std::iterator;
using const_iterator = typename __std::const_iterator;
/**
* @brief Initializes a new instance of the vector class.
*/
vector() : concurrent_lock(),
m_vector()
{
/* stub */
}
/**
* @brief Initializes a new instance of the vector class.
* @param size Initial size of the vector.
*/
vector(size_t size) : concurrent_lock(),
m_vector(size)
{
/* stub */
}
/**
* @brief Finalizes a instance of the vector class.
*/
virtual ~vector()
{
m_vector.clear();
}
/**
* @brief Vector assignment operator.
* @param other A vector of identical element and allocator types.
*/
vector& operator=(const vector& other)
{
__lock();
m_vector = other.m_vector;
__unlock();
return *this;
}
/**
* @brief Vector assignment operator.
* @param other A vector of identical element and allocator types.
*/
vector& operator=(const std::vector<T>& other)
{
__lock();
m_vector = other;
__unlock();
return *this;
}
/**
* @brief Vector assignment operator.
* @param other A vector of identical element and allocator types.
*/
vector& operator=(vector& other)
{
__lock();
m_vector = other.m_vector;
__unlock();
return *this;
}
/**
* @brief Vector assignment operator.
* @param other A vector of identical element and allocator types.
*/
vector& operator=(std::vector<T>& other)
{
__lock();
m_vector = other;
__unlock();
return *this;
}
/**
* @brief Assigns a given value to a %vector.
* @param size Number of elements to be assigned.
* @param value Value to be assigned.
*/
void assign(size_t size, const T& value)
{
__lock();
m_vector.assign(size, value);
__unlock();
}
/**
* @brief Returns a read/write iterator that points to the first
* element in the vector. Iteration is done in ordinary
* element order.
* @returns iterator
*/
iterator begin()
{
__spinlock();
return m_vector.begin();
}
/**
* @brief Returns a read-only (constant) iterator that points to the
* first element in the vector. Iteration is done in ordinary
* element order.
* @returns const_iterator
*/
const_iterator begin() const
{
__spinlock();
return m_vector.begin();
}
/**
* @brief Returns a read/write iterator that points one past the last
* element in the vector. Iteration is done in ordinary
* element order.
* @returns iterator
*/
iterator end()
{
__spinlock();
return m_vector.end();
}
/**
* @brief Returns a read-only (constant) iterator that points one past
* the last element in the vector. Iteration is done in ordinary
* element order.
* @returns const_iterator
*/
const_iterator end() const
{
__spinlock();
return m_vector.end();
}
/**
* @brief Returns a read-only (constant) iterator that points to the
* first element in the vector. Iteration is done in ordinary
* element order.
* @returns const_iterator
*/
const_iterator cbegin() const
{
__spinlock();
return m_vector.cbegin();
}
/**
* @brief Returns a read-only (constant) iterator that points one past
* the last element in the vector. Iteration is done in ordinary
* element order.
* @returns const_iterator
*/
const_iterator cend() const
{
__spinlock();
return m_vector.cend();
}
/**
* @brief Gets the number of elements in the vector.
* @returns size_t Number of elements in the vector.
*/
size_t size() const
{
__spinlock();
return m_vector.size();
}
/**
* @brief Resizes the %vector to the specified number of elements.
* @param size Number of elements the %vector should contain.
*/
void resize(size_t size)
{
__lock();
m_vector.resize(size);
__unlock();
}
/**
* @brief Returns the total number of elements that the %vector can
* hold before needing to allocate more memory.
* @returns size_t
*/
size_t capacity() const
{
__spinlock();
return m_vector.capacity();
}
/**
* @brief Checks if the vector is empty.
* @returns bool True if the vector is empty, false otherwise.
*/
bool empty() const
{
__spinlock();
return m_vector.empty();
}
/**
* @brief Gets the element at the specified index.
* @param index Index of the element to get.
* @returns T& Element at the specified index.
*/
T& operator[](size_t index)
{
__spinlock();
return m_vector[index];
}
/**
* @brief Gets the element at the specified index.
* @param index Index of the element to get.
* @returns const T& Element at the specified index.
*/
const T& operator[](size_t index) const
{
__spinlock();
return m_vector[index];
}
/**
* @brief Gets the element at the specified index.
* @param index Index of the element to get.
* @returns T& Element at the specified index.
*/
T& at(size_t index)
{
__spinlock();
return m_vector.at(index);
}
/**
* @brief Gets the element at the specified index.
* @param index Index of the element to get.
* @returns const T& Element at the specified index.
*/
const T& at(size_t index) const
{
__spinlock();
return m_vector.at(index);
}
/**
* @brief Gets the first element of the vector.
* @returns T& First element of the vector.
*/
T& front()
{
__spinlock();
return m_vector.front();
}
/**
* @brief Gets the first element of the vector.
* @returns const T& First element of the vector.
*/
const T& front() const
{
__spinlock();
return m_vector.front();
}
/**
* @brief Gets the last element of the vector.
* @returns T& Last element of the vector.
*/
T& back()
{
__spinlock();
return m_vector.back();
}
/**
* @brief Gets the last element of the vector.
* @returns const T& Last element of the vector.
*/
const T& back() const
{
__spinlock();
return m_vector.back();
}
/**
* @brief Adds an element to the end of the vector.
* @param value Value to add.
*/
void push_back(const T& value)
{
__lock();
m_vector.push_back(value);
__unlock();
}
/**
* @brief Adds an element to the end of the vector.
* @param value Value to add.
*/
void push_back(T&& value)
{
__lock();
m_vector.push_back(std::move(value));
__unlock();
}
/**
* @brief Removes last element.
*/
void pop_back()
{
__lock();
m_vector.pop_back();
__unlock();
}
/**
* @brief Inserts given value into vector before specified iterator.
* @param position A const_iterator into the vector.
* @param value Data to be inserted.
* @return iterator An iterator that points to the inserted data.
*/
iterator insert(iterator position, const T& value)
{
__lock();
auto it = m_vector.insert(position, value);
__unlock();
return it;
}
/**
* @brief Removes the element at the specified index.
* @param index Index of the element to remove.
*/
void erase(size_t index)
{
__lock();
m_vector.erase(m_vector.begin() + index);
__unlock();
}
/**
* @brief Removes the element at the specified iterator.
* @param position Iterator of the element to remove.
*/
void erase(const_iterator position)
{
__lock();
m_vector.erase(position);
__unlock();
}
/**
* @brief Removes the elements in the specified range.
* @param first Iterator of the first element to remove.
* @param last Iterator of the last element to remove.
*/
void erase(const_iterator first, const_iterator last)
{
__lock();
m_vector.erase(first, last);
__unlock();
}
/**
* @brief Swaps data with another vector.
* @param other A vector of the same element and allocator types.
*/
void swap(vector& other)
{
__lock();
m_vector.swap(other.m_vector);
__unlock();
}
/**
* @brief Clears the vector.
*/
void clear()
{
__lock();
m_vector.clear();
__unlock();
}
/**
* @brief Gets the underlying vector.
* @returns std::vector<T>& Underlying vector.
*/
std::vector<T>& get()
{
__spinlock();
return m_vector;
}
/**
* @brief Gets the underlying vector.
* @returns const std::vector<T>& Underlying vector.
*/
const std::vector<T>& get() const
{
__spinlock();
return m_vector;
}
private:
std::vector<T> m_vector;
};
} // namespace concurrent
#endif // __CONCURRENCY_VECTOR_H__

@ -20,12 +20,6 @@ using namespace lookups;
const uint32_t UNIT_REG_TIMEOUT = 43200U; // 12 hours
// ---------------------------------------------------------------------------
// Static Class Members
// ---------------------------------------------------------------------------
std::mutex AffiliationLookup::m_mutex;
// ---------------------------------------------------------------------------
// Public Class Members
// ---------------------------------------------------------------------------
@ -104,11 +98,14 @@ bool AffiliationLookup::unitDereg(uint32_t srcId, bool automatic)
m_unitRegTimers[srcId].stop();
// remove dynamic unit registration table entry
m_unitRegTable.lock(false);
if (std::find(m_unitRegTable.begin(), m_unitRegTable.end(), srcId) != m_unitRegTable.end()) {
auto it = std::find(m_unitRegTable.begin(), m_unitRegTable.end(), srcId);
m_unitRegTable.unlock();
m_unitRegTable.erase(it);
ret = true;
}
m_unitRegTable.unlock();
if (ret) {
if (m_unitDereg != nullptr) {
@ -167,10 +164,13 @@ uint32_t AffiliationLookup::unitRegTimer(uint32_t srcId)
bool AffiliationLookup::isUnitReg(uint32_t srcId) const
{
// lookup dynamic unit registration table entry
m_unitRegTable.lock(false);
if (std::find(m_unitRegTable.begin(), m_unitRegTable.end(), srcId) != m_unitRegTable.end()) {
m_unitRegTable.unlock();
return true;
}
else {
m_unitRegTable.unlock();
return false;
}
}
@ -230,11 +230,15 @@ bool AffiliationLookup::groupUnaff(uint32_t srcId)
bool AffiliationLookup::hasGroupAff(uint32_t dstId) const
{
// lookup dynamic affiliation table entry
m_grpAffTable.lock(false);
for (auto entry : m_grpAffTable) {
if (entry.second == dstId) {
m_grpAffTable.unlock();
return true;
}
}
m_grpAffTable.unlock();
return false;
}
@ -244,15 +248,15 @@ bool AffiliationLookup::hasGroupAff(uint32_t dstId) const
bool AffiliationLookup::isGroupAff(uint32_t srcId, uint32_t dstId) const
{
// lookup dynamic affiliation table entry
m_grpAffTable.lock(false);
if (m_grpAffTable.find(srcId) != m_grpAffTable.end()) {
uint32_t tblDstId = m_grpAffTable.at(srcId);
if (tblDstId == dstId) {
m_grpAffTable.unlock();
return true;
}
else {
return false;
}
}
m_grpAffTable.unlock();
return false;
}
@ -299,8 +303,6 @@ bool AffiliationLookup::grantCh(uint32_t dstId, uint32_t srcId, uint32_t grantTi
return false;
}
std::lock_guard<std::mutex> lock(m_mutex);
if (!m_chLookup->isRFChAvailable()) {
return false;
}
@ -336,8 +338,6 @@ void AffiliationLookup::touchGrant(uint32_t dstId)
return;
}
std::lock_guard<std::mutex> lock(m_mutex);
if (isGranted(dstId)) {
m_grantTimers[dstId].start();
}
@ -345,32 +345,29 @@ void AffiliationLookup::touchGrant(uint32_t dstId)
/* Helper to release the channel grant for the destination ID. */
bool AffiliationLookup::releaseGrant(uint32_t dstId, bool releaseAll, bool noLock)
bool AffiliationLookup::releaseGrant(uint32_t dstId, bool releaseAll)
{
if (dstId == 0U && !releaseAll) {
return false;
}
if (!noLock)
m_mutex.lock();
// are we trying to release all grants?
if (dstId == 0U && releaseAll) {
LogWarning(LOG_HOST, "%s, force releasing all channel grants", m_name.c_str());
m_grantChTable.lock();
std::vector<uint32_t> gntsToRel = std::vector<uint32_t>();
for (auto entry : m_grantChTable) {
uint32_t dstId = entry.first;
gntsToRel.push_back(dstId);
}
m_grantChTable.unlock();
// release grants
for (uint32_t dstId : gntsToRel) {
releaseGrant(dstId, false);
}
if (!noLock)
m_mutex.unlock();
return true;
}
@ -401,13 +398,9 @@ bool AffiliationLookup::releaseGrant(uint32_t dstId, bool releaseAll, bool noLoc
m_grantTimers[dstId].stop();
if (!noLock)
m_mutex.unlock();
return true;
}
if (!noLock)
m_mutex.unlock();
return false;
}
@ -420,11 +413,14 @@ bool AffiliationLookup::isChBusy(uint32_t chNo) const
}
// lookup dynamic channel grant table entry
m_grantChTable.lock(false);
for (auto entry : m_grantChTable) {
if (entry.second == chNo) {
m_grantChTable.unlock();
return true;
}
}
m_grantChTable.unlock();
return false;
}
@ -438,18 +434,20 @@ bool AffiliationLookup::isGranted(uint32_t dstId) const
}
// lookup dynamic channel grant table entry
try {
uint32_t chNo = m_grantChTable.at(dstId);
if (chNo != 0U) {
m_grantChTable.lock(false);
for (auto entry : m_grantChTable) {
uint32_t gntDstId = entry.first;
uint32_t chNo = entry.second;
if (gntDstId == dstId && chNo != 0U) {
m_grantChTable.unlock();
return true;
}
else {
return false;
}
} catch (...) {
m_grantChTable.unlock();
return false;
}
}
/* Helper to determine if the destination ID is network granted. */
@ -459,14 +457,21 @@ bool AffiliationLookup::isGroup(uint32_t dstId) const
return true;
}
// lookup dynamic channel grant table entry
try {
bool uu = m_uuGrantedTable.at(dstId);
// lookup U-U grant flag table entry
m_uuGrantedTable.lock(false);
for (auto entry : m_uuGrantedTable) {
uint32_t gntDstId = entry.first;
bool uu = entry.second;
if (gntDstId == dstId) {
m_uuGrantedTable.unlock();
return !uu;
} catch (...) {
return true;
}
}
m_uuGrantedTable.unlock();
return true;
}
/* Helper to determine if the destination ID is network granted. */
@ -476,14 +481,21 @@ bool AffiliationLookup::isNetGranted(uint32_t dstId) const
return false;
}
// lookup dynamic channel grant table entry
try {
bool net = m_netGrantedTable.at(dstId);
// lookup net granted flag table entry
m_netGrantedTable.lock(false);
for (auto entry : m_netGrantedTable) {
uint32_t gntDstId = entry.first;
bool net = entry.second;
if (gntDstId == dstId) {
m_netGrantedTable.unlock();
return net;
} catch (...) {
return false;
}
}
m_netGrantedTable.unlock();
return false;
}
/* Helper to get the channel granted for the given destination ID. */
@ -504,10 +516,15 @@ uint32_t AffiliationLookup::getGrantedCh(uint32_t dstId)
uint32_t AffiliationLookup::getGrantedDstByCh(uint32_t chNo)
{
// lookup dynamic channel grant table entry
m_grantChTable.lock(false);
for (auto entry : m_grantChTable) {
if (entry.second == chNo)
if (entry.second == chNo) {
m_grantChTable.unlock();
return entry.first;
}
}
m_grantChTable.unlock();
return 0U;
}
@ -520,12 +537,15 @@ uint32_t AffiliationLookup::getGrantedBySrcId(uint32_t srcId)
return 0U;
}
// lookup dynamic channel grant table entry
// lookup dynamic channel grant source table entry
m_grantSrcIdTable.lock(false);
for (auto entry : m_grantSrcIdTable) {
if (entry.second == srcId) {
m_grantSrcIdTable.unlock();
return entry.first;
}
}
m_grantSrcIdTable.unlock();
return 0U;
}
@ -549,9 +569,10 @@ uint32_t AffiliationLookup::getGrantedSrcId(uint32_t dstId)
void AffiliationLookup::clock(uint32_t ms)
{
std::lock_guard<std::mutex> lock(m_mutex);
m_grantChTable.spinlock();
// clock all the grant timers
m_grantChTable.lock(false);
std::vector<uint32_t> gntsToRel = std::vector<uint32_t>();
for (auto entry : m_grantChTable) {
uint32_t dstId = entry.first;
@ -561,14 +582,18 @@ void AffiliationLookup::clock(uint32_t ms)
gntsToRel.push_back(dstId);
}
}
m_grantChTable.unlock();
// release grants that have timed out
for (uint32_t dstId : gntsToRel) {
releaseGrant(dstId, false, true);
releaseGrant(dstId, false);
}
if (!m_disableUnitRegTimeout) {
m_unitRegTable.spinlock();
// clock all the unit registration timers
m_unitRegTable.lock(false);
std::vector<uint32_t> unitsToDereg = std::vector<uint32_t>();
for (uint32_t srcId : m_unitRegTable) {
m_unitRegTimers[srcId].clock(ms);
@ -576,6 +601,7 @@ void AffiliationLookup::clock(uint32_t ms)
unitsToDereg.push_back(srcId);
}
}
m_unitRegTable.unlock();
// release units registrations that have timed out
for (uint32_t srcId : unitsToDereg) {

@ -21,15 +21,14 @@
#define __AFFILIATION_LOOKUP_H__
#include "common/Defines.h"
#include "common/concurrent/vector.h"
#include "common/concurrent/unordered_map.h"
#include "common/lookups/ChannelLookup.h"
#include "common/Timer.h"
#include <cstdio>
#include <unordered_map>
#include <algorithm>
#include <vector>
#include <functional>
#include <mutex>
namespace lookups
{
@ -66,7 +65,7 @@ namespace lookups
* @brief Gets the unit registration table.
* @returns std::vector<uint32> Unit Registration Table.
*/
std::vector<uint32_t> unitRegTable() const { return m_unitRegTable; }
std::vector<uint32_t> unitRegTable() const { return m_unitRegTable.get(); }
/**
* @brief Helper to register a source ID.
* @param srcId Source Radio ID.
@ -118,7 +117,7 @@ namespace lookups
* @brief Gets the group affiliation table.
* @returns std::unordered_map<uint32_t, uint32_t> Group Affiliation Table.
*/
std::unordered_map<uint32_t, uint32_t> grpAffTable() const { return m_grpAffTable; }
std::unordered_map<uint32_t, uint32_t> grpAffTable() const { return m_grpAffTable.get(); }
/**
* @brief Helper to group affiliate a source ID.
* @param srcId Source Radio ID.
@ -163,7 +162,7 @@ namespace lookups
* @brief Gets the grant table.
* @returns std::unordered_map<uint32_t, uint32_t> Channel Grant Table.
*/
std::unordered_map<uint32_t, uint32_t> grantTable() const { return m_grantChTable; }
std::unordered_map<uint32_t, uint32_t> grantTable() const { return m_grantChTable.get(); }
/**
* @brief Helper to grant a channel.
* @param dstId Destination Address.
@ -183,10 +182,9 @@ namespace lookups
* @brief Helper to release the channel grant for the destination ID.
* @param dstId Destination Address.
* @param releaseAll Flag indicating all channel grants should be released.
* @param noLock Flag indicating no mutex lock operation should be performed while releasing.
* @returns bool True, if channel grant was released, otherwise false.
*/
virtual bool releaseGrant(uint32_t dstId, bool releaseAll, bool noLock = false);
virtual bool releaseGrant(uint32_t dstId, bool releaseAll);
/**
* @brief Helper to determine if the channel number is busy.
* @param chNo Channel Number.
@ -279,15 +277,15 @@ namespace lookups
protected:
uint8_t m_rfGrantChCnt;
std::vector<uint32_t> m_unitRegTable;
std::unordered_map<uint32_t, Timer> m_unitRegTimers;
std::unordered_map<uint32_t, uint32_t> m_grpAffTable;
concurrent::vector<uint32_t> m_unitRegTable;
concurrent::unordered_map<uint32_t, Timer> m_unitRegTimers;
concurrent::unordered_map<uint32_t, uint32_t> m_grpAffTable;
std::unordered_map<uint32_t, uint32_t> m_grantChTable;
std::unordered_map<uint32_t, uint32_t> m_grantSrcIdTable;
std::unordered_map<uint32_t, bool> m_uuGrantedTable;
std::unordered_map<uint32_t, bool> m_netGrantedTable;
std::unordered_map<uint32_t, Timer> m_grantTimers;
concurrent::unordered_map<uint32_t, uint32_t> m_grantChTable;
concurrent::unordered_map<uint32_t, uint32_t> m_grantSrcIdTable;
concurrent::unordered_map<uint32_t, bool> m_uuGrantedTable;
concurrent::unordered_map<uint32_t, bool> m_netGrantedTable;
concurrent::unordered_map<uint32_t, Timer> m_grantTimers;
// chNo dstId slot
std::function<void(uint32_t, uint32_t, uint8_t)> m_releaseGrant;
@ -300,8 +298,6 @@ namespace lookups
bool m_disableUnitRegTimeout;
bool m_verbose;
static std::mutex m_mutex;
};
} // namespace lookups

@ -12,12 +12,6 @@
using namespace lookups;
// ---------------------------------------------------------------------------
// Static Class Members
// ---------------------------------------------------------------------------
std::mutex ChannelLookup::m_mutex;
// ---------------------------------------------------------------------------
// Public Class Members
// ---------------------------------------------------------------------------
@ -39,8 +33,6 @@ ChannelLookup::~ChannelLookup() = default;
VoiceChData ChannelLookup::getRFChData(uint32_t chNo) const
{
std::lock_guard<std::mutex> lock(m_mutex);
if (chNo == 0U) {
return VoiceChData();
}
@ -59,8 +51,6 @@ VoiceChData ChannelLookup::getRFChData(uint32_t chNo) const
bool ChannelLookup::addRFCh(uint32_t chNo, bool force)
{
std::lock_guard<std::mutex> lock(m_mutex);
if (chNo == 0U) {
return false;
}
@ -83,8 +73,6 @@ bool ChannelLookup::addRFCh(uint32_t chNo, bool force)
bool ChannelLookup::removeRFCh(uint32_t chNo)
{
std::lock_guard<std::mutex> lock(m_mutex);
if (chNo == 0U) {
return false;
}

@ -24,14 +24,11 @@
#define __CHANNEL_LOOKUP_H__
#include "common/Defines.h"
#include "common/concurrent/vector.h"
#include "common/concurrent/unordered_map.h"
#include "common/Timer.h"
#include <cstdio>
#include <unordered_map>
#include <algorithm>
#include <vector>
#include <functional>
#include <mutex>
namespace lookups
{
@ -163,7 +160,7 @@ namespace lookups
* @brief Gets the RF channel data table.
* @returns std::unordered_map<uint32_t, VoiceChData> RF channel data table.
*/
std::unordered_map<uint32_t, VoiceChData> rfChDataTable() const { return m_rfChDataTable; }
std::unordered_map<uint32_t, VoiceChData> rfChDataTable() const { return m_rfChDataTable.get(); }
/**
* @brief Helper to set RF channel data.
* @param chData RF Channel data table.
@ -196,7 +193,7 @@ namespace lookups
* @brief Gets the RF channels table.
* @returns std::vector<uint32_t> RF channel table.
*/
std::vector<uint32_t> rfChTable() const { return m_rfChTable; }
std::vector<uint32_t> rfChTable() const { return m_rfChTable.get(); }
/**
* @brief Helper to add a RF channel.
* @param chNo Channel Number.
@ -218,10 +215,8 @@ namespace lookups
/** @} */
private:
std::vector<uint32_t> m_rfChTable;
std::unordered_map<uint32_t, VoiceChData> m_rfChDataTable;
static std::mutex m_mutex;
concurrent::vector<uint32_t> m_rfChTable;
concurrent::unordered_map<uint32_t, VoiceChData> m_rfChDataTable;
};
} // namespace lookups

@ -37,7 +37,7 @@ bool PeerListLookup::m_locked = false;
// Unlock the table.
#define __UNLOCK_TABLE() m_locked = false;
// Spinlock wait for table to be released.
// Spinlock wait for table to be read unlocked.
#define __SPINLOCK() \
if (m_locked) { \
while (m_locked) \

@ -263,8 +263,8 @@ namespace lookups
private:
Mode m_mode;
static std::mutex m_mutex; //! Mutex used for hard locking.
static bool m_locked; //! Flag used for soft locking (prevents find lookups), should be used when atomic operations (add/erase/etc) are being used.
static std::mutex m_mutex; //! Mutex used for change locking.
static bool m_locked; //! Flag used for read locking (prevents find lookups), should be used when atomic operations (add/erase/etc) are being used.
};
} // namespace lookups

@ -39,7 +39,7 @@ bool RadioIdLookup::m_locked = false;
// Unlock the table.
#define __UNLOCK_TABLE() m_locked = false;
// Spinlock wait for table to be released.
// Spinlock wait for table to be read unlocked.
#define __SPINLOCK() \
if (m_locked) { \
while (m_locked) \

@ -208,8 +208,8 @@ namespace lookups
bool save() override;
private:
static std::mutex m_mutex; //! Mutex used for hard locking.
static bool m_locked; //! Flag used for soft locking (prevents find lookups), should be used when atomic operations (add/erase/etc) are being used.
static std::mutex m_mutex; //! Mutex used for change locking.
static bool m_locked; //! Flag used for read locking (prevents find lookups), should be used when atomic operations (add/erase/etc) are being used.
};
} // namespace lookups

@ -37,7 +37,7 @@ bool TalkgroupRulesLookup::m_locked = false;
// Unlock the table.
#define __UNLOCK_TABLE() m_locked = false;
// Spinlock wait for table to be released.
// Spinlock wait for table to be read unlocked.
#define __SPINLOCK() \
if (m_locked) { \
while (m_locked) \
@ -232,6 +232,7 @@ TalkgroupRuleGroupVoice TalkgroupRulesLookup::find(uint32_t id, uint8_t slot)
__SPINLOCK();
std::lock_guard<std::mutex> lock(m_mutex);
auto it = std::find_if(m_groupVoice.begin(), m_groupVoice.end(),
[&](TalkgroupRuleGroupVoice x)
{
@ -258,6 +259,7 @@ TalkgroupRuleGroupVoice TalkgroupRulesLookup::findByRewrite(uint32_t peerId, uin
__SPINLOCK();
std::lock_guard<std::mutex> lock(m_mutex);
auto it = std::find_if(m_groupVoice.begin(), m_groupVoice.end(),
[&](TalkgroupRuleGroupVoice x)
{

@ -643,8 +643,8 @@ namespace lookups
bool m_acl;
bool m_stop;
static std::mutex m_mutex; //! Mutex used for hard locking.
static bool m_locked; //! Flag used for soft locking (prevents find lookups), should be used when atomic operations (add/erase/etc) are being used.
static std::mutex m_mutex; //! Mutex used for change locking.
static bool m_locked; //! Flag used for read locking (prevents find lookups), should be used when atomic operations (add/erase/etc) are being used.
/**
* @brief Loads the table from the passed lookup table file.

@ -14,6 +14,7 @@
#include "network/RTPHeader.h"
#include "network/RTPExtensionHeader.h"
#include "network/RTPFNEHeader.h"
#include "Clock.h"
#include "Log.h"
#include "Utils.h"
@ -23,12 +24,6 @@ using namespace network::frame;
#include <cassert>
#include <cstring>
// ---------------------------------------------------------------------------
// Static Class Members
// ---------------------------------------------------------------------------
std::mutex FrameQueue::m_fqTimestampLock;
// ---------------------------------------------------------------------------
// Public Class Members
// ---------------------------------------------------------------------------
@ -108,6 +103,11 @@ UInt8Array FrameQueue::read(int& messageLength, sockaddr_storage& address, uint3
return nullptr;
}
if (_fneHeader.getMessageLength() == 0U) {
LogError(LOG_NET, "FrameQueue::read(), invalid FNE packet length received from network");
return nullptr;
}
if (fneHeader != nullptr) {
*fneHeader = _fneHeader;
}
@ -136,8 +136,14 @@ UInt8Array FrameQueue::read(int& messageLength, sockaddr_storage& address, uint3
bool FrameQueue::write(const uint8_t* message, uint32_t length, uint32_t streamId, uint32_t peerId,
uint32_t ssrc, OpcodePair opcode, uint16_t rtpSeq, sockaddr_storage& addr, uint32_t addrLen)
{
assert(message != nullptr);
assert(length > 0U);
if (message == nullptr) {
LogError(LOG_NET, "FrameQueue::write(), message is null");
return false;
}
if (length == 0U) {
LogError(LOG_NET, "FrameQueue::write(), message length is zero");
return false;
}
uint32_t bufferLen = 0U;
uint8_t* buffer = generateMessage(message, length, streamId, peerId, ssrc, opcode, rtpSeq, &bufferLen);
@ -165,8 +171,14 @@ void FrameQueue::enqueueMessage(const uint8_t* message, uint32_t length, uint32_
void FrameQueue::enqueueMessage(const uint8_t* message, uint32_t length, uint32_t streamId, uint32_t peerId,
uint32_t ssrc, OpcodePair opcode, uint16_t rtpSeq, sockaddr_storage& addr, uint32_t addrLen)
{
assert(message != nullptr);
assert(length > 0U);
if (message == nullptr) {
LogError(LOG_NET, "FrameQueue::enqueueMessage(), message is null");
return;
}
if (length == 0U) {
LogError(LOG_NET, "FrameQueue::enqueueMessage(), message length is zero");
return;
}
uint32_t bufferLen = 0U;
uint8_t* buffer = generateMessage(message, length, streamId, peerId, ssrc, opcode, rtpSeq, &bufferLen);
@ -184,7 +196,6 @@ void FrameQueue::enqueueMessage(const uint8_t* message, uint32_t length, uint32_
void FrameQueue::clearTimestamps()
{
std::lock_guard<std::mutex> lock(m_fqTimestampLock);
m_streamTimestamps.clear();
}
@ -197,12 +208,18 @@ void FrameQueue::clearTimestamps()
uint8_t* FrameQueue::generateMessage(const uint8_t* message, uint32_t length, uint32_t streamId, uint32_t peerId,
uint32_t ssrc, OpcodePair opcode, uint16_t rtpSeq, uint32_t* outBufferLen)
{
assert(message != nullptr);
assert(length > 0U);
if (message == nullptr) {
LogError(LOG_NET, "FrameQueue::generateMessage(), message is null");
return nullptr;
}
if (length == 0U) {
LogError(LOG_NET, "FrameQueue::generateMessage(), message length is zero");
return nullptr;
}
uint32_t timestamp = INVALID_TS;
if (streamId != 0U) {
std::lock_guard<std::mutex> lock(m_fqTimestampLock);
m_streamTimestamps.lock(false);
auto entry = m_streamTimestamps.find(streamId);
if (entry != m_streamTimestamps.end()) {
timestamp = entry->second;
@ -214,6 +231,7 @@ uint8_t* FrameQueue::generateMessage(const uint8_t* message, uint32_t length, ui
LogDebugEx(LOG_NET, "FrameQueue::generateMessage()", "RTP streamId = %u, previous TS = %u, TS = %u, rtpSeq = %u", streamId, m_streamTimestamps[streamId], timestamp, rtpSeq);
m_streamTimestamps[streamId] = timestamp;
}
m_streamTimestamps.unlock();
}
uint32_t bufferLen = RTP_HEADER_LENGTH_BYTES + RTP_EXTENSION_HEADER_LENGTH_BYTES + RTP_FNE_HEADER_LENGTH_BYTES + length;
@ -228,22 +246,28 @@ uint8_t* FrameQueue::generateMessage(const uint8_t* message, uint32_t length, ui
header.setSequence(rtpSeq);
header.setSSRC(ssrc);
header.encode(buffer);
if (streamId != 0U && timestamp == INVALID_TS && rtpSeq != RTP_END_OF_CALL_SEQ) {
if (m_debug)
LogDebugEx(LOG_NET, "FrameQueue::generateMessage()", "RTP streamId = %u, initial TS = %u, rtpSeq = %u", streamId, header.getTimestamp(), rtpSeq);
m_streamTimestamps[streamId] = header.getTimestamp();
timestamp = (uint32_t)system_clock::ntp::now();
header.setTimestamp(timestamp);
m_streamTimestamps.insert(streamId, timestamp);
}
header.encode(buffer);
if (streamId != 0U && rtpSeq == RTP_END_OF_CALL_SEQ) {
std::lock_guard<std::mutex> lock(m_fqTimestampLock);
m_streamTimestamps.lock(false);
auto entry = m_streamTimestamps.find(streamId);
if (entry != m_streamTimestamps.end()) {
if (m_debug)
LogDebugEx(LOG_NET, "FrameQueue::generateMessage()", "RTP streamId = %u, rtpSeq = %u", streamId, rtpSeq);
m_streamTimestamps.unlock();
m_streamTimestamps.erase(streamId);
}
m_streamTimestamps.unlock();
}
RTPFNEHeader fneHeader = RTPFNEHeader();

@ -17,13 +17,11 @@
#define __FRAME_QUEUE_H__
#include "common/Defines.h"
#include "common/concurrent/unordered_map.h"
#include "common/network/RTPHeader.h"
#include "common/network/RTPFNEHeader.h"
#include "common/network/RawFrameQueue.h"
#include <mutex>
#include <unordered_map>
namespace network
{
// ---------------------------------------------------------------------------
@ -116,8 +114,8 @@ namespace network
private:
uint32_t m_peerId;
std::unordered_map<uint32_t, uint32_t> m_streamTimestamps;
static std::mutex m_fqTimestampLock;
concurrent::unordered_map<uint32_t, uint32_t> m_streamTimestamps;
/**
* @brief Generate RTP message for the frame queue.

@ -4,7 +4,7 @@
* GPLv2 Open Source. Use is subject to license terms.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* Copyright (C) 2024 Bryan Biedenkapp, N2PLL
* Copyright (C) 2024-2025 Bryan Biedenkapp, N2PLL
*
*/
#include "Defines.h"
@ -90,8 +90,14 @@ UInt8Array RawFrameQueue::read(int& messageLength, sockaddr_storage& address, ui
bool RawFrameQueue::write(const uint8_t* message, uint32_t length, sockaddr_storage& addr, uint32_t addrLen, ssize_t* lenWritten)
{
assert(message != nullptr);
assert(length > 0U);
if (message == nullptr) {
LogError(LOG_NET, "RawFrameQueue::write(), message is null");
return false;
}
if (length == 0U) {
LogError(LOG_NET, "RawFrameQueue::write(), message length is zero");
return false;
}
uint8_t* buffer = new uint8_t[length];
::memset(buffer, 0x00U, length);
@ -106,6 +112,7 @@ bool RawFrameQueue::write(const uint8_t* message, uint32_t length, sockaddr_stor
ret = false;
}
delete[] buffer;
return ret;
}
@ -113,8 +120,14 @@ bool RawFrameQueue::write(const uint8_t* message, uint32_t length, sockaddr_stor
void RawFrameQueue::enqueueMessage(const uint8_t* message, uint32_t length, sockaddr_storage& addr, uint32_t addrLen)
{
assert(message != nullptr);
assert(length > 0U);
if (message == nullptr) {
LogError(LOG_NET, "RawFrameQueue::enqueueMessage(), message is null");
return;
}
if (length == 0U) {
LogError(LOG_NET, "RawFrameQueue::enqueueMessage(), message length is zero");
return;
}
// if the queue is flushing -- don't attempt to enqueue any messages
if (m_queueFlushing) {

@ -739,6 +739,7 @@ namespace p25
OSP_SNDCP_CH_GNT = 0x14U, //! SNDCP CH GNT - SNDCP Data Channel Grant
OSP_SNDCP_CH_ANN = 0x16U, //! SNDCP CH ANN - SNDCP Data Channel Announcement
OSP_STS_Q = 0x1AU, //! STS Q - Status Query
OSP_QUE_RSP = 0x21U, //! QUE RSP - Queued Response
OSP_DENY_RSP = 0x27U, //! DENY RSP - Deny Response
OSP_SCCB_EXP = 0x29U, //! SCCB - Secondary Control Channel Broadcast - Explicit
OSP_GRP_AFF_Q = 0x2AU, //! GRP AFF Q - Group Affiliation Query
@ -748,7 +749,6 @@ namespace p25
OSP_SYNC_BCAST = 0x30U, //! SYNC BCAST - Synchronization Broadcast
OSP_AUTH_DMD = 0x31U, //! AUTH DMD - Authentication Demand
OSP_AUTH_FNE_RESP = 0x32U, //! AUTH FNE RESP - Authentication FNE Response
OSP_QUE_RSP = 0x33U, //! QUE RSP - Queued Response
OSP_IDEN_UP_VU = 0x34U, //! IDEN UP VU - Channel Identifier Update for VHF/UHF Bands
OSP_TIME_DATE_ANN = 0x35U, //! TIME DATE ANN - Time and Date Announcement
OSP_SYS_SRV_BCAST = 0x38U, //! SYS SRV BCAST - System Service Broadcast

@ -0,0 +1,169 @@
// SPDX-License-Identifier: GPL-2.0-only
/*
* Digital Voice Modem - Common Library
* GPLv2 Open Source. Use is subject to license terms.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* Copyright (C) 2025 Bryan Biedenkapp, N2PLL
*
*/
#include "Defines.h"
#include "zlib/Compression.h"
#include "zlib/zlib.h"
#include "Log.h"
#include "Utils.h"
#include <cassert>
#include <vector>
using namespace compress;
// ---------------------------------------------------------------------------
// Public Class Members
// ---------------------------------------------------------------------------
/* Compress the given input buffer. */
uint8_t* Compression::compress(const uint8_t* buffer, uint32_t len, uint32_t* compressedLen)
{
assert(buffer != nullptr);
assert(len > 0U);
if (compressedLen != nullptr) {
*compressedLen = 0U;
}
uint8_t* data = new uint8_t[len];
::memset(data, 0x00U, len);
::memcpy(data, buffer, len);
// compression structures
z_stream strm;
strm.zalloc = Z_NULL;
strm.zfree = Z_NULL;
strm.opaque = Z_NULL;
// initialize compression
if (deflateInit(&strm, Z_DEFAULT_COMPRESSION) != Z_OK) {
LogError(LOG_HOST, "Error initializing ZLIB");
delete[] data;
return nullptr;
}
// set input data
strm.avail_in = len;
strm.next_in = data;
// compress data
std::vector<uint8_t> compressedData;
int ret;
do {
// resize the output buffer as needed
compressedData.resize(compressedData.size() + 16384);
strm.avail_out = 16384;
strm.next_out = compressedData.data() + compressedData.size() - 16384;
ret = deflate(&strm, Z_FINISH);
if (ret == Z_STREAM_ERROR) {
LogError(LOG_HOST, "ZLIB error compressing data; stream error");
deflateEnd(&strm);
delete[] data;
return nullptr;
}
} while (ret != Z_STREAM_END);
// resize the output buffer to the actual compressed data size
compressedData.resize(strm.total_out);
// cleanup
deflateEnd(&strm);
if (compressedLen != nullptr) {
*compressedLen = strm.total_out;
}
uint8_t* compressed = compressedData.data();
#if DEBUG_COMPRESS
Utils::dump(2U, "Compression::compress(), Compressed Data", compressed, strm.total_out);
#endif
// reuse data buffer to return compressed data
delete[] data;
data = new uint8_t[strm.total_out];
::memset(data, 0x00U, strm.total_out);
::memcpy(data, compressed, strm.total_out);
return data;
}
/* Decompress the given input buffer. */
uint8_t* Compression::decompress(const uint8_t* buffer, uint32_t len, uint32_t* decompressedLen)
{
assert(buffer != nullptr);
assert(len > 0U);
if (decompressedLen != nullptr) {
*decompressedLen = 0U;
}
uint8_t* data = new uint8_t[len];
::memset(data, 0x00U, len);
::memcpy(data, buffer, len);
// compression structures
z_stream strm;
strm.zalloc = Z_NULL;
strm.zfree = Z_NULL;
strm.opaque = Z_NULL;
// set input data
strm.avail_in = len;
strm.next_in = data;
// initialize decompression
int ret = inflateInit(&strm);
if (ret != Z_OK) {
LogError(LOG_HOST, "Error initializing ZLIB");
delete[] data;
return nullptr;
}
// decompress data
std::vector<uint8_t> decompressedData;
uint8_t outbuffer[1024];
do {
strm.avail_out = sizeof(outbuffer);
strm.next_out = outbuffer;
ret = inflate(&strm, Z_NO_FLUSH);
if (ret == Z_STREAM_ERROR) {
LogError(LOG_HOST, "ZLIB error decompressing compressed data; stream error");
inflateEnd(&strm);
delete[] data;
return nullptr;
}
decompressedData.insert(decompressedData.end(), outbuffer, outbuffer + sizeof(outbuffer) - strm.avail_out);
} while (ret != Z_STREAM_END);
// cleanup
inflateEnd(&strm);
if (decompressedLen != nullptr) {
*decompressedLen = strm.total_out;
}
uint8_t* decompressed = decompressedData.data();
#if DEBUG_COMPRESS
Utils::dump(2U, "Compression::decompress(), Decompressed Data", decompressed, strm.total_out);
#endif
// reuse data buffer to return decompressed data
delete[] data;
data = new uint8_t[strm.total_out];
::memset(data, 0x00U, strm.total_out);
::memcpy(data, decompressed, strm.total_out);
return data;
}

@ -0,0 +1,57 @@
// SPDX-License-Identifier: GPL-2.0-only
/*
* Digital Voice Modem - Common Library
* GPLv2 Open Source. Use is subject to license terms.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* Copyright (C) 2025 Bryan Biedenkapp, N2PLL
*
*/
/**
* @defgroup compression Compression Routines
* @brief Defines and implements common compression routines.
* @ingroup common
*
* @file Compression.h
* @ingroup compression
* @file Compression.cpp
* @ingroup compression
*/
#if !defined(__COMPRESSION_H__)
#define __COMPRESSION_H__
#include "common/Defines.h"
namespace compress
{
// ---------------------------------------------------------------------------
// Class Declaration
// ---------------------------------------------------------------------------
/**
* @brief zlib Compression Helper.
* @ingroup compression
*/
class HOST_SW_API Compression {
public:
/**
* @brief Compress the given input buffer using zlib compression.
* @param[in] buffer Buffer containing data to zlib compress.
* @param[in] len Length of data to compress.
* @param[out] compressedLen Length of compressed data.
* @returns uint8_t* Buffer containing compressed data.
*/
static uint8_t* compress(const uint8_t* buffer, uint32_t len, uint32_t* compressedLen);
/**
* @brief Decompress the given input buffer using zlib compression.
* @param[in] buffer Buffer containing zlib compressed data.
* @param[in] len Length of compressed data.
* @param[out] decompressedLen Length of decompressed data.
* @returns uint8_t* Buffer containing decompressed data.
*/
static uint8_t* decompress(const uint8_t* buffer, uint32_t len, uint32_t* decompressedLen);
};
} // namespace compress
#endif // __COMPRESSION_H__

@ -451,6 +451,7 @@ bool CryptoContainer::load()
::memcpy(key, keyIv, keyLength);
::memcpy(iv, keyIv + keyLength, EVP_MAX_IV_LENGTH);
}
free(salt); // base64Decode allocates memory with malloc()
// get inner container encrypted data
// bryanb: annoying levels of XML encapsulation...
@ -476,6 +477,7 @@ bool CryptoContainer::load()
// decrypt inner container
AES aes = AES(AESKeyLength::AES_256);
uint8_t* innerContainer = aes.decryptCBC(innerContainerCrypted, innerContainerLen, key, iv);
free(innerContainerCrypted); // base64Decode allocates memory with malloc()
/*
** bryanb: this is probably slightly error prone...
@ -553,6 +555,8 @@ bool CryptoContainer::load()
}
}
}
delete[] innerContainer;
}
} catch(const std::exception& e) {
::LogError(LOG_HOST, "Error opening EKC: %s", e.what());

@ -39,6 +39,9 @@ using namespace lookups;
// Constants
// ---------------------------------------------------------------------------
#define MIN_WORKER_CNT 4U
#define MAX_WORKER_CNT 128U
#define THREAD_CYCLE_THRESHOLD 2U
#define IDLE_WARMUP_MS 5U
@ -502,6 +505,15 @@ bool HostFNE::createMasterNetwork()
std::string password = masterConf["password"].as<std::string>();
bool verbose = masterConf["verbose"].as<bool>(false);
bool debug = masterConf["debug"].as<bool>(false);
uint16_t workerCnt = (uint16_t)masterConf["workers"].as<uint32_t>(16U);
// clamp worker thread count properly
if (workerCnt > MAX_WORKER_CNT)
workerCnt = MAX_WORKER_CNT;
if (workerCnt > 64U)
LogWarning(LOG_HOST, "Packet worker thread count >64 threads, this is excessive and may result in poor performance.");
if (workerCnt < MIN_WORKER_CNT)
workerCnt = MIN_WORKER_CNT;
bool reportPeerPing = masterConf["reportPeerPing"].as<bool>(false);
@ -563,6 +575,8 @@ bool HostFNE::createMasterNetwork()
LogInfo(" Parrot Repeat Delay: %u ms", parrotDelay);
LogInfo(" Parrot Grant Demand: %s", parrotGrantDemand ? "yes" : "no");
LogInfo(" Worker Threads: %u", workerCnt);
LogInfo(" Encrypted: %s", encrypted ? "yes" : "no");
LogInfo(" Report Peer Pings: %s", reportPeerPing ? "yes" : "no");
@ -577,7 +591,7 @@ bool HostFNE::createMasterNetwork()
// initialize networking
m_network = new FNENetwork(this, address, port, id, password, debug, verbose, reportPeerPing, m_dmrEnabled, m_p25Enabled, m_nxdnEnabled,
parrotDelay, parrotGrantDemand, m_allowActivityTransfer, m_allowDiagnosticTransfer, m_pingTime, m_updateLookupTime);
parrotDelay, parrotGrantDemand, m_allowActivityTransfer, m_allowDiagnosticTransfer, m_pingTime, m_updateLookupTime, workerCnt);
m_network->setOptions(masterConf, true);
m_network->setLookups(m_ridLookup, m_tidLookup, m_peerListLookup, m_cryptoLookup);
@ -600,7 +614,7 @@ bool HostFNE::createMasterNetwork()
// setup alternate port for diagnostics/activity logging
if (m_useAlternatePortForDiagnostics) {
m_diagNetwork = new DiagNetwork(this, m_network, address, port + 1U);
m_diagNetwork = new DiagNetwork(this, m_network, address, port + 1U, workerCnt);
bool ret = m_diagNetwork->open();
if (!ret) {

@ -8,7 +8,7 @@
*
*/
#include "fne/Defines.h"
#include "common/zlib/zlib.h"
#include "common/zlib/Compression.h"
#include "common/Log.h"
#include "common/Utils.h"
#include "network/DiagNetwork.h"
@ -17,6 +17,7 @@
using namespace network;
using namespace network::callhandler;
using namespace compress;
#include <cassert>
@ -26,12 +27,13 @@ using namespace network::callhandler;
/* Initializes a new instance of the DiagNetwork class. */
DiagNetwork::DiagNetwork(HostFNE* host, FNENetwork* fneNetwork, const std::string& address, uint16_t port) :
DiagNetwork::DiagNetwork(HostFNE* host, FNENetwork* fneNetwork, const std::string& address, uint16_t port, uint16_t workerCnt) :
BaseNetwork(fneNetwork->m_peerId, true, fneNetwork->m_debug, true, true, fneNetwork->m_allowActivityTransfer, fneNetwork->m_allowDiagnosticTransfer),
m_fneNetwork(fneNetwork),
m_host(host),
m_address(address),
m_port(port)
m_port(port),
m_threadPool(workerCnt, "diag")
{
assert(fneNetwork != nullptr);
assert(host != nullptr);
@ -73,6 +75,7 @@ void DiagNetwork::processNetwork()
uint32_t peerId = fneHeader.getPeerId();
NetPacketRequest* req = new NetPacketRequest();
req->obj = m_fneNetwork;
req->peerId = peerId;
req->address = address;
@ -84,11 +87,14 @@ void DiagNetwork::processNetwork()
req->buffer = new uint8_t[length];
::memcpy(req->buffer, buffer.get(), length);
if (!Thread::runAsThread(m_fneNetwork, threadedNetworkRx, req)) {
if (!m_threadPool.enqueue(new_pooltask(taskNetworkRx, req))) {
LogError(LOG_NET, "Failed to task enqueue network packet request, peerId = %u, %s:%u", peerId,
udp::Socket::address(address).c_str(), udp::Socket::port(address));
if (req != nullptr) {
if (req->buffer != nullptr)
delete[] req->buffer;
delete req;
return;
}
}
}
}
@ -109,6 +115,8 @@ bool DiagNetwork::open()
if (m_debug)
LogMessage(LOG_NET, "Opening Network");
m_threadPool.start();
m_status = NET_STAT_MST_RUNNING;
m_socket = new udp::Socket(m_address, m_port);
@ -134,6 +142,9 @@ void DiagNetwork::close()
if (m_debug)
LogMessage(LOG_NET, "Closing Network");
m_threadPool.stop();
m_threadPool.wait();
m_socket->close();
m_status = NET_STAT_INVALID;
@ -145,16 +156,9 @@ void DiagNetwork::close()
/* Process a data frames from the network. */
void* DiagNetwork::threadedNetworkRx(void* arg)
void DiagNetwork::taskNetworkRx(NetPacketRequest* req)
{
NetPacketRequest* req = (NetPacketRequest*)arg;
if (req != nullptr) {
#if defined(_WIN32)
::CloseHandle(req->thread);
#else
::pthread_detach(req->thread);
#endif // defined(_WIN32)
FNENetwork* network = static_cast<FNENetwork*>(req->obj);
if (network == nullptr) {
if (req != nullptr) {
@ -163,20 +167,15 @@ void* DiagNetwork::threadedNetworkRx(void* arg)
delete req;
}
return nullptr;
return;
}
if (req == nullptr)
return nullptr;
return;
if (req->length > 0) {
uint32_t peerId = req->fneHeader.getPeerId();
std::stringstream peerName;
peerName << peerId << ":diag-rx-pckt";
#ifdef _GNU_SOURCE
::pthread_setname_np(req->thread, peerName.str().c_str());
#endif // _GNU_SOURCE
uint32_t streamId = req->fneHeader.getStreamId();
// process incoming message function opcodes
switch (req->fneHeader.getFunction()) {
@ -381,29 +380,157 @@ void* DiagNetwork::threadedNetworkRx(void* arg)
// validate peer (simple validation really)
if (connection->connected() && connection->address() == ip && connection->isExternalPeer() &&
connection->isPeerLink()) {
UInt8Array __rawPayload = std::make_unique<uint8_t[]>(req->length - 8U);
UInt8Array __rawPayload = std::make_unique<uint8_t[]>(req->length);
uint8_t* rawPayload = __rawPayload.get();
::memset(rawPayload, 0x00U, req->length - 8U);
::memcpy(rawPayload, req->buffer + 8U, req->length - 8U);
std::string payload(rawPayload, rawPayload + (req->length - 8U));
::memset(rawPayload, 0x00U, req->length);
::memcpy(rawPayload, req->buffer, req->length);
// Utils::dump(1U, "Raw Payload", rawPayload, req->length);
uint8_t curBlock = rawPayload[8U];
uint8_t blockCnt = rawPayload[9U];
if (network->m_peerLinkActPkt.find(peerId) == network->m_peerLinkActPkt.end()) {
network->m_peerLinkActPkt.insert(peerId, FNENetwork::PLActPeerPkt());
FNENetwork::PLActPeerPkt& pkt = network->m_peerLinkActPkt[peerId];
pkt.fragments = std::unordered_map<uint8_t, uint8_t*>();
pkt.streamId = streamId;
pkt.locked = false;
} else {
FNENetwork::PLActPeerPkt& pkt = network->m_peerLinkActPkt[peerId];
if (!pkt.locked && pkt.streamId != streamId) {
LogError(LOG_NET, "PEER %u Peer-Link, Active Peer List, stream ID mismatch, expected %u, got %u", peerId, pkt.streamId, streamId);
for (auto& frag : pkt.fragments) {
if (frag.second != nullptr)
delete[] frag.second;
}
pkt.fragments = std::unordered_map<uint8_t, uint8_t*>();
pkt.streamId = streamId;
}
if (pkt.streamId != streamId) {
// otherwise drop the packet
break;
}
}
FNENetwork::PLActPeerPkt& pkt = network->m_peerLinkActPkt[peerId];
if (pkt.locked) {
while (pkt.locked)
Thread::sleep(1U);
}
pkt.locked = true;
// if this is the first block store sizes and initialize temp buffer
if (curBlock == 0U) {
uint32_t size = __GET_UINT32(rawPayload, 0U);
uint32_t compressedSize = __GET_UINT32(rawPayload, 4U);
FNENetwork::PLActPeerPkt& pkt = network->m_peerLinkActPkt[peerId];
pkt.size = size;
pkt.compressedSize = compressedSize;
}
// scope is intentional
{
pkt.lastBlock = curBlock;
uint8_t* buffer = nullptr;
if (pkt.size < PEER_LINK_BLOCK_SIZE)
buffer = new uint8_t[PEER_LINK_BLOCK_SIZE + 1U];
else
buffer = new uint8_t[pkt.size + 1U];
::memcpy(buffer, rawPayload + 10U, PEER_LINK_BLOCK_SIZE);
// Utils::dump(1U, "Block Payload", buffer, PEER_LINK_BLOCK_SIZE);
pkt.fragments.insert({curBlock, buffer});
}
LogInfoEx(LOG_NET, "PEER %u Peer-Link, Active Peer List, block %u of %u, rxFragments = %u, streamId = %u", peerId, curBlock, blockCnt, pkt.fragments.size(), streamId);
// do we have all the blocks?
if (pkt.fragments.size() == blockCnt + 1U) {
uint8_t* buffer = nullptr;
if (pkt.size == 0U) {
LogError(LOG_NET, "PEER %u Peer-Link, Active Peer List, error missing size information", peerId);
goto pl_act_cleanup;
}
if (pkt.compressedSize == 0U) {
LogError(LOG_NET, "PEER %u Peer-Link, Active Peer List, error missing compressed size information", peerId);
goto pl_act_cleanup;
}
buffer = new uint8_t[pkt.size + 1U];
::memset(buffer, 0x00U, pkt.size + 1U);
if (pkt.fragments.size() == 1U) {
::memcpy(buffer, pkt.fragments[0U], pkt.size);
} else {
for (uint8_t i = 0U; i < pkt.fragments.size(); i++) {
uint32_t offs = i * PEER_LINK_BLOCK_SIZE;
::memcpy(buffer + offs, pkt.fragments[i], PEER_LINK_BLOCK_SIZE);
}
}
// Utils::dump(1U, "Peer-Link Active Peer List", buffer, pkt.size + 1U);
// scope is intentional
{
uint32_t decompressedLen = 0U;
uint8_t* decompressed = Compression::decompress(buffer, pkt.compressedSize, &decompressedLen);
// check that we got the appropriate data
if (decompressedLen == pkt.size && decompressed != nullptr) {
std::string payload(decompressed + 8U, decompressed + decompressedLen);
// parse JSON body
json::value v;
std::string err = json::parse(v, payload);
if (!err.empty()) {
LogError(LOG_NET, "PEER %u error parsing active peer list, %s", peerId, err.c_str());
break;
}
else {
// ensure parsed JSON is an array
if (!v.is<json::array>()) {
LogError(LOG_NET, "PEER %u error parsing active peer list, data was not valid", peerId);
break;
}
else {
json::array arr = v.get<json::array>();
LogInfoEx(LOG_NET, "PEER %u Peer-Link, Active Peer List, updating %u peer entries", peerId, arr.size());
network->m_peerLinkPeers[peerId] = arr;
}
}
}
else {
LogError(LOG_NET, "PEER %u Peer-Link, error decompressed list, was not of expected size! %u != %u", peerId, decompressedLen, pkt.size);
}
}
pl_act_cleanup:
pkt.size = 0U;
pkt.compressedSize = 0U;
if (buffer != nullptr)
delete[] buffer;
for (auto& frag : pkt.fragments) {
if (frag.second != nullptr)
delete[] frag.second;
}
pkt.fragments = std::unordered_map<uint8_t, uint8_t*>();
pkt.streamId = 0U;
network->m_peerLinkActPkt.erase(peerId);
} else {
pkt.locked = false;
}
}
else {
network->writePeerNAK(peerId, 0U, TAG_PEER_LINK, NET_CONN_NAK_FNE_UNAUTHORIZED);
}
@ -422,6 +549,4 @@ void* DiagNetwork::threadedNetworkRx(void* arg)
delete[] req->buffer;
delete req;
}
return nullptr;
}

@ -18,6 +18,7 @@
#include "fne/Defines.h"
#include "common/network/BaseNetwork.h"
#include "common/ThreadPool.h"
#include "fne/network/FNENetwork.h"
#include <string>
@ -46,8 +47,9 @@ namespace network
* @param network Instance of the FNENetwork class.
* @param address Network Hostname/IP address to listen on.
* @param port Network port number.
* @param workerCnt Number of worker threads.
*/
DiagNetwork(HostFNE* host, FNENetwork* fneNetwork, const std::string& address, uint16_t port);
DiagNetwork(HostFNE* host, FNENetwork* fneNetwork, const std::string& address, uint16_t port, uint16_t workerCnt);
/**
* @brief Finalizes a instance of the DiagNetwork class.
*/
@ -97,12 +99,13 @@ namespace network
NET_CONN_STATUS m_status;
ThreadPool m_threadPool;
/**
* @brief Entry point to process a given network packet.
* @param arg Instance of the NetPacketRequest structure.
* @returns void* (Ignore)
* @param req Instance of the NetPacketRequest structure.
*/
static void* threadedNetworkRx(void* arg);
static void taskNetworkRx(NetPacketRequest* req);
};
} // namespace network

@ -11,7 +11,7 @@
#include "common/edac/SHA256.h"
#include "common/network/json/json.h"
#include "common/p25/kmm/KMMFactory.h"
#include "common/zlib/zlib.h"
#include "common/zlib/Compression.h"
#include "common/Log.h"
#include "common/Utils.h"
#include "network/FNENetwork.h"
@ -23,6 +23,7 @@
using namespace network;
using namespace network::callhandler;
using namespace compress;
#include <cassert>
#include <cerrno>
@ -38,11 +39,12 @@ const uint32_t MAX_HARD_CONN_CAP = 250U;
const uint8_t MAX_PEER_LIST_BEFORE_FLUSH = 10U;
const uint32_t MAX_RID_LIST_CHUNK = 50U;
const uint64_t PACKET_LATE_TIME = 200U; // 200ms
// ---------------------------------------------------------------------------
// Static Class Members
// ---------------------------------------------------------------------------
std::mutex FNENetwork::m_peerMutex;
std::timed_mutex FNENetwork::m_keyQueueMutex;
// ---------------------------------------------------------------------------
@ -53,7 +55,7 @@ std::timed_mutex FNENetwork::m_keyQueueMutex;
FNENetwork::FNENetwork(HostFNE* host, const std::string& address, uint16_t port, uint32_t peerId, const std::string& password,
bool debug, bool verbose, bool reportPeerPing, bool dmr, bool p25, bool nxdn, uint32_t parrotDelay, bool parrotGrantDemand,
bool allowActivityTransfer, bool allowDiagnosticTransfer, uint32_t pingTime, uint32_t updateLookupTime) :
bool allowActivityTransfer, bool allowDiagnosticTransfer, uint32_t pingTime, uint32_t updateLookupTime, uint16_t workerCnt) :
BaseNetwork(peerId, true, debug, true, true, allowActivityTransfer, allowDiagnosticTransfer),
m_tagDMR(nullptr),
m_tagP25(nullptr),
@ -78,6 +80,7 @@ FNENetwork::FNENetwork(HostFNE* host, const std::string& address, uint16_t port,
m_peerAffiliations(),
m_ccPeerMap(),
m_peerLinkKeyQueue(),
m_peerLinkActPkt(),
m_maintainenceTimer(1000U, pingTime),
m_updateLookupTime(updateLookupTime * 60U),
m_softConnLimit(0U),
@ -100,6 +103,7 @@ FNENetwork::FNENetwork(HostFNE* host, const std::string& address, uint16_t port,
m_influxOrg("dvm"),
m_influxBucket("dvm"),
m_influxLogRawData(false),
m_threadPool(workerCnt, "fne"),
m_disablePacketData(false),
m_dumpPacketData(false),
m_verbosePacketData(false),
@ -253,6 +257,7 @@ void FNENetwork::processNetwork()
uint32_t peerId = fneHeader.getPeerId();
NetPacketRequest* req = new NetPacketRequest();
req->obj = this;
req->peerId = peerId;
req->address = address;
@ -260,15 +265,21 @@ void FNENetwork::processNetwork()
req->rtpHeader = rtpHeader;
req->fneHeader = fneHeader;
req->pktRxTime = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::system_clock::now().time_since_epoch()).count();
req->length = length;
req->buffer = new uint8_t[length];
::memcpy(req->buffer, buffer.get(), length);
if (!Thread::runAsThread(this, threadedNetworkRx, req)) {
// enqueue the task
if (!m_threadPool.enqueue(new_pooltask(taskNetworkRx, req))) {
LogError(LOG_NET, "Failed to task enqueue network packet request, peerId = %u, %s:%u", peerId,
udp::Socket::address(address).c_str(), udp::Socket::port(address));
if (req != nullptr) {
if (req->buffer != nullptr)
delete[] req->buffer;
delete req;
return;
}
}
}
}
@ -328,12 +339,10 @@ void FNENetwork::clock(uint32_t ms)
// remove any peers
for (uint32_t peerId : peersToRemove) {
FNEPeerConnection* connection = m_peers[peerId];
m_peers.erase(peerId);
erasePeer(peerId);
if (connection != nullptr) {
delete connection;
}
erasePeerAffiliations(peerId);
}
// roll the RTP timestamp if no call is in progress
@ -426,6 +435,14 @@ bool FNENetwork::open()
if (m_debug)
LogMessage(LOG_NET, "Opening Network");
// start thread pool
m_threadPool.start();
// start FluxQL thread pool
if (m_enableInfluxDB) {
influxdb::detail::TSCaller::start();
}
m_status = NET_STAT_MST_RUNNING;
m_maintainenceTimer.start();
@ -462,10 +479,20 @@ void FNENetwork::close()
}
}
m_socket->close();
m_maintainenceTimer.stop();
// stop thread pool
m_threadPool.stop();
m_threadPool.wait();
// stop FluxQL thread pool
if (m_enableInfluxDB) {
influxdb::detail::TSCaller::stop();
influxdb::detail::TSCaller::wait();
}
m_socket->close();
m_status = NET_STAT_INVALID;
}
@ -475,16 +502,9 @@ void FNENetwork::close()
/* Process a data frames from the network. */
void* FNENetwork::threadedNetworkRx(void* arg)
void FNENetwork::taskNetworkRx(NetPacketRequest* req)
{
NetPacketRequest* req = (NetPacketRequest*)arg;
if (req != nullptr) {
#if defined(_WIN32)
::CloseHandle(req->thread);
#else
::pthread_detach(req->thread);
#endif // defined(_WIN32)
uint64_t now = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::system_clock::now().time_since_epoch()).count();
FNENetwork* network = static_cast<FNENetwork*>(req->obj);
@ -495,21 +515,23 @@ void* FNENetwork::threadedNetworkRx(void* arg)
delete req;
}
return nullptr;
return;
}
if (req == nullptr)
return nullptr;
return;
if (req->length > 0) {
uint32_t peerId = req->fneHeader.getPeerId();
uint32_t streamId = req->fneHeader.getStreamId();
std::stringstream peerName;
peerName << peerId << ":rx-pckt";
#ifdef _GNU_SOURCE
::pthread_setname_np(req->thread, peerName.str().c_str());
#endif // _GNU_SOURCE
// determine if this packet is late (i.e. are we processing this packet more than 200ms after it was received?)
uint64_t dt = req->pktRxTime + PACKET_LATE_TIME;
if (dt < now) {
std::string peerIdentity = network->resolvePeerIdentity(peerId);
LogWarning(LOG_NET, "PEER %u (%s) packet processing latency >200ms, dt = %u, now = %u", peerId, peerIdentity.c_str(),
dt, now);
}
// update current peer packet sequence and stream ID
if (peerId > 0 && (network->m_peers.find(peerId) != network->m_peers.end()) && streamId != 0U) {
@ -548,7 +570,7 @@ void* FNENetwork::threadedNetworkRx(void* arg)
delete[] req->buffer;
delete req;
return nullptr;
return;
}
// process incoming message function opcodes
@ -677,8 +699,8 @@ void* FNENetwork::threadedNetworkRx(void* arg)
network->writePeerNAK(peerId, TAG_REPEATER_LOGIN, NET_CONN_NAK_PEER_ACL, req->address, req->addrLen);
delete connection;
network->erasePeer(peerId);
delete connection;
}
}
}
@ -714,8 +736,8 @@ void* FNENetwork::threadedNetworkRx(void* arg)
network->writePeerNAK(peerId, TAG_REPEATER_LOGIN, NET_CONN_NAK_PEER_ACL, req->address, req->addrLen);
delete connection;
network->erasePeer(peerId);
delete connection;
}
}
} else {
@ -724,8 +746,8 @@ void* FNENetwork::threadedNetworkRx(void* arg)
LogWarning(LOG_NET, "PEER %u (%s) RPTL NAK, bad connection state, connectionState = %u", peerId, connection->identity().c_str(),
connection->connectionState());
delete connection;
network->erasePeer(peerId);
delete connection;
}
} else {
network->writePeerNAK(peerId, TAG_REPEATER_LOGIN, NET_CONN_NAK_BAD_CONN_STATE, req->address, req->addrLen);
@ -832,8 +854,8 @@ void* FNENetwork::threadedNetworkRx(void* arg)
LogWarning(LOG_NET, "PEER %u RPTK NAK, login exchange while in an incorrect state, connectionState = %u", peerId, connection->connectionState());
network->writePeerNAK(peerId, TAG_REPEATER_AUTH, NET_CONN_NAK_BAD_CONN_STATE, req->address, req->addrLen);
delete connection;
network->erasePeer(peerId);
delete connection;
}
}
}
@ -866,6 +888,7 @@ void* FNENetwork::threadedNetworkRx(void* arg)
LogWarning(LOG_NET, "PEER %u RPTC NAK, supplied invalid configuration data", peerId);
network->writePeerNAK(peerId, TAG_REPEATER_AUTH, NET_CONN_NAK_INVALID_CONFIG_DATA, req->address, req->addrLen);
network->erasePeer(peerId);
delete connection;
}
else {
// ensure parsed JSON is an object
@ -873,6 +896,7 @@ void* FNENetwork::threadedNetworkRx(void* arg)
LogWarning(LOG_NET, "PEER %u RPTC NAK, supplied invalid configuration data", peerId);
network->writePeerNAK(peerId, TAG_REPEATER_AUTH, NET_CONN_NAK_INVALID_CONFIG_DATA, req->address, req->addrLen);
network->erasePeer(peerId);
delete connection;
}
else {
connection->config(v.get<json::object>());
@ -962,6 +986,7 @@ void* FNENetwork::threadedNetworkRx(void* arg)
connection->connectionState());
network->writePeerNAK(peerId, TAG_REPEATER_CONFIG, NET_CONN_NAK_BAD_CONN_STATE, req->address, req->addrLen);
network->erasePeer(peerId);
delete connection;
}
}
}
@ -982,14 +1007,12 @@ void* FNENetwork::threadedNetworkRx(void* arg)
// validate peer (simple validation really)
if (connection->connected() && connection->address() == ip) {
LogInfoEx(LOG_NET, "PEER %u (%s) is closing down", peerId, connection->identity().c_str());
if (network->erasePeer(peerId)) {
network->erasePeerAffiliations(peerId);
network->erasePeer(peerId);
delete connection;
}
}
}
}
}
break;
case NET_FUNC::PING: // Repeater Ping
{
@ -1303,7 +1326,7 @@ void* FNENetwork::threadedNetworkRx(void* arg)
bool currState = g_disableTimeDisplay;
g_disableTimeDisplay = true;
::Log(9999U, nullptr, "%.9u (%8s) %s", peerId, connection->identity().c_str(), payload.c_str());
::Log(9999U, nullptr, nullptr, 0U, nullptr, "%.9u (%8s) %s", peerId, connection->identity().c_str(), payload.c_str());
g_disableTimeDisplay = currState;
// report diagnostic log to InfluxDB
@ -1606,8 +1629,6 @@ void* FNENetwork::threadedNetworkRx(void* arg)
delete[] req->buffer;
delete req;
}
return nullptr;
}
/* Checks if the passed peer ID is blocked from unit-to-unit traffic. */
@ -1631,7 +1652,6 @@ void FNENetwork::eraseStreamPktSeq(uint32_t peerId, uint32_t streamId)
if (peerId > 0 && (m_peers.find(peerId) != m_peers.end())) {
FNEPeerConnection* connection = m_peers[peerId];
if (connection != nullptr) {
std::lock_guard<std::mutex> lock(m_peerMutex);
connection->eraseStreamPktSeq(streamId);
}
}
@ -1643,7 +1663,6 @@ void FNENetwork::createPeerAffiliations(uint32_t peerId, std::string peerName)
{
erasePeerAffiliations(peerId);
std::lock_guard<std::mutex> lock(m_peerMutex);
lookups::ChannelLookup* chLookup = new lookups::ChannelLookup();
m_peerAffiliations[peerId] = new lookups::AffiliationLookup(peerName, chLookup, m_verbose);
m_peerAffiliations[peerId]->setDisableUnitRegTimeout(true); // FNE doesn't allow unit registration timeouts (notification must come from the peers)
@ -1653,7 +1672,6 @@ void FNENetwork::createPeerAffiliations(uint32_t peerId, std::string peerName)
bool FNENetwork::erasePeerAffiliations(uint32_t peerId)
{
std::lock_guard<std::mutex> lock(m_peerMutex);
auto it = std::find_if(m_peerAffiliations.begin(), m_peerAffiliations.end(), [&](PeerAffiliationMapPair x) { return x.first == peerId; });
if (it != m_peerAffiliations.end()) {
lookups::AffiliationLookup* aff = m_peerAffiliations[peerId];
@ -1673,9 +1691,8 @@ bool FNENetwork::erasePeerAffiliations(uint32_t peerId)
/* Helper to erase the peer from the peers list. */
bool FNENetwork::erasePeer(uint32_t peerId)
void FNENetwork::erasePeer(uint32_t peerId)
{
std::lock_guard<std::mutex> lock(m_peerMutex);
{
auto it = std::find_if(m_peers.begin(), m_peers.end(), [&](PeerMapPair x) { return x.first == peerId; });
if (it != m_peers.end()) {
@ -1699,7 +1716,8 @@ bool FNENetwork::erasePeer(uint32_t peerId)
}
}
return true;
// cleanup peer affiliations
erasePeerAffiliations(peerId);
}
@ -1757,8 +1775,8 @@ bool FNENetwork::resetPeer(uint32_t peerId)
writePeerNAK(peerId, TAG_REPEATER_LOGIN, NET_CONN_NAK_PEER_RESET, addr, addrLen);
delete connection;
erasePeer(peerId);
delete connection;
return true;
}
@ -1772,7 +1790,6 @@ bool FNENetwork::resetPeer(uint32_t peerId)
std::string FNENetwork::resolvePeerIdentity(uint32_t peerId)
{
std::lock_guard<std::mutex> lock(m_peerMutex);
auto it = std::find_if(m_peers.begin(), m_peers.end(), [&](PeerMapPair x) { return x.first == peerId; });
if (it != m_peers.end()) {
if (it->second != nullptr) {
@ -1810,43 +1827,31 @@ void FNENetwork::setupRepeaterLogin(uint32_t peerId, uint32_t streamId, FNEPeerC
void FNENetwork::peerACLUpdate(uint32_t peerId)
{
ACLUpdateRequest* req = new ACLUpdateRequest();
req->obj = this;
req->peerId = peerId;
std::stringstream peerName;
peerName << peerId << ":acl-update";
if (!Thread::runAsThread(this, threadedACLUpdate, req)) {
// enqueue the task
if (!m_threadPool.enqueue(new_pooltask(taskACLUpdate, req))) {
LogError(LOG_NET, "Failed to task enqueue ACL update, peerId = %u", peerId);
if (req != nullptr)
delete req;
return;
}
// pthread magic to rename the thread properly
#ifdef _GNU_SOURCE
::pthread_setname_np(req->thread, peerName.str().c_str());
#endif // _GNU_SOURCE
}
/* Helper to send the ACL lists to the specified peer in a separate thread. */
void* FNENetwork::threadedACLUpdate(void* arg)
void FNENetwork::taskACLUpdate(ACLUpdateRequest* req)
{
ACLUpdateRequest* req = (ACLUpdateRequest*)arg;
if (req != nullptr) {
#if defined(_WIN32)
::CloseHandle(req->thread);
#else
::pthread_detach(req->thread);
#endif // defined(_WIN32)
FNENetwork* network = static_cast<FNENetwork*>(req->obj);
if (network == nullptr) {
if (req != nullptr)
delete req;
return nullptr;
return;
}
if (req == nullptr)
return nullptr;
return;
std::string peerIdentity = network->resolvePeerIdentity(req->peerId);
@ -1875,8 +1880,6 @@ void* FNENetwork::threadedACLUpdate(void* arg)
delete req;
}
return nullptr;
}
/* Helper to send the list of whitelisted RIDs to the specified peer. */
@ -1912,50 +1915,10 @@ void FNENetwork::writeWhitelistRIDs(uint32_t peerId, uint32_t streamId, bool isE
::memset(buffer, 0x00U, len);
::memcpy(buffer, b.str().data(), len);
// compression structures
z_stream strm;
strm.zalloc = Z_NULL;
strm.zfree = Z_NULL;
strm.opaque = Z_NULL;
// initialize compression
if (deflateInit(&strm, Z_DEFAULT_COMPRESSION) != Z_OK) {
LogError(LOG_NET, "PEER %u (%s) error initializing ZLIB", peerId, connection->identity().c_str());
return;
}
// set input data
strm.avail_in = len;
strm.next_in = buffer;
// compress data
std::vector<uint8_t> compressedData;
int ret;
do {
// resize the output buffer as needed
compressedData.resize(compressedData.size() + 16384);
strm.avail_out = 16384;
strm.next_out = compressedData.data() + compressedData.size() - 16384;
ret = deflate(&strm, Z_FINISH);
if (ret == Z_STREAM_ERROR) {
LogError(LOG_NET, "PEER %u (%s) error compressing TGID list", peerId, connection->identity().c_str());
deflateEnd(&strm);
return;
}
} while (ret != Z_STREAM_END);
// resize the output buffer to the actual compressed data size
compressedData.resize(strm.total_out);
// cleanup
deflateEnd(&strm);
uint32_t compressedLen = strm.total_out;
uint8_t* compressed = compressedData.data();
// Utils::dump(1U, "Compressed Payload", compressed, compressedLen);
uint32_t compressedLen = 0U;
uint8_t* compressed = Compression::compress((uint8_t*)buffer, len, &compressedLen);
if (compressed != nullptr) {
// transmit TGIDs
uint8_t blockCnt = (compressedLen / PEER_LINK_BLOCK_SIZE) + (compressedLen % PEER_LINK_BLOCK_SIZE ? 1U : 0U);
uint32_t offs = 0U;
@ -1990,6 +1953,9 @@ void FNENetwork::writeWhitelistRIDs(uint32_t peerId, uint32_t streamId, bool isE
}
connection->lastPing(now);
} else {
LogError(LOG_NET, "PEER %u error compressing RID list", peerId);
}
}
return;
@ -2174,50 +2140,10 @@ void FNENetwork::writeTGIDs(uint32_t peerId, uint32_t streamId, bool isExternalP
::memset(buffer, 0x00U, len);
::memcpy(buffer, b.str().data(), len);
// compression structures
z_stream strm;
strm.zalloc = Z_NULL;
strm.zfree = Z_NULL;
strm.opaque = Z_NULL;
// initialize compression
if (deflateInit(&strm, Z_DEFAULT_COMPRESSION) != Z_OK) {
LogError(LOG_NET, "PEER %u (%s) error initializing ZLIB", peerId, connection->identity().c_str());
return;
}
// set input data
strm.avail_in = len;
strm.next_in = buffer;
// compress data
std::vector<uint8_t> compressedData;
int ret;
do {
// resize the output buffer as needed
compressedData.resize(compressedData.size() + 16384);
strm.avail_out = 16384;
strm.next_out = compressedData.data() + compressedData.size() - 16384;
ret = deflate(&strm, Z_FINISH);
if (ret == Z_STREAM_ERROR) {
LogError(LOG_NET, "PEER %u (%s) error compressing TGID list", peerId, connection->identity().c_str());
deflateEnd(&strm);
return;
}
} while (ret != Z_STREAM_END);
// resize the output buffer to the actual compressed data size
compressedData.resize(strm.total_out);
// cleanup
deflateEnd(&strm);
uint32_t compressedLen = strm.total_out;
uint8_t* compressed = compressedData.data();
// Utils::dump(1U, "Compressed Payload", compressed, compressedLen);
uint32_t compressedLen = 0U;
uint8_t* compressed = Compression::compress((uint8_t*)buffer, len, &compressedLen);
if (compressed != nullptr) {
// transmit TGIDs
uint8_t blockCnt = (compressedLen / PEER_LINK_BLOCK_SIZE) + (compressedLen % PEER_LINK_BLOCK_SIZE ? 1U : 0U);
uint32_t offs = 0U;
@ -2252,6 +2178,9 @@ void FNENetwork::writeTGIDs(uint32_t peerId, uint32_t streamId, bool isExternalP
}
connection->lastPing(now);
} else {
LogError(LOG_NET, "PEER %u error compressing TGID list", peerId);
}
}
return;
@ -2425,51 +2354,11 @@ void FNENetwork::writePeerList(uint32_t peerId, uint32_t streamId)
::memset(buffer, 0x00U, len);
::memcpy(buffer, b.str().data(), len);
// compression structures
z_stream strm;
strm.zalloc = Z_NULL;
strm.zfree = Z_NULL;
strm.opaque = Z_NULL;
// initialize compression
if (deflateInit(&strm, Z_DEFAULT_COMPRESSION) != Z_OK) {
LogError(LOG_NET, "PEER %u (%s) error initializing ZLIB", peerId, connection->identity().c_str());
return;
}
uint32_t compressedLen = 0U;
uint8_t* compressed = Compression::compress((uint8_t*)buffer, len, &compressedLen);
// set input data
strm.avail_in = len;
strm.next_in = buffer;
// compress data
std::vector<uint8_t> compressedData;
int ret;
do {
// resize the output buffer as needed
compressedData.resize(compressedData.size() + 16384);
strm.avail_out = 16384;
strm.next_out = compressedData.data() + compressedData.size() - 16384;
ret = deflate(&strm, Z_FINISH);
if (ret == Z_STREAM_ERROR) {
LogError(LOG_NET, "PEER %u (%s) error compressing TGID list", peerId, connection->identity().c_str());
deflateEnd(&strm);
return;
}
} while (ret != Z_STREAM_END);
// resize the output buffer to the actual compressed data size
compressedData.resize(strm.total_out);
// cleanup
deflateEnd(&strm);
uint32_t compressedLen = strm.total_out;
uint8_t* compressed = compressedData.data();
// Utils::dump(1U, "Compressed Payload", compressed, compressedLen);
// transmit TGIDs
if (compressed != nullptr) {
// transmit PIDs
uint8_t blockCnt = (compressedLen / PEER_LINK_BLOCK_SIZE) + (compressedLen % PEER_LINK_BLOCK_SIZE ? 1U : 0U);
uint32_t offs = 0U;
for (uint8_t i = 0U; i < blockCnt; i++) {
@ -2503,6 +2392,9 @@ void FNENetwork::writePeerList(uint32_t peerId, uint32_t streamId)
}
connection->lastPing(now);
} else {
LogError(LOG_NET, "PEER %u error compressing PID list", peerId);
}
}
return;

@ -25,6 +25,7 @@
#define __FNE_NETWORK_H__
#include "fne/Defines.h"
#include "common/concurrent/unordered_map.h"
#include "common/network/BaseNetwork.h"
#include "common/network/json/json.h"
#include "common/lookups/AffiliationLookup.h"
@ -32,6 +33,7 @@
#include "common/lookups/TalkgroupRulesLookup.h"
#include "common/lookups/PeerListLookup.h"
#include "common/network/Network.h"
#include "common/ThreadPool.h"
#include "fne/network/influxdb/InfluxDB.h"
#include "fne/CryptoContainer.h"
@ -391,6 +393,8 @@ namespace network
frame::RTPFNEHeader fneHeader; //! RTP FNE Header
int length = 0U; //! Length of raw data buffer
uint8_t* buffer = nullptr; //! Raw data buffer
uint64_t pktRxTime; //! Packet receive time
};
// ---------------------------------------------------------------------------
@ -422,10 +426,11 @@ namespace network
* @param allowDiagnosticTransfer Flag indicating that the system diagnostic logs will be sent to the network.
* @param pingTime
* @param updateLookupTime
* @param workerCnt Number of worker threads.
*/
FNENetwork(HostFNE* host, const std::string& address, uint16_t port, uint32_t peerId, const std::string& password,
bool debug, bool verbose, bool reportPeerPing, bool dmr, bool p25, bool nxdn, uint32_t parrotDelay, bool parrotGrantDemand,
bool allowActivityTransfer, bool allowDiagnosticTransfer, uint32_t pingTime, uint32_t updateLookupTime);
bool allowActivityTransfer, bool allowDiagnosticTransfer, uint32_t pingTime, uint32_t updateLookupTime, uint16_t workerCnt);
/**
* @brief Finalizes a instance of the FNENetwork class.
*/
@ -548,16 +553,47 @@ namespace network
NET_CONN_STATUS m_status;
static std::mutex m_peerMutex;
typedef std::pair<const uint32_t, network::FNEPeerConnection*> PeerMapPair;
std::unordered_map<uint32_t, FNEPeerConnection*> m_peers;
std::unordered_map<uint32_t, json::array> m_peerLinkPeers;
concurrent::unordered_map<uint32_t, FNEPeerConnection*> m_peers;
concurrent::unordered_map<uint32_t, json::array> m_peerLinkPeers;
typedef std::pair<const uint32_t, lookups::AffiliationLookup*> PeerAffiliationMapPair;
std::unordered_map<uint32_t, lookups::AffiliationLookup*> m_peerAffiliations;
std::unordered_map<uint32_t, std::vector<uint32_t>> m_ccPeerMap;
concurrent::unordered_map<uint32_t, lookups::AffiliationLookup*> m_peerAffiliations;
concurrent::unordered_map<uint32_t, std::vector<uint32_t>> m_ccPeerMap;
static std::timed_mutex m_keyQueueMutex;
std::unordered_map<uint32_t, uint16_t> m_peerLinkKeyQueue;
/**
* @brief Represents a Peer-Link Active Peer List fragment packet.
*/
class PLActPeerPkt {
public:
/**
* @brief Compressed size of the packet.
*/
uint32_t compressedSize;
/**
* @brief Uncompressed size of the packet.
*/
uint32_t size;
/**
* @brief Last block of the packet.
*/
uint8_t lastBlock;
/**
* @brief Stream ID of the packet.
*/
uint32_t streamId;
/**
* @brief Packet fragments.
*/
std::unordered_map<uint8_t, uint8_t*> fragments;
bool locked;
};
concurrent::unordered_map<uint32_t, PLActPeerPkt> m_peerLinkActPkt;
Timer m_maintainenceTimer;
uint32_t m_updateLookupTime;
@ -590,6 +626,8 @@ namespace network
bool m_influxLogRawData;
influxdb::ServerInfo m_influxServer;
ThreadPool m_threadPool;
bool m_disablePacketData;
bool m_dumpPacketData;
bool m_verbosePacketData;
@ -599,10 +637,9 @@ namespace network
/**
* @brief Entry point to process a given network packet.
* @param arg Instance of the NetPacketRequest structure.
* @returns void* (Ignore)
* @param req Instance of the NetPacketRequest structure.
*/
static void* threadedNetworkRx(void* arg);
static void taskNetworkRx(NetPacketRequest* req);
/**
* @brief Checks if the passed peer ID is blocked from unit-to-unit traffic.
@ -632,10 +669,11 @@ namespace network
bool erasePeerAffiliations(uint32_t peerId);
/**
* @brief Helper to erase the peer from the peers list.
* @note This does not delete or otherwise free the FNEConnection instance!
* @param peerId Peer ID.
* @returns bool True, if peer was deleted, otherwise false.
*/
bool erasePeer(uint32_t peerId);
void erasePeer(uint32_t peerId);
/**
* @brief Helper to resolve the peer ID to its identity string.
@ -659,10 +697,9 @@ namespace network
void peerACLUpdate(uint32_t peerId);
/**
* @brief Entry point to send the ACL lists to the specified peer in a separate thread.
* @param arg Instance of the ACLUpdateRequest structure.
* @returns void* (Ignore)
* @param req Instance of the ACLUpdateRequest structure.
*/
static void* threadedACLUpdate(void* arg);
static void taskACLUpdate(ACLUpdateRequest* req);
/**
* @brief Helper to send the list of whitelisted RIDs to the specified peer.

@ -9,12 +9,13 @@
*/
#include "fne/Defines.h"
#include "common/network/json/json.h"
#include "common/zlib/zlib.h"
#include "common/zlib/Compression.h"
#include "common/Log.h"
#include "common/Utils.h"
#include "fne/network/PeerNetwork.h"
using namespace network;
using namespace compress;
#include <cstdio>
#include <cassert>
@ -113,14 +114,56 @@ bool PeerNetwork::writePeerLinkPeers(json::array* peerList)
json::value v = json::value(*peerList);
std::string json = std::string(v.serialize());
CharArray __buffer = std::make_unique<char[]>(json.length() + 9U);
size_t len = json.length() + 9U;
CharArray __buffer = std::make_unique<char[]>(len);
char* buffer = __buffer.get();
::memcpy(buffer + 0U, TAG_PEER_LINK, 4U);
::snprintf(buffer + 8U, json.length() + 1U, "%s", json.c_str());
return writeMaster({ NET_FUNC::PEER_LINK, NET_SUBFUNC::PL_ACT_PEER_LIST },
(uint8_t*)buffer, json.length() + 8U, RTP_END_OF_CALL_SEQ, createStreamId(), false, true);
uint32_t compressedLen = 0U;
uint8_t* compressed = Compression::compress((uint8_t*)buffer, len, &compressedLen);
if (compressed != nullptr) {
// transmit peer link active peer list
uint32_t streamId = createStreamId();
uint8_t blockCnt = (compressedLen / PEER_LINK_BLOCK_SIZE) + (compressedLen % PEER_LINK_BLOCK_SIZE ? 1U : 0U);
uint32_t offs = 0U;
for (uint8_t i = 0U; i < blockCnt; i++) {
// build dataset
uint16_t bufSize = 10U + (PEER_LINK_BLOCK_SIZE);
UInt8Array __payload = std::make_unique<uint8_t[]>(bufSize);
uint8_t* payload = __payload.get();
::memset(payload, 0x00U, bufSize);
if (i == 0U) {
__SET_UINT32(len, payload, 0U);
__SET_UINT32(compressedLen, payload, 4U);
}
payload[8U] = i;
payload[9U] = blockCnt - 1U;
uint32_t blockSize = PEER_LINK_BLOCK_SIZE;
if (offs + PEER_LINK_BLOCK_SIZE > compressedLen)
blockSize = PEER_LINK_BLOCK_SIZE - ((offs + PEER_LINK_BLOCK_SIZE) - compressedLen);
::memcpy(payload + 10U, compressed + offs, blockSize);
if (m_debug)
Utils::dump(1U, "Peer-Link Active Peer Payload", payload, bufSize);
offs += PEER_LINK_BLOCK_SIZE;
writeMaster({ NET_FUNC::PEER_LINK, NET_SUBFUNC::PL_ACT_PEER_LIST },
payload, bufSize, RTP_END_OF_CALL_SEQ, streamId, false, true);
Thread::sleep(60U); // pace block transmission
}
return true;
} else {
LogError(LOG_NET, "PEER %u error compressing active peer list", m_peerId);
return false;
}
}
return false;
@ -166,61 +209,14 @@ void PeerNetwork::userPacketHandler(uint32_t peerId, FrameQueue::OpcodePair opco
::memcpy(m_tgidBuffer + offs, data + 10U, PEER_LINK_BLOCK_SIZE);
// Utils::dump(1U, "Block Payload", data, 10U + PEER_LINK_BLOCK_SIZE);
// Utils::dump(1U, "Compressed Payload", m_tgidBuffer, m_tgidCompressedSize);
// handle last block
// compression structures
z_stream strm;
strm.zalloc = Z_NULL;
strm.zfree = Z_NULL;
strm.opaque = Z_NULL;
// set input data
strm.avail_in = m_tgidCompressedSize;
strm.next_in = m_tgidBuffer;
// initialize decompression
int ret = inflateInit(&strm);
if (ret != Z_OK) {
LogError(LOG_NET, "PEER %u error initializing ZLIB", peerId);
m_tgidSize = 0U;
m_tgidCompressedSize = 0U;
if (m_tgidBuffer != nullptr)
delete[] m_tgidBuffer;
m_tgidBuffer = nullptr;
break;
}
// decompress data
std::vector<uint8_t> decompressedData;
uint8_t outbuffer[1024];
do {
strm.avail_out = sizeof(outbuffer);
strm.next_out = outbuffer;
ret = inflate(&strm, Z_NO_FLUSH);
if (ret == Z_STREAM_ERROR) {
LogError(LOG_NET, "PEER %u error decompressing TGID list", peerId);
inflateEnd(&strm);
goto tid_lookup_cleanup; // yes - I hate myself; but this is quick
}
decompressedData.insert(decompressedData.end(), outbuffer, outbuffer + sizeof(outbuffer) - strm.avail_out);
} while (ret != Z_STREAM_END);
// cleanup
inflateEnd(&strm);
// scope is intentional
{
uint32_t decompressedLen = strm.total_out;
uint8_t* decompressed = decompressedData.data();
// Utils::dump(1U, "Raw TGID Data", decompressed, decompressedLen);
uint32_t decompressedLen = 0U;
uint8_t* decompressed = Compression::decompress(m_tgidBuffer, m_tgidCompressedSize, &decompressedLen);
// check that we got the appropriate data
if (decompressedLen == m_tgidSize) {
if (decompressedLen == m_tgidSize && decompressed != nullptr) {
if (m_tidLookup == nullptr) {
LogError(LOG_NET, "Talkgroup ID lookup not available yet.");
goto tid_lookup_cleanup; // yes - I hate myself; but this is quick
@ -304,61 +300,14 @@ void PeerNetwork::userPacketHandler(uint32_t peerId, FrameQueue::OpcodePair opco
::memcpy(m_ridBuffer + offs, data + 10U, PEER_LINK_BLOCK_SIZE);
// Utils::dump(1U, "Block Payload", data, 10U + PEER_LINK_BLOCK_SIZE);
// Utils::dump(1U, "Compressed Payload", m_ridBuffer, m_ridCompressedSize);
// handle last block
// compression structures
z_stream strm;
strm.zalloc = Z_NULL;
strm.zfree = Z_NULL;
strm.opaque = Z_NULL;
// set input data
strm.avail_in = m_ridCompressedSize;
strm.next_in = m_ridBuffer;
// initialize decompression
int ret = inflateInit(&strm);
if (ret != Z_OK) {
LogError(LOG_NET, "PEER %u error initializing ZLIB", peerId);
m_ridSize = 0U;
m_ridCompressedSize = 0U;
if (m_ridBuffer != nullptr)
delete[] m_ridBuffer;
m_ridBuffer = nullptr;
break;
}
// decompress data
std::vector<uint8_t> decompressedData;
uint8_t outbuffer[1024];
do {
strm.avail_out = sizeof(outbuffer);
strm.next_out = outbuffer;
ret = inflate(&strm, Z_NO_FLUSH);
if (ret == Z_STREAM_ERROR) {
LogError(LOG_NET, "PEER %u error decompressing RID list", peerId);
inflateEnd(&strm);
goto rid_lookup_cleanup; // yes - I hate myself; but this is quick
}
decompressedData.insert(decompressedData.end(), outbuffer, outbuffer + sizeof(outbuffer) - strm.avail_out);
} while (ret != Z_STREAM_END);
// cleanup
inflateEnd(&strm);
// scope is intentional
{
uint32_t decompressedLen = strm.total_out;
uint8_t* decompressed = decompressedData.data();
// Utils::dump(1U, "Raw RID Data", decompressed, decompressedLen);
uint32_t decompressedLen = 0U;
uint8_t* decompressed = Compression::decompress(m_ridBuffer, m_ridCompressedSize, &decompressedLen);
// check that we got the appropriate data
if (decompressedLen == m_ridSize) {
if (decompressedLen == m_ridSize && decompressed != nullptr) {
if (m_ridLookup == nullptr) {
LogError(LOG_NET, "Radio ID lookup not available yet.");
goto rid_lookup_cleanup; // yes - I hate myself; but this is quick
@ -442,61 +391,14 @@ void PeerNetwork::userPacketHandler(uint32_t peerId, FrameQueue::OpcodePair opco
::memcpy(m_pidBuffer + offs, data + 10U, PEER_LINK_BLOCK_SIZE);
// Utils::dump(1U, "Block Payload", data, 10U + PEER_LINK_BLOCK_SIZE);
// Utils::dump(1U, "Compressed Payload", m_pidBuffer, m_pidCompressedSize);
// handle last block
// compression structures
z_stream strm;
strm.zalloc = Z_NULL;
strm.zfree = Z_NULL;
strm.opaque = Z_NULL;
// set input data
strm.avail_in = m_pidCompressedSize;
strm.next_in = m_pidBuffer;
// initialize decompression
int ret = inflateInit(&strm);
if (ret != Z_OK) {
LogError(LOG_NET, "PEER %u error initializing ZLIB", peerId);
m_pidSize = 0U;
m_pidCompressedSize = 0U;
if (m_pidBuffer != nullptr)
delete[] m_pidBuffer;
m_pidBuffer = nullptr;
break;
}
// decompress data
std::vector<uint8_t> decompressedData;
uint8_t outbuffer[1024];
do {
strm.avail_out = sizeof(outbuffer);
strm.next_out = outbuffer;
ret = inflate(&strm, Z_NO_FLUSH);
if (ret == Z_STREAM_ERROR) {
LogError(LOG_NET, "PEER %u error decompressing peer list", peerId);
inflateEnd(&strm);
goto pid_lookup_cleanup; // yes - I hate myself; but this is quick
}
decompressedData.insert(decompressedData.end(), outbuffer, outbuffer + sizeof(outbuffer) - strm.avail_out);
} while (ret != Z_STREAM_END);
// cleanup
inflateEnd(&strm);
// scope is intentional
{
uint32_t decompressedLen = strm.total_out;
uint8_t* decompressed = decompressedData.data();
// Utils::dump(1U, "Raw Peer List Data", decompressed, decompressedLen);
uint32_t decompressedLen = 0U;
uint8_t* decompressed = Compression::decompress(m_pidBuffer, m_pidCompressedSize, &decompressedLen);
// check that we got the appropriate data
if (decompressedLen == m_pidSize) {
if (decompressedLen == m_pidSize && decompressed != nullptr) {
if (m_pidLookup == nullptr) {
LogError(LOG_NET, "Peer ID lookup not available yet.");
goto pid_lookup_cleanup; // yes - I hate myself; but this is quick

@ -545,7 +545,12 @@ RESTAPI::RESTAPI(const std::string& address, uint16_t port, const std::string& p
/* Finalizes a instance of the RESTAPI class. */
RESTAPI::~RESTAPI() = default;
RESTAPI::~RESTAPI()
{
if (m_passwordHash != nullptr) {
delete[] m_passwordHash;
}
}
/* Sets the instances of the Radio ID and Talkgroup ID lookup tables. */

@ -664,6 +664,21 @@ bool TagDMRData::isPeerPermitted(uint32_t peerId, data::NetData& data, uint32_t
return false;
}
FNEPeerConnection* connection = nullptr; // bryanb: this is a possible null ref concurrency issue
// it is possible if the timing is just right to get a valid
// connection back initially, and then for it to be deleted
if (peerId > 0 && (m_network->m_peers.find(peerId) != m_network->m_peers.end())) {
connection = m_network->m_peers[peerId];
}
// is this peer a Peer-Link peer?
if (connection != nullptr) {
if (connection->isPeerLink()) {
return true; // Peer Link peers are *always* allowed to receive traffic and no other rules may filter
// these peers
}
}
// is this a group call?
if (data.getFLCO() == FLCO::GROUP) {
lookups::TalkgroupRuleGroupVoice tg = m_network->m_tidLookup->find(data.getDstId(), data.getSlotNo());
@ -696,11 +711,6 @@ bool TagDMRData::isPeerPermitted(uint32_t peerId, data::NetData& data, uint32_t
}
}
FNEPeerConnection* connection = nullptr;
if (peerId > 0 && (m_network->m_peers.find(peerId) != m_network->m_peers.end())) {
connection = m_network->m_peers[peerId];
}
// is this peer a conventional peer?
if (m_network->m_allowConvSiteAffOverride) {
if (connection != nullptr) {

@ -17,6 +17,8 @@
#define __CALLHANDLER__TAG_DMR_DATA_H__
#include "fne/Defines.h"
#include "common/concurrent/deque.h"
#include "common/concurrent/unordered_map.h"
#include "common/dmr/DMRDefines.h"
#include "common/dmr/data/NetData.h"
#include "common/dmr/lc/CSBK.h"
@ -24,8 +26,6 @@
#include "network/FNENetwork.h"
#include "network/callhandler/packetdata/DMRPacketData.h"
#include <deque>
namespace network
{
namespace callhandler
@ -147,7 +147,7 @@ namespace network
*/
uint32_t dstId;
};
std::deque<ParrotFrame> m_parrotFrames;
concurrent::deque<ParrotFrame> m_parrotFrames;
bool m_parrotFramesReady;
/**
@ -196,7 +196,7 @@ namespace network
}
};
typedef std::pair<const uint32_t, RxStatus> StatusMapPair;
std::unordered_map<uint32_t, RxStatus> m_status;
concurrent::unordered_map<uint32_t, RxStatus> m_status;
friend class packetdata::DMRPacketData;
packetdata::DMRPacketData* m_packetData;

@ -471,6 +471,21 @@ bool TagNXDNData::isPeerPermitted(uint32_t peerId, lc::RTCH& lc, uint8_t message
return false;
}
FNEPeerConnection* connection = nullptr; // bryanb: this is a possible null ref concurrency issue
// it is possible if the timing is just right to get a valid
// connection back initially, and then for it to be deleted
if (peerId > 0 && (m_network->m_peers.find(peerId) != m_network->m_peers.end())) {
connection = m_network->m_peers[peerId];
}
// is this peer a Peer-Link peer?
if (connection != nullptr) {
if (connection->isPeerLink()) {
return true; // Peer Link peers are *always* allowed to receive traffic and no other rules may filter
// these peers
}
}
// is this a group call?
if (lc.getGroup()) {
lookups::TalkgroupRuleGroupVoice tg = m_network->m_tidLookup->find(lc.getDstId());
@ -503,11 +518,6 @@ bool TagNXDNData::isPeerPermitted(uint32_t peerId, lc::RTCH& lc, uint8_t message
}
}
FNEPeerConnection* connection = nullptr;
if (peerId > 0 && (m_network->m_peers.find(peerId) != m_network->m_peers.end())) {
connection = m_network->m_peers[peerId];
}
// is this peer a conventional peer?
if (m_network->m_allowConvSiteAffOverride) {
if (connection != nullptr) {

@ -18,13 +18,13 @@
#include "fne/Defines.h"
#include "common/Clock.h"
#include "common/concurrent/deque.h"
#include "common/concurrent/unordered_map.h"
#include "common/nxdn/NXDNDefines.h"
#include "common/nxdn/lc/RTCH.h"
#include "common/nxdn/lc/RCCH.h"
#include "network/FNENetwork.h"
#include <deque>
namespace network
{
namespace callhandler
@ -116,7 +116,7 @@ namespace network
*/
uint32_t dstId;
};
std::deque<ParrotFrame> m_parrotFrames;
concurrent::deque<ParrotFrame> m_parrotFrames;
bool m_parrotFramesReady;
/**
@ -160,7 +160,7 @@ namespace network
}
};
typedef std::pair<const uint32_t, RxStatus> StatusMapPair;
std::unordered_map<uint32_t, RxStatus> m_status;
concurrent::unordered_map<uint32_t, RxStatus> m_status;
bool m_debug;

@ -918,6 +918,21 @@ bool TagP25Data::isPeerPermitted(uint32_t peerId, lc::LC& control, DUID::E duid,
return false;
}
FNEPeerConnection* connection = nullptr; // bryanb: this is a possible null ref concurrency issue
// it is possible if the timing is just right to get a valid
// connection back initially, and then for it to be deleted
if (peerId > 0 && (m_network->m_peers.find(peerId) != m_network->m_peers.end())) {
connection = m_network->m_peers[peerId];
}
// is this peer a Peer-Link peer?
if (connection != nullptr) {
if (connection->isPeerLink()) {
return true; // Peer Link peers are *always* allowed to receive traffic and no other rules may filter
// these peers
}
}
// always permit a TSDU or PDU
if (duid == DUID::TSDU || duid == DUID::PDU)
return true;
@ -931,6 +946,15 @@ bool TagP25Data::isPeerPermitted(uint32_t peerId, lc::LC& control, DUID::E duid,
return true;
}
// is this peer excluded from the group?
std::vector<uint32_t> exclusion = tg.config().exclusion();
if (exclusion.size() > 0) {
auto it = std::find(exclusion.begin(), exclusion.end(), peerId);
if (it != exclusion.end()) {
return false;
}
}
tg = m_network->m_tidLookup->findByRewrite(peerId, control.getDstId());
if (!tg.isInvalid()) {
return true;
@ -964,6 +988,15 @@ bool TagP25Data::isPeerPermitted(uint32_t peerId, lc::LC& control, DUID::E duid,
return true;
}
// is this peer excluded from the group?
std::vector<uint32_t> exclusion = tg.config().exclusion();
if (exclusion.size() > 0) {
auto it = std::find(exclusion.begin(), exclusion.end(), peerId);
if (it != exclusion.end()) {
return false;
}
}
tg = m_network->m_tidLookup->findByRewrite(peerId, control.getDstId());
if (!tg.isInvalid()) {
return true;
@ -1005,7 +1038,7 @@ bool TagP25Data::isPeerPermitted(uint32_t peerId, lc::LC& control, DUID::E duid,
}
}
// peer always send list takes priority over any following affiliation rules
// peer always send list takes priority over any other rules
std::vector<uint32_t> alwaysSend = tg.config().alwaysSend();
if (alwaysSend.size() > 0) {
auto it = std::find(alwaysSend.begin(), alwaysSend.end(), peerId);
@ -1014,11 +1047,6 @@ bool TagP25Data::isPeerPermitted(uint32_t peerId, lc::LC& control, DUID::E duid,
}
}
FNEPeerConnection* connection = nullptr;
if (peerId > 0 && (m_network->m_peers.find(peerId) != m_network->m_peers.end())) {
connection = m_network->m_peers[peerId];
}
// is this peer a conventional peer?
if (m_network->m_allowConvSiteAffOverride) {
if (connection != nullptr) {

@ -18,6 +18,8 @@
#include "fne/Defines.h"
#include "common/Clock.h"
#include "common/concurrent/deque.h"
#include "common/concurrent/unordered_map.h"
#include "common/p25/P25Defines.h"
#include "common/p25/data/DataHeader.h"
#include "common/p25/data/LowSpeedData.h"
@ -29,8 +31,6 @@
#include "network/FNENetwork.h"
#include "network/callhandler/packetdata/P25PacketData.h"
#include <deque>
namespace network
{
namespace callhandler
@ -164,7 +164,7 @@ namespace network
*/
uint32_t dstId;
};
std::deque<ParrotFrame> m_parrotFrames;
concurrent::deque<ParrotFrame> m_parrotFrames;
bool m_parrotFramesReady;
bool m_parrotFirstFrame;
@ -209,7 +209,7 @@ namespace network
}
};
typedef std::pair<const uint32_t, RxStatus> StatusMapPair;
std::unordered_map<uint32_t, RxStatus> m_status;
concurrent::unordered_map<uint32_t, RxStatus> m_status;
friend class packetdata::P25PacketData;
packetdata::P25PacketData* m_packetData;

@ -18,6 +18,8 @@
#include "fne/Defines.h"
#include "common/Clock.h"
#include "common/concurrent/deque.h"
#include "common/concurrent/unordered_map.h"
#include "common/dmr/DMRDefines.h"
#include "common/dmr/data/DataHeader.h"
#include "network/FNENetwork.h"
@ -115,7 +117,7 @@ namespace network
}
};
typedef std::pair<const uint32_t, RxStatus*> StatusMapPair;
std::unordered_map<uint32_t, RxStatus*> m_status;
concurrent::unordered_map<uint32_t, RxStatus*> m_status;
bool m_debug;

@ -18,6 +18,8 @@
#include "fne/Defines.h"
#include "common/Clock.h"
#include "common/concurrent/deque.h"
#include "common/concurrent/unordered_map.h"
#include "common/p25/P25Defines.h"
#include "common/p25/data/DataBlock.h"
#include "common/p25/data/DataHeader.h"
@ -103,7 +105,7 @@ namespace network
uint64_t timestamp; //! Timestamp in milliseconds
};
std::deque<VTUNDataFrame*> m_dataFrames;
concurrent::deque<VTUNDataFrame*> m_dataFrames;
/**
* @brief Represents the receive status of a call.
@ -162,7 +164,7 @@ namespace network
}
};
typedef std::pair<const uint32_t, RxStatus*> StatusMapPair;
std::unordered_map<uint32_t, RxStatus*> m_status;
concurrent::unordered_map<uint32_t, RxStatus*> m_status;
typedef std::pair<const uint32_t, uint32_t> ArpTablePair;
std::unordered_map<uint32_t, uint32_t> m_arpTable;

@ -29,7 +29,7 @@ using namespace network::influxdb;
// Static Class Members
// ---------------------------------------------------------------------------
int32_t detail::TSCaller::m_currThreadCnt = 0U;
ThreadPool detail::TSCaller::m_fluxReqThreadPool{MAX_INFLUXQL_THREAD_CNT, "fluxql"};
/* Generates a InfluxDB REST API request. */

@ -5,7 +5,7 @@
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* Copyright (c) 2010-2018 <http://ez8.co> <orca.zhang@yahoo.com>
* Copyright (C) 2024 Bryan Biedenkapp, N2PLL
* Copyright (C) 2024-2025 Bryan Biedenkapp, N2PLL
*
*/
/**
@ -21,7 +21,7 @@
#include "fne/Defines.h"
#include "common/Log.h"
#include "common/Thread.h"
#include "common/ThreadPool.h"
#include <sstream>
#include <cstring>
@ -63,7 +63,8 @@ namespace network
// Constants
// ---------------------------------------------------------------------------
#define MAX_INFLUXQL_THREAD_CNT 125 // Extreme maximum number of pending Flux queries
#define MAX_INFLUXQL_THREAD_CNT 16U
#define MAX_INFLUXQL_QUEUED_CNT 256U
// ---------------------------------------------------------------------------
// Class Declaration
@ -430,54 +431,41 @@ namespace network
int request(const ServerInfo& si, std::string* resp = nullptr) { return detail::inner::request("POST", "write", "", m_lines.str(), si, resp); }
int requestAsync(const ServerInfo& si)
{
if (m_currThreadCnt < 0) {
m_currThreadCnt = 0;
}
if (m_currThreadCnt >= MAX_INFLUXQL_THREAD_CNT) {
::LogError(LOG_HOST, "Maximum concurrent FluxQL thread count reached, dropping request!");
return 1;
}
TSCallerRequest* req = new TSCallerRequest();
req->obj = this;
req->si = ServerInfo(si.host(), si.port(), si.org(), si.token(), si.bucket());
req->lines = std::string(m_lines.str());
if (!Thread::runAsThread(this, threadedRequest, req)) {
// enqueue the task
if (!m_fluxReqThreadPool.enqueue(new_pooltask(taskFluxRequest, req))) {
LogError(LOG_NET, "Failed to task enqueue Influx query request");
if (req != nullptr)
delete req;
return 1;
} else {
m_currThreadCnt++;
}
return 0;
}
static void start()
{
m_fluxReqThreadPool.setMaxQueuedTasks(MAX_INFLUXQL_QUEUED_CNT);
m_fluxReqThreadPool.start();
}
static void stop() { m_fluxReqThreadPool.stop(); }
static void wait() { m_fluxReqThreadPool.wait(); }
private:
static int32_t m_currThreadCnt;
static ThreadPool m_fluxReqThreadPool;
/**
* @brief
*/
static void* threadedRequest(void* arg)
static void taskFluxRequest(TSCallerRequest* req)
{
TSCallerRequest* req = (TSCallerRequest*)arg;
if (req != nullptr) {
#if defined(_WIN32)
::CloseHandle(req->thread);
#else
::pthread_detach(req->thread);
#endif // defined(_WIN32)
#ifdef _GNU_SOURCE
::pthread_setname_np(req->thread, "fluxql:request");
#endif // _GNU_SOURCE
if (req == nullptr) {
m_currThreadCnt--;
return nullptr;
return;
}
TSCaller* caller = static_cast<TSCaller*>(req->obj);
@ -486,8 +474,7 @@ namespace network
delete req;
}
m_currThreadCnt--;
return nullptr;
return;
}
const ServerInfo& si = req->si;
@ -495,10 +482,6 @@ namespace network
delete req;
}
m_currThreadCnt--;
return nullptr;
}
};
// ---------------------------------------------------------------------------

@ -1355,7 +1355,7 @@ json::object Host::getStatus()
uint32_t dstId = 0U, srcId = 0U;
// fetch affiliations from DMR if we're a DMR CC
if (m_dmrTSCCData) {
if (m_dmrTSCCData && m_dmr->affiliations() != nullptr) {
if (m_dmr->affiliations()->isChBusy(chNo)) {
chData["tx"].set<bool>(_true);
} else {
@ -1368,29 +1368,29 @@ json::object Host::getStatus()
}
// fetch affiliations from P25 if we're a P25 CC
if (m_p25CCData) {
if (m_p25->affiliations().isChBusy(chNo)) {
if (m_p25CCData && m_p25->affiliations() != nullptr) {
if (m_p25->affiliations()->isChBusy(chNo)) {
chData["tx"].set<bool>(_true);
} else {
chData["tx"].set<bool>(_false);
}
dstId = m_p25->affiliations().getGrantedDstByCh(chNo);
dstId = m_p25->affiliations()->getGrantedDstByCh(chNo);
if (dstId > 0U)
srcId = m_p25->affiliations().getGrantedSrcId(dstId);
srcId = m_p25->affiliations()->getGrantedSrcId(dstId);
}
// fetch affiliations from NXDN if we're a NXDN CC
if (m_nxdnCCData) {
if (m_nxdn->affiliations().isChBusy(chNo)) {
if (m_nxdnCCData && m_nxdn->affiliations() != nullptr) {
if (m_nxdn->affiliations()->isChBusy(chNo)) {
chData["tx"].set<bool>(_true);
} else {
chData["tx"].set<bool>(_false);
}
dstId = m_nxdn->affiliations().getGrantedDstByCh(chNo);
dstId = m_nxdn->affiliations()->getGrantedDstByCh(chNo);
if (dstId > 0U)
srcId = m_nxdn->affiliations().getGrantedSrcId(dstId);
srcId = m_nxdn->affiliations()->getGrantedSrcId(dstId);
}
chData["lastDstId"].set<uint32_t>(dstId);

@ -55,6 +55,8 @@ std::string g_remoteAddress = std::string("127.0.0.1");
uint16_t g_remotePort = REMOTE_MODEM_PORT;
uint16_t g_remoteLocalPort = 0U;
bool g_bootloader = false;
bool g_fireDMRBeacon = false;
bool g_fireP25Control = false;
bool g_fireNXDNControl = false;
@ -117,9 +119,9 @@ void usage(const char* message, const char* arg)
" [--syslog]"
#if defined(ENABLE_SETUP_TUI)
" [--setup]"
#else
"[--cal]"
#endif
" [--cal]"
"[--boot]"
" [-c <configuration file>]"
" [--remote [-a <address>] [-p <port>]]"
"\n\n"
@ -131,10 +133,11 @@ void usage(const char* message, const char* arg)
" --syslog force logging to syslog\n"
"\n"
#if defined(ENABLE_SETUP_TUI)
" --setup setup and calibration mode\n"
#else
" --cal old calibration mode\n"
" --setup TUI setup and calibration mode\n"
"\n"
#endif
" --cal simple calibration mode\n"
" --boot connects to modem and reboots into bootloader mode\n"
"\n"
" -c <file> specifies the configuration file to use\n"
"\n"
@ -187,6 +190,11 @@ int checkArgs(int argc, char* argv[])
g_calibrate = true;
#endif // defined(ENABLE_SETUP_TUI)
}
else if (IS("--boot")) {
g_bootloader = true;
g_calibrate = true;
g_setup = false;
}
else if (IS("-c")) {
if (argc-- <= 0)
usage("error: %s", "must specify the configuration file to use");

@ -52,6 +52,9 @@ extern uint16_t g_remotePort;
/** @brief (Global) Local Remote Modem Port (Listening Port). */
extern uint16_t g_remoteLocalPort;
/** @brief (Global) Set Modem into Bootloader Mode. */
extern bool g_bootloader;
/** @brief (Global) Fire DMR beacon flag. */
extern bool g_fireDMRBeacon;
/** @brief (Global) Fire P25 control flag. */

@ -121,6 +121,15 @@ int HostCal::run(int argc, char **argv)
return 1;
}
if (g_bootloader) {
writeBootload();
m_isConnected = false;
m_modem->close();
m_console.close();
return 0;
}
readFlash();
writeFifoLength();

@ -86,15 +86,12 @@ bool DMRAffiliationLookup::grantChSlot(uint32_t dstId, uint32_t srcId, uint8_t s
/* Helper to release the channel grant for the destination ID. */
bool DMRAffiliationLookup::releaseGrant(uint32_t dstId, bool releaseAll, bool noLock)
bool DMRAffiliationLookup::releaseGrant(uint32_t dstId, bool releaseAll)
{
if (dstId == 0U && !releaseAll) {
return false;
}
if (!noLock)
m_mutex.lock();
// are we trying to release all grants?
if (dstId == 0U && releaseAll) {
LogWarning(LOG_HOST, "%s, force releasing all channel grants", m_name.c_str());
@ -110,8 +107,6 @@ bool DMRAffiliationLookup::releaseGrant(uint32_t dstId, bool releaseAll, bool no
releaseGrant(dstId, false);
}
if (!noLock)
m_mutex.unlock();
return true;
}
@ -145,13 +140,9 @@ bool DMRAffiliationLookup::releaseGrant(uint32_t dstId, bool releaseAll, bool no
m_grantTimers[dstId].stop();
if (!noLock)
m_mutex.unlock();
return true;
}
if (!noLock)
m_mutex.unlock();
return false;
}

@ -73,10 +73,9 @@ namespace dmr
* @brief Helper to release the channel grant for the destination ID.
* @param dstId Destination Address.
* @param releaseAll Flag indicating all channel grants should be released.
* @param noLock Flag indicating no mutex lock operation should be performed while releasing.
* @returns bool True, if channel grant was released, otherwise false.
*/
bool releaseGrant(uint32_t dstId, bool releaseAll, bool noLock = false) override;
bool releaseGrant(uint32_t dstId, bool releaseAll) override;
/**
* @brief Helper to determine if the channel number is busy.
* @param chNo Channel Number.
@ -111,7 +110,7 @@ namespace dmr
uint8_t getAvailableSlotForChannel(uint32_t chNo) const;
protected:
std::unordered_map<uint32_t, std::tuple<uint32_t, uint8_t>> m_grantChSlotTable;
concurrent::unordered_map<uint32_t, std::tuple<uint32_t, uint8_t>> m_grantChSlotTable;
uint32_t m_tsccChNo;
uint8_t m_tsccSlot;

@ -31,6 +31,7 @@ using namespace network::udp;
// Constants
// ---------------------------------------------------------------------------
#define MAX_THREAD_CNT 4U
#define RTP_END_OF_CALL_SEQ 65535
const uint32_t BUFFER_LENGTH = 2000U;
@ -80,6 +81,8 @@ V24UDPPort::V24UDPPort(uint32_t peerId, const std::string& address, uint16_t mod
m_fscState(CS_NOT_CONNECTED),
m_modemState(STATE_P25),
m_tx(false),
m_ctrlThreadPool(MAX_THREAD_CNT, "v24cc"),
m_vcThreadPool(MAX_THREAD_CNT, "v24vc"),
m_debug(debug)
{
assert(peerId > 0U);
@ -205,6 +208,9 @@ bool V24UDPPort::openFSC()
return false;
}
m_ctrlThreadPool.start();
m_vcThreadPool.start();
if (m_controlSocket != nullptr) {
return m_controlSocket->open(m_controlAddr);
}
@ -219,6 +225,8 @@ bool V24UDPPort::open()
if (m_controlSocket != nullptr) {
return true; // FSC mode always returns that the port was opened
} else {
m_vcThreadPool.start();
if (m_addrLen == 0U) {
LogError(LOG_NET, "Unable to resolve the address of the modem");
return false;
@ -317,6 +325,12 @@ void V24UDPPort::closeFSC()
Thread::sleep(500U);
}
m_ctrlThreadPool.stop();
m_ctrlThreadPool.wait();
m_vcThreadPool.stop();
m_vcThreadPool.wait();
m_controlSocket->close();
}
}
@ -325,6 +339,9 @@ void V24UDPPort::closeFSC()
void V24UDPPort::close()
{
m_vcThreadPool.stop();
m_vcThreadPool.wait();
if (m_socket != nullptr)
m_socket->close();
}
@ -348,6 +365,8 @@ void V24UDPPort::processCtrlNetwork()
Utils::dump(1U, "FSC Control Network Message", buffer.get(), length);
V24PacketRequest* req = new V24PacketRequest();
req->obj = this;
req->address = address;
req->addrLen = addrLen;
@ -355,30 +374,28 @@ void V24UDPPort::processCtrlNetwork()
req->buffer = new uint8_t[length];
::memcpy(req->buffer, buffer.get(), length);
if (!Thread::runAsThread(this, threadedCtrlNetworkRx, req)) {
// enqueue the task
if (!m_ctrlThreadPool.enqueue(new_pooltask(taskCtrlNetworkRx, req))) {
LogError(LOG_NET, "Failed to task enqueue control network packet request, %s:%u",
udp::Socket::address(address).c_str(), udp::Socket::port(address));
if (req != nullptr) {
if (req->buffer != nullptr)
delete[] req->buffer;
delete req;
return;
}
}
}
}
/* Process a data frames from the network. */
void* V24UDPPort::threadedCtrlNetworkRx(void* arg)
void V24UDPPort::taskCtrlNetworkRx(V24PacketRequest* req)
{
V24PacketRequest* req = (V24PacketRequest*)arg;
if (req != nullptr) {
#if defined(_WIN32)
::CloseHandle(req->thread);
#else
::pthread_detach(req->thread);
#endif // defined(_WIN32)
V24UDPPort* network = static_cast<V24UDPPort*>(req->obj);
if (network == nullptr) {
delete req;
return nullptr;
return;
}
if (req->length > 0) {
@ -625,8 +642,6 @@ void* V24UDPPort::threadedCtrlNetworkRx(void* arg)
delete[] req->buffer;
delete req;
}
return nullptr;
}
/* Process voice conveyance frames from the network. */
@ -652,6 +667,8 @@ void V24UDPPort::processVCNetwork()
Utils::dump("!!! Rx Incoming DFSI UDP", data, ret);
V24PacketRequest* req = new V24PacketRequest();
req->obj = this;
req->address = addr;
req->addrLen = addrLen;
@ -672,10 +689,15 @@ void V24UDPPort::processVCNetwork()
::memcpy(req->buffer, data + RTP_HEADER_LENGTH_BYTES, req->length);
if (!Thread::runAsThread(this, threadedVCNetworkRx, req)) {
// enqueue the task
if (!m_vcThreadPool.enqueue(new_pooltask(taskVCNetworkRx, req))) {
LogError(LOG_NET, "Failed to task enqueue voice network packet request, %s:%u",
udp::Socket::address(addr).c_str(), udp::Socket::port(addr));
if (req != nullptr) {
if (req->buffer != nullptr)
delete[] req->buffer;
delete req;
return;
}
}
}
}
@ -684,20 +706,13 @@ void V24UDPPort::processVCNetwork()
/* Process a data frames from the network. */
void* V24UDPPort::threadedVCNetworkRx(void* arg)
void V24UDPPort::taskVCNetworkRx(V24PacketRequest* req)
{
V24PacketRequest* req = (V24PacketRequest*)arg;
if (req != nullptr) {
#if defined(_WIN32)
::CloseHandle(req->thread);
#else
::pthread_detach(req->thread);
#endif // defined(_WIN32)
V24UDPPort* network = static_cast<V24UDPPort*>(req->obj);
if (network == nullptr) {
delete req;
return nullptr;
return;
}
if (req->length > 0) {
@ -726,8 +741,6 @@ void* V24UDPPort::threadedVCNetworkRx(void* arg)
delete[] req->buffer;
delete req;
}
return nullptr;
}
/* Internal helper to setup the local voice channel port. */

@ -26,7 +26,7 @@
#include "common/network/RTPHeader.h"
#include "common/RingBuffer.h"
#include "common/Timer.h"
#include "common/Thread.h"
#include "common/ThreadPool.h"
#include "modem/port/IModemPort.h"
#include <string>
@ -182,6 +182,9 @@ namespace modem
uint8_t m_modemState;
bool m_tx;
ThreadPool m_ctrlThreadPool;
ThreadPool m_vcThreadPool;
bool m_debug;
static std::mutex m_bufferMutex;
@ -193,10 +196,9 @@ namespace modem
/**
* @brief Entry point to process a given network packet.
* @param arg Instance of the NetPacketRequest structure.
* @returns void* (Ignore)
* @param arg Instance of the V24PacketRequest structure.
*/
static void* threadedCtrlNetworkRx(void* arg);
static void taskCtrlNetworkRx(V24PacketRequest* req);
/**
* @brief Process voice conveyance frames from the network.
@ -205,10 +207,9 @@ namespace modem
/**
* @brief Entry point to process a given network packet.
* @param arg Instance of the NetPacketRequest structure.
* @returns void* (Ignore)
* @param req Instance of the V24PacketRequest structure.
*/
static void* threadedVCNetworkRx(void* arg);
static void taskVCNetworkRx(V24PacketRequest* req);
/**
* @brief Internal helper to setup the local voice channel port.

@ -195,7 +195,12 @@ RESTAPI::RESTAPI(const std::string& address, uint16_t port, const std::string& p
/* Finalizes a instance of the RESTAPI class. */
RESTAPI::~RESTAPI() = default;
RESTAPI::~RESTAPI()
{
if (m_passwordHash != nullptr) {
delete[] m_passwordHash;
}
}
/* Sets the instances of the Radio ID and Talkgroup ID lookup tables. */
@ -934,11 +939,13 @@ void RESTAPI::restAPI_GetReleaseGrants(const HTTPPayload& request, HTTPPayload&
}
if (m_p25 != nullptr) {
m_p25->affiliations().releaseGrant(0, true);
if (m_p25->affiliations() != nullptr)
m_p25->affiliations()->releaseGrant(0, true);
}
if (m_nxdn != nullptr) {
m_nxdn->affiliations().releaseGrant(0, true);
if (m_nxdn->affiliations() != nullptr)
m_nxdn->affiliations()->releaseGrant(0, true);
}
}
@ -957,11 +964,13 @@ void RESTAPI::restAPI_GetReleaseAffs(const HTTPPayload& request, HTTPPayload& re
}
if (m_p25 != nullptr) {
m_p25->affiliations().clearGroupAff(0, true);
if (m_p25->affiliations() != nullptr)
m_p25->affiliations()->clearGroupAff(0, true);
}
if (m_nxdn != nullptr) {
m_nxdn->affiliations().clearGroupAff(0, true);
if (m_nxdn->affiliations() != nullptr)
m_nxdn->affiliations()->clearGroupAff(0, true);
}
}
@ -1620,7 +1629,8 @@ void RESTAPI::restAPI_GetP25AffList(const HTTPPayload& request, HTTPPayload& rep
setResponseDefaultStatus(response);
json::array affs = json::array();
std::unordered_map<uint32_t, uint32_t> affTable = m_p25->affiliations().grpAffTable();
if (m_p25->affiliations() != nullptr) {
std::unordered_map<uint32_t, uint32_t> affTable = m_p25->affiliations()->grpAffTable();
if (affTable.size() > 0) {
for (auto entry : affTable) {
uint32_t srcId = entry.first;
@ -1633,6 +1643,7 @@ void RESTAPI::restAPI_GetP25AffList(const HTTPPayload& request, HTTPPayload& rep
affs.push_back(json::value(aff));
}
}
}
response["affiliations"].set<json::array>(affs);
reply.payload(response);
@ -1787,7 +1798,8 @@ void RESTAPI::restAPI_GetNXDNAffList(const HTTPPayload& request, HTTPPayload& re
setResponseDefaultStatus(response);
json::array affs = json::array();
std::unordered_map<uint32_t, uint32_t> affTable = m_nxdn->affiliations().grpAffTable();
if (m_nxdn->affiliations() != nullptr) {
std::unordered_map<uint32_t, uint32_t> affTable = m_nxdn->affiliations()->grpAffTable();
if (affTable.size() > 0) {
for (auto entry : affTable) {
uint32_t srcId = entry.first;
@ -1800,6 +1812,7 @@ void RESTAPI::restAPI_GetNXDNAffList(const HTTPPayload& request, HTTPPayload& re
affs.push_back(json::value(aff));
}
}
}
response["affiliations"].set<json::array>(affs);
reply.payload(response);

@ -42,6 +42,8 @@
#define RPC_PERMIT_NXDN_TG 0x0003U
#define RPC_DMR_TSCC_PAYLOAD_ACT 0x0010U
#define RPC_ACTIVE_P25_TG 0x0020U
#define RPC_CLEAR_ACTIVE_P25_TG 0x0021U
/** @} */

@ -80,7 +80,7 @@ Control::Control(bool authoritative, uint32_t ran, uint32_t callHang, uint32_t q
m_idenTable(idenTable),
m_ridLookup(ridLookup),
m_tidLookup(tidLookup),
m_affiliations("NXDN Affiliations", chLookup, verbose),
m_affiliations(nullptr),
m_controlChData(),
m_idenEntry(),
m_txImmQueue(queueSize, "NXDN Imm Frame"),
@ -125,6 +125,8 @@ Control::Control(bool authoritative, uint32_t ran, uint32_t callHang, uint32_t q
assert(idenTable != nullptr);
assert(rssiMapper != nullptr);
m_affiliations = new lookups::AffiliationLookup("NXDN Affiliations", chLookup, verbose);
m_interval.start();
acl::AccessControl::init(m_ridLookup, m_tidLookup);
@ -146,6 +148,10 @@ Control::Control(bool authoritative, uint32_t ran, uint32_t callHang, uint32_t q
Control::~Control()
{
if (m_affiliations != nullptr) {
delete m_affiliations;
}
if (m_voice != nullptr) {
delete m_voice;
}
@ -262,13 +268,13 @@ void Control::setOptions(yaml::Node& conf, bool supervisor, const std::string cw
m_controlChData = controlChData;
bool disableUnitRegTimeout = nxdnProtocol["disableUnitRegTimeout"].as<bool>(false);
m_affiliations.setDisableUnitRegTimeout(disableUnitRegTimeout);
m_affiliations->setDisableUnitRegTimeout(disableUnitRegTimeout);
// set the grant release callback
m_affiliations.setReleaseGrantCallback([=](uint32_t chNo, uint32_t dstId, uint8_t slot) {
m_affiliations->setReleaseGrantCallback([=](uint32_t chNo, uint32_t dstId, uint8_t slot) {
// callback REST API to clear TG permit for the granted TG on the specified voice channel
if (m_authoritative && m_supervisor) {
::lookups::VoiceChData voiceChData = m_affiliations.rfCh()->getRFChData(chNo);
::lookups::VoiceChData voiceChData = m_affiliations->rfCh()->getRFChData(chNo);
if (voiceChData.isValidCh() && !voiceChData.address().empty() && voiceChData.port() > 0 &&
chNo != m_siteData.channelNo()) {
json::object req = json::object();
@ -284,7 +290,7 @@ void Control::setOptions(yaml::Node& conf, bool supervisor, const std::string cw
});
// set the unit deregistration callback
m_affiliations.setUnitDeregCallback([=](uint32_t srcId, bool automatic) {
m_affiliations->setUnitDeregCallback([=](uint32_t srcId, bool automatic) {
if (m_network != nullptr)
m_network->announceUnitDeregistration(srcId);
});
@ -609,8 +615,8 @@ void Control::clock()
if (m_adjSiteUpdate.isRunning() && m_adjSiteUpdate.hasExpired()) {
if (m_rfState == RS_RF_LISTENING && m_netState == RS_NET_IDLE) {
if (m_network != nullptr) {
if (m_affiliations.grpAffSize() > 0) {
auto affs = m_affiliations.grpAffTable();
if (m_affiliations->grpAffSize() > 0) {
auto affs = m_affiliations->grpAffTable();
m_network->announceAffiliationUpdate(affs);
}
}
@ -692,7 +698,7 @@ void Control::clock()
m_networkWatchdog.stop();
if (m_enableControl) {
m_affiliations.releaseGrant(m_netLC.getDstId(), false);
m_affiliations->releaseGrant(m_netLC.getDstId(), false);
}
if (m_dedicatedControl) {
@ -726,7 +732,7 @@ void Control::clockSiteData(uint32_t ms)
{
if (m_enableControl) {
// clock all the grant timers
m_affiliations.clock(ms);
m_affiliations->clock(ms);
}
}
@ -1000,7 +1006,7 @@ void Control::processFrameLoss()
LogMessage(LOG_RF, "NXDN, " NXDN_RTCH_MSG_TYPE_TX_REL ", total frames: %d, bits: %d, undecodable LC: %d, errors: %d, BER: %.4f%%",
m_voice->m_rfFrames, m_voice->m_rfBits, m_voice->m_rfUndecodableLC, m_voice->m_rfErrs, float(m_voice->m_rfErrs * 100U) / float(m_voice->m_rfBits));
m_affiliations.releaseGrant(m_rfLC.getDstId(), false);
m_affiliations->releaseGrant(m_rfLC.getDstId(), false);
if (m_notifyCC) {
notifyCC_ReleaseGrant(m_rfLC.getDstId());
}
@ -1027,8 +1033,8 @@ void Control::processInCallCtrl(network::NET_ICC::ENUM command, uint32_t dstId)
{
if (m_rfState == RS_RF_AUDIO && m_rfLC.getDstId() == dstId) {
LogWarning(LOG_P25, "network requested in-call traffic reject, dstId = %u", dstId);
if (m_affiliations.isGranted(dstId)) {
m_affiliations.releaseGrant(dstId, false);
if (m_affiliations->isGranted(dstId)) {
m_affiliations->releaseGrant(dstId, false);
if (!m_enableControl) {
notifyCC_ReleaseGrant(dstId);
}
@ -1185,16 +1191,16 @@ void Control::RPC_releaseGrantTG(json::object& req, json::object& reply)
LogMessage(LOG_P25, "VC request, release TG grant, dstId = %u", dstId);
}
if (m_affiliations.isGranted(dstId)) {
uint32_t chNo = m_affiliations.getGrantedCh(dstId);
uint32_t srcId = m_affiliations.getGrantedSrcId(dstId);
::lookups::VoiceChData voiceCh = m_affiliations.rfCh()->getRFChData(chNo);
if (m_affiliations->isGranted(dstId)) {
uint32_t chNo = m_affiliations->getGrantedCh(dstId);
uint32_t srcId = m_affiliations->getGrantedSrcId(dstId);
::lookups::VoiceChData voiceCh = m_affiliations->rfCh()->getRFChData(chNo);
if (m_verbose) {
LogMessage(LOG_P25, "VC %s:%u, TG grant released, srcId = %u, dstId = %u, chId = %u, chNo = %u", voiceCh.address().c_str(), voiceCh.port(), srcId, dstId, voiceCh.chId(), chNo);
}
m_affiliations.releaseGrant(dstId, false);
m_affiliations->releaseGrant(dstId, false);
}
}
@ -1224,16 +1230,16 @@ void Control::RPC_touchGrantTG(json::object& req, json::object& reply)
// LogDebugEx(LOG_NXDN, "Control::RPC_touchGrantTG()", "callback, dstId = %u", dstId);
if (m_affiliations.isGranted(dstId)) {
uint32_t chNo = m_affiliations.getGrantedCh(dstId);
uint32_t srcId = m_affiliations.getGrantedSrcId(dstId);
::lookups::VoiceChData voiceCh = m_affiliations.rfCh()->getRFChData(chNo);
if (m_affiliations->isGranted(dstId)) {
uint32_t chNo = m_affiliations->getGrantedCh(dstId);
uint32_t srcId = m_affiliations->getGrantedSrcId(dstId);
::lookups::VoiceChData voiceCh = m_affiliations->rfCh()->getRFChData(chNo);
if (m_verbose) {
LogMessage(LOG_P25, "VC %s:%u, call in progress, srcId = %u, dstId = %u, chId = %u, chNo = %u", voiceCh.address().c_str(), voiceCh.port(), srcId, dstId, voiceCh.chId(), chNo);
}
m_affiliations.touchGrant(dstId);
m_affiliations->touchGrant(dstId);
}
}

@ -201,7 +201,7 @@ namespace nxdn
* @brief Gets instance of the AffiliationLookup class.
* @returns AffiliationLookup Instance of the AffiliationLookup class.
*/
lookups::AffiliationLookup affiliations() { return m_affiliations; }
lookups::AffiliationLookup* affiliations() { return m_affiliations; }
/**
* @brief Returns the current operating RF state of the NXDN controller.
@ -291,7 +291,7 @@ namespace nxdn
lookups::IdenTableLookup* m_idenTable;
lookups::RadioIdLookup* m_ridLookup;
lookups::TalkgroupRulesLookup* m_tidLookup;
lookups::AffiliationLookup m_affiliations;
lookups::AffiliationLookup* m_affiliations;
::lookups::VoiceChData m_controlChData;
lookups::IdenTable m_idenEntry;

@ -71,7 +71,7 @@ using namespace nxdn::packet;
// Verify the source RID is registered.
#define VERIFY_SRCID_REG(_PCKT_STR, _PCKT, _SRCID, _RSN) \
if (!m_nxdn->m_affiliations.isUnitReg(_SRCID) && m_verifyReg) { \
if (!m_nxdn->m_affiliations->isUnitReg(_SRCID) && m_verifyReg) { \
LogWarning(LOG_RF, "NXDN, %s denial, RID not registered, srcId = %u", _PCKT_STR.c_str(), _SRCID); \
writeRF_Message_Deny(0U, _SRCID, _RSN, _PCKT); \
m_nxdn->m_rfState = RS_RF_REJECTED; \
@ -80,7 +80,7 @@ using namespace nxdn::packet;
// Verify the source RID is affiliated.
#define VERIFY_SRCID_AFF(_PCKT_STR, _PCKT, _SRCID, _DSTID, _RSN) \
if (!m_nxdn->m_affiliations.isGroupAff(_SRCID, _DSTID) && m_verifyAff) { \
if (!m_nxdn->m_affiliations->isGroupAff(_SRCID, _DSTID) && m_verifyAff) { \
LogWarning(LOG_RF, "NXDN, %s denial, RID not affiliated to TGID, srcId = %u, dstId = %u", _PCKT_STR.c_str(), _SRCID, _DSTID); \
writeRF_Message_Deny(0U, _SRCID, _RSN, _PCKT); \
m_nxdn->m_rfState = RS_RF_REJECTED; \
@ -156,7 +156,7 @@ bool ControlSignaling::process(FuncChannelType::E fct, ChOption::E option, uint8
uint16_t srcId = rcch->getSrcId();
uint16_t dstId = rcch->getDstId();
m_nxdn->m_affiliations.touchUnitReg(srcId);
m_nxdn->m_affiliations->touchUnitReg(srcId);
switch (rcch->getMessageType()) {
case MessageType::RTCH_VCALL:
@ -250,7 +250,7 @@ bool ControlSignaling::processNetwork(FuncChannelType::E fct, ChOption::E option
case MessageType::RTCH_VCALL:
{
if (m_nxdn->m_dedicatedControl) {
if (!m_nxdn->m_affiliations.isGranted(dstId)) {
if (!m_nxdn->m_affiliations->isGranted(dstId)) {
if (m_verbose) {
LogMessage(LOG_NET, "NXDN, %s, emerg = %u, encrypt = %u, prio = %u, chNo = %u, srcId = %u, dstId = %u",
rcch->toString().c_str(), rcch->getEmergency(), rcch->getEncrypted(), rcch->getPriority(), rcch->getGrpVchNo(), srcId, dstId);
@ -473,12 +473,12 @@ bool ControlSignaling::writeRF_Message_Grant(uint32_t srcId, uint32_t dstId, uin
}
}
if (!m_nxdn->m_affiliations.isGranted(dstId)) {
if (!m_nxdn->m_affiliations->isGranted(dstId)) {
if (grp && !m_nxdn->m_ignoreAffiliationCheck) {
// is this an affiliation required group?
::lookups::TalkgroupRuleGroupVoice tid = m_nxdn->m_tidLookup->find(dstId);
if (tid.config().affiliated()) {
if (!m_nxdn->m_affiliations.hasGroupAff(dstId)) {
if (!m_nxdn->m_affiliations->hasGroupAff(dstId)) {
LogWarning(LOG_RF, "NXDN, %s ignored, no group affiliations, dstId = %u", rcch->toString().c_str(), dstId);
return false;
}
@ -487,13 +487,13 @@ bool ControlSignaling::writeRF_Message_Grant(uint32_t srcId, uint32_t dstId, uin
if (!grp && !m_nxdn->m_ignoreAffiliationCheck) {
// is this the target registered?
if (!m_nxdn->m_affiliations.isUnitReg(dstId)) {
if (!m_nxdn->m_affiliations->isUnitReg(dstId)) {
LogWarning(LOG_RF, "NXDN, %s ignored, no unit registration, dstId = %u", rcch->toString().c_str(), dstId);
return false;
}
}
if (!m_nxdn->m_affiliations.rfCh()->isRFChAvailable()) {
if (!m_nxdn->m_affiliations->rfCh()->isRFChAvailable()) {
if (grp) {
if (!net) {
LogWarning(LOG_RF, "NXDN, %s queued, no channels available, dstId = %u", rcch->toString().c_str(), dstId);
@ -518,8 +518,8 @@ bool ControlSignaling::writeRF_Message_Grant(uint32_t srcId, uint32_t dstId, uin
}
}
else {
if (m_nxdn->m_affiliations.grantCh(dstId, srcId, GRANT_TIMER_TIMEOUT, grp, net)) {
chNo = m_nxdn->m_affiliations.getGrantedCh(dstId);
if (m_nxdn->m_affiliations->grantCh(dstId, srcId, GRANT_TIMER_TIMEOUT, grp, net)) {
chNo = m_nxdn->m_affiliations->getGrantedCh(dstId);
}
}
}
@ -527,7 +527,7 @@ bool ControlSignaling::writeRF_Message_Grant(uint32_t srcId, uint32_t dstId, uin
if (!m_disableGrantSrcIdCheck && !net) {
// do collision check between grants to see if a SU is attempting a "grant retry" or if this is a
// different source from the original grant
uint32_t grantedSrcId = m_nxdn->m_affiliations.getGrantedSrcId(dstId);
uint32_t grantedSrcId = m_nxdn->m_affiliations->getGrantedSrcId(dstId);
if (srcId != grantedSrcId) {
if (!net) {
LogWarning(LOG_RF, "NXDN, %s denied, traffic in progress, dstId = %u", rcch->toString().c_str(), dstId);
@ -541,14 +541,14 @@ bool ControlSignaling::writeRF_Message_Grant(uint32_t srcId, uint32_t dstId, uin
}
}
chNo = m_nxdn->m_affiliations.getGrantedCh(dstId);
m_nxdn->m_affiliations.touchGrant(dstId);
chNo = m_nxdn->m_affiliations->getGrantedCh(dstId);
m_nxdn->m_affiliations->touchGrant(dstId);
}
}
else {
if (m_nxdn->m_affiliations.isGranted(dstId)) {
chNo = m_nxdn->m_affiliations.getGrantedCh(dstId);
m_nxdn->m_affiliations.touchGrant(dstId);
if (m_nxdn->m_affiliations->isGranted(dstId)) {
chNo = m_nxdn->m_affiliations->getGrantedCh(dstId);
m_nxdn->m_affiliations->touchGrant(dstId);
}
else {
return false;
@ -568,7 +568,7 @@ bool ControlSignaling::writeRF_Message_Grant(uint32_t srcId, uint32_t dstId, uin
// callback RPC to permit the granted TG on the specified voice channel
if (m_nxdn->m_authoritative && m_nxdn->m_supervisor) {
::lookups::VoiceChData voiceChData = m_nxdn->m_affiliations.rfCh()->getRFChData(chNo);
::lookups::VoiceChData voiceChData = m_nxdn->m_affiliations->rfCh()->getRFChData(chNo);
if (voiceChData.isValidCh() && !voiceChData.address().empty() && voiceChData.port() > 0 &&
chNo != m_nxdn->m_siteData.channelNo()) {
json::object req = json::object();
@ -596,7 +596,7 @@ bool ControlSignaling::writeRF_Message_Grant(uint32_t srcId, uint32_t dstId, uin
if (requestFailed) {
::LogError((net) ? LOG_NET : LOG_RF, "NXDN, %s, failed to permit TG for use, chNo = %u", rcch->toString().c_str(), chNo);
m_nxdn->m_affiliations.releaseGrant(dstId, false);
m_nxdn->m_affiliations->releaseGrant(dstId, false);
if (!net) {
writeRF_Message_Deny(0U, srcId, CauseResponse::VD_QUE_GRP_BUSY, MessageType::RTCH_VCALL);
m_nxdn->m_rfState = RS_RF_REJECTED;
@ -680,7 +680,7 @@ bool ControlSignaling::writeRF_Message_Grp_Reg_Rsp(uint32_t srcId, uint32_t dstI
}
// validate the source RID is registered
if (!m_nxdn->m_affiliations.isUnitReg(srcId) && m_verifyReg) {
if (!m_nxdn->m_affiliations->isUnitReg(srcId) && m_verifyReg) {
LogWarning(LOG_RF, "NXDN, %s denial, RID not registered, srcId = %u", rcch->toString().c_str(), srcId);
::ActivityLog("NXDN", true, "group affiliation request from %u to %s %u denied", srcId, "TG ", dstId);
rcch->setCauseResponse(CauseResponse::MM_REG_REFUSED);
@ -705,7 +705,7 @@ bool ControlSignaling::writeRF_Message_Grp_Reg_Rsp(uint32_t srcId, uint32_t dstI
ret = true;
// update dynamic affiliation table
m_nxdn->m_affiliations.groupAff(srcId, dstId);
m_nxdn->m_affiliations->groupAff(srcId, dstId);
if (m_nxdn->m_network != nullptr)
m_nxdn->m_network->announceGroupAffiliation(srcId, dstId);
@ -757,8 +757,8 @@ void ControlSignaling::writeRF_Message_U_Reg_Rsp(uint32_t srcId, uint32_t dstId,
::ActivityLog("NXDN", true, "unit registration request from %u", srcId);
// update dynamic unit registration table
if (!m_nxdn->m_affiliations.isUnitReg(srcId)) {
m_nxdn->m_affiliations.unitReg(srcId);
if (!m_nxdn->m_affiliations->isUnitReg(srcId)) {
m_nxdn->m_affiliations->unitReg(srcId);
}
if (m_nxdn->m_network != nullptr)

@ -59,7 +59,7 @@ using namespace nxdn::packet;
} \
\
if (m_nxdn->m_enableControl && _DST_ID == m_nxdn->m_netLastDstId) { \
if (m_nxdn->m_affiliations.isNetGranted(_DST_ID)) { \
if (m_nxdn->m_affiliations->isNetGranted(_DST_ID)) { \
LogWarning(LOG_RF, "Traffic collision detect, preempting new RF traffic to existing granted network traffic (Are we in a voting condition?), rfSrcId = %u, rfDstId = %u, netSrcId = %u, netDstId = %u", _SRC_ID, _DST_ID, \
m_nxdn->m_netLC.getSrcId(), m_nxdn->m_netLastDstId); \
resetRF(); \

@ -42,6 +42,7 @@ const uint32_t MAX_PREAMBLE_TDU_CNT = 64U;
// ---------------------------------------------------------------------------
std::mutex Control::m_queueLock;
std::mutex Control::m_activeTGLock;
// ---------------------------------------------------------------------------
// Public Class Members
@ -82,9 +83,10 @@ Control::Control(bool authoritative, uint32_t nac, uint32_t callHang, uint32_t q
m_idenTable(idenTable),
m_ridLookup(ridLookup),
m_tidLookup(tidLookup),
m_affiliations(this, chLookup, verbose),
m_affiliations(nullptr),
m_controlChData(),
m_idenEntry(),
m_activeTG(),
m_txImmQueue(queueSize, "P25 Imm Frame"),
m_txQueue(queueSize, "P25 Frame"),
m_rfState(RS_RF_LISTENING),
@ -105,6 +107,7 @@ Control::Control(bool authoritative, uint32_t nac, uint32_t callHang, uint32_t q
m_netTGHang(1000U, 2U),
m_networkWatchdog(1000U, 0U, 1500U),
m_adjSiteUpdate(1000U, 75U),
m_activeTGUpdate(1000U, 5U),
m_ccPacketInterval(1000U, 0U, 10U),
m_interval(),
m_hangCount(3U * 8U),
@ -126,6 +129,7 @@ Control::Control(bool authoritative, uint32_t nac, uint32_t callHang, uint32_t q
m_minRSSI(0U),
m_aveRSSI(0U),
m_rssiCount(0U),
m_ccNotifyActiveTG(false),
m_notifyCC(true),
m_ccDebug(debug),
m_verbose(verbose),
@ -137,6 +141,8 @@ Control::Control(bool authoritative, uint32_t nac, uint32_t callHang, uint32_t q
assert(idenTable != nullptr);
assert(rssiMapper != nullptr);
m_affiliations = new lookups::P25AffiliationLookup(this, chLookup, verbose);
// bryanb: this is a hacky check to see if the modem is a ModemV24 or not...
modem::ModemV24* modemV24 = dynamic_cast<modem::ModemV24*>(modem);
if (modemV24 != nullptr)
@ -163,6 +169,8 @@ Control::Control(bool authoritative, uint32_t nac, uint32_t callHang, uint32_t q
// register RPC handlers
g_RPC->registerHandler(RPC_PERMIT_P25_TG, RPC_FUNC_BIND(Control::RPC_permittedTG, this));
g_RPC->registerHandler(RPC_ACTIVE_P25_TG, RPC_FUNC_BIND(Control::RPC_activeTG, this));
g_RPC->registerHandler(RPC_CLEAR_ACTIVE_P25_TG, RPC_FUNC_BIND(Control::RPC_clearActiveTG, this));
g_RPC->registerHandler(RPC_RELEASE_P25_TG, RPC_FUNC_BIND(Control::RPC_releaseGrantTG, this));
g_RPC->registerHandler(RPC_TOUCH_P25_TG, RPC_FUNC_BIND(Control::RPC_touchGrantTG, this));
}
@ -171,6 +179,10 @@ Control::Control(bool authoritative, uint32_t nac, uint32_t callHang, uint32_t q
Control::~Control()
{
if (m_affiliations != nullptr) {
delete m_affiliations;
}
if (m_voice != nullptr) {
delete m_voice;
}
@ -313,6 +325,8 @@ void Control::setOptions(yaml::Node& conf, bool supervisor, const std::string cw
m_control->m_redundantGrant = control["redundantGrantTransmit"].as<bool>(false);
m_ccDebug = control["debug"].as<bool>(false);
m_ccNotifyActiveTG = control["notifyActiveTG"].as<bool>(true);
m_allowExplicitSourceId = p25Protocol["allowExplicitSourceId"].as<bool>(true);
m_convNetGrantDemand = p25Protocol["convNetGrantDemand"].as<bool>(false);
@ -410,18 +424,18 @@ void Control::setOptions(yaml::Node& conf, bool supervisor, const std::string cw
}
}
m_siteData.setChCnt((uint8_t)m_affiliations.rfCh()->rfChSize());
m_siteData.setChCnt((uint8_t)m_affiliations->rfCh()->rfChSize());
m_controlChData = controlChData;
bool disableUnitRegTimeout = p25Protocol["disableUnitRegTimeout"].as<bool>(false);
m_affiliations.setDisableUnitRegTimeout(disableUnitRegTimeout);
m_affiliations->setDisableUnitRegTimeout(disableUnitRegTimeout);
// set the grant release callback
m_affiliations.setReleaseGrantCallback([=](uint32_t chNo, uint32_t dstId, uint8_t slot) {
m_affiliations->setReleaseGrantCallback([=](uint32_t chNo, uint32_t dstId, uint8_t slot) {
// callback REST API to clear TG permit for the granted TG on the specified voice channel
if (m_authoritative && m_supervisor) {
::lookups::VoiceChData voiceChData = m_affiliations.rfCh()->getRFChData(chNo);
::lookups::VoiceChData voiceChData = m_affiliations->rfCh()->getRFChData(chNo);
if (voiceChData.isValidCh() && !voiceChData.address().empty() && voiceChData.port() > 0 &&
chNo != m_siteData.channelNo()) {
json::object req = json::object();
@ -437,7 +451,7 @@ void Control::setOptions(yaml::Node& conf, bool supervisor, const std::string cw
});
// set the unit deregistration callback
m_affiliations.setUnitDeregCallback([=](uint32_t srcId, bool automatic) {
m_affiliations->setUnitDeregCallback([=](uint32_t srcId, bool automatic) {
if (m_network != nullptr)
m_network->announceUnitDeregistration(srcId);
@ -511,6 +525,8 @@ void Control::setOptions(yaml::Node& conf, bool supervisor, const std::string cw
LogInfo(" Conventional Network Grant Demand: %s", m_convNetGrantDemand ? "yes" : "no");
LogInfo(" Demand Unit Registration for Refused Affiliation: %s", m_demandUnitRegForRefusedAff ? "yes" : "no");
LogInfo(" Notify VCs of Active TGs: %s", m_ccNotifyActiveTG ? "yes" : "no");
if (disableUnitRegTimeout) {
LogInfo(" Disable Unit Registration Timeout: yes");
}
@ -677,7 +693,7 @@ bool Control::processFrame(uint8_t* data, uint32_t len)
if (!m_dedicatedControl || m_control->m_convFallback)
ret = m_voice->process(data, len);
else {
if (m_voiceOnControl && m_affiliations.isChBusy(m_siteData.channelNo()))
if (m_voiceOnControl && m_affiliations->isChBusy(m_siteData.channelNo()))
ret = m_voice->process(data, len);
}
break;
@ -941,7 +957,7 @@ void Control::clock()
}
m_networkWatchdog.stop();
m_affiliations.releaseGrant(m_voice->m_netLC.getDstId(), false);
m_affiliations->releaseGrant(m_voice->m_netLC.getDstId(), false);
if (m_dedicatedControl) {
if (m_network != nullptr)
@ -980,7 +996,7 @@ void Control::clockSiteData(uint32_t ms)
{
if (m_enableControl) {
// clock all the grant timers
m_affiliations.clock(ms);
m_affiliations->clock(ms);
}
if (m_control != nullptr) {
@ -995,8 +1011,8 @@ void Control::clockSiteData(uint32_t ms)
if (m_rfState == RS_RF_LISTENING && m_netState == RS_NET_IDLE) {
m_control->writeAdjSSNetwork();
if (m_network != nullptr) {
if (m_affiliations.grpAffSize() > 0) {
auto affs = m_affiliations.grpAffTable();
if (m_affiliations->grpAffSize() > 0) {
auto affs = m_affiliations->grpAffTable();
m_network->announceAffiliationUpdate(affs);
}
}
@ -1049,6 +1065,91 @@ void Control::clockSiteData(uint32_t ms)
m_control->m_adjSiteUpdateTimer.setTimeout(m_control->m_adjSiteUpdateInterval);
m_control->m_adjSiteUpdateTimer.start();
}
if (m_ccNotifyActiveTG) {
if (!m_activeTGUpdate.isRunning()) {
m_activeTGUpdate.start();
}
m_activeTGUpdate.clock(ms);
if (m_activeTGUpdate.isRunning() && m_activeTGUpdate.hasExpired()) {
m_activeTGUpdate.start();
// do we have any granted channels?
if (m_affiliations->getGrantedRFChCnt() > 0U) {
uint8_t activeCnt = m_affiliations->getGrantedRFChCnt();
std::unordered_map<uint32_t, uint32_t> grantTable = m_affiliations->grantTable();
// iterate dynamic channel grant table entries
json::array active = json::array();
for (auto entry : grantTable) {
uint32_t dstId = entry.first;
active.push_back(json::value((double)dstId));
}
std::unordered_map<uint32_t, ::lookups::VoiceChData> voiceChs = m_affiliations->rfCh()->rfChDataTable();
for (auto entry : voiceChs) {
::lookups::VoiceChData voiceChData = entry.second;
// callback RPC to transmit active TG list to the voice channels
if (voiceChData.isValidCh() && !voiceChData.address().empty() && voiceChData.port() > 0) {
if (voiceChData.address() != "0.0.0.0") {
json::object req = json::object();
req["active"].set<json::array>(active);
g_RPC->req(RPC_ACTIVE_P25_TG, req, [=](json::object& req, json::object& reply) {
if (!req["status"].is<int>()) {
::LogError(LOG_P25, "failed to send active TG list to VC %s:%u, invalid RPC response", voiceChData.address().c_str(), voiceChData.port());
return;
}
int status = req["status"].get<int>();
if (status != network::NetRPC::OK) {
::LogError(LOG_P25, "failed to send active TG list to VC %s:%u", voiceChData.address().c_str(), voiceChData.port());
if (req["message"].is<std::string>()) {
std::string retMsg = req["message"].get<std::string>();
::LogError(LOG_P25, "RPC failed, %s", retMsg.c_str());
}
}
else
::LogMessage(LOG_P25, "VC %s:%u, active TG update, activeCnt = %u", voiceChData.address().c_str(), voiceChData.port(), activeCnt);
}, voiceChData.address(), voiceChData.port());
}
}
}
} else {
std::unordered_map<uint32_t, ::lookups::VoiceChData> voiceChs = m_affiliations->rfCh()->rfChDataTable();
for (auto entry : voiceChs) {
::lookups::VoiceChData voiceChData = entry.second;
// callback RPC to transmit active TG list to the voice channels
if (voiceChData.isValidCh() && !voiceChData.address().empty() && voiceChData.port() > 0) {
if (voiceChData.address() != "0.0.0.0") {
json::object req = json::object();
g_RPC->req(RPC_CLEAR_ACTIVE_P25_TG, req, [=](json::object& req, json::object& reply) {
if (!req["status"].is<int>()) {
::LogError(LOG_P25, "failed to send clear active TG list to VC %s:%u, invalid RPC response", voiceChData.address().c_str(), voiceChData.port());
return;
}
int status = req["status"].get<int>();
if (status != network::NetRPC::OK) {
::LogError(LOG_P25, "failed to send clear active TG list to VC %s:%u", voiceChData.address().c_str(), voiceChData.port());
if (req["message"].is<std::string>()) {
std::string retMsg = req["message"].get<std::string>();
::LogError(LOG_P25, "RPC failed, %s", retMsg.c_str());
}
}
else
::LogMessage(LOG_P25, "VC %s:%u, clear active TG update", voiceChData.address().c_str(), voiceChData.port());
}, voiceChData.address(), voiceChData.port());
}
}
}
}
}
}
}
}
}
@ -1486,7 +1587,7 @@ void Control::processFrameLoss()
LogMessage(LOG_RF, P25_TDU_STR ", total frames: %d, bits: %d, undecodable LC: %d, errors: %d, BER: %.4f%%",
m_voice->m_rfFrames, m_voice->m_rfBits, m_voice->m_rfUndecodableLC, m_voice->m_rfErrs, float(m_voice->m_rfErrs * 100U) / float(m_voice->m_rfBits));
m_affiliations.releaseGrant(m_voice->m_rfLC.getDstId(), false);
m_affiliations->releaseGrant(m_voice->m_rfLC.getDstId(), false);
if (!m_enableControl) {
notifyCC_ReleaseGrant(m_voice->m_rfLC.getDstId());
}
@ -1543,10 +1644,10 @@ void Control::processInCallCtrl(network::NET_ICC::ENUM command, uint32_t dstId)
{
if (m_rfState == RS_RF_AUDIO && m_voice->m_rfLC.getDstId() == dstId) {
LogWarning(LOG_P25, "network requested in-call traffic reject, dstId = %u", dstId);
if (m_affiliations.isGranted(dstId)) {
uint32_t srcId = m_affiliations.getGrantedSrcId(dstId);
if (m_affiliations->isGranted(dstId)) {
uint32_t srcId = m_affiliations->getGrantedSrcId(dstId);
m_affiliations.releaseGrant(dstId, false);
m_affiliations->releaseGrant(dstId, false);
if (!m_enableControl) {
notifyCC_ReleaseGrant(dstId);
}
@ -1823,6 +1924,47 @@ void Control::RPC_permittedTG(json::object& req, json::object& reply)
permittedTG(dstId, dataPermit);
}
/* (RPC Handler) Active TGID list from the authoritative CC host. */
void Control::RPC_activeTG(json::object& req, json::object& reply)
{
g_RPC->defaultResponse(reply, "OK", network::NetRPC::OK);
if (!req["active"].is<json::array>()) {
g_RPC->defaultResponse(reply, "\"active\" was not a valid JSON array", network::NetRPC::INVALID_ARGS);
return;
}
json::array active = req["active"].get<json::array>();
std::lock_guard<std::mutex> lock(m_activeTGLock);
m_activeTG.clear();
if (active.size() > 0) {
for (auto entry : active) {
if (!entry.is<uint32_t>()) {
g_RPC->defaultResponse(reply, "active TG was not a valid number", network::NetRPC::INVALID_ARGS);
continue;
}
m_activeTG.push_back(entry.get<uint32_t>());
}
}
::LogMessage(LOG_P25, "active TG update, activeCnt = %u", m_activeTG.size());
}
/* (RPC Handler) Clear active TGID list from the authoritative CC host. */
void Control::RPC_clearActiveTG(json::object& req, json::object& reply)
{
g_RPC->defaultResponse(reply, "OK", network::NetRPC::OK);
if (m_activeTG.size() > 0) {
std::lock_guard<std::mutex> lock(m_activeTGLock);
m_activeTG.clear();
}
}
/* (RPC Handler) Releases a granted TG. */
void Control::RPC_releaseGrantTG(json::object& req, json::object& reply)
@ -1853,16 +1995,16 @@ void Control::RPC_releaseGrantTG(json::object& req, json::object& reply)
LogMessage(LOG_P25, "VC request, release TG grant, dstId = %u", dstId);
}
if (m_affiliations.isGranted(dstId)) {
uint32_t chNo = m_affiliations.getGrantedCh(dstId);
uint32_t srcId = m_affiliations.getGrantedSrcId(dstId);
::lookups::VoiceChData voiceCh = m_affiliations.rfCh()->getRFChData(chNo);
if (m_affiliations->isGranted(dstId)) {
uint32_t chNo = m_affiliations->getGrantedCh(dstId);
uint32_t srcId = m_affiliations->getGrantedSrcId(dstId);
::lookups::VoiceChData voiceCh = m_affiliations->rfCh()->getRFChData(chNo);
if (m_verbose) {
LogMessage(LOG_P25, "VC %s:%u, TG grant released, srcId = %u, dstId = %u, chId = %u, chNo = %u", voiceCh.address().c_str(), voiceCh.port(), srcId, dstId, voiceCh.chId(), chNo);
}
m_affiliations.releaseGrant(dstId, false);
m_affiliations->releaseGrant(dstId, false);
}
}
@ -1892,16 +2034,16 @@ void Control::RPC_touchGrantTG(json::object& req, json::object& reply)
// LogDebugEx(LOG_P25, "Control::RPC_touchGrantTG()", "callback, dstId = %u", dstId);
if (m_affiliations.isGranted(dstId)) {
uint32_t chNo = m_affiliations.getGrantedCh(dstId);
uint32_t srcId = m_affiliations.getGrantedSrcId(dstId);
::lookups::VoiceChData voiceCh = m_affiliations.rfCh()->getRFChData(chNo);
if (m_affiliations->isGranted(dstId)) {
uint32_t chNo = m_affiliations->getGrantedCh(dstId);
uint32_t srcId = m_affiliations->getGrantedSrcId(dstId);
::lookups::VoiceChData voiceCh = m_affiliations->rfCh()->getRFChData(chNo);
if (m_verbose) {
LogMessage(LOG_P25, "VC %s:%u, call in progress, srcId = %u, dstId = %u, chId = %u, chNo = %u", voiceCh.address().c_str(), voiceCh.port(), srcId, dstId, voiceCh.chId(), chNo);
}
m_affiliations.touchGrant(dstId);
m_affiliations->touchGrant(dstId);
}
}

@ -228,7 +228,7 @@ namespace p25
* @brief Gets instance of the P25AffiliationLookup class.
* @returns P25AffiliationLookup Instance of the P25AffiliationLookup class.
*/
lookups::P25AffiliationLookup affiliations() { return m_affiliations; }
lookups::P25AffiliationLookup* affiliations() { return m_affiliations; }
/**
* @brief Returns the current operating RF state of the P25 controller.
@ -315,11 +315,13 @@ namespace p25
::lookups::IdenTableLookup* m_idenTable;
::lookups::RadioIdLookup* m_ridLookup;
::lookups::TalkgroupRulesLookup* m_tidLookup;
lookups::P25AffiliationLookup m_affiliations;
lookups::P25AffiliationLookup* m_affiliations;
::lookups::VoiceChData m_controlChData;
::lookups::IdenTable m_idenEntry;
std::vector<uint32_t> m_activeTG;
RingBuffer<uint8_t> m_txImmQueue;
RingBuffer<uint8_t> m_txQueue;
static std::mutex m_queueLock;
@ -346,6 +348,7 @@ namespace p25
Timer m_networkWatchdog;
Timer m_adjSiteUpdate;
Timer m_activeTGUpdate;
Timer m_ccPacketInterval;
@ -378,6 +381,9 @@ namespace p25
uint32_t m_aveRSSI;
uint32_t m_rssiCount;
static std::mutex m_activeTGLock;
bool m_ccNotifyActiveTG;
bool m_notifyCC;
bool m_ccDebug;
@ -454,6 +460,18 @@ namespace p25
* @param reply JSON response.
*/
void RPC_permittedTG(json::object& req, json::object& reply);
/**
* @brief (RPC Handler) Active TGID list from the authoritative CC host.
* @param req JSON request.
* @param reply JSON response.
*/
void RPC_activeTG(json::object& req, json::object& reply);
/**
* @brief (RPC Handler) Clear active TGID list from the authoritative CC host.
* @param req JSON request.
* @param reply JSON response.
*/
void RPC_clearActiveTG(json::object& req, json::object& reply);
/**
* @brief (RPC Handler) Releases a granted TG.
* @param req JSON request.

@ -46,9 +46,9 @@ std::vector<uint32_t> P25AffiliationLookup::clearGroupAff(uint32_t dstId, bool r
/* Helper to release the channel grant for the destination ID. */
bool P25AffiliationLookup::releaseGrant(uint32_t dstId, bool releaseAll, bool noLock)
bool P25AffiliationLookup::releaseGrant(uint32_t dstId, bool releaseAll)
{
bool ret = ::lookups::AffiliationLookup::releaseGrant(dstId, releaseAll, noLock);
bool ret = ::lookups::AffiliationLookup::releaseGrant(dstId, releaseAll);
if (ret) {
if (m_rfGrantChCnt > 0U) {
m_p25->m_siteData.setChCnt(m_chLookup->rfChSize() + m_rfGrantChCnt);

@ -72,10 +72,9 @@ namespace p25
* @brief Helper to release the channel grant for the destination ID.
* @param dstId Destination Address.
* @param releaseAll Flag indicating all channel grants should be released.
* @param noLock Flag indicating no mutex lock operation should be performed while releasing.
* @returns bool True, if channel grant was released, otherwise false.
*/
bool releaseGrant(uint32_t dstId, bool releaseAll, bool noLock = false) override;
bool releaseGrant(uint32_t dstId, bool releaseAll) override;
/** @} */
protected:

@ -79,7 +79,7 @@ using namespace p25::packet;
// Verify the source RID is registered.
#define VERIFY_SRCID_REG(_PCKT_STR, _PCKT, _SRCID) \
if (!m_p25->m_affiliations.isUnitReg(_SRCID) && m_verifyReg) { \
if (!m_p25->m_affiliations->isUnitReg(_SRCID) && m_verifyReg) { \
LogWarning(LOG_RF, P25_TSDU_STR ", %s denial, RID not registered, srcId = %u", _PCKT_STR.c_str(), _SRCID); \
writeRF_TSDU_Deny(_SRCID, WUID_FNE, ReasonCode::DENY_REQ_UNIT_NOT_AUTH, _PCKT); \
writeRF_TSDU_U_Reg_Cmd(_SRCID); \
@ -89,7 +89,7 @@ using namespace p25::packet;
// Verify the source RID is affiliated.
#define VERIFY_SRCID_AFF(_PCKT_STR, _PCKT, _SRCID, _DSTID) \
if (!m_p25->m_affiliations.isGroupAff(_SRCID, _DSTID) && m_verifyAff) { \
if (!m_p25->m_affiliations->isGroupAff(_SRCID, _DSTID) && m_verifyAff) { \
LogWarning(LOG_RF, P25_TSDU_STR ", %s denial, RID not affiliated to TGID, srcId = %u, dstId = %u", _PCKT_STR.c_str(), _SRCID, _DSTID); \
writeRF_TSDU_Deny(_SRCID, _DSTID, ReasonCode::DENY_REQ_UNIT_NOT_AUTH, _PCKT); \
writeRF_TSDU_U_Reg_Cmd(_SRCID); \
@ -209,7 +209,7 @@ bool ControlSignaling::process(uint8_t* data, uint32_t len, std::unique_ptr<lc::
uint32_t srcId = tsbk->getSrcId();
uint32_t dstId = tsbk->getDstId();
m_p25->m_affiliations.touchUnitReg(srcId);
m_p25->m_affiliations->touchUnitReg(srcId);
m_lastMFID = tsbk->getMFId();
// handle standard P25 reference opcodes
@ -570,9 +570,9 @@ bool ControlSignaling::process(uint8_t* data, uint32_t len, std::unique_ptr<lc::
::ActivityLog("P25", true, "group affiliation query response from %u to %s %u", srcId, "TG ", dstId);
if (!m_p25->m_affiliations.isGroupAff(srcId, dstId)) {
if (!m_p25->m_affiliations->isGroupAff(srcId, dstId)) {
// update dynamic affiliation table
m_p25->m_affiliations.groupAff(srcId, dstId);
m_p25->m_affiliations->groupAff(srcId, dstId);
if (m_p25->m_network != nullptr)
m_p25->m_network->announceGroupAffiliation(srcId, dstId);
@ -792,7 +792,7 @@ bool ControlSignaling::processNetwork(uint8_t* data, uint32_t len, lc::LC& contr
{
if (m_p25->m_dedicatedControl) {
// is the specified channel granted?
if (/*m_p25->m_affiliations.isChBusy(chNo) &&*/ m_p25->m_affiliations.isGranted(dstId)) {
if (/*m_p25->m_affiliations->isChBusy(chNo) &&*/ m_p25->m_affiliations->isGranted(dstId)) {
uint32_t chNo = tsbk->getGrpVchNo();
if (m_verbose) {
@ -800,7 +800,7 @@ bool ControlSignaling::processNetwork(uint8_t* data, uint32_t len, lc::LC& contr
tsbk->toString().c_str(), chNo, srcId, dstId);
}
m_p25->m_affiliations.releaseGrant(dstId, false);
m_p25->m_affiliations->releaseGrant(dstId, false);
}
}
@ -824,7 +824,7 @@ bool ControlSignaling::processNetwork(uint8_t* data, uint32_t len, lc::LC& contr
case TSBKO::IOSP_UU_VCH:
{
if (m_p25->m_enableControl && m_p25->m_dedicatedControl) {
if (!m_p25->m_affiliations.isGranted(dstId)) {
if (!m_p25->m_affiliations->isGranted(dstId)) {
if (m_verbose) {
LogMessage(LOG_NET, P25_TSDU_STR ", %s, emerg = %u, encrypt = %u, prio = %u, chNo = %u, srcId = %u, dstId = %u",
tsbk->toString(true).c_str(), tsbk->getEmergency(), tsbk->getEncrypted(), tsbk->getPriority(), tsbk->getGrpVchNo(), srcId, dstId);
@ -1829,7 +1829,7 @@ void ControlSignaling::writeRF_ControlData(uint8_t frameCnt, uint8_t n, bool adj
break;
/** update data */
case 5:
if (m_p25->m_affiliations.grantSize() > 0) {
if (m_p25->m_affiliations->grantSize() > 0) {
writeRF_TSDU_Grant_Update();
}
break;
@ -2198,12 +2198,12 @@ bool ControlSignaling::writeRF_TSDU_Grant(uint32_t srcId, uint32_t dstId, uint8_
}
}
if (!m_p25->m_affiliations.isGranted(dstId)) {
if (!m_p25->m_affiliations->isGranted(dstId)) {
if (grp && !m_p25->m_ignoreAffiliationCheck) {
// is this an affiliation required group?
::lookups::TalkgroupRuleGroupVoice tid = m_p25->m_tidLookup->find(dstId);
if (tid.config().affiliated()) {
if (!m_p25->m_affiliations.hasGroupAff(dstId)) {
if (!m_p25->m_affiliations->hasGroupAff(dstId)) {
LogWarning(LOG_NET, P25_TSDU_STR ", TSBKO, IOSP_GRP_VCH (Group Voice Channel Request) ignored, no group affiliations, dstId = %u", dstId);
return false;
}
@ -2212,13 +2212,13 @@ bool ControlSignaling::writeRF_TSDU_Grant(uint32_t srcId, uint32_t dstId, uint8_
if (!grp && !m_p25->m_ignoreAffiliationCheck) {
// is this the target registered?
if (!m_p25->m_affiliations.isUnitReg(dstId)) {
if (!m_p25->m_affiliations->isUnitReg(dstId)) {
LogWarning(LOG_NET, P25_TSDU_STR ", TSBKO, IOSP_UU_VCH (Unit-to-Unit Voice Channel Request) ignored, no unit registration, dstId = %u", dstId);
return false;
}
}
if (!m_p25->m_affiliations.rfCh()->isRFChAvailable()) {
if (!m_p25->m_affiliations->rfCh()->isRFChAvailable()) {
if (grp) {
if (!net) {
LogWarning(LOG_RF, P25_TSDU_STR ", TSBKO, IOSP_GRP_VCH (Group Voice Channel Request) denied, no channels available, dstId = %u", dstId);
@ -2249,9 +2249,9 @@ bool ControlSignaling::writeRF_TSDU_Grant(uint32_t srcId, uint32_t dstId, uint8_
}
}
else {
if (m_p25->m_affiliations.grantCh(dstId, srcId, GRANT_TIMER_TIMEOUT, grp, net)) {
chNo = m_p25->m_affiliations.getGrantedCh(dstId);
m_p25->m_siteData.setChCnt(m_p25->m_affiliations.rfCh()->rfChSize() + m_p25->m_affiliations.getGrantedRFChCnt());
if (m_p25->m_affiliations->grantCh(dstId, srcId, GRANT_TIMER_TIMEOUT, grp, net)) {
chNo = m_p25->m_affiliations->getGrantedCh(dstId);
m_p25->m_siteData.setChCnt(m_p25->m_affiliations->rfCh()->rfChSize() + m_p25->m_affiliations->getGrantedRFChCnt());
}
}
}
@ -2259,7 +2259,7 @@ bool ControlSignaling::writeRF_TSDU_Grant(uint32_t srcId, uint32_t dstId, uint8_
if (!m_disableGrantSrcIdCheck && !net) {
// do collision check between grants to see if a SU is attempting a "grant retry" or if this is a
// different source from the original grant
uint32_t grantedSrcId = m_p25->m_affiliations.getGrantedSrcId(dstId);
uint32_t grantedSrcId = m_p25->m_affiliations->getGrantedSrcId(dstId);
if (srcId != grantedSrcId) {
if (!net) {
LogWarning(LOG_RF, P25_TSDU_STR ", TSBKO, IOSP_GRP_VCH (Group Voice Channel Request) denied, traffic collision, dstId = %u", dstId);
@ -2276,14 +2276,14 @@ bool ControlSignaling::writeRF_TSDU_Grant(uint32_t srcId, uint32_t dstId, uint8_
}
}
chNo = m_p25->m_affiliations.getGrantedCh(dstId);
m_p25->m_affiliations.touchGrant(dstId);
chNo = m_p25->m_affiliations->getGrantedCh(dstId);
m_p25->m_affiliations->touchGrant(dstId);
}
}
else {
if (m_p25->m_affiliations.isGranted(dstId)) {
chNo = m_p25->m_affiliations.getGrantedCh(dstId);
m_p25->m_affiliations.touchGrant(dstId);
if (m_p25->m_affiliations->isGranted(dstId)) {
chNo = m_p25->m_affiliations->getGrantedCh(dstId);
m_p25->m_affiliations->touchGrant(dstId);
}
else {
return false;
@ -2291,7 +2291,7 @@ bool ControlSignaling::writeRF_TSDU_Grant(uint32_t srcId, uint32_t dstId, uint8_
}
if (chNo > 0U) {
::lookups::VoiceChData voiceChData = m_p25->m_affiliations.rfCh()->getRFChData(chNo);
::lookups::VoiceChData voiceChData = m_p25->m_affiliations->rfCh()->getRFChData(chNo);
if (grp) {
// callback RPC to permit the granted TG on the specified voice channel
@ -2323,7 +2323,7 @@ bool ControlSignaling::writeRF_TSDU_Grant(uint32_t srcId, uint32_t dstId, uint8_
if (requestFailed) {
::LogError((net) ? LOG_NET : LOG_RF, P25_TSDU_STR ", TSBKO, IOSP_GRP_VCH (Group Voice Channel Request), failed to permit TG for use, chNo = %u", chNo);
m_p25->m_affiliations.releaseGrant(dstId, false);
m_p25->m_affiliations->releaseGrant(dstId, false);
if (!net) {
writeRF_TSDU_Deny(srcId, dstId, ReasonCode::DENY_PTT_BONK, (grp) ? TSBKO::IOSP_GRP_VCH : TSBKO::IOSP_UU_VCH, grp, true);
m_p25->m_rfState = RS_RF_REJECTED;
@ -2393,7 +2393,7 @@ bool ControlSignaling::writeRF_TSDU_Grant(uint32_t srcId, uint32_t dstId, uint8_
if (requestFailed) {
::LogError((net) ? LOG_NET : LOG_RF, P25_TSDU_STR ", TSBKO, IOSP_UU_VCH (Unit-to-Unit Voice Channel Request), failed to permit TG for use, chNo = %u", chNo);
m_p25->m_affiliations.releaseGrant(dstId, false);
m_p25->m_affiliations->releaseGrant(dstId, false);
if (!net) {
writeRF_TSDU_Deny(srcId, dstId, ReasonCode::DENY_PTT_BONK, (grp) ? TSBKO::IOSP_GRP_VCH : TSBKO::IOSP_UU_VCH, grp, true);
m_p25->m_rfState = RS_RF_REJECTED;
@ -2446,15 +2446,15 @@ void ControlSignaling::writeRF_TSDU_Grant_Update()
return;
// write group voice grant update
if (m_p25->m_affiliations.grantSize() > 0) {
if (m_mbfGrpGrntCnt >= m_p25->m_affiliations.grantSize())
if (m_p25->m_affiliations->grantSize() > 0) {
if (m_mbfGrpGrntCnt >= m_p25->m_affiliations->grantSize())
m_mbfGrpGrntCnt = 0U;
std::unique_ptr<lc::TSBK> osp;
bool noData = false;
uint8_t i = 0U;
std::unordered_map<uint32_t, uint32_t> grantTable = m_p25->m_affiliations.grantTable();
std::unordered_map<uint32_t, uint32_t> grantTable = m_p25->m_affiliations->grantTable();
for (auto entry : grantTable) {
// no good very bad way of skipping entries...
if (i != m_mbfGrpGrntCnt) {
@ -2464,9 +2464,9 @@ void ControlSignaling::writeRF_TSDU_Grant_Update()
else {
uint32_t dstId = entry.first;
uint32_t chNo = entry.second;
bool grp = m_p25->m_affiliations.isGroup(dstId);
bool grp = m_p25->m_affiliations->isGroup(dstId);
::lookups::VoiceChData voiceChData = m_p25->m_affiliations.rfCh()->getRFChData(chNo);
::lookups::VoiceChData voiceChData = m_p25->m_affiliations->rfCh()->getRFChData(chNo);
if (chNo == 0U) {
noData = true;
@ -2487,7 +2487,7 @@ void ControlSignaling::writeRF_TSDU_Grant_Update()
m_mbfGrpGrntCnt++;
break;
} else {
uint32_t srcId = m_p25->m_affiliations.getGrantedSrcId(dstId);
uint32_t srcId = m_p25->m_affiliations->getGrantedSrcId(dstId);
osp = std::make_unique<OSP_UU_VCH_GRANT_UPD>();
DEBUG_LOG_TSBK(osp->toString());
@ -2542,8 +2542,8 @@ bool ControlSignaling::writeRF_TSDU_SNDCP_Grant(uint32_t srcId, bool skip, uint3
return false;
}
if (!m_p25->m_affiliations.isGranted(srcId)) {
if (!m_p25->m_affiliations.rfCh()->isRFChAvailable()) {
if (!m_p25->m_affiliations->isGranted(srcId)) {
if (!m_p25->m_affiliations->rfCh()->isRFChAvailable()) {
LogWarning(LOG_RF, P25_TSDU_STR ", TSBKO, ISP_SNDCP_CH_REQ (SNDCP Data Channel Request) denied, no channels available, srcId = %u", srcId);
writeRF_TSDU_Deny(WUID_FNE, srcId, ReasonCode::DENY_NO_RF_RSRC_AVAIL, TSBKO::ISP_SNDCP_CH_REQ, false, true);
@ -2552,31 +2552,31 @@ bool ControlSignaling::writeRF_TSDU_SNDCP_Grant(uint32_t srcId, bool skip, uint3
return false;
}
else {
if (m_p25->m_affiliations.grantCh(srcId, srcId, GRANT_TIMER_TIMEOUT, false, false)) {
chNo = m_p25->m_affiliations.getGrantedCh(srcId);
::lookups::VoiceChData voiceChData = m_p25->m_affiliations.rfCh()->getRFChData(chNo);
if (m_p25->m_affiliations->grantCh(srcId, srcId, GRANT_TIMER_TIMEOUT, false, false)) {
chNo = m_p25->m_affiliations->getGrantedCh(srcId);
::lookups::VoiceChData voiceChData = m_p25->m_affiliations->rfCh()->getRFChData(chNo);
osp->setGrpVchId(voiceChData.chId());
osp->setGrpVchNo(chNo);
osp->setDataChnNo(chNo);
m_p25->m_siteData.setChCnt(m_p25->m_affiliations.rfCh()->rfChSize() + m_p25->m_affiliations.getGrantedRFChCnt());
m_p25->m_siteData.setChCnt(m_p25->m_affiliations->rfCh()->rfChSize() + m_p25->m_affiliations->getGrantedRFChCnt());
}
}
}
else {
chNo = m_p25->m_affiliations.getGrantedCh(srcId);
::lookups::VoiceChData voiceChData = m_p25->m_affiliations.rfCh()->getRFChData(chNo);
chNo = m_p25->m_affiliations->getGrantedCh(srcId);
::lookups::VoiceChData voiceChData = m_p25->m_affiliations->rfCh()->getRFChData(chNo);
osp->setGrpVchId(voiceChData.chId());
osp->setGrpVchNo(chNo);
osp->setDataChnNo(chNo);
m_p25->m_affiliations.touchGrant(srcId);
m_p25->m_affiliations->touchGrant(srcId);
}
}
if (chNo > 0U) {
::lookups::VoiceChData voiceChData = m_p25->m_affiliations.rfCh()->getRFChData(chNo);
::lookups::VoiceChData voiceChData = m_p25->m_affiliations->rfCh()->getRFChData(chNo);
// callback RPC to permit the granted TG on the specified voice channel
if (m_p25->m_authoritative && m_p25->m_supervisor) {
@ -2611,7 +2611,7 @@ bool ControlSignaling::writeRF_TSDU_SNDCP_Grant(uint32_t srcId, bool skip, uint3
if (requestFailed) {
::LogError(LOG_RF, P25_TSDU_STR ", TSBKO, ISP_SNDCP_CH_REQ (SNDCP Data Channel Request), failed to permit for use, chNo = %u", chNo);
m_p25->m_affiliations.releaseGrant(srcId, false);
m_p25->m_affiliations->releaseGrant(srcId, false);
writeRF_TSDU_Deny(srcId, srcId, ReasonCode::DENY_PTT_BONK, TSBKO::ISP_SNDCP_CH_REQ, false, true);
m_p25->m_rfState = RS_RF_REJECTED;
@ -2717,7 +2717,7 @@ uint8_t ControlSignaling::writeRF_TSDU_Grp_Aff_Rsp(uint32_t srcId, uint32_t dstI
}
// register the RID if the MFID is $90 (this is typically DVRS, and DVRS won't unit register so we'll do it for them)
if (!m_p25->m_affiliations.isUnitReg(srcId) && m_lastMFID == MFG_MOT) {
if (!m_p25->m_affiliations->isUnitReg(srcId) && m_lastMFID == MFG_MOT) {
// validate the source RID
if (!acl::AccessControl::validateSrcId(srcId)) {
LogWarning(LOG_RF, P25_TSDU_STR ", %s denial, RID rejection, srcId = %u", iosp->toString().c_str(), srcId);
@ -2727,8 +2727,8 @@ uint8_t ControlSignaling::writeRF_TSDU_Grp_Aff_Rsp(uint32_t srcId, uint32_t dstI
}
else {
// update dynamic unit registration table
if (!m_p25->m_affiliations.isUnitReg(srcId)) {
m_p25->m_affiliations.unitReg(srcId);
if (!m_p25->m_affiliations->isUnitReg(srcId)) {
m_p25->m_affiliations->unitReg(srcId);
}
if (m_p25->m_network != nullptr)
@ -2737,7 +2737,7 @@ uint8_t ControlSignaling::writeRF_TSDU_Grp_Aff_Rsp(uint32_t srcId, uint32_t dstI
}
// validate the source RID is registered
if (!m_p25->m_affiliations.isUnitReg(srcId) && m_verifyReg) {
if (!m_p25->m_affiliations->isUnitReg(srcId) && m_verifyReg) {
LogWarning(LOG_RF, P25_TSDU_STR ", %s denial, RID not registered, srcId = %u", iosp->toString().c_str(), srcId);
::ActivityLog("P25", true, "group affiliation request from %u to %s %u denied", srcId, "TG ", dstId);
iosp->setResponse(ResponseCode::REFUSED);
@ -2774,7 +2774,7 @@ uint8_t ControlSignaling::writeRF_TSDU_Grp_Aff_Rsp(uint32_t srcId, uint32_t dstI
::ActivityLog("P25", true, "group affiliation request from %u to %s %u", srcId, "TG ", dstId);
// update dynamic affiliation table
m_p25->m_affiliations.groupAff(srcId, dstId);
m_p25->m_affiliations->groupAff(srcId, dstId);
if (m_p25->m_network != nullptr)
m_p25->m_network->announceGroupAffiliation(srcId, dstId);
@ -2816,8 +2816,8 @@ void ControlSignaling::writeRF_TSDU_U_Reg_Rsp(uint32_t srcId, uint32_t sysId)
::ActivityLog("P25", true, "unit registration request from %u", srcId);
// update dynamic unit registration table
if (!m_p25->m_affiliations.isUnitReg(srcId)) {
m_p25->m_affiliations.unitReg(srcId);
if (!m_p25->m_affiliations->isUnitReg(srcId)) {
m_p25->m_affiliations->unitReg(srcId);
}
if (m_p25->m_network != nullptr)
@ -2839,7 +2839,7 @@ void ControlSignaling::writeRF_TSDU_U_Dereg_Ack(uint32_t srcId)
bool dereged = false;
// remove dynamic unit registration table entry
dereged = m_p25->m_affiliations.unitDereg(srcId);
dereged = m_p25->m_affiliations->unitDereg(srcId);
if (dereged) {
std::unique_ptr<OSP_U_DEREG_ACK> osp = std::make_unique<OSP_U_DEREG_ACK>();
@ -2903,7 +2903,7 @@ bool ControlSignaling::writeRF_TSDU_Loc_Reg_Rsp(uint32_t srcId, uint32_t dstId,
}
// validate the source RID is registered
if (!m_p25->m_affiliations.isUnitReg(srcId)) {
if (!m_p25->m_affiliations->isUnitReg(srcId)) {
LogWarning(LOG_RF, P25_TSDU_STR ", %s denial, RID not registered, srcId = %u", osp->toString().c_str(), srcId);
::ActivityLog("P25", true, "location registration request from %u denied", srcId);
writeRF_TSDU_U_Reg_Cmd(srcId);
@ -2941,7 +2941,7 @@ bool ControlSignaling::writeRF_TSDU_Loc_Reg_Rsp(uint32_t srcId, uint32_t dstId,
::ActivityLog("P25", true, "location registration request from %u", srcId);
// update dynamic affiliation table
m_p25->m_affiliations.groupAff(srcId, dstId);
m_p25->m_affiliations->groupAff(srcId, dstId);
if (m_p25->m_network != nullptr)
m_p25->m_network->announceGroupAffiliation(srcId, dstId);
@ -2990,8 +2990,8 @@ void ControlSignaling::writeRF_TSDU_Auth_Dmd(uint32_t srcId)
bool ControlSignaling::writeNet_TSDU_Call_Term(uint32_t srcId, uint32_t dstId)
{
// is the specified channel granted?
if (m_p25->m_affiliations.isGranted(dstId)) {
m_p25->m_affiliations.releaseGrant(dstId, false);
if (m_p25->m_affiliations->isGranted(dstId)) {
m_p25->m_affiliations->releaseGrant(dstId, false);
}
std::unique_ptr<OSP_DVM_LC_CALL_TERM> osp = std::make_unique<OSP_DVM_LC_CALL_TERM>();

@ -35,7 +35,7 @@ using namespace p25::packet;
// Constants
// ---------------------------------------------------------------------------
const uint32_t VOC_LDU1_COUNT = 3U;
const uint32_t PKT_LDU1_COUNT = 3U;
const uint32_t ROAM_LDU1_COUNT = 1U;
// ---------------------------------------------------------------------------
@ -59,7 +59,8 @@ void Voice::resetRF()
m_rfErrs = 0U;
m_rfBits = 1U;
m_rfUndecodableLC = 0U;
m_vocLDU1Count = 0U;
m_pktLDU1Count = 0U;
m_grpUpdtCount = 0U;
m_roamLDU1Count = 0U;
m_inbound = false;
@ -80,7 +81,8 @@ void Voice::resetNet()
m_netFrames = 0U;
m_netLost = 0U;
m_vocLDU1Count = 0U;
m_pktLDU1Count = 0U;
m_grpUpdtCount = 0U;
m_roamLDU1Count = 0U;
m_p25->m_networkWatchdog.stop();
@ -160,7 +162,7 @@ bool Voice::process(uint8_t* data, uint32_t len)
LogWarning(LOG_RF, "Traffic collision detect, preempting existing network traffic to new RF traffic, rfDstId = %u, netDstId = %u", lc.getDstId(),
m_p25->m_netLastDstId);
if (!m_p25->m_dedicatedControl) {
m_p25->m_affiliations.releaseGrant(m_p25->m_netLastDstId, false);
m_p25->m_affiliations->releaseGrant(m_p25->m_netLastDstId, false);
}
resetNet();
@ -256,7 +258,7 @@ bool Voice::process(uint8_t* data, uint32_t len)
LogWarning(LOG_RF, "Traffic collision detect, preempting existing network traffic to new RF traffic, rfDstId = %u, netDstId = %u", dstId,
m_p25->m_netLastDstId);
if (!m_p25->m_dedicatedControl) {
m_p25->m_affiliations.releaseGrant(m_p25->m_netLastDstId, false);
m_p25->m_affiliations->releaseGrant(m_p25->m_netLastDstId, false);
}
resetNet();
@ -272,7 +274,7 @@ bool Voice::process(uint8_t* data, uint32_t len)
// is control is enabled, and the group was granted by network already ignore RF traffic
if (m_p25->m_enableControl && dstId == m_p25->m_netLastDstId) {
if (m_p25->m_affiliations.isNetGranted(dstId)) {
if (m_p25->m_affiliations->isNetGranted(dstId)) {
LogWarning(LOG_RF, "Traffic collision detect, preempting new RF traffic to existing granted network traffic (Are we in a voting condition?), rfSrcId = %u, rfDstId = %u, netSrcId = %u, netDstId = %u", srcId, dstId,
m_netLC.getSrcId(), m_p25->m_netLastDstId);
resetRF();
@ -363,7 +365,7 @@ bool Voice::process(uint8_t* data, uint32_t len)
// verify the source RID is affiliated to the group TGID; only if control data
// is supported
if (group && m_p25->m_enableControl) {
if (!m_p25->m_affiliations.isGroupAff(srcId, dstId) && m_p25->m_control->m_verifyAff) {
if (!m_p25->m_affiliations->isGroupAff(srcId, dstId) && m_p25->m_control->m_verifyAff) {
if (m_lastRejectId == 0 || m_lastRejectId != srcId) {
LogWarning(LOG_RF, P25_HDU_STR " denial, RID not affiliated to TGID, srcId = %u, dstId = %u", srcId, dstId);
m_p25->m_control->writeRF_TSDU_Deny(srcId, dstId, ReasonCode::DENY_REQ_UNIT_NOT_AUTH, TSBKO::IOSP_GRP_VCH, true, true);
@ -414,11 +416,11 @@ bool Voice::process(uint8_t* data, uint32_t len)
if (m_p25->m_enableControl) {
// if the group wasn't granted out -- explicitly grant the group
if (!m_p25->m_affiliations.isGranted(dstId)) {
if (!m_p25->m_affiliations->isGranted(dstId)) {
if (m_p25->m_legacyGroupGrnt) {
// are we auto-registering legacy radios to groups?
if (m_p25->m_legacyGroupReg && group) {
if (!m_p25->m_affiliations.isGroupAff(srcId, dstId)) {
if (!m_p25->m_affiliations->isGroupAff(srcId, dstId)) {
if (m_p25->m_control->writeRF_TSDU_Grp_Aff_Rsp(srcId, dstId) != ResponseCode::ACCEPT) {
LogWarning(LOG_RF, P25_HDU_STR " denial, conventional affiliation required, not affiliated to TGID, srcId = %u, dstId = %u", srcId, dstId);
m_p25->m_rfLastDstId = 0U;
@ -447,15 +449,15 @@ bool Voice::process(uint8_t* data, uint32_t len)
// conventional registration or DVRS support?
if ((m_p25->m_enableControl && !m_p25->m_dedicatedControl) || m_p25->m_voiceOnControl) {
if (!m_p25->m_affiliations.isGranted(dstId)) {
if (!m_p25->m_affiliations->isGranted(dstId)) {
m_p25->m_control->writeRF_TSDU_Grant(srcId, dstId, serviceOptions, group, false, true);
}
// if voice on control; insert grant updates before voice traffic
if (m_p25->m_voiceOnControl) {
uint32_t chNo = m_p25->m_affiliations.getGrantedCh(dstId);
::lookups::VoiceChData voiceChData = m_p25->m_affiliations.rfCh()->getRFChData(chNo);
bool grp = m_p25->m_affiliations.isGroup(dstId);
uint32_t chNo = m_p25->m_affiliations->getGrantedCh(dstId);
::lookups::VoiceChData voiceChData = m_p25->m_affiliations->rfCh()->getRFChData(chNo);
bool grp = m_p25->m_affiliations->isGroup(dstId);
std::unique_ptr<lc::TSBK> osp;
@ -469,7 +471,7 @@ bool Voice::process(uint8_t* data, uint32_t len)
osp->setGrpVchNo(chNo);
}
else {
uint32_t srcId = m_p25->m_affiliations.getGrantedSrcId(dstId);
uint32_t srcId = m_p25->m_affiliations->getGrantedSrcId(dstId);
osp = std::make_unique<lc::tsbk::OSP_UU_VCH_GRANT_UPD>();
@ -558,9 +560,9 @@ bool Voice::process(uint8_t* data, uint32_t len)
// if voice on control; insert group voice channel updates directly after HDU but before LDUs
if (m_p25->m_voiceOnControl) {
uint32_t chNo = m_p25->m_affiliations.getGrantedCh(dstId);
::lookups::VoiceChData voiceChData = m_p25->m_affiliations.rfCh()->getRFChData(chNo);
bool grp = m_p25->m_affiliations.isGroup(dstId);
uint32_t chNo = m_p25->m_affiliations->getGrantedCh(dstId);
::lookups::VoiceChData voiceChData = m_p25->m_affiliations->rfCh()->getRFChData(chNo);
bool grp = m_p25->m_affiliations->isGroup(dstId);
std::unique_ptr<lc::TSBK> osp;
@ -574,7 +576,7 @@ bool Voice::process(uint8_t* data, uint32_t len)
osp->setGrpVchNo(chNo);
}
else {
uint32_t srcId = m_p25->m_affiliations.getGrantedSrcId(dstId);
uint32_t srcId = m_p25->m_affiliations->getGrantedSrcId(dstId);
osp = std::make_unique<lc::tsbk::OSP_UU_VCH_GRANT_UPD>();
@ -599,7 +601,8 @@ bool Voice::process(uint8_t* data, uint32_t len)
m_rfErrs = 0U;
m_rfBits = 1U;
m_rfUndecodableLC = 0U;
m_vocLDU1Count = 0U;
m_pktLDU1Count = 0U;
m_grpUpdtCount = 0U;
m_roamLDU1Count = 0U;
m_p25->m_rfTimeout.start();
m_lastDUID = DUID::HDU;
@ -629,7 +632,7 @@ bool Voice::process(uint8_t* data, uint32_t len)
// is control is enabled, and the group was granted by network already ignore RF traffic
if (m_p25->m_enableControl && m_rfLC.getDstId() == m_p25->m_netLastDstId) {
if (m_p25->m_affiliations.isNetGranted(m_rfLC.getDstId())) {
if (m_p25->m_affiliations->isNetGranted(m_rfLC.getDstId())) {
LogWarning(LOG_RF, "Traffic collision detect, preempting new RF traffic to existing granted network traffic (Are we in a voting condition?), rfSrcId = %u, rfDstId = %u, netSrcId = %u, netDstId = %u", m_rfLC.getSrcId(), m_rfLC.getDstId(),
m_netLC.getSrcId(), m_p25->m_netLastDstId);
resetRF();
@ -686,22 +689,48 @@ bool Voice::process(uint8_t* data, uint32_t len)
alreadyDecoded = false;
if (m_p25->m_enableControl) {
m_p25->m_affiliations.touchGrant(m_rfLC.getDstId());
m_p25->m_affiliations->touchGrant(m_rfLC.getDstId());
}
if (m_p25->m_notifyCC) {
m_p25->notifyCC_TouchGrant(m_rfLC.getDstId());
}
// are we swapping the LC out for the RFSS_STS_BCAST or LC_GROUP_UPDT?
m_pktLDU1Count++;
if (m_pktLDU1Count > PKT_LDU1_COUNT) {
m_pktLDU1Count = 0U;
// conventional registration or DVRS support?
if ((m_p25->m_enableControl && !m_p25->m_dedicatedControl) || m_p25->m_voiceOnControl) {
// per TIA-102.AABD-B transmit RFSS_STS_BCAST every 3 superframes (e.g. every 3 LDU1s)
m_vocLDU1Count++;
if (m_vocLDU1Count > VOC_LDU1_COUNT) {
m_vocLDU1Count = 0U;
m_rfLC.setMFId(MFG_STANDARD);
m_rfLC.setLCO(LCO::RFSS_STS_BCAST);
}
else {
std::lock_guard<std::mutex> lock(m_p25->m_activeTGLock);
if (m_p25->m_activeTG.size() > 0) {
if (m_grpUpdtCount > m_p25->m_activeTG.size())
m_grpUpdtCount = 0U;
if (m_p25->m_activeTG.size() < 2) {
uint32_t dstId = m_p25->m_activeTG.at(0);
m_rfLC.setMFId(MFG_STANDARD);
m_rfLC.setLCO(LCO::GROUP_UPDT);
m_rfLC.setDstId(dstId);
}
else {
uint32_t dstId = m_p25->m_activeTG.at(m_grpUpdtCount);
uint32_t dstIdB = m_p25->m_activeTG.at(m_grpUpdtCount + 1U);
m_rfLC.setMFId(MFG_STANDARD);
m_rfLC.setLCO(LCO::GROUP_UPDT);
m_rfLC.setDstId(dstId);
m_rfLC.setDstIdB(dstIdB);
m_grpUpdtCount++;
}
}
}
}
// generate Sync
@ -988,7 +1017,8 @@ bool Voice::process(uint8_t* data, uint32_t len)
m_rfErrs = 0U;
m_rfBits = 1U;
m_rfUndecodableLC = 0U;
m_vocLDU1Count = 0U;
m_pktLDU1Count = 0U;
m_grpUpdtCount = 0U;
m_roamLDU1Count = 0U;
m_p25->m_rfTimeout.start();
m_lastDUID = DUID::HDU;
@ -1070,7 +1100,7 @@ bool Voice::process(uint8_t* data, uint32_t len)
}
else if (duid == DUID::TDU || duid == DUID::TDULC) {
if (!m_p25->m_enableControl) {
m_p25->m_affiliations.releaseGrant(m_rfLC.getDstId(), false);
m_p25->m_affiliations->releaseGrant(m_rfLC.getDstId(), false);
}
if (m_p25->m_notifyCC) {
@ -1275,7 +1305,7 @@ bool Voice::processNetwork(uint8_t* data, uint32_t len, lc::LC& control, data::L
if (m_p25->m_enableControl) {
lc::LC control = lc::LC(*m_dfsiLC.control());
m_p25->m_affiliations.touchGrant(control.getDstId());
m_p25->m_affiliations->touchGrant(control.getDstId());
}
if (m_p25->m_notifyCC) {
@ -1349,7 +1379,7 @@ bool Voice::processNetwork(uint8_t* data, uint32_t len, lc::LC& control, data::L
if (m_p25->m_enableControl) {
lc::LC control = lc::LC(*m_dfsiLC.control());
m_p25->m_affiliations.touchGrant(control.getDstId());
m_p25->m_affiliations->touchGrant(control.getDstId());
}
if (m_p25->m_notifyCC) {
@ -1419,7 +1449,7 @@ bool Voice::processNetwork(uint8_t* data, uint32_t len, lc::LC& control, data::L
m_netLastDUID = duid;
if (!m_p25->m_enableControl) {
m_p25->m_affiliations.releaseGrant(m_netLC.getDstId(), false);
m_p25->m_affiliations->releaseGrant(m_netLC.getDstId(), false);
}
if (m_p25->m_notifyCC) {
@ -1478,7 +1508,8 @@ Voice::Voice(Control* p25, bool debug, bool verbose) :
m_hadVoice(false),
m_lastRejectId(0U),
m_silenceThreshold(DEFAULT_SILENCE_THRESHOLD),
m_vocLDU1Count(0U),
m_pktLDU1Count(0U),
m_grpUpdtCount(0U),
m_roamLDU1Count(0U),
m_inbound(false),
m_verbose(verbose),
@ -1749,7 +1780,7 @@ void Voice::writeNet_LDU1()
(m_netLC.getEncrypted() ? 0x40U : 0x00U) + // Encrypted Flag
(m_netLC.getPriority() & 0x07U); // Priority
if (!m_p25->m_affiliations.isGranted(dstId)) {
if (!m_p25->m_affiliations->isGranted(dstId)) {
if (!m_p25->m_control->writeRF_TSDU_Grant(srcId, dstId, serviceOptions, group, true)) {
LogError(LOG_NET, P25_HDU_STR " call rejected, network call not granted, dstId = %u", dstId);
@ -1785,9 +1816,9 @@ void Voice::writeNet_LDU1()
// if voice on control; insert grant updates before voice traffic
if (m_p25->m_voiceOnControl) {
uint32_t chNo = m_p25->m_affiliations.getGrantedCh(dstId);
::lookups::VoiceChData voiceChData = m_p25->m_affiliations.rfCh()->getRFChData(chNo);
bool grp = m_p25->m_affiliations.isGroup(dstId);
uint32_t chNo = m_p25->m_affiliations->getGrantedCh(dstId);
::lookups::VoiceChData voiceChData = m_p25->m_affiliations->rfCh()->getRFChData(chNo);
bool grp = m_p25->m_affiliations->isGroup(dstId);
std::unique_ptr<lc::TSBK> osp;
@ -1801,7 +1832,7 @@ void Voice::writeNet_LDU1()
osp->setGrpVchNo(chNo);
}
else {
uint32_t srcId = m_p25->m_affiliations.getGrantedSrcId(dstId);
uint32_t srcId = m_p25->m_affiliations->getGrantedSrcId(dstId);
osp = std::make_unique<lc::tsbk::OSP_UU_VCH_GRANT_UPD>();
@ -1831,7 +1862,8 @@ void Voice::writeNet_LDU1()
m_p25->m_netTimeout.start();
m_netFrames = 0U;
m_netLost = 0U;
m_vocLDU1Count = 0U;
m_pktLDU1Count = 0U;
m_grpUpdtCount = 0U;
m_roamLDU1Count = 0U;
if (!m_p25->m_disableNetworkHDU) {
@ -1911,15 +1943,41 @@ void Voice::writeNet_LDU1()
sysId = lc::LC::getSiteData().sysId();
}
// are we swapping the LC out for the RFSS_STS_BCAST or LC_GROUP_UPDT?
m_pktLDU1Count++;
if (m_pktLDU1Count > PKT_LDU1_COUNT) {
m_pktLDU1Count = 0U;
// conventional registration or DVRS support?
if ((m_p25->m_enableControl && !m_p25->m_dedicatedControl) || m_p25->m_voiceOnControl) {
// per TIA-102.AABD-B transmit RFSS_STS_BCAST every 3 superframes (e.g. every 3 LDU1s)
m_vocLDU1Count++;
if (m_vocLDU1Count > VOC_LDU1_COUNT) {
m_vocLDU1Count = 0U;
m_netLC.setMFId(MFG_STANDARD);
m_netLC.setLCO(LCO::RFSS_STS_BCAST);
}
else {
std::lock_guard<std::mutex> lock(m_p25->m_activeTGLock);
if (m_p25->m_activeTG.size() > 0) {
if (m_grpUpdtCount > m_p25->m_activeTG.size())
m_grpUpdtCount = 0U;
if (m_p25->m_activeTG.size() < 2) {
uint32_t dstId = m_p25->m_activeTG.at(0);
m_netLC.setMFId(MFG_STANDARD);
m_netLC.setLCO(LCO::GROUP_UPDT);
m_netLC.setDstId(dstId);
}
else {
uint32_t dstId = m_p25->m_activeTG.at(m_grpUpdtCount);
uint32_t dstIdB = m_p25->m_activeTG.at(m_grpUpdtCount + 1U);
m_netLC.setMFId(MFG_STANDARD);
m_netLC.setLCO(LCO::GROUP_UPDT);
m_netLC.setDstId(dstId);
m_netLC.setDstIdB(dstIdB);
m_grpUpdtCount++;
}
}
}
}
uint8_t buffer[P25_LDU_FRAME_LENGTH_BYTES + 2U];

@ -125,7 +125,8 @@ namespace p25
uint32_t m_silenceThreshold;
uint8_t m_vocLDU1Count;
uint8_t m_pktLDU1Count;
uint8_t m_grpUpdtCount;
uint8_t m_roamLDU1Count;
bool m_inbound;

@ -2051,6 +2051,8 @@ bool HostSetup::writeFlash()
void HostSetup::writeBootload()
{
m_reqBootload = true;
LogMessage(LOG_CAL, "Rebooting modem into ST bootloader mode...");
if (writeFlash()) {
uint8_t buffer[4U];
::memset(buffer, 0x00U, 4U);

@ -0,0 +1,155 @@
// SPDX-License-Identifier: GPL-2.0-only
/*
* Digital Voice Modem - TG Patch
* GPLv2 Open Source. Use is subject to license terms.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* Copyright (C) 2025 Bryan Biedenkapp, N2PLL
*
*/
#include "ActivityLog.h"
#include "common/network/BaseNetwork.h"
#include "common/Log.h" // for CurrentLogFileLevel() and LogGetNetwork()
#if defined(_WIN32)
#include "common/Clock.h"
#else
#include <sys/time.h>
#endif // defined(_WIN32)
#if defined(CATCH2_TEST_COMPILATION)
#include <catch2/catch_test_macros.hpp>
#endif
#include <cstdio>
#include <cstdarg>
#include <ctime>
#include <cassert>
#include <cstring>
// ---------------------------------------------------------------------------
// Constants
// ---------------------------------------------------------------------------
#define EOL "\r\n"
const uint32_t ACT_LOG_BUFFER_LEN = 501U;
// ---------------------------------------------------------------------------
// Global Variables
// ---------------------------------------------------------------------------
static std::string m_actFilePath;
static std::string m_actFileRoot;
static FILE* m_actFpLog = nullptr;
static struct tm m_actTm;
// ---------------------------------------------------------------------------
// Global Functions
// ---------------------------------------------------------------------------
/* Helper to open the activity log file, file handle. */
static bool ActivityLogOpen()
{
if (CurrentLogFileLevel() == 0U)
return true;
time_t now;
::time(&now);
struct tm* tm = ::gmtime(&now);
if (tm->tm_mday == m_actTm.tm_mday && tm->tm_mon == m_actTm.tm_mon && tm->tm_year == m_actTm.tm_year) {
if (m_actFpLog != nullptr)
return true;
}
else {
if (m_actFpLog != nullptr)
::fclose(m_actFpLog);
}
char filename[200U];
::sprintf(filename, "%s/%s-%04d-%02d-%02d.activity.log", LogGetFilePath().c_str(), LogGetFileRoot().c_str(), tm->tm_year + 1900, tm->tm_mon + 1, tm->tm_mday);
m_actFpLog = ::fopen(filename, "a+t");
m_actTm = *tm;
return m_actFpLog != nullptr;
}
/* Initializes the activity log. */
bool ActivityLogInitialise(const std::string& filePath, const std::string& fileRoot)
{
#if defined(CATCH2_TEST_COMPILATION)
return true;
#endif
m_actFilePath = filePath;
m_actFileRoot = fileRoot;
return ::ActivityLogOpen();
}
/* Finalizes the activity log. */
void ActivityLogFinalise()
{
#if defined(CATCH2_TEST_COMPILATION)
return;
#endif
if (m_actFpLog != nullptr)
::fclose(m_actFpLog);
}
/* Writes a new entry to the activity log. */
void ActivityLog(const char* msg, ...)
{
#if defined(CATCH2_TEST_COMPILATION)
return;
#endif
assert(msg != nullptr);
char buffer[ACT_LOG_BUFFER_LEN];
time_t now;
::time(&now);
struct tm* tm = ::localtime(&now);
struct timeval nowMillis;
::gettimeofday(&nowMillis, NULL);
::sprintf(buffer, "A: %04d-%02d-%02d %02d:%02d:%02d.%03lu ", tm->tm_year + 1900, tm->tm_mon + 1, tm->tm_mday, tm->tm_hour, tm->tm_min, tm->tm_sec, nowMillis.tv_usec / 1000U);
va_list vl, vl_len;
va_start(vl, msg);
va_copy(vl_len, vl);
size_t len = ::vsnprintf(nullptr, 0U, msg, vl_len);
::vsnprintf(buffer + ::strlen(buffer), len + 1U, msg, vl);
va_end(vl_len);
va_end(vl);
bool ret = ::ActivityLogOpen();
if (!ret)
return;
if (LogGetNetwork() != nullptr) {
network::BaseNetwork* network = (network::BaseNetwork*)LogGetNetwork();;
network->writeActLog(buffer);
}
if (CurrentLogFileLevel() == 0U)
return;
::fprintf(m_actFpLog, "%s\n", buffer);
::fflush(m_actFpLog);
if (2U >= g_logDisplayLevel && g_logDisplayLevel != 0U) {
::fprintf(stdout, "%s" EOL, buffer);
::fflush(stdout);
}
}

@ -0,0 +1,45 @@
// SPDX-License-Identifier: GPL-2.0-only
/*
* Digital Voice Modem - TG Patch
* GPLv2 Open Source. Use is subject to license terms.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* Copyright (C) 2025 Bryan Biedenkapp, N2PLL
*
*/
/**
* @file ActivityLog.h
* @ingroup patch
* @file ActivityLog.cpp
* @ingroup patch
*/
#if !defined(__ACTIVITY_LOG_H__)
#define __ACTIVITY_LOG_H__
#include "Defines.h"
#include <string>
// ---------------------------------------------------------------------------
// Global Functions
// ---------------------------------------------------------------------------
/**
* @brief Initializes the activity log.
* @param filePath File path for the log file.
* @param fileRoot Root name for log file.
*/
extern HOST_SW_API bool ActivityLogInitialise(const std::string& filePath, const std::string& fileRoot);
/**
* @brief Finalizes the activity log.
*/
extern HOST_SW_API void ActivityLogFinalise();
/**
* @brief Writes a new entry to the activity log.
* @param msg String format.
*
* This is a variable argument function.
*/
extern HOST_SW_API void ActivityLog(const char* msg, ...);
#endif // __ACTIVITY_LOG_H__

@ -0,0 +1,15 @@
# SPDX-License-Identifier: GPL-2.0-only
#/*
# * Digital Voice Modem - TG Patch
# * GPLv2 Open Source. Use is subject to license terms.
# * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
# *
# * Copyright (C) 2025 Bryan Biedenkapp, N2PLL
# *
# */
file(GLOB patch_SRC
"src/patch/network/*.h"
"src/patch/network/*.cpp"
"src/patch/*.h"
"src/patch/*.cpp"
)

@ -0,0 +1,41 @@
// SPDX-License-Identifier: GPL-2.0-only
/*
* Digital Voice Modem - TG Patch
* GPLv2 Open Source. Use is subject to license terms.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* Copyright (C) 2025 Bryan Biedenkapp, N2PLL
*
*/
/**
* @defgroup patch TG Patch
* @brief Digital Voice Modem - TG Patch
* @details Talkgroup patching utility, this provides facilities to patch two talkgroups together.
* @ingroup patch
*
* @file Defines.h
* @ingroup patch
*/
#if !defined(__DEFINES_H__)
#define __DEFINES_H__
#include "common/Defines.h"
// ---------------------------------------------------------------------------
// Constants
// ---------------------------------------------------------------------------
#undef __PROG_NAME__
#define __PROG_NAME__ "Digital Voice Modem (DVM) TG Patch"
#undef __EXE_NAME__
#define __EXE_NAME__ "patch"
#undef __NETVER__
#define __NETVER__ "PATCH_R" VERSION_MAJOR VERSION_REV VERSION_MINOR
#undef DEFAULT_CONF_FILE
#define DEFAULT_CONF_FILE "patch-config.yml"
#undef DEFAULT_LOCK_FILE
#define DEFAULT_LOCK_FILE "/tmp/dvmpatch.lock"
#endif // __DEFINES_H__

File diff suppressed because it is too large Load Diff

@ -0,0 +1,134 @@
// SPDX-License-Identifier: GPL-2.0-only
/*
* Digital Voice Modem - TG Patch
* GPLv2 Open Source. Use is subject to license terms.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* Copyright (C) 2025 Bryan Biedenkapp, N2PLL
*
*/
/**
* @file HostPatch.h
* @ingroup patch
* @file HostPatch.cpp
* @ingroup patch
*/
#if !defined(__HOST_PATCH_H__)
#define __HOST_PATCH_H__
#include "Defines.h"
#include "common/dmr/data/EmbeddedData.h"
#include "common/dmr/lc/LC.h"
#include "common/dmr/lc/PrivacyLC.h"
#include "common/network/udp/Socket.h"
#include "common/yaml/Yaml.h"
#include "common/Timer.h"
#include "network/PeerNetwork.h"
#include <string>
#include <mutex>
// ---------------------------------------------------------------------------
// Constants
// ---------------------------------------------------------------------------
const uint8_t TX_MODE_DMR = 1U;
const uint8_t TX_MODE_P25 = 2U;
// ---------------------------------------------------------------------------
// Class Declaration
// ---------------------------------------------------------------------------
/**
* @brief This class implements the core service logic.
* @ingroup bridge
*/
class HOST_SW_API HostPatch {
public:
/**
* @brief Initializes a new instance of the HostPatch class.
* @param confFile Full-path to the configuration file.
*/
HostPatch(const std::string& confFile);
/**
* @brief Finalizes a instance of the HostPatch class.
*/
~HostPatch();
/**
* @brief Executes the main host processing loop.
* @returns int Zero if successful, otherwise error occurred.
*/
int run();
private:
const std::string& m_confFile;
yaml::Node m_conf;
network::PeerNetwork* m_network;
uint32_t m_srcTGId;
uint8_t m_srcSlot;
uint32_t m_dstTGId;
uint8_t m_dstSlot;
bool m_twoWayPatch;
std::string m_identity;
uint8_t m_digiMode;
dmr::data::EmbeddedData m_dmrEmbeddedData;
bool m_grantDemand;
bool m_callInProgress;
uint64_t m_rxStartTime;
uint32_t m_rxStreamId;
bool m_running;
bool m_trace;
bool m_debug;
static std::mutex m_networkMutex;
/**
* @brief Reads basic configuration parameters from the INI.
* @returns bool True, if configuration was read successfully, otherwise false.
*/
bool readParams();
/**
* @brief Initializes network connectivity.
* @returns bool True, if network connectivity was initialized, otherwise false.
*/
bool createNetwork();
/**
* @brief Helper to process DMR network traffic.
* @param buffer
* @param length
*/
void processDMRNetwork(uint8_t* buffer, uint32_t length);
/**
* @brief Helper to process P25 network traffic.
* @param buffer
* @param length
*/
void processP25Network(uint8_t* buffer, uint32_t length);
/**
* @brief Entry point to network processing thread.
* @param arg Instance of the thread_t structure.
* @returns void* (Ignore)
*/
static void* threadNetworkProcess(void* arg);
/**
* @brief Helper to reset IMBE buffer with null frames.
* @param data Buffer containing frame data.
* @param encrypted Flag indicating whether or not the data is encrypted.
*/
void resetWithNullAudio(uint8_t* data, bool encrypted);
};
#endif // __HOST_PATCH_H__

@ -0,0 +1,228 @@
// SPDX-License-Identifier: GPL-2.0-only
/*
* Digital Voice Modem - TG Patch
* GPLv2 Open Source. Use is subject to license terms.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* Copyright (C) 2025 Bryan Biedenkapp, N2PLL
*
*/
#include "Defines.h"
#include "common/Log.h"
#include "patch/ActivityLog.h"
#include "PatchMain.h"
#include "HostPatch.h"
using namespace network;
using namespace lookups;
#include <cstdio>
#include <cstdarg>
#include <vector>
#include <signal.h>
// ---------------------------------------------------------------------------
// Macros
// ---------------------------------------------------------------------------
#define IS(s) (::strcmp(argv[i], s) == 0)
// ---------------------------------------------------------------------------
// Global Variables
// ---------------------------------------------------------------------------
#ifndef SIGHUP
#define SIGHUP 1
#endif
int g_signal = 0;
std::string g_progExe = std::string(__EXE_NAME__);
std::string g_iniFile = std::string(DEFAULT_CONF_FILE);
std::string g_lockFile = std::string(DEFAULT_LOCK_FILE);
bool g_foreground = false;
bool g_killed = false;
bool g_hideMessages = false;
uint8_t* g_gitHashBytes = nullptr;
// ---------------------------------------------------------------------------
// Global Functions
// ---------------------------------------------------------------------------
#if !defined(CATCH2_TEST_COMPILATION)
/* Internal signal handler. */
static void sigHandler(int signum)
{
g_signal = signum;
g_killed = true;
}
#endif
/* Helper to print a fatal error message and exit. */
void fatal(const char* msg, ...)
{
char buffer[400U];
::memset(buffer, 0x20U, 400U);
va_list vl;
va_start(vl, msg);
::vsprintf(buffer, msg, vl);
va_end(vl);
::fprintf(stderr, "%s: FATAL PANIC; %s\n", g_progExe.c_str(), buffer);
exit(EXIT_FAILURE);
}
/* Helper to pring usage the command line arguments. (And optionally an error.) */
void usage(const char* message, const char* arg)
{
::fprintf(stdout, __PROG_NAME__ " %s (built %s)\r\n", __VER__, __BUILD__);
::fprintf(stdout, "Copyright (c) 2025 Bryan Biedenkapp, N2PLL and DVMProject (https://github.com/dvmproject) Authors.\n");
::fprintf(stdout, "Portions Copyright (c) 2015-2021 by Jonathan Naylor, G4KLX and others\n\n");
if (message != nullptr) {
::fprintf(stderr, "%s: ", g_progExe.c_str());
::fprintf(stderr, message, arg);
::fprintf(stderr, "\n\n");
}
::fprintf(stdout,
"usage: %s [-vhf]"
"[-c <configuration file>]"
"\n\n"
" -v show version information\n"
" -h show this screen\n"
" -f foreground mode\n"
"\n"
" -c <file> specifies the configuration file to use\n"
"\n"
" -- stop handling options\n",
g_progExe.c_str());
exit(EXIT_FAILURE);
}
/* Helper to validate the command line arguments. */
int checkArgs(int argc, char* argv[])
{
int i, p = 0;
// iterate through arguments
for (i = 1; i <= argc; i++)
{
if (argv[i] == nullptr) {
break;
}
if (*argv[i] != '-') {
continue;
}
else if (IS("--")) {
++p;
break;
}
else if (IS("-f")) {
g_foreground = true;
}
else if (IS("-c")) {
if (argc-- <= 0)
usage("error: %s", "must specify the configuration file to use");
g_iniFile = std::string(argv[++i]);
if (g_iniFile.empty())
usage("error: %s", "configuration file cannot be blank!");
p += 2;
}
else if (IS("-v")) {
::fprintf(stdout, __PROG_NAME__ " %s (built %s)\r\n", __VER__, __BUILD__);
::fprintf(stdout, "Copyright (c) 2025 Bryan Biedenkapp, N2PLL and DVMProject (https://github.com/dvmproject) Authors.\n");
::fprintf(stdout, "Portions Copyright (c) 2015-2021 by Jonathan Naylor, G4KLX and others\n\n");
if (argc == 2)
exit(EXIT_SUCCESS);
}
else if (IS("-h")) {
usage(nullptr, nullptr);
if (argc == 2)
exit(EXIT_SUCCESS);
}
else {
usage("unrecognized option `%s'", argv[i]);
}
}
if (p < 0 || p > argc) {
p = 0;
}
return ++p;
}
// ---------------------------------------------------------------------------
// Program Entry Point
// ---------------------------------------------------------------------------
#if !defined(CATCH2_TEST_COMPILATION)
int main(int argc, char** argv)
{
g_gitHashBytes = new uint8_t[4U];
::memset(g_gitHashBytes, 0x00U, 4U);
uint32_t hash = ::strtoul(__GIT_VER_HASH__, 0, 16);
__SET_UINT32(hash, g_gitHashBytes, 0U);
if (argv[0] != nullptr && *argv[0] != 0)
g_progExe = std::string(argv[0]);
if (argc > 1) {
// check arguments
int i = checkArgs(argc, argv);
if (i < argc) {
argc -= i;
argv += i;
}
else {
argc--;
argv++;
}
}
::signal(SIGINT, sigHandler);
::signal(SIGTERM, sigHandler);
#if !defined(_WIN32)
::signal(SIGHUP, sigHandler);
#endif // !defined(_WIN32)
int ret = 0;
do {
g_signal = 0;
g_killed = false;
HostPatch* patch = new HostPatch(g_iniFile);
ret = patch->run();
delete patch;
if (g_signal == SIGINT)
::LogInfoEx(LOG_HOST, "Exited on receipt of SIGINT");
if (g_signal == SIGTERM)
::LogInfoEx(LOG_HOST, "Exited on receipt of SIGTERM");
if (g_signal == SIGHUP)
::LogInfoEx(LOG_HOST, "Restarting on receipt of SIGHUP");
} while (g_signal == SIGHUP);
::LogFinalise();
::ActivityLogFinalise();
return ret;
}
#endif

@ -0,0 +1,53 @@
// SPDX-License-Identifier: GPL-2.0-only
/*
* Digital Voice Modem - TG Patch
* GPLv2 Open Source. Use is subject to license terms.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* Copyright (C) 2025 Bryan Biedenkapp, N2PLL
*
*/
/**
* @file PatchMain.h
* @ingroup bridge
* @file PatchMain.cpp
* @ingroup bridge
*/
#if !defined(__PATCH_MAIN_H__)
#define __PATCH_MAIN_H__
#include "Defines.h"
#include <string>
// ---------------------------------------------------------------------------
// Externs
// ---------------------------------------------------------------------------
/** @brief */
extern int g_signal;
/** @brief */
extern std::string g_progExe;
/** @brief */
extern std::string g_iniFile;
/** @brief */
extern std::string g_lockFile;
/** @brief (Global) Flag indicating foreground operation. */
extern bool g_foreground;
/** @brief (Global) Flag indicating the FNE should stop immediately. */
extern bool g_killed;
extern uint8_t* g_gitHashBytes;
/**
* @brief Helper to trigger a fatal error message. This will cause the program to terminate
* immediately with an error message.
*
* @param msg String format.
*
* This is a variable argument function.
*/
extern HOST_SW_API void fatal(const char* msg, ...);
#endif // __PATCH_MAIN_H__

@ -0,0 +1,360 @@
// SPDX-License-Identifier: GPL-2.0-only
/*
* Digital Voice Modem - TG Patch
* GPLv2 Open Source. Use is subject to license terms.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* Copyright (C) 2025 Bryan Biedenkapp, N2PLL
*
*/
#include "patch/Defines.h"
#include "common/dmr/data/EMB.h"
#include "common/dmr/lc/FullLC.h"
#include "common/dmr/SlotType.h"
#include "common/network/json/json.h"
#include "common/p25/dfsi/DFSIDefines.h"
#include "common/p25/dfsi/LC.h"
#include "common/Utils.h"
#include "network/PeerNetwork.h"
using namespace network;
#include <cassert>
// ---------------------------------------------------------------------------
// Public Class Members
// ---------------------------------------------------------------------------
/* Initializes a new instance of the PeerNetwork class. */
PeerNetwork::PeerNetwork(const std::string& address, uint16_t port, uint16_t localPort, uint32_t peerId, const std::string& password,
bool duplex, bool debug, bool dmr, bool p25, bool nxdn, bool slot1, bool slot2, bool allowActivityTransfer, bool allowDiagnosticTransfer, bool updateLookup, bool saveLookup) :
Network(address, port, localPort, peerId, password, duplex, debug, dmr, p25, nxdn, slot1, slot2, allowActivityTransfer, allowDiagnosticTransfer, updateLookup, saveLookup)
{
assert(!address.empty());
assert(port > 0U);
assert(!password.empty());
}
/* Writes P25 LDU1 frame data to the network. */
bool PeerNetwork::writeP25LDU1(const p25::lc::LC& control, const p25::data::LowSpeedData& lsd, const uint8_t* data, p25::defines::FrameType::E frameType)
{
if (m_status != NET_STAT_RUNNING && m_status != NET_STAT_MST_RUNNING)
return false;
bool resetSeq = false;
if (m_p25StreamId == 0U) {
resetSeq = true;
m_p25StreamId = createStreamId();
}
uint32_t messageLength = 0U;
UInt8Array message = createP25_LDU1Message_Raw(messageLength, control, lsd, data, frameType);
if (message == nullptr) {
return false;
}
return writeMaster({ NET_FUNC::PROTOCOL, NET_SUBFUNC::PROTOCOL_SUBFUNC_P25 }, message.get(), messageLength, pktSeq(resetSeq), m_p25StreamId);
}
/* Writes P25 LDU2 frame data to the network. */
bool PeerNetwork::writeP25LDU2(const p25::lc::LC& control, const p25::data::LowSpeedData& lsd, const uint8_t* data)
{
if (m_status != NET_STAT_RUNNING && m_status != NET_STAT_MST_RUNNING)
return false;
bool resetSeq = false;
if (m_p25StreamId == 0U) {
resetSeq = true;
m_p25StreamId = createStreamId();
}
uint32_t messageLength = 0U;
UInt8Array message = createP25_LDU2Message_Raw(messageLength, control, lsd, data);
if (message == nullptr) {
return false;
}
return writeMaster({ NET_FUNC::PROTOCOL, NET_SUBFUNC::PROTOCOL_SUBFUNC_P25 }, message.get(), messageLength, pktSeq(resetSeq), m_p25StreamId);
}
/* Helper to send a DMR terminator with LC message. */
void PeerNetwork::writeDMRTerminator(dmr::data::NetData& data, uint32_t* seqNo, uint8_t* dmrN, dmr::data::EmbeddedData& embeddedData)
{
using namespace dmr;
using namespace dmr::defines;
uint8_t n = (uint8_t)((*seqNo - 3U) % 6U);
uint32_t fill = 6U - n;
uint8_t* buffer = nullptr;
if (n > 0U) {
for (uint32_t i = 0U; i < fill; i++) {
// generate DMR AMBE data
buffer = new uint8_t[DMR_FRAME_LENGTH_BYTES];
::memcpy(buffer, SILENCE_DATA, DMR_FRAME_LENGTH_BYTES);
uint8_t lcss = embeddedData.getData(buffer, n);
// generated embedded signalling
data::EMB emb = data::EMB();
emb.setColorCode(0U);
emb.setLCSS(lcss);
emb.encode(buffer);
// generate DMR network frame
data.setData(buffer);
writeDMR(data);
seqNo++;
dmrN++;
delete[] buffer;
}
}
buffer = new uint8_t[DMR_FRAME_LENGTH_BYTES];
// generate DMR LC
lc::LC dmrLC = lc::LC();
dmrLC.setFLCO(FLCO::GROUP);
dmrLC.setSrcId(data.getSrcId());
dmrLC.setDstId(data.getDstId());
// generate the Slot Type
SlotType slotType = SlotType();
slotType.setDataType(DataType::TERMINATOR_WITH_LC);
slotType.encode(buffer);
lc::FullLC fullLC = lc::FullLC();
fullLC.encode(dmrLC, buffer, DataType::TERMINATOR_WITH_LC);
// generate DMR network frame
data.setData(buffer);
writeDMR(data);
seqNo = 0;
dmrN = 0;
}
// ---------------------------------------------------------------------------
// Protected Class Members
// ---------------------------------------------------------------------------
/* Writes configuration to the network. */
bool PeerNetwork::writeConfig()
{
if (m_loginStreamId == 0U) {
LogWarning(LOG_NET, "BUGBUG: tried to write network authorisation with no stream ID?");
return false;
}
const char* software = __NETVER__;
json::object config = json::object();
// identity and frequency
config["identity"].set<std::string>(m_metadata->identity); // Identity
config["rxFrequency"].set<uint32_t>(m_metadata->rxFrequency); // Rx Frequency
config["txFrequency"].set<uint32_t>(m_metadata->txFrequency); // Tx Frequency
// system info
json::object sysInfo = json::object();
sysInfo["latitude"].set<float>(m_metadata->latitude); // Latitude
sysInfo["longitude"].set<float>(m_metadata->longitude); // Longitude
sysInfo["height"].set<int>(m_metadata->height); // Height
sysInfo["location"].set<std::string>(m_metadata->location); // Location
config["info"].set<json::object>(sysInfo);
// channel data
json::object channel = json::object();
channel["txPower"].set<uint32_t>(m_metadata->power); // Tx Power
channel["txOffsetMhz"].set<float>(m_metadata->txOffsetMhz); // Tx Offset (Mhz)
channel["chBandwidthKhz"].set<float>(m_metadata->chBandwidthKhz); // Ch. Bandwidth (khz)
channel["channelId"].set<uint8_t>(m_metadata->channelId); // Channel ID
channel["channelNo"].set<uint32_t>(m_metadata->channelNo); // Channel No
config["channel"].set<json::object>(channel);
// RCON
json::object rcon = json::object();
rcon["password"].set<std::string>(m_metadata->restApiPassword); // REST API Password
rcon["port"].set<uint16_t>(m_metadata->restApiPort); // REST API Port
config["rcon"].set<json::object>(rcon);
config["software"].set<std::string>(std::string(software)); // Software ID
json::value v = json::value(config);
std::string json = v.serialize();
CharArray __buffer = std::make_unique<char[]>(json.length() + 9U);
char* buffer = __buffer.get();
::memcpy(buffer + 0U, TAG_REPEATER_CONFIG, 4U);
::snprintf(buffer + 8U, json.length() + 1U, "%s", json.c_str());
if (m_debug) {
Utils::dump(1U, "Network Message, Configuration", (uint8_t*)buffer, json.length() + 8U);
}
return writeMaster({ NET_FUNC::RPTC, NET_SUBFUNC::NOP }, (uint8_t*)buffer, json.length() + 8U, RTP_END_OF_CALL_SEQ, m_loginStreamId);
}
// ---------------------------------------------------------------------------
// Private Class Members
// ---------------------------------------------------------------------------
/* Creates an P25 LDU1 frame message. */
UInt8Array PeerNetwork::createP25_LDU1Message_Raw(uint32_t& length, const p25::lc::LC& control, const p25::data::LowSpeedData& lsd,
const uint8_t* data, p25::defines::FrameType::E frameType)
{
using namespace p25::defines;
using namespace p25::dfsi::defines;
assert(data != nullptr);
p25::dfsi::LC dfsiLC = p25::dfsi::LC(control, lsd);
uint8_t* buffer = new uint8_t[P25_LDU1_PACKET_LENGTH + PACKET_PAD];
::memset(buffer, 0x00U, P25_LDU1_PACKET_LENGTH + PACKET_PAD);
// construct P25 message header
createP25_MessageHdr(buffer, DUID::LDU1, control, lsd, frameType);
// pack DFSI data
uint32_t count = MSG_HDR_SIZE;
uint8_t imbe[RAW_IMBE_LENGTH_BYTES];
dfsiLC.setFrameType(DFSIFrameType::LDU1_VOICE1);
::memcpy(imbe, data + 10U, RAW_IMBE_LENGTH_BYTES);
dfsiLC.encodeLDU1(buffer + 24U, imbe);
count += DFSI_LDU1_VOICE1_FRAME_LENGTH_BYTES;
dfsiLC.setFrameType(DFSIFrameType::LDU1_VOICE2);
::memcpy(imbe, data + 26U, RAW_IMBE_LENGTH_BYTES);
dfsiLC.encodeLDU1(buffer + 46U, imbe);
count += DFSI_LDU1_VOICE2_FRAME_LENGTH_BYTES;
dfsiLC.setFrameType(DFSIFrameType::LDU1_VOICE3);
::memcpy(imbe, data + 55U, RAW_IMBE_LENGTH_BYTES);
dfsiLC.encodeLDU1(buffer + 60U, imbe);
count += DFSI_LDU1_VOICE3_FRAME_LENGTH_BYTES;
dfsiLC.setFrameType(DFSIFrameType::LDU1_VOICE4);
::memcpy(imbe, data + 80U, RAW_IMBE_LENGTH_BYTES);
dfsiLC.encodeLDU1(buffer + 77U, imbe);
count += DFSI_LDU1_VOICE4_FRAME_LENGTH_BYTES;
dfsiLC.setFrameType(DFSIFrameType::LDU1_VOICE5);
::memcpy(imbe, data + 105U, RAW_IMBE_LENGTH_BYTES);
dfsiLC.encodeLDU1(buffer + 94U, imbe);
count += DFSI_LDU1_VOICE5_FRAME_LENGTH_BYTES;
dfsiLC.setFrameType(DFSIFrameType::LDU1_VOICE6);
::memcpy(imbe, data + 130U, RAW_IMBE_LENGTH_BYTES);
dfsiLC.encodeLDU1(buffer + 111U, imbe);
count += DFSI_LDU1_VOICE6_FRAME_LENGTH_BYTES;
dfsiLC.setFrameType(DFSIFrameType::LDU1_VOICE7);
::memcpy(imbe, data + 155U, RAW_IMBE_LENGTH_BYTES);
dfsiLC.encodeLDU1(buffer + 128U, imbe);
count += DFSI_LDU1_VOICE7_FRAME_LENGTH_BYTES;
dfsiLC.setFrameType(DFSIFrameType::LDU1_VOICE8);
::memcpy(imbe, data + 180U, RAW_IMBE_LENGTH_BYTES);
dfsiLC.encodeLDU1(buffer + 145U, imbe);
count += DFSI_LDU1_VOICE8_FRAME_LENGTH_BYTES;
dfsiLC.setFrameType(DFSIFrameType::LDU1_VOICE9);
::memcpy(imbe, data + 204U, RAW_IMBE_LENGTH_BYTES);
dfsiLC.encodeLDU1(buffer + 162U, imbe);
count += DFSI_LDU1_VOICE9_FRAME_LENGTH_BYTES;
buffer[23U] = count;
if (m_debug)
Utils::dump(1U, "Network Message, P25 LDU1", buffer, (P25_LDU1_PACKET_LENGTH + PACKET_PAD));
length = (P25_LDU1_PACKET_LENGTH + PACKET_PAD);
return UInt8Array(buffer);
}
/* Creates an P25 LDU2 frame message. */
UInt8Array PeerNetwork::createP25_LDU2Message_Raw(uint32_t& length, const p25::lc::LC& control, const p25::data::LowSpeedData& lsd,
const uint8_t* data)
{
using namespace p25::defines;
using namespace p25::dfsi::defines;
assert(data != nullptr);
p25::dfsi::LC dfsiLC = p25::dfsi::LC(control, lsd);
uint8_t* buffer = new uint8_t[P25_LDU2_PACKET_LENGTH + PACKET_PAD];
::memset(buffer, 0x00U, P25_LDU2_PACKET_LENGTH + PACKET_PAD);
// construct P25 message header
createP25_MessageHdr(buffer, DUID::LDU2, control, lsd, FrameType::DATA_UNIT);
// pack DFSI data
uint32_t count = MSG_HDR_SIZE;
uint8_t imbe[RAW_IMBE_LENGTH_BYTES];
dfsiLC.setFrameType(DFSIFrameType::LDU2_VOICE10);
::memcpy(imbe, data + 10U, RAW_IMBE_LENGTH_BYTES);
dfsiLC.encodeLDU2(buffer + 24U, imbe);
count += DFSI_LDU2_VOICE10_FRAME_LENGTH_BYTES;
dfsiLC.setFrameType(DFSIFrameType::LDU2_VOICE11);
::memcpy(imbe, data + 26U, RAW_IMBE_LENGTH_BYTES);
dfsiLC.encodeLDU2(buffer + 46U, imbe);
count += DFSI_LDU2_VOICE11_FRAME_LENGTH_BYTES;
dfsiLC.setFrameType(DFSIFrameType::LDU2_VOICE12);
::memcpy(imbe, data + 55U, RAW_IMBE_LENGTH_BYTES);
dfsiLC.encodeLDU2(buffer + 60U, imbe);
count += DFSI_LDU2_VOICE12_FRAME_LENGTH_BYTES;
dfsiLC.setFrameType(DFSIFrameType::LDU2_VOICE13);
::memcpy(imbe, data + 80U, RAW_IMBE_LENGTH_BYTES);
dfsiLC.encodeLDU2(buffer + 77U, imbe);
count += DFSI_LDU2_VOICE13_FRAME_LENGTH_BYTES;
dfsiLC.setFrameType(DFSIFrameType::LDU2_VOICE14);
::memcpy(imbe, data + 105U, RAW_IMBE_LENGTH_BYTES);
dfsiLC.encodeLDU2(buffer + 94U, imbe);
count += DFSI_LDU2_VOICE14_FRAME_LENGTH_BYTES;
dfsiLC.setFrameType(DFSIFrameType::LDU2_VOICE15);
::memcpy(imbe, data + 130U, RAW_IMBE_LENGTH_BYTES);
dfsiLC.encodeLDU2(buffer + 111U, imbe);
count += DFSI_LDU2_VOICE15_FRAME_LENGTH_BYTES;
dfsiLC.setFrameType(DFSIFrameType::LDU2_VOICE16);
::memcpy(imbe, data + 155U, RAW_IMBE_LENGTH_BYTES);
dfsiLC.encodeLDU2(buffer + 128U, imbe);
count += DFSI_LDU2_VOICE16_FRAME_LENGTH_BYTES;
dfsiLC.setFrameType(DFSIFrameType::LDU2_VOICE17);
::memcpy(imbe, data + 180U, RAW_IMBE_LENGTH_BYTES);
dfsiLC.encodeLDU2(buffer + 145U, imbe);
count += DFSI_LDU2_VOICE17_FRAME_LENGTH_BYTES;
dfsiLC.setFrameType(DFSIFrameType::LDU2_VOICE18);
::memcpy(imbe, data + 204U, RAW_IMBE_LENGTH_BYTES);
dfsiLC.encodeLDU2(buffer + 162U, imbe);
count += DFSI_LDU2_VOICE18_FRAME_LENGTH_BYTES;
buffer[23U] = count;
if (m_debug)
Utils::dump(1U, "Network Message, P25 LDU2", buffer, (P25_LDU2_PACKET_LENGTH + PACKET_PAD));
length = (P25_LDU2_PACKET_LENGTH + PACKET_PAD);
return UInt8Array(buffer);
}

@ -0,0 +1,128 @@
// SPDX-License-Identifier: GPL-2.0-only
/*
* Digital Voice Modem - TG Patch
* GPLv2 Open Source. Use is subject to license terms.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* Copyright (C) 2025 Bryan Biedenkapp, N2PLL
*
*/
/**
* @defgroup patch_network Networking
* @brief Implementation for the bridge networking.
* @ingroup patch
*
* @file PeerNetwork.h
* @ingroup patch_network
* @file PeerNetwork.cpp
* @ingroup patch_network
*/
#if !defined(__PEER_NETWORK_H__)
#define __PEER_NETWORK_H__
#include "Defines.h"
#include "common/dmr/data/EmbeddedData.h"
#include "common/network/Network.h"
#include <string>
#include <cstdint>
namespace network
{
// ---------------------------------------------------------------------------
// Class Declaration
// Implements the core peer networking logic.
// ---------------------------------------------------------------------------
class HOST_SW_API PeerNetwork : public Network {
public:
/**
* @brief Initializes a new instance of the PeerNetwork class.
* @param address Network Hostname/IP address to connect to.
* @param port Network port number.
* @param local
* @param peerId Unique ID on the network.
* @param password Network authentication password.
* @param duplex Flag indicating full-duplex operation.
* @param debug Flag indicating whether network debug is enabled.
* @param dmr Flag indicating whether DMR is enabled.
* @param p25 Flag indicating whether P25 is enabled.
* @param nxdn Flag indicating whether NXDN is enabled.
* @param slot1 Flag indicating whether DMR slot 1 is enabled for network traffic.
* @param slot2 Flag indicating whether DMR slot 2 is enabled for network traffic.
* @param allowActivityTransfer Flag indicating that the system activity logs will be sent to the network.
* @param allowDiagnosticTransfer Flag indicating that the system diagnostic logs will be sent to the network.
* @param updateLookup Flag indicating that the system will accept radio ID and talkgroup ID lookups from the network.
*/
PeerNetwork(const std::string& address, uint16_t port, uint16_t localPort, uint32_t peerId, const std::string& password,
bool duplex, bool debug, bool dmr, bool p25, bool nxdn, bool slot1, bool slot2, bool allowActivityTransfer, bool allowDiagnosticTransfer, bool updateLookup, bool saveLookup);
/**
* @brief Writes P25 LDU1 frame data to the network.
* @param[in] control Instance of p25::lc::LC containing link control data.
* @param[in] lsd Instance of p25::data::LowSpeedData containing low speed data.
* @param[in] data Buffer containing P25 LDU1 data to send.
* @param[in] frameType DVM P25 frame type.
* @returns bool True, if message was sent, otherwise false.
*/
bool writeP25LDU1(const p25::lc::LC& control, const p25::data::LowSpeedData& lsd, const uint8_t* data,
p25::defines::FrameType::E frameType) override;
/**
* @brief Writes P25 LDU2 frame data to the network.
* @param[in] control Instance of p25::lc::LC containing link control data.
* @param[in] lsd Instance of p25::data::LowSpeedData containing low speed data.
* @param[in] data Buffer containing P25 LDU2 data to send.
* @returns bool True, if message was sent, otherwise false.
*/
bool writeP25LDU2(const p25::lc::LC& control, const p25::data::LowSpeedData& lsd, const uint8_t* data) override;
/**
* @brief Helper to send a DMR terminator with LC message.
* @param data
* @param seqNo
* @param dmrN
* @param embeddedData
*/
void writeDMRTerminator(dmr::data::NetData& data, uint32_t* seqNo, uint8_t* dmrN, dmr::data::EmbeddedData& embeddedData);
protected:
/**
* @brief Writes configuration to the network.
* @returns bool True, if configuration was sent, otherwise false.
*/
bool writeConfig() override;
private:
/**
* @brief Creates an P25 LDU1 frame message.
*
* The data packed into a P25 LDU1 frame message is near standard DFSI messaging, just instead of
* 9 individual frames, they are packed into a single message one right after another.
*
* @param[out] length Length of network message buffer.
* @param[in] control Instance of p25::lc::LC containing link control data.
* @param[in] lsd Instance of p25::data::LowSpeedData containing low speed data.
* @param[in] data Buffer containing P25 LDU1 data to send.
* @param[in] frameType DVM P25 frame type.
* @returns UInt8Array Buffer containing the built network message.
*/
UInt8Array createP25_LDU1Message_Raw(uint32_t& length, const p25::lc::LC& control, const p25::data::LowSpeedData& lsd,
const uint8_t* data, p25::defines::FrameType::E frameType);
/**
* @brief Creates an P25 LDU2 frame message.
*
* The data packed into a P25 LDU2 frame message is near standard DFSI messaging, just instead of
* 9 individual frames, they are packed into a single message one right after another.
*
* @param[out] length Length of network message buffer.
* @param[in] control Instance of p25::lc::LC containing link control data.
* @param[in] lsd Instance of p25::data::LowSpeedData containing low speed data.
* @param[in] data Buffer containing P25 LDU2 data to send.
* @returns UInt8Array Buffer containing the built network message.
*/
UInt8Array createP25_LDU2Message_Raw(uint32_t& length, const p25::lc::LC& control, const p25::data::LowSpeedData& lsd,
const uint8_t* data);
};
} // namespace network
#endif // __PEER_NETWORK_H__

@ -39,7 +39,7 @@ if [ -z $2 ]; then
fi
COMMAND="${R_PATH}/bin/dvmfne -c ${R_PATH}/${CONFIG}"
nice -n -20 ${COMMAND}
nice -n -10 ${COMMAND}
if [ -e /tmp/${CONFIG}.pid ]; then rm -f /tmp/${CONFIG}.pid; fi

Loading…
Cancel
Save

Powered by TurnKey Linux.