diff --git a/CMakeLists.txt b/CMakeLists.txt index 13b81907..30f17591 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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\" diff --git a/README.md b/README.md index 02f86eb3..5e202475 100644 --- a/README.md +++ b/README.md @@ -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 ][--remote [-a
] [-p ]] +usage: ./dvmhost [-vhdf] [--syslog] [--setup] [--cal][--boot] [-c ] [--remote [-a
] [-p ]] -v show version information -h show this screen @@ -229,13 +236,17 @@ usage: ./dvmhost [-vhdf][--syslog][--setup][-c ][--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 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: ... ... ``` +### dvmpatch Command Line Parameters + +``` +usage: ./dvmpatch [-vhf][-c ] + + -v show version information + -h show this screen + -f foreground mode + + -c specifies the configuration file to use + + -- stop handling options +``` + ### dvmcmd Command Line Parameters ``` diff --git a/configs/bridge-config.example.yml b/configs/bridge-config.example.yml index 1a918baa..36fc829a 100644 --- a/configs/bridge-config.example.yml +++ b/configs/bridge-config.example.yml @@ -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 diff --git a/configs/config.example.yml b/configs/config.example.yml index 4f733792..96e7e68c 100644 --- a/configs/config.example.yml +++ b/configs/config.example.yml @@ -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. diff --git a/configs/fne-config.example.yml b/configs/fne-config.example.yml index 66594e5a..72b5c900 100644 --- a/configs/fne-config.example.yml +++ b/configs/fne-config.example.yml @@ -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 diff --git a/configs/patch-config.example.yml b/configs/patch-config.example.yml new file mode 100644 index 00000000..d2997368 --- /dev/null +++ b/configs/patch-config.example.yml @@ -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 diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index a55f5332..489fbfd4 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -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) diff --git a/src/CompilerOptions.cmake b/src/CompilerOptions.cmake index 0fad339c..6c667071 100644 --- a/src/CompilerOptions.cmake +++ b/src/CompilerOptions.cmake @@ -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) diff --git a/src/bridge/HostBridge.cpp b/src/bridge/HostBridge.cpp index b26c62bc..e4cd1c9e 100644 --- a/src/bridge/HostBridge.cpp +++ b/src/bridge/HostBridge.cpp @@ -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(); m_udpUseULaw = networkConf["udpUseULaw"].as(false); m_udpUsrp = networkConf["udpUsrp"].as(false); + m_udpInterFrameDelay = (uint8_t)networkConf["udpInterFrameDelay"].as(0U); + m_udpJitter = (uint16_t)networkConf["udpJitter"].as(200U); m_udpSilenceDuringHang = networkConf["udpHangSilence"].as(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; + NetPacketRequest* req = new NetPacketRequest(); + req->pcm = new uint8_t[pcmLength]; + ::memset(req->pcm, 0x00U, pcmLength); + ::memcpy(req->pcm, pcm, pcmLength); - 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; - } + req->pcmLength = pcmLength; - m_udpSrcId = udpSrcId; - } else { - if (m_udpSrcId == 0U) { - m_udpSrcId = m_srcId; - } - } - } else { - m_udpSrcId = m_srcId; - } - } else { - m_udpSrcId = m_srcId; - } - - std::lock_guard lock(m_audioMutex); + req->srcId = m_srcId; + req->dstId = m_dstId; - 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); - - 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::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; - } - } - else { - for (uint32_t pcmIdx = 0; pcmIdx < pcmLength; pcmIdx += 2) { - samples[smpIdx] = (short)((pcm[pcmIdx + 1] << 8) + pcm[pcmIdx + 0]); - smpIdx++; + // if this is our first message, timestamp is just now + the jitter buffer offset in ms + if (m_lastUdpFrameTime == 0U) { + pktTime = now + m_udpJitter; } - } - - 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; - } + 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 { + pktTime = m_lastUdpFrameTime + 20U; } } - m_udpCallClock.stop(); - m_udpDropTime.stop(); - - if (!m_udpDropTime.isRunning()) - m_udpDropTime.start(); - m_udpCallClock.start(); + req->pktRxTime = pktTime; + m_lastUdpFrameTime = pktTime; + } else { + req->pktRxTime = now; } - // 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; - } + if (m_udpMetadata) { + req->srcId = __GET_UINT32(buffer, pcmLength + 8U); } + + 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(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(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::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 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) diff --git a/src/bridge/HostBridge.h b/src/bridge/HostBridge.h index bb35a522..398935e8 100644 --- a/src/bridge/HostBridge.h +++ b/src/bridge/HostBridge.h @@ -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 -#include -#include #include #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 m_inputAudio; RingBuffer m_outputAudio; + concurrent::deque 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. diff --git a/src/common/CMakeLists.txt b/src/common/CMakeLists.txt index de41287e..a7d394f4 100644 --- a/src/common/CMakeLists.txt +++ b/src/common/CMakeLists.txt @@ -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" diff --git a/src/common/Defines.h b/src/common/Defines.h index dc7bfdba..6dce8186 100644 --- a/src/common/Defines.h +++ b/src/common/Defines.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__ ")" diff --git a/src/common/Thread.h b/src/common/Thread.h index 66a0c81e..9b5cf31b 100644 --- a/src/common/Thread.h +++ b/src/common/Thread.h @@ -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. diff --git a/src/common/ThreadPool.cpp b/src/common/ThreadPool.cpp new file mode 100644 index 00000000..92ea7473 --- /dev/null +++ b/src/common/ThreadPool.cpp @@ -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 +#include +#if !defined(_WIN32) +#include +#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 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 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(task)); + } + + m_cond.notify_one(); + return true; +} + +/* Starts the thread pool. */ + +void ThreadPool::start() +{ + // scope is intentional + { + std::unique_lock 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 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 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) +} diff --git a/src/common/ThreadPool.h b/src/common/ThreadPool.h new file mode 100644 index 00000000..8b9253c0 --- /dev/null +++ b/src/common/ThreadPool.h @@ -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 +#include +#include +#include +#include + +// --------------------------------------------------------------------------- +// 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 + ThreadPoolTask(F&& f, Args&&... args) + { + task = std::bind(std::forward(f), std::forward(args)...); + } + virtual ~ThreadPoolTask() = default; + + /** + * @brief Starts the task function. + */ + void run() { task(); } + +private: + std::function task; +}; + +/** + * @brief + * @tparam F + * @tparam Args + * @param f + * @param args + * @return ThreadPoolTask* + */ +template +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 m_workers; + std::queue> 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__ diff --git a/src/common/concurrent/concurrent_lock.h b/src/common/concurrent/concurrent_lock.h new file mode 100644 index 00000000..0352d3a2 --- /dev/null +++ b/src/common/concurrent/concurrent_lock.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 +#include + +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__ diff --git a/src/common/concurrent/deque.h b/src/common/concurrent/deque.h new file mode 100644 index 00000000..9af8f3d9 --- /dev/null +++ b/src/common/concurrent/deque.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 +#include + +namespace concurrent +{ + // --------------------------------------------------------------------------- + // Class Declaration + // --------------------------------------------------------------------------- + + /** + * @brief Thread-safe std::deque. + * @ingroup concurrency + */ + template + class deque : public concurrent_lock + { + using __std = std::deque; + 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& 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& 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& Underlying deque. + */ + std::deque& get() + { + __spinlock(); + return m_deque; + } + /** + * @brief Gets the underlying deque. + * @returns const std::deque& Underlying deque. + */ + const std::deque& get() const + { + __spinlock(); + return m_deque; + } + + private: + std::deque m_deque; + }; +} // namespace concurrent + +#endif // __CONCURRENCY_DEQUE_H__ diff --git a/src/common/concurrent/map.h b/src/common/concurrent/map.h new file mode 100644 index 00000000..83116080 --- /dev/null +++ b/src/common/concurrent/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 map.h + * @ingroup concurrency + */ +#if !defined(__CONCURRENCY_MAP_H__) +#define __CONCURRENCY_MAP_H__ + +#include "common/concurrent/concurrent_lock.h" +#include "common/Thread.h" + +#include +#include + +namespace concurrent +{ + // --------------------------------------------------------------------------- + // Class Declaration + // --------------------------------------------------------------------------- + + /** + * @brief Thread-safe std::map. + * @ingroup concurrency + */ + template + class map : public concurrent_lock + { + using __std = std::map; + 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& 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& 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& Underlying map. + */ + std::map& get() + { + __spinlock(); + return m_map; + } + /** + * @brief Gets the underlying map. + * @returns const std::map& Underlying map. + */ + const std::map& get() const + { + __spinlock(); + return m_map; + } + + private: + std::map m_map; + }; +} // namespace concurrent + +#endif // __CONCURRENCY_MAP_H__ diff --git a/src/common/concurrent/unordered_map.h b/src/common/concurrent/unordered_map.h new file mode 100644 index 00000000..0a131c38 --- /dev/null +++ b/src/common/concurrent/unordered_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 +#include + +namespace concurrent +{ + // --------------------------------------------------------------------------- + // Class Declaration + // --------------------------------------------------------------------------- + + /** + * @brief Thread-safe std::unordered_map. + * @ingroup concurrency + */ + template + class unordered_map : public concurrent_lock + { + using __std = std::unordered_map; + 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& 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& 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& Underlying unordered_map. + */ + std::unordered_map& get() + { + __spinlock(); + return m_map; + } + /** + * @brief Gets the underlying unordered_map. + * @returns const std::unordered_map& Underlying unordered_map. + */ + const std::unordered_map& get() const + { + __spinlock(); + return m_map; + } + + private: + std::unordered_map m_map; + }; +} // namespace concurrent + +#endif // __CONCURRENCY_UNORDERED_MAP_H__ diff --git a/src/common/concurrent/vector.h b/src/common/concurrent/vector.h new file mode 100644 index 00000000..a365bf33 --- /dev/null +++ b/src/common/concurrent/vector.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 +#include + +namespace concurrent +{ + // --------------------------------------------------------------------------- + // Class Declaration + // --------------------------------------------------------------------------- + + /** + * @brief Thread-safe std::vector. + * @ingroup concurrency + */ + template + class vector : public concurrent_lock + { + using __std = std::vector; + 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& 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& 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& Underlying vector. + */ + std::vector& get() + { + __spinlock(); + return m_vector; + } + /** + * @brief Gets the underlying vector. + * @returns const std::vector& Underlying vector. + */ + const std::vector& get() const + { + __spinlock(); + return m_vector; + } + + private: + std::vector m_vector; + }; +} // namespace concurrent + +#endif // __CONCURRENCY_VECTOR_H__ diff --git a/src/common/lookups/AffiliationLookup.cpp b/src/common/lookups/AffiliationLookup.cpp index 099c8365..d0b53033 100644 --- a/src/common/lookups/AffiliationLookup.cpp +++ b/src/common/lookups/AffiliationLookup.cpp @@ -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 lock(m_mutex); - if (!m_chLookup->isRFChAvailable()) { return false; } @@ -336,8 +338,6 @@ void AffiliationLookup::touchGrant(uint32_t dstId) return; } - std::lock_guard 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 gntsToRel = std::vector(); 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,17 +434,19 @@ 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 (...) { - return false; } + m_grantChTable.unlock(); + + return false; } /* Helper to determine if the destination ID is network granted. */ @@ -459,13 +457,20 @@ bool AffiliationLookup::isGroup(uint32_t dstId) const return true; } - // lookup dynamic channel grant table entry - try { - bool uu = m_uuGrantedTable.at(dstId); - return !uu; - } catch (...) { - return true; + // 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; + } } + m_uuGrantedTable.unlock(); + + return true; } /* Helper to determine if the destination ID is network granted. */ @@ -476,13 +481,20 @@ bool AffiliationLookup::isNetGranted(uint32_t dstId) const return false; } - // lookup dynamic channel grant table entry - try { - bool net = m_netGrantedTable.at(dstId); - return net; - } catch (...) { - return false; + // 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; + } } + 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 lock(m_mutex); + m_grantChTable.spinlock(); // clock all the grant timers + m_grantChTable.lock(false); std::vector gntsToRel = std::vector(); 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 unitsToDereg = std::vector(); 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) { diff --git a/src/common/lookups/AffiliationLookup.h b/src/common/lookups/AffiliationLookup.h index 2c7ac586..23893914 100644 --- a/src/common/lookups/AffiliationLookup.h +++ b/src/common/lookups/AffiliationLookup.h @@ -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 -#include #include -#include #include -#include namespace lookups { @@ -66,7 +65,7 @@ namespace lookups * @brief Gets the unit registration table. * @returns std::vector Unit Registration Table. */ - std::vector unitRegTable() const { return m_unitRegTable; } + std::vector 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 Group Affiliation Table. */ - std::unordered_map grpAffTable() const { return m_grpAffTable; } + std::unordered_map 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 Channel Grant Table. */ - std::unordered_map grantTable() const { return m_grantChTable; } + std::unordered_map 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 m_unitRegTable; - std::unordered_map m_unitRegTimers; - std::unordered_map m_grpAffTable; + concurrent::vector m_unitRegTable; + concurrent::unordered_map m_unitRegTimers; + concurrent::unordered_map m_grpAffTable; - std::unordered_map m_grantChTable; - std::unordered_map m_grantSrcIdTable; - std::unordered_map m_uuGrantedTable; - std::unordered_map m_netGrantedTable; - std::unordered_map m_grantTimers; + concurrent::unordered_map m_grantChTable; + concurrent::unordered_map m_grantSrcIdTable; + concurrent::unordered_map m_uuGrantedTable; + concurrent::unordered_map m_netGrantedTable; + concurrent::unordered_map m_grantTimers; // chNo dstId slot std::function m_releaseGrant; @@ -300,8 +298,6 @@ namespace lookups bool m_disableUnitRegTimeout; bool m_verbose; - - static std::mutex m_mutex; }; } // namespace lookups diff --git a/src/common/lookups/ChannelLookup.cpp b/src/common/lookups/ChannelLookup.cpp index fed733be..5f081e23 100644 --- a/src/common/lookups/ChannelLookup.cpp +++ b/src/common/lookups/ChannelLookup.cpp @@ -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 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 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 lock(m_mutex); - if (chNo == 0U) { return false; } diff --git a/src/common/lookups/ChannelLookup.h b/src/common/lookups/ChannelLookup.h index b9207061..c5ce28b5 100644 --- a/src/common/lookups/ChannelLookup.h +++ b/src/common/lookups/ChannelLookup.h @@ -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 -#include -#include -#include -#include -#include namespace lookups { @@ -163,7 +160,7 @@ namespace lookups * @brief Gets the RF channel data table. * @returns std::unordered_map RF channel data table. */ - std::unordered_map rfChDataTable() const { return m_rfChDataTable; } + std::unordered_map 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 RF channel table. */ - std::vector rfChTable() const { return m_rfChTable; } + std::vector 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 m_rfChTable; - std::unordered_map m_rfChDataTable; - - static std::mutex m_mutex; + concurrent::vector m_rfChTable; + concurrent::unordered_map m_rfChDataTable; }; } // namespace lookups diff --git a/src/common/lookups/PeerListLookup.cpp b/src/common/lookups/PeerListLookup.cpp index 60044ef0..0554c015 100644 --- a/src/common/lookups/PeerListLookup.cpp +++ b/src/common/lookups/PeerListLookup.cpp @@ -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) \ diff --git a/src/common/lookups/PeerListLookup.h b/src/common/lookups/PeerListLookup.h index 527102a1..8738a1e1 100644 --- a/src/common/lookups/PeerListLookup.h +++ b/src/common/lookups/PeerListLookup.h @@ -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 diff --git a/src/common/lookups/RadioIdLookup.cpp b/src/common/lookups/RadioIdLookup.cpp index b0c0f84c..9aba2377 100644 --- a/src/common/lookups/RadioIdLookup.cpp +++ b/src/common/lookups/RadioIdLookup.cpp @@ -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) \ diff --git a/src/common/lookups/RadioIdLookup.h b/src/common/lookups/RadioIdLookup.h index 2c859f2d..e26d800f 100644 --- a/src/common/lookups/RadioIdLookup.h +++ b/src/common/lookups/RadioIdLookup.h @@ -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 diff --git a/src/common/lookups/TalkgroupRulesLookup.cpp b/src/common/lookups/TalkgroupRulesLookup.cpp index f85c6df0..90fa0148 100644 --- a/src/common/lookups/TalkgroupRulesLookup.cpp +++ b/src/common/lookups/TalkgroupRulesLookup.cpp @@ -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 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 lock(m_mutex); auto it = std::find_if(m_groupVoice.begin(), m_groupVoice.end(), [&](TalkgroupRuleGroupVoice x) { diff --git a/src/common/lookups/TalkgroupRulesLookup.h b/src/common/lookups/TalkgroupRulesLookup.h index 66f3c455..68e22cb3 100644 --- a/src/common/lookups/TalkgroupRulesLookup.h +++ b/src/common/lookups/TalkgroupRulesLookup.h @@ -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. diff --git a/src/common/network/FrameQueue.cpp b/src/common/network/FrameQueue.cpp index 2502a154..880691a8 100644 --- a/src/common/network/FrameQueue.cpp +++ b/src/common/network/FrameQueue.cpp @@ -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 #include -// --------------------------------------------------------------------------- -// 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 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 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 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(); diff --git a/src/common/network/FrameQueue.h b/src/common/network/FrameQueue.h index 413c69d9..6806d51f 100644 --- a/src/common/network/FrameQueue.h +++ b/src/common/network/FrameQueue.h @@ -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 -#include - namespace network { // --------------------------------------------------------------------------- @@ -116,8 +114,8 @@ namespace network private: uint32_t m_peerId; - std::unordered_map m_streamTimestamps; - static std::mutex m_fqTimestampLock; + + concurrent::unordered_map m_streamTimestamps; /** * @brief Generate RTP message for the frame queue. diff --git a/src/common/network/RawFrameQueue.cpp b/src/common/network/RawFrameQueue.cpp index 8cc9014b..cd250b73 100644 --- a/src/common/network/RawFrameQueue.cpp +++ b/src/common/network/RawFrameQueue.cpp @@ -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) { diff --git a/src/common/p25/P25Defines.h b/src/common/p25/P25Defines.h index f5a1a9dc..b88e56a1 100644 --- a/src/common/p25/P25Defines.h +++ b/src/common/p25/P25Defines.h @@ -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 diff --git a/src/common/zlib/Compression.cpp b/src/common/zlib/Compression.cpp new file mode 100644 index 00000000..0d8dd5d2 --- /dev/null +++ b/src/common/zlib/Compression.cpp @@ -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 +#include + +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 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 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; +} diff --git a/src/common/zlib/Compression.h b/src/common/zlib/Compression.h new file mode 100644 index 00000000..f740aea1 --- /dev/null +++ b/src/common/zlib/Compression.h @@ -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__ diff --git a/src/fne/CryptoContainer.cpp b/src/fne/CryptoContainer.cpp index 92b8e282..d6ae6099 100644 --- a/src/fne/CryptoContainer.cpp +++ b/src/fne/CryptoContainer.cpp @@ -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()); diff --git a/src/fne/HostFNE.cpp b/src/fne/HostFNE.cpp index 1343d9d2..f99070a7 100644 --- a/src/fne/HostFNE.cpp +++ b/src/fne/HostFNE.cpp @@ -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(); bool verbose = masterConf["verbose"].as(false); bool debug = masterConf["debug"].as(false); + uint16_t workerCnt = (uint16_t)masterConf["workers"].as(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(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) { diff --git a/src/fne/network/DiagNetwork.cpp b/src/fne/network/DiagNetwork.cpp index dd54690b..1c0b4511 100644 --- a/src/fne/network/DiagNetwork.cpp +++ b/src/fne/network/DiagNetwork.cpp @@ -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 @@ -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 (req->buffer != nullptr) - delete[] req->buffer; - delete req; - return; + 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; + } } } } @@ -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(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,27 +380,155 @@ 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(req->length - 8U); + UInt8Array __rawPayload = std::make_unique(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)); - - // parse JSON body - json::value v; - std::string err = json::parse(v, payload); - if (!err.empty()) { - break; - } - else { - // ensure parsed JSON is an array - if (!v.is()) { + ::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(); + 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(); + pkt.streamId = streamId; + } + + if (pkt.streamId != streamId) { + // otherwise drop the packet break; } - else { - json::array arr = v.get(); - network->m_peerLinkPeers[peerId] = arr; + } + + 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()) { + LogError(LOG_NET, "PEER %u error parsing active peer list, data was not valid", peerId); + break; + } + else { + json::array arr = v.get(); + 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(); + pkt.streamId = 0U; + + network->m_peerLinkActPkt.erase(peerId); + } else { + pkt.locked = false; } } else { @@ -422,6 +549,4 @@ void* DiagNetwork::threadedNetworkRx(void* arg) delete[] req->buffer; delete req; } - - return nullptr; } diff --git a/src/fne/network/DiagNetwork.h b/src/fne/network/DiagNetwork.h index 220cab46..2961ede8 100644 --- a/src/fne/network/DiagNetwork.h +++ b/src/fne/network/DiagNetwork.h @@ -18,6 +18,7 @@ #include "fne/Defines.h" #include "common/network/BaseNetwork.h" +#include "common/ThreadPool.h" #include "fne/network/FNENetwork.h" #include @@ -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 diff --git a/src/fne/network/FNENetwork.cpp b/src/fne/network/FNENetwork.cpp index 5e75a8ce..9ffb7843 100644 --- a/src/fne/network/FNENetwork.cpp +++ b/src/fne/network/FNENetwork.cpp @@ -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 #include @@ -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::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)) { - if (req->buffer != nullptr) - delete[] req->buffer; - delete req; - return; + // 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; + } } } } @@ -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::system_clock::now().time_since_epoch()).count(); FNENetwork* network = static_cast(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()); @@ -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,10 +1007,8 @@ 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); - delete connection; - } + network->erasePeer(peerId); + delete connection; } } } @@ -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 lock(m_peerMutex); connection->eraseStreamPktSeq(streamId); } } @@ -1643,7 +1663,6 @@ void FNENetwork::createPeerAffiliations(uint32_t peerId, std::string peerName) { erasePeerAffiliations(peerId); - std::lock_guard 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 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 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 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)) { - delete req; - return; + // 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; } - - // 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(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,84 +1915,47 @@ 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 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); + 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; + for (uint8_t i = 0U; i < blockCnt; i++) { + // build dataset + uint16_t bufSize = 10U + (PEER_LINK_BLOCK_SIZE); + UInt8Array __payload = std::make_unique(bufSize); + uint8_t* payload = __payload.get(); + ::memset(payload, 0x00U, bufSize); + + if (i == 0U) { + __SET_UINT32(len, payload, 0U); + __SET_UINT32(compressedLen, payload, 4U); + } - // resize the output buffer to the actual compressed data size - compressedData.resize(strm.total_out); + payload[8U] = i; + payload[9U] = blockCnt - 1U; - // cleanup - deflateEnd(&strm); + 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); - uint32_t compressedLen = strm.total_out; - uint8_t* compressed = compressedData.data(); + ::memcpy(payload + 10U, compressed + offs, blockSize); - // Utils::dump(1U, "Compressed Payload", compressed, compressedLen); + if (m_debug) + Utils::dump(1U, "Peer-Link RID Block Payload", payload, bufSize); - // transmit TGIDs - 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(bufSize); - uint8_t* payload = __payload.get(); - ::memset(payload, 0x00U, bufSize); + offs += PEER_LINK_BLOCK_SIZE; - if (i == 0U) { - __SET_UINT32(len, payload, 0U); - __SET_UINT32(compressedLen, payload, 4U); + writePeer(peerId, { NET_FUNC::PEER_LINK, NET_SUBFUNC::PL_RID_LIST }, + payload, bufSize, 0U, streamId, false, true, true); } - 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 RID Block Payload", payload, bufSize); - - offs += PEER_LINK_BLOCK_SIZE; - - writePeer(peerId, { NET_FUNC::PEER_LINK, NET_SUBFUNC::PL_RID_LIST }, - payload, bufSize, 0U, streamId, false, true, true); + connection->lastPing(now); + } else { + LogError(LOG_NET, "PEER %u error compressing RID list", peerId); } - - connection->lastPing(now); } return; @@ -2174,84 +2140,47 @@ 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 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); + 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; + for (uint8_t i = 0U; i < blockCnt; i++) { + // build dataset + uint16_t bufSize = 10U + (PEER_LINK_BLOCK_SIZE); + UInt8Array __payload = std::make_unique(bufSize); + uint8_t* payload = __payload.get(); + ::memset(payload, 0x00U, bufSize); + + if (i == 0U) { + __SET_UINT32(len, payload, 0U); + __SET_UINT32(compressedLen, payload, 4U); + } - // resize the output buffer to the actual compressed data size - compressedData.resize(strm.total_out); + payload[8U] = i; + payload[9U] = blockCnt - 1U; - // cleanup - deflateEnd(&strm); + 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); - uint32_t compressedLen = strm.total_out; - uint8_t* compressed = compressedData.data(); + ::memcpy(payload + 10U, compressed + offs, blockSize); - // Utils::dump(1U, "Compressed Payload", compressed, compressedLen); + if (m_debug) + Utils::dump(1U, "Peer-Link TGID Block Payload", payload, bufSize); - // transmit TGIDs - 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(bufSize); - uint8_t* payload = __payload.get(); - ::memset(payload, 0x00U, bufSize); + offs += PEER_LINK_BLOCK_SIZE; - if (i == 0U) { - __SET_UINT32(len, payload, 0U); - __SET_UINT32(compressedLen, payload, 4U); + writePeer(peerId, { NET_FUNC::PEER_LINK, NET_SUBFUNC::PL_TALKGROUP_LIST }, + payload, bufSize, 0U, streamId, false, true, true); } - 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 TGID Block Payload", payload, bufSize); - - offs += PEER_LINK_BLOCK_SIZE; - - writePeer(peerId, { NET_FUNC::PEER_LINK, NET_SUBFUNC::PL_TALKGROUP_LIST }, - payload, bufSize, 0U, streamId, false, true, true); + connection->lastPing(now); + } else { + LogError(LOG_NET, "PEER %u error compressing TGID list", peerId); } - - connection->lastPing(now); } return; @@ -2425,84 +2354,47 @@ 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; + uint32_t compressedLen = 0U; + uint8_t* compressed = Compression::compress((uint8_t*)buffer, len, &compressedLen); - // 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; - } + 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++) { + // build dataset + uint16_t bufSize = 10U + (PEER_LINK_BLOCK_SIZE); + UInt8Array __payload = std::make_unique(bufSize); + uint8_t* payload = __payload.get(); + ::memset(payload, 0x00U, bufSize); - // set input data - strm.avail_in = len; - strm.next_in = buffer; - - // compress data - std::vector 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); + if (i == 0U) { + __SET_UINT32(len, payload, 0U); + __SET_UINT32(compressedLen, payload, 4U); + } - // resize the output buffer to the actual compressed data size - compressedData.resize(strm.total_out); + payload[8U] = i; + payload[9U] = blockCnt - 1U; - // cleanup - deflateEnd(&strm); + 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); - uint32_t compressedLen = strm.total_out; - uint8_t* compressed = compressedData.data(); + ::memcpy(payload + 10U, compressed + offs, blockSize); - // Utils::dump(1U, "Compressed Payload", compressed, compressedLen); + if (m_debug) + Utils::dump(1U, "Peer-Link Peer List Block Payload", payload, bufSize); - // transmit TGIDs - 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(bufSize); - uint8_t* payload = __payload.get(); - ::memset(payload, 0x00U, bufSize); + offs += PEER_LINK_BLOCK_SIZE; - if (i == 0U) { - __SET_UINT32(len, payload, 0U); - __SET_UINT32(compressedLen, payload, 4U); + writePeer(peerId, { NET_FUNC::PEER_LINK, NET_SUBFUNC::PL_PEER_LIST }, + payload, bufSize, 0U, streamId, false, true, true); } - 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 Peer List Block Payload", payload, bufSize); - - offs += PEER_LINK_BLOCK_SIZE; - - writePeer(peerId, { NET_FUNC::PEER_LINK, NET_SUBFUNC::PL_PEER_LIST }, - payload, bufSize, 0U, streamId, false, true, true); + connection->lastPing(now); + } else { + LogError(LOG_NET, "PEER %u error compressing PID list", peerId); } - - connection->lastPing(now); } return; diff --git a/src/fne/network/FNENetwork.h b/src/fne/network/FNENetwork.h index 8f4103f2..db18a3de 100644 --- a/src/fne/network/FNENetwork.h +++ b/src/fne/network/FNENetwork.h @@ -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 PeerMapPair; - std::unordered_map m_peers; - std::unordered_map m_peerLinkPeers; + concurrent::unordered_map m_peers; + concurrent::unordered_map m_peerLinkPeers; typedef std::pair PeerAffiliationMapPair; - std::unordered_map m_peerAffiliations; - std::unordered_map> m_ccPeerMap; + concurrent::unordered_map m_peerAffiliations; + concurrent::unordered_map> m_ccPeerMap; static std::timed_mutex m_keyQueueMutex; std::unordered_map 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 fragments; + + bool locked; + }; + concurrent::unordered_map 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. diff --git a/src/fne/network/PeerNetwork.cpp b/src/fne/network/PeerNetwork.cpp index 4a01740f..f4e70c74 100644 --- a/src/fne/network/PeerNetwork.cpp +++ b/src/fne/network/PeerNetwork.cpp @@ -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 #include @@ -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(json.length() + 9U); + size_t len = json.length() + 9U; + CharArray __buffer = std::make_unique(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(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 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 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 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 diff --git a/src/fne/network/RESTAPI.cpp b/src/fne/network/RESTAPI.cpp index 7364cc67..4248c6ae 100644 --- a/src/fne/network/RESTAPI.cpp +++ b/src/fne/network/RESTAPI.cpp @@ -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. */ diff --git a/src/fne/network/callhandler/TagDMRData.cpp b/src/fne/network/callhandler/TagDMRData.cpp index e6df61ab..f5eb5dff 100644 --- a/src/fne/network/callhandler/TagDMRData.cpp +++ b/src/fne/network/callhandler/TagDMRData.cpp @@ -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) { diff --git a/src/fne/network/callhandler/TagDMRData.h b/src/fne/network/callhandler/TagDMRData.h index 23e7873b..9a20d8cb 100644 --- a/src/fne/network/callhandler/TagDMRData.h +++ b/src/fne/network/callhandler/TagDMRData.h @@ -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 - namespace network { namespace callhandler @@ -147,7 +147,7 @@ namespace network */ uint32_t dstId; }; - std::deque m_parrotFrames; + concurrent::deque m_parrotFrames; bool m_parrotFramesReady; /** @@ -196,7 +196,7 @@ namespace network } }; typedef std::pair StatusMapPair; - std::unordered_map m_status; + concurrent::unordered_map m_status; friend class packetdata::DMRPacketData; packetdata::DMRPacketData* m_packetData; diff --git a/src/fne/network/callhandler/TagNXDNData.cpp b/src/fne/network/callhandler/TagNXDNData.cpp index be7e8765..00264b98 100644 --- a/src/fne/network/callhandler/TagNXDNData.cpp +++ b/src/fne/network/callhandler/TagNXDNData.cpp @@ -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) { diff --git a/src/fne/network/callhandler/TagNXDNData.h b/src/fne/network/callhandler/TagNXDNData.h index 195d2497..35ec58cd 100644 --- a/src/fne/network/callhandler/TagNXDNData.h +++ b/src/fne/network/callhandler/TagNXDNData.h @@ -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 - namespace network { namespace callhandler @@ -116,7 +116,7 @@ namespace network */ uint32_t dstId; }; - std::deque m_parrotFrames; + concurrent::deque m_parrotFrames; bool m_parrotFramesReady; /** @@ -160,7 +160,7 @@ namespace network } }; typedef std::pair StatusMapPair; - std::unordered_map m_status; + concurrent::unordered_map m_status; bool m_debug; diff --git a/src/fne/network/callhandler/TagP25Data.cpp b/src/fne/network/callhandler/TagP25Data.cpp index 1f169839..43ef6e3d 100644 --- a/src/fne/network/callhandler/TagP25Data.cpp +++ b/src/fne/network/callhandler/TagP25Data.cpp @@ -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 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 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 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) { diff --git a/src/fne/network/callhandler/TagP25Data.h b/src/fne/network/callhandler/TagP25Data.h index 4ab19f33..32433840 100644 --- a/src/fne/network/callhandler/TagP25Data.h +++ b/src/fne/network/callhandler/TagP25Data.h @@ -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 - namespace network { namespace callhandler @@ -164,7 +164,7 @@ namespace network */ uint32_t dstId; }; - std::deque m_parrotFrames; + concurrent::deque m_parrotFrames; bool m_parrotFramesReady; bool m_parrotFirstFrame; @@ -209,7 +209,7 @@ namespace network } }; typedef std::pair StatusMapPair; - std::unordered_map m_status; + concurrent::unordered_map m_status; friend class packetdata::P25PacketData; packetdata::P25PacketData* m_packetData; diff --git a/src/fne/network/callhandler/packetdata/DMRPacketData.h b/src/fne/network/callhandler/packetdata/DMRPacketData.h index 743d7a6e..d1481029 100644 --- a/src/fne/network/callhandler/packetdata/DMRPacketData.h +++ b/src/fne/network/callhandler/packetdata/DMRPacketData.h @@ -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 StatusMapPair; - std::unordered_map m_status; + concurrent::unordered_map m_status; bool m_debug; diff --git a/src/fne/network/callhandler/packetdata/P25PacketData.h b/src/fne/network/callhandler/packetdata/P25PacketData.h index d57b2ca2..a1d3848f 100644 --- a/src/fne/network/callhandler/packetdata/P25PacketData.h +++ b/src/fne/network/callhandler/packetdata/P25PacketData.h @@ -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 m_dataFrames; + concurrent::deque m_dataFrames; /** * @brief Represents the receive status of a call. @@ -162,7 +164,7 @@ namespace network } }; typedef std::pair StatusMapPair; - std::unordered_map m_status; + concurrent::unordered_map m_status; typedef std::pair ArpTablePair; std::unordered_map m_arpTable; diff --git a/src/fne/network/influxdb/InfluxDB.cpp b/src/fne/network/influxdb/InfluxDB.cpp index 269fa112..d0c82cbd 100644 --- a/src/fne/network/influxdb/InfluxDB.cpp +++ b/src/fne/network/influxdb/InfluxDB.cpp @@ -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. */ diff --git a/src/fne/network/influxdb/InfluxDB.h b/src/fne/network/influxdb/InfluxDB.h index ef909a65..5c974830 100644 --- a/src/fne/network/influxdb/InfluxDB.h +++ b/src/fne/network/influxdb/InfluxDB.h @@ -5,7 +5,7 @@ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * Copyright (c) 2010-2018 - * 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 #include @@ -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,74 +431,56 @@ 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)) { - delete 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; - } - - TSCaller* caller = static_cast(req->obj); - if (caller == nullptr) { - if (req != nullptr) { - delete req; - } + if (req == nullptr) { + return; + } - m_currThreadCnt--; - return nullptr; + TSCaller* caller = static_cast(req->obj); + if (caller == nullptr) { + if (req != nullptr) { + delete req; } - const ServerInfo& si = req->si; - detail::inner::request("POST", "write", "", req->lines, si, nullptr); - - delete req; + return; } - m_currThreadCnt--; - return nullptr; + const ServerInfo& si = req->si; + detail::inner::request("POST", "write", "", req->lines, si, nullptr); + + delete req; } }; diff --git a/src/host/Host.cpp b/src/host/Host.cpp index 81b80d73..c067d834 100644 --- a/src/host/Host.cpp +++ b/src/host/Host.cpp @@ -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(_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(_true); } else { chData["tx"].set(_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(_true); } else { chData["tx"].set(_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(dstId); diff --git a/src/host/HostMain.cpp b/src/host/HostMain.cpp index 608d5bf0..694d0f60 100644 --- a/src/host/HostMain.cpp +++ b/src/host/HostMain.cpp @@ -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; @@ -114,14 +116,14 @@ void usage(const char* message, const char* arg) ::fprintf(stdout, "usage: %s [-vhdf]" - "[--syslog]" + " [--syslog]" #if defined(ENABLE_SETUP_TUI) - "[--setup]" -#else - "[--cal]" + " [--setup]" #endif - "[-c ]" - "[--remote [-a
] [-p ]]" + " [--cal]" + "[--boot]" + " [-c ]" + " [--remote [-a
] [-p ]]" "\n\n" " -v show version information\n" " -h show this screen\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 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"); diff --git a/src/host/HostMain.h b/src/host/HostMain.h index ca9b9790..00b1bbe2 100644 --- a/src/host/HostMain.h +++ b/src/host/HostMain.h @@ -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. */ diff --git a/src/host/calibrate/HostCal.cpp b/src/host/calibrate/HostCal.cpp index 7a19fcf9..2b2e2cb0 100644 --- a/src/host/calibrate/HostCal.cpp +++ b/src/host/calibrate/HostCal.cpp @@ -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(); diff --git a/src/host/dmr/lookups/DMRAffiliationLookup.cpp b/src/host/dmr/lookups/DMRAffiliationLookup.cpp index 8a486922..6a03b862 100644 --- a/src/host/dmr/lookups/DMRAffiliationLookup.cpp +++ b/src/host/dmr/lookups/DMRAffiliationLookup.cpp @@ -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; } diff --git a/src/host/dmr/lookups/DMRAffiliationLookup.h b/src/host/dmr/lookups/DMRAffiliationLookup.h index cf5587cc..3a8e475f 100644 --- a/src/host/dmr/lookups/DMRAffiliationLookup.h +++ b/src/host/dmr/lookups/DMRAffiliationLookup.h @@ -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> m_grantChSlotTable; + concurrent::unordered_map> m_grantChSlotTable; uint32_t m_tsccChNo; uint8_t m_tsccSlot; diff --git a/src/host/modem/port/specialized/V24UDPPort.cpp b/src/host/modem/port/specialized/V24UDPPort.cpp index 1c85194c..563ac0bd 100644 --- a/src/host/modem/port/specialized/V24UDPPort.cpp +++ b/src/host/modem/port/specialized/V24UDPPort.cpp @@ -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)) { - delete[] req->buffer; - delete req; - return; + // 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; + } } } } /* 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(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)) { - delete[] req->buffer; - delete req; - return; + // 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; + } } } } @@ -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(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. */ diff --git a/src/host/modem/port/specialized/V24UDPPort.h b/src/host/modem/port/specialized/V24UDPPort.h index 0329f54a..76788fca 100644 --- a/src/host/modem/port/specialized/V24UDPPort.h +++ b/src/host/modem/port/specialized/V24UDPPort.h @@ -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 @@ -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. diff --git a/src/host/network/RESTAPI.cpp b/src/host/network/RESTAPI.cpp index 1c922c9d..acccc157 100644 --- a/src/host/network/RESTAPI.cpp +++ b/src/host/network/RESTAPI.cpp @@ -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,17 +1629,19 @@ void RESTAPI::restAPI_GetP25AffList(const HTTPPayload& request, HTTPPayload& rep setResponseDefaultStatus(response); json::array affs = json::array(); - std::unordered_map affTable = m_p25->affiliations().grpAffTable(); - if (affTable.size() > 0) { - for (auto entry : affTable) { - uint32_t srcId = entry.first; - uint32_t grpId = entry.second; + if (m_p25->affiliations() != nullptr) { + std::unordered_map affTable = m_p25->affiliations()->grpAffTable(); + if (affTable.size() > 0) { + for (auto entry : affTable) { + uint32_t srcId = entry.first; + uint32_t grpId = entry.second; - json::object aff = json::object(); - aff["srcId"].set(srcId); - aff["grpId"].set(grpId); + json::object aff = json::object(); + aff["srcId"].set(srcId); + aff["grpId"].set(grpId); - affs.push_back(json::value(aff)); + affs.push_back(json::value(aff)); + } } } @@ -1787,17 +1798,19 @@ void RESTAPI::restAPI_GetNXDNAffList(const HTTPPayload& request, HTTPPayload& re setResponseDefaultStatus(response); json::array affs = json::array(); - std::unordered_map affTable = m_nxdn->affiliations().grpAffTable(); - if (affTable.size() > 0) { - for (auto entry : affTable) { - uint32_t srcId = entry.first; - uint32_t grpId = entry.second; + if (m_nxdn->affiliations() != nullptr) { + std::unordered_map affTable = m_nxdn->affiliations()->grpAffTable(); + if (affTable.size() > 0) { + for (auto entry : affTable) { + uint32_t srcId = entry.first; + uint32_t grpId = entry.second; - json::object aff = json::object(); - aff["srcId"].set(srcId); - aff["grpId"].set(grpId); + json::object aff = json::object(); + aff["srcId"].set(srcId); + aff["grpId"].set(grpId); - affs.push_back(json::value(aff)); + affs.push_back(json::value(aff)); + } } } diff --git a/src/host/network/RPCDefines.h b/src/host/network/RPCDefines.h index f8587997..6d01d099 100644 --- a/src/host/network/RPCDefines.h +++ b/src/host/network/RPCDefines.h @@ -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 /** @} */ diff --git a/src/host/nxdn/Control.cpp b/src/host/nxdn/Control.cpp index d49e025a..62f78d62 100644 --- a/src/host/nxdn/Control.cpp +++ b/src/host/nxdn/Control.cpp @@ -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(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); } } diff --git a/src/host/nxdn/Control.h b/src/host/nxdn/Control.h index 8b655307..883ed2c0 100644 --- a/src/host/nxdn/Control.h +++ b/src/host/nxdn/Control.h @@ -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; diff --git a/src/host/nxdn/packet/ControlSignaling.cpp b/src/host/nxdn/packet/ControlSignaling.cpp index 9a831a2e..d7c274a3 100644 --- a/src/host/nxdn/packet/ControlSignaling.cpp +++ b/src/host/nxdn/packet/ControlSignaling.cpp @@ -37,13 +37,13 @@ using namespace nxdn::packet; #define IS_SUPPORT_CONTROL_CHECK(_PCKT_STR, _PCKT, _SRCID) \ if (!m_nxdn->m_enableControl) { \ LogWarning(LOG_RF, "NXDN, %s denial, unsupported service, srcId = %u", _PCKT_STR.c_str(), _SRCID); \ - writeRF_Message_Deny(0U, _SRCID, CauseResponse::SVC_UNAVAILABLE, _PCKT); \ + writeRF_Message_Deny(0U, _SRCID, CauseResponse::SVC_UNAVAILABLE, _PCKT); \ m_nxdn->m_rfState = RS_RF_REJECTED; \ return false; \ } // Validate the source RID. -#define VALID_SRCID(_PCKT_STR, _PCKT, _SRCID, _RSN) \ +#define VALID_SRCID(_PCKT_STR, _PCKT, _SRCID, _RSN) \ if (!acl::AccessControl::validateSrcId(_SRCID)) { \ LogWarning(LOG_RF, "NXDN, %s denial, RID rejection, srcId = %u", _PCKT_STR.c_str(), _SRCID); \ writeRF_Message_Deny(0U, _SRCID, _RSN, _PCKT); \ @@ -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; \ @@ -96,7 +96,7 @@ using namespace nxdn::packet; // Macro helper to verbose log a generic message. #define VERBOSE_LOG_MSG_DST(_PCKT_STR, _DSTID) \ if (m_verbose) { \ - LogMessage(LOG_RF, "NXDN, %s, dstId = %u", _PCKT_STR.c_str(), _DSTID); \ + LogMessage(LOG_RF, "NXDN, %s, dstId = %u", _PCKT_STR.c_str(), _DSTID); \ } // Macro helper to verbose log a generic network message. @@ -108,7 +108,7 @@ using namespace nxdn::packet; // Macro helper to verbose log a generic network message. #define DEBUG_LOG_MSG(_PCKT_STR) \ if (m_debug) { \ - LogMessage(LOG_RF, "NXDN, %s", _PCKT_STR.c_str()); \ + LogMessage(LOG_RF, "NXDN, %s", _PCKT_STR.c_str()); \ } // --------------------------------------------------------------------------- @@ -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) diff --git a/src/host/nxdn/packet/Voice.cpp b/src/host/nxdn/packet/Voice.cpp index 837ac4fa..7ae86824 100644 --- a/src/host/nxdn/packet/Voice.cpp +++ b/src/host/nxdn/packet/Voice.cpp @@ -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(); \ diff --git a/src/host/p25/Control.cpp b/src/host/p25/Control.cpp index 1080b576..ba80e271 100644 --- a/src/host/p25/Control.cpp +++ b/src/host/p25/Control.cpp @@ -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); 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(false); m_ccDebug = control["debug"].as(false); + m_ccNotifyActiveTG = control["notifyActiveTG"].as(true); + m_allowExplicitSourceId = p25Protocol["allowExplicitSourceId"].as(true); m_convNetGrantDemand = p25Protocol["convNetGrantDemand"].as(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(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 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 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(active); + + g_RPC->req(RPC_ACTIVE_P25_TG, req, [=](json::object& req, json::object& reply) { + if (!req["status"].is()) { + ::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(); + 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 retMsg = req["message"].get(); + ::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 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()) { + ::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(); + 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 retMsg = req["message"].get(); + ::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()) { + g_RPC->defaultResponse(reply, "\"active\" was not a valid JSON array", network::NetRPC::INVALID_ARGS); + return; + } + json::array active = req["active"].get(); + + std::lock_guard lock(m_activeTGLock); + m_activeTG.clear(); + + if (active.size() > 0) { + for (auto entry : active) { + if (!entry.is()) { + g_RPC->defaultResponse(reply, "active TG was not a valid number", network::NetRPC::INVALID_ARGS); + continue; + } + + m_activeTG.push_back(entry.get()); + } + } + + ::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 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); } } diff --git a/src/host/p25/Control.h b/src/host/p25/Control.h index ecb7f652..07e17dbc 100644 --- a/src/host/p25/Control.h +++ b/src/host/p25/Control.h @@ -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 m_activeTG; + RingBuffer m_txImmQueue; RingBuffer 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. diff --git a/src/host/p25/lookups/P25AffiliationLookup.cpp b/src/host/p25/lookups/P25AffiliationLookup.cpp index 10bdbedf..72dda5e5 100644 --- a/src/host/p25/lookups/P25AffiliationLookup.cpp +++ b/src/host/p25/lookups/P25AffiliationLookup.cpp @@ -46,9 +46,9 @@ std::vector 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); diff --git a/src/host/p25/lookups/P25AffiliationLookup.h b/src/host/p25/lookups/P25AffiliationLookup.h index 875f524f..56540b6a 100644 --- a/src/host/p25/lookups/P25AffiliationLookup.h +++ b/src/host/p25/lookups/P25AffiliationLookup.h @@ -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: diff --git a/src/host/p25/packet/ControlSignaling.cpp b/src/host/p25/packet/ControlSignaling.cpp index 91680b53..045912c1 100644 --- a/src/host/p25/packet/ControlSignaling.cpp +++ b/src/host/p25/packet/ControlSignaling.cpp @@ -63,7 +63,7 @@ using namespace p25::packet; #define VALID_DSTID(_PCKT_STR, _PCKT, _SRCID, _DSTID) \ if (!acl::AccessControl::validateSrcId(_DSTID)) { \ LogWarning(LOG_RF, P25_TSDU_STR ", %s denial, RID rejection, dstId = %u", _PCKT_STR.c_str(), _DSTID); \ - writeRF_TSDU_Deny(_SRCID, _DSTID, ReasonCode::DENY_TGT_UNIT_NOT_VALID, _PCKT); \ + writeRF_TSDU_Deny(_SRCID, _DSTID, ReasonCode::DENY_TGT_UNIT_NOT_VALID, _PCKT); \ m_p25->m_rfState = RS_RF_REJECTED; \ return false; \ } @@ -72,14 +72,14 @@ using namespace p25::packet; #define VALID_TGID(_PCKT_STR, _PCKT, _SRCID, _DSTID) \ if (!acl::AccessControl::validateTGId(_DSTID)) { \ LogWarning(LOG_RF, P25_TSDU_STR ", %s denial, TGID rejection, dstId = %u", _PCKT_STR.c_str(), _DSTID); \ - writeRF_TSDU_Deny(_SRCID, _DSTID, ReasonCode::DENY_TGT_GROUP_NOT_VALID, _PCKT); \ + writeRF_TSDU_Deny(_SRCID, _DSTID, ReasonCode::DENY_TGT_GROUP_NOT_VALID, _PCKT); \ m_p25->m_rfState = RS_RF_REJECTED; \ return false; \ } // 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,9 +89,9 @@ 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_Deny(_SRCID, _DSTID, ReasonCode::DENY_REQ_UNIT_NOT_AUTH, _PCKT); \ writeRF_TSDU_U_Reg_Cmd(_SRCID); \ m_p25->m_rfState = RS_RF_REJECTED; \ return false; \ @@ -209,7 +209,7 @@ bool ControlSignaling::process(uint8_t* data, uint32_t len, std::unique_ptrgetSrcId(); 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_ptrm_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 osp; bool noData = false; uint8_t i = 0U; - std::unordered_map grantTable = m_p25->m_affiliations.grantTable(); + std::unordered_map 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(); 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 = std::make_unique(); @@ -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 = std::make_unique(); diff --git a/src/host/p25/packet/Voice.cpp b/src/host/p25/packet/Voice.cpp index db6c47be..408d2e61 100644 --- a/src/host/p25/packet/Voice.cpp +++ b/src/host/p25/packet/Voice.cpp @@ -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 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(); @@ -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 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(); @@ -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()); } - // 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; + // 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_rfLC.setMFId(MFG_STANDARD); m_rfLC.setLCO(LCO::RFSS_STS_BCAST); } + else { + std::lock_guard 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 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(); @@ -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(); } - // 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; + // 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_netLC.setMFId(MFG_STANDARD); m_netLC.setLCO(LCO::RFSS_STS_BCAST); } + else { + std::lock_guard 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]; diff --git a/src/host/p25/packet/Voice.h b/src/host/p25/packet/Voice.h index 818b2be7..89de8eb3 100644 --- a/src/host/p25/packet/Voice.h +++ b/src/host/p25/packet/Voice.h @@ -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; diff --git a/src/host/setup/HostSetup.cpp b/src/host/setup/HostSetup.cpp index a623103a..08726c36 100644 --- a/src/host/setup/HostSetup.cpp +++ b/src/host/setup/HostSetup.cpp @@ -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); diff --git a/src/patch/ActivityLog.cpp b/src/patch/ActivityLog.cpp new file mode 100644 index 00000000..2680141f --- /dev/null +++ b/src/patch/ActivityLog.cpp @@ -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 +#endif // defined(_WIN32) + +#if defined(CATCH2_TEST_COMPILATION) +#include +#endif + +#include +#include +#include +#include +#include + +// --------------------------------------------------------------------------- +// 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); + } +} diff --git a/src/patch/ActivityLog.h b/src/patch/ActivityLog.h new file mode 100644 index 00000000..8f9ffec8 --- /dev/null +++ b/src/patch/ActivityLog.h @@ -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 + +// --------------------------------------------------------------------------- +// 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__ diff --git a/src/patch/CMakeLists.txt b/src/patch/CMakeLists.txt new file mode 100644 index 00000000..d04d5004 --- /dev/null +++ b/src/patch/CMakeLists.txt @@ -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" +) diff --git a/src/patch/Defines.h b/src/patch/Defines.h new file mode 100644 index 00000000..394c328c --- /dev/null +++ b/src/patch/Defines.h @@ -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__ diff --git a/src/patch/HostPatch.cpp b/src/patch/HostPatch.cpp new file mode 100644 index 00000000..6f575669 --- /dev/null +++ b/src/patch/HostPatch.cpp @@ -0,0 +1,1078 @@ +// 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/dmr/DMRDefines.h" +#include "common/dmr/data/EMB.h" +#include "common/dmr/data/NetData.h" +#include "common/dmr/lc/FullLC.h" +#include "common/dmr/SlotType.h" +#include "common/p25/P25Defines.h" +#include "common/p25/data/LowSpeedData.h" +#include "common/p25/dfsi/DFSIDefines.h" +#include "common/p25/dfsi/LC.h" +#include "common/p25/lc/LC.h" +#include "common/p25/P25Utils.h" +#include "common/network/RTPHeader.h" +#include "common/network/udp/Socket.h" +#include "common/Clock.h" +#include "common/StopWatch.h" +#include "common/Thread.h" +#include "common/Log.h" +#include "common/Utils.h" +#include "patch/ActivityLog.h" +#include "HostPatch.h" +#include "PatchMain.h" + +using namespace network; +using namespace network::frame; +using namespace network::udp; + +#include +#include +#include +#include + +#if !defined(_WIN32) +#include +#include +#endif // !defined(_WIN32) + +// --------------------------------------------------------------------------- +// Static Class Members +// --------------------------------------------------------------------------- + +std::mutex HostPatch::m_networkMutex; + +// --------------------------------------------------------------------------- +// Public Class Members +// --------------------------------------------------------------------------- + +/* Initializes a new instance of the HostPatch class. */ + +HostPatch::HostPatch(const std::string& confFile) : + m_confFile(confFile), + m_conf(), + m_network(nullptr), + m_srcTGId(0U), + m_srcSlot(1U), + m_dstTGId(0U), + m_dstSlot(1U), + m_identity(), + m_digiMode(1U), + m_dmrEmbeddedData(), + m_grantDemand(false), + m_callInProgress(false), + m_rxStartTime(0U), + m_rxStreamId(0U), + m_running(false), + m_trace(false), + m_debug(false) +{ + /* stub */ +} + +/* Finalizes a instance of the HostPatch class. */ + +HostPatch::~HostPatch() = default; + +/* Executes the main FNE processing loop. */ + +int HostPatch::run() +{ + bool ret = false; + try { + ret = yaml::Parse(m_conf, m_confFile.c_str()); + if (!ret) { + ::fatal("cannot read the configuration file, %s\n", m_confFile.c_str()); + } + } + catch (yaml::OperationException const& e) { + ::fatal("cannot read the configuration file - %s (%s)", m_confFile.c_str(), e.message()); + } + + bool m_daemon = m_conf["daemon"].as(false); + if (m_daemon && g_foreground) + m_daemon = false; + + // initialize system logging + yaml::Node logConf = m_conf["log"]; + ret = ::LogInitialise(logConf["filePath"].as(), logConf["fileRoot"].as(), + logConf["fileLevel"].as(0U), logConf["displayLevel"].as(0U)); + if (!ret) { + ::fatal("unable to open the log file\n"); + } + + ret = ::ActivityLogInitialise(logConf["activityFilePath"].as(), logConf["fileRoot"].as()); + if (!ret) { + ::fatal("unable to open the activity log file\n"); + } + +#if !defined(_WIN32) + // handle POSIX process forking + if (m_daemon) { + // create new process + pid_t pid = ::fork(); + if (pid == -1) { + ::fprintf(stderr, "%s: Couldn't fork() , exiting\n", g_progExe.c_str()); + ::LogFinalise(); + return EXIT_FAILURE; + } + else if (pid != 0) { + ::LogFinalise(); + exit(EXIT_SUCCESS); + } + + // create new session and process group + if (::setsid() == -1) { + ::fprintf(stderr, "%s: Couldn't setsid(), exiting\n", g_progExe.c_str()); + ::LogFinalise(); + return EXIT_FAILURE; + } + + // set the working directory to the root directory + if (::chdir("/") == -1) { + ::fprintf(stderr, "%s: Couldn't cd /, exiting\n", g_progExe.c_str()); + ::LogFinalise(); + return EXIT_FAILURE; + } + + ::close(STDIN_FILENO); + ::close(STDOUT_FILENO); + ::close(STDERR_FILENO); + } +#endif // !defined(_WIN32) + + ::LogInfo(__BANNER__ "\r\n" __PROG_NAME__ " " __VER__ " (built " __BUILD__ ")\r\n" \ + "Copyright (c) 2025 Bryan Biedenkapp, N2PLL and DVMProject (https://github.com/dvmproject) Authors.\r\n" \ + "Portions Copyright (c) 2015-2021 by Jonathan Naylor, G4KLX and others\r\n" \ + ">> Talkgroup Patch\r\n"); + + // read base parameters from configuration + ret = readParams(); + if (!ret) + return EXIT_FAILURE; + + yaml::Node systemConf = m_conf["system"]; + + // initialize peer networking + ret = createNetwork(); + if (!ret) + return EXIT_FAILURE; + + /* + ** Initialize Threads + */ + + if (!Thread::runAsThread(this, threadNetworkProcess)) + return EXIT_FAILURE; + + ::LogInfoEx(LOG_HOST, "Patch is up and running"); + + m_running = true; + + StopWatch stopWatch; + stopWatch.start(); + + // main execution loop + while (!g_killed) { + uint32_t ms = stopWatch.elapsed(); + + ms = stopWatch.elapsed(); + stopWatch.start(); + + // ------------------------------------------------------ + // -- Network Clocking -- + // ------------------------------------------------------ + + if (m_network != nullptr) { + std::lock_guard lock(HostPatch::m_networkMutex); + m_network->clock(ms); + } + + if (ms < 2U) + Thread::sleep(1U); + } + + ::LogSetNetwork(nullptr); + if (m_network != nullptr) { + m_network->close(); + delete m_network; + } + + return EXIT_SUCCESS; +} + +// --------------------------------------------------------------------------- +// Private Class Members +// --------------------------------------------------------------------------- + +/* Reads basic configuration parameters from the YAML configuration file. */ + +bool HostPatch::readParams() +{ + yaml::Node systemConf = m_conf["system"]; + + m_identity = systemConf["identity"].as(); + + m_digiMode = (uint8_t)systemConf["digiMode"].as(1U); + if (m_digiMode < TX_MODE_DMR) + m_digiMode = TX_MODE_DMR; + if (m_digiMode > TX_MODE_P25) + m_digiMode = TX_MODE_P25; + + m_grantDemand = systemConf["grantDemand"].as(false); + + m_trace = systemConf["trace"].as(false); + m_debug = systemConf["debug"].as(false); + + LogInfo("General Parameters"); + LogInfo(" Digital Mode: %s", m_digiMode == TX_MODE_DMR ? "DMR" : "P25"); + LogInfo(" Grant Demands: %s", m_grantDemand ? "yes" : "no"); + + if (m_debug) { + LogInfo(" Debug: yes"); + } + + return true; +} + +/* Initializes network connectivity. */ + +bool HostPatch::createNetwork() +{ + yaml::Node networkConf = m_conf["network"]; + + std::string address = networkConf["address"].as(); + uint16_t port = (uint16_t)networkConf["port"].as(TRAFFIC_DEFAULT_PORT); + uint16_t local = (uint16_t)networkConf["local"].as(0U); + uint32_t id = networkConf["id"].as(1000U); + std::string password = networkConf["password"].as(); + bool allowDiagnosticTransfer = networkConf["allowDiagnosticTransfer"].as(false); + bool debug = networkConf["debug"].as(false); + + m_srcTGId = (uint32_t)networkConf["sourceTGID"].as(1U); + m_srcSlot = (uint8_t)networkConf["sourceSlot"].as(1U); + m_dstTGId = (uint32_t)networkConf["destinationTGID"].as(1U); + m_dstSlot = (uint8_t)networkConf["destinationSlot"].as(1U); + m_twoWayPatch = networkConf["twoWay"].as(false); + + // make sure our destination ID is sane + if (m_srcTGId == 0U) { + ::LogError(LOG_HOST, "Patch source TGID cannot be set to 0."); + return false; + } + + if (m_dstTGId == 0U) { + ::LogError(LOG_HOST, "Patch destination TGID cannot be set to 0."); + return false; + } + + if (m_srcTGId == m_dstTGId) { + ::LogError(LOG_HOST, "Patch source TGID and destination TGID cannot be the same."); + return false; + } + + // make sure we're range checked + switch (m_digiMode) { + case TX_MODE_DMR: + { + if (m_srcTGId > 16777215) { + ::LogError(LOG_HOST, "Patch source TGID cannot be greater than 16777215."); + return false; + } + + if (m_dstTGId > 16777215) { + ::LogError(LOG_HOST, "Patch source TGID cannot be greater than 16777215."); + return false; + } + } + break; + case TX_MODE_P25: + { + if (m_srcTGId > 65535) { + ::LogError(LOG_HOST, "Patch source TGID cannot be greater than 65535."); + return false; + } + + if (m_dstTGId > 65535) { + ::LogError(LOG_HOST, "Patch destination TGID cannot be greater than 65535."); + return false; + } + } + break; + } + + bool encrypted = networkConf["encrypted"].as(false); + std::string key = networkConf["presharedKey"].as(); + uint8_t presharedKey[AES_WRAPPED_PCKT_KEY_LEN]; + if (!key.empty()) { + if (key.size() == 32) { + // bryanb: shhhhhhh....dirty nasty hacks + key = key.append(key); // since the key is 32 characters (16 hex pairs), double it on itself for 64 characters (32 hex pairs) + LogWarning(LOG_HOST, "Half-length network preshared encryption key detected, doubling key on itself."); + } + + if (key.size() == 64) { + if ((key.find_first_not_of("0123456789abcdefABCDEF", 2) == std::string::npos)) { + const char* keyPtr = key.c_str(); + ::memset(presharedKey, 0x00U, AES_WRAPPED_PCKT_KEY_LEN); + + for (uint8_t i = 0; i < AES_WRAPPED_PCKT_KEY_LEN; i++) { + char t[4] = { keyPtr[0], keyPtr[1], 0 }; + presharedKey[i] = (uint8_t)::strtoul(t, NULL, 16); + keyPtr += 2 * sizeof(char); + } + } + else { + LogWarning(LOG_HOST, "Invalid characters in the network preshared encryption key. Encryption disabled."); + encrypted = false; + } + } + else { + LogWarning(LOG_HOST, "Invalid network preshared encryption key length, key should be 32 hex pairs, or 64 characters. Encryption disabled."); + encrypted = false; + } + } + + if (id > 999999999U) { + ::LogError(LOG_HOST, "Network Peer ID cannot be greater then 999999999."); + return false; + } + + LogInfo("Network Parameters"); + LogInfo(" Peer ID: %u", id); + LogInfo(" Address: %s", address.c_str()); + LogInfo(" Port: %u", port); + if (local > 0U) + LogInfo(" Local: %u", local); + else + LogInfo(" Local: random"); + + LogInfo(" Encrypted: %s", encrypted ? "yes" : "no"); + + LogInfo(" Source TGID: %u", m_srcTGId); + LogInfo(" Source DMR Slot: %u", m_srcSlot); + LogInfo(" Destination TGID: %u", m_dstTGId); + LogInfo(" Destination DMR Slot: %u", m_dstSlot); + LogInfo(" Two-Way Patch: %s", m_twoWayPatch ? "yes" : "no"); + + if (debug) { + LogInfo(" Debug: yes"); + } + + bool dmr = false, p25 = false; + switch (m_digiMode) { + case TX_MODE_DMR: + dmr = true; + break; + case TX_MODE_P25: + p25 = true; + break; + } + + // initialize networking + m_network = new PeerNetwork(address, port, local, id, password, true, debug, dmr, p25, false, true, true, true, allowDiagnosticTransfer, true, false); + + m_network->setMetadata(m_identity, 0U, 0U, 0.0F, 0.0F, 0, 0, 0, 0.0F, 0.0F, 0, ""); + m_network->setConventional(true); + + if (encrypted) { + m_network->setPresharedKey(presharedKey); + } + + m_network->enable(true); + bool ret = m_network->open(); + if (!ret) { + delete m_network; + m_network = nullptr; + LogError(LOG_HOST, "failed to initialize traffic networking!"); + return false; + } + + ::LogSetNetwork(m_network); + + return true; +} + +/* Helper to process DMR network traffic. */ + +void HostPatch::processDMRNetwork(uint8_t* buffer, uint32_t length) +{ + assert(buffer != nullptr); + using namespace dmr; + using namespace dmr::defines; + + if (m_digiMode != TX_MODE_DMR) + return; + + // process network message header + uint32_t seqNo = buffer[4U]; + + uint32_t srcId = __GET_UINT16(buffer, 5U); + uint32_t dstId = __GET_UINT16(buffer, 8U); + + uint8_t controlByte = buffer[14U]; + + FLCO::E flco = (buffer[15U] & 0x40U) == 0x40U ? FLCO::PRIVATE : FLCO::GROUP; + + uint32_t slotNo = (buffer[15U] & 0x80U) == 0x80U ? 2U : 1U; + + if (slotNo > 3U) { + LogError(LOG_DMR, "DMR, invalid slot, slotNo = %u", slotNo); + return; + } + + // DMO mode slot disabling + if (slotNo == 1U && !m_network->getDuplex()) { + LogError(LOG_DMR, "DMR/DMO, invalid slot, slotNo = %u", slotNo); + return; + } + + // Individual slot disabling + if (slotNo == 1U && !m_network->getDMRSlot1()) { + LogError(LOG_DMR, "DMR, invalid slot, slot 1 disabled, slotNo = %u", slotNo); + return; + } + if (slotNo == 2U && !m_network->getDMRSlot2()) { + LogError(LOG_DMR, "DMR, invalid slot, slot 2 disabled, slotNo = %u", slotNo); + return; + } + + bool dataSync = (buffer[15U] & 0x20U) == 0x20U; + bool voiceSync = (buffer[15U] & 0x10U) == 0x10U; + + if (m_debug) { + LogDebug(LOG_NET, "DMR, seqNo = %u, srcId = %u, dstId = %u, flco = $%02X, slotNo = %u, len = %u", seqNo, srcId, dstId, flco, slotNo, length); + } + + // process raw DMR data bytes + UInt8Array data = std::unique_ptr(new uint8_t[DMR_FRAME_LENGTH_BYTES]); + ::memset(data.get(), 0x00U, DMR_FRAME_LENGTH_BYTES); + DataType::E dataType = DataType::VOICE_SYNC; + uint8_t n = 0U; + if (dataSync) { + dataType = (DataType::E)(buffer[15U] & 0x0FU); + ::memcpy(data.get(), buffer + 20U, DMR_FRAME_LENGTH_BYTES); + } + else if (voiceSync) { + ::memcpy(data.get(), buffer + 20U, DMR_FRAME_LENGTH_BYTES); + } + else { + n = buffer[15U] & 0x0FU; + dataType = DataType::VOICE; + ::memcpy(data.get(), buffer + 20U, DMR_FRAME_LENGTH_BYTES); + } + + if (flco == FLCO::GROUP) { + if (srcId == 0) + return; + + // ensure destination ID matches and slot matches + if (dstId != m_srcTGId && dstId != m_dstTGId) + return; + if (slotNo != m_srcSlot && slotNo != m_dstSlot) + return; + + uint32_t actualDstId = m_dstTGId; + if (m_twoWayPatch) { + if (dstId == m_dstTGId) + actualDstId = m_srcTGId; + } else { + if (dstId == m_dstTGId) + return; + } + + // is this a new call stream? + if (m_network->getDMRStreamId(slotNo) != m_rxStreamId) { + m_callInProgress = true; + + uint64_t now = std::chrono::duration_cast(std::chrono::system_clock::now().time_since_epoch()).count(); + m_rxStartTime = now; + + LogMessage(LOG_HOST, "DMR, call start, srcId = %u, dstId = %u, slot = %u", srcId, dstId, slotNo); + } + + if (dataSync && (dataType == DataType::TERMINATOR_WITH_LC)) { + // generate DMR network frame + data::NetData dmrData; + dmrData.setSlotNo(m_dstSlot); + dmrData.setDataType(DataType::TERMINATOR_WITH_LC); + dmrData.setSrcId(srcId); + dmrData.setDstId(actualDstId); + dmrData.setFLCO(flco); + dmrData.setControl(controlByte); + + uint8_t n = data[15U] & 0x0FU; + + dmrData.setN(n); + dmrData.setSeqNo(seqNo); + dmrData.setBER(0U); + dmrData.setRSSI(0U); + + dmrData.setData(data.get()); + + m_network->writeDMRTerminator(dmrData, &seqNo, &n, m_dmrEmbeddedData); + m_network->resetDMR(dmrData.getSlotNo()); + + if (m_rxStartTime > 0U) { + uint64_t now = std::chrono::duration_cast(std::chrono::system_clock::now().time_since_epoch()).count(); + uint64_t diff = now - m_rxStartTime; + + LogMessage(LOG_HOST, "DMR, call end, srcId = %u, dstId = %u, dur = %us", srcId, dstId, diff / 1000U); + } + + m_callInProgress = false; + m_rxStartTime = 0U; + m_rxStreamId = 0U; + return; + } + + m_rxStreamId = m_network->getDMRStreamId(slotNo); + + uint8_t* buffer = nullptr; + + // if we can, use the LC from the voice header as to keep all options intact + if (dataSync && (dataType == DataType::VOICE_LC_HEADER)) { + lc::LC lc = lc::LC(); + lc::FullLC fullLC = lc::FullLC(); + lc = *fullLC.decode(data.get(), DataType::VOICE_LC_HEADER); + + LogMessage(LOG_HOST, DMR_DT_VOICE_LC_HEADER ", slot = %u, srcId = %u, dstId = %u, FLCO = $%02X", m_srcSlot, + lc.getSrcId(), lc.getDstId(), flco); + + // send DMR voice header + buffer = new uint8_t[DMR_FRAME_LENGTH_BYTES]; + + lc.setDstId(actualDstId); + m_dmrEmbeddedData.setLC(lc); + + // generate the Slot TYpe + SlotType slotType = SlotType(); + slotType.setDataType(DataType::VOICE_LC_HEADER); + slotType.encode(buffer); + + fullLC.encode(lc, buffer, DataType::VOICE_LC_HEADER); + + // generate DMR network frame + data::NetData dmrData; + dmrData.setSlotNo(m_dstSlot); + dmrData.setDataType(DataType::VOICE_LC_HEADER); + dmrData.setSrcId(srcId); + dmrData.setDstId(actualDstId); + dmrData.setFLCO(flco); + dmrData.setControl(controlByte); + if (m_grantDemand) { + dmrData.setControl(0x80U); // DMR remote grant demand flag + } else { + dmrData.setControl(0U); + } + + uint8_t n = data[15U] & 0x0FU; + + dmrData.setN(n); + dmrData.setSeqNo(seqNo); + dmrData.setBER(0U); + dmrData.setRSSI(0U); + + dmrData.setData(buffer); + + m_network->writeDMR(dmrData, false); + delete[] buffer; + } + + // if we can, use the PI LC from the PI voice header as to keep all options intact + if (dataSync && (dataType == DataType::VOICE_PI_HEADER)) { + lc::PrivacyLC lc = lc::PrivacyLC(); + lc::FullLC fullLC = lc::FullLC(); + lc = *fullLC.decodePI(data.get()); + + LogMessage(LOG_HOST, DMR_DT_VOICE_PI_HEADER ", slot = %u, algId = %u, kId = %u, dstId = %u", m_srcSlot, + lc.getAlgId(), lc.getKId(), lc.getDstId()); + + // send DMR voice header + buffer = new uint8_t[DMR_FRAME_LENGTH_BYTES]; + + lc.setDstId(actualDstId); + + // generate the Slot TYpe + SlotType slotType = SlotType(); + slotType.setDataType(DataType::VOICE_PI_HEADER); + slotType.encode(buffer); + + fullLC.encodePI(lc, buffer); + + // generate DMR network frame + data::NetData dmrData; + dmrData.setSlotNo(m_dstSlot); + dmrData.setDataType(DataType::VOICE_PI_HEADER); + dmrData.setSrcId(srcId); + dmrData.setDstId(actualDstId); + dmrData.setFLCO(flco); + dmrData.setControl(controlByte); + if (m_grantDemand) { + dmrData.setControl(0x80U); // DMR remote grant demand flag + } else { + dmrData.setControl(0U); + } + + uint8_t n = data[15U] & 0x0FU; + + dmrData.setN(n); + dmrData.setSeqNo(seqNo); + dmrData.setBER(0U); + dmrData.setRSSI(0U); + + dmrData.setData(buffer); + + m_network->writeDMR(dmrData, false); + delete[] buffer; + } + + if (dataType == DataType::VOICE_SYNC || dataType == DataType::VOICE) { + // send DMR voice + buffer = new uint8_t[DMR_FRAME_LENGTH_BYTES]; + ::memcpy(buffer, data.get(), DMR_FRAME_LENGTH_BYTES); + + uint8_t n = data[15U] & 0x0FU; + + DataType::E dataType = DataType::VOICE_SYNC; + if (n == 0) + dataType = DataType::VOICE_SYNC; + else { + dataType = DataType::VOICE; + + uint8_t lcss = m_dmrEmbeddedData.getData(buffer, n); + + // generated embedded signalling + data::EMB emb = data::EMB(); + emb.setColorCode(0U); + emb.setLCSS(lcss); + emb.encode(buffer); + } + + LogMessage(LOG_HOST, DMR_DT_VOICE ", srcId = %u, dstId = %u, slot = %u, seqNo = %u", srcId, dstId, m_srcSlot, seqNo); + + // generate DMR network frame + data::NetData dmrData; + dmrData.setSlotNo(m_dstSlot); + dmrData.setDataType(dataType); + dmrData.setSrcId(srcId); + dmrData.setDstId(actualDstId); + dmrData.setFLCO(flco); + dmrData.setN(n); + dmrData.setSeqNo(seqNo); + dmrData.setBER(0U); + dmrData.setRSSI(0U); + + dmrData.setData(buffer); + + m_network->writeDMR(dmrData, false); + } + } +} + +/* Helper to process P25 network traffic. */ + +void HostPatch::processP25Network(uint8_t* buffer, uint32_t length) +{ + assert(buffer != nullptr); + using namespace p25; + using namespace p25::defines; + using namespace p25::dfsi::defines; + + if (m_digiMode != TX_MODE_P25) + return; + + bool grantDemand = (buffer[14U] & 0x80U) == 0x80U; + bool grantDenial = (buffer[14U] & 0x40U) == 0x40U; + bool unitToUnit = (buffer[14U] & 0x01U) == 0x01U; + + // process network message header + DUID::E duid = (DUID::E)buffer[22U]; + uint8_t MFId = buffer[15U]; + + if (duid == DUID::HDU || duid == DUID::TSDU || duid == DUID::PDU) + return; + + // process raw P25 data bytes + UInt8Array data; + uint8_t frameLength = buffer[23U]; + if (duid == DUID::PDU) { + frameLength = length; + data = std::unique_ptr(new uint8_t[length]); + ::memset(data.get(), 0x00U, length); + ::memcpy(data.get(), buffer, length); + } + else { + if (frameLength <= 24) { + data = std::unique_ptr(new uint8_t[frameLength]); + ::memset(data.get(), 0x00U, frameLength); + } + else { + data = std::unique_ptr(new uint8_t[frameLength]); + ::memset(data.get(), 0x00U, frameLength); + ::memcpy(data.get(), buffer + 24U, frameLength); + } + } + + // handle LDU, TDU or TSDU frame + uint8_t lco = buffer[4U]; + + uint32_t srcId = __GET_UINT16(buffer, 5U); + uint32_t dstId = __GET_UINT16(buffer, 8U); + + uint8_t lsd1 = buffer[20U]; + uint8_t lsd2 = buffer[21U]; + + FrameType::E frameType = (FrameType::E)buffer[180U]; + + lc::LC control; + data::LowSpeedData lsd; + + control.setLCO(lco); + control.setSrcId(srcId); + control.setDstId(dstId); + control.setMFId(MFId); + + if (!control.isStandardMFId()) { + control.setLCO(LCO::GROUP); + } + else { + if (control.getLCO() == LCO::GROUP_UPDT || control.getLCO() == LCO::RFSS_STS_BCAST) { + control.setLCO(LCO::GROUP); + } + } + + lsd.setLSD1(lsd1); + lsd.setLSD2(lsd2); + + if (control.getLCO() == LCO::GROUP) { + if (srcId == 0) + return; + + // ensure destination ID matches + if (dstId != m_srcTGId && dstId != m_dstTGId) + return; + + uint32_t actualDstId = m_dstTGId; + if (m_twoWayPatch) { + if (dstId == m_dstTGId) + actualDstId = m_srcTGId; + } else { + if (dstId == m_dstTGId) + return; + } + + if (m_network->getP25StreamId() != m_rxStreamId && ((duid != DUID::TDU) && (duid != DUID::TDULC))) { + m_callInProgress = true; + + uint64_t now = std::chrono::duration_cast(std::chrono::system_clock::now().time_since_epoch()).count(); + m_rxStartTime = now; + + LogMessage(LOG_HOST, "P25, call start, srcId = %u, dstId = %u", srcId, dstId); + + if (m_grantDemand) { + p25::lc::LC lc = p25::lc::LC(); + lc.setLCO(p25::defines::LCO::GROUP); + lc.setDstId(dstId); + lc.setSrcId(srcId); + + p25::data::LowSpeedData lsd = p25::data::LowSpeedData(); + + uint8_t controlByte = 0x80U; + m_network->writeP25TDU(lc, lsd, controlByte); + } + } + + if ((duid == DUID::TDU) || (duid == DUID::TDULC)) { + // ignore TDU's that are grant demands + if (grantDemand) + return; + + p25::lc::LC lc = p25::lc::LC(); + lc.setLCO(p25::defines::LCO::GROUP); + lc.setDstId(actualDstId); + lc.setSrcId(srcId); + + p25::data::LowSpeedData lsd = p25::data::LowSpeedData(); + + LogMessage(LOG_HOST, P25_TDU_STR); + + uint8_t controlByte = 0x00U; + m_network->writeP25TDU(lc, lsd, controlByte); + + if (m_rxStartTime > 0U) { + uint64_t now = std::chrono::duration_cast(std::chrono::system_clock::now().time_since_epoch()).count(); + uint64_t diff = now - m_rxStartTime; + + LogMessage(LOG_HOST, "P25, call end, srcId = %u, dstId = %u, dur = %us", srcId, dstId, diff / 1000U); + } + + m_rxStartTime = 0U; + m_rxStreamId = 0U; + + m_callInProgress = false; + m_rxStartTime = 0U; + m_rxStreamId = 0U; + return; + } + + m_rxStreamId = m_network->getP25StreamId(); + + uint8_t* netLDU = new uint8_t[9U * 25U]; + ::memset(netLDU, 0x00U, 9U * 25U); + + int count = 0; + switch (duid) + { + case DUID::LDU1: + if ((data[0U] == DFSIFrameType::LDU1_VOICE1) && (data[22U] == DFSIFrameType::LDU1_VOICE2) && + (data[36U] == DFSIFrameType::LDU1_VOICE3) && (data[53U] == DFSIFrameType::LDU1_VOICE4) && + (data[70U] == DFSIFrameType::LDU1_VOICE5) && (data[87U] == DFSIFrameType::LDU1_VOICE6) && + (data[104U] == DFSIFrameType::LDU1_VOICE7) && (data[121U] == DFSIFrameType::LDU1_VOICE8) && + (data[138U] == DFSIFrameType::LDU1_VOICE9)) { + + dfsi::LC dfsiLC = dfsi::LC(control, lsd); + + dfsiLC.setFrameType(DFSIFrameType::LDU1_VOICE1); + dfsiLC.decodeLDU1(data.get() + count, netLDU + 10U); + count += DFSI_LDU1_VOICE1_FRAME_LENGTH_BYTES; + + dfsiLC.setFrameType(DFSIFrameType::LDU1_VOICE2); + dfsiLC.decodeLDU1(data.get() + count, netLDU + 26U); + count += DFSI_LDU1_VOICE2_FRAME_LENGTH_BYTES; + + dfsiLC.setFrameType(DFSIFrameType::LDU1_VOICE3); + dfsiLC.decodeLDU1(data.get() + count, netLDU + 55U); + count += DFSI_LDU1_VOICE3_FRAME_LENGTH_BYTES; + + dfsiLC.setFrameType(DFSIFrameType::LDU1_VOICE4); + dfsiLC.decodeLDU1(data.get() + count, netLDU + 80U); + count += DFSI_LDU1_VOICE4_FRAME_LENGTH_BYTES; + + dfsiLC.setFrameType(DFSIFrameType::LDU1_VOICE5); + dfsiLC.decodeLDU1(data.get() + count, netLDU + 105U); + count += DFSI_LDU1_VOICE5_FRAME_LENGTH_BYTES; + + dfsiLC.setFrameType(DFSIFrameType::LDU1_VOICE6); + dfsiLC.decodeLDU1(data.get() + count, netLDU + 130U); + count += DFSI_LDU1_VOICE6_FRAME_LENGTH_BYTES; + + dfsiLC.setFrameType(DFSIFrameType::LDU1_VOICE7); + dfsiLC.decodeLDU1(data.get() + count, netLDU + 155U); + count += DFSI_LDU1_VOICE7_FRAME_LENGTH_BYTES; + + dfsiLC.setFrameType(DFSIFrameType::LDU1_VOICE8); + dfsiLC.decodeLDU1(data.get() + count, netLDU + 180U); + count += DFSI_LDU1_VOICE8_FRAME_LENGTH_BYTES; + + dfsiLC.setFrameType(DFSIFrameType::LDU1_VOICE9); + dfsiLC.decodeLDU1(data.get() + count, netLDU + 204U); + count += DFSI_LDU1_VOICE9_FRAME_LENGTH_BYTES; + + LogMessage(LOG_NET, P25_LDU1_STR " audio, srcId = %u, dstId = %u", srcId, dstId); + + control = lc::LC(*dfsiLC.control()); + + control.setSrcId(srcId); + control.setDstId(actualDstId); + + // if this is the beginning of a call and we have a valid HDU frame, extract the algo ID + if (frameType == FrameType::HDU_VALID) { + uint8_t algoId = buffer[181U]; + if (algoId != ALGO_UNENCRYPT) { + uint16_t kid = __GET_UINT16B(buffer, 182U); + + uint8_t mi[MI_LENGTH_BYTES]; + ::memset(mi, 0x00U, MI_LENGTH_BYTES); + for (uint8_t i = 0; i < MI_LENGTH_BYTES; i++) { + mi[i] = buffer[184U + i]; + } + + control.setAlgId(algoId); + control.setKId(kid); + control.setMI(mi); + } + } + + m_network->writeP25LDU1(control, lsd, netLDU, frameType); + } + break; + case DUID::LDU2: + if ((data[0U] == DFSIFrameType::LDU2_VOICE10) && (data[22U] == DFSIFrameType::LDU2_VOICE11) && + (data[36U] == DFSIFrameType::LDU2_VOICE12) && (data[53U] == DFSIFrameType::LDU2_VOICE13) && + (data[70U] == DFSIFrameType::LDU2_VOICE14) && (data[87U] == DFSIFrameType::LDU2_VOICE15) && + (data[104U] == DFSIFrameType::LDU2_VOICE16) && (data[121U] == DFSIFrameType::LDU2_VOICE17) && + (data[138U] == DFSIFrameType::LDU2_VOICE18)) { + + dfsi::LC dfsiLC = dfsi::LC(control, lsd); + + dfsiLC.setFrameType(DFSIFrameType::LDU2_VOICE10); + dfsiLC.decodeLDU2(data.get() + count, netLDU + 10U); + count += DFSI_LDU2_VOICE10_FRAME_LENGTH_BYTES; + + dfsiLC.setFrameType(DFSIFrameType::LDU2_VOICE11); + dfsiLC.decodeLDU2(data.get() + count, netLDU + 26U); + count += DFSI_LDU2_VOICE11_FRAME_LENGTH_BYTES; + + dfsiLC.setFrameType(DFSIFrameType::LDU2_VOICE12); + dfsiLC.decodeLDU2(data.get() + count, netLDU + 55U); + count += DFSI_LDU2_VOICE12_FRAME_LENGTH_BYTES; + + dfsiLC.setFrameType(DFSIFrameType::LDU2_VOICE13); + dfsiLC.decodeLDU2(data.get() + count, netLDU + 80U); + count += DFSI_LDU2_VOICE13_FRAME_LENGTH_BYTES; + + dfsiLC.setFrameType(DFSIFrameType::LDU2_VOICE14); + dfsiLC.decodeLDU2(data.get() + count, netLDU + 105U); + count += DFSI_LDU2_VOICE14_FRAME_LENGTH_BYTES; + + dfsiLC.setFrameType(DFSIFrameType::LDU2_VOICE15); + dfsiLC.decodeLDU2(data.get() + count, netLDU + 130U); + count += DFSI_LDU2_VOICE15_FRAME_LENGTH_BYTES; + + dfsiLC.setFrameType(DFSIFrameType::LDU2_VOICE16); + dfsiLC.decodeLDU2(data.get() + count, netLDU + 155U); + count += DFSI_LDU2_VOICE16_FRAME_LENGTH_BYTES; + + dfsiLC.setFrameType(DFSIFrameType::LDU2_VOICE17); + dfsiLC.decodeLDU2(data.get() + count, netLDU + 180U); + count += DFSI_LDU2_VOICE17_FRAME_LENGTH_BYTES; + + dfsiLC.setFrameType(DFSIFrameType::LDU2_VOICE18); + dfsiLC.decodeLDU2(data.get() + count, netLDU + 204U); + count += DFSI_LDU2_VOICE18_FRAME_LENGTH_BYTES; + + LogMessage(LOG_NET, P25_LDU2_STR " audio, algo = $%02X, kid = $%04X", dfsiLC.control()->getAlgId(), dfsiLC.control()->getKId()); + + control = lc::LC(*dfsiLC.control()); + + control.setSrcId(srcId); + control.setDstId(actualDstId); + + m_network->writeP25LDU2(control, lsd, netLDU); + } + break; + + case DUID::HDU: + case DUID::PDU: + case DUID::TDU: + case DUID::TDULC: + case DUID::TSDU: + case DUID::VSELP1: + case DUID::VSELP2: + default: + // this makes GCC happy + break; + } + } +} + +/* Entry point to network processing thread. */ + +void* HostPatch::threadNetworkProcess(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("patch:net-process"); + HostPatch* patch = static_cast(th->obj); + if (patch == 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 (!patch->m_running) { + Thread::sleep(1U); + continue; + } + + uint32_t length = 0U; + bool netReadRet = false; + if (patch->m_digiMode == TX_MODE_DMR) { + std::lock_guard lock(HostPatch::m_networkMutex); + UInt8Array dmrBuffer = patch->m_network->readDMR(netReadRet, length); + if (netReadRet) { + patch->processDMRNetwork(dmrBuffer.get(), length); + } + } + + if (patch->m_digiMode == TX_MODE_P25) { + std::lock_guard lock(HostPatch::m_networkMutex); + UInt8Array p25Buffer = patch->m_network->readP25(netReadRet, length); + if (netReadRet) { + patch->processP25Network(p25Buffer.get(), length); + } + } + + Thread::sleep(1U); + } + + LogMessage(LOG_HOST, "[STOP] %s", threadName.c_str()); + delete th; + } + + return nullptr; +} + +/* Helper to reset IMBE buffer with null frames. */ + +void HostPatch::resetWithNullAudio(uint8_t* data, bool encrypted) +{ + if (data == nullptr) + return; + + // clear buffer for next sequence + ::memset(data, 0x00U, 9U * 25U); + + // fill with null + if (!encrypted) { + ::memcpy(data + 10U, P25DEF::NULL_IMBE, 11U); + ::memcpy(data + 26U, P25DEF::NULL_IMBE, 11U); + ::memcpy(data + 55U, P25DEF::NULL_IMBE, 11U); + + ::memcpy(data + 80U, P25DEF::NULL_IMBE, 11U); + ::memcpy(data + 105U, P25DEF::NULL_IMBE, 11U); + ::memcpy(data + 130U, P25DEF::NULL_IMBE, 11U); + + ::memcpy(data + 155U, P25DEF::NULL_IMBE, 11U); + ::memcpy(data + 180U, P25DEF::NULL_IMBE, 11U); + ::memcpy(data + 204U, P25DEF::NULL_IMBE, 11U); + } + else { + ::memcpy(data + 10U, P25DEF::ENCRYPTED_NULL_IMBE, 11U); + ::memcpy(data + 26U, P25DEF::ENCRYPTED_NULL_IMBE, 11U); + ::memcpy(data + 55U, P25DEF::ENCRYPTED_NULL_IMBE, 11U); + + ::memcpy(data + 80U, P25DEF::ENCRYPTED_NULL_IMBE, 11U); + ::memcpy(data + 105U, P25DEF::ENCRYPTED_NULL_IMBE, 11U); + ::memcpy(data + 130U, P25DEF::ENCRYPTED_NULL_IMBE, 11U); + + ::memcpy(data + 155U, P25DEF::ENCRYPTED_NULL_IMBE, 11U); + ::memcpy(data + 180U, P25DEF::ENCRYPTED_NULL_IMBE, 11U); + ::memcpy(data + 204U, P25DEF::ENCRYPTED_NULL_IMBE, 11U); + } +} diff --git a/src/patch/HostPatch.h b/src/patch/HostPatch.h new file mode 100644 index 00000000..2e91a899 --- /dev/null +++ b/src/patch/HostPatch.h @@ -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 +#include + +// --------------------------------------------------------------------------- +// 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__ diff --git a/src/patch/PatchMain.cpp b/src/patch/PatchMain.cpp new file mode 100644 index 00000000..382c05cf --- /dev/null +++ b/src/patch/PatchMain.cpp @@ -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 +#include +#include + +#include + +// --------------------------------------------------------------------------- +// 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 ]" + "\n\n" + " -v show version information\n" + " -h show this screen\n" + " -f foreground mode\n" + "\n" + " -c 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 \ No newline at end of file diff --git a/src/patch/PatchMain.h b/src/patch/PatchMain.h new file mode 100644 index 00000000..9c2383a2 --- /dev/null +++ b/src/patch/PatchMain.h @@ -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 + +// --------------------------------------------------------------------------- +// 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__ diff --git a/src/patch/network/PeerNetwork.cpp b/src/patch/network/PeerNetwork.cpp new file mode 100644 index 00000000..63cd8c22 --- /dev/null +++ b/src/patch/network/PeerNetwork.cpp @@ -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 + +// --------------------------------------------------------------------------- +// 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(m_metadata->identity); // Identity + config["rxFrequency"].set(m_metadata->rxFrequency); // Rx Frequency + config["txFrequency"].set(m_metadata->txFrequency); // Tx Frequency + + // system info + json::object sysInfo = json::object(); + sysInfo["latitude"].set(m_metadata->latitude); // Latitude + sysInfo["longitude"].set(m_metadata->longitude); // Longitude + + sysInfo["height"].set(m_metadata->height); // Height + sysInfo["location"].set(m_metadata->location); // Location + config["info"].set(sysInfo); + + // channel data + json::object channel = json::object(); + channel["txPower"].set(m_metadata->power); // Tx Power + channel["txOffsetMhz"].set(m_metadata->txOffsetMhz); // Tx Offset (Mhz) + channel["chBandwidthKhz"].set(m_metadata->chBandwidthKhz); // Ch. Bandwidth (khz) + channel["channelId"].set(m_metadata->channelId); // Channel ID + channel["channelNo"].set(m_metadata->channelNo); // Channel No + config["channel"].set(channel); + + // RCON + json::object rcon = json::object(); + rcon["password"].set(m_metadata->restApiPassword); // REST API Password + rcon["port"].set(m_metadata->restApiPort); // REST API Port + config["rcon"].set(rcon); + + config["software"].set(std::string(software)); // Software ID + + json::value v = json::value(config); + std::string json = v.serialize(); + + CharArray __buffer = std::make_unique(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); +} diff --git a/src/patch/network/PeerNetwork.h b/src/patch/network/PeerNetwork.h new file mode 100644 index 00000000..081856bc --- /dev/null +++ b/src/patch/network/PeerNetwork.h @@ -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 +#include + +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__ diff --git a/tools/start-dvm-fne.sh b/tools/start-dvm-fne.sh index 8e4f05b0..51778e76 100755 --- a/tools/start-dvm-fne.sh +++ b/tools/start-dvm-fne.sh @@ -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