Merge R05A04 (r05a04_dev) into master (#114)

* rebuild r05a04_dev from nasty Git merge bullshit; implement handling of SNDCP on the FNE instead of dvmhost; add quick sanity Catch2 testcases; BUGFIX: NXDN SACCH was incorrectly handling the RAN and structure causing the structure value to become overwritten; correct badly set example IP range in FNE config; add AI generated documentation for the network statck, FNE REST and DVMHost REST; update version number for next dev version;

* remove old documents;

* BUGFIX: fix some unbounded array checking;

* enhance InfluxDB implementation: fix bad handling on Windows; fix bad use of free() for address info freeaddrinfo(); fix partial data writes for bigger payloads; enhance getting responses back from InfluxDB (this makes it so that InfluxDB cannot fail silently easily);

* EXPERIMENTAL: add an adaptive jitter buffer implementation to the FNE to better deal with peers on bad connections that may send packets out-of-sequence;

* remove unnecessary garbage in AI generated doc; use references for autos when looping peers; normalize jitter buffer log messages;

* cleanup doc;

* properly move hastily erected debug YAML config to the peer ACL file instead;

* update jitter buffer doc;

* BUGFIX: correct naive implementation that did not consider the length of the packet being sent, which could result in a nullptr after casting the ip struct; BUGFIX: send actual bytes read from VTUN vs the default MTU size;

* BUGFIX: correct similar ip struct casting problem;

* implement a very preliminary DMR to VTUN IP dispatcher based on the P25 implementation (while I am sure this is probably going to be quite iffy, I am quite sure the VTUN TAP interface method we are using *is* the right direction; even though the P25 implementation is iffy, with iteration over time I suspect it is the correct approach, as such I have implemented a DMR equiviliant based on the P25 implementation);

* use 3/4 rate instead of 1 rate; add some dev notes future me;

* implement preliminary positive response to KMM HELLO (this still will fail with No-Service due to missing implementation for per-SU UKEK implementation from the crypto container);

* add some extra logging for VTUN PDU;

* better implement V(R) V(S) checking;

* add prereq logging of last load time for various lookup tables;

* add fne-stats; add reload peers and crypto;

* update FNE REST API doc;

* WIN32: fix ctime_r and add Win32 compat for ctime_s so we can compile on Win32 again;

* Win32: FNE's P25PacketData clock() in its entirety should do nothing on Windows;

* shut the compiler up about order of constructor initializers;

* EXPERIMENTAL: add option for 1 second delayed network TDU when a subscriber dekeys;

* slight TUI color palette change;

* implement support to configure adaptive jitter buffer from peered;

* initialize FinalCut palette earlier;

* document that key IDs are hex not dec;

* BUGFIX: lets not memset past the possible end of the key array....;

* track active call counts (this might be slightly error prone, so we will at best call this an approximate number);

* attempt to validate source IP/port during a RPTL, RPTK or RPTC event when RPTL, RPTK or RPTC is sent while not in the correct login state;

* BUGFIX: dvmpatch did not properly release the network stream ID at the termination of a call, resulting in a hang up;

* BUGFIX: for R05A04s 1s delayed TDU, make sure we have valid IDs before allowing a delayed TDU;

* BUGFIX: add more stream ID lockout reset handling;

* BUGFIX: add more stream ID lockout reset handling;

* add some experiemental support to dvmpatch to support clear to enc one-way patching;

* BUGFIX: only consider jitter buffer for RTP frames not carrying the end of call sequence (65535);

* BUGFIX: always allow the network stack to process and pass RTP frames carrying the end of call sequence (65535);

* BUGFIX: dvmbridge, ignore UDP frame timeouts when using RTP or USRP UDP streams;

* add dropped call support to dvmpatch; refactor/rewrite dvmpatch encryption support;

* hide debug messages;

* BUGFIX: validate destination matches before handling TDU;

* report ssrc for late packets;

* prevent issue with 0 callDstId on dvmpatch; reject TDUs with no destination (these are invalid and can fuck off);

* add separate packet dumping option to the network stack, to allow for less-verbose debugging messaging (debugigng where packet dump/trace is not required);

* BUGFIX: fix condition in P25 call handler where if the TSBK was not decoded it could result in a crash;

* add network debug trace around stream ID reset calls;

* better handle framequeue debugging when not performing packet dumping;

* implement proper packet reordering for P25 PDU data; BUGFIX: add boundary checking for P25 PDU disassembly;

* BUGFIX: fix startup crash of dvmbridge when using analog audio mode; add transmit voice frame status for analog audio; correct core network issue determining length of analog audio frames;

* bump copyright dates;

* BUGFIX: for unauthorized, unconnected failures the NAK response was not sending the appropriate RTP sequence;

* enhance REST API logging; add dev REST API commands to force send NAK packets;

* add PDU block reordering support to dvmhost;

* minor bugfix, correct incorrect log module for host sourced analog audio;

* very experimental fix for #111, along with additional enhancements to repeat parrot traffic to all VCs of a trunk site to ensure parrot traffic repeats;

* update bridge config and expressly document the need to properly configure timing parameters;

* make the program status variable m_running a static s_running for dvmpatch; ensure PeerNetwork for dvmpatch uses m_packetDump instead of m_debug for packet dumping;

* (we're still hunting the cause of bridge ceasing UDP socket operations after a period of time, it *seems* to occur on Win32 only though); make the program status variable m_running a static s_running for dvmbridge; ensure PeerNetwork for dvmbridge uses m_packetDump instead of m_debug for packet dumping; fix shutdown issue with local audio causing a crash for dvmbridge; refactor how RTP and uLaw frames work; remove and deprecate the no include length configuration parameter; refactor and centralize UDP audio writing functionality; add verbose log messages for if the s_running flag becomes non-true during operation;

* fix concurrency lockup for DMR granting;

* remove main thread sleep control this causes WinSock on Win32 to lose its mind when polled too fast;

* move sample level dumping option to CLI;

* always reset the call streams at the end of a call;

* increase packet processing latency warning to 250ms (1/4 second);

* begin adding support for P25P2 network transiting;

* add some verbose debug trace for checkNetTrafficCollision();

* begin defining P25 Phase 2 primitives;

* P2 abbreviated/explicit partition MCOs are essentially just TSBK opcodes;

* add MFID specific partition type;

* refactor MAC MCO defines;

* correct comment;

* make slot numerical;

* whoops meant == not =;

* implement RTP audio pacing by timestamp sent by source;

* for maintainability split modes into separate source CPP files; BUGFIX: fix buffer overflow when copying PCM data; allow uLaw encoded RTP frames to also carry source and dest metadata;

* preamble tones are not supported for UDP audio;

* BUGFIX: dvmpatch do not send LDU2s until network is in non-idle state;

* convert direct MMDVM TDU to a timed TDU; correct some handling of LDU1 srcId and dstId;

* remove dvmmon, R05A02 will be the last version to support dvmmon, R05A04+ will not support it in favor of using sysview from the FNE for monitoring;

* add more P25 Phase 2 primitives for S-OEMI sync and Hamming (8,4,4) for P2 DUID handling;

* more P25 Phase 2 constants;

* extreme preliminary work to handle P25 Phase 2 MAC messages (nothing uses this yet this is for future proofing);

* continued work on preliminary instrumentation for Phase 2 MAC handling;

* fix missing parens;

* split the decodeVCH_MACPDU into two functions, one for IEMI the other for OEMI;

* bugfix and correct encode/decode for RS 52,30,23, 46,26,21, 45,26,20, 44,16,29; implement catch2 cases for P25 Phase 2 RS codes; correct bad EC check for RS 24,12,13, 24,16,9 and 36,20,17 that could cause false positive failures in edge case conditions;

* implement and add more EDAC verification test;

* add DMR EMB and QR 16,7,6 tests; correct issue where QR 16,7,6 decode() was correcting and returning the wrong number of bits; correct issue where DMR EMB would not actually use the corrected QR 16,7,6 codeword;

* finally what I was trying to get to -- add testcases for the new P2 MAC PDU logic; correct implementation problems with the P2 MAC PDU handling for OEMI and IEMI (the implementation still requires handling scrambling);

* add missing Hamming FEC test cases;

* finish out the base crypto test suite and include the DES algo;

* (this first part is subject to Git revert if it becomes problematic, but because this is a dev branch I am gonna roll with it) revert FrameQueue timestamp list/map changes back to pre-R04J32, the original implementation was far cleaner and faster with O(1) speed for timestamp lookup vs the O(n) lookup, additionally, the newer R04J32+ implementation introduces a pointer that can be dereferenced incorrectly causing a edge case crash; fix the implementation for handling the mutex for timestamp map locking, the implementation used a incorrect instance mutex which could in high-traffic conditions cause a race condition and ultimately a crash;

* for condition where a downstream peer is trying to connect to us, only validate the IP instead of IP/port;

* EXPERIMENTAL (and untested): allow source ID overriding for parrot playback, this feature will rewrite the source ID to a static ID configured in the FNE configuration, for P25, NXDN and Analog this rewrite is striaghtfoward, for DMR this will likely work oddly due to the way DMR has source ID data embedded in the transited data frames;

* validate the RPC data length includes the message;

* BUGFIX: handle edge case where a opcode $27 is not an ISP_EMERG_ALARM_REQ but rather its a network sourced OSP_DENY_RSP; BUGFIX: handle incorrect decoding of OSP_DENY_RSP by SysView; BUGFIX: for DVRS operation properly set MFG_MOT for DENY and QUEUE outbounds;

* do not use [] access for grantTimers and unitRegTimers, use find() lookups these are safer;

* issue a notify CC channel release on a ChanRelease TDULC;

* add explicit TDU TG release option, this option allows a CC to process incoming TDUs to determine if a channel grant should be released; add more explicit TDU process logging to the FNE, ensure TDUs being sent outside a call are being logged properly; during a TG unpermit on a non-authoritative VC, transmit a burst of TDUs on the outbound VC RF interface; add extra verbosity to the active TG logging;

* add option displayModemDebugMessages to optionally disable or enable debug log messages coming *from* the modem; properly check if we're operating in DFSI or not and if we are do not attempt to set the FIFO buffer lengths or clear buffers related to DMR or NXDN; correct deletion of array types in the DFSICallData structure; correct bad length of the VHDR1 during DFSICallData initialization, the length defaulted to TIA mode which would cause a crash when trying to delete and deallocate VHDR1 when used in V.24 mode;

* correct TSBK, TDULC and PDU transmission using V.24 DFSI, timing must be applied like IMBE; lengthen the high-level CC generation timer by 10ms, this has no impact on regular air and hotspot modems, but better times CC frames for V.24 DFSI modems; refactor naming to be clearer for the V.24 jitter buffer transmit types; because V.24 uses OSP_TSBK_RAW and LC_TDULC_RAW correct several off by 2 shift bugs; remove unnecessary V.24 start of stream and end of stream calls;

* allow TDUs to be sent regardless of whether a V.24 call was started or not;

* do work on V.24 PDU support (inbound should be working pretty much perfect now, outbound -- is still broken);

* V.24 PDU work;

* allow the user to adjust the frame timeout length when using frame timing at the bridge; ensure audio frames aren't greater then x2 the size of an expected audio frame;

* after consideration entirely refactor how UDP frame timing is generated internally when we're using raw PCM; fix a variety of bugs related to locking and handling of the deque for UDP audio frames; better insert silence at the start and end of calls;

* disable this debug trace, its too noisy;

* do not consider active call or call counts for parrot;

* add call collisions to the stats counters; add REST APIs to reset and zero call counters;

* log non-call DMR terminators like P25;

* correct endpoints for counter reset APIs;

* reset active call count on a maintainence loop;

* increase V.24 outbound rate for TSDUs;

* properly set the V.24 Tx buffer size to the FIFO length like done for air and hotspot modems; pass whether or not the frame being written to the modem is an immediate frame; modify V.24 modem to properly maintain two independant buffers, one for background/normal priority frames, and one for immediate priority frames (this helps with CC mode delays, there are more buffering issues, likely at the modem to mitgate but thats a future project);

* update gitignore to include some python stuff; add very very preliminary Python tool that helps generate dvmhost configurations;

* add option to disable deny responses in conventional for radios that are braindead and interpret an OSP_DENY as a ISP_EMERG because of opcode reuse (thanks TIA-102);

* delete array instead of delete;

* add protections to prevent the network mutex from locking too long; add proper network watchdog to prevent unterminated calls from hanging bridge;

* ICC and GRANT shouldnt NAK, as this will cause a straight disconnect/reconnect, ignore them instead;

* fix issue where in-call control might start looping requests;

* lets make sure RID0 is never used, treat as WUID_FNE instead;

* only send ICC upstream when the ICC request originates on the local FNE;

* Revise cross-compilation instructions in README

Updated cross-compilation instructions for ARM 32-bit and ARM 64-bit, including examples and required packages.

* relabel some source files in the FNE, FNENetwork -> TrafficNetwork, DiagNetwork -> MetadataNetwork; remove support for disabling the alternate port, this feature is mandatory now;

* identify a software SDR;

* add support to dvmcfggen for logging configuration; add support to dvmcfggen for user supplied answers files to automate skipping certain wizard questions;

* typo;

* HIGHLY_UNECESSARY_DISCLAIMER_FOR_THE_MENTAL;

* the typo of that define was driving my OCD nuts;

* include new statement in README.md;

* enforce a users usage understanding by adding a startup configuration parameter that must be set;

* these flags are root level;

* make not be stupid error more clear;

* copy legalese from config.yml to README.md;

* policy updates

* Update usage_guidelines.md

* create issue & pr templates

* remove dvmmon from bug & pr template

* revert how timestamping is generated;

* BUGFIX: fix incorrectly set length for RTP packets, we were incorrectly adding 4U bytes which does not apply to RTP frames;

* enrage the entire community by cleaning up the bridge configuration to be more sane -- a lot of these parameters had nothing to do with FNE network configuration;

* code cleanup (no functional change);

* fix missing marker bit on RTP seq 0 to make jsb stop complaining; fix missing CTS controller teardown;

* properly set the FIRST seq (when seq is reset to 0) as a marker; because JSB is *REALLY* hell bent on this, implement continuous RTP seq to make him happy;

* wtf;

* cleanup typo and bad spacing due to bad VSCode configuration; correct memory leak when handling buffer drop due to new stream packet wraparound;

* correct issue where RTP sequences were not stepped appropriately;

* ignore m_audioDetect flag for UDP audio outbound to the FNE; attempt to correct becoming stuck on incoming RTP timestamps;

* whoops remove debug trace line;

* remove incorrect shift;

* ensure network watchdog resets all states; ignore TDU/TDULC when local audio or UDP traffic is running;

* reset callStartTime for TGs who receive Non-Call TDU/Terminators;

* remove duration log from Non-Call TDU;

* disable the expected TS code for RTP frames;

* add mutex locking when making a copy of the RadioId table to prevent thread-based race conditions;

---------

Co-authored-by: Dev_Ranger <30966416+DevRanger@users.noreply.github.com>
fix_nid_array_cleanup 2026-03-02
Bryan Biedenkapp 4 weeks ago committed by GitHub
parent 49ff1a461b
commit 274b805517
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,139 @@
name: Bug report
description: Report a reproducible bug in dvmhost or related components
title: "[BUG] "
labels: ["bug"]
body:
- type: markdown
attributes:
value: |
**Issues are for reproducible bugs only.
Feature requests and roadmap suggestions will be closed.**
Please provide enough detail for us to reproduce the issue.
Full logs are strongly preferred over screenshots or partial snippets.
- type: dropdown
id: component
attributes:
label: Component
description: Select the affected module
options:
- dvmhost
- dvmfne
- dvmbridge
- dvmpatch
- dvmcmd
- sysview
- tged
- peered
- Other
validations:
required: true
- type: input
id: component_other
attributes:
label: If "Other", specify component
placeholder: "Describe the affected component"
- type: input
id: version
attributes:
label: Version / Commit
description: Provide the exact commit SHA or tag
placeholder: "ex: v1.2.3 or a1b2c3d4"
validations:
required: true
- type: dropdown
id: build_type
attributes:
label: Build type
options:
- Built from source (native)
- Cross-compiled
- Custom packaging
validations:
required: true
- type: input
id: compiler
attributes:
label: Compiler version
description: Output of `gcc --version` or `clang --version`
placeholder: "ex: GCC 13.2.0"
validations:
required: true
- type: input
id: environment
attributes:
label: Operating system / architecture
placeholder: "ex: Debian 12 x86_64"
validations:
required: true
- type: textarea
id: build_flags
attributes:
label: Build flags / CMake options
description: Include any special flags used (ex: cross-compile options)
placeholder: |
Example:
-DCROSS_COMPILE_ARM=1
-DCMAKE_BUILD_TYPE=Release
render: text
- type: textarea
id: summary
attributes:
label: Summary
description: Brief description of the issue
validations:
required: true
- type: textarea
id: expected
attributes:
label: Expected behavior
validations:
required: true
- type: textarea
id: actual
attributes:
label: Actual behavior
description: Include exact error messages if applicable
validations:
required: true
- type: textarea
id: repro
attributes:
label: Steps to reproduce
placeholder: |
1.
2.
3.
validations:
required: true
- type: textarea
id: logs
attributes:
label: Full logs
description: Paste complete logs or attach files. Redact secrets.
render: text
- type: textarea
id: config
attributes:
label: Relevant config (redacted)
description: Include only relevant sections with secrets removed
render: text
- type: textarea
id: additional
attributes:
label: Additional context

@ -0,0 +1,6 @@
blank_issues_enabled: false
contact_links:
- name: DVMProject Community (Discord)
url: https://discord.dvmproject.io/
about: For questions, discussion, and general support, please join our Discord community.

@ -0,0 +1,49 @@
## Summary
Describe the change and why it is needed.
## Component(s)
Which part(s) of the project does this affect?
- [ ] dvmhost
- [ ] dvmfne
- [ ] dvmbridge
- [ ] dvmpatch
- [ ] dvmcmd
- [ ] sysview
- [ ] tged
- [ ] peered
- [ ] Other (explain below)
## Type of change
- [ ] Bug fix
- [ ] Refactor / cleanup
- [ ] Performance improvement
- [ ] Documentation update
- [ ] Build / tooling change
## Related issues
Link any related bug reports.
Closes #
Refs #
## Build & test notes
Explain how this was built and tested.
Include:
- OS / distro
- Compiler version
- Any special build flags
## Logs / output (if applicable)
Paste relevant output demonstrating the fix or behavior change.
## Checklist
- [ ] Change is scoped and focused
- [ ] Existing functionality verified
- [ ] No unrelated refactors included
- [ ] Documentation updated if needed
- [ ] No secrets or credentials included
## Notes for maintainers
Anything reviewers should be aware of?

3
.gitignore vendored

@ -65,6 +65,9 @@ package/
*.ini
.vs
.idea/
venv/
__pycache__/
*.pyc
# Prerequisites
*.d

@ -278,7 +278,6 @@ install(TARGETS dvmhost DESTINATION ${CMAKE_INSTALL_PREFIX}/bin)
install(TARGETS dvmcmd DESTINATION ${CMAKE_INSTALL_PREFIX}/bin)
install(TARGETS dvmfne DESTINATION ${CMAKE_INSTALL_PREFIX}/bin)
if (ENABLE_TUI_SUPPORT AND (NOT DISABLE_TUI_APPS))
install(TARGETS dvmmon DESTINATION ${CMAKE_INSTALL_PREFIX}/bin)
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))
@ -302,7 +301,6 @@ if (NOT 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 dvmmon
COMMAND arm-linux-gnueabihf-strip -s sysview
COMMAND arm-linux-gnueabihf-strip -s tged
COMMAND arm-linux-gnueabihf-strip -s peered
@ -322,7 +320,6 @@ if (NOT 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 dvmmon
COMMAND aarch64-linux-gnu-strip -s sysview
COMMAND aarch64-linux-gnu-strip -s tged
COMMAND aarch64-linux-gnu-strip -s peered
@ -342,7 +339,6 @@ if (NOT TARGET strip)
COMMAND strip -s dvmhost
COMMAND strip -s dvmfne
COMMAND strip -s dvmcmd
COMMAND strip -s dvmmon
COMMAND strip -s sysview
COMMAND strip -s tged
COMMAND strip -s peered
@ -378,7 +374,6 @@ if (NOT TARGET tarball)
COMMAND touch ${CMAKE_INSTALL_PREFIX_TARBALL}/dvm/log/INCLUDE_DIRECTORY
COMMAND cp -v dvmhost ${CMAKE_INSTALL_PREFIX_TARBALL}/dvm/bin
COMMAND cp -v dvmcmd ${CMAKE_INSTALL_PREFIX_TARBALL}/dvm/bin
COMMAND cp -v dvmmon ${CMAKE_INSTALL_PREFIX_TARBALL}/dvm/bin
COMMAND cp -v sysview ${CMAKE_INSTALL_PREFIX_TARBALL}/dvm/bin
COMMAND cp -v tged ${CMAKE_INSTALL_PREFIX_TARBALL}/dvm/bin
COMMAND cp -v peered ${CMAKE_INSTALL_PREFIX_TARBALL}/dvm/bin
@ -459,7 +454,6 @@ if (NOT TARGET tarball_notools)
COMMAND touch ${CMAKE_INSTALL_PREFIX_TARBALL}/dvm/log/INCLUDE_DIRECTORY
COMMAND cp -v dvmhost ${CMAKE_INSTALL_PREFIX_TARBALL}/dvm/bin
COMMAND cp -v dvmcmd ${CMAKE_INSTALL_PREFIX_TARBALL}/dvm/bin
COMMAND cp -v dvmmon ${CMAKE_INSTALL_PREFIX_TARBALL}/dvm/bin
COMMAND cp -v sysview ${CMAKE_INSTALL_PREFIX_TARBALL}/dvm/bin
COMMAND cp -v tged ${CMAKE_INSTALL_PREFIX_TARBALL}/dvm/bin
COMMAND cp -v peered ${CMAKE_INSTALL_PREFIX_TARBALL}/dvm/bin
@ -537,7 +531,6 @@ add_custom_target(old_install
COMMAND mkdir -p ${CMAKE_LEGACY_INSTALL_PREFIX}/schema
COMMAND install -m 755 dvmhost ${CMAKE_LEGACY_INSTALL_PREFIX}/bin
COMMAND install -m 755 dvmcmd ${CMAKE_LEGACY_INSTALL_PREFIX}/bin
COMMAND install -m 755 dvmmon ${CMAKE_LEGACY_INSTALL_PREFIX}/bin
COMMAND install -m 755 sysview ${CMAKE_LEGACY_INSTALL_PREFIX}/bin
COMMAND install -m 755 tged ${CMAKE_LEGACY_INSTALL_PREFIX}/bin
COMMAND install -m 755 peered ${CMAKE_LEGACY_INSTALL_PREFIX}/bin

@ -8,6 +8,8 @@ The Digital Voice Modem ("DVM") Core Software Suite, provides the a set of appli
Please feel free to reach out to us for help, comments or otherwise, on our Discord: https://discord.gg/3pBe8xgrEz
**THIS SOFTWARE MUST NEVER BE USED IN PUBLIC SAFETY OR LIFE SAFETY CRITICAL APPLICATIONS! This software project is provided solely for personal, non-commercial, hobbyist use; any commercial, professional, governmental, or other non-hobbyist use is strictly discouraged, fully unsupported and expressly disclaimed by the authors. For full project policies and support limitations, see: [Usage & Support Guidelines](usage_guidelines.md).**
This project suite generates a few executables:
### Core Applications
@ -90,12 +92,21 @@ sudo apt-get install libdw-dev:arm64
dvmhost/build # sudo make old_install
```
If cross-compiling is required (for either ARM 32bit, 64bit or old Raspberry Pi ARM 32bit), the CMake build system has some options:
If cross-compiling is required (ARM 32-bit or ARM64), the CMake build system provides the following options:
- `-DCROSS_COMPILE_ARM=1` - Cross-compile **dvmhost** for generic ARM 32-bit
(ex: Raspberry Pi running a 32-bit OS; Debian/Raspbian Bullseye or newer)
- `-DCROSS_COMPILE_AARCH64=1` - Cross-compile **dvmhost** for generic ARM 64-bit
(ex: Raspberry Pi 4+ or other ARM64 systems; Debian/Raspbian Bullseye or newer)
- `-DCROSS_COMPILE_ARM=1` - This will cross-compile dvmhost for generic ARM 32bit. (RPi4+ running 32-bit distro's can fall into this category [on Debian/Rasbpian anything bullseye or newer])
- `-DCROSS_COMPILE_AARCH64=1` - This will cross-compile dvmhost for generic ARM 64bit. (RPi4+ running 64-bit distro's can fall into this category [on Debian/Rasbpian anything bullseye or newer])
Please note cross-compilation requires you to have the appropriate development packages installed for your system.:
Please note cross-compliation requires you to have the appropriate development packages installed for your system. For ARM 32-bit, on Debian/Ubuntu OS install the "arm-linux-gnueabihf-gcc" and "arm-linux-gnueabihf-g++" packages. For ARM 64-bit, on Debian/Ubuntu OS install the "aarch64-linux-gnu-gcc" and "aarch64-linux-gnu-g++" packages.
- **ARM 32-bit (Debian/Ubuntu):**
`arm-linux-gnueabihf-gcc`, `arm-linux-gnueabihf-g++`
- **ARM 64-bit (Debian/Ubuntu):**
`aarch64-linux-gnu-gcc`, `aarch64-linux-gnu-g++`
[See project notes](#project-notes).
@ -406,3 +417,7 @@ After finishing these steps, reboot.
## License
This project is licensed under the GPLv2 License - see the [LICENSE](LICENSE) file for details. Use of this project is intended, for amateur and/or educational use ONLY. Any other use is at the risk of user and all commercial purposes is strictly discouraged.
**THIS SOFTWARE MUST NEVER BE USED IN PUBLIC SAFETY OR LIFE SAFETY CRITICAL APPLICATIONS! This software project is provided solely for personal, non-commercial, hobbyist use; any commercial, professional, governmental, or other non-hobbyist use is strictly discouraged, fully unsupported and expressly disclaimed by the authors.**
By using this software, you agree to indemnify, defend, and hold harmless the authors, contributors, and affiliated parties from and against any and all claims, liabilities, damages, losses, or expenses (including reasonable attorneys fees) arising out of or relating to any unlawful, unauthorized, or improper use of the software.

@ -52,48 +52,11 @@ network:
# Flag indicating whether or not the host diagnostic log will be sent to the network.
allowDiagnosticTransfer: true
# Flag indicating whether or not packet dumping is enabled.
packetDump: false
# Flag indicating whether or not verbose debug logging is enabled.
debug: false
# Enable PCM audio over UDP.
udpAudio: false
# Enable meta data such as dstId and srcId in the UDP data.
udpMetadata: false
# PCM over UDP send port.
udpSendPort: 34001
# PCM over UDP send address destination.
udpSendAddress: "127.0.0.1"
# PCM over UDP receive port.
udpReceivePort: 32001
# PCM over UDP receive address.
udpReceiveAddress: "127.0.0.1"
# Flag indicating UDP audio should be encoded using G.711 uLaw.
udpUseULaw: false
# Flag indicating UDP audio should be transmitted without the length leader.
# NOTE: This flag is only applicable when encoding G.711 uLaw.
udpNoIncludeLength: false
# Flag indicating UDP audio should be RTP framed.
# NOTE: This flag is only applicable when encoding G.711 uLaw.
udpRTPFrames: false
# Flag indicating UDP audio should follow the USRP format.
udpUsrp: false
# Flag indicating UDP audio frame timing will be performed at the bridge.
# (This allows the sending source to send audio as fast as it wants.)
udpFrameTiming: false
# Traffic Encryption
tek:
# Flag indicating whether or not traffic encryption is enabled.
enable: false
# Traffic Encryption Key Algorithm
# aes - AES-256 Encryption
# arc4 - ARC4/ADP Encryption
tekAlgo: "aes"
# Traffic Encryption Key ID
tekKeyId: 1
# Source "Radio ID" for transmitted audio frames.
sourceId: 1234567
# Flag indicating the source "Radio ID" will be overridden from the detected
@ -119,6 +82,12 @@ system:
# System ID.
sysId: 001
# Flag indicating whether a network grant demand packet will be sent before audio.
grantDemand: false
# Audio transmit mode (1 - DMR, 2 - P25, 3 - Analog).
txMode: 1
# PCM audio gain for received (from digital network) audio frames.
# - This is used to apply gain to the decoded IMBE/AMBE audio, post-decoding.
rxAudioGain: 1.0
@ -139,8 +108,30 @@ system:
# - (Not used when utilizing external USB vocoder!)
vocoderEncoderAudioGain: 3.0
# Audio transmit mode (1 - DMR, 2 - P25, 3 - Analog).
txMode: 1
# Flag indicating whether or not trace logging is enabled.
trace: false
# Flag indicating whether or not debug logging is enabled.
debug: false
#
# Traffic Encryption Key (TEK) Configuration
#
tek:
# Flag indicating whether or not traffic encryption is enabled.
enable: false
# Traffic Encryption Key Algorithm
# aes - AES-256 Encryption
# arc4 - ARC4/ADP Encryption
tekAlgo: "aes"
# Traffic Encryption Key ID (Hex)
tekKeyId: 1
#
# Local Audio Transport
#
# Enable local audio over speakers.
localAudio: true
# Relative sample level for VOX to activate.
voxSampleLevel: 80.0
@ -160,21 +151,58 @@ system:
# Amount of time (ms) to transmit preamble tone.
preambleLength: 200
# Flag indicating the detected VOX sample level should be dumped to the log (useful for setting VOX levels).
dumpSampleLevel: false
#
# UDP Audio Transport Configuration
# NOTE: Validate the UDP Audio Configuration section below when using UDP audio. Bridge does *not*
# by default, perform any timing for UDP audio, so proper configuration is required.
#
# Flag indicating whether a network grant demand packet will be sent before audio.
grantDemand: false
# Enable PCM audio over UDP.
udpAudio: false
# Enable meta data such as dstId and srcId in the UDP data.
udpMetadata: false
# PCM over UDP send port.
udpSendPort: 34001
# PCM over UDP send address destination.
udpSendAddress: "127.0.0.1"
# PCM over UDP receive port.
udpReceivePort: 32001
# PCM over UDP receive address.
udpReceiveAddress: "127.0.0.1"
# Enable local audio over speakers.
localAudio: true
#
# UDP Audio Configuration
# NOTE: When configuring UDP audio for back-to-back transcoding, it is highly recommended to
# enable 'udpRTPFrames' and 'udpUseULaw' to ensure proper timing and framing of audio packets.
#
# Flag indicating whether or not trace logging is enabled.
trace: false
# Flag indicating whether or not debug logging is enabled.
debug: false
# Flag indicating UDP audio should be RTP framed.
udpRTPFrames: true
# Flag indicating UDP audio RTP timing should be ignored.
# (This allows the sending source to send audio as fast as it wants. This should not be used in combination
# with 'udpFrameTiming', and is intended for diagnostic purposes only.)
udpIgnoreRTPTiming: false
# Flag indicating UDP audio RTP sequence numbers should be continuous across packets and only reset to 0
# upon sequence rollover. (DO NOT use this option for back-to-back transcoding with another instance of the bridge,
# as it may cause issues with RTP sequence number handling, this option is intended for third-party RTP consumers
# who idiotically implement RTP and do not properly adhere to marker bits.)
udpRTPContinuousSeq: false
# Flag indicating UDP audio should be encoded using G.711 uLaw.
# NOTE: This flag is only applicable when sending audio via RTP.
udpUseULaw: false
# Flag indicating UDP audio should follow the USRP format.
udpUsrp: false
# Flag indicating UDP audio frame timing will be performed by the bridge.
# (This allows the sending source to send audio as fast as it wants. This should not be used in combination
# with 'udpRTPFrames'.)
udpFrameTiming: false
#
# RTS PTT Configuration
#
# Flag indicating whether RTS PTT control is enabled.
rtsPttEnable: false
# Serial port device for RTS PTT control (e.g., /dev/ttyUSB0).
@ -182,7 +210,10 @@ system:
# Hold-off time (ms) before clearing RTS PTT after last audio output.
rtsPttHoldoffMs: 250
#
# CTS COR Configuration
#
# Flag indicating whether CTS-based COR detection is enabled.
ctsCorEnable: false
# Serial port device for CTS COR (e.g., /dev/ttyUSB0). Often same as RTS PTT.

@ -5,6 +5,19 @@
# Flag indicating whether the host will run as a background or foreground task.
daemon: true
#
# This flag should be set to 'true', it simply means that you acknowledge the license and restrictions of use for this
# software, at no time should THIS SOFTWARE EVER BE USED IN PUBLIC SAFETY OR LIFE SAFETY CRITICAL APPLICATIONS. This
# software project is provided solely for personal, non-commercial, hobbyist use; any commercial, professional,
# governmental, or other non-hobbyist use is strictly discouraged, fully unsupported and expressly disclaimed by
# the authors.
#
# By using this software, you agree to indemnify, defend, and hold harmless the authors, contributors, and affiliated
# parties from and against any and all claims, liabilities, damages, losses, or expenses (including reasonable
# attorneys fees) arising out of or relating to any unlawful, unauthorized, or improper use of the software.
#
iAgreeNotToBeStupid: false
#
# Logging Configuration
#
@ -76,6 +89,8 @@ network:
# Flag indicating whether or not the host status will be sent to the network.
allowStatusTransfer: true
# Flag indicating whether or not packet dumping is enabled.
packetDump: false
# Flag indicating whether or not verbose debug logging is enabled.
debug: false
@ -229,6 +244,8 @@ protocols:
disableGrantSourceIdCheck: false
# Flag indicating whether or not the adjacent site broadcasts are disabled.
disableAdjSiteBroadcast: false
# Flag indicating a dedicated CC while process call TDUs and explicitly release grants when a call TDU occurs.
explicitTDUGrantRelease: true
# Flag indicating immediate TSDUs will be sent twice.
redundantImmediate: true
# Flag indicating whether redundant grant responses should be transmitted.
@ -249,6 +266,9 @@ protocols:
forceAllowTG0: false
# Flag indicating whether or not a TGID will be tested for affiliations before being granted.
ignoreAffiliationCheck: false
# Flag indicating whether or not a P25 TDU (call terminator) will occur to the network immmediately after a
# SU dekey, or on a fixed 1 second delay. (This is useful for systems that require fast call termination after dekeying.)
immediateCallTerm: true
# Flag indicating that the host will attempt to automatically inhibit unauthorized RIDs (those not in the
# RID ACL list).
inhibitUnauthorized: false
@ -288,6 +308,11 @@ protocols:
unitToUnitAvailCheck: false
# Flag indicating explicit source ID support is enabled.
allowExplicitSourceId: false
# Flag indicating whether or not the host will disable sending deny responses when operating in conventional.
# (Some subscriber radios do not handle deny responses in conventional, and rather interpret them as
# a emergency call trigger, this option allows the deny response to be disabled to prevent errant emergency
# call triggers.)
disableDenyResponse: false
# Flag indicating whether or not the host will respond to SNDCP data requests.
sndcpSupport: false
# BER/Error threshold for silencing voice packets. (0 or 1233 disables)
@ -682,6 +707,8 @@ system:
ignoreModemConfigArea: false
# Flag indicating whether verbose dumping of the modem status is enabled.
dumpModemStatus: false
# Flag indicating whether or not modem debug messages are displayed to the log.
displayModemDebugMessages: false
# Flag indicating whether or not trace logging is enabled.
trace: false
# Flag indicating whether or not debug logging is enabled.

@ -5,6 +5,19 @@
# Flag indicating whether the host will run as a background or foreground task.
daemon: true
#
# This flag should be set to 'true', it simply means that you acknowledge the license and restrictions of use for this
# software, at no time should THIS SOFTWARE EVER BE USED IN PUBLIC SAFETY OR LIFE SAFETY CRITICAL APPLICATIONS. This
# software project is provided solely for personal, non-commercial, hobbyist use; any commercial, professional,
# governmental, or other non-hobbyist use is strictly discouraged, fully unsupported and expressly disclaimed by
# the authors.
#
# By using this software, you agree to indemnify, defend, and hold harmless the authors, contributors, and affiliated
# parties from and against any and all claims, liabilities, damages, losses, or expenses (including reasonable
# attorneys fees) arising out of or relating to any unlawful, unauthorized, or improper use of the software.
#
iAgreeNotToBeStupid: false
#
# Logging Configuration
# Logging Levels:
@ -40,13 +53,15 @@ master:
# Hostname/IP address to listen on (blank for all).
address: 0.0.0.0
# Port number to listen on.
# NOTE: This port number includes itself for traffic, and master port + 1 for diagnostics and activity logging. (For
# example, a master port of 62031 will use 62032 for diagnostic and activity messages.)
# NOTE: This port number includes itself for traffic, and master port + 1 for metadata. (For
# example, a master port of 62031 will use 62032 for metadata messages.)
port: 62031
# FNE access password.
password: RPT1234
# Flag indicating whether or not verbose logging is enabled.
verbose: true
# Flag indicating whether or not packet dumping is enabled.
packetDump: false
# Flag indicating whether or not verbose debug logging is enabled.
debug: false
@ -63,6 +78,25 @@ master:
# This port is advertised to the network as a globally WAN accessible port.
advertisedWANPort: 62031
#
# Adaptive Jitter Buffer Configuration
# NOTE: In 99% of cases, the adaptive jitter buffer, if needed, should only be enabled on a per-peer basis,
# and remain disabled for all peers globally. Enabling the jitter buffer adds latency to voice traffic,
# and should only be used in specific network conditions where high jitter or out-of-order packets are
# common (i.e. satellite links, cellular networks, etc.).
#
jitterBuffer:
# Flag indicating whether the adaptive jitter buffer is enabled by default for all peers.
enabled: false
# Default maximum buffer size in frames (range: 2-8 frames).
# Larger values provide more reordering capability but add latency.
# Recommended: 4 frames for most networks, 6-8 for high-jitter links (satellite, cellular).
defaultMaxSize: 4
# Default maximum wait time in microseconds (range: 10000-200000 us).
# Frames exceeding this age are delivered even if gaps exist.
# Recommended: 40000 us (40ms) for terrestrial, 80000 us (80ms) for satellite.
defaultMaxWait: 40000
# Flag indicating whether or not denied traffic will be logged.
# (This is useful for debugging talkgroup rules and other ACL issues, but can be very noisy on a busy system.)
logDenials: false
@ -118,6 +152,8 @@ master:
parrotGrantDemand: true
# Flag indicating whether or not a parrot TG call will only be sent to the originating peer.
parrotOnlyToOrginiatingPeer: false
# Source ID to override parrot TG calls with (0 for no override).
parrotOverrideSrcId: 0
# Flag indicating whether or not P25 OTAR KMF services are enabled.
kmfServicesEnabled: false
@ -243,6 +279,8 @@ peers:
# Textual location for this host.
location: Anywhere, USA
# Flag indicating whether or not packet dumping is enabled.
packetDump: false
# Flag indicating whether or not verbose debug logging is enabled.
debug: false
@ -326,3 +364,12 @@ vtun:
netmask: 255.255.255.0
# Broadcast address of the tunnel network interface
broadcast: 192.168.1.255
#
# P25 SNDCP Dynamic IP Allocation
#
sndcp:
# Starting IP address for dynamic IP allocation pool
startAddress: 192.168.1.10
# Ending IP address for dynamic IP allocation pool
endAddress: 192.168.1.200

@ -52,6 +52,8 @@ network:
# Flag indicating whether or not the host diagnostic log will be sent to the network.
allowDiagnosticTransfer: true
# Flag indicating whether or not packet dumping is enabled.
packetDump: false
# Flag indicating whether or not verbose debug logging is enabled.
debug: false
@ -68,7 +70,7 @@ network:
# aes - AES-256 Encryption
# arc4 - ARC4/ADP Encryption
tekAlgo: "aes"
# Traffic Encryption Key ID
# Traffic Encryption Key ID (Hex)
tekKeyId: 1
# Destination Talkgroup ID for transmitted/received audio frames.
@ -84,10 +86,16 @@ network:
# aes - AES-256 Encryption
# arc4 - ARC4/ADP Encryption
tekAlgo: "aes"
# Traffic Encryption Key ID
# Traffic Encryption Key ID (Hex)
tekKeyId: 1
# Amount of time (ms) from loss of active call before ending call.
dropTimeMs: 1000
# Flag indicating whether or not the patch is two-way.
# NOTE: If false (one-way patch from source to destination), and patching clear to
# encrypted traffic, only the destination TEK will be used for encryption. The clear
# traffic must appear on the source side only.
twoWay: false
# Hostname/IP address of MMDVM gateway to connect to.

@ -22,11 +22,14 @@
# call, normal call collision rules are applied to the traffic being transmitted.
# If this flag is enabled (1), and the connected peer tries to transmit over an on going
# call, call collision rules are ignored, and the peer is given priority.
# * JITTER ENABLED [OPTIONAL] - Flag indicating whether the adaptive jitter buffer is enabled.
# * JITTER MAX FRAMES [OPTIONAL] - Maximum buffer size in frames (range: 2-8 frames).
# * JITTER MAX WAIT [OPTIONAL] - Maximum wait time in microseconds (range: 10000-200000 us).
#
# Entry Format: "Peer ID,Peer Password,Peer Replication (1 = Enabled / 0 = Disabled),Peer Alias (optional),Can Request Keys (1 = Enabled / 0 = Disabled),Can Issue Inhibit (1 = Enabled / 0 = Disabled),Has Call Priority (1 = Enabled / 0 = Disabled)<newline>"
# Entry Format: "Peer ID,Peer Password,Peer Replication (1 = Enabled / 0 = Disabled),Peer Alias (optional),Can Request Keys (1 = Enabled / 0 = Disabled),Can Issue Inhibit (1 = Enabled / 0 = Disabled),Has Call Priority (1 = Enabled / 0 = Disabled),Jitter Enabled (1 = Enabled / 0 = Disabled),Jitter Max Size, Jitter Max Wait<newline>"
# Examples:
#1234,,0,,1,0,0,
#5678,MYSECUREPASSWORD,0,,0,0,0,
#9876,MYSECUREPASSWORD,1,,0,0,0,
#5432,MYSECUREPASSWORD,,Peer Alias 1,0,0,0,
#1012,MYSECUREPASSWORD,1,Peer Alias 2,1,0,0,
#1234,,0,,1,0,0,0,4,40000,
#5678,MYSECUREPASSWORD,0,,0,0,0,0,4,40000,
#9876,MYSECUREPASSWORD,1,,0,0,0,0,4,40000,
#5432,MYSECUREPASSWORD,,Peer Alias 1,0,0,0,0,4,40000,
#1012,MYSECUREPASSWORD,1,Peer Alias 2,1,0,0,0,4,40000,

@ -0,0 +1,190 @@
# Adaptive Jitter Buffer Configuration Guide
## Overview
The FNE (Fixed Network Equipment) includes an adaptive jitter buffer system that can automatically reorder out-of-sequence RTP packets from peers experiencing network issues such as:
- **Satellite links** with high latency and variable jitter
- **Cellular connections** with packet reordering
- **Congested network paths** causing sporadic delays
- **Multi-path routing** leading to out-of-order delivery
The jitter buffer operates with **zero latency for perfect networks** - if packets arrive in order, they pass through immediately without buffering. Only out-of-order packets trigger the adaptive buffering mechanism.
## How It Works
### Zero-Latency Fast Path
When packets arrive in perfect sequence order, they are processed immediately with **no additional latency**. The jitter buffer is effectively transparent.
### Adaptive Reordering
When an out-of-order packet is detected:
1. The jitter buffer holds the packet temporarily
2. Waits for missing packets to arrive
3. Delivers frames in correct sequence order
4. Times out after a configurable period if gaps persist
### Per-Peer, Per-Stream Isolation
- Each peer connection can have independent jitter buffer settings
- Within each peer, each call/stream has its own isolated buffer
- This prevents one problematic stream from affecting others
## Configuration
### Location
Jitter buffer configuration is defined in the FNE configuration file (typically `fne-config.yml`) under the `master` section:
```yaml
master:
# ... other master configuration ...
jitterBuffer:
enabled: false
defaultMaxSize: 4
defaultMaxWait: 40000
```
### Parameters
#### Global Settings
- **enabled** (boolean, default: `false`)
- Master enable/disable switch for jitter buffering
- When `false`, all peers operate with zero-latency pass-through
- When `true`, peers use jitter buffering with default parameters
- **defaultMaxSize** (integer, range: 2-8, default: `4`)
- Maximum number of frames to buffer per stream
- Larger values provide more reordering capability but add latency
- **Recommended values:**
- `4` - Standard networks (LAN, stable WAN)
- `6` - High-jitter networks (cellular, congested paths)
- `8` - Extreme conditions (satellite, very poor links)
- **defaultMaxWait** (integer, range: 10000-200000 microseconds, default: `40000`)
- Maximum time to wait for missing packets
- Frames older than this are delivered even with gaps
- **Recommended values:**
- `40000` (40ms) - Terrestrial networks
- `60000` (60ms) - Cellular networks
- `80000` (80ms) - Satellite links
Per-Peer overrides occur with the jitter buffer parameters within the peer ACL file. The same global parameters, apply
there but on a per-peer basis. Global jitter buffer parameters take precedence over per-peer.
## Configuration Examples
### Example 1: Disabled (Default)
For networks with reliable connectivity:
```yaml
master:
jitterBuffer:
enabled: false
defaultMaxSize: 4
defaultMaxWait: 40000
```
All peers operate with zero-latency pass-through. Best for:
- Local area networks
- Stable dedicated connections
- Networks with minimal packet loss/reordering
### Example 2: Global Enable with Defaults
Enable jitter buffering for all peers with conservative settings:
```yaml
master:
jitterBuffer:
enabled: true
defaultMaxSize: 4
defaultMaxWait: 40000
```
Good starting point for:
- Mixed network environments
- Networks with occasional jitter
- General purpose deployments
## Performance Characteristics
### CPU Impact
- **Zero-latency path:** Negligible overhead (~1 comparison per packet)
- **Buffering path:** Minimal overhead (~map lookup + timestamp check)
- **Memory:** ~500 bytes per active stream buffer
### Latency Impact
- **In-order packets:** 0ms additional latency
- **Out-of-order packets:** Buffered until:
- Missing packets arrive, OR
- `maxWait` timeout expires
- **Typical latency:** 10-40ms for reordered packets on terrestrial networks
### Effectiveness
Based on the adaptive jitter buffer design:
- **100% pass-through** for perfect networks (zero latency)
- **~95-99% recovery** of out-of-order packets within timeout window
- **Automatic timeout delivery** prevents indefinite stalling
## Troubleshooting
### Symptom: Audio/Data Gaps Despite Jitter Buffer
**Possible Causes:**
1. `maxWait` timeout too short for network conditions
2. `maxSize` buffer too small for reordering depth
3. Actual packet loss (not just reordering)
**Solutions:**
- Increase `maxWait` by 20-40ms increments
- Increase `maxSize` by 1-2 frames
- Verify network packet loss with diagnostics
### Symptom: Excessive Latency
**Possible Causes:**
1. Jitter buffer enabled on stable connections
2. `maxWait` set too high
3. `maxSize` set too large
**Solutions:**
- Disable jitter buffer for known-good peers using overrides
- Reduce `maxWait` in 10-20ms decrements
- Reduce `maxSize` to minimum (2-4 frames)
### Symptom: No Improvement
**Possible Causes:**
1. Jitter buffer not actually enabled for the problematic peer
2. Issues beyond reordering (e.g., corruption, auth failures)
3. Problems at application layer, not transport layer
**Solutions:**
- Verify peer override configuration is correct
- Check FNE logs for peer-specific configuration messages
- Enable verbose and debug logging to trace packet flow
## Best Practices
1. **Start Disabled**: Begin with jitter buffering disabled and enable only as needed
2. **Target Specific Peers**: Use per-peer overrides rather than global enable when possible
3. **Conservative Tuning**: Start with default parameters and adjust incrementally
4. **Monitor Performance**: Watch for signs of latency or audio quality issues
5. **Document Changes**: Keep records of which peers need special configuration
6. **Test Thoroughly**: Validate changes don't introduce unintended latency
## Reference
### Configuration Schema
```yaml
jitterBuffer:
enabled: <boolean> # false
defaultMaxSize: <2-8> # 4
defaultMaxWait: <10000-200000> # 40000
```

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

@ -83,16 +83,6 @@ endif (COMPILE_WIN32)
target_link_libraries(dvmfne PRIVATE common ${OPENSSL_LIBRARIES} ${LIBDW_LIBRARY} asio::asio Threads::Threads)
target_include_directories(dvmfne PRIVATE ${OPENSSL_INCLUDE_DIR} ${LIBDW_INCLUDE_DIR} src src/fne)
#
## dvmmon
#
if (ENABLE_TUI_SUPPORT AND (NOT DISABLE_TUI_APPS))
include(src/monitor/CMakeLists.txt)
add_executable(dvmmon ${common_INCLUDE} ${dvmmon_SRC})
target_link_libraries(dvmmon PRIVATE common ${OPENSSL_LIBRARIES} ${LIBDW_LIBRARY} asio::asio finalcut Threads::Threads)
target_include_directories(dvmmon PRIVATE ${OPENSSL_INCLUDE_DIR} ${LIBDW_INCLUDE_DIR} src src/host src/monitor)
endif (ENABLE_TUI_SUPPORT AND (NOT DISABLE_TUI_APPS))
#
## sysview
#

@ -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,2026 Bryan Biedenkapp, N2PLL
*
*/
#include "Defines.h"
@ -48,6 +48,8 @@ bool g_hideMessages = false;
int g_inputDevice = -1;
int g_outputDevice = -1;
bool g_dumpSampleLevels = false;
uint8_t* g_gitHashBytes = nullptr;
#ifdef _WIN32
@ -96,8 +98,9 @@ void fatal(const char* msg, ...)
void usage(const char* message, const char* arg)
{
::fprintf(stdout, __PROG_NAME__ " %s (built %s)\r\n", __VER__, __BUILD__);
::fprintf(stdout, "Copyright (c) 2017-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");
::fprintf(stdout, "Copyright (c) 2017-2026 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");
::fprintf(stdout, HIGHLY_UNNECESSARY_DISCLAIMER_FOR_THE_MENTAL "\n\n");
if (message != nullptr) {
::fprintf(stderr, "%s: ", g_progExe.c_str());
::fprintf(stderr, message, arg);
@ -116,6 +119,7 @@ void usage(const char* message, const char* arg)
"\n"
" -i input audio device\n"
" -o output audio device\n"
" -level dump sample levels\n"
#ifdef _WIN32
"\n"
" -winmm use WinMM audio on Windows\n"
@ -211,10 +215,14 @@ int checkArgs(int argc, char* argv[])
g_backends[2] = ma_backend_null;
}
#endif
else if (IS("-level")) {
g_dumpSampleLevels = true;
}
else if (IS("-v")) {
::fprintf(stdout, __PROG_NAME__ " %s (built %s)\r\n", __VER__, __BUILD__);
::fprintf(stdout, "Copyright (c) 2017-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");
::fprintf(stdout, "Copyright (c) 2017-2026 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");
::fprintf(stdout, HIGHLY_UNNECESSARY_DISCLAIMER_FOR_THE_MENTAL "\n");
if (argc == 2)
exit(EXIT_SUCCESS);
}

@ -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,2026 Bryan Biedenkapp, N2PLL
*
*/
/**
@ -45,6 +45,9 @@ extern int g_inputDevice;
/** @brief Audio Output Device Index. */
extern int g_outputDevice;
/** @brief (Global) Flag indicating local audio sample levels should be dumped to the log. */
extern bool g_dumpSampleLevels;
extern uint8_t* g_gitHashBytes;
#ifdef _WIN32

@ -18,6 +18,12 @@
#include <errno.h>
#endif
// ---------------------------------------------------------------------------
// Public Class Members
// ---------------------------------------------------------------------------
/* Initializes a new instance of the CtsCorController class. */
CtsCorController::CtsCorController(const std::string& port)
: m_port(port), m_isOpen(false), m_ownsFd(true)
#if defined(_WIN32)
@ -28,11 +34,15 @@ CtsCorController::CtsCorController(const std::string& port)
{
}
/* Finalizes a instance of the RtsPttController class. */
CtsCorController::~CtsCorController()
{
close();
}
/* Opens the serial port for CTS control. */
bool CtsCorController::open(int reuseFd)
{
if (m_isOpen)
@ -155,6 +165,8 @@ bool CtsCorController::open(int reuseFd)
return true;
}
/* Closes the serial port. */
void CtsCorController::close()
{
if (!m_isOpen)
@ -180,6 +192,8 @@ void CtsCorController::close()
::LogInfo(LOG_HOST, "CTS COR Controller closed");
}
/* Return wether the CTS signal is high (asserted CTS) to trigger COR detection. */
bool CtsCorController::isCtsAsserted()
{
if (!m_isOpen)
@ -202,6 +216,12 @@ bool CtsCorController::isCtsAsserted()
#endif // defined(_WIN32)
}
// ---------------------------------------------------------------------------
// Private Class Members
// ---------------------------------------------------------------------------
/* Sets the termios settings on the serial port. */
bool CtsCorController::setTermios()
{
#if !defined(_WIN32)

@ -56,7 +56,7 @@ public:
void close();
/**
* @brief Reads the current CTS signal state.
* @brief Return wether the CTS signal is high (asserted CTS) to trigger COR detection.
* @returns bool True if CTS is asserted (active), otherwise false.
*/
bool isCtsAsserted();

@ -0,0 +1,247 @@
// SPDX-License-Identifier: GPL-2.0-only
/*
* Digital Voice Modem - Bridge
* GPLv2 Open Source. Use is subject to license terms.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* Copyright (C) 2024-2026 Bryan Biedenkapp, N2PLL
* Copyright (C) 2025 Caleb, K4PHP
* Copyright (C) 2025 Lorenzo L Romero, K2LLR
*
*/
#include "Defines.h"
#include "common/analog/AnalogDefines.h"
#include "common/analog/AnalogAudio.h"
#include "common/analog/data/NetData.h"
#include "common/Log.h"
#include "common/Utils.h"
#include "bridge/ActivityLog.h"
#include "HostBridge.h"
#include "BridgeMain.h"
using namespace analog;
using namespace analog::defines;
using namespace network;
using namespace network::frame;
using namespace network::udp;
#include <cstdio>
#include <algorithm>
#include <functional>
#include <random>
// ---------------------------------------------------------------------------
// Public Class Members
// ---------------------------------------------------------------------------
/*
** Analog
*/
/* Helper to process analog network traffic. */
void HostBridge::processAnalogNetwork(uint8_t* buffer, uint32_t length)
{
assert(buffer != nullptr);
using namespace analog;
using namespace analog::defines;
if (m_txMode != TX_MODE_ANALOG)
return;
// process network message header
uint8_t seqNo = buffer[4U];
uint32_t srcId = GET_UINT24(buffer, 5U);
uint32_t dstId = GET_UINT24(buffer, 8U);
bool individual = (buffer[15] & 0x40U) == 0x40U;
AudioFrameType::E frameType = (AudioFrameType::E)(buffer[15U] & 0x0FU);
data::NetData analogData;
analogData.setSeqNo(seqNo);
analogData.setSrcId(srcId);
analogData.setDstId(dstId);
analogData.setFrameType(frameType);
analogData.setAudio(buffer + 20U);
uint8_t frame[AUDIO_SAMPLES_LENGTH_BYTES];
analogData.getAudio(frame);
if (m_debug) {
LogDebug(LOG_NET, "Analog, seqNo = %u, srcId = %u, dstId = %u, len = %u", seqNo, srcId, dstId, length);
}
if (!individual) {
if (srcId == 0)
return;
// ensure destination ID matches and slot matches
if (dstId != m_dstId)
return;
m_networkWatchdog.start();
// is this a new call stream?
if (m_network->getAnalogStreamId() != m_rxStreamId && !m_callInProgress) {
m_callInProgress = true;
m_callAlgoId = 0U;
uint64_t now = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::system_clock::now().time_since_epoch()).count();
m_rxStartTime = now;
LogInfoEx(LOG_HOST, "Analog, call start, srcId = %u, dstId = %u", srcId, dstId);
if (m_preambleLeaderTone)
generatePreambleTone();
}
// process call termination
if (frameType == AudioFrameType::TERMINATOR) {
m_callInProgress = false;
m_networkWatchdog.stop();
m_ignoreCall = false;
m_callAlgoId = 0U;
if (m_rxStartTime > 0U) {
uint64_t now = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::system_clock::now().time_since_epoch()).count();
uint64_t diff = now - m_rxStartTime;
LogInfoEx(LOG_HOST, "Analog, call end, srcId = %u, dstId = %u, dur = %us", srcId, dstId, diff / 1000U);
}
m_rxStartTime = 0U;
m_rxStreamId = 0U;
if (!m_udpRTPContinuousSeq) {
m_rtpInitialFrame = false;
m_rtpSeqNo = 0U;
}
m_rtpTimestamp = INVALID_TS;
m_network->resetAnalog();
return;
}
if (m_ignoreCall && m_callAlgoId == 0U)
m_ignoreCall = false;
if (m_ignoreCall)
return;
// decode audio frames
if (frameType == AudioFrameType::VOICE_START || frameType == AudioFrameType::VOICE) {
LogInfoEx(LOG_NET, ANO_VOICE ", audio, srcId = %u, dstId = %u, seqNo = %u", srcId, dstId, analogData.getSeqNo());
short samples[AUDIO_SAMPLES_LENGTH];
int smpIdx = 0;
for (uint32_t pcmIdx = 0; pcmIdx < AUDIO_SAMPLES_LENGTH; pcmIdx++) {
samples[smpIdx] = AnalogAudio::decodeMuLaw(frame[pcmIdx]);
smpIdx++;
}
// post-process: apply gain to decoded audio frames
AnalogAudio::gain(samples, AUDIO_SAMPLES_LENGTH, m_rxAudioGain);
if (m_localAudio) {
m_outputAudio.addData(samples, AUDIO_SAMPLES_LENGTH);
}
if (m_udpAudio) {
int pcmIdx = 0;
uint8_t pcm[AUDIO_SAMPLES_LENGTH * 2U];
if (m_udpUseULaw) {
for (uint32_t smpIdx = 0; smpIdx < AUDIO_SAMPLES_LENGTH; smpIdx++) {
pcm[smpIdx] = AnalogAudio::encodeMuLaw(samples[smpIdx]);
}
if (m_trace)
Utils::dump(1U, "HostBridge()::processAnalogNetwork(), Encoded uLaw Audio", pcm, AUDIO_SAMPLES_LENGTH);
writeUDPAudio(srcId, dstId, pcm, AUDIO_SAMPLES_LENGTH_BYTES / 2U);
} else {
for (uint32_t smpIdx = 0; smpIdx < AUDIO_SAMPLES_LENGTH; smpIdx++) {
pcm[pcmIdx + 0] = (uint8_t)(samples[smpIdx] & 0xFF);
pcm[pcmIdx + 1] = (uint8_t)((samples[smpIdx] >> 8) & 0xFF);
pcmIdx += 2;
}
writeUDPAudio(srcId, dstId, pcm, AUDIO_SAMPLES_LENGTH_BYTES);
}
}
}
m_rxStreamId = m_network->getAnalogStreamId();
}
}
/* Helper to encode analog network traffic audio frames. */
void HostBridge::encodeAnalogAudioFrame(uint8_t* pcm, uint32_t forcedSrcId, uint32_t forcedDstId)
{
assert(pcm != nullptr);
using namespace analog;
using namespace analog::defines;
using namespace analog::data;
if (m_analogN == 254U)
m_analogN = 0;
int smpIdx = 0;
short samples[AUDIO_SAMPLES_LENGTH];
for (uint32_t pcmIdx = 0; pcmIdx < (AUDIO_SAMPLES_LENGTH * 2U); pcmIdx += 2) {
samples[smpIdx] = (short)((pcm[pcmIdx + 1] << 8) + pcm[pcmIdx + 0]);
smpIdx++;
}
// pre-process: apply gain to PCM audio frames
AnalogAudio::gain(samples, AUDIO_SAMPLES_LENGTH, m_txAudioGain);
uint32_t srcId = m_srcId;
if (m_srcIdOverride != 0 && (m_overrideSrcIdFromMDC))
srcId = m_srcIdOverride;
if (m_overrideSrcIdFromUDP)
srcId = m_udpSrcId;
if (forcedSrcId > 0 && forcedSrcId != m_srcId)
srcId = forcedSrcId;
uint32_t dstId = m_dstId;
if (forcedDstId > 0 && forcedDstId != m_dstId)
dstId = forcedDstId;
// never allow a source ID of 0
if (srcId == 0U)
srcId = m_srcId;
data::NetData analogData;
analogData.setSeqNo(m_analogN);
analogData.setSrcId(srcId);
analogData.setDstId(dstId);
analogData.setControl(0U);
analogData.setFrameType(AudioFrameType::VOICE);
if (m_txStreamId <= 1U) {
analogData.setFrameType(AudioFrameType::VOICE_START);
if (m_grantDemand) {
analogData.setControl(0x80U); // analog remote grant demand flag
}
}
int pcmIdx = 0;
uint8_t outPcm[AUDIO_SAMPLES_LENGTH * 2U];
for (uint32_t smpIdx = 0; smpIdx < AUDIO_SAMPLES_LENGTH; smpIdx++) {
outPcm[smpIdx] = AnalogAudio::encodeMuLaw(samples[smpIdx]);
}
if (m_trace)
Utils::dump(1U, "HostBridge()::encodeAnalogAudioFrame(), Encoded uLaw Audio", outPcm, AUDIO_SAMPLES_LENGTH);
analogData.setAudio(outPcm);
if (analogData.getFrameType() == AudioFrameType::VOICE) {
LogInfoEx(LOG_HOST, ANO_VOICE ", audio, srcId = %u, dstId = %u, seqNo = %u", srcId, dstId, analogData.getSeqNo());
}
m_network->writeAnalog(analogData);
m_txStreamId = m_network->getAnalogStreamId();
m_analogN++;
}

@ -0,0 +1,472 @@
// SPDX-License-Identifier: GPL-2.0-only
/*
* Digital Voice Modem - Bridge
* GPLv2 Open Source. Use is subject to license terms.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* Copyright (C) 2024-2026 Bryan Biedenkapp, N2PLL
* Copyright (C) 2025 Caleb, K4PHP
* Copyright (C) 2025 Lorenzo L Romero, K2LLR
*
*/
#include "Defines.h"
#include "common/analog/AnalogDefines.h"
#include "common/analog/AnalogAudio.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/Log.h"
#include "common/Utils.h"
#include "bridge/ActivityLog.h"
#include "HostBridge.h"
#include "BridgeMain.h"
using namespace analog;
using namespace analog::defines;
using namespace network;
using namespace network::frame;
using namespace network::udp;
#include <cstdio>
#include <algorithm>
#include <functional>
#include <random>
#if !defined(_WIN32)
#include <unistd.h>
#include <pwd.h>
#endif // !defined(_WIN32)
// ---------------------------------------------------------------------------
// Public Class Members
// ---------------------------------------------------------------------------
/*
** Digital Mobile Radio
*/
/* Helper to process DMR network traffic. */
void HostBridge::processDMRNetwork(uint8_t* buffer, uint32_t length)
{
assert(buffer != nullptr);
using namespace dmr;
using namespace dmr::defines;
if (m_txMode != TX_MODE_DMR) {
m_network->resetDMR(1U);
m_network->resetDMR(2U);
return;
}
// process network message header
uint8_t seqNo = buffer[4U];
uint32_t srcId = GET_UINT24(buffer, 5U);
uint32_t dstId = GET_UINT24(buffer, 8U);
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);
m_network->resetDMR(1U);
m_network->resetDMR(2U);
return;
}
// DMO mode slot disabling
if (slotNo == 1U && !m_network->getDuplex()) {
LogError(LOG_DMR, "DMR/DMO, invalid slot, slotNo = %u", slotNo);
m_network->resetDMR(1U);
return;
}
// Individual slot disabling
if (slotNo == 1U && !m_network->getSlot1()) {
LogError(LOG_DMR, "DMR, invalid slot, slot 1 disabled, slotNo = %u", slotNo);
m_network->resetDMR(1U);
return;
}
if (slotNo == 2U && !m_network->getSlot2()) {
LogError(LOG_DMR, "DMR, invalid slot, slot 2 disabled, slotNo = %u", slotNo);
m_network->resetDMR(2U);
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<uint8_t[]>(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) {
m_network->resetDMR(slotNo);
return;
}
// ensure destination ID matches and slot matches
if (dstId != m_dstId) {
m_network->resetDMR(slotNo);
return;
}
if (slotNo != m_slot) {
m_network->resetDMR(slotNo);
return;
}
m_networkWatchdog.start();
// is this a new call stream?
if (m_network->getDMRStreamId(slotNo) != m_rxStreamId && !m_callInProgress) {
m_callInProgress = true;
m_callAlgoId = 0U;
uint64_t now = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::system_clock::now().time_since_epoch()).count();
m_rxStartTime = now;
LogInfoEx(LOG_HOST, "DMR, call start, srcId = %u, dstId = %u, slot = %u", srcId, dstId, slotNo);
if (m_preambleLeaderTone)
generatePreambleTone();
// 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);
m_rxDMRLC = lc;
}
else {
// if we don't have a voice header; don't wait to decode it, just make a dummy header
m_rxDMRLC = lc::LC();
m_rxDMRLC.setDstId(dstId);
m_rxDMRLC.setSrcId(srcId);
}
m_rxDMRPILC = lc::PrivacyLC();
}
// 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());
m_rxDMRPILC = lc;
m_callAlgoId = lc.getAlgId();
}
// process call termination
if (dataSync && (dataType == DataType::TERMINATOR_WITH_LC)) {
m_callInProgress = false;
m_networkWatchdog.stop();
m_ignoreCall = false;
m_callAlgoId = 0U;
if (m_rxStartTime > 0U) {
uint64_t now = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::system_clock::now().time_since_epoch()).count();
uint64_t diff = now - m_rxStartTime;
LogInfoEx(LOG_HOST, "DMR, call end, srcId = %u, dstId = %u, dur = %us", srcId, dstId, diff / 1000U);
}
m_rxDMRLC = lc::LC();
m_rxDMRPILC = lc::PrivacyLC();
m_rxStartTime = 0U;
m_rxStreamId = 0U;
if (!m_udpRTPContinuousSeq) {
m_rtpInitialFrame = false;
m_rtpSeqNo = 0U;
}
m_rtpTimestamp = INVALID_TS;
m_network->resetDMR(slotNo);
return;
}
if (m_ignoreCall && m_callAlgoId == 0U)
m_ignoreCall = false;
if (m_ignoreCall) {
m_network->resetDMR(slotNo);
return;
}
if (m_callAlgoId != 0U) {
if (m_callInProgress) {
m_callInProgress = false;
uint64_t now = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::system_clock::now().time_since_epoch()).count();
uint64_t diff = now - m_rxStartTime;
// send USRP end of transmission
if (m_udpUsrp)
sendUsrpEot();
LogInfoEx(LOG_HOST, "P25, call end (T), srcId = %u, dstId = %u, dur = %us", srcId, dstId, diff / 1000U);
}
m_ignoreCall = true;
m_network->resetDMR(slotNo);
return;
}
// process audio frames
if (dataType == DataType::VOICE_SYNC || dataType == DataType::VOICE) {
uint8_t ambe[27U];
::memcpy(ambe, data.get(), 14U);
ambe[13] &= 0xF0;
ambe[13] |= (uint8_t)(data[19] & 0x0F);
::memcpy(ambe + 14U, data.get() + 20U, 13U);
LogInfoEx(LOG_NET, DMR_DT_VOICE ", audio, slot = %u, srcId = %u, dstId = %u, seqNo = %u", slotNo, srcId, dstId, n);
decodeDMRAudioFrame(ambe, srcId, dstId, n);
}
m_rxStreamId = m_network->getDMRStreamId(slotNo);
}
}
/* Helper to decode DMR network traffic audio frames. */
void HostBridge::decodeDMRAudioFrame(uint8_t* ambe, uint32_t srcId, uint32_t dstId, uint8_t dmrN)
{
assert(ambe != nullptr);
using namespace dmr;
using namespace dmr::defines;
for (uint32_t n = 0; n < AMBE_PER_SLOT; n++) {
uint8_t ambePartial[RAW_AMBE_LENGTH_BYTES];
for (uint32_t i = 0; i < RAW_AMBE_LENGTH_BYTES; i++)
ambePartial[i] = ambe[i + (n * 9)];
short samples[AUDIO_SAMPLES_LENGTH];
int errs = 0;
#if defined(_WIN32)
if (m_useExternalVocoder) {
ambeDecode(ambePartial, RAW_AMBE_LENGTH_BYTES, samples);
}
else {
#endif // defined(_WIN32)
m_decoder->decode(ambePartial, samples);
#if defined(_WIN32)
}
#endif // defined(_WIN32)
if (m_debug)
LogInfoEx(LOG_HOST, DMR_DT_VOICE ", Frame, VC%u.%u, srcId = %u, dstId = %u, errs = %u", dmrN, n, srcId, dstId, errs);
// post-process: apply gain to decoded audio frames
AnalogAudio::gain(samples, AUDIO_SAMPLES_LENGTH, m_rxAudioGain);
if (m_localAudio) {
m_outputAudio.addData(samples, AUDIO_SAMPLES_LENGTH);
// Assert RTS PTT when audio is being sent to output
assertRtsPtt();
}
if (m_udpAudio) {
int pcmIdx = 0;
uint8_t pcm[AUDIO_SAMPLES_LENGTH * 2U];
// are we sending uLaw encoded audio?
if (m_udpUseULaw) {
for (uint32_t smpIdx = 0; smpIdx < AUDIO_SAMPLES_LENGTH; smpIdx++) {
pcm[smpIdx] = AnalogAudio::encodeMuLaw(samples[smpIdx]);
}
if (m_trace)
Utils::dump(1U, "HostBridge()::decodeDMRAudioFrame(), Encoded uLaw Audio", pcm, AUDIO_SAMPLES_LENGTH);
writeUDPAudio(srcId, dstId, pcm, AUDIO_SAMPLES_LENGTH_BYTES / 2U);
} else {
// raw PCM audio
for (uint32_t smpIdx = 0; smpIdx < AUDIO_SAMPLES_LENGTH; smpIdx++) {
pcm[pcmIdx + 0] = (uint8_t)(samples[smpIdx] & 0xFF);
pcm[pcmIdx + 1] = (uint8_t)((samples[smpIdx] >> 8) & 0xFF);
pcmIdx += 2;
}
writeUDPAudio(srcId, dstId, pcm, AUDIO_SAMPLES_LENGTH_BYTES);
}
}
}
}
/* Helper to encode DMR network traffic audio frames. */
void HostBridge::encodeDMRAudioFrame(uint8_t* pcm, uint32_t forcedSrcId, uint32_t forcedDstId)
{
assert(pcm != nullptr);
using namespace dmr;
using namespace dmr::defines;
using namespace dmr::data;
uint32_t srcId = m_srcId;
if (m_srcIdOverride != 0 && (m_overrideSrcIdFromMDC))
srcId = m_srcIdOverride;
if (m_overrideSrcIdFromUDP)
srcId = m_udpSrcId;
if (forcedSrcId > 0 && forcedSrcId != m_srcId)
srcId = forcedSrcId;
uint32_t dstId = m_dstId;
if (forcedDstId > 0 && forcedDstId != m_dstId)
dstId = forcedDstId;
// never allow a source ID of 0
if (srcId == 0U)
srcId = m_srcId;
uint8_t* data = nullptr;
m_dmrN = (uint8_t)(m_dmrSeqNo % 6);
if (m_ambeCount == AMBE_PER_SLOT) {
// is this the intitial sequence?
if (m_dmrSeqNo == 0) {
// send DMR voice header
data = new uint8_t[DMR_FRAME_LENGTH_BYTES];
// generate DMR LC
lc::LC dmrLC = lc::LC();
dmrLC.setFLCO(FLCO::GROUP);
dmrLC.setSrcId(srcId);
dmrLC.setDstId(dstId);
m_dmrEmbeddedData.setLC(dmrLC);
// generate the Slot TYpe
SlotType slotType = SlotType();
slotType.setDataType(DataType::VOICE_LC_HEADER);
slotType.encode(data);
lc::FullLC fullLC = lc::FullLC();
fullLC.encode(dmrLC, data, DataType::VOICE_LC_HEADER);
// generate DMR network frame
NetData dmrData;
dmrData.setSlotNo(m_slot);
dmrData.setDataType(DataType::VOICE_LC_HEADER);
dmrData.setSrcId(srcId);
dmrData.setDstId(dstId);
dmrData.setFLCO(FLCO::GROUP);
uint8_t controlByte = 0U;
if (m_grantDemand)
controlByte = network::NET_CTRL_GRANT_DEMAND; // Grant Demand Flag
controlByte |= network::NET_CTRL_SWITCH_OVER;
dmrData.setControl(controlByte);
dmrData.setN(m_dmrN);
dmrData.setSeqNo(m_dmrSeqNo);
dmrData.setBER(0U);
dmrData.setRSSI(0U);
dmrData.setData(data);
LogInfoEx(LOG_HOST, DMR_DT_VOICE_LC_HEADER ", slot = %u, srcId = %u, dstId = %u, FLCO = $%02X", m_slot,
dmrLC.getSrcId(), dmrLC.getDstId(), dmrData.getFLCO());
m_network->writeDMR(dmrData, false);
m_txStreamId = m_network->getDMRStreamId(m_slot);
m_dmrSeqNo++;
delete[] data;
}
// send DMR voice
data = new uint8_t[DMR_FRAME_LENGTH_BYTES];
::memcpy(data, m_ambeBuffer, 13U);
data[13U] = (uint8_t)(m_ambeBuffer[13U] & 0xF0);
data[19U] = (uint8_t)(m_ambeBuffer[13U] & 0x0F);
::memcpy(data + 20U, m_ambeBuffer + 14U, 13U);
DataType::E dataType = DataType::VOICE_SYNC;
if (m_dmrN == 0)
dataType = DataType::VOICE_SYNC;
else {
dataType = DataType::VOICE;
uint8_t lcss = m_dmrEmbeddedData.getData(data, m_dmrN);
// generated embedded signalling
EMB emb = EMB();
emb.setColorCode(0U);
emb.setLCSS(lcss);
emb.encode(data);
}
LogInfoEx(LOG_HOST, DMR_DT_VOICE ", srcId = %u, dstId = %u, slot = %u, seqNo = %u", srcId, dstId, m_slot, m_dmrN);
// generate DMR network frame
NetData dmrData;
dmrData.setSlotNo(m_slot);
dmrData.setDataType(dataType);
dmrData.setSrcId(srcId);
dmrData.setDstId(dstId);
dmrData.setFLCO(FLCO::GROUP);
dmrData.setN(m_dmrN);
dmrData.setSeqNo(m_dmrSeqNo);
dmrData.setBER(0U);
dmrData.setRSSI(0U);
dmrData.setData(data);
m_network->writeDMR(dmrData, false);
m_txStreamId = m_network->getDMRStreamId(m_slot);
m_dmrSeqNo++;
::memset(m_ambeBuffer, 0x00U, 27U);
m_ambeCount = 0U;
}
int smpIdx = 0;
short samples[AUDIO_SAMPLES_LENGTH];
for (uint32_t pcmIdx = 0; pcmIdx < (AUDIO_SAMPLES_LENGTH * 2U); pcmIdx += 2) {
samples[smpIdx] = (short)((pcm[pcmIdx + 1] << 8) + pcm[pcmIdx + 0]);
smpIdx++;
}
// pre-process: apply gain to PCM audio frames
AnalogAudio::gain(samples, AUDIO_SAMPLES_LENGTH, m_txAudioGain);
// encode PCM samples into AMBE codewords
uint8_t ambe[RAW_AMBE_LENGTH_BYTES];
::memset(ambe, 0x00U, RAW_AMBE_LENGTH_BYTES);
#if defined(_WIN32)
if (m_useExternalVocoder) {
ambeEncode(samples, AUDIO_SAMPLES_LENGTH, ambe);
}
else {
#endif // defined(_WIN32)
m_encoder->encode(samples, ambe);
#if defined(_WIN32)
}
#endif // defined(_WIN32)
// Utils::dump(1U, "HostBridge::encodeDMRAudioFrame(), Encoded AMBE", ambe, RAW_AMBE_LENGTH_BYTES);
::memcpy(m_ambeBuffer + (m_ambeCount * 9U), ambe, RAW_AMBE_LENGTH_BYTES);
m_ambeCount++;
}

@ -0,0 +1,713 @@
// SPDX-License-Identifier: GPL-2.0-only
/*
* Digital Voice Modem - Bridge
* GPLv2 Open Source. Use is subject to license terms.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* Copyright (C) 2024-2026 Bryan Biedenkapp, N2PLL
* Copyright (C) 2025 Caleb, K4PHP
* Copyright (C) 2025 Lorenzo L Romero, K2LLR
*
*/
#include "Defines.h"
#include "common/analog/AnalogDefines.h"
#include "common/analog/AnalogAudio.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/Log.h"
#include "common/Utils.h"
#include "bridge/ActivityLog.h"
#include "HostBridge.h"
#include "BridgeMain.h"
using namespace analog;
using namespace analog::defines;
using namespace network;
using namespace network::frame;
using namespace network::udp;
#include <cstdio>
#include <algorithm>
#include <functional>
#include <random>
#if !defined(_WIN32)
#include <unistd.h>
#include <pwd.h>
#endif // !defined(_WIN32)
// ---------------------------------------------------------------------------
// Public Class Members
// ---------------------------------------------------------------------------
/*
** Project 25
*/
/* Helper to process P25 network traffic. */
void HostBridge::processP25Network(uint8_t* buffer, uint32_t length)
{
assert(buffer != nullptr);
using namespace p25;
using namespace p25::defines;
using namespace p25::dfsi::defines;
using namespace p25::data;
if (m_txMode != TX_MODE_P25) {
m_network->resetP25();
return;
}
bool grantDemand = (buffer[14U] & network::NET_CTRL_GRANT_DEMAND) == network::NET_CTRL_GRANT_DEMAND;
bool grantDenial = (buffer[14U] & network::NET_CTRL_GRANT_DENIAL) == network::NET_CTRL_GRANT_DENIAL;
bool unitToUnit = (buffer[14U] & network::NET_CTRL_U2U) == network::NET_CTRL_U2U;
// 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<uint8_t[]>(new uint8_t[length]);
::memset(data.get(), 0x00U, length);
::memcpy(data.get(), buffer, length);
}
else {
if (frameLength <= 24) {
data = std::unique_ptr<uint8_t[]>(new uint8_t[frameLength]);
::memset(data.get(), 0x00U, frameLength);
}
else {
data = std::unique_ptr<uint8_t[]>(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_UINT24(buffer, 5U);
uint32_t dstId = GET_UINT24(buffer, 8U);
uint8_t lsd1 = buffer[20U];
uint8_t lsd2 = buffer[21U];
lc::LC control;
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 ((duid == DUID::TDU) || (duid == DUID::TDULC)) {
// ignore TDU/TDULC entirely when local audio detect or
// traffic from UDP is running
if (m_audioDetect || m_trafficFromUDP)
return;
// ignore TDU's that are grant demands
if (grantDemand) {
m_network->resetP25();
return;
}
}
if (srcId == 0) {
m_network->resetP25();
return;
}
// ensure destination ID matches
if (dstId != m_dstId) {
m_network->resetP25();
return;
}
m_networkWatchdog.start();
// is this a new call stream?
uint16_t callKID = 0U;
if (m_network->getP25StreamId() != m_rxStreamId && ((duid != DUID::TDU) && (duid != DUID::TDULC)) && !m_callInProgress) {
m_callInProgress = true;
m_callAlgoId = ALGO_UNENCRYPT;
// if this is the beginning of a call and we have a valid HDU frame, extract the algo ID
uint8_t frameType = buffer[180U];
if (frameType == FrameType::HDU_VALID) {
m_callAlgoId = buffer[181U];
if (m_callAlgoId != ALGO_UNENCRYPT) {
callKID = GET_UINT16(buffer, 182U);
if (m_callAlgoId != m_tekAlgoId && callKID != m_tekKeyId) {
m_callAlgoId = ALGO_UNENCRYPT;
m_callInProgress = false;
m_ignoreCall = true;
LogWarning(LOG_HOST, "P25, call ignored, using different encryption parameters, callAlgoId = $%02X, callKID = $%04X, tekAlgoId = $%02X, tekKID = $%04X", m_callAlgoId, callKID, m_tekAlgoId, m_tekKeyId);
m_network->resetP25();
return;
} else {
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];
}
m_p25Crypto->setMI(mi);
m_p25Crypto->generateKeystream();
}
}
}
uint64_t now = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::system_clock::now().time_since_epoch()).count();
m_rxStartTime = now;
LogInfoEx(LOG_HOST, "P25, call start, srcId = %u, dstId = %u, callAlgoId = $%02X, callKID = $%04X", srcId, dstId, m_callAlgoId, callKID);
if (m_preambleLeaderTone)
generatePreambleTone();
}
// process call termination
if ((duid == DUID::TDU) || (duid == DUID::TDULC)) {
m_callInProgress = false;
m_networkWatchdog.stop();
m_ignoreCall = false;
m_callAlgoId = ALGO_UNENCRYPT;
if (m_rxStartTime > 0U) {
uint64_t now = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::system_clock::now().time_since_epoch()).count();
uint64_t diff = now - m_rxStartTime;
// send USRP end of transmission
if (m_udpUsrp) {
sendUsrpEot();
}
LogInfoEx(LOG_HOST, "P25, call end, srcId = %u, dstId = %u, dur = %us", srcId, dstId, diff / 1000U);
}
m_rxP25LC = lc::LC();
m_rxStartTime = 0U;
m_rxStreamId = 0U;
if (!m_udpRTPContinuousSeq) {
m_rtpInitialFrame = false;
m_rtpSeqNo = 0U;
}
m_rtpTimestamp = INVALID_TS;
m_network->resetP25();
return;
}
if (m_ignoreCall && m_callAlgoId == ALGO_UNENCRYPT)
m_ignoreCall = false;
if (m_ignoreCall && m_callAlgoId == m_tekAlgoId)
m_ignoreCall = false;
if (duid == DUID::LDU2 && !m_ignoreCall) {
m_callAlgoId = data[88U];
callKID = GET_UINT16(buffer, 89U);
}
if (m_callAlgoId != ALGO_UNENCRYPT) {
if (m_callAlgoId == m_tekAlgoId)
m_ignoreCall = false;
else
m_ignoreCall = true;
}
if (m_ignoreCall) {
m_network->resetP25();
return;
}
// unsupported change of encryption parameters during call
if (m_callAlgoId != ALGO_UNENCRYPT && m_callAlgoId != m_tekAlgoId && callKID != m_tekKeyId) {
if (m_callInProgress) {
m_callInProgress = false;
if (m_callAlgoId != m_tekAlgoId && callKID != m_tekKeyId) {
LogWarning(LOG_HOST, "P25, unsupported change of encryption parameters during call, callAlgoId = $%02X, callKID = $%04X, tekAlgoId = $%02X, tekKID = $%04X", m_callAlgoId, callKID, m_tekAlgoId, m_tekKeyId);
}
uint64_t now = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::system_clock::now().time_since_epoch()).count();
uint64_t diff = now - m_rxStartTime;
LogInfoEx(LOG_HOST, "P25, call end (T), srcId = %u, dstId = %u, dur = %us", srcId, dstId, diff / 1000U);
}
m_ignoreCall = true;
m_network->resetP25();
return;
}
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, m_netLDU1 + 10U);
count += DFSI_LDU1_VOICE1_FRAME_LENGTH_BYTES;
dfsiLC.setFrameType(DFSIFrameType::LDU1_VOICE2);
dfsiLC.decodeLDU1(data.get() + count, m_netLDU1 + 26U);
count += DFSI_LDU1_VOICE2_FRAME_LENGTH_BYTES;
dfsiLC.setFrameType(DFSIFrameType::LDU1_VOICE3);
dfsiLC.decodeLDU1(data.get() + count, m_netLDU1 + 55U);
count += DFSI_LDU1_VOICE3_FRAME_LENGTH_BYTES;
dfsiLC.setFrameType(DFSIFrameType::LDU1_VOICE4);
dfsiLC.decodeLDU1(data.get() + count, m_netLDU1 + 80U);
count += DFSI_LDU1_VOICE4_FRAME_LENGTH_BYTES;
dfsiLC.setFrameType(DFSIFrameType::LDU1_VOICE5);
dfsiLC.decodeLDU1(data.get() + count, m_netLDU1 + 105U);
count += DFSI_LDU1_VOICE5_FRAME_LENGTH_BYTES;
dfsiLC.setFrameType(DFSIFrameType::LDU1_VOICE6);
dfsiLC.decodeLDU1(data.get() + count, m_netLDU1 + 130U);
count += DFSI_LDU1_VOICE6_FRAME_LENGTH_BYTES;
dfsiLC.setFrameType(DFSIFrameType::LDU1_VOICE7);
dfsiLC.decodeLDU1(data.get() + count, m_netLDU1 + 155U);
count += DFSI_LDU1_VOICE7_FRAME_LENGTH_BYTES;
dfsiLC.setFrameType(DFSIFrameType::LDU1_VOICE8);
dfsiLC.decodeLDU1(data.get() + count, m_netLDU1 + 180U);
count += DFSI_LDU1_VOICE8_FRAME_LENGTH_BYTES;
dfsiLC.setFrameType(DFSIFrameType::LDU1_VOICE9);
dfsiLC.decodeLDU1(data.get() + count, m_netLDU1 + 204U);
count += DFSI_LDU1_VOICE9_FRAME_LENGTH_BYTES;
LogInfoEx(LOG_NET, P25_LDU1_STR " audio, srcId = %u, dstId = %u", srcId, dstId);
// decode 9 IMBE codewords into PCM samples
decodeP25AudioFrame(m_netLDU1, srcId, dstId, 1U);
}
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, m_netLDU2 + 10U);
count += DFSI_LDU2_VOICE10_FRAME_LENGTH_BYTES;
dfsiLC.setFrameType(DFSIFrameType::LDU2_VOICE11);
dfsiLC.decodeLDU2(data.get() + count, m_netLDU2 + 26U);
count += DFSI_LDU2_VOICE11_FRAME_LENGTH_BYTES;
dfsiLC.setFrameType(DFSIFrameType::LDU2_VOICE12);
dfsiLC.decodeLDU2(data.get() + count, m_netLDU2 + 55U);
count += DFSI_LDU2_VOICE12_FRAME_LENGTH_BYTES;
dfsiLC.setFrameType(DFSIFrameType::LDU2_VOICE13);
dfsiLC.decodeLDU2(data.get() + count, m_netLDU2 + 80U);
count += DFSI_LDU2_VOICE13_FRAME_LENGTH_BYTES;
dfsiLC.setFrameType(DFSIFrameType::LDU2_VOICE14);
dfsiLC.decodeLDU2(data.get() + count, m_netLDU2 + 105U);
count += DFSI_LDU2_VOICE14_FRAME_LENGTH_BYTES;
dfsiLC.setFrameType(DFSIFrameType::LDU2_VOICE15);
dfsiLC.decodeLDU2(data.get() + count, m_netLDU2 + 130U);
count += DFSI_LDU2_VOICE15_FRAME_LENGTH_BYTES;
dfsiLC.setFrameType(DFSIFrameType::LDU2_VOICE16);
dfsiLC.decodeLDU2(data.get() + count, m_netLDU2 + 155U);
count += DFSI_LDU2_VOICE16_FRAME_LENGTH_BYTES;
dfsiLC.setFrameType(DFSIFrameType::LDU2_VOICE17);
dfsiLC.decodeLDU2(data.get() + count, m_netLDU2 + 180U);
count += DFSI_LDU2_VOICE17_FRAME_LENGTH_BYTES;
dfsiLC.setFrameType(DFSIFrameType::LDU2_VOICE18);
dfsiLC.decodeLDU2(data.get() + count, m_netLDU2 + 204U);
count += DFSI_LDU2_VOICE18_FRAME_LENGTH_BYTES;
LogInfoEx(LOG_NET, P25_LDU2_STR " audio, algo = $%02X, kid = $%04X", dfsiLC.control()->getAlgId(), dfsiLC.control()->getKId());
// decode 9 IMBE codewords into PCM samples
decodeP25AudioFrame(m_netLDU2, srcId, dstId, 2U);
// copy out the MI for the next super frame
if (dfsiLC.control()->getAlgId() == m_tekAlgoId && dfsiLC.control()->getKId() == m_tekKeyId) {
uint8_t mi[MI_LENGTH_BYTES];
dfsiLC.control()->getMI(mi);
m_p25Crypto->setMI(mi);
m_p25Crypto->generateKeystream();
} else {
m_p25Crypto->clearMI();
}
}
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;
}
m_rxStreamId = m_network->getP25StreamId();
}
}
/* Helper to decode P25 network traffic audio frames. */
void HostBridge::decodeP25AudioFrame(uint8_t* ldu, uint32_t srcId, uint32_t dstId, uint8_t p25N)
{
assert(ldu != nullptr);
using namespace p25;
using namespace p25::defines;
if (m_debug) {
uint8_t mi[MI_LENGTH_BYTES];
::memset(mi, 0x00U, MI_LENGTH_BYTES);
m_p25Crypto->getMI(mi);
LogInfoEx(LOG_NET, "Crypto, Enc Sync, MI = %02X %02X %02X %02X %02X %02X %02X %02X %02X",
mi[0U], mi[1U], mi[2U], mi[3U], mi[4U], mi[5U], mi[6U], mi[7U], mi[8U]);
}
// decode 9 IMBE codewords into PCM samples
for (int n = 0; n < 9; n++) {
uint8_t imbe[RAW_IMBE_LENGTH_BYTES];
switch (n) {
case 0:
::memcpy(imbe, ldu + 10U, RAW_IMBE_LENGTH_BYTES);
break;
case 1:
::memcpy(imbe, ldu + 26U, RAW_IMBE_LENGTH_BYTES);
break;
case 2:
::memcpy(imbe, ldu + 55U, RAW_IMBE_LENGTH_BYTES);
break;
case 3:
::memcpy(imbe, ldu + 80U, RAW_IMBE_LENGTH_BYTES);
break;
case 4:
::memcpy(imbe, ldu + 105U, RAW_IMBE_LENGTH_BYTES);
break;
case 5:
::memcpy(imbe, ldu + 130U, RAW_IMBE_LENGTH_BYTES);
break;
case 6:
::memcpy(imbe, ldu + 155U, RAW_IMBE_LENGTH_BYTES);
break;
case 7:
::memcpy(imbe, ldu + 180U, RAW_IMBE_LENGTH_BYTES);
break;
case 8:
::memcpy(imbe, ldu + 204U, RAW_IMBE_LENGTH_BYTES);
break;
}
// Utils::dump(1U, "HostBridge::decodeP25AudioFrame(), IMBE", imbe, RAW_IMBE_LENGTH_BYTES);
if (m_tekAlgoId != P25DEF::ALGO_UNENCRYPT && m_tekKeyId > 0U && m_p25Crypto->getTEKLength() > 0U) {
switch (m_tekAlgoId) {
case P25DEF::ALGO_AES_256:
m_p25Crypto->cryptAES_IMBE(imbe, (p25N == 1U) ? DUID::LDU1 : DUID::LDU2);
break;
case P25DEF::ALGO_ARC4:
m_p25Crypto->cryptARC4_IMBE(imbe, (p25N == 1U) ? DUID::LDU1 : DUID::LDU2);
break;
case P25DEF::ALGO_DES:
m_p25Crypto->cryptDES_IMBE(imbe, (p25N == 1U) ? DUID::LDU1 : DUID::LDU2);
break;
default:
LogError(LOG_HOST, "unsupported TEK algorithm, tekAlgoId = $%02X", m_tekAlgoId);
break;
}
}
// Utils::dump(1U, "HostBridge::decodeP25AudioFrame(), Decrypted IMBE", imbe, RAW_IMBE_LENGTH_BYTES);
short samples[AUDIO_SAMPLES_LENGTH];
int errs = 0;
#if defined(_WIN32)
if (m_useExternalVocoder) {
ambeDecode(imbe, RAW_IMBE_LENGTH_BYTES, samples);
}
else {
#endif // defined(_WIN32)
m_decoder->decode(imbe, samples);
#if defined(_WIN32)
}
#endif // defined(_WIN32)
if (m_debug)
LogDebug(LOG_HOST, "P25, LDU (Logical Link Data Unit), Frame, VC%u.%u, srcId = %u, dstId = %u, errs = %u", p25N, n, srcId, dstId, errs);
// post-process: apply gain to decoded audio frames
AnalogAudio::gain(samples, AUDIO_SAMPLES_LENGTH, m_rxAudioGain);
if (m_localAudio) {
m_outputAudio.addData(samples, AUDIO_SAMPLES_LENGTH);
// Assert RTS PTT when audio is being sent to output
assertRtsPtt();
}
if (m_udpAudio) {
int pcmIdx = 0;
uint8_t pcm[AUDIO_SAMPLES_LENGTH * 2U];
if (m_udpUseULaw) {
for (uint32_t smpIdx = 0; smpIdx < AUDIO_SAMPLES_LENGTH; smpIdx++) {
pcm[smpIdx] = AnalogAudio::encodeMuLaw(samples[smpIdx]);
}
if (m_trace)
Utils::dump(1U, "HostBridge()::decodeP25AudioFrame(), Encoded uLaw Audio", pcm, AUDIO_SAMPLES_LENGTH);
writeUDPAudio(srcId, dstId, pcm, AUDIO_SAMPLES_LENGTH_BYTES / 2U);
} else {
for (uint32_t smpIdx = 0; smpIdx < AUDIO_SAMPLES_LENGTH; smpIdx++) {
pcm[pcmIdx + 0] = (uint8_t)(samples[smpIdx] & 0xFF);
pcm[pcmIdx + 1] = (uint8_t)((samples[smpIdx] >> 8) & 0xFF);
pcmIdx += 2;
}
writeUDPAudio(srcId, dstId, pcm, AUDIO_SAMPLES_LENGTH_BYTES);
}
}
}
}
/* Helper to encode P25 network traffic audio frames. */
void HostBridge::encodeP25AudioFrame(uint8_t* pcm, uint32_t forcedSrcId, uint32_t forcedDstId)
{
assert(pcm != nullptr);
using namespace p25;
using namespace p25::defines;
using namespace p25::data;
if (m_p25N > 17)
m_p25N = 0;
if (m_p25N == 0)
::memset(m_netLDU1, 0x00U, 9U * 25U);
if (m_p25N == 9)
::memset(m_netLDU2, 0x00U, 9U * 25U);
int smpIdx = 0;
short samples[AUDIO_SAMPLES_LENGTH];
for (uint32_t pcmIdx = 0; pcmIdx < (AUDIO_SAMPLES_LENGTH * 2U); pcmIdx += 2) {
samples[smpIdx] = (short)((pcm[pcmIdx + 1] << 8) + pcm[pcmIdx + 0]);
smpIdx++;
}
// pre-process: apply gain to PCM audio frames
AnalogAudio::gain(samples, AUDIO_SAMPLES_LENGTH, m_txAudioGain);
// encode PCM samples into IMBE codewords
uint8_t imbe[RAW_IMBE_LENGTH_BYTES];
::memset(imbe, 0x00U, RAW_IMBE_LENGTH_BYTES);
#if defined(_WIN32)
if (m_useExternalVocoder) {
ambeEncode(samples, AUDIO_SAMPLES_LENGTH, imbe);
}
else {
#endif // defined(_WIN32)
m_encoder->encode(samples, imbe);
#if defined(_WIN32)
}
#endif // defined(_WIN32)
// Utils::dump(1U, "HostBridge::encodeP25AudioFrame(), Encoded IMBE", imbe, RAW_IMBE_LENGTH_BYTES);
if (m_tekAlgoId != P25DEF::ALGO_UNENCRYPT && m_tekKeyId > 0U && m_p25Crypto->getTEKLength() > 0U) {
// generate initial MI for the HDU
if (m_p25N == 0U && !m_p25Crypto->hasValidKeystream()) {
if (!m_p25Crypto->hasValidMI()) {
m_p25Crypto->generateMI();
m_p25Crypto->generateKeystream();
}
}
// perform crypto
switch (m_tekAlgoId) {
case P25DEF::ALGO_AES_256:
m_p25Crypto->cryptAES_IMBE(imbe, (m_p25N < 9U) ? DUID::LDU1 : DUID::LDU2);
break;
case P25DEF::ALGO_ARC4:
m_p25Crypto->cryptARC4_IMBE(imbe, (m_p25N < 9U) ? DUID::LDU1 : DUID::LDU2);
break;
case P25DEF::ALGO_DES:
m_p25Crypto->cryptDES_IMBE(imbe, (m_p25N < 9U) ? DUID::LDU1 : DUID::LDU2);
break;
default:
LogError(LOG_HOST, "unsupported TEK algorithm, tekAlgoId = $%02X", m_tekAlgoId);
break;
}
// if we're on the last block of the LDU2 -- generate the next MI
if (m_p25N == 17U) {
m_p25Crypto->generateNextMI();
// generate new keystream
m_p25Crypto->generateKeystream();
}
}
// fill the LDU buffers appropriately
switch (m_p25N) {
// LDU1
case 0:
::memcpy(m_netLDU1 + 10U, imbe, RAW_IMBE_LENGTH_BYTES);
break;
case 1:
::memcpy(m_netLDU1 + 26U, imbe, RAW_IMBE_LENGTH_BYTES);
break;
case 2:
::memcpy(m_netLDU1 + 55U, imbe, RAW_IMBE_LENGTH_BYTES);
break;
case 3:
::memcpy(m_netLDU1 + 80U, imbe, RAW_IMBE_LENGTH_BYTES);
break;
case 4:
::memcpy(m_netLDU1 + 105U, imbe, RAW_IMBE_LENGTH_BYTES);
break;
case 5:
::memcpy(m_netLDU1 + 130U, imbe, RAW_IMBE_LENGTH_BYTES);
break;
case 6:
::memcpy(m_netLDU1 + 155U, imbe, RAW_IMBE_LENGTH_BYTES);
break;
case 7:
::memcpy(m_netLDU1 + 180U, imbe, RAW_IMBE_LENGTH_BYTES);
break;
case 8:
::memcpy(m_netLDU1 + 204U, imbe, RAW_IMBE_LENGTH_BYTES);
break;
// LDU2
case 9:
::memcpy(m_netLDU2 + 10U, imbe, RAW_IMBE_LENGTH_BYTES);
break;
case 10:
::memcpy(m_netLDU2 + 26U, imbe, RAW_IMBE_LENGTH_BYTES);
break;
case 11:
::memcpy(m_netLDU2 + 55U, imbe, RAW_IMBE_LENGTH_BYTES);
break;
case 12:
::memcpy(m_netLDU2 + 80U, imbe, RAW_IMBE_LENGTH_BYTES);
break;
case 13:
::memcpy(m_netLDU2 + 105U, imbe, RAW_IMBE_LENGTH_BYTES);
break;
case 14:
::memcpy(m_netLDU2 + 130U, imbe, RAW_IMBE_LENGTH_BYTES);
break;
case 15:
::memcpy(m_netLDU2 + 155U, imbe, RAW_IMBE_LENGTH_BYTES);
break;
case 16:
::memcpy(m_netLDU2 + 180U, imbe, RAW_IMBE_LENGTH_BYTES);
break;
case 17:
::memcpy(m_netLDU2 + 204U, imbe, RAW_IMBE_LENGTH_BYTES);
break;
}
uint32_t srcId = m_srcId;
if (m_srcIdOverride != 0 && (m_overrideSrcIdFromMDC))
srcId = m_srcIdOverride;
if (m_overrideSrcIdFromUDP)
srcId = m_udpSrcId;
if (forcedSrcId > 0 && forcedSrcId != m_srcId)
srcId = forcedSrcId;
uint32_t dstId = m_dstId;
if (forcedDstId > 0 && forcedDstId != m_dstId)
dstId = forcedDstId;
// never allow a source ID of 0
if (srcId == 0U)
srcId = m_srcId;
lc::LC lc = lc::LC();
lc.setLCO(LCO::GROUP);
lc.setGroup(true);
lc.setPriority(4U);
lc.setDstId(dstId);
lc.setSrcId(srcId);
lc.setAlgId(m_tekAlgoId);
lc.setKId(m_tekKeyId);
uint8_t mi[MI_LENGTH_BYTES];
m_p25Crypto->getMI(mi);
lc.setMI(mi);
LowSpeedData lsd = LowSpeedData();
uint8_t controlByte = network::NET_CTRL_SWITCH_OVER;
// send P25 LDU1
if (m_p25N == 8U) {
LogInfoEx(LOG_HOST, P25_LDU1_STR " audio, srcId = %u, dstId = %u", srcId, dstId);
m_network->writeP25LDU1(lc, lsd, m_netLDU1, FrameType::HDU_VALID, controlByte);
m_txStreamId = m_network->getP25StreamId();
}
// send P25 LDU2
if (m_p25N == 17U) {
LogInfoEx(LOG_HOST, P25_LDU2_STR " audio, algo = $%02X, kid = $%04X", m_tekAlgoId, m_tekKeyId);
m_network->writeP25LDU2(lc, lsd, m_netLDU2, controlByte);
}
m_p25SeqNo++;
m_p25N++;
// if N is >17 reset sequence
if (m_p25N > 17)
m_p25N = 0;
}

File diff suppressed because it is too large Load Diff

@ -23,6 +23,7 @@
#include "common/dmr/lc/PrivacyLC.h"
#include "common/p25/Crypto.h"
#include "common/network/udp/Socket.h"
#include "common/network/RTPHeader.h"
#include "common/yaml/Yaml.h"
#include "common/RingBuffer.h"
#include "common/Timer.h"
@ -105,11 +106,13 @@ void mdcPacketDetected(int frameCount, mdc_u8_t op, mdc_u8_t arg, mdc_u16_t unit
* @ingroup bridge
*/
struct NetPacketRequest {
uint32_t srcId; //!< Source Address
uint32_t dstId; //!< Destination Address
uint32_t srcId; //!< Source Address
uint32_t dstId; //!< Destination Address
int pcmLength = 0U; //!< Length of PCM data buffer
uint8_t* pcm = nullptr; //!< Raw PCM buffer
network::frame::RTPHeader rtpHeader; //!< RTP Header
int pcmLength = 0U; //!< Length of PCM data buffer
uint8_t* pcm = nullptr; //!< Raw PCM buffer
};
// ---------------------------------------------------------------------------
@ -149,25 +152,6 @@ private:
network::PeerNetwork* m_network;
network::udp::Socket* m_udpAudioSocket;
bool m_udpAudio;
bool m_udpMetadata;
uint16_t m_udpSendPort;
std::string m_udpSendAddress;
uint16_t m_udpReceivePort;
std::string m_udpReceiveAddress;
bool m_udpNoIncludeLength;
bool m_udpUseULaw;
bool m_udpRTPFrames;
bool m_udpUsrp;
bool m_udpFrameTiming;
uint32_t m_udpFrameCnt;
uint8_t m_tekAlgoId;
uint16_t m_tekKeyId;
bool m_requestedTek;
p25::crypto::P25Crypto* m_p25Crypto;
uint32_t m_srcId;
uint32_t m_srcIdOverride;
bool m_overrideSrcIdFromMDC;
@ -177,13 +161,31 @@ private:
uint8_t m_slot;
std::string m_identity;
uint32_t m_netId;
uint32_t m_sysId;
bool m_grantDemand;
uint8_t m_txMode;
float m_rxAudioGain;
float m_vocoderDecoderAudioGain;
bool m_vocoderDecoderAutoGain;
float m_txAudioGain;
float m_vocoderEncoderAudioGain;
uint8_t m_txMode;
bool m_trace;
bool m_debug;
uint8_t m_tekAlgoId;
uint16_t m_tekKeyId;
bool m_requestedTek;
p25::crypto::P25Crypto* m_p25Crypto;
bool m_localAudio;
float m_voxSampleLevel;
uint16_t m_dropTimeMS;
@ -196,10 +198,6 @@ private:
uint16_t m_preambleTone;
uint16_t m_preambleLength;
bool m_grantDemand;
bool m_localAudio;
ma_context m_maContext;
ma_device_info* m_maPlaybackDevices;
ma_device_info* m_maCaptureDevices;
@ -217,6 +215,26 @@ private:
vocoder::MBEEncoder* m_encoder;
mdc_decoder_t* m_mdcDecoder;
bool m_udpAudio;
bool m_udpMetadata;
uint16_t m_udpSendPort;
std::string m_udpSendAddress;
uint16_t m_udpReceivePort;
std::string m_udpReceiveAddress;
bool m_udpRTPFrames;
bool m_udpIgnoreRTPTiming;
bool m_udpRTPContinuousSeq;
bool m_udpUseULaw;
bool m_udpUsrp;
bool m_udpFrameTiming;
uint32_t m_udpFrameTimeout;
uint32_t m_udpFrameCnt;
/*
** Digital Mobile Radio
*/
dmr::data::EmbeddedData m_dmrEmbeddedData;
dmr::lc::LC m_rxDMRLC;
@ -226,37 +244,22 @@ private:
uint32_t m_dmrSeqNo;
uint8_t m_dmrN;
/*
** Project 25
*/
p25::lc::LC m_rxP25LC;
uint8_t* m_netLDU1;
uint8_t* m_netLDU2;
uint32_t m_p25SeqNo;
uint8_t m_p25N;
uint32_t m_netId;
uint32_t m_sysId;
/*
** Analog
*/
uint8_t m_analogN;
bool m_audioDetect;
bool m_trafficFromUDP;
uint32_t m_udpSrcId;
uint32_t m_udpDstId;
bool m_callInProgress;
bool m_ignoreCall;
uint8_t m_callAlgoId;
uint64_t m_rxStartTime;
uint32_t m_rxStreamId;
uint32_t m_txStreamId;
uint8_t m_detectedSampleCnt;
bool m_dumpSampleLevel;
bool m_mtNoSleep;
bool m_running;
bool m_trace;
bool m_debug;
// RTS PTT Control
bool m_rtsPttEnable;
std::string m_rtsPttPort;
@ -275,9 +278,30 @@ private:
Timer m_ctsPadTimeout; // drives silence padding while CTS is active
uint32_t m_ctsCorHoldoffMs; // hold-off time before clearing COR after it deasserts
bool m_audioDetect;
bool m_trafficFromUDP;
uint32_t m_udpSrcId;
uint32_t m_udpDstId;
bool m_callInProgress;
bool m_ignoreCall;
uint8_t m_callAlgoId;
uint64_t m_rxStartTime;
uint32_t m_rxStreamId;
uint32_t m_txStreamId;
uint8_t m_detectedSampleCnt;
Timer m_networkWatchdog;
static bool s_running;
bool m_rtpInitialFrame;
uint16_t m_rtpSeqNo;
uint32_t m_rtpTimestamp;
uint16_t m_udpNetPktSeq;
uint16_t m_udpNetLastPktSeq;
uint32_t m_usrpSeqNo;
static std::mutex s_audioMutex;
@ -429,6 +453,15 @@ private:
*/
void processUDPAudio();
/**
* @brief Helper to write UDP audio to the UDP audio socket.
* @param srcId Source ID.
* @param dstId Destination ID.
* @param pcm PCM audio buffer.
* @param pcmLength Length of PCM audio buffer.
*/
void writeUDPAudio(uint32_t srcId, uint32_t dstId, uint8_t* pcm, uint32_t pcmLength);
/**
* @brief Helper to process an In-Call Control message.
* @param command In-Call Control Command.
@ -438,66 +471,8 @@ private:
void processInCallCtrl(network::NET_ICC::ENUM command, uint32_t dstId, uint8_t slotNo);
/**
* @brief Helper to process DMR network traffic.
* @param buffer
* @param length
* @brief Helper to generate USRP end of transmission
*/
void processDMRNetwork(uint8_t* buffer, uint32_t length);
/**
* @brief Helper to decode DMR network traffic audio frames.
* @param ambe
* @param srcId
* @param dstId
* @param dmrN
*/
void decodeDMRAudioFrame(uint8_t* ambe, uint32_t srcId, uint32_t dstId, uint8_t dmrN);
/**
* @brief Helper to encode DMR network traffic audio frames.
* @param pcm
* @param forcedSrcId
* @param forcedDstId
*/
void encodeDMRAudioFrame(uint8_t* pcm, uint32_t forcedSrcId = 0U, uint32_t forcedDstId = 0U);
/**
* @brief Helper to process P25 network traffic.
* @param buffer
* @param length
*/
void processP25Network(uint8_t* buffer, uint32_t length);
/**
* @brief Helper to decode P25 network traffic audio frames.
* @param ldu
* @param srcId
* @param dstId
* @param p25N
*/
void decodeP25AudioFrame(uint8_t* ldu, uint32_t srcId, uint32_t dstId, uint8_t p25N);
/**
* @brief Helper to encode P25 network traffic audio frames.
* @param pcm
* @param forcedSrcId
* @param forcedDstId
*/
void encodeP25AudioFrame(uint8_t* pcm, uint32_t forcedSrcId = 0U, uint32_t forcedDstId = 0U);
/**
* @brief Helper to process analog network traffic.
* @param buffer
* @param length
*/
void processAnalogNetwork(uint8_t* buffer, uint32_t length);
/**
* @brief Helper to encode analog network traffic audio frames.
* @param pcm
* @param forcedSrcId
* @param forcedDstId
*/
void encodeAnalogAudioFrame(uint8_t* pcm, uint32_t forcedSrcId = 0U, uint32_t forcedDstId = 0U);
/**
* @brief Helper to generate USRP end of transmission
*/
void sendUsrpEot();
/**
@ -594,6 +569,67 @@ private:
* @returns void* (Ignore)
*/
static void* threadCtsCorMonitor(void* arg);
// Digital Mobile Radio (HostBridge.DMR.cpp)
/**
* @brief Helper to process DMR network traffic.
* @param buffer
* @param length
*/
void processDMRNetwork(uint8_t* buffer, uint32_t length);
/**
* @brief Helper to decode DMR network traffic audio frames.
* @param ambe
* @param srcId
* @param dstId
* @param dmrN
*/
void decodeDMRAudioFrame(uint8_t* ambe, uint32_t srcId, uint32_t dstId, uint8_t dmrN);
/**
* @brief Helper to encode DMR network traffic audio frames.
* @param pcm
* @param forcedSrcId
* @param forcedDstId
*/
void encodeDMRAudioFrame(uint8_t* pcm, uint32_t forcedSrcId = 0U, uint32_t forcedDstId = 0U);
// Project 25 (HostBridge.P25.cpp)
/**
* @brief Helper to process P25 network traffic.
* @param buffer
* @param length
*/
void processP25Network(uint8_t* buffer, uint32_t length);
/**
* @brief Helper to decode P25 network traffic audio frames.
* @param ldu
* @param srcId
* @param dstId
* @param p25N
*/
void decodeP25AudioFrame(uint8_t* ldu, uint32_t srcId, uint32_t dstId, uint8_t p25N);
/**
* @brief Helper to encode P25 network traffic audio frames.
* @param pcm
* @param forcedSrcId
* @param forcedDstId
*/
void encodeP25AudioFrame(uint8_t* pcm, uint32_t forcedSrcId = 0U, uint32_t forcedDstId = 0U);
// Analog (HostBridge.Analog.cpp)
/**
* @brief Helper to process analog network traffic.
* @param buffer
* @param length
*/
void processAnalogNetwork(uint8_t* buffer, uint32_t length);
/**
* @brief Helper to encode analog network traffic audio frames.
* @param pcm
* @param forcedSrcId
* @param forcedDstId
*/
void encodeAnalogAudioFrame(uint8_t* pcm, uint32_t forcedSrcId = 0U, uint32_t forcedDstId = 0U);
};
#endif // __HOST_BRIDGE_H__

@ -199,7 +199,7 @@ bool PeerNetwork::writeConfig()
::memcpy(buffer + 0U, TAG_REPEATER_CONFIG, 4U);
::snprintf(buffer + 8U, json.length() + 1U, "%s", json.c_str());
if (m_debug) {
if (m_packetDump) {
Utils::dump(1U, "Network Message, Configuration", (uint8_t*)buffer, json.length() + 8U);
}
@ -278,7 +278,7 @@ UInt8Array PeerNetwork::createP25_LDU1Message_Raw(uint32_t& length, const p25::l
buffer[23U] = count;
if (m_debug)
if (m_packetDump)
Utils::dump(1U, "Network Message, P25 LDU1", buffer, (P25_LDU1_PACKET_LENGTH + PACKET_PAD));
length = (P25_LDU1_PACKET_LENGTH + PACKET_PAD);
@ -353,7 +353,7 @@ UInt8Array PeerNetwork::createP25_LDU2Message_Raw(uint32_t& length, const p25::l
buffer[23U] = count;
if (m_debug)
if (m_packetDump)
Utils::dump(1U, "Network Message, P25 LDU2", buffer, (P25_LDU2_PACKET_LENGTH + PACKET_PAD));
length = (P25_LDU2_PACKET_LENGTH + PACKET_PAD);

Binary file not shown.

@ -110,7 +110,7 @@ typedef unsigned long long ulong64_t;
#define __EXE_NAME__ ""
#define VERSION_MAJOR "05"
#define VERSION_MINOR "02"
#define VERSION_MINOR "04"
#define VERSION_REV "A"
#define __NETVER__ "DVM_R" VERSION_MAJOR VERSION_REV VERSION_MINOR
@ -135,6 +135,10 @@ typedef unsigned long long ulong64_t;
"8 8888 ,o88P' `8.`8' ,8' `8 `8.`8888. \r\n" \
"8 888888888P' `8.` ,8' ` `8.`8888. \r\n"
#define HIGHLY_UNNECESSARY_DISCLAIMER_FOR_THE_MENTAL "THIS SOFTWARE MUST NEVER BE USED IN PUBLIC SAFETY OR LIFE SAFETY CRITICAL APPLICATIONS! This software project\n" \
"is provided solely for personal, non-commercial, hobbyist use; any commercial, professional, governmental,\n" \
"or other non-hobbyist use is strictly discouraged, fully unsupported and expressly disclaimed by the authors."
#define HOST_SW_API
/**

@ -38,7 +38,7 @@ namespace analog
*/
const uint32_t AUDIO_SAMPLES_LENGTH = 160U; //!< Sample size for 20ms of 16-bit audio at 8kHz.
const uint32_t AUDIO_SAMPLES_LENGTH_BYTES = 320U; //!< Sample size for 20ms of 16-bit audio at 8kHz in bytes.
const uint32_t AUDIO_SAMPLES_LENGTH_BYTES = 320U; //!< Sample size for 20ms of 16-bit audio at 8kHz in bytes. (AUDIO_SAMPLES_LENGTH * 2)
/** @} */
/** @brief Audio Frame Type(s) */

@ -120,6 +120,12 @@ namespace dmr
const uint32_t DMR_PDU_CONFIRMED_HR_DATA_LENGTH_BYTES = 10U;
const uint32_t DMR_PDU_CONFIRMED_UNCODED_DATA_LENGTH_BYTES = 22U;
const uint32_t DMR_PDU_ARP_PCKT_LENGTH = 22U;
const uint32_t DMR_PDU_ARP_HW_ADDR_LENGTH = 3U;
const uint32_t DMR_PDU_ARP_PROTO_ADDR_LENGTH = 4U;
const uint8_t DMR_PDU_ARP_CAI_TYPE = 0x21U;
const uint32_t MI_LENGTH_BYTES = 4U; // This was guessed based on OTA data captures -- the message indicator seems to be the same length as a source/destination address
const uint32_t RAW_AMBE_LENGTH_BYTES = 9U;
/** @} */
@ -149,6 +155,7 @@ namespace dmr
const uint16_t DMR_LOGICAL_CH_ABSOLUTE = 0xFFFU;
const uint32_t WUID_IPI = 0xFFFEC3U; //!< IP Interface Working Unit ID
const uint32_t WUID_SUPLI = 0xFFFEC4U; //!< Supplementary Data Service Working Unit ID
const uint32_t WUID_SDMI = 0xFFFEC5U; //!< UDT Short Data Service Working Unit ID
const uint32_t WUID_REGI = 0xFFFEC6U; //!< Registration Working Unit ID
@ -179,6 +186,22 @@ namespace dmr
};
};
/** @brief Service Access Point */
namespace PDUSAP {
/** @brief Service Access Point */
enum : uint8_t {
UDT = 0x00U, //!< Unified Data Transport Header
PACKET_DATA = 0x04U, //!< IP based Packet Data
ARP = 0x05U, //!< ARP
PROP_PACKET_DATA = 0x09U, //!< Proprietary Packet Data
SHORT_DATA = 0x0AU //!< Defined Short Data
};
}
/** @brief Data Response Class */
namespace PDUResponseClass {
/** @brief Data Response Class */
@ -187,7 +210,7 @@ namespace dmr
NACK = 0x01U, //!< Negative Acknowledge
ACK_RETRY = 0x02U //!< Acknowlege Retry
};
};
}
/** @brief Data Response Type */
namespace PDUResponseType {
@ -200,7 +223,7 @@ namespace dmr
NACK_MEMORY_FULL = 0x02U, //!< Memory Full
NACK_UNDELIVERABLE = 0x04U //!< Undeliverable
};
};
}
/** @brief ARP Request */
const uint8_t DMR_PDU_ARP_REQUEST = 0x01U;

@ -47,8 +47,9 @@ void EMB::decode(const uint8_t* data)
DMREMB[1U] = (data[18U] << 4) & 0xF0U;
DMREMB[1U] |= (data[19U] >> 4) & 0x0FU;
// decode QR (16,7,6) FEC
edac::QR1676::decode(DMREMB);
// decode QR (16,7,6) FEC and get corrected data
uint8_t corrected = edac::QR1676::decode(DMREMB);
DMREMB[0U] = (corrected << 1) & 0xFEU;
m_colorCode = (DMREMB[0U] >> 4) & 0x0FU;
m_PI = (DMREMB[0U] & 0x08U) == 0x08U;

@ -5,6 +5,7 @@
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* Copyright (C) 2015,2016 Jonathan Naylor, G4KLX
* Copyright (C) 2026 Bryan Biedenkapp, N2PLL
*
*/
#include "edac/Hamming.h"
@ -360,3 +361,93 @@ void Hamming::encode17123(bool* d)
d[15] = d[0] ^ d[1] ^ d[4] ^ d[5] ^ d[7] ^ d[10];
d[16] = d[0] ^ d[1] ^ d[2] ^ d[5] ^ d[6] ^ d[8] ^ d[11];
}
/* Decode Hamming (8,4,4). */
bool Hamming::decode844(bool* d)
{
assert(d != nullptr);
// Hamming(8,4,4) extended code layout:
// d[0..3] = data bits (4 bits)
// d[4..6] = parity bits (3 bits) - Hamming(7,4,3) parity
// d[7] = overall parity bit (1 bit)
//
// Parity check matrix:
// P0 (d[4]) = d[0] ^ d[1] ^ d[3]
// P1 (d[5]) = d[0] ^ d[2] ^ d[3]
// P2 (d[6]) = d[1] ^ d[2] ^ d[3]
// P3 (d[7]) = d[0] ^ d[1] ^ d[2] ^ d[3] ^ d[4] ^ d[5] ^ d[6] (overall parity)
// Calculate syndrome bits for Hamming(7,4,3) portion
bool c0 = d[0] ^ d[1] ^ d[3] ^ d[4]; // Check P0
bool c1 = d[0] ^ d[2] ^ d[3] ^ d[5]; // Check P1
bool c2 = d[1] ^ d[2] ^ d[3] ^ d[6]; // Check P2
// Calculate overall parity
bool c3 = d[0] ^ d[1] ^ d[2] ^ d[3] ^ d[4] ^ d[5] ^ d[6] ^ d[7];
// Build syndrome
unsigned char syndrome = 0x00U;
syndrome |= c0 ? 0x01U : 0x00U;
syndrome |= c1 ? 0x02U : 0x00U;
syndrome |= c2 ? 0x04U : 0x00U;
// If overall parity is wrong and syndrome is non-zero, single bit error
// If overall parity is wrong and syndrome is zero, error in parity bit d[7]
// If overall parity is correct and syndrome is non-zero, double bit error (uncorrectable)
// If both are correct, no error
if (c3) {
// Overall parity error detected
if (syndrome == 0x00U) {
// Error in overall parity bit
d[7] = !d[7];
return true;
}
else {
// Single bit error - syndrome tells us which bit
switch (syndrome) {
case 0x03U: d[0] = !d[0]; return true; // d0 position
case 0x05U: d[1] = !d[1]; return true; // d1 position
case 0x06U: d[2] = !d[2]; return true; // d2 position
case 0x07U: d[3] = !d[3]; return true; // d3 position
case 0x01U: d[4] = !d[4]; return true; // P0 position
case 0x02U: d[5] = !d[5]; return true; // P1 position
case 0x04U: d[6] = !d[6]; return true; // P2 position
default: return false; // Should not happen
}
}
}
else {
// Overall parity correct
if (syndrome == 0x00U) {
// No errors
return false;
}
else {
// Double bit error detected - uncorrectable
return false;
}
}
}
/* Encode Hamming (8,4,4). */
void Hamming::encode844(bool* d)
{
assert(d != nullptr);
// Hamming(8,4,4) extended code
// d[0..3] = data bits (input)
// d[4..6] = parity bits (calculated)
// d[7] = overall parity bit (calculated)
// Calculate Hamming(7,4,3) parity bits
d[4] = d[0] ^ d[1] ^ d[3]; // P0
d[5] = d[0] ^ d[2] ^ d[3]; // P1
d[6] = d[1] ^ d[2] ^ d[3]; // P2
// Calculate overall parity bit
d[7] = d[0] ^ d[1] ^ d[2] ^ d[3] ^ d[4] ^ d[5] ^ d[6];
}

@ -5,6 +5,7 @@
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* Copyright (C) 2015,2016 Jonathan Naylor, G4KLX
* Copyright (C) 2026 Bryan Biedenkapp, N2PLL
*
*/
/**
@ -102,6 +103,18 @@ namespace edac
* @param d Boolean bit array.
*/
static void encode17123(bool* d);
/**
* @brief Decode Hamming (8,4,4).
* @param d Boolean bit array.
* @returns bool True, if bit errors are detected, otherwise false.
*/
static bool decode844(bool* d);
/**
* @brief Encode Hamming (8,4,4).
* @param d Boolean bit array.
*/
static void encode844(bool* d);
};
} // namespace edac

@ -76,7 +76,7 @@ uint8_t QR1676::decode(const uint8_t* data)
code ^= error_pattern;
return code >> 7;
return (code >> 8) & 0x7FU;
}
/* Encode QR (16,7,6) FEC. */

@ -5,7 +5,7 @@
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* Copyright (C) 2016 Jonathan Naylor, G4KLX
* Copyright (C) 2017,2023,2025 Bryan Biedenkapp, N2PLL
* Copyright (C) 2017,2023,2025,2026 Bryan Biedenkapp, N2PLL
*
*/
#include "Defines.h"
@ -23,6 +23,8 @@ using namespace edac;
// Constants
// ---------------------------------------------------------------------------
/** Project 25 Phase I Reed-Solomon (TIA-102.BAAA-B Section 4.9) */
const uint8_t ENCODE_MATRIX[12U][24U] = {
{ 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 062, 044, 003, 025, 014, 016, 027, 003, 053, 004, 036, 047 },
{ 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 011, 012, 011, 011, 016, 064, 067, 055, 001, 076, 026, 073 },
@ -77,6 +79,8 @@ const uint8_t ENCODE_MATRIX_362017[20U][36U] = {
{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 002, 001, 053, 074, 002, 014, 052, 074, 012, 057, 024, 063, 015, 042, 052, 033 },
{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 034, 035, 002, 023, 021, 027, 022, 033, 064, 042, 005, 073, 051, 046, 073, 060 } };
/** Project 25 Phase II Reed-Solomon (TIA-102.BBAC-A Section 5.6) */
const uint8_t ENCODE_MATRIX_633529[35U][63U] = {
{ 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 013, 014, 023, 076, 015, 077, 050, 062, 015, 014, 012, 007, 074, 045, 023, 071, 050, 064, 010, 016, 022, 071, 077, 020, 051, 061, 032, 006 },
{ 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 072, 043, 045, 021, 020, 011, 012, 002, 034, 045, 060, 030, 011, 047, 014, 003, 014, 026, 004, 054, 041, 002, 075, 034, 043, 011, 056, 016 },
@ -115,110 +119,110 @@ const uint8_t ENCODE_MATRIX_633529[35U][63U] = {
{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 026, 055, 065, 012, 051, 067, 043, 012, 026, 035, 027, 015, 075, 055, 042, 067, 050, 045, 056, 061, 042, 051, 011, 053, 007, 024, 013, 034 } };
const uint8_t ENCODE_MATRIX_523023[30U][52U] = {
{ 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 023, 076, 015, 077, 050, 062, 015, 014, 012, 007, 074, 045, 023, 071, 050, 064, 010, 016, 022, 071, 077, 020 },
{ 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 045, 021, 020, 011, 012, 002, 034, 045, 060, 030, 011, 047, 014, 003, 014, 026, 004, 054, 041, 002, 075, 034 },
{ 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 024, 036, 024, 045, 063, 072, 007, 027, 012, 032, 077, 066, 020, 035, 071, 030, 045, 023, 025, 060, 067, 030 },
{ 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 011, 022, 057, 030, 071, 016, 013, 074, 020, 074, 010, 022, 016, 040, 001, 070, 013, 012, 041, 045, 074, 021 },
{ 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 071, 034, 045, 005, 050, 044, 071, 003, 060, 053, 024, 017, 061, 040, 020, 030, 011, 076, 026, 017, 017, 035 },
{ 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 061, 045, 002, 035, 037, 016, 072, 003, 044, 011, 074, 020, 073, 024, 072, 053, 064, 070, 056, 063, 067, 024 },
{ 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 045, 031, 046, 021, 013, 017, 015, 002, 047, 003, 024, 051, 074, 064, 002, 066, 072, 071, 057, 041, 040, 025 },
{ 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 057, 013, 053, 071, 033, 046, 075, 016, 041, 066, 014, 054, 075, 003, 076, 017, 064, 030, 034, 020, 076, 044 },
{ 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 010, 001, 071, 064, 063, 066, 024, 076, 055, 060, 071, 064, 070, 002, 011, 063, 015, 026, 075, 043, 017, 072 },
{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 056, 036, 017, 025, 012, 021, 070, 040, 020, 015, 021, 011, 013, 016, 074, 061, 052, 016, 023, 013, 017, 075 },
{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 054, 071, 007, 041, 020, 075, 010, 030, 020, 071, 053, 015, 003, 065, 013, 033, 060, 073, 075, 055, 045, 015 },
{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 054, 030, 044, 054, 055, 046, 040, 012, 033, 016, 063, 072, 025, 051, 071, 074, 046, 014, 074, 027, 006, 034 },
{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 076, 055, 017, 046, 027, 070, 061, 064, 024, 022, 011, 037, 017, 035, 022, 046, 044, 064, 072, 064, 025, 066 },
{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 034, 020, 037, 020, 054, 072, 012, 062, 027, 005, 035, 061, 013, 060, 027, 037, 044, 006, 021, 005, 053, 021 },
{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 074, 024, 056, 017, 001, 002, 004, 054, 007, 034, 075, 062, 023, 010, 041, 052, 032, 062, 074, 022, 025, 041 },
{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 013, 052, 032, 002, 061, 043, 014, 060, 002, 047, 075, 015, 015, 045, 066, 031, 063, 031, 067, 012, 076, 047 },
{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 017, 037, 012, 062, 011, 071, 003, 020, 042, 060, 010, 026, 033, 053, 056, 060, 060, 024, 063, 021, 042, 057 },
{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 071, 071, 054, 045, 013, 025, 012, 051, 057, 056, 064, 002, 047, 041, 022, 047, 075, 050, 074, 011, 076, 070 },
{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 054, 046, 036, 022, 061, 022, 062, 014, 054, 015, 060, 007, 052, 032, 065, 010, 043, 072, 041, 001, 067, 066 },
{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 061, 050, 071, 026, 073, 046, 015, 041, 067, 010, 021, 006, 026, 012, 063, 012, 053, 050, 047, 001, 011, 062 },
{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 021, 016, 062, 004, 005, 034, 074, 025, 065, 071, 063, 030, 040, 047, 031, 030, 032, 067, 014, 026, 074, 051 },
{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 040, 054, 072, 013, 042, 010, 050, 014, 075, 051, 014, 041, 027, 001, 001, 014, 070, 042, 074, 055, 057, 077 },
{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 073, 076, 034, 006, 044, 056, 070, 072, 027, 026, 060, 023, 074, 042, 056, 004, 020, 055, 035, 011, 021, 027 },
{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 002, 012, 053, 075, 030, 020, 073, 075, 034, 044, 007, 073, 057, 076, 074, 071, 002, 065, 001, 037, 050, 035 },
{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 041, 034, 072, 027, 022, 024, 040, 051, 046, 067, 075, 030, 046, 032, 021, 071, 045, 027, 012, 064, 043, 020 },
{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 067, 037, 021, 005, 077, 040, 031, 054, 043, 041, 013, 030, 013, 037, 062, 045, 061, 053, 005, 063, 013, 063 },
{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 074, 007, 035, 062, 040, 036, 042, 010, 024, 031, 067, 054, 021, 001, 072, 072, 073, 006, 061, 017, 020, 067 },
{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 057, 075, 020, 037, 011, 065, 011, 066, 026, 035, 036, 033, 031, 031, 072, 045, 042, 051, 060, 071, 015, 040 },
{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 040, 013, 037, 033, 061, 040, 027, 004, 034, 036, 044, 022, 004, 065, 067, 064, 022, 062, 031, 034, 062, 040 },
{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 044, 074, 025, 047, 001, 027, 076, 055, 043, 045, 011, 040, 046, 041, 057, 014, 030, 043, 042, 074, 044, 051 } };
{ 001, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 003, 011, 061, 045, 002, 035, 037, 016, 072, 003, 044, 011, 074, 020, 073, 024, 072, 053, 064, 070, 056, 063 },
{ 000, 001, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 037, 073, 045, 031, 046, 021, 013, 017, 015, 002, 047, 003, 024, 051, 074, 064, 002, 066, 072, 071, 057, 041 },
{ 000, 000, 001, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 022, 034, 057, 013, 053, 071, 033, 046, 075, 016, 041, 066, 014, 054, 075, 003, 076, 017, 064, 030, 034, 020 },
{ 000, 000, 000, 001, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 022, 021, 010, 001, 071, 064, 063, 066, 024, 076, 055, 060, 071, 064, 070, 002, 011, 063, 015, 026, 075, 043 },
{ 000, 000, 000, 000, 001, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 024, 046, 056, 036, 017, 025, 012, 021, 070, 040, 020, 015, 021, 011, 013, 016, 074, 061, 052, 016, 023, 013 },
{ 000, 000, 000, 000, 000, 001, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 011, 064, 054, 071, 007, 041, 020, 075, 010, 030, 020, 071, 053, 015, 003, 065, 013, 033, 060, 073, 075, 055 },
{ 000, 000, 000, 000, 000, 000, 001, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 034, 013, 054, 030, 044, 054, 055, 046, 040, 012, 033, 016, 063, 072, 025, 051, 071, 074, 046, 014, 074, 027 },
{ 000, 000, 000, 000, 000, 000, 000, 001, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 035, 010, 076, 055, 017, 046, 027, 070, 061, 064, 024, 022, 011, 037, 017, 035, 022, 046, 044, 064, 072, 064 },
{ 000, 000, 000, 000, 000, 000, 000, 000, 001, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 022, 036, 034, 020, 037, 020, 054, 072, 012, 062, 027, 005, 035, 061, 013, 060, 027, 037, 044, 006, 021, 005 },
{ 000, 000, 000, 000, 000, 000, 000, 000, 000, 001, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 055, 064, 074, 024, 056, 017, 001, 002, 004, 054, 007, 034, 075, 062, 023, 010, 041, 052, 032, 062, 074, 022 },
{ 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 001, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 024, 031, 013, 052, 032, 002, 061, 043, 014, 060, 002, 047, 075, 015, 015, 045, 066, 031, 063, 031, 067, 012 },
{ 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 001, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 056, 010, 017, 037, 012, 062, 011, 071, 003, 020, 042, 060, 010, 026, 033, 053, 056, 060, 060, 024, 063, 021 },
{ 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 001, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 046, 004, 071, 071, 054, 045, 013, 025, 012, 051, 057, 056, 064, 002, 047, 041, 022, 047, 075, 050, 074, 011 },
{ 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 001, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 044, 040, 054, 046, 036, 022, 061, 022, 062, 014, 054, 015, 060, 007, 052, 032, 065, 010, 043, 072, 041, 001 },
{ 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 001, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 067, 067, 061, 050, 071, 026, 073, 046, 015, 041, 067, 010, 021, 006, 026, 012, 063, 012, 053, 050, 047, 001 },
{ 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 001, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 026, 057, 021, 016, 062, 004, 005, 034, 074, 025, 065, 071, 063, 030, 040, 047, 031, 030, 032, 067, 014, 026 },
{ 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 001, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 054, 046, 040, 054, 072, 013, 042, 010, 050, 014, 075, 051, 014, 041, 027, 001, 001, 014, 070, 042, 074, 055 },
{ 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 001, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 071, 076, 073, 076, 034, 006, 044, 056, 070, 072, 027, 026, 060, 023, 074, 042, 056, 004, 020, 055, 035, 011 },
{ 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 001, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 066, 074, 002, 012, 053, 075, 030, 020, 073, 075, 034, 044, 007, 073, 057, 076, 074, 071, 002, 065, 001, 037 },
{ 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 001, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 071, 044, 041, 034, 072, 027, 022, 024, 040, 051, 046, 067, 075, 030, 046, 032, 021, 071, 045, 027, 012, 064 },
{ 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 001, 000, 000, 000, 000, 000, 000, 000, 000, 000, 013, 065, 067, 037, 021, 005, 077, 040, 031, 054, 043, 041, 013, 030, 013, 037, 062, 045, 061, 053, 005, 063 },
{ 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 001, 000, 000, 000, 000, 000, 000, 000, 000, 053, 032, 074, 007, 035, 062, 040, 036, 042, 010, 024, 031, 067, 054, 021, 001, 072, 072, 073, 006, 061, 017 },
{ 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 001, 000, 000, 000, 000, 000, 000, 000, 035, 077, 057, 075, 020, 037, 011, 065, 011, 066, 026, 035, 036, 033, 031, 031, 072, 045, 042, 051, 060, 071 },
{ 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 001, 000, 000, 000, 000, 000, 000, 005, 020, 040, 013, 037, 033, 061, 040, 027, 004, 034, 036, 044, 022, 004, 065, 067, 064, 022, 062, 031, 034 },
{ 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 001, 000, 000, 000, 000, 000, 003, 077, 044, 074, 025, 047, 001, 027, 076, 055, 043, 045, 011, 040, 046, 041, 057, 014, 030, 043, 042, 074 },
{ 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 001, 000, 000, 000, 000, 052, 004, 033, 041, 064, 037, 065, 003, 037, 071, 010, 016, 076, 023, 004, 016, 063, 017, 067, 001, 010, 012 },
{ 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 001, 000, 000, 000, 015, 021, 074, 035, 020, 070, 003, 010, 062, 044, 076, 076, 034, 023, 053, 064, 022, 062, 034, 030, 063, 070 },
{ 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 001, 000, 000, 054, 075, 036, 001, 051, 051, 036, 016, 074, 002, 014, 042, 013, 016, 034, 012, 022, 007, 022, 044, 023, 022 },
{ 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 001, 000, 065, 023, 065, 063, 012, 060, 055, 014, 005, 003, 003, 006, 044, 004, 006, 073, 016, 076, 055, 006, 030, 064 },
{ 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 001, 026, 055, 065, 012, 051, 067, 043, 012, 026, 035, 027, 015, 075, 055, 042, 067, 050, 045, 056, 061, 042, 051 } };
const uint8_t ENCODE_MATRIX_462621[26U][46U] = {
{ 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 055, 043, 012, 026, 035, 027, 015, 075, 055, 042, 067, 050, 045, 056, 061, 042, 051, 011, 053, 007 },
{ 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 014, 012, 007, 074, 045, 023, 071, 050, 064, 010, 016, 022, 071, 077, 020, 021, 020, 011, 012, 002 },
{ 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 060, 030, 011, 047, 014, 003, 014, 026, 004, 054, 041, 002, 075, 034, 036, 024, 045, 063, 072, 007 },
{ 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 012, 032, 077, 066, 020, 035, 071, 030, 045, 023, 025, 060, 067, 030, 022, 057, 030, 071, 016, 013 },
{ 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 020, 074, 010, 022, 016, 040, 001, 070, 013, 012, 041, 045, 074, 021, 034, 045, 005, 050, 044, 071 },
{ 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 060, 053, 024, 017, 061, 040, 020, 030, 011, 076, 026, 017, 017, 035, 045, 002, 035, 037, 016, 072 },
{ 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 044, 011, 074, 020, 073, 024, 072, 053, 064, 070, 056, 063, 067, 024, 031, 046, 021, 013, 017, 015 },
{ 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 047, 003, 024, 051, 074, 064, 002, 066, 072, 071, 057, 041, 040, 025, 013, 053, 071, 033, 046, 075 },
{ 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 041, 066, 014, 054, 075, 003, 076, 017, 064, 030, 034, 020, 076, 044, 001, 071, 064, 063, 066, 024 },
{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 055, 060, 071, 064, 070, 002, 011, 063, 015, 026, 075, 043, 017, 072, 036, 017, 025, 012, 021, 070 },
{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 020, 015, 021, 011, 013, 016, 074, 061, 052, 016, 023, 013, 017, 075, 071, 007, 041, 020, 075, 010 },
{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 020, 071, 053, 015, 003, 065, 013, 033, 060, 073, 075, 055, 045, 015, 030, 044, 054, 055, 046, 040 },
{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 033, 016, 063, 072, 025, 051, 071, 074, 046, 014, 074, 027, 006, 034, 055, 017, 046, 027, 070, 061 },
{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 024, 022, 011, 037, 017, 035, 022, 046, 044, 064, 072, 064, 025, 066, 020, 037, 020, 054, 072, 012 },
{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 027, 005, 035, 061, 013, 060, 027, 037, 044, 006, 021, 005, 053, 021, 024, 056, 017, 001, 002, 004 },
{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 007, 034, 075, 062, 023, 010, 041, 052, 032, 062, 074, 022, 025, 041, 052, 032, 002, 061, 043, 014 },
{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 002, 047, 075, 015, 015, 045, 066, 031, 063, 031, 067, 012, 076, 047, 037, 012, 062, 011, 071, 003 },
{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 042, 060, 010, 026, 033, 053, 056, 060, 060, 024, 063, 021, 042, 057, 071, 054, 045, 013, 025, 012 },
{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 057, 056, 064, 002, 047, 041, 022, 047, 075, 050, 074, 011, 076, 070, 046, 036, 022, 061, 022, 062 },
{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 054, 015, 060, 007, 052, 032, 065, 010, 043, 072, 041, 001, 067, 066, 050, 071, 026, 073, 046, 015 },
{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 067, 010, 021, 006, 026, 012, 063, 012, 053, 050, 047, 001, 011, 062, 016, 062, 004, 005, 034, 074 },
{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 065, 071, 063, 030, 040, 047, 031, 030, 032, 067, 014, 026, 074, 051, 054, 072, 013, 042, 010, 050 },
{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 075, 051, 014, 041, 027, 001, 001, 014, 070, 042, 074, 055, 057, 077, 076, 034, 006, 044, 056, 070 },
{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 027, 026, 060, 023, 074, 042, 056, 004, 020, 055, 035, 011, 021, 027, 012, 053, 075, 030, 020, 073 },
{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 034, 044, 007, 073, 057, 076, 074, 071, 002, 065, 001, 037, 050, 035, 034, 072, 027, 022, 024, 040 },
{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 046, 067, 075, 030, 046, 032, 021, 071, 045, 027, 012, 064, 043, 020, 037, 021, 005, 077, 040, 031 } };
{ 001, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 024, 046, 056, 036, 017, 025, 012, 021, 070, 040, 020, 015, 021, 011, 013, 016, 074, 061, 052, 016 },
{ 000, 001, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 011, 064, 054, 071, 007, 041, 020, 075, 010, 030, 020, 071, 053, 015, 003, 065, 013, 033, 060, 073 },
{ 000, 000, 001, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 034, 013, 054, 030, 044, 054, 055, 046, 040, 012, 033, 016, 063, 072, 025, 051, 071, 074, 046, 014 },
{ 000, 000, 000, 001, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 035, 010, 076, 055, 017, 046, 027, 070, 061, 064, 024, 022, 011, 037, 017, 035, 022, 046, 044, 064 },
{ 000, 000, 000, 000, 001, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 022, 036, 034, 020, 037, 020, 054, 072, 012, 062, 027, 005, 035, 061, 013, 060, 027, 037, 044, 006 },
{ 000, 000, 000, 000, 000, 001, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 055, 064, 074, 024, 056, 017, 001, 002, 004, 054, 007, 034, 075, 062, 023, 010, 041, 052, 032, 062 },
{ 000, 000, 000, 000, 000, 000, 001, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 024, 031, 013, 052, 032, 002, 061, 043, 014, 060, 002, 047, 075, 015, 015, 045, 066, 031, 063, 031 },
{ 000, 000, 000, 000, 000, 000, 000, 001, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 056, 010, 017, 037, 012, 062, 011, 071, 003, 020, 042, 060, 010, 026, 033, 053, 056, 060, 060, 024 },
{ 000, 000, 000, 000, 000, 000, 000, 000, 001, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 046, 004, 071, 071, 054, 045, 013, 025, 012, 051, 057, 056, 064, 002, 047, 041, 022, 047, 075, 050 },
{ 000, 000, 000, 000, 000, 000, 000, 000, 000, 001, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 044, 040, 054, 046, 036, 022, 061, 022, 062, 014, 054, 015, 060, 007, 052, 032, 065, 010, 043, 072 },
{ 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 001, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 067, 067, 061, 050, 071, 026, 073, 046, 015, 041, 067, 010, 021, 006, 026, 012, 063, 012, 053, 050 },
{ 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 001, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 026, 057, 021, 016, 062, 004, 005, 034, 074, 025, 065, 071, 063, 030, 040, 047, 031, 030, 032, 067 },
{ 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 001, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 054, 046, 040, 054, 072, 013, 042, 010, 050, 014, 075, 051, 014, 041, 027, 001, 001, 014, 070, 042 },
{ 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 001, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 071, 076, 073, 076, 034, 006, 044, 056, 070, 072, 027, 026, 060, 023, 074, 042, 056, 004, 020, 055 },
{ 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 001, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 066, 074, 002, 012, 053, 075, 030, 020, 073, 075, 034, 044, 007, 073, 057, 076, 074, 071, 002, 065 },
{ 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 001, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 071, 044, 041, 034, 072, 027, 022, 024, 040, 051, 046, 067, 075, 030, 046, 032, 021, 071, 045, 027 },
{ 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 001, 000, 000, 000, 000, 000, 000, 000, 000, 000, 013, 065, 067, 037, 021, 005, 077, 040, 031, 054, 043, 041, 013, 030, 013, 037, 062, 045, 061, 053 },
{ 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 001, 000, 000, 000, 000, 000, 000, 000, 000, 053, 032, 074, 007, 035, 062, 040, 036, 042, 010, 024, 031, 067, 054, 021, 001, 072, 072, 073, 006 },
{ 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 001, 000, 000, 000, 000, 000, 000, 000, 035, 077, 057, 075, 020, 037, 011, 065, 011, 066, 026, 035, 036, 033, 031, 031, 072, 045, 042, 051 },
{ 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 001, 000, 000, 000, 000, 000, 000, 005, 020, 040, 013, 037, 033, 061, 040, 027, 004, 034, 036, 044, 022, 004, 065, 067, 064, 022, 062 },
{ 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 001, 000, 000, 000, 000, 000, 003, 077, 044, 074, 025, 047, 001, 027, 076, 055, 043, 045, 011, 040, 046, 041, 057, 014, 030, 043 },
{ 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 001, 000, 000, 000, 000, 052, 004, 033, 041, 064, 037, 065, 003, 037, 071, 010, 016, 076, 023, 004, 016, 063, 017, 067, 001 },
{ 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 001, 000, 000, 000, 015, 021, 074, 035, 020, 070, 003, 010, 062, 044, 076, 076, 034, 023, 053, 064, 022, 062, 034, 030 },
{ 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 001, 000, 000, 054, 075, 036, 001, 051, 051, 036, 016, 074, 002, 014, 042, 013, 016, 034, 012, 022, 007, 022, 044 },
{ 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 001, 000, 065, 023, 065, 063, 012, 060, 055, 014, 005, 003, 003, 006, 044, 004, 006, 073, 016, 076, 055, 006 },
{ 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 001, 026, 055, 065, 012, 051, 067, 043, 012, 026, 035, 027, 015, 075, 055, 042, 067, 050, 045, 056, 061 } };
const uint8_t ENCODE_MATRIX_452620[26U][45U] = {
{ 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 012, 026, 035, 027, 015, 075, 055, 042, 067, 050, 045, 056, 061, 042, 051, 011, 053, 007, 024 },
{ 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 007, 074, 045, 023, 071, 050, 064, 010, 016, 022, 071, 077, 020, 021, 020, 011, 012, 002, 034 },
{ 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 030, 011, 047, 014, 003, 014, 026, 004, 054, 041, 002, 075, 034, 036, 024, 045, 063, 072, 007 },
{ 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 032, 077, 066, 020, 035, 071, 030, 045, 023, 025, 060, 067, 030, 022, 057, 030, 071, 016, 013 },
{ 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 074, 010, 022, 016, 040, 001, 070, 013, 012, 041, 045, 074, 021, 034, 045, 005, 050, 044, 071 },
{ 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 053, 024, 017, 061, 040, 020, 030, 011, 076, 026, 017, 017, 035, 045, 002, 035, 037, 016, 072 },
{ 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 011, 074, 020, 073, 024, 072, 053, 064, 070, 056, 063, 067, 024, 031, 046, 021, 013, 017, 015 },
{ 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 003, 024, 051, 074, 064, 002, 066, 072, 071, 057, 041, 040, 025, 013, 053, 071, 033, 046, 075 },
{ 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 066, 014, 054, 075, 003, 076, 017, 064, 030, 034, 020, 076, 044, 001, 071, 064, 063, 066, 024 },
{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 060, 071, 064, 070, 002, 011, 063, 015, 026, 075, 043, 017, 072, 036, 017, 025, 012, 021, 070 },
{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 015, 021, 011, 013, 016, 074, 061, 052, 016, 023, 013, 017, 075, 071, 007, 041, 020, 075, 010 },
{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 071, 053, 015, 003, 065, 013, 033, 060, 073, 075, 055, 045, 015, 030, 044, 054, 055, 046, 040 },
{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 016, 063, 072, 025, 051, 071, 074, 046, 014, 074, 027, 006, 034, 055, 017, 046, 027, 070, 061 },
{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 022, 011, 037, 017, 035, 022, 046, 044, 064, 072, 064, 025, 066, 020, 037, 020, 054, 072, 012 },
{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 005, 035, 061, 013, 060, 027, 037, 044, 006, 021, 005, 053, 021, 024, 056, 017, 001, 002, 004 },
{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 034, 075, 062, 023, 010, 041, 052, 032, 062, 074, 022, 025, 041, 052, 032, 002, 061, 043, 014 },
{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 047, 075, 015, 015, 045, 066, 031, 063, 031, 067, 012, 076, 047, 037, 012, 062, 011, 071, 003 },
{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 060, 010, 026, 033, 053, 056, 060, 060, 024, 063, 021, 042, 057, 071, 054, 045, 013, 025, 012 },
{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 056, 064, 002, 047, 041, 022, 047, 075, 050, 074, 011, 076, 070, 046, 036, 022, 061, 022, 062 },
{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 015, 060, 007, 052, 032, 065, 010, 043, 072, 041, 001, 067, 066, 050, 071, 026, 073, 046, 015 },
{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 010, 021, 006, 026, 012, 063, 012, 053, 050, 047, 001, 011, 062, 016, 062, 004, 005, 034, 074 },
{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 071, 063, 030, 040, 047, 031, 030, 032, 067, 014, 026, 074, 051, 054, 072, 013, 042, 010, 050 },
{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 051, 014, 041, 027, 001, 001, 014, 070, 042, 074, 055, 057, 077, 076, 034, 006, 044, 056, 070 },
{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 026, 060, 023, 074, 042, 056, 004, 020, 055, 035, 011, 021, 027, 012, 053, 075, 030, 020, 073 },
{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 044, 007, 073, 057, 076, 074, 071, 002, 065, 001, 037, 050, 035, 034, 072, 027, 022, 024, 040 },
{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 067, 075, 030, 046, 032, 021, 071, 045, 027, 012, 064, 043, 020, 037, 021, 005, 077, 040, 031 } };
{ 001, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 024, 046, 056, 036, 017, 025, 012, 021, 070, 040, 020, 015, 021, 011, 013, 016, 074, 061, 052 },
{ 000, 001, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 011, 064, 054, 071, 007, 041, 020, 075, 010, 030, 020, 071, 053, 015, 003, 065, 013, 033, 060 },
{ 000, 000, 001, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 034, 013, 054, 030, 044, 054, 055, 046, 040, 012, 033, 016, 063, 072, 025, 051, 071, 074, 046 },
{ 000, 000, 000, 001, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 035, 010, 076, 055, 017, 046, 027, 070, 061, 064, 024, 022, 011, 037, 017, 035, 022, 046, 044 },
{ 000, 000, 000, 000, 001, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 022, 036, 034, 020, 037, 020, 054, 072, 012, 062, 027, 005, 035, 061, 013, 060, 027, 037, 044 },
{ 000, 000, 000, 000, 000, 001, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 055, 064, 074, 024, 056, 017, 001, 002, 004, 054, 007, 034, 075, 062, 023, 010, 041, 052, 032 },
{ 000, 000, 000, 000, 000, 000, 001, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 024, 031, 013, 052, 032, 002, 061, 043, 014, 060, 002, 047, 075, 015, 015, 045, 066, 031, 063 },
{ 000, 000, 000, 000, 000, 000, 000, 001, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 056, 010, 017, 037, 012, 062, 011, 071, 003, 020, 042, 060, 010, 026, 033, 053, 056, 060, 060 },
{ 000, 000, 000, 000, 000, 000, 000, 000, 001, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 046, 004, 071, 071, 054, 045, 013, 025, 012, 051, 057, 056, 064, 002, 047, 041, 022, 047, 075 },
{ 000, 000, 000, 000, 000, 000, 000, 000, 000, 001, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 044, 040, 054, 046, 036, 022, 061, 022, 062, 014, 054, 015, 060, 007, 052, 032, 065, 010, 043 },
{ 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 001, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 067, 067, 061, 050, 071, 026, 073, 046, 015, 041, 067, 010, 021, 006, 026, 012, 063, 012, 053 },
{ 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 001, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 026, 057, 021, 016, 062, 004, 005, 034, 074, 025, 065, 071, 063, 030, 040, 047, 031, 030, 032 },
{ 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 001, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 054, 046, 040, 054, 072, 013, 042, 010, 050, 014, 075, 051, 014, 041, 027, 001, 001, 014, 070 },
{ 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 001, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 071, 076, 073, 076, 034, 006, 044, 056, 070, 072, 027, 026, 060, 023, 074, 042, 056, 004, 020 },
{ 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 001, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 066, 074, 002, 012, 053, 075, 030, 020, 073, 075, 034, 044, 007, 073, 057, 076, 074, 071, 002 },
{ 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 001, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 071, 044, 041, 034, 072, 027, 022, 024, 040, 051, 046, 067, 075, 030, 046, 032, 021, 071, 045 },
{ 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 001, 000, 000, 000, 000, 000, 000, 000, 000, 000, 013, 065, 067, 037, 021, 005, 077, 040, 031, 054, 043, 041, 013, 030, 013, 037, 062, 045, 061 },
{ 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 001, 000, 000, 000, 000, 000, 000, 000, 000, 053, 032, 074, 007, 035, 062, 040, 036, 042, 010, 024, 031, 067, 054, 021, 001, 072, 072, 073 },
{ 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 001, 000, 000, 000, 000, 000, 000, 000, 035, 077, 057, 075, 020, 037, 011, 065, 011, 066, 026, 035, 036, 033, 031, 031, 072, 045, 042 },
{ 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 001, 000, 000, 000, 000, 000, 000, 005, 020, 040, 013, 037, 033, 061, 040, 027, 004, 034, 036, 044, 022, 004, 065, 067, 064, 022 },
{ 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 001, 000, 000, 000, 000, 000, 003, 077, 044, 074, 025, 047, 001, 027, 076, 055, 043, 045, 011, 040, 046, 041, 057, 014, 030 },
{ 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 001, 000, 000, 000, 000, 052, 004, 033, 041, 064, 037, 065, 003, 037, 071, 010, 016, 076, 023, 004, 016, 063, 017, 067 },
{ 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 001, 000, 000, 000, 015, 021, 074, 035, 020, 070, 003, 010, 062, 044, 076, 076, 034, 023, 053, 064, 022, 062, 034 },
{ 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 001, 000, 000, 054, 075, 036, 001, 051, 051, 036, 016, 074, 002, 014, 042, 013, 016, 034, 012, 022, 007, 022 },
{ 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 001, 000, 065, 023, 065, 063, 012, 060, 055, 014, 005, 003, 003, 006, 044, 004, 006, 073, 016, 076, 055 },
{ 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 001, 026, 055, 065, 012, 051, 067, 043, 012, 026, 035, 027, 015, 075, 055, 042, 067, 050, 045, 056 } };
const uint8_t ENCODE_MATRIX_441629[16U][44U] = {
{ 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 026, 035, 027, 015, 075, 055, 042, 067, 050, 045, 056, 061, 042, 051, 011, 053, 007, 024, 013, 034, 045, 060, 030, 011, 047, 014, 003, 014 },
{ 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 074, 045, 023, 071, 050, 064, 010, 016, 022, 071, 077, 020, 021, 020, 011, 012, 002, 034, 045, 060, 030, 011, 047, 014, 003, 014, 026, 004 },
{ 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 011, 047, 014, 003, 014, 026, 004, 054, 041, 002, 075, 034, 036, 024, 045, 063, 072, 007, 027, 012, 032, 077, 066, 020, 035, 071, 030, 045 },
{ 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 077, 066, 020, 035, 071, 030, 045, 023, 025, 060, 067, 030, 022, 057, 030, 071, 016, 013, 074, 020, 074, 010, 022, 016, 040, 001, 070, 013 },
{ 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 010, 022, 016, 040, 001, 070, 013, 012, 041, 045, 074, 021, 034, 045, 005, 050, 044, 071, 003, 060, 053, 024, 017, 061, 040, 020, 030, 011 },
{ 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 024, 017, 061, 040, 020, 030, 011, 076, 026, 017, 017, 035, 045, 002, 035, 037, 016, 072, 003, 044, 011, 074, 020, 073, 024, 072, 053, 064 },
{ 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 074, 020, 073, 024, 072, 053, 064, 070, 056, 063, 067, 024, 031, 046, 021, 013, 017, 015, 002, 047, 003, 024, 051, 074, 064, 002, 066, 072 },
{ 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 024, 051, 074, 064, 002, 066, 072, 071, 057, 041, 040, 025, 013, 053, 071, 033, 046, 075, 016, 041, 066, 014, 054, 075, 003, 076, 017, 064 },
{ 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 014, 054, 075, 003, 076, 017, 064, 030, 034, 020, 076, 044, 001, 071, 064, 063, 066, 024, 076, 055, 060, 071, 064, 070, 002, 011, 063, 015 },
{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 071, 064, 070, 002, 011, 063, 015, 026, 075, 043, 017, 072, 036, 017, 025, 012, 021, 070, 040, 020, 015, 021, 011, 013, 016, 074, 061, 052 },
{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 021, 011, 013, 016, 074, 061, 052, 016, 023, 013, 017, 075, 071, 007, 041, 020, 075, 010, 030, 020, 071, 053, 015, 003, 065, 013, 033, 060 },
{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 053, 015, 003, 065, 013, 033, 060, 073, 075, 055, 045, 015, 030, 044, 054, 055, 046, 040, 012, 033, 016, 063, 072, 025, 051, 071, 074, 046 },
{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 063, 072, 025, 051, 071, 074, 046, 014, 074, 027, 006, 034, 055, 017, 046, 027, 070, 061, 064, 024, 022, 011, 037, 017, 035, 022, 046, 044 },
{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 011, 037, 017, 035, 022, 046, 044, 064, 072, 064, 025, 066, 020, 037, 020, 054, 072, 012, 062, 027, 005, 035, 061, 013, 060, 027, 037, 044 },
{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 035, 061, 013, 060, 027, 037, 044, 006, 021, 005, 053, 021, 024, 056, 017, 001, 002, 004, 054, 007, 034, 075, 062, 023, 010, 041, 052, 032 },
{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 075, 062, 023, 010, 041, 052, 032, 062, 074, 022, 025, 041, 052, 032, 002, 061, 043, 014, 060, 002, 047, 075, 015, 015, 045, 066, 031, 063 } };
{ 001, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 067, 067, 061, 050, 071, 026, 073, 046, 015, 041, 067, 010, 021, 006, 026, 012, 063, 012, 053, 050, 047, 001, 011, 062, 023, 016, 010, 002 },
{ 000, 001, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 026, 057, 021, 016, 062, 004, 005, 034, 074, 025, 065, 071, 063, 030, 040, 047, 031, 030, 032, 067, 014, 026, 074, 051, 043, 062, 072, 004 },
{ 000, 000, 001, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 054, 046, 040, 054, 072, 013, 042, 010, 050, 014, 075, 051, 014, 041, 027, 001, 001, 014, 070, 042, 074, 055, 057, 077, 013, 042, 031, 042 },
{ 000, 000, 000, 001, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 071, 076, 073, 076, 034, 006, 044, 056, 070, 072, 027, 026, 060, 023, 074, 042, 056, 004, 020, 055, 035, 011, 021, 027, 062, 042, 001, 020 },
{ 000, 000, 000, 000, 001, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 066, 074, 002, 012, 053, 075, 030, 020, 073, 075, 034, 044, 007, 073, 057, 076, 074, 071, 002, 065, 001, 037, 050, 035, 031, 066, 010, 042 },
{ 000, 000, 000, 000, 000, 001, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 071, 044, 041, 034, 072, 027, 022, 024, 040, 051, 046, 067, 075, 030, 046, 032, 021, 071, 045, 027, 012, 064, 043, 020, 020, 060, 025, 001 },
{ 000, 000, 000, 000, 000, 000, 001, 000, 000, 000, 000, 000, 000, 000, 000, 000, 013, 065, 067, 037, 021, 005, 077, 040, 031, 054, 043, 041, 013, 030, 013, 037, 062, 045, 061, 053, 005, 063, 013, 063, 071, 041, 052, 023 },
{ 000, 000, 000, 000, 000, 000, 000, 001, 000, 000, 000, 000, 000, 000, 000, 000, 053, 032, 074, 007, 035, 062, 040, 036, 042, 010, 024, 031, 067, 054, 021, 001, 072, 072, 073, 006, 061, 017, 020, 067, 005, 055, 045, 003 },
{ 000, 000, 000, 000, 000, 000, 000, 000, 001, 000, 000, 000, 000, 000, 000, 000, 035, 077, 057, 075, 020, 037, 011, 065, 011, 066, 026, 035, 036, 033, 031, 031, 072, 045, 042, 051, 060, 071, 015, 040, 017, 025, 003, 057 },
{ 000, 000, 000, 000, 000, 000, 000, 000, 000, 001, 000, 000, 000, 000, 000, 000, 005, 020, 040, 013, 037, 033, 061, 040, 027, 004, 034, 036, 044, 022, 004, 065, 067, 064, 022, 062, 031, 034, 062, 040, 041, 024, 022, 044 },
{ 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 001, 000, 000, 000, 000, 000, 003, 077, 044, 074, 025, 047, 001, 027, 076, 055, 043, 045, 011, 040, 046, 041, 057, 014, 030, 043, 042, 074, 044, 051, 036, 050, 050, 017 },
{ 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 001, 000, 000, 000, 000, 052, 004, 033, 041, 064, 037, 065, 003, 037, 071, 010, 016, 076, 023, 004, 016, 063, 017, 067, 001, 010, 012, 066, 021, 064, 015, 070, 012 },
{ 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 001, 000, 000, 000, 015, 021, 074, 035, 020, 070, 003, 010, 062, 044, 076, 076, 034, 023, 053, 064, 022, 062, 034, 030, 063, 070, 006, 020, 007, 027, 054, 004 },
{ 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 001, 000, 000, 054, 075, 036, 001, 051, 051, 036, 016, 074, 002, 014, 042, 013, 016, 034, 012, 022, 007, 022, 044, 023, 022, 001, 005, 062, 006, 074, 064 },
{ 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 001, 000, 065, 023, 065, 063, 012, 060, 055, 014, 005, 003, 003, 006, 044, 004, 006, 073, 016, 076, 055, 006, 030, 064, 013, 026, 065, 077, 020, 002 },
{ 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 000, 001, 026, 055, 065, 012, 051, 067, 043, 012, 026, 035, 027, 015, 075, 055, 042, 067, 050, 045, 056, 061, 042, 051, 011, 053, 007, 024, 013, 034 } };
/**
* @brief Define a reed-solomon codec.
@ -274,6 +278,7 @@ RS6355 rs24169; // 8 bit / 4 bit corrections max / 2 bytes total
/**
* @brief Implements Reed-Solomon (63,35,29)
* Also used as mother code for shortened codes RS(52,30,23), RS(46,26,21), RS(45,26,20), RS(44,16,29)
*/
class RS6335 : public __RS_63(35) {
public:
@ -313,7 +318,7 @@ bool RS634717::decode241213(uint8_t* data)
for (uint32_t i = 0U; i < 12U; i++, offset += 6)
Utils::hex2Bin(codeword[39 + i], data, offset);
if ((ec == -1) || (ec >= 6)) {
if ((ec == -1) || (ec > 6)) {
return false;
}
@ -357,13 +362,13 @@ bool RS634717::decode24169(uint8_t* data)
int ec = rs24169.decode(codeword);
#if DEBUG_RS
LogDebugEx(LOG_HOST, "RS634717::decode24169()", "errors = %d\n", ec);
LogDebugEx(LOG_HOST, "RS634717::decode24169()", "errors = %d", ec);
#endif
offset = 0U;
for (uint32_t i = 0U; i < 16U; i++, offset += 6)
Utils::hex2Bin(codeword[39 + i], data, offset);
if ((ec == -1) || (ec >= 4)) {
if ((ec == -1) || (ec > 4)) {
return false;
}
@ -407,13 +412,13 @@ bool RS634717::decode362017(uint8_t* data)
int ec = rs634717.decode(codeword);
#if DEBUG_RS
LogDebugEx(LOG_HOST, "RS634717::decode362017()", "errors = %d\n", ec);
LogDebugEx(LOG_HOST, "RS634717::decode362017()", "errors = %d", ec);
#endif
offset = 0U;
for (uint32_t i = 0U; i < 20U; i++, offset += 6)
Utils::hex2Bin(codeword[27 + i], data, offset);
if ((ec == -1) || (ec >= 8)) {
if ((ec == -1) || (ec > 8)) {
return false;
}
@ -459,6 +464,7 @@ bool RS634717::decode523023(uint8_t* data)
#if DEBUG_RS
LogDebugEx(LOG_HOST, "RS634717::decode523023()", "errors = %d\n", ec);
#endif
offset = 0U;
for (uint32_t i = 0U; i < 30U; i++, offset += 6)
Utils::hex2Bin(codeword[11 + i], data, offset);
@ -501,9 +507,13 @@ bool RS634717::decode462621(uint8_t* data)
std::vector<uint8_t> codeword(63, 0);
// RS(46,26,21) from RS(63,35,29): S=9 shortening, U=8 puncturing
// Layout: [9 zeros | 26 data | 20 parity | 8 zeros]
uint32_t offset = 0U;
for (uint32_t i = 0U; i < 46U; i++, offset += 6)
codeword[17 + i] = Utils::bin2Hex(data, offset);
for (uint32_t i = 0U; i < 26U; i++, offset += 6)
codeword[9 + i] = Utils::bin2Hex(data, offset); // Data at positions 9-34
for (uint32_t i = 0U; i < 20U; i++, offset += 6)
codeword[35 + i] = Utils::bin2Hex(data, offset); // Parity at positions 35-54 (8 zeros at 55-62)
int ec = rs633529.decode(codeword);
#if DEBUG_RS
@ -511,9 +521,9 @@ bool RS634717::decode462621(uint8_t* data)
#endif
offset = 0U;
for (uint32_t i = 0U; i < 26U; i++, offset += 6)
Utils::hex2Bin(codeword[17 + i], data, offset);
Utils::hex2Bin(codeword[9 + i], data, offset);
if ((ec == -1) || (ec >= 10)) {
if ((ec == -1) || (ec > 10)) {
return false;
}
@ -551,9 +561,13 @@ bool RS634717::decode452620(uint8_t* data)
std::vector<uint8_t> codeword(63, 0);
// RS(45,26,20) from RS(63,35,29): S=9 shortening, U=9 puncturing
// Layout: [9 zeros | 26 data | 19 parity | 9 zeros]
uint32_t offset = 0U;
for (uint32_t i = 0U; i < 45U; i++, offset += 6)
codeword[18 + i] = Utils::bin2Hex(data, offset);
for (uint32_t i = 0U; i < 26U; i++, offset += 6)
codeword[9 + i] = Utils::bin2Hex(data, offset); // Data at positions 9-34
for (uint32_t i = 0U; i < 19U; i++, offset += 6)
codeword[35 + i] = Utils::bin2Hex(data, offset); // Parity at positions 35-53 (9 zeros at 54-62)
int ec = rs633529.decode(codeword);
#if DEBUG_RS
@ -561,9 +575,9 @@ bool RS634717::decode452620(uint8_t* data)
#endif
offset = 0U;
for (uint32_t i = 0U; i < 26U; i++, offset += 6)
Utils::hex2Bin(codeword[18 + i], data, offset);
Utils::hex2Bin(codeword[9 + i], data, offset);
if ((ec == -1) || (ec >= 9)) {
if ((ec == -1) || (ec > 9)) {
return false;
}
@ -601,9 +615,13 @@ bool RS634717::decode441629(uint8_t* data)
std::vector<uint8_t> codeword(63, 0);
// RS(44,16,29) from RS(63,35,29): S=19 shortening, U=0 puncturing (no puncturing!)
// Layout: [19 zeros | 16 data | 28 parity]
uint32_t offset = 0U;
for (uint32_t i = 0U; i < 44U; i++, offset += 6)
codeword[19 + i] = Utils::bin2Hex(data, offset);
for (uint32_t i = 0U; i < 16U; i++, offset += 6)
codeword[19 + i] = Utils::bin2Hex(data, offset); // Data at positions 19-34
for (uint32_t i = 0U; i < 28U; i++, offset += 6)
codeword[35 + i] = Utils::bin2Hex(data, offset); // Parity at positions 35-62 (no puncturing)
int ec = rs633529.decode(codeword);
#if DEBUG_RS
@ -613,7 +631,7 @@ bool RS634717::decode441629(uint8_t* data)
for (uint32_t i = 0U; i < 16U; i++, offset += 6)
Utils::hex2Bin(codeword[19 + i], data, offset);
if ((ec == -1) || (ec >= 14)) {
if ((ec == -1) || (ec > 14)) {
return false;
}

@ -53,6 +53,7 @@ AdjSiteMapLookup::AdjSiteMapLookup(const std::string& filename, uint32_t reloadT
m_rulesFile(filename),
m_reloadTime(reloadTime),
m_rules(),
m_lastLoadTime(0U),
m_stop(false),
m_adjPeerMap()
{
@ -241,6 +242,9 @@ bool AdjSiteMapLookup::load()
return false;
}
uint64_t now = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::system_clock::now().time_since_epoch()).count();
m_lastLoadTime = now;
LogInfoEx(LOG_HOST, "Loaded %lu entries into adjacent site map table", size);
return true;

@ -226,11 +226,19 @@ namespace lookups
*/
void setReloadTime(uint32_t reloadTime) { m_reloadTime = reloadTime; }
/**
* @brief Returns the last load time of this lookup table.
* @return const uint64_t Last load time in milliseconds since epoch.
*/
const uint64_t lastLoadTime() const { return m_lastLoadTime; }
private:
std::string m_rulesFile;
uint32_t m_reloadTime;
yaml::Node m_rules;
uint64_t m_lastLoadTime;
bool m_stop;
static std::mutex s_mutex; //!< Mutex used for change locking.

@ -672,9 +672,12 @@ void AffiliationLookup::clock(uint32_t ms)
for (auto entry : m_grantChTable) {
uint32_t dstId = entry.first;
m_grantTimers[dstId].clock(ms);
if (m_grantTimers[dstId].isRunning() && m_grantTimers[dstId].hasExpired()) {
gntsToRel.push_back(dstId);
auto it = m_grantTimers.find(dstId);
if (it != m_grantTimers.end()) {
it->second.clock(ms);
if (it->second.isRunning() && it->second.hasExpired()) {
gntsToRel.push_back(dstId);
}
}
}
m_grantChTable.unlock();
@ -691,9 +694,12 @@ void AffiliationLookup::clock(uint32_t ms)
m_unitRegTable.lock(false);
std::vector<uint32_t> unitsToDereg = std::vector<uint32_t>();
for (uint32_t srcId : m_unitRegTable) {
m_unitRegTimers[srcId].clock(ms);
if (m_unitRegTimers[srcId].isRunning() && m_unitRegTimers[srcId].hasExpired()) {
unitsToDereg.push_back(srcId);
auto it = m_unitRegTimers.find(srcId);
if (it != m_unitRegTimers.end()) {
it->second.clock(ms);
if (it->second.isRunning() && it->second.hasExpired()) {
unitsToDereg.push_back(srcId);
}
}
}
m_unitRegTable.unlock();

@ -156,6 +156,9 @@ bool IdenTableLookup::load()
if (size == 0U)
return false;
uint64_t now = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::system_clock::now().time_since_epoch()).count();
m_lastLoadTime = now;
LogInfoEx(LOG_HOST, "Loaded %u entries into lookup table", size);
return true;

@ -55,7 +55,8 @@ namespace lookups
m_filename(filename),
m_reloadTime(reloadTime),
m_table(),
m_stop(false)
m_stop(false),
m_lastLoadTime(0U)
{
/* stub */
}
@ -185,12 +186,20 @@ namespace lookups
*/
void setReloadTime(uint32_t reloadTime) { m_reloadTime = reloadTime; }
/**
* @brief Returns the last load time of this lookup table.
* @return const uint64_t Last load time in milliseconds since epoch.
*/
const uint64_t lastLoadTime() const { return m_lastLoadTime; }
protected:
std::string m_filename;
uint32_t m_reloadTime;
std::unordered_map<uint32_t, T> m_table;
bool m_stop;
uint64_t m_lastLoadTime;
/**
* @brief Loads the table from the passed lookup table file.
* @returns bool True, if lookup table was loaded, otherwise false.

@ -242,6 +242,21 @@ bool PeerListLookup::load()
if (parsed.size() >= 7)
hasCallPriority = ::atoi(parsed[6].c_str()) == 1;
// parse jitter buffer enabled flag
bool jitterBufferEnabled = false;
if (parsed.size() >= 8)
jitterBufferEnabled = ::atoi(parsed[7].c_str()) == 1;
// parse jitter buffer max size
uint16_t jitterBufferMaxSize = DEFAULT_JITTER_MAX_SIZE;
if (parsed.size() >= 9)
jitterBufferMaxSize = (uint16_t)::atoi(parsed[8].c_str());
// parse jitter buffer max wait time
uint32_t jitterBufferMaxWait = DEFAULT_JITTER_MAX_WAIT;
if (parsed.size() >= 10)
jitterBufferMaxWait = (uint32_t)::atoi(parsed[9].c_str());
// parse optional password
std::string password = "";
if (parsed.size() >= 2)
@ -253,17 +268,21 @@ bool PeerListLookup::load()
entry.canRequestKeys(canRequestKeys);
entry.canIssueInhibit(canIssueInhibit);
entry.hasCallPriority(hasCallPriority);
entry.jitterBufferEnabled(jitterBufferEnabled);
entry.jitterBufferMaxSize(jitterBufferMaxSize);
entry.jitterBufferMaxWait(jitterBufferMaxWait);
m_table[id] = entry;
// log depending on what was loaded
LogInfoEx(LOG_HOST, "Loaded peer ID %u%s into peer ID lookup table, %s%s%s%s", id,
LogInfoEx(LOG_HOST, "Loaded peer ID %u%s into peer ID lookup table, %s%s%s%s%s%s", id,
(!alias.empty() ? (" (" + alias + ")").c_str() : ""),
(!password.empty() ? "using unique peer password" : "using master password"),
(peerReplica) ? ", Replication Enabled" : "",
(canRequestKeys) ? ", Can Request Keys" : "",
(canIssueInhibit) ? ", Can Issue Inhibit" : "",
(hasCallPriority) ? ", Has Call Priority" : "");
(hasCallPriority) ? ", Has Call Priority" : "",
(jitterBufferEnabled) ? ", Jitter Buffer Enabled" : "");
}
}
@ -274,6 +293,9 @@ bool PeerListLookup::load()
if (size == 0U)
return false;
uint64_t now = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::system_clock::now().time_since_epoch()).count();
m_lastLoadTime = now;
LogInfoEx(LOG_HOST, "Loaded %lu entries into peer list lookup table", size);
return true;
}
@ -358,6 +380,22 @@ bool PeerListLookup::save(bool quiet)
line += "0,";
}
// add jitter buffer enabled flag
bool jitterBufferEnabled = entry.second.jitterBufferEnabled();
if (jitterBufferEnabled) {
line += "1,";
} else {
line += "0,";
}
// add jitter buffer max size
uint16_t jitterBufferMaxSize = entry.second.jitterBufferMaxSize();
line += std::to_string(jitterBufferMaxSize) + ",";
// add jitter buffer max wait time
uint32_t jitterBufferMaxWait = entry.second.jitterBufferMaxWait();
line += std::to_string(jitterBufferMaxWait);
line += "\n";
file << line;
lines++;

@ -25,6 +25,7 @@
#include "common/Defines.h"
#include "common/lookups/LookupTable.h"
#include "common/network/AdaptiveJitterBuffer.h"
#include <string>
#include <vector>
@ -53,6 +54,9 @@ namespace lookups
m_canRequestKeys(false),
m_canIssueInhibit(false),
m_hasCallPriority(false),
m_jitterBufferEnabled(false),
m_jitterBufferMaxSize(4U),
m_jitterBufferMaxWait(40000U),
m_peerDefault(false)
{
/* stub */
@ -73,6 +77,9 @@ namespace lookups
m_canRequestKeys(false),
m_canIssueInhibit(false),
m_hasCallPriority(false),
m_jitterBufferEnabled(false),
m_jitterBufferMaxSize(4U),
m_jitterBufferMaxWait(40000U),
m_peerDefault(peerDefault)
{
/* stub */
@ -92,6 +99,9 @@ namespace lookups
m_canRequestKeys = data.m_canRequestKeys;
m_canIssueInhibit = data.m_canIssueInhibit;
m_hasCallPriority = data.m_hasCallPriority;
m_jitterBufferEnabled = data.m_jitterBufferEnabled;
m_jitterBufferMaxSize = data.m_jitterBufferMaxSize;
m_jitterBufferMaxWait = data.m_jitterBufferMaxWait;
m_peerDefault = data.m_peerDefault;
}
@ -114,6 +124,29 @@ namespace lookups
m_peerDefault = peerDefault;
}
/**
* @brief Sets jitter buffer parameters.
* @param maxWait Maximum wait time in microseconds.
* @param maxSize Maximum buffer size in frames.
* @param enabled Jitter buffer enabled flag.
*/
void setJitterBuffer(uint32_t maxWait, uint16_t maxSize, bool enabled)
{
m_jitterBufferMaxWait = maxWait;
m_jitterBufferMaxSize = maxSize;
m_jitterBufferEnabled = enabled;
// clamp jitter buffer parameters
if (m_jitterBufferMaxSize < MIN_JITTER_MAX_SIZE)
m_jitterBufferMaxSize = MIN_JITTER_MAX_SIZE;
if (m_jitterBufferMaxSize > MAX_JITTER_MAX_SIZE)
m_jitterBufferMaxSize = MAX_JITTER_MAX_SIZE;
if (m_jitterBufferMaxWait < MIN_JITTER_MAX_WAIT)
m_jitterBufferMaxWait = MIN_JITTER_MAX_WAIT;
if (m_jitterBufferMaxWait > MAX_JITTER_MAX_WAIT)
m_jitterBufferMaxWait = MAX_JITTER_MAX_WAIT;
}
public:
/**
* @brief Peer ID.
@ -143,6 +176,20 @@ namespace lookups
* @brief Flag indicating if the peer has call transmit priority.
*/
DECLARE_PROPERTY_PLAIN(bool, hasCallPriority);
/**
* @brief Jitter buffer enabled flag.
*/
DECLARE_PROPERTY_PLAIN(bool, jitterBufferEnabled);
/**
* @brief Maximum buffer size in frames.
*/
DECLARE_PROPERTY_PLAIN(uint16_t, jitterBufferMaxSize);
/**
* @brief Maximum wait time in microseconds.
*/
DECLARE_PROPERTY_PLAIN(uint32_t, jitterBufferMaxWait);
/**
* @brief Flag indicating if the peer is default.
*/

@ -234,6 +234,9 @@ bool RadioIdLookup::load()
if (size == 0U)
return false;
uint64_t now = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::system_clock::now().time_since_epoch()).count();
m_lastLoadTime = now;
LogInfoEx(LOG_HOST, "Loaded %lu entries into radio ID lookup table", size);
return true;

@ -175,6 +175,17 @@ namespace lookups
* @param id Unique ID to erase.
*/
void eraseEntry(uint32_t id);
/**
* @brief Helper to return the lookup table.
* @returns std::unordered_map<uint32_t, RadioId> Table.
*/
std::unordered_map<uint32_t, RadioId> table() override
{
std::lock_guard<std::mutex> lock(s_mutex);
return m_table;
}
/**
* @brief Finds a table entry in this lookup table.
* @param id Unique identifier for table entry.

@ -54,6 +54,7 @@ TalkgroupRulesLookup::TalkgroupRulesLookup(const std::string& filename, uint32_t
m_rulesFile(filename),
m_reloadTime(reloadTime),
m_rules(),
m_lastLoadTime(0U),
m_acl(acl),
m_stop(false),
m_groupHangTime(5U),
@ -376,6 +377,9 @@ bool TalkgroupRulesLookup::load()
return false;
}
uint64_t now = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::system_clock::now().time_since_epoch()).count();
m_lastLoadTime = now;
LogInfoEx(LOG_HOST, "Loaded %lu entries into talkgroup rules table", size);
return true;

@ -636,11 +636,19 @@ namespace lookups
*/
void setReloadTime(uint32_t reloadTime) { m_reloadTime = reloadTime; }
/**
* @brief Returns the last load time of this lookup table.
* @return const uint64_t Last load time in milliseconds since epoch.
*/
const uint64_t lastLoadTime() const { return m_lastLoadTime; }
private:
std::string m_rulesFile;
uint32_t m_reloadTime;
yaml::Node m_rules;
uint64_t m_lastLoadTime;
bool m_acl;
bool m_stop;

@ -0,0 +1,279 @@
// 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 "common/Log.h"
#include "network/AdaptiveJitterBuffer.h"
using namespace network;
#include <algorithm>
#include <cassert>
// ---------------------------------------------------------------------------
// Constants
// ---------------------------------------------------------------------------
#define RTP_SEQ_MOD (1U << 16) // 65536
// ---------------------------------------------------------------------------
// Public Class Members
// ---------------------------------------------------------------------------
/* Initializes a new instance of the AdaptiveJitterBuffer class. */
AdaptiveJitterBuffer::AdaptiveJitterBuffer(uint16_t maxBufferSize, uint32_t maxWaitTime) :
m_buffer(),
m_mutex(),
m_nextExpectedSeq(0U),
m_maxBufferSize(maxBufferSize),
m_maxWaitTime(maxWaitTime),
m_totalFrames(0ULL),
m_reorderedFrames(0ULL),
m_droppedFrames(0ULL),
m_timedOutFrames(0ULL),
m_initialized(false)
{
assert(maxBufferSize > 0U);
assert(maxWaitTime > 0U);
}
/* Finalizes a instance of the AdaptiveJitterBuffer class. */
AdaptiveJitterBuffer::~AdaptiveJitterBuffer()
{
std::lock_guard<std::mutex> lock(m_mutex);
// clean up any buffered frames
for (auto& pair : m_buffer) {
if (pair.second != nullptr) {
delete pair.second;
}
}
m_buffer.clear();
}
/* Processes an incoming RTP frame. */
bool AdaptiveJitterBuffer::processFrame(uint16_t seq, const uint8_t* data, uint32_t length,
std::vector<BufferedFrame*>& readyFrames)
{
if (data == nullptr || length == 0U) {
return false;
}
std::lock_guard<std::mutex> lock(m_mutex);
m_totalFrames++;
// initialize on first frame
if (!m_initialized) {
m_nextExpectedSeq = seq;
m_initialized = true;
}
// zero-latency fast path: in-order packet
if (seq == m_nextExpectedSeq) {
// create frame and add to ready list
BufferedFrame* frame = new BufferedFrame(seq, data, length);
readyFrames.push_back(frame);
// advance expected sequence
m_nextExpectedSeq = (m_nextExpectedSeq + 1) & 0xFFFF;
// flush any subsequent sequential frames from buffer
flushSequentialFrames(readyFrames);
return true;
}
int32_t diff = seqDiff(seq, m_nextExpectedSeq);
// frame is in the past (duplicate or very late)
if (diff < 0) {
// check if it's severely out of order (> 1000 packets behind)
if (diff < -1000) {
// likely a sequence wraparound with new stream - reset
m_nextExpectedSeq = seq;
// cleanup any buffered frames, delete and clear list
for (auto& pair : m_buffer) {
if (pair.second != nullptr) {
delete pair.second;
}
}
m_buffer.clear();
BufferedFrame* frame = new BufferedFrame(seq, data, length);
readyFrames.push_back(frame);
m_nextExpectedSeq = (m_nextExpectedSeq + 1) & 0xFFFF;
return true;
}
// drop duplicate/late frame
m_droppedFrames++;
return false;
}
// frame is in the future - buffer it
m_reorderedFrames++;
// check buffer capacity
if (m_buffer.size() >= m_maxBufferSize) {
// buffer is full - drop oldest frame to make room
auto oldestIt = m_buffer.begin();
delete oldestIt->second;
m_buffer.erase(oldestIt);
m_droppedFrames++;
}
// add frame to buffer
BufferedFrame* frame = new BufferedFrame(seq, data, length);
m_buffer[seq] = frame;
// check if we now have the next expected frame
flushSequentialFrames(readyFrames);
return true;
}
/* Checks for timed-out buffered frames and forces their delivery. */
void AdaptiveJitterBuffer::checkTimeouts(std::vector<BufferedFrame*>& timedOutFrames,
uint64_t currentTime)
{
std::lock_guard<std::mutex> lock(m_mutex);
if (m_buffer.empty()) {
return;
}
// get current time if not provided
if (currentTime == 0ULL) {
currentTime = std::chrono::duration_cast<std::chrono::microseconds>(
std::chrono::steady_clock::now().time_since_epoch()).count();
}
// find frames that have exceeded the wait time
std::vector<uint16_t> toRemove;
for (auto& pair : m_buffer) {
BufferedFrame* frame = pair.second;
if (frame != nullptr) {
uint64_t age = currentTime - frame->timestamp;
if (age >= m_maxWaitTime) {
toRemove.push_back(pair.first);
}
}
}
// remove and deliver timed-out frames in sequence order
if (!toRemove.empty()) {
// sort by sequence number
std::sort(toRemove.begin(), toRemove.end(), [this](uint16_t a, uint16_t b) {
return seqDiff(a, b) < 0;
});
for (uint16_t seq : toRemove) {
auto it = m_buffer.find(seq);
if (it != m_buffer.end() && it->second != nullptr) {
timedOutFrames.push_back(it->second);
m_buffer.erase(it);
m_timedOutFrames++;
// update next expected sequence to skip the gap
int32_t diff = seqDiff(seq, m_nextExpectedSeq);
if (diff >= 0) {
m_nextExpectedSeq = (seq + 1) & 0xFFFF;
// try to flush any sequential frames after this one
flushSequentialFrames(timedOutFrames);
}
}
}
}
}
/* Resets the jitter buffer state. */
void AdaptiveJitterBuffer::reset(bool clearStats)
{
std::lock_guard<std::mutex> lock(m_mutex);
// clean up buffered frames
for (auto& pair : m_buffer) {
if (pair.second != nullptr) {
delete pair.second;
}
}
m_buffer.clear();
m_initialized = false;
m_nextExpectedSeq = 0U;
if (clearStats) {
m_totalFrames = 0ULL;
m_reorderedFrames = 0ULL;
m_droppedFrames = 0ULL;
m_timedOutFrames = 0ULL;
}
}
/* Gets statistics about jitter buffer performance. */
void AdaptiveJitterBuffer::getStatistics(uint64_t& totalFrames, uint64_t& reorderedFrames,
uint64_t& droppedFrames, uint64_t& timedOutFrames) const
{
std::lock_guard<std::mutex> lock(m_mutex);
totalFrames = m_totalFrames;
reorderedFrames = m_reorderedFrames;
droppedFrames = m_droppedFrames;
timedOutFrames = m_timedOutFrames;
}
// ---------------------------------------------------------------------------
// Private Class Members
// ---------------------------------------------------------------------------
/* Delivers all sequential frames from the buffer. */
void AdaptiveJitterBuffer::flushSequentialFrames(std::vector<BufferedFrame*>& readyFrames)
{
while (!m_buffer.empty()) {
auto it = m_buffer.find(m_nextExpectedSeq);
if (it == m_buffer.end()) {
// gap in sequence - stop flushing
break;
}
// found next sequential frame
BufferedFrame* frame = it->second;
readyFrames.push_back(frame);
m_buffer.erase(it);
// advance to next expected sequence
m_nextExpectedSeq = (m_nextExpectedSeq + 1) & 0xFFFF;
}
}
/* Calculates sequence number difference handling wraparound. */
int32_t AdaptiveJitterBuffer::seqDiff(uint16_t seq1, uint16_t seq2) const
{
// handle RTP sequence number wraparound (RFC 3550)
int32_t diff = (int32_t)seq1 - (int32_t)seq2;
// adjust for wraparound
if (diff > (int32_t)(RTP_SEQ_MOD / 2)) {
diff -= (int32_t)RTP_SEQ_MOD;
} else if (diff < -(int32_t)(RTP_SEQ_MOD / 2)) {
diff += (int32_t)RTP_SEQ_MOD;
}
return diff;
}

@ -0,0 +1,220 @@
// 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 AdaptiveJitterBuffer.h
* @ingroup network_core
* @file AdaptiveJitterBuffer.cpp
* @ingroup network_core
*/
#if !defined(__ADAPTIVE_JITTER_BUFFER_H__)
#define __ADAPTIVE_JITTER_BUFFER_H__
#include "common/Defines.h"
#include <cstdint>
#include <cstring>
#include <map>
#include <vector>
#include <mutex>
#include <chrono>
namespace network
{
// ---------------------------------------------------------------------------
// Constants
// ---------------------------------------------------------------------------
#define DEFAULT_JITTER_MAX_SIZE 4U
#define DEFAULT_JITTER_MAX_WAIT 40000U
#define MIN_JITTER_MAX_SIZE 2U
#define MAX_JITTER_MAX_SIZE 8U
#define MIN_JITTER_MAX_WAIT 10000U
#define MAX_JITTER_MAX_WAIT 200000U
// ---------------------------------------------------------------------------
// Structure Declaration
// ---------------------------------------------------------------------------
/**
* @brief Represents a buffered frame in the jitter buffer.
* @ingroup network_core
*/
struct BufferedFrame {
uint16_t seq; //<! RTP sequence number
uint8_t* data; //<! Frame data
uint32_t length; //<! Frame length
uint64_t timestamp; //<! Reception timestamp (microseconds)
/**
* @brief Initializes a new instance of the BufferedFrame struct.
*/
BufferedFrame() : seq(0U), data(nullptr), length(0U), timestamp(0ULL) { /* stub */ }
/**
* @brief Initializes a new instance of the BufferedFrame struct.
* @param sequence RTP sequence number.
* @param buffer Frame data buffer.
* @param len Frame length.
*/
BufferedFrame(uint16_t sequence, const uint8_t* buffer, uint32_t len) :
seq(sequence),
data(nullptr),
length(len),
timestamp(std::chrono::duration_cast<std::chrono::microseconds>(
std::chrono::steady_clock::now().time_since_epoch()).count())
{
if (len > 0U && buffer != nullptr) {
data = new uint8_t[len];
::memcpy(data, buffer, len);
}
}
/**
* @brief Finalizes a instance of the BufferedFrame struct.
*/
~BufferedFrame()
{
if (data != nullptr) {
delete[] data;
data = nullptr;
}
}
};
// ---------------------------------------------------------------------------
// Class Declaration
// ---------------------------------------------------------------------------
/**
* @brief Implements an adaptive jitter buffer for RTP streams.
* @ingroup network_core
*
* This class provides minimal-latency jitter buffering with a zero-latency
* fast path for in-order packets. Out-of-order packets are buffered briefly
* to allow reordering, with adaptive timeout based on observed jitter.
*/
class HOST_SW_API AdaptiveJitterBuffer {
public:
/**
* @brief Initializes a new instance of the AdaptiveJitterBuffer class.
* @param maxBufferSize Maximum number of frames to buffer (default: 4).
* @param maxWaitTime Maximum time to wait for out-of-order frames in microseconds (default: 40000 = 40ms).
*/
AdaptiveJitterBuffer(uint16_t maxBufferSize = 4U, uint32_t maxWaitTime = 40000U);
/**
* @brief Finalizes a instance of the AdaptiveJitterBuffer class.
*/
~AdaptiveJitterBuffer();
/**
* @brief Processes an incoming RTP frame.
* @param seq RTP sequence number.
* @param data Frame data.
* @param length Frame length.
* @param[out] readyFrames Vector of frames ready for delivery (in sequence order).
* @returns bool True if frame was processed successfully, otherwise false.
*
* This method implements a zero-latency fast path for in-order packets.
* Out-of-order packets are buffered and returned when they become sequential.
*/
bool processFrame(uint16_t seq, const uint8_t* data, uint32_t length,
std::vector<BufferedFrame*>& readyFrames);
/**
* @brief Checks for timed-out buffered frames and forces their delivery.
* @param[out] timedOutFrames Vector of frames that have exceeded the wait time.
* @param currentTime Current time in microseconds (0 = use system clock).
*
* This should be called periodically (e.g., every 10-20ms) to ensure
* buffered frames are delivered even if missing packets never arrive.
*/
void checkTimeouts(std::vector<BufferedFrame*>& timedOutFrames,
uint64_t currentTime = 0ULL);
/**
* @brief Resets the jitter buffer state.
* @param clearStats If true, also resets statistics (default: false).
*
* This should be called when a stream ends or restarts.
*/
void reset(bool clearStats = false);
/**
* @brief Gets the current buffer occupancy.
* @returns size_t Number of frames currently buffered.
*/
size_t getBufferSize() const { return m_buffer.size(); }
/**
* @brief Gets the next expected sequence number.
* @returns uint16_t Next expected sequence number.
*/
uint16_t getNextExpectedSeq() const { return m_nextExpectedSeq; }
/**
* @brief Gets statistics about jitter buffer performance.
* @param[out] totalFrames Total frames processed.
* @param[out] reorderedFrames Frames that were out-of-order but successfully reordered.
* @param[out] droppedFrames Frames dropped due to buffer overflow or severe reordering.
* @param[out] timedOutFrames Frames delivered due to timeout (missing packets).
*/
void getStatistics(uint64_t& totalFrames, uint64_t& reorderedFrames,
uint64_t& droppedFrames, uint64_t& timedOutFrames) const;
/**
* @brief Sets the maximum buffer size.
* @param maxBufferSize Maximum number of frames to buffer.
*/
void setMaxBufferSize(uint16_t maxBufferSize) { m_maxBufferSize = maxBufferSize; }
/**
* @brief Sets the maximum wait time for out-of-order frames.
* @param maxWaitTime Maximum wait time in microseconds.
*/
void setMaxWaitTime(uint32_t maxWaitTime) { m_maxWaitTime = maxWaitTime; }
private:
std::map<uint16_t, BufferedFrame*> m_buffer;
mutable std::mutex m_mutex;
uint16_t m_nextExpectedSeq;
uint16_t m_maxBufferSize;
uint32_t m_maxWaitTime;
uint64_t m_totalFrames;
uint64_t m_reorderedFrames;
uint64_t m_droppedFrames;
uint64_t m_timedOutFrames;
bool m_initialized;
/**
* @brief Delivers all sequential frames from the buffer.
* @param[out] readyFrames Vector to append ready frames to.
*
* Internal helper that flushes all frames starting from m_nextExpectedSeq
* until a gap is encountered.
*/
void flushSequentialFrames(std::vector<BufferedFrame*>& readyFrames);
/**
* @brief Calculates sequence number difference handling wraparound.
* @param seq1 First sequence number.
* @param seq2 Second sequence number.
* @returns int32_t Signed difference (seq1 - seq2).
*/
int32_t seqDiff(uint16_t seq1, uint16_t seq2) const;
};
} // namespace network
#endif // __ADAPTIVE_JITTER_BUFFER_H__

@ -5,7 +5,7 @@
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* Copyright (C) 2015,2016,2017 Jonathan Naylor, G4KLX
* Copyright (C) 2020-2025 Bryan Biedenkapp, N2PLL
* Copyright (C) 2020-2026 Bryan Biedenkapp, N2PLL
* Copyright (C) 2024 Caleb, KO4UYJ
*
*/
@ -45,14 +45,15 @@ BaseNetwork::BaseNetwork(uint32_t peerId, bool duplex, bool debug, bool slot1, b
m_slot1(slot1),
m_slot2(slot2),
m_duplex(duplex),
m_useAlternatePortForDiagnostics(false),
m_allowActivityTransfer(allowActivityTransfer),
m_allowDiagnosticTransfer(allowDiagnosticTransfer),
m_packetDump(false),
m_debug(debug),
m_socket(nullptr),
m_frameQueue(nullptr),
m_rxDMRData(NET_RING_BUF_SIZE, "DMR Net Buffer"),
m_rxP25Data(NET_RING_BUF_SIZE, "P25 Net Buffer"),
m_rxP25P2Data(NET_RING_BUF_SIZE, "P25 Phase 2 Net Buffer"),
m_rxNXDNData(NET_RING_BUF_SIZE, "NXDN Net Buffer"),
m_rxAnalogData(NET_RING_BUF_SIZE, "Analog Net Buffer"),
m_random(),
@ -66,7 +67,7 @@ BaseNetwork::BaseNetwork(uint32_t peerId, bool duplex, bool debug, bool slot1, b
assert(peerId < 999999999U);
m_socket = new udp::Socket(localPort);
m_frameQueue = new FrameQueue(m_socket, peerId, debug);
m_frameQueue = new FrameQueue(m_socket, peerId, false);
std::random_device rd;
std::mt19937 mt(rd());
@ -76,6 +77,9 @@ BaseNetwork::BaseNetwork(uint32_t peerId, bool duplex, bool debug, bool slot1, b
m_dmrStreamId[0U] = createStreamId();
m_dmrStreamId[1U] = createStreamId();
m_p25StreamId = createStreamId();
m_p25P2StreamId = new uint32_t[2U];
m_p25P2StreamId[0U] = createStreamId();
m_p25P2StreamId[1U] = createStreamId();
m_nxdnStreamId = createStreamId();
m_analogStreamId = createStreamId();
}
@ -93,6 +97,7 @@ BaseNetwork::~BaseNetwork()
}
delete[] m_dmrStreamId;
delete[] m_p25P2StreamId;
}
/* Writes grant request to the network. */
@ -173,7 +178,7 @@ bool BaseNetwork::writeActLog(const char* message)
#endif
return writeMaster({ NET_FUNC::TRANSFER, NET_SUBFUNC::TRANSFER_SUBFUNC_ACTIVITY }, (uint8_t*)buffer, (uint32_t)len + 11U,
RTP_END_OF_CALL_SEQ, 0U, m_useAlternatePortForDiagnostics);
RTP_END_OF_CALL_SEQ, 0U, true);
}
/* Writes the local diagnostics log to the network. */
@ -201,7 +206,7 @@ bool BaseNetwork::writeDiagLog(const char* message)
#endif
return writeMaster({ NET_FUNC::TRANSFER, NET_SUBFUNC::TRANSFER_SUBFUNC_DIAG }, (uint8_t*)buffer, (uint32_t)len + 11U,
RTP_END_OF_CALL_SEQ, 0U, m_useAlternatePortForDiagnostics);
RTP_END_OF_CALL_SEQ, 0U, true);
}
/* Writes the local status to the network. */
@ -213,9 +218,6 @@ bool BaseNetwork::writePeerStatus(json::object obj)
if (!m_allowActivityTransfer)
return false;
if (!m_useAlternatePortForDiagnostics)
return false; // this is intentional -- peer status is a noisy message and it shouldn't be done
// when the FNE is configured for main port transfers
json::value v = json::value(obj);
std::string json = std::string(v.serialize());
@ -233,7 +235,7 @@ bool BaseNetwork::writePeerStatus(json::object obj)
#endif
return writeMaster({ NET_FUNC::TRANSFER, NET_SUBFUNC::TRANSFER_SUBFUNC_STATUS }, (uint8_t*)buffer, (uint32_t)len + 11U,
RTP_END_OF_CALL_SEQ, 0U, m_useAlternatePortForDiagnostics);
RTP_END_OF_CALL_SEQ, 0U, true);
}
/* Writes a group affiliation to the network. */
@ -349,6 +351,10 @@ void BaseNetwork::resetDMR(uint32_t slotNo)
m_dmrStreamId[1U] = createStreamId();
}
if (m_debug)
LogDebugEx(LOG_NET, "BaseNetwork::resetDMR()", "reset DMR Slot %u stream ID, streamId = %u", slotNo,
(slotNo == 1U) ? m_dmrStreamId[0U] : m_dmrStreamId[1U]);
m_pktSeq = 0U;
m_rxDMRData.clear();
}
@ -358,15 +364,44 @@ void BaseNetwork::resetDMR(uint32_t slotNo)
void BaseNetwork::resetP25()
{
m_p25StreamId = createStreamId();
if (m_debug)
LogDebugEx(LOG_NET, "BaseNetwork::resetP25()", "reset P25 stream ID, streamId = %u", m_p25StreamId);
m_pktSeq = 0U;
m_rxP25Data.clear();
}
/* Resets the P25 Phase 2 ring buffer for the given slot. */
void BaseNetwork::resetP25P2(uint32_t slotNo)
{
assert(slotNo == 1U || slotNo == 2U);
if (slotNo == 1U) {
m_p25P2StreamId[0U] = createStreamId();
}
else {
m_p25P2StreamId[1U] = createStreamId();
}
if (m_debug)
LogDebugEx(LOG_NET, "BaseNetwork::resetP25P2()", "reset P25 Phase 2 Slot %u stream ID, streamId = %u", slotNo,
(slotNo == 1U) ? m_p25P2StreamId[0U] : m_p25P2StreamId[1U]);
m_pktSeq = 0U;
m_rxP25P2Data.clear();
}
/* Resets the NXDN ring buffer. */
void BaseNetwork::resetNXDN()
{
m_nxdnStreamId = createStreamId();
if (m_debug)
LogDebugEx(LOG_NET, "BaseNetwork::resetNXDN()", "reset NXDN stream ID, streamId = %u", m_nxdnStreamId);
m_pktSeq = 0U;
m_rxNXDNData.clear();
}
@ -376,6 +411,10 @@ void BaseNetwork::resetNXDN()
void BaseNetwork::resetAnalog()
{
m_analogStreamId = createStreamId();
if (m_debug)
LogDebugEx(LOG_NET, "BaseNetwork::resetAnalog()", "reset analog stream ID, streamId = %u", m_analogStreamId);
m_pktSeq = 0U;
m_rxAnalogData.clear();
}
@ -394,17 +433,31 @@ uint32_t BaseNetwork::getDMRStreamId(uint32_t slotNo) const
}
}
/* Gets the current P25 Phase 2 stream ID. */
uint32_t BaseNetwork::getP25P2StreamId(uint32_t slotNo) const
{
assert(slotNo == 1U || slotNo == 2U);
if (slotNo == 1U) {
return m_p25P2StreamId[0U];
}
else {
return m_p25P2StreamId[1U];
}
}
/* Helper to send a data message to the master. */
bool BaseNetwork::writeMaster(FrameQueue::OpcodePair opcode, const uint8_t* data, uint32_t length, uint16_t pktSeq, uint32_t streamId,
bool useAlternatePort, uint32_t peerId, uint32_t ssrc)
bool metadata, uint32_t peerId, uint32_t ssrc)
{
if (peerId == 0U)
peerId = m_peerId;
if (ssrc == 0U)
ssrc = m_peerId;
if (useAlternatePort) {
if (metadata) {
sockaddr_storage addr;
uint32_t addrLen;
@ -678,6 +731,58 @@ bool BaseNetwork::writeP25PDU(const p25::data::DataHeader& header, const uint8_t
return writeMaster({ NET_FUNC::PROTOCOL, NET_SUBFUNC::PROTOCOL_SUBFUNC_P25 }, message.get(), messageLength, seq, m_p25StreamId);
}
/* Reads P25 raw frame data from the P25 ring buffer. */
UInt8Array BaseNetwork::readP25P2(bool& ret, uint32_t& frameLength)
{
if (m_status != NET_STAT_RUNNING && m_status != NET_STAT_MST_RUNNING)
return nullptr;
ret = true;
if (m_rxP25P2Data.isEmpty()) {
ret = false;
return nullptr;
}
uint8_t length = 0U;
m_rxP25P2Data.get(&length, 1U);
if (length == 0U) {
ret = false;
return nullptr;
}
UInt8Array buffer;
frameLength = length;
buffer = std::unique_ptr<uint8_t[]>(new uint8_t[length]);
::memset(buffer.get(), 0x00U, length);
m_rxP25P2Data.get(buffer.get(), length);
return buffer;
}
/* Writes P25 Phase 2 frame data to the network. */
bool BaseNetwork::writeP25P2(const p25::lc::LC& control, p25::defines::P2_DUID::E duid, uint8_t slot, const uint8_t* data,
const uint8_t controlByte)
{
if (m_status != NET_STAT_RUNNING && m_status != NET_STAT_MST_RUNNING)
return false;
bool resetSeq = false;
if (m_p25P2StreamId[slot] == 0U) {
resetSeq = true;
m_p25P2StreamId[slot] = createStreamId();
}
uint32_t messageLength = 0U;
UInt8Array message = createP25P2_Message(messageLength, control, duid, slot, data, controlByte);
if (message == nullptr) {
return false;
}
return writeMaster({ NET_FUNC::PROTOCOL, NET_SUBFUNC::PROTOCOL_SUBFUNC_P25_P2 }, message.get(), messageLength, pktSeq(resetSeq), m_p25P2StreamId[slot]);
}
/* Helper to test if the P25 ring buffer has data. */
bool BaseNetwork::hasP25Data() const
@ -688,6 +793,16 @@ bool BaseNetwork::hasP25Data() const
return true;
}
/* Helper to test if the P25 Phase 2 ring buffer has data. */
bool BaseNetwork::hasP25P2Data() const
{
if (m_rxP25P2Data.isEmpty())
return false;
return true;
}
/* Helper to validate a P25 network frame length. */
bool BaseNetwork::validateP25FrameLength(uint8_t& frameLength, uint32_t len, const P25DEF::DUID::E duid)
@ -875,25 +990,15 @@ UInt8Array BaseNetwork::readAnalog(bool& ret, uint32_t& frameLength)
return nullptr;
}
uint8_t length = 0U;
m_rxAnalogData.get(&length, 1U);
if (length == 0U) {
ret = false;
return nullptr;
}
uint8_t lenOffs = 0U;
m_rxAnalogData.get(&lenOffs, 1U);
if (length < 254U) {
// if the length is less than 254, the analog packet is malformed, analog packets should never be less than 254 bytes
LogError(LOG_NET, "malformed analog packet, length < 254 (%u), shouldn't happen", length);
uint16_t length = 254U + lenOffs;
if (length == 254U) {
ret = false;
return nullptr;
}
if (length == 254U) {
m_rxAnalogData.get(&length, 1U); // read the next byte for the actual length
length += 254U; // a packet length of 254 is a special case for P25 frames, so we need to add the 254 to the length
}
UInt8Array buffer;
frameLength = length;
buffer = std::unique_ptr<uint8_t[]>(new uint8_t[length]);
@ -1023,7 +1128,7 @@ UInt8Array BaseNetwork::createDMR_Message(uint32_t& length, const uint32_t strea
// pack raw DMR message bytes
data.getData(buffer + 20U);
if (m_debug)
if (m_packetDump)
Utils::dump(1U, "BaseNetwork::createDMR_Message(), Message", buffer, (DMR_PACKET_LENGTH + PACKET_PAD));
length = (DMR_PACKET_LENGTH + PACKET_PAD);
@ -1080,7 +1185,7 @@ void BaseNetwork::createP25_MessageHdr(uint8_t* buffer, p25::defines::DUID::E du
::memset(mi, 0x00U, MI_LENGTH_BYTES);
control.getMI(mi);
if (m_debug) {
if (m_packetDump) {
Utils::dump(1U, "BaseNetwork::createP25_Message(), HDU MI", mi, MI_LENGTH_BYTES);
}
@ -1158,7 +1263,7 @@ UInt8Array BaseNetwork::createP25_LDU1Message(uint32_t& length, const p25::lc::L
buffer[23U] = count;
if (m_debug)
if (m_packetDump)
Utils::dump(1U, "BaseNetwork::createP25_LDU1Message(), Message, LDU1", buffer, (P25_LDU1_PACKET_LENGTH + PACKET_PAD));
length = (P25_LDU1_PACKET_LENGTH + PACKET_PAD);
@ -1233,7 +1338,7 @@ UInt8Array BaseNetwork::createP25_LDU2Message(uint32_t& length, const p25::lc::L
buffer[23U] = count;
if (m_debug)
if (m_packetDump)
Utils::dump(1U, "BaseNetwork::createP25_LDU2Message(), Message, LDU2", buffer, (P25_LDU2_PACKET_LENGTH + PACKET_PAD));
length = (P25_LDU2_PACKET_LENGTH + PACKET_PAD);
@ -1254,7 +1359,7 @@ UInt8Array BaseNetwork::createP25_TDUMessage(uint32_t& length, const p25::lc::LC
buffer[14U] = controlByte;
buffer[23U] = MSG_HDR_SIZE;
if (m_debug)
if (m_packetDump)
Utils::dump(1U, "BaseNetwork::createP25_TDUMessage(), Message, TDU", buffer, (MSG_HDR_SIZE + PACKET_PAD));
length = (MSG_HDR_SIZE + PACKET_PAD);
@ -1280,7 +1385,7 @@ UInt8Array BaseNetwork::createP25_TSDUMessage(uint32_t& length, const p25::lc::L
buffer[23U] = P25_TSDU_FRAME_LENGTH_BYTES;
if (m_debug)
if (m_packetDump)
Utils::dump(1U, "BaseNetwork::createP25_TSDUMessage(), Message, TDSU", buffer, (P25_TSDU_PACKET_LENGTH + PACKET_PAD));
length = (P25_TSDU_PACKET_LENGTH + PACKET_PAD);
@ -1306,7 +1411,7 @@ UInt8Array BaseNetwork::createP25_TDULCMessage(uint32_t& length, const p25::lc::
buffer[23U] = P25_TDULC_FRAME_LENGTH_BYTES;
if (m_debug)
if (m_packetDump)
Utils::dump(1U, "BaseNetwork::createP25_TDULCMessage(), Message, TDULC", buffer, (P25_TDULC_PACKET_LENGTH + PACKET_PAD));
length = (P25_TDULC_PACKET_LENGTH + PACKET_PAD);
@ -1354,13 +1459,48 @@ UInt8Array BaseNetwork::createP25_PDUMessage(uint32_t& length, const p25::data::
buffer[23U] = count;
if (m_debug)
if (m_packetDump)
Utils::dump(1U, "BaseNetwork::createP25_PDUMessage(), Message, PDU", buffer, (count + PACKET_PAD));
length = (count + PACKET_PAD);
return UInt8Array(buffer);
}
/* Creates an P25 Phase 2 frame message. */
UInt8Array BaseNetwork::createP25P2_Message(uint32_t& length, const p25::lc::LC& control, p25::defines::P2_DUID::E duid,
const bool slot, const uint8_t* data, uint8_t controlByte)
{
using namespace p25::defines;
uint8_t* buffer = new uint8_t[DATA_PACKET_LENGTH];
::memset(buffer, 0x00U, DATA_PACKET_LENGTH);
// create dummy low speed data
p25::data::LowSpeedData lsd = p25::data::LowSpeedData();
// construct P25 message header
createP25_MessageHdr(buffer, DUID::PDU, control, lsd, FrameType::DATA_UNIT);
buffer[14U] = controlByte;
buffer[19U] = slot ? 0x00U : 0x80U; // Slot Number
buffer[19U] |= (uint8_t)duid; // Phase 2 DUID
// pack raw P25 Phase 2 bytes
uint32_t count = MSG_HDR_SIZE;
::memcpy(buffer + 24U, data, P25_P2_FRAME_LENGTH_BYTES);
count += P25_P2_FRAME_LENGTH_BYTES;
buffer[23U] = count;
if (m_packetDump)
Utils::dump(1U, "BaseNetwork::createP25P2_Message(), Message, Phase 2", buffer, (count + PACKET_PAD));
length = (count + PACKET_PAD);
return UInt8Array(buffer);
}
/* Writes NXDN frame data to the network. */
UInt8Array BaseNetwork::createNXDN_Message(uint32_t& length, const nxdn::lc::RTCH& lc, const uint8_t* data, const uint32_t len)
@ -1393,7 +1533,7 @@ UInt8Array BaseNetwork::createNXDN_Message(uint32_t& length, const nxdn::lc::RTC
buffer[23U] = count;
if (m_debug)
if (m_packetDump)
Utils::dump(1U, "BaseNetwork::createNXDN_Message(), Message", buffer, (NXDN_PACKET_LENGTH + PACKET_PAD));
length = (NXDN_PACKET_LENGTH + PACKET_PAD);
@ -1428,7 +1568,7 @@ UInt8Array BaseNetwork::createAnalog_Message(uint32_t& length, const uint32_t st
// pack raw audio message bytes
data.getAudio(buffer + 20U);
if (m_debug)
if (m_packetDump)
Utils::dump(1U, "BaseNetwork::createAnalog_Message(), Message", buffer, (ANALOG_PACKET_LENGTH + PACKET_PAD));
length = (ANALOG_PACKET_LENGTH + PACKET_PAD);

@ -5,7 +5,7 @@
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* Copyright (C) 2015,2016,2017,2018 Jonathan Naylor, G4KLX
* Copyright (C) 2020-2025 Bryan Biedenkapp, N2PLL
* Copyright (C) 2020-2026 Bryan Biedenkapp, N2PLL
*
*/
/**
@ -92,8 +92,9 @@ namespace network
const uint32_t P25_LDU2_PACKET_LENGTH = 181U; // 24 byte header + DFSI data + 1 byte frame type
const uint32_t P25_TSDU_PACKET_LENGTH = 69U; // 24 byte header + TSDU data
const uint32_t P25_TDULC_PACKET_LENGTH = 78U; // 24 byte header + TDULC data
const uint32_t P25_P2_PACKET_LENGTH = 66U; // 24 byte header + P25_P2_FRAME_LENGTH_BYTES + 2 byte trailer
const uint32_t NXDN_PACKET_LENGTH = 70U; // 20 byte header + NXDN_FRAME_LENGTH_BYTES + 2 byte trailer
const uint32_t ANALOG_PACKET_LENGTH = 324U; // 20 byte header + AUDIO_SAMPLES_LENGTH_BYTES + 4 byte trailer
const uint32_t ANALOG_PACKET_LENGTH = 344U; // 20 byte header + AUDIO_SAMPLES_LENGTH_BYTES + 4 byte trailer
const uint32_t HA_PARAMS_ENTRY_LEN = 20U;
@ -376,8 +377,8 @@ namespace network
* @param peerId Unique ID of this modem on the network.
* @param duplex Flag indicating full-duplex operation.
* @param debug Flag indicating whether network debug 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 slot1 Flag indicating whether DMR/P25 Phase 2 slot 1 is enabled for network traffic.
* @param slot2 Flag indicating whether DMR/P25 Phase 2 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 localPort Local port used to listen for incoming data.
@ -394,6 +395,17 @@ namespace network
*/
FrameQueue* getFrameQueue() const { return m_frameQueue; }
/**
* @brief Helper to enable or disable packet dump logging.
* @param enable Flag indicating whether packet dump logging is enabled.
*/
void setPacketDump(bool enable)
{
m_packetDump = enable;
if (m_frameQueue != nullptr)
m_frameQueue->setDebug(enable);
}
/**
* @brief Writes a grant request to the network.
* \code{.unparsed}
@ -696,6 +708,11 @@ namespace network
* @brief Resets the P25 ring buffer.
*/
virtual void resetP25();
/**
* @brief Resets the P25 Phase 2 ring buffer for the given slot.
* @param slotNo P25 Phase 2 slot number.
*/
virtual void resetP25P2(uint32_t slotNo);
/**
* @brief Resets the NXDN ring buffer.
*/
@ -716,6 +733,12 @@ namespace network
* @return uint32_t Stream ID.
*/
uint32_t getP25StreamId() const { return m_p25StreamId; }
/**
* @brief Gets the current P25 Phase 2 stream ID.
* @param slotNo P25 Phase 2 slot to get stream ID for.
* @return uint32_t Stream ID for the given P25 Phase 2 slot.
*/
uint32_t getP25P2StreamId(uint32_t slotNo) const;
/**
* @brief Gets the current NXDN stream ID.
* @return uint32_t Stream ID.
@ -734,13 +757,13 @@ namespace network
* @param length Length of buffer to write.
* @param pktSeq RTP packet sequence.
* @param streamId Stream ID.
* @param useAlternatePort Flag indicating the message shuold be sent using the alternate port (mainly for activity and diagnostics).
* @param metadata Flag indicating the message should be sent to the metadata port.
* @param peerId If non-zero, overrides the peer ID sent in the packet to the master.
* @param ssrc If non-zero, overrides the RTP synchronization source ID sent in the packet to the master.
* @returns bool True, if message was sent, otherwise false.
*/
bool writeMaster(FrameQueue::OpcodePair opcode, const uint8_t* data, uint32_t length,
uint16_t pktSeq, uint32_t streamId, bool useAlternatePort = false, uint32_t peerId = 0U, uint32_t ssrc = 0U);
uint16_t pktSeq, uint32_t streamId, bool metadata = false, uint32_t peerId = 0U, uint32_t ssrc = 0U);
// Digital Mobile Radio
/**
@ -827,12 +850,37 @@ namespace network
virtual bool writeP25PDU(const p25::data::DataHeader& header, const uint8_t currentBlock, const uint8_t* data,
const uint32_t len, bool lastBlock);
/**
* @brief Reads P25 Phase 2 raw frame data from the P25 Phase 2 ring buffer.
* @param[out] ret Flag indicating whether or not data was received.
* @param[out] frameLength Length in bytes of received frame.
* @returns UInt8Array Buffer containing received frame.
*/
virtual UInt8Array readP25P2(bool& ret, uint32_t& frameLength);
/**
* @brief Writes P25 Phase 2 frame data to the network.
* @param[in] control Instance of p25::lc::LC containing link control data.
* @param[in] duid P25 Phase 2 DUID type.
* @param[in] slot P25 Phase 2 slot number.
* @param[in] data Buffer containing P25 Phase 2 data to send.
* @param[in] controlByte DVM control byte.
* @returns bool True, if message was sent, otherwise false.
*/
virtual bool writeP25P2(const p25::lc::LC& control, p25::defines::P2_DUID::E duid, uint8_t slot, const uint8_t* data,
const uint8_t controlByte = 0U);
/**
* @brief Helper to test if the P25 ring buffer has data.
* @returns bool True, if the network P25 ring buffer has data, otherwise false.
*/
bool hasP25Data() const;
/**
* @brief Helper to test if the P25 Phase 2 ring buffer has data.
* @returns bool True, if the network P25 Phase 2 ring buffer has data, otherwise false.
*/
bool hasP25P2Data() const;
/**
* @brief Helper to validate a P25 network frame length.
* @param frameLength P25 encapsulated frame length.
@ -907,13 +955,13 @@ namespace network
DECLARE_PROTECTED_RO_PROPERTY_PLAIN(uint32_t, addrLen);
/**
* @brief Flag indicating whether network DMR slot 1 traffic is permitted.
* @brief Flag indicating whether network DMR/P25 Phase 2 slot 1 traffic is permitted.
*/
DECLARE_PROTECTED_RO_PROPERTY(bool, slot1, DMRSlot1);
DECLARE_PROTECTED_RO_PROPERTY(bool, slot1, Slot1);
/**
* @brief Flag indicating whether network DMR slot 2 traffic is permitted.
* @brief Flag indicating whether network DMR/P25 Phase 2 slot 2 traffic is permitted.
*/
DECLARE_PROTECTED_RO_PROPERTY(bool, slot2, DMRSlot2);
DECLARE_PROTECTED_RO_PROPERTY(bool, slot2, Slot2);
/**
* @brief Flag indicating whether network traffic is duplex.
@ -921,11 +969,10 @@ namespace network
DECLARE_PROTECTED_RO_PROPERTY(bool, duplex, Duplex);
protected:
bool m_useAlternatePortForDiagnostics;
bool m_allowActivityTransfer;
bool m_allowDiagnosticTransfer;
bool m_packetDump;
bool m_debug;
udp::Socket* m_socket;
@ -933,6 +980,7 @@ namespace network
RingBuffer<uint8_t> m_rxDMRData;
RingBuffer<uint8_t> m_rxP25Data;
RingBuffer<uint8_t> m_rxP25P2Data;
RingBuffer<uint8_t> m_rxNXDNData;
RingBuffer<uint8_t> m_rxAnalogData;
@ -940,6 +988,7 @@ namespace network
uint32_t* m_dmrStreamId;
uint32_t m_p25StreamId;
uint32_t* m_p25P2StreamId;
uint32_t m_nxdnStreamId;
uint32_t m_analogStreamId;
@ -1013,11 +1062,13 @@ namespace network
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* | System ID | Reserved | Control Flags | MFId |
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* | Network ID | Reserved |
* | Network ID |S|Rsvd |P2 DUID|
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* | LSD1 | LSD2 | DUID | Frame Length |
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
*
* S = Slot Number (clear Slot 1, set Slot 2)
*
* The data starting at offset 20 for variable number of bytes (DUID dependant)
* is the P25 frame.
*
@ -1156,6 +1207,28 @@ namespace network
UInt8Array createP25_PDUMessage(uint32_t& length, const p25::data::DataHeader& header, const uint8_t currentBlock,
const uint8_t* data, const uint32_t len);
/**
* @brief Creates an P25 Phase 2 frame message.
* \code{.unparsed}
*
* The data packed into a P25 Phase 2 frame message is essentially just a message header with the FEC encoded
* raw Phase 2 data.
*
* The data starting at offset 24 for 40 bytes of the raw P25 Phase 2 frame.
*
* \endcode
* @param[out] length Length of network message buffer.
* @param[in] control Instance of p25::lc::LC containing link control data.
* @param duid P25 Phase 2 DUID.
* @param[in] slot P25 Phase 2 slot (clear Slot 1, set Slot 2).
* @param[in] data Buffer containing P25 LDU2 data to send.
* @param[in] controlByte DVM Network Control Byte.
* @param data Instance of the dmr::data::Data class containing the DMR message.
* @returns UInt8Array Buffer containing the built network message.
*/
UInt8Array createP25P2_Message(uint32_t& length, const p25::lc::LC& control, p25::defines::P2_DUID::E duid,
const bool slot, const uint8_t* data, uint8_t controlByte = 0U);
/**
* @brief Creates an NXDN frame message.
* \code{.unparsed}

@ -28,7 +28,8 @@ using namespace network::frame;
// Static Class Members
// ---------------------------------------------------------------------------
std::vector<FrameQueue::Timestamp> FrameQueue::m_streamTimestamps;
std::mutex FrameQueue::s_timestampMtx;
std::unordered_map<uint32_t, uint32_t> FrameQueue::s_streamTimestamps;
// ---------------------------------------------------------------------------
// Public Class Members
@ -37,8 +38,7 @@ std::vector<FrameQueue::Timestamp> FrameQueue::m_streamTimestamps;
/* Initializes a new instance of the FrameQueue class. */
FrameQueue::FrameQueue(udp::Socket* socket, uint32_t peerId, bool debug) : RawFrameQueue(socket, debug),
m_peerId(peerId),
m_timestampMtx()
m_peerId(peerId)
{
assert(peerId < 999999999U);
}
@ -218,8 +218,8 @@ void FrameQueue::enqueueMessage(udp::BufferQueue* queue, const uint8_t* message,
void FrameQueue::clearTimestamps()
{
std::lock_guard<std::mutex> lock(m_timestampMtx);
m_streamTimestamps.clear();
std::lock_guard<std::mutex> lock(s_timestampMtx);
s_streamTimestamps.clear();
}
// ---------------------------------------------------------------------------
@ -228,59 +228,36 @@ void FrameQueue::clearTimestamps()
/* Search for a timestamp entry by stream ID. */
FrameQueue::Timestamp* FrameQueue::findTimestamp(uint32_t streamId)
uint32_t FrameQueue::findTimestamp(uint32_t streamId)
{
std::lock_guard<std::mutex> lock(m_timestampMtx);
for (size_t i = 0; i < m_streamTimestamps.size(); i++) {
if (m_streamTimestamps[i].streamId == streamId)
return &m_streamTimestamps[i];
std::lock_guard<std::mutex> lock(s_timestampMtx);
auto it = s_streamTimestamps.find(streamId);
if (it != s_streamTimestamps.end()) {
return it->second;
}
return nullptr;
return INVALID_TS;
}
/* Insert a timestamp for a stream ID. */
/* Insert/update a timestamp for a stream ID. */
void FrameQueue::insertTimestamp(uint32_t streamId, uint32_t timestamp)
void FrameQueue::setTimestamp(uint32_t streamId, uint32_t timestamp)
{
std::lock_guard<std::mutex> lock(m_timestampMtx);
std::lock_guard<std::mutex> lock(s_timestampMtx);
if (streamId == 0U || timestamp == INVALID_TS) {
LogError(LOG_NET, "FrameQueue::insertTimestamp(), invalid streamId or timestamp");
LogError(LOG_NET, "FrameQueue::setTimestamp(), invalid streamId or timestamp");
return;
}
Timestamp entry = { streamId, timestamp };
m_streamTimestamps.push_back(entry);
}
/* Update a timestamp for a stream ID. */
void FrameQueue::updateTimestamp(uint32_t streamId, uint32_t timestamp)
{
std::lock_guard<std::mutex> lock(m_timestampMtx);
if (streamId == 0U || timestamp == INVALID_TS) {
LogError(LOG_NET, "FrameQueue::updateTimestamp(), invalid streamId or timestamp");
return;
}
// find the timestamp entry and update it
for (size_t i = 0; i < m_streamTimestamps.size(); i++) {
if (m_streamTimestamps[i].streamId == streamId) {
m_streamTimestamps[i].timestamp = timestamp;
break;
}
}
s_streamTimestamps[streamId] = timestamp;
}
/* Erase a timestamp for a stream ID. */
void FrameQueue::eraseTimestamp(uint32_t streamId)
{
std::lock_guard<std::mutex> lock(m_timestampMtx);
m_streamTimestamps.erase(
std::remove_if(m_streamTimestamps.begin(), m_streamTimestamps.end(),
[streamId](const Timestamp& entry) { return entry.streamId == streamId; }),
m_streamTimestamps.end());
std::lock_guard<std::mutex> lock(s_timestampMtx);
s_streamTimestamps.erase(streamId);
}
/* Generate RTP message for the frame queue. */
@ -300,8 +277,8 @@ uint8_t* FrameQueue::generateMessage(const uint8_t* message, uint32_t length, ui
uint32_t timestamp = INVALID_TS;
if (streamId != 0U) {
auto entry = findTimestamp(streamId);
if (entry != nullptr) {
timestamp = entry->timestamp;
if (entry != INVALID_TS) {
timestamp = entry;
}
if (timestamp != INVALID_TS) {
@ -309,7 +286,7 @@ uint8_t* FrameQueue::generateMessage(const uint8_t* message, uint32_t length, ui
timestamp += (RTP_GENERIC_CLOCK_RATE / 133);
if (m_debug)
LogDebugEx(LOG_NET, "FrameQueue::generateMessage()", "RTP streamId = %u, previous TS = %u, TS = %u, rtpSeq = %u", streamId, prevTimestamp, timestamp, rtpSeq);
updateTimestamp(streamId, timestamp);
setTimestamp(streamId, timestamp);
}
}
@ -332,14 +309,14 @@ uint8_t* FrameQueue::generateMessage(const uint8_t* message, uint32_t length, ui
timestamp = (uint32_t)system_clock::ntp::now();
header.setTimestamp(timestamp);
insertTimestamp(streamId, timestamp);
setTimestamp(streamId, timestamp);
}
header.encode(buffer);
if (streamId != 0U && rtpSeq == RTP_END_OF_CALL_SEQ) {
auto entry = findTimestamp(streamId);
if (entry != nullptr) {
if (entry != INVALID_TS) {
if (m_debug)
LogDebugEx(LOG_NET, "FrameQueue::generateMessage()", "RTP streamId = %u, rtpSeq = %u", streamId, rtpSeq);
eraseTimestamp(streamId);

@ -45,11 +45,6 @@ namespace network
class HOST_SW_API FrameQueue : public RawFrameQueue {
public: typedef std::pair<const NET_FUNC::ENUM, const NET_SUBFUNC::ENUM> OpcodePair;
public:
typedef struct {
uint32_t streamId;
uint32_t timestamp;
} Timestamp;
auto operator=(FrameQueue&) -> FrameQueue& = delete;
auto operator=(FrameQueue&&) -> FrameQueue& = delete;
FrameQueue(FrameQueue&) = delete;
@ -126,28 +121,21 @@ namespace network
private:
uint32_t m_peerId;
std::mutex m_timestampMtx;
static std::vector<Timestamp> m_streamTimestamps;
static std::mutex s_timestampMtx;
static std::unordered_map<uint32_t, uint32_t> s_streamTimestamps;
/**
* @brief Search for a timestamp entry by stream ID.
* @param streamId Stream ID to find.
* @return Timestamp* Table entry.
*/
Timestamp* findTimestamp(uint32_t streamId);
/**
* @brief Insert a timestamp for a stream ID.
* @param streamId Stream ID.
* @param timestamp Timestamp.
* @return uint32_t Table entry.
*/
void insertTimestamp(uint32_t streamId, uint32_t timestamp);
uint32_t findTimestamp(uint32_t streamId);
/**
* @brief Update a timestamp for a stream ID.
* @brief Insert/update a timestamp for a stream ID.
* @param streamId Stream ID.
* @param timestamp Timestamp.
*/
void updateTimestamp(uint32_t streamId, uint32_t timestamp);
void setTimestamp(uint32_t streamId, uint32_t timestamp);
/**
* @brief Erase a timestamp for a stream ID.
* @param streamId Stream ID.

@ -25,7 +25,7 @@ using namespace network::frame;
#include <cmath>
// ---------------------------------------------------------------------------
// Public Class Members
// Constants
// ---------------------------------------------------------------------------
#define REPLY_WAIT 200 // 200ms
@ -98,6 +98,12 @@ void NetRPC::clock(uint32_t ms)
udp::Socket::address(address).c_str(), udp::Socket::port(address), rpcHeader.getFunction(), rpcHeader.getMessageLength());
}
if (length < RPC_HEADER_LENGTH_BYTES + rpcHeader.getMessageLength()) {
LogError(LOG_NET, "NetRPC::clock(), message received from network is malformed! %u bytes != %u bytes",
RPC_HEADER_LENGTH_BYTES + rpcHeader.getMessageLength(), length);
return;
}
// copy message
uint32_t messageLength = rpcHeader.getMessageLength();
UInt8Array message = std::unique_ptr<uint8_t[]>(new uint8_t[messageLength]);

@ -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) 2017-2025 Bryan Biedenkapp, N2PLL
* Copyright (C) 2017-2026 Bryan Biedenkapp, N2PLL
*
*/
#include "Defines.h"
@ -94,6 +94,9 @@ Network::Network(const std::string& address, uint16_t port, uint16_t localPort,
m_rxDMRStreamId[0U] = 0U;
m_rxDMRStreamId[1U] = 0U;
m_rxP25StreamId = 0U;
m_rxP25P2StreamId = new uint32_t[2U];
m_rxP25P2StreamId[0U] = 0U;
m_rxP25P2StreamId[1U] = 0U;
m_rxNXDNStreamId = 0U;
m_rxAnalogStreamId = 0U;
@ -107,6 +110,7 @@ Network::~Network()
{
delete[] m_salt;
delete[] m_rxDMRStreamId;
delete[] m_rxP25P2StreamId;
delete m_metadata;
delete m_mux;
}
@ -124,6 +128,9 @@ void Network::resetDMR(uint32_t slotNo)
else {
m_rxDMRStreamId[1U] = 0U;
}
if (m_debug)
LogDebugEx(LOG_NET, "Network::resetDMR()", "reset DMR Slot %u rx stream ID", slotNo);
}
/* Resets the P25 ring buffer. */
@ -132,6 +139,27 @@ void Network::resetP25()
{
BaseNetwork::resetP25();
m_rxP25StreamId = 0U;
if (m_debug)
LogDebugEx(LOG_NET, "Network::resetP25()", "reset P25 rx stream ID");
}
/* Resets the P25 Phase 2 ring buffer for the given slot. */
void Network::resetP25P2(uint32_t slotNo)
{
assert(slotNo == 1U || slotNo == 2U);
BaseNetwork::resetP25P2(slotNo);
if (slotNo == 1U) {
m_rxP25P2StreamId[0U] = 0U;
}
else {
m_rxP25P2StreamId[1U] = 0U;
}
if (m_debug)
LogDebugEx(LOG_NET, "Network::resetP25P2()", "reset P25 Phase 2 Slot %u rx stream ID", slotNo);
}
/* Resets the NXDN ring buffer. */
@ -140,6 +168,9 @@ void Network::resetNXDN()
{
BaseNetwork::resetNXDN();
m_rxNXDNStreamId = 0U;
if (m_debug)
LogDebugEx(LOG_NET, "Network::resetNXDN()", "reset NXDN rx stream ID");
}
/* Resets the analog ring buffer. */
@ -148,6 +179,9 @@ void Network::resetAnalog()
{
BaseNetwork::resetAnalog();
m_rxAnalogStreamId = 0U;
if (m_debug)
LogDebugEx(LOG_NET, "Network::resetAnalog()", "reset analog rx stream ID");
}
/* Sets the instances of the Radio ID and Talkgroup ID lookup tables. */
@ -383,12 +417,13 @@ void Network::clock(uint32_t ms)
// check if we need to skip this stream -- a non-zero stream ID means the network client is locked
// to receiving a specific stream; a zero stream ID means the network is promiscuously
// receiving streams sent to this peer
if (m_rxDMRStreamId[slotNo] != 0U && m_rxDMRStreamId[slotNo] != streamId) {
if (m_rxDMRStreamId[slotNo] != 0U && m_rxDMRStreamId[slotNo] != streamId &&
rtpHeader.getSequence() != RTP_END_OF_CALL_SEQ) {
break;
}
}
if (m_debug)
if (m_packetDump)
Utils::dump(1U, "Network::clock(), Network Rx, DMR", buffer.get(), length);
if (length > (int)(DMR_PACKET_LENGTH + PACKET_PAD))
LogError(LOG_NET, "DMR Stream %u, frame oversized? this shouldn't happen, pktSeq = %u, len = %u", streamId, m_pktSeq, length);
@ -467,13 +502,13 @@ void Network::clock(uint32_t ms)
// check if we need to skip this stream -- a non-zero stream ID means the network client is locked
// to receiving a specific stream; a zero stream ID means the network is promiscuously
// receiving streams sent to this peer
if (m_rxP25StreamId != 0U && m_rxP25StreamId != streamId) {
if (m_rxP25StreamId != 0U && m_rxP25StreamId != streamId &&
rtpHeader.getSequence() != RTP_END_OF_CALL_SEQ) {
break;
}
}
if (m_debug)
if (m_packetDump)
Utils::dump(1U, "Network::clock(), Network Rx, P25", buffer.get(), length);
if (length > 512)
LogError(LOG_NET, "P25 Stream %u, frame oversized? this shouldn't happen, pktSeq = %u, len = %u", streamId, m_pktSeq, length);
@ -494,6 +529,93 @@ void Network::clock(uint32_t ms)
}
break;
case NET_SUBFUNC::PROTOCOL_SUBFUNC_P25_P2: // Encapsulated P25 Phase 2 data frame
{
if (m_enabled && m_p25Enabled) {
uint32_t slotNo = (buffer[19U] & 0x80U) == 0x80U ? 1U : 0U; // this is the raw index for the stream ID array
if (m_debug) {
LogDebug(LOG_NET, "P25 Phase 2 Slot %u, peer = %u, len = %u, pktSeq = %u, streamId = %u",
slotNo + 1U, peerId, length, rtpHeader.getSequence(), streamId);
}
if (m_promiscuousPeer) {
m_rxP25P2StreamId[slotNo] = streamId;
m_pktLastSeq = m_pktSeq;
uint16_t lastRxSeq = 0U;
MULTIPLEX_RET_CODE ret = m_mux->verifyStream(streamId, rtpHeader.getSequence(), fneHeader.getFunction(), &lastRxSeq);
if (ret == MUX_LOST_FRAMES) {
LogError(LOG_NET, "PEER %u stream %u possible lost frames; got %u, expected %u", peerId,
streamId, rtpHeader.getSequence(), lastRxSeq, rtpHeader.getSequence());
}
else if (ret == MUX_OUT_OF_ORDER) {
LogError(LOG_NET, "PEER %u stream %u out-of-order; got %u, expected >%u", peerId,
streamId, rtpHeader.getSequence(), lastRxSeq);
}
#if DEBUG_RTP_MUX
else {
LogDebugEx(LOG_NET, "Network::clock()", "PEER %u valid mux, seq = %u, streamId = %u", peerId, rtpHeader.getSequence(), streamId);
}
#endif
}
else {
if (m_rxP25P2StreamId[slotNo] == 0U) {
if (rtpHeader.getSequence() == RTP_END_OF_CALL_SEQ) {
m_rxP25P2StreamId[slotNo] = 0U;
}
else {
m_rxP25P2StreamId[slotNo] = streamId;
}
m_pktLastSeq = m_pktSeq;
}
else {
if (m_rxP25P2StreamId[slotNo] == streamId) {
uint16_t lastRxSeq = 0U;
MULTIPLEX_RET_CODE ret = verifyStream(&lastRxSeq);
if (ret == MUX_LOST_FRAMES) {
LogWarning(LOG_NET, "DMR Slot %u stream %u possible lost frames; got %u, expected %u",
slotNo, streamId, m_pktSeq, lastRxSeq);
}
else if (ret == MUX_OUT_OF_ORDER) {
LogWarning(LOG_NET, "DMR Slot %u stream %u out-of-order; got %u, expected %u",
slotNo, streamId, m_pktSeq, lastRxSeq);
}
#if DEBUG_RTP_MUX
else {
LogDebugEx(LOG_NET, "Network::clock()", "P25 Phase 2 Slot %u valid seq, seq = %u, streamId = %u", slotNo, rtpHeader.getSequence(), streamId);
}
#endif
if (rtpHeader.getSequence() == RTP_END_OF_CALL_SEQ) {
m_rxP25P2StreamId[slotNo] = 0U;
}
}
}
// check if we need to skip this stream -- a non-zero stream ID means the network client is locked
// to receiving a specific stream; a zero stream ID means the network is promiscuously
// receiving streams sent to this peer
if (m_rxP25P2StreamId[slotNo] != 0U && m_rxP25P2StreamId[slotNo] != streamId &&
rtpHeader.getSequence() != RTP_END_OF_CALL_SEQ) {
break;
}
}
if (m_packetDump)
Utils::dump(1U, "Network::clock(), Network Rx, P25 Phase 2", buffer.get(), length);
if (length > (int)(P25_P2_PACKET_LENGTH + PACKET_PAD))
LogError(LOG_NET, "P25 Phase 2 Stream %u, frame oversized? this shouldn't happen, pktSeq = %u, len = %u", streamId, m_pktSeq, length);
uint8_t len = length;
m_rxP25P2Data.addData(&len, 1U);
m_rxP25P2Data.addData(buffer.get(), len);
}
}
break;
case NET_SUBFUNC::PROTOCOL_SUBFUNC_NXDN: // Encapsulated NXDN data frame
{
if (m_enabled && m_nxdnEnabled) {
@ -561,12 +683,13 @@ void Network::clock(uint32_t ms)
// check if we need to skip this stream -- a non-zero stream ID means the network client is locked
// to receiving a specific stream; a zero stream ID means the network is promiscuously
// receiving streams sent to this peer
if (m_rxNXDNStreamId != 0U && m_rxNXDNStreamId != streamId) {
if (m_rxNXDNStreamId != 0U && m_rxNXDNStreamId != streamId &&
rtpHeader.getSequence() != RTP_END_OF_CALL_SEQ) {
break;
}
}
if (m_debug)
if (m_packetDump)
Utils::dump(1U, "Network::clock(), Network Rx, NXDN", buffer.get(), length);
if (length > (int)(NXDN_PACKET_LENGTH + PACKET_PAD))
LogError(LOG_NET, "NXDN Stream %u, frame oversized? this shouldn't happen, pktSeq = %u, len = %u", streamId, m_pktSeq, length);
@ -645,12 +768,13 @@ void Network::clock(uint32_t ms)
// check if we need to skip this stream -- a non-zero stream ID means the network client is locked
// to receiving a specific stream; a zero stream ID means the network is promiscuously
// receiving streams sent to this peer
if (m_rxAnalogStreamId != 0U && m_rxAnalogStreamId != streamId) {
if (m_rxAnalogStreamId != 0U && m_rxAnalogStreamId != streamId &&
rtpHeader.getSequence() != RTP_END_OF_CALL_SEQ) {
break;
}
}
if (m_debug)
if (m_packetDump)
Utils::dump(1U, "Network::clock(), Network Rx, Analog", buffer.get(), length);
if (length < (int)ANALOG_PACKET_LENGTH) {
LogError(LOG_NET, "Analog Stream %u, frame too short? this shouldn't happen, pktSeq = %u, len = %u", streamId, m_pktSeq, length);
@ -659,12 +783,10 @@ void Network::clock(uint32_t ms)
LogError(LOG_NET, "Analog Stream %u, frame oversized? this shouldn't happen, pktSeq = %u, len = %u", streamId, m_pktSeq, length);
// Analog frames are larger then 254 bytes, but we need to handle the case where the frame is larger than 255 bytes
uint8_t len = 254U;
m_rxAnalogData.addData(&len, 1U);
len = length - 254U;
uint8_t len = length - 254U;
m_rxAnalogData.addData(&len, 1U);
m_rxAnalogData.addData(buffer.get(), len);
m_rxAnalogData.addData(buffer.get(), length);
}
}
}
@ -684,7 +806,7 @@ void Network::clock(uint32_t ms)
case NET_SUBFUNC::MASTER_SUBFUNC_WL_RID: // Radio ID Whitelist
{
if (m_enabled && m_updateLookup) {
if (m_debug)
if (m_packetDump)
Utils::dump(1U, "Network::clock(), Network Rx, WL RID", buffer.get(), length);
if (m_ridLookup != nullptr) {
@ -710,7 +832,7 @@ void Network::clock(uint32_t ms)
case NET_SUBFUNC::MASTER_SUBFUNC_BL_RID: // Radio ID Blacklist
{
if (m_enabled && m_updateLookup) {
if (m_debug)
if (m_packetDump)
Utils::dump(1U, "Network::clock(), Network Rx, BL RID", buffer.get(), length);
if (m_ridLookup != nullptr) {
@ -737,7 +859,7 @@ void Network::clock(uint32_t ms)
case NET_SUBFUNC::MASTER_SUBFUNC_ACTIVE_TGS: // Talkgroup Active IDs
{
if (m_enabled && m_updateLookup) {
if (m_debug)
if (m_packetDump)
Utils::dump(1U, "Network::clock(), Network Rx, ACTIVE TGS", buffer.get(), length);
if (m_tidLookup != nullptr) {
@ -787,7 +909,7 @@ void Network::clock(uint32_t ms)
case NET_SUBFUNC::MASTER_SUBFUNC_DEACTIVE_TGS: // Talkgroup Deactivated IDs
{
if (m_enabled && m_updateLookup) {
if (m_debug)
if (m_packetDump)
Utils::dump(1U, "Network::clock(), Network Rx, DEACTIVE TGS", buffer.get(), length);
if (m_tidLookup != nullptr) {
@ -821,7 +943,7 @@ void Network::clock(uint32_t ms)
case NET_SUBFUNC::MASTER_HA_PARAMS: // HA Parameters
{
if (m_enabled) {
if (m_debug)
if (m_packetDump)
Utils::dump(1U, "Network::clock(), Network Rx, HA PARAMS", buffer.get(), length);
m_haIPs.clear();
@ -1084,14 +1206,13 @@ void Network::clock(uint32_t ms)
m_retryTimer.start();
if (length > 6) {
m_useAlternatePortForDiagnostics = (buffer[6U] & 0x80U) == 0x80U;
if (m_useAlternatePortForDiagnostics) {
LogInfoEx(LOG_NET, "PEER %u RPTC ACK, master commanded alternate port for diagnostics and activity logging, remotePeerId = %u", m_peerId, rtpHeader.getSSRC());
} else {
// disable diagnostic and activity logging automatically if the master doesn't utilize the alternate port
bool useAlternatePortForDiagnostics = (buffer[6U] & 0x80U) == 0x80U;
if (!useAlternatePortForDiagnostics) {
// disable diagnostic and activity logging automatically if the master doesn't utilize the secondary port
m_allowDiagnosticTransfer = false;
m_allowActivityTransfer = false;
LogWarning(LOG_NET, "PEER %u RPTC ACK, master does not enable alternate port for diagnostics and activity logging, diagnostic and activity logging are disabled, remotePeerId = %u", m_peerId, rtpHeader.getSSRC());
LogError(LOG_NET, "PEER %u RPTC ACK, master does not enable secondary port for metadata, diagnostic and activity logging are disabled, remotePeerId = %u", m_peerId, rtpHeader.getSSRC());
LogError(LOG_NET, "PEER %u RPTC ACK, **please update your FNE**, secondary port for metadata, is required for all services as of R05A04, remotePeerId = %u", m_peerId, rtpHeader.getSSRC());
}
}
break;
@ -1117,7 +1238,7 @@ void Network::clock(uint32_t ms)
case NET_FUNC::PONG: // Master Ping Response
m_timeoutTimer.start();
if (length >= 14) {
if (m_debug)
if (m_packetDump)
Utils::dump(1U, "Network::clock(), Network Rx, PONG", buffer.get(), length);
ulong64_t serverNow = 0U;
@ -1329,7 +1450,7 @@ bool Network::writeLogin()
::memcpy(buffer + 0U, TAG_REPEATER_LOGIN, 4U);
SET_UINT32(m_peerId, buffer, 4U); // Peer ID
if (m_debug)
if (m_packetDump)
Utils::dump(1U, "Network::writeLogin(), Message, Login", buffer, 8U);
m_loginStreamId = createStreamId();
@ -1362,7 +1483,7 @@ bool Network::writeAuthorisation()
delete[] in;
if (m_debug)
if (m_packetDump)
Utils::dump(1U, "Network::writeAuthorisation(), Message, Authorisation", out, 40U);
return writeMaster({ NET_FUNC::RPTK, NET_SUBFUNC::NOP }, out, 40U, pktSeq(), m_loginStreamId);
@ -1423,7 +1544,7 @@ bool Network::writeConfig()
::memcpy(buffer + 0U, TAG_REPEATER_CONFIG, 4U);
::snprintf(buffer + 8U, json.length() + 1U, "%s", json.c_str());
if (m_debug) {
if (m_packetDump) {
Utils::dump(1U, "Network::writeConfig(), Message, Configuration", (uint8_t*)buffer, json.length() + 8U);
}
@ -1437,7 +1558,7 @@ bool Network::writePing()
uint8_t buffer[1U];
::memset(buffer, 0x00U, 1U);
if (m_debug)
if (m_packetDump)
Utils::dump(1U, "Network Message, Ping", buffer, 11U);
return writeMaster({ NET_FUNC::PING, NET_SUBFUNC::NOP }, buffer, 1U, RTP_END_OF_CALL_SEQ, createStreamId());

@ -167,6 +167,10 @@ namespace network
* @brief Resets the P25 ring buffer.
*/
void resetP25() override;
/**
* @brief Resets the P25 Phase 2 ring buffer.
*/
void resetP25P2(uint32_t slotNo) override;
/**
* @brief Resets the NXDN ring buffer.
*/
@ -330,6 +334,7 @@ namespace network
uint32_t* m_rxDMRStreamId;
uint32_t m_rxP25StreamId;
uint32_t* m_rxP25P2StreamId;
uint32_t m_rxNXDNStreamId;
uint32_t m_rxAnalogStreamId;

@ -17,6 +17,13 @@ using namespace compress;
#include <cassert>
// ---------------------------------------------------------------------------
// Constants
// ---------------------------------------------------------------------------
#define MAX_FRAGMENT_SIZE 8192 * 1024 // 8MB max
// ---------------------------------------------------------------------------
// Public Class Members
// ---------------------------------------------------------------------------
@ -59,6 +66,14 @@ bool PacketBuffer::decode(const uint8_t* data, uint8_t** message, uint32_t* outL
uint32_t size = GET_UINT32(data, 0U);
uint32_t compressedSize = GET_UINT32(data, 4U);
// make sure we can't exceed max fragment size -- prevent potential DOS attack by sending
// enormous fragment sizes
if (size > MAX_FRAGMENT_SIZE || compressedSize > MAX_FRAGMENT_SIZE) {
LogError(LOG_NET, "%s, fragment size exceeds maximum. BUGBUG.", m_name);
delete frag;
return false;
}
frag->size = size;
frag->compressedSize = compressedSize;
}

@ -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-2025 Bryan Biedenkapp, N2PLL
* Copyright (C) 2024-2026 Bryan Biedenkapp, N2PLL
*
*/
/**
@ -92,6 +92,12 @@ namespace network
*/
bool flushQueue(udp::BufferQueue* queue);
/**
* @brief Helper to enable or disable debug logging.
* @param enable Flag indicating whether debug logging is enabled.
*/
void setDebug(bool enable) { m_debug = enable; }
protected:
sockaddr_storage m_addr;
uint32_t m_addrLen;

@ -325,6 +325,12 @@ ssize_t Socket::read(uint8_t* buffer, uint32_t length, sockaddr_storage& address
// does the network packet contain the appropriate magic leader?
uint16_t magic = GET_UINT16(buffer, 0U);
if (magic == AES_WRAPPED_PCKT_MAGIC) {
// prevent malicious packets that are too short
if (len < 2U + crypto::AES::BLOCK_BYTES_LEN) {
LogError(LOG_NET, "Encrypted packet too short");
return -1;
}
uint32_t cryptedLen = (len - 2U) * sizeof(uint8_t);
uint8_t* cryptoBuffer = buffer + 2U;

@ -136,18 +136,7 @@ CAC::~CAC()
CAC& CAC::operator=(const CAC& data)
{
if (&data != this) {
::memcpy(m_data, data.m_data, NXDN_CAC_CRC_LENGTH_BYTES);
m_ran = m_data[0U] & 0x3FU;
m_structure = (ChStructure::E)((m_data[0U] >> 6) & 0x03U);
m_longInbound = data.m_longInbound;
m_idleBusy = data.m_idleBusy;
m_txContinuous = data.m_txContinuous;
m_receive = data.m_receive;
m_rxCRC = data.m_rxCRC;
copy(data);
}
return *this;
@ -427,11 +416,15 @@ void CAC::setData(const uint8_t* data)
void CAC::copy(const CAC& data)
{
m_data = new uint8_t[NXDN_CAC_CRC_LENGTH_BYTES];
if (m_data == nullptr)
m_data = new uint8_t[NXDN_CAC_CRC_LENGTH_BYTES];
::memcpy(m_data, data.m_data, NXDN_CAC_CRC_LENGTH_BYTES);
m_ran = m_data[0U] & 0x3FU;
m_structure = (ChStructure::E)((m_data[0U] >> 6) & 0x03U);
m_ran = data.m_ran;
m_structure = data.m_structure;
m_data[0U] = m_ran;
m_data[0U] |= ((m_structure << 6) & 0xC0U);
m_longInbound = data.m_longInbound;

@ -76,7 +76,7 @@ FACCH1::~FACCH1()
FACCH1& FACCH1::operator=(const FACCH1& data)
{
if (&data != this) {
::memcpy(m_data, data.m_data, NXDN_FACCH1_CRC_LENGTH_BYTES);
copy(data);
}
return *this;
@ -226,6 +226,7 @@ void FACCH1::setData(const uint8_t* data)
void FACCH1::copy(const FACCH1& data)
{
m_data = new uint8_t[NXDN_FACCH1_CRC_LENGTH_BYTES];
if (m_data == nullptr)
m_data = new uint8_t[NXDN_FACCH1_CRC_LENGTH_BYTES];
::memcpy(m_data, data.m_data, NXDN_FACCH1_CRC_LENGTH_BYTES);
}

@ -57,12 +57,7 @@ LICH::~LICH() = default;
LICH& LICH::operator=(const LICH& data)
{
if (&data != this) {
m_lich = data.m_lich;
m_rfct = data.m_rfct;
m_fct = data.m_fct;
m_option = data.m_option;
m_outbound = data.m_outbound;
copy(data);
}
return *this;
@ -155,10 +150,10 @@ void LICH::copy(const LICH& data)
{
m_lich = data.m_lich;
m_rfct = (RFChannelType::E)((m_lich >> 6) & 0x03U);
m_fct = (FuncChannelType::E)((m_lich >> 4) & 0x03U);
m_option = (ChOption::E)((m_lich >> 2) & 0x03U);
m_outbound = ((m_lich >> 1) & 0x01U) == 0x01U;
m_rfct = data.m_rfct;
m_fct = data.m_fct;
m_option = data.m_option;
m_outbound = data.m_outbound;
}
/* Internal helper to generate the parity bit for the LICH. */

@ -73,10 +73,7 @@ SACCH::~SACCH()
SACCH& SACCH::operator=(const SACCH& data)
{
if (&data != this) {
::memcpy(m_data, data.m_data, NXDN_SACCH_CRC_LENGTH_BYTES);
m_ran = m_data[0U] & 0x3FU;
m_structure = (ChStructure::E)((m_data[0U] >> 6) & 0x03U);
copy(data);
}
return *this;
@ -161,11 +158,9 @@ void SACCH::encode(uint8_t* data) const
{
assert(data != nullptr);
m_data[0U] &= 0xC0U;
m_data[0U] |= m_ran;
m_data[0U] &= 0x3FU;
m_data[0U] |= (m_structure << 6) & 0xC0U;
// rebuild byte 0 from member variables: upper 2 bits = structure, lower 6 bits = RAN
m_data[0U] = (m_structure << 6) & 0xC0U; // set structure in upper 2 bits
m_data[0U] |= m_ran & 0x3FU; // set RAN in lower 6 bits
uint8_t buffer[NXDN_SACCH_CRC_LENGTH_BYTES];
::memset(buffer, 0x00U, NXDN_SACCH_CRC_LENGTH_BYTES);
@ -249,9 +244,14 @@ void SACCH::setData(const uint8_t* data)
void SACCH::copy(const SACCH& data)
{
m_data = new uint8_t[NXDN_SACCH_CRC_LENGTH_BYTES];
if (m_data == nullptr)
m_data = new uint8_t[NXDN_SACCH_CRC_LENGTH_BYTES];
::memcpy(m_data, data.m_data, NXDN_SACCH_CRC_LENGTH_BYTES);
m_ran = m_data[0U] & 0x3FU;
m_structure = (ChStructure::E)((m_data[0U] >> 6) & 0x03U);
m_ran = data.m_ran;
m_structure = data.m_structure;
// rebuild byte 0 from member variables: upper 2 bits = structure, lower 6 bits = RAN
m_data[0U] = (m_structure << 6) & 0xC0U; // set structure in upper 2 bits
m_data[0U] |= m_ran & 0x3FU; // set RAN in lower 6 bits
}

@ -100,9 +100,7 @@ UDCH::~UDCH()
UDCH& UDCH::operator=(const UDCH& data)
{
if (&data != this) {
::memcpy(m_data, data.m_data, NXDN_UDCH_CRC_LENGTH_BYTES);
m_ran = m_data[0U] & 0x3FU;
copy(data);
}
return *this;
@ -256,8 +254,10 @@ void UDCH::setData(const uint8_t* data)
void UDCH::copy(const UDCH& data)
{
m_data = new uint8_t[NXDN_UDCH_CRC_LENGTH_BYTES];
if (m_data == nullptr)
m_data = new uint8_t[NXDN_UDCH_CRC_LENGTH_BYTES];
::memcpy(m_data, data.m_data, NXDN_UDCH_CRC_LENGTH_BYTES);
m_ran = data.m_ran;
m_ran = m_data[0U] & 0x3FU;
}

@ -766,7 +766,6 @@ void P25Crypto::setKey(const uint8_t* key, uint8_t len)
m_tek.reset();
m_tek = std::make_unique<uint8_t[]>(len);
::memset(m_tek.get(), 0x00U, MAX_ENC_KEY_LENGTH_BYTES);
::memset(m_tek.get(), 0x00U, m_tekLength);
::memcpy(m_tek.get(), key, len);
}

@ -5,7 +5,7 @@
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* Copyright (C) 2016 Jonathan Naylor, G4KLX
* Copyright (C) 2017-2025 Bryan Biedenkapp, N2PLL
* Copyright (C) 2017-2026 Bryan Biedenkapp, N2PLL
*
*/
/**
@ -39,6 +39,7 @@ namespace p25
*/
/** @name Frame Lengths and Misc Constants */
// TIA-102.BAAA-B Section 4 and 5; TIA-102.AABB-B Section 5
const uint32_t P25_HDU_FRAME_LENGTH_BYTES = 99U;
const uint32_t P25_HDU_FRAME_LENGTH_BITS = P25_HDU_FRAME_LENGTH_BYTES * 8U;
@ -63,12 +64,29 @@ namespace p25
const uint32_t P25_TDULC_FRAME_LENGTH_BYTES = 54U;
const uint32_t P25_TDULC_FRAME_LENGTH_BITS = P25_TDULC_FRAME_LENGTH_BYTES * 8U;
const uint32_t P25_P2_FRAME_LENGTH_BYTES = 45U;
const uint32_t P25_P2_FRAME_LENGTH_BITS = P25_P2_FRAME_LENGTH_BYTES * 8U;
const uint32_t P25_NID_LENGTH_BYTES = 8U;
const uint32_t P25_NID_LENGTH_BITS = P25_NID_LENGTH_BYTES * 8U;
// TIA-102.BBAC-A Section 4
const uint32_t P25_P2_FRAME_LENGTH_BYTES = 40U;
const uint32_t P25_P2_FRAME_LENGTH_BITS = P25_P2_FRAME_LENGTH_BYTES * 8U;
const uint32_t P25_P2_IEMI_LENGTH_BITS = 312U;
const uint32_t P25_P2_IEMI_LENGTH_BYTES = (P25_P2_IEMI_LENGTH_BITS / 8U) + 1U;
const uint32_t P25_P2_IEMI_WSYNC_LENGTH_BITS = 276U;
const uint32_t P25_P2_IEMI_WSYNC_LENGTH_BYTES = (P25_P2_IEMI_WSYNC_LENGTH_BITS / 8U) + 1U;
const uint32_t P25_P2_IEMI_MAC_LENGTH_BITS = 156U;
const uint32_t P25_P2_IEMI_MAC_LENGTH_BYTES = (P25_P2_IEMI_MAC_LENGTH_BITS / 8U) + 1U;
const uint32_t P25_P2_SOEMI_MAC_LENGTH_BITS = 156U;
const uint32_t P25_P2_SOEMI_MAC_LENGTH_BYTES = (P25_P2_SOEMI_MAC_LENGTH_BITS / 8U) + 1U;
const uint32_t P25_P2_SOEMI_LENGTH_BITS = 270U;
const uint32_t P25_P2_SOEMI_LENGTH_BYTES = (P25_P2_SOEMI_LENGTH_BITS / 8U) + 1U;
const uint32_t P25_P2_IOEMI_MAC_LENGTH_BITS = 180U;
const uint32_t P25_P2_IOEMI_MAC_LENGTH_BYTES = (P25_P2_IOEMI_MAC_LENGTH_BITS / 8U) + 1U;
// TIA-102.BAAA-B Section 7.3
// 5 5 7 5 F 5 F F 7 7 F F
// 01 01 01 01 01 11 01 01 11 11 01 01 11 11 11 11 01 11 01 11 11 11 11 11
@ -112,7 +130,7 @@ namespace p25
const uint32_t P25_TDULC_FEC_LENGTH_BYTES = 36U;
const uint32_t P25_TDULC_LENGTH_BYTES = 18U;
const uint32_t P25_TDULC_PAYLOAD_LENGTH_BYTES = 8U;
const uint32_t P25_TDULC_PAYLOAD_LENGTH_BYTES = 8U; // 9 bytes including LCO, 8 bytes payload
const uint32_t P25_TSBK_FEC_LENGTH_BYTES = 25U;
const uint32_t P25_TSBK_FEC_LENGTH_BITS = P25_TSBK_FEC_LENGTH_BYTES * 8U - 4U; // Trellis is actually 196 bits
@ -837,6 +855,70 @@ namespace p25
};
}
// TIA-102.BBAD-D Section 4.1
/** @brief Phase 2 MAC PDU Opcode(s) */
namespace P2_MAC_HEADER_OPCODE {
/** @brief Phase 2 MAC PDU Opcode(s) */
enum : uint8_t {
SIGNAL = 0x00U, //!<
PTT = 0x01U, //!< Push-To-Talk
END_PTT = 0x02U, //!< End Push-To-Talk
IDLE = 0x03U, //!< Idle
ACTIVE = 0x04U, //!< Active
HANGTIME = 0x06U, //!< Call Hangtime
};
}
// TIA-102.BBAD-D Section 4.2
/** @brief Phase 2 MAC 4V/SACCH Offset(s) */
namespace P2_MAC_HEADER_OFFSET {
/** @brief Phase 2 MAC 4V/SACCH Offset(s) */
enum : uint8_t {
FIRST_4V_NEXT = 0x00U, //!< First 4V Next non-SACCH Burst on Slot
FIRST_4V_2ND = 0x01U, //!< First 4V Second non-SACCH Burst on Slot
FIRST_4V_3RD = 0x02U, //!< First 4V Third non-SACCH Burst on Slot
FIRST_4V_4TH = 0x03U, //!< First 4V Fourth non-SACCH Burst on Slot
FIRST_4V_5TH = 0x04U, //!< First 4V Fifth non-SACCH Burst on Slot
FIRST_4V_6TH = 0x05U, //!< First 4V Sixth non-SACCH Burst on Slot (Inbound Reserved)
INBOUND_RANDOM_SACCH = 0x06U, //!< Inbound Random SACCH (Outbound Reserved)
NO_VOICE_OR_UNK = 0x07U //!< No Voice or Unknown
};
}
// TIA-102.BBAD-D Section 3
/** @brief Phase 2 MAC MCO Partitioning */
namespace P2_MAC_MCO_PARTITION {
/** @brief Phase 2 MAC MCO Partitioning */
enum : uint8_t {
UNIQUE = 0x00U, //!< Unique
ABBREVIATED = 0x40U, //!< Abbreviate
MFID_SPECIFIC = 0x80U, //!< MFID Specific
EXPLICIT = 0xC0U //!< Explicit
};
}
// TIA-102.BBAD-D Section 3
/** @brief Phase 2 MAC PDU Opcode(s) */
namespace P2_MAC_MCO {
/** @brief Phase 2 MAC PDU Opcode(s) */
enum : uint8_t {
// MAC PDU ISP/OSP Shared Opcode(s) (Unique Partition)
PDU_NULL = 0x00U, //!< Null MAC
GROUP = 0x01U, //!< GRP VCH USER - Group Voice Channel User
PRIVATE = 0x02U, //!< UU VCH USER - Unit-to-Unit Voice Channel User
TEL_INT_VCH_USER = 0x03U, //!< TEL INT VCH USER - Telephone Interconnect Voice Channel User
MAC_RELEASE = 0x61U, //!< MAC RELEASE - MAC Release
/* Any abbreviated or explicit partition opcodes are essentially just TSBKO's. */
};
}
// TIA-102.BAAC-D Section 2.11
/** @brief Data Unit ID(s) */
namespace DUID {
@ -854,6 +936,7 @@ namespace p25
};
}
// TIA-102.BBAC-A Section 5.4.1
/** @brief Phase 2 Data Unit ID(s) */
namespace P2_DUID {
/** @brief Data Unit ID(s) */

@ -5,7 +5,7 @@
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* Copyright (C) 2015,2016 Jonathan Naylor, G4KLX
* Copyright (C) 2024 Bryan Biedenkapp, N2PLL
* Copyright (C) 2024,2026 Bryan Biedenkapp, N2PLL
*
*/
#include "Defines.h"
@ -30,3 +30,17 @@ void Sync::addP25Sync(uint8_t* data)
::memcpy(data, P25_SYNC_BYTES, P25_SYNC_LENGTH_BYTES);
}
/* Helper to append P25 Phase 2 S-OEMI sync bytes to the passed buffer. */
void Sync::addP25P2_SOEMISync(uint8_t* data)
{
assert(data != nullptr);
for (uint32_t i = 0U; i < P25_P2_OEMI_SYNC_LENGTH_BITS; i++) {
uint32_t n = i + 4U + 134U; // this skips the 4 bits of the DUID and remaining 134 bits of Field 1 and 2 for
// a S-OEMI
bool b = READ_BIT(P25_P2_OEMI_SYNC_BYTES, i);
WRITE_BIT(data, n, b);
}
}

@ -5,6 +5,7 @@
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* Copyright (C) 2015,2016 Jonathan Naylor, G4KLX
* Copyright (C) 2026 Bryan Biedenkapp, N2PLL
*
*/
/**
@ -35,6 +36,12 @@ namespace p25
* @param data Buffer to append P25 sync bytes to.
*/
static void addP25Sync(uint8_t* data);
/**
* @brief Helper to append P25 Phase 2 S-OEMI sync bytes to the passed buffer.
* @param data Buffer to append P25 Phase 2 OEMI sync bytes to.
*/
static void addP25P2_SOEMISync(uint8_t* data);
};
} // namespace p25

@ -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) 2025 Bryan Biedenkapp, N2PLL
* Copyright (C) 2025-2026 Bryan Biedenkapp, N2PLL
*
*/
#include "Defines.h"
@ -104,6 +104,14 @@ bool Assembler::disassemble(const uint8_t* pduBlock, uint32_t blockLength, bool
dataHeader.getHeaderOffset(), dataHeader.getLLId());
}
if (dataHeader.getPacketLength() > P25_MAX_PDU_BLOCKS * P25_PDU_CONFIRMED_LENGTH_BYTES + 2U) {
LogError(LOG_P25, P25_PDU_STR ", ISP, packet length %u exceeds maximum supported size %u",
dataHeader.getPacketLength(), P25_MAX_PDU_BLOCKS * P25_PDU_CONFIRMED_LENGTH_BYTES);
resetDisassemblyState();
return false;
}
// make sure we don't get a PDU with more blocks then we support
if (dataHeader.getBlocksToFollow() >= P25_MAX_PDU_BLOCKS) {
LogError(LOG_P25, P25_PDU_STR ", ISP, too many PDU blocks to process, %u > %u", dataHeader.getBlocksToFollow(), P25_MAX_PDU_BLOCKS);
@ -485,6 +493,21 @@ uint32_t Assembler::getUserData(uint8_t* buffer) const
assert(buffer != nullptr);
assert(m_pduUserData != nullptr);
if (m_pduUserDataLength == 0U) {
LogError(LOG_P25, P25_PDU_STR ", no user data available to retrieve! BUGBUG!");
return 0U;
}
if (m_pduUserDataLength > (P25_MAX_PDU_BLOCKS * P25_PDU_CONFIRMED_LENGTH_BYTES + 2U)) {
LogError(LOG_P25, P25_PDU_STR ", user data length %u exceeds maximum allowable size! BUGBUG!", m_pduUserDataLength);
return 0U;
}
if (m_pduUserData == nullptr) {
LogError(LOG_P25, P25_PDU_STR ", no user data available to retrieve! BUGBUG!");
return 0U;
}
if (m_complete) {
::memcpy(buffer, m_pduUserData, m_pduUserDataLength);
return m_pduUserDataLength;

@ -82,7 +82,7 @@ bool DataBlock::decode(const uint8_t* data, const DataHeader& header, bool noTre
return false;
}
} else {
::memcpy(buffer, data, P25_PDU_CONFIRMED_DATA_LENGTH_BYTES);
::memcpy(buffer, data, P25_PDU_CONFIRMED_LENGTH_BYTES);
}
#if DEBUG_P25_PDU_DATA

@ -559,6 +559,20 @@ void DataHeader::reset()
m_mi = new uint8_t[MI_LENGTH_BYTES];
::memset(m_mi, 0x00U, MI_LENGTH_BYTES);
}
if (m_extAddrData != nullptr) {
::memset(m_extAddrData, 0x00U, P25_PDU_HEADER_LENGTH_BYTES);
} else {
m_extAddrData = new uint8_t[P25_PDU_HEADER_LENGTH_BYTES];
::memset(m_extAddrData, 0x00U, P25_PDU_HEADER_LENGTH_BYTES);
}
if (m_auxESData != nullptr) {
::memset(m_auxESData, 0x00U, P25_PDU_CONFIRMED_DATA_LENGTH_BYTES);
} else {
m_auxESData = new uint8_t[P25_PDU_CONFIRMED_DATA_LENGTH_BYTES];
::memset(m_auxESData, 0x00U, P25_PDU_CONFIRMED_DATA_LENGTH_BYTES);
}
}
/* Gets the total length in bytes of enclosed packet data. */

@ -104,6 +104,8 @@ namespace p25
namespace MotStreamPayload {
/** @brief Motorola Stream Payload */
enum E : uint8_t {
DATA_12 = 0x05U, //!< P25 12 Block Data
DATA_18 = 0x06U, //!< P25 18 Block Data
VOICE = 0x0BU, //!< P25 Voice
DATA = 0x0CU, //!< P25 Data
TERM_LC = 0x0EU, //!< P25 Termination Link Control

@ -54,7 +54,7 @@ MotStartOfStream::MotStartOfStream(uint8_t* data) :
MotStartOfStream::~MotStartOfStream()
{
if (icw != nullptr)
delete icw;
delete[] icw;
}
/* Decode a start of stream frame. */

@ -79,22 +79,7 @@ bool MotTDULCFrame::decode(const uint8_t* data)
// decode start of stream
startOfStream->decode(startBuffer);
uint8_t tdulcBuffer[9U];
::memcpy(tdulcBuffer, data + DFSI_MOT_START_LEN, 9U);
::memset(tdulcData, 0x00U, P25_TDULC_FRAME_LENGTH_BYTES);
tdulcData[0U] = (uint8_t)((tdulcBuffer[0U] >> 3) & 0x3FU);
tdulcData[1U] = (uint8_t)(((tdulcBuffer[0U] & 0x07U) << 3) | ((tdulcBuffer[1U] >> 4) & 0x07U));
tdulcData[2U] = (uint8_t)(((tdulcBuffer[1U] & 0x0FU) << 2) | ((tdulcBuffer[2U] >> 5) & 0x03U));
tdulcData[3U] = (uint8_t)(tdulcBuffer[2U] & 0x1FU);
tdulcData[4U] = (uint8_t)((tdulcBuffer[3U] >> 3) & 0x3FU);
tdulcData[5U] = (uint8_t)(((tdulcBuffer[3U] & 0x07U) << 3) | ((tdulcBuffer[4U] >> 4) & 0x07U));
tdulcData[6U] = (uint8_t)(((tdulcBuffer[4U] & 0x0FU) << 2) | ((tdulcBuffer[5U] >> 5) & 0x03U));
tdulcData[7U] = (uint8_t)(tdulcBuffer[5U] & 0x1FU);
tdulcData[8U] = (uint8_t)((tdulcBuffer[6U] >> 3) & 0x3FU);
tdulcData[9U] = (uint8_t)(((tdulcBuffer[6U] & 0x07U) << 3) | ((tdulcBuffer[7U] >> 4) & 0x07U));
tdulcData[10U] = (uint8_t)(((tdulcBuffer[7U] & 0x0FU) << 2) | ((tdulcBuffer[8U] >> 5) & 0x03U));
tdulcData[11U] = (uint8_t)(tdulcBuffer[8U] & 0x1FU);
::memcpy(tdulcData, data + DFSI_MOT_START_LEN, P25_TDULC_PAYLOAD_LENGTH_BYTES + 1U);
return true;
}
@ -118,18 +103,7 @@ void MotTDULCFrame::encode(uint8_t* data)
// encode TDULC - scope is intentional
{
data[0U] = DFSIFrameType::MOT_TDULC;
data[DFSI_MOT_START_LEN + 1U] = (uint8_t)((tdulcData[0U] & 0x3FU) << 3) | ((tdulcData[1U] >> 3) & 0x07U);
data[DFSI_MOT_START_LEN + 2U] = (uint8_t)((tdulcData[1U] & 0x0FU) << 4) | ((tdulcData[2U] >> 2) & 0x0FU);
data[DFSI_MOT_START_LEN + 3U] = (uint8_t)((tdulcData[2U] & 0x03U)) | (tdulcData[3U] & 0x3FU);
data[DFSI_MOT_START_LEN + 4U] = (uint8_t)((tdulcData[4U] & 0x3FU) << 3) | ((tdulcData[5U] >> 3) & 0x07U);
data[DFSI_MOT_START_LEN + 5U] = (uint8_t)((tdulcData[5U] & 0x0FU) << 4) | ((tdulcData[6U] >> 2) & 0x0FU);
data[DFSI_MOT_START_LEN + 6U] = (uint8_t)((tdulcData[6U] & 0x03U)) | (tdulcData[7U] & 0x3FU);
data[DFSI_MOT_START_LEN + 7U] = (uint8_t)((tdulcData[8U] & 0x3FU) << 3) | ((tdulcData[9U] >> 3) & 0x07U);
data[DFSI_MOT_START_LEN + 8U] = (uint8_t)((tdulcData[9U] & 0x0FU) << 4) | ((tdulcData[10U] >> 2) & 0x0FU);
data[DFSI_MOT_START_LEN + 9U] = (uint8_t)((tdulcData[10U] & 0x03U)) | (tdulcData[11U] & 0x3FU);
::memcpy(data + DFSI_MOT_START_LEN, tdulcData, P25_TDULC_PAYLOAD_LENGTH_BYTES + 1U);
data[DFSI_MOT_START_LEN + 11U] = DFSI_BUSY_BITS_IDLE;
}

@ -5,13 +5,15 @@
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* Copyright (C) 2016,2017 Jonathan Naylor, G4KLX
* Copyright (C) 2017-2025 Bryan Biedenkapp, N2PLL
* Copyright (C) 2017-2026 Bryan Biedenkapp, N2PLL
*
*/
#include "Defines.h"
#include "p25/P25Defines.h"
#include "p25/lc/LC.h"
#include "p25/P25Utils.h"
#include "p25/Sync.h"
#include "edac/CRC.h"
#include "edac/Golay24128.h"
#include "edac/Hamming.h"
#include "Log.h"
@ -29,6 +31,12 @@ using namespace p25::lc;
// Static Class Members
// ---------------------------------------------------------------------------
#if FORCE_TSBK_CRC_WARN
bool LC::s_warnCRC = true;
#else
bool LC::s_warnCRC = false;
#endif
SiteData LC::s_siteData = SiteData();
// ---------------------------------------------------------------------------
@ -63,7 +71,13 @@ LC::LC() :
m_algId(ALGO_UNENCRYPT),
m_kId(0U),
m_slotNo(0U),
m_p2DUID(P2_DUID::VTCH_4V),
m_colorCode(0U),
m_macPduOpcode(P2_MAC_HEADER_OPCODE::IDLE),
m_macPduOffset(P2_MAC_HEADER_OFFSET::NO_VOICE_OR_UNK),
m_macPartition(P2_MAC_MCO_PARTITION::ABBREVIATED),
m_rsValue(0U),
p2MCOData(nullptr),
m_rs(),
m_encryptOverride(false),
m_tsbkVendorSkip(false),
@ -93,6 +107,11 @@ LC::~LC()
delete[] m_userAlias;
m_userAlias = nullptr;
}
if (p2MCOData != nullptr) {
delete[] p2MCOData;
p2MCOData = nullptr;
}
}
/* Equals operator. */
@ -465,6 +484,311 @@ void LC::encodeLDU2(uint8_t* data)
#endif
}
/* Decode a IEMI VCH MAC PDU. */
bool LC::decodeVCH_MACPDU_IEMI(const uint8_t* data, bool sync)
{
assert(data != nullptr);
// determine buffer size based on sync flag
uint32_t lengthBits = sync ? P25_P2_IEMI_WSYNC_LENGTH_BITS : P25_P2_IEMI_LENGTH_BITS;
uint32_t lengthBytes = sync ? P25_P2_IEMI_WSYNC_LENGTH_BYTES : P25_P2_IEMI_LENGTH_BYTES;
// decode the Phase 2 DUID
uint8_t duid[1U], raw[P25_P2_IEMI_LENGTH_BYTES]; // Use max size for stack allocation
::memset(duid, 0x00U, 1U);
::memset(raw, 0x00U, lengthBytes);
// DUID bit extraction differs based on sync flag
if (sync) {
// IEMI with sync: 14 LSB sync bits + 36-bit field 1, then DUIDs at different positions
for (uint8_t i = 0U; i < 8U; i++) {
uint32_t n = i + 14U + 36U; // skip 14-bit sync + field 1 (36 bits)
if (i >= 2U)
n += 72U; // skip field 2
if (i >= 4U)
n += 96U; // skip field 3
if (i >= 6U)
n += 72U; // skip field 4
bool b = READ_BIT(data, n);
WRITE_BIT(raw, i, b);
}
} else {
// IEMI without sync: 72-bit field 1, then DUIDs
for (uint8_t i = 0U; i < 8U; i++) {
uint32_t n = i + 72U; // skip field 1
if (i >= 2U)
n += 72U; // skip field 2
if (i >= 4U)
n += 96U; // skip field 3
if (i >= 6U)
n += 72U; // skip field 4
bool b = READ_BIT(data, n);
WRITE_BIT(raw, i, b);
}
}
decodeP2_DUIDHamming(raw, duid);
m_p2DUID = duid[0U] >> 4U;
if (m_p2DUID == P2_DUID::VTCH_4V || m_p2DUID == P2_DUID::VTCH_2V || !sync)
return true; // don't handle 4V or 2V voice PDUs here -- user code will handle
else {
::memset(raw, 0x00U, lengthBytes);
// IEMI with sync: extract data bits (skip 14-bit sync and DUIDs)
for (uint32_t i = 0U; i < lengthBits; i++) {
uint32_t n = i + 14U; // Skip 14-bit sync
if (i >= 36U)
n += 2U; // skip DUID 1 after field 1 (36 bits)
if (i >= 108U)
n += 2U; // skip DUID 2 after field 2 (36+72)
if (i >= 204U)
n += 2U; // skip DUID 3 after field 3 (36+72+96)
bool b = READ_BIT(data, n);
WRITE_BIT(raw, i, b);
}
#if DEBUG_P25_MAC_PDU
Utils::dump(2U, "P25, LC::decodeVCH_MACPDU_IEMI(), MAC PDU", raw, lengthBytes);
#endif
// decode RS (46,26,21) FEC
try {
bool ret = m_rs.decode462621(raw);
if (!ret) {
LogError(LOG_P25, "LC::decodeVCH_MACPDU_IEMI(), failed to decode RS (46,26,21) FEC");
return false;
}
}
catch (...) {
Utils::dump(2U, "P25, LC::decodeVCH_MACPDU_IEMI(), RS excepted with input data", raw, lengthBytes);
return false;
}
#if DEBUG_P25_MAC_PDU
Utils::dump(2U, "P25, LC::decodeVCH_MACPDU_IEMI(), MAC PDU", raw, lengthBytes);
#endif
// are we decoding a FACCH with scrambling?
if (m_p2DUID == P2_DUID::FACCH_SCRAMBLED) {
/* TODO: if scrambled handle scrambling */
}
// are we decoding a SACCH with scrambling?
if (m_p2DUID == P2_DUID::SACCH_SCRAMBLED) {
/* TODO: if scrambled handle scrambling */
}
return decodeMACPDU(raw, P25_P2_IEMI_MAC_LENGTH_BITS);
}
return true;
}
/* Decode a xOEMI VCH MAC PDU. */
bool LC::decodeVCH_MACPDU_OEMI(const uint8_t* data, bool sync)
{
assert(data != nullptr);
// decode the Phase 2 DUID
uint8_t duid[1U], raw[P25_P2_IEMI_LENGTH_BYTES];
::memset(duid, 0x00U, 1U);
::memset(raw, 0x00U, P25_P2_IEMI_LENGTH_BYTES);
for (uint8_t i = 0U; i < 8U; i++) {
uint32_t n = i;
if (i >= 2U)
n += 72U; // skip field 1
if (i >= 4U)
n += 168U; // skip field 2, sync and field 3 (or just field 2)
if (i >= 6U)
n += 72U; // skip field 3
bool b = READ_BIT(data, n);
WRITE_BIT(raw, i, b);
}
decodeP2_DUIDHamming(raw, duid);
m_p2DUID = duid[0U] >> 4U;
if (m_p2DUID == P2_DUID::VTCH_4V || m_p2DUID == P2_DUID::VTCH_2V)
return true; // don't handle 4V or 2V voice PDUs here -- user code will handle
else {
::memset(raw, 0x00U, P25_P2_IEMI_LENGTH_BYTES);
if (sync) {
for (uint32_t i = 0U; i < P25_P2_SOEMI_LENGTH_BITS; i++) {
uint32_t n = i + 2U; // skip DUID 1
if (i >= 72U)
n += 2U; // skip DUID 2
if (i >= 134U)
n += 42U; // skip sync
if (i >= 198U)
n += 2U; // skip DUID 3
bool b = READ_BIT(data, n);
WRITE_BIT(raw, i, b);
}
#if DEBUG_P25_MAC_PDU
Utils::dump(2U, "P25, LC::decodeVCH_MACPDU_OEMI(), MAC PDU", raw, P25_P2_IEMI_LENGTH_BYTES);
#endif
// decode RS (45,26,20) FEC
try {
bool ret = m_rs.decode452620(raw);
if (!ret) {
LogError(LOG_P25, "LC::decodeVCH_MACPDU_OEMI(), failed to decode RS (45,26,20) FEC");
return false;
}
}
catch (...) {
Utils::dump(2U, "P25, LC::decodeVCH_MACPDU_OEMI(), RS excepted with input data", raw, P25_P2_IEMI_LENGTH_BYTES);
return false;
}
#if DEBUG_P25_MAC_PDU
Utils::dump(2U, "P25, LC::decodeVCH_MACPDU_OEMI(), MAC PDU", raw, P25_P2_IEMI_LENGTH_BYTES);
#endif
} else {
for (uint32_t i = 0U; i < P25_P2_IEMI_LENGTH_BITS; i++) {
uint32_t n = i + 2U; // skip DUID 1
if (i >= 72U)
n += 2U; // skip DUID 2
if (i >= 168U)
n += 2U; // skip DUID 3
bool b = READ_BIT(data, n);
WRITE_BIT(raw, i, b);
}
#if DEBUG_P25_MAC_PDU
Utils::dump(2U, "P25, LC::decodeVCH_MACPDU_OEMI(), MAC PDU", raw, P25_P2_IEMI_LENGTH_BYTES);
#endif
// decode RS (52,30,23) FEC
try {
bool ret = m_rs.decode523023(raw);
if (!ret) {
LogError(LOG_P25, "LC::decodeVCH_MACPDU_OEMI(), failed to decode RS (52,30,23) FEC");
return false;
}
}
catch (...) {
Utils::dump(2U, "P25, LC::decodeVCH_MACPDU_OEMI(), RS excepted with input data", raw, P25_P2_IEMI_LENGTH_BYTES);
return false;
}
#if DEBUG_P25_MAC_PDU
Utils::dump(2U, "P25, LC::decodeVCH_MACPDU_OEMI(), MAC PDU", raw, P25_P2_IEMI_LENGTH_BYTES);
#endif
}
// are we decoding a FACCH with scrambling?
if (m_p2DUID == P2_DUID::FACCH_SCRAMBLED) {
/* TODO: if scrambled handle scrambling */
}
// are we decoding a SACCH with scrambling?
if (m_p2DUID == P2_DUID::SACCH_SCRAMBLED) {
/* TODO: if scrambled handle scrambling */
}
return decodeMACPDU(raw, sync ? P25_P2_SOEMI_MAC_LENGTH_BITS : P25_P2_IOEMI_MAC_LENGTH_BITS);
}
return true;
}
/* Encode a VCH MAC PDU. */
void LC::encodeVCH_MACPDU(uint8_t* data, bool sync)
{
assert(data != nullptr);
uint8_t raw[P25_P2_IEMI_LENGTH_BYTES];
::memset(raw, 0x00U, P25_P2_IEMI_LENGTH_BYTES);
if (m_p2DUID != P2_DUID::VTCH_4V && m_p2DUID != P2_DUID::VTCH_2V) {
encodeMACPDU(raw, sync ? P25_P2_SOEMI_MAC_LENGTH_BITS : P25_P2_IOEMI_MAC_LENGTH_BITS);
#if DEBUG_P25_MAC_PDU
Utils::dump(2U, "P25, LC::encodeVCH_MACPDU(), MAC PDU", raw, P25_P2_IEMI_LENGTH_BYTES);
#endif
// if sync is being included we're an S-OEMI, otherwise an I-OEMI
if (sync) {
// encode RS (46,26,21) FEC
m_rs.encode452620(raw);
#if DEBUG_P25_MAC_PDU
Utils::dump(2U, "P25, LC::encodeVCH_MACPDU(), MAC PDU", raw, P25_P2_IEMI_LENGTH_BYTES);
#endif
for (uint32_t i = 0U; i < P25_P2_SOEMI_LENGTH_BITS; i++) {
uint32_t n = i + 2U; // skip DUID 1
if (i >= 72U)
n += 2U; // skip DUID 2
if (i >= 134U)
n += 42U; // skip sync
if (i >= 198U)
n += 2U; // skip DUID 3
bool b = READ_BIT(raw, i);
WRITE_BIT(data, n, b);
}
} else {
// encode RS (52,30,23) FEC
m_rs.encode523023(raw);
#if DEBUG_P25_MAC_PDU
Utils::dump(2U, "P25, LC::encodeVCH_MACPDU(), MAC PDU", raw, P25_P2_IEMI_LENGTH_BYTES);
#endif
for (uint32_t i = 0U; i < P25_P2_IEMI_LENGTH_BITS; i++) {
uint32_t n = i + 2U; // skip DUID 1
if (i >= 72U)
n += 2U; // skip DUID 2
if (i >= 168U)
n += 2U; // skip DUID 3
bool b = READ_BIT(raw, i);
WRITE_BIT(data, n, b);
}
}
}
if (sync) {
Sync::addP25P2_SOEMISync(data);
}
// encode the Phase 2 DUID
uint8_t duid[1U];
::memset(duid, 0x00U, 1U);
duid[0U] = (m_p2DUID & 0x0FU) << 4U;
::memset(raw, 0x00U, 1U);
encodeP2_DUIDHamming(raw, duid);
for (uint8_t i = 0U; i < 8U; i++) {
uint32_t n = i;
if (i >= 2U)
n += 72U; // skip field 1
if (i >= 4U)
n += 168U; // skip field 2, sync and field 3 (or just field 2)
if (i >= 6U)
n += 72U; // skip field 4
bool b = READ_BIT(raw, i);
WRITE_BIT(data, n, b);
}
}
/* Helper to determine if the MFId is a standard MFId. */
bool LC::isStandardMFId() const
@ -754,6 +1078,233 @@ void LC::encodeLC(uint8_t* rs)
*/
}
/* Decode MAC PDU. */
bool LC::decodeMACPDU(const uint8_t* raw, uint32_t macLength)
{
assert(raw != nullptr);
bool ret = edac::CRC::checkCRC12(raw, macLength - 12U);
if (!ret) {
if (s_warnCRC) {
LogWarning(LOG_P25, "TSBK::decode(), failed CRC CCITT-162 check");
ret = true; // ignore CRC error
}
else {
LogError(LOG_P25, "TSBK::decode(), failed CRC CCITT-162 check");
}
}
if (!ret)
return false;
m_macPduOpcode = (raw[0U] >> 5U) & 0x07U; // MAC PDU Opcode
m_macPduOffset = (raw[0U] >> 2U) & 0x07U; // MAC PDU Offset
switch (m_macPduOpcode) {
case P2_MAC_HEADER_OPCODE::PTT:
m_algId = raw[10U]; // Algorithm ID
if (m_algId != ALGO_UNENCRYPT) {
if (m_mi != nullptr)
delete[] m_mi;
m_mi = new uint8_t[MI_LENGTH_BYTES];
::memset(m_mi, 0x00U, MI_LENGTH_BYTES);
::memcpy(m_mi, raw + 1U, MI_LENGTH_BYTES); // Message Indicator
m_kId = (raw[10U] << 8) + raw[11U]; // Key ID
if (!m_encrypted) {
m_encryptOverride = true;
m_encrypted = true;
}
} else {
if (m_mi != nullptr)
delete[] m_mi;
m_mi = new uint8_t[MI_LENGTH_BYTES];
::memset(m_mi, 0x00U, MI_LENGTH_BYTES);
m_kId = 0x0000U;
if (m_encrypted) {
m_encryptOverride = true;
m_encrypted = false;
}
}
m_srcId = GET_UINT24(raw, 13U); // Source Radio Address
m_dstId = GET_UINT16(raw, 16U); // Talkgroup Address
break;
case P2_MAC_HEADER_OPCODE::END_PTT:
m_colorCode = ((raw[1U] & 0x0FU) << 8U) + // Color Code
raw[2U]; // ...
m_srcId = GET_UINT24(raw, 13U); // Source Radio Address
m_dstId = GET_UINT16(raw, 16U); // Talkgroup Address
break;
case P2_MAC_HEADER_OPCODE::IDLE:
case P2_MAC_HEADER_OPCODE::ACTIVE:
case P2_MAC_HEADER_OPCODE::HANGTIME:
/*
** bryanb: likely will need extra work here -- IDLE,ACTIVE,HANGTIME PDUs can contain multiple
** MCOs; for now we're only gonna be decoding the first one...
*/
m_macPartition = raw[1U] >> 5U; // MAC Partition
m_lco = raw[1U] & 0x1FU; // MCO
if (m_macPartition == P2_MAC_MCO_PARTITION::UNIQUE) {
switch (m_lco) {
case P2_MAC_MCO::GROUP:
m_group = true;
m_emergency = (raw[2U] & 0x80U) == 0x80U; // Emergency Flag
if (!m_encryptOverride) {
m_encrypted = (raw[2U] & 0x40U) == 0x40U; // Encryption Flag
}
m_priority = (raw[2U] & 0x07U); // Priority
m_dstId = GET_UINT16(raw, 3U); // Talkgroup Address
m_srcId = GET_UINT24(raw, 5U); // Source Radio Address
break;
case P2_MAC_MCO::PRIVATE:
m_group = false;
m_emergency = (raw[2U] & 0x80U) == 0x80U; // Emergency Flag
if (!m_encryptOverride) {
m_encrypted = (raw[2U] & 0x40U) == 0x40U; // Encryption Flag
}
m_priority = (raw[2U] & 0x07U); // Priority
m_dstId = GET_UINT24(raw, 3U); // Talkgroup Address
m_srcId = GET_UINT24(raw, 6U); // Source Radio Address
break;
case P2_MAC_MCO::TEL_INT_VCH_USER:
m_emergency = (raw[2U] & 0x80U) == 0x80U; // Emergency Flag
if (!m_encryptOverride) {
m_encrypted = (raw[2U] & 0x40U) == 0x40U; // Encryption Flag
}
m_priority = (raw[2U] & 0x07U); // Priority
m_callTimer = GET_UINT16(raw, 3U); // Call Timer
if (m_srcId == 0U) {
m_srcId = GET_UINT24(raw, 5U); // Source/Target Address
}
break;
case P2_MAC_MCO::PDU_NULL:
break;
default:
LogError(LOG_P25, "LC::decodeMACPDU(), unknown MAC PDU LCO, lco = $%02X", m_lco);
return false;
}
} else {
// for non-unique partitions, we currently do not decode
// instead we will copy the MCO bytes out and allow user code to decode
if (p2MCOData != nullptr)
delete[] p2MCOData;
uint32_t macLengthBytes = (macLength / 8U) + ((macLength % 8U) ? 1U : 0U);
p2MCOData = new uint8_t[macLengthBytes];
::memset(p2MCOData, 0x00U, macLengthBytes);
// this will include the entire MCO (and depending on message length multiple MCOs)
::memcpy(p2MCOData, raw + 1U, macLengthBytes - 3U); // excluding MAC PDU header and CRC
}
break;
default:
LogError(LOG_P25, "LC::decodeMACPDU(), unknown MDC PDU header opcode, opcode = $%02X", m_macPduOpcode);
return false;
}
return true;
}
/* Encode MAC PDU. */
void LC::encodeMACPDU(uint8_t* raw, uint32_t macLength)
{
assert(raw != nullptr);
raw[0U] = ((m_macPduOpcode & 0x07U) << 5U) + // MAC PDU Opcode
((m_macPduOffset & 0x07U) << 2U); // MAC PDU Offset
switch (m_macPduOpcode) {
case P2_MAC_HEADER_OPCODE::PTT:
for (uint32_t i = 0; i < MI_LENGTH_BYTES; i++)
raw[i + 1U] = m_mi[i]; // Message Indicator
raw[10U] = m_algId; // Algorithm ID
raw[11U] = (uint8_t)(m_kId & 0xFFU); // Key ID
raw[12U] = (uint8_t)((m_kId >> 8U) & 0xFFU); // ...
SET_UINT24(m_srcId, raw, 13U); // Source Radio Address
SET_UINT16((uint16_t)(m_dstId & 0xFFFFU), raw, 16U); // Talkgroup Address
break;
case P2_MAC_HEADER_OPCODE::END_PTT:
raw[1U] = (uint8_t)((m_colorCode >> 8U & 0x0FU)); // Color Code
raw[2U] = (uint8_t)(m_colorCode & 0xFFU); // ...
SET_UINT24(m_srcId, raw, 13U); // Source Radio Address
SET_UINT16((uint16_t)(m_dstId & 0xFFFFU), raw, 16U); // Talkgroup Address
break;
case P2_MAC_HEADER_OPCODE::IDLE:
case P2_MAC_HEADER_OPCODE::ACTIVE:
case P2_MAC_HEADER_OPCODE::HANGTIME:
/*
** bryanb: likely will need extra work here -- IDLE,ACTIVE,HANGTIME PDUs can contain multiple
** MCOs; for now we're only gonna be decoding the first one...
*/
raw[1U] = ((m_macPartition & 0x07U) << 5U) + // MAC Partition
(m_lco & 0x1FU); // MCO
if (m_macPartition == P2_MAC_MCO_PARTITION::UNIQUE) {
switch (m_lco) {
case P2_MAC_MCO::GROUP:
raw[2U] = (m_emergency ? 0x80U : 0x00U) + // Emergency Flag
(m_encrypted ? 0x40U : 0x00U) + // Encryption Flag
(m_priority & 0x07U); // Priority
SET_UINT16((uint16_t)(m_dstId & 0xFFFFU), raw, 3U); // Talkgroup Address
SET_UINT24(m_srcId, raw, 5U); // Source Radio Address
break;
case P2_MAC_MCO::PRIVATE:
raw[2U] = (m_emergency ? 0x80U : 0x00U) + // Emergency Flag
(m_encrypted ? 0x40U : 0x00U) + // Encryption Flag
(m_priority & 0x07U); // Priority
SET_UINT24(m_dstId, raw, 3U); // Talkgroup Address
SET_UINT24(m_srcId, raw, 6U); // Source Radio Address
break;
case P2_MAC_MCO::TEL_INT_VCH_USER:
raw[2U] = (m_emergency ? 0x80U : 0x00U) + // Emergency Flag
(m_encrypted ? 0x40U : 0x00U) + // Encryption Flag
(m_priority & 0x07U); // Priority
SET_UINT16((uint16_t)(m_callTimer & 0xFFFFU), raw, 3U); // Call Timer
SET_UINT24(m_srcId, raw, 5U); // Source/Target Radio Address
break;
case P2_MAC_MCO::MAC_RELEASE:
raw[2U] = 0x80U; // Force Preemption (Fixed)
SET_UINT24(m_srcId, raw, 3U); // Source Radio Address
break;
case P2_MAC_MCO::PDU_NULL:
break;
default:
LogError(LOG_P25, "LC::encodeMACPDU(), unknown MAC PDU LCO, lco = $%02X", m_lco);
break;
}
break;
} else {
if (p2MCOData != nullptr) {
// this will include the entire MCO (and depending on message length multiple MCOs)
uint32_t macLengthBytes = (macLength / 8U) + ((macLength % 8U) ? 1U : 0U);
::memcpy(raw + 1U, p2MCOData, macLengthBytes - 3U); // excluding MAC PDU header and CRC
}
}
break;
default:
LogError(LOG_P25, "LC::encodeMACPDU(), unknown MDC PDU header opcode, opcode = $%02X", m_macPduOpcode);
break;
}
edac::CRC::addCRC12(raw, macLength - 12U);
}
/*
** Encryption data
*/
@ -840,6 +1391,11 @@ void LC::copy(const LC& data)
m_callTimer = data.m_callTimer;
m_slotNo = data.m_slotNo;
m_p2DUID = data.m_p2DUID;
m_colorCode = data.m_colorCode;
m_macPduOpcode = data.m_macPduOpcode;
m_macPduOffset = data.m_macPduOffset;
m_macPartition = data.m_macPartition;
m_rsValue = data.m_rsValue;
@ -884,6 +1440,21 @@ void LC::copy(const LC& data)
m_gotUserAlias = false;
}
// do we have Phase 2 MCO data to copy?
if (data.p2MCOData != nullptr) {
if (p2MCOData != nullptr)
delete[] p2MCOData;
p2MCOData = new uint8_t[P25_P2_IOEMI_MAC_LENGTH_BYTES];
::memset(p2MCOData, 0x00U, P25_P2_IOEMI_MAC_LENGTH_BYTES);
::memcpy(p2MCOData, data.p2MCOData, P25_P2_IOEMI_MAC_LENGTH_BYTES);
} else {
if (p2MCOData != nullptr) {
delete[] p2MCOData;
p2MCOData = nullptr;
}
}
s_siteData = data.s_siteData;
}
@ -995,3 +1566,49 @@ void LC::encodeHDUGolay(uint8_t* data, const uint8_t* raw)
}
}
}
/* Decode Phase 2 DUID hamming FEC. */
void LC::decodeP2_DUIDHamming(const uint8_t* data, uint8_t* raw)
{
uint32_t n = 0U;
uint32_t m = 0U;
for (uint32_t i = 0U; i < 4U; i++) {
bool hamming[8U];
for (uint32_t j = 0U; j < 8U; j++) {
hamming[j] = READ_BIT(data, n);
n++;
}
edac::Hamming::decode844(hamming);
for (uint32_t j = 0U; j < 4U; j++) {
WRITE_BIT(raw, m, hamming[j]);
m++;
}
}
}
/* Encode Phase 2 DUID hamming FEC. */
void LC::encodeP2_DUIDHamming(uint8_t* data, const uint8_t* raw)
{
uint32_t n = 0U;
uint32_t m = 0U;
for (uint32_t i = 0U; i < 4U; i++) {
bool hamming[8U];
for (uint32_t j = 0U; j < 4U; j++) {
hamming[j] = READ_BIT(raw, m);
m++;
}
edac::Hamming::encode844(hamming);
for (uint32_t j = 0U; j < 8U; j++) {
WRITE_BIT(data, n, hamming[j]);
n++;
}
}
}

@ -5,7 +5,7 @@
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* Copyright (C) 2016 Jonathan Naylor, G4KLX
* Copyright (C) 2017-2025 Bryan Biedenkapp, N2PLL
* Copyright (C) 2017-2026 Bryan Biedenkapp, N2PLL
*
*/
/**
@ -70,6 +70,8 @@ namespace p25
*/
LC& operator=(const LC& data);
/** Project 25 Phase I CAI (TIA-102.BAAA-B Section 4.2, 4.5) */
/**
* @brief Decode a header data unit.
* @param[in] data Buffer containing the HDU to decode.
@ -88,7 +90,7 @@ namespace p25
* @brief Decode a logical link data unit 1.
* @param[in] data Buffer containing an LDU1 to decode.
* @param rawOnly Flag indicating only the raw bytes of the LC should be decoded.
* @returns True, if LDU1 decoded, otherwise false.
* @returns bool True, if LDU1 decoded, otherwise false.
*/
bool decodeLDU1(const uint8_t* data, bool rawOnly = false);
/**
@ -100,7 +102,7 @@ namespace p25
/**
* @brief Decode a logical link data unit 2.
* @param[in] data Buffer containing an LDU2 to decode.
* @returns True, if LDU2 decoded, otherwise false.
* @returns bool True, if LDU2 decoded, otherwise false.
*/
bool decodeLDU2(const uint8_t* data);
/**
@ -109,6 +111,29 @@ namespace p25
*/
void encodeLDU2(uint8_t* data);
/** Project 25 Phase II (TIA-102.BBAD-D Section 2) */
/**
* @brief Decode a IEMI VCH MAC PDU.
* @param data Buffer containing the MAC PDU to decode.
* @param sync Flag indicating if sync is included (true=276 bits with RS, false=312 bits no RS).
* @return bool True, if MAC PDU decoded, otherwise false.
*/
bool decodeVCH_MACPDU_IEMI(const uint8_t* data, bool sync);
/**
* @brief Decode a xOEMI VCH MAC PDU.
* @param data Buffer containing the MAC PDU to decode.
* @param sync Flag indicating if sync is included.
* @return bool True, if MAC PDU decoded, otherwise false.
*/
bool decodeVCH_MACPDU_OEMI(const uint8_t* data, bool sync);
/**
* @brief Encode a VCH MAC PDU.
* @param[out] data Buffer to encode a MAC PDU.
* @param sync Flag indicating if sync is to be included.
*/
void encodeVCH_MACPDU(uint8_t* data, bool sync);
/**
* @brief Helper to determine if the MFId is a standard MFId.
* @returns bool True, if the MFId contained for this LC is standard, otherwise false.
@ -128,6 +153,20 @@ namespace p25
*/
void encodeLC(uint8_t* rs);
/**
* @brief Decode MAC PDU.
* @param[in] raw Buffer containing the decoded Reed-Solomon MAC PDU data.
* @param macLength MAC PDU length in bits (156 for IEMI/S-OEMI, 180 for I-OEMI).
* @returns bool True, if MAC PDU is decoded, otherwise false.
*/
bool decodeMACPDU(const uint8_t* raw, uint32_t macLength = defines::P25_P2_IOEMI_MAC_LENGTH_BITS);
/**
* @brief Encode MAC PDU.
* @param[out] raw Buffer to encode MAC PDU data.
* @param macLength MAC PDU length in bits (156 for IEMI/S-OEMI, 180 for I-OEMI).
*/
void encodeMACPDU(uint8_t* raw, uint32_t macLength = defines::P25_P2_IOEMI_MAC_LENGTH_BITS);
/** @name Encryption data */
/**
* @brief Sets the encryption message indicator.
@ -166,6 +205,12 @@ namespace p25
static void setSiteData(SiteData siteData) { s_siteData = siteData; }
/** @} */
/**
* @brief Sets the flag indicating CRC-errors should be warnings and not errors.
* @param warnCRC Flag indicating CRC-errors should be treated as warnings.
*/
static void setWarnCRC(bool warnCRC) { s_warnCRC = warnCRC; }
public:
/** @name Common Data */
/**
@ -254,6 +299,27 @@ namespace p25
* @brief Slot Number.
*/
DECLARE_PROPERTY(uint8_t, slotNo, SlotNo);
/**
* @brief Phase 2 DUID.
*/
DECLARE_PROPERTY(uint8_t, p2DUID, P2DUID);
/**
* @brief Color Code.
*/
DECLARE_PROPERTY(uint16_t, colorCode, ColorCode);
/**
* @brief MAC PDU Opcode.
*/
DECLARE_PROPERTY(uint8_t, macPduOpcode, MACPDUOpcode);
/**
* @brief MAC PDU SACCH Offset.
*/
DECLARE_PROPERTY(uint8_t, macPduOffset, MACPDUOffset);
/**
* @brief MAC Partition.
*/
DECLARE_PROPERTY(uint8_t, macPartition, MACPartition);
/** @} */
/** @name Packed RS Data */
@ -263,6 +329,10 @@ namespace p25
DECLARE_PROPERTY(ulong64_t, rsValue, RS);
/** @} */
/** @name Phase 2 Raw MCO Data */
uint8_t* p2MCOData; // ?? - this should probably be private with getters/setters
/** @} */
private:
friend class TSBK;
friend class TDULC;
@ -280,6 +350,8 @@ namespace p25
bool m_gotUserAliasPartA;
bool m_gotUserAlias;
static bool s_warnCRC;
// Local Site data
static SiteData s_siteData;
@ -313,6 +385,19 @@ namespace p25
* @param[in] raw
*/
void encodeHDUGolay(uint8_t* data, const uint8_t* raw);
/**
* @brief Decode Phase 2 DUID hamming FEC.
* @param[in] raw
* @param[out] data
*/
void decodeP2_DUIDHamming(const uint8_t* raw, uint8_t* data);
/**
* @brief Encode Phase 2 DUID hamming FEC.
* @param[out] data
* @param[in] raw
*/
void encodeP2_DUIDHamming(uint8_t* data, const uint8_t* raw);
};
} // namespace lc
} // namespace p25

@ -192,8 +192,8 @@ bool TDULC::decode(const uint8_t* data, uint8_t* payload, bool rawTDULC)
if (m_raw != nullptr)
delete[] m_raw;
m_raw = new uint8_t[P25_TDULC_PAYLOAD_LENGTH_BYTES];
::memcpy(m_raw, rs + 1U, P25_TDULC_PAYLOAD_LENGTH_BYTES);
m_raw = new uint8_t[P25_TDULC_PAYLOAD_LENGTH_BYTES + 1U];
::memcpy(m_raw, rs, P25_TDULC_PAYLOAD_LENGTH_BYTES + 1U);
::memcpy(payload, rs + 1U, P25_TDULC_PAYLOAD_LENGTH_BYTES);
return true;

@ -94,6 +94,8 @@ void LC_TDULC_RAW::setTDULC(const uint8_t* tdulc)
{
assert(tdulc != nullptr);
m_lco = tdulc[0U] & 0x3F; // LCO
m_tdulc = new uint8_t[P25_TDULC_PAYLOAD_LENGTH_BYTES];
::memset(m_tdulc, 0x00U, P25_TDULC_PAYLOAD_LENGTH_BYTES);

@ -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,2026 Bryan Biedenkapp, N2PLL
*
*/
#include "Defines.h"
@ -47,16 +47,27 @@ bool ISP_EMERG_ALRM_REQ::decode(const uint8_t* data, bool rawTSBK)
** bryanb: this is a bit of a hack -- because the EMERG ALRM and DENY have the same
** opcode; the following are used by TSBK_OSP_DENY_RSP; best way to check is for m_response > 0
*/
m_aivFlag = (((tsbkValue >> 56) & 0xFFU) & 0x80U) == 0x80U; // Additional Info. Flag
m_service = (uint8_t)((tsbkValue >> 56) & 0x3FU); // Service Type
m_response = (uint8_t)((tsbkValue >> 48) & 0xFFU); // Reason
uint8_t si1 = (uint8_t)((tsbkValue >> 56) & 0xFFU); // Emerg. Special Info 1
bool manDown = si1 & 0x01U; // Man Down Flag
uint8_t si2 = (uint8_t)((tsbkValue >> 48) & 0xFFU); // Emerg. Special Info 2
// if we have no special info, this is a defacto emergency button press
if (si1 == 0U && si2 == 0U) {
m_emergency = true;
}
if (m_response == 0U) {
// if we have a man down flag set and no special info 2, this is a man-down emergency
if (manDown && si2 == 0U) {
m_emergency = true;
} else {
m_emergency = false;
}
// all other emergency alarms aren't supported and are ignored (and infact most code will treat that as
// OSP_DENY_RSP)
m_aivFlag = (((tsbkValue >> 56) & 0xFFU) & 0x80U) == 0x80U; // Additional Info. Flag
m_service = (uint8_t)((tsbkValue >> 56) & 0x3FU); // Service Type
m_response = (uint8_t)((tsbkValue >> 48) & 0xFFU); // Reason
m_dstId = (uint32_t)((tsbkValue >> 24) & 0xFFFFU); // Target Radio Address
m_srcId = (uint32_t)(tsbkValue & 0xFFFFFFU); // Source Radio Address

@ -145,6 +145,7 @@ CryptoContainer::CryptoContainer(const std::string& filename, const std::string&
m_file(filename),
m_password(password),
m_reloadTime(reloadTime),
m_lastLoadTime(0U),
#if !defined(ENABLE_SSL)
m_enabled(false),
#else
@ -606,6 +607,9 @@ bool CryptoContainer::load()
if (size == 0U)
return false;
uint64_t now = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::system_clock::now().time_since_epoch()).count();
m_lastLoadTime = now;
LogInfoEx(LOG_HOST, "Loaded %lu entries into crypto lookup table", size);
return true;

@ -245,11 +245,19 @@ public:
*/
void setReloadTime(uint32_t reloadTime) { m_reloadTime = reloadTime; }
/**
* @brief Returns the last load time of this lookup table.
* @return const uint64_t Last load time in milliseconds since epoch.
*/
const uint64_t lastLoadTime() const { return m_lastLoadTime; }
private:
std::string m_file;
std::string m_password;
uint32_t m_reloadTime;
uint64_t m_lastLoadTime;
bool m_enabled;
bool m_stop;

@ -90,8 +90,9 @@ void fatal(const char* msg, ...)
void usage(const char* message, const char* arg)
{
::fprintf(stdout, __PROG_NAME__ " %s (built %s)\r\n", __VER__, __BUILD__);
::fprintf(stdout, "Copyright (c) 2017-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");
::fprintf(stdout, "Copyright (c) 2017-2026 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");
::fprintf(stdout, HIGHLY_UNNECESSARY_DISCLAIMER_FOR_THE_MENTAL "\n\n");
if (message != nullptr) {
::fprintf(stderr, "%s: ", g_progExe.c_str());
::fprintf(stderr, message, arg);
@ -160,8 +161,9 @@ int checkArgs(int argc, char* argv[])
}
else if (IS("-v")) {
::fprintf(stdout, __PROG_NAME__ " %s (built %s)\r\n", __VER__, __BUILD__);
::fprintf(stdout, "Copyright (c) 2017-2025 Bryan Biedenkapp, N2PLL and DVMProject (https://github.com/dvmproject) Authors.\n");
::fprintf(stdout, "Copyright (c) 2017-2026 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");
::fprintf(stdout, HIGHLY_UNNECESSARY_DISCLAIMER_FOR_THE_MENTAL "\n");
if (argc == 2)
exit(EXIT_SUCCESS);
}

@ -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) 2023,2024,2025 Bryan Biedenkapp, N2PLL
* Copyright (C) 2023-2026 Bryan Biedenkapp, N2PLL
*
*/
#include "Defines.h"
@ -49,6 +49,7 @@ using namespace lookups;
#define IDLE_WARMUP_MS 5U
#define DEFAULT_MTU_SIZE 496
#define MAX_MTU_SIZE 65535
// ---------------------------------------------------------------------------
// Public Class Members
@ -60,7 +61,7 @@ HostFNE::HostFNE(const std::string& confFile) :
m_confFile(confFile),
m_conf(),
m_network(nullptr),
m_diagNetwork(nullptr),
m_mdNetwork(nullptr),
m_vtunEnabled(false),
m_packetDataMode(PacketDataMode::PROJECT25),
#if !defined(_WIN32)
@ -79,7 +80,6 @@ HostFNE::HostFNE(const std::string& confFile) :
m_maxMissedPings(5U),
m_updateLookupTime(10U),
m_peerReplicaSavesACL(false),
m_useAlternatePortForDiagnostics(false),
m_allowActivityTransfer(false),
m_allowDiagnosticTransfer(false),
m_RESTAPI(nullptr)
@ -162,8 +162,9 @@ int HostFNE::run()
#endif // !defined(_WIN32)
::LogInfo(__BANNER__ "\r\n" __PROG_NAME__ " " __VER__ " (built " __BUILD__ ")\r\n" \
"Copyright (c) 2017-2025 Bryan Biedenkapp, N2PLL and DVMProject (https://github.com/dvmproject) Authors.\r\n" \
"Copyright (c) 2017-2026 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" \
HIGHLY_UNNECESSARY_DISCLAIMER_FOR_THE_MENTAL "\r\n" \
">> Fixed Network Equipment\r\n");
// read base parameters from configuration
@ -210,9 +211,9 @@ int HostFNE::run()
** Initialize Threads
*/
if (!Thread::runAsThread(this, threadMasterNetwork))
if (!Thread::runAsThread(this, threadTrafficNetwork))
return EXIT_FAILURE;
if (!Thread::runAsThread(this, threadDiagNetwork))
if (!Thread::runAsThread(this, threadMetadataNetwork))
return EXIT_FAILURE;
#if !defined(_WIN32)
if (!Thread::runAsThread(this, threadVirtualNetworking))
@ -243,8 +244,8 @@ int HostFNE::run()
// clock master
if (m_network != nullptr)
m_network->clock(ms);
if (m_diagNetwork != nullptr)
m_diagNetwork->clock(ms);
if (m_mdNetwork != nullptr)
m_mdNetwork->clock(ms);
// clock peers
for (auto network : m_peerNetworks) {
@ -258,7 +259,7 @@ int HostFNE::run()
if (m_vtunEnabled) {
switch (m_packetDataMode) {
case PacketDataMode::DMR:
// TODO: not supported yet
m_network->dmrTrafficHandler()->packetData()->clock(ms);
break;
case PacketDataMode::PROJECT25:
@ -278,9 +279,9 @@ int HostFNE::run()
delete m_network;
}
if (m_diagNetwork != nullptr) {
m_diagNetwork->close();
delete m_diagNetwork;
if (m_mdNetwork != nullptr) {
m_mdNetwork->close();
delete m_mdNetwork;
}
for (auto network : m_peerNetworks) {
@ -341,6 +342,13 @@ bool HostFNE::readParams()
bool sendTalkgroups = systemConf["sendTalkgroups"].as<bool>(true);
m_peerReplicaSavesACL = systemConf["peerReplicaSaveACL"].as<bool>(false);
bool iAgreeNotToBeStupid = m_conf["iAgreeNotToBeStupid"].as<bool>(false);
if (!iAgreeNotToBeStupid) {
LogError(LOG_HOST, HIGHLY_UNNECESSARY_DISCLAIMER_FOR_THE_MENTAL);
LogError(LOG_HOST, "You must agree to software license terms, and not to be stupid to use this software. Please set 'iAgreeNotToBeStupid' in the configuration file properly.");
return false;
}
if (m_pingTime == 0U) {
m_pingTime = 5U;
}
@ -358,15 +366,9 @@ bool HostFNE::readParams()
m_updateLookupTime = 10U;
}
m_useAlternatePortForDiagnostics = systemConf["useAlternatePortForDiagnostics"].as<bool>(true);
m_allowActivityTransfer = systemConf["allowActivityTransfer"].as<bool>(true);
m_allowDiagnosticTransfer = systemConf["allowDiagnosticTransfer"].as<bool>(true);
if (!m_useAlternatePortForDiagnostics) {
LogWarning(LOG_HOST, "Alternate port for diagnostics and activity logging is disabled, this severely limits functionality and will prevent peer connections from transmitting diagnostic and activity logging to this FNE!");
LogWarning(LOG_HOST, "It is *not* recommended to disable the \"useAlternatePortForDiagnostics\" option.");
}
if (!m_allowActivityTransfer) {
LogWarning(LOG_HOST, "Peer activity logging is disabled, this severely limits functionality and can prevent proper operations by prohibiting activity logging to this FNE!");
LogWarning(LOG_HOST, "It is *not* recommended to disable the \"allowActivityTransfer\" option.");
@ -387,10 +389,6 @@ bool HostFNE::readParams()
LogInfo(" Send Talkgroups: %s", sendTalkgroups ? "yes" : "no");
LogInfo(" Peer Replication ACL is retained: %s", m_peerReplicaSavesACL ? "yes" : "no");
if (m_useAlternatePortForDiagnostics)
LogInfo(" Use Alternate Port for Diagnostics: yes");
else
LogInfo(" !! Use Alternate Port for Diagnostics: no");
if (m_allowActivityTransfer)
LogInfo(" Allow Activity Log Transfer: yes");
else
@ -520,7 +518,7 @@ bool HostFNE::initializeRESTAPI()
// initialize network remote command
if (restApiEnable) {
m_RESTAPI = new RESTAPI(restApiAddress, restApiPort, restApiPassword, restApiSSLKey, restApiSSLCert, restApiEnableSSL, this, restApiDebug);
m_RESTAPI->setLookups(m_ridLookup, m_tidLookup, m_peerListLookup, m_adjSiteMapLookup);
m_RESTAPI->setLookups(m_ridLookup, m_tidLookup, m_peerListLookup, m_adjSiteMapLookup, m_cryptoLookup);
bool ret = m_RESTAPI->open();
if (!ret) {
delete m_RESTAPI;
@ -549,6 +547,7 @@ bool HostFNE::createMasterNetwork()
uint32_t id = masterConf["peerId"].as<uint32_t>(1001U);
std::string password = masterConf["password"].as<std::string>();
bool verbose = masterConf["verbose"].as<bool>(false);
bool packetDump = masterConf["packetDump"].as<bool>(false);
bool debug = masterConf["debug"].as<bool>(false);
bool kmfDebug = masterConf["kmfDebug"].as<bool>(false);
uint16_t workerCnt = (uint16_t)masterConf["workers"].as<uint32_t>(16U);
@ -616,7 +615,8 @@ bool HostFNE::createMasterNetwork()
LogInfo(" Identity: %s", identity.c_str());
LogInfo(" Peer ID: %u", id);
LogInfo(" Address: %s", address.c_str());
LogInfo(" Port: %u", port);
LogInfo(" Traffic Port: %u", port);
LogInfo(" Metadata Port: %u", port + 1U);
LogInfo(" Allow DMR Traffic: %s", m_dmrEnabled ? "yes" : "no");
LogInfo(" Allow P25 Traffic: %s", m_p25Enabled ? "yes" : "no");
LogInfo(" Allow NXDN Traffic: %s", m_nxdnEnabled ? "yes" : "no");
@ -634,6 +634,10 @@ bool HostFNE::createMasterNetwork()
LogInfo(" Verbose: yes");
}
if (packetDump) {
LogInfo(" Packet Dump: yes");
}
if (debug) {
LogInfo(" Debug: yes");
}
@ -642,12 +646,13 @@ bool HostFNE::createMasterNetwork()
LogInfo(" P25 OTAR KMF Services Debug: yes");
}
// initialize networking
m_network = new FNENetwork(this, address, port, id, password, identity, debug, kmfDebug, verbose, reportPeerPing,
// initialize traffic networking
m_network = new TrafficNetwork(this, address, port, id, password, identity, debug, kmfDebug, verbose, reportPeerPing,
m_dmrEnabled, m_p25Enabled, m_nxdnEnabled, m_analogEnabled,
parrotDelay, parrotGrantDemand, m_allowActivityTransfer, m_allowDiagnosticTransfer,
m_pingTime, m_updateLookupTime, workerCnt);
m_network->setOptions(masterConf, true);
m_network->setPacketDump(packetDump);
m_network->setLookups(m_ridLookup, m_tidLookup, m_peerListLookup, m_cryptoLookup, m_adjSiteMapLookup);
@ -667,30 +672,29 @@ bool HostFNE::createMasterNetwork()
m_network->setPresharedKey(presharedKey);
}
// setup alternate port for diagnostics/activity logging
if (m_useAlternatePortForDiagnostics) {
m_diagNetwork = new DiagNetwork(this, m_network, address, port + 1U, workerCnt);
// initialize metadata networking
m_mdNetwork = new MetadataNetwork(this, m_network, address, port + 1U, workerCnt);
m_mdNetwork->setPacketDump(packetDump);
bool ret = m_diagNetwork->open();
if (!ret) {
delete m_diagNetwork;
m_diagNetwork = nullptr;
LogError(LOG_HOST, "failed to initialize diagnostic log networking!");
m_useAlternatePortForDiagnostics = false; // this isn't fatal so just disable alternate port
}
else {
if (encrypted) {
m_diagNetwork->setPresharedKey(presharedKey);
}
ret = m_mdNetwork->open();
if (!ret) {
delete m_mdNetwork;
m_mdNetwork = nullptr;
LogError(LOG_HOST, "failed to initialize metadata networking!");
return false;
}
else {
if (encrypted) {
m_mdNetwork->setPresharedKey(presharedKey);
}
}
return true;
}
/* Entry point to master FNE network thread. */
/* Entry point to master traffic network thread. */
void* HostFNE::threadMasterNetwork(void* arg)
void* HostFNE::threadTrafficNetwork(void* arg)
{
thread_t* th = (thread_t*)arg;
if (th != nullptr) {
@ -700,7 +704,7 @@ void* HostFNE::threadMasterNetwork(void* arg)
::pthread_detach(th->thread);
#endif // defined(_WIN32)
std::string threadName("fne:net");
std::string threadName("fne:traf-net");
HostFNE* fne = static_cast<HostFNE*>(th->obj);
if (fne == nullptr) {
g_killed = true;
@ -739,9 +743,9 @@ void* HostFNE::threadMasterNetwork(void* arg)
return nullptr;
}
/* Entry point to master FNE diagnostics network thread. */
/* Entry point to master metadata network thread. */
void* HostFNE::threadDiagNetwork(void* arg)
void* HostFNE::threadMetadataNetwork(void* arg)
{
thread_t* th = (thread_t*)arg;
if (th != nullptr) {
@ -751,7 +755,7 @@ void* HostFNE::threadDiagNetwork(void* arg)
::pthread_detach(th->thread);
#endif // defined(_WIN32)
std::string threadName("fne:diag-net");
std::string threadName("fne:meta-net");
HostFNE* fne = static_cast<HostFNE*>(th->obj);
if (fne == nullptr) {
g_killed = true;
@ -763,11 +767,6 @@ void* HostFNE::threadDiagNetwork(void* arg)
return nullptr;
}
if (!fne->m_useAlternatePortForDiagnostics) {
delete th;
return nullptr;
}
LogInfoEx(LOG_HOST, "[ OK ] %s", threadName.c_str());
#ifdef _GNU_SOURCE
::pthread_setname_np(th->thread, threadName.c_str());
@ -776,12 +775,12 @@ void* HostFNE::threadDiagNetwork(void* arg)
StopWatch stopWatch;
stopWatch.start();
if (fne->m_diagNetwork != nullptr) {
if (fne->m_mdNetwork != nullptr) {
while (!g_killed) {
uint32_t ms = stopWatch.elapsed();
stopWatch.start();
fne->m_diagNetwork->processNetwork();
fne->m_mdNetwork->processNetwork();
if (ms < THREAD_CYCLE_THRESHOLD)
Thread::sleep(THREAD_CYCLE_THRESHOLD);
@ -819,6 +818,7 @@ bool HostFNE::createPeerNetworks()
uint16_t masterPort = (uint16_t)peerConf["masterPort"].as<uint32_t>(TRAFFIC_DEFAULT_PORT);
std::string password = peerConf["password"].as<std::string>();
uint32_t id = peerConf["peerId"].as<uint32_t>(1001U);
bool packetDump = peerConf["packetDump"].as<bool>(false);
bool debug = peerConf["debug"].as<bool>(false);
bool encrypted = peerConf["encrypted"].as<bool>(false);
@ -873,6 +873,7 @@ bool HostFNE::createPeerNetworks()
// initialize networking
network::PeerNetwork* network = new PeerNetwork(masterAddress, masterPort, 0U, id, password, true, debug, m_dmrEnabled, m_p25Enabled, m_nxdnEnabled, m_analogEnabled, true, true,
m_allowActivityTransfer, m_allowDiagnosticTransfer, false, false);
network->setPacketDump(packetDump);
network->setMetadata(identity, 0U, 0U, 0.0F, 0.0F, 0, 0, 0, latitude, longitude, 0, location);
network->setLookups(m_ridLookup, m_tidLookup);
network->setMasterPeerId(masterPeerId);
@ -998,18 +999,18 @@ void* HostFNE::threadVirtualNetworking(void* arg)
uint32_t ms = stopWatch.elapsed();
stopWatch.start();
uint8_t packet[DEFAULT_MTU_SIZE];
::memset(packet, 0x00U, DEFAULT_MTU_SIZE);
uint8_t packet[MAX_MTU_SIZE];
::memset(packet, 0x00U, MAX_MTU_SIZE);
ssize_t len = fne->m_tun->read(packet);
if (len > 0) {
switch (fne->m_packetDataMode) {
case PacketDataMode::DMR:
// TODO: not supported yet
fne->m_network->dmrTrafficHandler()->packetData()->processPacketFrame(packet, (uint32_t)len);
break;
case PacketDataMode::PROJECT25:
fne->m_network->p25TrafficHandler()->packetData()->processPacketFrame(packet, DEFAULT_MTU_SIZE);
fne->m_network->p25TrafficHandler()->packetData()->processPacketFrame(packet, (uint32_t)len);
break;
}
}

@ -24,8 +24,8 @@
#include "common/network/viface/VIFace.h"
#include "common/yaml/Yaml.h"
#include "common/Timer.h"
#include "network/FNENetwork.h"
#include "network/DiagNetwork.h"
#include "network/TrafficNetwork.h"
#include "network/MetadataNetwork.h"
#include "network/PeerNetwork.h"
#include "restapi/RESTAPI.h"
#include "CryptoContainer.h"
@ -83,16 +83,16 @@ private:
const std::string& m_confFile;
yaml::Node m_conf;
friend class network::FNENetwork;
friend class network::DiagNetwork;
friend class network::TrafficNetwork;
friend class network::MetadataNetwork;
friend class network::callhandler::TagDMRData;
friend class network::callhandler::packetdata::DMRPacketData;
friend class network::callhandler::TagP25Data;
friend class network::callhandler::packetdata::P25PacketData;
friend class network::callhandler::TagNXDNData;
friend class network::callhandler::TagAnalogData;
network::FNENetwork* m_network;
network::DiagNetwork* m_diagNetwork;
network::TrafficNetwork* m_network;
network::MetadataNetwork* m_mdNetwork;
bool m_vtunEnabled;
PacketDataMode m_packetDataMode;
@ -120,8 +120,6 @@ private:
bool m_peerReplicaSavesACL;
bool m_useAlternatePortForDiagnostics;
bool m_allowActivityTransfer;
bool m_allowDiagnosticTransfer;
@ -144,17 +142,17 @@ private:
*/
bool createMasterNetwork();
/**
* @brief Entry point to master FNE network thread.
* @brief Entry point to master traffic network thread.
* @param arg Instance of the thread_t structure.
* @returns void* (Ignore)
*/
static void* threadMasterNetwork(void* arg);
static void* threadTrafficNetwork(void* arg);
/**
* @brief Entry point to master FNE diagnostics network thread.
* @brief Entry point to master metadata network thread.
* @param arg Instance of the thread_t structure.
* @returns void* (Ignore)
*/
static void* threadDiagNetwork(void* arg);
static void* threadMetadataNetwork(void* arg);
/**
* @brief Initializes peer FNE network connectivity.
* @returns bool True, if network connectivity was initialized, otherwise false.

@ -0,0 +1,70 @@
// SPDX-License-Identifier: GPL-2.0-only
/*
* Digital Voice Modem - Converged FNE Software
* 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 "common/Log.h"
#include "network/FNEPeerConnection.h"
using namespace network;
// ---------------------------------------------------------------------------
// Public Class Members
// ---------------------------------------------------------------------------
/* Gets or creates a jitter buffer for the specified stream. */
AdaptiveJitterBuffer* FNEPeerConnection::getOrCreateJitterBuffer(uint64_t streamId)
{
std::lock_guard<std::mutex> lock(m_jitterMutex);
if (m_jitterBuffers.find(streamId) == m_jitterBuffers.end()) {
m_jitterBuffers[streamId] = new AdaptiveJitterBuffer(m_jitterMaxSize, m_jitterMaxWait);
}
return m_jitterBuffers[streamId];
}
/* Cleans up jitter buffer for the specified stream. */
void FNEPeerConnection::cleanupJitterBuffer(uint64_t streamId)
{
std::lock_guard<std::mutex> lock(m_jitterMutex);
auto it = m_jitterBuffers.find(streamId);
if (it != m_jitterBuffers.end()) {
delete it->second;
m_jitterBuffers.erase(it);
}
}
/* Checks for timed-out buffered frames across all streams. */
void FNEPeerConnection::checkJitterTimeouts()
{
if (!m_jitterBufferEnabled) {
return;
}
std::lock_guard<std::mutex> lock(m_jitterMutex);
// check timeouts for all active jitter buffers
for (auto& pair : m_jitterBuffers) {
AdaptiveJitterBuffer* buffer = pair.second;
if (buffer != nullptr) {
std::vector<BufferedFrame*> timedOutFrames;
buffer->checkTimeouts(timedOutFrames);
// note: timed-out frames are handled by the calling context
// this method just ensures the buffers are checked periodically
// the frames themselves are cleaned up by the caller
for (BufferedFrame* frame : timedOutFrames) {
delete frame;
}
}
}
}

@ -10,14 +10,18 @@
/**
* @file FNEPeerConnection.h
* @ingroup fne_network
* @file FNEPeerConnection.cpp
* @ingroup fne_network
*/
#if !defined(__FNE_PEER_CONNECTION_H__)
#define __FNE_PEER_CONNECTION_H__
#include "fne/Defines.h"
#include "common/network/BaseNetwork.h"
#include "common/network/AdaptiveJitterBuffer.h"
#include <string>
#include <map>
#include <shared_mutex>
namespace network
@ -60,7 +64,12 @@ namespace network
m_isConventionalPeer(false),
m_isSysView(false),
m_config(),
m_peerLockMtx()
m_peerLockMtx(),
m_jitterBuffers(),
m_jitterMutex(),
m_jitterBufferEnabled(false),
m_jitterMaxSize(4U),
m_jitterMaxWait(40000U)
{
/* stub */
}
@ -91,7 +100,12 @@ namespace network
m_isConventionalPeer(false),
m_isSysView(false),
m_config(),
m_peerLockMtx()
m_peerLockMtx(),
m_jitterBuffers(),
m_jitterMutex(),
m_jitterBufferEnabled(false),
m_jitterMaxSize(4U),
m_jitterMaxWait(40000U)
{
assert(id > 0U);
assert(sockStorageLen > 0U);
@ -124,6 +138,43 @@ namespace network
*/
inline void unlock() const { m_peerLockMtx.unlock(); }
/**
* @brief Gets or creates a jitter buffer for the specified stream.
* @param streamId Stream ID.
* @returns AdaptiveJitterBuffer* Jitter buffer instance.
*/
AdaptiveJitterBuffer* getOrCreateJitterBuffer(uint64_t streamId);
/**
* @brief Cleans up jitter buffer for the specified stream.
* @param streamId Stream ID.
*/
void cleanupJitterBuffer(uint64_t streamId);
/**
* @brief Checks for timed-out buffered frames across all streams.
*/
void checkJitterTimeouts();
/**
* @brief Gets jitter buffer enabled state.
* @returns bool True if jitter buffer is enabled.
*/
bool jitterBufferEnabled() const { return m_jitterBufferEnabled; }
/**
* @brief Sets jitter buffer parameters.
* @param enabled Enable/disable jitter buffer.
* @param maxSize Maximum buffer size in frames.
* @param maxWait Maximum wait time in microseconds.
*/
void setJitterBufferParams(bool enabled, uint16_t maxSize = 4U, uint32_t maxWait = 40000U)
{
m_jitterBufferEnabled = enabled;
m_jitterMaxSize = maxSize;
m_jitterMaxWait = maxWait;
}
public:
/**
* @brief Peer ID.
@ -218,6 +269,13 @@ namespace network
private:
mutable std::mutex m_peerLockMtx;
std::map<uint64_t, AdaptiveJitterBuffer*> m_jitterBuffers;
mutable std::mutex m_jitterMutex;
bool m_jitterBufferEnabled;
uint16_t m_jitterMaxSize;
uint32_t m_jitterMaxWait;
};
} // namespace network

@ -11,7 +11,7 @@
#include "common/zlib/Compression.h"
#include "common/Log.h"
#include "common/Utils.h"
#include "network/DiagNetwork.h"
#include "network/MetadataNetwork.h"
#include "fne/ActivityLog.h"
#include "HostFNE.h"
@ -25,11 +25,11 @@ using namespace compress;
// Public Class Members
// ---------------------------------------------------------------------------
/* Initializes a new instance of the DiagNetwork class. */
/* Initializes a new instance of the MetadataNetwork class. */
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),
MetadataNetwork::MetadataNetwork(HostFNE* host, TrafficNetwork* trafficNetwork, const std::string& address, uint16_t port, uint16_t workerCnt) :
BaseNetwork(trafficNetwork->m_peerId, true, trafficNetwork->m_debug, true, true, trafficNetwork->m_allowActivityTransfer, trafficNetwork->m_allowDiagnosticTransfer),
m_trafficNetwork(trafficNetwork),
m_host(host),
m_address(address),
m_port(port),
@ -38,26 +38,26 @@ DiagNetwork::DiagNetwork(HostFNE* host, FNENetwork* fneNetwork, const std::strin
m_peerTreeListPkt(),
m_threadPool(workerCnt, "diag")
{
assert(fneNetwork != nullptr);
assert(trafficNetwork != nullptr);
assert(host != nullptr);
assert(!address.empty());
assert(port > 0U);
}
/* Finalizes a instance of the DiagNetwork class. */
/* Finalizes a instance of the MetadataNetwork class. */
DiagNetwork::~DiagNetwork() = default;
MetadataNetwork::~MetadataNetwork() = default;
/* Sets endpoint preshared encryption key. */
void DiagNetwork::setPresharedKey(const uint8_t* presharedKey)
void MetadataNetwork::setPresharedKey(const uint8_t* presharedKey)
{
m_socket->setPresharedKey(presharedKey);
}
/* Process a data frames from the network. */
void DiagNetwork::processNetwork()
void MetadataNetwork::processNetwork()
{
if (m_status != NET_STAT_MST_RUNNING) {
return;
@ -73,13 +73,13 @@ void DiagNetwork::processNetwork()
UInt8Array buffer = m_frameQueue->read(length, address, addrLen, &rtpHeader, &fneHeader);
if (length > 0) {
if (m_debug)
Utils::dump(1U, "DiagNetwork::processNetwork(), Network Message", buffer.get(), length);
Utils::dump(1U, "MetadataNetwork::processNetwork(), Network Message", buffer.get(), length);
uint32_t peerId = fneHeader.getPeerId();
NetPacketRequest* req = new NetPacketRequest();
req->obj = m_fneNetwork;
req->diagObj = this;
req->obj = m_trafficNetwork;
req->metadataObj = this;
req->peerId = peerId;
req->address = address;
@ -105,7 +105,7 @@ void DiagNetwork::processNetwork()
/* Updates the timer by the passed number of milliseconds. */
void DiagNetwork::clock(uint32_t ms)
void MetadataNetwork::clock(uint32_t ms)
{
if (m_status != NET_STAT_MST_RUNNING) {
return;
@ -114,7 +114,7 @@ void DiagNetwork::clock(uint32_t ms)
/* Opens connection to the network. */
bool DiagNetwork::open()
bool MetadataNetwork::open()
{
if (m_debug)
LogInfoEx(LOG_DIAG, "Opening Network");
@ -143,7 +143,7 @@ bool DiagNetwork::open()
/* Closes connection to the network. */
void DiagNetwork::close()
void MetadataNetwork::close()
{
if (m_debug)
LogInfoEx(LOG_DIAG, "Closing Network");
@ -162,10 +162,10 @@ void DiagNetwork::close()
/* Process a data frames from the network. */
void DiagNetwork::taskNetworkRx(NetPacketRequest* req)
void MetadataNetwork::taskNetworkRx(NetPacketRequest* req)
{
if (req != nullptr) {
FNENetwork* network = static_cast<FNENetwork*>(req->obj);
TrafficNetwork* network = static_cast<TrafficNetwork*>(req->obj);
if (network == nullptr) {
if (req != nullptr) {
if (req->buffer != nullptr)
@ -176,8 +176,8 @@ void DiagNetwork::taskNetworkRx(NetPacketRequest* req)
return;
}
DiagNetwork* diagNetwork = static_cast<DiagNetwork*>(req->diagObj);
if (diagNetwork == nullptr) {
MetadataNetwork* mdNetwork = static_cast<MetadataNetwork*>(req->metadataObj);
if (mdNetwork == nullptr) {
if (req != nullptr) {
if (req->buffer != nullptr)
delete[] req->buffer;
@ -396,18 +396,18 @@ void DiagNetwork::taskNetworkRx(NetPacketRequest* req)
DECLARE_UINT8_ARRAY(rawPayload, req->length);
::memcpy(rawPayload, req->buffer, req->length);
// Utils::dump(1U, "DiagNetwork::taskNetworkRx(), REPL_ACT_PEER_LIST, Raw Payload", rawPayload, req->length);
// Utils::dump(1U, "MetadataNetwork::taskNetworkRx(), REPL_ACT_PEER_LIST, Raw Payload", rawPayload, req->length);
if (diagNetwork->m_peerReplicaActPkt.find(peerId) == diagNetwork->m_peerReplicaActPkt.end()) {
diagNetwork->m_peerReplicaActPkt.insert(peerId, DiagNetwork::PacketBufferEntry());
if (mdNetwork->m_peerReplicaActPkt.find(peerId) == mdNetwork->m_peerReplicaActPkt.end()) {
mdNetwork->m_peerReplicaActPkt.insert(peerId, MetadataNetwork::PacketBufferEntry());
DiagNetwork::PacketBufferEntry& pkt = diagNetwork->m_peerReplicaActPkt[peerId];
MetadataNetwork::PacketBufferEntry& pkt = mdNetwork->m_peerReplicaActPkt[peerId];
pkt.buffer = new PacketBuffer(true, "Peer Replication, Active Peer List");
pkt.streamId = streamId;
pkt.locked = false;
} else {
DiagNetwork::PacketBufferEntry& pkt = diagNetwork->m_peerReplicaActPkt[peerId];
MetadataNetwork::PacketBufferEntry& pkt = mdNetwork->m_peerReplicaActPkt[peerId];
if (!pkt.locked && pkt.streamId != streamId) {
LogError(LOG_REPL, "PEER %u (%s) Peer Replication, Active Peer List, stream ID mismatch, expected %u, got %u", peerId,
connection->identWithQualifier().c_str(), pkt.streamId, streamId);
@ -421,7 +421,7 @@ void DiagNetwork::taskNetworkRx(NetPacketRequest* req)
}
}
DiagNetwork::PacketBufferEntry& pkt = diagNetwork->m_peerReplicaActPkt[peerId];
MetadataNetwork::PacketBufferEntry& pkt = mdNetwork->m_peerReplicaActPkt[peerId];
if (pkt.locked) {
while (pkt.locked)
Thread::sleep(1U);
@ -433,7 +433,7 @@ void DiagNetwork::taskNetworkRx(NetPacketRequest* req)
uint8_t* decompressed = nullptr;
if (pkt.buffer->decode(rawPayload, &decompressed, &decompressedLen)) {
diagNetwork->m_peerReplicaActPkt.lock();
mdNetwork->m_peerReplicaActPkt.lock();
std::string payload(decompressed + 8U, decompressed + decompressedLen);
// parse JSON body
@ -446,8 +446,8 @@ void DiagNetwork::taskNetworkRx(NetPacketRequest* req)
if (decompressed != nullptr) {
delete[] decompressed;
}
diagNetwork->m_peerReplicaActPkt.unlock();
diagNetwork->m_peerReplicaActPkt.erase(peerId);
mdNetwork->m_peerReplicaActPkt.unlock();
mdNetwork->m_peerReplicaActPkt.erase(peerId);
break;
}
else {
@ -460,8 +460,8 @@ void DiagNetwork::taskNetworkRx(NetPacketRequest* req)
if (decompressed != nullptr) {
delete[] decompressed;
}
diagNetwork->m_peerReplicaActPkt.unlock();
diagNetwork->m_peerReplicaActPkt.erase(peerId);
mdNetwork->m_peerReplicaActPkt.unlock();
mdNetwork->m_peerReplicaActPkt.erase(peerId);
break;
}
else {
@ -477,8 +477,8 @@ void DiagNetwork::taskNetworkRx(NetPacketRequest* req)
if (decompressed != nullptr) {
delete[] decompressed;
}
diagNetwork->m_peerReplicaActPkt.unlock();
diagNetwork->m_peerReplicaActPkt.erase(peerId);
mdNetwork->m_peerReplicaActPkt.unlock();
mdNetwork->m_peerReplicaActPkt.erase(peerId);
} else {
pkt.locked = false;
}
@ -535,7 +535,7 @@ void DiagNetwork::taskNetworkRx(NetPacketRequest* req)
if (network->m_debug) {
std::string address = __IP_FROM_UINT(rxEntry.masterIP);
LogDebugEx(LOG_REPL, "DiagNetwork::taskNetworkRx", "PEER %u (%s) Peer Replication, HA Parameters, %s:%u", peerId, connection->identWithQualifier().c_str(),
LogDebugEx(LOG_REPL, "MetadataNetwork::taskNetworkRx", "PEER %u (%s) Peer Replication, HA Parameters, %s:%u", peerId, connection->identWithQualifier().c_str(),
address.c_str(), rxEntry.masterPort);
}
}
@ -585,18 +585,18 @@ void DiagNetwork::taskNetworkRx(NetPacketRequest* req)
DECLARE_UINT8_ARRAY(rawPayload, req->length);
::memcpy(rawPayload, req->buffer, req->length);
// Utils::dump(1U, "DiagNetwork::taskNetworkRx(), NET_TREE_LIST, Raw Payload", rawPayload, req->length);
// Utils::dump(1U, "MetadataNetwork::taskNetworkRx(), NET_TREE_LIST, Raw Payload", rawPayload, req->length);
if (diagNetwork->m_peerTreeListPkt.find(peerId) == diagNetwork->m_peerTreeListPkt.end()) {
diagNetwork->m_peerTreeListPkt.insert(peerId, DiagNetwork::PacketBufferEntry());
if (mdNetwork->m_peerTreeListPkt.find(peerId) == mdNetwork->m_peerTreeListPkt.end()) {
mdNetwork->m_peerTreeListPkt.insert(peerId, MetadataNetwork::PacketBufferEntry());
DiagNetwork::PacketBufferEntry& pkt = diagNetwork->m_peerTreeListPkt[peerId];
MetadataNetwork::PacketBufferEntry& pkt = mdNetwork->m_peerTreeListPkt[peerId];
pkt.buffer = new PacketBuffer(true, "Network Tree, Tree List");
pkt.streamId = streamId;
pkt.locked = false;
} else {
DiagNetwork::PacketBufferEntry& pkt = diagNetwork->m_peerTreeListPkt[peerId];
MetadataNetwork::PacketBufferEntry& pkt = mdNetwork->m_peerTreeListPkt[peerId];
if (!pkt.locked && pkt.streamId != streamId) {
LogError(LOG_STP, "PEER %u (%s) Network Tree, Tree List, stream ID mismatch, expected %u, got %u", peerId,
connection->identWithQualifier().c_str(), pkt.streamId, streamId);
@ -610,7 +610,7 @@ void DiagNetwork::taskNetworkRx(NetPacketRequest* req)
}
}
DiagNetwork::PacketBufferEntry& pkt = diagNetwork->m_peerTreeListPkt[peerId];
MetadataNetwork::PacketBufferEntry& pkt = mdNetwork->m_peerTreeListPkt[peerId];
if (pkt.locked) {
while (pkt.locked)
Thread::sleep(1U);
@ -622,7 +622,7 @@ void DiagNetwork::taskNetworkRx(NetPacketRequest* req)
uint8_t* decompressed = nullptr;
if (pkt.buffer->decode(rawPayload, &decompressed, &decompressedLen)) {
diagNetwork->m_peerTreeListPkt.lock();
mdNetwork->m_peerTreeListPkt.lock();
std::string payload(decompressed + 8U, decompressed + decompressedLen);
// parse JSON body
@ -635,8 +635,8 @@ void DiagNetwork::taskNetworkRx(NetPacketRequest* req)
if (decompressed != nullptr) {
delete[] decompressed;
}
diagNetwork->m_peerTreeListPkt.unlock();
diagNetwork->m_peerTreeListPkt.erase(peerId);
mdNetwork->m_peerTreeListPkt.unlock();
mdNetwork->m_peerTreeListPkt.erase(peerId);
break;
}
else {
@ -649,8 +649,8 @@ void DiagNetwork::taskNetworkRx(NetPacketRequest* req)
if (decompressed != nullptr) {
delete[] decompressed;
}
diagNetwork->m_peerTreeListPkt.unlock();
diagNetwork->m_peerTreeListPkt.erase(peerId);
mdNetwork->m_peerTreeListPkt.unlock();
mdNetwork->m_peerTreeListPkt.erase(peerId);
break;
}
else {
@ -680,8 +680,8 @@ void DiagNetwork::taskNetworkRx(NetPacketRequest* req)
if (decompressed != nullptr) {
delete[] decompressed;
}
diagNetwork->m_peerTreeListPkt.unlock();
diagNetwork->m_peerTreeListPkt.erase(peerId);
mdNetwork->m_peerTreeListPkt.unlock();
mdNetwork->m_peerTreeListPkt.erase(peerId);
} else {
pkt.locked = false;
}

@ -8,18 +8,18 @@
*
*/
/**
* @file DiagNetwork.h
* @file MetadataNetwork.h
* @ingroup fne_network
* @file DiagNetwork.cpp
* @file MetadataNetwork.cpp
* @ingroup fne_network
*/
#if !defined(__DIAG_NETWORK_H__)
#define __DIAG_NETWORK_H__
#if !defined(__METADATA_NETWORK_H__)
#define __METADATA_NETWORK_H__
#include "fne/Defines.h"
#include "common/network/BaseNetwork.h"
#include "common/ThreadPool.h"
#include "fne/network/FNENetwork.h"
#include "fne/network/TrafficNetwork.h"
#include <string>
@ -39,21 +39,21 @@ namespace network
* @brief Implements the diagnostic/activity log networking logic.
* @ingroup fne_network
*/
class HOST_SW_API DiagNetwork : public BaseNetwork {
class HOST_SW_API MetadataNetwork : public BaseNetwork {
public:
/**
* @brief Initializes a new instance of the DiagNetwork class.
* @brief Initializes a new instance of the MetadataNetwork class.
* @param host Instance of the HostFNE class.
* @param network Instance of the FNENetwork class.
* @param network Instance of the TrafficNetwork 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, uint16_t workerCnt);
MetadataNetwork(HostFNE* host, TrafficNetwork* trafficNetwork, const std::string& address, uint16_t port, uint16_t workerCnt);
/**
* @brief Finalizes a instance of the DiagNetwork class.
* @brief Finalizes a instance of the MetadataNetwork class.
*/
~DiagNetwork() override;
~MetadataNetwork() override;
/**
* @brief Gets the current status of the network.
@ -90,8 +90,8 @@ namespace network
void close() override;
private:
friend class FNENetwork;
FNENetwork* m_fneNetwork;
friend class TrafficNetwork;
TrafficNetwork* m_trafficNetwork;
HostFNE* m_host;
std::string m_address;
@ -129,4 +129,4 @@ namespace network
};
} // namespace network
#endif // __FNE_NETWORK_H__
#endif // __METADATA_NETWORK_H__

@ -12,7 +12,7 @@
#include "common/Log.h"
#include "common/Thread.h"
#include "common/Utils.h"
#include "network/FNENetwork.h"
#include "network/TrafficNetwork.h"
#include "network/P25OTARService.h"
#include "HostFNE.h"
@ -36,7 +36,7 @@ using namespace p25::kmm;
// Macro helper to verbose log a generic KMM.
#define VERBOSE_LOG_KMM(_PCKT_STR, __LLID) \
if (m_verbose) { \
LogInfoEx(LOG_P25, "KMM, %s, llId = %u", _PCKT_STR.c_str(), __LLID); \
LogInfoEx(LOG_P25, "KMM, %s, llId = %u", _PCKT_STR.c_str(), __LLID); \
}
// ---------------------------------------------------------------------------
@ -51,7 +51,7 @@ using namespace p25::kmm;
/* Initializes a new instance of the P25OTARService class. */
P25OTARService::P25OTARService(FNENetwork* network, P25PacketData* packetData, bool debug, bool verbose) :
P25OTARService::P25OTARService(TrafficNetwork* network, P25PacketData* packetData, bool debug, bool verbose) :
m_socket(nullptr),
m_frameQueue(nullptr),
m_threadPool(MAX_THREAD_CNT, "otar"),
@ -383,8 +383,24 @@ UInt8Array P25OTARService::processKMM(const uint8_t* data, uint32_t len, uint32_
}
// respond with No-Service if KMF services are disabled
// if (!m_network->m_kmfServicesEnabled)
if (!m_network->m_kmfServicesEnabled)
return write_KMM_NoService(llId, kmm->getSrcLLId(), payloadSize);
else {
if (kmm->getFlag() == KMM_HelloFlag::REKEY_REQUEST_UKEK ||
(kmm->getFlag() == KMM_HelloFlag::REKEY_REQUEST_NO_UKEK && m_allowNoUKEKRekey)) {
// send rekey-command
EKCKeyItem keyItem = m_network->m_cryptoLookup->findUKEK(llId);
if (keyItem.isInvalid()) {
LogInfoEx(LOG_P25, P25_KMM_STR ", %s, no UKEK found for rekey request, llId = %u", kmm->toString().c_str(), llId);
return write_KMM_NoService(llId, kmm->getSrcLLId(), payloadSize);
} else {
return write_KMM_Rekey_Command(llId, kmm->getSrcLLId(), kmm->getFlag(), payloadSize);
}
} else {
LogInfoEx(LOG_P25, P25_KMM_STR ", %s, rekey request denied, llId = %u", kmm->toString().c_str(), llId);
return write_KMM_NoService(llId, kmm->getSrcLLId(), payloadSize);
}
}
}
break;

@ -22,7 +22,7 @@
#include "common/p25/Crypto.h"
#include "common/network/udp/Socket.h"
#include "common/network/RawFrameQueue.h"
#include "network/FNENetwork.h"
#include "network/TrafficNetwork.h"
#include "network/callhandler/packetdata/P25PacketData.h"
namespace network
@ -54,12 +54,12 @@ namespace network
public:
/**
* @brief Initializes a new instance of the P25OTARService class.
* @param network Instance of the FNENetwork class.
* @param network Instance of the TrafficNetwork class.
* @param packetData Instance of the P25PacketData class.
* @param debug Flag indicating whether debug is enabled.
* @param verbose Flag indicating whether verbose logging is enabled.
*/
P25OTARService(FNENetwork* network, network::callhandler::packetdata::P25PacketData* packetData, bool debug, bool verbose);
P25OTARService(TrafficNetwork* network, network::callhandler::packetdata::P25PacketData* packetData, bool debug, bool verbose);
/**
* @brief Finalizes a instance of the P25OTARService class.
*/
@ -102,7 +102,7 @@ namespace network
ThreadPool m_threadPool;
FNENetwork* m_network;
TrafficNetwork* m_network;
network::callhandler::packetdata::P25PacketData* m_packetData;
concurrent::unordered_map<uint32_t, uint16_t> m_rsiMessageNumber;

@ -29,7 +29,7 @@ using namespace compress;
#define WORKER_CNT 8U
const uint64_t PACKET_LATE_TIME = 200U; // 200ms
const uint64_t PACKET_LATE_TIME = 250U; // 250ms
// ---------------------------------------------------------------------------
// Public Class Members
@ -573,10 +573,10 @@ void PeerNetwork::taskNetworkRx(PeerPacketRequest* req)
return;
if (req->length > 0) {
// determine if this packet is late (i.e. are we processing this packet more than 200ms after it was received?)
// determine if this packet is late (i.e. are we processing this packet more than 250ms after it was received?)
uint64_t dt = req->pktRxTime + PACKET_LATE_TIME;
if (dt < now) {
LogWarning(LOG_PEER, "PEER %u packet processing latency >200ms, dt = %u, now = %u", req->peerId, dt, now);
LogWarning(LOG_PEER, "PEER %u packet processing latency >250ms, dt = %u, now = %u", req->peerId, dt, now);
}
uint16_t lastRxSeq = 0U;

@ -57,7 +57,7 @@ namespace network
// ---------------------------------------------------------------------------
/**
* @brief Implements the FNE peer networking logic.
* @brief Implements the FNE upstream peer networking logic.
* @ingroup fne_network
*/
class HOST_SW_API PeerNetwork : public Network {

@ -16,13 +16,13 @@
* @brief Implementation for the FNE call handlers.
* @ingroup fne_network
*
* @file FNENetwork.h
* @file TrafficNetwork.h
* @ingroup fne_network
* @file FNENetwork.cpp
* @file TrafficNetwork.cpp
* @ingroup fne_network
*/
#if !defined(__FNE_NETWORK_H__)
#define __FNE_NETWORK_H__
#if !defined(__TRAFFIC_NETWORK_H__)
#define __TRAFFIC_NETWORK_H__
#include "fne/Defines.h"
#include "common/concurrent/unordered_map.h"
@ -95,8 +95,8 @@ namespace network
// Class Prototypes
// ---------------------------------------------------------------------------
class HOST_SW_API DiagNetwork;
class HOST_SW_API FNENetwork;
class HOST_SW_API MetadataNetwork;
class HOST_SW_API TrafficNetwork;
// ---------------------------------------------------------------------------
// Structure Declaration
@ -120,7 +120,7 @@ namespace network
*/
struct NetPacketRequest : thread_t {
uint32_t peerId; //!< Peer ID for this request.
void* diagObj; //!< Network diagnostics network object.
void* metadataObj; //!< Network metadata network object.
sockaddr_storage address; //!< IP Address and Port.
uint32_t addrLen; //!<
@ -137,13 +137,13 @@ namespace network
// ---------------------------------------------------------------------------
/**
* @brief Implements the core FNE networking logic.
* @brief Implements the core traffic networking logic.
* @ingroup fne_network
*/
class HOST_SW_API FNENetwork : public BaseNetwork {
class HOST_SW_API TrafficNetwork : public BaseNetwork {
public:
/**
* @brief Initializes a new instance of the FNENetwork class.
* @brief Initializes a new instance of the TrafficNetwork class.
* @param host Instance of the HostFNE class.
* @param address Network Hostname/IP address to listen on.
* @param port Network port number.
@ -166,15 +166,15 @@ namespace network
* @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,
TrafficNetwork(HostFNE* host, const std::string& address, uint16_t port, uint32_t peerId, const std::string& password,
std::string identity, bool debug, bool kmfDebug, bool verbose, bool reportPeerPing,
bool dmr, bool p25, bool nxdn, bool analog,
uint32_t parrotDelay, bool parrotGrantDemand, bool allowActivityTransfer, bool allowDiagnosticTransfer,
uint32_t pingTime, uint32_t updateLookupTime, uint16_t workerCnt);
/**
* @brief Finalizes a instance of the FNENetwork class.
* @brief Finalizes a instance of the TrafficNetwork class.
*/
~FNENetwork() override;
~TrafficNetwork() override;
/**
* @brief Helper to set configuration options.
@ -289,7 +289,7 @@ namespace network
void setPeerReplica(bool replica);
private:
friend class DiagNetwork;
friend class MetadataNetwork;
friend class callhandler::TagDMRData;
friend class callhandler::packetdata::DMRPacketData;
callhandler::TagDMRData* m_tagDMR;
@ -323,6 +323,7 @@ namespace network
Timer m_parrotDelayTimer;
bool m_parrotGrantDemand;
bool m_parrotOnlyOriginating;
uint32_t m_parrotOverrideSrcId;
bool m_kmfServicesEnabled;
@ -340,7 +341,7 @@ namespace network
concurrent::unordered_map<uint32_t, json::array> m_peerReplicaPeers;
typedef std::pair<const uint32_t, lookups::AffiliationLookup*> PeerAffiliationMapPair;
concurrent::unordered_map<uint32_t, fne_lookups::AffiliationLookup*> m_peerAffiliations;
concurrent::unordered_map<uint32_t, std::vector<uint32_t>> m_ccPeerMap;
concurrent::shared_unordered_map<uint32_t, std::vector<uint32_t>> m_ccPeerMap;
static std::timed_mutex s_keyQueueMutex;
std::unordered_map<uint32_t, uint16_t> m_peerReplicaKeyQueue;
@ -393,12 +394,23 @@ namespace network
bool m_influxLogRawData;
influxdb::ServerInfo m_influxServer;
bool m_jitterBufferEnabled;
uint16_t m_jitterMaxSize;
uint32_t m_jitterMaxWait;
ThreadPool m_threadPool;
bool m_disablePacketData;
bool m_dumpPacketData;
bool m_verbosePacketData;
uint32_t m_sndcpStartAddr;
uint32_t m_sndcpEndAddr;
int32_t m_totalActiveCalls;
uint64_t m_totalCallsProcessed;
uint64_t m_totalCallCollisions;
bool m_logDenials;
bool m_logUpstreamCallStartEnd;
bool m_reportPeerPing;
@ -429,6 +441,13 @@ namespace network
*/
void logSpanningTree(FNEPeerConnection* connection = nullptr);
/**
* @brief Applies jitter buffer configuration to a peer connection.
* @param peerId Peer ID.
* @param connection Instance of the FNEPeerConnection class.
*/
void applyJitterBufferConfig(uint32_t peerId, FNEPeerConnection* connection);
/**
* @brief Erases a stream ID from the given peer ID connection.
* @param peerId Peer ID.
@ -785,4 +804,4 @@ namespace network
};
} // namespace network
#endif // __FNE_NETWORK_H__
#endif // __TRAFFIC_NETWORK_H__

@ -13,7 +13,7 @@
#include "common/Clock.h"
#include "common/Log.h"
#include "common/Utils.h"
#include "network/FNENetwork.h"
#include "network/TrafficNetwork.h"
#include "network/callhandler/TagAnalogData.h"
#include "HostFNE.h"
@ -33,7 +33,7 @@ using namespace analog::defines;
/* Initializes a new instance of the TagAnalogData class. */
TagAnalogData::TagAnalogData(FNENetwork* network, bool debug) :
TagAnalogData::TagAnalogData(TrafficNetwork* network, bool debug) :
m_network(network),
m_parrotFrames(),
m_parrotFramesReady(false),
@ -127,6 +127,12 @@ bool TagAnalogData::processFrame(const uint8_t* data, uint32_t len, uint32_t pee
else if (!fromUpstream)
LogInfoEx(LOG_MASTER, CALL_END_LOG);
if (!tg.config().parrot()) {
m_network->m_totalActiveCalls--;
if (m_network->m_totalActiveCalls < 0)
m_network->m_totalActiveCalls = 0;
}
// report call event to InfluxDB
if (m_network->m_enableInfluxDB) {
influxdb::QueryBuilder()
@ -212,6 +218,9 @@ bool TagAnalogData::processFrame(const uint8_t* data, uint32_t len, uint32_t pee
else {
LogWarning((fromUpstream) ? LOG_PEER : LOG_MASTER, "Analog, Call Collision, peer = %u, ssrc = %u, srcId = %u, dstId = %u, streamId = %u, rxPeer = %u, rxSrcId = %u, rxDstId = %u, rxStreamId = %u, fromUpstream = %u",
peerId, ssrc, srcId, dstId, streamId, status.peerId, status.srcId, status.dstId, status.streamId, fromUpstream);
m_network->m_totalCallCollisions++;
return false;
}
} else {
@ -265,6 +274,11 @@ bool TagAnalogData::processFrame(const uint8_t* data, uint32_t len, uint32_t pee
m_status[dstId].activeCall = true;
m_status.unlock();
if (!tg.config().parrot()) {
m_network->m_totalCallsProcessed++;
m_network->m_totalActiveCalls++;
}
#define CALL_START_LOG "Analog, Call Start, peer = %u, ssrc = %u, srcId = %u, dstId = %u, streamId = %u, fromUpstream = %u", peerId, ssrc, srcId, dstId, streamId, fromUpstream
if (m_network->m_logUpstreamCallStartEnd && fromUpstream)
LogInfoEx(LOG_PEER, CALL_START_LOG);
@ -434,6 +448,14 @@ void TagAnalogData::playbackParrot()
auto& pkt = m_parrotFrames[0];
m_parrotFrames.lock();
if (pkt.buffer != nullptr) {
// has the override source ID been set?
if (m_network->m_parrotOverrideSrcId > 0U) {
pkt.srcId = m_network->m_parrotOverrideSrcId;
// override source ID
SET_UINT24(m_network->m_parrotOverrideSrcId, pkt.buffer, 5U);
}
m_lastParrotPeerId = pkt.peerId;
m_lastParrotSrcId = pkt.srcId;
m_lastParrotDstId = pkt.dstId;

@ -23,7 +23,7 @@
#include "common/dmr/data/NetData.h"
#include "common/dmr/lc/CSBK.h"
#include "common/Clock.h"
#include "network/FNENetwork.h"
#include "network/TrafficNetwork.h"
#include "network/callhandler/packetdata/DMRPacketData.h"
namespace network
@ -35,17 +35,17 @@ namespace network
// ---------------------------------------------------------------------------
/**
* @brief Implements the analog call handler and data FNE networking logic.
* @brief Implements the analog call handler and data networking logic.
* @ingroup fne_callhandler
*/
class HOST_SW_API TagAnalogData {
public:
/**
* @brief Initializes a new instance of the TagAnalogData class.
* @param network Instance of the FNENetwork class.
* @param network Instance of the TrafficNetwork class.
* @param debug Flag indicating whether network debug is enabled.
*/
TagAnalogData(FNENetwork* network, bool debug);
TagAnalogData(TrafficNetwork* network, bool debug);
/**
* @brief Finalizes a instance of the TagAnalogData class.
*/
@ -112,7 +112,7 @@ namespace network
uint32_t lastParrotDstId() const { return m_lastParrotDstId; }
private:
FNENetwork* m_network;
TrafficNetwork* m_network;
/**
* @brief Represents a stored parrot frame.

@ -16,7 +16,7 @@
#include "common/Clock.h"
#include "common/Log.h"
#include "common/Utils.h"
#include "network/FNENetwork.h"
#include "network/TrafficNetwork.h"
#include "network/callhandler/TagDMRData.h"
#include "HostFNE.h"
#include "FNEMain.h"
@ -37,7 +37,7 @@ using namespace dmr::defines;
/* Initializes a new instance of the TagDMRData class. */
TagDMRData::TagDMRData(FNENetwork* network, bool debug) :
TagDMRData::TagDMRData(TrafficNetwork* network, bool debug) :
m_network(network),
m_parrotFrames(),
m_parrotFramesReady(false),
@ -207,6 +207,12 @@ bool TagDMRData::processFrame(const uint8_t* data, uint32_t len, uint32_t peerId
LogInfoEx(LOG_MASTER, CALL_END_LOG);
}
if (!tg.config().parrot()) {
m_network->m_totalActiveCalls--;
if (m_network->m_totalActiveCalls < 0)
m_network->m_totalActiveCalls = 0;
}
// report call event to InfluxDB
if (m_network->m_enableInfluxDB) {
influxdb::QueryBuilder()
@ -223,6 +229,16 @@ bool TagDMRData::processFrame(const uint8_t* data, uint32_t len, uint32_t peerId
}
m_network->eraseStreamPktSeq(peerId, streamId);
} else {
#define NONCALL_END_LOG "DMR, Non-Call Terminator, peer = %u, ssrc = %u, srcId = %u, dstId = %u, slot = %u, streamId = %u, fromUpstream = %u", peerId, ssrc, srcId, dstId, slotNo, streamId, fromUpstream
if (m_network->m_logUpstreamCallStartEnd && fromUpstream)
LogInfoEx(LOG_PEER, NONCALL_END_LOG);
else if (!fromUpstream)
LogInfoEx(LOG_MASTER, NONCALL_END_LOG);
m_status.lock(false);
m_status[dstId].callStartTime = pktTime; // because Non-Call Terminators can just happen lets reset the callStartTime to pktTime to prevent insane durations
m_status.unlock();
}
}
@ -310,6 +326,9 @@ bool TagDMRData::processFrame(const uint8_t* data, uint32_t len, uint32_t peerId
} else {
LogWarning((fromUpstream) ? LOG_PEER : LOG_MASTER, "DMR, Call Collision, peer = %u, ssrc = %u, srcId = %u, dstId = %u, slotNo = %u, streamId = %u, rxPeer = %u, rxSrcId = %u, rxDstId = %u, rxSlotNo = %u, rxStreamId = %u, fromUpstream = %u",
peerId, ssrc, srcId, dstId, slotNo, streamId, status.peerId, status.srcId, status.dstId, status.slotNo, status.streamId, fromUpstream);
m_network->m_totalCallCollisions++;
return false;
}
} else {
@ -366,6 +385,11 @@ bool TagDMRData::processFrame(const uint8_t* data, uint32_t len, uint32_t peerId
m_status[dstId].activeCall = true;
m_status.unlock();
if (!tg.config().parrot()) {
m_network->m_totalCallsProcessed++;
m_network->m_totalActiveCalls++;
}
// is this a private call?
if (flco == FLCO::PRIVATE) {
m_statusPVCall.lock(false);
@ -684,6 +708,21 @@ void TagDMRData::playbackParrot()
auto& pkt = m_parrotFrames[0];
m_parrotFrames.lock();
if (pkt.buffer != nullptr) {
// has the override source ID been set?
if (m_network->m_parrotOverrideSrcId > 0U) {
pkt.srcId = m_network->m_parrotOverrideSrcId;
// override source ID
SET_UINT24(m_network->m_parrotOverrideSrcId, pkt.buffer, 5U);
/*
** bryanb: DMR is problematic because the VOICE_LC_HEADER, TERMINATOR_WITH_LC,
** and VOICE_PI_HEADER all contain the source ID in the LC portion of the frame
** and because we are not updating that the parrot playback will appear to come from
** the original source ID in those frames
*/
}
m_lastParrotPeerId = pkt.peerId;
m_lastParrotSrcId = pkt.srcId;
m_lastParrotDstId = pkt.dstId;

@ -23,7 +23,7 @@
#include "common/dmr/data/NetData.h"
#include "common/dmr/lc/CSBK.h"
#include "common/Clock.h"
#include "network/FNENetwork.h"
#include "network/TrafficNetwork.h"
#include "network/callhandler/packetdata/DMRPacketData.h"
namespace network
@ -35,17 +35,17 @@ namespace network
// ---------------------------------------------------------------------------
/**
* @brief Implements the DMR call handler and data FNE networking logic.
* @brief Implements the DMR call handler and data networking logic.
* @ingroup fne_callhandler
*/
class HOST_SW_API TagDMRData {
public:
/**
* @brief Initializes a new instance of the TagDMRData class.
* @param network Instance of the FNENetwork class.
* @param network Instance of the TrafficNetwork class.
* @param debug Flag indicating whether network debug is enabled.
*/
TagDMRData(FNENetwork* network, bool debug);
TagDMRData(TrafficNetwork* network, bool debug);
/**
* @brief Finalizes a instance of the TagDMRData class.
*/
@ -148,7 +148,7 @@ namespace network
packetdata::DMRPacketData* packetData() { return m_packetData; }
private:
FNENetwork* m_network;
TrafficNetwork* m_network;
/**
* @brief Represents a stored parrot frame.

@ -20,7 +20,7 @@
#include "common/Clock.h"
#include "common/Log.h"
#include "common/Utils.h"
#include "network/FNENetwork.h"
#include "network/TrafficNetwork.h"
#include "network/callhandler/TagNXDNData.h"
#include "HostFNE.h"
#include "FNEMain.h"
@ -40,7 +40,7 @@ using namespace nxdn::defines;
/* Initializes a new instance of the TagNXDNData class. */
TagNXDNData::TagNXDNData(FNENetwork* network, bool debug) :
TagNXDNData::TagNXDNData(TrafficNetwork* network, bool debug) :
m_network(network),
m_parrotFrames(),
m_parrotFramesReady(false),
@ -246,6 +246,12 @@ bool TagNXDNData::processFrame(const uint8_t* data, uint32_t len, uint32_t peerI
LogInfoEx(LOG_MASTER, CALL_END_LOG);
}
if (!tg.config().parrot()) {
m_network->m_totalActiveCalls--;
if (m_network->m_totalActiveCalls < 0)
m_network->m_totalActiveCalls = 0;
}
// report call event to InfluxDB
if (m_network->m_enableInfluxDB) {
influxdb::QueryBuilder()
@ -348,6 +354,9 @@ bool TagNXDNData::processFrame(const uint8_t* data, uint32_t len, uint32_t peerI
} else {
LogWarning((fromUpstream) ? LOG_PEER : LOG_MASTER, "NXDN, Call Collision, peer = %u, ssrc = %u, srcId = %u, dstId = %u, streamId = %u, rxPeer = %u, rxSrcId = %u, rxDstId = %u, rxStreamId = %u, fromUpstream = %u",
peerId, ssrc, srcId, dstId, streamId, status.peerId, status.srcId, status.dstId, status.streamId, fromUpstream);
m_network->m_totalCallCollisions++;
return false;
}
} else {
@ -402,6 +411,11 @@ bool TagNXDNData::processFrame(const uint8_t* data, uint32_t len, uint32_t peerI
m_status[dstId].activeCall = true;
m_status.unlock();
if (!tg.config().parrot()) {
m_network->m_totalCallsProcessed++;
m_network->m_totalActiveCalls++;
}
// is this a private call?
if (!group) {
m_statusPVCall.lock(false);
@ -713,6 +727,14 @@ void TagNXDNData::playbackParrot()
auto& pkt = m_parrotFrames[0];
m_parrotFrames.lock();
if (pkt.buffer != nullptr) {
// has the override source ID been set?
if (m_network->m_parrotOverrideSrcId > 0U) {
pkt.srcId = m_network->m_parrotOverrideSrcId;
// override source ID
SET_UINT24(m_network->m_parrotOverrideSrcId, pkt.buffer, 5U);
}
m_lastParrotPeerId = pkt.peerId;
m_lastParrotSrcId = pkt.srcId;
m_lastParrotDstId = pkt.dstId;

@ -23,7 +23,7 @@
#include "common/nxdn/NXDNDefines.h"
#include "common/nxdn/lc/RTCH.h"
#include "common/nxdn/lc/RCCH.h"
#include "network/FNENetwork.h"
#include "network/TrafficNetwork.h"
namespace network
{
@ -34,17 +34,17 @@ namespace network
// ---------------------------------------------------------------------------
/**
* @brief Implements the NXDN call handler and data FNE networking logic.
* @brief Implements the NXDN call handler and data networking logic.
* @ingroup fne_callhandler
*/
class HOST_SW_API TagNXDNData {
public:
/**
* @brief Initializes a new instance of the TagNXDNData class.
* @param network Instance of the FNENetwork class.
* @param network Instance of the TrafficNetwork class.
* @param debug Flag indicating whether network debug is enabled.
*/
TagNXDNData(FNENetwork* network, bool debug);
TagNXDNData(TrafficNetwork* network, bool debug);
/**
* @brief Finalizes a instance of the TagNXDNData class.
*/
@ -122,7 +122,7 @@ namespace network
uint32_t lastParrotDstId() const { return m_lastParrotDstId; }
private:
FNENetwork* m_network;
TrafficNetwork* m_network;
/**
* @brief Represents a stored parrot frame.

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save

Powered by TurnKey Linux.