From 7c2bfb3914fe7ad28d824c070a5d7f036047c36b Mon Sep 17 00:00:00 2001 From: Bryan Biedenkapp Date: Tue, 2 Sep 2025 22:12:26 -0400 Subject: [PATCH] R04J32 Merge to Master (#95) * implement user-defined adj site mapping to allow fine grained control over neighboring site announcements; * BUGFIX: patch issue where the FNE would not maintain the source peer ID for certain routing scenarios; * fix typo in original implementation, peerId should be the peerId and ssrc should become the originating peer ID; remove peer check throwing a warning; * log ssrc for various data points (the ssrc is the RTP originating sync source which would be the peer ID of the origination of a RTP packet); * bump build number; * add peer ID masking support (this is useful for FNEs that are "public" links where internal peer IDs don't need to be retained); more work for maintaining the originating stream sync source; refactor how the FNE handles peer network connections; * throw a warning if the user configured more then 8 upstream peer connections (more than this number can cause performance problems); * make non-peer-link peer ID masking optional; * add checking for traffic repeat to verify we are not trying to send traffic back to the source; * add checking for traffic repeat to verify we are not trying to send traffic back to an external source; * convert peer network protocol packet processing to a threaded model; * update copyright dates; * implement REST API for manipulating adjacent site map entries; * fix missed REST API initializer, need to pass the adj site map; * add DFSI/V.24 full duplex option (this allows dvmhost to repeat incoming frames back out); * Fix incorrect check of DFSI and FSC operating modes that resulted in a segfault (#92) Co-authored-by: faulty * document p25CorrCount in depth more; set the default p25CorrCount to 2; * properly call PacketBuffers clear() before leaving a scope to ensure contained buffers are deleted (otherwise we'll leak memory); * skip the first 6 bits of the TIA-102 DFSI VHDR (this is very strange, and is probably not kosher); * transparently pass voice frames with FID $20 (assumed to be Kenwood) when the PF flag is set (this allows Kenwoods flavor of encryption to pass); * relabel FID_DMRA to FID_MOT (this feature ID is assigned to Motorola); handle more conditions for FID $20 (Kenwood) on voice frames; * remove filterHeaders (this is deprecated, HDUs aren't sent over the network); perform frame length validation for P25 network frames (unlike DMR and NXDN P25 frames are variable length, requring length validation to prevent buffer under or overflows); fix issue with addr variable not being freed in InfluxDB handler; * [EXPERIMENTAL] add very initial support for dvmpatch to talk to a MMDVM P25 Gateway in P25 mode; * fixup dvmpatch support for talking to MMDVM P25 Gateway; * fix up handling of MMDVM call termination; * more work on better signalling end of call for MMDVM P25 gateway patches; better log traffic from MMDVM; * correct some incorrect timing; * add support for properly authorizing a peer to send Inhibit/Uninhibit commands; add some NXDN constants for remote control; * add columns to main peered peer list to represent if a peer is allowed to inhibit; * fixup informational logging; * add auto generation of a peer password; slightly increase the dialog size of the peer edit window; * fix TIA VHDR incorrect length; correct gatekeep accidentally setting TIA StartOfStream to 4 bytes instead of 3, because you know the TIA spec is incomprehensible with its bit alignment; * add some permanent log trace for DFSI over UDP tracing; correct startOfStreamTIA incorrectly sending a LDU1 NID; correct some offsets; * make sure we send out heartbeats *before* the heartbeat time; * send Start of Stream with voice data; * free memory in error case; remove unused variables; * correct some buffer allocations; ensure the AES class is deleted after use; * implement support for legacy radio affiliation (like P25) for DMR and NXDN (this fixes an issue where conventional DMR and NXDN systems would be unable to transmit onto affiliation only TGs); correct bad CSBK decoding in the FNE (forgot to check for dataSync); * code cleanup to correct compiler warnings; * add string file meta data info to Win32 EXEs; add icons to Win32 EXEs; * migrate and condense analog audio helper and G.711 codec routines into a common class called AnalogAudio for easy reuse; stub network protocol entries to eventually handle analog audio; * add support in the DVM network protocol for analog FM audio traffic; add support to the DVM FNE to properly switch analog FM audio traffic; bump revision from H to J; * add analog enable flag to fne config YAML; * prelim work to make dvmbridge pass analog audio directly into FNE network; * handle locking inside deleteBuffers(); * refactor FrameQueue mutex handling; don't start and stop the thread pool when the peer network opens/closes; * use unique_ptr for compression class instead of passing raw pointers; * refactor FrameQueue from using a std::unordered_map to a simple fixed array that is directly controlled; * refactor compression slightly; * better handle multi-threaded problems; * use a std::vector instead of a classical C allocated array for timestamp list; add error/logic checking for V.24/DFSI where the VHDR may send a TGID0 during a call causing a call rejection; * make timestamps vector static; * dump out of VHDR processing if call is in progress; * bump tarball version build stamp; * add option to forcibly allow TGID0 for badly behaved systems this will cause dvmhost to accept the call and not drop it, with the caveat that the TGID will be rewritten to TGID1, the network stack simply cannot service a TGID of 0 and as such we must rewrite the TGID a TIA-102 valid TGID; * simplify late entry handling where V.24 may send a dstId of 0; * whoops correct my own stupidity; * whoops correct my own stupidity (again); * add opcodes for Harris User Alias and GPS; add opcode for Motorola GPS on PTT; decode Harris User Alias in LDU LCs; * attempt at decoding the LDU RS values for both V.24 and TIA DFSI, this does not abort decoding in the ModemV24 right now and will simply log an error if the RS values for the LDU1 or LDU2 are to badly invalid; * alter field data based on recent testing; enhance and update debug log entries; add debug logging for Voice 1 and 10 start of voice frames for V.24; * add missing tag information for V.24 PDU; * fix incorrect handling of RFSS ID in host setup; * experimental fix to ignore wildly illegal TGIDs in the HDU; * simplify the V.24 HDU TGID logic, entirely ignore the TGID presented in the HDU and just wait for the LDU1 to set the rfLastDstId; * add grantDemand documentation to dvmpatch; * [EXPERIMENTAL] add experimental support for cross-encrypting patched P25 traffic; * reset call algo when a P25 call ends; * properly handle algo ID's for source and destinations; * missed reverseCrypto check; * [EXPERIMENTAL] for some configurations (**you know who you are god damn it**), allow dvmhost to be configured to "idle" on a non-zero TG when the RF talkgroup hangtimer stops, this effectively disables the promiscuous network Rx of the host for certain conventional operations, and requires RF-based TG steering to change the active talkgroup; cleanup the macros for DMR and NXDN that perform various repeated checks; * major refactor for V.24 support; * hide RSSI2 ICW errors; report on any voice frames reporting more then 0 errors; * properly sent VHDR1 and VHDR2 with proper opcodes (whoops); * add experimental support for TDULC over V.24; add some documentation for the V.24 and TIA voice header byte layout; * experimental support for transporting V.24 over IP; * initial super frame should start at 1; make sure to use proper constants instead of magic numbers; * reorganize code slightly; * cleanup format for, and make slightly more precise trace and error dumping log messaging; * add option for the FNE to directly log traffic denials to the system log; * centralize string for illegal RID access; * log total voice frame errors for TIA/DFSI mode; * minor PDU refactoring on when network PDU data is sent; correct issue with accepting a conventional data reg; * disable allowExplicitSourceId by default (not all subscribers support this); correct some bad handling of LC data; * log sysId and netId info for call start and end on the FNE log; * add setting netId and sysId to dvmbridge and dvmpatch for future use; * reorganize and expose decodeLC/encodeLC from LC; correct typo in TDULC headers; add explicit source id TDULC; add LC_GROUP explicit ID flag; * use shorthand macros where able; * enable or disable explicit source ID directly, don't require control to be enabled (important for trunked VC); * minor correction to handling explicit source ID; * properly set dummy SiteData for dvmbridge and dvmpatch (necessary to set the WACN and SysId); * handle condition defaulting WACN and SysId if network data for WACN and SysId is 0; * ensure unencrypted parameters if encryption is disabled; * copy dvmbridge change to dvmpatch for: ensure unencrypted parameters if encryption is disabled; * ignore extra ICW opcodes; * use raw LDU values from the call data instead of processed values if the MFId on the LC isn't TIA standard; * collate bit errors reported from V.24 or TIA; * typos; * allow a group affiliation on Conv+ (DVRS) to terminate a running TG hang timer; * add missing stop to the timer; * add WACN/SysId logging to SysView; * add global to disable transmitting logging to the network (this is used when the UDP socket fails to send data to prevent a crash condition talking to a null socket); unify errno to string message processing for clearity in logs; refactor network reconnect and retry logic to better handle a full connection reset cycle (this is useful for conditions where the network loss isn't transient and is something like a Ethernet or WiFi link drop causing the entire interface to become down temporarily); * remove unnecessary debug logging; * allow encoding user alias at the LC level; * see if we can calculate error percentages; * get rid of magic numbers for properly defined constants; * implement proper support for P25 LDU1 and LDU2 to pass call control bytes; allow a end-point to signal that a call is handing over/switching over from one stream ID/source ID to another; implement support on dvmbridge to properly handle stream/source ID switch over; * log call source switch over events; * whoops this should be a OR equals not equals; * correct issue where network frames would be ignored during RF calls causing buffer overflows; * cleanup and remove unnecessary and confusing C compiler macros; * cleanup and unify design a bit, for P25 move traffic collision checking into separate function calls; * add colorize-host.sh helper tool; update colorize-fne.sh tool for DMR and NXDN; * store status data for a private call; * during unit registration store the originating source peer ID for a given unit registration; use unit registration source ID to select the destination peer to send private call data to; * initial experimental implementation of private call routing: this introduces a new configuration flag "restrictPrivateCallToRegOnly", when set, this flag will influence how private calls are routed through the system private calls using restrictPrivateCallToRegOnly require both the source and destination subscribers to be unit registered with the system, when a private call occurs the system will utilize the source peer ID to restrict repeating the private call traffic only to the 2 peers involved in the private call FNEs will *always* receive private call traffic * rework stream validation for U2U calls slightly for P25; ensure P25 always sends the U2U control byte for private calls; * enhance handling of NXDN LC on the FNE; fix issue where dvmhost would not process a network RCCH burst; * refactor NXDN CC handling a bit; * add support to translate a raw DENY/QUEUE/CAUSE/REASON value for DMR/P25/NXDN into a human readable string; * whoops hastily missed std::string -> c_str conversion; * ensure SSRC is maintained for unit registration announcements; * more implementation for private call routing; * add missing clear flag to DMR payload activate RPC; * correct crash for fsc set to true when not using DFSI TIA-102/UDP; * add support to disable a failed CRC-32 for P25 PDU data; ignore CRC-32 errors for AMBT PDUs; * Add support in dvmbridge for a serial PTT activation switch. RTS is asserted on the serial port defined in bridge-config.yml for the duration of audio received, then is removed. (#102) * P25 PDU packet handling refactor (for future use); * remove test code; * fix issue where PDU RSPs weren't being sent to the FNE; correct timing around ARP and packet retry when subscriber is not ready; * add some more verbose logging; * experimental changes to PDU data handling; * correct CSV parsing for iden, peer list and RID lookup tables (we would skip parameters if they were empty); make the FNE P25 packet data handler operate in the TIA-102 asymmetric addressing mode (as would be required with FNE configurations); --------- Co-authored-by: Jamie <25770089+faultywarrior@users.noreply.github.com> Co-authored-by: faulty Co-authored-by: Lorenzo L. Romero --- configs/adj_site_map.example.yml | 14 + configs/bridge-config.example.yml | 13 +- configs/config.example.yml | 33 +- configs/fne-config.example.yml | 28 +- configs/patch-config.example.yml | 41 + configs/peer_list.example.dat | 12 +- src/CMakeLists.txt | 11 +- src/CompilerOptions.cmake | 5 + src/bridge/CMakeLists.txt | 10 +- src/bridge/HostBridge.cpp | 1252 ++++++++++------- src/bridge/HostBridge.h | 77 +- src/bridge/RtsPttController.cpp | 264 ++++ src/bridge/RtsPttController.h | 91 ++ src/bridge/SampleTimeConversion.h | 54 - src/bridge/network/PeerNetwork.cpp | 22 +- src/bridge/network/PeerNetwork.h | 16 +- src/bridge/win32/project.ico | Bin 0 -> 25277 bytes src/bridge/win32/resource.h | 12 + src/bridge/win32/resource.rc | Bin 0 -> 5140 bytes src/common/CMakeLists.txt | 10 +- src/common/Defines.h | 4 +- src/common/Log.cpp | 3 +- src/common/Log.h | 4 + src/common/RC4Crypto.cpp | 2 +- src/common/VariableLengthArray.h | 21 +- src/common/analog/AnalogAudio.cpp | 189 +++ src/common/analog/AnalogAudio.h | 97 ++ src/common/analog/AnalogDefines.h | 59 + src/common/analog/data/NetData.cpp | 97 ++ src/common/analog/data/NetData.h | 104 ++ src/common/dmr/DMRDefines.h | 11 +- src/common/dmr/DMRUtils.cpp | 85 ++ src/common/dmr/DMRUtils.h | 9 +- src/common/dmr/lc/CSBK.cpp | 4 +- src/common/dmr/lc/csbk/CSBKFactory.cpp | 4 +- src/common/dmr/lc/csbk/CSBK_CALL_ALRT.cpp | 2 +- src/common/dmr/lc/csbk/CSBK_EXT_FNCT.cpp | 2 +- src/common/lookups/AdjSiteMapLookup.cpp | 288 ++++ src/common/lookups/AdjSiteMapLookup.h | 258 ++++ src/common/lookups/IdenTableLookup.cpp | 17 +- src/common/lookups/PeerListLookup.cpp | 48 +- src/common/lookups/PeerListLookup.h | 31 +- src/common/lookups/RadioIdLookup.cpp | 18 +- src/common/lookups/TalkgroupRulesLookup.cpp | 2 +- src/common/network/BaseNetwork.cpp | 308 +++- src/common/network/BaseNetwork.h | 113 +- src/common/network/FrameQueue.cpp | 113 +- src/common/network/FrameQueue.h | 46 +- src/common/network/Network.cpp | 169 ++- src/common/network/Network.h | 29 +- src/common/network/PacketBuffer.cpp | 21 +- src/common/network/RTPFNEHeader.h | 1 + src/common/network/RawFrameQueue.cpp | 47 +- src/common/network/rest/http/HTTPPayload.cpp | 2 +- .../rest/http/SecureServerConnection.h | 8 +- .../network/rest/http/ServerConnection.h | 8 +- src/common/network/sip/SIPPayload.cpp | 2 +- src/common/network/tcp/SecureTcpClient.h | 18 +- src/common/network/tcp/SecureTcpListener.h | 6 +- src/common/network/tcp/Socket.cpp | 8 +- src/common/network/tcp/TcpClient.h | 4 +- src/common/network/tcp/TcpListener.h | 6 +- src/common/network/udp/Socket.cpp | 48 +- src/common/network/viface/VIFace.cpp | 80 +- src/common/nxdn/NXDNDefines.h | 13 + src/common/nxdn/NXDNUtils.cpp | 43 + src/common/nxdn/NXDNUtils.h | 9 +- src/common/nxdn/channel/CAC.cpp | 14 +- src/common/nxdn/channel/FACCH1.cpp | 8 +- src/common/nxdn/channel/SACCH.cpp | 4 +- src/common/nxdn/channel/UDCH.cpp | 8 +- src/common/nxdn/lc/RCCH.cpp | 4 +- src/common/nxdn/lc/RTCH.cpp | 18 +- src/common/nxdn/lc/rcch/MESSAGE_TYPE_REG.cpp | 4 + src/common/p25/P25Defines.h | 20 +- src/common/p25/P25Utils.cpp | 70 +- src/common/p25/P25Utils.h | 15 +- src/common/p25/acl/AccessControl.cpp | 8 +- src/common/p25/acl/AccessControl.h | 3 +- src/common/p25/data/DataBlock.cpp | 2 +- src/common/p25/data/DataHeader.cpp | 3 + src/common/p25/data/DataHeader.h | 11 +- src/common/p25/data/LowSpeedData.cpp | 2 +- src/common/p25/dfsi/DFSIDefines.h | 51 +- src/common/p25/dfsi/LC.cpp | 8 +- src/common/p25/dfsi/frames/FrameDefines.h | 60 +- src/common/p25/dfsi/frames/Frames.h | 8 +- src/common/p25/dfsi/frames/FullRateVoice.cpp | 4 +- .../p25/dfsi/frames/MotFullRateVoice.cpp | 46 +- src/common/p25/dfsi/frames/MotFullRateVoice.h | 14 +- src/common/p25/dfsi/frames/MotPDUFrame.cpp | 108 -- .../p25/dfsi/frames/MotStartOfStream.cpp | 43 +- src/common/p25/dfsi/frames/MotStartOfStream.h | 107 +- .../p25/dfsi/frames/MotStartVoiceFrame.cpp | 40 +- .../p25/dfsi/frames/MotStartVoiceFrame.h | 36 +- src/common/p25/dfsi/frames/MotTDULCFrame.cpp | 136 ++ .../frames/{MotPDUFrame.h => MotTDULCFrame.h} | 54 +- src/common/p25/dfsi/frames/MotTSBKFrame.cpp | 22 +- src/common/p25/dfsi/frames/MotTSBKFrame.h | 20 +- .../p25/dfsi/frames/MotVoiceHeader1.cpp | 129 -- src/common/p25/dfsi/frames/MotVoiceHeader1.h | 117 -- .../p25/dfsi/frames/MotVoiceHeader2.cpp | 87 -- src/common/p25/dfsi/frames/MotVoiceHeader2.h | 100 -- src/common/p25/dfsi/frames/StartOfStream.h | 2 +- src/common/p25/lc/AMBT.cpp | 2 +- src/common/p25/lc/LC.cpp | 389 +++-- src/common/p25/lc/LC.h | 45 +- src/common/p25/lc/TDULC.cpp | 126 +- src/common/p25/lc/TDULC.h | 32 +- src/common/p25/lc/TSBK.cpp | 13 +- src/common/p25/lc/tdulc/LC_ADJ_STS_BCAST.h | 6 +- src/common/p25/lc/tdulc/LC_CALL_TERM.h | 6 +- src/common/p25/lc/tdulc/LC_CONV_FALLBACK.h | 6 +- .../p25/lc/tdulc/LC_EXPLICIT_SOURCE_ID.cpp | 69 + .../p25/lc/tdulc/LC_EXPLICIT_SOURCE_ID.h | 59 + src/common/p25/lc/tdulc/LC_FAILSOFT.h | 6 +- src/common/p25/lc/tdulc/LC_GROUP.cpp | 6 +- src/common/p25/lc/tdulc/LC_GROUP.h | 8 +- src/common/p25/lc/tdulc/LC_GROUP_UPDT.h | 6 +- src/common/p25/lc/tdulc/LC_IDEN_UP.h | 6 +- src/common/p25/lc/tdulc/LC_NET_STS_BCAST.h | 6 +- src/common/p25/lc/tdulc/LC_PRIVATE.h | 6 +- src/common/p25/lc/tdulc/LC_RFSS_STS_BCAST.h | 6 +- src/common/p25/lc/tdulc/LC_SYS_SRV_BCAST.h | 6 +- src/common/p25/lc/tdulc/LC_TDULC_RAW.cpp | 101 ++ src/common/p25/lc/tdulc/LC_TDULC_RAW.h | 86 ++ src/common/p25/lc/tdulc/LC_TEL_INT_VCH_USER.h | 6 +- src/common/p25/lc/tdulc/TDULCFactory.cpp | 12 +- src/common/p25/lc/tdulc/TDULCFactory.h | 4 +- src/common/p25/lc/tsbk/TSBKFactory.cpp | 2 +- src/common/zlib/Compression.cpp | 46 +- src/common/zlib/Compression.h | 8 +- src/fne/CMakeLists.txt | 12 +- src/fne/HostFNE.cpp | 207 +-- src/fne/HostFNE.h | 52 +- src/fne/lookups/AffiliationLookup.cpp | 101 ++ src/fne/lookups/AffiliationLookup.h | 83 ++ src/fne/network/DiagNetwork.cpp | 7 +- src/fne/network/FNENetwork.cpp | 163 ++- src/fne/network/FNENetwork.h | 40 +- src/fne/network/PeerNetwork.cpp | 177 ++- src/fne/network/PeerNetwork.h | 100 +- src/fne/network/RESTAPI.cpp | 149 +- src/fne/network/RESTAPI.h | 37 +- src/fne/network/RESTDefines.h | 7 +- src/fne/network/callhandler/TagAnalogData.cpp | 710 ++++++++++ src/fne/network/callhandler/TagAnalogData.h | 196 +++ src/fne/network/callhandler/TagDMRData.cpp | 282 +++- src/fne/network/callhandler/TagDMRData.h | 22 +- src/fne/network/callhandler/TagNXDNData.cpp | 289 +++- src/fne/network/callhandler/TagNXDNData.h | 8 +- src/fne/network/callhandler/TagP25Data.cpp | 418 ++++-- src/fne/network/callhandler/TagP25Data.h | 8 +- .../callhandler/packetdata/DMRPacketData.cpp | 8 +- .../callhandler/packetdata/P25PacketData.cpp | 368 +++-- .../callhandler/packetdata/P25PacketData.h | 42 +- src/fne/network/influxdb/InfluxDB.cpp | 58 +- src/fne/win32/fne.ico | Bin 0 -> 21413 bytes src/fne/win32/resource.h | 12 + src/fne/win32/resource.rc | Bin 0 -> 5194 bytes src/host/CMakeLists.txt | 10 +- src/host/Host.Config.cpp | 6 +- src/host/Host.cpp | 15 +- src/host/dmr/Control.cpp | 13 + src/host/dmr/Slot.cpp | 16 +- src/host/dmr/Slot.h | 14 + src/host/dmr/packet/ControlSignaling.cpp | 52 +- src/host/dmr/packet/Data.cpp | 79 +- src/host/dmr/packet/Voice.cpp | 225 ++- src/host/dmr/packet/Voice.h | 15 +- src/host/modem/Modem.cpp | 50 +- src/host/modem/ModemV24.cpp | 817 ++++++++--- src/host/modem/ModemV24.h | 240 +++- src/host/modem/port/ISerialPort.h | 11 + src/host/modem/port/PseudoPTYPort.cpp | 2 +- src/host/modem/port/UARTPort.cpp | 66 +- src/host/modem/port/UARTPort.h | 11 + .../modem/port/specialized/V24UDPPort.cpp | 38 +- src/host/modem/port/specialized/V24UDPPort.h | 6 + src/host/network/RESTAPI.cpp | 6 +- src/host/nxdn/Control.cpp | 70 +- src/host/nxdn/Control.h | 5 +- src/host/nxdn/packet/ControlSignaling.cpp | 135 +- src/host/nxdn/packet/ControlSignaling.h | 26 +- src/host/nxdn/packet/Data.cpp | 243 ++-- src/host/nxdn/packet/Voice.cpp | 460 +++--- src/host/nxdn/packet/Voice.h | 18 +- src/host/p25/Control.cpp | 61 +- src/host/p25/Control.h | 6 + src/host/p25/packet/ControlSignaling.cpp | 32 +- src/host/p25/packet/Data.cpp | 352 +++-- src/host/p25/packet/Data.h | 17 +- src/host/p25/packet/Voice.cpp | 332 +++-- src/host/p25/packet/Voice.h | 16 + src/host/setup/HostSetup.cpp | 13 +- src/host/setup/SiteParamSetWnd.h | 2 +- src/host/win32/host.ico | Bin 0 -> 20989 bytes src/host/win32/resource.h | 12 + src/host/win32/resource.rc | Bin 0 -> 5140 bytes src/patch/CMakeLists.txt | 10 + src/patch/HostPatch.cpp | 823 ++++++++++- src/patch/HostPatch.h | 73 + src/patch/mmdvm/P25Network.cpp | 495 +++++++ src/patch/mmdvm/P25Network.h | 123 ++ src/patch/network/PeerNetwork.cpp | 26 +- src/patch/network/PeerNetwork.h | 15 +- src/patch/win32/project.ico | Bin 0 -> 25277 bytes src/patch/win32/resource.h | 12 + src/patch/win32/resource.rc | Bin 0 -> 5142 bytes src/peered/PeerEditWnd.h | 84 +- src/peered/PeerListWnd.h | 9 +- src/remote/CMakeLists.txt | 9 +- src/remote/win32/project.ico | Bin 0 -> 25277 bytes src/remote/win32/resource.h | 12 + src/remote/win32/resource.rc | Bin 0 -> 5200 bytes src/sysview/HostWS.cpp | 2 +- src/sysview/SysViewMain.cpp | 70 +- src/sysview/network/PeerNetwork.cpp | 11 +- src/sysview/network/PeerNetwork.h | 11 +- tools/colorize-fne.sh | 8 +- tools/colorize-host.sh | 36 + 221 files changed, 12104 insertions(+), 3659 deletions(-) create mode 100644 configs/adj_site_map.example.yml create mode 100644 src/bridge/RtsPttController.cpp create mode 100644 src/bridge/RtsPttController.h delete mode 100644 src/bridge/SampleTimeConversion.h create mode 100644 src/bridge/win32/project.ico create mode 100644 src/bridge/win32/resource.h create mode 100644 src/bridge/win32/resource.rc create mode 100644 src/common/analog/AnalogAudio.cpp create mode 100644 src/common/analog/AnalogAudio.h create mode 100644 src/common/analog/AnalogDefines.h create mode 100644 src/common/analog/data/NetData.cpp create mode 100644 src/common/analog/data/NetData.h create mode 100644 src/common/dmr/DMRUtils.cpp create mode 100644 src/common/lookups/AdjSiteMapLookup.cpp create mode 100644 src/common/lookups/AdjSiteMapLookup.h delete mode 100644 src/common/p25/dfsi/frames/MotPDUFrame.cpp create mode 100644 src/common/p25/dfsi/frames/MotTDULCFrame.cpp rename src/common/p25/dfsi/frames/{MotPDUFrame.h => MotTDULCFrame.h} (62%) delete mode 100644 src/common/p25/dfsi/frames/MotVoiceHeader1.cpp delete mode 100644 src/common/p25/dfsi/frames/MotVoiceHeader1.h delete mode 100644 src/common/p25/dfsi/frames/MotVoiceHeader2.cpp delete mode 100644 src/common/p25/dfsi/frames/MotVoiceHeader2.h create mode 100644 src/common/p25/lc/tdulc/LC_EXPLICIT_SOURCE_ID.cpp create mode 100644 src/common/p25/lc/tdulc/LC_EXPLICIT_SOURCE_ID.h create mode 100644 src/common/p25/lc/tdulc/LC_TDULC_RAW.cpp create mode 100644 src/common/p25/lc/tdulc/LC_TDULC_RAW.h create mode 100644 src/fne/lookups/AffiliationLookup.cpp create mode 100644 src/fne/lookups/AffiliationLookup.h create mode 100644 src/fne/network/callhandler/TagAnalogData.cpp create mode 100644 src/fne/network/callhandler/TagAnalogData.h create mode 100644 src/fne/win32/fne.ico create mode 100644 src/fne/win32/resource.h create mode 100644 src/fne/win32/resource.rc create mode 100644 src/host/win32/host.ico create mode 100644 src/host/win32/resource.h create mode 100644 src/host/win32/resource.rc create mode 100644 src/patch/mmdvm/P25Network.cpp create mode 100644 src/patch/mmdvm/P25Network.h create mode 100644 src/patch/win32/project.ico create mode 100644 src/patch/win32/resource.h create mode 100644 src/patch/win32/resource.rc create mode 100644 src/remote/win32/project.ico create mode 100644 src/remote/win32/resource.h create mode 100644 src/remote/win32/resource.rc create mode 100755 tools/colorize-host.sh diff --git a/configs/adj_site_map.example.yml b/configs/adj_site_map.example.yml new file mode 100644 index 00000000..cc1d0cf5 --- /dev/null +++ b/configs/adj_site_map.example.yml @@ -0,0 +1,14 @@ +# +# Digital Voice Modem - Adj. Site Map +# + +# +# Peer List +# +peers: + # Peer ID. + - peerId: 1234567 + # Flag indicating whether this talkgroup is active or not. + active: true + # List of peer IDs that are neighbors for this peer. + neighbors: [] diff --git a/configs/bridge-config.example.yml b/configs/bridge-config.example.yml index 36fc829a..436b5e1b 100644 --- a/configs/bridge-config.example.yml +++ b/configs/bridge-config.example.yml @@ -125,6 +125,11 @@ system: # Textual Name identity: BRIDGE + # Network ID (WACN). + netId: BB800 + # System ID. + sysId: 001 + # 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 @@ -145,7 +150,7 @@ system: # - (Not used when utilizing external USB vocoder!) vocoderEncoderAudioGain: 3.0 - # Audio transmit mode (1 - DMR, 2 - P25). + # Audio transmit mode (1 - DMR, 2 - P25, 3 - Analog). txMode: 1 # Relative sample level for VOX to activate. @@ -179,3 +184,9 @@ system: trace: false # Flag indicating whether or not debug logging is enabled. debug: 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). + rtsPttPort: "/dev/ttyUSB0" diff --git a/configs/config.example.yml b/configs/config.example.yml index f7d045c6..14359fc3 100644 --- a/configs/config.example.yml +++ b/configs/config.example.yml @@ -155,6 +155,10 @@ protocols: ignoreAffiliationCheck: false # Flag indicating the host should send a network grant demand for conventional traffic. convNetGrantDemand: false + # Flag indicating the fallback legacy group registration for radios that do not support group affiliation. + # (Useful for alerting the FNE to affiliations to TGIDs for radios that do not properly support group + # affiliation.) + legacyGroupReg: false # Flag indicating whether or not received RF embedded LC data only should be transmitted. embeddedLCOnly: false # Flag indicating whether talker alias data should be dumped to the log. @@ -240,12 +244,16 @@ protocols: # (This applies only in conventional operations where channel granting is utilized and RF-only talkgroup # steering is required.) disableNetworkGrant: false + # Flag indicating whether or not TGID 0 (P25 blackhole talkgroup) will be allowed. + # (Normally this should *never* be enabled, it is here to support poorly behaved systems that transmit + # TGID 0 for some reason.) + forceAllowTG0: false # Flag indicating whether or not a TGID will be tested for affiliations before being granted. ignoreAffiliationCheck: false # Flag indicating that the host will attempt to automatically inhibit unauthorized RIDs (those not in the # RID ACL list). inhibitUnauthorized: false - # Flag indicating the fallback legacy group grant for radios that do not support group affilition to + # Flag indicating the fallback legacy group grant for radios that do not support group affiliation to # have group grants transmitted. (Useful for alerting the FNE to affiliations to TGIDs for radios that # do not properly support group affiliation.) legacyGroupGrnt: true @@ -267,6 +275,8 @@ protocols: dumpDataPacket: false # Flag indicating whether or not this host will repeat P25 data traffic. repeatDataPacket: true + # Flag indicating whether or not the host will ignore CRC errors in P25 PDU data packets. + ignoreDataCRC: false # Flag indicating whether verbose dumping of P25 TSBK data is enabled. dumpTsbkData: false # Amount of time to hang after a voice call. @@ -278,7 +288,7 @@ protocols: # Flag indicating that unit-to-unit availiability checks should be performed for a private call. unitToUnitAvailCheck: false # Flag indicating explicit source ID support is enabled. - allowExplicitSourceId: true + allowExplicitSourceId: 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) @@ -319,6 +329,10 @@ protocols: # Flag indicating whether or not a TGID will be tested for affiliations before being granted. ignoreAffiliationCheck: false + # Flag indicating the fallback legacy group registration for radios that do not support group affiliation. + # (Useful for alerting the FNE to affiliations to TGIDs for radios that do not properly support group + # affiliation.) + legacyGroupReg: false # Flag indicating the host should verify group affiliation. verifyAff: false # Flag indicating the host should verify unit registration. @@ -470,6 +484,10 @@ system: pSuperGroup: FFFE # Announcment Talkgroup Group. announcementGroup: FFFE + # Default Talkgroup for idle network traffic. + # NOTE: TGID 0 is used here to indicate "promiscuous" mode, where first-come-first-serve traffic from the network + # is allowed. + defaultNetIdleTalkgroup: 0 # DMR network ID. dmrNetId: 1 @@ -532,8 +550,12 @@ system: # Amount of time to wait before starting DMR transmissions after a signal is received. dmrRxDelay: 7 # Amount of packet correlations that should occur before P25 data is returned from the modem to the host. - # (Note: Changing this value will impact P25 protocol stability, and should not be altered.) - p25CorrCount: 8 + # (Note: Changing this value will impact P25 protocol stability, and should not be altered. This is set to + # a default value of 2 for fast synchronization of a P25 signal, if the air modem is having difficulty + # synchronizing to a P25 signal, this value can be increased to 3 or 4, or beyond. It should be noted that + # increasing this value will increase the time it takes to synchronize to a P25 signal, and may cause + # issues with P25 voice calls (especially encrypted voice calls).) + p25CorrCount: 2 # Size (in bytes) of the DMR transmit FIFO buffer. It is not recommended to adjust this unless absolutely # necessary, excessive sizes will cause delays in transmitted frames due to excessive buffering. # Calculation Formula: (DMR Frame Size (33 bytes) * x Frames) + Pad Bytes = FIFO Buffer Size Bytes @@ -626,6 +648,9 @@ system: jitter: 200 # Timer which will reset local/remote call flags if frames aren't received longer than this time in ms callTimeout: 200 + # Flag indicating whether or not the V.24 modem is operating in full-duplex mode. + # (This enables DVM to repeat incoming V.24 frames back out after processing.) + fullDuplex: false # Flag indicating when operating in V.24 UDP mode, the FSC protocol should be used to negotiate connection. fsc: false # Sets the heartbeat interval for the FSC connection. diff --git a/configs/fne-config.example.yml b/configs/fne-config.example.yml index 036a5544..3ef1f78f 100644 --- a/configs/fne-config.example.yml +++ b/configs/fne-config.example.yml @@ -48,6 +48,10 @@ master: # Flag indicating whether or not verbose debug logging is enabled. debug: false + # 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 + # Maximum number of concurrent packet processing workers. workers: 16 @@ -70,6 +74,8 @@ master: allowP25Traffic: true # Flag indicating whether or not NXDN traffic will be passed. allowNXDNTraffic: true + # Flag indicating whether or not analog traffic will be passed. + allowAnalogTraffic: false # Flag indicating whether packet data will be passed. disablePacketData: false @@ -87,6 +93,8 @@ master: # Flag indicating whether or not a grant responses will only be sent to TGs with affiliations, if the TG is configured for affiliation gating. restrictGrantToAffiliatedOnly: false + # Flag indicating whether or not a private call will only be routed to the network peers the RID registers with. + restrictPrivateCallToRegOnly: false # Flag indicating whether or not a adjacent site broadcasts will pass to any peers. disallowAdjStsBcast: false @@ -103,9 +111,14 @@ master: # Flag indicating whether or not a TDULC call terminations will pass to any peers. disallowCallTerm: false - # Flag indicating that traffic headers will be filtered by destination ID (i.e. valid RID or valid TGID). - filterHeaders: true - # Flag indicating that terminators will be filtered by destination ID (i.e. valid RID or valid TGID). + # Flag indicating whether or not the FNE will mask all outbound traffic to use the FNE's own peer ID. + # (This is useful for FNEs that are public facing, and the originating traffic peer ID should be masked.) + maskOutboundPeerID: false + # Flag indicating whether or not the FNE will mask only non-Peer-Link outbound traffic to use the FNE's own peer ID. + # (This is useful for networked FNEs that are have a mix of connections, and the originating traffic peer ID to non-Peer-Link FNEs should be masked.) + maskOutboundPeerIDForNonPeerLink: false + + # Flag indicating that P25 terminators will be filtered by destination ID (i.e. valid RID or valid TGID). filterTerminators: true # Flag indicating the FNE will drop all inbound Unit-to-Unit calls. @@ -150,6 +163,15 @@ master: # Amount of time between updates of talkgroup rules file. (minutes) time: 30 + # + # Adj. Site Map Configuration + # + adj_site_map: + # Full path to the Adj. Site Map file. + file: adj_site_map.yml + # Amount of time between updates of Adj. Site Map file. (minutes) + time: 30 + # # External Peers # diff --git a/configs/patch-config.example.yml b/configs/patch-config.example.yml index d2997368..39954a1f 100644 --- a/configs/patch-config.example.yml +++ b/configs/patch-config.example.yml @@ -60,21 +60,62 @@ network: sourceTGID: 1 # Source Slot for received/transmitted audio frames. sourceSlot: 1 + + # Source Traffic Encryption + srcTek: + # 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 + # Destination Talkgroup ID for transmitted/received audio frames. destinationTGID: 1 # Destination Slot for received/transmitted audio frames. destinationSlot: 1 + # Destination Traffic Encryption + dstTek: + # 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 + # Flag indicating whether or not the patch is two-way. twoWay: false + # Hostname/IP address of MMDVM gateway to connect to. + mmdvmGatewayAddress: 127.0.0.1 + # Port number to connect to. + mmdvmGatewayPort: 42020 + # Local port number for gateway to connect to. + localGatewayPort: 32010 + system: # Textual Name identity: PATCH + # Network ID (WACN). + netId: BB800 + # System ID. + sysId: 001 + # Digital mode (1 - DMR, 2 - P25). digiMode: 1 + # Flag indicating whether a network grant demand packet will be sent before audio. + grantDemand: false + + # Flag indicating whether or not the patch is from/to a MMDVM P25 reflector. + mmdvmP25Reflector: false + # Flag indicating whether or not trace logging is enabled. trace: false # Flag indicating whether or not debug logging is enabled. diff --git a/configs/peer_list.example.dat b/configs/peer_list.example.dat index bfed0ea9..60cf3299 100644 --- a/configs/peer_list.example.dat +++ b/configs/peer_list.example.dat @@ -1,9 +1,9 @@ # # This file sets the valid peer IDs allowed on a FNE. # -# Entry Format: "Peer ID,Peer Password,Peer Link (1 = Enabled / 0 = Disabled),Peer Alias (optional),Can Request Keys (1 = Enabled / 0 = Disabled)," -#1234,,0,,1, -#5678,MYSECUREPASSWORD,0,,0, -#9876,MYSECUREPASSWORD,1,,0, -#5432,MYSECUREPASSWORD,,Peer Alias 1,0, -#1012,MYSECUREPASSWORD,1,Peer Alias 2,1, +# Entry Format: "Peer ID,Peer Password,Peer Link (1 = Enabled / 0 = Disabled),Peer Alias (optional),Can Request Keys (1 = Enabled / 0 = Disabled),Can Issue Inhibit (1 = Enabled / 0 = Disabled)" +#1234,,0,,1,0, +#5678,MYSECUREPASSWORD,0,,0,0, +#9876,MYSECUREPASSWORD,1,,0,0, +#5432,MYSECUREPASSWORD,,Peer Alias 1,0,0, +#1012,MYSECUREPASSWORD,1,Peer Alias 2,1,0, diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 8b110299..d3e486a4 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -42,6 +42,7 @@ if (ENABLE_SETUP_TUI) target_link_libraries(dvmhost PRIVATE common ${OPENSSL_LIBRARIES} asio::asio finalcut Threads::Threads util) else () if (COMPILE_WIN32) + target_sources(dvmhost PRIVATE ${dvmhost_RC}) target_link_libraries(dvmhost PRIVATE common ${OPENSSL_LIBRARIES} asio::asio Threads::Threads) else () target_link_libraries(dvmhost PRIVATE common ${OPENSSL_LIBRARIES} asio::asio Threads::Threads util) @@ -60,7 +61,7 @@ set(CPACK_PACKAGE_VENDOR "DVMProject") set(CPACK_DEBIAN_PACKAGE_DESCRIPTION "The DVM Host software provides the host computer implementation of a mixed-mode DMR, P25 and/or NXDN or dedicated-mode DMR, P25 or NXDN repeater system that talks to the actual modem hardware. The host software; is the portion of a complete Over-The-Air modem implementation that performs the data processing, decision making and FEC correction for a digital repeater.") set(CPACK_DEBIAN_PACKAGE_MAINTAINER "DVMProject Authors") -set(CPACK_DEBIAN_PACKAGE_VERSION "R04Hxx") +set(CPACK_DEBIAN_PACKAGE_VERSION "R04Jxx") set(CPACK_DEBIAN_PACKAGE_RELEASE "0") set(CPACK_DEBIAN_PACKAGE_HOMEPAGE "https://github.com/dvmproject") @@ -76,6 +77,9 @@ include(CPack) # include(src/fne/CMakeLists.txt) add_executable(dvmfne ${common_INCLUDE} ${dvmfne_SRC}) +if (COMPILE_WIN32) + target_sources(dvmfne PRIVATE ${dvmfne_RC}) +endif (COMPILE_WIN32) target_link_libraries(dvmfne PRIVATE common ${OPENSSL_LIBRARIES} asio::asio Threads::Threads) target_include_directories(dvmfne PRIVATE ${OPENSSL_INCLUDE_DIR} src src/fne) @@ -128,6 +132,9 @@ endif (ENABLE_TUI_SUPPORT AND (NOT DISABLE_TUI_APPS)) # include(src/remote/CMakeLists.txt) add_executable(dvmcmd ${common_INCLUDE} ${dvmcmd_SRC}) +if (COMPILE_WIN32) + target_sources(dvmcmd PRIVATE ${dvmcmd_RC}) +endif (COMPILE_WIN32) target_link_libraries(dvmcmd PRIVATE common ${OPENSSL_LIBRARIES} asio::asio Threads::Threads) target_include_directories(dvmcmd PRIVATE ${OPENSSL_INCLUDE_DIR} src src/remote) @@ -137,6 +144,7 @@ target_include_directories(dvmcmd PRIVATE ${OPENSSL_INCLUDE_DIR} src src/remote) include(src/bridge/CMakeLists.txt) add_executable(dvmbridge ${common_INCLUDE} ${bridge_SRC}) if (COMPILE_WIN32) + target_sources(dvmbridge PRIVATE ${bridge_RC}) target_link_libraries(dvmbridge PRIVATE common vocoder ${OPENSSL_LIBRARIES} asio::asio Threads::Threads) else () if (ARCH STREQUAL "arm64" OR ARCH STREQUAL "armhf") @@ -153,6 +161,7 @@ target_include_directories(dvmbridge PRIVATE ${OPENSSL_INCLUDE_DIR} src src/brid include(src/patch/CMakeLists.txt) add_executable(dvmpatch ${common_INCLUDE} ${patch_SRC}) if (COMPILE_WIN32) + target_sources(dvmpatch PRIVATE ${patch_RC}) target_link_libraries(dvmpatch PRIVATE common ${OPENSSL_LIBRARIES} asio::asio Threads::Threads) else () target_link_libraries(dvmpatch PRIVATE common ${OPENSSL_LIBRARIES} dl asio::asio Threads::Threads) diff --git a/src/CompilerOptions.cmake b/src/CompilerOptions.cmake index 6c667071..0c8eb7c4 100644 --- a/src/CompilerOptions.cmake +++ b/src/CompilerOptions.cmake @@ -35,6 +35,7 @@ option(DEBUG_NXDN_SACCH "" off) option(DEBUG_NXDN_UDCH "" off) option(DEBUG_NXDN_LICH "" off) option(DEBUG_NXDN_CAC "" off) +option(DEBUG_NXDN_RTCH "" off) option(DEBUG_P25_PDU_DATA "" off) option(DEBUG_P25_HDU "" off) option(DEBUG_P25_LDU1 "" off) @@ -96,6 +97,10 @@ if (DEBUG_NXDN_CAC) message(CHECK_START "NXDN CAC Debug") add_definitions(-DDEBUG_NXDN_CAC) endif (DEBUG_NXDN_CAC) +if (DEBUG_NXDN_RTCH) + message(CHECK_START "NXDN RTCH Debug") + add_definitions(-DDEBUG_NXDN_RTCH) +endif (DEBUG_NXDN_RTCH) if (DEBUG_P25_PDU_DATA) message(CHECK_START "P25 PDU Data Debug") add_definitions(-DDEBUG_P25_PDU_DATA) diff --git a/src/bridge/CMakeLists.txt b/src/bridge/CMakeLists.txt index 647081a2..43e37252 100644 --- a/src/bridge/CMakeLists.txt +++ b/src/bridge/CMakeLists.txt @@ -4,7 +4,7 @@ # * GPLv2 Open Source. Use is subject to license terms. # * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. # * -# * Copyright (C) 2024 Bryan Biedenkapp, N2PLL +# * Copyright (C) 2024,2025 Bryan Biedenkapp, N2PLL # * # */ file(GLOB bridge_SRC @@ -16,6 +16,14 @@ file(GLOB bridge_SRC "src/bridge/network/*.h" "src/bridge/network/*.cpp" + "src/bridge/win32/*.h" "src/bridge/*.h" "src/bridge/*.cpp" ) + +# +# Windows Resource Scripts +# +file(GLOB bridge_RC + "src/bridge/win32/*.rc" +) diff --git a/src/bridge/HostBridge.cpp b/src/bridge/HostBridge.cpp index a6607420..75b04c3f 100644 --- a/src/bridge/HostBridge.cpp +++ b/src/bridge/HostBridge.cpp @@ -9,6 +9,9 @@ * */ #include "Defines.h" +#include "common/analog/AnalogDefines.h" +#include "common/analog/AnalogAudio.h" +#include "common/analog/data/NetData.h" #include "common/dmr/DMRDefines.h" #include "common/dmr/data/EMB.h" #include "common/dmr/data/NetData.h" @@ -30,8 +33,9 @@ #include "bridge/ActivityLog.h" #include "HostBridge.h" #include "BridgeMain.h" -#include "SampleTimeConversion.h" +using namespace analog; +using namespace analog::defines; using namespace network; using namespace network::frame; using namespace network::udp; @@ -59,20 +63,6 @@ const int NUMBER_OF_BUFFERS = 32; #define LOCAL_CALL "Local Traffic" #define UDP_CALL "UDP Traffic" -#define SIGN_BIT (0x80) // sign bit for a A-law byte -#define QUANT_MASK (0xf) // quantization field mask -#define NSEGS (8) // number of A-law segments -#define SEG_SHIFT (4) // left shift for segment number -#define SEG_MASK (0x70) // segment field mask - -static short seg_aend[8] = { 0x1F, 0x3F, 0x7F, 0xFF, 0x1FF, 0x3FF, 0x7FF, 0xFFF }; -static short seg_uend[8] = { 0x3F, 0x7F, 0xFF, 0x1FF, 0x3FF, 0x7FF, 0xFFF, 0x1FFF }; - -#define BIAS (0x84) // bias for linear code -#define CLIP 8159 - -const uint8_t RTP_G711_PAYLOAD_TYPE = 0x00U; - #define TEK_AES "aes" #define TEK_ARC4 "arc4" @@ -102,27 +92,34 @@ void audioCallback(ma_device* device, void* output, const void* input, ma_uint32 std::lock_guard lock(HostBridge::m_audioMutex); int smpIdx = 0; - short samples[MBE_SAMPLES_LENGTH]; + short samples[AUDIO_SAMPLES_LENGTH]; const uint8_t* pcm = (const uint8_t*)input; for (uint32_t pcmIdx = 0; pcmIdx < pcmBytes; pcmIdx += 2) { samples[smpIdx] = (short)((pcm[pcmIdx + 1] << 8) + pcm[pcmIdx + 0]); smpIdx++; } - bridge->m_inputAudio.addData(samples, MBE_SAMPLES_LENGTH); + bridge->m_inputAudio.addData(samples, AUDIO_SAMPLES_LENGTH); } // playback output audio - if (bridge->m_outputAudio.dataSize() >= MBE_SAMPLES_LENGTH) { - short samples[MBE_SAMPLES_LENGTH]; - bridge->m_outputAudio.get(samples, MBE_SAMPLES_LENGTH); + if (bridge->m_outputAudio.dataSize() >= AUDIO_SAMPLES_LENGTH) { + short samples[AUDIO_SAMPLES_LENGTH]; + bridge->m_outputAudio.get(samples, AUDIO_SAMPLES_LENGTH); uint8_t* pcm = (uint8_t*)output; int pcmIdx = 0; - for (uint32_t smpIdx = 0; smpIdx < MBE_SAMPLES_LENGTH; smpIdx++) { + 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; } + + // Assert RTS PTT when audio is being sent to output + bridge->assertRtsPtt(); + } + else { + // Deassert RTS PTT when no audio is being sent to output + bridge->deassertRtsPtt(); } } @@ -139,7 +136,7 @@ void mdcPacketDetected(int frameCount, mdc_u8_t op, mdc_u8_t arg, mdc_u16_t unit ::LogMessage(LOG_HOST, "Local Traffic, MDC Detect, unitId = $%04X", unitID); // HACK: nasty bullshit to convert MDC unitID to decimal - char* pCharRes = new (char); + char* pCharRes = new char[16]; // enough space for "0xFFFFFFFF" ::sprintf(pCharRes, "0x%X", unitID); uint32_t res = 0U; @@ -151,133 +148,12 @@ void mdcPacketDetected(int frameCount, mdc_u8_t op, mdc_u8_t arg, mdc_u16_t unit res = (uint32_t)std::stoi(pCharRes, 0, 16); } + delete[] pCharRes; bridge->m_srcIdOverride = res; ::LogMessage(LOG_HOST, "Local Traffic, MDC Detect, converted srcId = %u", bridge->m_srcIdOverride); } } -/* */ - -static short search(short val, short* table, short size) -{ - for (short i = 0; i < size; i++) { - if (val <= *table++) - return (i); - } - - return (size); -} - -/* Helper to convert PCM into G.711 aLaw. */ - -uint8_t encodeALaw(short pcm) -{ - short mask; - uint8_t aval; - - pcm = pcm >> 3; - - if (pcm >= 0) { - mask = 0xD5U; // sign (7th) bit = 1 - } else { - mask = 0x55U; // sign bit = 0 - pcm = -pcm - 1; - } - - // convert the scaled magnitude to segment number - short seg = search(pcm, seg_aend, 8); - - /* - ** combine the sign, segment, quantization bits - */ - if (seg >= 8) // out of range, return maximum value - return (uint8_t)(0x7F ^ mask); - else { - aval = (uint8_t) seg << SEG_SHIFT; - if (seg < 2) - aval |= (pcm >> 1) & QUANT_MASK; - else - aval |= (pcm >> seg) & QUANT_MASK; - - return (aval ^ mask); - } -} - -/* Helper to convert G.711 aLaw into PCM. */ - -short decodeALaw(uint8_t alaw) -{ - alaw ^= 0x55U; - - short t = (alaw & QUANT_MASK) << 4; - short seg = ((unsigned)alaw & SEG_MASK) >> SEG_SHIFT; - switch (seg) { - case 0: - t += 8; - break; - case 1: - t += 0x108U; - break; - default: - t += 0x108U; - t <<= seg - 1; - } - - return ((alaw & SIGN_BIT) ? t : -t); -} - -/* Helper to convert PCM into G.711 MuLaw. */ - -uint8_t encodeMuLaw(short pcm) -{ - short mask; - - // get the sign and the magnitude of the value - pcm = pcm >> 2; - if (pcm < 0) { - pcm = -pcm; - mask = 0x7FU; - } else { - mask = 0xFFU; - } - - // clip the magnitude - if (pcm > CLIP) - pcm = CLIP; - pcm += (BIAS >> 2); - - // convert the scaled magnitude to segment number - short seg = search(pcm, seg_uend, 8); - - /* - ** combine the sign, segment, quantization bits; - ** and complement the code word - */ - if (seg >= 8) // out of range, return maximum value. - return (uint8_t)(0x7F ^ mask); - else { - uint8_t ulaw = (uint8_t)(seg << 4) | ((pcm >> (seg + 1)) & 0xF); - return (ulaw ^ mask); - } -} - -/* Helper to convert G.711 MuLaw into PCM. */ - -short decodeMuLaw(uint8_t ulaw) -{ - // complement to obtain normal u-law value - ulaw = ~ulaw; - - /* - ** extract and bias the quantization bits; then - ** shift up by the segment number and subtract out the bias - */ - short t = ((ulaw & QUANT_MASK) << 3) + BIAS; - t <<= ((unsigned)ulaw & SEG_MASK) >> SEG_SHIFT; - - return ((ulaw & SIGN_BIT) ? (BIAS - t) : (t - BIAS)); -} - // --------------------------------------------------------------------------- // Public Class Members // --------------------------------------------------------------------------- @@ -303,11 +179,11 @@ HostBridge::HostBridge(const std::string& confFile) : m_udpJitter(200U), m_udpSilenceDuringHang(true), m_lastUdpFrameTime(0U), - m_tekAlgoId(p25::defines::ALGO_UNENCRYPT), + m_tekAlgoId(P25DEF::ALGO_UNENCRYPT), m_tekKeyId(0U), m_requestedTek(false), m_p25Crypto(nullptr), - m_srcId(p25::defines::WUID_FNE), + m_srcId(P25DEF::WUID_FNE), m_srcIdOverride(0U), m_overrideSrcIdFromMDC(false), m_overrideSrcIdFromUDP(false), @@ -338,8 +214,8 @@ HostBridge::HostBridge(const std::string& confFile) : m_maCaptureDevices(nullptr), m_maDeviceConfig(), m_maDevice(), - m_inputAudio(MBE_SAMPLES_LENGTH * NUMBER_OF_BUFFERS, "Input Audio Buffer"), - m_outputAudio(MBE_SAMPLES_LENGTH * NUMBER_OF_BUFFERS, "Output Audio Buffer"), + m_inputAudio(AUDIO_SAMPLES_LENGTH * NUMBER_OF_BUFFERS, "Input Audio Buffer"), + m_outputAudio(AUDIO_SAMPLES_LENGTH * NUMBER_OF_BUFFERS, "Output Audio Buffer"), m_udpPackets(), m_decoder(nullptr), m_encoder(nullptr), @@ -356,13 +232,16 @@ HostBridge::HostBridge(const std::string& confFile) : m_netLDU2(nullptr), m_p25SeqNo(0U), m_p25N(0U), + m_netId(P25DEF::WACN_STD_DEFAULT), + m_sysId(P25DEF::SID_STD_DEFAULT), + m_analogN(0U), m_audioDetect(false), m_trafficFromUDP(false), m_udpSrcId(0U), m_udpDstId(0U), m_callInProgress(false), m_ignoreCall(false), - m_callAlgoId(p25::defines::ALGO_UNENCRYPT), + m_callAlgoId(P25DEF::ALGO_UNENCRYPT), m_rxStartTime(0U), m_rxStreamId(0U), m_txStreamId(0U), @@ -371,6 +250,10 @@ HostBridge::HostBridge(const std::string& confFile) : m_running(false), m_trace(false), m_debug(false), + m_rtsPttEnable(false), + m_rtsPttPort(), + m_rtsPttController(nullptr), + m_rtsPttActive(false), m_rtpSeqNo(0U), m_rtpTimestamp(INVALID_TS), m_usrpSeqNo(0U) @@ -411,6 +294,12 @@ HostBridge::HostBridge(const std::string& confFile) : HostBridge::~HostBridge() { + if (m_rtsPttController != nullptr) { + m_rtsPttController->close(); + delete m_rtsPttController; + m_rtsPttController = nullptr; + } + delete[] m_ambeBuffer; delete[] m_netLDU1; delete[] m_netLDU2; @@ -518,6 +407,11 @@ int HostBridge::run() if (!ret) return EXIT_FAILURE; + // initialize RTS PTT control + ret = initializeRtsPtt(); + if (!ret) + return EXIT_FAILURE; + ma_result result; if (m_localAudio) { // initialize audio devices @@ -552,7 +446,7 @@ int HostBridge::run() m_maDeviceConfig.playback.channels = 1; m_maDeviceConfig.playback.shareMode = ma_share_mode_shared; - m_maDeviceConfig.periodSizeInFrames = MBE_SAMPLES_LENGTH; + m_maDeviceConfig.periodSizeInFrames = AUDIO_SAMPLES_LENGTH; m_maDeviceConfig.dataCallback = audioCallback; m_maDeviceConfig.pUserData = this; @@ -627,6 +521,10 @@ int HostBridge::run() if (m_txMode == TX_MODE_P25) { m_network->setP25ICCCallback([=](network::NET_ICC::ENUM command, uint32_t dstId) { processInCallCtrl(command, dstId, 0U); }); } + + if (m_txMode == TX_MODE_ANALOG) { + m_network->setAnalogICCCallback([=](network::NET_ICC::ENUM command, uint32_t dstId) { processInCallCtrl(command, dstId, 0U); }); + } } /* @@ -823,7 +721,7 @@ int HostBridge::ambeDecode(const uint8_t* codeword, uint32_t codewordLength, sho assert(codeword != nullptr); assert(samples != nullptr); - //samples = new short[MBE_SAMPLES_LENGTH]; + //samples = new short[AUDIO_SAMPLES_LENGTH]; UInt8Array cw = std::make_unique(codewordLength); ::memcpy(cw.get(), codeword, codewordLength); @@ -855,17 +753,17 @@ int HostBridge::ambeDecode(const uint8_t* codeword, uint32_t codewordLength, sho std::unique_ptr codewordBits = std::make_unique(m_frameLengthInBits * 2); unpackBytesToBits(codewordBits.get(), cw.get(), m_frameLengthInBytes, m_frameLengthInBits); - std::unique_ptr n0 = std::make_unique(MBE_SAMPLES_LENGTH / 2); - ambe_voice_dec(n0.get(), MBE_SAMPLES_LENGTH / 2, codewordBits.get(), NO_BIT_STEAL, m_dcMode, 0, m_decoderState); + std::unique_ptr n0 = std::make_unique(AUDIO_SAMPLES_LENGTH / 2); + ambe_voice_dec(n0.get(), AUDIO_SAMPLES_LENGTH / 2, codewordBits.get(), NO_BIT_STEAL, m_dcMode, 0, m_decoderState); - std::unique_ptr n1 = std::make_unique(MBE_SAMPLES_LENGTH / 2); - ambe_voice_dec(n1.get(), MBE_SAMPLES_LENGTH / 2, codewordBits.get(), NO_BIT_STEAL, m_dcMode, 1, m_decoderState); + std::unique_ptr n1 = std::make_unique(AUDIO_SAMPLES_LENGTH / 2); + ambe_voice_dec(n1.get(), AUDIO_SAMPLES_LENGTH / 2, codewordBits.get(), NO_BIT_STEAL, m_dcMode, 1, m_decoderState); // combine sample segments into contiguous samples - for (int i = 0; i < MBE_SAMPLES_LENGTH / 2; i++) + for (int i = 0; i < AUDIO_SAMPLES_LENGTH / 2; i++) samples[i] = n0[i]; - for (int i = 0; i < MBE_SAMPLES_LENGTH / 2; i++) - samples[i + (MBE_SAMPLES_LENGTH / 2)] = n1[i]; + for (int i = 0; i < AUDIO_SAMPLES_LENGTH / 2; i++) + samples[i + (AUDIO_SAMPLES_LENGTH / 2)] = n1[i]; return 0; // this always just returns no errors? } @@ -929,29 +827,29 @@ void HostBridge::ambeEncode(const short* samples, uint32_t sampleLength, uint8_t //codeword = new byte[this.frameLengthInBytes]; - if (sampleLength > MBE_SAMPLES_LENGTH) { - ::LogError(LOG_HOST, "Samples length is > %u", MBE_SAMPLES_LENGTH); + if (sampleLength > AUDIO_SAMPLES_LENGTH) { + ::LogError(LOG_HOST, "Samples length is > %u", AUDIO_SAMPLES_LENGTH); return; } - if (sampleLength < MBE_SAMPLES_LENGTH) { - ::LogError(LOG_HOST, "Samples length is < %u", MBE_SAMPLES_LENGTH); + if (sampleLength < AUDIO_SAMPLES_LENGTH) { + ::LogError(LOG_HOST, "Samples length is < %u", AUDIO_SAMPLES_LENGTH); return; } std::unique_ptr codewordBits = std::make_unique(m_frameLengthInBits * 2); // split samples into 2 segments - std::unique_ptr n0 = std::make_unique(MBE_SAMPLES_LENGTH / 2); - for (int i = 0; i < MBE_SAMPLES_LENGTH / 2; i++) + std::unique_ptr n0 = std::make_unique(AUDIO_SAMPLES_LENGTH / 2); + for (int i = 0; i < AUDIO_SAMPLES_LENGTH / 2; i++) n0[i] = samples[i]; - ambe_voice_enc(codewordBits.get(), NO_BIT_STEAL, n0.get(), MBE_SAMPLES_LENGTH / 2, m_ecMode, 0, 8192, m_encoderState); + ambe_voice_enc(codewordBits.get(), NO_BIT_STEAL, n0.get(), AUDIO_SAMPLES_LENGTH / 2, m_ecMode, 0, 8192, m_encoderState); - std::unique_ptr n1 = std::make_unique(MBE_SAMPLES_LENGTH / 2); - for (int i = 0; i < MBE_SAMPLES_LENGTH / 2; i++) - n1[i] = samples[i + (MBE_SAMPLES_LENGTH / 2)]; + std::unique_ptr n1 = std::make_unique(AUDIO_SAMPLES_LENGTH / 2); + for (int i = 0; i < AUDIO_SAMPLES_LENGTH / 2; i++) + n1[i] = samples[i + (AUDIO_SAMPLES_LENGTH / 2)]; - ambe_voice_enc(codewordBits.get(), NO_BIT_STEAL, n1.get(), MBE_SAMPLES_LENGTH / 2, m_ecMode, 1, 8192, m_encoderState); + ambe_voice_enc(codewordBits.get(), NO_BIT_STEAL, n1.get(), AUDIO_SAMPLES_LENGTH / 2, m_ecMode, 1, 8192, m_encoderState); // is this to be a DMR codeword? if (m_txMode == TX_MODE_DMR) { @@ -977,6 +875,24 @@ bool HostBridge::readParams() m_identity = systemConf["identity"].as(); + m_netId = (uint32_t)::strtoul(systemConf["netId"].as("BB800").c_str(), NULL, 16); + m_netId = p25::P25Utils::netId(m_netId); + if (m_netId == 0xBEE00) { + ::fatal("error 4\n"); + } + + m_sysId = (uint32_t)::strtoul(systemConf["sysId"].as("001").c_str(), NULL, 16); + m_sysId = p25::P25Utils::sysId(m_sysId); + + /* + ** Site Data + */ + int8_t lto = (int8_t)systemConf["localTimeOffset"].as(0); + p25::SiteData siteData = p25::SiteData(m_netId, m_sysId, 1U, 1U, 0U, 0U, 1U, P25DEF::ServiceClass::VOICE, lto); + siteData.setNetActive(true); + + p25::lc::LC::setSiteData(siteData); + m_rxAudioGain = systemConf["rxAudioGain"].as(1.0f); m_vocoderDecoderAudioGain = systemConf["vocoderDecoderAudioGain"].as(3.0f); m_vocoderDecoderAutoGain = systemConf["vocoderDecoderAutoGain"].as(false); @@ -986,8 +902,8 @@ bool HostBridge::readParams() m_txMode = (uint8_t)systemConf["txMode"].as(1U); if (m_txMode < TX_MODE_DMR) m_txMode = TX_MODE_DMR; - if (m_txMode > TX_MODE_P25) - m_txMode = TX_MODE_P25; + if (m_txMode > TX_MODE_ANALOG) + m_txMode = TX_MODE_ANALOG; m_voxSampleLevel = systemConf["voxSampleLevel"].as(30.0f); m_dropTimeMS = (uint16_t)systemConf["dropTimeMs"].as(180U); @@ -1007,6 +923,8 @@ bool HostBridge::readParams() } } break; + case TX_MODE_ANALOG: + break; } m_localDropTime = Timer(1000U, 0U, m_dropTimeMS); @@ -1031,13 +949,25 @@ bool HostBridge::readParams() m_trace = systemConf["trace"].as(false); m_debug = systemConf["debug"].as(false); + // RTS PTT Configuration + m_rtsPttEnable = systemConf["rtsPttEnable"].as(false); + m_rtsPttPort = systemConf["rtsPttPort"].as("/dev/ttyUSB0"); + + std::string txModeStr = "DMR"; + if (m_txMode == TX_MODE_P25) + txModeStr = "P25"; + if (m_txMode == TX_MODE_ANALOG) + txModeStr = "Analog"; + LogInfo("General Parameters"); + LogInfo(" System Id: $%03X", m_sysId); + LogInfo(" P25 Network Id: $%05X", m_netId); LogInfo(" Rx Audio Gain: %.1f", m_rxAudioGain); LogInfo(" Vocoder Decoder Audio Gain: %.1f", m_vocoderDecoderAudioGain); LogInfo(" Vocoder Decoder Auto Gain: %s", m_vocoderDecoderAutoGain ? "yes" : "no"); LogInfo(" Tx Audio Gain: %.1f", m_txAudioGain); LogInfo(" Vocoder Encoder Audio Gain: %.1f", m_vocoderEncoderAudioGain); - LogInfo(" Transmit Mode: %s", m_txMode == TX_MODE_DMR ? "DMR" : "P25"); + LogInfo(" Transmit Mode: %s", txModeStr.c_str()); LogInfo(" VOX Sample Level: %.1f", m_voxSampleLevel); LogInfo(" Drop Time: %ums", m_dropTimeMS); LogInfo(" Detect Analog MDC1200: %s", m_detectAnalogMDC1200 ? "yes" : "no"); @@ -1048,6 +978,10 @@ bool HostBridge::readParams() LogInfo(" Grant Demands: %s", m_grantDemand ? "yes" : "no"); LogInfo(" Local Audio: %s", m_localAudio ? "yes" : "no"); LogInfo(" UDP Audio: %s", m_udpAudio ? "yes" : "no"); + LogInfo(" RTS PTT Enable: %s", m_rtsPttEnable ? "yes" : "no"); + if (m_rtsPttEnable) { + LogInfo(" RTS PTT Port: %s", m_rtsPttPort.c_str()); + } if (m_debug) { LogInfo(" Debug: yes"); @@ -1107,24 +1041,36 @@ bool HostBridge::createNetwork() m_tekKeyId = (uint32_t)::strtoul(tekConf["tekKeyId"].as("0").c_str(), NULL, 16); if (tekEnable && m_tekKeyId > 0U) { if (tekAlgo == TEK_AES) - m_tekAlgoId = p25::defines::ALGO_AES_256; + m_tekAlgoId = P25DEF::ALGO_AES_256; else if (tekAlgo == TEK_ARC4) - m_tekAlgoId = p25::defines::ALGO_ARC4; + m_tekAlgoId = P25DEF::ALGO_ARC4; else { ::LogError(LOG_HOST, "Invalid TEK algorithm specified, must be \"aes\" or \"adp\"."); - m_tekAlgoId = p25::defines::ALGO_UNENCRYPT; + m_tekAlgoId = P25DEF::ALGO_UNENCRYPT; m_tekKeyId = 0U; } } + if (!tekEnable) + m_tekAlgoId = P25DEF::ALGO_UNENCRYPT; + if (m_tekAlgoId == P25DEF::ALGO_UNENCRYPT) + m_tekKeyId = 0U; + // ensure encryption is currently disabled for DMR (its not supported) - if (m_txMode == TX_MODE_DMR && m_tekAlgoId != p25::defines::ALGO_UNENCRYPT && m_tekKeyId > 0U) { + if (m_txMode == TX_MODE_DMR && m_tekAlgoId != P25DEF::ALGO_UNENCRYPT && m_tekKeyId > 0U) { ::LogError(LOG_HOST, "Encryption is not supported for DMR. Disabling."); - m_tekAlgoId = p25::defines::ALGO_UNENCRYPT; + m_tekAlgoId = P25DEF::ALGO_UNENCRYPT; + m_tekKeyId = 0U; + } + + // ensure encryption is currently disabled for analog (its not supported) + if (m_txMode == TX_MODE_ANALOG && m_tekAlgoId != P25DEF::ALGO_UNENCRYPT && m_tekKeyId > 0U) { + ::LogError(LOG_HOST, "Encryption is not supported for Analog. Disabling."); + m_tekAlgoId = P25DEF::ALGO_UNENCRYPT; m_tekKeyId = 0U; } - m_srcId = (uint32_t)networkConf["sourceId"].as(p25::defines::WUID_FNE); + m_srcId = (uint32_t)networkConf["sourceId"].as(P25DEF::WUID_FNE); m_overrideSrcIdFromMDC = networkConf["overrideSourceIdFromMDC"].as(false); m_overrideSrcIdFromUDP = networkConf["overrideSourceIdFromUDP"].as(false); m_resetCallForSourceIdChange = networkConf["resetCallForSourceIdChange"].as(false); @@ -1154,6 +1100,7 @@ bool HostBridge::createNetwork() } break; case TX_MODE_P25: + case TX_MODE_ANALOG: { if (m_dstId > 65535) { ::LogError(LOG_HOST, "Bridge destination ID cannot be greater than 65535."); @@ -1253,7 +1200,7 @@ bool HostBridge::createNetwork() LogInfo(" Debug: yes"); } - bool dmr = false, p25 = false; + bool dmr = false, p25 = false, analog = false; switch (m_txMode) { case TX_MODE_DMR: dmr = true; @@ -1261,10 +1208,13 @@ bool HostBridge::createNetwork() case TX_MODE_P25: p25 = true; break; + case TX_MODE_ANALOG: + analog = true; + break; } // initialize networking - m_network = new PeerNetwork(address, port, local, id, password, true, debug, dmr, p25, false, true, true, true, allowDiagnosticTransfer, true, false); + m_network = new PeerNetwork(address, port, local, id, password, true, debug, dmr, p25, false, analog, true, true, true, allowDiagnosticTransfer, true, false); m_network->setMetadata(m_identity, 0U, 0U, 0.0F, 0.0F, 0, 0, 0, 0.0F, 0.0F, 0, ""); m_network->setConventional(true); @@ -1317,7 +1267,7 @@ void HostBridge::processUDPAudio() if (length > 0) { if (m_trace) - Utils::dump(1U, "HostBridge()::processUDPAudio() Audio Network Packet", buffer, length); + Utils::dump(1U, "HostBridge()::processUDPAudio(), Audio Network Packet", buffer, length); uint32_t pcmLength = 0; if (m_udpNoIncludeLength) { @@ -1327,7 +1277,7 @@ void HostBridge::processUDPAudio() } if (m_udpRTPFrames || m_udpUsrp) - pcmLength = MBE_SAMPLES_LENGTH * 2U; + pcmLength = AUDIO_SAMPLES_LENGTH * 2U; DECLARE_UINT8_ARRAY(pcm, pcmLength); @@ -1341,7 +1291,7 @@ void HostBridge::processUDPAudio() return; } - ::memcpy(pcm, buffer + RTP_HEADER_LENGTH_BYTES, MBE_SAMPLES_LENGTH * 2U); + ::memcpy(pcm, buffer + RTP_HEADER_LENGTH_BYTES, AUDIO_SAMPLES_LENGTH * 2U); } else { if (m_udpNoIncludeLength) { @@ -1362,7 +1312,7 @@ void HostBridge::processUDPAudio() delete[] usrpHeader; } - // Utils::dump(1U, "PCM RECV BYTE BUFFER", pcm, pcmLength); + // Utils::dump(1U, "HostBridge::processUDPAudio(), PCM RECV BYTE BUFFER", pcm, pcmLength); NetPacketRequest* req = new NetPacketRequest(); req->pcm = new uint8_t[pcmLength]; @@ -1627,7 +1577,7 @@ void HostBridge::decodeDMRAudioFrame(uint8_t* ambe, uint32_t srcId, uint32_t dst for (uint32_t i = 0; i < RAW_AMBE_LENGTH_BYTES; i++) ambePartial[i] = ambe[i + (n * 9)]; - short samples[MBE_SAMPLES_LENGTH]; + short samples[AUDIO_SAMPLES_LENGTH]; int errs = 0; #if defined(_WIN32) if (m_useExternalVocoder) { @@ -1644,69 +1594,57 @@ void HostBridge::decodeDMRAudioFrame(uint8_t* ambe, uint32_t srcId, uint32_t dst LogMessage(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 - if (m_rxAudioGain != 1.0f) { - for (int n = 0; n < MBE_SAMPLES_LENGTH; n++) { - short sample = samples[n]; - float newSample = sample * m_rxAudioGain; - sample = (short)newSample; - - // clip if necessary - if (newSample > 32767) - sample = 32767; - else if (newSample < -32767) - sample = -32767; - - samples[n] = sample; - } - } + AnalogAudio::gain(samples, AUDIO_SAMPLES_LENGTH, m_rxAudioGain); if (m_localAudio) { - m_outputAudio.addData(samples, MBE_SAMPLES_LENGTH); + 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[MBE_SAMPLES_LENGTH * 2U]; + uint8_t pcm[AUDIO_SAMPLES_LENGTH * 2U]; if (m_udpUseULaw) { - for (uint32_t smpIdx = 0; smpIdx < MBE_SAMPLES_LENGTH; smpIdx++) { - pcm[smpIdx] = encodeMuLaw(samples[smpIdx]); + 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, MBE_SAMPLES_LENGTH); + Utils::dump(1U, "HostBridge()::decodeDMRAudioFrame(), Encoded uLaw Audio", pcm, AUDIO_SAMPLES_LENGTH); } else { - for (uint32_t smpIdx = 0; smpIdx < MBE_SAMPLES_LENGTH; smpIdx++) { + 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; } } - uint32_t length = (MBE_SAMPLES_LENGTH * 2U) + 4U; + uint32_t length = (AUDIO_SAMPLES_LENGTH * 2U) + 4U; uint8_t* audioData = nullptr; if (!m_udpUsrp) { if (!m_udpMetadata) { - audioData = new uint8_t[(MBE_SAMPLES_LENGTH * 2U) + 4U]; // PCM + 4 bytes (PCM length) + audioData = new uint8_t[(AUDIO_SAMPLES_LENGTH * 2U) + 4U]; // PCM + 4 bytes (PCM length) if (m_udpUseULaw) { - length = (MBE_SAMPLES_LENGTH)+4U; + length = (AUDIO_SAMPLES_LENGTH)+4U; if (m_udpNoIncludeLength) { - length = MBE_SAMPLES_LENGTH; - ::memcpy(audioData, pcm, MBE_SAMPLES_LENGTH); + length = AUDIO_SAMPLES_LENGTH; + ::memcpy(audioData, pcm, AUDIO_SAMPLES_LENGTH); } else { - SET_UINT32(MBE_SAMPLES_LENGTH, audioData, 0U); - ::memcpy(audioData + 4U, pcm, MBE_SAMPLES_LENGTH); + SET_UINT32(AUDIO_SAMPLES_LENGTH, audioData, 0U); + ::memcpy(audioData + 4U, pcm, AUDIO_SAMPLES_LENGTH); } // are we sending RTP audio frames? if (m_udpRTPFrames) { - uint8_t* rtpFrame = generateRTPHeaders(MBE_SAMPLES_LENGTH, m_rtpSeqNo); + uint8_t* rtpFrame = generateRTPHeaders(AUDIO_SAMPLES_LENGTH, m_rtpSeqNo); if (rtpFrame != nullptr) { length += RTP_HEADER_LENGTH_BYTES; uint8_t* newAudioData = new uint8_t[length]; ::memcpy(newAudioData, rtpFrame, RTP_HEADER_LENGTH_BYTES); - ::memcpy(newAudioData + RTP_HEADER_LENGTH_BYTES, audioData, MBE_SAMPLES_LENGTH); + ::memcpy(newAudioData + RTP_HEADER_LENGTH_BYTES, audioData, AUDIO_SAMPLES_LENGTH); delete[] audioData; audioData = newAudioData; @@ -1716,26 +1654,26 @@ void HostBridge::decodeDMRAudioFrame(uint8_t* ambe, uint32_t srcId, uint32_t dst } } else { - SET_UINT32((MBE_SAMPLES_LENGTH * 2U), audioData, 0U); - ::memcpy(audioData + 4U, pcm, MBE_SAMPLES_LENGTH * 2U); + SET_UINT32((AUDIO_SAMPLES_LENGTH * 2U), audioData, 0U); + ::memcpy(audioData + 4U, pcm, AUDIO_SAMPLES_LENGTH * 2U); } } else { - length = (MBE_SAMPLES_LENGTH * 2U) + 12U; - audioData = new uint8_t[(MBE_SAMPLES_LENGTH * 2U) + 12U]; // PCM + (4 bytes (PCM length) + 4 bytes (srcId) + 4 bytes (dstId)) - SET_UINT32((MBE_SAMPLES_LENGTH * 2U), audioData, 0U); - ::memcpy(audioData + 4U, pcm, MBE_SAMPLES_LENGTH * 2U); + length = (AUDIO_SAMPLES_LENGTH * 2U) + 12U; + audioData = new uint8_t[(AUDIO_SAMPLES_LENGTH * 2U) + 12U]; // PCM + (4 bytes (PCM length) + 4 bytes (srcId) + 4 bytes (dstId)) + SET_UINT32((AUDIO_SAMPLES_LENGTH * 2U), audioData, 0U); + ::memcpy(audioData + 4U, pcm, AUDIO_SAMPLES_LENGTH * 2U); // embed destination and source IDs - SET_UINT32(dstId, audioData, ((MBE_SAMPLES_LENGTH * 2U) + 4U)); - SET_UINT32(srcId, audioData, ((MBE_SAMPLES_LENGTH * 2U) + 8U)); + SET_UINT32(dstId, audioData, ((AUDIO_SAMPLES_LENGTH * 2U) + 4U)); + SET_UINT32(srcId, audioData, ((AUDIO_SAMPLES_LENGTH * 2U) + 8U)); } } else { uint8_t* usrpHeader = new uint8_t[USRP_HEADER_LENGTH]; - length = (MBE_SAMPLES_LENGTH * 2U) + USRP_HEADER_LENGTH; - audioData = new uint8_t[(MBE_SAMPLES_LENGTH * 2U) + USRP_HEADER_LENGTH]; // PCM + 32 bytes (USRP Header) + length = (AUDIO_SAMPLES_LENGTH * 2U) + USRP_HEADER_LENGTH; + audioData = new uint8_t[(AUDIO_SAMPLES_LENGTH * 2U) + USRP_HEADER_LENGTH]; // PCM + 32 bytes (USRP Header) m_usrpSeqNo++; usrpHeader[15U] = 1; // set PTT state to true @@ -1743,7 +1681,7 @@ void HostBridge::decodeDMRAudioFrame(uint8_t* ambe, uint32_t srcId, uint32_t dst ::memcpy(usrpHeader, "USRP", 4); ::memcpy(audioData, usrpHeader, USRP_HEADER_LENGTH); // copy USRP header into the UDP payload - ::memcpy(audioData + USRP_HEADER_LENGTH, pcm, MBE_SAMPLES_LENGTH * 2U); + ::memcpy(audioData + USRP_HEADER_LENGTH, pcm, AUDIO_SAMPLES_LENGTH * 2U); } sockaddr_storage addr; @@ -1765,6 +1703,7 @@ void HostBridge::encodeDMRAudioFrame(uint8_t* pcm, uint32_t forcedSrcId, uint32_ 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)) @@ -1805,17 +1744,19 @@ void HostBridge::encodeDMRAudioFrame(uint8_t* pcm, uint32_t forcedSrcId, uint32_ fullLC.encode(dmrLC, data, DataType::VOICE_LC_HEADER); // generate DMR network frame - data::NetData dmrData; + NetData dmrData; dmrData.setSlotNo(m_slot); dmrData.setDataType(DataType::VOICE_LC_HEADER); dmrData.setSrcId(srcId); dmrData.setDstId(dstId); dmrData.setFLCO(FLCO::GROUP); - if (m_grantDemand) { - dmrData.setControl(0x80U); // DMR remote grant demand flag - } else { - dmrData.setControl(0U); - } + + 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); @@ -1850,7 +1791,7 @@ void HostBridge::encodeDMRAudioFrame(uint8_t* pcm, uint32_t forcedSrcId, uint32_ uint8_t lcss = m_dmrEmbeddedData.getData(data, m_dmrN); // generated embedded signalling - data::EMB emb = data::EMB(); + EMB emb = EMB(); emb.setColorCode(0U); emb.setLCSS(lcss); emb.encode(data); @@ -1859,7 +1800,7 @@ void HostBridge::encodeDMRAudioFrame(uint8_t* pcm, uint32_t forcedSrcId, uint32_ LogMessage(LOG_HOST, DMR_DT_VOICE ", srcId = %u, dstId = %u, slot = %u, seqNo = %u", srcId, dstId, m_slot, m_dmrN); // generate DMR network frame - data::NetData dmrData; + NetData dmrData; dmrData.setSlotNo(m_slot); dmrData.setDataType(dataType); dmrData.setSrcId(srcId); @@ -1881,35 +1822,21 @@ void HostBridge::encodeDMRAudioFrame(uint8_t* pcm, uint32_t forcedSrcId, uint32_ } int smpIdx = 0; - short samples[MBE_SAMPLES_LENGTH]; - for (uint32_t pcmIdx = 0; pcmIdx < (MBE_SAMPLES_LENGTH * 2U); pcmIdx += 2) { + 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 - if (m_txAudioGain != 1.0f) { - for (int n = 0; n < MBE_SAMPLES_LENGTH; n++) { - short sample = samples[n]; - float newSample = sample * m_txAudioGain; - sample = (short)newSample; - - // clip if necessary - if (newSample > 32767) - sample = 32767; - else if (newSample < -32767) - sample = -32767; - - samples[n] = sample; - } - } + 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, MBE_SAMPLES_LENGTH, ambe); + ambeEncode(samples, AUDIO_SAMPLES_LENGTH, ambe); } else { #endif // defined(_WIN32) @@ -1918,7 +1845,7 @@ void HostBridge::encodeDMRAudioFrame(uint8_t* pcm, uint32_t forcedSrcId, uint32_ } #endif // defined(_WIN32) - // Utils::dump(1U, "Encoded AMBE", ambe, RAW_AMBE_LENGTH_BYTES); + // 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++; @@ -1932,13 +1859,14 @@ void HostBridge::processP25Network(uint8_t* buffer, uint32_t length) using namespace p25; using namespace p25::defines; using namespace p25::dfsi::defines; + using namespace p25::data; if (m_txMode != TX_MODE_P25) return; - bool grantDemand = (buffer[14U] & 0x80U) == 0x80U; - bool grantDenial = (buffer[14U] & 0x40U) == 0x40U; - bool unitToUnit = (buffer[14U] & 0x01U) == 0x01U; + 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]; @@ -1978,7 +1906,7 @@ void HostBridge::processP25Network(uint8_t* buffer, uint32_t length) uint8_t lsd2 = buffer[21U]; lc::LC control; - data::LowSpeedData lsd; + LowSpeedData lsd; control.setLCO(lco); control.setSrcId(srcId); @@ -2290,14 +2218,14 @@ void HostBridge::decodeP25AudioFrame(uint8_t* ldu, uint32_t srcId, uint32_t dstI break; } - // Utils::dump(1U, "IMBE", imbe, RAW_IMBE_LENGTH_BYTES); + // Utils::dump(1U, "HostBridge::decodeP25AudioFrame(), IMBE", imbe, RAW_IMBE_LENGTH_BYTES); - if (m_tekAlgoId != p25::defines::ALGO_UNENCRYPT && m_tekKeyId > 0U && m_p25Crypto->getTEKLength() > 0U) { + if (m_tekAlgoId != P25DEF::ALGO_UNENCRYPT && m_tekKeyId > 0U && m_p25Crypto->getTEKLength() > 0U) { switch (m_tekAlgoId) { - case p25::defines::ALGO_AES_256: + case P25DEF::ALGO_AES_256: m_p25Crypto->cryptAES_IMBE(imbe, (p25N == 1U) ? DUID::LDU1 : DUID::LDU2); break; - case p25::defines::ALGO_ARC4: + case P25DEF::ALGO_ARC4: m_p25Crypto->cryptARC4_IMBE(imbe, (p25N == 1U) ? DUID::LDU1 : DUID::LDU2); break; default: @@ -2306,7 +2234,7 @@ void HostBridge::decodeP25AudioFrame(uint8_t* ldu, uint32_t srcId, uint32_t dstI } } - short samples[MBE_SAMPLES_LENGTH]; + short samples[AUDIO_SAMPLES_LENGTH]; int errs = 0; #if defined(_WIN32) if (m_useExternalVocoder) { @@ -2323,70 +2251,58 @@ void HostBridge::decodeP25AudioFrame(uint8_t* ldu, uint32_t srcId, uint32_t dstI 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 - if (m_rxAudioGain != 1.0f) { - for (int n = 0; n < MBE_SAMPLES_LENGTH; n++) { - short sample = samples[n]; - float newSample = sample * m_rxAudioGain; - sample = (short)newSample; - - // clip if necessary - if (newSample > 32767) - sample = 32767; - else if (newSample < -32767) - sample = -32767; - - samples[n] = sample; - } - } + AnalogAudio::gain(samples, AUDIO_SAMPLES_LENGTH, m_rxAudioGain); if (m_localAudio) { - m_outputAudio.addData(samples, MBE_SAMPLES_LENGTH); + 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[MBE_SAMPLES_LENGTH * 2U]; + uint8_t pcm[AUDIO_SAMPLES_LENGTH * 2U]; if (m_udpUseULaw) { - for (uint32_t smpIdx = 0; smpIdx < MBE_SAMPLES_LENGTH; smpIdx++) { - pcm[smpIdx] = encodeMuLaw(samples[smpIdx]); + 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, MBE_SAMPLES_LENGTH); + Utils::dump(1U, "HostBridge()::decodeP25AudioFrame(), Encoded uLaw Audio", pcm, AUDIO_SAMPLES_LENGTH); } else { - for (uint32_t smpIdx = 0; smpIdx < MBE_SAMPLES_LENGTH; smpIdx++) { + 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; } } - uint32_t length = (MBE_SAMPLES_LENGTH * 2U) + 4U; + uint32_t length = (AUDIO_SAMPLES_LENGTH * 2U) + 4U; uint8_t* audioData = nullptr; if (!m_udpUsrp) { if (!m_udpMetadata) { - audioData = new uint8_t[(MBE_SAMPLES_LENGTH * 2U) + 4U]; // PCM + 4 bytes (PCM length) + audioData = new uint8_t[(AUDIO_SAMPLES_LENGTH * 2U) + 4U]; // PCM + 4 bytes (PCM length) if (m_udpUseULaw) { - length = (MBE_SAMPLES_LENGTH)+4U; + length = (AUDIO_SAMPLES_LENGTH)+4U; if (m_udpNoIncludeLength) { - length = MBE_SAMPLES_LENGTH; - ::memcpy(audioData, pcm, MBE_SAMPLES_LENGTH); + length = AUDIO_SAMPLES_LENGTH; + ::memcpy(audioData, pcm, AUDIO_SAMPLES_LENGTH); } else { - SET_UINT32(MBE_SAMPLES_LENGTH, audioData, 0U); - ::memcpy(audioData + 4U, pcm, MBE_SAMPLES_LENGTH); + SET_UINT32(AUDIO_SAMPLES_LENGTH, audioData, 0U); + ::memcpy(audioData + 4U, pcm, AUDIO_SAMPLES_LENGTH); } // are we sending RTP audio frames? if (m_udpRTPFrames) { - uint8_t* rtpFrame = generateRTPHeaders(MBE_SAMPLES_LENGTH, m_rtpSeqNo); + uint8_t* rtpFrame = generateRTPHeaders(AUDIO_SAMPLES_LENGTH, m_rtpSeqNo); if (rtpFrame != nullptr) { length += RTP_HEADER_LENGTH_BYTES; uint8_t* newAudioData = new uint8_t[length]; ::memcpy(newAudioData, rtpFrame, RTP_HEADER_LENGTH_BYTES); - ::memcpy(newAudioData + RTP_HEADER_LENGTH_BYTES, audioData, MBE_SAMPLES_LENGTH); + ::memcpy(newAudioData + RTP_HEADER_LENGTH_BYTES, audioData, AUDIO_SAMPLES_LENGTH); delete[] audioData; audioData = newAudioData; @@ -2396,26 +2312,26 @@ void HostBridge::decodeP25AudioFrame(uint8_t* ldu, uint32_t srcId, uint32_t dstI } } else { - SET_UINT32((MBE_SAMPLES_LENGTH * 2U), audioData, 0U); - ::memcpy(audioData + 4U, pcm, MBE_SAMPLES_LENGTH * 2U); + SET_UINT32((AUDIO_SAMPLES_LENGTH * 2U), audioData, 0U); + ::memcpy(audioData + 4U, pcm, AUDIO_SAMPLES_LENGTH * 2U); } } else { - length = (MBE_SAMPLES_LENGTH * 2U) + 12U; - audioData = new uint8_t[(MBE_SAMPLES_LENGTH * 2U) + 12U]; // PCM + (4 bytes (PCM length) + 4 bytes (srcId) + 4 bytes (dstId)) - SET_UINT32((MBE_SAMPLES_LENGTH * 2U), audioData, 0U); - ::memcpy(audioData + 4U, pcm, MBE_SAMPLES_LENGTH * 2U); + length = (AUDIO_SAMPLES_LENGTH * 2U) + 12U; + audioData = new uint8_t[(AUDIO_SAMPLES_LENGTH * 2U) + 12U]; // PCM + (4 bytes (PCM length) + 4 bytes (srcId) + 4 bytes (dstId)) + SET_UINT32((AUDIO_SAMPLES_LENGTH * 2U), audioData, 0U); + ::memcpy(audioData + 4U, pcm, AUDIO_SAMPLES_LENGTH * 2U); // embed destination and source IDs - SET_UINT32(dstId, audioData, ((MBE_SAMPLES_LENGTH * 2U) + 4U)); - SET_UINT32(srcId, audioData, ((MBE_SAMPLES_LENGTH * 2U) + 8U)); + SET_UINT32(dstId, audioData, ((AUDIO_SAMPLES_LENGTH * 2U) + 4U)); + SET_UINT32(srcId, audioData, ((AUDIO_SAMPLES_LENGTH * 2U) + 8U)); } } else { uint8_t* usrpHeader = new uint8_t[USRP_HEADER_LENGTH]; - length = (MBE_SAMPLES_LENGTH * 2U) + USRP_HEADER_LENGTH; - audioData = new uint8_t[(MBE_SAMPLES_LENGTH * 2U) + USRP_HEADER_LENGTH]; // PCM + 32 bytes (USRP Header) + length = (AUDIO_SAMPLES_LENGTH * 2U) + USRP_HEADER_LENGTH; + audioData = new uint8_t[(AUDIO_SAMPLES_LENGTH * 2U) + USRP_HEADER_LENGTH]; // PCM + 32 bytes (USRP Header) m_usrpSeqNo++; usrpHeader[15U] = 1; // set PTT state to true @@ -2423,7 +2339,7 @@ void HostBridge::decodeP25AudioFrame(uint8_t* ldu, uint32_t srcId, uint32_t dstI ::memcpy(usrpHeader, "USRP", 4); ::memcpy(audioData, usrpHeader, USRP_HEADER_LENGTH); // copy USRP header into the UDP payload - ::memcpy(audioData + USRP_HEADER_LENGTH, pcm, MBE_SAMPLES_LENGTH * 2U); + ::memcpy(audioData + USRP_HEADER_LENGTH, pcm, AUDIO_SAMPLES_LENGTH * 2U); } sockaddr_storage addr; @@ -2445,6 +2361,7 @@ void HostBridge::encodeP25AudioFrame(uint8_t* pcm, uint32_t forcedSrcId, uint32_ assert(pcm != nullptr); using namespace p25; using namespace p25::defines; + using namespace p25::data; if (m_p25N > 17) m_p25N = 0; @@ -2454,35 +2371,21 @@ void HostBridge::encodeP25AudioFrame(uint8_t* pcm, uint32_t forcedSrcId, uint32_ ::memset(m_netLDU2, 0x00U, 9U * 25U); int smpIdx = 0; - short samples[MBE_SAMPLES_LENGTH]; - for (uint32_t pcmIdx = 0; pcmIdx < (MBE_SAMPLES_LENGTH * 2U); pcmIdx += 2) { + 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 - if (m_txAudioGain != 1.0f) { - for (int n = 0; n < MBE_SAMPLES_LENGTH; n++) { - short sample = samples[n]; - float newSample = sample * m_txAudioGain; - sample = (short)newSample; - - // clip if necessary - if (newSample > 32767) - sample = 32767; - else if (newSample < -32767) - sample = -32767; - - samples[n] = sample; - } - } + 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, MBE_SAMPLES_LENGTH, imbe); + ambeEncode(samples, AUDIO_SAMPLES_LENGTH, imbe); } else { #endif // defined(_WIN32) @@ -2491,9 +2394,9 @@ void HostBridge::encodeP25AudioFrame(uint8_t* pcm, uint32_t forcedSrcId, uint32_ } #endif // defined(_WIN32) - // Utils::dump(1U, "Encoded IMBE", imbe, RAW_IMBE_LENGTH_BYTES); + // Utils::dump(1U, "HostBridge::encodeP25AudioFrame(), Encoded IMBE", imbe, RAW_IMBE_LENGTH_BYTES); - if (m_tekAlgoId != p25::defines::ALGO_UNENCRYPT && m_tekKeyId > 0U && m_p25Crypto->getTEKLength() > 0U) { + 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()) { @@ -2504,10 +2407,10 @@ void HostBridge::encodeP25AudioFrame(uint8_t* pcm, uint32_t forcedSrcId, uint32_ // perform crypto switch (m_tekAlgoId) { - case p25::defines::ALGO_AES_256: + case P25DEF::ALGO_AES_256: m_p25Crypto->cryptAES_IMBE(imbe, (m_p25N < 9U) ? DUID::LDU1 : DUID::LDU2); break; - case p25::defines::ALGO_ARC4: + case P25DEF::ALGO_ARC4: m_p25Crypto->cryptARC4_IMBE(imbe, (m_p25N < 9U) ? DUID::LDU1 : DUID::LDU2); break; default: @@ -2614,28 +2517,295 @@ void HostBridge::encodeP25AudioFrame(uint8_t* pcm, uint32_t forcedSrcId, uint32_ m_p25Crypto->getMI(mi); lc.setMI(mi); - data::LowSpeedData lsd = data::LowSpeedData(); + LowSpeedData lsd = LowSpeedData(); + + uint8_t controlByte = network::NET_CTRL_SWITCH_OVER; // send P25 LDU1 if (m_p25N == 8U) { LogMessage(LOG_HOST, P25_LDU1_STR " audio, srcId = %u, dstId = %u", srcId, dstId); - m_network->writeP25LDU1(lc, lsd, m_netLDU1, FrameType::HDU_VALID); + m_network->writeP25LDU1(lc, lsd, m_netLDU1, FrameType::HDU_VALID, controlByte); m_txStreamId = m_network->getP25StreamId(); } // send P25 LDU2 if (m_p25N == 17U) { LogMessage(LOG_HOST, P25_LDU2_STR " audio, algo = $%02X, kid = $%04X", m_tekAlgoId, m_tekKeyId); - m_network->writeP25LDU2(lc, lsd, m_netLDU2); + m_network->writeP25LDU2(lc, lsd, m_netLDU2, controlByte); } m_p25SeqNo++; m_p25N++; } +/* 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; + + // is this a new call stream? + if (m_network->getAnalogStreamId() != m_rxStreamId) { + m_callInProgress = true; + m_callAlgoId = 0U; + + uint64_t now = std::chrono::duration_cast(std::chrono::system_clock::now().time_since_epoch()).count(); + m_rxStartTime = now; + + LogMessage(LOG_HOST, "Analog, call start, srcId = %u, dstId = %u", srcId, dstId); + if (m_preambleLeaderTone) + generatePreambleTone(); + } + + if (frameType == AudioFrameType::TERMINATOR) { + m_callInProgress = false; + m_ignoreCall = false; + m_callAlgoId = 0U; + + if (m_rxStartTime > 0U) { + uint64_t now = std::chrono::duration_cast(std::chrono::system_clock::now().time_since_epoch()).count(); + uint64_t diff = now - m_rxStartTime; + + LogMessage(LOG_HOST, "Analog, call end, srcId = %u, dstId = %u, dur = %us", srcId, dstId, diff / 1000U); + } + + m_rxStartTime = 0U; + m_rxStreamId = 0U; + + m_rtpSeqNo = 0U; + m_rtpTimestamp = INVALID_TS; + return; + } + + if (m_ignoreCall && m_callAlgoId == 0U) + m_ignoreCall = false; + + if (m_ignoreCall) + return; + + if (frameType == AudioFrameType::VOICE_START || frameType == AudioFrameType::VOICE) { + LogMessage(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); + } + 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; + } + } + + uint32_t length = (AUDIO_SAMPLES_LENGTH * 2U) + 4U; + uint8_t* audioData = nullptr; + + if (!m_udpUsrp) { + if (!m_udpMetadata) { + audioData = new uint8_t[(AUDIO_SAMPLES_LENGTH * 2U) + 4U]; // PCM + 4 bytes (PCM length) + if (m_udpUseULaw) { + length = (AUDIO_SAMPLES_LENGTH)+4U; + if (m_udpNoIncludeLength) { + length = AUDIO_SAMPLES_LENGTH; + ::memcpy(audioData, pcm, AUDIO_SAMPLES_LENGTH); + } + else { + SET_UINT32(AUDIO_SAMPLES_LENGTH, audioData, 0U); + ::memcpy(audioData + 4U, pcm, AUDIO_SAMPLES_LENGTH); + } + + // are we sending RTP audio frames? + if (m_udpRTPFrames) { + uint8_t* rtpFrame = generateRTPHeaders(AUDIO_SAMPLES_LENGTH, m_rtpSeqNo); + if (rtpFrame != nullptr) { + length += RTP_HEADER_LENGTH_BYTES; + uint8_t* newAudioData = new uint8_t[length]; + ::memcpy(newAudioData, rtpFrame, RTP_HEADER_LENGTH_BYTES); + ::memcpy(newAudioData + RTP_HEADER_LENGTH_BYTES, audioData, AUDIO_SAMPLES_LENGTH); + delete[] audioData; + + audioData = newAudioData; + } + + m_rtpSeqNo++; + } + } + else { + SET_UINT32((AUDIO_SAMPLES_LENGTH * 2U), audioData, 0U); + ::memcpy(audioData + 4U, pcm, AUDIO_SAMPLES_LENGTH * 2U); + } + } + else { + length = (AUDIO_SAMPLES_LENGTH * 2U) + 12U; + audioData = new uint8_t[(AUDIO_SAMPLES_LENGTH * 2U) + 12U]; // PCM + (4 bytes (PCM length) + 4 bytes (srcId) + 4 bytes (dstId)) + SET_UINT32((AUDIO_SAMPLES_LENGTH * 2U), audioData, 0U); + ::memcpy(audioData + 4U, pcm, AUDIO_SAMPLES_LENGTH * 2U); + + // embed destination and source IDs + SET_UINT32(dstId, audioData, ((AUDIO_SAMPLES_LENGTH * 2U) + 4U)); + SET_UINT32(srcId, audioData, ((AUDIO_SAMPLES_LENGTH * 2U) + 8U)); + } + } + else { + uint8_t* usrpHeader = new uint8_t[USRP_HEADER_LENGTH]; + + length = (AUDIO_SAMPLES_LENGTH * 2U) + USRP_HEADER_LENGTH; + audioData = new uint8_t[(AUDIO_SAMPLES_LENGTH * 2U) + USRP_HEADER_LENGTH]; // PCM + 32 bytes (USRP Header) + + m_usrpSeqNo++; + usrpHeader[15U] = 1; // set PTT state to true + SET_UINT32(m_usrpSeqNo, usrpHeader, 4U); + + ::memcpy(usrpHeader, "USRP", 4); + ::memcpy(audioData, usrpHeader, USRP_HEADER_LENGTH); // copy USRP header into the UDP payload + ::memcpy(audioData + USRP_HEADER_LENGTH, pcm, AUDIO_SAMPLES_LENGTH * 2U); + } + + sockaddr_storage addr; + uint32_t addrLen; + + if (udp::Socket::lookup(m_udpSendAddress, m_udpSendPort, addr, addrLen) == 0) { + m_udpAudioSocket->write(audioData, length, addr, addrLen); + } + + delete[] audioData; + } + } + + 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); + + m_network->writeAnalog(analogData); + m_txStreamId = m_network->getAnalogStreamId(); + m_analogN++; +} + /* Helper to send USRP end of transmission */ -void HostBridge::sendUsrpEot() +void HostBridge::sendUsrpEot() { sockaddr_storage addr; uint32_t addrLen; @@ -2656,7 +2826,7 @@ void HostBridge::generatePreambleTone() { std::lock_guard lock(m_audioMutex); - uint64_t frameCount = SampleTimeConvert::ToSamples(SAMPLE_RATE, 1, m_preambleLength); + uint64_t frameCount = AnalogAudio::toSamples(SAMPLE_RATE, 1, m_preambleLength); if (frameCount > m_outputAudio.freeSpace()) { ::LogError(LOG_HOST, "failed to generate preamble tone"); return; @@ -2670,8 +2840,7 @@ void HostBridge::generatePreambleTone() ma_waveform_read_pcm_frames(&m_maSineWaveform, sine, frameCount, NULL); int smpIdx = 0; - std::unique_ptr __UNIQUE_sineSamples = std::make_unique(frameCount); - short* sineSamples = __UNIQUE_sineSamples.get(); + DECLARE_SHORT_ARRAY(sineSamples, frameCount); const uint8_t* pcm = (const uint8_t*)sine; for (uint32_t pcmIdx = 0; pcmIdx < pcmBytes; pcmIdx += 2) { sineSamples[smpIdx] = (short)((pcm[pcmIdx + 1] << 8) + pcm[pcmIdx + 0]); @@ -2745,47 +2914,67 @@ void HostBridge::callEnd(uint32_t srcId, uint32_t dstId) if (!m_callInProgress) { switch (m_txMode) { case TX_MODE_DMR: - { - dmr::defines::DataType::E dataType = dmr::defines::DataType::VOICE_SYNC; - if (m_dmrN == 0) - dataType = dmr::defines::DataType::VOICE_SYNC; - else { - dataType = dmr::defines::DataType::VOICE; - } - - dmr::data::NetData data = dmr::data::NetData(); - data.setSlotNo(m_slot); - data.setDataType(dataType); - data.setSrcId(srcId); - data.setDstId(dstId); - data.setFLCO(dmr::defines::FLCO::GROUP); - data.setN(m_dmrN); - data.setSeqNo(m_dmrSeqNo); - data.setBER(0U); - data.setRSSI(0U); - - LogMessage(LOG_HOST, DMR_DT_TERMINATOR_WITH_LC ", slot = %u, dstId = %u", m_slot, dstId); + { + DMRDEF::DataType::E dataType = DMRDEF::DataType::VOICE_SYNC; + if (m_dmrN == 0) + dataType = DMRDEF::DataType::VOICE_SYNC; + else { + dataType = DMRDEF::DataType::VOICE; + } - m_network->writeDMRTerminator(data, &m_dmrSeqNo, &m_dmrN, m_dmrEmbeddedData); - m_network->resetDMR(data.getSlotNo()); - } - break; + dmr::data::NetData data = dmr::data::NetData(); + data.setSlotNo(m_slot); + data.setDataType(dataType); + data.setSrcId(srcId); + data.setDstId(dstId); + data.setFLCO(DMRDEF::FLCO::GROUP); + data.setN(m_dmrN); + data.setSeqNo(m_dmrSeqNo); + data.setBER(0U); + data.setRSSI(0U); + + LogMessage(LOG_HOST, DMR_DT_TERMINATOR_WITH_LC ", slot = %u, dstId = %u", m_slot, dstId); + + m_network->writeDMRTerminator(data, &m_dmrSeqNo, &m_dmrN, m_dmrEmbeddedData); + m_network->resetDMR(data.getSlotNo()); + } + break; case TX_MODE_P25: - { - p25::lc::LC lc = p25::lc::LC(); - lc.setLCO(p25::defines::LCO::GROUP); - lc.setDstId(dstId); - lc.setSrcId(srcId); + { + p25::lc::LC lc = p25::lc::LC(); + lc.setLCO(P25DEF::LCO::GROUP); + lc.setDstId(dstId); + lc.setSrcId(srcId); - p25::data::LowSpeedData lsd = p25::data::LowSpeedData(); + p25::data::LowSpeedData lsd = p25::data::LowSpeedData(); - LogMessage(LOG_HOST, P25_TDU_STR); + LogMessage(LOG_HOST, P25_TDU_STR); - uint8_t controlByte = 0x00U; - m_network->writeP25TDU(lc, lsd, controlByte); - m_network->resetP25(); - } - break; + uint8_t controlByte = 0x00U; + m_network->writeP25TDU(lc, lsd, controlByte); + m_network->resetP25(); + } + break; + case TX_MODE_ANALOG: + { + LogMessage(LOG_HOST, ANO_TERMINATOR); + + uint8_t controlByte = 0x00U; + + data::NetData analogData; + analogData.setSeqNo(m_analogN); + analogData.setSrcId(srcId); + analogData.setDstId(dstId); + analogData.setFrameType(AudioFrameType::TERMINATOR); + + uint8_t pcm[AUDIO_SAMPLES_LENGTH * 2U]; + ::memset(pcm, 0x00U, AUDIO_SAMPLES_LENGTH * 2U); + analogData.setAudio(pcm); + + m_network->writeAnalog(analogData, true); + m_network->resetAnalog(); + } + break; } } @@ -2801,6 +2990,7 @@ void HostBridge::callEnd(uint32_t srcId, uint32_t dstId) m_dmrN = 0U; m_p25SeqNo = 0U; m_p25N = 0U; + m_analogN = 0U; m_rtpSeqNo = 0U; m_rtpTimestamp = INVALID_TS; @@ -2871,13 +3061,13 @@ void* HostBridge::threadAudioProcess(void* arg) { std::lock_guard lock(m_audioMutex); - if (bridge->m_inputAudio.dataSize() >= MBE_SAMPLES_LENGTH) { - short samples[MBE_SAMPLES_LENGTH]; - bridge->m_inputAudio.get(samples, MBE_SAMPLES_LENGTH); + if (bridge->m_inputAudio.dataSize() >= AUDIO_SAMPLES_LENGTH) { + short samples[AUDIO_SAMPLES_LENGTH]; + bridge->m_inputAudio.get(samples, AUDIO_SAMPLES_LENGTH); // process MDC, if necessary if (bridge->m_overrideSrcIdFromMDC) - mdc_decoder_process_samples(bridge->m_mdcDecoder, samples, MBE_SAMPLES_LENGTH); + mdc_decoder_process_samples(bridge->m_mdcDecoder, samples, AUDIO_SAMPLES_LENGTH); float sampleLevel = bridge->m_voxSampleLevel / 1000; @@ -2895,7 +3085,7 @@ void* HostBridge::threadAudioProcess(void* arg) // perform maximum sample detection float maxSample = 0.0f; - for (int i = 0; i < MBE_SAMPLES_LENGTH; i++) { + for (int i = 0; i < (int)AUDIO_SAMPLES_LENGTH; i++) { float sampleValue = fabs((float)samples[i]); maxSample = fmax(maxSample, sampleValue); } @@ -2919,19 +3109,21 @@ void* HostBridge::threadAudioProcess(void* arg) if (bridge->m_grantDemand) { switch (bridge->m_txMode) { - case TX_MODE_P25: - { - p25::lc::LC lc = p25::lc::LC(); - lc.setLCO(p25::defines::LCO::GROUP); - lc.setDstId(dstId); - lc.setSrcId(srcId); - - p25::data::LowSpeedData lsd = p25::data::LowSpeedData(); - - uint8_t controlByte = 0x80U; - bridge->m_network->writeP25TDU(lc, lsd, controlByte); - } - break; + case TX_MODE_P25: + { + p25::lc::LC lc = p25::lc::LC(); + lc.setLCO(P25DEF::LCO::GROUP); + lc.setDstId(dstId); + lc.setSrcId(srcId); + + p25::data::LowSpeedData lsd = p25::data::LowSpeedData(); + + uint8_t controlByte = network::NET_CTRL_GRANT_DEMAND; + if (bridge->m_tekAlgoId != P25DEF::ALGO_UNENCRYPT) + controlByte |= network::NET_CTRL_GRANT_ENCRYPT; + bridge->m_network->writeP25TDU(lc, lsd, controlByte); + } + break; } } } @@ -2950,11 +3142,11 @@ void* HostBridge::threadAudioProcess(void* arg) } if (bridge->m_audioDetect && !bridge->m_callInProgress) { - ma_uint32 pcmBytes = MBE_SAMPLES_LENGTH * ma_get_bytes_per_frame(bridge->m_maDevice.capture.format, bridge->m_maDevice.capture.channels); + ma_uint32 pcmBytes = AUDIO_SAMPLES_LENGTH * ma_get_bytes_per_frame(bridge->m_maDevice.capture.format, bridge->m_maDevice.capture.channels); DECLARE_UINT8_ARRAY(pcm, pcmBytes); int pcmIdx = 0; - for (uint32_t smpIdx = 0; smpIdx < MBE_SAMPLES_LENGTH; smpIdx++) { + 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; @@ -2968,6 +3160,9 @@ void* HostBridge::threadAudioProcess(void* arg) case TX_MODE_P25: bridge->encodeP25AudioFrame(pcm); break; + case TX_MODE_ANALOG: + bridge->encodeAnalogAudioFrame(pcm); + break; } } } @@ -3025,7 +3220,7 @@ void* HostBridge::threadUDPAudioProcess(void* arg) if (req != nullptr) { if (bridge->m_udpInterFrameDelay > 0U) { - int64_t now = std::chrono::duration_cast(std::chrono::system_clock::now().time_since_epoch()).count(); + uint64_t now = std::chrono::duration_cast(std::chrono::system_clock::now().time_since_epoch()).count(); if (req->pktRxTime > now) { Thread::sleep(1U); @@ -3065,25 +3260,25 @@ void* HostBridge::threadUDPAudioProcess(void* arg) std::lock_guard lock(m_audioMutex); int smpIdx = 0; - short samples[MBE_SAMPLES_LENGTH]; + short samples[AUDIO_SAMPLES_LENGTH]; if (bridge->m_udpUseULaw) { if (bridge->m_trace) - Utils::dump(1U, "HostBridge()::threadUDPAudioProcess() uLaw Audio", req->pcm, MBE_SAMPLES_LENGTH * 2U); + Utils::dump(1U, "HostBridge()::threadUDPAudioProcess(), uLaw Audio", req->pcm, AUDIO_SAMPLES_LENGTH * 2U); - for (uint32_t pcmIdx = 0; pcmIdx < MBE_SAMPLES_LENGTH; pcmIdx++) { - samples[smpIdx] = decodeMuLaw(req->pcm[pcmIdx]); + for (uint32_t pcmIdx = 0; pcmIdx < AUDIO_SAMPLES_LENGTH; pcmIdx++) { + samples[smpIdx] = AnalogAudio::decodeMuLaw(req->pcm[pcmIdx]); smpIdx++; } int pcmIdx = 0; - for (uint32_t smpIdx = 0; smpIdx < MBE_SAMPLES_LENGTH; smpIdx++) { + for (uint32_t smpIdx = 0; smpIdx < AUDIO_SAMPLES_LENGTH; smpIdx++) { req->pcm[pcmIdx + 0] = (uint8_t)(samples[smpIdx] & 0xFF); req->pcm[pcmIdx + 1] = (uint8_t)((samples[smpIdx] >> 8) & 0xFF); pcmIdx += 2; } } else { - for (uint32_t pcmIdx = 0; pcmIdx < req->pcmLength; pcmIdx += 2) { + for (int pcmIdx = 0; pcmIdx < req->pcmLength; pcmIdx += 2) { samples[smpIdx] = (short)((req->pcm[pcmIdx + 1] << 8) + req->pcm[pcmIdx + 0]); smpIdx++; } @@ -3099,19 +3294,22 @@ void* HostBridge::threadUDPAudioProcess(void* arg) LogMessage(LOG_HOST, "%s, call start, srcId = %u, dstId = %u", UDP_CALL, bridge->m_udpSrcId, bridge->m_udpDstId); if (bridge->m_grantDemand) { switch (bridge->m_txMode) { - case TX_MODE_P25: - { - p25::lc::LC lc = p25::lc::LC(); - lc.setLCO(p25::defines::LCO::GROUP); - lc.setDstId(bridge->m_udpDstId); - lc.setSrcId(bridge->m_udpSrcId); - - p25::data::LowSpeedData lsd = p25::data::LowSpeedData(); - - uint8_t controlByte = 0x80U; - bridge->m_network->writeP25TDU(lc, lsd, controlByte); - } - break; + case TX_MODE_P25: + { + p25::lc::LC lc = p25::lc::LC(); + lc.setLCO(P25DEF::LCO::GROUP); + lc.setDstId(bridge->m_udpDstId); + lc.setSrcId(bridge->m_udpSrcId); + + p25::data::LowSpeedData lsd = p25::data::LowSpeedData(); + + uint8_t controlByte = network::NET_CTRL_GRANT_DEMAND; + if (bridge->m_tekAlgoId != P25DEF::ALGO_UNENCRYPT) + controlByte |= network::NET_CTRL_GRANT_ENCRYPT; + controlByte |= network::NET_CTRL_SWITCH_OVER; + bridge->m_network->writeP25TDU(lc, lsd, controlByte); + } + break; } } } @@ -3136,6 +3334,9 @@ void* HostBridge::threadUDPAudioProcess(void* arg) case TX_MODE_P25: bridge->encodeP25AudioFrame(req->pcm, bridge->m_udpSrcId); break; + case TX_MODE_ANALOG: + bridge->encodeAnalogAudioFrame(req->pcm, bridge->m_udpSrcId); + break; } } @@ -3198,7 +3399,7 @@ void* HostBridge::threadNetworkProcess(void* arg) } if (bridge->m_network->getStatus() == NET_STAT_RUNNING) { - if (bridge->m_tekAlgoId != p25::defines::ALGO_UNENCRYPT && bridge->m_tekKeyId > 0U) { + if (bridge->m_tekAlgoId != P25DEF::ALGO_UNENCRYPT && bridge->m_tekKeyId > 0U) { if (bridge->m_p25Crypto->getTEKLength() == 0U && !bridge->m_requestedTek) { bridge->m_requestedTek = true; LogMessage(LOG_HOST, "Bridge encryption enabled, requesting TEK from network."); @@ -3225,6 +3426,14 @@ void* HostBridge::threadNetworkProcess(void* arg) } } + if (bridge->m_txMode == TX_MODE_ANALOG) { + std::lock_guard lock(HostBridge::m_networkMutex); + UInt8Array analogBuffer = bridge->m_network->readAnalog(netReadRet, length); + if (netReadRet) { + bridge->processAnalogNetwork(analogBuffer.get(), length); + } + } + Thread::sleep(1U); } @@ -3280,113 +3489,115 @@ void HostBridge::callEndSilence(uint32_t srcId, uint32_t dstId) { switch (m_txMode) { case TX_MODE_DMR: - { - using namespace dmr; - using namespace dmr::defines; + { + using namespace dmr; + using namespace dmr::defines; + using namespace dmr::data; - m_dmrN = (uint8_t)(m_dmrSeqNo % 6); + m_dmrN = (uint8_t)(m_dmrSeqNo % 6); - // send DMR voice - uint8_t data[DMR_FRAME_LENGTH_BYTES]; + // send DMR voice + uint8_t data[DMR_FRAME_LENGTH_BYTES]; - m_ambeCount = 0U; + m_ambeCount = 0U; - ::memcpy(m_ambeBuffer + (m_ambeCount * 9U), NULL_AMBE, RAW_AMBE_LENGTH_BYTES); - m_ambeCount++; - ::memcpy(m_ambeBuffer + (m_ambeCount * 9U), NULL_AMBE, RAW_AMBE_LENGTH_BYTES); - m_ambeCount++; - ::memcpy(m_ambeBuffer + (m_ambeCount * 9U), NULL_AMBE, RAW_AMBE_LENGTH_BYTES); - m_ambeCount++; + ::memcpy(m_ambeBuffer + (m_ambeCount * 9U), NULL_AMBE, RAW_AMBE_LENGTH_BYTES); + m_ambeCount++; + ::memcpy(m_ambeBuffer + (m_ambeCount * 9U), NULL_AMBE, RAW_AMBE_LENGTH_BYTES); + m_ambeCount++; + ::memcpy(m_ambeBuffer + (m_ambeCount * 9U), NULL_AMBE, RAW_AMBE_LENGTH_BYTES); + m_ambeCount++; - ::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); + ::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; + 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); + uint8_t lcss = m_dmrEmbeddedData.getData(data, m_dmrN); - // generated embedded signalling - data::EMB emb = data::EMB(); - emb.setColorCode(0U); - emb.setLCSS(lcss); - emb.encode(data); - } + // generated embedded signalling + EMB emb = EMB(); + emb.setColorCode(0U); + emb.setLCSS(lcss); + emb.encode(data); + } - LogMessage(LOG_HOST, DMR_DT_VOICE ", silence srcId = %u, dstId = %u, slot = %u, seqNo = %u", srcId, dstId, m_slot, m_dmrN); + LogMessage(LOG_HOST, DMR_DT_VOICE ", silence srcId = %u, dstId = %u, slot = %u, seqNo = %u", srcId, dstId, m_slot, m_dmrN); - // generate DMR network frame - data::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); + // 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); + dmrData.setData(data); - m_network->writeDMR(dmrData, false); - m_txStreamId = m_network->getDMRStreamId(m_slot); + m_network->writeDMR(dmrData, false); + m_txStreamId = m_network->getDMRStreamId(m_slot); - m_dmrSeqNo++; - } - break; + m_dmrSeqNo++; + } + break; case TX_MODE_P25: - { - using namespace p25; - using namespace p25::defines; - - // fill the LDU buffers appropriately - switch (m_p25N) { - // LDU1 - case 0: - resetWithNullAudio(m_netLDU1, false); - break; + { + using namespace p25; + using namespace p25::defines; + using namespace p25::data; + + // fill the LDU buffers appropriately + switch (m_p25N) { + // LDU1 + case 0: + resetWithNullAudio(m_netLDU1, false); + break; - // LDU2 - case 1: - resetWithNullAudio(m_netLDU2, false); - break; - } + // LDU2 + case 1: + resetWithNullAudio(m_netLDU2, false); + break; + } - lc::LC lc = lc::LC(); - lc.setLCO(LCO::GROUP); - lc.setGroup(true); - lc.setPriority(4U); - lc.setDstId(dstId); - lc.setSrcId(srcId); + lc::LC lc = lc::LC(); + lc.setLCO(LCO::GROUP); + lc.setGroup(true); + lc.setPriority(4U); + lc.setDstId(dstId); + lc.setSrcId(srcId); - lc.setAlgId(ALGO_UNENCRYPT); - lc.setKId(0); + lc.setAlgId(ALGO_UNENCRYPT); + lc.setKId(0); - data::LowSpeedData lsd = data::LowSpeedData(); + LowSpeedData lsd = LowSpeedData(); - // send P25 LDU1 - if (m_p25N == 0U) { - LogMessage(LOG_HOST, P25_LDU1_STR " silence audio, srcId = %u, dstId = %u", srcId, dstId); - m_network->writeP25LDU1(lc, lsd, m_netLDU1, FrameType::DATA_UNIT); - m_p25N++; - break; - } + // send P25 LDU1 + if (m_p25N == 0U) { + LogMessage(LOG_HOST, P25_LDU1_STR " silence audio, srcId = %u, dstId = %u", srcId, dstId); + m_network->writeP25LDU1(lc, lsd, m_netLDU1, FrameType::DATA_UNIT); + m_p25N++; + break; + } - // send P25 LDU2 - if (m_p25N == 1U) { - LogMessage(LOG_HOST, P25_LDU2_STR " silence audio, algo = $%02X, kid = $%04X", ALGO_UNENCRYPT, 0U); - m_network->writeP25LDU2(lc, lsd, m_netLDU2); - m_p25N = 0U; - break; + // send P25 LDU2 + if (m_p25N == 1U) { + LogMessage(LOG_HOST, P25_LDU2_STR " silence audio, algo = $%02X, kid = $%04X", ALGO_UNENCRYPT, 0U); + m_network->writeP25LDU2(lc, lsd, m_netLDU2); + m_p25N = 0U; + break; + } } - } - break; + break; } } @@ -3467,6 +3678,7 @@ void* HostBridge::threadCallWatchdog(void* arg) bridge->m_dmrN = 0U; bridge->m_p25N = 0U; + bridge->m_analogN = 0U; } if (bridge->m_udpHangTime.isRunning()) @@ -3512,3 +3724,53 @@ void* HostBridge::threadCallWatchdog(void* arg) return nullptr; } + +/* Helper to initialize RTS PTT control. */ + +bool HostBridge::initializeRtsPtt() +{ + if (!m_rtsPttEnable) + return true; + + if (m_rtsPttPort.empty()) { + ::LogError(LOG_HOST, "RTS PTT port is not specified"); + return false; + } + + m_rtsPttController = new RtsPttController(m_rtsPttPort); + if (!m_rtsPttController->open()) { + ::LogError(LOG_HOST, "Failed to open RTS PTT port %s", m_rtsPttPort.c_str()); + delete m_rtsPttController; + m_rtsPttController = nullptr; + return false; + } + + ::LogInfo(LOG_HOST, "RTS PTT Controller initialized on %s", m_rtsPttPort.c_str()); + return true; +} + +/* Helper to assert RTS PTT (start transmission). */ + +void HostBridge::assertRtsPtt() +{ + if (!m_rtsPttEnable || m_rtsPttController == nullptr || m_rtsPttActive) + return; + + if (m_rtsPttController->setPTT()) { + m_rtsPttActive = true; + ::LogDebug(LOG_HOST, "RTS PTT asserted"); + } +} + +/* Helper to deassert RTS PTT (stop transmission). */ + +void HostBridge::deassertRtsPtt() +{ + if (!m_rtsPttEnable || m_rtsPttController == nullptr || !m_rtsPttActive) + return; + + if (m_rtsPttController->clearPTT()) { + m_rtsPttActive = false; + ::LogDebug(LOG_HOST, "RTS PTT deasserted"); + } +} diff --git a/src/bridge/HostBridge.h b/src/bridge/HostBridge.h index 398935e8..dbd6485f 100644 --- a/src/bridge/HostBridge.h +++ b/src/bridge/HostBridge.h @@ -32,6 +32,7 @@ #include "audio/miniaudio.h" #include "mdc/mdc_decode.h" #include "network/PeerNetwork.h" +#include "RtsPttController.h" #include #include @@ -47,7 +48,6 @@ // Constants // --------------------------------------------------------------------------- -#define MBE_SAMPLES_LENGTH 160 #define NO_BIT_STEAL 0 #define ECMODE_NOISE_SUPPRESS 0x40 @@ -63,6 +63,7 @@ const uint8_t HALF_RATE_MODE = 0x01U; const uint8_t TX_MODE_DMR = 1U; const uint8_t TX_MODE_P25 = 2U; +const uint8_t TX_MODE_ANALOG = 3U; // --------------------------------------------------------------------------- @@ -93,31 +94,6 @@ void audioCallback(ma_device* device, void* output, const void* input, ma_uint32 void mdcPacketDetected(int frameCount, mdc_u8_t op, mdc_u8_t arg, mdc_u16_t unitID, mdc_u8_t extra0, mdc_u8_t extra1, mdc_u8_t extra2, mdc_u8_t extra3, void* context); -/** - * @brief Helper to convert PCM into G.711 aLaw. - * @param pcm PCM value. - * @return uint8_t aLaw value. - */ -uint8_t encodeALaw(short pcm); -/** - * @brief Helper to convert G.711 aLaw into PCM. - * @param alaw aLaw value. - * @return short PCM value. - */ -short decodeALaw(uint8_t alaw); -/** - * @brief Helper to convert PCM into G.711 MuLaw. - * @param pcm PCM value. - * @return uint8_t MuLaw value. - */ -uint8_t encodeMuLaw(short pcm); -/** - * @brief Helper to convert G.711 MuLaw into PCM. - * @param ulaw MuLaw value. - * @return short PCM value. - */ -short decodeMuLaw(uint8_t ulaw); - // --------------------------------------------------------------------------- // Structure Declaration // --------------------------------------------------------------------------- @@ -260,6 +236,11 @@ private: uint32_t m_p25SeqNo; uint8_t m_p25N; + uint32_t m_netId; + uint32_t m_sysId; + + uint8_t m_analogN; + bool m_audioDetect; bool m_trafficFromUDP; uint32_t m_udpSrcId; @@ -278,6 +259,12 @@ private: bool m_trace; bool m_debug; + // RTS PTT Control + bool m_rtsPttEnable; + std::string m_rtsPttPort; + RtsPttController* m_rtsPttController; + bool m_rtsPttActive; + uint16_t m_rtpSeqNo; uint32_t m_rtpTimestamp; @@ -485,12 +472,18 @@ private: void encodeP25AudioFrame(uint8_t* pcm, uint32_t forcedSrcId = 0U, uint32_t forcedDstId = 0U); /** - * @brief Helper to generate outgoing RTP headers. - * @param msgLen Message Length. - * @param rtpSeq RTP Sequence. - * @returns uint8_t* Buffer containing the encoded RTP headers. + * @brief Helper to process analog network traffic. + * @param buffer + * @param length */ - uint8_t* generateRTPHeaders(uint8_t msgLen, uint16_t& rtpSeq); + 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 @@ -502,6 +495,14 @@ private: */ void generatePreambleTone(); + /** + * @brief Helper to generate outgoing RTP headers. + * @param msgLen Message Length. + * @param rtpSeq RTP Sequence. + * @returns uint8_t* Buffer containing the encoded RTP headers. + */ + uint8_t* generateRTPHeaders(uint8_t msgLen, uint16_t& rtpSeq); + /** * @brief Helper to end a local or UDP call. * @param srcId @@ -517,6 +518,20 @@ private: */ void processTEKResponse(p25::kmm::KeyItem* ki, uint8_t algId, uint8_t keyLength); + /** + * @brief Helper to initialize RTS PTT control. + * @returns bool True, if RTS PTT was initialized successfully, otherwise false. + */ + bool initializeRtsPtt(); + /** + * @brief Helper to assert RTS PTT (start transmission). + */ + void assertRtsPtt(); + /** + * @brief Helper to deassert RTS PTT (stop transmission). + */ + void deassertRtsPtt(); + /** * @brief Entry point to audio processing thread. * @param arg Instance of the thread_t structure. diff --git a/src/bridge/RtsPttController.cpp b/src/bridge/RtsPttController.cpp new file mode 100644 index 00000000..e82be9c9 --- /dev/null +++ b/src/bridge/RtsPttController.cpp @@ -0,0 +1,264 @@ +// 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-2025 Bryan Biedenkapp, N2PLL + * Copyright (C) 2025 Lorenzo L. Romero, K2LLR + * + */ +#include "Defines.h" +#include "RtsPttController.h" + +#include + +// --------------------------------------------------------------------------- +// Public Class Members +// --------------------------------------------------------------------------- + +/* Initializes a new instance of the RtsPttController class. */ + +RtsPttController::RtsPttController(const std::string& port) : + m_port(port), + m_isOpen(false), +#if defined(_WIN32) + m_fd(INVALID_HANDLE_VALUE) +#else + m_fd(-1) +#endif // defined(_WIN32) +{ + assert(!port.empty()); +} + +/* Finalizes a instance of the RtsPttController class. */ + +RtsPttController::~RtsPttController() +{ + close(); +} + +/* Opens the serial port for RTS control. */ + +bool RtsPttController::open() +{ + if (m_isOpen) + return true; + +#if defined(_WIN32) + assert(m_fd == INVALID_HANDLE_VALUE); + + std::string deviceName = m_port; + if (deviceName.find("\\\\.\\") == std::string::npos) { + deviceName = "\\\\.\\" + m_port; + } + + m_fd = ::CreateFileA(deviceName.c_str(), GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); + if (m_fd == INVALID_HANDLE_VALUE) { + ::LogError(LOG_HOST, "Cannot open RTS PTT device - %s, err=%04lx", m_port.c_str(), ::GetLastError()); + return false; + } + + DCB dcb; + if (::GetCommState(m_fd, &dcb) == 0) { + ::LogError(LOG_HOST, "Cannot get the attributes for %s, err=%04lx", m_port.c_str(), ::GetLastError()); + ::CloseHandle(m_fd); + m_fd = INVALID_HANDLE_VALUE; + return false; + } + + dcb.BaudRate = 9600; + dcb.ByteSize = 8; + dcb.Parity = NOPARITY; + dcb.fParity = FALSE; + dcb.StopBits = ONESTOPBIT; + dcb.fInX = FALSE; + dcb.fOutX = FALSE; + dcb.fOutxCtsFlow = FALSE; + dcb.fOutxDsrFlow = FALSE; + dcb.fDsrSensitivity = FALSE; + dcb.fDtrControl = DTR_CONTROL_DISABLE; + dcb.fRtsControl = RTS_CONTROL_DISABLE; + + if (::SetCommState(m_fd, &dcb) == 0) { + ::LogError(LOG_HOST, "Cannot set the attributes for %s, err=%04lx", m_port.c_str(), ::GetLastError()); + ::CloseHandle(m_fd); + m_fd = INVALID_HANDLE_VALUE; + return false; + } + + // Clear RTS initially + if (::EscapeCommFunction(m_fd, CLRRTS) == 0) { + ::LogError(LOG_HOST, "Cannot clear RTS for %s, err=%04lx", m_port.c_str(), ::GetLastError()); + ::CloseHandle(m_fd); + m_fd = INVALID_HANDLE_VALUE; + return false; + } + +#else + assert(m_fd == -1); + + m_fd = ::open(m_port.c_str(), O_RDWR | O_NOCTTY | O_NDELAY, 0); + if (m_fd < 0) { + ::LogError(LOG_HOST, "Cannot open RTS PTT device - %s", m_port.c_str()); + return false; + } + + if (::isatty(m_fd) == 0) { + ::LogError(LOG_HOST, "%s is not a TTY device", m_port.c_str()); + ::close(m_fd); + m_fd = -1; + return false; + } + + if (!setTermios()) { + ::close(m_fd); + m_fd = -1; + return false; + } +#endif // defined(_WIN32) + + ::LogInfo(LOG_HOST, "RTS PTT Controller opened on %s", m_port.c_str()); + m_isOpen = true; + return true; +} + +/* Closes the serial port. */ + +void RtsPttController::close() +{ + if (!m_isOpen) + return; + + // Clear RTS before closing + clearPTT(); + +#if defined(_WIN32) + if (m_fd != INVALID_HANDLE_VALUE) { + ::CloseHandle(m_fd); + m_fd = INVALID_HANDLE_VALUE; + } +#else + if (m_fd != -1) { + ::close(m_fd); + m_fd = -1; + } +#endif // defined(_WIN32) + + m_isOpen = false; + ::LogInfo(LOG_HOST, "RTS PTT Controller closed"); +} + +/* Sets RTS signal high (asserts RTS) to trigger PTT. */ + +bool RtsPttController::setPTT() +{ + if (!m_isOpen) + return false; + +#if defined(_WIN32) + if (::EscapeCommFunction(m_fd, SETRTS) == 0) { + ::LogError(LOG_HOST, "Cannot set RTS PTT for %s, err=%04lx", m_port.c_str(), ::GetLastError()); + return false; + } +#else + uint32_t y; + if (::ioctl(m_fd, TIOCMGET, &y) < 0) { + ::LogError(LOG_HOST, "Cannot get the control attributes for %s", m_port.c_str()); + return false; + } + + y |= TIOCM_RTS; + + if (::ioctl(m_fd, TIOCMSET, &y) < 0) { + ::LogError(LOG_HOST, "Cannot set RTS PTT for %s", m_port.c_str()); + return false; + } +#endif // defined(_WIN32) + + ::LogDebug(LOG_HOST, "RTS PTT asserted on %s", m_port.c_str()); + return true; +} + +/* Sets RTS signal low (clears RTS) to release PTT. */ + +bool RtsPttController::clearPTT() +{ + if (!m_isOpen) + return false; + +#if defined(_WIN32) + if (::EscapeCommFunction(m_fd, CLRRTS) == 0) { + ::LogError(LOG_HOST, "Cannot clear RTS PTT for %s, err=%04lx", m_port.c_str(), ::GetLastError()); + return false; + } +#else + uint32_t y; + if (::ioctl(m_fd, TIOCMGET, &y) < 0) { + ::LogError(LOG_HOST, "Cannot get the control attributes for %s", m_port.c_str()); + return false; + } + + y &= ~TIOCM_RTS; + + if (::ioctl(m_fd, TIOCMSET, &y) < 0) { + ::LogError(LOG_HOST, "Cannot clear RTS PTT for %s", m_port.c_str()); + return false; + } +#endif // defined(_WIN32) + + ::LogDebug(LOG_HOST, "RTS PTT cleared on %s", m_port.c_str()); + return true; +} + +// --------------------------------------------------------------------------- +// Private Class Members +// --------------------------------------------------------------------------- + +/* Sets the termios settings on the serial port. */ + +bool RtsPttController::setTermios() +{ +#if !defined(_WIN32) + termios termios; + if (::tcgetattr(m_fd, &termios) < 0) { + ::LogError(LOG_HOST, "Cannot get the attributes for %s", m_port.c_str()); + return false; + } + + termios.c_iflag &= ~(IGNBRK | BRKINT | IGNPAR | PARMRK | INPCK); + termios.c_iflag &= ~(ISTRIP | INLCR | IGNCR | ICRNL); + termios.c_iflag &= ~(IXON | IXOFF | IXANY); + termios.c_oflag &= ~(OPOST); + termios.c_cflag &= ~(CSIZE | CSTOPB | PARENB | CRTSCTS); + termios.c_cflag |= (CS8 | CLOCAL | CREAD); + termios.c_lflag &= ~(ISIG | ICANON | IEXTEN); + termios.c_lflag &= ~(ECHO | ECHOE | ECHOK | ECHONL); + termios.c_cc[VMIN] = 0; + termios.c_cc[VTIME] = 10; + + ::cfsetospeed(&termios, B9600); + ::cfsetispeed(&termios, B9600); + + if (::tcsetattr(m_fd, TCSANOW, &termios) < 0) { + ::LogError(LOG_HOST, "Cannot set the attributes for %s", m_port.c_str()); + return false; + } + + // Clear RTS initially + uint32_t y; + if (::ioctl(m_fd, TIOCMGET, &y) < 0) { + ::LogError(LOG_HOST, "Cannot get the control attributes for %s", m_port.c_str()); + return false; + } + + y &= ~TIOCM_RTS; + + if (::ioctl(m_fd, TIOCMSET, &y) < 0) { + ::LogError(LOG_HOST, "Cannot clear RTS for %s", m_port.c_str()); + return false; + } +#endif // !defined(_WIN32) + + return true; +} diff --git a/src/bridge/RtsPttController.h b/src/bridge/RtsPttController.h new file mode 100644 index 00000000..5cc8860c --- /dev/null +++ b/src/bridge/RtsPttController.h @@ -0,0 +1,91 @@ +// 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-2025 Bryan Biedenkapp, N2PLL + * Copyright (C) 2025 Lorenzo L. Romero, K2LLR + * + */ +/** + * @file RtsPttController.h + * @ingroup bridge + * @file RtsPttController.cpp + * @ingroup bridge + */ +#if !defined(__RTS_PTT_CONTROLLER_H__) +#define __RTS_PTT_CONTROLLER_H__ + +#include "Defines.h" +#include "common/Log.h" + +#include + +#if defined(_WIN32) +#include +#else +#include +#include +#include +#include +#endif // defined(_WIN32) + +// --------------------------------------------------------------------------- +// Class Declaration +// --------------------------------------------------------------------------- + +/** + * @brief This class implements RTS PTT control for the bridge. + * @ingroup bridge + */ +class HOST_SW_API RtsPttController { +public: + /** + * @brief Initializes a new instance of the RtsPttController class. + * @param port Serial port device (e.g., /dev/ttyUSB0). + */ + RtsPttController(const std::string& port); + /** + * @brief Finalizes a instance of the RtsPttController class. + */ + ~RtsPttController(); + + /** + * @brief Opens the serial port for RTS control. + * @returns bool True, if port was opened successfully, otherwise false. + */ + bool open(); + /** + * @brief Closes the serial port. + */ + void close(); + + /** + * @brief Sets RTS signal high (asserts RTS) to trigger PTT. + * @returns bool True, if RTS was set successfully, otherwise false. + */ + bool setPTT(); + /** + * @brief Sets RTS signal low (clears RTS) to release PTT. + * @returns bool True, if RTS was cleared successfully, otherwise false. + */ + bool clearPTT(); + +private: + std::string m_port; + bool m_isOpen; +#if defined(_WIN32) + HANDLE m_fd; +#else + int m_fd; +#endif // defined(_WIN32) + + /** + * @brief Sets the termios settings on the serial port. + * @returns bool True, if settings are set, otherwise false. + */ + bool setTermios(); +}; + +#endif // __RTS_PTT_CONTROLLER_H__ diff --git a/src/bridge/SampleTimeConversion.h b/src/bridge/SampleTimeConversion.h deleted file mode 100644 index 66254799..00000000 --- a/src/bridge/SampleTimeConversion.h +++ /dev/null @@ -1,54 +0,0 @@ -// 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 Bryan Biedenkapp, N2PLL - * - */ - /** - * @file SampleTimeConversion.h - * @ingroup bridge - */ -#if !defined(__SAMPLE_TIME_CONVERSION_H__) -#define __SAMPLE_TIME_CONVERSION_H__ - -#include "Defines.h" - -// --------------------------------------------------------------------------- -// Class Declaration -// --------------------------------------------------------------------------- - -/** -* @brief -* @ingroup bridge -*/ -class HOST_SW_API SampleTimeConvert { -public: - /** - * @brief (ms) to sample count conversion - * @param sampleRate Sample rate. - * @param channels Number of audio channels. - * @param ms Number of milliseconds. - * @returns int Number of samples. - */ - static int ToSamples(uint32_t sampleRate, uint8_t channels, int ms) - { - return (int)(((long)ms) * sampleRate * channels / 1000); - } - - /** - * @brief Sample count to (ms) conversion - * @param sampleRate Sample rate. - * @param channels Number of audio channels. - * @param samples Number of samples. - * @returns int Number of milliseconds. - */ - static int ToMS(uint32_t sampleRate, uint8_t channels, int samples) - { - return (int)(((float)samples / (float)sampleRate / (float)channels) * 1000); - } -}; - -#endif // __SAMPLE_TIME_CONVERSION_H__ diff --git a/src/bridge/network/PeerNetwork.cpp b/src/bridge/network/PeerNetwork.cpp index 80f980e2..449d68b8 100644 --- a/src/bridge/network/PeerNetwork.cpp +++ b/src/bridge/network/PeerNetwork.cpp @@ -28,8 +28,8 @@ using namespace network; /* Initializes a new instance of the PeerNetwork class. */ PeerNetwork::PeerNetwork(const std::string& address, uint16_t port, uint16_t localPort, uint32_t peerId, const std::string& password, - bool duplex, bool debug, bool dmr, bool p25, bool nxdn, bool slot1, bool slot2, bool allowActivityTransfer, bool allowDiagnosticTransfer, bool updateLookup, bool saveLookup) : - Network(address, port, localPort, peerId, password, duplex, debug, dmr, p25, nxdn, slot1, slot2, allowActivityTransfer, allowDiagnosticTransfer, updateLookup, saveLookup) + bool duplex, bool debug, bool dmr, bool p25, bool nxdn, bool analog, bool slot1, bool slot2, bool allowActivityTransfer, bool allowDiagnosticTransfer, bool updateLookup, bool saveLookup) : + Network(address, port, localPort, peerId, password, duplex, debug, dmr, p25, nxdn, analog, slot1, slot2, allowActivityTransfer, allowDiagnosticTransfer, updateLookup, saveLookup) { assert(!address.empty()); assert(port > 0U); @@ -38,7 +38,8 @@ PeerNetwork::PeerNetwork(const std::string& address, uint16_t port, uint16_t loc /* Writes P25 LDU1 frame data to the network. */ -bool PeerNetwork::writeP25LDU1(const p25::lc::LC& control, const p25::data::LowSpeedData& lsd, const uint8_t* data, p25::defines::FrameType::E frameType) +bool PeerNetwork::writeP25LDU1(const p25::lc::LC& control, const p25::data::LowSpeedData& lsd, const uint8_t* data, + P25DEF::FrameType::E frameType, uint8_t controlByte) { if (m_status != NET_STAT_RUNNING && m_status != NET_STAT_MST_RUNNING) return false; @@ -50,7 +51,7 @@ bool PeerNetwork::writeP25LDU1(const p25::lc::LC& control, const p25::data::LowS } uint32_t messageLength = 0U; - UInt8Array message = createP25_LDU1Message_Raw(messageLength, control, lsd, data, frameType); + UInt8Array message = createP25_LDU1Message_Raw(messageLength, control, lsd, data, frameType, controlByte); if (message == nullptr) { return false; } @@ -60,7 +61,8 @@ bool PeerNetwork::writeP25LDU1(const p25::lc::LC& control, const p25::data::LowS /* Writes P25 LDU2 frame data to the network. */ -bool PeerNetwork::writeP25LDU2(const p25::lc::LC& control, const p25::data::LowSpeedData& lsd, const uint8_t* data) +bool PeerNetwork::writeP25LDU2(const p25::lc::LC& control, const p25::data::LowSpeedData& lsd, const uint8_t* data, + uint8_t controlByte) { if (m_status != NET_STAT_RUNNING && m_status != NET_STAT_MST_RUNNING) return false; @@ -72,7 +74,7 @@ bool PeerNetwork::writeP25LDU2(const p25::lc::LC& control, const p25::data::LowS } uint32_t messageLength = 0U; - UInt8Array message = createP25_LDU2Message_Raw(messageLength, control, lsd, data); + UInt8Array message = createP25_LDU2Message_Raw(messageLength, control, lsd, data, controlByte); if (message == nullptr) { return false; } @@ -211,7 +213,7 @@ bool PeerNetwork::writeConfig() /* Creates an P25 LDU1 frame message. */ UInt8Array PeerNetwork::createP25_LDU1Message_Raw(uint32_t& length, const p25::lc::LC& control, const p25::data::LowSpeedData& lsd, - const uint8_t* data, p25::defines::FrameType::E frameType) + const uint8_t* data, p25::defines::FrameType::E frameType, uint8_t controlByte) { using namespace p25::defines; using namespace p25::dfsi::defines; @@ -223,7 +225,7 @@ UInt8Array PeerNetwork::createP25_LDU1Message_Raw(uint32_t& length, const p25::l ::memset(buffer, 0x00U, P25_LDU1_PACKET_LENGTH + PACKET_PAD); // construct P25 message header - createP25_MessageHdr(buffer, DUID::LDU1, control, lsd, frameType); + createP25_MessageHdr(buffer, DUID::LDU1, control, lsd, frameType, controlByte); // pack DFSI data uint32_t count = MSG_HDR_SIZE; @@ -286,7 +288,7 @@ UInt8Array PeerNetwork::createP25_LDU1Message_Raw(uint32_t& length, const p25::l /* Creates an P25 LDU2 frame message. */ UInt8Array PeerNetwork::createP25_LDU2Message_Raw(uint32_t& length, const p25::lc::LC& control, const p25::data::LowSpeedData& lsd, - const uint8_t* data) + const uint8_t* data, uint8_t controlByte) { using namespace p25::defines; using namespace p25::dfsi::defines; @@ -298,7 +300,7 @@ UInt8Array PeerNetwork::createP25_LDU2Message_Raw(uint32_t& length, const p25::l ::memset(buffer, 0x00U, P25_LDU2_PACKET_LENGTH + PACKET_PAD); // construct P25 message header - createP25_MessageHdr(buffer, DUID::LDU2, control, lsd, FrameType::DATA_UNIT); + createP25_MessageHdr(buffer, DUID::LDU2, control, lsd, FrameType::DATA_UNIT, controlByte); // pack DFSI data uint32_t count = MSG_HDR_SIZE; diff --git a/src/bridge/network/PeerNetwork.h b/src/bridge/network/PeerNetwork.h index 0d58146a..8fe3b8f8 100644 --- a/src/bridge/network/PeerNetwork.h +++ b/src/bridge/network/PeerNetwork.h @@ -48,6 +48,7 @@ namespace network * @param dmr Flag indicating whether DMR is enabled. * @param p25 Flag indicating whether P25 is enabled. * @param nxdn Flag indicating whether NXDN is enabled. + * @param analog Flag indicating whether analog is enabled. * @param slot1 Flag indicating whether DMR slot 1 is enabled for network traffic. * @param slot2 Flag indicating whether DMR slot 2 is enabled for network traffic. * @param allowActivityTransfer Flag indicating that the system activity logs will be sent to the network. @@ -55,7 +56,7 @@ namespace network * @param updateLookup Flag indicating that the system will accept radio ID and talkgroup ID lookups from the network. */ PeerNetwork(const std::string& address, uint16_t port, uint16_t localPort, uint32_t peerId, const std::string& password, - bool duplex, bool debug, bool dmr, bool p25, bool nxdn, bool slot1, bool slot2, bool allowActivityTransfer, bool allowDiagnosticTransfer, bool updateLookup, bool saveLookup); + bool duplex, bool debug, bool dmr, bool p25, bool nxdn, bool analog, bool slot1, bool slot2, bool allowActivityTransfer, bool allowDiagnosticTransfer, bool updateLookup, bool saveLookup); /** * @brief Writes P25 LDU1 frame data to the network. @@ -63,18 +64,21 @@ namespace network * @param[in] lsd Instance of p25::data::LowSpeedData containing low speed data. * @param[in] data Buffer containing P25 LDU1 data to send. * @param[in] frameType DVM P25 frame type. + * @param[in] controlByte DVM Network Control Byte. * @returns bool True, if message was sent, otherwise false. */ bool writeP25LDU1(const p25::lc::LC& control, const p25::data::LowSpeedData& lsd, const uint8_t* data, - p25::defines::FrameType::E frameType) override; + P25DEF::FrameType::E frameType, uint8_t controlByte = 0U) override; /** * @brief Writes P25 LDU2 frame data to the network. * @param[in] control Instance of p25::lc::LC containing link control data. * @param[in] lsd Instance of p25::data::LowSpeedData containing low speed data. * @param[in] data Buffer containing P25 LDU2 data to send. + * @param[in] controlByte DVM Network Control Byte. * @returns bool True, if message was sent, otherwise false. */ - bool writeP25LDU2(const p25::lc::LC& control, const p25::data::LowSpeedData& lsd, const uint8_t* data) override; + bool writeP25LDU2(const p25::lc::LC& control, const p25::data::LowSpeedData& lsd, const uint8_t* data, + uint8_t controlByte = 0U) override; /** * @brief Helper to send a DMR terminator with LC message. @@ -104,10 +108,11 @@ namespace network * @param[in] lsd Instance of p25::data::LowSpeedData containing low speed data. * @param[in] data Buffer containing P25 LDU1 data to send. * @param[in] frameType DVM P25 frame type. + * @param[in] controlByte DVM Network Control Byte. * @returns UInt8Array Buffer containing the built network message. */ UInt8Array createP25_LDU1Message_Raw(uint32_t& length, const p25::lc::LC& control, const p25::data::LowSpeedData& lsd, - const uint8_t* data, p25::defines::FrameType::E frameType); + const uint8_t* data, P25DEF::FrameType::E frameType, uint8_t controlByte); /** * @brief Creates an P25 LDU2 frame message. * @@ -118,10 +123,11 @@ namespace network * @param[in] control Instance of p25::lc::LC containing link control data. * @param[in] lsd Instance of p25::data::LowSpeedData containing low speed data. * @param[in] data Buffer containing P25 LDU2 data to send. + * @param[in] controlByte DVM Network Control Byte. * @returns UInt8Array Buffer containing the built network message. */ UInt8Array createP25_LDU2Message_Raw(uint32_t& length, const p25::lc::LC& control, const p25::data::LowSpeedData& lsd, - const uint8_t* data); + const uint8_t* data, uint8_t controlByte); }; } // namespace network diff --git a/src/bridge/win32/project.ico b/src/bridge/win32/project.ico new file mode 100644 index 0000000000000000000000000000000000000000..7557168b5740a7aa088f5368a1d4c7d4ec1c0ca5 GIT binary patch literal 25277 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 literal 0 HcmV?d00001 diff --git a/src/bridge/win32/resource.h b/src/bridge/win32/resource.h new file mode 100644 index 00000000..7ebe3a27 --- /dev/null +++ b/src/bridge/win32/resource.h @@ -0,0 +1,12 @@ +// 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) 2025 Bryan Biedenkapp, N2PLL + * + */ + +#define IDS_APP_TITLE 102 +#define IDI_APP_ICON 103 diff --git a/src/bridge/win32/resource.rc b/src/bridge/win32/resource.rc new file mode 100644 index 0000000000000000000000000000000000000000..964be84fbe4cb6acaaa7c8b99f5d9c13a998968f GIT binary patch literal 5140 zcmdUzTTfd@5Xa}aQoqC2c_C3ThEP@Q8w5kF8ruTnrcxwCu(`CpAWYIA^|QDA{b#*C zUSq4o<$>tvc=zni%+7sh{LkMTwqe1Jtz~=m&epAN*LH3_8`_xPb*ov|zwTJS;O*J1 z{S3wd7RvodFT718h7a*dcS#AF#g8y9@Pe zRxVi+rAQ&3^@+2BHSNav5ziz02)wv0vvcAc2k^&yjoJHPzq5DF$$@voZ+~)fluoOX zX8?40=Da;l3D)4Z&A5Y{ZL8W5^QQkE@pZ_&!x-!hvzGnFP7STRb)1M>W3LLI39rp= z&A;~<>yG`F-yL)$`UMY#?4mzH=6l!7rCqr+YplM6?iRY=vLBd9bN6U?=yTaA+U*b4 zZg{?Dy#cJOR-ek6&e`ylWVcrn`)Lf0As&yN-&{9UIIp?Hi)8!N_ufK7yV6z1EkMmSVP{aYTfi!eiC#{?gB_v)*7`UUA@dPb&$s z4W6PXb*St{w{d0Q)S8Hj*$d~`VVvLuK2FLr~;Kkx8MWwH@p z@@-|e8hQx!$!-3T)j-7e`BHA|@+C{&PGDmuj2T|}Q_wL-#O!zm#cStAxisf1`py7& zd4}?A+#mBTPDEAwNET7;^C%bJPj-t;D~@aU!VdYR#i(4_#1F!f=fC#n@k(gm7CzDq zP@c$=N3=4=#l&Nk>04P&5?2j!R?~j*T$JqfwRn|ot#?F)q zb{Cujry=>1o)>4<_uNNO!&T1Qo5rJV?p4lMM(_2?^lt4vz8QHeWLb933-{=*dr5+h zG5Hu@nT_JF#b-K5tV|IzK=Sw`Nr(72^}6SXneOd|$8LihtUFv4QL!Vx(|YuqRP*F8 z!M4fkmFXK{TTp4@KPUKYdyeX2j6{9k?}h`oJ0zZuiQEA7&-R))k9AKT^YI4R<)ni- zJ_|~b!sZmeM^;mHoXa&2Uzvd)l4F!ag6|Ra`VDu>kXxm}IDq4l*9Ij#ri0yES>|7S_G0Qu1FD(E+-=g# z&GbGjPI+8MquNXSs5YAIY*yhYkITf#Hq~%t`hE#r**JDTL%*lzHQA+_7|=l#{a6{D ze0E(&FHRcF$J|mNB{r; literal 0 HcmV?d00001 diff --git a/src/common/CMakeLists.txt b/src/common/CMakeLists.txt index a7d394f4..ea4983db 100644 --- a/src/common/CMakeLists.txt +++ b/src/common/CMakeLists.txt @@ -4,7 +4,7 @@ # * GPLv2 Open Source. Use is subject to license terms. # * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. # * -# * Copyright (C) 2024 Bryan Biedenkapp, N2PLL +# * Copyright (C) 2024,2025 Bryan Biedenkapp, N2PLL # * # */ file(GLOB common_SRC @@ -39,6 +39,10 @@ file(GLOB common_SRC "src/common/nxdn/lc/*.cpp" "src/common/nxdn/lc/rcch/*.cpp" + # Analog Module + "src/common/analog/*.cpp" + "src/common/analog/data/*.cpp" + # Core "src/common/edac/*.cpp" "src/common/lookups/*.cpp" @@ -87,6 +91,10 @@ file(GLOB common_INCLUDE "src/common/nxdn/lc/*.h" "src/common/nxdn/lc/rcch/*.h" + # Analog Module + "src/common/analog/*.h" + "src/common/analog/data/*.h" + # Core "src/common/concurrent/*.h" "src/common/edac/*.h" diff --git a/src/common/Defines.h b/src/common/Defines.h index 2c4b29be..bb9db1e2 100644 --- a/src/common/Defines.h +++ b/src/common/Defines.h @@ -117,8 +117,8 @@ typedef unsigned long long ulong64_t; #define __EXE_NAME__ "" #define VERSION_MAJOR "04" -#define VERSION_MINOR "31" -#define VERSION_REV "H" +#define VERSION_MINOR "32" +#define VERSION_REV "J" #define __NETVER__ "DVM_R" VERSION_MAJOR VERSION_REV VERSION_MINOR #define __VER__ VERSION_MAJOR "." VERSION_MINOR VERSION_REV " (R" VERSION_MAJOR VERSION_REV VERSION_MINOR " " __GIT_VER__ ")" diff --git a/src/common/Log.cpp b/src/common/Log.cpp index b1668f06..10a835a1 100644 --- a/src/common/Log.cpp +++ b/src/common/Log.cpp @@ -54,6 +54,7 @@ uint32_t g_logDisplayLevel = 2U; bool g_disableTimeDisplay = false; bool g_useSyslog = false; +bool g_disableNetworkLog = false; static struct tm m_tm; @@ -314,7 +315,7 @@ void Log(uint32_t level, const char *module, const char* file, const int lineNo, m_outStream << buffer << std::endl; } - if (m_network != nullptr) { + if (m_network != nullptr && !g_disableNetworkLog) { // don't transfer debug data... if (level > 1U) { m_network->writeDiagLog(buffer); diff --git a/src/common/Log.h b/src/common/Log.h index 349c1816..2ee85a2f 100644 --- a/src/common/Log.h +++ b/src/common/Log.h @@ -139,6 +139,10 @@ extern bool g_disableTimeDisplay; * @brief (Global) Flag indicating whether or not logging goes to the syslog. */ extern bool g_useSyslog; +/** + * @brief (Global) Flag indicating whether or not network logging is disabled. + */ +extern bool g_disableNetworkLog; // --------------------------------------------------------------------------- // Global Functions diff --git a/src/common/RC4Crypto.cpp b/src/common/RC4Crypto.cpp index f8b42553..78c9e6cf 100644 --- a/src/common/RC4Crypto.cpp +++ b/src/common/RC4Crypto.cpp @@ -22,7 +22,7 @@ using namespace crypto; // Public Class Members // --------------------------------------------------------------------------- -/* Initializes a new instance of the AES class. */ +/* Initializes a new instance of the RC4 class. */ RC4::RC4() = default; diff --git a/src/common/VariableLengthArray.h b/src/common/VariableLengthArray.h index 24364839..afe62707 100644 --- a/src/common/VariableLengthArray.h +++ b/src/common/VariableLengthArray.h @@ -41,6 +41,12 @@ typedef std::unique_ptr UInt8Array; */ typedef std::unique_ptr CharArray; +/** + * @brief Unique char array. + * @ingroup common + */ +typedef std::unique_ptr ShortArray; + /** @} */ // --------------------------------------------------------------------------- @@ -66,7 +72,7 @@ typedef std::unique_ptr CharArray; /** * @brief Declares a unique char array/buffer. - * This macro creates a unique pointer to a uint8_t array of the specified length and initializes it to zero. + * This macro creates a unique pointer to a char array of the specified length and initializes it to zero. * The resulting pointer is named after the parameter name passed to the macro. * @ingroup common * @param name Name of array/buffer. @@ -77,6 +83,19 @@ typedef std::unique_ptr CharArray; char* name = __##name##__CharArray.get(); \ ::memset(name, 0, len); +/** + * @brief Declares a unique short array/buffer. + * This macro creates a unique pointer to a short array of the specified length and initializes it to zero. + * The resulting pointer is named after the parameter name passed to the macro. + * @ingroup common + * @param name Name of array/buffer. + * @param len Length of array/buffer. + */ +#define DECLARE_SHORT_ARRAY(name, len) \ + ShortArray __##name##__ShortArray = std::make_unique(len); \ + short* name = __##name##__ShortArray.get(); \ + ::memset(name, 0, len); + /** @} */ #endif // __VARIABLE_LENGTH_ARRAY_H__ \ No newline at end of file diff --git a/src/common/analog/AnalogAudio.cpp b/src/common/analog/AnalogAudio.cpp new file mode 100644 index 00000000..1d1dab26 --- /dev/null +++ b/src/common/analog/AnalogAudio.cpp @@ -0,0 +1,189 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Digital Voice Modem - Common Library + * GPLv2 Open Source. Use is subject to license terms. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * Copyright (C) 2025 Bryan Biedenkapp, N2PLL + * + */ +#include "Defines.h" +#include "analog/AnalogAudio.h" +#include "Log.h" +#include "Utils.h" + +using namespace analog; + +#include +#include +#include + +// --------------------------------------------------------------------------- +// Constants +// --------------------------------------------------------------------------- + +#define SIGN_BIT (0x80) // sign bit for a A-law byte +#define QUANT_MASK (0xf) // quantization field mask +#define NSEGS (8) // number of A-law segments +#define SEG_SHIFT (4) // left shift for segment number +#define SEG_MASK (0x70) // segment field mask + +static short seg_aend[8] = { 0x1F, 0x3F, 0x7F, 0xFF, 0x1FF, 0x3FF, 0x7FF, 0xFFF }; +static short seg_uend[8] = { 0x3F, 0x7F, 0xFF, 0x1FF, 0x3FF, 0x7FF, 0xFFF, 0x1FFF }; + +#define BIAS (0x84) // bias for linear code +#define CLIP 8159 + +#define SHORT_CLIP_SMPL_MIN -32767 +#define SHORT_CLIP_SMPL_MAX 32767 + +// --------------------------------------------------------------------------- +// Public Static Class Members +// --------------------------------------------------------------------------- + +/* Helper to convert PCM into G.711 aLaw. */ + +uint8_t AnalogAudio::encodeALaw(short pcm) +{ + short mask; + uint8_t aval; + + pcm = pcm >> 3; + + if (pcm >= 0) { + mask = 0xD5U; // sign (7th) bit = 1 + } else { + mask = 0x55U; // sign bit = 0 + pcm = -pcm - 1; + } + + // convert the scaled magnitude to segment number + short seg = search(pcm, seg_aend, 8); + + /* + ** combine the sign, segment, quantization bits + */ + if (seg >= 8) // out of range, return maximum value + return (uint8_t)(0x7F ^ mask); + else { + aval = (uint8_t) seg << SEG_SHIFT; + if (seg < 2) + aval |= (pcm >> 1) & QUANT_MASK; + else + aval |= (pcm >> seg) & QUANT_MASK; + + return (aval ^ mask); + } +} + +/* Helper to convert G.711 aLaw into PCM. */ + +short AnalogAudio::decodeALaw(uint8_t alaw) +{ + alaw ^= 0x55U; + + short t = (alaw & QUANT_MASK) << 4; + short seg = ((unsigned)alaw & SEG_MASK) >> SEG_SHIFT; + switch (seg) { + case 0: + t += 8; + break; + case 1: + t += 0x108U; + break; + default: + t += 0x108U; + t <<= seg - 1; + } + + return ((alaw & SIGN_BIT) ? t : -t); +} + +/* Helper to convert PCM into G.711 MuLaw. */ + +uint8_t AnalogAudio::encodeMuLaw(short pcm) +{ + short mask; + + // get the sign and the magnitude of the value + pcm = pcm >> 2; + if (pcm < 0) { + pcm = -pcm; + mask = 0x7FU; + } else { + mask = 0xFFU; + } + + // clip the magnitude + if (pcm > CLIP) + pcm = CLIP; + pcm += (BIAS >> 2); + + // convert the scaled magnitude to segment number + short seg = search(pcm, seg_uend, 8); + + /* + ** combine the sign, segment, quantization bits; + ** and complement the code word + */ + if (seg >= 8) // out of range, return maximum value. + return (uint8_t)(0x7F ^ mask); + else { + uint8_t ulaw = (uint8_t)(seg << 4) | ((pcm >> (seg + 1)) & 0xF); + return (ulaw ^ mask); + } +} + +/* Helper to convert G.711 MuLaw into PCM. */ + +short AnalogAudio::decodeMuLaw(uint8_t ulaw) +{ + // complement to obtain normal u-law value + ulaw = ~ulaw; + + /* + ** extract and bias the quantization bits; then + ** shift up by the segment number and subtract out the bias + */ + short t = ((ulaw & QUANT_MASK) << 3) + BIAS; + t <<= ((unsigned)ulaw & SEG_MASK) >> SEG_SHIFT; + + return ((ulaw & SIGN_BIT) ? (BIAS - t) : (t - BIAS)); +} + +/* Helper to apply linear gain to PCM samples. */ + +void AnalogAudio::gain(short* pcm, int length, float gain) +{ + assert(pcm != nullptr); + assert(length > 0); + + if (gain == 1.0f) + return; + + for (int i = 0; i < length; i++) { + int sample = static_cast(pcm[i] * gain); + if (sample > SHORT_CLIP_SMPL_MAX) + sample = SHORT_CLIP_SMPL_MAX; + else if (sample < SHORT_CLIP_SMPL_MIN) + sample = SHORT_CLIP_SMPL_MIN; + + pcm[i] = static_cast(sample); + } +} + +// --------------------------------------------------------------------------- +// Private Static Class Members +// --------------------------------------------------------------------------- + +/* Searches for the segment in the given array. */ + +short AnalogAudio::search(short val, short* table, short size) +{ + for (short i = 0; i < size; i++) { + if (val <= *table++) + return (i); + } + + return (size); +} diff --git a/src/common/analog/AnalogAudio.h b/src/common/analog/AnalogAudio.h new file mode 100644 index 00000000..dfcbf955 --- /dev/null +++ b/src/common/analog/AnalogAudio.h @@ -0,0 +1,97 @@ +// 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) 2024,2025 Bryan Biedenkapp, N2PLL + * + */ +/** + * @file AnalogAudio.h + * @ingroup analog + * @file AnalogAudio.cpp + * @ingroup analog + */ +#if !defined(__ANALOG_AUDIO_H__) +#define __ANALOG_AUDIO_H__ + +#include "common/Defines.h" +#include "common/analog/AnalogDefines.h" + +namespace analog +{ + // --------------------------------------------------------------------------- + // Class Declaration + // --------------------------------------------------------------------------- + + /** + * @brief Implements aLaw and uLaw audio codecs, along with helper routines for analog audio. + * @ingroup analog + */ + class HOST_SW_API AnalogAudio { + public: + /** + * @brief Helper to convert PCM into G.711 aLaw. + * @param pcm PCM value. + * @return uint8_t aLaw value. + */ + static uint8_t encodeALaw(short pcm); + /** + * @brief Helper to convert G.711 aLaw into PCM. + * @param alaw aLaw value. + * @return short PCM value. + */ + static short decodeALaw(uint8_t alaw); + /** + * @brief Helper to convert PCM into G.711 MuLaw. + * @param pcm PCM value. + * @return uint8_t MuLaw value. + */ + static uint8_t encodeMuLaw(short pcm); + /** + * @brief Helper to convert G.711 MuLaw into PCM. + * @param ulaw MuLaw value. + * @return short PCM value. + */ + static short decodeMuLaw(uint8_t ulaw); + + /** + * @brief Helper to apply linear gain to PCM samples. + * @param pcm Buffer containing PCM samples. + * @param length Length of the PCM buffer in samples. + * @param gain Gain factor to apply to the PCM samples (1.0f = no change). + */ + static void gain(short *pcm, int length, float gain); + + /** + * @brief (ms) to sample count conversion. + * @param sampleRate Sample rate. + * @param channels Number of audio channels. + * @param ms Number of milliseconds. + * @returns int Number of samples. + */ + static int toSamples(uint32_t sampleRate, uint8_t channels, int ms) { return (int)(((long)ms) * sampleRate * channels / 1000); } + + /** + * @brief Sample count to (ms) conversion. + * @param sampleRate Sample rate. + * @param channels Number of audio channels. + * @param samples Number of samples. + * @returns int Number of milliseconds. + */ + static int toMS(uint32_t sampleRate, uint8_t channels, int samples) { return (int)(((float)samples / (float)sampleRate / (float)channels) * 1000); } + + private: + /** + * @brief Searches for the segment in the given array. + * @param val PCM value to search for. + * @param table Array of segment values. + * @param size Size of the segment array. + * @return short + */ + static short search(short val, short* table, short size); + }; +} // namespace analog + +#endif // __ANALOG_AUDIO_H__ diff --git a/src/common/analog/AnalogDefines.h b/src/common/analog/AnalogDefines.h new file mode 100644 index 00000000..ee604b16 --- /dev/null +++ b/src/common/analog/AnalogDefines.h @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Digital Voice Modem - Common Library + * GPLv2 Open Source. Use is subject to license terms. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * Copyright (C) 2025 Bryan Biedenkapp, N2PLL + * + */ +/** + * @defgroup analog Digital Mobile Radio + * @brief Defines and implements aLaw and uLaw audio codecs, along with helper routines for analog audio. + * @ingroup common + * + * @file AnalogDefines.h + * @ingroup analog + */ +#if !defined(__ANALOG_DEFINES_H__) +#define __ANALOG_DEFINES_H__ + +#include "common/Defines.h" + +// Shorthand macro to analog::defines -- keeps source code that doesn't use "using" concise +#if !defined(ANODEF) +#define ANODEF analog::defines +#endif // DMRDEF +namespace analog +{ + namespace defines + { + // --------------------------------------------------------------------------- + // Constants + // --------------------------------------------------------------------------- + + /** + * @addtogroup 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. + /** @} */ + + /** @brief Audio Frame Type(s) */ + namespace AudioFrameType { + /** @brief Audio Frame Type(s) */ + enum E : uint8_t { + VOICE_START = 0x00U, //! Voice Start Frame + VOICE = 0x01U, //! Voice Continuation Frame + TERMINATOR = 0x02U, //! Voice End Frame / Call Terminator + }; + } + + #define ANO_TERMINATOR "Analog, TERMINATOR (Terminator)" + #define ANO_VOICE "Analog, VOICE (Voice Data)" + } // namespace defines +} // namespace analog + +#endif // __ANALOG_DEFINES_H__ diff --git a/src/common/analog/data/NetData.cpp b/src/common/analog/data/NetData.cpp new file mode 100644 index 00000000..f6a509fe --- /dev/null +++ b/src/common/analog/data/NetData.cpp @@ -0,0 +1,97 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Digital Voice Modem - Common Library + * GPLv2 Open Source. Use is subject to license terms. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * Copyright (C) 2025 Bryan Biedenkapp, N2PLL + * + */ +#include "Defines.h" +#include "analog/AnalogAudio.h" +#include "analog/data/NetData.h" + +using namespace analog; +using namespace analog::defines; +using namespace analog::data; + +#include +#include + +// --------------------------------------------------------------------------- +// Public Class Members +// --------------------------------------------------------------------------- + +/* Initializes a new instance of the NetData class. */ + +NetData::NetData(const NetData& data) : + m_srcId(data.m_srcId), + m_dstId(data.m_dstId), + m_control(data.m_control), + m_seqNo(data.m_seqNo), + m_group(true), + m_frameType(data.m_frameType), + m_audio(nullptr) +{ + m_audio = new uint8_t[2U * AUDIO_SAMPLES_LENGTH_BYTES]; + ::memcpy(m_audio, data.m_audio, 2U * AUDIO_SAMPLES_LENGTH_BYTES); +} + +/* Initializes a new instance of the NetData class. */ + +NetData::NetData() : + m_srcId(0U), + m_dstId(0U), + m_control(0U), + m_seqNo(0U), + m_group(true), + m_frameType(AudioFrameType::TERMINATOR), + m_audio(nullptr) +{ + m_audio = new uint8_t[2U * AUDIO_SAMPLES_LENGTH_BYTES]; +} + +/* Finalizes a instance of the NetData class. */ + +NetData::~NetData() +{ + delete[] m_audio; +} + +/* Equals operator. */ + +NetData& NetData::operator=(const NetData& data) +{ + if (this != &data) { + ::memcpy(m_audio, data.m_audio, 2U * AUDIO_SAMPLES_LENGTH_BYTES); + + m_srcId = data.m_srcId; + m_dstId = data.m_dstId; + m_control = data.m_control; + m_frameType = data.m_frameType; + m_seqNo = data.m_seqNo; + m_group = data.m_group; + } + + return *this; +} + +/* Sets audio data. */ + +void NetData::setAudio(const uint8_t* buffer) +{ + assert(buffer != nullptr); + + ::memcpy(m_audio, buffer, AUDIO_SAMPLES_LENGTH_BYTES); +} + +/* Gets audio data. */ + +uint32_t NetData::getAudio(uint8_t* buffer) const +{ + assert(buffer != nullptr); + + ::memcpy(buffer, m_audio, AUDIO_SAMPLES_LENGTH_BYTES); + + return AUDIO_SAMPLES_LENGTH_BYTES; +} diff --git a/src/common/analog/data/NetData.h b/src/common/analog/data/NetData.h new file mode 100644 index 00000000..2691c5ef --- /dev/null +++ b/src/common/analog/data/NetData.h @@ -0,0 +1,104 @@ +// 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 NetData.h + * @ingroup analog + * @file NetData.cpp + * @ingroup analog + */ +#if !defined(__ANALOG_DATA__NET_DATA_H__) +#define __ANALOG_DATA__NET_DATA_H__ + +#include "common/Defines.h" +#include "common/analog/AnalogDefines.h" + +namespace analog +{ + namespace data + { + // --------------------------------------------------------------------------- + // Class Declaration + // --------------------------------------------------------------------------- + + /** + * @brief Represents network analog data. When setting audio data, it is expected to be 20ms of 16-bit + * audio at 8kHz, or 320 bytes in length. + * @ingroup analog + */ + class HOST_SW_API NetData { + public: + /** + * @brief Initializes a new instance of the NetData class. + * @param data Instance of NetData class to copy from. + */ + NetData(const NetData& data); + /** + * @brief Initializes a new instance of the NetData class. + */ + NetData(); + /** + * @brief Finalizes a instance of the NetData class. + */ + ~NetData(); + + /** + * @brief Equals operator. + * @param data Instance of NetData class to copy from. + */ + NetData& operator=(const NetData& data); + + /** + * @brief Sets audio data. This data buffer should be MuLaw encoded. + * @param[in] buffer Audio data buffer. + */ + void setAudio(const uint8_t* buffer); + /** + * @brief Gets audio data. + * @param[out] buffer Audio data buffer. + */ + uint32_t getAudio(uint8_t* buffer) const; + + public: + /** + * @brief Source ID. + */ + DECLARE_PROPERTY(uint32_t, srcId, SrcId); + /** + * @brief Destination ID. + */ + DECLARE_PROPERTY(uint32_t, dstId, DstId); + + /** + * @brief + */ + DECLARE_PROPERTY(uint8_t, control, Control); + + /** + * @brief Sequence number. + */ + DECLARE_PROPERTY(uint8_t, seqNo, SeqNo); + + /** + * @brief Flag indicatin if this group audio or individual audio. + */ + DECLARE_PROPERTY(bool, group, Group); + + /** + * @brief Audio frame type. + */ + DECLARE_PROPERTY(defines::AudioFrameType::E, frameType, FrameType); + + private: + uint8_t* m_audio; + }; + } // namespace data +} // namespace analog + +#endif // __ANALOG_DATA__NET_DATA_H__ diff --git a/src/common/dmr/DMRDefines.h b/src/common/dmr/DMRDefines.h index 30f84b2a..2277faa2 100644 --- a/src/common/dmr/DMRDefines.h +++ b/src/common/dmr/DMRDefines.h @@ -200,11 +200,14 @@ namespace dmr }; }; - /** @name Feature IDs */ + /** @name Feature Set IDs */ /** @brief ETSI Standard Feature Set */ const uint8_t FID_ETSI = 0x00U; + /** @brief Motorola */ - const uint8_t FID_DMRA = 0x10U; + const uint8_t FID_MOT = 0x10U; + /** @brief Kenwood */ + const uint8_t FID_KENWOOD = 0x20U; /** @brief DVM; Omaha Communication Systems, LLC ($9C) */ const uint8_t FID_DVM_OCS = 0x9CU; /** @} */ @@ -259,9 +262,9 @@ namespace dmr }; } - /** @brief FID_DMRA Extended Functions. */ + /** @brief FID_MOT Extended Functions. */ namespace ExtendedFunctions { - /** @brief FID_DMRA Extended Functions. */ + /** @brief FID_MOT Extended Functions. */ enum : uint16_t { CHECK = 0x0000U, //! Radio Check UNINHIBIT = 0x007EU, //! Radio Uninhibit diff --git a/src/common/dmr/DMRUtils.cpp b/src/common/dmr/DMRUtils.cpp new file mode 100644 index 00000000..964f68cf --- /dev/null +++ b/src/common/dmr/DMRUtils.cpp @@ -0,0 +1,85 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Digital Voice Modem - Common Library + * GPLv2 Open Source. Use is subject to license terms. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * Copyright (C) 2025 Bryan Biedenkapp, N2PLL + * + */ +#include "Defines.h" +#include "dmr/DMRDefines.h" +#include "dmr/DMRUtils.h" +#include "Utils.h" + +using namespace dmr; +using namespace dmr::defines; + +#include + +// --------------------------------------------------------------------------- +// Static Class Members +// --------------------------------------------------------------------------- + +/* Helper to convert a denial reason code to a string. */ + +std::string DMRUtils::rsnToString(uint8_t reason) +{ + switch (reason) { + case ReasonCode::TS_ACK_RSN_MSG: + return std::string("TS_ACK_RSN_MSG (Message Accepted)"); + case ReasonCode::TS_ACK_RSN_REG: + return std::string("TS_ACK_RSN_REG (Registration Accepted)"); + case ReasonCode::TS_ACK_RSN_AUTH_RESP: + return std::string("TS_ACK_RSN_AUTH_RESP (Authentication Challenge Response)"); + case ReasonCode::TS_ACK_RSN_REG_SUB_ATTACH: + return std::string("TS_ACK_RSN_REG_SUB_ATTACH (Registration Response with subscription)"); + case ReasonCode::MS_ACK_RSN_MSG: + return std::string("MS_ACK_RSN_MSG (Message Accepted)"); + case ReasonCode::MS_ACK_RSN_AUTH_RESP: + return std::string("MS_ACK_RSN_AUTH_RESP (Authentication Challenge Response)"); + + case ReasonCode::TS_DENY_RSN_SYS_UNSUPPORTED_SVC: + return std::string("TS_DENY_RSN_SYS_UNSUPPORTED_SVC (System Unsupported Service)"); + case ReasonCode::TS_DENY_RSN_PERM_USER_REFUSED: + return std::string("TS_DENY_RSN_PERM_USER_REFUSED (User Permenantly Refused)"); + case ReasonCode::TS_DENY_RSN_TEMP_USER_REFUSED: + return std::string("TS_DENY_RSN_TEMP_USER_REFUSED (User Temporarily Refused)"); + case ReasonCode::TS_DENY_RSN_TRSN_SYS_REFUSED: + return std::string("TS_DENY_RSN_TRSN_SYS_REFUSED (System Refused)"); + case ReasonCode::TS_DENY_RSN_TGT_NOT_REG: + return std::string("TS_DENY_RSN_TGT_NOT_REG (Target Not Registered)"); + case ReasonCode::TS_DENY_RSN_TGT_UNAVAILABLE: + return std::string("TS_DENY_RSN_TGT_UNAVAILABLE (Target Unavailable)"); + case ReasonCode::TS_DENY_RSN_SYS_BUSY: + return std::string("TS_DENY_RSN_SYS_BUSY (System Busy)"); + case ReasonCode::TS_DENY_RSN_SYS_NOT_READY: + return std::string("TS_DENY_RSN_SYS_NOT_READY (System Not Ready)"); + case ReasonCode::TS_DENY_RSN_CALL_CNCL_REFUSED: + return std::string("TS_DENY_RSN_CALL_CNCL_REFUSED (Call Cancel Refused)"); + case ReasonCode::TS_DENY_RSN_REG_REFUSED: + return std::string("TS_DENY_RSN_REG_REFUSED (Registration Refused)"); + case ReasonCode::TS_DENY_RSN_REG_DENIED: + return std::string("TS_DENY_RSN_REG_DENIED (Registration Denied)"); + case ReasonCode::TS_DENY_RSN_MS_NOT_REG: + return std::string("TS_DENY_RSN_MS_NOT_REG (MS Not Registered)"); + case ReasonCode::TS_DENY_RSN_TGT_BUSY: + return std::string("TS_DENY_RSN_TGT_BUSY (Target Busy)"); + case ReasonCode::TS_DENY_RSN_TGT_GROUP_NOT_VALID: + return std::string("TS_DENY_RSN_TGT_GROUP_NOT_VALID (Group Not Valid)"); + + case ReasonCode::TS_QUEUED_RSN_NO_RESOURCE: + return std::string("TS_QUEUED_RSN_NO_RESOURCE (No Resources Available)"); + case ReasonCode::TS_QUEUED_RSN_SYS_BUSY: + return std::string("TS_QUEUED_RSN_SYS_BUSY (System Busy)"); + + case ReasonCode::TS_WAIT_RSN: + return std::string("TS_WAIT_RSN (Wait)"); + + case ReasonCode::MS_DENY_RSN_UNSUPPORTED_SVC: + return std::string("MS_DENY_RSN_UNSUPPORTED_SVC (Service Unsupported)"); + + default: + return std::string(); + } +} diff --git a/src/common/dmr/DMRUtils.h b/src/common/dmr/DMRUtils.h index 471c4447..aa4aaa9a 100644 --- a/src/common/dmr/DMRUtils.h +++ b/src/common/dmr/DMRUtils.h @@ -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) 2021,2024 Bryan Biedenkapp, N2PLL + * Copyright (C) 2021,2024,2025 Bryan Biedenkapp, N2PLL * */ /** @@ -136,6 +136,13 @@ namespace dmr return id; } + + /** + * @brief Helper to convert a reason code to a string. + * @param reason Reason code. + * @returns std::string Reason code string. + */ + static std::string rsnToString(uint8_t reason); }; } // namespace dmr diff --git a/src/common/dmr/lc/CSBK.cpp b/src/common/dmr/lc/CSBK.cpp index ca54cf2a..9c8a655d 100644 --- a/src/common/dmr/lc/CSBK.cpp +++ b/src/common/dmr/lc/CSBK.cpp @@ -272,7 +272,7 @@ bool CSBK::decode(const uint8_t* data, uint8_t* payload) } if (m_verbose) { - Utils::dump(2U, "Decoded CSBK", csbk, DMR_CSBK_LENGTH_BYTES); + Utils::dump(2U, "CSBK::decode(), Decoded CSBK", csbk, DMR_CSBK_LENGTH_BYTES); } m_raw = new uint8_t[DMR_CSBK_LENGTH_BYTES]; @@ -340,7 +340,7 @@ void CSBK::encode(uint8_t* data, const uint8_t* payload) } if (m_verbose) { - Utils::dump(2U, "Encoded CSBK", csbk, DMR_CSBK_LENGTH_BYTES); + Utils::dump(2U, "CSBK::encode(), Encoded CSBK", csbk, DMR_CSBK_LENGTH_BYTES); } // encode BPTC (196,96) FEC diff --git a/src/common/dmr/lc/csbk/CSBKFactory.cpp b/src/common/dmr/lc/csbk/CSBKFactory.cpp index c16dc620..bdb4dc47 100644 --- a/src/common/dmr/lc/csbk/CSBKFactory.cpp +++ b/src/common/dmr/lc/csbk/CSBKFactory.cpp @@ -93,10 +93,10 @@ std::unique_ptr CSBKFactory::createCSBK(const uint8_t* data, DataType::E d return decode(new CSBK_UU_ANS_RSP(), data); case CSBKO::PRECCSBK: return decode(new CSBK_PRECCSBK(), data); - case CSBKO::RAND: // CSBKO::CALL_ALRT when FID == FID_DMRA + case CSBKO::RAND: // CSBKO::CALL_ALRT when FID == FID_MOT switch (FID) { - case FID_DMRA: + case FID_MOT: return decode(new CSBK_CALL_ALRT(), data); case FID_ETSI: default: diff --git a/src/common/dmr/lc/csbk/CSBK_CALL_ALRT.cpp b/src/common/dmr/lc/csbk/CSBK_CALL_ALRT.cpp index 98b9ab0d..8872452a 100644 --- a/src/common/dmr/lc/csbk/CSBK_CALL_ALRT.cpp +++ b/src/common/dmr/lc/csbk/CSBK_CALL_ALRT.cpp @@ -26,7 +26,7 @@ using namespace dmr::lc::csbk; CSBK_CALL_ALRT::CSBK_CALL_ALRT() : CSBK() { m_CSBKO = CSBKO::RAND; - m_FID = FID_DMRA; + m_FID = FID_MOT; } /* Decode a control signalling block. */ diff --git a/src/common/dmr/lc/csbk/CSBK_EXT_FNCT.cpp b/src/common/dmr/lc/csbk/CSBK_EXT_FNCT.cpp index 74534319..0f20d561 100644 --- a/src/common/dmr/lc/csbk/CSBK_EXT_FNCT.cpp +++ b/src/common/dmr/lc/csbk/CSBK_EXT_FNCT.cpp @@ -27,7 +27,7 @@ CSBK_EXT_FNCT::CSBK_EXT_FNCT() : CSBK(), m_extendedFunction(ExtendedFunctions::CHECK) { m_CSBKO = CSBKO::EXT_FNCT; - m_FID = FID_DMRA; + m_FID = FID_MOT; } /* Decode a control signalling block. */ diff --git a/src/common/lookups/AdjSiteMapLookup.cpp b/src/common/lookups/AdjSiteMapLookup.cpp new file mode 100644 index 00000000..9d5686ff --- /dev/null +++ b/src/common/lookups/AdjSiteMapLookup.cpp @@ -0,0 +1,288 @@ +// 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 "lookups/AdjSiteMapLookup.h" +#include "Log.h" +#include "Timer.h" +#include "Utils.h" + +using namespace lookups; + +#include +#include + +// --------------------------------------------------------------------------- +// Static Class Members +// --------------------------------------------------------------------------- + +std::mutex AdjSiteMapLookup::m_mutex; +bool AdjSiteMapLookup::m_locked = false; + +// --------------------------------------------------------------------------- +// Macros +// --------------------------------------------------------------------------- + +// Lock the table. +#define __LOCK_TABLE() \ + std::lock_guard lock(m_mutex); \ + m_locked = true; + +// Unlock the table. +#define __UNLOCK_TABLE() m_locked = false; + +// Spinlock wait for table to be read unlocked. +#define __SPINLOCK() \ + if (m_locked) { \ + while (m_locked) \ + Thread::sleep(2U); \ + } + +// --------------------------------------------------------------------------- +// Public Class Members +// --------------------------------------------------------------------------- + +/* Initializes a new instance of the AdjSiteMapLookup class. */ + +AdjSiteMapLookup::AdjSiteMapLookup(const std::string& filename, uint32_t reloadTime) : Thread(), + m_rulesFile(filename), + m_reloadTime(reloadTime), + m_rules(), + m_stop(false), + m_adjPeerMap() +{ + /* stub */ +} + +/* Finalizes a instance of the AdjSiteMapLookup class. */ + +AdjSiteMapLookup::~AdjSiteMapLookup() = default; + +/* Thread entry point. This function is provided to run the thread for the lookup table. */ + +void AdjSiteMapLookup::entry() +{ + if (m_reloadTime == 0U) { + return; + } + + Timer timer(1U, 60U * m_reloadTime); + timer.start(); + + while (!m_stop) { + sleep(1000U); + + timer.clock(); + if (timer.hasExpired()) { + load(); + timer.start(); + } + } +} + +/* Stops and unloads this lookup table. */ + +void AdjSiteMapLookup::stop(bool noDestroy) +{ + if (m_reloadTime == 0U) { + if (!noDestroy) + delete this; + return; + } + + m_stop = true; + + wait(); +} + +/* Reads the lookup table from the specified lookup table file. */ + +bool AdjSiteMapLookup::read() +{ + bool ret = load(); + + if (m_reloadTime > 0U) + run(); + setName("host:adj-site-map"); + + return ret; +} + +/* Clears all entries from the lookup table. */ + +void AdjSiteMapLookup::clear() +{ + __LOCK_TABLE(); + + m_adjPeerMap.clear(); + + __UNLOCK_TABLE(); +} + +/* Adds a new entry to the lookup table by the specified unique ID. */ + +void AdjSiteMapLookup::addEntry(AdjPeerMapEntry entry) +{ + uint32_t id = entry.peerId(); + + __LOCK_TABLE(); + + auto it = std::find_if(m_adjPeerMap.begin(), m_adjPeerMap.end(), + [&](AdjPeerMapEntry x) + { + return x.peerId() == id; + }); + if (it != m_adjPeerMap.end()) { + m_adjPeerMap[it - m_adjPeerMap.begin()] = entry; + } + else { + m_adjPeerMap.push_back(entry); + } + + __UNLOCK_TABLE(); +} + +/* Erases an existing entry from the lookup table by the specified unique ID. */ + +void AdjSiteMapLookup::eraseEntry(uint32_t id) +{ + __LOCK_TABLE(); + + auto it = std::find_if(m_adjPeerMap.begin(), m_adjPeerMap.end(), [&](AdjPeerMapEntry x) { return x.peerId() == id; }); + if (it != m_adjPeerMap.end()) { + m_adjPeerMap.erase(it); + } + + __UNLOCK_TABLE(); +} + +/* Finds a table entry in this lookup table. */ + +AdjPeerMapEntry AdjSiteMapLookup::find(uint32_t id) +{ + AdjPeerMapEntry entry; + + __SPINLOCK(); + + std::lock_guard lock(m_mutex); + auto it = std::find_if(m_adjPeerMap.begin(), m_adjPeerMap.end(), + [&](AdjPeerMapEntry x) + { + return x.peerId() == id; + }); + if (it != m_adjPeerMap.end()) { + entry = *it; + } else { + entry = AdjPeerMapEntry(); + } + + return entry; +} + +/* Saves loaded talkgroup rules. */ + +bool AdjSiteMapLookup::commit() +{ + return save(); +} + +// --------------------------------------------------------------------------- +// Private Class Members +// --------------------------------------------------------------------------- + +/* Loads the table from the passed lookup table file. */ + +bool AdjSiteMapLookup::load() +{ + if (m_rulesFile.length() <= 0) { + return false; + } + + try { + bool ret = yaml::Parse(m_rules, m_rulesFile.c_str()); + if (!ret) { + LogError(LOG_HOST, "Cannot open the adjacent site map lookup file - %s - error parsing YML", m_rulesFile.c_str()); + return false; + } + } + catch (yaml::OperationException const& e) { + LogError(LOG_HOST, "Cannot open the adjacent site map lookup file - %s (%s)", m_rulesFile.c_str(), e.message()); + return false; + } + + // clear table + clear(); + + __LOCK_TABLE(); + + yaml::Node& peerList = m_rules["peers"]; + + if (peerList.size() == 0U) { + ::LogError(LOG_HOST, "No adj site map peer list defined!"); + m_locked = false; + return false; + } + + for (size_t i = 0; i < peerList.size(); i++) { + AdjPeerMapEntry entry = AdjPeerMapEntry(peerList[i]); + m_adjPeerMap.push_back(entry); + } + + __UNLOCK_TABLE(); + + size_t size = m_adjPeerMap.size(); + if (size == 0U) { + return false; + } + + LogInfoEx(LOG_HOST, "Loaded %lu entries into adjacent site map table", size); + + return true; +} + +/* Saves the table to the passed lookup table file. */ + +bool AdjSiteMapLookup::save() +{ + // Make sure file is valid + if (m_rulesFile.length() <= 0) { + return false; + } + + std::lock_guard lock(m_mutex); + + // New list for our new group voice rules + yaml::Node peerList; + yaml::Node newRules; + + for (auto entry : m_adjPeerMap) { + yaml::Node& gv = peerList.push_back(); + entry.getYaml(gv); + } + + // Set the new rules + newRules["peers"] = peerList; + + // Make sure we actually did stuff right + if (newRules["peers"].size() != m_adjPeerMap.size()) { + LogError(LOG_HOST, "Generated YAML node for group lists did not match loaded map size! (%u != %u)", newRules["peers"].size(), m_adjPeerMap.size()); + return false; + } + + try { + LogMessage(LOG_HOST, "Saving adjacent site map file to %s", m_rulesFile.c_str()); + yaml::Serialize(newRules, m_rulesFile.c_str()); + LogDebug(LOG_HOST, "Saved adj. site map file to %s", m_rulesFile.c_str()); + } + catch (yaml::OperationException const& e) { + LogError(LOG_HOST, "Cannot save the adjacent site map lookup file - %s (%s)", m_rulesFile.c_str(), e.message()); + return false; + } + + return true; +} diff --git a/src/common/lookups/AdjSiteMapLookup.h b/src/common/lookups/AdjSiteMapLookup.h new file mode 100644 index 00000000..ca196f6c --- /dev/null +++ b/src/common/lookups/AdjSiteMapLookup.h @@ -0,0 +1,258 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Digital Voice Modem - Common Library + * GPLv2 Open Source. Use is subject to license terms. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * Copyright (C) 2025 Bryan Biedenkapp, N2PLL + * + */ +/** + * @defgroup lookups_asm Adjacent Site Map Lookups + * @brief Implementation for adjacent site map lookup tables. + * @ingroup lookups + * + * @file AdjSiteMapLookup.h + * @ingroup lookups_asm + * @file AdjSiteMapLookup.cpp + * @ingroup lookups_asm + */ +#if !defined(__ADJ_SITE_MAP_LOOKUP_H__) +#define __ADJ_SITE_MAP_LOOKUP_H__ + +#include "common/Defines.h" +#include "common/lookups/LookupTable.h" +#include "common/yaml/Yaml.h" +#include "common/Utils.h" + +#include +#include +#include + +namespace lookups +{ + // --------------------------------------------------------------------------- + // Class Declaration + // --------------------------------------------------------------------------- + + /** + * @brief Represents an adjacent peer map entry. + * @ingroup lookups_asm + */ + class HOST_SW_API AdjPeerMapEntry { + public: + /** + * @brief Initializes a new instance of the AdjPeerMapEntry class. + */ + AdjPeerMapEntry() : + m_active(false), + m_peerId(0U), + m_neighbors() + { + /* stub */ + } + /** + * @brief Initializes a new instance of the AdjPeerMapEntry class. + * @param node YAML node for this configuration block. + */ + AdjPeerMapEntry(yaml::Node& node) : + AdjPeerMapEntry() + { + m_active = node["active"].as(false); + m_peerId = node["peer_id"].as(0U); + + yaml::Node& neighborList = node["neighbors"]; + if (neighborList.size() > 0U) { + for (size_t i = 0; i < neighborList.size(); i++) { + uint32_t peerId = neighborList[i].as(0U); + m_neighbors.push_back(peerId); + } + } + } + + /** + * @brief Equals operator. Copies this AdjPeerMapEntry to another AdjPeerMapEntry. + * @param data Instance of AdjPeerMapEntry to copy. + */ + virtual AdjPeerMapEntry& operator= (const AdjPeerMapEntry& data) + { + if (this != &data) { + m_active = data.m_active; + m_peerId = data.m_peerId; + m_neighbors = data.m_neighbors; + } + + return *this; + } + + /** + * @brief Gets the count of neighbors. + * @returns uint8_t Total count of peer neighbors. + */ + uint8_t neighborSize() const { return m_neighbors.size(); } + + /** + * @brief Helper to quickly determine if a entry is valid. + * @returns bool True, if entry is valid, otherwise false. + */ + bool isEmpty() const + { + if (m_neighbors.size() > 0U) + return true; + return false; + } + + /** + * @brief Return the YAML structure for this TalkgroupRuleConfig. + * @param[out] node YAML node. + */ + void getYaml(yaml::Node &node) + { + // We have to convert the bools back to strings to pass to the yaml node + node["active"] = __BOOL_STR(m_active); + node["peerId"] = __INT_STR(m_peerId); + + // Get the lists + yaml::Node neighborList; + if (m_neighbors.size() > 0U) { + for (auto neighbor : m_neighbors) { + yaml::Node& newNeighbor = neighborList.push_back(); + newNeighbor = __INT_STR(neighbor); + } + } + } + + public: + /** + * @brief Flag indicating whether the rule is active. + */ + DECLARE_PROPERTY_PLAIN(bool, active); + /** + * @brief Peer ID. + */ + DECLARE_PROPERTY_PLAIN(uint32_t, peerId); + + /** + * @brief List of neighbor peers. + */ + DECLARE_PROPERTY_PLAIN(std::vector, neighbors); + }; + + // --------------------------------------------------------------------------- + // Class Declaration + // --------------------------------------------------------------------------- + + /** + * @brief Implements a threading lookup table class that contains adjacent site map + * information. + * @ingroup lookups_asm + */ + class HOST_SW_API AdjSiteMapLookup : public Thread { + public: + /** + * @brief Initializes a new instance of the AdjSiteMapLookup class. + * @param filename Full-path to the routing rules file. + * @param reloadTime Interval of time to reload the routing rules. + */ + AdjSiteMapLookup(const std::string& filename, uint32_t reloadTime); + /** + * @brief Finalizes a instance of the AdjSiteMapLookup class. + */ + ~AdjSiteMapLookup() override; + + /** + * @brief Thread entry point. This function is provided to run the thread + * for the lookup table. + */ + void entry() override; + + /** + * @brief Stops and unloads this lookup table. + * @param noDestroy Flag indicating the lookup table should remain resident in memory after stopping. + */ + void stop(bool noDestroy = false); + /** + * @brief Reads the lookup table from the specified lookup table file. + * (NOTE: If the reload time for this lookup table is set to 0, a call to stop will also delete the object.) + * @returns bool True, if lookup table was read, otherwise false. + */ + bool read(); + /** + * @brief Reads the lookup table from the specified lookup table file. + * @returns bool True, if lookup table was read, otherwise false. + */ + bool reload() { return load(); } + /** + * @brief Clears all entries from the lookup table. + */ + void clear(); + + /** + * @brief Adds a new entry to the lookup table. + * @param entry Peer map entry. + */ + void addEntry(AdjPeerMapEntry entry); + /** + * @brief Erases an existing entry from the lookup table by the specified unique ID. + * @param id Unique ID to erase. + */ + void eraseEntry(uint32_t id); + /** + * @brief Finds a table entry in this lookup table. + * @param id Unique identifier for table entry. + * @returns AdjPeerMapEntry Table entry. + */ + virtual AdjPeerMapEntry find(uint32_t id); + + /** + * @brief Saves loaded talkgroup rules. + */ + bool commit(); + + /** + * @brief Returns the filename used to load this lookup table. + * @return std::string Full-path to the lookup table file. + */ + const std::string filename() { return m_rulesFile; }; + /** + * @brief Sets the filename used to load this lookup table. + * @param filename Full-path to the routing rules file. + */ + void filename(std::string filename) { m_rulesFile = filename; }; + + /** + * @brief Helper to set the reload time of this lookup table. + * @param reloadTime Lookup time in seconds. + */ + void setReloadTime(uint32_t reloadTime) { m_reloadTime = reloadTime; } + + private: + std::string m_rulesFile; + uint32_t m_reloadTime; + yaml::Node m_rules; + + bool m_stop; + + static std::mutex m_mutex; //! Mutex used for change locking. + static bool m_locked; //! Flag used for read locking (prevents find lookups), should be used when atomic operations (add/erase/etc) are being used. + + /** + * @brief Loads the table from the passed lookup table file. + * @return True, if lookup table was loaded, otherwise false. + */ + bool load(); + /** + * @brief Saves the table to the passed lookup table file. + * @return True, if lookup table was saved, otherwise false. + */ + bool save(); + + public: + /** + * @brief List of adjacent site map entries. + */ + DECLARE_PROPERTY_PLAIN(std::vector, adjPeerMap); + }; +} // namespace lookups + +#endif // __ADJ_SITE_MAP_LOOKUP_H__ diff --git a/src/common/lookups/IdenTableLookup.cpp b/src/common/lookups/IdenTableLookup.cpp index 0c38016a..50bb082e 100644 --- a/src/common/lookups/IdenTableLookup.cpp +++ b/src/common/lookups/IdenTableLookup.cpp @@ -113,22 +113,13 @@ bool IdenTableLookup::load() continue; // tokenize line - std::string next; std::vector parsed; + std::stringstream ss(line); + std::string field; char delim = ','; - for (auto it = line.begin(); it != line.end(); it++) { - if (*it == delim) { - if (!next.empty()) { - parsed.push_back(next); - next.clear(); - } - } - else - next += *it; - } - if (!next.empty()) - parsed.push_back(next); + while (std::getline(ss, field, delim)) + parsed.push_back(field); // ensure we have at least 5 fields if (parsed.size() < 5) { diff --git a/src/common/lookups/PeerListLookup.cpp b/src/common/lookups/PeerListLookup.cpp index d96d291c..9215e3b9 100644 --- a/src/common/lookups/PeerListLookup.cpp +++ b/src/common/lookups/PeerListLookup.cpp @@ -69,17 +69,15 @@ void PeerListLookup::clear() /* Adds a new entry to the list. */ -void PeerListLookup::addEntry(uint32_t id, const std::string& alias, const std::string& password, bool peerLink, bool canRequestKeys) +void PeerListLookup::addEntry(uint32_t id, PeerId entry) { - PeerId entry = PeerId(id, alias, password, peerLink, canRequestKeys, false); - __LOCK_TABLE(); try { PeerId _entry = m_table.at(id); // if either the alias or the enabled flag doesn't match, update the entry if (_entry.peerId() == id) { - _entry = PeerId(id, alias, password, peerLink, canRequestKeys, false); + _entry = entry; m_table[id] = _entry; } } catch (...) { @@ -117,7 +115,7 @@ PeerId PeerListLookup::find(uint32_t id) try { entry = m_table.at(id); } catch (...) { - entry = PeerId(0U, "", "", false, false, true); + entry = PeerId(0U, "", "", true); } return entry; @@ -208,22 +206,13 @@ bool PeerListLookup::load() continue; // tokenize line - std::string next; std::vector parsed; + std::stringstream ss(line); + std::string field; char delim = ','; - for (char c : line) { - if (c == delim) { - //if (!next.empty()) { - parsed.push_back(next); - next.clear(); - //} - } - else - next += c; - } - if (!next.empty()) - parsed.push_back(next); + while (std::getline(ss, field, delim)) + parsed.push_back(field); // parse tokenized line uint32_t id = ::atoi(parsed[0].c_str()); @@ -243,20 +232,31 @@ bool PeerListLookup::load() if (parsed.size() >= 5) canRequestKeys = ::atoi(parsed[4].c_str()) == 1; + // parse can issue inhibit flag + bool canIssueInhibit = false; + if (parsed.size() >= 6) + canIssueInhibit = ::atoi(parsed[5].c_str()) == 1; + // parse optional password std::string password = ""; if (parsed.size() >= 2) password = parsed[1].c_str(); // load into table - m_table[id] = PeerId(id, alias, password, peerLink, canRequestKeys, false); + PeerId entry = PeerId(id, alias, password, false); + entry.peerLink(peerLink); + entry.canRequestKeys(canRequestKeys); + entry.canIssueInhibit(canIssueInhibit); + + m_table[id] = entry; // log depending on what was loaded LogMessage(LOG_HOST, "Loaded peer ID %u%s into peer ID lookup table, %s%s%s", id, (!alias.empty() ? (" (" + alias + ")").c_str() : ""), (!password.empty() ? "using unique peer password" : "using master password"), (peerLink) ? ", Peer-Link Enabled" : "", - (canRequestKeys) ? ", Can Request Keys" : ""); + (canRequestKeys) ? ", Can Request Keys" : "", + (canIssueInhibit) ? ", Can Issue Inhibit" : ""); } } @@ -334,6 +334,14 @@ bool PeerListLookup::save() line += "0,"; } + // add canIssueInhibit flag + bool canIssueInhibit = entry.second.canIssueInhibit(); + if (canIssueInhibit) { + line += "1,"; + } else { + line += "0,"; + } + line += "\n"; file << line; lines++; diff --git a/src/common/lookups/PeerListLookup.h b/src/common/lookups/PeerListLookup.h index d4ae371e..5b7dd15b 100644 --- a/src/common/lookups/PeerListLookup.h +++ b/src/common/lookups/PeerListLookup.h @@ -51,6 +51,7 @@ namespace lookups m_peerPassword(), m_peerLink(false), m_canRequestKeys(false), + m_canIssueInhibit(false), m_peerDefault(false) { /* stub */ @@ -61,16 +62,15 @@ namespace lookups * @param peerAlias Peer alias * @param peerPassword Per Peer Password. * @param sendConfiguration Flag indicating this peer participates in peer link and should be sent configuration. - * @param peerLink lag indicating if the peer participates in peer link and should be sent configuration. - * @param canRequestKeys Flag indicating if the peer can request encryption keys. * @param peerDefault Flag indicating this is a "default" (i.e. undefined) peer. */ - PeerId(uint32_t peerId, const std::string& peerAlias, const std::string& peerPassword, bool peerLink, bool canRequestKeys, bool peerDefault) : + PeerId(uint32_t peerId, const std::string& peerAlias, const std::string& peerPassword, bool peerDefault) : m_peerId(peerId), m_peerAlias(peerAlias), m_peerPassword(peerPassword), - m_peerLink(peerLink), - m_canRequestKeys(canRequestKeys), + m_peerLink(false), + m_canRequestKeys(false), + m_canIssueInhibit(false), m_peerDefault(peerDefault) { /* stub */ @@ -88,6 +88,7 @@ namespace lookups m_peerPassword = data.m_peerPassword; m_peerLink = data.m_peerLink; m_canRequestKeys = data.m_canRequestKeys; + m_canIssueInhibit = data.m_canIssueInhibit; m_peerDefault = data.m_peerDefault; } @@ -100,17 +101,13 @@ namespace lookups * @param peerAlias Peer Alias * @param peerPassword Per Peer Password. * @param sendConfiguration Flag indicating this peer participates in peer link and should be sent configuration. - * @param peerLink lag indicating if the peer participates in peer link and should be sent configuration. - * @param canRequestKeys Flag indicating if the peer can request encryption keys. * @param peerDefault Flag indicating this is a "default" (i.e. undefined) peer. */ - void set(uint32_t peerId, const std::string& peerAlias, const std::string& peerPassword, bool peerLink, bool canRequestKeys, bool peerDefault) + void set(uint32_t peerId, const std::string& peerAlias, const std::string& peerPassword, bool peerDefault) { m_peerId = peerId; m_peerAlias = peerAlias; m_peerPassword = peerPassword; - m_peerLink = peerLink; - m_canRequestKeys = canRequestKeys; m_peerDefault = peerDefault; } @@ -135,6 +132,10 @@ namespace lookups * @brief Flag indicating if the peer can request encryption keys. */ DECLARE_PROPERTY_PLAIN(bool, canRequestKeys); + /** + * @brief Flag indicating if the peer can issue inhibit/uninhibit packets. + */ + DECLARE_PROPERTY_PLAIN(bool, canIssueInhibit); /** * @brief Flag indicating if the peer is default. */ @@ -167,15 +168,13 @@ namespace lookups /** * @brief Adds a new entry to the list. - * @param peerId Unique peer ID to add. - * @param password Per Peer Password. - * @param peerLink Flag indicating this peer will participate in peer link and should be sent configuration. - * @param canRequestKeys Flag indicating if the peer can request encryption keys. + * @param id Unique peer ID to add. + * @param entry Peer ID entry to add. */ - void addEntry(uint32_t id, const std::string& alias = "", const std::string& password = "", bool peerLink = false, bool canRequestKeys = false); + void addEntry(uint32_t id, PeerId entry); /** * @brief Removes an existing entry from the list. - * @param peerId Unique peer ID to remove. + * @param id Unique peer ID to remove. */ void eraseEntry(uint32_t id); /** diff --git a/src/common/lookups/RadioIdLookup.cpp b/src/common/lookups/RadioIdLookup.cpp index 55c16889..8cb5fc12 100644 --- a/src/common/lookups/RadioIdLookup.cpp +++ b/src/common/lookups/RadioIdLookup.cpp @@ -192,22 +192,13 @@ bool RadioIdLookup::load() continue; // tokenize line - std::string next; std::vector parsed; + std::stringstream ss(line); + std::string field; char delim = ','; - for (char c : line) { - if (c == delim) { - if (!next.empty()) { - parsed.push_back(next); - next.clear(); - } - } - else - next += c; - } - if (!next.empty()) - parsed.push_back(next); + while (std::getline(ss, field, delim)) + parsed.push_back(field); // ensure we have at least 2 fields if (parsed.size() < 2) { @@ -232,6 +223,7 @@ bool RadioIdLookup::load() } m_table[id] = RadioId(radioEnabled, false, alias, ipAddress); + //::LogInfoEx(LOG_HOST, "Radio NAME: %s RID: %u ENABLED: %u IPADDR: %s", alias.c_str(), id, radioEnabled, ipAddress.c_str()); } } diff --git a/src/common/lookups/TalkgroupRulesLookup.cpp b/src/common/lookups/TalkgroupRulesLookup.cpp index 90fa0148..5e6d175c 100644 --- a/src/common/lookups/TalkgroupRulesLookup.cpp +++ b/src/common/lookups/TalkgroupRulesLookup.cpp @@ -420,7 +420,7 @@ bool TalkgroupRulesLookup::save() LogDebug(LOG_HOST, "Saved TGID config file to %s", m_rulesFile.c_str()); } catch (yaml::OperationException const& e) { - LogError(LOG_HOST, "Cannot open the talkgroup rules lookup file - %s (%s)", m_rulesFile.c_str(), e.message()); + LogError(LOG_HOST, "Cannot save the talkgroup rules lookup file - %s (%s)", m_rulesFile.c_str(), e.message()); return false; } diff --git a/src/common/network/BaseNetwork.cpp b/src/common/network/BaseNetwork.cpp index ead726ed..b3f6e229 100644 --- a/src/common/network/BaseNetwork.cpp +++ b/src/common/network/BaseNetwork.cpp @@ -10,6 +10,7 @@ * */ #include "Defines.h" +#include "common/analog/AnalogDefines.h" #include "common/dmr/DMRDefines.h" #include "common/p25/P25Defines.h" #include "common/nxdn/NXDNDefines.h" @@ -53,10 +54,12 @@ BaseNetwork::BaseNetwork(uint32_t peerId, bool duplex, bool debug, bool slot1, b m_rxDMRData(NET_RING_BUF_SIZE, "DMR Net Buffer"), m_rxP25Data(NET_RING_BUF_SIZE, "P25 Net Buffer"), m_rxNXDNData(NET_RING_BUF_SIZE, "NXDN Net Buffer"), + m_rxAnalogData(NET_RING_BUF_SIZE, "Analog Net Buffer"), m_random(), m_dmrStreamId(nullptr), m_p25StreamId(0U), m_nxdnStreamId(0U), + m_analogStreamId(0U), m_pktSeq(0U), m_audio() { @@ -74,6 +77,7 @@ BaseNetwork::BaseNetwork(uint32_t peerId, bool duplex, bool debug, bool slot1, b m_dmrStreamId[1U] = createStreamId(); m_p25StreamId = createStreamId(); m_nxdnStreamId = createStreamId(); + m_analogStreamId = createStreamId(); } /* Finalizes a instance of the BaseNetwork class. */ @@ -139,7 +143,7 @@ bool BaseNetwork::writeKeyReq(const uint16_t kId, const uint8_t algId) modifyKeyCmd.encode(buffer + 11U); - //Utils::dump("writeKeyReq", buffer, modifyKeyCmd.length() + 11U); + //Utils::dump("BaseNetwork::writeKeyReq(), KMM Buffer", buffer, modifyKeyCmd.length() + 11U); return writeMaster({ NET_FUNC::KEY_REQ, NET_SUBFUNC::NOP }, buffer, modifyKeyCmd.length() + 11U, RTP_END_OF_CALL_SEQ, 0U); } @@ -346,6 +350,15 @@ void BaseNetwork::resetNXDN() m_rxNXDNData.clear(); } +/* Resets the analog ring buffer. */ + +void BaseNetwork::resetAnalog() +{ + m_analogStreamId = createStreamId(); + m_pktSeq = 0U; + m_rxAnalogData.clear(); +} + /* Gets the current DMR stream ID. */ uint32_t BaseNetwork::getDMRStreamId(uint32_t slotNo) const @@ -363,10 +376,12 @@ uint32_t BaseNetwork::getDMRStreamId(uint32_t slotNo) const /* 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 queueOnly, bool useAlternatePort, uint32_t peerId) + bool queueOnly, bool useAlternatePort, uint32_t peerId, uint32_t ssrc) { if (peerId == 0U) peerId = m_peerId; + if (ssrc == 0U) + ssrc = m_peerId; if (useAlternatePort) { sockaddr_storage addr; @@ -377,14 +392,14 @@ bool BaseNetwork::writeMaster(FrameQueue::OpcodePair opcode, const uint8_t* data if (udp::Socket::lookup(address, port, addr, addrLen) == 0) { if (!queueOnly) - return m_frameQueue->write(data, length, streamId, peerId, m_peerId, opcode, pktSeq, addr, addrLen); + return m_frameQueue->write(data, length, streamId, peerId, ssrc, opcode, pktSeq, addr, addrLen); else m_frameQueue->enqueueMessage(data, length, streamId, m_peerId, opcode, pktSeq, addr, addrLen); } } else { if (!queueOnly) - return m_frameQueue->write(data, length, streamId, peerId, m_peerId, opcode, pktSeq, m_addr, m_addrLen); + return m_frameQueue->write(data, length, streamId, peerId, ssrc, opcode, pktSeq, m_addr, m_addrLen); else m_frameQueue->enqueueMessage(data, length, streamId, m_peerId, opcode, pktSeq, m_addr, m_addrLen); } @@ -500,6 +515,11 @@ UInt8Array BaseNetwork::readP25(bool& ret, uint32_t& frameLength) return nullptr; } + if (length == 254U) { + m_rxP25Data.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(new uint8_t[length]); @@ -511,7 +531,8 @@ UInt8Array BaseNetwork::readP25(bool& ret, uint32_t& frameLength) /* Writes P25 LDU1 frame data to the network. */ -bool BaseNetwork::writeP25LDU1(const p25::lc::LC& control, const p25::data::LowSpeedData& lsd, const uint8_t* data, p25::defines::FrameType::E frameType) +bool BaseNetwork::writeP25LDU1(const p25::lc::LC& control, const p25::data::LowSpeedData& lsd, const uint8_t* data, + P25DEF::FrameType::E frameType, uint8_t controlByte) { if (m_status != NET_STAT_RUNNING && m_status != NET_STAT_MST_RUNNING) return false; @@ -523,7 +544,7 @@ bool BaseNetwork::writeP25LDU1(const p25::lc::LC& control, const p25::data::LowS } uint32_t messageLength = 0U; - UInt8Array message = createP25_LDU1Message(messageLength, control, lsd, data, frameType); + UInt8Array message = createP25_LDU1Message(messageLength, control, lsd, data, frameType, controlByte); if (message == nullptr) { return false; } @@ -533,7 +554,8 @@ bool BaseNetwork::writeP25LDU1(const p25::lc::LC& control, const p25::data::LowS /* Writes P25 LDU2 frame data to the network. */ -bool BaseNetwork::writeP25LDU2(const p25::lc::LC& control, const p25::data::LowSpeedData& lsd, const uint8_t* data) +bool BaseNetwork::writeP25LDU2(const p25::lc::LC& control, const p25::data::LowSpeedData& lsd, const uint8_t* data, + uint8_t controlByte) { if (m_status != NET_STAT_RUNNING && m_status != NET_STAT_MST_RUNNING) return false; @@ -545,7 +567,7 @@ bool BaseNetwork::writeP25LDU2(const p25::lc::LC& control, const p25::data::LowS } uint32_t messageLength = 0U; - UInt8Array message = createP25_LDU2Message(messageLength, control, lsd, data); + UInt8Array message = createP25_LDU2Message(messageLength, control, lsd, data, controlByte); if (message == nullptr) { return false; } @@ -560,9 +582,7 @@ bool BaseNetwork::writeP25TDU(const p25::lc::LC& control, const p25::data::LowSp if (m_status != NET_STAT_RUNNING && m_status != NET_STAT_MST_RUNNING) return false; - bool resetSeq = false; if (m_p25StreamId == 0U) { - resetSeq = true; m_p25StreamId = createStreamId(); } @@ -653,6 +673,108 @@ bool BaseNetwork::hasP25Data() const return true; } +/* Helper to validate a P25 network frame length. */ + +bool BaseNetwork::validateP25FrameLength(uint8_t& frameLength, uint32_t len, const P25DEF::DUID::E duid) +{ + using namespace p25::defines; + + // P25 network frame should never be less then 24 bytes + if (len < 24U) { + LogError(LOG_NET, "malformed P25 packet, len < 24, shouldn't happen"); + return false; + } + + // frame length should never be 0 + if (frameLength == 0U) { + LogError(LOG_NET, "DUID $%02X, sent with frame length of 0?", duid); + return false; + } + + // frame length should never be larger then the network packet length + if (frameLength > len) { + LogError(LOG_NET, "malformed P25 packet, frameLength > len (%u > %u), shouldn't happen", frameLength, len); + return false; + } + + // validate frame length, because P25 has variable network frame lengths we should be validating + // the actual frame length to ensure we don't have buffer overflow vulnerabilities + switch (duid) { + case DUID::HDU: + // HDU's aren't actually ever sent over the network, they are packaged with the first LDU1 for the + // initating superframe + return false; + case DUID::TDU: + // TDUs are sent with the P25 message header only + if (frameLength != 24U) { + LogError(LOG_NET, P25_TDU_STR ", malformed TDU, discard."); + break; + } + break; + + case DUID::LDU1: + // LDU1 with message header only, this shouldn't happen + if (frameLength <= 24U) { + LogError(LOG_NET, P25_LDU1_STR ", malformed LDU1, discard."); + break; + } + break; + case DUID::VSELP1: + // VSELP1 frames aren't actually sent over the network right now + return false; + + case DUID::TSDU: + // oversized TSDU -- this shouldn't happen, truncate and only handle the size of the TSDU frame length + if (frameLength > P25_TSDU_FRAME_LENGTH_BYTES) + frameLength = P25_TSDU_FRAME_LENGTH_BYTES; + + // TSDU with message header only, this shouldn't happen + if (frameLength <= 24U) { + LogError(LOG_NET, P25_TSDU_STR ", malformed TSDU, discard."); + break; + } + break; + + case DUID::LDU2: + // LDU2 with message header only, this shouldn't happen + if (frameLength <= 24U) { + LogError(LOG_NET, P25_LDU2_STR ", malformed LDU2, discard."); + break; + } + break; + case DUID::VSELP2: + // VSELP2 frames aren't actually sent over the network right now + return false; + + case DUID::PDU: + // PDU with message header only, this shouldn't happen + if (frameLength <= 24U) { + LogError(LOG_NET, P25_PDU_STR ", malformed PDU, discard."); + break; + } + break; + + + case DUID::TDULC: + // oversized TDULC -- this shouldn't happen, truncate and only handle the size of the TSDU frame length + if (frameLength > P25_TDULC_FRAME_LENGTH_BYTES) + frameLength = P25_TDULC_FRAME_LENGTH_BYTES; + + // TDULC with message header only, this shouldn't happen + if (frameLength <= 24U) { + LogError(LOG_NET, P25_TSDU_STR ", malformed TDULC, discard."); + break; + } + break; + + default: + LogError(LOG_NET, "unsupported DUID $%02X", duid); + return false; + } + + return true; +} + /* Reads NXDN raw frame data from the NXDN ring buffer. */ UInt8Array BaseNetwork::readNXDN(bool& ret, uint32_t& frameLength) @@ -725,6 +847,91 @@ bool BaseNetwork::hasNXDNData() const return true; } +/* Reads analog raw frame data from the analog ring buffer. */ + +UInt8Array BaseNetwork::readAnalog(bool& ret, uint32_t& frameLength) +{ + if (m_status != NET_STAT_RUNNING && m_status != NET_STAT_MST_RUNNING) + return nullptr; + + ret = true; + if (m_rxAnalogData.isEmpty()) { + ret = false; + return nullptr; + } + + uint8_t length = 0U; + m_rxAnalogData.get(&length, 1U); + if (length == 0U) { + ret = false; + return nullptr; + } + + 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); + 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(new uint8_t[length]); + ::memset(buffer.get(), 0x00U, length); + m_rxAnalogData.get(buffer.get(), length); + + return buffer; +} + +/* Writes analog frame data to the network. */ + +bool BaseNetwork::writeAnalog(const analog::data::NetData& data, bool noSequence) +{ + using namespace analog::defines; + if (m_status != NET_STAT_RUNNING && m_status != NET_STAT_MST_RUNNING) + return false; + + AudioFrameType::E frameType = data.getFrameType(); + + bool resetSeq = false; + if (m_analogStreamId == 0U) { + resetSeq = true; + m_analogStreamId = createStreamId(); + } + + uint32_t messageLength = 0U; + UInt8Array message = createAnalog_Message(messageLength, m_analogStreamId, data); + if (message == nullptr) { + return false; + } + + uint16_t seq = pktSeq(resetSeq); + if (frameType == AudioFrameType::TERMINATOR) { + seq = RTP_END_OF_CALL_SEQ; + } + + if (noSequence) { + seq = RTP_END_OF_CALL_SEQ; + } + + return writeMaster({ NET_FUNC::PROTOCOL, NET_SUBFUNC::PROTOCOL_SUBFUNC_ANALOG }, message.get(), messageLength, seq, m_analogStreamId); +} + +/* Helper to test if the analog ring buffer has data. */ + +bool BaseNetwork::hasAnalogData() const +{ + if (m_rxAnalogData.isEmpty()) + return false; + + return true; +} + // --------------------------------------------------------------------------- // Protected Class Members // --------------------------------------------------------------------------- @@ -802,7 +1009,7 @@ UInt8Array BaseNetwork::createDMR_Message(uint32_t& length, const uint32_t strea data.getData(buffer + 20U); if (m_debug) - Utils::dump(1U, "Network Message, DMR", buffer, (DMR_PACKET_LENGTH + PACKET_PAD)); + Utils::dump(1U, "BaseNetwork::createDMR_Message(), Message", buffer, (DMR_PACKET_LENGTH + PACKET_PAD)); length = (DMR_PACKET_LENGTH + PACKET_PAD); return UInt8Array(buffer); @@ -811,7 +1018,7 @@ UInt8Array BaseNetwork::createDMR_Message(uint32_t& length, const uint32_t strea /* Creates an P25 frame message header. */ void BaseNetwork::createP25_MessageHdr(uint8_t* buffer, p25::defines::DUID::E duid, const p25::lc::LC& control, const p25::data::LowSpeedData& lsd, - p25::defines::FrameType::E frameType) + p25::defines::FrameType::E frameType, uint8_t controlByte) { using namespace p25::defines; assert(buffer != nullptr); @@ -830,7 +1037,7 @@ void BaseNetwork::createP25_MessageHdr(uint8_t* buffer, p25::defines::DUID::E du uint16_t sysId = control.getSiteData().sysId(); // System ID SET_UINT16(sysId, buffer, 11U); - buffer[14U] = 0U; // Control Bits + buffer[14U] = controlByte; // Control Bits buffer[15U] = control.getMFId(); // MFId @@ -859,7 +1066,7 @@ void BaseNetwork::createP25_MessageHdr(uint8_t* buffer, p25::defines::DUID::E du control.getMI(mi); if (m_debug) { - Utils::dump(1U, "P25 HDU MI written to network", mi, MI_LENGTH_BYTES); + Utils::dump(1U, "BaseNetwork::createP25_Message(), HDU MI", mi, MI_LENGTH_BYTES); } for (uint8_t i = 0; i < MI_LENGTH_BYTES; i++) { @@ -871,7 +1078,7 @@ void BaseNetwork::createP25_MessageHdr(uint8_t* buffer, p25::defines::DUID::E du /* Creates an P25 LDU1 frame message. */ UInt8Array BaseNetwork::createP25_LDU1Message(uint32_t& length, const p25::lc::LC& control, const p25::data::LowSpeedData& lsd, - const uint8_t* data, p25::defines::FrameType::E frameType) + const uint8_t* data, p25::defines::FrameType::E frameType, uint8_t controlByte) { using namespace p25::defines; using namespace p25::dfsi::defines; @@ -883,7 +1090,7 @@ UInt8Array BaseNetwork::createP25_LDU1Message(uint32_t& length, const p25::lc::L ::memset(buffer, 0x00U, P25_LDU1_PACKET_LENGTH + PACKET_PAD); // construct P25 message header - createP25_MessageHdr(buffer, DUID::LDU1, control, lsd, frameType); + createP25_MessageHdr(buffer, DUID::LDU1, control, lsd, frameType, controlByte); // pack DFSI data uint32_t count = MSG_HDR_SIZE; @@ -937,7 +1144,7 @@ UInt8Array BaseNetwork::createP25_LDU1Message(uint32_t& length, const p25::lc::L buffer[23U] = count; if (m_debug) - Utils::dump(1U, "Network Message, P25 LDU1", buffer, (P25_LDU1_PACKET_LENGTH + PACKET_PAD)); + Utils::dump(1U, "BaseNetwork::createP25_LDU1Message(), Message, LDU1", buffer, (P25_LDU1_PACKET_LENGTH + PACKET_PAD)); length = (P25_LDU1_PACKET_LENGTH + PACKET_PAD); return UInt8Array(buffer); @@ -946,7 +1153,7 @@ UInt8Array BaseNetwork::createP25_LDU1Message(uint32_t& length, const p25::lc::L /* Creates an P25 LDU2 frame message. */ UInt8Array BaseNetwork::createP25_LDU2Message(uint32_t& length, const p25::lc::LC& control, const p25::data::LowSpeedData& lsd, - const uint8_t* data) + const uint8_t* data, uint8_t controlByte) { using namespace p25::defines; using namespace p25::dfsi::defines; @@ -958,7 +1165,7 @@ UInt8Array BaseNetwork::createP25_LDU2Message(uint32_t& length, const p25::lc::L ::memset(buffer, 0x00U, P25_LDU2_PACKET_LENGTH + PACKET_PAD); // construct P25 message header - createP25_MessageHdr(buffer, DUID::LDU2, control, lsd, FrameType::DATA_UNIT); + createP25_MessageHdr(buffer, DUID::LDU2, control, lsd, FrameType::DATA_UNIT, controlByte); // pack DFSI data uint32_t count = MSG_HDR_SIZE; @@ -1012,7 +1219,7 @@ UInt8Array BaseNetwork::createP25_LDU2Message(uint32_t& length, const p25::lc::L buffer[23U] = count; if (m_debug) - Utils::dump(1U, "Network Message, P25 LDU2", buffer, (P25_LDU2_PACKET_LENGTH + PACKET_PAD)); + Utils::dump(1U, "BaseNetwork::createP25_LDU2Message(), Message, LDU2", buffer, (P25_LDU2_PACKET_LENGTH + PACKET_PAD)); length = (P25_LDU2_PACKET_LENGTH + PACKET_PAD); return UInt8Array(buffer); @@ -1033,7 +1240,7 @@ UInt8Array BaseNetwork::createP25_TDUMessage(uint32_t& length, const p25::lc::LC buffer[23U] = MSG_HDR_SIZE; if (m_debug) - Utils::dump(1U, "Network Message, P25 TDU", buffer, (MSG_HDR_SIZE + PACKET_PAD)); + Utils::dump(1U, "BaseNetwork::createP25_TDUMessage(), Message, TDU", buffer, (MSG_HDR_SIZE + PACKET_PAD)); length = (MSG_HDR_SIZE + PACKET_PAD); return UInt8Array(buffer); @@ -1054,15 +1261,12 @@ UInt8Array BaseNetwork::createP25_TSDUMessage(uint32_t& length, const p25::lc::L createP25_MessageHdr(buffer, DUID::TSDU, control, lsd, FrameType::TERMINATOR); // pack raw P25 TSDU bytes - uint32_t count = MSG_HDR_SIZE; - ::memcpy(buffer + 24U, data, P25_TSDU_FRAME_LENGTH_BYTES); - count += P25_TSDU_FRAME_LENGTH_BYTES; - buffer[23U] = count; + buffer[23U] = P25_TSDU_FRAME_LENGTH_BYTES; if (m_debug) - Utils::dump(1U, "Network Message, P25 TDSU", buffer, (P25_TSDU_PACKET_LENGTH + PACKET_PAD)); + Utils::dump(1U, "BaseNetwork::createP25_TSDUMessage(), Message, TDSU", buffer, (P25_TSDU_PACKET_LENGTH + PACKET_PAD)); length = (P25_TSDU_PACKET_LENGTH + PACKET_PAD); return UInt8Array(buffer); @@ -1083,15 +1287,12 @@ UInt8Array BaseNetwork::createP25_TDULCMessage(uint32_t& length, const p25::lc:: createP25_MessageHdr(buffer, DUID::TDULC, control, lsd, FrameType::TERMINATOR); // pack raw P25 TSDU bytes - uint32_t count = MSG_HDR_SIZE; - ::memcpy(buffer + 24U, data, P25_TDULC_FRAME_LENGTH_BYTES); - count += P25_TDULC_FRAME_LENGTH_BYTES; - buffer[23U] = count; + buffer[23U] = P25_TDULC_FRAME_LENGTH_BYTES; if (m_debug) - Utils::dump(1U, "Network Message, P25 TDULC", buffer, (P25_TDULC_PACKET_LENGTH + PACKET_PAD)); + Utils::dump(1U, "BaseNetwork::createP25_TDULCMessage(), Message, TDULC", buffer, (P25_TDULC_PACKET_LENGTH + PACKET_PAD)); length = (P25_TDULC_PACKET_LENGTH + PACKET_PAD); return UInt8Array(buffer); @@ -1139,7 +1340,7 @@ UInt8Array BaseNetwork::createP25_PDUMessage(uint32_t& length, const p25::data:: buffer[23U] = count; if (m_debug) - Utils::dump(1U, "Network Message, P25 PDU", buffer, (count + PACKET_PAD)); + Utils::dump(1U, "BaseNetwork::createP25_PDUMessage(), Message, PDU", buffer, (count + PACKET_PAD)); length = (count + PACKET_PAD); return UInt8Array(buffer); @@ -1151,8 +1352,8 @@ UInt8Array BaseNetwork::createNXDN_Message(uint32_t& length, const nxdn::lc::RTC { assert(data != nullptr); - uint8_t* buffer = new uint8_t[DATA_PACKET_LENGTH]; - ::memset(buffer, 0x00U, DATA_PACKET_LENGTH); + uint8_t* buffer = new uint8_t[NXDN_PACKET_LENGTH + PACKET_PAD]; + ::memset(buffer, 0x00U, NXDN_PACKET_LENGTH + PACKET_PAD); // construct NXDN message header ::memcpy(buffer + 0U, TAG_NXDN_DATA, 4U); @@ -1178,8 +1379,43 @@ UInt8Array BaseNetwork::createNXDN_Message(uint32_t& length, const nxdn::lc::RTC buffer[23U] = count; if (m_debug) - Utils::dump(1U, "Network Message, NXDN", buffer, (count + PACKET_PAD)); + Utils::dump(1U, "BaseNetwork::createNXDN_Message(), Message", buffer, (NXDN_PACKET_LENGTH + PACKET_PAD)); - length = (count + PACKET_PAD); + length = (NXDN_PACKET_LENGTH + PACKET_PAD); + return UInt8Array(buffer); +} + +/* Creates an analog frame message. */ + +UInt8Array BaseNetwork::createAnalog_Message(uint32_t& length, const uint32_t streamId, const analog::data::NetData& data) +{ + using namespace analog::defines; + uint8_t* buffer = new uint8_t[ANALOG_PACKET_LENGTH + PACKET_PAD]; + ::memset(buffer, 0x00U, ANALOG_PACKET_LENGTH + PACKET_PAD); + + // construct analog message header + ::memcpy(buffer + 0U, TAG_ANALOG_DATA, 4U); + + uint32_t srcId = data.getSrcId(); // Source Address + SET_UINT24(srcId, buffer, 5U); + + uint32_t dstId = data.getDstId(); // Target Address + SET_UINT24(dstId, buffer, 8U); + + buffer[14U] = data.getControl(); // Control Bits + + AudioFrameType::E frameType = data.getFrameType(); + buffer[15U] = (uint8_t)frameType; // Audio Frame Type + buffer[15U] |= data.getGroup() ? 0x00U : 0x40U; // Group + + buffer[4U] = data.getSeqNo(); // Sequence Number + + // pack raw audio message bytes + data.getAudio(buffer + 20U); + + if (m_debug) + Utils::dump(1U, "BaseNetwork::createAnalog_Message(), Message", buffer, (ANALOG_PACKET_LENGTH + PACKET_PAD)); + + length = (ANALOG_PACKET_LENGTH + PACKET_PAD); return UInt8Array(buffer); } diff --git a/src/common/network/BaseNetwork.h b/src/common/network/BaseNetwork.h index ccb2092b..79cdb8f3 100644 --- a/src/common/network/BaseNetwork.h +++ b/src/common/network/BaseNetwork.h @@ -25,6 +25,7 @@ #define __BASE_NETWORK_H__ #include "common/Defines.h" +#include "common/analog/data/NetData.h" #include "common/dmr/data/NetData.h" #include "common/p25/data/DataHeader.h" #include "common/p25/data/LowSpeedData.h" @@ -52,6 +53,7 @@ #define TAG_DMR_DATA "DMRD" #define TAG_P25_DATA "P25D" #define TAG_NXDN_DATA "NXDD" +#define TAG_ANALOG_DATA "ANOD" #define TAG_REPEATER_LOGIN "RPTL" #define TAG_REPEATER_AUTH "RPTK" @@ -88,6 +90,8 @@ 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 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 /** * @brief Network Peer Connection Status @@ -131,6 +135,19 @@ namespace network NET_CONN_NAK_INVALID = 0xFFFF //! Invalid }; + /** + * @brief Network Control Enumerations + * @note These values are used typically for terminators to specify specific DVM in-band control operations. + * @ingroup network_core + */ + enum CONTROL_BYTE { + NET_CTRL_GRANT_DEMAND = 0x80U, //! Grant Demand + NET_CTRL_GRANT_DENIAL = 0x40U, //! Grant Denial + NET_CTRL_SWITCH_OVER = 0x20U, //! Call Source RID Switch Over + NET_CTRL_GRANT_ENCRYPT = 0x08U, //! Grant Encrypt + NET_CTRL_U2U = 0x01U, //! Unit-to-Unit + }; + // --------------------------------------------------------------------------- // Class Declaration // --------------------------------------------------------------------------- @@ -297,6 +314,10 @@ namespace network * @brief Resets the NXDN ring buffer. */ virtual void resetNXDN(); + /** + * @brief Resets the analog ring buffer. + */ + virtual void resetAnalog(); /** * @brief Gets the current DMR stream ID. @@ -314,6 +335,11 @@ namespace network * @return uint32_t Stream ID. */ uint32_t getNXDNStreamId() const { return m_nxdnStreamId; } + /** + * @brief Gets the current analog stream ID. + * @return uint32_t Stream ID. + */ + uint32_t getAnalogStreamId() const { return m_analogStreamId; } /** * @brief Helper to send a data message to the master. @@ -325,10 +351,12 @@ namespace network * @param queueOnly Flag indicating this message should be queued instead of send immediately. * @param useAlternatePort Flag indicating the message shuold be sent using the alternate port (mainly for activity and diagnostics). * @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 queueOnly = false, bool useAlternatePort = false, uint32_t peerId = 0U); + uint16_t pktSeq, uint32_t streamId, bool queueOnly = false, bool useAlternatePort = false, uint32_t peerId = 0U, + uint32_t ssrc = 0U); // Digital Mobile Radio /** @@ -366,18 +394,21 @@ namespace network * @param[in] lsd Instance of p25::data::LowSpeedData containing low speed data. * @param[in] data Buffer containing P25 LDU1 data to send. * @param[in] frameType DVM P25 frame type. + * @param[in] controlByte DVM Network Control Byte. * @returns bool True, if message was sent, otherwise false. */ virtual bool writeP25LDU1(const p25::lc::LC& control, const p25::data::LowSpeedData& lsd, const uint8_t* data, - p25::defines::FrameType::E frameType); + P25DEF::FrameType::E frameType, uint8_t controlByte = 0U); /** * @brief Writes P25 LDU2 frame data to the network. * @param[in] control Instance of p25::lc::LC containing link control data. * @param[in] lsd Instance of p25::data::LowSpeedData containing low speed data. * @param[in] data Buffer containing P25 LDU2 data to send. + * @param[in] controlByte DVM Network Control Byte. * @returns bool True, if message was sent, otherwise false. */ - virtual bool writeP25LDU2(const p25::lc::LC& control, const p25::data::LowSpeedData& lsd, const uint8_t* data); + virtual bool writeP25LDU2(const p25::lc::LC& control, const p25::data::LowSpeedData& lsd, const uint8_t* data, + uint8_t controlByte = 0U); /** * @brief Writes P25 TDU frame data to the network. * @param[in] control Instance of p25::lc::LC containing link control data. @@ -418,6 +449,14 @@ namespace network */ bool hasP25Data() const; + /** + * @brief Helper to validate a P25 network frame length. + * @param frameLength P25 encapsulated frame length. + * @param len Network packet length. + * @return bool True, if validated, otherwise false. + */ + bool validateP25FrameLength(uint8_t& frameLength, const uint32_t len, const P25DEF::DUID::E duid); + // Next Generation Digital Narrowband /** * @brief Reads NXDN raw frame data from the NXDN ring buffer. @@ -442,6 +481,28 @@ namespace network */ bool hasNXDNData() const; + // Analog Audio + /** + * @brief Reads analog MuLaw audio frame data from the analog 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 readAnalog(bool& ret, uint32_t& frameLength); + /** + * @brief Writes analog MuLaw audio frame data to the network. + * @param[in] data Instance of the analog::data::NetData class containing the analog message. + * @param noSequence Flag indicating the message should be sent with no RTP sequence (65535). + * @returns bool True, if message was sent, otherwise false. + */ + virtual bool writeAnalog(const analog::data::NetData& data, bool noSequence = false); + + /** + * @brief Helper to test if the analog ring buffer has data. + * @returns bool True, if the network analog ring buffer has data, otherwise false. + */ + bool hasAnalogData() const; + public: /** * @brief Gets the peer ID of the network. @@ -489,12 +550,14 @@ namespace network RingBuffer m_rxDMRData; RingBuffer m_rxP25Data; RingBuffer m_rxNXDNData; + RingBuffer m_rxAnalogData; std::mt19937 m_random; uint32_t* m_dmrStreamId; uint32_t m_p25StreamId; uint32_t m_nxdnStreamId; + uint32_t m_analogStreamId; /** * @brief Helper to update the RTP packet sequence. @@ -529,7 +592,7 @@ namespace network * | Reserved | * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ * - * The data starting at offset 20 for 33 bytes if the raw DMR frame. + * The data starting at offset 20 for 33 bytes of the raw DMR frame. * * DMR frame message has 2 trailing bytes: * @@ -591,9 +654,10 @@ namespace network * @param[in] control Instance of p25::lc::LC containing link control data. * @param[in] lsd Instance of p25::data::LowSpeedData containing low speed data. * @param[in] frameType DVM P25 frame type. + * @param[in] controlByte DVM Network Control Byte. */ void createP25_MessageHdr(uint8_t* buffer, p25::defines::DUID::E duid, const p25::lc::LC& control, const p25::data::LowSpeedData& lsd, - p25::defines::FrameType::E frameType = p25::defines::FrameType::DATA_UNIT); + p25::defines::FrameType::E frameType = p25::defines::FrameType::DATA_UNIT, uint8_t controlByte = 0U); /** * @brief Creates an P25 LDU1 frame message. @@ -606,10 +670,11 @@ namespace network * @param[in] lsd Instance of p25::data::LowSpeedData containing low speed data. * @param[in] data Buffer containing P25 LDU1 data to send. * @param[in] frameType DVM P25 frame type. + * @param[in] controlByte DVM Network Control Byte. * @returns UInt8Array Buffer containing the built network message. */ UInt8Array createP25_LDU1Message(uint32_t& length, const p25::lc::LC& control, const p25::data::LowSpeedData& lsd, - const uint8_t* data, p25::defines::FrameType::E frameType); + const uint8_t* data, p25::defines::FrameType::E frameType, uint8_t controlByte = 0U); /** * @brief Creates an P25 LDU2 frame message. * @@ -620,10 +685,11 @@ namespace network * @param[in] control Instance of p25::lc::LC containing link control data. * @param[in] lsd Instance of p25::data::LowSpeedData containing low speed data. * @param[in] data Buffer containing P25 LDU2 data to send. + * @param[in] controlByte DVM Network Control Byte. * @returns UInt8Array Buffer containing the built network message. */ UInt8Array createP25_LDU2Message(uint32_t& length, const p25::lc::LC& control, const p25::data::LowSpeedData& lsd, - const uint8_t* data); + const uint8_t* data, uint8_t controlByte = 0U); /** * @brief Creates an P25 TDU frame message. @@ -700,7 +766,7 @@ 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 NXDN frame message. * \code{.unparsed} @@ -732,7 +798,36 @@ namespace network * @returns UInt8Array Buffer containing the built network message. */ UInt8Array createNXDN_Message(uint32_t& length, const nxdn::lc::RTCH& lc, const uint8_t* data, const uint32_t len); - + + /** + * @brief Creates an analog frame message. + * \code{.unparsed} + * Below is the representation of the data layout for the analog frame + * message header. The header is 20 bytes in length. + * + * Byte 0 1 2 3 + * Bit 7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0 + * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + * | Protocol Tag (ANOD) | + * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + * | Seq No. | Source ID | + * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + * | Destination ID | Reserved | + * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + * | Reserved | Control Flags | R | Data Type | + * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + * | Reserved | + * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + * + * The data starting at offset 20 for 320 bytes of the raw analog frame. + * \endcode + * @param[out] length Length of network message buffer. + * @param streamId Stream ID. + * @param data Instance of the analog::data::Data class containing the analog message. + * @returns UInt8Array Buffer containing the built network message. + */ + UInt8Array createAnalog_Message(uint32_t& length, const uint32_t streamId, const analog::data::NetData& data); + private: uint16_t m_pktSeq; diff --git a/src/common/network/FrameQueue.cpp b/src/common/network/FrameQueue.cpp index 1d5b0422..0ca7eeab 100644 --- a/src/common/network/FrameQueue.cpp +++ b/src/common/network/FrameQueue.cpp @@ -24,6 +24,12 @@ using namespace network::frame; #include #include +// --------------------------------------------------------------------------- +// Static Class Members +// --------------------------------------------------------------------------- + +std::vector FrameQueue::m_streamTimestamps; + // --------------------------------------------------------------------------- // Public Class Members // --------------------------------------------------------------------------- @@ -32,10 +38,7 @@ using namespace network::frame; FrameQueue::FrameQueue(udp::Socket* socket, uint32_t peerId, bool debug) : RawFrameQueue(socket, debug), m_peerId(peerId), -#if defined(_WIN32) - m_streamTSMtx(), -#endif // defined(_WIN32) - m_streamTimestamps() + m_timestampMtx() { assert(peerId < 999999999U); } @@ -67,7 +70,7 @@ UInt8Array FrameQueue::read(int& messageLength, sockaddr_storage& address, uint3 if (length > 0) { if (m_debug) - Utils::dump(1U, "Network Packet", buffer, length); + Utils::dump(1U, "FrameQueue::read(), Network Packet", buffer, length); m_failedReadCnt = 0U; @@ -211,6 +214,7 @@ void FrameQueue::enqueueMessage(const uint8_t* message, uint32_t length, uint32_ void FrameQueue::clearTimestamps() { + std::lock_guard lock(m_timestampMtx); m_streamTimestamps.clear(); } @@ -218,6 +222,63 @@ void FrameQueue::clearTimestamps() // Private Class Members // --------------------------------------------------------------------------- +/* Search for a timestamp entry by stream ID. */ + +FrameQueue::Timestamp* FrameQueue::findTimestamp(uint32_t streamId) +{ + std::lock_guard lock(m_timestampMtx); + for (size_t i = 0; i < m_streamTimestamps.size(); i++) { + if (m_streamTimestamps[i].streamId == streamId) + return &m_streamTimestamps[i]; + } + + return nullptr; +} + +/* Insert a timestamp for a stream ID. */ + +void FrameQueue::insertTimestamp(uint32_t streamId, uint32_t timestamp) +{ + std::lock_guard lock(m_timestampMtx); + if (streamId == 0U || timestamp == INVALID_TS) { + LogError(LOG_NET, "FrameQueue::insertTimestamp(), 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 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; + } + } +} + +/* Erase a timestamp for a stream ID. */ + +void FrameQueue::eraseTimestamp(uint32_t streamId) +{ + std::lock_guard 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()); +} + /* Generate RTP message for the frame queue. */ uint8_t* FrameQueue::generateMessage(const uint8_t* message, uint32_t length, uint32_t streamId, uint32_t peerId, @@ -234,25 +295,17 @@ uint8_t* FrameQueue::generateMessage(const uint8_t* message, uint32_t length, ui uint32_t timestamp = INVALID_TS; if (streamId != 0U) { -#if defined(_WIN32) - std::lock_guard lock(m_streamTSMtx); -#else - m_streamTimestamps.lock(false); -#endif // defined(_WIN32) - auto entry = m_streamTimestamps.find(streamId); - if (entry != m_streamTimestamps.end()) { - timestamp = entry->second; + auto entry = findTimestamp(streamId); + if (entry != nullptr) { + timestamp = entry->timestamp; } if (timestamp != INVALID_TS) { 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, m_streamTimestamps[streamId], timestamp, rtpSeq); - m_streamTimestamps[streamId] = timestamp; + updateTimestamp(streamId, timestamp); } -#if !defined(_WIN32) - m_streamTimestamps.unlock(); -#endif // defined(_WIN32) } uint32_t bufferLen = RTP_HEADER_LENGTH_BYTES + RTP_EXTENSION_HEADER_LENGTH_BYTES + RTP_FNE_HEADER_LENGTH_BYTES + length; @@ -274,34 +327,18 @@ uint8_t* FrameQueue::generateMessage(const uint8_t* message, uint32_t length, ui timestamp = (uint32_t)system_clock::ntp::now(); header.setTimestamp(timestamp); -#if defined(_WIN32) - std::lock_guard lock(m_streamTSMtx); - m_streamTimestamps.insert({ streamId, timestamp }); -#else - m_streamTimestamps.insert(streamId, timestamp); -#endif // defined(_WIN32) + insertTimestamp(streamId, timestamp); } header.encode(buffer); if (streamId != 0U && rtpSeq == RTP_END_OF_CALL_SEQ) { -#if defined(_WIN32) - std::lock_guard lock(m_streamTSMtx); -#else - m_streamTimestamps.lock(false); -#endif // defined(_WIN32) - auto entry = m_streamTimestamps.find(streamId); - if (entry != m_streamTimestamps.end()) { + auto entry = findTimestamp(streamId); + if (entry != nullptr) { if (m_debug) LogDebugEx(LOG_NET, "FrameQueue::generateMessage()", "RTP streamId = %u, rtpSeq = %u", streamId, rtpSeq); -#if !defined(_WIN32) - m_streamTimestamps.unlock(); -#endif // defined(_WIN32) - m_streamTimestamps.erase(streamId); + eraseTimestamp(streamId); } -#if !defined(_WIN32) - m_streamTimestamps.unlock(); -#endif // defined(_WIN32) } RTPFNEHeader fneHeader = RTPFNEHeader(); @@ -318,7 +355,7 @@ uint8_t* FrameQueue::generateMessage(const uint8_t* message, uint32_t length, ui ::memcpy(buffer + RTP_HEADER_LENGTH_BYTES + RTP_EXTENSION_HEADER_LENGTH_BYTES + RTP_FNE_HEADER_LENGTH_BYTES, message, length); if (m_debug) - Utils::dump(1U, "FrameQueue::generateMessage() Buffered Message", buffer, bufferLen); + Utils::dump(1U, "FrameQueue::generateMessage(), Buffered Message", buffer, bufferLen); if (outBufferLen != nullptr) { *outBufferLen = bufferLen; diff --git a/src/common/network/FrameQueue.h b/src/common/network/FrameQueue.h index 041863f1..b4411ee0 100644 --- a/src/common/network/FrameQueue.h +++ b/src/common/network/FrameQueue.h @@ -17,17 +17,21 @@ #define __FRAME_QUEUE_H__ #include "common/Defines.h" -#include "common/concurrent/unordered_map.h" #include "common/network/RTPHeader.h" #include "common/network/RTPFNEHeader.h" #include "common/network/RawFrameQueue.h" +#include +#include + namespace network { // --------------------------------------------------------------------------- // Constants // --------------------------------------------------------------------------- - + + const uint8_t RTP_G711_PAYLOAD_TYPE = 0x00U; + const uint8_t DVM_RTP_PAYLOAD_TYPE = 0x56U; // --------------------------------------------------------------------------- @@ -41,6 +45,11 @@ namespace network class HOST_SW_API FrameQueue : public RawFrameQueue { public: typedef std::pair OpcodePair; public: + typedef struct { + uint32_t streamId; + uint32_t timestamp; + } Timestamp; + auto operator=(FrameQueue&) -> FrameQueue& = delete; auto operator=(FrameQueue&&) -> FrameQueue& = delete; FrameQueue(FrameQueue&) = delete; @@ -115,12 +124,33 @@ namespace network private: uint32_t m_peerId; -#if defined(_WIN32) - std::mutex m_streamTSMtx; - std::unordered_map m_streamTimestamps; -#else - concurrent::unordered_map m_streamTimestamps; -#endif // defined(_WIN32) + std::mutex m_timestampMtx; + + static std::vector m_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. + */ + void insertTimestamp(uint32_t streamId, uint32_t timestamp); + /** + * @brief Update a timestamp for a stream ID. + * @param streamId Stream ID. + * @param timestamp Timestamp. + */ + void updateTimestamp(uint32_t streamId, uint32_t timestamp); + /** + * @brief Erase a timestamp for a stream ID. + * @param streamId Stream ID. + */ + void eraseTimestamp(uint32_t streamId); /** * @brief Generate RTP message for the frame queue. diff --git a/src/common/network/Network.cpp b/src/common/network/Network.cpp index 7581c239..fee31014 100644 --- a/src/common/network/Network.cpp +++ b/src/common/network/Network.cpp @@ -9,8 +9,6 @@ */ #include "Defines.h" #include "common/edac/SHA256.h" -#include "common/network/RTPHeader.h" -#include "common/network/RTPFNEHeader.h" #include "common/network/json/json.h" #include "common/p25/kmm/KMMFactory.h" #include "common/Log.h" @@ -27,6 +25,7 @@ using namespace network; // Constants // --------------------------------------------------------------------------- +#define MAX_RETRY_BEFORE_RECONNECT 4U #define MAX_SERVER_DIFF 360ULL // maximum difference in time between a server timestamp and local timestamp in milliseconds // --------------------------------------------------------------------------- @@ -36,7 +35,8 @@ using namespace network; /* Initializes a new instance of the Network class. */ Network::Network(const std::string& address, uint16_t port, uint16_t localPort, uint32_t peerId, const std::string& password, - bool duplex, bool debug, bool dmr, bool p25, bool nxdn, bool slot1, bool slot2, bool allowActivityTransfer, bool allowDiagnosticTransfer, bool updateLookup, bool saveLookup) : + bool duplex, bool debug, bool dmr, bool p25, bool nxdn, bool analog, bool slot1, bool slot2, + bool allowActivityTransfer, bool allowDiagnosticTransfer, bool updateLookup, bool saveLookup) : BaseNetwork(peerId, duplex, debug, slot1, slot2, allowActivityTransfer, allowDiagnosticTransfer, localPort), m_pktLastSeq(0U), m_address(address), @@ -46,12 +46,14 @@ Network::Network(const std::string& address, uint16_t port, uint16_t localPort, m_dmrEnabled(dmr), m_p25Enabled(p25), m_nxdnEnabled(nxdn), + m_analogEnabled(analog), m_updateLookup(updateLookup), m_saveLookup(saveLookup), m_ridLookup(nullptr), m_tidLookup(nullptr), m_salt(nullptr), m_retryTimer(1000U, 10U), + m_retryCount(0U), m_timeoutTimer(1000U, MAX_PEER_PING_TIME), m_pktSeq(0U), m_loginStreamId(0U), @@ -65,6 +67,7 @@ Network::Network(const std::string& address, uint16_t port, uint16_t localPort, m_dmrInCallCallback(nullptr), m_p25InCallCallback(nullptr), m_nxdnInCallCallback(nullptr), + m_analogInCallCallback(nullptr), m_keyRespCallback(nullptr) { assert(!address.empty()); @@ -78,6 +81,7 @@ Network::Network(const std::string& address, uint16_t port, uint16_t localPort, m_rxDMRStreamId[1U] = 0U; m_rxP25StreamId = 0U; m_rxNXDNStreamId = 0U; + m_rxAnalogStreamId = 0U; m_metadata = new PeerMetadata(); } @@ -122,6 +126,14 @@ void Network::resetNXDN() m_rxNXDNStreamId = 0U; } +/* Resets the analog ring buffer. */ + +void Network::resetAnalog() +{ + BaseNetwork::resetAnalog(); + m_rxAnalogStreamId = 0U; +} + /* Sets the instances of the Radio ID and Talkgroup ID lookup tables. */ void Network::setLookups(lookups::RadioIdLookup* ridLookup, lookups::TalkgroupRulesLookup* tidLookup) @@ -175,11 +187,23 @@ void Network::clock(uint32_t ms) m_retryTimer.clock(ms); if (m_retryTimer.isRunning() && m_retryTimer.hasExpired()) { if (m_enabled) { + if (m_retryCount > MAX_RETRY_BEFORE_RECONNECT) { + m_retryCount = 0U; + LogError(LOG_NET, "PEER %u connection to the master has timed out, retrying connection, remotePeerId = %u", m_peerId, m_remotePeerId); + + close(); + open(); + + m_retryTimer.start(); + return; + } + bool ret = m_socket->open(m_addr.ss_family); if (ret) { ret = writeLogin(); if (!ret) { m_retryTimer.start(); + m_retryCount++; return; } @@ -189,6 +213,7 @@ void Network::clock(uint32_t ms) } m_retryTimer.start(); + m_retryCount++; } return; @@ -236,16 +261,10 @@ void Network::clock(uint32_t ms) } if (m_debug) { - LogDebugEx(LOG_NET, "Network::clock()", "RTP, peerId = %u, seq = %u, streamId = %u, func = %02X, subFunc = %02X", fneHeader.getPeerId(), rtpHeader.getSequence(), + LogDebugEx(LOG_NET, "Network::clock()", "RTP, peerId = %u, ssrc = %u, seq = %u, streamId = %u, func = %02X, subFunc = %02X", fneHeader.getPeerId(), rtpHeader.getSSRC(), rtpHeader.getSequence(), fneHeader.getStreamId(), fneHeader.getFunction(), fneHeader.getSubFunction()); } - // ensure the RTP synchronization source ID matches the FNE peer ID - if (m_remotePeerId != 0U && rtpHeader.getSSRC() != m_remotePeerId) { - LogWarning(LOG_NET, "RTP header and traffic session do not agree on remote peer ID? %u != %u", rtpHeader.getSSRC(), m_remotePeerId); - // should this be a fatal error? - } - // is this RTP packet destined for us? uint32_t peerId = fneHeader.getPeerId(); if ((m_peerId != peerId) && !m_promiscuousPeer) { @@ -273,7 +292,7 @@ void Network::clock(uint32_t ms) // are protocol messages being user handled? if (m_userHandleProtocol) { userPacketHandler(fneHeader.getPeerId(), { fneHeader.getFunction(), fneHeader.getSubFunction() }, - buffer.get(), length, fneHeader.getStreamId()); + buffer.get(), length, fneHeader.getStreamId(), fneHeader, rtpHeader); break; } @@ -322,8 +341,8 @@ void Network::clock(uint32_t ms) } if (m_debug) - Utils::dump(1U, "[Network::clock()] Network Received, DMR", buffer.get(), length); - if (length > 255) + 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); uint8_t len = length; @@ -374,12 +393,21 @@ void Network::clock(uint32_t ms) } if (m_debug) - Utils::dump(1U, "[Network::clock()] Network Received, P25", buffer.get(), length); - if (length > 255) + 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); + // P25 frames can be up to 512 bytes, but we need to handle the case where the frame is larger than 255 bytes uint8_t len = length; - m_rxP25Data.addData(&len, 1U); + if (length > 254) { + len = 254U; + m_rxP25Data.addData(&len, 1U); + len = length - 254U; + m_rxP25Data.addData(&len, 1U); + } else { + m_rxP25Data.addData(&len, 1U); + } + m_rxP25Data.addData(buffer.get(), len); } } @@ -426,8 +454,8 @@ void Network::clock(uint32_t ms) } if (m_debug) - Utils::dump(1U, "[Network::clock()] Network Received, NXDN", buffer.get(), length); - if (length > 255) + 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); uint8_t len = length; @@ -437,6 +465,66 @@ void Network::clock(uint32_t ms) } break; + case NET_SUBFUNC::PROTOCOL_SUBFUNC_ANALOG: // Encapsulated Analog data frame + { + if (m_enabled && m_analogEnabled) { + if (m_debug) { + LogDebug(LOG_NET, "Analog, peer = %u, len = %u, pktSeq = %u, streamId = %u", + peerId, length, rtpHeader.getSequence(), streamId); + } + + if (m_promiscuousPeer) { + m_rxAnalogStreamId = streamId; + m_pktLastSeq = m_pktSeq; + } + else { + if (m_rxAnalogStreamId == 0U) { + if (rtpHeader.getSequence() == RTP_END_OF_CALL_SEQ) { + m_rxAnalogStreamId = 0U; + } + else { + m_rxAnalogStreamId = streamId; + } + + m_pktLastSeq = m_pktSeq; + } + else { + if (m_rxAnalogStreamId == streamId) { + if (m_pktSeq != 0U && m_pktLastSeq != 0U) { + if (m_pktSeq >= 1U && ((m_pktSeq != m_pktLastSeq + 1) && (m_pktSeq - 1 != m_pktLastSeq + 1))) { + LogWarning(LOG_NET, "Analog Stream %u out-of-sequence; %u != %u", streamId, m_pktSeq, m_pktLastSeq + 1); + } + } + + m_pktLastSeq = m_pktSeq; + } + + if (rtpHeader.getSequence() == RTP_END_OF_CALL_SEQ) { + m_rxAnalogStreamId = 0U; + } + } + } + + if (m_debug) + 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); + } else { + if (length > 512) + 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; + m_rxAnalogData.addData(&len, 1U); + + m_rxAnalogData.addData(buffer.get(), len); + } + } + } + break; + default: Utils::dump("unknown protocol opcode from the master", buffer.get(), length); break; @@ -452,7 +540,7 @@ void Network::clock(uint32_t ms) { if (m_enabled && m_updateLookup) { if (m_debug) - Utils::dump(1U, "Network Received, WL RID", buffer.get(), length); + Utils::dump(1U, "Network::clock(), Network Rx, WL RID", buffer.get(), length); if (m_ridLookup != nullptr) { // update RID lists @@ -478,7 +566,7 @@ void Network::clock(uint32_t ms) { if (m_enabled && m_updateLookup) { if (m_debug) - Utils::dump(1U, "Network Received, BL RID", buffer.get(), length); + Utils::dump(1U, "Network::clock(), Network Rx, BL RID", buffer.get(), length); if (m_ridLookup != nullptr) { // update RID lists @@ -505,7 +593,7 @@ void Network::clock(uint32_t ms) { if (m_enabled && m_updateLookup) { if (m_debug) - Utils::dump(1U, "Network Received, ACTIVE TGS", buffer.get(), length); + Utils::dump(1U, "Network::clock(), Network Rx, ACTIVE TGS", buffer.get(), length); if (m_tidLookup != nullptr) { // update TGID lists @@ -555,7 +643,7 @@ void Network::clock(uint32_t ms) { if (m_enabled && m_updateLookup) { if (m_debug) - Utils::dump(1U, "Network Received, DEACTIVE TGS", buffer.get(), length); + Utils::dump(1U, "Network::clock(), Network Rx, DEACTIVE TGS", buffer.get(), length); if (m_tidLookup != nullptr) { // update TGID lists @@ -636,6 +724,19 @@ void Network::clock(uint32_t ms) } } break; + case NET_SUBFUNC::PROTOCOL_SUBFUNC_ANALOG: // Analog In-Call Control + { + if (m_enabled && m_nxdnEnabled) { + NET_ICC::ENUM command = (NET_ICC::ENUM)buffer[10U]; + uint32_t dstId = GET_UINT24(buffer, 11U); + + // fire off analog in-call callback if we have one + if (m_analogInCallCallback != nullptr) { + m_analogInCallCallback(command, dstId); + } + } + } + break; default: Utils::dump("unknown incall control opcode from the master", buffer.get(), length); @@ -816,7 +917,7 @@ void Network::clock(uint32_t ms) m_timeoutTimer.start(); if (length >= 14) { if (m_debug) - Utils::dump(1U, "Network Received, PONG", buffer.get(), length); + Utils::dump(1U, "Network::clock(), Network Rx, PONG", buffer.get(), length); ulong64_t serverNow = 0U; @@ -838,7 +939,7 @@ void Network::clock(uint32_t ms) break; default: userPacketHandler(fneHeader.getPeerId(), { fneHeader.getFunction(), fneHeader.getSubFunction() }, - buffer.get(), length, fneHeader.getStreamId()); + buffer.get(), length, fneHeader.getStreamId(), fneHeader, rtpHeader); break; } } @@ -889,15 +990,16 @@ bool Network::open() if (m_debug) LogMessage(LOG_NET, "PEER %u opening network", m_peerId); + m_status = NET_STAT_WAITING_CONNECT; + m_timeoutTimer.start(); + m_retryTimer.start(); + m_retryCount = 0U; + if (udp::Socket::lookup(m_address, m_port, m_addr, m_addrLen) != 0) { LogMessage(LOG_NET, "!!! Could not lookup the address of the master!"); return false; } - m_status = NET_STAT_WAITING_CONNECT; - m_timeoutTimer.start(); - m_retryTimer.start(); - return true; } @@ -936,9 +1038,10 @@ void Network::enable(bool enabled) /* User overrideable handler that allows user code to process network packets not handled by this class. */ -void Network::userPacketHandler(uint32_t peerId, FrameQueue::OpcodePair opcode, const uint8_t* data, uint32_t length, uint32_t streamId) +void Network::userPacketHandler(uint32_t peerId, FrameQueue::OpcodePair opcode, const uint8_t* data, uint32_t length, uint32_t streamId, + const frame::RTPFNEHeader& fneHeader, const frame::RTPHeader& rtpHeader) { - Utils::dump("unknown opcode from the master", data, length); + Utils::dump("Unknown opcode from the master", data, length); } /* Writes login request to the network. */ @@ -954,7 +1057,7 @@ bool Network::writeLogin() SET_UINT32(m_peerId, buffer, 4U); // Peer ID if (m_debug) - Utils::dump(1U, "Network Message, Login", buffer, 8U); + Utils::dump(1U, "Network::writeLogin(), Message, Login", buffer, 8U); m_loginStreamId = createStreamId(); m_remotePeerId = 0U; @@ -987,7 +1090,7 @@ bool Network::writeAuthorisation() delete[] in; if (m_debug) - Utils::dump(1U, "Network Message, Authorisation", out, 40U); + Utils::dump(1U, "Network::writeAuthorisation(), Message, Authorisation", out, 40U); return writeMaster({ NET_FUNC::RPTK, NET_SUBFUNC::NOP }, out, 40U, pktSeq(), m_loginStreamId); } @@ -1048,7 +1151,7 @@ bool Network::writeConfig() ::snprintf(buffer + 8U, json.length() + 1U, "%s", json.c_str()); if (m_debug) { - Utils::dump(1U, "Network Message, Configuration", (uint8_t*)buffer, json.length() + 8U); + Utils::dump(1U, "Network::writeConfig(), Message, Configuration", (uint8_t*)buffer, json.length() + 8U); } return writeMaster({ NET_FUNC::RPTC, NET_SUBFUNC::NOP }, (uint8_t*)buffer, json.length() + 8U, RTP_END_OF_CALL_SEQ, m_loginStreamId); diff --git a/src/common/network/Network.h b/src/common/network/Network.h index 290b77e2..740bfbc0 100644 --- a/src/common/network/Network.h +++ b/src/common/network/Network.h @@ -18,6 +18,8 @@ #include "common/Defines.h" #include "common/network/BaseNetwork.h" +#include "common/network/RTPHeader.h" +#include "common/network/RTPFNEHeader.h" #include "common/lookups/RadioIdLookup.h" #include "common/lookups/TalkgroupRulesLookup.h" #include "common/p25/kmm/KeysetItem.h" @@ -90,6 +92,7 @@ namespace network * @param dmr Flag indicating whether DMR is enabled. * @param p25 Flag indicating whether P25 is enabled. * @param nxdn Flag indicating whether NXDN is enabled. + * @param analog Flag indicating whether analog is enabled. * @param slot1 Flag indicating whether DMR slot 1 is enabled for network traffic. * @param slot2 Flag indicating whether DMR slot 2 is enabled for network traffic. * @param allowActivityTransfer Flag indicating that the system activity logs will be sent to the network. @@ -97,7 +100,8 @@ namespace network * @param updateLookup Flag indicating that the system will accept radio ID and talkgroup ID lookups from the network. */ Network(const std::string& address, uint16_t port, uint16_t localPort, uint32_t peerId, const std::string& password, - bool duplex, bool debug, bool dmr, bool p25, bool nxdn, bool slot1, bool slot2, bool allowActivityTransfer, bool allowDiagnosticTransfer, bool updateLookup, bool saveLookup); + bool duplex, bool debug, bool dmr, bool p25, bool nxdn, bool analog, bool slot1, bool slot2, + bool allowActivityTransfer, bool allowDiagnosticTransfer, bool updateLookup, bool saveLookup); /** * @brief Finalizes a instance of the Network class. */ @@ -116,7 +120,11 @@ namespace network * @brief Resets the NXDN ring buffer. */ void resetNXDN() override; - + /** + * @brief Resets the analog ring buffer. + */ + void resetAnalog() override; + /** * @brief Sets the instances of the Radio ID and Talkgroup ID lookup tables. * @param ridLookup Radio ID Lookup Table Instance @@ -211,6 +219,11 @@ namespace network * @param callback */ void setNXDNICCCallback(std::function&& callback) { m_nxdnInCallCallback = callback; } + /** + * @brief Helper to set the analog In-Call Control callback. + * @param callback + */ + void setAnalogICCCallback(std::function&& callback) { m_analogInCallCallback = callback; } /** * @brief Helper to set the enc. key response callback. @@ -235,6 +248,7 @@ namespace network bool m_dmrEnabled; bool m_p25Enabled; bool m_nxdnEnabled; + bool m_analogEnabled; bool m_updateLookup; bool m_saveLookup; @@ -245,11 +259,13 @@ namespace network uint8_t* m_salt; Timer m_retryTimer; + uint8_t m_retryCount; Timer m_timeoutTimer; uint32_t* m_rxDMRStreamId; uint32_t m_rxP25StreamId; uint32_t m_rxNXDNStreamId; + uint32_t m_rxAnalogStreamId; uint16_t m_pktSeq; uint32_t m_loginStreamId; @@ -298,6 +314,11 @@ namespace network * (This is called once the master sends a In-Call Control request.) */ std::function m_nxdnInCallCallback; + /** + * @brief Analog In-Call Control Function Callback. + * (This is called once the master sends a In-Call Control request.) + */ + std::function m_analogInCallCallback; /** * @brief Encryption Key Response Function Callback. @@ -312,9 +333,11 @@ namespace network * @param[in] data Buffer containing message to send to peer. * @param length Length of buffer. * @param streamId Stream ID. + * @param fneHeader RTP FNE Header. + * @param rtpHeader RTP Header. */ virtual void userPacketHandler(uint32_t peerId, FrameQueue::OpcodePair opcode, const uint8_t* data = nullptr, uint32_t length = 0U, - uint32_t streamId = 0U); + uint32_t streamId = 0U, const frame::RTPFNEHeader& fneHeader = frame::RTPFNEHeader(), const frame::RTPHeader& rtpHeader = frame::RTPHeader()); /** * @brief Writes login request to the network. diff --git a/src/common/network/PacketBuffer.cpp b/src/common/network/PacketBuffer.cpp index 5bb9fe8a..686a34b6 100644 --- a/src/common/network/PacketBuffer.cpp +++ b/src/common/network/PacketBuffer.cpp @@ -72,7 +72,7 @@ bool PacketBuffer::decode(const uint8_t* data, uint8_t** message, uint32_t* outL frag->data = new uint8_t[frag->size + 1U]; ::memcpy(frag->data, data + FRAG_HDR_SIZE, FRAG_BLOCK_SIZE); - // Utils::dump(1U, "Block Payload", frag->data, FRAG_BLOCK_SIZE); + // Utils::dump(1U, "PacketBuffer::decode(), Block Payload", frag->data, FRAG_BLOCK_SIZE); fragments.insert(curBlock, frag); } @@ -106,7 +106,7 @@ bool PacketBuffer::decode(const uint8_t* data, uint8_t** message, uint32_t* outL uint32_t compressedLen = fragments[0]->compressedSize; uint32_t len = fragments[0]->size; - buffer = new uint8_t[len +1U]; + buffer = new uint8_t[len + 1U]; ::memset(buffer, 0x00U, len + 1U); if (fragments.size() == 1U) { ::memcpy(buffer, fragments[0U]->data, len); @@ -119,7 +119,10 @@ bool PacketBuffer::decode(const uint8_t* data, uint8_t** message, uint32_t* outL if (m_compression) { uint32_t decompressedLen = 0U; - *message = Compression::decompress(buffer, compressedLen, &decompressedLen); + UInt8Array decompressed = Compression::decompress(buffer, compressedLen, &decompressedLen); + *message = new uint8_t[decompressedLen]; + ::memset(*message, 0x00U, decompressedLen); + ::memcpy(*message, decompressed.get(), decompressedLen); // check that we got the appropriate data if (decompressedLen == len && message != nullptr) { @@ -161,13 +164,13 @@ void PacketBuffer::encode(uint8_t* data, uint32_t length) // create temporary buffer uint32_t compressedLen = 0U; - uint8_t* buffer = nullptr; + UInt8Array buffer = nullptr; if (m_compression) { buffer = Compression::compress(data, length, &compressedLen); } else { - buffer = new uint8_t[length]; - ::memset(buffer, 0x00U, length); - ::memcpy(buffer, data, length); + buffer = std::make_unique(length + 1U); + ::memset(buffer.get(), 0x00U, length + 1U); + ::memcpy(buffer.get(), data, length); compressedLen = length; } @@ -198,15 +201,13 @@ void PacketBuffer::encode(uint8_t* data, uint32_t length) if (offs + FRAG_BLOCK_SIZE > compressedLen) blockSize = FRAG_BLOCK_SIZE - ((offs + FRAG_BLOCK_SIZE) - compressedLen); - ::memcpy(frag->data + FRAG_HDR_SIZE, buffer + offs, blockSize); + ::memcpy(frag->data + FRAG_HDR_SIZE, buffer.get() + offs, blockSize); offs += FRAG_BLOCK_SIZE; fragments.insert(i, frag); LogInfoEx(LOG_NET, "%s, Outbound Packet Fragment, block %u of %u, txFragments = %u", m_name, i, blockCnt - 1U, fragments.size()); } - - delete[] buffer; } /* Helper to clear currently buffered fragments. */ diff --git a/src/common/network/RTPFNEHeader.h b/src/common/network/RTPFNEHeader.h index 02d106fe..8719f8dc 100644 --- a/src/common/network/RTPFNEHeader.h +++ b/src/common/network/RTPFNEHeader.h @@ -87,6 +87,7 @@ namespace network PROTOCOL_SUBFUNC_DMR = 0x00U, //! DMR PROTOCOL_SUBFUNC_P25 = 0x01U, //! P25 PROTOCOL_SUBFUNC_NXDN = 0x02U, //! NXDN + PROTOCOL_SUBFUNC_ANALOG = 0x0FU, //! Analog MASTER_SUBFUNC_WL_RID = 0x00U, //! Whitelist RIDs MASTER_SUBFUNC_BL_RID = 0x01U, //! Blacklist RIDs diff --git a/src/common/network/RawFrameQueue.cpp b/src/common/network/RawFrameQueue.cpp index fbd87459..efbd3ad5 100644 --- a/src/common/network/RawFrameQueue.cpp +++ b/src/common/network/RawFrameQueue.cpp @@ -71,7 +71,7 @@ UInt8Array RawFrameQueue::read(int& messageLength, sockaddr_storage& address, ui if (length > 0) { if (m_debug) - Utils::dump(1U, "Network Packet", buffer, length); + Utils::dump(1U, "RawFrameQueue::read(), Network Packet", buffer, length); m_failedReadCnt = 0U; @@ -104,7 +104,7 @@ bool RawFrameQueue::write(const uint8_t* message, uint32_t length, sockaddr_stor ::memcpy(buffer, message, length); if (m_debug) - Utils::dump(1U, "RawFrameQueue::write() Message", buffer, length); + Utils::dump(1U, "RawFrameQueue::write(), Message", buffer, length); // bryanb: this is really a developer warning not a end-user warning, there's nothing the end-users can do about // this message @@ -153,7 +153,7 @@ void RawFrameQueue::enqueueMessage(const uint8_t* message, uint32_t length, sock ::memcpy(buffer, message, length); if (m_debug) - Utils::dump(1U, "RawFrameQueue::enqueueMessage() Buffered Message", buffer, length); + Utils::dump(1U, "RawFrameQueue::enqueueMessage(), Buffered Message", buffer, length); udp::UDPDatagram* dgram = new udp::UDPDatagram; dgram->buffer = buffer; @@ -169,29 +169,34 @@ void RawFrameQueue::enqueueMessage(const uint8_t* message, uint32_t length, sock bool RawFrameQueue::flushQueue() { bool ret = true; - std::lock_guard lock(m_queueMutex); - m_queueFlushing = true; - if (m_buffers.empty()) { - return false; - } + // scope is intentional + { + std::lock_guard lock(m_queueMutex); + m_queueFlushing = true; - // bryanb: this is the same as above -- but for some assinine reason prevents - // weirdness - if (m_buffers.size() == 0U) { - return false; - } + if (m_buffers.empty()) { + return false; + } - // LogDebug(LOG_NET, "m_buffers len = %u", m_buffers.size()); + // bryanb: this is the same as above -- but for some assinine reason prevents + // weirdness + if (m_buffers.size() == 0U) { + return false; + } - ret = true; - if (!m_socket->write(m_buffers)) { - // LogError(LOG_NET, "Failed writing data to the network"); - ret = false; + // LogDebug(LOG_NET, "m_buffers len = %u", m_buffers.size()); + + ret = true; + if (!m_socket->write(m_buffers)) { + // LogError(LOG_NET, "Failed writing data to the network"); + ret = false; + } + + m_queueFlushing = false; } deleteBuffers(); - m_queueFlushing = false; return ret; } @@ -203,6 +208,9 @@ bool RawFrameQueue::flushQueue() void RawFrameQueue::deleteBuffers() { + std::lock_guard lock(m_queueMutex); + m_queueFlushing = true; + for (auto& buffer : m_buffers) { if (buffer != nullptr) { // LogDebug(LOG_NET, "deleting buffer, addr %p len %u", buffer->buffer, buffer->length); @@ -217,4 +225,5 @@ void RawFrameQueue::deleteBuffers() } } m_buffers.clear(); + m_queueFlushing = false; } diff --git a/src/common/network/rest/http/HTTPPayload.cpp b/src/common/network/rest/http/HTTPPayload.cpp index ca2fb0f0..088db94b 100644 --- a/src/common/network/rest/http/HTTPPayload.cpp +++ b/src/common/network/rest/http/HTTPPayload.cpp @@ -311,7 +311,7 @@ std::vector HTTPPayload::toBuffers() #if DEBUG_HTTP_PAYLOAD ::LogDebugEx(LOG_REST, "HTTPPayload::toBuffers()", "content = %s", content.c_str()); for (auto buffer : buffers) - Utils::dump("[HTTPPayload::toBuffers()] buffer[]", (uint8_t*)buffer.data(), buffer.size()); + Utils::dump("HTTPPayload::toBuffers(), buffer[]", (uint8_t*)buffer.data(), buffer.size()); #endif return buffers; diff --git a/src/common/network/rest/http/SecureServerConnection.h b/src/common/network/rest/http/SecureServerConnection.h index 3b98033c..38c22bc6 100644 --- a/src/common/network/rest/http/SecureServerConnection.h +++ b/src/common/network/rest/http/SecureServerConnection.h @@ -154,7 +154,7 @@ namespace network ((m_request.method == HTTP_POST) || (m_request.method == HTTP_PUT))) { if (m_debug) { LogDebug(LOG_REST, "HTTPS Partial Request, recvLength = %u, consumed = %u, result = %u", recvLength, consumed, result); - Utils::dump(1U, "m_buffer", (uint8_t*)m_buffer.data(), recvLength); + Utils::dump(1U, "SecureServerConnection::read(), m_buffer", (uint8_t*)m_buffer.data(), recvLength); } result = HTTPLexer::INDETERMINATE; @@ -163,7 +163,7 @@ namespace network } else { if (m_debug) { LogDebug(LOG_REST, "HTTP Partial Request, recvLength = %u, result = %u", recvLength, result); - Utils::dump(1U, "m_buffer", (uint8_t*)m_buffer.data(), recvLength); + Utils::dump(1U, "SecureServerConnection::read(), m_buffer", (uint8_t*)m_buffer.data(), recvLength); } if (m_contResult == HTTPLexer::INDETERMINATE) { @@ -180,7 +180,7 @@ namespace network if (result == HTTPLexer::GOOD) { if (m_debug) { - Utils::dump(1U, "HTTPS Request Content", (uint8_t*)m_request.content.c_str(), m_request.content.length()); + Utils::dump(1U, "SecureServerConnection::read(), HTTPS Request Content", (uint8_t*)m_request.content.c_str(), m_request.content.length()); } m_continue = false; @@ -188,7 +188,7 @@ namespace network m_requestHandler.handleRequest(m_request, m_reply); if (m_debug) { - Utils::dump(1U, "HTTP Reply Content", (uint8_t*)m_reply.content.c_str(), m_reply.content.length()); + Utils::dump(1U, "SecureServerConnection::read(), HTTPS Reply Content", (uint8_t*)m_reply.content.c_str(), m_reply.content.length()); } write(); diff --git a/src/common/network/rest/http/ServerConnection.h b/src/common/network/rest/http/ServerConnection.h index da5f32ad..c7b932a3 100644 --- a/src/common/network/rest/http/ServerConnection.h +++ b/src/common/network/rest/http/ServerConnection.h @@ -135,7 +135,7 @@ namespace network ((m_request.method == HTTP_POST) || (m_request.method == HTTP_PUT))) { if (m_debug) { LogDebug(LOG_REST, "HTTP Partial Request, recvLength = %u, consumed = %u, result = %u", recvLength, consumed, result); - Utils::dump(1U, "m_buffer", (uint8_t*)m_buffer.data(), recvLength); + Utils::dump(1U, "ServerConnection::read(), m_buffer", (uint8_t*)m_buffer.data(), recvLength); } m_contResult = result = HTTPLexer::INDETERMINATE; @@ -144,7 +144,7 @@ namespace network } else { if (m_debug) { LogDebug(LOG_REST, "HTTP Partial Request, recvLength = %u, result = %u", recvLength, result); - Utils::dump(1U, "m_buffer", (uint8_t*)m_buffer.data(), recvLength); + Utils::dump(1U, "ServerConnection::read(), m_buffer", (uint8_t*)m_buffer.data(), recvLength); } if (m_contResult == HTTPLexer::INDETERMINATE) { @@ -161,7 +161,7 @@ namespace network if (result == HTTPLexer::GOOD) { if (m_debug) { - Utils::dump(1U, "HTTP Request Content", (uint8_t*)m_request.content.c_str(), m_request.content.length()); + Utils::dump(1U, "ServerConnection::read(), HTTP Request Content", (uint8_t*)m_request.content.c_str(), m_request.content.length()); } m_continue = false; @@ -169,7 +169,7 @@ namespace network m_requestHandler.handleRequest(m_request, m_reply); if (m_debug) { - Utils::dump(1U, "HTTP Reply Content", (uint8_t*)m_reply.content.c_str(), m_reply.content.length()); + Utils::dump(1U, "ServerConnection::read(), HTTP Reply Content", (uint8_t*)m_reply.content.c_str(), m_reply.content.length()); } write(); diff --git a/src/common/network/sip/SIPPayload.cpp b/src/common/network/sip/SIPPayload.cpp index ffc7b012..29e20bdc 100644 --- a/src/common/network/sip/SIPPayload.cpp +++ b/src/common/network/sip/SIPPayload.cpp @@ -139,7 +139,7 @@ std::vector SIPPayload::toBuffers() #if DEBUG_SIP_PAYLOAD ::LogDebugEx(LOG_SIP, "SIPPayload::toBuffers()", "content = %s", content.c_str()); for (auto buffer : buffers) - Utils::dump("[SIPPayload::toBuffers()] buffer[]", (uint8_t*)buffer.data(), buffer.size()); + Utils::dump("SIPPayload::toBuffers(), buffer[]", (uint8_t*)buffer.data(), buffer.size()); #endif return buffers; diff --git a/src/common/network/tcp/SecureTcpClient.h b/src/common/network/tcp/SecureTcpClient.h index 5e9ae415..657ce8c8 100644 --- a/src/common/network/tcp/SecureTcpClient.h +++ b/src/common/network/tcp/SecureTcpClient.h @@ -70,12 +70,12 @@ namespace network // setup socket for non-blocking operations int flags = fcntl(fd, F_GETFL, 0); if (flags < 0) { - LogError(LOG_NET, "failed fcntl(F_GETFL), err: %d", errno); + LogError(LOG_NET, "failed fcntl(F_GETFL), err: %d (%s)", errno, strerror(errno)); throw std::runtime_error("Cannot accept SSL client"); } if (fcntl(fd, F_SETFL, flags | O_NONBLOCK) < 0) { - LogError(LOG_NET, "failed fcntl(F_SETFL), err: %d", errno); + LogError(LOG_NET, "failed fcntl(F_SETFL), err: %d (%s)", errno, strerror(errno)); throw std::runtime_error("Cannot accept SSL client"); } @@ -139,12 +139,12 @@ namespace network if (!nonBlocking) { flags = fcntl(fd, F_GETFL, 0); if (flags < 0) { - LogError(LOG_NET, "failed fcntl(F_GETFL), err: %d", errno); + LogError(LOG_NET, "failed fcntl(F_GETFL), err: %d (%s)", errno, strerror(errno)); throw std::runtime_error("Cannot accept SSL client"); } if (fcntl(fd, F_SETFL, flags & (~O_NONBLOCK)) < 0) { - LogError(LOG_NET, "failed fcntl(F_SETFL), err: %d", errno); + LogError(LOG_NET, "failed fcntl(F_SETFL), err: %d (%s)", errno, strerror(errno)); throw std::runtime_error("Cannot accept SSL client"); } } @@ -176,7 +176,7 @@ namespace network ssize_t ret = ::connect(m_fd, reinterpret_cast(&addr), sizeof(addr)); if (ret < 0) { - LogError(LOG_NET, "Failed to connect to server, err: %d", errno); + LogError(LOG_NET, "Failed to connect to server, err: %d (%s)", errno, strerror(errno)); } initSsl(m_pSSLCtx); @@ -189,12 +189,12 @@ namespace network if (nonBlocking) { int flags = fcntl(m_fd, F_GETFL, 0); if (flags < 0) { - LogError(LOG_NET, "failed fcntl(F_GETFL), err: %d", errno); + LogError(LOG_NET, "failed fcntl(F_GETFL), err: %d (%s)", errno, strerror(errno)); throw std::runtime_error("Failed to set SSL server connection to non-blocking"); } if (fcntl(m_fd, F_SETFL, flags | O_NONBLOCK) < 0) { - LogError(LOG_NET, "failed fcntl(F_SETFL), err: %d", errno); + LogError(LOG_NET, "failed fcntl(F_SETFL), err: %d (%s)", errno, strerror(errno)); throw std::runtime_error("Failed to set SSL server connection to non-blocking"); } } @@ -260,13 +260,13 @@ namespace network { m_fd = ::socket(AF_INET, SOCK_STREAM, 0); if (m_fd < 0) { - LogError(LOG_NET, "Cannot create the TCP socket, err: %d", errno); + LogError(LOG_NET, "Cannot create the TCP socket, err: %d (%s)", errno, strerror(errno)); throw std::runtime_error("Cannot create the TCP socket"); } int reuse = 1; if (::setsockopt(m_fd, IPPROTO_TCP, TCP_NODELAY, (char*)& reuse, sizeof(reuse)) != 0) { - LogError(LOG_NET, "Cannot set the TCP socket option, err: %d", errno); + LogError(LOG_NET, "Cannot set the TCP socket option, err: %d (%s)", errno, strerror(errno)); throw std::runtime_error("Cannot set the TCP socket option"); } } diff --git a/src/common/network/tcp/SecureTcpListener.h b/src/common/network/tcp/SecureTcpListener.h index 0b8ad912..978c4bcd 100644 --- a/src/common/network/tcp/SecureTcpListener.h +++ b/src/common/network/tcp/SecureTcpListener.h @@ -72,13 +72,13 @@ namespace network m_fd = ::socket(AF_INET, SOCK_STREAM, 0); if (m_fd < 0) { - LogError(LOG_NET, "Cannot create the TCP socket, err: %d", errno); + LogError(LOG_NET, "Cannot create the TCP socket, err: %d (%s)", errno, strerror(errno)); throw std::runtime_error("Cannot create the TCP socket"); } int reuse = 1; if (::setsockopt(m_fd, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, (char*)& reuse, sizeof(reuse)) != 0) { - LogError(LOG_NET, "Cannot set the TCP socket option, err: %d", errno); + LogError(LOG_NET, "Cannot set the TCP socket option, err: %d (%s)", errno, strerror(errno)); throw std::runtime_error("Cannot set the TCP socket option"); } @@ -94,7 +94,7 @@ namespace network SecureTcpListener(const std::string& keyFile, const std::string& certFile, const uint16_t port, const std::string& address = "0.0.0.0") : SecureTcpListener(keyFile, certFile) { if (!bind(address, port)) { - LogError(LOG_NET, "Cannot to bind secure TCP server, err: %d", errno); + LogError(LOG_NET, "Cannot to bind secure TCP server, err: %d (%s)", errno, strerror(errno)); throw std::runtime_error("Cannot to bind secure TCP server"); } } diff --git a/src/common/network/tcp/Socket.cpp b/src/common/network/tcp/Socket.cpp index 6a6537a6..17b5f3b8 100644 --- a/src/common/network/tcp/Socket.cpp +++ b/src/common/network/tcp/Socket.cpp @@ -110,7 +110,7 @@ int Socket::accept(sockaddr* address, socklen_t* addrlen) noexcept #if defined(_WIN32) LogError(LOG_NET, "Error returned from TCP poll, err: %lu", ::GetLastError()); #else - LogError(LOG_NET, "Error returned from TCP poll, err: %d", errno); + LogError(LOG_NET, "Error returned from TCP poll, err: %d (%s)", errno, strerror(errno)); #endif // defined(_WIN32) return -1; } @@ -180,7 +180,7 @@ ssize_t Socket::listen(const std::string& ipAddr, const uint16_t port, int backl #if defined(_WIN32) LogError(LOG_NET, "Error returned from TCP poll, err: %lu", ::GetLastError()); #else - LogError(LOG_NET, "Error returned from TCP poll, err: %d", errno); + LogError(LOG_NET, "Error returned from TCP poll, err: %d (%s)", errno, strerror(errno)); #endif // defined(_WIN32) return -1; } @@ -316,7 +316,7 @@ bool Socket::initSocket(const int domain, const int type, const int protocol) } #else if (m_fd < 0) { - LogError(LOG_NET, "Cannot create the TCP socket, err: %d", errno); + LogError(LOG_NET, "Cannot create the TCP socket, err: %d (%s)", errno, strerror(errno)); return false; } #endif // defined(_WIN32) @@ -340,7 +340,7 @@ bool Socket::bind(const std::string& ipAddr, const uint16_t port) #if defined(_WIN32) LogError(LOG_NET, "Cannot bind the TCP address, err: %lu", ::GetLastError()); #else - LogError(LOG_NET, "Cannot bind the TCP address, err: %d", errno); + LogError(LOG_NET, "Cannot bind the TCP address, err: %d (%s)", errno, strerror(errno)); #endif // defined(_WIN32) retval = false; } diff --git a/src/common/network/tcp/TcpClient.h b/src/common/network/tcp/TcpClient.h index 3d1c14e4..8f242101 100644 --- a/src/common/network/tcp/TcpClient.h +++ b/src/common/network/tcp/TcpClient.h @@ -82,7 +82,7 @@ namespace network #if defined(_WIN32) LogError(LOG_NET, "Failed to connect to server, err: %lu", ::GetLastError()); #else - LogError(LOG_NET, "Failed to connect to server, err: %d", errno); + LogError(LOG_NET, "Failed to connect to server, err: %d (%s)", errno, strerror(errno)); #endif // defined(_WIN32) } } @@ -106,7 +106,7 @@ namespace network #if defined(_WIN32) LogError(LOG_NET, "Cannot set the TCP socket option, err: %lu", ::GetLastError()); #else - LogError(LOG_NET, "Cannot set the TCP socket option, err: %d", errno); + LogError(LOG_NET, "Cannot set the TCP socket option, err: %d (%s)", errno, strerror(errno)); #endif // defined(_WIN32) throw std::runtime_error("Cannot set the TCP socket option"); } diff --git a/src/common/network/tcp/TcpListener.h b/src/common/network/tcp/TcpListener.h index 6da63e70..9201957f 100644 --- a/src/common/network/tcp/TcpListener.h +++ b/src/common/network/tcp/TcpListener.h @@ -52,7 +52,7 @@ namespace network } #else if (::setsockopt(m_fd, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, (char*)& reuse, sizeof(reuse)) != 0) { - LogError(LOG_NET, "Cannot set the TCP socket option, err: %d", errno); + LogError(LOG_NET, "Cannot set the TCP socket option, err: %d (%s)", errno, strerror(errno)); throw std::runtime_error("Cannot set the TCP socket option"); } #endif // defined(_WIN32) @@ -68,7 +68,7 @@ namespace network #if defined(_WIN32) LogError(LOG_NET, "Cannot to bind TCP server, err: %lu", ::GetLastError()); #else - LogError(LOG_NET, "Cannot to bind TCP server, err: %d", errno); + LogError(LOG_NET, "Cannot to bind TCP server, err: %d (%s)", errno, strerror(errno)); #endif // defined(_WIN32) throw std::runtime_error("Cannot to bind TCP server"); } @@ -85,7 +85,7 @@ namespace network #if defined(_WIN32) LogError(LOG_NET, "Failed to listen on TCP server, err: %lu", ::GetLastError()); #else - LogError(LOG_NET, "Failed to listen on TCP server, err: %d", errno); + LogError(LOG_NET, "Failed to listen on TCP server, err: %d (%s)", errno, strerror(errno)); #endif // defined(_WIN32) throw std::runtime_error("Failed to listen on TCP server."); } diff --git a/src/common/network/udp/Socket.cpp b/src/common/network/udp/Socket.cpp index 0d55a210..8ec1f2cf 100644 --- a/src/common/network/udp/Socket.cpp +++ b/src/common/network/udp/Socket.cpp @@ -145,7 +145,11 @@ bool Socket::open(const uint32_t af, const std::string& address, const uint16_t if (port > 0U) { int reuse = 1; if (::setsockopt(m_fd, SOL_SOCKET, SO_REUSEADDR, (char*)& reuse, sizeof(reuse)) == -1) { - LogError(LOG_NET, "Cannot set the UDP socket option, err: %d", errno); +#if defined(_WIN32) + LogError(LOG_NET, "Cannot bind the UDP socket option, err: %lu", ::GetLastError()); +#else + LogError(LOG_NET, "Cannot set the UDP socket option, err: %d (%s)", errno, strerror(errno)); +#endif // _WIN32 return false; } @@ -205,7 +209,7 @@ ssize_t Socket::read(uint8_t* buffer, uint32_t length, sockaddr_storage& address #if defined(_WIN32) LogError(LOG_NET, "Error returned from UDP poll, err: %lu", ::GetLastError()); #else - LogError(LOG_NET, "Error returned from UDP poll, err: %d", errno); + LogError(LOG_NET, "Error returned from UDP poll, err: %d (%s)", errno, strerror(errno)); #endif // defined(_WIN32) return -1; } @@ -219,7 +223,7 @@ ssize_t Socket::read(uint8_t* buffer, uint32_t length, sockaddr_storage& address #if defined(_WIN32) LogError(LOG_NET, "Error returned from recvfrom, err: %lu", ::GetLastError()); #else - LogError(LOG_NET, "Error returned from recvfrom, err: %d", errno); + LogError(LOG_NET, "Error returned from recvfrom, err: %d (%s)", errno, strerror(errno)); #endif // defined(_WIN32) if (len == -1 && errno == ENOTSOCK) { @@ -255,12 +259,12 @@ ssize_t Socket::read(uint8_t* buffer, uint32_t length, sockaddr_storage& address ::memcpy(cryptoBuffer, buffer + 2U, len - 2U); } - // Utils::dump(1U, "Socket::read() crypted", cryptoBuffer, cryptedLen); + // Utils::dump(1U, "Socket::read(), crypted", cryptoBuffer, cryptedLen); // decrypt uint8_t* decrypted = m_aes->decryptECB(cryptoBuffer, cryptedLen, m_presharedKey); - // Utils::dump(1U, "Socket::read() decrypted", decrypted, cryptedLen); + // Utils::dump(1U, "Socket::read(), decrypted", decrypted, cryptedLen); // finalize, cleanup buffers and replace with new if (decrypted != nullptr) { @@ -340,7 +344,7 @@ bool Socket::write(const uint8_t* buffer, uint32_t length, const sockaddr_storag // encrypt uint8_t* crypted = m_aes->encryptECB(cryptoBuffer, cryptedLen, m_presharedKey); - // Utils::dump(1U, "Socket::write() crypted", crypted, cryptedLen); + // Utils::dump(1U, "Socket::write(), crypted", crypted, cryptedLen); // finalize, cleanup buffers and replace with new out = std::unique_ptr(new uint8_t[cryptedLen + 2U]); @@ -365,10 +369,16 @@ bool Socket::write(const uint8_t* buffer, uint32_t length, const sockaddr_storag ssize_t sent = ::sendto(m_fd, (char*)out.get(), length, 0, (sockaddr*)& address, addrLen); if (sent < 0) { + if (errno == ENETUNREACH || errno == EHOSTUNREACH) { + // if we were not able to send a frame and the network logging is enabled -- disable network logging + if (!g_disableNetworkLog) + g_disableNetworkLog = true; + } + #if defined(_WIN32) LogError(LOG_NET, "Error returned from sendto, err: %lu", ::GetLastError()); #else - LogError(LOG_NET, "Error returned from sendto, err: %d", errno); + LogError(LOG_NET, "Error returned from sendto, err: %d (%s)", errno, strerror(errno)); #endif // defined(_WIN32) if (lenWritten != nullptr) { @@ -376,6 +386,10 @@ bool Socket::write(const uint8_t* buffer, uint32_t length, const sockaddr_storag } } else { + // if we were able to send a frame and the network logging is disabled -- reenable network logging + if (g_disableNetworkLog) + g_disableNetworkLog = false; + if (sent == ssize_t(length)) result = true; @@ -491,7 +505,7 @@ bool Socket::write(BufferVector& buffers, ssize_t* lenWritten) noexcept continue; } - // Utils::dump(1U, "Socket::write() crypted", crypted, cryptedLen); + // Utils::dump(1U, "Socket::write(), crypted", crypted, cryptedLen); // finalize DECLARE_UINT8_ARRAY(out, cryptedLen + 2U); @@ -553,14 +567,22 @@ bool Socket::write(BufferVector& buffers, ssize_t* lenWritten) noexcept } if (sendmmsg(m_fd, headers, size, 0) < 0) { - LogError(LOG_NET, "Error returned from sendmmsg, err: %d", errno); +#if defined(_WIN32) + LogError(LOG_NET, "Error returned from sendmmsg, err: %lu", ::GetLastError()); +#else + LogError(LOG_NET, "Error returned from sendmmsg, err: %d (%s)", errno, strerror(errno)); +#endif // _WIN32 if (lenWritten != nullptr) { *lenWritten = -1; } } if (sent < 0) { - LogError(LOG_NET, "Error returned from sendmmsg, err: %d", errno); +#if defined(_WIN32) + LogError(LOG_NET, "Error returned from sendmmsg, err: %lu", ::GetLastError()); +#else + LogError(LOG_NET, "Error returned from sendmmsg, err: %d (%s)", errno, strerror(errno)); +#endif // _WIN32 if (lenWritten != nullptr) { *lenWritten = -1; } @@ -655,7 +677,7 @@ std::string Socket::getLocalAddress() err = getnameinfo(ifa->ifa_addr, (family == AF_INET) ? sizeof(struct sockaddr_in) : sizeof(struct sockaddr_in6), host, NI_MAXHOST, NULL, 0, NI_NUMERICHOST); if (err != 0) { - LogError(LOG_NET, "Cannot retreive system network interfaces, err: %d", errno); + LogError(LOG_NET, "Cannot retreive system network interfaces, err: %d (%s)", errno, strerror(errno)); break; } @@ -800,7 +822,7 @@ bool Socket::initSocket(const int domain, const int type, const int protocol) no } #else if (m_fd < 0) { - LogError(LOG_NET, "Cannot create the UDP socket, err: %d", errno); + LogError(LOG_NET, "Cannot create the UDP socket, err: %d (%s)", errno, strerror(errno)); return false; } #endif // defined(_WIN32) @@ -825,7 +847,7 @@ bool Socket::bind(const std::string& ipAddr, const uint16_t port) noexcept(false #if defined(_WIN32) LogError(LOG_NET, "Cannot bind the UDP address, err: %lu", ::GetLastError()); #else - LogError(LOG_NET, "Cannot bind the UDP address, err: %d", errno); + LogError(LOG_NET, "Cannot bind the UDP address, err: %d (%s)", errno, strerror(errno)); #endif // defined(_WIN32) retval = false; } diff --git a/src/common/network/viface/VIFace.cpp b/src/common/network/viface/VIFace.cpp index 58c1f399..530b2b13 100644 --- a/src/common/network/viface/VIFace.cpp +++ b/src/common/network/viface/VIFace.cpp @@ -91,7 +91,7 @@ void hookVirtualInterface(std::string name, struct viface_queues* queues) // creates the socket fd = socket(AF_PACKET, SOCK_RAW, htons(ETH_P_ALL)); if (fd < 0) { - LogError(LOG_NET, "Unable to create the Tx/Rx socket channel %s, queue: %d, err: %d, error: %s", name.c_str(), i, errno, strerror(errno)); + LogError(LOG_NET, "Unable to create the Tx/Rx socket channel %s, queue: %d, err: %d (%s)", name.c_str(), i, errno, strerror(errno)); goto hookErr; // bryanb: no good very bad way to handle this -- but if its good enough for the Linux kernel its good enough for us right? this is easiset way to clean up quickly... } @@ -102,7 +102,7 @@ void hookVirtualInterface(std::string name, struct viface_queues* queues) // obtains the network index number if (ioctl(fd, SIOCGIFINDEX, &ifr) != 0) { - LogError(LOG_NET, "Unable to get network index number %s, queue: %d, err: %d, error: %s", name.c_str(), i, errno, strerror(errno)); + LogError(LOG_NET, "Unable to get network index number %s, queue: %d, err: %d (%s)", name.c_str(), i, errno, strerror(errno)); goto hookErr; // bryanb: no good very bad way to handle this -- but if its good enough for the Linux kernel its good enough for us right? this is easiset way to clean up quickly... } @@ -115,7 +115,7 @@ void hookVirtualInterface(std::string name, struct viface_queues* queues) // binds the socket to the 'socket_addr' address if (bind(fd, (struct sockaddr*) &socket_addr, sizeof(socket_addr)) != 0) { - LogError(LOG_NET, "Unable to bind the Tx/Rx socket channel to the network interface %s, queue: %d, err: %d, error: %s", name.c_str(), i, errno, strerror(errno)); + LogError(LOG_NET, "Unable to bind the Tx/Rx socket channel to the network interface %s, queue: %d, err: %d (%s)", name.c_str(), i, errno, strerror(errno)); goto hookErr; // bryanb: no good very bad way to handle this -- but if its good enough for the Linux kernel its good enough for us right? this is easiset way to clean up quickly... } @@ -128,7 +128,7 @@ hookErr: // Rollback close file descriptors for (--i; i >= 0; i--) { if (close(((int *)queues)[i]) < 0) { - LogError(LOG_NET, "Unable to close a Rx/Tx socket %s, queue: %d, err: %d, error: %s", name.c_str(), i, errno, strerror(errno)); + LogError(LOG_NET, "Unable to close a Rx/Tx socket %s, queue: %d, err: %d (%s)", name.c_str(), i, errno, strerror(errno)); } } @@ -171,15 +171,15 @@ std::string allocateVirtualInterface(std::string name, bool tap, struct viface_q // open TUN/TAP device fd = open("/dev/net/tun", O_RDWR | O_NONBLOCK); if (fd < 0) { - LogError(LOG_NET, "Unable to open TUN/TAP device %s, queue: %d, err: %d, error: %s", name.c_str(), i, errno, strerror(errno)); + LogError(LOG_NET, "Unable to open TUN/TAP device %s, queue: %d, err: %d (%s)", name.c_str(), i, errno, strerror(errno)); goto allocErr; // bryanb: no good very bad way to handle this -- but if its good enough for the Linux kernel its good enough for us right? this is easiset way to clean up quickly... } // register a network device with the kernel if (ioctl(fd, TUNSETIFF, (void *)&ifr) != 0) { - LogError(LOG_NET, "Unable to register a TUN/TAP device %s, queue: %d, err: %d, error: %s", name.c_str(), i, errno, strerror(errno)); + LogError(LOG_NET, "Unable to register a TUN/TAP device %s, queue: %d, err: %d (%s)", name.c_str(), i, errno, strerror(errno)); if (close(fd) < 0) { - LogError(LOG_NET, "Unable to close a TUN/TAP device %s, queue: %d, err: %d, error: %s", name.c_str(), i, errno, strerror(errno)); + LogError(LOG_NET, "Unable to close a TUN/TAP device %s, queue: %d, err: %d (%s)", name.c_str(), i, errno, strerror(errno)); } goto allocErr; // bryanb: no good very bad way to handle this -- but if its good enough for the Linux kernel its good enough for us right? this is easiset way to clean up quickly... @@ -194,7 +194,7 @@ allocErr: // rollback close file descriptors for (--i; i >= 0; i--) { if (close(((int *)queues)[i]) < 0) { - LogError(LOG_NET, "Unable to close a TUN/TAP device %s, queue: %d, err: %d, error: %s", name.c_str(), i, errno, strerror(errno)); + LogError(LOG_NET, "Unable to close a TUN/TAP device %s, queue: %d, err: %d (%s)", name.c_str(), i, errno, strerror(errno)); } } @@ -217,7 +217,7 @@ void readVIFlags(int sockfd, std::string name, struct ifreq& ifr) // read interface flags if (ioctl(sockfd, SIOCGIFFLAGS, &ifr) != 0) { - LogError(LOG_NET, "Unable to read virtual interface flags %s, err: %d, error: %s", name.c_str(), errno, strerror(errno)); + LogError(LOG_NET, "Unable to read virtual interface flags %s, err: %d (%s)", name.c_str(), errno, strerror(errno)); } } @@ -235,7 +235,7 @@ uint32_t readMTU(std::string name, size_t size) // opens MTU file fd = open(("/sys/class/net/" + name + "/mtu").c_str(), O_RDONLY | O_NONBLOCK); if (fd < 0) { - LogError(LOG_NET, "Unable to open MTU file for virtual interface %s, err: %d, error: %s", name.c_str(), errno, strerror(errno)); + LogError(LOG_NET, "Unable to open MTU file for virtual interface %s, err: %d (%s)", name.c_str(), errno, strerror(errno)); goto readMTUErr; // bryanb: no good very bad way to handle this -- but if its good enough for the Linux kernel its good enough for us right? this is easiset way to clean up quickly... } @@ -245,12 +245,12 @@ uint32_t readMTU(std::string name, size_t size) // Handles errors if (nread == -1) { - LogError(LOG_NET, "Unable to read MTU for virtual interface %s, err: %d, error: %s", name.c_str(), errno, strerror(errno)); + LogError(LOG_NET, "Unable to read MTU for virtual interface %s, err: %d (%s)", name.c_str(), errno, strerror(errno)); goto readMTUErr; // bryanb: no good very bad way to handle this -- but if its good enough for the Linux kernel its good enough for us right? this is easiset way to clean up quickly... } if (close(fd) < 0) { - LogError(LOG_NET, "Unable to close MTU file for virtual interface %s, err: %d, error: %s", name.c_str(), errno, strerror(errno)); + LogError(LOG_NET, "Unable to close MTU file for virtual interface %s, err: %d (%s)", name.c_str(), errno, strerror(errno)); goto readMTUErr; // bryanb: no good very bad way to handle this -- but if its good enough for the Linux kernel its good enough for us right? this is easiset way to clean up quickly... } @@ -307,7 +307,7 @@ VIFace::VIFace(std::string name, bool tap, int id) : // epoll create m_epollFd = epoll_create1(0); if (m_epollFd == -1) { - LogError(LOG_NET, "Unable to initialize epoll %s, err: %d, error: %s", name.c_str(), errno, strerror(errno)); + LogError(LOG_NET, "Unable to initialize epoll %s, err: %d (%s)", name.c_str(), errno, strerror(errno)); throw std::runtime_error("Unable to initialize epoll."); } @@ -317,20 +317,20 @@ VIFace::VIFace(std::string name, bool tap, int id) : }; if (epoll_ctl(m_epollFd, EPOLL_CTL_ADD, ev.data.fd, &ev) == -1) { - LogError(LOG_NET, "Unable to configure epoll %s, err: %d, error: %s", name.c_str(), errno, strerror(errno)); + LogError(LOG_NET, "Unable to configure epoll %s, err: %d (%s)", name.c_str(), errno, strerror(errno)); throw std::runtime_error("Unable to configure epoll."); } ev.data.fd = m_queues.txFd; if (epoll_ctl(m_epollFd, EPOLL_CTL_ADD, ev.data.fd, &ev) == -1) { - LogError(LOG_NET, "Unable to configure epoll %s, err: %d, error: %s", name.c_str(), errno, strerror(errno)); + LogError(LOG_NET, "Unable to configure epoll %s, err: %d (%s)", name.c_str(), errno, strerror(errno)); throw std::runtime_error("Unable to configure epoll."); } // create socket channels to the NET kernel for later ioctl m_ksFd = -1; m_ksFd = socket(AF_INET, SOCK_STREAM, 0); if (m_ksFd < 0) { - LogError(LOG_NET, "Unable to create IPv4 socket channel to the NET kernel %s, err: %d, error: %s", name.c_str(), errno, strerror(errno)); + LogError(LOG_NET, "Unable to create IPv4 socket channel to the NET kernel %s, err: %d (%s)", name.c_str(), errno, strerror(errno)); throw std::runtime_error("Unable to create IPv4 socket channel to the NET kernel."); } @@ -378,7 +378,7 @@ void VIFace::up() } if (ioctl(m_ksFd, SIOCSIFHWADDR, &ifr) != 0) { - LogError(LOG_NET, "Unable to set MAC address %s, err: %d, error: %s", m_name.c_str(), errno, strerror(errno)); + LogError(LOG_NET, "Unable to set MAC address %s, err: %d (%s)", m_name.c_str(), errno, strerror(errno)); return; } } @@ -390,12 +390,12 @@ void VIFace::up() // address if (!m_ipv4Address.empty()) { if (!inet_pton(AF_INET, m_ipv4Address.c_str(), &addr->sin_addr)) { - LogError(LOG_NET, "Invalid cached IPv4 address %s, err: %d, error: %s", m_name.c_str(), errno, strerror(errno)); + LogError(LOG_NET, "Invalid cached IPv4 address %s, err: %d (%s)", m_name.c_str(), errno, strerror(errno)); return; } if (ioctl(m_ksFd, SIOCSIFADDR, &ifr) != 0) { - LogError(LOG_NET, "Unable to set IPv4 address %s, err: %d, error: %s", m_name.c_str(), errno, strerror(errno)); + LogError(LOG_NET, "Unable to set IPv4 address %s, err: %d (%s)", m_name.c_str(), errno, strerror(errno)); return; } } @@ -403,12 +403,12 @@ void VIFace::up() // netmask if (!m_ipv4Netmask.empty()) { if (!inet_pton(AF_INET, m_ipv4Netmask.c_str(), &addr->sin_addr)) { - LogError(LOG_NET, "Invalid cached IPv4 netmask %s, err: %d, error: %s", m_name.c_str(), errno, strerror(errno)); + LogError(LOG_NET, "Invalid cached IPv4 netmask %s, err: %d (%s)", m_name.c_str(), errno, strerror(errno)); return; } if (ioctl(m_ksFd, SIOCSIFNETMASK, &ifr) != 0) { - LogError(LOG_NET, "Unable to set IPv4 netmask %s, err: %d, error: %s", m_name.c_str(), errno, strerror(errno)); + LogError(LOG_NET, "Unable to set IPv4 netmask %s, err: %d (%s)", m_name.c_str(), errno, strerror(errno)); return; } } @@ -416,12 +416,12 @@ void VIFace::up() // broadcast if (!m_ipv4Broadcast.empty()) { if (!inet_pton(AF_INET, m_ipv4Broadcast.c_str(), &addr->sin_addr)) { - LogError(LOG_NET, "Invalid cached IPv4 broadcast %s, err: %d, error: %s", m_name.c_str(), errno, strerror(errno)); + LogError(LOG_NET, "Invalid cached IPv4 broadcast %s, err: %d (%s)", m_name.c_str(), errno, strerror(errno)); return; } if (ioctl(m_ksFd, SIOCSIFBRDADDR, &ifr) != 0) { - LogError(LOG_NET, "Unable to set IPv4 broadcast %s, err: %d, error: %s", m_name.c_str(), errno, strerror(errno)); + LogError(LOG_NET, "Unable to set IPv4 broadcast %s, err: %d (%s)", m_name.c_str(), errno, strerror(errno)); return; } } @@ -429,14 +429,14 @@ void VIFace::up() // MTU ifr.ifr_mtu = m_mtu; if (ioctl(m_ksFd, SIOCSIFMTU, &ifr) != 0) { - LogError(LOG_NET, "Unable to set MTU %s, err: %d, error: %s", m_name.c_str(), errno, strerror(errno)); + LogError(LOG_NET, "Unable to set MTU %s, err: %d (%s)", m_name.c_str(), errno, strerror(errno)); return; } // bring-up interface ifr.ifr_flags |= IFF_UP; if (ioctl(m_ksFd, SIOCSIFFLAGS, &ifr) != 0) { - LogError(LOG_NET, "Unable to bring-up virtual interface %s, err: %d, error: %s", m_name.c_str(), errno, strerror(errno)); + LogError(LOG_NET, "Unable to bring-up virtual interface %s, err: %d (%s)", m_name.c_str(), errno, strerror(errno)); } } @@ -451,7 +451,7 @@ void VIFace::down() const // bring-down interface ifr.ifr_flags &= ~IFF_UP; if (ioctl(m_ksFd, SIOCSIFFLAGS, &ifr) != 0) { - LogError(LOG_NET, "Unable to bring-down virtual interface %s, err: %d, error: %s", m_name.c_str(), errno, strerror(errno)); + LogError(LOG_NET, "Unable to bring-down virtual interface %s, err: %d (%s)", m_name.c_str(), errno, strerror(errno)); } } @@ -477,7 +477,7 @@ ssize_t VIFace::read(uint8_t* buffer) int ret = epoll_wait(m_epollFd, &wait_event, 1, 0); if ((ret < 0) && (errno != EINTR)) { - LogError(LOG_NET, "Error returned from epoll_wait, err: %d, error: %s", errno, strerror(errno)); + LogError(LOG_NET, "Error returned from epoll_wait, err: %d (%s)", errno, strerror(errno)); return -1; } @@ -490,7 +490,7 @@ ssize_t VIFace::read(uint8_t* buffer) // Read packet into our buffer ssize_t len = ::read(wait_event.data.fd, buffer, m_mtu); if (len == -1) { - LogError(LOG_NET, "Error returned from read, err: %d, error: %s", errno, strerror(errno)); + LogError(LOG_NET, "Error returned from read, err: %d (%s)", errno, strerror(errno)); } return len; @@ -510,7 +510,7 @@ bool VIFace::write(const uint8_t* buffer, uint32_t length, ssize_t* lenWritten) *lenWritten = -1; } - LogError(LOG_NET, "Packet is too small, err: %d, error: %s", errno, strerror(errno)); + LogError(LOG_NET, "Packet is too small, err: %d (%s)", errno, strerror(errno)); return false; } @@ -519,7 +519,7 @@ bool VIFace::write(const uint8_t* buffer, uint32_t length, ssize_t* lenWritten) *lenWritten = -1; } - LogError(LOG_NET, "Packet is too large, err: %d, error: %s", errno, strerror(errno)); + LogError(LOG_NET, "Packet is too large, err: %d (%s)", errno, strerror(errno)); return false; } @@ -527,7 +527,7 @@ bool VIFace::write(const uint8_t* buffer, uint32_t length, ssize_t* lenWritten) bool result = false; ssize_t sent = ::write(m_queues.txFd, buffer, length); if (sent < 0) { - LogError(LOG_NET, "Error returned from write, err: %d, error: %s", errno, strerror(errno)); + LogError(LOG_NET, "Error returned from write, err: %d (%s)", errno, strerror(errno)); if (lenWritten != nullptr) { *lenWritten = -1; @@ -585,7 +585,7 @@ std::string VIFace::getMAC() const readVIFlags(m_ksFd, m_name, ifr); if (ioctl(m_ksFd, SIOCGIFHWADDR, &ifr) != 0) { - LogError(LOG_NET, "Unable to get MAC address for virtual interface %s, err: %d, error: %s", m_name.c_str(), errno, strerror(errno)); + LogError(LOG_NET, "Unable to get MAC address for virtual interface %s, err: %d (%s)", m_name.c_str(), errno, strerror(errno)); return std::string(); } @@ -608,7 +608,7 @@ void VIFace::setIPv4(std::string address) { struct in_addr addr; if (!inet_pton(AF_INET, address.c_str(), &addr)) { - LogError(LOG_NET, "Invalid IPv4 address %s, err: %d, error: %s", m_name.c_str(), errno, strerror(errno)); + LogError(LOG_NET, "Invalid IPv4 address %s, err: %d (%s)", m_name.c_str(), errno, strerror(errno)); return; } @@ -628,7 +628,7 @@ void VIFace::setIPv4Netmask(std::string netmask) { struct in_addr addr; if (!inet_pton(AF_INET, netmask.c_str(), &addr)) { - LogError(LOG_NET, "Invalid IPv4 address %s, err: %d, error: %s", m_name.c_str(), errno, strerror(errno)); + LogError(LOG_NET, "Invalid IPv4 address %s, err: %d (%s)", m_name.c_str(), errno, strerror(errno)); return; } @@ -648,7 +648,7 @@ void VIFace::setIPv4Broadcast(std::string broadcast) { struct in_addr addr; if (!inet_pton(AF_INET, broadcast.c_str(), &addr)) { - LogError(LOG_NET, "Invalid IPv4 address %s, err: %d, error: %s", m_name.c_str(), errno, strerror(errno)); + LogError(LOG_NET, "Invalid IPv4 address %s, err: %d (%s)", m_name.c_str(), errno, strerror(errno)); return; } @@ -667,14 +667,14 @@ std::string VIFace::getIPv4Broadcast() const void VIFace::setMTU(uint32_t mtu) { if (mtu < ETH_HLEN) { - LogError(LOG_NET, "MTU %d is too small %s, err: %d, error: %s", mtu, m_name.c_str(), errno, strerror(errno)); + LogError(LOG_NET, "MTU %d is too small %s, err: %d (%s)", mtu, m_name.c_str(), errno, strerror(errno)); return; } // are we sure about this upper validation? // lo interface reports this number for its MTU if (mtu > 65536) { - LogError(LOG_NET, "MTU %d is too large %s, err: %d, error: %s", mtu, m_name.c_str(), errno, strerror(errno)); + LogError(LOG_NET, "MTU %d is too large %s, err: %d (%s)", mtu, m_name.c_str(), errno, strerror(errno)); return; } @@ -690,7 +690,7 @@ uint32_t VIFace::getMTU() const readVIFlags(m_ksFd, m_name, ifr); if (ioctl(m_ksFd, SIOCGIFMTU, &ifr) != 0) { - LogError(LOG_NET, "Unable to get MTU address for virtual interface %s, err: %d, error: %s", m_name.c_str(), errno, strerror(errno)); + LogError(LOG_NET, "Unable to get MTU address for virtual interface %s, err: %d (%s)", m_name.c_str(), errno, strerror(errno)); return 0U; } @@ -710,7 +710,7 @@ std::string VIFace::ioctlGetIPv4(uint64_t request) const readVIFlags(m_ksFd, m_name, ifr); if (ioctl(m_ksFd, request, &ifr) != 0) { - LogError(LOG_NET, "Unable to get IPv4 address for virtual interface %s, err: %d, error: %s", m_name.c_str(), errno, strerror(errno)); + LogError(LOG_NET, "Unable to get IPv4 address for virtual interface %s, err: %d (%s)", m_name.c_str(), errno, strerror(errno)); return std::string(); } @@ -720,7 +720,7 @@ std::string VIFace::ioctlGetIPv4(uint64_t request) const struct sockaddr_in* ipaddr = (struct sockaddr_in*) &ifr.ifr_addr; if (inet_ntop(AF_INET, &(ipaddr->sin_addr), addr, sizeof(addr)) == NULL) { - LogError(LOG_NET, "Unable to convert IPv4 address for virtual interface %s, err: %d, error: %s", m_name.c_str(), errno, strerror(errno)); + LogError(LOG_NET, "Unable to convert IPv4 address for virtual interface %s, err: %d (%s)", m_name.c_str(), errno, strerror(errno)); return std::string(); } diff --git a/src/common/nxdn/NXDNDefines.h b/src/common/nxdn/NXDNDefines.h index 077c28e5..a9e4c363 100644 --- a/src/common/nxdn/NXDNDefines.h +++ b/src/common/nxdn/NXDNDefines.h @@ -284,6 +284,17 @@ namespace nxdn }; } + /** @brief Remote Control Commands */ + namespace RemoteControlCommand { + /** @brief Remote Control Commands */ + enum : uint8_t { + STUN = 0x00U, //! Stun + REVIVE = 0x01U, //! Revive + KILL = 0x02U, //! Kill + REMOTE_MONITOR = 0x04U //! Remote Monitor + }; + } + /** @brief Channel Access - Step */ namespace ChAccessStep { /** @brief Channel Access - Step */ @@ -340,6 +351,8 @@ namespace nxdn SRV_INFO = 0x19U, //! SRV_INFO - Service Information CCH_INFO = 0x1AU, //! CCH_INFO - Control Channel Information ADJ_SITE_INFO = 0x1BU, //! ADJ_SITE_INFO - Adjacent Site Information + REM_CON_REQ = 0x34U, //! REM_CON_REQ - Remote Control Request + REM_CON_RESP = 0x35U, //! REM_CON_RESP - Remote Control Response // Traffic Channel Message Types RTCH_VCALL = 0x01U, //! VCALL - Voice Call diff --git a/src/common/nxdn/NXDNUtils.cpp b/src/common/nxdn/NXDNUtils.cpp index c94e32af..0c069114 100644 --- a/src/common/nxdn/NXDNUtils.cpp +++ b/src/common/nxdn/NXDNUtils.cpp @@ -55,3 +55,46 @@ void NXDNUtils::addPostBits(uint8_t* data) WRITE_BIT(data, n, b); } } + +/* Helper to convert a cause code to a string. */ + +std::string NXDNUtils::causeToString(uint8_t cause) +{ + switch (cause) { + case CauseResponse::RSRC_NOT_AVAIL_NETWORK: + return std::string("RSRC_NOT_AVAIL_NETWORK (Resource Not Available - Network)"); + case CauseResponse::RSRC_NOT_AVAIL_TEMP: + return std::string("RSRC_NOT_AVAIL_TEMP (Resource Not Available - Temporary)"); + case CauseResponse::RSRC_NOT_AVAIL_QUEUED: + return std::string("RSRC_NOT_AVAIL_QUEUED (Resource Not Available - Queued)"); + case CauseResponse::SVC_UNAVAILABLE: + return std::string("SVC_UNAVAILABLE (Service Unavailable)"); + case CauseResponse::PROC_ERROR: + return std::string("PROC_ERROR (Procedure Error - Lack of packet data)"); + case CauseResponse::PROC_ERROR_UNDEF: + return std::string("PROC_ERROR_UNDEF (Procedure Error - Invalid packet data)"); + + case CauseResponse::VD_GRP_NOT_PERM: + return std::string("VD_GRP_NOT_PERM (Voice Group Not Permitted)"); + case CauseResponse::VD_REQ_UNIT_NOT_PERM: + return std::string("VD_REQ_UNIT_NOT_PERM (Voice Requesting Unit Not Permitted)"); + case CauseResponse::VD_TGT_UNIT_NOT_PERM: + return std::string("VD_TGT_UNIT_NOT_PERM (Voice Target Unit Not Permitted)"); + case CauseResponse::VD_REQ_UNIT_NOT_REG: + return std::string("VD_REQ_UNIT_NOT_REG (Voice Requesting Unit Not Registered)"); + case CauseResponse::VD_QUE_CHN_RESOURCE_NOT_AVAIL: + return std::string("VD_QUE_CHN_RESOURCE_NOT_AVAIL (Voice Channel Resources Unavailable)"); + case CauseResponse::VD_QUE_TGT_UNIT_BUSY: + return std::string("VD_QUE_TGT_UNIT_BUSY (Voice Target Unit Busy)"); + case CauseResponse::VD_QUE_GRP_BUSY: + return std::string("VD_QUE_GRP_BUSY (Voice Group Busy)"); + + case CauseResponse::DISC_USER: + return std::string("DISC_USER (Disconnect by User)"); + case CauseResponse::DISC_OTHER: + return std::string("DISC_OTHER (Other Disconnect)"); + + default: + return std::string(); + } +} diff --git a/src/common/nxdn/NXDNUtils.h b/src/common/nxdn/NXDNUtils.h index 6ceafb13..264931b2 100644 --- a/src/common/nxdn/NXDNUtils.h +++ b/src/common/nxdn/NXDNUtils.h @@ -5,7 +5,7 @@ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * Copyright (C) 2020 Jonathan Naylor, G4KLX - * Copyright (C) 2022 Bryan Biedenkapp, N2PLL + * Copyright (C) 2022,2025 Bryan Biedenkapp, N2PLL * */ /** @@ -41,6 +41,13 @@ namespace nxdn * @param data Buffer to apply post field bits to. */ static void addPostBits(uint8_t* data); + + /** + * @brief Helper to convert a cause code to a string. + * @param cause Cause code. + * @returns std::string Cause code string. + */ + static std::string causeToString(uint8_t cause); }; } // namespace nxdn diff --git a/src/common/nxdn/channel/CAC.cpp b/src/common/nxdn/channel/CAC.cpp index 87422b74..ab86a375 100644 --- a/src/common/nxdn/channel/CAC.cpp +++ b/src/common/nxdn/channel/CAC.cpp @@ -170,7 +170,7 @@ bool CAC::decode(const uint8_t* data, bool longInbound) } #if DEBUG_NXDN_CAC - Utils::dump(2U, "CAC::decode(), CAC Raw", buffer, NXDN_CAC_IN_FEC_LENGTH_BYTES); + Utils::dump(2U, "NXDN, CAC::decode(), CAC Raw", buffer, NXDN_CAC_IN_FEC_LENGTH_BYTES); #endif if (longInbound) { @@ -213,7 +213,7 @@ bool CAC::decode(const uint8_t* data, bool longInbound) conv.chainback(m_data, NXDN_CAC_LONG_CRC_LENGTH_BITS); #if DEBUG_NXDN_CAC - Utils::dump(2U, "Decoded Long CAC", m_data, (NXDN_CAC_LONG_CRC_LENGTH_BITS / 8U) + 1U); + Utils::dump(2U, "NXDN, CAC::decode(), Decoded Long CAC", m_data, (NXDN_CAC_LONG_CRC_LENGTH_BITS / 8U) + 1U); #endif // check CRC-16 @@ -271,7 +271,7 @@ bool CAC::decode(const uint8_t* data, bool longInbound) conv.chainback(m_data, NXDN_CAC_SHORT_CRC_LENGTH_BITS); #if DEBUG_NXDN_CAC - Utils::dump(2U, "Decoded CAC", m_data, (NXDN_CAC_SHORT_CRC_LENGTH_BITS / 8U) + 1U); + Utils::dump(2U, "NXDN, CAC::decode(), Decoded CAC", m_data, (NXDN_CAC_SHORT_CRC_LENGTH_BITS / 8U) + 1U); #endif // check CRC-16 @@ -298,7 +298,7 @@ bool CAC::decode(const uint8_t* data, bool longInbound) } #if DEBUG_NXDN_CAC - Utils::dump(2U, "Raw CAC Buffer", m_data, NXDN_CAC_FEC_LENGTH_BYTES); + Utils::dump(2U, "NXDN, CAC::decode(), Raw CAC Buffer", m_data, NXDN_CAC_FEC_LENGTH_BYTES); #endif return true; @@ -324,7 +324,7 @@ void CAC::encode(uint8_t* data) const uint16_t crc = edac::CRC::addCRC16(buffer, NXDN_CAC_LENGTH_BITS); #if DEBUG_NXDN_CAC - Utils::dump(2U, "Encoded CAC", buffer, NXDN_CAC_FEC_LENGTH_BYTES); + Utils::dump(2U, "NXDN, CAC::encode(), Encoded CAC", buffer, NXDN_CAC_FEC_LENGTH_BYTES); #endif // encode convolution @@ -357,7 +357,7 @@ void CAC::encode(uint8_t* data) const } #if DEBUG_NXDN_CAC - Utils::dump(2U, "CAC::encode(), CAC Puncture and Interleave", data, NXDN_FRAME_LENGTH_BYTES); + Utils::dump(2U, "NXDN, CAC::encode(), CAC Puncture and Interleave", data, NXDN_FRAME_LENGTH_BYTES); #endif // apply control field @@ -380,7 +380,7 @@ void CAC::encode(uint8_t* data) const } #if DEBUG_NXDN_CAC - Utils::dump(2U, "CAC::encode(), CAC + Control", data, NXDN_FRAME_LENGTH_BYTES); + Utils::dump(2U, "NXDN, CAC::encode(), CAC + Control", data, NXDN_FRAME_LENGTH_BYTES); #endif } diff --git a/src/common/nxdn/channel/FACCH1.cpp b/src/common/nxdn/channel/FACCH1.cpp index 94bb717d..d590d050 100644 --- a/src/common/nxdn/channel/FACCH1.cpp +++ b/src/common/nxdn/channel/FACCH1.cpp @@ -99,7 +99,7 @@ bool FACCH1::decode(const uint8_t* data, uint32_t offset) } #if DEBUG_NXDN_FACCH1 - Utils::dump(2U, "FACCH1::decode(), FACCH1 Raw", buffer, NXDN_FACCH1_FEC_LENGTH_BYTES); + Utils::dump(2U, "NXDN, FACCH1::decode(), FACCH1 Raw", buffer, NXDN_FACCH1_FEC_LENGTH_BYTES); #endif // depuncture @@ -137,7 +137,7 @@ bool FACCH1::decode(const uint8_t* data, uint32_t offset) conv.chainback(m_data, NXDN_FACCH1_CRC_LENGTH_BITS); #if DEBUG_NXDN_FACCH1 - Utils::dump(2U, "Decoded FACCH1", m_data, NXDN_FACCH1_CRC_LENGTH_BYTES); + Utils::dump(2U, "NXDN, FACCH1::decode(), Decoded FACCH1", m_data, NXDN_FACCH1_CRC_LENGTH_BYTES); #endif // check CRC-12 @@ -163,7 +163,7 @@ void FACCH1::encode(uint8_t* data, uint32_t offset) const edac::CRC::addCRC12(buffer, NXDN_FACCH1_LENGTH_BITS); #if DEBUG_NXDN_FACCH1 - Utils::dump(2U, "Encoded FACCH1", buffer, NXDN_FACCH1_CRC_LENGTH_BYTES); + Utils::dump(2U, "NXDN, FACCH1::encode(), Encoded FACCH1", buffer, NXDN_FACCH1_CRC_LENGTH_BYTES); #endif // encode convolution @@ -196,7 +196,7 @@ void FACCH1::encode(uint8_t* data, uint32_t offset) const } #if DEBUG_NXDN_SACCH - Utils::dump(2U, "FACCH1::encode(), FACCH1 Puncture and Interleave", data, NXDN_FACCH1_FEC_LENGTH_BYTES); + Utils::dump(2U, "NXDN, FACCH1::encode(), FACCH1 Puncture and Interleave", data, NXDN_FACCH1_FEC_LENGTH_BYTES); #endif } diff --git a/src/common/nxdn/channel/SACCH.cpp b/src/common/nxdn/channel/SACCH.cpp index 91f2221d..6aeb3a76 100644 --- a/src/common/nxdn/channel/SACCH.cpp +++ b/src/common/nxdn/channel/SACCH.cpp @@ -139,7 +139,7 @@ bool SACCH::decode(const uint8_t* data) conv.chainback(m_data, NXDN_SACCH_CRC_LENGTH_BITS); #if DEBUG_NXDN_SACCH - Utils::dump(2U, "Decoded SACCH", m_data, NXDN_SACCH_CRC_LENGTH_BYTES); + Utils::dump(2U, "SACCH::decode(), Decoded SACCH", m_data, NXDN_SACCH_CRC_LENGTH_BYTES); #endif // check CRC-6 @@ -178,7 +178,7 @@ void SACCH::encode(uint8_t* data) const edac::CRC::addCRC6(buffer, NXDN_SACCH_LENGTH_BITS); #if DEBUG_NXDN_SACCH - Utils::dump(2U, "Encoded SACCH", buffer, NXDN_SACCH_CRC_LENGTH_BYTES); + Utils::dump(2U, "SACCH::encode(), Encoded SACCH", buffer, NXDN_SACCH_CRC_LENGTH_BYTES); #endif // encode convolution diff --git a/src/common/nxdn/channel/UDCH.cpp b/src/common/nxdn/channel/UDCH.cpp index c42fc6a6..92aa821f 100644 --- a/src/common/nxdn/channel/UDCH.cpp +++ b/src/common/nxdn/channel/UDCH.cpp @@ -125,7 +125,7 @@ bool UDCH::decode(const uint8_t* data) } #if DEBUG_NXDN_UDCH - Utils::dump(2U, "UDCH::decode(), UDCH Raw", buffer, NXDN_UDCH_FEC_LENGTH_BYTES); + Utils::dump(2U, "NXDN, UDCH::decode(), UDCH Raw", buffer, NXDN_UDCH_FEC_LENGTH_BYTES); #endif // depuncture @@ -163,7 +163,7 @@ bool UDCH::decode(const uint8_t* data) conv.chainback(m_data, NXDN_UDCH_CRC_LENGTH_BITS); #if DEBUG_NXDN_UDCH - Utils::dump(2U, "Decoded UDCH", m_data, NXDN_UDCH_CRC_LENGTH_BYTES); + Utils::dump(2U, "NXDN, UDCH::decode(), Decoded UDCH", m_data, NXDN_UDCH_CRC_LENGTH_BYTES); #endif // check CRC-15 @@ -193,7 +193,7 @@ void UDCH::encode(uint8_t* data) const edac::CRC::addCRC15(buffer, NXDN_UDCH_LENGTH_BITS); #if DEBUG_NXDN_UDCH - Utils::dump(2U, "Encoded UDCH", m_data, NXDN_UDCH_CRC_LENGTH_BYTES); + Utils::dump(2U, "NXDN, UDCH::encode(), Encoded UDCH", m_data, NXDN_UDCH_CRC_LENGTH_BYTES); #endif // encode convolution @@ -226,7 +226,7 @@ void UDCH::encode(uint8_t* data) const } #if DEBUG_NXDN_UDCH - Utils::dump(2U, "UDCH::encode(), UDCH Puncture and Interleave", data, NXDN_UDCH_FEC_LENGTH_BYTES); + Utils::dump(2U, "NXDN, UDCH::encode(), UDCH Puncture and Interleave", data, NXDN_UDCH_FEC_LENGTH_BYTES); #endif } diff --git a/src/common/nxdn/lc/RCCH.cpp b/src/common/nxdn/lc/RCCH.cpp index f409f42d..b5a69671 100644 --- a/src/common/nxdn/lc/RCCH.cpp +++ b/src/common/nxdn/lc/RCCH.cpp @@ -115,7 +115,7 @@ void RCCH::decode(const uint8_t* data, uint8_t* rcch, uint32_t length, uint32_t } if (m_verbose) { - Utils::dump(2U, "Decoded RCCH Data", rcch, NXDN_RCCH_LC_LENGTH_BYTES); + Utils::dump(2U, "NXDN, RCCH::decode(), Decoded RCCH Data", rcch, NXDN_RCCH_LC_LENGTH_BYTES); } m_messageType = data[0U] & 0x3FU; // Message Type @@ -138,7 +138,7 @@ void RCCH::encode(uint8_t* data, const uint8_t* rcch, uint32_t length, uint32_t } if (m_verbose) { - Utils::dump(2U, "Encoded RCCH Data", data, NXDN_RCCH_LC_LENGTH_BYTES); + Utils::dump(2U, "NXDN, RCCH::encode(), Encoded RCCH Data", data, NXDN_RCCH_LC_LENGTH_BYTES); } } diff --git a/src/common/nxdn/lc/RTCH.cpp b/src/common/nxdn/lc/RTCH.cpp index 01fbd954..a476dc62 100644 --- a/src/common/nxdn/lc/RTCH.cpp +++ b/src/common/nxdn/lc/RTCH.cpp @@ -114,7 +114,7 @@ void RTCH::decode(const uint8_t* data, uint32_t length, uint32_t offset) } if (m_verbose) { - Utils::dump(2U, "Decoded RTCH Data", rtch, NXDN_RTCH_LC_LENGTH_BYTES); + Utils::dump(2U, "NXDN, RTCH::decode(), Decoded RTCH Data", rtch, NXDN_RTCH_LC_LENGTH_BYTES); } decodeLC(rtch); @@ -137,7 +137,7 @@ void RTCH::encode(uint8_t* data, uint32_t length, uint32_t offset) } if (m_verbose) { - Utils::dump(2U, "Encoded RTCH Data", data, length); + Utils::dump(2U, "NXDN, RTCH::encode(), Encoded RTCH Data", data, length); } } @@ -181,6 +181,10 @@ bool RTCH::decodeLC(const uint8_t* data) { assert(data != nullptr); +#if DEBUG_NXDN_RTCH + Utils::dump(2U, "NXDN, RTCH::decodeLC(), RTCH", data, NXDN_RTCH_LC_LENGTH_BYTES); +#endif + m_messageType = data[0U] & 0x3FU; // Message Type // message type opcodes @@ -286,6 +290,12 @@ bool RTCH::decodeLC(const uint8_t* data) return false; } + // is this a private call? + if (m_callType == CallType::INDIVIDUAL) + m_group = false; + else + m_group = true; + return true; } @@ -405,6 +415,10 @@ void RTCH::encodeLC(uint8_t* data) LogError(LOG_NXDN, "RTCH::encodeRTCH(), unknown RTCH value, messageType = $%02X", m_messageType); return; } + +#if DEBUG_NXDN_RTCH + Utils::dump(2U, "NXDN, RTCH::encodeLC(), RTCH", data, NXDN_RTCH_LC_LENGTH_BYTES); +#endif } /* Internal helper to copy the the class. */ diff --git a/src/common/nxdn/lc/rcch/MESSAGE_TYPE_REG.cpp b/src/common/nxdn/lc/rcch/MESSAGE_TYPE_REG.cpp index b6f496ed..3b3002f9 100644 --- a/src/common/nxdn/lc/rcch/MESSAGE_TYPE_REG.cpp +++ b/src/common/nxdn/lc/rcch/MESSAGE_TYPE_REG.cpp @@ -22,12 +22,14 @@ using namespace nxdn::lc::rcch; // --------------------------------------------------------------------------- /* Initializes a new instance of the MESSAGE_TYPE_REG class. */ + MESSAGE_TYPE_REG::MESSAGE_TYPE_REG() : RCCH() { m_messageType = MessageType::RCCH_REG; } /* Decode RCCH data. */ + void MESSAGE_TYPE_REG::decode(const uint8_t* data, uint32_t length, uint32_t offset) { assert(data != nullptr); @@ -49,6 +51,7 @@ void MESSAGE_TYPE_REG::decode(const uint8_t* data, uint32_t length, uint32_t off } /* Encode RCCH data. */ + void MESSAGE_TYPE_REG::encode(uint8_t* data, uint32_t length, uint32_t offset) { assert(data != nullptr); @@ -73,6 +76,7 @@ void MESSAGE_TYPE_REG::encode(uint8_t* data, uint32_t length, uint32_t offset) } /* Returns a string that represents the current RCCH. */ + std::string MESSAGE_TYPE_REG::toString(bool isp) { return (isp) ? std::string("RCCH_REG (Registration Request)") : diff --git a/src/common/p25/P25Defines.h b/src/common/p25/P25Defines.h index b88e56a1..1b7ff9cb 100644 --- a/src/common/p25/P25Defines.h +++ b/src/common/p25/P25Defines.h @@ -112,6 +112,7 @@ namespace p25 const uint8_t ENCRYPTED_NULL_IMBE[] = { 0xFCU, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U }; const uint8_t MOT_CALLSIGN_LENGTH_BYTES = 8U; + const uint8_t HARRIS_USER_ALIAS_LENGTH_BYTES = 14U; const uint8_t AUTH_RES_LENGTH_BYTES = 4U; const uint8_t AUTH_RAND_SEED_LENGTH_BYTES = 10U; @@ -138,6 +139,8 @@ namespace p25 /** @brief Motorola MFId */ const uint8_t MFG_MOT = 0x90U; + /** @brief L3Harris MFId */ + const uint8_t MFG_HARRIS = 0xA4U; /** @brief DVM; Omaha Communication Systems, LLC ($9C) */ const uint8_t MFG_DVM_OCS = 0x9CU; /** @} */ @@ -683,6 +686,7 @@ namespace p25 TEL_INT_VCH_USER = 0x06U, //! TEL INT VCH USER - Telephone Interconnect Voice Channel User / MOT GPS DATA - Motorola In-Band GPS Data TEL_INT_ANS_RQST = 0x07U, //! TEL INT ANS RQST - Telephone Interconnect Answer Request EXPLICIT_SOURCE_ID = 0x09U, //! EXPLICIT SOURCE ID - Explicit Source ID + PRIVATE_EXT = 0x0AU, //! UU VCH USER EXT - Unit-to-Unit Voice Channel User Extended CALL_TERM = 0x0FU, //! CALL TERM - Call Termination or Cancellation IDEN_UP = 0x18U, //! IDEN UP - Channel Identifier Update SYS_SRV_BCAST = 0x20U, //! SYS SRV BCAST - System Service Broadcast @@ -692,7 +696,21 @@ namespace p25 CONV_FALLBACK = 0x2AU, //! CONV FALLBACK - Conventional Fallback // LDUx/TDULC Motorola Link Control Opcode(s) - FAILSOFT = 0x02U //! FAILSOFT - Failsoft + FAILSOFT = 0x02U, //! FAILSOFT - Failsoft + + MOT_PTT_LOC_HEADER = 0x29U, //! MOT PTT LOC HEADER - Motorola PTT Location Header + MOT_PTT_LOC_PAYLOAD = 0x2AU, //! MOT PTT LOC PAYLOAD - Motorola PTT Location Payload + + // LDUx/TDULC Harris Link Control Opcode(s) + HARRIS_PTT_PA_ODD = 0x2AU, //! HARRIS PTT PA ODD - Harris PTT Position and Altitude Odd + HARRIS_PTT_PB_ODD = 0x2BU, //! HARRIS PTT PB ODD - Harris PTT Position and Bearing Odd + HARRIS_PTT_PA_EVEN = 0x2CU, //! HARRIS PTT PA EVEN - Harris PTT Position and Altitude Even + HARRIS_PTT_PB_EVEN = 0x2DU, //! HARRIS PTT PB EVEN - Harris PTT Position and Bearing Even + + HARRIS_USER_ALIAS_PA_ODD = 0x32U, //! HARRIS USER ALIAS PA ODD - Harris User Alias Position and Altitude Odd + HARRIS_USER_ALIAS_PB_ODD = 0x33U, //! HARRIS USER ALIAS PB ODD - Harris User Alias Position and Bearing Odd + HARRIS_USER_ALIAS_PA_EVEN = 0x34U, //! HARRIS USER ALIAS PA EVEN - Harris User Alias Position and Altitude Even + HARRIS_USER_ALIAS_PB_EVEN = 0x35U, //! HARRIS USER ALIAS PB EVEN - Harris User Alias Position and Bearing Even }; } diff --git a/src/common/p25/P25Utils.cpp b/src/common/p25/P25Utils.cpp index e7b7c1ff..6ac0adf9 100644 --- a/src/common/p25/P25Utils.cpp +++ b/src/common/p25/P25Utils.cpp @@ -5,7 +5,7 @@ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * Copyright (C) 2016 Jonathan Naylor, G4KLX - * Copyright (C) 2024 Bryan Biedenkapp, N2PLL + * Copyright (C) 2024-2025 Bryan Biedenkapp, N2PLL * */ #include "Defines.h" @@ -242,3 +242,71 @@ uint32_t P25Utils::compare(const uint8_t* data1, const uint8_t* data2, uint32_t return errs; } + +/* Helper to convert a denial reason code to a string. */ + +std::string P25Utils::denyRsnToString(uint8_t reason) +{ + switch (reason) { + case ReasonCode::DENY_REQ_UNIT_NOT_VALID: + return std::string("DENY_REQ_UNIT_NOT_VALID (Requesting Unit Not Valid)"); + case ReasonCode::DENY_REQ_UNIT_NOT_AUTH: + return std::string("DENY_REQ_UNIT_NOT_AUTH (Requesting Unit Not Authenticated)"); + + case ReasonCode::DENY_TGT_UNIT_NOT_VALID: + return std::string("DENY_TGT_UNIT_NOT_VALID (Target Unit Not Valid)"); + case ReasonCode::DENY_TGT_UNIT_NOT_AUTH: + return std::string("DENY_TGT_UNIT_NOT_AUTH (Target Unit Not Authenticated)"); + case ReasonCode::DENY_SU_FAILED_AUTH: + return std::string("DENY_SU_FAILED_AUTH (Target Unit Failed Authentication)"); + case ReasonCode::DENY_TGT_UNIT_REFUSED: + return std::string("DENY_TGT_UNIT_REFUSED (Target Unit Refused)"); + + case ReasonCode::DENY_TGT_GROUP_NOT_VALID: + return std::string("DENY_TGT_GROUP_NOT_VALID (Target Group Not Valid)"); + case ReasonCode::DENY_TGT_GROUP_NOT_AUTH: + return std::string("DENY_TGT_GROUP_NOT_AUTH (Target Group Not Authenticated)"); + + case ReasonCode::DENY_NO_NET_RSRC_AVAIL: + return std::string("DENY_NO_NET_RSRC_AVAIL (Requested Network Resources Not Available)"); + case ReasonCode::DENY_NO_RF_RSRC_AVAIL: + return std::string("DENY_NO_RF_RSRC_AVAIL (Requested RF Resources Not Available)"); + case ReasonCode::DENY_SVC_IN_USE: + return std::string("DENY_SVC_IN_USE (Service In Use)"); + + case ReasonCode::DENY_SITE_ACCESS_DENIAL: + return std::string("DENY_SITE_ACCESS_DENIAL (Site Access Denial)"); + + case ReasonCode::DENY_PTT_COLLIDE: + return std::string("DENY_PTT_COLLIDE (Push-to-Talk Collision)"); + case ReasonCode::DENY_PTT_BONK: + return std::string("DENY_PTT_BONK (Push-to-Talk Denial/Bonk)"); + + case ReasonCode::DENY_SYS_UNSUPPORTED_SVC: + return std::string("DENY_SYS_UNSUPPORTED_SVC (Service Unsupported)"); + + default: + return std::string(); + } +} + +/* Helper to convert a queue reason code to a string. */ + +std::string P25Utils::queueRsnToString(uint8_t reason) +{ + switch (reason) { + case ReasonCode::QUE_REQ_ACTIVE_SERVICE: + return std::string("QUE_REQ_ACTIVE_SERVICE (Requested Service Active)"); + case ReasonCode::QUE_TGT_ACTIVE_SERVICE: + return std::string("QUE_TGT_ACTIVE_SERVICE (Target Service Active)"); + + case ReasonCode::QUE_TGT_UNIT_QUEUED: + return std::string("QUE_TGT_UNIT_QUEUED (Target Unit Queued)"); + + case ReasonCode::QUE_CHN_RESOURCE_NOT_AVAIL: + return std::string("QUE_CHN_RESOURCE_NOT_AVAIL (Channel Resource Not Available)"); + + default: + return std::string(); + } +} diff --git a/src/common/p25/P25Utils.h b/src/common/p25/P25Utils.h index bc030a43..66b4cb27 100644 --- a/src/common/p25/P25Utils.h +++ b/src/common/p25/P25Utils.h @@ -5,7 +5,7 @@ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * Copyright (C) 2016 Jonathan Naylor, G4KLX - * Copyright (C) 2021 Bryan Biedenkapp, N2PLL + * Copyright (C) 2021-2025 Bryan Biedenkapp, N2PLL * */ /** @@ -198,6 +198,19 @@ namespace p25 * @returns uint32_t */ static uint32_t compare(const uint8_t* data1, const uint8_t* data2, uint32_t length); + + /** + * @brief Helper to convert a denial reason code to a string. + * @param reason Reason code. + * @returns std::string Reason code string. + */ + static std::string denyRsnToString(uint8_t reason); + /** + * @brief Helper to convert a queue reason code to a string. + * @param reason Reason code. + * @returns std::string Reason code string. + */ + static std::string queueRsnToString(uint8_t reason); }; } // namespace p25 diff --git a/src/common/p25/acl/AccessControl.cpp b/src/common/p25/acl/AccessControl.cpp index 881902bc..2c0fd791 100644 --- a/src/common/p25/acl/AccessControl.cpp +++ b/src/common/p25/acl/AccessControl.cpp @@ -53,12 +53,16 @@ bool AccessControl::validateSrcId(uint32_t id) /* Helper to validate a talkgroup ID. */ -bool AccessControl::validateTGId(uint32_t id) +bool AccessControl::validateTGId(uint32_t id, bool allowZero) { // TG0 is never valid - if (id == 0U) + if (id == 0U && !allowZero) return false; + // TG0 is always valid if allow zero is set + if (id == 0U && allowZero) + return true; + // check if TID ACLs are enabled if (!m_tidLookup->getACL()) { return true; diff --git a/src/common/p25/acl/AccessControl.h b/src/common/p25/acl/AccessControl.h index b14593b1..7f1baf76 100644 --- a/src/common/p25/acl/AccessControl.h +++ b/src/common/p25/acl/AccessControl.h @@ -54,9 +54,10 @@ namespace p25 /** * @brief Helper to validate a talkgroup ID. * @param id Talkgroup ID (TGID). + * @param allowZero Flag indicating whether TGID 0 is allowed or not. * @returns bool True, if talkgroup ID is valid, otherwise false. */ - static bool validateTGId(uint32_t id); + static bool validateTGId(uint32_t id, bool allowZero = false); /** * @brief Helper to determine if a talkgroup ID is non-preferred. diff --git a/src/common/p25/data/DataBlock.cpp b/src/common/p25/data/DataBlock.cpp index cf6b44bc..e750ef32 100644 --- a/src/common/p25/data/DataBlock.cpp +++ b/src/common/p25/data/DataBlock.cpp @@ -128,7 +128,7 @@ bool DataBlock::decode(const uint8_t* data, const DataHeader& header) ::memcpy(m_data, buffer, P25_PDU_UNCONFIRMED_LENGTH_BYTES); // Payload Data } catch (...) { - Utils::dump(2U, "P25, decoding excepted with input data", data, P25_PDU_UNCONFIRMED_LENGTH_BYTES); + Utils::dump(2U, "P25, DataBlock::decode(), decoding excepted with input data", data, P25_PDU_UNCONFIRMED_LENGTH_BYTES); return false; } } diff --git a/src/common/p25/data/DataHeader.cpp b/src/common/p25/data/DataHeader.cpp index 923ded10..211953da 100644 --- a/src/common/p25/data/DataHeader.cpp +++ b/src/common/p25/data/DataHeader.cpp @@ -225,6 +225,9 @@ void DataHeader::encode(uint8_t* data, bool noTrellis) header[1U] = ((m_rspClass & 0x03U) << 6) + // Response Class ((m_rspType & 0x07U) << 3) + // Response Type ((m_rspStatus & 0x07U)); // Response Status + + // the "full message" flag in a response PDU indicates whether or not the response is to an + // extended addressing PDU if (!m_F) { header[7U] = (m_srcLlId >> 16) & 0xFFU; // Source Logical Link ID header[8U] = (m_srcLlId >> 8) & 0xFFU; diff --git a/src/common/p25/data/DataHeader.h b/src/common/p25/data/DataHeader.h index 97164d6b..d59c8c5c 100644 --- a/src/common/p25/data/DataHeader.h +++ b/src/common/p25/data/DataHeader.h @@ -160,6 +160,8 @@ namespace p25 DECLARE_PROPERTY(uint8_t, padLength, PadLength); /** * @brief Flag indicating whether or not this data packet is a full message. + * @note This is used on extended addressing response packets to indicate whether or not + * the response is for a extended addressing request. */ DECLARE_PROPERTY(bool, F, FullMessage); /** @@ -183,7 +185,7 @@ namespace p25 */ DECLARE_PROPERTY(uint8_t, headerOffset, HeaderOffset); - // Extended Addressing Data + /** @name Symmetric Addressing Data */ /** * @brief Service access point. */ @@ -192,8 +194,9 @@ namespace p25 * @brief Source Logical link ID. */ DECLARE_PROPERTY(uint32_t, srcLlId, SrcLLId); + /** @} */ - // Response Data + /** @name Response Packet Data */ /** * @brief Response class. */ @@ -206,8 +209,9 @@ namespace p25 * @brief Response status. */ DECLARE_PROPERTY(uint8_t, rspStatus, ResponseStatus); + /** @} */ - // AMBT Data + /** @name AMBT Packet Data */ /** * @brief Alternate Trunking Block Opcode */ @@ -220,6 +224,7 @@ namespace p25 * @brief Alternate Trunking Block Field 9 */ DECLARE_PROPERTY(uint8_t, ambtField9, AMBTField9); + /** @} */ private: edac::Trellis m_trellis; diff --git a/src/common/p25/data/LowSpeedData.cpp b/src/common/p25/data/LowSpeedData.cpp index 83891b49..1cd6c111 100644 --- a/src/common/p25/data/LowSpeedData.cpp +++ b/src/common/p25/data/LowSpeedData.cpp @@ -106,7 +106,7 @@ void LowSpeedData::process(uint8_t* data) } } - // Utils::dump(1U, "P25 Low Speed Data", lsd, 4U); + // Utils::dump(1U, "P25, LowSpeedData::process(), Low Speed Data", lsd, 4U); m_lsd1 = lsd[0U]; m_lsd2 = lsd[2U]; diff --git a/src/common/p25/dfsi/DFSIDefines.h b/src/common/p25/dfsi/DFSIDefines.h index bf78aff6..9630cd99 100644 --- a/src/common/p25/dfsi/DFSIDefines.h +++ b/src/common/p25/dfsi/DFSIDefines.h @@ -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-2025 Bryan Biedenkapp, N2PLL * */ /** @@ -43,6 +43,16 @@ namespace p25 const uint32_t DFSI_VHDR_RAW_LEN = 36U; const uint32_t DFSI_VHDR_LEN = 27U; + const uint32_t DFSI_MOT_START_LEN = 9U; + const uint32_t DFSI_MOT_VHDR_1_LEN = 30U; + const uint32_t DFSI_MOT_VHDR_2_LEN = 22U; + const uint32_t DFSI_MOT_TSBK_LEN = 24U; + const uint32_t DFSI_MOT_TDULC_LEN = 21U; + + const uint32_t DFSI_TIA_VHDR_LEN = 22U; + + const uint32_t DFSI_MOT_ICW_LENGTH = 6U; + const uint32_t DFSI_LDU1_VOICE1_FRAME_LENGTH_BYTES = 22U; const uint32_t DFSI_LDU1_VOICE2_FRAME_LENGTH_BYTES = 14U; const uint32_t DFSI_LDU1_VOICE3_FRAME_LENGTH_BYTES = 17U; @@ -68,31 +78,35 @@ namespace p25 * @{ */ - const uint8_t DFSI_RTP_PAYLOAD_TYPE = 0x64U; //! - - const uint8_t DFSI_STATUS_NO_ERROR = 0x00U; //! - const uint8_t DFSI_STATUS_ERASE = 0x02U; //! + const uint8_t DFSI_RTP_PAYLOAD_TYPE = 0x64U; //! + const uint8_t DFSI_RTP_MOT_PAYLOAD_TYPE = 0x5DU; //! - const uint8_t DFSI_RT_ENABLED = 0x02U; //! - const uint8_t DFSI_RT_DISABLED = 0x04U; //! + const uint8_t DFSI_RTP_SEQ_HANDSHAKE = 0x00U; //! + const uint8_t DFSI_RTP_SEQ_STARTSTOP = 0x01U; //! - const uint8_t DFSI_START_FLAG = 0x0CU; //! - const uint8_t DFSI_STOP_FLAG = 0x25U; //! + const uint8_t DFSI_MOT_ICW_FMT_TYPE3 = 0x02U; //! - const uint8_t DFSI_TYPE_DATA_PAYLOAD = 0x06U; //! - const uint8_t DFSI_TYPE_VOICE = 0x0BU; //! + const uint8_t DFSI_MOT_ICW_PARM_NOP = 0x00U; //! No Operation + const uint8_t DSFI_MOT_ICW_PARM_PAYLOAD = 0x0CU; //! Stream Payload + const uint8_t DFSI_MOT_ICW_PARM_RSSI1 = 0x1AU; //! RSSI Data + const uint8_t DFSI_MOT_ICW_PARM_RSSI2 = 0x1BU; //! RSSI Data + const uint8_t DFSI_MOT_ICW_PARM_STOP = 0x25U; //! Stop Stream + const uint8_t DFSI_MOT_ICW_TX_ADDRESS = 0x2CU; //! Tx Device Address + const uint8_t DFSI_MOT_ICW_RX_ADDRESS = 0x35U; //! Rx Device Address - const uint8_t DFSI_DEF_ICW_SOURCE = 0x00U; //! Infrastructure Source - Default Source - const uint8_t DFSI_DEF_SOURCE = 0x00U; //! + const uint8_t DFSI_BUSY_BITS_TALKAROUND = 0x00U; //! Talkaround + const uint8_t DFSI_BUSY_BITS_BUSY = 0x01U; //! Busy + const uint8_t DFSI_BUSY_BITS_INBOUND = 0x02U; //! Inbound + const uint8_t DFSI_BUSY_BITS_IDLE = 0x03U; //! Idle /** @brief DFSI Frame Type */ namespace DFSIFrameType { /** @brief DFSI Frame Type */ enum E : uint8_t { - MOT_START_STOP = 0x00U, // Motorola Start/Stop + MOT_START_STOP = 0x00U, // Motorola/V.24 Start/Stop Stream - MOT_VHDR_1 = 0x60U, // Motorola Voice Header 1 - MOT_VHDR_2 = 0x61U, // Motorola Voice Header 2 + MOT_VHDR_1 = 0x60U, // Motorola/V.24 Voice Header 1 + MOT_VHDR_2 = 0x61U, // Motorola/V.24 Voice Header 2 LDU1_VOICE1 = 0x62U, // IMBE LDU1 - Voice 1 LDU1_VOICE2 = 0x63U, // IMBE LDU1 - Voice 2 @@ -114,8 +128,9 @@ namespace p25 LDU2_VOICE17 = 0x72U, // IMBE LDU2 - Voice 17 + Encryption Sync LDU2_VOICE18 = 0x73U, // IMBE LDU2 - Voice 18 + Low Speed Data - PDU = 0x87U, // PDU - TSBK = 0xA1U // TSBK + MOT_TDULC = 0x74U, // Motorola/V.24 TDULC + MOT_PDU_SINGLE = 0x87U, // Motorola/V.24 PDU (Single Block) + MOT_TSBK = 0xA1U // Motorola/V.24 TSBK (Single Block) }; } diff --git a/src/common/p25/dfsi/LC.cpp b/src/common/p25/dfsi/LC.cpp index bfd9cfe2..eaa3ee3a 100644 --- a/src/common/p25/dfsi/LC.cpp +++ b/src/common/p25/dfsi/LC.cpp @@ -404,8 +404,8 @@ void LC::encodeLDU1(uint8_t* data, const uint8_t* imbe) } #if DEBUG_P25_DFSI - LogDebugEx(LOG_P25, "LC::encodeLDU1()", "frameType = $%02X", m_frameType); - Utils::dump(2U, "[LC::encodeLDU1()] DFSI LDU1 Frame", dfsiFrame, frameLength); + LogDebugEx(LOG_P25, "dfsi::LC::encodeLDU1()", "frameType = $%02X", m_frameType); + Utils::dump(2U, "P25, dfsi::LC::encodeLDU1(), DFSI LDU1 Frame", dfsiFrame, frameLength); #endif ::memcpy(data, dfsiFrame, frameLength); @@ -644,8 +644,8 @@ void LC::encodeLDU2(uint8_t* data, const uint8_t* imbe) } #if DEBUG_P25_DFSI - LogDebugEx(LOG_P25, "LC::encodeLDU2()", "frameType = $%02X", m_frameType); - Utils::dump(2U, "[LC::encodeLDU2()] DFSI LDU2 Frame", dfsiFrame, frameLength); + LogDebugEx(LOG_P25, "dfsi::LC::encodeLDU2()", "frameType = $%02X", m_frameType); + Utils::dump(2U, "P25, dfsi::LC::encodeLDU2(), DFSI LDU2 Frame", dfsiFrame, frameLength); #endif ::memcpy(data, dfsiFrame, frameLength); diff --git a/src/common/p25/dfsi/frames/FrameDefines.h b/src/common/p25/dfsi/frames/FrameDefines.h index a2db0b9e..eb8c01bf 100644 --- a/src/common/p25/dfsi/frames/FrameDefines.h +++ b/src/common/p25/dfsi/frames/FrameDefines.h @@ -5,7 +5,7 @@ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * Copyright (C) 2024 Patrick McDonnell, W3AXL - * Copyright (C) 2024 Bryan Biedenkapp, N2PLL + * Copyright (C) 2024-2025 Bryan Biedenkapp, N2PLL * */ /** @@ -91,57 +91,23 @@ namespace p25 }; } - /** @brief RT/RT Flag */ - namespace RTFlag { - /** @brief RT/RT Flag */ + /** @brief Motorola Start of Stream Operation */ + namespace MotStartStreamOpcode { + /** @brief Motorola Start of Stream Operation */ enum E : uint8_t { - ENABLED = 0x02U, //! RT/RT Enabled - DISABLED = 0x04U //! RT/RT Disabled + TRANSMIT = 0x02U, //! Transmit + RECEIVE = 0x04U, //! Receive }; } - /** @brief Start/Stop Flag */ - namespace StartStopFlag { - /** @brief Start/Stop Flag */ + /** @brief Motorola Stream Payload */ + namespace MotStreamPayload { + /** @brief Motorola Stream Payload */ enum E : uint8_t { - START = 0x0CU, //! Start - STOP = 0x25U //! Stop - }; - } - - /** @brief V.24 Data Stream Type */ - namespace StreamTypeFlag { - /** @brief V.24 Data Stream Type */ - enum E : uint8_t { - VOICE = 0x0BU, //! Voice - TSBK = 0x0FU //! TSBK - }; - } - - /** @brief RSSI Data Validity */ - namespace RssiValidityFlag { - /** @brief RSSI Data Validity */ - enum E : uint8_t { - INVALID = 0x00U, //! Invalid - VALID = 0x1A //! Valid - }; - } - - /** @brief V.24 Data Source */ - namespace SourceFlag { - /** @brief V.24 Data Source */ - enum E : uint8_t { - DIU = 0x00U, //! DIU - QUANTAR = 0x02U //! Quantar - }; - } - - /** @brief */ - namespace ICWFlag { - /** @brief */ - enum E : uint8_t { - DIU = 0x00U, //! DIU - QUANTAR = 0x1B //! Quantar + VOICE = 0x0BU, //! P25 Voice + DATA = 0x0CU, //! P25 Data + TERM_LC = 0x0EU, //! P25 Termination Link Control + TSBK = 0x0FU //! P25 TSBK }; } /** @} */ diff --git a/src/common/p25/dfsi/frames/Frames.h b/src/common/p25/dfsi/frames/Frames.h index 7779b8d1..498546e4 100644 --- a/src/common/p25/dfsi/frames/Frames.h +++ b/src/common/p25/dfsi/frames/Frames.h @@ -5,7 +5,7 @@ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * Copyright (C) 2024 Patrick McDonnell, W3AXL - * Copyright (C) 2024 Bryan Biedenkapp, N2PLL + * Copyright (C) 2024-2025 Bryan Biedenkapp, N2PLL * */ #if !defined(__DFSI_FRAMES_H__) @@ -20,13 +20,11 @@ #include "common/p25/dfsi/frames/FullRateVoice.h" // "The" Manufacturer -#include "common/p25/dfsi/frames/MotFullRateVoice.h" #include "common/p25/dfsi/frames/MotStartOfStream.h" #include "common/p25/dfsi/frames/MotStartVoiceFrame.h" -#include "common/p25/dfsi/frames/MotVoiceHeader1.h" -#include "common/p25/dfsi/frames/MotVoiceHeader2.h" +#include "common/p25/dfsi/frames/MotFullRateVoice.h" +#include "common/p25/dfsi/frames/MotTDULCFrame.h" #include "common/p25/dfsi/frames/MotTSBKFrame.h" -#include "common/p25/dfsi/frames/MotPDUFrame.h" // FSC #include "common/p25/dfsi/frames/fsc/FSCMessage.h" diff --git a/src/common/p25/dfsi/frames/FullRateVoice.cpp b/src/common/p25/dfsi/frames/FullRateVoice.cpp index d334e84f..b9c67707 100644 --- a/src/common/p25/dfsi/frames/FullRateVoice.cpp +++ b/src/common/p25/dfsi/frames/FullRateVoice.cpp @@ -34,7 +34,7 @@ FullRateVoice::FullRateVoice() : m_muteFrame(false), m_lostFrame(false), m_superframeCnt(0U), - m_busy(0U) + m_busy(DFSI_BUSY_BITS_TALKAROUND) { imbeData = new uint8_t[IMBE_BUF_LEN]; ::memset(imbeData, 0x00U, IMBE_BUF_LEN); @@ -52,7 +52,7 @@ FullRateVoice::FullRateVoice(uint8_t* data) : m_muteFrame(false), m_lostFrame(false), m_superframeCnt(0U), - m_busy(0U) + m_busy(DFSI_BUSY_BITS_TALKAROUND) { decode(data); } diff --git a/src/common/p25/dfsi/frames/MotFullRateVoice.cpp b/src/common/p25/dfsi/frames/MotFullRateVoice.cpp index 542ff327..55279558 100644 --- a/src/common/p25/dfsi/frames/MotFullRateVoice.cpp +++ b/src/common/p25/dfsi/frames/MotFullRateVoice.cpp @@ -5,7 +5,7 @@ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * Copyright (C) 2024 Patrick McDonnell, W3AXL - * Copyright (C) 2024 Bryan Biedenkapp, N2PLL + * Copyright (C) 2024-2025 Bryan Biedenkapp, N2PLL * */ #include "common/p25/dfsi/frames/MotFullRateVoice.h" @@ -33,7 +33,8 @@ MotFullRateVoice::MotFullRateVoice() : imbeData(nullptr), additionalData(nullptr), m_frameType(DFSIFrameType::LDU1_VOICE1), - m_source(SourceFlag::QUANTAR) + m_totalErrors(0U), + m_busy(DFSI_BUSY_BITS_TALKAROUND) { imbeData = new uint8_t[RAW_IMBE_LENGTH_BYTES]; ::memset(imbeData, 0x00U, RAW_IMBE_LENGTH_BYTES); @@ -41,11 +42,17 @@ MotFullRateVoice::MotFullRateVoice() : /* Initializes a instance of the MotFullRateVoice class. */ -MotFullRateVoice::MotFullRateVoice(uint8_t* data) +MotFullRateVoice::MotFullRateVoice(uint8_t* data) : + imbeData(nullptr), + additionalData(nullptr), + m_frameType(DFSIFrameType::LDU1_VOICE1), + m_totalErrors(0U), + m_busy(DFSI_BUSY_BITS_TALKAROUND) { // set our pointers to null since it doesn't get initialized otherwise imbeData = nullptr; additionalData = nullptr; + // decode decode(data); } @@ -100,11 +107,14 @@ bool MotFullRateVoice::decode(const uint8_t* data, bool shortened) if (shortened) { ::memcpy(imbeData, data + 1U, RAW_IMBE_LENGTH_BYTES); - m_source = (SourceFlag::E)data[12U]; - // Forgot to set this originally and left additionalData uninitialized, whoops! + + m_totalErrors = (uint8_t)((data[12U] >> 3) & 0x0FU); // Total Errors + m_busy = (uint8_t)(data[13U] & 0x03U); // Busy Status + + // forgot to set this originally and left additionalData uninitialized, whoops! additionalData = nullptr; } else { - // Frames 0x6A and 0x73 are missing the 0x00 padding byte, so we start IMBE data 1 byte earlier + // frames $6A and $73 are missing the 0x00 padding byte, so we start IMBE data 1 byte earlier uint8_t imbeStart = 5U; if (isVoice9or18()) { imbeStart = 4U; @@ -119,7 +129,13 @@ bool MotFullRateVoice::decode(const uint8_t* data, bool shortened) // copy IMBE data based on our imbe start position ::memcpy(imbeData, data + imbeStart, RAW_IMBE_LENGTH_BYTES); - m_source = (SourceFlag::E)data[RAW_IMBE_LENGTH_BYTES + imbeStart]; + if (isVoice9or18()) { + m_totalErrors = 0U; // these frames don't have total errors + m_busy = (uint8_t)(data[3U] & 0x03U); // Busy Status + } else { + m_totalErrors = (uint8_t)((data[imbeStart + RAW_IMBE_LENGTH_BYTES] >> 2) & 0x0FU); // Total Errors + m_busy = (uint8_t)(data[imbeStart + RAW_IMBE_LENGTH_BYTES] & 0x03U); // Busy Status + } } return true; @@ -141,26 +157,28 @@ void MotFullRateVoice::encode(uint8_t* data, bool shortened) // copy based on shortened frame or not if (shortened) { ::memcpy(data + 1U, imbeData, RAW_IMBE_LENGTH_BYTES); - data[12U] = (uint8_t)m_source; - } + data[13U] = (uint8_t)(m_busy & 0x03U); // Busy Status + } // if not shortened, our IMBE data start position depends on frame type else { - // Starting index for the IMBE data + // starting index for the IMBE data uint8_t imbeStart = 5U; if (isVoice9or18()) { imbeStart = 4U; } - // Check if we have additional data + // check if we have additional data if (additionalData != nullptr) { ::memcpy(data + 1U, additionalData, ADDITIONAL_LENGTH); } - // Copy rest of data ::memcpy(data + imbeStart, imbeData, RAW_IMBE_LENGTH_BYTES); - // Source byte at the end - data[11U + imbeStart] = (uint8_t)m_source; + if (isVoice9or18()) { + data[3U] = (uint8_t)(m_busy & 0x03U); // Busy Status + } else { + data[imbeStart + RAW_IMBE_LENGTH_BYTES] = (uint8_t)(m_busy & 0x03U); // Busy Status + } } } diff --git a/src/common/p25/dfsi/frames/MotFullRateVoice.h b/src/common/p25/dfsi/frames/MotFullRateVoice.h index 51da7152..06c43616 100644 --- a/src/common/p25/dfsi/frames/MotFullRateVoice.h +++ b/src/common/p25/dfsi/frames/MotFullRateVoice.h @@ -5,7 +5,7 @@ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * Copyright (C) 2024 Patrick McDonnell, W3AXL - * Copyright (C) 2024 Bryan Biedenkapp, N2PLL + * Copyright (C) 2024-2025 Bryan Biedenkapp, N2PLL * */ /** @@ -35,7 +35,7 @@ namespace p25 // --------------------------------------------------------------------------- /** - * @brief Implements a P25 Motorola full rate voice packet. + * @brief Implements a P25 Motorola/V.24 full rate voice packet. * \code{.unparsed} * Byte 0 1 2 3 * Bit 7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0 @@ -48,7 +48,7 @@ namespace p25 * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ * | IMBE 8 | IMBE 9 | IMBE 10 | IMBE 11 | * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - * | Src Flag | + * | Busy Bits | * +=+=+=+=+=+=+=+=+ * \endcode * @ingroup dfsi_frames @@ -99,9 +99,13 @@ namespace p25 */ DECLARE_PROPERTY(defines::DFSIFrameType::E, frameType, FrameType); /** - * @brief V.24 Data Source. + * @brief Total errors detected in the frame. */ - DECLARE_PROPERTY(SourceFlag::E, source, Source); + DECLARE_PROPERTY(uint8_t, totalErrors, TotalErrors); + /** + * @brief Busy Status. + */ + DECLARE_PROPERTY(uint8_t, busy, Busy); private: /** diff --git a/src/common/p25/dfsi/frames/MotPDUFrame.cpp b/src/common/p25/dfsi/frames/MotPDUFrame.cpp deleted file mode 100644 index eb79ee9f..00000000 --- a/src/common/p25/dfsi/frames/MotPDUFrame.cpp +++ /dev/null @@ -1,108 +0,0 @@ -// 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) 2024 Bryan Biedenkapp, N2PLL - * - */ -#include "common/p25/P25Defines.h" -#include "common/p25/dfsi/frames/MotPDUFrame.h" -#include "common/p25/dfsi/DFSIDefines.h" -#include "common/Utils.h" -#include "common/Log.h" - -#include -#include - -using namespace p25; -using namespace p25::defines; -using namespace p25::dfsi; -using namespace p25::dfsi::defines; -using namespace p25::dfsi::frames; - -// --------------------------------------------------------------------------- -// Public Class Members -// --------------------------------------------------------------------------- - -/* Initializes a instance of the MotPDUFrame class. */ - -MotPDUFrame::MotPDUFrame() : - startOfStream(nullptr), - pduHeaderData(nullptr) -{ - pduHeaderData = new uint8_t[P25_PDU_HEADER_LENGTH_BYTES]; - ::memset(pduHeaderData, 0x00U, P25_PDU_HEADER_LENGTH_BYTES); - - startOfStream = new MotStartOfStream(); -} - -/* Initializes a instance of the MotPDUFrame class. */ - -MotPDUFrame::MotPDUFrame(uint8_t* data) : - startOfStream(nullptr), - pduHeaderData(nullptr) -{ - pduHeaderData = new uint8_t[P25_PDU_HEADER_LENGTH_BYTES]; - ::memset(pduHeaderData, 0x00U, P25_PDU_HEADER_LENGTH_BYTES); - - decode(data); -} - -/* Finalizes a instance of the MotPDUFrame class. */ - -MotPDUFrame::~MotPDUFrame() -{ - if (startOfStream != nullptr) - delete startOfStream; - if (pduHeaderData != nullptr) - delete[] pduHeaderData; -} - -/* Decode a PDU frame. (only the PDU data header...) */ - -bool MotPDUFrame::decode(const uint8_t* data) -{ - assert(data != nullptr); - - // create a new start of stream - if (startOfStream != nullptr) - delete startOfStream; - startOfStream = new MotStartOfStream(); - - // create a buffer to decode the start record skipping the 10th byte (adjMM) - uint8_t startBuffer[MotStartOfStream::LENGTH]; - ::memset(startBuffer, 0x00U, MotStartOfStream::LENGTH); - ::memcpy(startBuffer + 1U, data, 4U); - - // decode start of stream - startOfStream->decode(startBuffer); - - ::memcpy(pduHeaderData, data + 9U, P25_PDU_HEADER_LENGTH_BYTES); - - return true; -} - -/* Encode a PDU frame. (only the PDU data header...) */ - -void MotPDUFrame::encode(uint8_t* data) -{ - assert(data != nullptr); - assert(startOfStream != nullptr); - - // encode start of stream - scope is intentional - { - uint8_t buffer[MotStartOfStream::LENGTH]; - startOfStream->encode(buffer); - - // copy to data array (skipping first and last bytes) - ::memcpy(data + 1U, buffer + 1U, 4U); - } - - // encode PDU - scope is intentional - { - data[0U] = DFSIFrameType::PDU; - ::memcpy(data + 9U, pduHeaderData, P25_PDU_HEADER_LENGTH_BYTES); - } -} diff --git a/src/common/p25/dfsi/frames/MotStartOfStream.cpp b/src/common/p25/dfsi/frames/MotStartOfStream.cpp index 0ce7e6cd..edabe4be 100644 --- a/src/common/p25/dfsi/frames/MotStartOfStream.cpp +++ b/src/common/p25/dfsi/frames/MotStartOfStream.cpp @@ -5,7 +5,7 @@ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * Copyright (C) 2024 Patrick McDonnell, W3AXL - * Copyright (C) 2024 Bryan Biedenkapp, N2PLL + * Copyright (C) 2024-2025 Bryan Biedenkapp, N2PLL * */ #include "common/p25/dfsi/frames/MotStartOfStream.h" @@ -28,34 +28,44 @@ using namespace p25::dfsi::frames; /* Initializes a instance of the MotStartOfStream class. */ MotStartOfStream::MotStartOfStream() : - m_marker(FIXED_MARKER), - m_rt(RTFlag::DISABLED), - m_startStop(StartStopFlag::START), - m_streamType(StreamTypeFlag::VOICE) + m_format(DFSI_MOT_ICW_FMT_TYPE3), + m_opcode(MotStartStreamOpcode::TRANSMIT), + icw(nullptr) { - /* stub */ + icw = new uint8_t[DFSI_MOT_ICW_LENGTH]; + ::memset(icw, 0x00U, DFSI_MOT_ICW_LENGTH); } /* Initializes a instance of the MotStartOfStream class. */ MotStartOfStream::MotStartOfStream(uint8_t* data) : - m_marker(FIXED_MARKER), - m_rt(RTFlag::DISABLED), - m_startStop(StartStopFlag::START), - m_streamType(StreamTypeFlag::VOICE) + m_format(DFSI_MOT_ICW_FMT_TYPE3), + m_opcode(MotStartStreamOpcode::TRANSMIT), + icw(nullptr) { + icw = new uint8_t[DFSI_MOT_ICW_LENGTH]; + ::memset(icw, 0x00U, DFSI_MOT_ICW_LENGTH); + decode(data); } +/* Finalizes a instance of the MotStartOfStream class. */ + +MotStartOfStream::~MotStartOfStream() +{ + if (icw != nullptr) + delete icw; +} + /* Decode a start of stream frame. */ bool MotStartOfStream::decode(const uint8_t* data) { assert(data != nullptr); - m_rt = (RTFlag::E)data[2U]; - m_startStop = (StartStopFlag::E)data[3U]; - m_streamType = (StreamTypeFlag::E)data[4U]; + m_format = data[1U] & 0x3FU; + m_opcode = (MotStartStreamOpcode::E)data[2U]; + ::memcpy(icw, data + 3U, DFSI_MOT_ICW_LENGTH); return true; } @@ -67,8 +77,7 @@ void MotStartOfStream::encode(uint8_t* data) assert(data != nullptr); data[0U] = DFSIFrameType::MOT_START_STOP; - data[1U] = FIXED_MARKER; - data[2U] = m_rt; - data[3U] = m_startStop; - data[4U] = m_streamType; + data[1U] = m_format & 0x3FU; + data[2U] = m_opcode; + ::memcpy(data + 3U, icw, DFSI_MOT_ICW_LENGTH); } diff --git a/src/common/p25/dfsi/frames/MotStartOfStream.h b/src/common/p25/dfsi/frames/MotStartOfStream.h index 14f4897b..2d23868c 100644 --- a/src/common/p25/dfsi/frames/MotStartOfStream.h +++ b/src/common/p25/dfsi/frames/MotStartOfStream.h @@ -5,7 +5,7 @@ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * Copyright (C) 2024 Patrick McDonnell, W3AXL - * Copyright (C) 2024 Bryan Biedenkapp, N2PLL + * Copyright (C) 2024-2025 Bryan Biedenkapp, N2PLL * */ /** @@ -34,25 +34,22 @@ namespace p25 // --------------------------------------------------------------------------- /** - * @brief Implements a P25 Motorola start of stream packet. + * @brief Implements a P25 Motorola/V.24 start of stream packet. * \code{.unparsed} * Byte 0 1 2 3 * Bit 7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0 * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - * | Fixed Mark | RT Mode Flag | Start/Stop | Type Flag | + * | FT | ICW Format | Opcode | Param 1 | * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - * | Reserved | - * + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - * | | + * | Argment 1 | Param 2 | Argument 2 | Param 3 | + * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + * | Argment 3 | * +-+-+-+-+-+-+-+-+ * \endcode * @ingroup dfsi_frames */ class HOST_SW_API MotStartOfStream { public: - static const uint8_t LENGTH = 10U; - static const uint8_t FIXED_MARKER = 0x02U; - /** * @brief Initializes a copy instance of the MotStartOfStream class. */ @@ -62,6 +59,10 @@ namespace p25 * @param data Buffer to containing MotStartOfStream to decode. */ MotStartOfStream(uint8_t* data); + /** + * @brief Finalizes a instance of the MotStartOfStream class. + */ + ~MotStartOfStream(); /** * @brief Decode a start of stream frame. @@ -73,24 +74,92 @@ namespace p25 * @param[out] data Buffer to encode a MotStartOfStream. */ void encode(uint8_t* data); - - public: + + /** @name Start of Stream Type 3 control word parameters. */ + /** + * @brief Helper to get parameter 1 of the control word. + * @return uint8_t Parameter 1 of the control word. + */ + uint8_t getParam1() const { return icw[0U]; } /** - * @brief + * @brief Helper to get the argument for parameter 1 of the control word. + * @return uint8_t Argument 1 for parameter 1 of the control word. */ - DECLARE_PROPERTY(uint8_t, marker, Marker); + uint8_t getArgument1() const { return icw[1U]; } /** - * @brief RT/RT Flag. + * @brief Helper to set parameter 1 of the control word. + * @param value Parameter 1 of the control word. */ - DECLARE_PROPERTY(RTFlag::E, rt, RT); + void setParam1(uint8_t value) { icw[0U] = value; } /** - * @brief Start/Stop. + * @brief Helper to set the argument for parameter 1 of the control word. + * @param value Argument 1 for parameter 1 of the control word. */ - DECLARE_PROPERTY(StartStopFlag::E, startStop, StartStop); + void setArgument1(uint8_t value) { icw[1U] = value; } + /** - * @brief Stream Type. + * @brief Helper to get parameter 2 of the control word. + * @return uint8_t Parameter 2 of the control word. */ - DECLARE_PROPERTY(StreamTypeFlag::E, streamType, StreamType); + uint8_t getParam2() const { return icw[2U]; } + /** + * @brief Helper to get the argument for parameter 2 of the control word. + * @return uint8_t Argument 2 for parameter 2 of the control word. + */ + uint8_t getArgument2() const { return icw[3U]; } + /** + * @brief Helper to set parameter 1 of the control word. + * @param value Parameter 1 of the control word. + */ + void setParam2(uint8_t value) { icw[2U] = value; } + /** + * @brief Helper to set the argument for parameter 2 of the control word. + * @param value Argument for parameter 2 of the control word. + */ + void setArgument2(uint8_t value) { icw[3U] = value; } + + /** + * @brief Helper to get parameter 3 of the control word. + * @return uint8_t Parameter 3 of the control word. + */ + uint8_t getParam3() const { return icw[4U]; } + /** + * @brief Helper to get argument 3 for parameter 3 of the control word. + * @return uint8_t Argument for parameter 3 of the control word. + */ + uint8_t getArgument3() const { return icw[5U]; } + /** + * @brief Helper to set parameter 3 of the control word. + * @param value Parameter 3 of the control word. + */ + void setParam3(uint8_t value) { icw[4U] = value; } + /** + * @brief Helper to set the argument for parameter 3 of the control word. + * @param value Argument for parameter 3 of the control word. + */ + void setArgument3(uint8_t value) { icw[5U] = value; } + /** @} */ + + /** + * @brief Get the raw ICW parameter/argument buffer. + * @return uint8_t* Raw ICW buffer. + * @note The buffer is 6 bytes long and contains the parameters and arguments for the + * start of stream control word. + */ + uint8_t* getICW() const { return icw; } + + public: + /** + * @brief Format. + */ + DECLARE_PROPERTY(uint8_t, format, Format); + /** + * @brief Opcode. + */ + DECLARE_PROPERTY(MotStartStreamOpcode::E, opcode, Opcode); + + private: + uint8_t* icw; }; } // namespace frames } // namespace dfsi diff --git a/src/common/p25/dfsi/frames/MotStartVoiceFrame.cpp b/src/common/p25/dfsi/frames/MotStartVoiceFrame.cpp index 91070f4f..5a899ec5 100644 --- a/src/common/p25/dfsi/frames/MotStartVoiceFrame.cpp +++ b/src/common/p25/dfsi/frames/MotStartVoiceFrame.cpp @@ -5,7 +5,7 @@ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * Copyright (C) 2024 Patrick McDonnell, W3AXL - * Copyright (C) 2024 Bryan Biedenkapp, N2PLL + * Copyright (C) 2024-2025 Bryan Biedenkapp, N2PLL * */ #include "common/p25/dfsi/frames/MotStartVoiceFrame.h" @@ -30,11 +30,7 @@ using namespace p25::dfsi::frames; MotStartVoiceFrame::MotStartVoiceFrame() : startOfStream(nullptr), fullRateVoice(nullptr), - m_icw(ICWFlag::DIU), - m_rssi(0U), - m_rssiValidity(RssiValidityFlag::INVALID), - m_nRssi(0U), - m_adjMM(0U) + m_totalErrors(0U) { startOfStream = new MotStartOfStream(); fullRateVoice = new MotFullRateVoice(); @@ -45,11 +41,7 @@ MotStartVoiceFrame::MotStartVoiceFrame() : MotStartVoiceFrame::MotStartVoiceFrame(uint8_t* data) : startOfStream(nullptr), fullRateVoice(nullptr), - m_icw(ICWFlag::DIU), - m_rssi(0U), - m_rssiValidity(RssiValidityFlag::INVALID), - m_nRssi(0U), - m_adjMM(0U) + m_totalErrors(0U) { decode(data); } @@ -76,9 +68,9 @@ bool MotStartVoiceFrame::decode(const uint8_t* data) startOfStream = new MotStartOfStream(); // create a buffer to decode the start record skipping the 10th byte (adjMM) - uint8_t startBuffer[MotStartOfStream::LENGTH]; - ::memset(startBuffer, 0x00U, MotStartOfStream::LENGTH); - ::memcpy(startBuffer, data, 9U); + uint8_t startBuffer[DFSI_MOT_START_LEN]; + ::memset(startBuffer, 0x00U, DFSI_MOT_START_LEN); + ::memcpy(startBuffer, data, DFSI_MOT_START_LEN); // decode start of stream startOfStream->decode(startBuffer); @@ -94,13 +86,6 @@ bool MotStartVoiceFrame::decode(const uint8_t* data) ::memcpy(voiceBuffer + 1U, data + 10U, MotFullRateVoice::SHORTENED_LENGTH - 1); fullRateVoice->decode(voiceBuffer, true); - // get rest of data - m_icw = (ICWFlag::E)data[5U]; - m_rssi = data[6U]; - m_rssiValidity = (RssiValidityFlag::E)data[7U]; - m_nRssi = data[8U]; - m_adjMM = data[9U]; - return true; } @@ -114,11 +99,11 @@ void MotStartVoiceFrame::encode(uint8_t* data) // encode start of stream - scope is intentional { - uint8_t buffer[MotStartOfStream::LENGTH]; + uint8_t buffer[DFSI_MOT_START_LEN]; startOfStream->encode(buffer); - // copy to data array (skipping first and last bytes) - ::memcpy(data + 1U, buffer + 1U, MotStartOfStream::LENGTH - 2); + // copy to data array (skipping first byte which is frame type) + ::memcpy(data + 1U, buffer + 1U, DFSI_MOT_START_LEN - 1U); } // encode full rate voice - scope is intentional @@ -129,11 +114,4 @@ void MotStartVoiceFrame::encode(uint8_t* data) data[0U] = fullRateVoice->getFrameType(); ::memcpy(data + 10U, buffer + 1U, MotFullRateVoice::SHORTENED_LENGTH - 1); } - - // Copy the rest - data[5U] = m_icw; - data[6U] = m_rssi; - data[7U] = m_rssiValidity; - data[8U] = m_nRssi; - data[9U] = m_adjMM; } diff --git a/src/common/p25/dfsi/frames/MotStartVoiceFrame.h b/src/common/p25/dfsi/frames/MotStartVoiceFrame.h index 0f287724..66df6bf9 100644 --- a/src/common/p25/dfsi/frames/MotStartVoiceFrame.h +++ b/src/common/p25/dfsi/frames/MotStartVoiceFrame.h @@ -5,7 +5,7 @@ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * Copyright (C) 2024 Patrick McDonnell, W3AXL - * Copyright (C) 2024 Bryan Biedenkapp, N2PLL + * Copyright (C) 2024-2025 Bryan Biedenkapp, N2PLL * */ /** @@ -36,21 +36,21 @@ namespace p25 // --------------------------------------------------------------------------- /** - * @brief Implements a P25 Motorola voice frame 1/10 start. + * @brief Implements a P25 Motorola/V.24 voice frame 1/10 start. * \code{.unparsed} * Byte 0 1 2 3 * Bit 7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0 * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - * | Encoded Motorola Start of Stream | + * | FT | Encoded V.24 Start of Stream | + * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+--+-+-+-+-+-+-+--+-+-+-+-+-+-+-+-+ + * | | * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - * | ICW Flag ? | RSSI | RSSI Valid | RSSI | + * | | Full Rate Voice Frame | * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - * | Adj MM ? | Full Rate Voice Frame | - * +-+-+-+-+-+-+-+-+ + * | | - * + + + * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ * | | - * + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ * | | * +=+=+=+=+=+=+=+=+ * \endcode @@ -90,25 +90,9 @@ namespace p25 MotFullRateVoice* fullRateVoice; // ?? - this should probably be private with getters/setters /** - * @brief - */ - DECLARE_PROPERTY(ICWFlag::E, icw, ICW); - /** - * @brief RSSI Value. - */ - DECLARE_PROPERTY(uint8_t, rssi, RSSI); - /** - * @brief Flag indicating whether or not the RSSI field is valid. - */ - DECLARE_PROPERTY(RssiValidityFlag::E, rssiValidity, RSSIValidity); - /** - * @brief - */ - DECLARE_PROPERTY(uint8_t, nRssi, NRSSI); - /** - * @brief + * @brief Total errors detected in the frame. */ - DECLARE_PROPERTY(uint8_t, adjMM, AdjMM); + DECLARE_PROPERTY(uint8_t, totalErrors, TotalErrors); }; } // namespace frames } // namespace dfsi diff --git a/src/common/p25/dfsi/frames/MotTDULCFrame.cpp b/src/common/p25/dfsi/frames/MotTDULCFrame.cpp new file mode 100644 index 00000000..a022bbef --- /dev/null +++ b/src/common/p25/dfsi/frames/MotTDULCFrame.cpp @@ -0,0 +1,136 @@ +// 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/p25/P25Defines.h" +#include "common/p25/dfsi/frames/MotTDULCFrame.h" +#include "common/p25/dfsi/DFSIDefines.h" +#include "common/Utils.h" +#include "common/Log.h" + +#include +#include + +using namespace p25; +using namespace p25::defines; +using namespace p25::dfsi; +using namespace p25::dfsi::defines; +using namespace p25::dfsi::frames; + +// --------------------------------------------------------------------------- +// Public Class Members +// --------------------------------------------------------------------------- + +/* Initializes a instance of the MotTDULCFrame class. */ + +MotTDULCFrame::MotTDULCFrame() : + startOfStream(nullptr), + tdulcData(nullptr) +{ + tdulcData = new uint8_t[P25_TDULC_FRAME_LENGTH_BYTES]; + ::memset(tdulcData, 0x00U, P25_TDULC_FRAME_LENGTH_BYTES); + + startOfStream = new MotStartOfStream(); +} + +/* Initializes a instance of the MotTDULCFrame class. */ + +MotTDULCFrame::MotTDULCFrame(uint8_t* data) : + startOfStream(nullptr), + tdulcData(nullptr) +{ + tdulcData = new uint8_t[P25_TDULC_FRAME_LENGTH_BYTES]; + ::memset(tdulcData, 0x00U, P25_TDULC_FRAME_LENGTH_BYTES); + + decode(data); +} + +/* Finalizes a instance of the MotTDULCFrame class. */ + +MotTDULCFrame::~MotTDULCFrame() +{ + if (startOfStream != nullptr) + delete startOfStream; + if (tdulcData != nullptr) + delete[] tdulcData; +} + +/* Decode a TDULC frame. */ + +bool MotTDULCFrame::decode(const uint8_t* data) +{ + assert(data != nullptr); + + // create a new start of stream + if (startOfStream != nullptr) + delete startOfStream; + startOfStream = new MotStartOfStream(); + + // create a buffer to decode the start record + uint8_t startBuffer[DFSI_MOT_START_LEN]; + ::memset(startBuffer, 0x00U, DFSI_MOT_START_LEN); + ::memcpy(startBuffer + 1U, data, DFSI_MOT_START_LEN - 1U); + + // 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); + + return true; +} + +/* Encode a TDULC frame. */ + +void MotTDULCFrame::encode(uint8_t* data) +{ + assert(data != nullptr); + assert(startOfStream != nullptr); + + // encode start of stream - scope is intentional + { + uint8_t buffer[DFSI_MOT_START_LEN]; + startOfStream->encode(buffer); + + // copy to data array + ::memcpy(data + 1U, buffer + 1U, DFSI_MOT_START_LEN - 1U); + } + + // 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); + + data[DFSI_MOT_START_LEN + 11U] = DFSI_BUSY_BITS_IDLE; + } +} diff --git a/src/common/p25/dfsi/frames/MotPDUFrame.h b/src/common/p25/dfsi/frames/MotTDULCFrame.h similarity index 62% rename from src/common/p25/dfsi/frames/MotPDUFrame.h rename to src/common/p25/dfsi/frames/MotTDULCFrame.h index 792143a6..66a30838 100644 --- a/src/common/p25/dfsi/frames/MotPDUFrame.h +++ b/src/common/p25/dfsi/frames/MotTDULCFrame.h @@ -4,17 +4,17 @@ * 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) 2025 Bryan Biedenkapp, N2PLL * */ /** - * @file MotPDUFrame.h + * @file MotTDULCFrame.h * @ingroup dfsi_frames - * @file MotPDUFrame.cpp + * @file MotTDULCFrame.cpp * @ingroup dfsi_frames */ -#if !defined(__MOT_PDU_FRAME_H__) -#define __MOT_PDU_FRAME_H__ +#if !defined(__MOT_TDULC_FRAME_H__) +#define __MOT_TDULC_FRAME_H__ #include "Defines.h" #include "common/Defines.h" @@ -35,59 +35,57 @@ namespace p25 // --------------------------------------------------------------------------- /** - * @brief Implements a P25 Motorola PDU frame. + * @brief Implements a P25 Motorola/V.24 TDULC frame. * \code{.unparsed} * Byte 0 1 2 3 * Bit 7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0 * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - * | Encoded Motorola Start of Stream | + * | FT | Encoded V.24 Start of Stream | * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - * | Reserved ? | - * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - * | PDU Header | - * + + * | | - * + + + * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + * | | TDULC | + * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ * | | * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + * | | Reserved | + * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ * \endcode * @ingroup dfsi_frames */ - class HOST_SW_API MotPDUFrame { + class HOST_SW_API MotTDULCFrame { public: - static const uint8_t LENGTH = 20U; - /** - * @brief Initializes a copy instance of the MotPDUFrame class. + * @brief Initializes a copy instance of the MotTDULCFrame class. */ - MotPDUFrame(); + MotTDULCFrame(); /** - * @brief Initializes a copy instance of the MotPDUFrame class. - * @param data Buffer to containing MotPDUFrame to decode. + * @brief Initializes a copy instance of the MotTDULCFrame class. + * @param data Buffer to containing MotTDULCFrame to decode. */ - MotPDUFrame(uint8_t* data); + MotTDULCFrame(uint8_t* data); /** - * @brief Finalizes a instance of the MotPDUFrame class. + * @brief Finalizes a instance of the MotTDULCFrame class. */ - ~MotPDUFrame(); + ~MotTDULCFrame(); /** - * @brief Decode a PDU frame. (only the PDU data header...) - * @param[in] data Buffer to containing MotPDUFrame to decode. + * @brief Decode a TDULC frame. + * @param[in] data Buffer to containing MotTDULCFrame to decode. */ bool decode(const uint8_t* data); /** - * @brief Encode a PDU frame. (only the PDU data header...) - * @param[out] data Buffer to encode a MotPDUFrame. + * @brief Encode a TDULC frame. + * @param[out] data Buffer to encode a MotTDULCFrame. */ void encode(uint8_t* data); public: MotStartOfStream* startOfStream; // ?? - this should probably be private with getters/setters - uint8_t* pduHeaderData; // ?? - this should probably be private with getters/setters + uint8_t* tdulcData; // ?? - this should probably be private with getters/setters }; } // namespace frames } // namespace dfsi } // namespace p25 -#endif // __MOT_PDU_FRAME_H__ \ No newline at end of file +#endif // __MOT_TDULC_FRAME_H__ \ No newline at end of file diff --git a/src/common/p25/dfsi/frames/MotTSBKFrame.cpp b/src/common/p25/dfsi/frames/MotTSBKFrame.cpp index 61fe0ed2..1ac6875b 100644 --- a/src/common/p25/dfsi/frames/MotTSBKFrame.cpp +++ b/src/common/p25/dfsi/frames/MotTSBKFrame.cpp @@ -4,7 +4,7 @@ * GPLv2 Open Source. Use is subject to license terms. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * - * Copyright (C) 2024 Bryan Biedenkapp, N2PLL + * Copyright (C) 2024-2025 Bryan Biedenkapp, N2PLL * */ #include "common/p25/P25Defines.h" @@ -71,15 +71,15 @@ bool MotTSBKFrame::decode(const uint8_t* data) delete startOfStream; startOfStream = new MotStartOfStream(); - // create a buffer to decode the start record skipping the 10th byte (adjMM) - uint8_t startBuffer[MotStartOfStream::LENGTH]; - ::memset(startBuffer, 0x00U, MotStartOfStream::LENGTH); - ::memcpy(startBuffer + 1U, data, 4U); + // create a buffer to decode the start record + uint8_t startBuffer[DFSI_MOT_START_LEN]; + ::memset(startBuffer, 0x00U, DFSI_MOT_START_LEN); + ::memcpy(startBuffer + 1U, data, DFSI_MOT_START_LEN - 1U); // decode start of stream startOfStream->decode(startBuffer); - ::memcpy(tsbkData, data + 9U, P25_TSBK_LENGTH_BYTES); + ::memcpy(tsbkData, data + DFSI_MOT_START_LEN, P25_TSBK_LENGTH_BYTES); return true; } @@ -93,16 +93,16 @@ void MotTSBKFrame::encode(uint8_t* data) // encode start of stream - scope is intentional { - uint8_t buffer[MotStartOfStream::LENGTH]; + uint8_t buffer[DFSI_MOT_START_LEN]; startOfStream->encode(buffer); - // copy to data array (skipping first and last bytes) - ::memcpy(data + 1U, buffer + 1U, 4U); + // copy to data array + ::memcpy(data + 1U, buffer + 1U, DFSI_MOT_START_LEN - 1U); } // encode TSBK - scope is intentional { - data[0U] = DFSIFrameType::TSBK; - ::memcpy(data + 9U, tsbkData, P25_TSBK_LENGTH_BYTES); + data[0U] = DFSIFrameType::MOT_TSBK; + ::memcpy(data + DFSI_MOT_START_LEN, tsbkData, P25_TSBK_LENGTH_BYTES); } } diff --git a/src/common/p25/dfsi/frames/MotTSBKFrame.h b/src/common/p25/dfsi/frames/MotTSBKFrame.h index 654f2640..2ffee559 100644 --- a/src/common/p25/dfsi/frames/MotTSBKFrame.h +++ b/src/common/p25/dfsi/frames/MotTSBKFrame.h @@ -4,7 +4,7 @@ * GPLv2 Open Source. Use is subject to license terms. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * - * Copyright (C) 2024 Bryan Biedenkapp, N2PLL + * Copyright (C) 2024-2025 Bryan Biedenkapp, N2PLL * */ /** @@ -35,30 +35,28 @@ namespace p25 // --------------------------------------------------------------------------- /** - * @brief Implements a P25 Motorola TSBK frame. + * @brief Implements a P25 Motorola/V.24 TSBK frame. * \code{.unparsed} * Byte 0 1 2 3 * Bit 7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0 * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - * | Encoded Motorola Start of Stream | + * | FT | Encoded V.24 Start of Stream | * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - * | Reserved ? | - * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - * | TSBK | - * + + * | | - * + + + * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + * | | TSBK | + * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ * | | * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - * | Unknown ? | + * | | * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + * | | Reserved | + * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ * \endcode * @ingroup dfsi_frames */ class HOST_SW_API MotTSBKFrame { public: - static const uint8_t LENGTH = 24U; - /** * @brief Initializes a copy instance of the MotTSBKFrame class. */ diff --git a/src/common/p25/dfsi/frames/MotVoiceHeader1.cpp b/src/common/p25/dfsi/frames/MotVoiceHeader1.cpp deleted file mode 100644 index 908cd831..00000000 --- a/src/common/p25/dfsi/frames/MotVoiceHeader1.cpp +++ /dev/null @@ -1,129 +0,0 @@ -// 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) 2024 Patrick McDonnell, W3AXL - * Copyright (C) 2024 Bryan Biedenkapp, N2PLL - * - */ -#include "common/p25/dfsi/frames/MotVoiceHeader1.h" -#include "common/p25/dfsi/DFSIDefines.h" -#include "common/Utils.h" -#include "common/Log.h" - -#include -#include - -using namespace p25; -using namespace p25::dfsi; -using namespace p25::dfsi::defines; -using namespace p25::dfsi::frames; - -// --------------------------------------------------------------------------- -// Public Class Members -// --------------------------------------------------------------------------- - -/* Initializes a instance of the MotVoiceHeader1 class. */ - -MotVoiceHeader1::MotVoiceHeader1() : - header(nullptr), - startOfStream(nullptr), - m_icw(ICWFlag::DIU), - m_rssi(0U), - m_rssiValidity(RssiValidityFlag::INVALID), - m_nRssi(0U) -{ - startOfStream = new MotStartOfStream(); - - header = new uint8_t[HCW_LENGTH]; - ::memset(header, 0x00U, HCW_LENGTH); -} - -/* Initializes a instance of the MotVoiceHeader1 class. */ - -MotVoiceHeader1::MotVoiceHeader1(uint8_t* data) : - header(nullptr), - startOfStream(nullptr), - m_icw(ICWFlag::DIU), - m_rssi(0U), - m_rssiValidity(RssiValidityFlag::INVALID), - m_nRssi(0U) -{ - decode(data); -} - -/* Finalizes a instance of the MotVoiceHeader1 class. */ - -MotVoiceHeader1::~MotVoiceHeader1() -{ - if (startOfStream != nullptr) - delete startOfStream; - if (header != nullptr) - delete[] header; -} - -/* Decode a voice header 1 frame. */ - -bool MotVoiceHeader1::decode(const uint8_t* data) -{ - assert(data != nullptr); - - // create a start of stream - if (startOfStream != nullptr) - delete startOfStream; - startOfStream = new MotStartOfStream(); - - uint8_t buffer[MotStartOfStream::LENGTH]; - ::memset(buffer, 0x00U, MotStartOfStream::LENGTH); - - // we copy the bytes from [1:4] - ::memcpy(buffer + 1U, data + 1U, 4); - startOfStream->decode(buffer); - - // decode the other stuff - m_icw = (ICWFlag::E)data[5U]; - m_rssi = data[6U]; - m_rssiValidity = (RssiValidityFlag::E)data[7U]; - m_nRssi = data[8U]; - - // our header includes the trailing source and check bytes - if (header != nullptr) - delete[] header; - header = new uint8_t[HCW_LENGTH]; - ::memset(header, 0x00U, HCW_LENGTH); - ::memcpy(header, data + 9U, HCW_LENGTH); - - return true; -} - -/* Encode a voice header 1 frame. */ - -void MotVoiceHeader1::encode(uint8_t* data) -{ - assert(data != nullptr); - assert(startOfStream != nullptr); - - data[0U] = DFSIFrameType::MOT_VHDR_1; - - // scope is intentional - { - uint8_t buffer[MotStartOfStream::LENGTH]; - ::memset(buffer, 0x00U, MotStartOfStream::LENGTH); - startOfStream->encode(buffer); - - // copy the 4 start record bytes from the start of stream frame - ::memcpy(data + 1U, buffer + 1U, 4U); - } - - data[5U] = m_icw; - data[6U] = m_rssi; - data[7U] = m_rssiValidity; - data[8U] = m_nRssi; - - // our header includes the trailing source and check bytes - if (header != nullptr) { - ::memcpy(data + 9U, header, HCW_LENGTH); - } -} diff --git a/src/common/p25/dfsi/frames/MotVoiceHeader1.h b/src/common/p25/dfsi/frames/MotVoiceHeader1.h deleted file mode 100644 index 05951a0a..00000000 --- a/src/common/p25/dfsi/frames/MotVoiceHeader1.h +++ /dev/null @@ -1,117 +0,0 @@ -// 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) 2024 Patrick McDonnell, W3AXL - * Copyright (C) 2024 Bryan Biedenkapp, N2PLL - * - */ -/** - * @file MotVoiceHeader1.h - * @ingroup dfsi_frames - * @file MotVoiceHeader1.cpp - * @ingroup dfsi_frames - */ -#if !defined(__MOT_VOICE_HEADER_1_H__) -#define __MOT_VOICE_HEADER_1_H__ - -#include "Defines.h" -#include "common/Defines.h" -#include "common/Log.h" -#include "common/Utils.h" -#include "common/p25/dfsi/frames/FrameDefines.h" -#include "common/p25/dfsi/frames/MotStartOfStream.h" - -namespace p25 -{ - namespace dfsi - { - namespace frames - { - // --------------------------------------------------------------------------- - // Class Declaration - // --------------------------------------------------------------------------- - - /** - * @brief Implements a P25 Motorola voice header frame 1. - * \code{.unparsed} - * Byte 0 1 2 3 - * Bit 7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0 - * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - * | Encoded Motorola Start of Stream | - * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - * | ICW Flag ? | RSSI | RSSI Valid | RSSI | - * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - * | Header Control Word | - * + + - * | | - * + + - * | | - * + + - * | | - * + + - * | | - * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - * | Src Flag | - * +-+-+-+-+-+-+-+-+ - * \endcode - * @ingroup dfsi_frames - */ - class HOST_SW_API MotVoiceHeader1 { - public: - static const uint8_t LENGTH = 30U; - static const uint8_t HCW_LENGTH = 21U; - - /** - * @brief Initializes a copy instance of the MotVoiceHeader1 class. - */ - MotVoiceHeader1(); - /** - * @brief Initializes a copy instance of the MotVoiceHeader1 class. - * @param data Buffer to containing MotVoiceHeader1 to decode. - */ - MotVoiceHeader1(uint8_t* data); - /** - * @brief Finalizes a instance of the MotVoiceHeader1 class. - */ - ~MotVoiceHeader1(); - - /** - * @brief Decode a voice header 1 frame. - * @param[in] data Buffer to containing MotVoiceHeader1 to decode. - */ - bool decode(const uint8_t* data); - /** - * @brief Encode a voice header 1 frame. - * @param[out] data Buffer to encode a MotVoiceHeader1. - */ - void encode(uint8_t* data); - - public: - uint8_t* header; // ?? - this should probably be private with getters/setters - MotStartOfStream* startOfStream; // ?? - this should probably be private with getters/setters - - /** - * @brief - */ - DECLARE_PROPERTY(ICWFlag::E, icw, ICW); - /** - * @brief RSSI Value. - */ - DECLARE_PROPERTY(uint8_t, rssi, RSSI); - /** - * @brief Flag indicating whether or not the RSSI field is valid. - */ - DECLARE_PROPERTY(RssiValidityFlag::E, rssiValidity, RSSIValidity); - /** - * @brief - */ - DECLARE_PROPERTY(uint8_t, nRssi, NRSSI); - }; - } // namespace frames - } // namespace dfsi -} // namespace p25 - -#endif // __MOT_VOICE_HEADER_1_H__ \ No newline at end of file diff --git a/src/common/p25/dfsi/frames/MotVoiceHeader2.cpp b/src/common/p25/dfsi/frames/MotVoiceHeader2.cpp deleted file mode 100644 index 0bfd227b..00000000 --- a/src/common/p25/dfsi/frames/MotVoiceHeader2.cpp +++ /dev/null @@ -1,87 +0,0 @@ -// 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) 2024 Patrick McDonnell, W3AXL - * Copyright (C) 2024 Bryan Biedenkapp, N2PLL - * - */ -#include "common/p25/dfsi/frames/MotVoiceHeader2.h" -#include "common/p25/dfsi/DFSIDefines.h" -#include "common/Utils.h" -#include "common/Log.h" - -#include -#include - -using namespace p25; -using namespace p25::dfsi; -using namespace p25::dfsi::defines; -using namespace p25::dfsi::frames; - -// --------------------------------------------------------------------------- -// Public Class Members -// --------------------------------------------------------------------------- - -/* Initializes a instance of the MotVoiceHeader2 class. */ - -MotVoiceHeader2::MotVoiceHeader2() : - header(nullptr), - m_source(SourceFlag::QUANTAR) -{ - header = new uint8_t[HCW_LENGTH]; - ::memset(header, 0x00U, HCW_LENGTH); -} - -/* Initializes a instance of the MotVoiceHeader2 class. */ - -MotVoiceHeader2::MotVoiceHeader2(uint8_t* data) : - header(nullptr), - m_source(SourceFlag::QUANTAR) -{ - decode(data); -} - -/* Finalizes a instance of the MotVoiceHeader2 class. */ - -MotVoiceHeader2::~MotVoiceHeader2() -{ - if (header != nullptr) - delete[] header; -} - -/* Decode a voice header 2 frame. */ - -bool MotVoiceHeader2::decode(const uint8_t* data) -{ - assert(data != nullptr); - - m_source = (SourceFlag::E)data[21]; - - if (header != nullptr) { - delete[] header; - } - - header = new uint8_t[HCW_LENGTH]; - ::memset(header, 0x00U, HCW_LENGTH); - ::memcpy(header, data + 1U, HCW_LENGTH); - - return true; -} - -/* Encode a voice header 2 frame. */ - -void MotVoiceHeader2::encode(uint8_t* data) -{ - assert(data != nullptr); - - data[0U] = DFSIFrameType::MOT_VHDR_2; - - if (header != nullptr) { - ::memcpy(data + 1U, header, HCW_LENGTH); - } - - data[LENGTH - 1U] = (uint8_t)m_source; -} diff --git a/src/common/p25/dfsi/frames/MotVoiceHeader2.h b/src/common/p25/dfsi/frames/MotVoiceHeader2.h deleted file mode 100644 index a1918a77..00000000 --- a/src/common/p25/dfsi/frames/MotVoiceHeader2.h +++ /dev/null @@ -1,100 +0,0 @@ -// 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) 2024 Patrick McDonnell, W3AXL - * Copyright (C) 2024 Bryan Biedenkapp, N2PLL - * - */ -/** - * @file MotVoiceHeader2.h - * @ingroup dfsi_frames - * @file MotVoiceHeader2.cpp - * @ingroup dfsi_frames - */ -#if !defined(__MOT_VOICE_HEADER_2_H__) -#define __MOT_VOICE_HEADER_2_H__ - -#include "Defines.h" -#include "common/Defines.h" -#include "common/Log.h" -#include "common/Utils.h" -#include "common/p25/dfsi/frames/FrameDefines.h" -#include "common/p25/dfsi/frames/MotStartOfStream.h" - -namespace p25 -{ - namespace dfsi - { - namespace frames - { - // --------------------------------------------------------------------------- - // Class Declaration - // --------------------------------------------------------------------------- - - /** - * @brief Implements a P25 Motorola voice header frame 2. - * \code{.unparsed} - * Byte 0 1 2 3 - * Bit 7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0 - * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - * | Header Control Word | - * + + - * | | - * + + - * | | - * + + - * | | - * + + - * | | - * + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - * | | Reserved | - * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - * \endcode - * @ingroup dfsi_frames - */ - class HOST_SW_API MotVoiceHeader2 { - public: - static const uint8_t LENGTH = 22U; - static const uint8_t HCW_LENGTH = 20U; - - /** - * @brief Initializes a copy instance of the MotVoiceHeader2 class. - */ - MotVoiceHeader2(); - /** - * @brief Initializes a copy instance of the MotVoiceHeader2 class. - * @param data Buffer to containing MotVoiceHeader2 to decode. - */ - MotVoiceHeader2(uint8_t* data); - /** - * @brief Finalizes a instance of the MotVoiceHeader2 class. - */ - ~MotVoiceHeader2(); - - /** - * @brief Decode a voice header 2 frame. - * @param[in] data Buffer to containing MotVoiceHeader2 to decode. - */ - bool decode(const uint8_t* data); - /** - * @brief Encode a voice header 2 frame. - * @param[out] data Buffer to encode a MotVoiceHeader2. - */ - void encode(uint8_t* data); - - public: - uint8_t* header; // ?? - this should probably be a private with getters/setters - - /** - * @brief V.24 Data Source. - */ - DECLARE_PROPERTY(SourceFlag::E, source, Source); - }; - } // namespace frames - } // namespace dfsi -} // namespace p25 - -#endif // __MOT_VOICE_HEADER_2_H__ \ No newline at end of file diff --git a/src/common/p25/dfsi/frames/StartOfStream.h b/src/common/p25/dfsi/frames/StartOfStream.h index 42680375..014b08c2 100644 --- a/src/common/p25/dfsi/frames/StartOfStream.h +++ b/src/common/p25/dfsi/frames/StartOfStream.h @@ -45,7 +45,7 @@ namespace p25 */ class HOST_SW_API StartOfStream { public: - static const uint8_t LENGTH = 4U; + static const uint8_t LENGTH = 3U; /** * @brief Initializes a copy instance of the StartOfStream class. diff --git a/src/common/p25/lc/AMBT.cpp b/src/common/p25/lc/AMBT.cpp index 36fb0f91..54cd1573 100644 --- a/src/common/p25/lc/AMBT.cpp +++ b/src/common/p25/lc/AMBT.cpp @@ -115,7 +115,7 @@ bool AMBT::decode(const data::DataHeader& dataHeader, const data::DataBlock* blo if (m_verbose) { LogDebugEx(LOG_P25, "AMBT::decode()", "mfId = $%02X, lco = $%02X, ambt8 = $%02X, ambt9 = $%02X", m_mfId, m_lco, dataHeader.getAMBTField8(), dataHeader.getAMBTField9()); - Utils::dump(2U, "[AMBT::decode()] pduUserData", pduUserData, P25_PDU_UNCONFIRMED_LENGTH_BYTES * dataHeader.getBlocksToFollow()); + Utils::dump(2U, "P25, AMBT::decode(), pduUserData", pduUserData, P25_PDU_UNCONFIRMED_LENGTH_BYTES * dataHeader.getBlocksToFollow()); } return true; diff --git a/src/common/p25/lc/LC.cpp b/src/common/p25/lc/LC.cpp index 9d67e65c..a0003e05 100644 --- a/src/common/p25/lc/LC.cpp +++ b/src/common/p25/lc/LC.cpp @@ -5,7 +5,7 @@ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * Copyright (C) 2016,2017 Jonathan Naylor, G4KLX -* Copyright (C) 2017-2024 Bryan Biedenkapp, N2PLL +* Copyright (C) 2017-2025 Bryan Biedenkapp, N2PLL * */ #include "Defines.h" @@ -67,10 +67,16 @@ LC::LC() : m_encryptOverride(false), m_tsbkVendorSkip(false), m_callTimer(0U), - m_mi(nullptr) + m_mi(nullptr), + m_userAlias(nullptr), + m_gotUserAliasPartA(false), + m_gotUserAlias(false) { m_mi = new uint8_t[MI_LENGTH_BYTES]; ::memset(m_mi, 0x00U, MI_LENGTH_BYTES); + + m_userAlias = new uint8_t[HARRIS_USER_ALIAS_LENGTH_BYTES]; + ::memset(m_userAlias, 0x00U, HARRIS_USER_ALIAS_LENGTH_BYTES); } /* Finalizes a instance of LC class. */ @@ -81,6 +87,11 @@ LC::~LC() delete[] m_mi; m_mi = nullptr; } + + if (m_userAlias != nullptr) { + delete[] m_userAlias; + m_userAlias = nullptr; + } } /* Equals operator. */ @@ -109,14 +120,14 @@ bool LC::decodeHDU(const uint8_t* data, bool rawOnly) P25Utils::decode(data, raw, 114U, 780U); #if DEBUG_P25_HDU - Utils::dump(2U, "LC::decodeHDU(), HDU Raw", raw, P25_HDU_LENGTH_BYTES); + Utils::dump(2U, "P25, LC::decodeHDU(), HDU Raw", raw, P25_HDU_LENGTH_BYTES); #endif // decode Golay (18,6,8) FEC decodeHDUGolay(raw, rs); #if DEBUG_P25_HDU - Utils::dump(2U, "LC::decodeHDU(), HDU RS", rs, P25_HDU_LENGTH_BYTES); + Utils::dump(2U, "P25, LC::decodeHDU(), HDU RS", rs, P25_HDU_LENGTH_BYTES); #endif // decode RS (36,20,17) FEC @@ -128,12 +139,12 @@ bool LC::decodeHDU(const uint8_t* data, bool rawOnly) } } catch (...) { - Utils::dump(2U, "P25, RS excepted with input data", rs, P25_HDU_LENGTH_BYTES); + Utils::dump(2U, "P25, LC::decodeHDU(), RS excepted with input data", rs, P25_HDU_LENGTH_BYTES); return false; } #if DEBUG_P25_HDU - Utils::dump(2U, "LC::decodeHDU(), HDU", rs, P25_HDU_LENGTH_BYTES); + Utils::dump(2U, "P25, LC::decodeHDU(), HDU", rs, P25_HDU_LENGTH_BYTES); #endif m_mfId = rs[9U]; // Mfg Id. @@ -193,14 +204,14 @@ void LC::encodeHDU(uint8_t* data, bool rawOnly) rs[14U] = (m_dstId >> 0) & 0xFFU; // ... #if DEBUG_P25_HDU - Utils::dump(2U, "LC::encodeHDU(), HDU", rs, P25_HDU_LENGTH_BYTES); + Utils::dump(2U, "P25, LC::encodeHDU(), HDU", rs, P25_HDU_LENGTH_BYTES); #endif // encode RS (36,20,17) FEC m_rs.encode362017(rs); #if DEBUG_P25_HDU - Utils::dump(2U, "LC::encodeHDU(), HDU RS", rs, P25_HDU_LENGTH_BYTES); + Utils::dump(2U, "P25, LC::encodeHDU(), HDU RS", rs, P25_HDU_LENGTH_BYTES); #endif uint8_t raw[P25_HDU_LENGTH_BYTES + 1U]; @@ -218,7 +229,7 @@ void LC::encodeHDU(uint8_t* data, bool rawOnly) P25Utils::encode(raw, data, 114U, 780U); #if DEBUG_P25_HDU - Utils::dump(2U, "LC::encodeHDU(), HDU Interleave", data, P25_HDU_FRAME_LENGTH_BYTES + P25_PREAMBLE_LENGTH_BYTES); + Utils::dump(2U, "P25, LC::encodeHDU(), HDU Interleave", data, P25_HDU_FRAME_LENGTH_BYTES + P25_PREAMBLE_LENGTH_BYTES); #endif } @@ -251,7 +262,7 @@ bool LC::decodeLDU1(const uint8_t* data, bool rawOnly) decodeLDUHamming(raw, rs + 15U); #if DEBUG_P25_LDU1 - Utils::dump(2U, "LC::decodeLDU1(), LDU1 RS", rs, P25_LDU_LC_FEC_LENGTH_BYTES); + Utils::dump(2U, "P25, LC::decodeLDU1(), LDU1 RS", rs, P25_LDU_LC_FEC_LENGTH_BYTES); #endif // decode RS (24,12,13) FEC @@ -263,12 +274,12 @@ bool LC::decodeLDU1(const uint8_t* data, bool rawOnly) } } catch (...) { - Utils::dump(2U, "P25, RS excepted with input data", rs, P25_LDU_LC_FEC_LENGTH_BYTES); + Utils::dump(2U, "P25, LC::decodeLDU1(), RS excepted with input data", rs, P25_LDU_LC_FEC_LENGTH_BYTES); return false; } #if DEBUG_P25_LDU1 - Utils::dump(2U, "LC::decodeLDU1(), LDU1 LC", rs, P25_LDU_LC_FEC_LENGTH_BYTES); + Utils::dump(2U, "P25, LC::decodeLDU1(), LDU1 LC", rs, P25_LDU_LC_FEC_LENGTH_BYTES); #endif return decodeLC(rs, rawOnly); @@ -286,14 +297,14 @@ void LC::encodeLDU1(uint8_t* data) encodeLC(rs); #if DEBUG_P25_LDU1 - Utils::dump(2U, "LC::encodeLDU1(), LDU1 LC", rs, P25_LDU_LC_FEC_LENGTH_BYTES); + Utils::dump(2U, "P25, LC::encodeLDU1(), LDU1 LC", rs, P25_LDU_LC_FEC_LENGTH_BYTES); #endif // encode RS (24,12,13) FEC m_rs.encode241213(rs); #if DEBUG_P25_LDU1 - Utils::dump(2U, "LC::encodeLDU1(), LDU1 RS", rs, P25_LDU_LC_FEC_LENGTH_BYTES); + Utils::dump(2U, "P25, LC::encodeLDU1(), LDU1 RS", rs, P25_LDU_LC_FEC_LENGTH_BYTES); #endif // encode Hamming (10,6,3) FEC and interleave for LC data @@ -317,7 +328,7 @@ void LC::encodeLDU1(uint8_t* data) P25Utils::encode(raw, data, 1356U, 1398U); #if DEBUG_P25_LDU1 - Utils::dump(2U, "LC::encodeLDU1(), LDU1 Interleave", data, P25_LDU_FRAME_LENGTH_BYTES + P25_PREAMBLE_LENGTH_BYTES); + Utils::dump(2U, "P25, LC::encodeLDU1(), LDU1 Interleave", data, P25_LDU_FRAME_LENGTH_BYTES + P25_PREAMBLE_LENGTH_BYTES); #endif } @@ -350,7 +361,7 @@ bool LC::decodeLDU2(const uint8_t* data) decodeLDUHamming(raw, rs + 15U); #if DEBUG_P25_LDU2 - Utils::dump(2U, "LC::decodeLDU2(), LDU2 RS", rs, P25_LDU_LC_FEC_LENGTH_BYTES); + Utils::dump(2U, "P25, LC::decodeLDU2(), LDU2 RS", rs, P25_LDU_LC_FEC_LENGTH_BYTES); #endif // decode RS (24,16,9) FEC @@ -362,12 +373,12 @@ bool LC::decodeLDU2(const uint8_t* data) } } catch (...) { - Utils::dump(2U, "P25, RS excepted with input data", rs, P25_LDU_LC_FEC_LENGTH_BYTES); + Utils::dump(2U, "P25, LC::decodeLDU2(), RS excepted with input data", rs, P25_LDU_LC_FEC_LENGTH_BYTES); return false; } #if DEBUG_P25_LDU2 - Utils::dump(2U, "LC::decodeLDU2(), LDU2 LC", rs, P25_LDU_LC_FEC_LENGTH_BYTES); + Utils::dump(2U, "P25, LC::decodeLDU2(), LDU2 LC", rs, P25_LDU_LC_FEC_LENGTH_BYTES); #endif m_algId = rs[9U]; // Algorithm ID @@ -418,14 +429,14 @@ void LC::encodeLDU2(uint8_t* data) rs[11U] = (m_kId >> 0) & 0xFFU; // ... #if DEBUG_P25_LDU2 - Utils::dump(2U, "LC::encodeLDU2(), LDU2 LC", rs, P25_LDU_LC_FEC_LENGTH_BYTES); + Utils::dump(2U, "P25, LC::encodeLDU2(), LDU2 LC", rs, P25_LDU_LC_FEC_LENGTH_BYTES); #endif // encode RS (24,16,9) FEC m_rs.encode24169(rs); #if DEBUG_P25_LDU2 - Utils::dump(2U, "LC::encodeLDU2(), LDU2 RS", rs, P25_LDU_LC_FEC_LENGTH_BYTES); + Utils::dump(2U, "P25, LC::encodeLDU2(), LDU2 RS", rs, P25_LDU_LC_FEC_LENGTH_BYTES); #endif // encode Hamming (10,6,3) FEC and interleave for LC data @@ -449,7 +460,7 @@ void LC::encodeLDU2(uint8_t* data) P25Utils::encode(raw, data, 1356U, 1398U); #if DEBUG_P25_LDU2 - Utils::dump(2U, "LC::encodeLDU2(), LDU2 Interleave", data, P25_LDU_FRAME_LENGTH_BYTES + P25_PREAMBLE_LENGTH_BYTES); + Utils::dump(2U, "P25, LC::encodeLDU2(), LDU2 Interleave", data, P25_LDU_FRAME_LENGTH_BYTES + P25_PREAMBLE_LENGTH_BYTES); #endif } @@ -462,97 +473,12 @@ bool LC::isStandardMFId() const return false; } -/* -** Encryption data -*/ - -/* Sets the encryption message indicator. */ - -void LC::setMI(const uint8_t* mi) -{ - assert(mi != nullptr); - - ::memcpy(m_mi, mi, MI_LENGTH_BYTES); -} - -/* Gets the encryption message indicator. */ - -void LC::getMI(uint8_t* mi) const -{ - assert(mi != nullptr); - - ::memcpy(mi, m_mi, MI_LENGTH_BYTES); -} - -// --------------------------------------------------------------------------- -// Private Class Members -// --------------------------------------------------------------------------- - -/* Internal helper to copy the the class. */ - -void LC::copy(const LC& data) -{ - m_lco = data.m_lco; - - m_protect = data.m_protect; - m_mfId = data.m_mfId; - - m_srcId = data.m_srcId; - m_dstId = data.m_dstId; - - m_grpVchNo = data.m_grpVchNo; - - m_grpVchNoB = data.m_grpVchNoB; - m_dstIdB = data.m_dstIdB; - - m_explicitId = data.m_explicitId; - - m_netId = data.m_netId; - m_sysId = data.m_sysId; - - m_emergency = data.m_emergency; - m_encrypted = data.m_encrypted; - m_priority = data.m_priority; - - m_group = data.m_group; - - m_callTimer = data.m_callTimer; - - m_rsValue = data.m_rsValue; - - m_algId = data.m_algId; - if (m_algId != ALGO_UNENCRYPT) { - delete[] m_mi; - - m_mi = new uint8_t[MI_LENGTH_BYTES]; - ::memcpy(m_mi, data.m_mi, MI_LENGTH_BYTES); - - m_kId = data.m_kId; - if (!m_encrypted) { - m_encryptOverride = true; - m_encrypted = true; - } - } - else { - 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_siteData = data.m_siteData; -} - /* Decode link control. */ bool LC::decodeLC(const uint8_t* rs, bool rawOnly) { + assert(rs != nullptr); + ulong64_t rsValue = 0U; // combine bytes into ulong64_t (8 byte) value @@ -569,7 +495,11 @@ bool LC::decodeLC(const uint8_t* rs, bool rawOnly) m_protect = (rs[0U] & 0x80U) == 0x80U; // Protect Flag m_lco = rs[0U] & 0x3FU; // LCO - m_mfId = rs[1U]; // Mfg Id. + bool sf = (rs[0U] & 0x40U) == 0x40U; // Implicit/Explicit Operation + if (sf) + m_mfId = MFG_STANDARD; + else + m_mfId = rs[1U]; // Mfg Id. if (rawOnly) return true; @@ -577,7 +507,36 @@ bool LC::decodeLC(const uint8_t* rs, bool rawOnly) // non-standard P25 vendor opcodes (these are just detected for passthru, and stored // as the packed RS value) if ((m_mfId != MFG_STANDARD) && (m_mfId != MFG_STANDARD_ALT)) { - //Utils::dump(1U, "Decoded P25 Non-Standard RS", rs, P25_LDU_LC_FEC_LENGTH_BYTES); + //Utils::dump(1U, "P25, LC::decodeLC(), Decoded P25 Non-Standard RS", rs, P25_LDU_LC_FEC_LENGTH_BYTES); + // Harris + if (m_mfId == MFG_HARRIS) { + // Harris P25 opcodes + switch (m_lco) { + case LCO::HARRIS_USER_ALIAS_PA_ODD: + case LCO::HARRIS_USER_ALIAS_PA_EVEN: + m_gotUserAliasPartA = true; + m_gotUserAlias = false; + + if (m_userAlias != nullptr) { + ::memset(m_userAlias, 0x00U, HARRIS_USER_ALIAS_LENGTH_BYTES); + ::memcpy(m_userAlias, rs + 2U, 7U); + m_gotUserAlias = true; + } + break; + + case LCO::HARRIS_USER_ALIAS_PB_ODD: + case LCO::HARRIS_USER_ALIAS_PB_EVEN: + if (m_gotUserAliasPartA && (m_userAlias != nullptr)) { + ::memcpy(m_userAlias + 7U, rs + 2U, 7U); + m_gotUserAlias = true; + } + break; + + default: + break; + } + } + return true; } @@ -622,6 +581,18 @@ bool LC::decodeLC(const uint8_t* rs, bool rawOnly) m_sysId = (uint32_t)((rsValue >> 24) & 0xFFFU); // System ID m_srcId = (uint32_t)(rsValue & 0xFFFFFFU); // Source Radio Address break; + case LCO::PRIVATE_EXT: + m_explicitId = (rs[1U] & 0x01U) == 0x01U; // Explicit Source ID Flag + m_group = false; + m_emergency = (rs[2U] & 0x80U) == 0x80U; // Emergency Flag + if (!m_encryptOverride) { + m_encrypted = (rs[2U] & 0x40U) == 0x40U; // Encryption Flag + } + m_priority = (rs[2U] & 0x07U); // Priority + m_explicitId = (rs[3U] & 0x01U) == 0x01U; // Explicit Source ID Flag + m_dstId = (uint32_t)((rsValue >> 24) & 0xFFFFFFU); // Target Radio Address + m_srcId = (uint32_t)(rsValue & 0xFFFFFFU); // Source Radio Address + break; default: LogError(LOG_P25, "LC::decodeLC(), unknown LC value, mfId = $%02X, lco = $%02X", m_mfId, m_lco); return false; @@ -639,19 +610,65 @@ bool LC::decodeLC(const uint8_t* rs, bool rawOnly) void LC::encodeLC(uint8_t* rs) { + assert(rs != nullptr); + ulong64_t rsValue = 0U; rs[0U] = m_lco; // LCO + // non-standard P25 vendor opcodes (these are just detected for passthru, and stored + // as the packed RS value) + if ((m_mfId != MFG_STANDARD) && (m_mfId != MFG_STANDARD_ALT)) { + //Utils::dump(1U, "P25, LC::decodeLC(), Decoded P25 Non-Standard RS", rs, P25_LDU_LC_FEC_LENGTH_BYTES); + if (m_mfId == MFG_HARRIS) { + // Harris P25 opcodes + switch (m_lco) { + case LCO::HARRIS_USER_ALIAS_PA_ODD: + case LCO::HARRIS_USER_ALIAS_PA_EVEN: + if (m_userAlias != nullptr) { + // split ulong64_t (8 byte) value into bytes + rs[1U] = m_mfId; // Manufacturer ID + rs[2U] = m_userAlias[0U]; + rs[3U] = m_userAlias[1U]; + rs[4U] = m_userAlias[2U]; + rs[5U] = m_userAlias[3U]; + rs[6U] = m_userAlias[4U]; + rs[7U] = m_userAlias[5U]; + rs[8U] = m_userAlias[6U]; + } + return; + + case LCO::HARRIS_USER_ALIAS_PB_ODD: + case LCO::HARRIS_USER_ALIAS_PB_EVEN: + if (m_userAlias != nullptr) { + // split ulong64_t (8 byte) value into bytes + rs[1U] = m_mfId; // Manufacturer ID + rs[2U] = m_userAlias[7U]; + rs[3U] = m_userAlias[8U]; + rs[4U] = m_userAlias[9U]; + rs[5U] = m_userAlias[10U]; + rs[6U] = m_userAlias[11U]; + rs[7U] = m_userAlias[12U]; + rs[8U] = m_userAlias[13U]; + } + return; + + default: + break; + } + } + } + if ((m_mfId == MFG_STANDARD) || (m_mfId == MFG_STANDARD_ALT)) { // standard P25 reference opcodes switch (m_lco) { case LCO::GROUP: - rsValue = m_mfId; + rsValue = m_mfId; // Manufacturer ID rsValue = (rsValue << 8) + (m_emergency ? 0x80U : 0x00U) + // Emergency Flag (m_encrypted ? 0x40U : 0x00U) + // Encrypted Flag (m_priority & 0x07U); // Priority - rsValue = (rsValue << 24) + m_dstId; // Talkgroup Address + rsValue = (rsValue << 8) + (m_explicitId ? 0x01U : 0x00U); // Explicit Source ID Flag + rsValue = (rsValue << 16) + m_dstId; // Talkgroup Address rsValue = (rsValue << 24) + m_srcId; // Source Radio Address break; case LCO::GROUP_UPDT: @@ -664,7 +681,7 @@ void LC::encodeLC(uint8_t* rs) rsValue = (rsValue << 16) + m_dstIdB; // Group B - Talkgroup Address break; case LCO::PRIVATE: - rsValue = m_mfId; + rsValue = m_mfId; // Manufacturer ID rsValue = (rsValue << 8) + (m_emergency ? 0x80U : 0x00U) + // Emergency Flag (m_encrypted ? 0x40U : 0x00U) + // Encrypted Flag @@ -682,10 +699,21 @@ void LC::encodeLC(uint8_t* rs) rsValue = (rsValue << 24) + m_srcId; // Source/Target Radio Address break; case LCO::EXPLICIT_SOURCE_ID: - rsValue = m_netId; // Network ID + rs[0U] |= 0x40U; // Implicit Operation + rsValue = (rsValue << 8) + m_netId; // Network ID rsValue = (rsValue << 12) + (m_sysId & 0xFFFU); // System ID rsValue = (rsValue << 24) + m_srcId; // Source Radio Address break; + case LCO::PRIVATE_EXT: + rs[0U] |= 0x40U; // Implicit Operation + rsValue = (m_explicitId ? 0x01U : 0x00U); // Explicit Source ID Flag + rsValue = (rsValue << 8) + + (m_emergency ? 0x80U : 0x00U) + // Emergency Flag + (m_encrypted ? 0x40U : 0x00U) + // Encrypted Flag + (m_priority & 0x07U); // Priority + rsValue = (rsValue << 24) + m_dstId; // Target Radio Address + rsValue = (rsValue << 24) + m_srcId; // Source Radio Address + break; case LCO::RFSS_STS_BCAST: rs[0U] |= 0x40U; // Implicit Operation rsValue = m_siteData.lra(); // Location Registration Area @@ -720,11 +748,142 @@ void LC::encodeLC(uint8_t* rs) rs[8U] = (uint8_t)((rsValue >> 0) & 0xFFU); /* if ((m_mfId != MFG_STANDARD) && (m_mfId != MFG_STANDARD_ALT)) { - Utils::dump(1U, "Encoded P25 Non-Standard RS", rs, P25_LDU_LC_FEC_LENGTH_BYTES); + Utils::dump(1U, "P25, LC::encodeLC(), Encoded P25 Non-Standard RS", rs, P25_LDU_LC_FEC_LENGTH_BYTES); } */ } +/* +** Encryption data +*/ + +/* Sets the encryption message indicator. */ + +void LC::setMI(const uint8_t* mi) +{ + assert(mi != nullptr); + + ::memcpy(m_mi, mi, MI_LENGTH_BYTES); +} + +/* Gets the encryption message indicator. */ + +void LC::getMI(uint8_t* mi) const +{ + assert(mi != nullptr); + + ::memcpy(mi, m_mi, MI_LENGTH_BYTES); +} + +/* +** User Alias data +*/ + +/* Gets the user alias. */ + +std::string LC::getUserAlias() const +{ + std::string alias; + if (m_gotUserAlias) { + for (uint32_t i = 0; i < HARRIS_USER_ALIAS_LENGTH_BYTES; i++) + alias[i] = m_userAlias[i]; + } + + return alias; +} + +/* Sets the user alias. */ + +void LC::setUserAlias(std::string alias) +{ + if (m_userAlias == nullptr) + m_userAlias = new uint8_t[HARRIS_USER_ALIAS_LENGTH_BYTES]; + + ::memset(m_userAlias, 0x00U, HARRIS_USER_ALIAS_LENGTH_BYTES); + for (uint32_t i = 0; i < HARRIS_USER_ALIAS_LENGTH_BYTES; i++) + m_userAlias[i] = alias[i]; +} + +// --------------------------------------------------------------------------- +// Private Class Members +// --------------------------------------------------------------------------- + +/* Internal helper to copy the the class. */ + +void LC::copy(const LC& data) +{ + m_lco = data.m_lco; + + m_protect = data.m_protect; + m_mfId = data.m_mfId; + + m_srcId = data.m_srcId; + m_dstId = data.m_dstId; + + m_grpVchNo = data.m_grpVchNo; + + m_grpVchNoB = data.m_grpVchNoB; + m_dstIdB = data.m_dstIdB; + + m_explicitId = data.m_explicitId; + + m_netId = data.m_netId; + m_sysId = data.m_sysId; + + m_emergency = data.m_emergency; + m_encrypted = data.m_encrypted; + m_priority = data.m_priority; + + m_group = data.m_group; + + m_callTimer = data.m_callTimer; + + m_rsValue = data.m_rsValue; + + m_algId = data.m_algId; + if (m_algId != ALGO_UNENCRYPT) { + delete[] m_mi; + + m_mi = new uint8_t[MI_LENGTH_BYTES]; + ::memcpy(m_mi, data.m_mi, MI_LENGTH_BYTES); + + m_kId = data.m_kId; + if (!m_encrypted) { + m_encryptOverride = true; + m_encrypted = true; + } + } + else { + 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; + } + } + + // do we have user alias data to copy? + if (data.m_gotUserAlias && data.m_userAlias != nullptr) { + delete[] m_userAlias; + + m_userAlias = new uint8_t[HARRIS_USER_ALIAS_LENGTH_BYTES]; + ::memcpy(m_userAlias, data.m_userAlias, HARRIS_USER_ALIAS_LENGTH_BYTES); + m_gotUserAlias = data.m_gotUserAlias; + } else { + delete[] m_userAlias; + + m_userAlias = new uint8_t[HARRIS_USER_ALIAS_LENGTH_BYTES]; + ::memset(m_userAlias, 0x00U, HARRIS_USER_ALIAS_LENGTH_BYTES); + m_gotUserAlias = false; + } + + m_siteData = data.m_siteData; +} + /* Decode LDU hamming FEC. */ void LC::decodeLDUHamming(const uint8_t* data, uint8_t* raw) diff --git a/src/common/p25/lc/LC.h b/src/common/p25/lc/LC.h index 0a54f933..1af45054 100644 --- a/src/common/p25/lc/LC.h +++ b/src/common/p25/lc/LC.h @@ -5,7 +5,7 @@ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * Copyright (C) 2016 Jonathan Naylor, G4KLX - * Copyright (C) 2017-2024 Bryan Biedenkapp, N2PLL + * Copyright (C) 2017-2025 Bryan Biedenkapp, N2PLL * */ /** @@ -115,6 +115,19 @@ namespace p25 */ bool isStandardMFId() const; + /** + * @brief Decode link control. + * @param[in] rs Buffer containing the decoded Reed-Solomon LC data. + * @param rawOnly Flag indicating only the raw bytes of the LC should be decoded. + * @returns bool True, if LC is decoded, otherwise false. + */ + bool decodeLC(const uint8_t* rs, bool rawOnly = false); + /** + * @brief Encode link control. + * @param[out] rs Buffer to encode LC data. + */ + void encodeLC(uint8_t* rs); + /** @name Encryption data */ /** * @brief Sets the encryption message indicator. @@ -128,6 +141,18 @@ namespace p25 void getMI(uint8_t* mi) const; /** @} */ + /** @name User Alias data */ + /** + * @brief Gets the user alias. + * @returns std::string User Alias. + */ + std::string getUserAlias() const; + /** + * @brief Sets the user alias. + * @param alias User alias. + */ + void setUserAlias(std::string alias); + /** @name Local Site data */ /** * @brief Gets the local site data. @@ -243,6 +268,11 @@ namespace p25 // Encryption data uint8_t* m_mi; + // User Alias data + uint8_t* m_userAlias; + bool m_gotUserAliasPartA; + bool m_gotUserAlias; + // Local Site data static SiteData m_siteData; @@ -251,19 +281,6 @@ namespace p25 */ void copy(const LC& data); - /** - * @brief Decode link control. - * @param[in] rs Buffer containing the decoded Reed-Solomon LC data. - * @param rawOnly Flag indicating only the raw bytes of the LC should be decoded. - * @returns bool True, if LC is decoded, otherwise false. - */ - bool decodeLC(const uint8_t* rs, bool rawOnly = false); - /** - * @brief Encode link control. - * @param[out] rs Buffer to encode LC data. - */ - void encodeLC(uint8_t* rs); - /** * @brief Decode LDU hamming FEC. * @param[in] raw diff --git a/src/common/p25/lc/TDULC.cpp b/src/common/p25/lc/TDULC.cpp index 9fec40f4..94025eb0 100644 --- a/src/common/p25/lc/TDULC.cpp +++ b/src/common/p25/lc/TDULC.cpp @@ -4,7 +4,7 @@ * GPLv2 Open Source. Use is subject to license terms. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * - * Copyright (C) 2017-2024 Bryan Biedenkapp, N2PLL + * Copyright (C) 2017-2025 Bryan Biedenkapp, N2PLL * */ #include "Defines.h" @@ -72,6 +72,9 @@ TDULC::TDULC() : m_srcId(0U), m_dstId(0U), m_grpVchNo(0U), + m_explicitId(false), + m_netId(WACN_STD_DEFAULT), + m_sysId(SID_STD_DEFAULT), m_emergency(false), m_encrypted(false), m_priority(4U), @@ -79,7 +82,8 @@ TDULC::TDULC() : m_siteIdenEntry(lookups::IdenTable()), m_rs(), m_implicit(false), - m_callTimer(0U) + m_callTimer(0U), + m_raw(nullptr) { m_grpVchNo = m_siteData.channelNo(); } @@ -88,7 +92,15 @@ TDULC::TDULC() : TDULC::~TDULC() { - /* stub */ + if (m_raw != nullptr) + delete[] m_raw; +} + +/* Returns a copy of the raw decoded TDULC bytes. */ + +uint8_t* TDULC::getDecodedRaw() const +{ + return m_raw; } // --------------------------------------------------------------------------- @@ -138,84 +150,99 @@ UInt8Array TDULC::fromValue(const ulong64_t value) /* Internal helper to decode a terminator data unit w/ link control. */ -bool TDULC::decode(const uint8_t* data, uint8_t* payload) +bool TDULC::decode(const uint8_t* data, uint8_t* payload, bool rawTDULC) { assert(data != nullptr); assert(payload != nullptr); - uint8_t rs[P25_TDULC_LENGTH_BYTES]; - ::memset(rs, 0x00U, P25_TDULC_LENGTH_BYTES); + if (rawTDULC) { + ::memcpy(payload, data, P25_TDULC_PAYLOAD_LENGTH_BYTES); + return true; + } else { + uint8_t rs[P25_TDULC_LENGTH_BYTES]; + ::memset(rs, 0x00U, P25_TDULC_LENGTH_BYTES); - // deinterleave - uint8_t raw[P25_TDULC_FEC_LENGTH_BYTES + 1U]; - P25Utils::decode(data, raw, 114U, 410U); + // deinterleave + uint8_t raw[P25_TDULC_FEC_LENGTH_BYTES + 1U]; + P25Utils::decode(data, raw, 114U, 410U); - // decode Golay (24,12,8) FEC - edac::Golay24128::decode24128(rs, raw, P25_TDULC_LENGTH_BYTES); + // decode Golay (24,12,8) FEC + edac::Golay24128::decode24128(rs, raw, P25_TDULC_LENGTH_BYTES); #if DEBUG_P25_TDULC - Utils::dump(2U, "TDULC::decode(), TDULC RS", rs, P25_TDULC_LENGTH_BYTES); + Utils::dump(2U, "P25, TDULC::decode(), TDULC RS", rs, P25_TDULC_LENGTH_BYTES); #endif - // decode RS (24,12,13) FEC - try { - bool ret = m_rs.decode241213(rs); - if (!ret) { - LogError(LOG_P25, "TDULC::decode(), failed to decode RS (24,12,13) FEC"); + // decode RS (24,12,13) FEC + try { + bool ret = m_rs.decode241213(rs); + if (!ret) { + LogError(LOG_P25, "TDULC::decode(), failed to decode RS (24,12,13) FEC"); + return false; + } + } + catch (...) { + Utils::dump(2U, "P25, TDULC::decode(), RS excepted with input data", rs, P25_TDULC_LENGTH_BYTES); return false; } - } - catch (...) { - Utils::dump(2U, "P25, RS excepted with input data", rs, P25_TDULC_LENGTH_BYTES); - return false; - } - if (m_verbose) { - Utils::dump(2U, "TDULC::decode(), TDULC Value", rs, P25_TDULC_LENGTH_BYTES); - } + if (m_verbose) { + Utils::dump(2U, "P25, TDULC::decode(), TDULC Value", rs, P25_TDULC_LENGTH_BYTES); + } + + 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); - ::memcpy(payload, rs + 1U, P25_TDULC_PAYLOAD_LENGTH_BYTES); - return true; + ::memcpy(payload, rs + 1U, P25_TDULC_PAYLOAD_LENGTH_BYTES); + return true; + } } /* Internal helper to encode a terminator data unit w/ link control. */ -void TDULC::encode(uint8_t* data, const uint8_t* payload) +void TDULC::encode(uint8_t* data, const uint8_t* payload, bool rawTDULC) { assert(data != nullptr); assert(payload != nullptr); - uint8_t rs[P25_TDULC_LENGTH_BYTES]; - ::memset(rs, 0x00U, P25_TDULC_LENGTH_BYTES); - ::memcpy(rs + 1U, payload, P25_TDULC_PAYLOAD_LENGTH_BYTES); + if (rawTDULC) { + ::memcpy(data, payload, P25_TDULC_PAYLOAD_LENGTH_BYTES); + return; + } else { + uint8_t rs[P25_TDULC_LENGTH_BYTES]; + ::memset(rs, 0x00U, P25_TDULC_LENGTH_BYTES); + ::memcpy(rs + 1U, payload, P25_TDULC_PAYLOAD_LENGTH_BYTES); - rs[0U] = m_lco; // LCO - if (m_implicit) - rs[0U] |= 0x40U; // Implicit Operation + rs[0U] = m_lco; // LCO + if (m_implicit) + rs[0U] |= 0x40U; // Implicit Operation - if (m_verbose) { - Utils::dump(2U, "TDULC::encode(), TDULC Value", rs, P25_TDULC_LENGTH_BYTES); - } + if (m_verbose) { + Utils::dump(2U, "P25, TDULC::encode(), TDULC Value", rs, P25_TDULC_LENGTH_BYTES); + } - // encode RS (24,12,13) FEC - m_rs.encode241213(rs); + // encode RS (24,12,13) FEC + m_rs.encode241213(rs); #if DEBUG_P25_TDULC - Utils::dump(2U, "TDULC::encode(), TDULC RS", rs, P25_TDULC_LENGTH_BYTES); + Utils::dump(2U, "P25, TDULC::encode(), TDULC RS", rs, P25_TDULC_LENGTH_BYTES); #endif - uint8_t raw[P25_TDULC_FEC_LENGTH_BYTES + 1U]; - ::memset(raw, 0x00U, P25_TDULC_FEC_LENGTH_BYTES + 1U); + uint8_t raw[P25_TDULC_FEC_LENGTH_BYTES + 1U]; + ::memset(raw, 0x00U, P25_TDULC_FEC_LENGTH_BYTES + 1U); - // encode Golay (24,12,8) FEC - edac::Golay24128::encode24128(raw, rs, P25_TDULC_LENGTH_BYTES); + // encode Golay (24,12,8) FEC + edac::Golay24128::encode24128(raw, rs, P25_TDULC_LENGTH_BYTES); - // interleave - P25Utils::encode(raw, data, 114U, 410U); + // interleave + P25Utils::encode(raw, data, 114U, 410U); #if DEBUG_P25_TDULC - Utils::dump(2U, "TDULC::encode(), TDULC Interleave", data, P25_TDULC_FRAME_LENGTH_BYTES + P25_PREAMBLE_LENGTH_BYTES); + Utils::dump(2U, "P25, TDULC::encode(), TDULC Interleave", data, P25_TDULC_FRAME_LENGTH_BYTES + P25_PREAMBLE_LENGTH_BYTES); #endif + } } /* Internal helper to copy the the class. */ @@ -232,6 +259,11 @@ void TDULC::copy(const TDULC& data) m_grpVchNo = data.m_grpVchNo; + m_explicitId = data.m_explicitId; + + m_netId = data.m_netId; + m_sysId = data.m_sysId; + m_emergency = data.m_emergency; m_encrypted = data.m_encrypted; m_priority = data.m_priority; diff --git a/src/common/p25/lc/TDULC.h b/src/common/p25/lc/TDULC.h index 163afa89..9e1ccf4d 100644 --- a/src/common/p25/lc/TDULC.h +++ b/src/common/p25/lc/TDULC.h @@ -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-2022 Bryan Biedenkapp, N2PLL + * Copyright (C) 2017-2025 Bryan Biedenkapp, N2PLL * */ /** @@ -79,6 +79,13 @@ namespace p25 */ virtual void encode(uint8_t* data) = 0; + /** + * @brief Returns a copy of the raw decoded TDULC bytes. + * This will only return data for a *decoded* TDULC, not a created or copied TDULC. + * @returns uint8_t* Raw decoded TDULC bytes. + */ + uint8_t* getDecodedRaw() const; + /** * @brief Sets the flag indicating verbose log output. * @param verbose Flag indicating verbose log output. @@ -126,6 +133,20 @@ namespace p25 * @brief Voice channel number. */ DECLARE_PROTECTED_PROPERTY(uint32_t, grpVchNo, GrpVchNo); + + /** + * @brief Flag indicating explicit addressing. + */ + DECLARE_PROTECTED_PROPERTY(bool, explicitId, ExplicitId); + + /** + * @brief Network ID. + */ + DECLARE_PROTECTED_PROPERTY(uint32_t, netId, NetId); + /** + * @brief System ID. + */ + DECLARE_PROTECTED_PROPERTY(uint32_t, sysId, SysId); /** @} */ /** @name Service Options */ @@ -184,16 +205,21 @@ namespace p25 * @brief Internal helper to decode terminator data unit w/ link control. * @param[in] data Raw data. * @param[out] payload TDULC payload buffer. + * @param rawTDULC Flag indicating whether or not the passed buffer is raw. */ - bool decode(const uint8_t* data, uint8_t* payload); + bool decode(const uint8_t* data, uint8_t* payload, bool rawTDULC = false); /** * @brief Internal helper to encode terminator data unit w/ link control. * @param[out] data Raw data. * @param[in] payload TDULC payload buffer. + * @param rawTDULC Flag indicating whether or not the passed buffer is raw. */ - void encode(uint8_t* data, const uint8_t* payload); + void encode(uint8_t* data, const uint8_t* payload, bool rawTDULC = false); DECLARE_PROTECTED_COPY(TDULC); + + private: + uint8_t* m_raw; }; } // namespace lc } // namespace p25 diff --git a/src/common/p25/lc/TSBK.cpp b/src/common/p25/lc/TSBK.cpp index 0e5c0aa1..da39bc4f 100644 --- a/src/common/p25/lc/TSBK.cpp +++ b/src/common/p25/lc/TSBK.cpp @@ -196,8 +196,7 @@ bool TSBK::decode(const uint8_t* data, uint8_t* payload, bool rawTSBK) uint8_t tsbk[P25_TSBK_LENGTH_BYTES]; ::memset(tsbk, 0x00U, P25_TSBK_LENGTH_BYTES); - if (rawTSBK) - { + if (rawTSBK) { ::memcpy(tsbk, data, P25_TSBK_LENGTH_BYTES); bool ret = edac::CRC::checkCCITT162(tsbk, P25_TSBK_LENGTH_BYTES); @@ -243,15 +242,17 @@ bool TSBK::decode(const uint8_t* data, uint8_t* payload, bool rawTSBK) return false; } catch (...) { - Utils::dump(2U, "P25, decoding excepted with input data", tsbk, P25_TSBK_LENGTH_BYTES); + Utils::dump(2U, "P25, TSBK::decode(), decoding excepted with input data", tsbk, P25_TSBK_LENGTH_BYTES); return false; } } if (m_verbose) { - Utils::dump(2U, "TSBK::decode(), TSBK Value", tsbk, P25_TSBK_LENGTH_BYTES); + Utils::dump(2U, "P25, TSBK::decode(), TSBK Value", tsbk, P25_TSBK_LENGTH_BYTES); } + if (m_raw != nullptr) + delete[] m_raw; m_raw = new uint8_t[P25_TSBK_LENGTH_BYTES]; ::memcpy(m_raw, tsbk, P25_TSBK_LENGTH_BYTES); @@ -282,7 +283,7 @@ void TSBK::encode(uint8_t* data, const uint8_t* payload, bool rawTSBK, bool noTr edac::CRC::addCCITT162(tsbk, P25_TSBK_LENGTH_BYTES); if (m_verbose) { - Utils::dump(2U, "TSBK::encode(), TSBK Value", tsbk, P25_TSBK_LENGTH_BYTES); + Utils::dump(2U, "P25, TSBK::encode(), TSBK Value", tsbk, P25_TSBK_LENGTH_BYTES); } uint8_t raw[P25_TSBK_FEC_LENGTH_BYTES]; @@ -305,7 +306,7 @@ void TSBK::encode(uint8_t* data, const uint8_t* payload, bool rawTSBK, bool noTr P25Utils::encode(raw, data, 114U, 318U); #if DEBUG_P25_TSBK - Utils::dump(2U, "TSBK::encode(), TSBK Interleave", data, P25_TSBK_FEC_LENGTH_BYTES + P25_PREAMBLE_LENGTH_BYTES); + Utils::dump(2U, "P25, TSBK::encode(), TSBK Interleave", data, P25_TSBK_FEC_LENGTH_BYTES + P25_PREAMBLE_LENGTH_BYTES); #endif } } diff --git a/src/common/p25/lc/tdulc/LC_ADJ_STS_BCAST.h b/src/common/p25/lc/tdulc/LC_ADJ_STS_BCAST.h index c0b52204..3c917977 100644 --- a/src/common/p25/lc/tdulc/LC_ADJ_STS_BCAST.h +++ b/src/common/p25/lc/tdulc/LC_ADJ_STS_BCAST.h @@ -13,8 +13,8 @@ * @file LC_ADJ_STS_BCAST.cpp * @ingroup p25_lc */ -#if !defined(__P25_LC_TSBK__LC_ADJ_STS_BCAST_H__) -#define __P25_LC_TSBK__LC_ADJ_STS_BCAST_H__ +#if !defined(__P25_LC_TDULC__LC_ADJ_STS_BCAST_H__) +#define __P25_LC_TDULC__LC_ADJ_STS_BCAST_H__ #include "common/Defines.h" #include "common/p25/lc/TDULC.h" @@ -88,4 +88,4 @@ namespace p25 } // namespace lc } // namespace p25 -#endif // __P25_LC_TSBK__LC_ADJ_STS_BCAST_H__ +#endif // __P25_LC_TDULC__LC_ADJ_STS_BCAST_H__ diff --git a/src/common/p25/lc/tdulc/LC_CALL_TERM.h b/src/common/p25/lc/tdulc/LC_CALL_TERM.h index d409cb06..fb0dc5ea 100644 --- a/src/common/p25/lc/tdulc/LC_CALL_TERM.h +++ b/src/common/p25/lc/tdulc/LC_CALL_TERM.h @@ -13,8 +13,8 @@ * @file LC_CALL_TERM.cpp * @ingroup p25_lc */ -#if !defined(__P25_LC_TSBK__LC_CALL_TERM_H__) -#define __P25_LC_TSBK__LC_CALL_TERM_H__ +#if !defined(__P25_LC_TDULC__LC_CALL_TERM_H__) +#define __P25_LC_TDULC__LC_CALL_TERM_H__ #include "common/Defines.h" #include "common/p25/lc/TDULC.h" @@ -56,4 +56,4 @@ namespace p25 } // namespace lc } // namespace p25 -#endif // __P25_LC_TSBK__LC_CALL_TERM_H__ +#endif // __P25_LC_TDULC__LC_CALL_TERM_H__ diff --git a/src/common/p25/lc/tdulc/LC_CONV_FALLBACK.h b/src/common/p25/lc/tdulc/LC_CONV_FALLBACK.h index 9c60aadd..858e2e08 100644 --- a/src/common/p25/lc/tdulc/LC_CONV_FALLBACK.h +++ b/src/common/p25/lc/tdulc/LC_CONV_FALLBACK.h @@ -13,8 +13,8 @@ * @file LC_CONV_FALLBACK.cpp * @ingroup p25_lc */ -#if !defined(__P25_LC_TSBK__LC_CONV_FALLBACK_H__) -#define __P25_LC_TSBK__LC_CONV_FALLBACK_H__ +#if !defined(__P25_LC_TDULC__LC_CONV_FALLBACK_H__) +#define __P25_LC_TDULC__LC_CONV_FALLBACK_H__ #include "common/Defines.h" #include "common/p25/lc/TDULC.h" @@ -56,4 +56,4 @@ namespace p25 } // namespace lc } // namespace p25 -#endif // __P25_LC_TSBK__LC_CONV_FALLBACK_H__ +#endif // __P25_LC_TDULC__LC_CONV_FALLBACK_H__ diff --git a/src/common/p25/lc/tdulc/LC_EXPLICIT_SOURCE_ID.cpp b/src/common/p25/lc/tdulc/LC_EXPLICIT_SOURCE_ID.cpp new file mode 100644 index 00000000..5ce90d51 --- /dev/null +++ b/src/common/p25/lc/tdulc/LC_EXPLICIT_SOURCE_ID.cpp @@ -0,0 +1,69 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Digital Voice Modem - Common Library + * GPLv2 Open Source. Use is subject to license terms. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * Copyright (C) 2025 Bryan Biedenkapp, N2PLL + * + */ +#include "Defines.h" +#include "p25/lc/tdulc/LC_EXPLICIT_SOURCE_ID.h" + +using namespace p25; +using namespace p25::defines; +using namespace p25::lc; +using namespace p25::lc::tdulc; + +#include + +// --------------------------------------------------------------------------- +// Public Class Members +// --------------------------------------------------------------------------- + +/* Initializes a new instance of the LC_EXPLICIT_SOURCE_ID class. */ + +LC_EXPLICIT_SOURCE_ID::LC_EXPLICIT_SOURCE_ID() : TDULC() +{ + m_lco = LCO::EXPLICIT_SOURCE_ID; +} + +/* Decode a terminator data unit w/ link control. */ + +bool LC_EXPLICIT_SOURCE_ID::decode(const uint8_t* data) +{ + assert(data != nullptr); + + uint8_t rs[P25_TDULC_LENGTH_BYTES + 1U]; + ::memset(rs, 0x00U, P25_TDULC_LENGTH_BYTES); + + bool ret = TDULC::decode(data, rs); + if (!ret) + return false; + + ulong64_t rsValue = TDULC::toValue(rs); + + m_netId = (uint32_t)((rsValue >> 36) & 0xFFFFFU); // Network ID + m_sysId = (uint32_t)((rsValue >> 24) & 0xFFFU); // System ID + m_srcId = (uint32_t)(rsValue & 0xFFFFFFU); // Source Radio Address + + return true; +} + +/* Encode a terminator data unit w/ link control. */ + +void LC_EXPLICIT_SOURCE_ID::encode(uint8_t* data) +{ + assert(data != nullptr); + + ulong64_t rsValue = 0U; + + m_implicit = true; + + rsValue = (rsValue << 8) + m_netId; // Network ID + rsValue = (rsValue << 12) + (m_sysId & 0xFFFU); // System ID + rsValue = (rsValue << 24) + m_srcId; // Source Radio Address + + std::unique_ptr rs = TDULC::fromValue(rsValue); + TDULC::encode(data, rs.get()); +} diff --git a/src/common/p25/lc/tdulc/LC_EXPLICIT_SOURCE_ID.h b/src/common/p25/lc/tdulc/LC_EXPLICIT_SOURCE_ID.h new file mode 100644 index 00000000..0f95255a --- /dev/null +++ b/src/common/p25/lc/tdulc/LC_EXPLICIT_SOURCE_ID.h @@ -0,0 +1,59 @@ +// 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 LC_EXPLICIT_SOURCE_ID.h + * @ingroup p25_lc + * @file LC_EXPLICIT_SOURCE_ID.cpp + * @ingroup p25_lc + */ +#if !defined(__P25_LC_TDULC__LC_EXPLICIT_SOURCE_ID_H__) +#define __P25_LC_TDULC__LC_EXPLICIT_SOURCE_ID_H__ + +#include "common/Defines.h" +#include "common/p25/lc/TDULC.h" + +namespace p25 +{ + namespace lc + { + namespace tdulc + { + // --------------------------------------------------------------------------- + // Class Declaration + // --------------------------------------------------------------------------- + + /** + * @brief Implements EXPLICIT SOURCE ID - Explicit Source ID + * @ingroup p25_lc + */ + class HOST_SW_API LC_EXPLICIT_SOURCE_ID : public TDULC { + public: + /** + * @brief Initializes a new instance of the LC_EXPLICIT_SOURCE_ID class. + */ + LC_EXPLICIT_SOURCE_ID(); + + /** + * @brief Decode a terminator data unit w/ link control. + * @param[in] data Buffer containing a TDULC to decode. + * @returns bool True, if TDULC decoded, otherwise false. + */ + bool decode(const uint8_t* data) override; + /** + * @brief Encode a terminator data unit w/ link control. + * @param[out] data Buffer to encode a TDULC. + */ + void encode(uint8_t* data) override; + }; + } // namespace tdulc + } // namespace lc +} // namespace p25 + +#endif // __P25_LC_TDULC__LC_EXPLICIT_SOURCE_ID_H__ diff --git a/src/common/p25/lc/tdulc/LC_FAILSOFT.h b/src/common/p25/lc/tdulc/LC_FAILSOFT.h index 7918a755..8cb418df 100644 --- a/src/common/p25/lc/tdulc/LC_FAILSOFT.h +++ b/src/common/p25/lc/tdulc/LC_FAILSOFT.h @@ -13,8 +13,8 @@ * @file LC_FAILSOFT.cpp * @ingroup p25_lc */ -#if !defined(__P25_LC_TSBK__LC_FAILSOFT_H__) -#define __P25_LC_TSBK__LC_FAILSOFT_H__ +#if !defined(__P25_LC_TDULC__LC_FAILSOFT_H__) +#define __P25_LC_TDULC__LC_FAILSOFT_H__ #include "common/Defines.h" #include "common/p25/lc/TDULC.h" @@ -56,4 +56,4 @@ namespace p25 } // namespace lc } // namespace p25 -#endif // __P25_LC_TSBK__LC_FAILSOFT_H__ +#endif // __P25_LC_TDULC__LC_FAILSOFT_H__ diff --git a/src/common/p25/lc/tdulc/LC_GROUP.cpp b/src/common/p25/lc/tdulc/LC_GROUP.cpp index 6395fcb7..2da97f32 100644 --- a/src/common/p25/lc/tdulc/LC_GROUP.cpp +++ b/src/common/p25/lc/tdulc/LC_GROUP.cpp @@ -48,7 +48,8 @@ bool LC_GROUP::decode(const uint8_t* data) m_emergency = (rs[2U] & 0x80U) == 0x80U; // Emergency Flag m_encrypted = (rs[2U] & 0x40U) == 0x40U; // Encryption Flag m_priority = (rs[2U] & 0x07U); // Priority - m_dstId = (uint32_t)((rsValue >> 24) & 0xFFFFU); // Talkgroup Address + m_explicitId = (rs[3U] & 0x01U) == 0x01U; // Explicit Source ID Flag + m_dstId = (uint32_t)((rsValue >> 16) & 0xFFFFU); // Talkgroup Address m_srcId = (uint32_t)(rsValue & 0xFFFFFFU); // Source Radio Address return true; @@ -67,7 +68,8 @@ void LC_GROUP::encode(uint8_t* data) (m_emergency ? 0x80U : 0x00U) + // Emergency Flag (m_encrypted ? 0x40U : 0x00U) + // Encrypted Flag (m_priority & 0x07U); // Priority - rsValue = (rsValue << 24) + m_dstId; // Talkgroup Address + rsValue = (rsValue << 8) + (m_explicitId ? 0x01U : 0x00U); // Explicit Source ID Flag + rsValue = (rsValue << 16) + m_dstId; // Talkgroup Address rsValue = (rsValue << 24) + m_srcId; // Source Radio Address std::unique_ptr rs = TDULC::fromValue(rsValue); diff --git a/src/common/p25/lc/tdulc/LC_GROUP.h b/src/common/p25/lc/tdulc/LC_GROUP.h index e80d7855..cb2e4c04 100644 --- a/src/common/p25/lc/tdulc/LC_GROUP.h +++ b/src/common/p25/lc/tdulc/LC_GROUP.h @@ -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 Bryan Biedenkapp, N2PLL + * Copyright (C) 2022-2025 Bryan Biedenkapp, N2PLL * */ /** @@ -13,8 +13,8 @@ * @file LC_GROUP.cpp * @ingroup p25_lc */ -#if !defined(__P25_LC_TSBK__LC_GROUP_H__) -#define __P25_LC_TSBK__LC_GROUP_H__ +#if !defined(__P25_LC_TDULC__LC_GROUP_H__) +#define __P25_LC_TDULC__LC_GROUP_H__ #include "common/Defines.h" #include "common/p25/lc/TDULC.h" @@ -56,4 +56,4 @@ namespace p25 } // namespace lc } // namespace p25 -#endif // __P25_LC_TSBK__LC_GROUP_H__ +#endif // __P25_LC_TDULC__LC_GROUP_H__ diff --git a/src/common/p25/lc/tdulc/LC_GROUP_UPDT.h b/src/common/p25/lc/tdulc/LC_GROUP_UPDT.h index de066ac4..5f9a2b33 100644 --- a/src/common/p25/lc/tdulc/LC_GROUP_UPDT.h +++ b/src/common/p25/lc/tdulc/LC_GROUP_UPDT.h @@ -13,8 +13,8 @@ * @file LC_GROUP_UPDT.cpp * @ingroup p25_lc */ -#if !defined(__P25_LC_TSBK__LC_GROUP_UPDT_H__) -#define __P25_LC_TSBK__LC_GROUP_UPDT_H__ +#if !defined(__P25_LC_TDULC__LC_GROUP_UPDT_H__) +#define __P25_LC_TDULC__LC_GROUP_UPDT_H__ #include "common/Defines.h" #include "common/p25/lc/TDULC.h" @@ -56,4 +56,4 @@ namespace p25 } // namespace lc } // namespace p25 -#endif // __P25_LC_TSBK__LC_GROUP_UPDT_H__ +#endif // __P25_LC_TDULC__LC_GROUP_UPDT_H__ diff --git a/src/common/p25/lc/tdulc/LC_IDEN_UP.h b/src/common/p25/lc/tdulc/LC_IDEN_UP.h index 97c295fa..fcc3048b 100644 --- a/src/common/p25/lc/tdulc/LC_IDEN_UP.h +++ b/src/common/p25/lc/tdulc/LC_IDEN_UP.h @@ -13,8 +13,8 @@ * @file LC_IDEN_UP.cpp * @ingroup p25_lc */ -#if !defined(__P25_LC_TSBK__LC_IDEN_UP_H__) -#define __P25_LC_TSBK__LC_IDEN_UP_H__ +#if !defined(__P25_LC_TDULC__LC_IDEN_UP_H__) +#define __P25_LC_TDULC__LC_IDEN_UP_H__ #include "common/Defines.h" #include "common/p25/lc/TDULC.h" @@ -56,4 +56,4 @@ namespace p25 } // namespace lc } // namespace p25 -#endif // __P25_LC_TSBK__LC_IDEN_UP_H__ +#endif // __P25_LC_TDULC__LC_IDEN_UP_H__ diff --git a/src/common/p25/lc/tdulc/LC_NET_STS_BCAST.h b/src/common/p25/lc/tdulc/LC_NET_STS_BCAST.h index 9cfb00c0..ca41156c 100644 --- a/src/common/p25/lc/tdulc/LC_NET_STS_BCAST.h +++ b/src/common/p25/lc/tdulc/LC_NET_STS_BCAST.h @@ -13,8 +13,8 @@ * @file LC_NET_STS_BCAST.cpp * @ingroup p25_lc */ -#if !defined(__P25_LC_TSBK__LC_NET_STS_BCAST_H__) -#define __P25_LC_TSBK__LC_NET_STS_BCAST_H__ +#if !defined(__P25_LC_TDULC__LC_NET_STS_BCAST_H__) +#define __P25_LC_TDULC__LC_NET_STS_BCAST_H__ #include "common/Defines.h" #include "common/p25/lc/TDULC.h" @@ -56,4 +56,4 @@ namespace p25 } // namespace lc } // namespace p25 -#endif // __P25_LC_TSBK__LC_NET_STS_BCAST_H__ +#endif // __P25_LC_TDULC__LC_NET_STS_BCAST_H__ diff --git a/src/common/p25/lc/tdulc/LC_PRIVATE.h b/src/common/p25/lc/tdulc/LC_PRIVATE.h index 869a96e2..8b24a655 100644 --- a/src/common/p25/lc/tdulc/LC_PRIVATE.h +++ b/src/common/p25/lc/tdulc/LC_PRIVATE.h @@ -13,8 +13,8 @@ * @file LC_PRIVATE.cpp * @ingroup p25_lc */ -#if !defined(__P25_LC_TSBK__LC_PRIVATE_H__) -#define __P25_LC_TSBK__LC_PRIVATE_H__ +#if !defined(__P25_LC_TDULC__LC_PRIVATE_H__) +#define __P25_LC_TDULC__LC_PRIVATE_H__ #include "common/Defines.h" #include "common/p25/lc/TDULC.h" @@ -56,4 +56,4 @@ namespace p25 } // namespace lc } // namespace p25 -#endif // __P25_LC_TSBK__LC_PRIVATE_H__ +#endif // __P25_LC_TDULC__LC_PRIVATE_H__ diff --git a/src/common/p25/lc/tdulc/LC_RFSS_STS_BCAST.h b/src/common/p25/lc/tdulc/LC_RFSS_STS_BCAST.h index 80b5afdd..ba8567aa 100644 --- a/src/common/p25/lc/tdulc/LC_RFSS_STS_BCAST.h +++ b/src/common/p25/lc/tdulc/LC_RFSS_STS_BCAST.h @@ -13,8 +13,8 @@ * @file LC_RFSS_STS_BCAST.cpp * @ingroup p25_lc */ -#if !defined(__P25_LC_TSBK__LC_RFSS_STS_BCAST_H__) -#define __P25_LC_TSBK__LC_RFSS_STS_BCAST_H__ +#if !defined(__P25_LC_TDULC__LC_RFSS_STS_BCAST_H__) +#define __P25_LC_TDULC__LC_RFSS_STS_BCAST_H__ #include "common/Defines.h" #include "common/p25/lc/TDULC.h" @@ -56,4 +56,4 @@ namespace p25 } // namespace lc } // namespace p25 -#endif // __P25_LC_TSBK__LC_RFSS_STS_BCAST_H__ +#endif // __P25_LC_TDULC__LC_RFSS_STS_BCAST_H__ diff --git a/src/common/p25/lc/tdulc/LC_SYS_SRV_BCAST.h b/src/common/p25/lc/tdulc/LC_SYS_SRV_BCAST.h index 064062cc..e8f81678 100644 --- a/src/common/p25/lc/tdulc/LC_SYS_SRV_BCAST.h +++ b/src/common/p25/lc/tdulc/LC_SYS_SRV_BCAST.h @@ -13,8 +13,8 @@ * @file LC_SYS_SRV_BCAST.cpp * @ingroup p25_lc */ -#if !defined(__P25_LC_TSBK__LC_SYS_SRV_BCAST_H__) -#define __P25_LC_TSBK__LC_SYS_SRV_BCAST_H__ +#if !defined(__P25_LC_TDULC__LC_SYS_SRV_BCAST_H__) +#define __P25_LC_TDULC__LC_SYS_SRV_BCAST_H__ #include "common/Defines.h" #include "common/p25/lc/TDULC.h" @@ -56,4 +56,4 @@ namespace p25 } // namespace lc } // namespace p25 -#endif // __P25_LC_TSBK__LC_SYS_SRV_BCAST_H__ +#endif // __P25_LC_TDULC__LC_SYS_SRV_BCAST_H__ diff --git a/src/common/p25/lc/tdulc/LC_TDULC_RAW.cpp b/src/common/p25/lc/tdulc/LC_TDULC_RAW.cpp new file mode 100644 index 00000000..e27fae28 --- /dev/null +++ b/src/common/p25/lc/tdulc/LC_TDULC_RAW.cpp @@ -0,0 +1,101 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Digital Voice Modem - Common Library + * GPLv2 Open Source. Use is subject to license terms. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * Copyright (C) 2025 Bryan Biedenkapp, N2PLL + * + */ +#include "Defines.h" +#include "p25/lc/tdulc/LC_TDULC_RAW.h" + +using namespace p25; +using namespace p25::defines; +using namespace p25::lc; +using namespace p25::lc::tdulc; + +#include + +// --------------------------------------------------------------------------- +// Public Class Members +// --------------------------------------------------------------------------- + +/* Initializes a new instance of the LC_TDULC_RAW class. */ + +LC_TDULC_RAW::LC_TDULC_RAW() : TDULC(), + m_tdulc(nullptr) +{ + m_lco = LCO::GROUP; +} + +/* Finalizes a new instance of the LC_TDULC_RAW class. */ + +LC_TDULC_RAW::~LC_TDULC_RAW() +{ + if (m_tdulc != nullptr) { + delete m_tdulc; + } +} + +/* Decode a terminator data unit w/ link control. */ + +bool LC_TDULC_RAW::decode(const uint8_t* data) +{ + assert(data != nullptr); + + return decode(data, false); +} + +/* Encode a terminator data unit w/ link control. */ + +void LC_TDULC_RAW::encode(uint8_t* data) +{ + assert(data != nullptr); + assert(m_tdulc != nullptr); + + encode(data, false); +} + +/* Decode a terminator data unit w/ link control. */ + +bool LC_TDULC_RAW::decode(const uint8_t* data, bool rawTDULC) +{ + assert(data != nullptr); + + if (m_tdulc != nullptr) + delete[] m_tdulc; + + m_tdulc = new uint8_t[P25_TDULC_PAYLOAD_LENGTH_BYTES + 1U]; + ::memset(m_tdulc, 0x00U, P25_TDULC_PAYLOAD_LENGTH_BYTES); + + bool ret = TDULC::decode(data, m_tdulc, rawTDULC); + if (!ret) + return false; + + return true; +} + +/* Encode a terminator data unit w/ link control. */ + +void LC_TDULC_RAW::encode(uint8_t* data, bool rawTDULC) +{ + assert(data != nullptr); + assert(m_tdulc != nullptr); + + /* stub */ + + TDULC::encode(data, m_tdulc, rawTDULC); +} + +/* Sets the TDULC to encode. */ + +void LC_TDULC_RAW::setTDULC(const uint8_t* tdulc) +{ + assert(tdulc != nullptr); + + m_tdulc = new uint8_t[P25_TDULC_PAYLOAD_LENGTH_BYTES]; + ::memset(m_tdulc, 0x00U, P25_TDULC_PAYLOAD_LENGTH_BYTES); + + ::memcpy(m_tdulc, tdulc, P25_TDULC_PAYLOAD_LENGTH_BYTES); +} diff --git a/src/common/p25/lc/tdulc/LC_TDULC_RAW.h b/src/common/p25/lc/tdulc/LC_TDULC_RAW.h new file mode 100644 index 00000000..e7cf5ce2 --- /dev/null +++ b/src/common/p25/lc/tdulc/LC_TDULC_RAW.h @@ -0,0 +1,86 @@ +// 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 LC_TDULC_RAW.h + * @ingroup p25_lc + * @file LC_TDULC_RAW.cpp + * @ingroup p25_lc + */ +#if !defined(__P25_LC_TDULC__RAW_H__) +#define __P25_LC_TDULC__RAW_H__ + +#include "common/Defines.h" +#include "common/p25/lc/TDULC.h" + +namespace p25 +{ + namespace lc + { + namespace tdulc + { + // --------------------------------------------------------------------------- + // Class Declaration + // --------------------------------------------------------------------------- + + /** + * @brief Implements a mechanism to generate raw TDULC data from bytes. + * @ingroup p25_lc + */ + class HOST_SW_API LC_TDULC_RAW : public TDULC { + public: + /** + * @brief Initializes a new instance of the LC_TDULC_RAW class. + */ + LC_TDULC_RAW(); + /** + * @brief Finalizes a instance of the LC_TDULC_RAW class. + */ + ~LC_TDULC_RAW(); + + /** + * @brief Decode a terminator data unit w/ link control. + * @param[in] data Buffer containing a TDULC to decode. + * @returns bool True, if TDULC decoded, otherwise false. + */ + bool decode(const uint8_t* data) override; + /** + * @brief Encode a terminator data unit w/ link control. + * @param[out] data Buffer to encode a TDULC. + */ + void encode(uint8_t* data) override; + + /** + * @brief Decode a terminator data unit w/ link control. + * @param[in] data Buffer containing a TDULC to decode. + * @param rawTDULC Flag indicating whether or not the passed buffer is raw. + * @returns bool True, if TDULC decoded, otherwise false. + */ + bool decode(const uint8_t* data, bool rawTDULC); + /** + * @brief Encode a terminator data unit w/ link control. + * @param rawTDULC Flag indicating whether or not the output buffer is raw. + * @param[out] data Buffer to encode a TDULC. + */ + void encode(uint8_t* data, bool rawTDULC); + + /** + * @brief Sets the TDULC to encode. + * @param[in] tdulc Buffer containing TDULC to encode. + */ + void setTDULC(const uint8_t* tdulc); + + private: + uint8_t* m_tdulc; + }; + } // namespace tdulc + } // namespace lc +} // namespace p25 + +#endif // __P25_LC_TDULC__RAW_H__ diff --git a/src/common/p25/lc/tdulc/LC_TEL_INT_VCH_USER.h b/src/common/p25/lc/tdulc/LC_TEL_INT_VCH_USER.h index 8b672d4b..e8c66a56 100644 --- a/src/common/p25/lc/tdulc/LC_TEL_INT_VCH_USER.h +++ b/src/common/p25/lc/tdulc/LC_TEL_INT_VCH_USER.h @@ -13,8 +13,8 @@ * @file LC_TEL_INT_VCH_USER.cpp * @ingroup p25_lc */ -#if !defined(__P25_LC_TSBK__LC_TEL_INT_VCH_USER_H__) -#define __P25_LC_TSBK__LC_TEL_INT_VCH_USER_H__ +#if !defined(__P25_LC_TDULC__LC_TEL_INT_VCH_USER_H__) +#define __P25_LC_TDULC__LC_TEL_INT_VCH_USER_H__ #include "common/Defines.h" #include "common/p25/lc/TDULC.h" @@ -56,4 +56,4 @@ namespace p25 } // namespace lc } // namespace p25 -#endif // __P25_LC_TSBK__LC_TEL_INT_VCH_USER_H__ +#endif // __P25_LC_TDULC__LC_TEL_INT_VCH_USER_H__ diff --git a/src/common/p25/lc/tdulc/TDULCFactory.cpp b/src/common/p25/lc/tdulc/TDULCFactory.cpp index 4d5a9890..e8a05088 100644 --- a/src/common/p25/lc/tdulc/TDULCFactory.cpp +++ b/src/common/p25/lc/tdulc/TDULCFactory.cpp @@ -4,7 +4,7 @@ * GPLv2 Open Source. Use is subject to license terms. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * - * Copyright (C) 2023,2024 Bryan Biedenkapp, N2PLL + * Copyright (C) 2023,2024,2025 Bryan Biedenkapp, N2PLL * */ #include "Defines.h" @@ -53,19 +53,19 @@ std::unique_ptr TDULCFactory::createTDULC(const uint8_t* data) edac::Golay24128::decode24128(rs, raw, P25_TDULC_LENGTH_BYTES); #if DEBUG_P25_TDULC - Utils::dump(2U, "TDULCFactory::decode(), TDULC RS", rs, P25_TDULC_LENGTH_BYTES); + Utils::dump(2U, "P25, TDULCFactory::createTDULC(), TDULC RS", rs, P25_TDULC_LENGTH_BYTES); #endif // decode RS (24,12,13) FEC try { bool ret = m_rs.decode241213(rs); if (!ret) { - LogError(LOG_P25, "TDULCFactory::decode(), failed to decode RS (24,12,13) FEC"); + LogError(LOG_P25, "TDULCFactory::createTDULC(), failed to decode RS (24,12,13) FEC"); return nullptr; } } catch (...) { - Utils::dump(2U, "P25, RS excepted with input data", rs, P25_TDULC_LENGTH_BYTES); + Utils::dump(2U, "P25, TDULCFactory::createTDULC(), RS excepted with input data", rs, P25_TDULC_LENGTH_BYTES); return nullptr; } @@ -81,8 +81,10 @@ std::unique_ptr TDULCFactory::createTDULC(const uint8_t* data) return decode(new LC_TEL_INT_VCH_USER(), data); case LCO::CALL_TERM: return decode(new LC_CALL_TERM(), data); + case LCO::EXPLICIT_SOURCE_ID: + return decode(new LC_EXPLICIT_SOURCE_ID(), data); default: - LogError(LOG_P25, "TDULCFactory::create(), unknown TDULC LCO value, lco = $%02X", lco); + LogError(LOG_P25, "TDULCFactory::createTDULC(), unknown TDULC LCO value, lco = $%02X", lco); break; } diff --git a/src/common/p25/lc/tdulc/TDULCFactory.h b/src/common/p25/lc/tdulc/TDULCFactory.h index d8704c9d..632ecd17 100644 --- a/src/common/p25/lc/tdulc/TDULCFactory.h +++ b/src/common/p25/lc/tdulc/TDULCFactory.h @@ -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 Bryan Biedenkapp, N2PLL + * Copyright (C) 2023-2025 Bryan Biedenkapp, N2PLL * */ /** @@ -23,6 +23,7 @@ #include "common/p25/lc/tdulc/LC_ADJ_STS_BCAST.h" #include "common/p25/lc/tdulc/LC_CALL_TERM.h" #include "common/p25/lc/tdulc/LC_CONV_FALLBACK.h" +#include "common/p25/lc/tdulc/LC_EXPLICIT_SOURCE_ID.h" #include "common/p25/lc/tdulc/LC_GROUP_UPDT.h" #include "common/p25/lc/tdulc/LC_GROUP.h" #include "common/p25/lc/tdulc/LC_IDEN_UP.h" @@ -31,6 +32,7 @@ #include "common/p25/lc/tdulc/LC_RFSS_STS_BCAST.h" #include "common/p25/lc/tdulc/LC_SYS_SRV_BCAST.h" #include "common/p25/lc/tdulc/LC_TEL_INT_VCH_USER.h" +#include "common/p25/lc/tdulc/LC_TDULC_RAW.h" #include "common/p25/lc/tdulc/LC_FAILSOFT.h" diff --git a/src/common/p25/lc/tsbk/TSBKFactory.cpp b/src/common/p25/lc/tsbk/TSBKFactory.cpp index 3f6a596b..742d7d49 100644 --- a/src/common/p25/lc/tsbk/TSBKFactory.cpp +++ b/src/common/p25/lc/tsbk/TSBKFactory.cpp @@ -101,7 +101,7 @@ std::unique_ptr TSBKFactory::createTSBK(const uint8_t* data, bool rawTSBK) return nullptr; } catch (...) { - Utils::dump(2U, "P25, decoding excepted with input data", tsbk, P25_TSBK_LENGTH_BYTES); + Utils::dump(2U, "P25, TSBKFactory::createTSBK(), decoding excepted with input data", tsbk, P25_TSBK_LENGTH_BYTES); return nullptr; } } diff --git a/src/common/zlib/Compression.cpp b/src/common/zlib/Compression.cpp index 0d8dd5d2..d3cf80d3 100644 --- a/src/common/zlib/Compression.cpp +++ b/src/common/zlib/Compression.cpp @@ -24,7 +24,7 @@ using namespace compress; /* Compress the given input buffer. */ -uint8_t* Compression::compress(const uint8_t* buffer, uint32_t len, uint32_t* compressedLen) +UInt8Array Compression::compress(const uint8_t* buffer, uint32_t len, uint32_t* compressedLen) { assert(buffer != nullptr); assert(len > 0U); @@ -33,8 +33,8 @@ uint8_t* Compression::compress(const uint8_t* buffer, uint32_t len, uint32_t* co *compressedLen = 0U; } - uint8_t* data = new uint8_t[len]; - ::memset(data, 0x00U, len); + uint8_t* data = new uint8_t[len + 1U]; + ::memset(data, 0x00U, len + 1U); ::memcpy(data, buffer, len); // compression structures @@ -87,18 +87,25 @@ uint8_t* Compression::compress(const uint8_t* buffer, uint32_t len, uint32_t* co Utils::dump(2U, "Compression::compress(), Compressed Data", compressed, strm.total_out); #endif - // reuse data buffer to return compressed data delete[] data; - data = new uint8_t[strm.total_out]; - ::memset(data, 0x00U, strm.total_out); - ::memcpy(data, compressed, strm.total_out); - return data; + if (strm.total_out == 0U) { + LogError(LOG_HOST, "ZLIB compression resulted in zero bytes of output"); + return nullptr; // return nullptr if no data was compressed + } + + // return compressed data + UInt8Array out = std::make_unique(strm.total_out + 1U); + ::memset(out.get(), 0x00U, strm.total_out + 1U); + ::memcpy(out.get(), compressed, strm.total_out); + + compressedData.clear(); // clear the vector to release memory + return out; } /* Decompress the given input buffer. */ -uint8_t* Compression::decompress(const uint8_t* buffer, uint32_t len, uint32_t* decompressedLen) +UInt8Array Compression::decompress(const uint8_t* buffer, uint32_t len, uint32_t* decompressedLen) { assert(buffer != nullptr); assert(len > 0U); @@ -107,8 +114,8 @@ uint8_t* Compression::decompress(const uint8_t* buffer, uint32_t len, uint32_t* *decompressedLen = 0U; } - uint8_t* data = new uint8_t[len]; - ::memset(data, 0x00U, len); + uint8_t* data = new uint8_t[len + 1U]; + ::memset(data, 0x00U, len + 1U); ::memcpy(data, buffer, len); // compression structures @@ -159,11 +166,18 @@ uint8_t* Compression::decompress(const uint8_t* buffer, uint32_t len, uint32_t* Utils::dump(2U, "Compression::decompress(), Decompressed Data", decompressed, strm.total_out); #endif - // reuse data buffer to return decompressed data delete[] data; - data = new uint8_t[strm.total_out]; - ::memset(data, 0x00U, strm.total_out); - ::memcpy(data, decompressed, strm.total_out); - return data; + if (strm.total_out == 0U) { + LogError(LOG_HOST, "ZLIB decompression resulted in zero bytes of output"); + return nullptr; // return nullptr if no data was decompressed + } + + // return decompressed data + UInt8Array out = std::make_unique(strm.total_out + 1U); + ::memset(out.get(), 0x00U, strm.total_out + 1U); + ::memcpy(out.get(), decompressed, strm.total_out); + + decompressedData.clear(); // clear the vector to release memory + return out; } diff --git a/src/common/zlib/Compression.h b/src/common/zlib/Compression.h index f740aea1..74519b7d 100644 --- a/src/common/zlib/Compression.h +++ b/src/common/zlib/Compression.h @@ -39,18 +39,18 @@ namespace compress * @param[in] buffer Buffer containing data to zlib compress. * @param[in] len Length of data to compress. * @param[out] compressedLen Length of compressed data. - * @returns uint8_t* Buffer containing compressed data. + * @returns UInt8Array Buffer containing compressed data. */ - static uint8_t* compress(const uint8_t* buffer, uint32_t len, uint32_t* compressedLen); + static UInt8Array compress(const uint8_t* buffer, uint32_t len, uint32_t* compressedLen); /** * @brief Decompress the given input buffer using zlib compression. * @param[in] buffer Buffer containing zlib compressed data. * @param[in] len Length of compressed data. * @param[out] decompressedLen Length of decompressed data. - * @returns uint8_t* Buffer containing decompressed data. + * @returns UInt8Array Buffer containing decompressed data. */ - static uint8_t* decompress(const uint8_t* buffer, uint32_t len, uint32_t* decompressedLen); + static UInt8Array decompress(const uint8_t* buffer, uint32_t len, uint32_t* decompressedLen); }; } // namespace compress diff --git a/src/fne/CMakeLists.txt b/src/fne/CMakeLists.txt index ae3ef35e..2425e6c0 100644 --- a/src/fne/CMakeLists.txt +++ b/src/fne/CMakeLists.txt @@ -4,10 +4,12 @@ # * GPLv2 Open Source. Use is subject to license terms. # * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. # * -# * Copyright (C) 2024 Bryan Biedenkapp, N2PLL +# * Copyright (C) 2024,2025 Bryan Biedenkapp, N2PLL # * # */ file(GLOB dvmfne_SRC + "src/fne/lookups/*.h" + "src/fne/lookups/*.cpp" "src/fne/network/callhandler/*.h" "src/fne/network/callhandler/*.cpp" "src/fne/network/callhandler/packetdata/*.h" @@ -17,6 +19,14 @@ file(GLOB dvmfne_SRC "src/fne/network/*.h" "src/fne/network/*.cpp" "src/fne/xml/*.h" + "src/fne/win32/*.h" "src/fne/*.h" "src/fne/*.cpp" ) + +# +# Windows Resource Scripts +# +file(GLOB dvmfne_RC + "src/fne/win32/*.rc" +) diff --git a/src/fne/HostFNE.cpp b/src/fne/HostFNE.cpp index f42e9d94..247690fd 100644 --- a/src/fne/HostFNE.cpp +++ b/src/fne/HostFNE.cpp @@ -15,6 +15,7 @@ #include "network/callhandler/TagDMRData.h" #include "network/callhandler/TagP25Data.h" #include "network/callhandler/TagNXDNData.h" +#include "network/callhandler/TagAnalogData.h" #include "ActivityLog.h" #include "HostFNE.h" #include "FNEMain.h" @@ -42,6 +43,8 @@ using namespace lookups; #define MIN_WORKER_CNT 4U #define MAX_WORKER_CNT 128U +#define MAX_RECOMMENDED_PEER_NETWORKS 8U + #define THREAD_CYCLE_THRESHOLD 2U #define IDLE_WARMUP_MS 5U @@ -66,6 +69,7 @@ HostFNE::HostFNE(const std::string& confFile) : m_dmrEnabled(false), m_p25Enabled(false), m_nxdnEnabled(false), + m_analogEnabled(false), m_ridLookup(nullptr), m_tidLookup(nullptr), m_peerListLookup(nullptr), @@ -213,8 +217,6 @@ int HostFNE::run() #if !defined(_WIN32) if (!Thread::runAsThread(this, threadVirtualNetworking)) return EXIT_FAILURE; - if (!Thread::runAsThread(this, threadVirtualNetworkingClock)) - return EXIT_FAILURE; #endif // !defined(_WIN32) /* ** Main execution loop @@ -249,16 +251,22 @@ int HostFNE::run() network::PeerNetwork* peerNetwork = network.second; if (peerNetwork != nullptr) { peerNetwork->clock(ms); + } + } - // skip peer if it isn't enabled - if (!peerNetwork->isEnabled()) { - continue; - } - - // process peer network traffic - processPeer(peerNetwork); +#if !defined(_WIN32) + if (m_vtunEnabled) { + switch (m_packetDataMode) { + case PacketDataMode::DMR: + // TODO: not supported yet + break; + + case PacketDataMode::PROJECT25: + m_network->p25TrafficHandler()->packetData()->clock(ms); + break; } } +#endif // !defined(_WIN32) if (ms < 2U) Thread::sleep(1U); @@ -388,6 +396,10 @@ bool HostFNE::readParams() std::string talkgroupConfig = talkgroupRules["file"].as(); uint32_t talkgroupConfigReload = talkgroupRules["time"].as(30U); + yaml::Node adjSiteMapRules = masterConf["adj_site_map"]; + std::string adjSiteMapConfig = adjSiteMapRules["file"].as(); + uint32_t adjSiteMapReload = adjSiteMapRules["time"].as(30U); + yaml::Node cryptoContainer = masterConf["crypto_container"]; bool cryptoContainerEnabled = cryptoContainer["enable"].as(false); #if !defined(ENABLE_SSL) @@ -420,6 +432,14 @@ bool HostFNE::readParams() m_peerListLookup = new PeerListLookup(peerListLookupFile, peerListConfigReload, peerListLookupEnable); m_peerListLookup->read(); + LogInfo("Adjacent Site Map Lookups"); + LogInfo(" File: %s", adjSiteMapConfig.length() > 0U ? adjSiteMapConfig.c_str() : "None"); + if (adjSiteMapReload > 0U) + LogInfo(" Reload: %u mins", adjSiteMapReload); + + m_adjSiteMapLookup = new AdjSiteMapLookup(adjSiteMapConfig, adjSiteMapReload); + m_adjSiteMapLookup->read(); + // try to load peer whitelist/blacklist LogInfo("Crypto Container Lookups"); LogInfo(" Enabled: %s", cryptoContainerEnabled ? "yes" : "no"); @@ -493,7 +513,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_RESTAPI->setLookups(m_ridLookup, m_tidLookup, m_peerListLookup, m_adjSiteMapLookup); bool ret = m_RESTAPI->open(); if (!ret) { delete m_RESTAPI; @@ -572,6 +592,7 @@ bool HostFNE::createMasterNetwork() m_dmrEnabled = masterConf["allowDMRTraffic"].as(true); m_p25Enabled = masterConf["allowP25Traffic"].as(true); m_nxdnEnabled = masterConf["allowNXDNTraffic"].as(true); + m_analogEnabled = masterConf["allowAnalogTraffic"].as(false); uint32_t parrotDelay = masterConf["parrotDelay"].as(2500U); if (m_pingTime * 1000U < parrotDelay) { @@ -587,6 +608,7 @@ bool HostFNE::createMasterNetwork() 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"); + LogInfo(" Allow Analog Traffic: %s", m_analogEnabled ? "yes" : "no"); LogInfo(" Parrot Repeat Delay: %u ms", parrotDelay); LogInfo(" Parrot Grant Demand: %s", parrotGrantDemand ? "yes" : "no"); @@ -605,11 +627,11 @@ bool HostFNE::createMasterNetwork() } // initialize networking - m_network = new FNENetwork(this, address, port, id, password, debug, verbose, reportPeerPing, m_dmrEnabled, m_p25Enabled, m_nxdnEnabled, + m_network = new FNENetwork(this, address, port, id, password, debug, 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->setLookups(m_ridLookup, m_tidLookup, m_peerListLookup, m_cryptoLookup); + m_network->setLookups(m_ridLookup, m_tidLookup, m_peerListLookup, m_cryptoLookup, m_adjSiteMapLookup); if (m_RESTAPI != nullptr) { m_RESTAPI->setNetwork(m_network); @@ -761,6 +783,10 @@ bool HostFNE::createPeerNetworks() { yaml::Node& peerList = m_conf["peers"]; if (peerList.size() > 0U) { + if (peerList.size() > MAX_RECOMMENDED_PEER_NETWORKS) { + LogWarning(LOG_HOST, "Peer network count (%zu) exceeds the recommended maximum of %u. This may result in poor performance.", peerList.size(), MAX_RECOMMENDED_PEER_NETWORKS); + } + for (size_t i = 0; i < peerList.size(); i++) { yaml::Node& peerConf = peerList[i]; @@ -818,7 +844,8 @@ bool HostFNE::createPeerNetworks() } // initialize networking - network::PeerNetwork* network = new PeerNetwork(masterAddress, masterPort, 0U, id, password, true, debug, m_dmrEnabled, m_p25Enabled, m_nxdnEnabled, true, true, m_allowActivityTransfer, m_allowDiagnosticTransfer, false, false); + 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->setMetadata(identity, rxFrequency, txFrequency, 0.0F, 0.0F, 0, 0, 0, latitude, longitude, 0, location); network->setLookups(m_ridLookup, m_tidLookup); network->setPeerLookups(m_peerListLookup); @@ -827,6 +854,11 @@ bool HostFNE::createPeerNetworks() network->setPresharedKey(presharedKey); } + network->setDMRCallback(std::bind(&HostFNE::processPeerDMR, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::placeholders::_4, std::placeholders::_5, std::placeholders::_6)); + network->setP25Callback(std::bind(&HostFNE::processPeerP25, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::placeholders::_4, std::placeholders::_5, std::placeholders::_6)); + network->setNXDNCallback(std::bind(&HostFNE::processPeerNXDN, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::placeholders::_4, std::placeholders::_5, std::placeholders::_6)); + network->setAnalogCallback(std::bind(&HostFNE::processPeerAnalog, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::placeholders::_4, std::placeholders::_5, std::placeholders::_6)); + /* ** Block Traffic To Peers */ @@ -969,115 +1001,92 @@ void* HostFNE::threadVirtualNetworking(void* arg) return nullptr; } +#endif // !defined(_WIN32) -/* Entry point to virtual networking clocking thread. */ +/* Processes DMR peer network traffic. */ -void* HostFNE::threadVirtualNetworkingClock(void* arg) +void HostFNE::processPeerDMR(network::PeerNetwork* peerNetwork, const uint8_t* data, uint32_t length, uint32_t streamId, + const network::frame::RTPFNEHeader& fneHeader, const network::frame::RTPHeader& rtpHeader) { - thread_t* th = (thread_t*)arg; - if (th != nullptr) { - ::pthread_detach(th->thread); - - std::string threadName("fne:vt-clock"); - HostFNE* fne = static_cast(th->obj); - if (fne == nullptr) { - g_killed = true; - LogError(LOG_HOST, "[FAIL] %s", threadName.c_str()); - } - - if (g_killed) { - delete th; - return nullptr; - } + if (peerNetwork == nullptr) + return; // this shouldn't happen... - if (!fne->m_vtunEnabled) { - delete th; - return nullptr; - } + // skip peer if it isn't enabled + if (!peerNetwork->isEnabled()) + return; - LogMessage(LOG_HOST, "[ OK ] %s", threadName.c_str()); -#ifdef _GNU_SOURCE - ::pthread_setname_np(th->thread, threadName.c_str()); -#endif // _GNU_SOURCE + if (peerNetwork->getStatus() != NET_STAT_RUNNING) + return; - if (fne->m_tun != nullptr) { - StopWatch stopWatch; - stopWatch.start(); + // process DMR data + if (length > 0U) { + uint32_t peerId = peerNetwork->getPeerId(); + m_network->dmrTrafficHandler()->processFrame(data, length, peerId, rtpHeader.getSSRC(), peerNetwork->pktLastSeq(), streamId, true); + } +} - while (!g_killed) { - uint32_t ms = stopWatch.elapsed(); - stopWatch.start(); +/* Processes P25 peer network traffic. */ - // clock traffic handler - switch (fne->m_packetDataMode) { - case PacketDataMode::DMR: - // TODO: not supported yet - break; +void HostFNE::processPeerP25(network::PeerNetwork* peerNetwork, const uint8_t* data, uint32_t length, uint32_t streamId, + const network::frame::RTPFNEHeader& fneHeader, const network::frame::RTPHeader& rtpHeader) +{ + if (peerNetwork == nullptr) + return; // this shouldn't happen... - case PacketDataMode::PROJECT25: - fne->m_network->p25TrafficHandler()->packetData()->clock(ms); - break; - } + // skip peer if it isn't enabled + if (!peerNetwork->isEnabled()) + return; - if (ms < THREAD_CYCLE_THRESHOLD) - Thread::sleep(THREAD_CYCLE_THRESHOLD); - } - } + if (peerNetwork->getStatus() != NET_STAT_RUNNING) + return; - LogMessage(LOG_HOST, "[STOP] %s", threadName.c_str()); - delete th; + // process P25 data + if (length > 0U) { + uint32_t peerId = peerNetwork->getPeerId(); + m_network->p25TrafficHandler()->processFrame(data, length, peerId, rtpHeader.getSSRC(), peerNetwork->pktLastSeq(), streamId, true); } - - return nullptr; } -#endif // !defined(_WIN32) -/* Processes any peer network traffic. */ +/* Processes NXDN peer network traffic. */ -void HostFNE::processPeer(network::PeerNetwork* peerNetwork) +void HostFNE::processPeerNXDN(network::PeerNetwork* peerNetwork, const uint8_t* data, uint32_t length, uint32_t streamId, + const network::frame::RTPFNEHeader& fneHeader, const network::frame::RTPHeader& rtpHeader) { if (peerNetwork == nullptr) return; // this shouldn't happen... + + // skip peer if it isn't enabled + if (!peerNetwork->isEnabled()) + return; + if (peerNetwork->getStatus() != NET_STAT_RUNNING) return; - // process DMR data - if (peerNetwork->hasDMRData()) { - uint32_t length = 100U; - bool ret = false; - UInt8Array data = peerNetwork->readDMR(ret, length); - if (ret) { - uint32_t peerId = peerNetwork->getPeerId(); - uint32_t slotNo = (data[15U] & 0x80U) == 0x80U ? 2U : 1U; - uint32_t streamId = peerNetwork->getRxDMRStreamId(slotNo); - - m_network->dmrTrafficHandler()->processFrame(data.get(), length, peerId, peerNetwork->pktLastSeq(), streamId, true); - } + // process NXDN data + if (length > 0U) { + uint32_t peerId = peerNetwork->getPeerId(); + m_network->nxdnTrafficHandler()->processFrame(data, length, peerId, rtpHeader.getSSRC(), peerNetwork->pktLastSeq(), streamId, true); } +} - // process P25 data - if (peerNetwork->hasP25Data()) { - uint32_t length = 100U; - bool ret = false; - UInt8Array data = peerNetwork->readP25(ret, length); - if (ret) { - uint32_t peerId = peerNetwork->getPeerId(); - uint32_t streamId = peerNetwork->getRxP25StreamId(); - - m_network->p25TrafficHandler()->processFrame(data.get(), length, peerId, peerNetwork->pktLastSeq(), streamId, true); - } - } +/* Processes analog peer network traffic. */ - // process NXDN data - if (peerNetwork->hasNXDNData()) { - uint32_t length = 100U; - bool ret = false; - UInt8Array data = peerNetwork->readNXDN(ret, length); - if (ret) { - uint32_t peerId = peerNetwork->getPeerId(); - uint32_t streamId = peerNetwork->getRxNXDNStreamId(); - - m_network->nxdnTrafficHandler()->processFrame(data.get(), length, peerId, peerNetwork->pktLastSeq(), streamId, true); - } +void HostFNE::processPeerAnalog(network::PeerNetwork* peerNetwork, const uint8_t* data, uint32_t length, uint32_t streamId, + const network::frame::RTPFNEHeader& fneHeader, const network::frame::RTPHeader& rtpHeader) +{ + if (peerNetwork == nullptr) + return; // this shouldn't happen... + + // skip peer if it isn't enabled + if (!peerNetwork->isEnabled()) + return; + + if (peerNetwork->getStatus() != NET_STAT_RUNNING) + return; + + // process analog data + if (length > 0U) { + uint32_t peerId = peerNetwork->getPeerId(); + m_network->analogTrafficHandler()->processFrame(data, length, peerId, rtpHeader.getSSRC(), peerNetwork->pktLastSeq(), streamId, true); } } diff --git a/src/fne/HostFNE.h b/src/fne/HostFNE.h index d3e0da48..4c76c47e 100644 --- a/src/fne/HostFNE.h +++ b/src/fne/HostFNE.h @@ -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 Bryan Biedenkapp, N2PLL + * Copyright (C) 2023-2025 Bryan Biedenkapp, N2PLL * */ /** @@ -20,6 +20,7 @@ #include "common/lookups/RadioIdLookup.h" #include "common/lookups/TalkgroupRulesLookup.h" #include "common/lookups/PeerListLookup.h" +#include "common/lookups/AdjSiteMapLookup.h" #include "common/network/viface/VIFace.h" #include "common/yaml/Yaml.h" #include "common/Timer.h" @@ -42,6 +43,7 @@ namespace network { namespace callhandler { namespace packetdata { class HOST_SW namespace network { namespace callhandler { class HOST_SW_API TagP25Data; } } namespace network { namespace callhandler { namespace packetdata { class HOST_SW_API P25PacketData; } } } namespace network { namespace callhandler { class HOST_SW_API TagNXDNData; } } +namespace network { namespace callhandler { class HOST_SW_API TagAnalogData; } } // --------------------------------------------------------------------------- // Class Declaration @@ -88,6 +90,7 @@ private: 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; @@ -100,10 +103,12 @@ private: bool m_dmrEnabled; bool m_p25Enabled; bool m_nxdnEnabled; + bool m_analogEnabled; lookups::RadioIdLookup* m_ridLookup; lookups::TalkgroupRulesLookup* m_tidLookup; lookups::PeerListLookup* m_peerListLookup; + lookups::AdjSiteMapLookup* m_adjSiteMapLookup; CryptoContainer* m_cryptoLookup; @@ -168,18 +173,47 @@ private: * @returns void* (Ignore) */ static void* threadVirtualNetworking(void* arg); +#endif // !defined(_WIN32) /** - * @brief Entry point to virtual networking clocking thread. - * @param arg Instance of the thread_t structure. - * @returns void* (Ignore) + * @brief Processes DMR peer network traffic. + * @param data Buffer containing DMR data. + * @param length Length of the buffer. + * @param streamId Stream ID. + * @param fneHeader FNE header for the packet. + * @param rtpHeader RTP header for the packet. */ - static void* threadVirtualNetworkingClock(void* arg); -#endif // !defined(_WIN32) + void processPeerDMR(network::PeerNetwork* peerNetwork, const uint8_t* data, uint32_t length, uint32_t streamId, + const network::frame::RTPFNEHeader& fneHeader, const network::frame::RTPHeader& rtpHeader); + /** + * @brief Processes P25 peer network traffic. + * @param data Buffer containing P25 data. + * @param length Length of the buffer. + * @param streamId Stream ID. + * @param fneHeader FNE header for the packet. + * @param rtpHeader RTP header for the packet. + */ + void processPeerP25(network::PeerNetwork* peerNetwork, const uint8_t* data, uint32_t length, uint32_t streamId, + const network::frame::RTPFNEHeader& fneHeader, const network::frame::RTPHeader& rtpHeader); + /** + * @brief Processes NXDN peer network traffic. + * @param data Buffer containing NXDN data. + * @param length Length of the buffer. + * @param streamId Stream ID. + * @param fneHeader FNE header for the packet. + * @param rtpHeader RTP header for the packet. + */ + void processPeerNXDN(network::PeerNetwork* peerNetwork, const uint8_t* data, uint32_t length, uint32_t streamId, + const network::frame::RTPFNEHeader& fneHeader, const network::frame::RTPHeader& rtpHeader); /** - * @brief Processes any peer network traffic. - * @param peerNetwork Instance of PeerNetwork to process traffic for. + * @brief Processes analog peer network traffic. + * @param data Buffer containing analog data. + * @param length Length of the buffer. + * @param streamId Stream ID. + * @param fneHeader FNE header for the packet. + * @param rtpHeader RTP header for the packet. */ - void processPeer(network::PeerNetwork* peerNetwork); + void processPeerAnalog(network::PeerNetwork* peerNetwork, const uint8_t* data, uint32_t length, uint32_t streamId, + const network::frame::RTPFNEHeader& fneHeader, const network::frame::RTPHeader& rtpHeader); }; #endif // __HOST_FNE_H__ diff --git a/src/fne/lookups/AffiliationLookup.cpp b/src/fne/lookups/AffiliationLookup.cpp new file mode 100644 index 00000000..b8494432 --- /dev/null +++ b/src/fne/lookups/AffiliationLookup.cpp @@ -0,0 +1,101 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Digital Voice Modem - Modem Host 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 "lookups/AffiliationLookup.h" + +using namespace fne_lookups; + +// --------------------------------------------------------------------------- +// Public Class Members +// --------------------------------------------------------------------------- + +/* Initializes a new instance of the AffiliationLookup class. */ + +AffiliationLookup::AffiliationLookup(const std::string name, ::lookups::ChannelLookup* chLookup, bool verbose) : + ::lookups::AffiliationLookup(name, chLookup, verbose), + m_unitRegPeerTable() +{ + m_unitRegPeerTable.clear(); +} + +/* Finalizes a instance of the AffiliationLookup class. */ + +AffiliationLookup::~AffiliationLookup() = default; + +/* Helper to group affiliate a source ID. */ + +void AffiliationLookup::unitReg(uint32_t srcId, uint32_t ssrc) +{ + if (isUnitReg(srcId)) { + return; + } + + ::lookups::AffiliationLookup::unitReg(srcId); + + m_unitRegPeerTable[srcId] = ssrc; +} + +/* Helper to group unaffiliate a source ID. */ + +bool AffiliationLookup::unitDereg(uint32_t srcId, bool automatic) +{ + bool ret = false; + + if (!isUnitReg(srcId)) { + return false; + } + + ret = ::lookups::AffiliationLookup::unitDereg(srcId, automatic); + + if (ret) { + // lookup dynamic table entry + if (m_unitRegPeerTable.find(srcId) == m_unitRegPeerTable.end()) { + return false; + } + + // remove dynamic table entry + try { + uint32_t entry = m_unitRegPeerTable.at(srcId); // this value will get discarded + (void)entry; // but some variants of C++ mark the unordered_map<>::at as nodiscard + m_unitRegPeerTable.erase(srcId); + return true; + } + catch (...) { + return false; + } + } + + return false; +} + +/* Helper to get the originating network peer ID by the registered source ID. */ + +uint32_t AffiliationLookup::getSSRCByUnitReg(uint32_t srcId) +{ + // lookup dynamic channel grant table entry + m_unitRegPeerTable.lock(false); + for (auto entry : m_grantChTable) { + if (entry.first == srcId) { + m_unitRegPeerTable.unlock(); + return entry.second; + } + } + m_unitRegPeerTable.unlock(); + + return 0U; +} + +/* Helper to release unit registrations. */ + +void AffiliationLookup::clearUnitReg() +{ + ::lookups::AffiliationLookup::clearUnitReg(); + m_unitRegPeerTable.clear(); +} diff --git a/src/fne/lookups/AffiliationLookup.h b/src/fne/lookups/AffiliationLookup.h new file mode 100644 index 00000000..d6faac60 --- /dev/null +++ b/src/fne/lookups/AffiliationLookup.h @@ -0,0 +1,83 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Digital Voice Modem - Modem Host 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 + * + */ +/** + * @defgroup fne_lookups Lookup Tables + * @brief Implementation for various data lookup tables. + * @ingroup fne + * + * @file P25AffiliationLookup.h + * @ingroup fne_lookups + * @file P25AffiliationLookup.cpp + * @ingroup fne_lookups + */ +#if !defined(__FNE_AFFILIATION_LOOKUP_H__) +#define __FNE_AFFILIATION_LOOKUP_H__ + +#include "Defines.h" +#include "common/lookups/AffiliationLookup.h" +#include "common/lookups/ChannelLookup.h" + +namespace fne_lookups +{ + // --------------------------------------------------------------------------- + // Class Declaration + // --------------------------------------------------------------------------- + + /** + * @brief Implements a lookup table class that contains subscriber registration + * and group affiliation information. + * @ingroup fne_lookups + */ + class HOST_SW_API AffiliationLookup : public ::lookups::AffiliationLookup { + public: + /** + * @brief Initializes a new instance of the AffiliationLookup class. + * @param name Name of lookup table. + * @param channelLookup Instance of the channel lookup class. + * @param verbose Flag indicating whether verbose logging is enabled. + */ + AffiliationLookup(const std::string name, ::lookups::ChannelLookup* chLookup, bool verbose); + /** + * @brief Finalizes a instance of the AffiliationLookup class. + */ + ~AffiliationLookup() override; + + /** @name Unit Registrations */ + /** + * @brief Helper to register a source ID. + * @param srcId Source Radio ID. + * @param ssrc Originating Peer ID. + */ + void unitReg(uint32_t srcId, uint32_t ssrc); + /** + * @brief Helper to deregister a source ID. + * @param srcId Source Radio ID. + * @param automatic Flag indicating the deregistration is a result of an automated timer. + * @returns bool True, if source ID is deregistered, otherwise false. + */ + bool unitDereg(uint32_t srcId, bool automatic = false) override; + /** + * @brief Helper to get the originating network peer ID by the registered source ID. + * @param srcId Source Radio ID. + * @returns uint32_t Originating Peer ID. + */ + uint32_t getSSRCByUnitReg(uint32_t srcId); + /** + * @brief Helper to release unit registrations. + */ + void clearUnitReg() override; + /** @} */ + + protected: + concurrent::unordered_map m_unitRegPeerTable; + }; +} // namespace fne_lookups + +#endif // __FNE_AFFILIATION_LOOKUP_H__ diff --git a/src/fne/network/DiagNetwork.cpp b/src/fne/network/DiagNetwork.cpp index 42834eed..fb5e260e 100644 --- a/src/fne/network/DiagNetwork.cpp +++ b/src/fne/network/DiagNetwork.cpp @@ -70,7 +70,7 @@ void DiagNetwork::processNetwork() UInt8Array buffer = m_frameQueue->read(length, address, addrLen, &rtpHeader, &fneHeader); if (length > 0) { if (m_debug) - Utils::dump(1U, "Network Message", buffer.get(), length); + Utils::dump(1U, "DiagNetwork::processNetwork(), Network Message", buffer.get(), length); uint32_t peerId = fneHeader.getPeerId(); @@ -360,7 +360,7 @@ void DiagNetwork::taskNetworkRx(NetPacketRequest* req) default: network->writePeerNAK(peerId, network->createStreamId(), TAG_TRANSFER, NET_CONN_NAK_ILLEGAL_PACKET); - Utils::dump("unknown transfer opcode from the peer", req->buffer, req->length); + Utils::dump("Unknown transfer opcode from the peer", req->buffer, req->length); break; } } @@ -379,7 +379,7 @@ void DiagNetwork::taskNetworkRx(NetPacketRequest* req) DECLARE_UINT8_ARRAY(rawPayload, req->length); ::memcpy(rawPayload, req->buffer, req->length); - // Utils::dump(1U, "Raw Payload", rawPayload, req->length); + // Utils::dump(1U, "DiagNetwork::taskNetworkRx(), PEER_LINK, Raw Payload", rawPayload, req->length); if (network->m_peerLinkActPkt.find(peerId) == network->m_peerLinkActPkt.end()) { network->m_peerLinkActPkt.insert(peerId, FNENetwork::PacketBufferEntry()); @@ -435,6 +435,7 @@ void DiagNetwork::taskNetworkRx(NetPacketRequest* req) if (!v.is()) { LogError(LOG_NET, "PEER %u error parsing active peer list, data was not valid", peerId); pkt.buffer->clear(); + delete pkt.buffer; pkt.streamId = 0U; if (decompressed != nullptr) { delete[] decompressed; diff --git a/src/fne/network/FNENetwork.cpp b/src/fne/network/FNENetwork.cpp index 3e75a1a7..7f04225c 100644 --- a/src/fne/network/FNENetwork.cpp +++ b/src/fne/network/FNENetwork.cpp @@ -18,6 +18,7 @@ #include "network/callhandler/TagDMRData.h" #include "network/callhandler/TagP25Data.h" #include "network/callhandler/TagNXDNData.h" +#include "network/callhandler/TagAnalogData.h" #include "fne/ActivityLog.h" #include "HostFNE.h" @@ -56,12 +57,13 @@ std::timed_mutex FNENetwork::m_keyQueueMutex; /* Initializes a new instance of the FNENetwork class. */ FNENetwork::FNENetwork(HostFNE* host, const std::string& address, uint16_t port, uint32_t peerId, const std::string& password, - bool debug, bool verbose, bool reportPeerPing, bool dmr, bool p25, bool nxdn, uint32_t parrotDelay, bool parrotGrantDemand, + bool debug, 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) : BaseNetwork(peerId, true, debug, true, true, allowActivityTransfer, allowDiagnosticTransfer), m_tagDMR(nullptr), m_tagP25(nullptr), m_tagNXDN(nullptr), + m_tagAnalog(nullptr), m_host(host), m_address(address), m_port(port), @@ -69,6 +71,7 @@ FNENetwork::FNENetwork(HostFNE* host, const std::string& address, uint16_t port, m_dmrEnabled(dmr), m_p25Enabled(p25), m_nxdnEnabled(nxdn), + m_analogEnabled(analog), m_parrotDelay(parrotDelay), m_parrotDelayTimer(1000U, 0U, parrotDelay), m_parrotGrantDemand(parrotGrantDemand), @@ -76,6 +79,8 @@ FNENetwork::FNENetwork(HostFNE* host, const std::string& address, uint16_t port, m_ridLookup(nullptr), m_tidLookup(nullptr), m_peerListLookup(nullptr), + m_adjSiteMapLookup(nullptr), + m_cryptoLookup(nullptr), m_status(NET_STAT_INVALID), m_peers(), m_peerLinkPeers(), @@ -92,10 +97,13 @@ FNENetwork::FNENetwork(HostFNE* host, const std::string& address, uint16_t port, m_allowConvSiteAffOverride(false), m_disallowCallTerm(false), m_restrictGrantToAffOnly(false), + m_restrictPVCallToRegOnly(false), m_enableInCallCtrl(true), m_rejectUnknownRID(false), - m_filterHeaders(true), + m_maskOutboundPeerID(false), + m_maskOutboundPeerIDForNonPL(false), m_filterTerminators(true), + m_forceListUpdate(false), m_disallowU2U(false), m_dropU2UPeerTable(), m_enableInfluxDB(false), @@ -109,6 +117,7 @@ FNENetwork::FNENetwork(HostFNE* host, const std::string& address, uint16_t port, m_disablePacketData(false), m_dumpPacketData(false), m_verbosePacketData(false), + m_logDenials(false), m_reportPeerPing(reportPeerPing), m_verbose(verbose) { @@ -120,6 +129,7 @@ FNENetwork::FNENetwork(HostFNE* host, const std::string& address, uint16_t port, m_tagDMR = new TagDMRData(this, debug); m_tagP25 = new TagP25Data(this, debug); m_tagNXDN = new TagNXDNData(this, debug); + m_tagAnalog = new TagAnalogData(this, debug); } /* Finalizes a instance of the FNENetwork class. */ @@ -129,6 +139,7 @@ FNENetwork::~FNENetwork() delete m_tagDMR; delete m_tagP25; delete m_tagNXDN; + delete m_tagAnalog; } /* Helper to set configuration options. */ @@ -140,6 +151,8 @@ void FNENetwork::setOptions(yaml::Node& conf, bool printOptions) m_allowConvSiteAffOverride = conf["allowConvSiteAffOverride"].as(true); m_enableInCallCtrl = conf["enableInCallCtrl"].as(false); m_rejectUnknownRID = conf["rejectUnknownRID"].as(false); + m_maskOutboundPeerID = conf["maskOutboundPeerID"].as(false); + m_maskOutboundPeerIDForNonPL = conf["maskOutboundPeerIDForNonPeerLink"].as(false); m_disallowCallTerm = conf["disallowCallTerm"].as(false); m_softConnLimit = conf["connectionLimit"].as(MAX_HARD_CONN_CAP); @@ -166,13 +179,15 @@ void FNENetwork::setOptions(yaml::Node& conf, bool printOptions) m_parrotOnlyOriginating = conf["parrotOnlyToOrginiatingPeer"].as(false); m_restrictGrantToAffOnly = conf["restrictGrantToAffiliatedOnly"].as(false); - m_filterHeaders = conf["filterHeaders"].as(true); + m_restrictPVCallToRegOnly = conf["restrictPrivateCallToRegOnly"].as(false); m_filterTerminators = conf["filterTerminators"].as(true); m_disablePacketData = conf["disablePacketData"].as(false); m_dumpPacketData = conf["dumpPacketData"].as(false); m_verbosePacketData = conf["verbosePacketData"].as(false); + m_logDenials = conf["logDenials"].as(false); + /* ** Drop Unit to Unit Peers */ @@ -202,8 +217,13 @@ void FNENetwork::setOptions(yaml::Node& conf, bool printOptions) LogInfo(" Allow conventional sites to override affiliation and receive all traffic: %s", m_allowConvSiteAffOverride ? "yes" : "no"); LogInfo(" Enable In-Call Control: %s", m_enableInCallCtrl ? "yes" : "no"); LogInfo(" Reject Unknown RIDs: %s", m_rejectUnknownRID ? "yes" : "no"); + LogInfo(" Log Traffic Denials: %s", m_logDenials ? "yes" : "no"); + LogInfo(" Mask Outbound Traffic Peer ID: %s", m_maskOutboundPeerID ? "yes" : "no"); + if (m_maskOutboundPeerIDForNonPL) { + LogInfo(" Mask Outbound Traffic Peer ID for Non-Peer Link: yes"); + } LogInfo(" Restrict grant response by affiliation: %s", m_restrictGrantToAffOnly ? "yes" : "no"); - LogInfo(" Traffic Headers Filtered by Destination ID: %s", m_filterHeaders ? "yes" : "no"); + LogInfo(" Restrict private call to registered units: %s", m_restrictPVCallToRegOnly ? "yes" : "no"); LogInfo(" Traffic Terminators Filtered by Destination ID: %s", m_filterTerminators ? "yes" : "no"); LogInfo(" Disallow Unit-to-Unit: %s", m_disallowU2U ? "yes" : "no"); LogInfo(" InfluxDB Reporting Enabled: %s", m_enableInfluxDB ? "yes" : "no"); @@ -221,12 +241,13 @@ void FNENetwork::setOptions(yaml::Node& conf, bool printOptions) /* Sets the instances of the Radio ID, Talkgroup ID Peer List, and Crypto lookup tables. */ void FNENetwork::setLookups(lookups::RadioIdLookup* ridLookup, lookups::TalkgroupRulesLookup* tidLookup, lookups::PeerListLookup* peerListLookup, - CryptoContainer* cryptoLookup) + CryptoContainer* cryptoLookup, lookups::AdjSiteMapLookup* adjSiteMapLookup) { m_ridLookup = ridLookup; m_tidLookup = tidLookup; m_peerListLookup = peerListLookup; m_cryptoLookup = cryptoLookup; + m_adjSiteMapLookup = adjSiteMapLookup; } /* Sets endpoint preshared encryption key. */ @@ -254,7 +275,7 @@ void FNENetwork::processNetwork() UInt8Array buffer = m_frameQueue->read(length, address, addrLen, &rtpHeader, &fneHeader); if (length > 0) { if (m_debug) - Utils::dump(1U, "Network Message", buffer.get(), length); + Utils::dump(1U, "FNENetwork::processNetwork(), Network Message", buffer.get(), length); uint32_t peerId = fneHeader.getPeerId(); @@ -441,9 +462,14 @@ void FNENetwork::clock(uint32_t ms) if (m_tagNXDN->hasParrotFrames()) { m_tagNXDN->playbackParrot(); } + + // if the analog handler has parrot frames to playback, playback a frame + if (m_tagAnalog->hasParrotFrames()) { + m_tagAnalog->playbackParrot(); + } } - if (!m_tagDMR->hasParrotFrames() && !m_tagP25->hasParrotFrames() && !m_tagNXDN->hasParrotFrames() && + if (!m_tagDMR->hasParrotFrames() && !m_tagP25->hasParrotFrames() && !m_tagNXDN->hasParrotFrames() && !m_tagAnalog->hasParrotFrames() && m_parrotDelayTimer.isRunning() && m_parrotDelayTimer.hasExpired()) { m_parrotDelayTimer.stop(); } @@ -497,7 +523,8 @@ void FNENetwork::close() uint32_t streamId = createStreamId(); for (auto peer : m_peers) { - writePeer(peer.first, { NET_FUNC::MST_DISC, NET_SUBFUNC::NOP }, buffer, 1U, RTP_END_OF_CALL_SEQ, streamId, false); + writePeer(peer.first, m_peerId, { NET_FUNC::MST_DISC, NET_SUBFUNC::NOP }, buffer, 1U, RTP_END_OF_CALL_SEQ, + streamId, false); } } @@ -546,6 +573,7 @@ void FNENetwork::taskNetworkRx(NetPacketRequest* req) if (req->length > 0) { uint32_t peerId = req->fneHeader.getPeerId(); + uint32_t ssrc = req->rtpHeader.getSSRC(); uint32_t streamId = req->fneHeader.getStreamId(); // determine if this packet is late (i.e. are we processing this packet more than 200ms after it was received?) @@ -614,7 +642,7 @@ void FNENetwork::taskNetworkRx(NetPacketRequest* req) if (connection->connected() && connection->address() == ip) { if (network->m_dmrEnabled) { if (network->m_tagDMR != nullptr) { - network->m_tagDMR->processFrame(req->buffer, req->length, peerId, req->rtpHeader.getSequence(), streamId); + network->m_tagDMR->processFrame(req->buffer, req->length, peerId, ssrc, req->rtpHeader.getSequence(), streamId); } } else { network->writePeerNAK(peerId, streamId, TAG_DMR_DATA, NET_CONN_NAK_MODE_NOT_ENABLED); @@ -640,7 +668,7 @@ void FNENetwork::taskNetworkRx(NetPacketRequest* req) if (connection->connected() && connection->address() == ip) { if (network->m_p25Enabled) { if (network->m_tagP25 != nullptr) { - network->m_tagP25->processFrame(req->buffer, req->length, peerId, req->rtpHeader.getSequence(), streamId); + network->m_tagP25->processFrame(req->buffer, req->length, peerId, ssrc, req->rtpHeader.getSequence(), streamId); } } else { network->writePeerNAK(peerId, streamId, TAG_P25_DATA, NET_CONN_NAK_MODE_NOT_ENABLED); @@ -666,7 +694,7 @@ void FNENetwork::taskNetworkRx(NetPacketRequest* req) if (connection->connected() && connection->address() == ip) { if (network->m_nxdnEnabled) { if (network->m_tagNXDN != nullptr) { - network->m_tagNXDN->processFrame(req->buffer, req->length, peerId, req->rtpHeader.getSequence(), streamId); + network->m_tagNXDN->processFrame(req->buffer, req->length, peerId, ssrc, req->rtpHeader.getSequence(), streamId); } } else { network->writePeerNAK(peerId, streamId, TAG_NXDN_DATA, NET_CONN_NAK_MODE_NOT_ENABLED); @@ -680,8 +708,34 @@ void FNENetwork::taskNetworkRx(NetPacketRequest* req) } break; + case NET_SUBFUNC::PROTOCOL_SUBFUNC_ANALOG: // Encapsulated analog data frame + { + if (peerId > 0 && (network->m_peers.find(peerId) != network->m_peers.end())) { + FNEPeerConnection* connection = network->m_peers[peerId]; + if (connection != nullptr) { + std::string ip = udp::Socket::address(req->address); + connection->lastPing(now); + + // validate peer (simple validation really) + if (connection->connected() && connection->address() == ip) { + if (network->m_analogEnabled) { + if (network->m_tagAnalog != nullptr) { + network->m_tagAnalog->processFrame(req->buffer, req->length, peerId, ssrc, req->rtpHeader.getSequence(), streamId); + } + } else { + network->writePeerNAK(peerId, streamId, TAG_ANALOG_DATA, NET_CONN_NAK_MODE_NOT_ENABLED); + } + } + } + } + else { + network->writePeerNAK(peerId, TAG_ANALOG_DATA, NET_CONN_NAK_FNE_UNAUTHORIZED, req->address, req->addrLen); + } + } + break; + default: - Utils::dump("unknown protocol opcode from peer", req->buffer, req->length); + Utils::dump("Unknown protocol opcode from peer", req->buffer, req->length); break; } } @@ -1112,7 +1166,7 @@ void FNENetwork::taskNetworkRx(NetPacketRequest* req) break; default: network->writePeerNAK(peerId, streamId, TAG_REPEATER_GRANT, NET_CONN_NAK_ILLEGAL_PACKET); - Utils::dump("unknown state for grant request from the peer", req->buffer, req->length); + Utils::dump("Unknown state for grant request from the peer", req->buffer, req->length); break; } } @@ -1177,7 +1231,7 @@ void FNENetwork::taskNetworkRx(NetPacketRequest* req) if (network->m_debug) { LogDebugEx(LOG_HOST, "FNENetwork::threadedNetworkRx()", "keyLength = %u", keyLength); - Utils::dump(1U, "Key", key, P25DEF::MAX_ENC_KEY_LENGTH_BYTES); + Utils::dump(1U, "FNENetwork::taskNetworkRx(), Key", key, P25DEF::MAX_ENC_KEY_LENGTH_BYTES); } LogMessage(LOG_NET, "PEER %u (%s) local enc. key, algId = $%02X, kID = $%04X", peerId, connection->identity().c_str(), @@ -1208,7 +1262,7 @@ void FNENetwork::taskNetworkRx(NetPacketRequest* req) modifyKeyRsp.encode(buffer + 11U); - network->writePeer(peerId, { NET_FUNC::KEY_RSP, NET_SUBFUNC::NOP }, buffer, modifyKeyRsp.length() + 11U, + network->writePeer(peerId, network->m_peerId, { NET_FUNC::KEY_RSP, NET_SUBFUNC::NOP }, buffer, modifyKeyRsp.length() + 11U, RTP_END_OF_CALL_SEQ, network->createStreamId(), false, false, true); } else { // attempt to forward KMM key request to Peer-Link masters @@ -1301,7 +1355,7 @@ void FNENetwork::taskNetworkRx(NetPacketRequest* req) FNEPeerConnection* connection = network->m_peers[peerId]; if (connection != nullptr) { std::string ip = udp::Socket::address(req->address); - lookups::AffiliationLookup* aff = network->m_peerAffiliations[peerId]; + fne_lookups::AffiliationLookup* aff = network->m_peerAffiliations[peerId]; if (aff == nullptr) { LogError(LOG_NET, "PEER %u (%s) has uninitialized affiliations lookup?", peerId, connection->identity().c_str()); network->writePeerNAK(peerId, streamId, TAG_ANNOUNCE, NET_CONN_NAK_INVALID); @@ -1310,7 +1364,7 @@ void FNENetwork::taskNetworkRx(NetPacketRequest* req) // validate peer (simple validation really) if (connection->connected() && connection->address() == ip && aff != nullptr) { uint32_t srcId = GET_UINT24(req->buffer, 0U); // Source Address - aff->unitReg(srcId); + aff->unitReg(srcId, ssrc); // attempt to repeat traffic to Peer-Link masters if (network->m_host->m_peerNetworks.size() > 0) { @@ -1318,7 +1372,7 @@ void FNENetwork::taskNetworkRx(NetPacketRequest* req) if (peer.second != nullptr) { if (peer.second->isEnabled() && peer.second->isPeerLink()) { peer.second->writeMaster({ NET_FUNC::ANNOUNCE, NET_SUBFUNC::ANNC_SUBFUNC_UNIT_REG }, - req->buffer, req->length, req->rtpHeader.getSequence(), streamId, false, false); + req->buffer, req->length, req->rtpHeader.getSequence(), streamId, false, false, 0U, ssrc); } } } @@ -1505,13 +1559,13 @@ void FNENetwork::taskNetworkRx(NetPacketRequest* req) break; default: network->writePeerNAK(peerId, streamId, TAG_ANNOUNCE, NET_CONN_NAK_ILLEGAL_PACKET); - Utils::dump("unknown announcement opcode from the peer", req->buffer, req->length); + Utils::dump("Unknown announcement opcode from the peer", req->buffer, req->length); } } break; default: - Utils::dump("unknown opcode from the peer", req->buffer, req->length); + Utils::dump("Unknown opcode from the peer", req->buffer, req->length); break; } } @@ -1555,7 +1609,7 @@ void FNENetwork::createPeerAffiliations(uint32_t peerId, std::string peerName) erasePeerAffiliations(peerId); lookups::ChannelLookup* chLookup = new lookups::ChannelLookup(); - m_peerAffiliations[peerId] = new lookups::AffiliationLookup(peerName, chLookup, m_verbose); + m_peerAffiliations[peerId] = new fne_lookups::AffiliationLookup(peerName, chLookup, m_verbose); m_peerAffiliations[peerId]->setDisableUnitRegTimeout(true); // FNE doesn't allow unit registration timeouts (notification must come from the peers) } @@ -1611,6 +1665,21 @@ void FNENetwork::erasePeer(uint32_t peerId) erasePeerAffiliations(peerId); } +/* Helper to find the unit registration for the given source ID. */ + +uint32_t FNENetwork::findPeerUnitReg(uint32_t srcId) +{ + for (auto it = m_peerAffiliations.begin(); it != m_peerAffiliations.end(); ++it) { + fne_lookups::AffiliationLookup* aff = it->second; + if (aff != nullptr) { + if (aff->isUnitReg(srcId)) { + return aff->getSSRCByUnitReg(srcId); + } + } + } + + return 0U; +} /* Helper to create a JSON representation of a FNE peer connection. */ @@ -1810,11 +1879,13 @@ void FNENetwork::writeWhitelistRIDs(uint32_t peerId, uint32_t streamId, bool isE LogInfoEx(LOG_NET, "PEER %u Peer-Link, RID List, blocks %u, streamId = %u", peerId, pkt.fragments.size(), streamId); if (pkt.fragments.size() > 0U) { for (auto frag : pkt.fragments) { - writePeer(peerId, { NET_FUNC::PEER_LINK, NET_SUBFUNC::PL_RID_LIST }, + writePeer(peerId, m_peerId, { NET_FUNC::PEER_LINK, NET_SUBFUNC::PL_RID_LIST }, frag.second->data, FRAG_SIZE, 0U, streamId, false, true, true); Thread::sleep(60U); // pace block transmission } } + + pkt.clear(); } return; @@ -1962,8 +2033,6 @@ void FNENetwork::writeBlacklistRIDs(uint32_t peerId, uint32_t streamId) void FNENetwork::writeTGIDs(uint32_t peerId, uint32_t streamId, bool isExternalPeer) { - uint64_t now = std::chrono::duration_cast(std::chrono::system_clock::now().time_since_epoch()).count(); - if (!m_tidLookup->sendTalkgroups()) { return; } @@ -1999,11 +2068,13 @@ void FNENetwork::writeTGIDs(uint32_t peerId, uint32_t streamId, bool isExternalP LogInfoEx(LOG_NET, "PEER %u Peer-Link, TGID List, blocks %u, streamId = %u", peerId, pkt.fragments.size(), streamId); if (pkt.fragments.size() > 0U) { for (auto frag : pkt.fragments) { - writePeer(peerId, { NET_FUNC::PEER_LINK, NET_SUBFUNC::PL_TALKGROUP_LIST }, + writePeer(peerId, m_peerId, { NET_FUNC::PEER_LINK, NET_SUBFUNC::PL_TALKGROUP_LIST }, frag.second->data, FRAG_SIZE, 0U, streamId, false, true, true); Thread::sleep(60U); // pace block transmission } } + + pkt.clear(); } return; @@ -2145,8 +2216,6 @@ void FNENetwork::writeDeactiveTGIDs(uint32_t peerId, uint32_t streamId) void FNENetwork::writePeerList(uint32_t peerId, uint32_t streamId) { - uint64_t now = std::chrono::duration_cast(std::chrono::system_clock::now().time_since_epoch()).count(); - // sending PEER_LINK style RID list to external peers FNEPeerConnection* connection = m_peers[peerId]; if (connection != nullptr) { @@ -2177,11 +2246,13 @@ void FNENetwork::writePeerList(uint32_t peerId, uint32_t streamId) LogInfoEx(LOG_NET, "PEER %u Peer-Link, PID List, blocks %u, streamId = %u", peerId, pkt.fragments.size(), streamId); if (pkt.fragments.size() > 0U) { for (auto frag : pkt.fragments) { - writePeer(peerId, { NET_FUNC::PEER_LINK, NET_SUBFUNC::PL_PEER_LIST }, + writePeer(peerId, m_peerId, { NET_FUNC::PEER_LINK, NET_SUBFUNC::PL_PEER_LIST }, frag.second->data, FRAG_SIZE, 0U, streamId, false, true, true); Thread::sleep(60U); // pace block transmission } } + + pkt.clear(); } return; @@ -2206,12 +2277,12 @@ bool FNENetwork::writePeerICC(uint32_t peerId, uint32_t streamId, NET_SUBFUNC::E SET_UINT24(dstId, buffer, 11U); // Destination ID buffer[14U] = slotNo; // DMR Slot No - return writePeer(peerId, { NET_FUNC::INCALL_CTRL, subFunc }, buffer, 15U, RTP_END_OF_CALL_SEQ, streamId, false); + return writePeer(peerId, m_peerId, { NET_FUNC::INCALL_CTRL, subFunc }, buffer, 15U, RTP_END_OF_CALL_SEQ, streamId, false); } /* Helper to send a data message to the specified peer with a explicit packet sequence. */ -bool FNENetwork::writePeer(uint32_t peerId, FrameQueue::OpcodePair opcode, const uint8_t* data, +bool FNENetwork::writePeer(uint32_t peerId, uint32_t ssrc, FrameQueue::OpcodePair opcode, const uint8_t* data, uint32_t length, uint16_t pktSeq, uint32_t streamId, bool queueOnly, bool incPktSeq, bool directWrite) const { if (streamId == 0U) { @@ -2229,10 +2300,26 @@ bool FNENetwork::writePeer(uint32_t peerId, FrameQueue::OpcodePair opcode, const pktSeq = connection->incStreamPktSeq(streamId, pktSeq); } + if (m_maskOutboundPeerID) + ssrc = m_peerId; // mask the source SSRC to our own peer ID + else { + if ((connection->isExternalPeer() && !connection->isPeerLink()) && m_maskOutboundPeerIDForNonPL) { + // if the peer is an external peer, and not a Peer-Link peer, we need to send the packet + // to the external peer with our peer ID as the source instead of the originating peer + // because we have routed it + ssrc = m_peerId; + } + + if (ssrc == 0U) { + LogError(LOG_NET, "BUGBUG: PEER %u, trying to send data with a ssrc of 0?, pktSeq = %u, streamId = %u", peerId, pktSeq, streamId); + ssrc = m_peerId; // fallback to our own peer ID + } + } + if (directWrite) - return m_frameQueue->write(data, length, streamId, peerId, m_peerId, opcode, pktSeq, addr, addrLen); + return m_frameQueue->write(data, length, streamId, peerId, ssrc, opcode, pktSeq, addr, addrLen); else { - m_frameQueue->enqueueMessage(data, length, streamId, peerId, m_peerId, opcode, pktSeq, addr, addrLen); + m_frameQueue->enqueueMessage(data, length, streamId, peerId, ssrc, opcode, pktSeq, addr, addrLen); if (queueOnly) return true; return m_frameQueue->flushQueue(); @@ -2259,7 +2346,7 @@ bool FNENetwork::writePeerCommand(uint32_t peerId, FrameQueue::OpcodePair opcode } uint32_t len = length + 6U; - return writePeer(peerId, opcode, buffer, len, RTP_END_OF_CALL_SEQ, streamId, false, incPktSeq, true); + return writePeer(peerId, m_peerId, opcode, buffer, len, RTP_END_OF_CALL_SEQ, streamId, false, incPktSeq, true); } /* Helper to send a ACK response to the specified peer. */ @@ -2275,8 +2362,8 @@ bool FNENetwork::writePeerACK(uint32_t peerId, uint32_t streamId, const uint8_t* ::memcpy(buffer + 6U, data, length); } - return writePeer(peerId, { NET_FUNC::ACK, NET_SUBFUNC::NOP }, buffer, length + 10U, RTP_END_OF_CALL_SEQ, streamId, - false, false, true); + return writePeer(peerId, m_peerId, { NET_FUNC::ACK, NET_SUBFUNC::NOP }, buffer, length + 10U, RTP_END_OF_CALL_SEQ, + streamId, false, false, true); } /* Helper to log a warning specifying which NAK reason is being sent a peer. */ @@ -2332,7 +2419,7 @@ bool FNENetwork::writePeerNAK(uint32_t peerId, uint32_t streamId, const char* ta SET_UINT16((uint16_t)reason, buffer, 10U); // Reason logPeerNAKReason(peerId, tag, reason); - return writePeer(peerId, { NET_FUNC::NAK, NET_SUBFUNC::NOP }, buffer, 10U, RTP_END_OF_CALL_SEQ, streamId, false); + return writePeer(peerId, m_peerId, { NET_FUNC::NAK, NET_SUBFUNC::NOP }, buffer, 10U, RTP_END_OF_CALL_SEQ, streamId, false); } /* Helper to send a NAK response to the specified peer. */ @@ -2382,7 +2469,7 @@ void FNENetwork::processTEKResponse(p25::kmm::KeyItem* rspKi, uint8_t algId, uin if (m_debug) { LogDebugEx(LOG_HOST, "FNENetwork::processTEKResponse()", "keyLength = %u", keyLength); - Utils::dump(1U, "Key", key, P25DEF::MAX_ENC_KEY_LENGTH_BYTES); + Utils::dump(1U, "FNENetwork::processTEKResponse(), Key", key, P25DEF::MAX_ENC_KEY_LENGTH_BYTES); } // build response buffer @@ -2410,7 +2497,7 @@ void FNENetwork::processTEKResponse(p25::kmm::KeyItem* rspKi, uint8_t algId, uin modifyKeyRsp.encode(buffer + 11U); - writePeer(peerId, { NET_FUNC::KEY_RSP, NET_SUBFUNC::NOP }, buffer, modifyKeyRsp.length() + 11U, + writePeer(peerId, m_peerId, { NET_FUNC::KEY_RSP, NET_SUBFUNC::NOP }, buffer, modifyKeyRsp.length() + 11U, RTP_END_OF_CALL_SEQ, createStreamId(), false, false, true); peersToRemove.push_back(peerId); diff --git a/src/fne/network/FNENetwork.h b/src/fne/network/FNENetwork.h index d19baa64..a0a230b2 100644 --- a/src/fne/network/FNENetwork.h +++ b/src/fne/network/FNENetwork.h @@ -28,13 +28,14 @@ #include "common/concurrent/unordered_map.h" #include "common/network/BaseNetwork.h" #include "common/network/json/json.h" -#include "common/lookups/AffiliationLookup.h" #include "common/lookups/RadioIdLookup.h" #include "common/lookups/TalkgroupRulesLookup.h" #include "common/lookups/PeerListLookup.h" +#include "common/lookups/AdjSiteMapLookup.h" #include "common/network/Network.h" #include "common/network/PacketBuffer.h" #include "common/ThreadPool.h" +#include "fne/lookups/AffiliationLookup.h" #include "fne/network/influxdb/InfluxDB.h" #include "fne/CryptoContainer.h" @@ -54,6 +55,7 @@ namespace network { namespace callhandler { namespace packetdata { class HOST_SW namespace network { namespace callhandler { class HOST_SW_API TagP25Data; } } namespace network { namespace callhandler { namespace packetdata { class HOST_SW_API P25PacketData; } } } namespace network { namespace callhandler { class HOST_SW_API TagNXDNData; } } +namespace network { namespace callhandler { class HOST_SW_API TagAnalogData; } } namespace network { @@ -80,6 +82,7 @@ namespace network #define INFLUXDB_ERRSTR_DISABLED_TALKGROUP "disabled talkgroup" #define INFLUXDB_ERRSTR_INV_SLOT "invalid slot for talkgroup" #define INFLUXDB_ERRSTR_RID_NOT_PERMITTED "RID not permitted for talkgroup" + #define INFLUXDB_ERRSTR_ILLEGAL_RID_ACCESS "illegal/unknown RID attempted access" // --------------------------------------------------------------------------- // Class Prototypes @@ -421,6 +424,7 @@ namespace network * @param dmr Flag indicating whether DMR is enabled. * @param p25 Flag indicating whether P25 is enabled. * @param nxdn Flag indicating whether NXDN is enabled. + * @param analog Flag indicating whether analog is enabled. * @param parrotDelay Delay for end of call to parrot TG playback. * @param parrotGrantDemand Flag indicating whether a parrot TG will generate a grant demand. * @param allowActivityTransfer Flag indicating that the system activity logs will be sent to the network. @@ -430,7 +434,7 @@ namespace network * @param workerCnt Number of worker threads. */ FNENetwork(HostFNE* host, const std::string& address, uint16_t port, uint32_t peerId, const std::string& password, - bool debug, bool verbose, bool reportPeerPing, bool dmr, bool p25, bool nxdn, uint32_t parrotDelay, bool parrotGrantDemand, + bool debug, 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. @@ -465,6 +469,11 @@ namespace network * @returns callhandler::TagNXDNData* Instance of the TagNXDNData call handler. */ callhandler::TagNXDNData* nxdnTrafficHandler() const { return m_tagNXDN; } + /** + * @brief Gets the instance of the analog call handler. + * @returns callhandler::TagAnalogData* Instance of the TagAnalogData call handler. + */ + callhandler::TagAnalogData* analogTrafficHandler() const { return m_tagAnalog; } /** * @brief Sets the instances of the Radio ID, Talkgroup ID Peer List, and Crypto lookup tables. @@ -472,9 +481,10 @@ namespace network * @param tidLookup Talkgroup Rules Lookup Table Instance * @param peerListLookup Peer List Lookup Table Instance * @param cryptoLookup Crypto Container Lookup Table Instance + * @param adjSiteMapLookup Adjacent Site Map Lookup Table Instance */ void setLookups(lookups::RadioIdLookup* ridLookup, lookups::TalkgroupRulesLookup* tidLookup, lookups::PeerListLookup* peerListLookup, - CryptoContainer* cryptoLookup); + CryptoContainer* cryptoLookup, lookups::AdjSiteMapLookup* adjSiteMapLookup); /** * @brief Sets endpoint preshared encryption key. * @param presharedKey Encryption preshared key for networking. @@ -528,6 +538,8 @@ namespace network callhandler::TagP25Data* m_tagP25; friend class callhandler::TagNXDNData; callhandler::TagNXDNData* m_tagNXDN; + friend class callhandler::TagAnalogData; + callhandler::TagAnalogData* m_tagAnalog; friend class ::RESTAPI; HostFNE* m_host; @@ -540,6 +552,7 @@ namespace network bool m_dmrEnabled; bool m_p25Enabled; bool m_nxdnEnabled; + bool m_analogEnabled; uint32_t m_parrotDelay; Timer m_parrotDelayTimer; @@ -549,6 +562,7 @@ namespace network lookups::RadioIdLookup* m_ridLookup; lookups::TalkgroupRulesLookup* m_tidLookup; lookups::PeerListLookup* m_peerListLookup; + lookups::AdjSiteMapLookup* m_adjSiteMapLookup; CryptoContainer* m_cryptoLookup; @@ -558,7 +572,7 @@ namespace network concurrent::unordered_map m_peers; concurrent::unordered_map m_peerLinkPeers; typedef std::pair PeerAffiliationMapPair; - concurrent::unordered_map m_peerAffiliations; + concurrent::unordered_map m_peerAffiliations; concurrent::unordered_map> m_ccPeerMap; static std::timed_mutex m_keyQueueMutex; std::unordered_map m_peerLinkKeyQueue; @@ -594,10 +608,13 @@ namespace network bool m_allowConvSiteAffOverride; bool m_disallowCallTerm; bool m_restrictGrantToAffOnly; + bool m_restrictPVCallToRegOnly; bool m_enableInCallCtrl; bool m_rejectUnknownRID; - bool m_filterHeaders; + bool m_maskOutboundPeerID; + bool m_maskOutboundPeerIDForNonPL; + bool m_filterTerminators; bool m_forceListUpdate; @@ -620,6 +637,7 @@ namespace network bool m_dumpPacketData; bool m_verbosePacketData; + bool m_logDenials; bool m_reportPeerPing; bool m_verbose; @@ -663,6 +681,13 @@ namespace network */ void erasePeer(uint32_t peerId); + /** + * @brief Helper to find the unit registration for the given source ID. + * @param srcId Source Radio ID. + * @returns uint32_t Peer ID, or 0 if not found. + */ + uint32_t findPeerUnitReg(uint32_t srcId); + /** * @brief Helper to resolve the peer ID to its identity string. * @param peerId Peer ID. @@ -736,7 +761,8 @@ namespace network /** * @brief Helper to send a data message to the specified peer with a explicit packet sequence. - * @param peerId Peer ID. + * @param peerId Destination Peer ID. + * @param ssrc RTP synchronization source ID. * @param opcode FNE network opcode pair. * @param[in] data Buffer containing message to send to peer. * @param length Length of buffer. @@ -746,7 +772,7 @@ namespace network * @param incPktSeq Flag indicating the message should increment the packet sequence after transmission. * @param directWrite Flag indicating this message should be immediately directly written. */ - bool writePeer(uint32_t peerId, FrameQueue::OpcodePair opcode, const uint8_t* data, uint32_t length, + bool writePeer(uint32_t peerId, uint32_t ssrc, FrameQueue::OpcodePair opcode, const uint8_t* data, uint32_t length, uint16_t pktSeq, uint32_t streamId, bool queueOnly, bool incPktSeq = false, bool directWrite = false) const; /** diff --git a/src/fne/network/PeerNetwork.cpp b/src/fne/network/PeerNetwork.cpp index 6677afb3..e3070e68 100644 --- a/src/fne/network/PeerNetwork.cpp +++ b/src/fne/network/PeerNetwork.cpp @@ -4,7 +4,7 @@ * GPLv2 Open Source. Use is subject to license terms. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * - * Copyright (C) 2024 Bryan Biedenkapp, N2PLL + * Copyright (C) 2024-2025 Bryan Biedenkapp, N2PLL * */ #include "fne/Defines.h" @@ -23,6 +23,14 @@ using namespace compress; #include #include +// --------------------------------------------------------------------------- +// Constants +// --------------------------------------------------------------------------- + +#define WORKER_CNT 8U + +const uint64_t PACKET_LATE_TIME = 200U; // 200ms + // --------------------------------------------------------------------------- // Public Class Members // --------------------------------------------------------------------------- @@ -30,16 +38,21 @@ using namespace compress; /* Initializes a new instance of the PeerNetwork class. */ PeerNetwork::PeerNetwork(const std::string& address, uint16_t port, uint16_t localPort, uint32_t peerId, const std::string& password, - bool duplex, bool debug, bool dmr, bool p25, bool nxdn, bool slot1, bool slot2, bool allowActivityTransfer, bool allowDiagnosticTransfer, bool updateLookup, bool saveLookup) : - Network(address, port, localPort, peerId, password, duplex, debug, dmr, p25, nxdn, slot1, slot2, allowActivityTransfer, allowDiagnosticTransfer, updateLookup, saveLookup), + bool duplex, bool debug, bool dmr, bool p25, bool nxdn, bool analog, bool slot1, bool slot2, bool allowActivityTransfer, bool allowDiagnosticTransfer, bool updateLookup, bool saveLookup) : + Network(address, port, localPort, peerId, password, duplex, debug, dmr, p25, nxdn, analog, slot1, slot2, allowActivityTransfer, allowDiagnosticTransfer, updateLookup, saveLookup), m_attachedKeyRSPHandler(false), m_blockTrafficToTable(), + m_dmrCallback(nullptr), + m_p25Callback(nullptr), + m_nxdnCallback(nullptr), + m_analogCallback(nullptr), m_pidLookup(nullptr), m_peerLink(false), m_peerLinkSavesACL(false), m_tgidPkt(true, "Peer-Link, TGID List"), m_ridPkt(true, "Peer-Link, RID List"), - m_pidPkt(true, "Peer-Link, PID List") + m_pidPkt(true, "Peer-Link, PID List"), + m_threadPool(WORKER_CNT, "peer") { assert(!address.empty()); assert(port > 0U); @@ -50,6 +63,21 @@ PeerNetwork::PeerNetwork(const std::string& address, uint16_t port, uint16_t loc // never disable peer network services on ACL NAK from master m_neverDisableOnACLNAK = true; + + // FNE peer network manually handle protocol packets + m_userHandleProtocol = true; + + // start thread pool + m_threadPool.start(); +} + +/* Finalizes a instance of the PeerNetwork class. */ + +PeerNetwork::~PeerNetwork() +{ + // stop thread pool + m_threadPool.stop(); + m_threadPool.wait(); } /* Sets the instances of the Peer List lookup tables. */ @@ -59,18 +87,21 @@ void PeerNetwork::setPeerLookups(lookups::PeerListLookup* pidLookup) m_pidLookup = pidLookup; } -/* Gets the received DMR stream ID. */ +/* Opens connection to the network. */ -uint32_t PeerNetwork::getRxDMRStreamId(uint32_t slotNo) const +bool PeerNetwork::open() { - assert(slotNo == 1U || slotNo == 2U); + if (!m_enabled) + return false; - if (slotNo == 1U) { - return m_rxDMRStreamId[0U]; - } - else { - return m_rxDMRStreamId[1U]; - } + return Network::open(); +} + +/* Closes connection to the network. */ + +void PeerNetwork::close() +{ + Network::close(); } /* Checks if the passed peer ID is blocked from sending to this peer. */ @@ -128,6 +159,7 @@ bool PeerNetwork::writePeerLinkPeers(json::array* peerList) } } + pkt.clear(); return true; } @@ -140,13 +172,44 @@ bool PeerNetwork::writePeerLinkPeers(json::array* peerList) /* User overrideable handler that allows user code to process network packets not handled by this class. */ -void PeerNetwork::userPacketHandler(uint32_t peerId, FrameQueue::OpcodePair opcode, const uint8_t* data, uint32_t length, uint32_t streamId) +void PeerNetwork::userPacketHandler(uint32_t peerId, FrameQueue::OpcodePair opcode, const uint8_t* data, uint32_t length, uint32_t streamId, + const frame::RTPFNEHeader& fneHeader, const frame::RTPHeader& rtpHeader) { switch (opcode.first) { - case NET_FUNC::PEER_LINK: + case NET_FUNC::PROTOCOL: // Protocol + { + PeerPacketRequest* req = new PeerPacketRequest(); + req->obj = this; + req->peerId = peerId; + req->streamId = streamId; + + req->rtpHeader = rtpHeader; + req->fneHeader = fneHeader; + + req->subFunc = opcode.second; + + req->pktRxTime = std::chrono::duration_cast(std::chrono::system_clock::now().time_since_epoch()).count(); + + req->length = length; + req->buffer = new uint8_t[length]; + ::memcpy(req->buffer, data, length); + + // enqueue the task + if (!m_threadPool.enqueue(new_pooltask(taskNetworkRx, req))) { + LogError(LOG_NET, "Failed to task enqueue network packet request, peerId = %u", peerId); + if (req != nullptr) { + if (req->buffer != nullptr) + delete[] req->buffer; + delete req; + } + } + } + break; + + case NET_FUNC::PEER_LINK: // Peer Link { switch (opcode.second) { - case NET_SUBFUNC::PL_TALKGROUP_LIST: + case NET_SUBFUNC::PL_TALKGROUP_LIST: // Talkgroup List { uint32_t decompressedLen = 0U; uint8_t* decompressed = nullptr; @@ -203,7 +266,7 @@ void PeerNetwork::userPacketHandler(uint32_t peerId, FrameQueue::OpcodePair opco } break; - case NET_SUBFUNC::PL_RID_LIST: + case NET_SUBFUNC::PL_RID_LIST: // Radio ID List { uint32_t decompressedLen = 0U; uint8_t* decompressed = nullptr; @@ -260,7 +323,7 @@ void PeerNetwork::userPacketHandler(uint32_t peerId, FrameQueue::OpcodePair opco } break; - case NET_SUBFUNC::PL_PEER_LIST: + case NET_SUBFUNC::PL_PEER_LIST: // Peer List { uint32_t decompressedLen = 0U; uint8_t* decompressed = nullptr; @@ -324,7 +387,7 @@ void PeerNetwork::userPacketHandler(uint32_t peerId, FrameQueue::OpcodePair opco break; default: - Utils::dump("unknown opcode from the master", data, length); + Utils::dump("Unknown opcode from the master", data, length); break; } } @@ -386,8 +449,82 @@ bool PeerNetwork::writeConfig() ::snprintf(buffer + 8U, json.length() + 1U, "%s", json.c_str()); if (m_debug) { - Utils::dump(1U, "Network Message, Configuration", (uint8_t*)buffer, json.length() + 8U); + Utils::dump(1U, "PeerNetwork::writeConfig(), Message, Configuration", (uint8_t*)buffer, json.length() + 8U); } return writeMaster({ NET_FUNC::RPTC, NET_SUBFUNC::NOP }, (uint8_t*)buffer, json.length() + 8U, pktSeq(), m_loginStreamId); } + +// --------------------------------------------------------------------------- +// Private Class Members +// --------------------------------------------------------------------------- + +/* Process a data frames from the network. */ + +void PeerNetwork::taskNetworkRx(PeerPacketRequest* req) +{ + if (req != nullptr) { + uint64_t now = std::chrono::duration_cast(std::chrono::system_clock::now().time_since_epoch()).count(); + + PeerNetwork* network = static_cast(req->obj); + if (network == nullptr) { + if (req != nullptr) { + if (req->buffer != nullptr) + delete[] req->buffer; + delete req; + } + + return; + } + + if (req == nullptr) + 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?) + uint64_t dt = req->pktRxTime + PACKET_LATE_TIME; + if (dt < now) { + LogWarning(LOG_NET, "PEER %u packet processing latency >200ms, dt = %u, now = %u", req->peerId, dt, now); + } + + // process incomfing message subfunction opcodes + switch (req->subFunc) { + case NET_SUBFUNC::PROTOCOL_SUBFUNC_DMR: // Encapsulated DMR data frame + { + if (network->m_dmrCallback != nullptr) + network->m_dmrCallback(network, req->buffer, req->length, req->streamId, req->fneHeader, req->rtpHeader); + } + break; + + case NET_SUBFUNC::PROTOCOL_SUBFUNC_P25: // Encapsulated P25 data frame + { + if (network->m_p25Callback != nullptr) + network->m_p25Callback(network, req->buffer, req->length, req->streamId, req->fneHeader, req->rtpHeader); + } + break; + + case NET_SUBFUNC::PROTOCOL_SUBFUNC_NXDN: // Encapsulated NXDN data frame + { + if (network->m_nxdnCallback != nullptr) + network->m_nxdnCallback(network, req->buffer, req->length, req->streamId, req->fneHeader, req->rtpHeader); + } + break; + + case NET_SUBFUNC::PROTOCOL_SUBFUNC_ANALOG: // Encapsulated analog data frame + { + if (network->m_analogCallback != nullptr) + network->m_analogCallback(network, req->buffer, req->length, req->streamId, req->fneHeader, req->rtpHeader); + } + break; + + default: + Utils::dump("Unknown protocol opcode from the master", req->buffer, req->length); + break; + } + } + + if (req->buffer != nullptr) + delete[] req->buffer; + delete req; + } +} diff --git a/src/fne/network/PeerNetwork.h b/src/fne/network/PeerNetwork.h index 4e3016c5..28c9d700 100644 --- a/src/fne/network/PeerNetwork.h +++ b/src/fne/network/PeerNetwork.h @@ -4,7 +4,7 @@ * GPLv2 Open Source. Use is subject to license terms. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * - * Copyright (C) 2024 Bryan Biedenkapp, N2PLL + * Copyright (C) 2024-2025 Bryan Biedenkapp, N2PLL * */ /** @@ -20,6 +20,7 @@ #include "common/lookups/PeerListLookup.h" #include "common/network/Network.h" #include "common/network/PacketBuffer.h" +#include "common/ThreadPool.h" #include #include @@ -27,6 +28,28 @@ namespace network { + // --------------------------------------------------------------------------- + // Structure Declaration + // --------------------------------------------------------------------------- + + /** + * @brief Represents the data required for a network packet handler thread. + * @ingroup fne_network + */ + struct PeerPacketRequest : thread_t { + uint32_t peerId; //! Peer ID for this request. + uint32_t streamId; //! Stream ID for this request. + + frame::RTPHeader rtpHeader; //! RTP Header + frame::RTPFNEHeader fneHeader; //! RTP FNE Header + int length = 0U; //! Length of raw data buffer + uint8_t* buffer = nullptr; //! Raw data buffer + + network::NET_SUBFUNC::ENUM subFunc; //! Sub-function of the packet + + uint64_t pktRxTime; //! Packet receive time + }; + // --------------------------------------------------------------------------- // Class Declaration // --------------------------------------------------------------------------- @@ -49,6 +72,7 @@ namespace network * @param dmr Flag indicating whether DMR is enabled. * @param p25 Flag indicating whether P25 is enabled. * @param nxdn Flag indicating whether NXDN is enabled. + * @param analog Flag indicating whether analog is enabled. * @param slot1 Flag indicating whether DMR slot 1 is enabled for network traffic. * @param slot2 Flag indicating whether DMR slot 2 is enabled for network traffic. * @param allowActivityTransfer Flag indicating that the system activity logs will be sent to the network. @@ -56,7 +80,11 @@ namespace network * @param updateLookup Flag indicating that the system will accept radio ID and talkgroup ID lookups from the network. */ PeerNetwork(const std::string& address, uint16_t port, uint16_t localPort, uint32_t peerId, const std::string& password, - bool duplex, bool debug, bool dmr, bool p25, bool nxdn, bool slot1, bool slot2, bool allowActivityTransfer, bool allowDiagnosticTransfer, bool updateLookup, bool saveLookup); + bool duplex, bool debug, bool dmr, bool p25, bool nxdn, bool analog, bool slot1, bool slot2, bool allowActivityTransfer, bool allowDiagnosticTransfer, bool updateLookup, bool saveLookup); + /** + * @brief Finalizes a instance of the PeerNetwork class. + */ + ~PeerNetwork() override; /** * @brief Sets the instances of the Peer List lookup tables. @@ -65,21 +93,36 @@ namespace network void setPeerLookups(lookups::PeerListLookup* pidLookup); /** - * @brief Gets the received DMR stream ID. - * @param slotNo DMR slot to get stream ID for. - * @return uint32_t Stream ID for the given DMR slot. + * @brief Opens connection to the network. + * @returns bool True, if networking has started, otherwise false. + */ + bool open() override; + + /** + * @brief Closes connection to the network. + */ + void close() override; + + /** + * @brief Helper to set the DMR protocol callback. + * @param callback */ - uint32_t getRxDMRStreamId(uint32_t slotNo) const; + void setDMRCallback(std::function&& callback) { m_dmrCallback = callback; } /** - * @brief Gets the received P25 stream ID. - * @return uint32_t Stream ID. + * @brief Helper to set the P25 protocol callback. + * @param callback */ - uint32_t getRxP25StreamId() const { return m_rxP25StreamId; } + void setP25Callback(std::function&& callback) { m_p25Callback = callback; } /** - * @brief Gets the received NXDN stream ID. - * @return uint32_t Stream ID. + * @brief Helper to set the NXDN protocol callback. + * @param callback */ - uint32_t getRxNXDNStreamId() const { return m_rxNXDNStreamId; } + void setNXDNCallback(std::function&& callback) { m_nxdnCallback = callback; } + /** + * @brief Helper to set the analog protocol callback. + * @param callback + */ + void setAnalogCallback(std::function&& callback) { m_analogCallback = callback; } /** * @brief Gets the blocked traffic peer ID table. @@ -125,6 +168,27 @@ namespace network protected: std::vector m_blockTrafficToTable; + /** + * @brief DMR Protocol Callback. + * (This is called when the master sends a DMR packet.) + */ + std::function m_dmrCallback; + /** + * @brief P25 Protocol Callback. + * (This is called when the master sends a P25 packet.) + */ + std::function m_p25Callback; + /** + * @brief NXDN Protocol Callback. + * (This is called when the master sends a NXDN packet.) + */ + std::function m_nxdnCallback; + /** + * @brief Analog Protocol Callback. + * (This is called when the master sends a analog packet.) + */ + std::function m_analogCallback; + /** * @brief User overrideable handler that allows user code to process network packets not handled by this class. * @param peerId Peer ID. @@ -132,9 +196,11 @@ namespace network * @param[in] data Buffer containing message to send to peer. * @param length Length of buffer. * @param streamId Stream ID. + * @param fneHeader RTP FNE Header. + * @param rtpHeader RTP Header. */ void userPacketHandler(uint32_t peerId, FrameQueue::OpcodePair opcode, const uint8_t* data = nullptr, uint32_t length = 0U, - uint32_t streamId = 0U) override; + uint32_t streamId = 0U, const frame::RTPFNEHeader& fneHeader = frame::RTPFNEHeader(), const frame::RTPHeader& rtpHeader = frame::RTPHeader()) override; /** * @brief Writes configuration to the network. @@ -150,6 +216,14 @@ namespace network PacketBuffer m_tgidPkt; PacketBuffer m_ridPkt; PacketBuffer m_pidPkt; + + ThreadPool m_threadPool; + + /** + * @brief Entry point to process a given network packet. + * @param req Instance of the PeerPacketRequest structure. + */ + static void taskNetworkRx(PeerPacketRequest* req); }; } // namespace network diff --git a/src/fne/network/RESTAPI.cpp b/src/fne/network/RESTAPI.cpp index 5a0949f8..f57c36ae 100644 --- a/src/fne/network/RESTAPI.cpp +++ b/src/fne/network/RESTAPI.cpp @@ -505,6 +505,7 @@ RESTAPI::RESTAPI(const std::string& address, uint16_t port, const std::string& p m_ridLookup(nullptr), m_tidLookup(nullptr), m_peerListLookup(nullptr), + m_adjSiteMapLookup(nullptr), m_authTokens() { assert(!address.empty()); @@ -526,7 +527,7 @@ RESTAPI::RESTAPI(const std::string& address, uint16_t port, const std::string& p delete[] in; if (m_debug) { - Utils::dump("REST Password Hash", m_passwordHash, 32U); + Utils::dump("RESTAPI::RESTAPI(), REST Password Hash", m_passwordHash, 32U); } #if defined(ENABLE_SSL) @@ -554,11 +555,13 @@ RESTAPI::~RESTAPI() /* Sets the instances of the Radio ID and Talkgroup ID lookup tables. */ -void RESTAPI::setLookups(lookups::RadioIdLookup* ridLookup, lookups::TalkgroupRulesLookup* tidLookup, ::lookups::PeerListLookup* peerListLookup) +void RESTAPI::setLookups(lookups::RadioIdLookup* ridLookup, lookups::TalkgroupRulesLookup* tidLookup, + ::lookups::PeerListLookup* peerListLookup, ::lookups::AdjSiteMapLookup* adjMapLookup) { m_ridLookup = ridLookup; m_tidLookup = tidLookup; m_peerListLookup = peerListLookup; + m_adjSiteMapLookup = adjMapLookup; } /* Sets the instance of the FNE network. */ @@ -651,6 +654,11 @@ void RESTAPI::initializeEndpoints() m_dispatcher.match(FNE_PUT_PEER_DELETE).put(REST_API_BIND(RESTAPI::restAPI_PutPeerDelete, this)); m_dispatcher.match(FNE_GET_PEER_COMMIT).get(REST_API_BIND(RESTAPI::restAPI_GetPeerCommit, this)); + m_dispatcher.match(FNE_GET_ADJ_MAP_LIST).get(REST_API_BIND(RESTAPI::restAPI_GetAdjMapList, this)); + m_dispatcher.match(FNE_PUT_ADJ_MAP_ADD).put(REST_API_BIND(RESTAPI::restAPI_PutAdjMapAdd, this)); + m_dispatcher.match(FNE_PUT_ADJ_MAP_DELETE).put(REST_API_BIND(RESTAPI::restAPI_PutAdjMapDelete, this)); + m_dispatcher.match(FNE_GET_ADJ_MAP_COMMIT).get(REST_API_BIND(RESTAPI::restAPI_GetAdjMapCommit, this)); + m_dispatcher.match(FNE_GET_FORCE_UPDATE).get(REST_API_BIND(RESTAPI::restAPI_GetForceUpdate, this)); m_dispatcher.match(FNE_GET_RELOAD_TGS).get(REST_API_BIND(RESTAPI::restAPI_GetReloadTGs, this)); @@ -771,7 +779,7 @@ void RESTAPI::restAPI_PutAuth(const HTTPPayload& request, HTTPPayload& reply, co } if (m_debug) { - Utils::dump("Password Hash", passwordHash, 32U); + Utils::dump("RESTAPI::restAPI_PutAuth(), Password Hash", passwordHash, 32U); } // compare hashes @@ -1287,7 +1295,10 @@ void RESTAPI::restAPI_PutPeerAdd(const HTTPPayload& request, HTTPPayload& reply, peerPassword = req["peerPassword"].get(); } - m_peerListLookup->addEntry(peerId, peerAlias, peerPassword, peerLink); + PeerId entry = PeerId(peerId, peerAlias, peerPassword, false); + entry.peerLink(peerLink); + + m_peerListLookup->addEntry(peerId, entry); } /* REST API endpoint; implements put peer delete request. */ @@ -1331,6 +1342,136 @@ void RESTAPI::restAPI_GetPeerCommit(const HTTPPayload& request, HTTPPayload& rep reply.payload(response); } + +/* REST API endpoint; implements get adjacent site map query request. */ + +void RESTAPI::restAPI_GetAdjMapList(const HTTPPayload& request, HTTPPayload& reply, const RequestMatch& match) +{ + if (!validateAuth(request, reply)) { + return; + } + + json::object response = json::object(); + setResponseDefaultStatus(response); + + json::array peers = json::array(); + if (m_adjSiteMapLookup != nullptr) { + if (m_adjSiteMapLookup->adjPeerMap().size() > 0) { + for (auto entry : m_adjSiteMapLookup->adjPeerMap()) { + json::object peerObj = json::object(); + + uint32_t peerId = entry.peerId(); + peerObj["peerId"].set(peerId); + + json::array neighbors = json::array(); + std::vector neighbor = entry.neighbors(); + if (neighbor.size() > 0) { + for (auto neighEntry : neighbor) { + uint32_t peerId = neighEntry; + neighbors.push_back(json::value((double)peerId)); + } + } + peerObj["neighbors"].set(neighbors); + peers.push_back(json::value(peerObj)); + } + } + } + + response["peers"].set(peers); + reply.payload(response); +} + +/* REST API endpoint; implements put adjacent site map add request. */ + +void RESTAPI::restAPI_PutAdjMapAdd(const HTTPPayload& request, HTTPPayload& reply, const RequestMatch& match) +{ + if (!validateAuth(request, reply)) { + return; + } + + json::object req = json::object(); + if (!parseRequestBody(request, reply, req)) { + return; + } + + errorPayload(reply, "OK", HTTPPayload::OK); + + // Validate peer ID (required) + if (!req["peerId"].is()) { + errorPayload(reply, "peerId was not a valid integer"); + return; + } + + // get + AdjPeerMapEntry entry = AdjPeerMapEntry(); + uint32_t peerId = req["peerId"].get(); + entry.peerId(peerId); + + if (!req["neighbors"].is()) { + errorPayload(reply, "Peer \"neighbors\" was not a valid JSON array"); + LogDebug(LOG_REST, "Peer \"neighbors\" was not a valid JSON array"); + return; + } + json::array neighbors = req["neighbors"].get(); + + std::vector neighbor = std::vector(); + if (neighbors.size() > 0) { + for (auto neighEntry : neighbors) { + if (!neighEntry.is()) { + errorPayload(reply, "Peer neighbor value was not a valid number"); + LogDebug(LOG_REST, "Peer neighbor value was not a valid number (was %s)", neighEntry.to_type().c_str()); + return; + } + + neighbor.push_back(neighEntry.get()); + } + entry.neighbors(neighbor); + } + + m_adjSiteMapLookup->addEntry(entry); +} + +/* REST API endpoint; implements put adjacent site map delete request. */ + +void RESTAPI::restAPI_PutAdjMapDelete(const HTTPPayload& request, HTTPPayload& reply, const RequestMatch& match) +{ + if (!validateAuth(request, reply)) { + return; + } + + json::object req = json::object(); + if (!parseRequestBody(request, reply, req)) { + return; + } + + errorPayload(reply, "OK", HTTPPayload::OK); + + if (!req["peerId"].is()) { + errorPayload(reply, "peerId was not a valid integer"); + return; + } + + uint32_t peerId = req["peerId"].get(); + + m_adjSiteMapLookup->eraseEntry(peerId); +} + +/* REST API endpoint; implements put adjacent site map commit request. */ + +void RESTAPI::restAPI_GetAdjMapCommit(const HTTPPayload& request, HTTPPayload& reply, const RequestMatch& match) +{ + if (!validateAuth(request, reply)) { + return; + } + + json::object response = json::object(); + setResponseDefaultStatus(response); + + m_adjSiteMapLookup->commit(); + + reply.payload(response); +} + /* */ void RESTAPI::restAPI_GetForceUpdate(const HTTPPayload& request, HTTPPayload& reply, const RequestMatch& match) diff --git a/src/fne/network/RESTAPI.h b/src/fne/network/RESTAPI.h index d18fcf9e..03a923de 100644 --- a/src/fne/network/RESTAPI.h +++ b/src/fne/network/RESTAPI.h @@ -4,7 +4,7 @@ * GPLv2 Open Source. Use is subject to license terms. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * - * Copyright (C) 2024 Bryan Biedenkapp, N2PLL + * Copyright (C) 2024-2025 Bryan Biedenkapp, N2PLL * */ /** @@ -20,6 +20,7 @@ #include "common/network/rest/RequestDispatcher.h" #include "common/network/rest/http/HTTPServer.h" #include "common/network/rest/http/SecureHTTPServer.h" +#include "common/lookups/AdjSiteMapLookup.h" #include "common/lookups/RadioIdLookup.h" #include "common/lookups/TalkgroupRulesLookup.h" #include "common/Thread.h" @@ -69,8 +70,10 @@ public: * @param ridLookup Radio ID Lookup Table Instance * @param tidLookup Talkgroup Rules Lookup Table Instance * @param peerListLookup Peer List Lookup Table Instance + * @param adjPeerMapLookup Adjacent Site Map Lookup Table Instance */ - void setLookups(::lookups::RadioIdLookup* ridLookup, ::lookups::TalkgroupRulesLookup* tidLookup, ::lookups::PeerListLookup* peerListLookup); + void setLookups(::lookups::RadioIdLookup* ridLookup, ::lookups::TalkgroupRulesLookup* tidLookup, + ::lookups::PeerListLookup* peerListLookup, ::lookups::AdjSiteMapLookup* adjPeerMapLookup); /** * @brief Sets the instance of the FNE network. * @param network Instance oft he FNENetwork class. @@ -110,6 +113,7 @@ private: ::lookups::RadioIdLookup* m_ridLookup; ::lookups::TalkgroupRulesLookup* m_tidLookup; ::lookups::PeerListLookup* m_peerListLookup; + ::lookups::AdjSiteMapLookup* m_adjSiteMapLookup; typedef std::unordered_map::value_type AuthTokenValueType; std::unordered_map m_authTokens; @@ -270,6 +274,35 @@ private: */ void restAPI_GetPeerCommit(const HTTPPayload& request, HTTPPayload& reply, const network::rest::RequestMatch& match); + /** + * @brief REST API endpoint; implements get adjacent site map list query request. + * @param request HTTP request. + * @param reply HTTP reply. + * @param match HTTP request matcher. + */ + void restAPI_GetAdjMapList(const HTTPPayload& request, HTTPPayload& reply, const network::rest::RequestMatch& match); + /** + * @brief REST API endpoint; implements put adjacent site map add request. + * @param request HTTP request. + * @param reply HTTP reply. + * @param match HTTP request matcher. + */ + void restAPI_PutAdjMapAdd(const HTTPPayload& request, HTTPPayload& reply, const network::rest::RequestMatch& match); + /** + * @brief REST API endpoint; implements put adjacent site map delete request. + * @param request HTTP request. + * @param reply HTTP reply. + * @param match HTTP request matcher. + */ + void restAPI_PutAdjMapDelete(const HTTPPayload& request, HTTPPayload& reply, const network::rest::RequestMatch& match); + /** + * @brief REST API endpoint; implements put adjacent site map commit request. + * @param request HTTP request. + * @param reply HTTP reply. + * @param match HTTP request matcher. + */ + void restAPI_GetAdjMapCommit(const HTTPPayload& request, HTTPPayload& reply, const network::rest::RequestMatch& match); + /** * @brief * @param request HTTP request. diff --git a/src/fne/network/RESTDefines.h b/src/fne/network/RESTDefines.h index 95208b5c..66fe955b 100644 --- a/src/fne/network/RESTDefines.h +++ b/src/fne/network/RESTDefines.h @@ -4,7 +4,7 @@ * GPLv2 Open Source. Use is subject to license terms. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * - * Copyright (C) 2024 Bryan Biedenkapp, N2PLL + * Copyright (C) 2024-2025 Bryan Biedenkapp, N2PLL * */ /** @@ -44,6 +44,11 @@ #define FNE_PUT_PEER_DELETE "/peer/delete" #define FNE_GET_PEER_COMMIT "/peer/commit" +#define FNE_GET_ADJ_MAP_LIST "/adjmap/list" +#define FNE_PUT_ADJ_MAP_ADD "/adjmap/add" +#define FNE_PUT_ADJ_MAP_DELETE "/adjmap/delete" +#define FNE_GET_ADJ_MAP_COMMIT "/adjmap/commit" + #define FNE_GET_FORCE_UPDATE "/force-update" #define FNE_GET_RELOAD_TGS "/reload-tgs" diff --git a/src/fne/network/callhandler/TagAnalogData.cpp b/src/fne/network/callhandler/TagAnalogData.cpp new file mode 100644 index 00000000..c809ab17 --- /dev/null +++ b/src/fne/network/callhandler/TagAnalogData.cpp @@ -0,0 +1,710 @@ +// 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 "fne/Defines.h" +#include "common/analog/AnalogDefines.h" +#include "common/analog/data/NetData.h" +#include "common/Clock.h" +#include "common/Log.h" +#include "common/Utils.h" +#include "network/FNENetwork.h" +#include "network/callhandler/TagAnalogData.h" +#include "HostFNE.h" + +using namespace system_clock; +using namespace network; +using namespace network::callhandler; +using namespace network::callhandler::packetdata; +using namespace analog; +using namespace analog::defines; + +#include +#include + +// --------------------------------------------------------------------------- +// Constants +// --------------------------------------------------------------------------- + +const uint32_t CALL_COLL_TIMEOUT = 10U; + +// --------------------------------------------------------------------------- +// Public Class Members +// --------------------------------------------------------------------------- + +/* Initializes a new instance of the TagAnalogData class. */ + +TagAnalogData::TagAnalogData(FNENetwork* network, bool debug) : + m_network(network), + m_parrotFrames(), + m_parrotFramesReady(false), + m_status(), + m_debug(debug) +{ + assert(network != nullptr); +} + +/* Finalizes a instance of the TagAnalogData class. */ + +TagAnalogData::~TagAnalogData() = default; + +/* Process a data frame from the network. */ + +bool TagAnalogData::processFrame(const uint8_t* data, uint32_t len, uint32_t peerId, uint32_t ssrc, uint16_t pktSeq, uint32_t streamId, bool external) +{ + hrc::hrc_t pktTime = hrc::now(); + + DECLARE_UINT8_ARRAY(buffer, len); + ::memcpy(buffer, data, len); + + uint8_t seqNo = data[4U]; + + uint32_t srcId = GET_UINT24(data, 5U); + uint32_t dstId = GET_UINT24(data, 8U); + + bool individual = (data[15] & 0x40U) == 0x40U; + + AudioFrameType::E frameType = (AudioFrameType::E)(data[15U] & 0x0FU); + + data::NetData analogData; + analogData.setSeqNo(seqNo); + analogData.setSrcId(srcId); + analogData.setDstId(dstId); + analogData.setFrameType(frameType); + + analogData.setAudio(data + 20U); + + uint8_t frame[AUDIO_SAMPLES_LENGTH_BYTES]; + analogData.getAudio(frame); + + // perform TGID route rewrites if configured + routeRewrite(buffer, peerId, dstId, false); + dstId = GET_UINT24(buffer, 8U); + + // is the stream valid? + if (validate(peerId, analogData, streamId)) { + // is this peer ignored? + if (!isPeerPermitted(peerId, analogData, streamId, external)) { + return false; + } + + // is this the end of the call stream? + if (frameType == AudioFrameType::TERMINATOR) { + if (srcId == 0U && dstId == 0U) { + LogWarning(LOG_NET, "Analog, invalid TERMINATOR, peer = %u, ssrc = %u, srcId = %u, dstId = %u, slot = %u, streamId = %u, external = %u", peerId, ssrc, srcId, dstId, streamId, external); + return false; + } + + RxStatus status = m_status[dstId]; + uint64_t duration = hrc::diff(pktTime, status.callStartTime); + + auto it = std::find_if(m_status.begin(), m_status.end(), [&](StatusMapPair x) { + if (x.second.dstId == dstId) { + if (x.second.activeCall) + return true; + } + return false; + }); + if (it != m_status.end()) { + m_status[dstId].reset(); + + // is this a parrot talkgroup? if so, clear any remaining frames from the buffer + lookups::TalkgroupRuleGroupVoice tg = m_network->m_tidLookup->find(dstId); + if (tg.config().parrot()) { + if (m_parrotFrames.size() > 0) { + m_parrotFramesReady = true; + LogMessage(LOG_NET, "Analog, Parrot Playback will Start, peer = %u, ssrc = %u, srcId = %u", peerId, ssrc, srcId); + m_network->m_parrotDelayTimer.start(); + } + } + + LogMessage(LOG_NET, "Analog, Call End, peer = %u, ssrc = %u, srcId = %u, dstId = %u, duration = %u, streamId = %u, external = %u", + peerId, ssrc, srcId, dstId, duration / 1000, streamId, external); + + // report call event to InfluxDB + if (m_network->m_enableInfluxDB) { + influxdb::QueryBuilder() + .meas("call_event") + .tag("peerId", std::to_string(peerId)) + .tag("mode", "Analog") + .tag("streamId", std::to_string(streamId)) + .tag("srcId", std::to_string(srcId)) + .tag("dstId", std::to_string(dstId)) + .field("duration", duration) + .timestamp(std::chrono::duration_cast(std::chrono::system_clock::now().time_since_epoch()).count()) + .requestAsync(m_network->m_influxServer); + } + + m_network->eraseStreamPktSeq(peerId, streamId); + m_network->m_callInProgress = false; + } + } + + // is this a new call stream? + if (frameType == AudioFrameType::VOICE_START) { + if (srcId == 0U && dstId == 0U) { + LogWarning(LOG_NET, "Analog, invalid call, peer = %u, ssrc = %u, srcId = %u, dstId = %u, streamId = %u, external = %u", peerId, ssrc, srcId, dstId, streamId, external); + return false; + } + + auto it = std::find_if(m_status.begin(), m_status.end(), [&](StatusMapPair x) { + if (x.second.dstId == dstId) { + if (x.second.activeCall) + return true; + } + return false; + }); + if (it != m_status.end()) { + RxStatus status = it->second; + if (streamId != status.streamId) { + if (status.srcId != 0U && status.srcId != srcId) { + uint64_t lastPktDuration = hrc::diff(hrc::now(), status.lastPacket); + if ((lastPktDuration / 1000) > CALL_COLL_TIMEOUT) { + LogWarning(LOG_NET, "Analog, Call Collision, lasted more then %us with no further updates, forcibly ending call"); + m_status[dstId].reset(); + m_network->m_callInProgress = false; + } + + LogWarning(LOG_NET, "Analog, Call Collision, peer = %u, ssrc = %u, srcId = %u, dstId = %u, streamId = %u, rxPeer = %u, rxSrcId = %u, rxDstId = %u, rxStreamId = %u, external = %u", + peerId, ssrc, srcId, dstId, streamId, status.peerId, status.srcId, status.dstId, status.streamId, external); + return false; + } + } + } + else { + // is this a parrot talkgroup? if so, clear any remaining frames from the buffer + lookups::TalkgroupRuleGroupVoice tg = m_network->m_tidLookup->find(dstId); + if (tg.config().parrot()) { + m_parrotFramesReady = false; + if (m_parrotFrames.size() > 0) { + for (auto& pkt : m_parrotFrames) { + if (pkt.buffer != nullptr) { + delete[] pkt.buffer; + } + } + m_parrotFrames.clear(); + } + } + + // this is a new call stream + // bryanb: this could be problematic and is naive, if a dstId appears on both slots (which shouldn't happen) + m_status[dstId].callStartTime = pktTime; + m_status[dstId].srcId = srcId; + m_status[dstId].dstId = dstId; + m_status[dstId].streamId = streamId; + m_status[dstId].peerId = peerId; + m_status[dstId].activeCall = true; + + LogMessage(LOG_NET, "Analog, Call Start, peer = %u, ssrc = %u, srcId = %u, dstId = %u, streamId = %u, external = %u", peerId, ssrc, srcId, dstId, streamId, external); + + m_network->m_callInProgress = true; + } + } + + // is this a parrot talkgroup? + lookups::TalkgroupRuleGroupVoice tg = m_network->m_tidLookup->find(dstId); + if (tg.config().parrot()) { + uint8_t* copy = new uint8_t[len]; + ::memcpy(copy, buffer, len); + + ParrotFrame parrotFrame = ParrotFrame(); + parrotFrame.buffer = copy; + parrotFrame.bufferLen = len; + + parrotFrame.pktSeq = pktSeq; + parrotFrame.streamId = streamId; + parrotFrame.peerId = peerId; + + parrotFrame.srcId = srcId; + parrotFrame.dstId = dstId; + + m_parrotFrames.push_back(parrotFrame); + + if (m_network->m_parrotOnlyOriginating) { + return true; // end here because parrot calls should never repeat anywhere + } + } + + m_status[dstId].lastPacket = hrc::now(); + + // repeat traffic to the connected peers + if (m_network->m_peers.size() > 0U) { + uint32_t i = 0U; + for (auto peer : m_network->m_peers) { + if (peerId != peer.first) { + if (ssrc == peer.first) { + // skip the peer if it is the source peer + continue; + } + + // is this peer ignored? + if (!isPeerPermitted(peer.first, analogData, streamId)) { + continue; + } + + // every 5 peers flush the queue + if (i % 5U == 0U) { + m_network->m_frameQueue->flushQueue(); + } + + DECLARE_UINT8_ARRAY(outboundPeerBuffer, len); + ::memcpy(outboundPeerBuffer, buffer, len); + + // perform TGID route rewrites if configured + routeRewrite(outboundPeerBuffer, peer.first, dstId); + + m_network->writePeer(peer.first, ssrc, { NET_FUNC::PROTOCOL, NET_SUBFUNC::PROTOCOL_SUBFUNC_ANALOG }, outboundPeerBuffer, len, pktSeq, streamId, true); + if (m_network->m_debug) { + LogDebug(LOG_NET, "Analog, ssrc = %u, srcPeer = %u, dstPeer = %u, seqNo = %u, srcId = %u, dstId = %u, len = %u, pktSeq = %u, stream = %u, external = %u", + ssrc, peerId, peer.first, seqNo, srcId, dstId, len, pktSeq, streamId, external); + } + + if (!m_network->m_callInProgress) + m_network->m_callInProgress = true; + i++; + } + } + m_network->m_frameQueue->flushQueue(); + } + + // repeat traffic to external peers + if (m_network->m_host->m_peerNetworks.size() > 0U && !tg.config().parrot()) { + for (auto peer : m_network->m_host->m_peerNetworks) { + uint32_t dstPeerId = peer.second->getPeerId(); + + // don't try to repeat traffic to the source peer...if this traffic + // is coming from a external peer + if (dstPeerId != peerId) { + if (ssrc == dstPeerId) { + // skip the peer if it is the source peer + continue; + } + + // is this peer ignored? + if (!isPeerPermitted(dstPeerId, analogData, streamId, true)) { + continue; + } + + // check if the source peer is blocked from sending to this peer + if (peer.second->checkBlockedPeer(peerId)) { + continue; + } + + // skip peer if it isn't enabled + if (!peer.second->isEnabled()) { + continue; + } + + DECLARE_UINT8_ARRAY(outboundPeerBuffer, len); + ::memcpy(outboundPeerBuffer, buffer, len); + + // perform TGID route rewrites if configured + routeRewrite(outboundPeerBuffer, dstPeerId, dstId); + + // are we a peer link? + if (peer.second->isPeerLink()) + peer.second->writeMaster({ NET_FUNC::PROTOCOL, NET_SUBFUNC::PROTOCOL_SUBFUNC_ANALOG }, outboundPeerBuffer, len, pktSeq, streamId, false, false, 0U, ssrc); + else + peer.second->writeMaster({ NET_FUNC::PROTOCOL, NET_SUBFUNC::PROTOCOL_SUBFUNC_ANALOG }, outboundPeerBuffer, len, pktSeq, streamId); + if (m_network->m_debug) { + LogDebug(LOG_NET, "Analog, ssrc = %u, srcPeer = %u, dstPeer = %u, seqNo = %u, srcId = %u, dstId = %u, len = %u, pktSeq = %u, stream = %u, external = %u", + ssrc, peerId, dstPeerId, seqNo, srcId, dstId, len, pktSeq, streamId, external); + } + + if (!m_network->m_callInProgress) + m_network->m_callInProgress = true; + } + } + } + + return true; + } + + return false; +} + +/* Helper to playback a parrot frame to the network. */ + +void TagAnalogData::playbackParrot() +{ + if (m_parrotFrames.size() == 0) { + m_parrotFramesReady = false; + return; + } + + auto& pkt = m_parrotFrames[0]; + if (pkt.buffer != nullptr) { + if (m_network->m_parrotOnlyOriginating) { + m_network->writePeer(pkt.peerId, pkt.peerId, { NET_FUNC::PROTOCOL, NET_SUBFUNC::PROTOCOL_SUBFUNC_ANALOG }, pkt.buffer, pkt.bufferLen, pkt.pktSeq, pkt.streamId, false); + if (m_network->m_debug) { + LogDebug(LOG_NET, "Analog, parrot, dstPeer = %u, len = %u, pktSeq = %u, streamId = %u", + pkt.peerId, pkt.bufferLen, pkt.pktSeq, pkt.streamId); + } + } + else { + // repeat traffic to the connected peers + for (auto peer : m_network->m_peers) { + m_network->writePeer(peer.first, pkt.peerId, { NET_FUNC::PROTOCOL, NET_SUBFUNC::PROTOCOL_SUBFUNC_ANALOG }, pkt.buffer, pkt.bufferLen, pkt.pktSeq, pkt.streamId, false); + if (m_network->m_debug) { + LogDebug(LOG_NET, "Analog, parrot, dstPeer = %u, len = %u, pktSeq = %u, streamId = %u", + peer.first, pkt.bufferLen, pkt.pktSeq, pkt.streamId); + } + } + } + + delete[] pkt.buffer; + } + Thread::sleep(60); + m_parrotFrames.pop_front(); +} + +// --------------------------------------------------------------------------- +// Private Class Members +// --------------------------------------------------------------------------- + +/* Helper to route rewrite the network data buffer. */ + +void TagAnalogData::routeRewrite(uint8_t* buffer, uint32_t peerId, uint32_t dstId, bool outbound) +{ + uint32_t rewriteDstId = dstId; + + // does the data require route writing? + if (peerRewrite(peerId, rewriteDstId, outbound)) { + // rewrite destination TGID in the frame + SET_UINT24(rewriteDstId, buffer, 8U); + } +} + +/* Helper to route rewrite destination ID and slot. */ + +bool TagAnalogData::peerRewrite(uint32_t peerId, uint32_t& dstId, bool outbound) +{ + lookups::TalkgroupRuleGroupVoice tg; + if (outbound) { + tg = m_network->m_tidLookup->find(dstId); + } + else { + tg = m_network->m_tidLookup->findByRewrite(peerId, dstId); + } + + bool rewrote = false; + if (tg.config().rewriteSize() > 0) { + std::vector rewrites = tg.config().rewrite(); + for (auto entry : rewrites) { + if (entry.peerId() == peerId) { + if (outbound) { + dstId = entry.tgId(); + } + else { + dstId = tg.source().tgId(); + } + rewrote = true; + break; + } + } + } + + return rewrote; +} + +/* Helper to determine if the peer is permitted for traffic. */ + +bool TagAnalogData::isPeerPermitted(uint32_t peerId, data::NetData& data, uint32_t streamId, bool external) +{ + if (!data.getGroup()) { + if (m_network->m_disallowU2U) + return false; + if (!m_network->checkU2UDroppedPeer(peerId)) + return true; + return false; + } + + FNEPeerConnection* connection = nullptr; // bryanb: this is a possible null ref concurrency issue + // it is possible if the timing is just right to get a valid + // connection back initially, and then for it to be deleted + if (peerId > 0 && (m_network->m_peers.find(peerId) != m_network->m_peers.end())) { + connection = m_network->m_peers[peerId]; + } + + // is this peer a Peer-Link peer? + if (connection != nullptr) { + if (connection->isPeerLink()) { + return true; // Peer Link peers are *always* allowed to receive traffic and no other rules may filter + // these peers + } + } + + // is this a group call? + if (data.getGroup()) { + lookups::TalkgroupRuleGroupVoice tg = m_network->m_tidLookup->find(data.getDstId()); + + std::vector inclusion = tg.config().inclusion(); + std::vector exclusion = tg.config().exclusion(); + + // peer inclusion lists take priority over exclusion lists + if (inclusion.size() > 0) { + auto it = std::find(inclusion.begin(), inclusion.end(), peerId); + if (it == inclusion.end()) { + return false; + } + } + else { + if (exclusion.size() > 0) { + auto it = std::find(exclusion.begin(), exclusion.end(), peerId); + if (it != exclusion.end()) { + return false; + } + } + } + + // peer always send list takes priority over any following affiliation rules + std::vector alwaysSend = tg.config().alwaysSend(); + if (alwaysSend.size() > 0) { + auto it = std::find(alwaysSend.begin(), alwaysSend.end(), peerId); + if (it != alwaysSend.end()) { + return true; // skip any following checks and always send traffic + } + } + + // is this peer a conventional peer? + if (m_network->m_allowConvSiteAffOverride) { + if (connection != nullptr) { + if (connection->isConventionalPeer()) { + external = true; // we'll just set the external flag to disable the affiliation check + // for conventional peers + } + } + } + + // is this peer a SysView peer? + if (connection != nullptr) { + if (connection->isSysView()) { + external = true; // we'll just set the external flag to disable the affiliation check + // for SysView peers + } + } + + // is this a TG that requires affiliations to repeat? + // NOTE: external peers *always* repeat traffic regardless of affiliation + if (tg.config().affiliated() && !external) { + uint32_t lookupPeerId = peerId; + if (connection != nullptr) { + if (connection->ccPeerId() > 0U) + lookupPeerId = connection->ccPeerId(); + } + + // check the affiliations for this peer to see if we can repeat traffic + lookups::AffiliationLookup* aff = m_network->m_peerAffiliations[lookupPeerId]; + if (aff == nullptr) { + std::string peerIdentity = m_network->resolvePeerIdentity(lookupPeerId); + //LogError(LOG_NET, "PEER %u (%s) has an invalid affiliations lookup? This shouldn't happen BUGBUG.", lookupPeerId, peerIdentity.c_str()); + return false; // this will cause no traffic to pass for this peer now...I'm not sure this is good behavior + } + else { + if (!aff->hasGroupAff(data.getDstId())) { + return false; + } + } + } + } + + return true; +} + +/* Helper to validate the analog call stream. */ + +bool TagAnalogData::validate(uint32_t peerId, data::NetData& data, uint32_t streamId) +{ + // is the source ID a blacklisted ID? + bool rejectUnknownBadCall = false; + lookups::RadioId rid = m_network->m_ridLookup->find(data.getSrcId()); + if (!rid.radioDefault()) { + if (!rid.radioEnabled()) { + // report error event to InfluxDB + if (m_network->m_enableInfluxDB) { + influxdb::QueryBuilder() + .meas("call_error_event") + .tag("peerId", std::to_string(peerId)) + .tag("streamId", std::to_string(streamId)) + .tag("srcId", std::to_string(data.getSrcId())) + .tag("dstId", std::to_string(data.getDstId())) + .field("message", std::string(INFLUXDB_ERRSTR_DISABLED_SRC_RID)) + .timestamp(std::chrono::duration_cast(std::chrono::system_clock::now().time_since_epoch()).count()) + .requestAsync(m_network->m_influxServer); + } + + // report In-Call Control to the peer sending traffic + m_network->writePeerICC(peerId, streamId, NET_SUBFUNC::PROTOCOL_SUBFUNC_ANALOG, NET_ICC::REJECT_TRAFFIC, data.getDstId()); + return false; + } + } + else { + // if this is a default radio -- and we are rejecting undefined radios + // report call error + if (m_network->m_rejectUnknownRID) { + rejectUnknownBadCall = true; + } + } + + // always validate a terminator if the source is valid + if (data.getFrameType() == AudioFrameType::TERMINATOR) + return true; + + // is this a private call? + if (!data.getGroup()) { + // is the destination ID a blacklisted ID? + lookups::RadioId rid = m_network->m_ridLookup->find(data.getDstId()); + if (!rid.radioDefault()) { + if (!rid.radioEnabled()) { + // report error event to InfluxDB + if (m_network->m_enableInfluxDB) { + influxdb::QueryBuilder() + .meas("call_error_event") + .tag("peerId", std::to_string(peerId)) + .tag("streamId", std::to_string(streamId)) + .tag("srcId", std::to_string(data.getSrcId())) + .tag("dstId", std::to_string(data.getDstId())) + .field("message", std::string(INFLUXDB_ERRSTR_DISABLED_DST_RID)) + .timestamp(std::chrono::duration_cast(std::chrono::system_clock::now().time_since_epoch()).count()) + .requestAsync(m_network->m_influxServer); + } + + return false; + } + } + else { + // if this is a default radio -- and we are rejecting undefined radios + // report call error + if (m_network->m_rejectUnknownRID) { + // report error event to InfluxDB + if (m_network->m_enableInfluxDB) { + influxdb::QueryBuilder() + .meas("call_error_event") + .tag("peerId", std::to_string(peerId)) + .tag("streamId", std::to_string(streamId)) + .tag("srcId", std::to_string(data.getSrcId())) + .tag("dstId", std::to_string(data.getDstId())) + .field("message", std::string(INFLUXDB_ERRSTR_DISABLED_SRC_RID)) + .timestamp(std::chrono::duration_cast(std::chrono::system_clock::now().time_since_epoch()).count()) + .requestAsync(m_network->m_influxServer); + } + + LogWarning(LOG_NET, "Analog, illegal/unknown RID attempted access, srcId = %u, dstId = %u", data.getSrcId(), data.getDstId()); + + // report In-Call Control to the peer sending traffic + m_network->writePeerICC(peerId, streamId, NET_SUBFUNC::PROTOCOL_SUBFUNC_ANALOG, NET_ICC::REJECT_TRAFFIC, data.getDstId()); + return false; + } + } + } + + // is this a group call? + if (data.getGroup()) { + lookups::TalkgroupRuleGroupVoice tg = m_network->m_tidLookup->find(data.getDstId()); + if (tg.isInvalid()) { + // report error event to InfluxDB + if (m_network->m_enableInfluxDB) { + influxdb::QueryBuilder() + .meas("call_error_event") + .tag("peerId", std::to_string(peerId)) + .tag("streamId", std::to_string(streamId)) + .tag("srcId", std::to_string(data.getSrcId())) + .tag("dstId", std::to_string(data.getDstId())) + .field("message", std::string(INFLUXDB_ERRSTR_INV_TALKGROUP)) + .timestamp(std::chrono::duration_cast(std::chrono::system_clock::now().time_since_epoch()).count()) + .requestAsync(m_network->m_influxServer); + } + + // report In-Call Control to the peer sending traffic + m_network->writePeerICC(peerId, streamId, NET_SUBFUNC::PROTOCOL_SUBFUNC_ANALOG, NET_ICC::REJECT_TRAFFIC, data.getDstId()); + return false; + } + + // peer always send list takes priority over any following affiliation rules + bool isAlwaysPeer = false; + std::vector alwaysSend = tg.config().alwaysSend(); + if (alwaysSend.size() > 0) { + auto it = std::find(alwaysSend.begin(), alwaysSend.end(), peerId); + if (it != alwaysSend.end()) { + isAlwaysPeer = true; // skip any following checks and always send traffic + rejectUnknownBadCall = false; + } + } + + // fail call if the reject flag is set + if (rejectUnknownBadCall) { + // report error event to InfluxDB + if (m_network->m_enableInfluxDB) { + influxdb::QueryBuilder() + .meas("call_error_event") + .tag("peerId", std::to_string(peerId)) + .tag("streamId", std::to_string(streamId)) + .tag("srcId", std::to_string(data.getSrcId())) + .tag("dstId", std::to_string(data.getDstId())) + .field("message", std::string(INFLUXDB_ERRSTR_DISABLED_SRC_RID)) + .timestamp(std::chrono::duration_cast(std::chrono::system_clock::now().time_since_epoch()).count()) + .requestAsync(m_network->m_influxServer); + } + + LogWarning(LOG_NET, "Analog, illegal/unknown RID attempted access, srcId = %u, dstId = %u", data.getSrcId(), data.getDstId()); + + // report In-Call Control to the peer sending traffic + m_network->writePeerICC(peerId, streamId, NET_SUBFUNC::PROTOCOL_SUBFUNC_ANALOG, NET_ICC::REJECT_TRAFFIC, data.getDstId()); + return false; + } + + // is the TGID active? + if (!tg.config().active()) { + // report error event to InfluxDB + if (m_network->m_enableInfluxDB) { + influxdb::QueryBuilder() + .meas("call_error_event") + .tag("peerId", std::to_string(peerId)) + .tag("streamId", std::to_string(streamId)) + .tag("srcId", std::to_string(data.getSrcId())) + .tag("dstId", std::to_string(data.getDstId())) + .field("message", std::string(INFLUXDB_ERRSTR_DISABLED_TALKGROUP)) + .timestamp(std::chrono::duration_cast(std::chrono::system_clock::now().time_since_epoch()).count()) + .requestAsync(m_network->m_influxServer); + } + + // report In-Call Control to the peer sending traffic + m_network->writePeerICC(peerId, streamId, NET_SUBFUNC::PROTOCOL_SUBFUNC_ANALOG, NET_ICC::REJECT_TRAFFIC, data.getDstId()); + return false; + } + + // always peers can violate the rules...hurray + if (!isAlwaysPeer) { + // does the TGID have a permitted RID list? + if (tg.config().permittedRIDs().size() > 0) { + // does the transmitting RID have permission? + std::vector permittedRIDs = tg.config().permittedRIDs(); + if (std::find(permittedRIDs.begin(), permittedRIDs.end(), data.getSrcId()) == permittedRIDs.end()) { + // report error event to InfluxDB + if (m_network->m_enableInfluxDB) { + influxdb::QueryBuilder() + .meas("call_error_event") + .tag("peerId", std::to_string(peerId)) + .tag("streamId", std::to_string(streamId)) + .tag("srcId", std::to_string(data.getSrcId())) + .tag("dstId", std::to_string(data.getDstId())) + .field("message", std::string(INFLUXDB_ERRSTR_RID_NOT_PERMITTED)) + .timestamp(std::chrono::duration_cast(std::chrono::system_clock::now().time_since_epoch()).count()) + .requestAsync(m_network->m_influxServer); + } + + // report In-Call Control to the peer sending traffic + m_network->writePeerICC(peerId, streamId, NET_SUBFUNC::PROTOCOL_SUBFUNC_ANALOG, NET_ICC::REJECT_TRAFFIC, data.getDstId()); + return false; + } + } + } + } + + return true; +} diff --git a/src/fne/network/callhandler/TagAnalogData.h b/src/fne/network/callhandler/TagAnalogData.h new file mode 100644 index 00000000..c7ae6624 --- /dev/null +++ b/src/fne/network/callhandler/TagAnalogData.h @@ -0,0 +1,196 @@ +// 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 + * + */ +/** + * @file TagAnalogData.h + * @ingroup fne_callhandler + * @file TagAnalogData.cpp + * @ingroup fne_callhandler + */ +#if !defined(__CALLHANDLER__TAG_ANALOG_DATA_H__) +#define __CALLHANDLER__TAG_ANALOG_DATA_H__ + +#include "fne/Defines.h" +#include "common/concurrent/deque.h" +#include "common/concurrent/unordered_map.h" +#include "common/dmr/DMRDefines.h" +#include "common/dmr/data/NetData.h" +#include "common/dmr/lc/CSBK.h" +#include "common/Clock.h" +#include "network/FNENetwork.h" +#include "network/callhandler/packetdata/DMRPacketData.h" + +namespace network +{ + namespace callhandler + { + // --------------------------------------------------------------------------- + // Class Declaration + // --------------------------------------------------------------------------- + + /** + * @brief Implements the analog call handler and data FNE 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 debug Flag indicating whether network debug is enabled. + */ + TagAnalogData(FNENetwork* network, bool debug); + /** + * @brief Finalizes a instance of the TagAnalogData class. + */ + ~TagAnalogData(); + + /** + * @brief Process a data frame from the network. + * @param data Network data buffer. + * @param len Length of data. + * @param peerId Peer ID. + * @param ssrc RTP Synchronization Source ID. + * @param pktSeq RTP packet sequence. + * @param streamId Stream ID. + * @param external Flag indicating traffic is from an external peer. + * @returns bool True, if frame is processed, otherwise false. + */ + bool processFrame(const uint8_t* data, uint32_t len, uint32_t peerId, uint32_t ssrc, uint16_t pktSeq, uint32_t streamId, bool external = false); + + /** + * @brief Helper to playback a parrot frame to the network. + */ + void playbackParrot(); + /** + * @brief Helper to determine if there are stored parrot frames. + * @returns True, if there are queued parrot frames to playback, otherwise false. + */ + bool hasParrotFrames() const { return m_parrotFramesReady && !m_parrotFrames.empty(); } + + private: + FNENetwork* m_network; + + /** + * @brief Represents a stored parrot frame. + */ + class ParrotFrame { + public: + uint8_t* buffer; + uint32_t bufferLen; + + /** + * @brief RTP Packet Sequence. + */ + uint16_t pktSeq; + /** + * @brief Call Stream ID. + */ + uint32_t streamId; + /** + * @brief Peer ID. + */ + uint32_t peerId; + + /** + * @brief Source ID. + */ + uint32_t srcId; + /** + * @brief Destination ID. + */ + uint32_t dstId; + }; + concurrent::deque m_parrotFrames; + bool m_parrotFramesReady; + + /** + * @brief Represents the receive status of a call. + */ + class RxStatus { + public: + system_clock::hrc::hrc_t callStartTime; + system_clock::hrc::hrc_t lastPacket; + /** + * @brief Source ID. + */ + uint32_t srcId; + /** + * @brief Destination ID. + */ + uint32_t dstId; + /** + * @brief Call Stream ID. + */ + uint32_t streamId; + /** + * @brief Peer ID. + */ + uint32_t peerId; + /** + * @brief Flag indicating this call is active with traffic currently in progress. + */ + bool activeCall; + + /** + * @brief Helper to reset call status. + */ + void reset() + { + srcId = 0U; + dstId = 0U; + streamId = 0U; + peerId = 0U; + activeCall = false; + } + }; + typedef std::pair StatusMapPair; + concurrent::unordered_map m_status; + + bool m_debug; + + /** + * @brief Helper to route rewrite the network data buffer. + * @param buffer Frame buffer. + * @param peerId Peer ID. + * @param dstId Destination ID. + * @param outbound Flag indicating whether or not this is outbound traffic. + */ + void routeRewrite(uint8_t* buffer, uint32_t peerId, uint32_t dstId, bool outbound = true); + /** + * @brief Helper to route rewrite destination ID and slot. + * @param peerId Peer ID. + * @param dstId Destination ID. + * @param outbound Flag indicating whether or not this is outbound traffic. + * @returns bool True, if rewritten successfully, otherwise false. + */ + bool peerRewrite(uint32_t peerId, uint32_t& dstId, bool outbound = true); + + /** + * @brief Helper to determine if the peer is permitted for traffic. + * @param peerId Peer ID. + * @param data Instance of data::NetData Analog data container class. + * @param streamId Stream ID. + * @param external Flag indicating this traffic came from an external peer. + * @returns bool True, if valid, otherwise false. + */ + bool isPeerPermitted(uint32_t peerId, analog::data::NetData& data, uint32_t streamId, bool external = false); + /** + * @brief Helper to validate the DMR call stream. + * @param peerId Peer ID. + * @param data Instance of data::NetData Analog data container class. + * @param streamId Stream ID. + * @returns bool True, if valid, otherwise false. + */ + bool validate(uint32_t peerId, analog::data::NetData& data, uint32_t streamId); + }; + } // namespace callhandler +} // namespace network + +#endif // __CALLHANDLER__TAG_ANALOG_DATA_H__ diff --git a/src/fne/network/callhandler/TagDMRData.cpp b/src/fne/network/callhandler/TagDMRData.cpp index e0064539..87e0c793 100644 --- a/src/fne/network/callhandler/TagDMRData.cpp +++ b/src/fne/network/callhandler/TagDMRData.cpp @@ -47,6 +47,7 @@ TagDMRData::TagDMRData(FNENetwork* network, bool debug) : m_parrotFrames(), m_parrotFramesReady(false), m_status(), + m_statusPVCall(), m_debug(debug) { assert(network != nullptr); @@ -63,7 +64,7 @@ TagDMRData::~TagDMRData() /* Process a data frame from the network. */ -bool TagDMRData::processFrame(const uint8_t* data, uint32_t len, uint32_t peerId, uint16_t pktSeq, uint32_t streamId, bool external) +bool TagDMRData::processFrame(const uint8_t* data, uint32_t len, uint32_t peerId, uint32_t ssrc, uint16_t pktSeq, uint32_t streamId, bool external) { hrc::hrc_t pktTime = hrc::now(); @@ -120,12 +121,18 @@ bool TagDMRData::processFrame(const uint8_t* data, uint32_t len, uint32_t peerId uint8_t frame[DMR_FRAME_LENGTH_BYTES]; dmrData.getData(frame); + // process a CSBK out into a class literal if possible + std::unique_ptr csbk; + if (dataSync && (dataType == DataType::CSBK)) { + csbk = lc::csbk::CSBKFactory::createCSBK(frame, dataType); + } + // perform TGID route rewrites if configured routeRewrite(buffer, peerId, dmrData, dataType, dstId, slotNo, false); dstId = GET_UINT24(buffer, 8U); // is the stream valid? - if (validate(peerId, dmrData, streamId)) { + if (validate(peerId, dmrData, csbk.get(), streamId)) { // is this peer ignored? if (!isPeerPermitted(peerId, dmrData, streamId, external)) { return false; @@ -134,7 +141,7 @@ bool TagDMRData::processFrame(const uint8_t* data, uint32_t len, uint32_t peerId // is this the end of the call stream? if (dataSync && (dataType == DataType::TERMINATOR_WITH_LC)) { if (srcId == 0U && dstId == 0U) { - LogWarning(LOG_NET, "DMR, invalid TERMINATOR, peer = %u, srcId = %u, dstId = %u, slot = %u, streamId = %u, external = %u", peerId, srcId, dstId, slotNo, streamId, external); + LogWarning(LOG_NET, "DMR, invalid TERMINATOR, peer = %u, ssrc = %u, srcId = %u, dstId = %u, slot = %u, streamId = %u, external = %u", peerId, ssrc, srcId, dstId, slotNo, streamId, external); return false; } @@ -147,8 +154,8 @@ bool TagDMRData::processFrame(const uint8_t* data, uint32_t len, uint32_t peerId return false; }); if (it == m_status.end()) { - LogError(LOG_NET, "DMR, tried to end call for non-existent call in progress?, peer = %u, srcId = %u, dstId = %u, slot = %u, streamId = %u, external = %u", - peerId, srcId, dstId, slotNo, streamId, external); + LogError(LOG_NET, "DMR, tried to end call for non-existent call in progress?, peer = %u, ssrc = %u, srcId = %u, dstId = %u, slot = %u, streamId = %u, external = %u", + peerId, ssrc, srcId, dstId, slotNo, streamId, external); } else { status = it->second; @@ -172,13 +179,27 @@ bool TagDMRData::processFrame(const uint8_t* data, uint32_t len, uint32_t peerId if (tg.config().parrot()) { if (m_parrotFrames.size() > 0) { m_parrotFramesReady = true; - LogMessage(LOG_NET, "DMR, Parrot Playback will Start, peer = %u, srcId = %u", peerId, srcId); + LogMessage(LOG_NET, "DMR, Parrot Playback will Start, peer = %u, ssrc = %u, srcId = %u", peerId, ssrc, srcId); m_network->m_parrotDelayTimer.start(); } } - LogMessage(LOG_NET, "DMR, Call End, peer = %u, srcId = %u, dstId = %u, slot = %u, duration = %u, streamId = %u, external = %u", - peerId, srcId, dstId, slotNo, duration / 1000, streamId, external); + // is this a private call? + auto it = std::find_if(m_statusPVCall.begin(), m_statusPVCall.end(), [&](StatusMapPair x) { + if (x.second.dstId == dstId) { + if (x.second.activeCall) + return true; + } + return false; + }); + if (it != m_statusPVCall.end()) { + m_statusPVCall[dstId].reset(); + LogMessage(LOG_NET, "DMR, Private Call End, peer = %u, ssrc = %u, srcId = %u, dstId = %u, slot = %u, duration = %u, streamId = %u, external = %u", + peerId, ssrc, srcId, dstId, slotNo, duration / 1000, streamId, external); + } + else + LogMessage(LOG_NET, "DMR, Call End, peer = %u, ssrc = %u, srcId = %u, dstId = %u, slot = %u, duration = %u, streamId = %u, external = %u", + peerId, ssrc, srcId, dstId, slotNo, duration / 1000, streamId, external); // report call event to InfluxDB if (m_network->m_enableInfluxDB) { @@ -203,10 +224,12 @@ bool TagDMRData::processFrame(const uint8_t* data, uint32_t len, uint32_t peerId // is this a new call stream? if (dataSync && (dataType == DataType::VOICE_LC_HEADER)) { if (srcId == 0U && dstId == 0U) { - LogWarning(LOG_NET, "DMR, invalid call, peer = %u, srcId = %u, dstId = %u, streamId = %u, external = %u", peerId, srcId, dstId, streamId, external); + LogWarning(LOG_NET, "DMR, invalid call, peer = %u, ssrc = %u, srcId = %u, dstId = %u, streamId = %u, external = %u", peerId, ssrc, srcId, dstId, streamId, external); return false; } + bool switchOver = (data[14U] & network::NET_CTRL_SWITCH_OVER) == network::NET_CTRL_SWITCH_OVER; + auto it = std::find_if(m_status.begin(), m_status.end(), [&](StatusMapPair x) { if (x.second.dstId == dstId && x.second.slotNo == slotNo) { if (x.second.activeCall) @@ -217,6 +240,15 @@ bool TagDMRData::processFrame(const uint8_t* data, uint32_t len, uint32_t peerId if (it != m_status.end()) { RxStatus status = it->second; if (streamId != status.streamId) { + // perform TG switch over -- this can happen in special conditions where a TG may rapidly switch + // from one source to another (primarily from bridge resources) + if (switchOver && status.slotNo == slotNo) { + status.streamId = streamId; + status.srcId = srcId; + LogMessage(LOG_NET, "DMR, Call Source Switched, peer = %u, ssrc = %u, srcId = %u, dstId = %u, slotNo = %u, streamId = %u, rxPeer = %u, rxSrcId = %u, rxDstId = %u, rxSlotNo = %u, rxStreamId = %u, external = %u", + peerId, ssrc, srcId, dstId, slotNo, streamId, status.peerId, status.srcId, status.dstId, status.slotNo, status.streamId, external); + } + if (status.srcId != 0U && status.srcId != srcId) { uint64_t lastPktDuration = hrc::diff(hrc::now(), status.lastPacket); if ((lastPktDuration / 1000) > CALL_COLL_TIMEOUT) { @@ -225,8 +257,8 @@ bool TagDMRData::processFrame(const uint8_t* data, uint32_t len, uint32_t peerId m_network->m_callInProgress = false; } - LogWarning(LOG_NET, "DMR, Call Collision, peer = %u, srcId = %u, dstId = %u, slotNo = %u, streamId = %u, rxPeer = %u, rxSrcId = %u, rxDstId = %u, rxSlotNo = %u, rxStreamId = %u, external = %u", - peerId, srcId, dstId, slotNo, streamId, status.peerId, status.srcId, status.dstId, status.slotNo, status.streamId, external); + LogWarning(LOG_NET, "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, external = %u", + peerId, ssrc, srcId, dstId, slotNo, streamId, status.peerId, status.srcId, status.dstId, status.slotNo, status.streamId, external); return false; } } @@ -256,7 +288,25 @@ bool TagDMRData::processFrame(const uint8_t* data, uint32_t len, uint32_t peerId m_status[dstId].peerId = peerId; m_status[dstId].activeCall = true; - LogMessage(LOG_NET, "DMR, Call Start, peer = %u, srcId = %u, dstId = %u, streamId = %u, external = %u", peerId, srcId, dstId, streamId, external); + // is this a private call? + if (flco == FLCO::PRIVATE) { + m_statusPVCall[dstId].callStartTime = pktTime; + m_statusPVCall[dstId].srcId = srcId; + m_statusPVCall[dstId].dstId = dstId; + m_statusPVCall[dstId].slotNo = slotNo; + m_statusPVCall[dstId].streamId = streamId; + m_statusPVCall[dstId].peerId = peerId; + m_statusPVCall[dstId].activeCall = true; + + // find the SSRC of the peer that registered this unit + uint32_t regSSRC = m_network->findPeerUnitReg(srcId); + m_statusPVCall[dstId].dstPeerId = regSSRC; + + LogMessage(LOG_NET, "DMR, Private Call Start, peer = %u, ssrc = %u, srcId = %u, dstId = %u, streamId = %u, external = %u", + peerId, ssrc, srcId, dstId, streamId, external); + } + else + LogMessage(LOG_NET, "DMR, Call Start, peer = %u, ssrc = %u, srcId = %u, dstId = %u, streamId = %u, external = %u", peerId, ssrc, srcId, dstId, streamId, external); m_network->m_callInProgress = true; } @@ -295,11 +345,81 @@ bool TagDMRData::processFrame(const uint8_t* data, uint32_t len, uint32_t peerId m_status[dstId].lastPacket = hrc::now(); + bool noConnectedPeerRepeat = false; + bool privateCallInProgress = false; + + // is this a private call in-progress? + if (m_network->m_restrictPVCallToRegOnly) { + if (flco == FLCO::PRIVATE) { + privateCallInProgress = true; + } + + if (privateCallInProgress) { + // if we've not determined the destination peer, we have to repeat it everywhere + if (m_statusPVCall[dstId].dstPeerId == 0U) { + noConnectedPeerRepeat = false; + privateCallInProgress = false; // trick the system to repeat everywhere + } else { + // if this is a private call, check if the destination peer is one directly connected to us, if not + // flag the call so it only repeats to external peers + if (m_network->m_peers.size() > 0U && !noConnectedPeerRepeat) { + noConnectedPeerRepeat = true; + for (auto peer : m_network->m_peers) { + if (peerId != peer.first) { + FNEPeerConnection* conn = peer.second; + if (conn != nullptr) { + if (conn->isExternalPeer()) { + continue; + } + } + + if (m_statusPVCall[dstId].dstPeerId == peer.first) { + noConnectedPeerRepeat = false; + break; + } + } + } + } + } + } + } + // repeat traffic to the connected peers - if (m_network->m_peers.size() > 0U) { + if (m_network->m_peers.size() > 0U && !noConnectedPeerRepeat) { uint32_t i = 0U; for (auto peer : m_network->m_peers) { if (peerId != peer.first) { + FNEPeerConnection* conn = peer.second; + if (ssrc == peer.first) { + // skip the peer if it is the source peer + continue; + } + + if (m_network->m_restrictPVCallToRegOnly) { + // is this peer an external peer? + bool external = false; + if (conn != nullptr) { + external = conn->isExternalPeer(); + } + + // is this a private call? + if ((flco == FLCO::PRIVATE) && !external) { + // is this a private call? if so only repeat to the peer that registered the unit + auto it = std::find_if(m_statusPVCall.begin(), m_statusPVCall.end(), [&](StatusMapPair x) { + if (x.second.dstId == dstId) { + if (x.second.activeCall) + return true; + } + return false; + }); + if (it != m_statusPVCall.end()) { + if (peer.first != m_statusPVCall[dstId].dstPeerId) { + continue; + } + } + } + } + // is this peer ignored? if (!isPeerPermitted(peer.first, dmrData, streamId)) { continue; @@ -316,10 +436,10 @@ bool TagDMRData::processFrame(const uint8_t* data, uint32_t len, uint32_t peerId // perform TGID route rewrites if configured routeRewrite(outboundPeerBuffer, peer.first, dmrData, dataType, dstId, slotNo); - m_network->writePeer(peer.first, { NET_FUNC::PROTOCOL, NET_SUBFUNC::PROTOCOL_SUBFUNC_DMR }, outboundPeerBuffer, len, pktSeq, streamId, true); + m_network->writePeer(peer.first, ssrc, { NET_FUNC::PROTOCOL, NET_SUBFUNC::PROTOCOL_SUBFUNC_DMR }, outboundPeerBuffer, len, pktSeq, streamId, true); if (m_network->m_debug) { - LogDebug(LOG_NET, "DMR, srcPeer = %u, dstPeer = %u, seqNo = %u, srcId = %u, dstId = %u, flco = $%02X, slotNo = %u, len = %u, pktSeq = %u, stream = %u, external = %u", - peerId, peer.first, seqNo, srcId, dstId, flco, slotNo, len, pktSeq, streamId, external); + LogDebug(LOG_NET, "DMR, ssrc = %u, srcPeer = %u, dstPeer = %u, seqNo = %u, srcId = %u, dstId = %u, flco = $%02X, slotNo = %u, len = %u, pktSeq = %u, stream = %u, external = %u", + ssrc, peerId, peer.first, seqNo, srcId, dstId, flco, slotNo, len, pktSeq, streamId, external); } if (!m_network->m_callInProgress) @@ -330,6 +450,12 @@ bool TagDMRData::processFrame(const uint8_t* data, uint32_t len, uint32_t peerId m_network->m_frameQueue->flushQueue(); } + // if this is a private call, and we have already repeated to the connected peer that registered + // the unit, don't repeat to any external peers + if (privateCallInProgress && !noConnectedPeerRepeat) { + return true; + } + // repeat traffic to external peers if (m_network->m_host->m_peerNetworks.size() > 0U && !tg.config().parrot()) { for (auto peer : m_network->m_host->m_peerNetworks) { @@ -338,6 +464,11 @@ bool TagDMRData::processFrame(const uint8_t* data, uint32_t len, uint32_t peerId // don't try to repeat traffic to the source peer...if this traffic // is coming from a external peer if (dstPeerId != peerId) { + if (ssrc == dstPeerId) { + // skip the peer if it is the source peer + continue; + } + // is this peer ignored? if (!isPeerPermitted(dstPeerId, dmrData, streamId, true)) { continue; @@ -359,10 +490,14 @@ bool TagDMRData::processFrame(const uint8_t* data, uint32_t len, uint32_t peerId // perform TGID route rewrites if configured routeRewrite(outboundPeerBuffer, dstPeerId, dmrData, dataType, dstId, slotNo); - peer.second->writeMaster({ NET_FUNC::PROTOCOL, NET_SUBFUNC::PROTOCOL_SUBFUNC_DMR }, outboundPeerBuffer, len, pktSeq, streamId); + // are we a peer link? + if (peer.second->isPeerLink()) + peer.second->writeMaster({ NET_FUNC::PROTOCOL, NET_SUBFUNC::PROTOCOL_SUBFUNC_DMR }, outboundPeerBuffer, len, pktSeq, streamId, false, false, 0U, ssrc); + else + peer.second->writeMaster({ NET_FUNC::PROTOCOL, NET_SUBFUNC::PROTOCOL_SUBFUNC_DMR }, outboundPeerBuffer, len, pktSeq, streamId); if (m_network->m_debug) { - LogDebug(LOG_NET, "DMR, srcPeer = %u, dstPeer = %u, seqNo = %u, srcId = %u, dstId = %u, flco = $%02X, slotNo = %u, len = %u, pktSeq = %u, stream = %u, external = %u", - peerId, dstPeerId, seqNo, srcId, dstId, flco, slotNo, len, pktSeq, streamId, external); + LogDebug(LOG_NET, "DMR, ssrc = %u, srcPeer = %u, dstPeer = %u, seqNo = %u, srcId = %u, dstId = %u, flco = $%02X, slotNo = %u, len = %u, pktSeq = %u, stream = %u, external = %u", + ssrc, peerId, dstPeerId, seqNo, srcId, dstId, flco, slotNo, len, pktSeq, streamId, external); } if (!m_network->m_callInProgress) @@ -436,7 +571,7 @@ void TagDMRData::playbackParrot() auto& pkt = m_parrotFrames[0]; if (pkt.buffer != nullptr) { if (m_network->m_parrotOnlyOriginating) { - m_network->writePeer(pkt.peerId, { NET_FUNC::PROTOCOL, NET_SUBFUNC::PROTOCOL_SUBFUNC_DMR }, pkt.buffer, pkt.bufferLen, pkt.pktSeq, pkt.streamId, false); + m_network->writePeer(pkt.peerId, pkt.peerId, { NET_FUNC::PROTOCOL, NET_SUBFUNC::PROTOCOL_SUBFUNC_DMR }, pkt.buffer, pkt.bufferLen, pkt.pktSeq, pkt.streamId, false); if (m_network->m_debug) { LogDebug(LOG_NET, "DMR, parrot, dstPeer = %u, len = %u, pktSeq = %u, streamId = %u", pkt.peerId, pkt.bufferLen, pkt.pktSeq, pkt.streamId); @@ -445,7 +580,7 @@ void TagDMRData::playbackParrot() else { // repeat traffic to the connected peers for (auto peer : m_network->m_peers) { - m_network->writePeer(peer.first, { NET_FUNC::PROTOCOL, NET_SUBFUNC::PROTOCOL_SUBFUNC_DMR }, pkt.buffer, pkt.bufferLen, pkt.pktSeq, pkt.streamId, false); + m_network->writePeer(peer.first, pkt.peerId, { NET_FUNC::PROTOCOL, NET_SUBFUNC::PROTOCOL_SUBFUNC_DMR }, pkt.buffer, pkt.bufferLen, pkt.pktSeq, pkt.streamId, false); if (m_network->m_debug) { LogDebug(LOG_NET, "DMR, parrot, dstPeer = %u, len = %u, pktSeq = %u, streamId = %u", peer.first, pkt.bufferLen, pkt.pktSeq, pkt.streamId); @@ -752,7 +887,7 @@ bool TagDMRData::isPeerPermitted(uint32_t peerId, data::NetData& data, uint32_t /* Helper to validate the DMR call stream. */ -bool TagDMRData::validate(uint32_t peerId, data::NetData& data, uint32_t streamId) +bool TagDMRData::validate(uint32_t peerId, data::NetData& data, lc::CSBK* csbk, uint32_t streamId) { // is the source ID a blacklisted ID? bool rejectUnknownBadCall = false; @@ -773,6 +908,9 @@ bool TagDMRData::validate(uint32_t peerId, data::NetData& data, uint32_t streamI .requestAsync(m_network->m_influxServer); } + if (m_network->m_logDenials) + LogError(LOG_NET, "DMR Slot %u, " INFLUXDB_ERRSTR_DISABLED_SRC_RID ", peer = %u, srcId = %u, dstId = %u", data.getSlotNo(), peerId, data.getSrcId(), data.getDstId()); + // report In-Call Control to the peer sending traffic m_network->writePeerICC(peerId, streamId, NET_SUBFUNC::PROTOCOL_SUBFUNC_DMR, NET_ICC::REJECT_TRAFFIC, data.getDstId(), data.getSlotNo()); return false; @@ -790,6 +928,67 @@ bool TagDMRData::validate(uint32_t peerId, data::NetData& data, uint32_t streamI if (data.getDataType() == DataType::TERMINATOR_WITH_LC) return true; + // always validate a CSBK if the source is valid + if (data.getDataType() == DataType::CSBK) { + if (rejectUnknownBadCall) + return false; + + if (csbk != nullptr) { + // handle standard DMR reference opcodes + switch (csbk->getCSBKO()) { + case CSBKO::PV_GRANT: + { + // is the destination ID a blacklisted ID? + lookups::RadioId rid = m_network->m_ridLookup->find(data.getDstId()); + if (!rid.radioDefault()) { + if (!rid.radioEnabled()) { + return false; + } + } + } + break; + case CSBKO::TV_GRANT: + { + lookups::TalkgroupRuleGroupVoice tg = m_network->m_tidLookup->find(csbk->getDstId()); + + // check TGID validity + if (tg.isInvalid()) { + return false; + } + + if (!tg.config().active()) { + return false; + } + } + break; + case CSBKO::EXT_FNCT: + { + if (csbk->getFID() == FID_MOT) { + const lc::csbk::CSBK_EXT_FNCT* iosp = static_cast(csbk); + if (iosp != nullptr) { + lookups::PeerId pid = m_network->m_peerListLookup->find(peerId); + uint32_t func = iosp->getExtendedFunction(); + switch (func) { + case ExtendedFunctions::INHIBIT: + case ExtendedFunctions::UNINHIBIT: + { + if (!pid.peerDefault() && !pid.canIssueInhibit()) { + LogWarning(LOG_NET, "DMR, PEER %u attempted inhibit/unhibit, not authorized", peerId); + return false; + } + } + break; + } + } + } + } + break; + } + } + + return true; + } + // is this a private call? if (data.getFLCO() == FLCO::PRIVATE) { // is the destination ID a blacklisted ID? @@ -810,6 +1009,9 @@ bool TagDMRData::validate(uint32_t peerId, data::NetData& data, uint32_t streamI .requestAsync(m_network->m_influxServer); } + if (m_network->m_logDenials) + LogError(LOG_NET, "DMR Slot %u, " INFLUXDB_ERRSTR_DISABLED_DST_RID ", peer = %u, srcId = %u, dstId = %u", data.getSlotNo(), peerId, data.getSrcId(), data.getDstId()); + return false; } } @@ -825,13 +1027,14 @@ bool TagDMRData::validate(uint32_t peerId, data::NetData& data, uint32_t streamI .tag("streamId", std::to_string(streamId)) .tag("srcId", std::to_string(data.getSrcId())) .tag("dstId", std::to_string(data.getDstId())) - .field("message", std::string(INFLUXDB_ERRSTR_DISABLED_SRC_RID)) + .field("message", std::string(INFLUXDB_ERRSTR_ILLEGAL_RID_ACCESS)) .field("slot", std::to_string(data.getSlotNo())) .timestamp(std::chrono::duration_cast(std::chrono::system_clock::now().time_since_epoch()).count()) .requestAsync(m_network->m_influxServer); } - LogWarning(LOG_NET, "DMR slot %s, illegal/unknown RID attempted access, srcId = %u, dstId = %u", data.getSlotNo(), data.getSrcId(), data.getDstId()); + if (m_network->m_logDenials) + LogWarning(LOG_NET, "DMR slot %s, " INFLUXDB_ERRSTR_ILLEGAL_RID_ACCESS ", srcId = %u, dstId = %u", data.getSlotNo(), data.getSrcId(), data.getDstId()); // report In-Call Control to the peer sending traffic m_network->writePeerICC(peerId, streamId, NET_SUBFUNC::PROTOCOL_SUBFUNC_DMR, NET_ICC::REJECT_TRAFFIC, data.getDstId(), data.getSlotNo()); @@ -858,6 +1061,9 @@ bool TagDMRData::validate(uint32_t peerId, data::NetData& data, uint32_t streamI .requestAsync(m_network->m_influxServer); } + if (m_network->m_logDenials) + LogError(LOG_NET, "DMR Slot %u, " INFLUXDB_ERRSTR_INV_TALKGROUP ", peer = %u, srcId = %u, dstId = %u", data.getSlotNo(), peerId, data.getSrcId(), data.getDstId()); + // report In-Call Control to the peer sending traffic m_network->writePeerICC(peerId, streamId, NET_SUBFUNC::PROTOCOL_SUBFUNC_DMR, NET_ICC::REJECT_TRAFFIC, data.getDstId(), data.getSlotNo()); return false; @@ -884,13 +1090,14 @@ bool TagDMRData::validate(uint32_t peerId, data::NetData& data, uint32_t streamI .tag("streamId", std::to_string(streamId)) .tag("srcId", std::to_string(data.getSrcId())) .tag("dstId", std::to_string(data.getDstId())) - .field("message", std::string(INFLUXDB_ERRSTR_DISABLED_SRC_RID)) + .field("message", std::string(INFLUXDB_ERRSTR_ILLEGAL_RID_ACCESS)) .field("slot", std::to_string(data.getSlotNo())) .timestamp(std::chrono::duration_cast(std::chrono::system_clock::now().time_since_epoch()).count()) .requestAsync(m_network->m_influxServer); } - LogWarning(LOG_NET, "DMR slot %s, illegal/unknown RID attempted access, srcId = %u, dstId = %u", data.getSlotNo(), data.getSrcId(), data.getDstId()); + if (m_network->m_logDenials) + LogWarning(LOG_NET, "DMR slot %s, " INFLUXDB_ERRSTR_ILLEGAL_RID_ACCESS ", srcId = %u, dstId = %u", data.getSlotNo(), data.getSrcId(), data.getDstId()); // report In-Call Control to the peer sending traffic m_network->writePeerICC(peerId, streamId, NET_SUBFUNC::PROTOCOL_SUBFUNC_DMR, NET_ICC::REJECT_TRAFFIC, data.getDstId(), data.getSlotNo()); @@ -913,6 +1120,9 @@ bool TagDMRData::validate(uint32_t peerId, data::NetData& data, uint32_t streamI .requestAsync(m_network->m_influxServer); } + if (m_network->m_logDenials) + LogError(LOG_NET, "DMR Slot %u, " INFLUXDB_ERRSTR_INV_SLOT ", peer = %u, srcId = %u, dstId = %u", data.getSlotNo(), peerId, data.getSrcId(), data.getDstId()); + // report In-Call Control to the peer sending traffic m_network->writePeerICC(peerId, streamId, NET_SUBFUNC::PROTOCOL_SUBFUNC_DMR, NET_ICC::REJECT_TRAFFIC, data.getDstId(), data.getSlotNo()); return false; @@ -934,6 +1144,9 @@ bool TagDMRData::validate(uint32_t peerId, data::NetData& data, uint32_t streamI .requestAsync(m_network->m_influxServer); } + if (m_network->m_logDenials) + LogError(LOG_NET, "DMR Slot %u, " INFLUXDB_ERRSTR_DISABLED_TALKGROUP ", peer = %u, srcId = %u, dstId = %u", data.getSlotNo(), peerId, data.getSrcId(), data.getDstId()); + // report In-Call Control to the peer sending traffic m_network->writePeerICC(peerId, streamId, NET_SUBFUNC::PROTOCOL_SUBFUNC_DMR, NET_ICC::REJECT_TRAFFIC, data.getDstId(), data.getSlotNo()); return false; @@ -959,6 +1172,9 @@ bool TagDMRData::validate(uint32_t peerId, data::NetData& data, uint32_t streamI .requestAsync(m_network->m_influxServer); } + if (m_network->m_logDenials) + LogError(LOG_NET, "DMR Slot %u, " INFLUXDB_ERRSTR_RID_NOT_PERMITTED ", peer = %u, srcId = %u, dstId = %u", data.getSlotNo(), peerId, data.getSrcId(), data.getDstId()); + // report In-Call Control to the peer sending traffic m_network->writePeerICC(peerId, streamId, NET_SUBFUNC::PROTOCOL_SUBFUNC_DMR, NET_ICC::REJECT_TRAFFIC, data.getDstId(), data.getSlotNo()); return false; @@ -1038,7 +1254,7 @@ bool TagDMRData::write_CSBK_Grant(uint32_t peerId, uint32_t srcId, uint32_t dstI /* Helper to write a NACK RSP packet. */ -void TagDMRData::write_CSBK_NACK_RSP(uint32_t peerId, uint32_t dstId, uint8_t reason, uint8_t service) +void TagDMRData::write_CSBK_NACK_RSP(uint32_t peerId, uint32_t dstId, uint8_t slot, uint8_t reason, uint8_t service) { std::unique_ptr csbk = std::make_unique(); csbk->setServiceKind(service); @@ -1046,7 +1262,13 @@ void TagDMRData::write_CSBK_NACK_RSP(uint32_t peerId, uint32_t dstId, uint8_t re csbk->setSrcId(WUID_ALL); // hmmm... csbk->setDstId(dstId); - write_CSBK(peerId, 1U, csbk.get()); + if (m_network->m_verbose) { + LogMessage(LOG_DMR, "DMR Slot %u, CSBK, %s, reason = $%02X (%s), srcId = %u, dstId = %u", + slot, csbk->toString().c_str(), reason, DMRUtils::rsnToString(reason).c_str(), + csbk->getSrcId(), csbk->getDstId()); + } + + write_CSBK(peerId, slot, csbk.get()); } /* Helper to write a network CSBK. */ @@ -1090,7 +1312,7 @@ void TagDMRData::write_CSBK(uint32_t peerId, uint8_t slot, lc::CSBK* csbk) } if (peerId > 0U) { - m_network->writePeer(peerId, { NET_FUNC::PROTOCOL, NET_SUBFUNC::PROTOCOL_SUBFUNC_DMR }, message.get(), messageLength, RTP_END_OF_CALL_SEQ, streamId, false); + m_network->writePeer(peerId, m_network->m_peerId, { NET_FUNC::PROTOCOL, NET_SUBFUNC::PROTOCOL_SUBFUNC_DMR }, message.get(), messageLength, RTP_END_OF_CALL_SEQ, streamId, false); } else { // repeat traffic to the connected peers if (m_network->m_peers.size() > 0U) { @@ -1101,7 +1323,7 @@ void TagDMRData::write_CSBK(uint32_t peerId, uint8_t slot, lc::CSBK* csbk) m_network->m_frameQueue->flushQueue(); } - m_network->writePeer(peer.first, { NET_FUNC::PROTOCOL, NET_SUBFUNC::PROTOCOL_SUBFUNC_DMR }, message.get(), messageLength, RTP_END_OF_CALL_SEQ, streamId, true); + m_network->writePeer(peer.first, m_network->m_peerId, { NET_FUNC::PROTOCOL, NET_SUBFUNC::PROTOCOL_SUBFUNC_DMR }, message.get(), messageLength, RTP_END_OF_CALL_SEQ, streamId, true); if (m_network->m_debug) { LogDebug(LOG_NET, "DMR, peer = %u, slotNo = %u, len = %u, stream = %u", peer.first, slot, messageLength, streamId); diff --git a/src/fne/network/callhandler/TagDMRData.h b/src/fne/network/callhandler/TagDMRData.h index 9a20d8cb..c1313b2e 100644 --- a/src/fne/network/callhandler/TagDMRData.h +++ b/src/fne/network/callhandler/TagDMRData.h @@ -8,9 +8,9 @@ * */ /** - * @file TagNXDNData.h + * @file TagDMRData.h * @ingroup fne_callhandler - * @file TagNXDNData.cpp + * @file TagDMRData.cpp * @ingroup fne_callhandler */ #if !defined(__CALLHANDLER__TAG_DMR_DATA_H__) @@ -56,12 +56,13 @@ namespace network * @param data Network data buffer. * @param len Length of data. * @param peerId Peer ID. + * @param ssrc RTP Synchronization Source ID. * @param pktSeq RTP packet sequence. * @param streamId Stream ID. * @param external Flag indicating traffic is from an external peer. * @returns bool True, if frame is processed, otherwise false. */ - bool processFrame(const uint8_t* data, uint32_t len, uint32_t peerId, uint16_t pktSeq, uint32_t streamId, bool external = false); + bool processFrame(const uint8_t* data, uint32_t len, uint32_t peerId, uint32_t ssrc, uint16_t pktSeq, uint32_t streamId, bool external = false); /** * @brief Process a grant request frame from the network. * @param srcId Source Radio ID. @@ -177,6 +178,10 @@ namespace network * @brief Peer ID. */ uint32_t peerId; + /** + * @brief Destination Peer ID. + */ + uint32_t dstPeerId; /** * @brief Flag indicating this call is active with traffic currently in progress. */ @@ -197,6 +202,7 @@ namespace network }; typedef std::pair StatusMapPair; concurrent::unordered_map m_status; + concurrent::unordered_map m_statusPVCall; friend class packetdata::DMRPacketData; packetdata::DMRPacketData* m_packetData; @@ -236,7 +242,7 @@ namespace network /** * @brief Helper to determine if the peer is permitted for traffic. * @param peerId Peer ID. - * @param dmrData Instance of data::NetData DMR data container class. + * @param data Instance of data::NetData DMR data container class. * @param streamId Stream ID. * @param external Flag indicating this traffic came from an external peer. * @returns bool True, if valid, otherwise false. @@ -245,11 +251,12 @@ namespace network /** * @brief Helper to validate the DMR call stream. * @param peerId Peer ID. - * @param dmrData Instance of data::NetData DMR data container class. + * @param data Instance of data::NetData DMR data container class. + * @param[in] csbk Instance of dmr::lc::CSBK. * @param streamId Stream ID. * @returns bool True, if valid, otherwise false. */ - bool validate(uint32_t peerId, dmr::data::NetData& data, uint32_t streamId); + bool validate(uint32_t peerId, dmr::data::NetData& data, dmr::lc::CSBK* csbk, uint32_t streamId); /** * @brief Helper to write a grant packet. @@ -266,9 +273,10 @@ namespace network * @param peerId Peer ID. * @param dstId Destination ID. * @param reason Denial Reason. + * @param slot DMR slot number. * @param service Service being denied. */ - void write_CSBK_NACK_RSP(uint32_t peerId, uint32_t dstId, uint8_t reason, uint8_t service); + void write_CSBK_NACK_RSP(uint32_t peerId, uint32_t dstId, uint8_t slot, uint8_t reason, uint8_t service); /** * @brief Helper to write a network CSBK. diff --git a/src/fne/network/callhandler/TagNXDNData.cpp b/src/fne/network/callhandler/TagNXDNData.cpp index 3efc82f9..cfd7a9ae 100644 --- a/src/fne/network/callhandler/TagNXDNData.cpp +++ b/src/fne/network/callhandler/TagNXDNData.cpp @@ -11,6 +11,8 @@ #include "common/nxdn/NXDNDefines.h" #include "common/nxdn/channel/LICH.h" #include "common/nxdn/channel/CAC.h" +#include "common/nxdn/channel/FACCH1.h" +#include "common/nxdn/channel/UDCH.h" #include "common/nxdn/lc/rcch/RCCHFactory.h" #include "common/nxdn/lc/RTCH.h" #include "common/nxdn/NXDNUtils.h" @@ -48,7 +50,8 @@ TagNXDNData::TagNXDNData(FNENetwork* network, bool debug) : m_parrotFrames(), m_parrotFramesReady(false), m_status(), - m_debug(debug) + m_statusPVCall(), + m_debug(debug) { assert(network != nullptr); } @@ -59,7 +62,7 @@ TagNXDNData::~TagNXDNData() = default; /* Process a data frame from the network. */ -bool TagNXDNData::processFrame(const uint8_t* data, uint32_t len, uint32_t peerId, uint16_t pktSeq, uint32_t streamId, bool external) +bool TagNXDNData::processFrame(const uint8_t* data, uint32_t len, uint32_t peerId, uint32_t ssrc, uint16_t pktSeq, uint32_t streamId, bool external) { hrc::hrc_t pktTime = hrc::now(); @@ -90,6 +93,95 @@ bool TagNXDNData::processFrame(const uint8_t* data, uint32_t len, uint32_t peerI bool group = (data[15U] & 0x40U) == 0x40U ? false : true; lc.setGroup(group); + // process raw NXDN data bytes + UInt8Array frame; + uint8_t frameLength = buffer[23U]; + if (frameLength <= 24) { + frame = std::unique_ptr(new uint8_t[frameLength]); + ::memset(frame.get(), 0x00U, frameLength); + } + else { + frame = std::unique_ptr(new uint8_t[frameLength]); + ::memset(frame.get(), 0x00U, frameLength); + ::memcpy(frame.get(), buffer + 24U, frameLength); + } + + NXDNUtils::scrambler(frame.get() + 2U); + + channel::LICH lich; + bool valid = lich.decode(frame.get() + 2U); + + std::unique_ptr rcch; + if (valid) { + RFChannelType::E rfct = lich.getRFCT(); + FuncChannelType::E fct = lich.getFCT(); + ChOption::E option = lich.getOption(); + + if (rfct == RFChannelType::RCCH) { + rcch = lc::rcch::RCCHFactory::createRCCH(frame.get(), frameLength); + } + else if (rfct == RFChannelType::RTCH || rfct == RFChannelType::RDCH) { + // forward onto the specific processor for final processing and delivery + switch (fct) { + case FuncChannelType::USC_UDCH: + { + channel::UDCH udch; + bool validUDCH = udch.decode(data + 2U); + if (validUDCH) { + // The layer3 data will only be correct if valid is true + uint8_t buffer[NXDN_RTCH_LC_LENGTH_BYTES]; + udch.getData(buffer); + + lc.decode(buffer, NXDN_UDCH_LENGTH_BITS); + } + } + break; + default: + { + if (fct == FuncChannelType::USC_SACCH_NS) { + // the SACCH on a non-superblock frame is usually an idle and not interesting apart from the RAN. + channel::FACCH1 facch; + bool valid = facch.decode(frame.get() + 2U, NXDN_FSW_LENGTH_BITS + NXDN_LICH_LENGTH_BITS + NXDN_SACCH_FEC_LENGTH_BITS); + if (!valid) + valid = facch.decode(frame.get() + 2U, NXDN_FSW_LENGTH_BITS + NXDN_LICH_LENGTH_BITS + NXDN_SACCH_FEC_LENGTH_BITS + NXDN_FACCH1_FEC_LENGTH_BITS); + if (valid) { + uint8_t buffer[10U]; + facch.getData(buffer); + + lc.decode(buffer, NXDN_FACCH1_FEC_LENGTH_BITS); + } + } else { + channel::FACCH1 facch; + bool valid = false; + switch (option) { + case ChOption::STEAL_FACCH: + valid = facch.decode(frame.get() + 2U, NXDN_FSW_LENGTH_BITS + NXDN_LICH_LENGTH_BITS + NXDN_SACCH_FEC_LENGTH_BITS); + if (!valid) + valid = facch.decode(frame.get() + 2U, NXDN_FSW_LENGTH_BITS + NXDN_LICH_LENGTH_BITS + NXDN_SACCH_FEC_LENGTH_BITS + NXDN_FACCH1_FEC_LENGTH_BITS); + break; + case ChOption::STEAL_FACCH1_1: + valid = facch.decode(frame.get() + 2U, NXDN_FSW_LENGTH_BITS + NXDN_LICH_LENGTH_BITS + NXDN_SACCH_FEC_LENGTH_BITS); + break; + case ChOption::STEAL_FACCH1_2: + valid = facch.decode(frame.get() + 2U, NXDN_FSW_LENGTH_BITS + NXDN_LICH_LENGTH_BITS + NXDN_SACCH_FEC_LENGTH_BITS + NXDN_FACCH1_FEC_LENGTH_BITS); + break; + default: + break; + } + + if (valid) { + uint8_t buffer[10U]; + facch.getData(buffer); + + lc.decode(buffer, NXDN_FACCH1_FEC_LENGTH_BITS); + } + } + } + break; + } + } + } + // is the stream valid? if (validate(peerId, lc, messageType, streamId)) { // is this peer ignored? @@ -104,7 +196,7 @@ bool TagNXDNData::processFrame(const uint8_t* data, uint32_t len, uint32_t peerI // is this the end of the call stream? if (messageType == MessageType::RTCH_TX_REL || messageType == MessageType::RTCH_TX_REL_EX) { if (srcId == 0U && dstId == 0U) { - LogWarning(LOG_NET, "NXDN, invalid TX_REL, peer = %u, srcId = %u, dstId = %u, streamId = %u, external = %u", peerId, srcId, dstId, streamId, external); + LogWarning(LOG_NET, "NXDN, invalid TX_REL, peer = %u, ssrc = %u, srcId = %u, dstId = %u, streamId = %u, external = %u", peerId, ssrc, srcId, dstId, streamId, external); return false; } @@ -131,8 +223,22 @@ bool TagNXDNData::processFrame(const uint8_t* data, uint32_t len, uint32_t peerI } } - LogMessage(LOG_NET, "NXDN, Call End, peer = %u, srcId = %u, dstId = %u, duration = %u, streamId = %u, external = %u", - peerId, srcId, dstId, duration / 1000, streamId, external); + // is this a private call? + auto it = std::find_if(m_statusPVCall.begin(), m_statusPVCall.end(), [&](StatusMapPair x) { + if (x.second.dstId == dstId) { + if (x.second.activeCall) + return true; + } + return false; + }); + if (it != m_statusPVCall.end()) { + m_statusPVCall[dstId].reset(); + LogMessage(LOG_NET, "NXDN, Private Call End, peer = %u, ssrc = %u, srcId = %u, dstId = %u, duration = %u, streamId = %u, external = %u", + peerId, ssrc, srcId, dstId, duration / 1000, streamId, external); + } + else + LogMessage(LOG_NET, "NXDN, Call End, peer = %u, ssrc = %u, srcId = %u, dstId = %u, duration = %u, streamId = %u, external = %u", + peerId, ssrc, srcId, dstId, duration / 1000, streamId, external); // report call event to InfluxDB if (m_network->m_enableInfluxDB) { @@ -156,10 +262,12 @@ bool TagNXDNData::processFrame(const uint8_t* data, uint32_t len, uint32_t peerI // is this a new call stream? if ((messageType != MessageType::RTCH_TX_REL && messageType != MessageType::RTCH_TX_REL_EX)) { if (srcId == 0U && dstId == 0U) { - LogWarning(LOG_NET, "NXDN, invalid call, peer = %u, srcId = %u, dstId = %u, streamId = %u, external = %u", peerId, srcId, dstId, streamId, external); + LogWarning(LOG_NET, "NXDN, invalid call, peer = %u, ssrc = %u, srcId = %u, dstId = %u, streamId = %u, external = %u", peerId, ssrc, srcId, dstId, streamId, external); return false; } + bool switchOver = (data[14U] & network::NET_CTRL_SWITCH_OVER) == network::NET_CTRL_SWITCH_OVER; + auto it = std::find_if(m_status.begin(), m_status.end(), [&](StatusMapPair x) { if (x.second.dstId == dstId) { if (x.second.activeCall) @@ -170,6 +278,15 @@ bool TagNXDNData::processFrame(const uint8_t* data, uint32_t len, uint32_t peerI if (it != m_status.end()) { RxStatus status = m_status[dstId]; if (streamId != status.streamId) { + // perform TG switch over -- this can happen in special conditions where a TG may rapidly switch + // from one source to another (primarily from bridge resources) + if (switchOver) { + status.streamId = streamId; + status.srcId = srcId; + LogMessage(LOG_NET, "NXDN, Call Source Switched, peer = %u, ssrc = %u, srcId = %u, dstId = %u, streamId = %u, rxPeer = %u, rxSrcId = %u, rxDstId = %u, rxStreamId = %u, external = %u", + peerId, ssrc, srcId, dstId, streamId, status.peerId, status.srcId, status.dstId, status.streamId, external); + } + if (status.srcId != 0U && status.srcId != srcId) { uint64_t lastPktDuration = hrc::diff(hrc::now(), status.lastPacket); if ((lastPktDuration / 1000) > CALL_COLL_TIMEOUT) { @@ -178,8 +295,8 @@ bool TagNXDNData::processFrame(const uint8_t* data, uint32_t len, uint32_t peerI m_network->m_callInProgress = false; } - LogWarning(LOG_NET, "NXDN, Call Collision, peer = %u, srcId = %u, dstId = %u, streamId = %u, rxPeer = %u, rxSrcId = %u, rxDstId = %u, rxStreamId = %u, external = %u", - peerId, srcId, dstId, streamId, status.peerId, status.srcId, status.dstId, status.streamId, external); + LogWarning(LOG_NET, "NXDN, Call Collision, peer = %u, ssrc = %u, srcId = %u, dstId = %u, streamId = %u, rxPeer = %u, rxSrcId = %u, rxDstId = %u, rxStreamId = %u, external = %u", + peerId, ssrc, srcId, dstId, streamId, status.peerId, status.srcId, status.dstId, status.streamId, external); return false; } } @@ -187,7 +304,7 @@ bool TagNXDNData::processFrame(const uint8_t* data, uint32_t len, uint32_t peerI else { // is this a parrot talkgroup? if so, clear any remaining frames from the buffer lookups::TalkgroupRuleGroupVoice tg = m_network->m_tidLookup->find(dstId); - if (tg.config().parrot()) { + if (tg.config().parrot()) { m_parrotFramesReady = false; if (m_parrotFrames.size() > 0) { for (auto& pkt : m_parrotFrames) { @@ -207,7 +324,25 @@ bool TagNXDNData::processFrame(const uint8_t* data, uint32_t len, uint32_t peerI m_status[dstId].peerId = peerId; m_status[dstId].activeCall = true; - LogMessage(LOG_NET, "NXDN, Call Start, peer = %u, srcId = %u, dstId = %u, streamId = %u, external = %u", peerId, srcId, dstId, streamId, external); + // is this a private call? + if (!group) { + m_statusPVCall[dstId].callStartTime = pktTime; + m_statusPVCall[dstId].srcId = srcId; + m_statusPVCall[dstId].dstId = dstId; + m_statusPVCall[dstId].streamId = streamId; + m_statusPVCall[dstId].peerId = peerId; + m_statusPVCall[dstId].activeCall = true; + + // find the SSRC of the peer that registered this unit + uint32_t regSSRC = m_network->findPeerUnitReg(srcId); + m_statusPVCall[dstId].dstPeerId = regSSRC; + + LogMessage(LOG_NET, "NXDN, Private Call Start, peer = %u, ssrc = %u, srcId = %u, dstId = %u, streamId = %u, external = %u", + peerId, ssrc, srcId, dstId, streamId, external); + } + else + LogMessage(LOG_NET, "NXDN, Call Start, peer = %u, ssrc = %u, srcId = %u, dstId = %u, streamId = %u, external = %u", + peerId, ssrc, srcId, dstId, streamId, external); m_network->m_callInProgress = true; } @@ -240,11 +375,81 @@ bool TagNXDNData::processFrame(const uint8_t* data, uint32_t len, uint32_t peerI m_status[dstId].lastPacket = hrc::now(); + bool noConnectedPeerRepeat = false; + bool privateCallInProgress = false; + + // is this a private call in-progress? + if (m_network->m_restrictPVCallToRegOnly) { + if (!group) { + privateCallInProgress = true; + } + + if (privateCallInProgress) { + // if we've not determined the destination peer, we have to repeat it everywhere + if (m_statusPVCall[dstId].dstPeerId == 0U) { + noConnectedPeerRepeat = false; + privateCallInProgress = false; // trick the system to repeat everywhere + } else { + // if this is a private call, check if the destination peer is one directly connected to us, if not + // flag the call so it only repeats to external peers + if (m_network->m_peers.size() > 0U && !noConnectedPeerRepeat) { + noConnectedPeerRepeat = true; + for (auto peer : m_network->m_peers) { + if (peerId != peer.first) { + FNEPeerConnection* conn = peer.second; + if (conn != nullptr) { + if (conn->isExternalPeer()) { + continue; + } + } + + if (m_statusPVCall[dstId].dstPeerId == peer.first) { + noConnectedPeerRepeat = false; + break; + } + } + } + } + } + } + } + // repeat traffic to the connected peers - if (m_network->m_peers.size() > 0U) { + if (m_network->m_peers.size() > 0U && !noConnectedPeerRepeat) { uint32_t i = 0U; for (auto peer : m_network->m_peers) { if (peerId != peer.first) { + FNEPeerConnection* conn = peer.second; + if (ssrc == peer.first) { + // skip the peer if it is the source peer + continue; + } + + if (m_network->m_restrictPVCallToRegOnly) { + // is this peer an external peer? + bool external = false; + if (conn != nullptr) { + external = conn->isExternalPeer(); + } + + // is this a private call? + if (!group && !external) { + // is this a private call? if so only repeat to the peer that registered the unit + auto it = std::find_if(m_statusPVCall.begin(), m_statusPVCall.end(), [&](StatusMapPair x) { + if (x.second.dstId == dstId) { + if (x.second.activeCall) + return true; + } + return false; + }); + if (it != m_statusPVCall.end()) { + if (peer.first != m_statusPVCall[dstId].dstPeerId) { + continue; + } + } + } + } + // is this peer ignored? if (!isPeerPermitted(peer.first, lc, messageType, streamId)) { continue; @@ -261,10 +466,10 @@ bool TagNXDNData::processFrame(const uint8_t* data, uint32_t len, uint32_t peerI // perform TGID route rewrites if configured routeRewrite(outboundPeerBuffer, peer.first, messageType, dstId); - m_network->writePeer(peer.first, { NET_FUNC::PROTOCOL, NET_SUBFUNC::PROTOCOL_SUBFUNC_NXDN }, outboundPeerBuffer, len, pktSeq, streamId, true); + m_network->writePeer(peer.first, ssrc, { NET_FUNC::PROTOCOL, NET_SUBFUNC::PROTOCOL_SUBFUNC_NXDN }, outboundPeerBuffer, len, pktSeq, streamId, true); if (m_network->m_debug) { - LogDebug(LOG_NET, "NXDN, srcPeer = %u, dstPeer = %u, messageType = $%02X, srcId = %u, dstId = %u, len = %u, pktSeq = %u, streamId = %u, external = %u", - peerId, peer.first, messageType, srcId, dstId, len, pktSeq, streamId, external); + LogDebug(LOG_NET, "NXDN, ssrc = %u, srcPeer = %u, dstPeer = %u, messageType = $%02X, srcId = %u, dstId = %u, len = %u, pktSeq = %u, streamId = %u, external = %u", + ssrc, peerId, peer.first, messageType, srcId, dstId, len, pktSeq, streamId, external); } if (!m_network->m_callInProgress) @@ -275,6 +480,12 @@ bool TagNXDNData::processFrame(const uint8_t* data, uint32_t len, uint32_t peerI m_network->m_frameQueue->flushQueue(); } + // if this is a private call, and we have already repeated to the connected peer that registered + // the unit, don't repeat to any external peers + if (privateCallInProgress && !noConnectedPeerRepeat) { + return true; + } + // repeat traffic to external peers if (m_network->m_host->m_peerNetworks.size() > 0U && !tg.config().parrot()) { for (auto peer : m_network->m_host->m_peerNetworks) { @@ -283,6 +494,11 @@ bool TagNXDNData::processFrame(const uint8_t* data, uint32_t len, uint32_t peerI // don't try to repeat traffic to the source peer...if this traffic // is coming from a external peer if (dstPeerId != peerId) { + if (ssrc == dstPeerId) { + // skip the peer if it is the source peer + continue; + } + // is this peer ignored? if (!isPeerPermitted(dstPeerId, lc, messageType, streamId, true)) { continue; @@ -304,10 +520,14 @@ bool TagNXDNData::processFrame(const uint8_t* data, uint32_t len, uint32_t peerI // perform TGID route rewrites if configured routeRewrite(outboundPeerBuffer, dstPeerId, messageType, dstId); - peer.second->writeMaster({ NET_FUNC::PROTOCOL, NET_SUBFUNC::PROTOCOL_SUBFUNC_NXDN }, outboundPeerBuffer, len, pktSeq, streamId); + // are we a peer link? + if (peer.second->isPeerLink()) + peer.second->writeMaster({ NET_FUNC::PROTOCOL, NET_SUBFUNC::PROTOCOL_SUBFUNC_NXDN }, outboundPeerBuffer, len, pktSeq, streamId, false, false, 0U, ssrc); + else + peer.second->writeMaster({ NET_FUNC::PROTOCOL, NET_SUBFUNC::PROTOCOL_SUBFUNC_NXDN }, outboundPeerBuffer, len, pktSeq, streamId); if (m_network->m_debug) { - LogDebug(LOG_NET, "NXDN, srcPeer = %u, dstPeer = %u, messageType = $%02X, srcId = %u, dstId = %u, len = %u, pktSeq = %u, streamId = %u, external = %u", - peerId, dstPeerId, messageType, srcId, dstId, len, pktSeq, streamId, external); + LogDebug(LOG_NET, "NXDN, ssrc = %u, srcPeer = %u, dstPeer = %u, messageType = $%02X, srcId = %u, dstId = %u, len = %u, pktSeq = %u, streamId = %u, external = %u", + ssrc, peerId, dstPeerId, messageType, srcId, dstId, len, pktSeq, streamId, external); } if (!m_network->m_callInProgress) @@ -381,7 +601,7 @@ void TagNXDNData::playbackParrot() auto& pkt = m_parrotFrames[0]; if (pkt.buffer != nullptr) { if (m_network->m_parrotOnlyOriginating) { - m_network->writePeer(pkt.peerId, { NET_FUNC::PROTOCOL, NET_SUBFUNC::PROTOCOL_SUBFUNC_NXDN }, pkt.buffer, pkt.bufferLen, pkt.pktSeq, pkt.streamId, false); + m_network->writePeer(pkt.peerId, pkt.peerId, { NET_FUNC::PROTOCOL, NET_SUBFUNC::PROTOCOL_SUBFUNC_NXDN }, pkt.buffer, pkt.bufferLen, pkt.pktSeq, pkt.streamId, false); if (m_network->m_debug) { LogDebug(LOG_NET, "NXDN, parrot, dstPeer = %u, len = %u, pktSeq = %u, streamId = %u", pkt.peerId, pkt.bufferLen, pkt.pktSeq, pkt.streamId); @@ -390,7 +610,7 @@ void TagNXDNData::playbackParrot() else { // repeat traffic to the connected peers for (auto peer : m_network->m_peers) { - m_network->writePeer(peer.first, { NET_FUNC::PROTOCOL, NET_SUBFUNC::PROTOCOL_SUBFUNC_NXDN }, pkt.buffer, pkt.bufferLen, pkt.pktSeq, pkt.streamId, false); + m_network->writePeer(peer.first, pkt.peerId, { NET_FUNC::PROTOCOL, NET_SUBFUNC::PROTOCOL_SUBFUNC_NXDN }, pkt.buffer, pkt.bufferLen, pkt.pktSeq, pkt.streamId, false); if (m_network->m_debug) { LogDebug(LOG_NET, "NXDN, parrot, dstPeer = %u, len = %u, pktSeq = %u, streamId = %u", peer.first, pkt.bufferLen, pkt.pktSeq, pkt.streamId); @@ -579,6 +799,9 @@ bool TagNXDNData::validate(uint32_t peerId, lc::RTCH& lc, uint8_t messageType, u .requestAsync(m_network->m_influxServer); } + if (m_network->m_logDenials) + LogError(LOG_NET, "NXDN, " INFLUXDB_ERRSTR_DISABLED_SRC_RID ", peer = %u, srcId = %u, dstId = %u", peerId, lc.getSrcId(), lc.getDstId()); + // report In-Call Control to the peer sending traffic m_network->writePeerICC(peerId, streamId, NET_SUBFUNC::PROTOCOL_SUBFUNC_NXDN, NET_ICC::REJECT_TRAFFIC, lc.getDstId()); return false; @@ -615,6 +838,9 @@ bool TagNXDNData::validate(uint32_t peerId, lc::RTCH& lc, uint8_t messageType, u .requestAsync(m_network->m_influxServer); } + if (m_network->m_logDenials) + LogError(LOG_NET, "NXDN, " INFLUXDB_ERRSTR_DISABLED_DST_RID ", peer = %u, srcId = %u, dstId = %u", peerId, lc.getSrcId(), lc.getDstId()); + // report In-Call Control to the peer sending traffic m_network->writePeerICC(peerId, streamId, NET_SUBFUNC::PROTOCOL_SUBFUNC_NXDN, NET_ICC::REJECT_TRAFFIC, lc.getDstId()); return false; @@ -632,12 +858,13 @@ bool TagNXDNData::validate(uint32_t peerId, lc::RTCH& lc, uint8_t messageType, u .tag("streamId", std::to_string(streamId)) .tag("srcId", std::to_string(lc.getSrcId())) .tag("dstId", std::to_string(lc.getDstId())) - .field("message", std::string(INFLUXDB_ERRSTR_DISABLED_SRC_RID)) + .field("message", std::string(INFLUXDB_ERRSTR_ILLEGAL_RID_ACCESS)) .timestamp(std::chrono::duration_cast(std::chrono::system_clock::now().time_since_epoch()).count()) .requestAsync(m_network->m_influxServer); } - LogWarning(LOG_NET, "NXDN, illegal/unknown RID attempted access, srcId = %u, dstId = %u", lc.getSrcId(), lc.getDstId()); + if (m_network->m_logDenials) + LogWarning(LOG_NET, "NXDN, " INFLUXDB_ERRSTR_ILLEGAL_RID_ACCESS ", srcId = %u, dstId = %u", lc.getSrcId(), lc.getDstId()); // report In-Call Control to the peer sending traffic m_network->writePeerICC(peerId, streamId, NET_SUBFUNC::PROTOCOL_SUBFUNC_NXDN, NET_ICC::REJECT_TRAFFIC, lc.getDstId()); @@ -665,6 +892,9 @@ bool TagNXDNData::validate(uint32_t peerId, lc::RTCH& lc, uint8_t messageType, u .requestAsync(m_network->m_influxServer); } + if (m_network->m_logDenials) + LogError(LOG_NET, "NXDN, " INFLUXDB_ERRSTR_INV_TALKGROUP ", peer = %u, srcId = %u, dstId = %u", peerId, lc.getSrcId(), lc.getDstId()); + // report In-Call Control to the peer sending traffic m_network->writePeerICC(peerId, streamId, NET_SUBFUNC::PROTOCOL_SUBFUNC_NXDN, NET_ICC::REJECT_TRAFFIC, lc.getDstId()); return false; @@ -691,12 +921,13 @@ bool TagNXDNData::validate(uint32_t peerId, lc::RTCH& lc, uint8_t messageType, u .tag("streamId", std::to_string(streamId)) .tag("srcId", std::to_string(lc.getSrcId())) .tag("dstId", std::to_string(lc.getDstId())) - .field("message", std::string(INFLUXDB_ERRSTR_DISABLED_SRC_RID)) + .field("message", std::string(INFLUXDB_ERRSTR_ILLEGAL_RID_ACCESS)) .timestamp(std::chrono::duration_cast(std::chrono::system_clock::now().time_since_epoch()).count()) .requestAsync(m_network->m_influxServer); } - LogWarning(LOG_NET, "NXDN, illegal/unknown RID attempted access, srcId = %u, dstId = %u", lc.getSrcId(), lc.getDstId()); + if (m_network->m_logDenials) + LogWarning(LOG_NET, "NXDN, " INFLUXDB_ERRSTR_ILLEGAL_RID_ACCESS ", srcId = %u, dstId = %u", lc.getSrcId(), lc.getDstId()); // report In-Call Control to the peer sending traffic m_network->writePeerICC(peerId, streamId, NET_SUBFUNC::PROTOCOL_SUBFUNC_NXDN, NET_ICC::REJECT_TRAFFIC, lc.getDstId()); @@ -718,6 +949,9 @@ bool TagNXDNData::validate(uint32_t peerId, lc::RTCH& lc, uint8_t messageType, u .requestAsync(m_network->m_influxServer); } + if (m_network->m_logDenials) + LogError(LOG_NET, "NXDN, " INFLUXDB_ERRSTR_DISABLED_TALKGROUP ", peer = %u, srcId = %u, dstId = %u", peerId, lc.getSrcId(), lc.getDstId()); + // report In-Call Control to the peer sending traffic m_network->writePeerICC(peerId, streamId, NET_SUBFUNC::PROTOCOL_SUBFUNC_NXDN, NET_ICC::REJECT_TRAFFIC, lc.getDstId()); return false; @@ -743,6 +977,9 @@ bool TagNXDNData::validate(uint32_t peerId, lc::RTCH& lc, uint8_t messageType, u .requestAsync(m_network->m_influxServer); } + if (m_network->m_logDenials) + LogError(LOG_NET, "NXDN, " INFLUXDB_ERRSTR_RID_NOT_PERMITTED ", peer = %u, srcId = %u, dstId = %u", peerId, lc.getSrcId(), lc.getDstId()); + // report In-Call Control to the peer sending traffic m_network->writePeerICC(peerId, streamId, NET_SUBFUNC::PROTOCOL_SUBFUNC_NXDN, NET_ICC::REJECT_TRAFFIC, lc.getDstId()); return false; @@ -814,8 +1051,8 @@ void TagNXDNData::write_Message_Deny(uint32_t peerId, uint32_t srcId, uint32_t d rcch->setDstId(dstId); if (m_network->m_verbose) { - LogMessage(LOG_RF, "NXDN, MSG_DENIAL (Message Denial), reason = $%02X, service = $%02X, srcId = %u, dstId = %u", - service, srcId, dstId); + LogMessage(LOG_RF, "NXDN, MSG_DENIAL (Message Denial), reason = $%02X (%s), service = $%02X, srcId = %u, dstId = %u", + reason, NXDNUtils::causeToString(reason).c_str(), service, srcId, dstId); } write_Message(peerId, rcch.get()); @@ -865,5 +1102,5 @@ void TagNXDNData::write_Message(uint32_t peerId, lc::RCCH* rcch) } uint32_t streamId = m_network->createStreamId(); - m_network->writePeer(peerId, { NET_FUNC::PROTOCOL, NET_SUBFUNC::PROTOCOL_SUBFUNC_NXDN }, message.get(), messageLength, RTP_END_OF_CALL_SEQ, streamId, false); + m_network->writePeer(peerId, m_network->m_peerId, { NET_FUNC::PROTOCOL, NET_SUBFUNC::PROTOCOL_SUBFUNC_NXDN }, message.get(), messageLength, RTP_END_OF_CALL_SEQ, streamId, false); } diff --git a/src/fne/network/callhandler/TagNXDNData.h b/src/fne/network/callhandler/TagNXDNData.h index 35ec58cd..f48328cd 100644 --- a/src/fne/network/callhandler/TagNXDNData.h +++ b/src/fne/network/callhandler/TagNXDNData.h @@ -55,12 +55,13 @@ namespace network * @param data Network data buffer. * @param len Length of data. * @param peerId Peer ID. + * @param ssrc RTP Synchronization Source ID. * @param pktSeq RTP packet sequence. * @param streamId Stream ID. * @param external Flag indicating traffic is from an external peer. * @returns bool True, if frame is processed, otherwise false. */ - bool processFrame(const uint8_t* data, uint32_t len, uint32_t peerId, uint16_t pktSeq, uint32_t streamId, bool external = false); + bool processFrame(const uint8_t* data, uint32_t len, uint32_t peerId, uint32_t ssrc, uint16_t pktSeq, uint32_t streamId, bool external = false); /** * @brief Process a grant request frame from the network. * @param srcId Source Radio ID. @@ -142,6 +143,10 @@ namespace network * @brief Peer ID. */ uint32_t peerId; + /** + * @brief Destination Peer ID. + */ + uint32_t dstPeerId; /** * @brief Flag indicating this call is active with traffic currently in progress. */ @@ -161,6 +166,7 @@ namespace network }; typedef std::pair StatusMapPair; concurrent::unordered_map m_status; + concurrent::unordered_map m_statusPVCall; bool m_debug; diff --git a/src/fne/network/callhandler/TagP25Data.cpp b/src/fne/network/callhandler/TagP25Data.cpp index 766240c3..4728f7d4 100644 --- a/src/fne/network/callhandler/TagP25Data.cpp +++ b/src/fne/network/callhandler/TagP25Data.cpp @@ -48,6 +48,7 @@ TagP25Data::TagP25Data(FNENetwork* network, bool debug) : m_parrotFramesReady(false), m_parrotFirstFrame(true), m_status(), + m_statusPVCall(), m_packetData(nullptr), m_debug(debug) { @@ -65,10 +66,16 @@ TagP25Data::~TagP25Data() /* Process a data frame from the network. */ -bool TagP25Data::processFrame(const uint8_t* data, uint32_t len, uint32_t peerId, uint16_t pktSeq, uint32_t streamId, bool external) +bool TagP25Data::processFrame(const uint8_t* data, uint32_t len, uint32_t peerId, uint32_t ssrc, uint16_t pktSeq, uint32_t streamId, bool external) { hrc::hrc_t pktTime = hrc::now(); + // P25 network frame should never be less then 24 bytes + if (len < 24U) { + LogError(LOG_NET, "malformed P25 packet, len < 24, shouldn't happen"); + return false; + } + DECLARE_UINT8_ARRAY(buffer, len); ::memcpy(buffer, data, len); @@ -77,8 +84,13 @@ bool TagP25Data::processFrame(const uint8_t* data, uint32_t len, uint32_t peerId uint32_t srcId = GET_UINT24(data, 5U); uint32_t dstId = GET_UINT24(data, 8U); + uint8_t controlByte = data[14U]; + uint8_t MFId = data[15U]; + uint32_t sysId = (data[11U] << 8) | (data[12U] << 0); + uint32_t netId = GET_UINT24(data, 16U); + uint8_t lsd1 = data[20U]; uint8_t lsd2 = data[21U]; @@ -138,19 +150,24 @@ bool TagP25Data::processFrame(const uint8_t* data, uint32_t len, uint32_t peerId control.setDstId(dstId); control.setMFId(MFId); + // set the LC group flag based on the control byte + control.setGroup((controlByte & NET_CTRL_U2U) != NET_CTRL_U2U); + lsd.setLSD1(lsd1); lsd.setLSD2(lsd2); - uint32_t frameLength = buffer[23U]; + uint8_t frameLength = buffer[23U]; + + if (!m_network->validateP25FrameLength(frameLength, len, duid)) + return false; // process a TSBK out into a class literal if possible std::unique_ptr tsbk; if (duid == DUID::TSDU) { - UInt8Array data = std::unique_ptr(new uint8_t[frameLength]); - ::memset(data.get(), 0x00U, frameLength); - ::memcpy(data.get(), buffer + 24U, frameLength); + DECLARE_UINT8_ARRAY(data, frameLength); + ::memcpy(data, buffer + 24U, frameLength); - tsbk = lc::tsbk::TSBKFactory::createTSBK(data.get()); + tsbk = lc::tsbk::TSBKFactory::createTSBK(data); } // is the stream valid? @@ -165,7 +182,7 @@ bool TagP25Data::processFrame(const uint8_t* data, uint32_t len, uint32_t peerId // is this the end of the call stream? if ((duid == DUID::TDU) || (duid == DUID::TDULC)) { if (srcId == 0U && dstId == 0U) { - LogWarning(LOG_NET, "P25, invalid TDU, peer = %u, srcId = %u, dstId = %u, streamId = %u, external = %u", peerId, srcId, dstId, streamId, external); + LogWarning(LOG_NET, "P25, invalid TDU, peer = %u, ssrc = %u, srcId = %u, dstId = %u, streamId = %u, external = %u", peerId, ssrc, srcId, dstId, streamId, external); return false; } @@ -173,7 +190,7 @@ bool TagP25Data::processFrame(const uint8_t* data, uint32_t len, uint32_t peerId uint64_t duration = hrc::diff(pktTime, status.callStartTime); // perform a test for grant demands, and if the TG isn't valid ignore the demand - bool grantDemand = (data[14U] & 0x80U) == 0x80U; + bool grantDemand = (data[14U] & network::NET_CTRL_GRANT_DEMAND) == network::NET_CTRL_GRANT_DEMAND; if (grantDemand) { lookups::TalkgroupRuleGroupVoice tg = m_network->m_tidLookup->find(control.getDstId()); if (!tg.config().active()) { @@ -181,6 +198,8 @@ bool TagP25Data::processFrame(const uint8_t* data, uint32_t len, uint32_t peerId } } + bool switchOver = (data[14U] & network::NET_CTRL_SWITCH_OVER) == network::NET_CTRL_SWITCH_OVER; + auto it = std::find_if(m_status.begin(), m_status.end(), [&](StatusMapPair x) { if (x.second.dstId == dstId) { if (x.second.activeCall) @@ -189,9 +208,9 @@ bool TagP25Data::processFrame(const uint8_t* data, uint32_t len, uint32_t peerId return false; }); if (it != m_status.end()) { - if (grantDemand) { - LogWarning(LOG_NET, "P25, Call Collision, peer = %u, srcId = %u, dstId = %u, streamId = %u, rxPeer = %u, rxSrcId = %u, rxDstId = %u, rxStreamId = %u, external = %u", - peerId, srcId, dstId, streamId, status.peerId, status.srcId, status.dstId, status.streamId, external); + if (grantDemand && !switchOver) { + LogWarning(LOG_NET, "P25, Call Collision, peer = %u, ssrc = %u, sysId = $%03X, netId = $%05X, srcId = %u, dstId = %u, streamId = %u, rxPeer = %u, rxSrcId = %u, rxDstId = %u, rxStreamId = %u, external = %u", + peerId, ssrc, sysId, netId, srcId, dstId, streamId, status.peerId, status.srcId, status.dstId, status.streamId, external); return false; } else { @@ -208,8 +227,22 @@ bool TagP25Data::processFrame(const uint8_t* data, uint32_t len, uint32_t peerId } } - LogMessage(LOG_NET, "P25, Call End, peer = %u, srcId = %u, dstId = %u, duration = %u, streamId = %u, external = %u", - peerId, srcId, dstId, duration / 1000, streamId, external); + // is this a private call? + auto it = std::find_if(m_statusPVCall.begin(), m_statusPVCall.end(), [&](StatusMapPair x) { + if (x.second.dstId == dstId) { + if (x.second.activeCall) + return true; + } + return false; + }); + if (it != m_statusPVCall.end()) { + m_statusPVCall[dstId].reset(); + LogMessage(LOG_NET, "P25, Private Call End, peer = %u, ssrc = %u, sysId = $%03X, netId = $%05X, srcId = %u, dstId = %u, duration = %u, streamId = %u, external = %u", + peerId, ssrc, sysId, netId, srcId, dstId, duration / 1000, streamId, external); + } + else + LogMessage(LOG_NET, "P25, Call End, peer = %u, ssrc = %u, sysId = $%03X, netId = $%05X, srcId = %u, dstId = %u, duration = %u, streamId = %u, external = %u", + peerId, ssrc, sysId, netId, srcId, dstId, duration / 1000, streamId, external); // report call event to InfluxDB if (m_network->m_enableInfluxDB) { @@ -234,10 +267,12 @@ bool TagP25Data::processFrame(const uint8_t* data, uint32_t len, uint32_t peerId // is this a new call stream? if ((duid != DUID::TDU) && (duid != DUID::TDULC)) { if (srcId == 0U && dstId == 0U) { - LogWarning(LOG_NET, "P25, invalid call, peer = %u, srcId = %u, dstId = %u, streamId = %u, external = %u", peerId, srcId, dstId, streamId, external); + LogWarning(LOG_NET, "P25, invalid call, peer = %u, ssrc = %u, srcId = %u, dstId = %u, streamId = %u, external = %u", peerId, srcId, dstId, streamId, external); return false; } + bool switchOver = (data[14U] & network::NET_CTRL_SWITCH_OVER) == network::NET_CTRL_SWITCH_OVER; + auto it = std::find_if(m_status.begin(), m_status.end(), [&](StatusMapPair x) { if (x.second.dstId == dstId) { if (x.second.activeCall) @@ -248,6 +283,15 @@ bool TagP25Data::processFrame(const uint8_t* data, uint32_t len, uint32_t peerId if (it != m_status.end()) { RxStatus status = m_status[dstId]; if (streamId != status.streamId && ((duid != DUID::TDU) && (duid != DUID::TDULC))) { + // perform TG switch over -- this can happen in special conditions where a TG may rapidly switch + // from one source to another (primarily from bridge resources) + if (switchOver) { + status.streamId = streamId; + status.srcId = srcId; + LogMessage(LOG_NET, "P25, Call Source Switched, peer = %u, ssrc = %u, sysId = $%03X, netId = $%05X, srcId = %u, dstId = %u, streamId = %u, rxPeer = %u, rxSrcId = %u, rxDstId = %u, rxStreamId = %u, external = %u", + peerId, ssrc, sysId, netId, srcId, dstId, streamId, status.peerId, status.srcId, status.dstId, status.streamId, external); + } + if (status.srcId != 0U && status.srcId != srcId) { uint64_t lastPktDuration = hrc::diff(hrc::now(), status.lastPacket); if ((lastPktDuration / 1000) > CALL_COLL_TIMEOUT) { @@ -256,8 +300,8 @@ bool TagP25Data::processFrame(const uint8_t* data, uint32_t len, uint32_t peerId m_network->m_callInProgress = false; } - LogWarning(LOG_NET, "P25, Call Collision, peer = %u, srcId = %u, dstId = %u, streamId = %u, rxPeer = %u, rxSrcId = %u, rxDstId = %u, rxStreamId = %u, external = %u", - peerId, srcId, dstId, streamId, status.peerId, status.srcId, status.dstId, status.streamId, external); + LogWarning(LOG_NET, "P25, Call Collision, peer = %u, ssrc = %u, sysId = $%03X, netId = $%05X, srcId = %u, dstId = %u, streamId = %u, rxPeer = %u, rxSrcId = %u, rxDstId = %u, rxStreamId = %u, external = %u", + peerId, ssrc, sysId, netId, srcId, dstId, streamId, status.peerId, status.srcId, status.dstId, status.streamId, external); return false; } } @@ -285,7 +329,25 @@ bool TagP25Data::processFrame(const uint8_t* data, uint32_t len, uint32_t peerId m_status[dstId].peerId = peerId; m_status[dstId].activeCall = true; - LogMessage(LOG_NET, "P25, Call Start, peer = %u, srcId = %u, dstId = %u, streamId = %u, external = %u", peerId, srcId, dstId, streamId, external); + // is this a private call? + if (lco == LCO::PRIVATE) { + m_statusPVCall[dstId].callStartTime = pktTime; + m_statusPVCall[dstId].srcId = srcId; + m_statusPVCall[dstId].dstId = dstId; + m_statusPVCall[dstId].streamId = streamId; + m_statusPVCall[dstId].peerId = peerId; + m_statusPVCall[dstId].activeCall = true; + + // find the SSRC of the peer that registered this unit + uint32_t regSSRC = m_network->findPeerUnitReg(dstId); + m_statusPVCall[dstId].dstPeerId = regSSRC; + + LogMessage(LOG_NET, "P25, Private Call Start, peer = %u, ssrc = %u, sysId = $%03X, netId = $%05X, srcId = %u, dstId = %u, streamId = %u, external = %u", + peerId, ssrc, sysId, netId, srcId, dstId, streamId, external); + } + else + LogMessage(LOG_NET, "P25, Call Start, peer = %u, ssrc = %u, sysId = $%03X, netId = $%05X, srcId = %u, dstId = %u, streamId = %u, external = %u", + peerId, ssrc, sysId, netId, srcId, dstId, streamId, external); m_network->m_callInProgress = true; } @@ -323,11 +385,95 @@ bool TagP25Data::processFrame(const uint8_t* data, uint32_t len, uint32_t peerId m_status[dstId].lastPacket = hrc::now(); + bool noConnectedPeerRepeat = false; + bool privateCallInProgress = false; + + // is this a private call in-progress? + if (m_network->m_restrictPVCallToRegOnly) { + if ((control.getLCO() != LCO::PRIVATE) && !control.getGroup()) { + // is this a private call? if so only repeat to the peer that registered the unit + auto it = std::find_if(m_statusPVCall.begin(), m_statusPVCall.end(), [&](StatusMapPair x) { + if (x.second.dstId == control.getDstId()) { + if (x.second.activeCall) + return true; + } + return false; + }); + if (it != m_statusPVCall.end()) { + privateCallInProgress = true; + } + } else { + if (lco == LCO::PRIVATE) { + privateCallInProgress = true; + } + } + + if (privateCallInProgress) { + // if we've not determined the destination peer, we have to repeat it everywhere + if (m_statusPVCall[dstId].dstPeerId == 0U) { + noConnectedPeerRepeat = false; + privateCallInProgress = false; // trick the system to repeat everywhere + } else { + // if this is a private call, check if the destination peer is one directly connected to us, if not + // flag the call so it only repeats to external peers + if (m_network->m_peers.size() > 0U && !noConnectedPeerRepeat) { + noConnectedPeerRepeat = true; + for (auto peer : m_network->m_peers) { + if (peerId != peer.first) { + FNEPeerConnection* conn = peer.second; + if (conn != nullptr) { + if (conn->isExternalPeer()) { + continue; + } + } + + if (m_statusPVCall[dstId].dstPeerId == peer.first) { + noConnectedPeerRepeat = false; + break; + } + } + } + } + } + } + } + // repeat traffic to the connected peers - if (m_network->m_peers.size() > 0U) { + if (m_network->m_peers.size() > 0U && !noConnectedPeerRepeat) { uint32_t i = 0U; for (auto peer : m_network->m_peers) { if (peerId != peer.first) { + FNEPeerConnection* conn = peer.second; + if (ssrc == peer.first) { + // skip the peer if it is the source peer + continue; + } + + if (m_network->m_restrictPVCallToRegOnly) { + // is this peer an external peer? + bool external = false; + if (conn != nullptr) { + external = conn->isExternalPeer(); + } + + // is this a private call? + if ((lco == LCO::PRIVATE) && !external) { + // is this a private call? if so only repeat to the peer that registered the unit + auto it = std::find_if(m_statusPVCall.begin(), m_statusPVCall.end(), [&](StatusMapPair x) { + if (x.second.dstId == dstId) { + if (x.second.activeCall) + return true; + } + return false; + }); + if (it != m_statusPVCall.end()) { + if (peer.first != m_statusPVCall[dstId].dstPeerId) { + continue; + } + } + } + } + // is this peer ignored? if (!isPeerPermitted(peer.first, control, duid, streamId)) { continue; @@ -349,10 +495,10 @@ bool TagP25Data::processFrame(const uint8_t* data, uint32_t len, uint32_t peerId // perform TGID route rewrites if configured routeRewrite(outboundPeerBuffer, peer.first, duid, dstId); - m_network->writePeer(peer.first, { NET_FUNC::PROTOCOL, NET_SUBFUNC::PROTOCOL_SUBFUNC_P25 }, outboundPeerBuffer, len, pktSeq, streamId, true); + m_network->writePeer(peer.first, ssrc, { NET_FUNC::PROTOCOL, NET_SUBFUNC::PROTOCOL_SUBFUNC_P25 }, outboundPeerBuffer, len, pktSeq, streamId, true); if (m_network->m_debug) { - LogDebug(LOG_NET, "P25, srcPeer = %u, dstPeer = %u, duid = $%02X, lco = $%02X, MFId = $%02X, srcId = %u, dstId = %u, len = %u, pktSeq = %u, streamId = %u, external = %u", - peerId, peer.first, duid, lco, MFId, srcId, dstId, len, pktSeq, streamId, external); + LogDebug(LOG_NET, "P25, ssrc = %u, srcPeer = %u, dstPeer = %u, duid = $%02X, lco = $%02X, MFId = $%02X, srcId = %u, dstId = %u, len = %u, pktSeq = %u, streamId = %u, external = %u", + ssrc, peerId, peer.first, duid, lco, MFId, srcId, dstId, len, pktSeq, streamId, external); } if (!m_network->m_callInProgress) @@ -363,6 +509,12 @@ bool TagP25Data::processFrame(const uint8_t* data, uint32_t len, uint32_t peerId m_network->m_frameQueue->flushQueue(); } + // if this is a private call, and we have already repeated to the connected peer that registered + // the unit, don't repeat to any external peers + if (privateCallInProgress && !noConnectedPeerRepeat) { + return true; + } + // repeat traffic to external peers if (m_network->m_host->m_peerNetworks.size() > 0U && !tg.config().parrot()) { for (auto peer : m_network->m_host->m_peerNetworks) { @@ -371,6 +523,11 @@ bool TagP25Data::processFrame(const uint8_t* data, uint32_t len, uint32_t peerId // don't try to repeat traffic to the source peer...if this traffic // is coming from a external peer if (dstPeerId != peerId) { + if (ssrc == dstPeerId) { + // skip the peer if it is the source peer + continue; + } + // is this peer ignored? if (!isPeerPermitted(dstPeerId, control, duid, streamId, true)) { continue; @@ -394,10 +551,14 @@ bool TagP25Data::processFrame(const uint8_t* data, uint32_t len, uint32_t peerId // process TSDUs going to external peers if (processTSDUToExternal(outboundPeerBuffer, peerId, dstPeerId, duid)) { - peer.second->writeMaster({ NET_FUNC::PROTOCOL, NET_SUBFUNC::PROTOCOL_SUBFUNC_P25 }, outboundPeerBuffer, len, pktSeq, streamId); + // are we a peer link? + if (peer.second->isPeerLink()) + peer.second->writeMaster({ NET_FUNC::PROTOCOL, NET_SUBFUNC::PROTOCOL_SUBFUNC_P25 }, outboundPeerBuffer, len, pktSeq, streamId, false, false, 0U, ssrc); + else + peer.second->writeMaster({ NET_FUNC::PROTOCOL, NET_SUBFUNC::PROTOCOL_SUBFUNC_P25 }, outboundPeerBuffer, len, pktSeq, streamId); if (m_network->m_debug) { - LogDebug(LOG_NET, "P25, srcPeer = %u, dstPeer = %u, duid = $%02X, lco = $%02X, MFId = $%02X, srcId = %u, dstId = %u, len = %u, pktSeq = %u, streamId = %u, external = %u", - peerId, dstPeerId, duid, lco, MFId, srcId, dstId, len, pktSeq, streamId, external); + LogDebug(LOG_NET, "P25, ssrc = %u, srcPeer = %u, dstPeer = %u, duid = $%02X, lco = $%02X, MFId = $%02X, srcId = %u, dstId = %u, len = %u, pktSeq = %u, streamId = %u, external = %u", + ssrc, peerId, dstPeerId, duid, lco, MFId, srcId, dstId, len, pktSeq, streamId, external); } } @@ -485,7 +646,7 @@ void TagP25Data::playbackParrot() // create empty LSD data::LowSpeedData lsd = data::LowSpeedData(); - uint8_t controlByte = 0x80U; + uint8_t controlByte = network::NET_CTRL_GRANT_DEMAND; // send grant demand uint32_t messageLength = 0U; @@ -493,13 +654,13 @@ void TagP25Data::playbackParrot() if (message != nullptr) { if (m_network->m_parrotOnlyOriginating) { LogMessage(LOG_NET, "P25, Parrot Grant Demand, peer = %u, srcId = %u, dstId = %u", pkt.peerId, srcId, dstId); - m_network->writePeer(pkt.peerId, { NET_FUNC::PROTOCOL, NET_SUBFUNC::PROTOCOL_SUBFUNC_P25 }, message.get(), messageLength, + m_network->writePeer(pkt.peerId, pkt.peerId, { NET_FUNC::PROTOCOL, NET_SUBFUNC::PROTOCOL_SUBFUNC_P25 }, message.get(), messageLength, RTP_END_OF_CALL_SEQ, m_network->createStreamId(), false); } else { // repeat traffic to the connected peers for (auto peer : m_network->m_peers) { LogMessage(LOG_NET, "P25, Parrot Grant Demand, peer = %u, srcId = %u, dstId = %u", peer.first, srcId, dstId); - m_network->writePeer(peer.first, { NET_FUNC::PROTOCOL, NET_SUBFUNC::PROTOCOL_SUBFUNC_P25 }, message.get(), messageLength, + m_network->writePeer(peer.first, pkt.peerId, { NET_FUNC::PROTOCOL, NET_SUBFUNC::PROTOCOL_SUBFUNC_P25 }, message.get(), messageLength, RTP_END_OF_CALL_SEQ, m_network->createStreamId(), false); } } @@ -510,7 +671,7 @@ void TagP25Data::playbackParrot() } if (m_network->m_parrotOnlyOriginating) { - m_network->writePeer(pkt.peerId, { NET_FUNC::PROTOCOL, NET_SUBFUNC::PROTOCOL_SUBFUNC_P25 }, pkt.buffer, pkt.bufferLen, pkt.pktSeq, pkt.streamId, false); + m_network->writePeer(pkt.peerId, pkt.peerId, { NET_FUNC::PROTOCOL, NET_SUBFUNC::PROTOCOL_SUBFUNC_P25 }, pkt.buffer, pkt.bufferLen, pkt.pktSeq, pkt.streamId, false); if (m_network->m_debug) { LogDebug(LOG_NET, "P25, parrot, dstPeer = %u, len = %u, pktSeq = %u, streamId = %u", pkt.peerId, pkt.bufferLen, pkt.pktSeq, pkt.streamId); @@ -518,7 +679,7 @@ void TagP25Data::playbackParrot() } else { // repeat traffic to the connected peers for (auto peer : m_network->m_peers) { - m_network->writePeer(peer.first, { NET_FUNC::PROTOCOL, NET_SUBFUNC::PROTOCOL_SUBFUNC_P25 }, pkt.buffer, pkt.bufferLen, pkt.pktSeq, pkt.streamId, false); + m_network->writePeer(peer.first, pkt.peerId, { NET_FUNC::PROTOCOL, NET_SUBFUNC::PROTOCOL_SUBFUNC_P25 }, pkt.buffer, pkt.bufferLen, pkt.pktSeq, pkt.streamId, false); if (m_network->m_debug) { LogDebug(LOG_NET, "P25, parrot, dstPeer = %u, len = %u, pktSeq = %u, streamId = %u", peer.first, pkt.bufferLen, pkt.pktSeq, pkt.streamId); @@ -625,11 +786,10 @@ void TagP25Data::routeRewrite(uint8_t* buffer, uint32_t peerId, uint8_t duid, ui // are we receiving a TSDU? if (duid == DUID::TSDU) { - UInt8Array data = std::unique_ptr(new uint8_t[frameLength]); - ::memset(data.get(), 0x00U, frameLength); - ::memcpy(data.get(), buffer + 24U, frameLength); + DECLARE_UINT8_ARRAY(data, frameLength); + ::memcpy(data, buffer + 24U, frameLength); - std::unique_ptr tsbk = lc::tsbk::TSBKFactory::createTSBK(data.get()); + std::unique_ptr tsbk = lc::tsbk::TSBKFactory::createTSBK(data); if (tsbk != nullptr) { // handle standard P25 reference opcodes switch (tsbk->getLCO()) { @@ -704,13 +864,12 @@ bool TagP25Data::processTSDUFrom(uint8_t* buffer, uint32_t peerId, uint8_t duid) { // are we receiving a TSDU? if (duid == DUID::TSDU) { - uint32_t frameLength = buffer[23U]; + uint32_t frameLength = P25_TSDU_FRAME_LENGTH_BYTES;//buffer[23U]; - UInt8Array data = std::unique_ptr(new uint8_t[frameLength]); - ::memset(data.get(), 0x00U, frameLength); - ::memcpy(data.get(), buffer + 24U, frameLength); + DECLARE_UINT8_ARRAY(data, frameLength); + ::memcpy(data, buffer + 24U, frameLength); - std::unique_ptr tsbk = lc::tsbk::TSBKFactory::createTSBK(data.get()); + std::unique_ptr tsbk = lc::tsbk::TSBKFactory::createTSBK(data); if (tsbk != nullptr) { // report tsbk event to InfluxDB if (m_network->m_enableInfluxDB && m_network->m_influxLogRawData) { @@ -754,6 +913,27 @@ bool TagP25Data::processTSDUFrom(uint8_t* buffer, uint32_t peerId, uint8_t duid) LogMessage(LOG_NET, P25_TSDU_STR ", %s, sysId = $%03X, rfss = $%02X, site = $%02X, chNo = %u-%u, svcClass = $%02X, peerId = %u", tsbk->toString().c_str(), osp->getAdjSiteSysId(), osp->getAdjSiteRFSSId(), osp->getAdjSiteId(), osp->getAdjSiteChnId(), osp->getAdjSiteChnNo(), osp->getAdjSiteSvcClass(), peerId); } + + // check if the sending peer is mapped + lookups::AdjPeerMapEntry adjPeerMap = m_network->m_adjSiteMapLookup->find(peerId); + if (!adjPeerMap.isEmpty()) { + if (!adjPeerMap.active()) { + // LogWarning(LOG_NET, "PEER %u, passing ADJ_STS_BCAST to other peers is disabled, dropping", peerId); + return false; + } else { + // if the peer is mapped, we can repeat the ADJ_STS_BCAST to other peers + if (m_network->m_peers.size() > 0U) { + for (auto peer : m_network->m_peers) { + if (peerId != peer.first) { + write_TSDU(peer.first, osp); + } + } + + // this seems strange -- but we want to prevent the main processing loop from repeating the ADJ_STS_BCAST + return false; + } + } + } } } break; @@ -770,11 +950,10 @@ bool TagP25Data::processTSDUFrom(uint8_t* buffer, uint32_t peerId, uint8_t duid) if (duid == DUID::TDULC) { uint32_t frameLength = buffer[23U]; - UInt8Array data = std::unique_ptr(new uint8_t[frameLength]); - ::memset(data.get(), 0x00U, frameLength); - ::memcpy(data.get(), buffer + 24U, frameLength); + DECLARE_UINT8_ARRAY(data, frameLength); + ::memcpy(data, buffer + 24U, frameLength); - std::unique_ptr tdulc = lc::tdulc::TDULCFactory::createTDULC(data.get()); + std::unique_ptr tdulc = lc::tdulc::TDULCFactory::createTDULC(data); if (tdulc != nullptr) { // handle standard P25 reference opcodes switch (tdulc->getLCO()) { @@ -800,13 +979,12 @@ bool TagP25Data::processTSDUTo(uint8_t* buffer, uint32_t peerId, uint8_t duid) { // are we receiving a TSDU? if (duid == DUID::TSDU) { - uint32_t frameLength = buffer[23U]; + uint32_t frameLength = P25_TSDU_FRAME_LENGTH_BYTES;//buffer[23U]; - UInt8Array data = std::unique_ptr(new uint8_t[frameLength]); - ::memset(data.get(), 0x00U, frameLength); - ::memcpy(data.get(), buffer + 24U, frameLength); + DECLARE_UINT8_ARRAY(data, frameLength); + ::memcpy(data, buffer + 24U, frameLength); - std::unique_ptr tsbk = lc::tsbk::TSBKFactory::createTSBK(data.get()); + std::unique_ptr tsbk = lc::tsbk::TSBKFactory::createTSBK(data); if (tsbk != nullptr) { //uint32_t srcId = tsbk->getSrcId(); uint32_t dstId = tsbk->getDstId(); @@ -869,11 +1047,10 @@ bool TagP25Data::processTSDUToExternal(uint8_t* buffer, uint32_t srcPeerId, uint if (duid == DUID::TSDU) { uint32_t frameLength = buffer[23U]; - UInt8Array data = std::unique_ptr(new uint8_t[frameLength]); - ::memset(data.get(), 0x00U, frameLength); - ::memcpy(data.get(), buffer + 24U, frameLength); + DECLARE_UINT8_ARRAY(data, frameLength); + ::memcpy(data, buffer + 24U, frameLength); - std::unique_ptr tsbk = lc::tsbk::TSBKFactory::createTSBK(data.get()); + std::unique_ptr tsbk = lc::tsbk::TSBKFactory::createTSBK(data); if (tsbk != nullptr) { // handle standard P25 reference opcodes switch (tsbk->getLCO()) { @@ -913,6 +1090,13 @@ bool TagP25Data::isPeerPermitted(uint32_t peerId, lc::LC& control, DUID::E duid, return false; if (!m_network->checkU2UDroppedPeer(peerId)) return true; + + // is this a U2U call? + lookups::RadioId rid = m_network->m_ridLookup->find(control.getDstId()); + if (!rid.radioDefault() && rid.radioEnabled()) { + return true; + } + return false; } @@ -935,43 +1119,6 @@ bool TagP25Data::isPeerPermitted(uint32_t peerId, lc::LC& control, DUID::E duid, if (duid == DUID::TSDU || duid == DUID::PDU) return true; - if (duid == DUID::HDU) { - if (m_network->m_filterHeaders) { - if (control.getSrcId() != 0U && control.getDstId() != 0U) { - // is this a group call? - lookups::TalkgroupRuleGroupVoice tg = m_network->m_tidLookup->find(control.getDstId()); - if (!tg.isInvalid()) { - return true; - } - - // is this peer excluded from the group? - std::vector exclusion = tg.config().exclusion(); - if (exclusion.size() > 0) { - auto it = std::find(exclusion.begin(), exclusion.end(), peerId); - if (it != exclusion.end()) { - return false; - } - } - - tg = m_network->m_tidLookup->findByRewrite(peerId, control.getDstId()); - if (!tg.isInvalid()) { - return true; - } - - // is this a U2U call? - lookups::RadioId rid = m_network->m_ridLookup->find(control.getDstId()); - if (!rid.radioDefault() && rid.radioEnabled()) { - return true; - } - - return false; - } - } - - // always permit a headers - return true; - } - if (duid == DUID::TDULC) { // always permit a terminator return true; @@ -1119,6 +1266,9 @@ bool TagP25Data::validate(uint32_t peerId, lc::LC& control, DUID::E duid, const .requestAsync(m_network->m_influxServer); } + if (m_network->m_logDenials) + LogError(LOG_NET, "P25, " INFLUXDB_ERRSTR_DISABLED_SRC_RID ", peer = %u, srcId = %u, dstId = %u", peerId, control.getSrcId(), control.getDstId()); + // report In-Call Control to the peer sending traffic m_network->writePeerICC(peerId, streamId, NET_SUBFUNC::PROTOCOL_SUBFUNC_P25, NET_ICC::REJECT_TRAFFIC, control.getDstId()); return false; @@ -1140,6 +1290,18 @@ bool TagP25Data::validate(uint32_t peerId, lc::LC& control, DUID::E duid, const // always validate a terminator if the source is valid if (m_network->m_filterTerminators) { if ((duid == DUID::TDU || duid == DUID::TDULC) && control.getDstId() != 0U) { + // is this a private call? + auto it = std::find_if(m_statusPVCall.begin(), m_statusPVCall.end(), [&](StatusMapPair x) { + if (x.second.dstId == control.getDstId()) { + if (x.second.activeCall) + return true; + } + return false; + }); + if (it != m_statusPVCall.end()) { + return true; + } + // is this a group call? lookups::TalkgroupRuleGroupVoice tg = m_network->m_tidLookup->find(control.getDstId()); if (!tg.isInvalid()) { @@ -1151,12 +1313,6 @@ bool TagP25Data::validate(uint32_t peerId, lc::LC& control, DUID::E duid, const return true; } - // is this a U2U call? - lookups::RadioId rid = m_network->m_ridLookup->find(control.getDstId()); - if (!rid.radioDefault() && rid.radioEnabled()) { - return true; - } - //LogDebugEx(LOG_NET, "TagP25Data::validate()", "TDU for invalid destination, dropped, dstId = %u", control.getDstId()); return false; } @@ -1168,8 +1324,24 @@ bool TagP25Data::validate(uint32_t peerId, lc::LC& control, DUID::E duid, const return true; } + // validate private call in-progress + bool privateCallInProgress = false; + if ((control.getLCO() != LCO::PRIVATE) && !control.getGroup()) { + // is this a private call? if so only repeat to the peer that registered the unit + auto it = std::find_if(m_statusPVCall.begin(), m_statusPVCall.end(), [&](StatusMapPair x) { + if (x.second.dstId == control.getDstId()) { + if (x.second.activeCall) + return true; + } + return false; + }); + if (it != m_statusPVCall.end()) { + privateCallInProgress = true; + } + } + // is this a private call? - if (control.getLCO() == LCO::PRIVATE) { + if ((control.getLCO() == LCO::PRIVATE) || privateCallInProgress) { // is the destination ID a blacklisted ID? lookups::RadioId rid = m_network->m_ridLookup->find(control.getDstId()); if (!rid.radioDefault()) { @@ -1187,6 +1359,9 @@ bool TagP25Data::validate(uint32_t peerId, lc::LC& control, DUID::E duid, const .requestAsync(m_network->m_influxServer); } + if (m_network->m_logDenials) + LogError(LOG_NET, "P25, " INFLUXDB_ERRSTR_DISABLED_DST_RID ", peer = %u, srcId = %u, dstId = %u", peerId, control.getSrcId(), control.getDstId()); + // report In-Call Control to the peer sending traffic m_network->writePeerICC(peerId, streamId, NET_SUBFUNC::PROTOCOL_SUBFUNC_P25, NET_ICC::REJECT_TRAFFIC, control.getDstId()); return false; @@ -1204,12 +1379,13 @@ bool TagP25Data::validate(uint32_t peerId, lc::LC& control, DUID::E duid, const .tag("streamId", std::to_string(streamId)) .tag("srcId", std::to_string(control.getSrcId())) .tag("dstId", std::to_string(control.getDstId())) - .field("message", std::string(INFLUXDB_ERRSTR_DISABLED_SRC_RID)) + .field("message", std::string(INFLUXDB_ERRSTR_ILLEGAL_RID_ACCESS)) .timestamp(std::chrono::duration_cast(std::chrono::system_clock::now().time_since_epoch()).count()) .requestAsync(m_network->m_influxServer); } - LogWarning(LOG_NET, "P25, illegal/unknown RID attempted access, srcId = %u, dstId = %u", control.getSrcId(), control.getDstId()); + if (m_network->m_logDenials) + LogWarning(LOG_NET, "P25, " INFLUXDB_ERRSTR_ILLEGAL_RID_ACCESS ", srcId = %u, dstId = %u", control.getSrcId(), control.getDstId()); // report In-Call Control to the peer sending traffic m_network->writePeerICC(peerId, streamId, NET_SUBFUNC::PROTOCOL_SUBFUNC_P25, NET_ICC::REJECT_TRAFFIC, control.getDstId()); @@ -1242,6 +1418,26 @@ bool TagP25Data::validate(uint32_t peerId, lc::LC& control, DUID::E duid, const } } break; + case TSBKO::IOSP_EXT_FNCT: + { + const lc::tsbk::IOSP_EXT_FNCT* iosp = static_cast(tsbk); + if (iosp != nullptr) { + lookups::PeerId pid = m_network->m_peerListLookup->find(peerId); + uint32_t func = iosp->getExtendedFunction(); + switch (func) { + case ExtendedFunctions::INHIBIT: + case ExtendedFunctions::UNINHIBIT: + { + if (!pid.peerDefault() && !pid.canIssueInhibit()) { + LogWarning(LOG_NET, "P25, PEER %u attempted inhibit/unhibit, not authorized", peerId); + return false; + } + } + break; + } + } + } + break; } // handle validating DVM call termination packets @@ -1285,6 +1481,9 @@ bool TagP25Data::validate(uint32_t peerId, lc::LC& control, DUID::E duid, const .requestAsync(m_network->m_influxServer); } + if (m_network->m_logDenials) + LogError(LOG_NET, "P25, " INFLUXDB_ERRSTR_INV_TALKGROUP ", peer = %u, srcId = %u, dstId = %u", peerId, control.getSrcId(), control.getDstId()); + // report In-Call Control to the peer sending traffic m_network->writePeerICC(peerId, streamId, NET_SUBFUNC::PROTOCOL_SUBFUNC_P25, NET_ICC::REJECT_TRAFFIC, control.getDstId()); return false; @@ -1311,12 +1510,13 @@ bool TagP25Data::validate(uint32_t peerId, lc::LC& control, DUID::E duid, const .tag("streamId", std::to_string(streamId)) .tag("srcId", std::to_string(control.getSrcId())) .tag("dstId", std::to_string(control.getDstId())) - .field("message", std::string(INFLUXDB_ERRSTR_DISABLED_SRC_RID)) + .field("message", std::string(INFLUXDB_ERRSTR_ILLEGAL_RID_ACCESS)) .timestamp(std::chrono::duration_cast(std::chrono::system_clock::now().time_since_epoch()).count()) .requestAsync(m_network->m_influxServer); } - LogWarning(LOG_NET, "P25, illegal/unknown RID attempted access, srcId = %u, dstId = %u", control.getSrcId(), control.getDstId()); + if (m_network->m_logDenials) + LogWarning(LOG_NET, "P25, " INFLUXDB_ERRSTR_ILLEGAL_RID_ACCESS ", srcId = %u, dstId = %u", control.getSrcId(), control.getDstId()); // report In-Call Control to the peer sending traffic m_network->writePeerICC(peerId, streamId, NET_SUBFUNC::PROTOCOL_SUBFUNC_P25, NET_ICC::REJECT_TRAFFIC, control.getDstId()); @@ -1338,6 +1538,9 @@ bool TagP25Data::validate(uint32_t peerId, lc::LC& control, DUID::E duid, const .requestAsync(m_network->m_influxServer); } + if (m_network->m_logDenials) + LogError(LOG_NET, "P25, " INFLUXDB_ERRSTR_DISABLED_TALKGROUP ", peer = %u, srcId = %u, dstId = %u", peerId, control.getSrcId(), control.getDstId()); + // report In-Call Control to the peer sending traffic m_network->writePeerICC(peerId, streamId, NET_SUBFUNC::PROTOCOL_SUBFUNC_P25, NET_ICC::REJECT_TRAFFIC, control.getDstId()); return false; @@ -1363,6 +1566,9 @@ bool TagP25Data::validate(uint32_t peerId, lc::LC& control, DUID::E duid, const .requestAsync(m_network->m_influxServer); } + if (m_network->m_logDenials) + LogError(LOG_NET, "P25, " INFLUXDB_ERRSTR_RID_NOT_PERMITTED ", peer = %u, srcId = %u, dstId = %u", peerId, control.getSrcId(), control.getDstId()); + // report In-Call Control to the peer sending traffic m_network->writePeerICC(peerId, streamId, NET_SUBFUNC::PROTOCOL_SUBFUNC_P25, NET_ICC::REJECT_TRAFFIC, control.getDstId()); return false; @@ -1449,8 +1655,9 @@ void TagP25Data::write_TSDU_Deny(uint32_t peerId, uint32_t srcId, uint32_t dstId osp->setGroup(grp); if (m_network->m_verbose) { - LogMessage(LOG_RF, P25_TSDU_STR ", %s, AIV = %u, reason = $%02X, srcId = %u, dstId = %u", - osp->toString().c_str(), osp->getAIV(), reason, osp->getSrcId(), osp->getDstId()); + LogMessage(LOG_RF, P25_TSDU_STR ", %s, AIV = %u, reason = $%02X (%s), srcId = %u, dstId = %u", + osp->toString().c_str(), osp->getAIV(), reason, P25Utils::denyRsnToString(reason).c_str(), + osp->getSrcId(), osp->getDstId()); } write_TSDU(peerId, osp.get()); @@ -1469,8 +1676,9 @@ void TagP25Data::write_TSDU_Queue(uint32_t peerId, uint32_t srcId, uint32_t dstI osp->setGroup(grp); if (m_network->m_verbose) { - LogMessage(LOG_RF, P25_TSDU_STR ", %s, AIV = %u, reason = $%02X, srcId = %u, dstId = %u", - osp->toString().c_str(), osp->getAIV(), reason, osp->getSrcId(), osp->getDstId()); + LogMessage(LOG_RF, P25_TSDU_STR ", %s, AIV = %u, reason = $%02X (%s), srcId = %u, dstId = %u", + osp->toString().c_str(), osp->getAIV(), reason, P25Utils::queueRsnToString(reason).c_str(), + osp->getSrcId(), osp->getDstId()); } write_TSDU(peerId, osp.get()); @@ -1518,7 +1726,7 @@ void TagP25Data::write_TSDU(uint32_t peerId, lc::TSBK* tsbk) uint32_t streamId = m_network->createStreamId(); if (peerId > 0U) { - m_network->writePeer(peerId, { NET_FUNC::PROTOCOL, NET_SUBFUNC::PROTOCOL_SUBFUNC_P25 }, message.get(), messageLength, + m_network->writePeer(peerId, m_network->m_peerId, { NET_FUNC::PROTOCOL, NET_SUBFUNC::PROTOCOL_SUBFUNC_P25 }, message.get(), messageLength, RTP_END_OF_CALL_SEQ, streamId, false); } else { // repeat traffic to the connected peers @@ -1530,7 +1738,7 @@ void TagP25Data::write_TSDU(uint32_t peerId, lc::TSBK* tsbk) m_network->m_frameQueue->flushQueue(); } - m_network->writePeer(peer.first, { NET_FUNC::PROTOCOL, NET_SUBFUNC::PROTOCOL_SUBFUNC_P25 }, message.get(), messageLength, + m_network->writePeer(peer.first, m_network->m_peerId, { NET_FUNC::PROTOCOL, NET_SUBFUNC::PROTOCOL_SUBFUNC_P25 }, message.get(), messageLength, RTP_END_OF_CALL_SEQ, streamId, true); if (m_network->m_debug) { LogDebug(LOG_NET, "P25, peer = %u, len = %u, streamId = %u", diff --git a/src/fne/network/callhandler/TagP25Data.h b/src/fne/network/callhandler/TagP25Data.h index 32433840..12de5c16 100644 --- a/src/fne/network/callhandler/TagP25Data.h +++ b/src/fne/network/callhandler/TagP25Data.h @@ -61,12 +61,13 @@ namespace network * @param data Network data buffer. * @param len Length of data. * @param peerId Peer ID. + * @param ssrc RTP Synchronization Source ID. * @param pktSeq RTP packet sequence. * @param streamId Stream ID. * @param external Flag indicating traffic is from an external peer. * @returns bool True, if frame is processed, otherwise false. */ - bool processFrame(const uint8_t* data, uint32_t len, uint32_t peerId, uint16_t pktSeq, uint32_t streamId, bool external = false); + bool processFrame(const uint8_t* data, uint32_t len, uint32_t peerId, uint32_t ssrc, uint16_t pktSeq, uint32_t streamId, bool external = false); /** * @brief Process a grant request frame from the network. * @param srcId Source Radio ID. @@ -191,6 +192,10 @@ namespace network * @brief Peer ID. */ uint32_t peerId; + /** + * @brief Destination Peer ID. + */ + uint32_t dstPeerId; /** * @brief Flag indicating this call is active with traffic currently in progress. */ @@ -210,6 +215,7 @@ namespace network }; typedef std::pair StatusMapPair; concurrent::unordered_map m_status; + concurrent::unordered_map m_statusPVCall; friend class packetdata::P25PacketData; packetdata::P25PacketData* m_packetData; diff --git a/src/fne/network/callhandler/packetdata/DMRPacketData.cpp b/src/fne/network/callhandler/packetdata/DMRPacketData.cpp index e12fc72c..89aaa829 100644 --- a/src/fne/network/callhandler/packetdata/DMRPacketData.cpp +++ b/src/fne/network/callhandler/packetdata/DMRPacketData.cpp @@ -92,7 +92,7 @@ bool DMRPacketData::processFrame(const uint8_t* data, uint32_t len, uint32_t pee dmrData.getData(frame); // is the stream valid? - if (m_tag->validate(peerId, dmrData, streamId)) { + if (m_tag->validate(peerId, dmrData, nullptr, streamId)) { // is this peer ignored? if (!m_tag->isPeerPermitted(peerId, dmrData, streamId)) { return false; @@ -131,7 +131,7 @@ bool DMRPacketData::processFrame(const uint8_t* data, uint32_t len, uint32_t pee bool ret = status->header.decode(frame); if (!ret) { LogError(LOG_NET, "DMR Slot %u, DataType::DATA_HEADER, unable to decode the network data header", status->slotNo); - Utils::dump(1U, "Unfixable PDU Data", frame, DMR_FRAME_LENGTH_BYTES); + Utils::dump(1U, "DMR, Unfixable PDU Data", frame, DMR_FRAME_LENGTH_BYTES); delete status; m_status.erase(peerId); @@ -254,7 +254,7 @@ void DMRPacketData::dispatch(uint32_t peerId, dmr::data::NetData& dmrData, const } if (m_network->m_dumpPacketData) { - Utils::dump(1U, "ISP PDU Packet", status->pduUserData, status->pduDataOffset); + Utils::dump(1U, "DMR, ISP PDU Packet", status->pduUserData, status->pduDataOffset); } } } @@ -283,7 +283,7 @@ void DMRPacketData::dispatchToFNE(uint32_t peerId, dmr::data::NetData& dmrData, m_network->m_frameQueue->flushQueue(); } - m_network->writePeer(peer.first, { NET_FUNC::PROTOCOL, NET_SUBFUNC::PROTOCOL_SUBFUNC_DMR }, data, len, pktSeq, streamId, true); + m_network->writePeer(peer.first, peerId, { NET_FUNC::PROTOCOL, NET_SUBFUNC::PROTOCOL_SUBFUNC_DMR }, data, len, pktSeq, streamId, true); if (m_network->m_debug) { LogDebug(LOG_NET, "DMR, srcPeer = %u, dstPeer = %u, seqNo = %u, srcId = %u, dstId = %u, slotNo = %u, len = %u, pktSeq = %u, stream = %u", peerId, peer.first, seqNo, srcId, dstId, status->slotNo, len, pktSeq, streamId); diff --git a/src/fne/network/callhandler/packetdata/P25PacketData.cpp b/src/fne/network/callhandler/packetdata/P25PacketData.cpp index 97965f0f..53fa0772 100644 --- a/src/fne/network/callhandler/packetdata/P25PacketData.cpp +++ b/src/fne/network/callhandler/packetdata/P25PacketData.cpp @@ -41,12 +41,11 @@ using namespace p25::sndcp; // --------------------------------------------------------------------------- const uint8_t DATA_CALL_COLL_TIMEOUT = 60U; +const uint8_t MAX_PKT_RETRY_CNT = 5U; -// --------------------------------------------------------------------------- -// Static Class Members -// --------------------------------------------------------------------------- - -std::timed_mutex P25PacketData::m_vtunMutex; +const uint32_t INTERPACKET_DELAY = 100U; // milliseconds +const uint32_t ARP_RETRY_MS = 5000U; // milliseconds +const uint32_t SUBSCRIBER_READY_RETRY_MS = 1000U; // milliseconds // --------------------------------------------------------------------------- // Public Class Members @@ -57,7 +56,7 @@ std::timed_mutex P25PacketData::m_vtunMutex; P25PacketData::P25PacketData(FNENetwork* network, TagP25Data* tag, bool debug) : m_network(network), m_tag(tag), - m_dataFrames(), + m_queuedFrames(), m_status(), m_arpTable(), m_readyForNextPkt(), @@ -140,7 +139,7 @@ bool P25PacketData::processFrame(const uint8_t* data, uint32_t len, uint32_t pee bool ret = status->header.decode(buffer); if (!ret) { LogWarning(LOG_NET, P25_PDU_STR ", unfixable RF 1/2 rate header data"); - Utils::dump(1U, "Unfixable PDU Data", buffer, P25_PDU_FEC_LENGTH_BYTES); + Utils::dump(1U, "P25, Unfixable PDU Data", buffer, P25_PDU_FEC_LENGTH_BYTES); delete status; m_status.erase(peerId); @@ -175,11 +174,6 @@ bool P25PacketData::processFrame(const uint8_t* data, uint32_t len, uint32_t pee return true; } - if (status->header.getSAP() != PDUSAP::EXT_ADDR && - status->header.getFormat() != PDUFormatType::UNCONFIRMED) { - m_suSendSeq[status->llId] = 0U; - } - LogMessage(LOG_NET, "P25, Data Call Start, peer = %u, llId = %u, streamId = %u, external = %u", peerId, status->llId, streamId, external); return true; } @@ -234,7 +228,7 @@ bool P25PacketData::processFrame(const uint8_t* data, uint32_t len, uint32_t pee bool ret = status->header.decodeExtAddr(buffer); if (!ret) { LogWarning(LOG_NET, P25_PDU_STR ", unfixable RF 1/2 rate second header data"); - Utils::dump(1U, "Unfixable PDU Data", buffer, P25_PDU_HEADER_LENGTH_BYTES); + Utils::dump(1U, "P25, Unfixable PDU Data", buffer, P25_PDU_HEADER_LENGTH_BYTES); delete status; m_status.erase(peerId); @@ -247,7 +241,6 @@ bool P25PacketData::processFrame(const uint8_t* data, uint32_t len, uint32_t pee status->extendedAddress = true; status->llId = status->header.getSrcLLId(); - m_suSendSeq[status->llId] = 0U; offset += P25_PDU_FEC_LENGTH_BYTES; blocksToFollow--; @@ -306,7 +299,7 @@ bool P25PacketData::processFrame(const uint8_t* data, uint32_t len, uint32_t pee LogWarning(LOG_NET, P25_PDU_STR ", unfixable PDU data (1/2 rate or CRC), block %u", i); if (m_network->m_dumpPacketData) { - Utils::dump(1U, "Unfixable PDU Data", buffer, P25_PDU_FEC_LENGTH_BYTES); + Utils::dump(1U, "P25, Unfixable PDU Data", buffer, P25_PDU_FEC_LENGTH_BYTES); } } @@ -368,33 +361,58 @@ void P25PacketData::processPacketFrame(const uint8_t* data, uint32_t len, bool a uint16_t pktLen = Utils::reverseEndian(ipHeader->ip_len); // bryanb: this could be problematic on different endianness #if DEBUG_P25_PDU_DATA - Utils::dump(1U, "P25PacketData::processPacketFrame() packet", data, pktLen); + Utils::dump(1U, "P25, P25PacketData::processPacketFrame() packet", data, pktLen); #endif - VTUNDataFrame* dataFrame = new VTUNDataFrame(); - dataFrame->buffer = new uint8_t[len]; - ::memcpy(dataFrame->buffer, data, len); - dataFrame->bufferLen = len; - dataFrame->pktLen = pktLen; - dataFrame->proto = proto; + uint32_t llId = getLLIdAddress(Utils::reverseEndian(ipHeader->ip_dst.s_addr)); - uint32_t dstLlId = getLLIdAddress(Utils::reverseEndian(ipHeader->ip_dst.s_addr)); + uint32_t srcProtoAddr = Utils::reverseEndian(ipHeader->ip_src.s_addr); + uint32_t tgtProtoAddr = Utils::reverseEndian(ipHeader->ip_dst.s_addr); - dataFrame->srcHWAddr = WUID_FNE; - dataFrame->srcProtoAddr = Utils::reverseEndian(ipHeader->ip_src.s_addr); - dataFrame->tgtHWAddr = dstLlId; - dataFrame->tgtProtoAddr = Utils::reverseEndian(ipHeader->ip_dst.s_addr); + std::string srcIpStr = __IP_FROM_UINT(srcProtoAddr); + std::string tgtIpStr = __IP_FROM_UINT(tgtProtoAddr); - dataFrame->timestamp = now; + LogMessage(LOG_NET, "P25, VTUN -> PDU IP Data, srcIp = %s (%u), dstIp = %s (%u), pktLen = %u, proto = %02X", + srcIpStr.c_str(), WUID_FNE, tgtIpStr.c_str(), llId, pktLen, proto); - if (dstLlId == 0U) { - LogMessage(LOG_NET, "P25, no ARP entry for, dstIp = %s", dstIp); - write_PDU_ARP(Utils::reverseEndian(ipHeader->ip_dst.s_addr)); + // assemble a P25 PDU frame header for transport... + data::DataHeader* pktHeader = new data::DataHeader(); + pktHeader->setFormat(PDUFormatType::CONFIRMED); + pktHeader->setMFId(MFG_STANDARD); + pktHeader->setAckNeeded(true); + pktHeader->setOutbound(true); + pktHeader->setSAP(PDUSAP::PACKET_DATA); + pktHeader->setLLId(llId); + pktHeader->setBlocksToFollow(1U); + + pktHeader->calculateLength(pktLen); + uint32_t pduLength = pktHeader->getPDULength(); + if (pduLength < pktLen) { + LogWarning(LOG_NET, "P25, VTUN, data truncated!"); + pktLen = pduLength; // don't overflow the buffer } - m_vtunMutex.try_lock_for(std::chrono::milliseconds(60)); - m_dataFrames.push_back(dataFrame); - m_vtunMutex.unlock(); + DECLARE_UINT8_ARRAY(pduUserData, pduLength); + ::memcpy(pduUserData, data, pktLen); +#if DEBUG_P25_PDU_DATA + Utils::dump(1U, "P25, P25PacketData::processPacketFrame(), pduUserData", pduUserData, pduLength); +#endif + + // queue frame for dispatch + QueuedDataFrame* qf = new QueuedDataFrame(); + qf->retryCnt = 0U; + qf->extendRetry = false; + qf->timestamp = now + INTERPACKET_DELAY; + + qf->header = pktHeader; + qf->llId = llId; + qf->tgtProtoAddr = tgtProtoAddr; + + qf->userData = new uint8_t[pduLength]; + ::memcpy(qf->userData, pduUserData, pduLength); + qf->userDataLen = pduLength; + + m_queuedFrames.push_back(qf); #endif // !defined(_WIN32) } @@ -404,93 +422,80 @@ void P25PacketData::clock(uint32_t ms) { uint64_t now = std::chrono::duration_cast(std::chrono::system_clock::now().time_since_epoch()).count(); - if (m_dataFrames.size() == 0U) { + if (m_queuedFrames.size() == 0U) { return; } // transmit queued data frames bool processed = false; - bool locked = m_vtunMutex.try_lock_for(std::chrono::milliseconds(60)); - - auto& dataFrame = m_dataFrames[0]; - - if (locked) - m_vtunMutex.unlock(); - - if (dataFrame != nullptr) { - if (now > dataFrame->timestamp + 500U) { + + auto& frame = m_queuedFrames[0]; + if (frame != nullptr) { + if (now > frame->timestamp) { processed = true; + if (frame->retryCnt >= MAX_PKT_RETRY_CNT && !frame->extendRetry) { + LogWarning(LOG_NET, "P25, max packet retry count exceeded, dropping packet, dstIp = %s", __IP_FROM_UINT(frame->tgtProtoAddr).c_str()); + goto pkt_clock_abort; + } + + if (frame->retryCnt >= (MAX_PKT_RETRY_CNT * 2U) && frame->extendRetry) { + LogWarning(LOG_NET, "P25, max packet retry count exceeded, dropping packet, dstIp = %s", __IP_FROM_UINT(frame->tgtProtoAddr).c_str()); + m_readyForNextPkt[frame->llId] = true; // force ready for next packet + goto pkt_clock_abort; + } + + std::string tgtIpStr = __IP_FROM_UINT(frame->tgtProtoAddr); + LogMessage(LOG_NET, "P25, VTUN -> PDU IP Data, dstIp = %s (%u), userDataLen = %u, retries = %u", + tgtIpStr.c_str(), frame->llId, frame->userDataLen, frame->retryCnt); + // do we have a valid target address? - if (dataFrame->tgtHWAddr == 0U) { - uint32_t dstLlId = getLLIdAddress(dataFrame->tgtProtoAddr); - if (dstLlId == 0U) { + if (frame->llId == 0U) { + frame->llId = getLLIdAddress(frame->tgtProtoAddr); + if (frame->llId == 0U) { + LogWarning(LOG_NET, "P25, no ARP entry for, dstIp = %s", tgtIpStr.c_str()); + write_PDU_ARP(frame->tgtProtoAddr); + processed = false; + frame->timestamp = now + ARP_RETRY_MS; + frame->retryCnt++; goto pkt_clock_abort; } - - dataFrame->tgtHWAddr = dstLlId; + else { + frame->header->setLLId(frame->llId); + } } // is the SU ready for the next packet? - auto ready = std::find_if(m_readyForNextPkt.begin(), m_readyForNextPkt.end(), [=](ReadyForNextPktPair x) { return x.first == dataFrame->tgtHWAddr; }); + auto ready = std::find_if(m_readyForNextPkt.begin(), m_readyForNextPkt.end(), [=](ReadyForNextPktPair x) { return x.first == frame->llId; }); if (ready != m_readyForNextPkt.end()) { if (!ready->second) { + LogWarning(LOG_NET, "P25, subscriber not ready, dstIp = %s", tgtIpStr.c_str()); processed = false; + frame->timestamp = now + SUBSCRIBER_READY_RETRY_MS; + frame->extendRetry = true; + frame->retryCnt++; goto pkt_clock_abort; } - } else { - processed = false; - goto pkt_clock_abort; } - m_readyForNextPkt[dataFrame->tgtHWAddr] = false; - - std::string srcIp = __IP_FROM_UINT(dataFrame->srcProtoAddr); - std::string tgtIp = __IP_FROM_UINT(dataFrame->tgtProtoAddr); - - LogMessage(LOG_NET, "P25, VTUN -> PDU IP Data, srcIp = %s (%u), dstIp = %s (%u), pktLen = %u, proto = %02X", - srcIp.c_str(), dataFrame->srcHWAddr, tgtIp.c_str(), dataFrame->tgtHWAddr, dataFrame->pktLen, dataFrame->proto); - - // assemble a P25 PDU frame header for transport... - data::DataHeader rspHeader = data::DataHeader(); - rspHeader.setFormat(PDUFormatType::CONFIRMED); - rspHeader.setMFId(MFG_STANDARD); - rspHeader.setAckNeeded(true); - rspHeader.setOutbound(true); - rspHeader.setSAP(PDUSAP::EXT_ADDR); - rspHeader.setLLId(dataFrame->tgtHWAddr); - rspHeader.setBlocksToFollow(1U); - - rspHeader.setEXSAP(PDUSAP::PACKET_DATA); - rspHeader.setSrcLLId(WUID_FNE); - - rspHeader.calculateLength(dataFrame->pktLen); - uint32_t pduLength = rspHeader.getPDULength(); - - DECLARE_UINT8_ARRAY(pduUserData, pduLength); - ::memcpy(pduUserData + 4U, dataFrame->buffer, dataFrame->pktLen); -#if DEBUG_P25_PDU_DATA - Utils::dump(1U, "P25PacketData::clock() pduUserData", pduUserData, pduLength); -#endif - dispatchUserFrameToFNE(rspHeader, true, pduUserData); + m_readyForNextPkt[frame->llId] = false; + dispatchUserFrameToFNE(*frame->header, false, frame->userData); } } pkt_clock_abort: - locked = m_vtunMutex.try_lock_for(std::chrono::milliseconds(60)); - m_dataFrames.pop_front(); + m_queuedFrames.pop_front(); if (processed) { - if (dataFrame->buffer != nullptr) - delete[] dataFrame->buffer; - delete dataFrame; + if (frame->userData != nullptr) + delete[] frame->userData; + if (frame->header != nullptr) + delete frame->header; + delete frame; } else { // requeue packet - m_dataFrames.push_back(dataFrame); + m_queuedFrames.push_back(frame); } - - if (locked) - m_vtunMutex.unlock(); } // --------------------------------------------------------------------------- @@ -511,35 +516,68 @@ void P25PacketData::dispatch(uint32_t peerId) bool crcValid = false; if (status->header.getBlocksToFollow() > 0U) { if (status->pduUserDataLength < 4U) { - LogError(LOG_NET, P25_PDU_STR ", illegal PDU packet length, blocks %u, len %u", status->header.getBlocksToFollow(), status->pduUserDataLength); + LogError(LOG_NET, P25_PDU_STR ", illegal PDU packet length, peer = %u, llId = %u, blocks %u, len %u", + peerId, status->header.getLLId(), status->header.getBlocksToFollow(), status->pduUserDataLength); return; } crcValid = edac::CRC::checkCRC32(status->pduUserData, status->pduUserDataLength); if (!crcValid) { - LogError(LOG_NET, P25_PDU_STR ", failed CRC-32 check, blocks %u, len %u", status->header.getBlocksToFollow(), status->pduUserDataLength); + LogError(LOG_NET, P25_PDU_STR ", failed CRC-32 check, peer = %u, llId = %u, blocks %u, len %u", + peerId, status->header.getLLId(), status->header.getBlocksToFollow(), status->pduUserDataLength); return; } } if (m_network->m_dumpPacketData && status->dataBlockCnt > 0U) { - Utils::dump(1U, "ISP PDU Packet", status->pduUserData, status->pduUserDataLength); - } + Utils::dump(1U, "P25, ISP PDU Packet", status->pduUserData, status->pduUserDataLength); + } if (status->header.getFormat() == PDUFormatType::RSP) { - LogMessage(LOG_NET, P25_PDU_STR ", ISP, response, fmt = $%02X, rspClass = $%02X, rspType = $%02X, rspStatus = $%02X, llId = %u, srcLlId = %u", - status->header.getFormat(), status->header.getResponseClass(), status->header.getResponseType(), status->header.getResponseStatus(), + LogMessage(LOG_NET, P25_PDU_STR ", ISP, response, peer = %u, fmt = $%02X, rspClass = $%02X, rspType = $%02X, rspStatus = $%02X, llId = %u, srcLlId = %u", + peerId, status->header.getFormat(), status->header.getResponseClass(), status->header.getResponseType(), status->header.getResponseStatus(), status->header.getLLId(), status->header.getSrcLLId()); + // bryanb: this is naive and possibly error prone + m_readyForNextPkt[status->header.getSrcLLId()] = true; + if (status->header.getResponseClass() == PDUAckClass::ACK && status->header.getResponseType() == PDUAckType::ACK) { - m_readyForNextPkt[status->header.getSrcLLId()] = true; + LogMessage(LOG_NET, P25_PDU_STR ", ISP, response, OSP ACK, peer = %u, llId = %u, all blocks received OK, n = %u", + peerId, status->header.getLLId(), status->header.getResponseStatus()); + } else { + if (status->header.getResponseClass() == PDUAckClass::NACK) { + switch (status->header.getResponseType()) { + case PDUAckType::NACK_ILLEGAL: + LogMessage(LOG_NET, P25_PDU_STR ", ISP, response, OSP NACK, illegal format, peer = %u, llId = %u", + peerId, status->header.getLLId()); + break; + case PDUAckType::NACK_PACKET_CRC: + LogMessage(LOG_NET, P25_PDU_STR ", ISP, response, OSP NACK, packet CRC error, peer = %u, llId = %u, n = %u", + peerId, status->header.getLLId(), status->header.getResponseStatus()); + break; + case PDUAckType::NACK_SEQ: + case PDUAckType::NACK_OUT_OF_SEQ: + LogMessage(LOG_NET, P25_PDU_STR ", ISP, response, OSP NACK, packet out of sequence, peer = %u, llId = %u, seqNo = %u", + peerId, status->header.getLLId(), status->header.getResponseStatus()); + break; + case PDUAckType::NACK_UNDELIVERABLE: + LogMessage(LOG_NET, P25_PDU_STR ", ISP, response, OSP NACK, packet undeliverable, peer = %u, llId = %u, n = %u", + peerId, status->header.getLLId(), status->header.getResponseStatus()); + break; + + default: + break; + } + } } - write_PDU_Ack_Response(status->header.getResponseClass(), status->header.getResponseType(), status->header.getResponseStatus(), - status->header.getLLId(), status->header.getSrcLLId()); return; } + if (status->header.getFormat() == PDUFormatType::UNCONFIRMED) { + m_readyForNextPkt[status->header.getSrcLLId()] = true; + } + uint8_t sap = (status->extendedAddress) ? status->header.getEXSAP() : status->header.getSAP(); // don't dispatch SNDCP control, conventional data registration or ARP @@ -666,7 +704,7 @@ void P25PacketData::dispatch(uint32_t peerId) DECLARE_UINT8_ARRAY(ipFrame, pktLen); ::memcpy(ipFrame, status->pduUserData + dataPktOffset, pktLen); #if DEBUG_P25_PDU_DATA - Utils::dump(1U, "P25PacketData::dispatch() ipFrame", ipFrame, pktLen); + Utils::dump(1U, "P25, P25PacketData::dispatch(), ipFrame", ipFrame, pktLen); #endif if (!m_network->m_host->m_tun->write(ipFrame, pktLen)) { LogError(LOG_NET, P25_PDU_STR ", failed to write IP frame to virtual tunnel, len %u", pktLen); @@ -674,15 +712,24 @@ void P25PacketData::dispatch(uint32_t peerId) // if the packet is unhandled and sent off to VTUN; ack the packet so the sender knows we received it if (!handled) { - write_PDU_Ack_Response(PDUAckClass::ACK, PDUAckType::ACK, status->header.getNs(), status->header.getSrcLLId(), status->header.getLLId()); + write_PDU_Ack_Response(PDUAckClass::ACK, PDUAckType::ACK, status->header.getNs(), status->header.getSrcLLId(), + true, status->header.getLLId()); } #endif // !defined(_WIN32) } break; + case PDUSAP::CONV_DATA_REG: + { + LogMessage(LOG_NET, P25_PDU_STR ", CONV_DATA_REG (Conventional Data Registration), peer = %u, blocksToFollow = %u", + peerId, status->header.getBlocksToFollow()); + + processConvDataReg(status); + } + break; case PDUSAP::SNDCP_CTRL_DATA: { - LogMessage(LOG_NET, P25_PDU_STR ", SNDCP_CTRL_DATA (SNDCP Control Data), blocksToFollow = %u", - status->header.getBlocksToFollow()); + LogMessage(LOG_NET, P25_PDU_STR ", SNDCP_CTRL_DATA (SNDCP Control Data), peer = %u, blocksToFollow = %u", + peerId, status->header.getBlocksToFollow()); processSNDCPControl(status); } @@ -690,8 +737,8 @@ void P25PacketData::dispatch(uint32_t peerId) case PDUSAP::UNENC_KMM: case PDUSAP::ENC_KMM: { - LogMessage(LOG_NET, P25_PDU_STR ", KMM (Key Management Message), blocksToFollow = %u", - status->header.getBlocksToFollow()); + LogMessage(LOG_NET, P25_PDU_STR ", KMM (Key Management Message), peer = %u, blocksToFollow = %u", + peerId, status->header.getBlocksToFollow()); processKMM(status); } @@ -719,7 +766,7 @@ void P25PacketData::dispatchToFNE(uint32_t peerId) m_network->m_frameQueue->flushQueue(); } - write_PDU_User(peer.first, nullptr, status->header, status->extendedAddress, status->pduUserData, true); + write_PDU_User(peer.first, peerId, nullptr, status->header, status->extendedAddress, status->pduUserData, true); if (m_network->m_debug) { LogDebug(LOG_NET, "P25, srcPeer = %u, dstPeer = %u, duid = $%02X, srcId = %u, dstId = %u", peerId, peer.first, DUID::PDU, srcId, dstId); @@ -749,7 +796,7 @@ void P25PacketData::dispatchToFNE(uint32_t peerId) continue; } - write_PDU_User(dstPeerId, peer.second, status->header, status->extendedAddress, status->pduUserData); + write_PDU_User(dstPeerId, peerId, peer.second, status->header, status->extendedAddress, status->pduUserData); if (m_network->m_debug) { LogDebug(LOG_NET, "P25, srcPeer = %u, dstPeer = %u, duid = $%02X, srcId = %u, dstId = %u", peerId, dstPeerId, DUID::PDU, srcId, dstId); @@ -766,16 +813,15 @@ void P25PacketData::dispatchUserFrameToFNE(p25::data::DataHeader& dataHeader, bo uint32_t srcId = (extendedAddress) ? dataHeader.getSrcLLId() : dataHeader.getLLId(); uint32_t dstId = dataHeader.getLLId(); - uint8_t sendSeqNo = m_suSendSeq[srcId]; - if (sendSeqNo == 0U) { + // update the sequence number + m_suSendSeq[srcId]++; + if (m_suSendSeq[srcId] >= 8U) + { + m_suSendSeq[srcId] = 0U; dataHeader.setSynchronize(true); } - dataHeader.setNs(sendSeqNo); - ++sendSeqNo; - if (sendSeqNo > 7U) - sendSeqNo = 0U; - m_suSendSeq[srcId] = sendSeqNo; + dataHeader.setNs(m_suSendSeq[srcId]); // repeat traffic to the connected peers if (m_network->m_peers.size() > 0U) { @@ -786,7 +832,7 @@ void P25PacketData::dispatchUserFrameToFNE(p25::data::DataHeader& dataHeader, bo m_network->m_frameQueue->flushQueue(); } - write_PDU_User(peer.first, nullptr, dataHeader, extendedAddress, pduUserData, true); + write_PDU_User(peer.first, m_network->m_peerId, nullptr, dataHeader, extendedAddress, pduUserData, true); if (m_network->m_debug) { LogDebug(LOG_NET, "P25, dstPeer = %u, duid = $%02X, srcId = %u, dstId = %u", peer.first, DUID::PDU, srcId, dstId); @@ -798,6 +844,44 @@ void P25PacketData::dispatchUserFrameToFNE(p25::data::DataHeader& dataHeader, bo } } + +/* Helper used to process conventional data registration from PDU data. */ + +bool P25PacketData::processConvDataReg(RxStatus* status) +{ + uint8_t regType = (status->pduUserData[0U] >> 4) & 0x0F; + switch (regType) { + case PDURegType::CONNECT: + { + uint32_t llId = (status->pduUserData[1U] << 16) + (status->pduUserData[2U] << 8) + status->pduUserData[3U]; + uint32_t ipAddr = (status->pduUserData[8U] << 24) + (status->pduUserData[9U] << 16) + (status->pduUserData[10U] << 8) + status->pduUserData[11U]; + + if (ipAddr == 0U) { + LogWarning(LOG_NET, P25_PDU_STR ", CONNECT (Registration Request Connect) with zero IP address, llId = %u", llId); + ipAddr = getIPAddress(llId); + } + + LogMessage(LOG_NET, P25_PDU_STR ", CONNECT (Registration Request Connect), llId = %u, ipAddr = %s", llId, __IP_FROM_UINT(ipAddr).c_str()); + m_arpTable[llId] = ipAddr; // update ARP table + } + break; + case PDURegType::DISCONNECT: + { + uint32_t llId = (status->pduUserData[1U] << 16) + (status->pduUserData[2U] << 8) + status->pduUserData[3U]; + + LogMessage(LOG_NET, P25_PDU_STR ", DISCONNECT (Registration Request Disconnect), llId = %u", llId); + + m_arpTable.erase(llId); + } + break; + default: + LogError(LOG_RF, "P25 unhandled PDU registration type, regType = $%02X", regType); + break; + } + + return true; +} + /* Helper used to process SNDCP control data from PDU data. */ bool P25PacketData::processSNDCPControl(RxStatus* status) @@ -914,7 +998,7 @@ void P25PacketData::write_PDU_ARP(uint32_t addr) SET_UINT32(addr, arpPacket, 18U); // Target Protocol Address #if DEBUG_P25_PDU_DATA - Utils::dump(1U, "P25PacketData::write_PDU_ARP() arpPacket", arpPacket, P25_PDU_ARP_PCKT_LENGTH); + Utils::dump(1U, "P25, P25PacketData::write_PDU_ARP(), arpPacket", arpPacket, P25_PDU_ARP_PCKT_LENGTH); #endif LogMessage(LOG_NET, P25_PDU_STR ", ARP request, who has %s? tell %s (%u)", __IP_FROM_UINT(addr).c_str(), fneIPv4.c_str(), WUID_FNE); @@ -970,7 +1054,7 @@ void P25PacketData::write_PDU_ARP_Reply(uint32_t targetAddr, uint32_t requestorL SET_UINT24(requestorLlid, arpPacket, 15U); // Requestor Hardware Address SET_UINT32(requestorAddr, arpPacket, 18U); // Requestor Protocol Address #if DEBUG_P25_PDU_DATA - Utils::dump(1U, "P25PacketData::write_PDU_ARP_Reply() arpPacket", arpPacket, P25_PDU_ARP_PCKT_LENGTH); + Utils::dump(1U, "P25, P25PacketData::write_PDU_ARP_Reply(), arpPacket", arpPacket, P25_PDU_ARP_PCKT_LENGTH); #endif LogMessage(LOG_NET, P25_PDU_STR ", ARP reply, %s is at %u", __IP_FROM_UINT(targetAddr).c_str(), tgtLlid); @@ -998,7 +1082,7 @@ void P25PacketData::write_PDU_ARP_Reply(uint32_t targetAddr, uint32_t requestorL /* Helper to write a PDU acknowledge response. */ -void P25PacketData::write_PDU_Ack_Response(uint8_t ackClass, uint8_t ackType, uint8_t ackStatus, uint32_t llId, uint32_t srcLlId) +void P25PacketData::write_PDU_Ack_Response(uint8_t ackClass, uint8_t ackType, uint8_t ackStatus, uint32_t llId, bool extendedAddress, uint32_t srcLlId) { if (ackClass == PDUAckClass::ACK && ackType != PDUAckType::ACK) return; @@ -1014,7 +1098,11 @@ void P25PacketData::write_PDU_Ack_Response(uint8_t ackClass, uint8_t ackType, ui if (srcLlId > 0U) { rspHeader.setSrcLLId(srcLlId); } - rspHeader.setFullMessage(true); + + if (!extendedAddress) + rspHeader.setFullMessage(true); + else + rspHeader.setFullMessage(false); rspHeader.setBlocksToFollow(0U); @@ -1023,7 +1111,7 @@ void P25PacketData::write_PDU_Ack_Response(uint8_t ackClass, uint8_t ackType, ui /* Helper to write user data as a P25 PDU packet. */ -void P25PacketData::write_PDU_User(uint32_t peerId, network::PeerNetwork* peerNet, data::DataHeader& dataHeader, +void P25PacketData::write_PDU_User(uint32_t peerId, uint32_t srcPeerId, network::PeerNetwork* peerNet, data::DataHeader& dataHeader, bool extendedAddress, uint8_t* pduUserData, bool queueOnly) { uint32_t streamId = m_network->createStreamId(); @@ -1042,7 +1130,7 @@ void P25PacketData::write_PDU_User(uint32_t peerId, network::PeerNetwork* peerNe // generate the PDU header and 1/2 rate Trellis dataHeader.encode(buffer); - writeNetwork(peerId, peerNet, dataHeader, 0U, buffer, P25_PDU_FEC_LENGTH_BYTES, pktSeq, streamId, queueOnly); + writeNetwork(peerId, srcPeerId, peerNet, dataHeader, 0U, buffer, P25_PDU_FEC_LENGTH_BYTES, pktSeq, streamId, queueOnly); if (pduUserData == nullptr) return; @@ -1060,7 +1148,7 @@ void P25PacketData::write_PDU_User(uint32_t peerId, network::PeerNetwork* peerNe ::memset(buffer, 0x00U, P25_PDU_FEC_LENGTH_BYTES); dataHeader.encodeExtAddr(buffer); - writeNetwork(peerId, peerNet, dataHeader, 1U, buffer, P25_PDU_FEC_LENGTH_BYTES, pktSeq, streamId, queueOnly); + writeNetwork(peerId, srcPeerId, peerNet, dataHeader, 1U, buffer, P25_PDU_FEC_LENGTH_BYTES, pktSeq, streamId, queueOnly); ++pktSeq; dataOffset += P25_PDU_HEADER_LENGTH_BYTES; @@ -1087,7 +1175,7 @@ void P25PacketData::write_PDU_User(uint32_t peerId, network::PeerNetwork* peerNe } if (m_network->m_dumpPacketData) { - Utils::dump("OSP PDU User Data", pduUserData, packetLength); + Utils::dump("P25, OSP PDU User Data", pduUserData, packetLength); } // generate the PDU data @@ -1104,7 +1192,7 @@ void P25PacketData::write_PDU_User(uint32_t peerId, network::PeerNetwork* peerNe ::memset(buffer, 0x00U, P25_PDU_FEC_LENGTH_BYTES); dataBlock.encode(buffer); - writeNetwork(peerId, peerNet, dataHeader, networkBlock, buffer, P25_PDU_FEC_LENGTH_BYTES, (dataBlock.getLastBlock()) ? RTP_END_OF_CALL_SEQ : pktSeq, streamId); + writeNetwork(peerId, srcPeerId, peerNet, dataHeader, networkBlock, buffer, P25_PDU_FEC_LENGTH_BYTES, (dataBlock.getLastBlock()) ? RTP_END_OF_CALL_SEQ : pktSeq, streamId); ++pktSeq; dataOffset += (dataHeader.getFormat() == PDUFormatType::CONFIRMED) ? P25_PDU_CONFIRMED_DATA_LENGTH_BYTES : P25_PDU_UNCONFIRMED_LENGTH_BYTES; @@ -1116,7 +1204,7 @@ void P25PacketData::write_PDU_User(uint32_t peerId, network::PeerNetwork* peerNe /* Write data processed to the network. */ -bool P25PacketData::writeNetwork(uint32_t peerId, network::PeerNetwork* peerNet, const p25::data::DataHeader& dataHeader, const uint8_t currentBlock, +bool P25PacketData::writeNetwork(uint32_t peerId, uint32_t srcPeerId, network::PeerNetwork* peerNet, const p25::data::DataHeader& dataHeader, const uint8_t currentBlock, const uint8_t *data, uint32_t len, uint16_t pktSeq, uint32_t streamId, bool queueOnly) { assert(data != nullptr); @@ -1130,7 +1218,7 @@ bool P25PacketData::writeNetwork(uint32_t peerId, network::PeerNetwork* peerNet, if (peerNet != nullptr) { return peerNet->writeMaster({ NET_FUNC::PROTOCOL, NET_SUBFUNC::PROTOCOL_SUBFUNC_P25 }, message.get(), messageLength, pktSeq, streamId); } else { - return m_network->writePeer(peerId, { NET_FUNC::PROTOCOL, NET_SUBFUNC::PROTOCOL_SUBFUNC_P25 }, message.get(), messageLength, pktSeq, streamId, false); + return m_network->writePeer(peerId, srcPeerId, { NET_FUNC::PROTOCOL, NET_SUBFUNC::PROTOCOL_SUBFUNC_P25 }, message.get(), messageLength, pktSeq, streamId, false); } } @@ -1166,6 +1254,16 @@ uint32_t P25PacketData::getIPAddress(uint32_t llId) if (hasARPEntry(llId)) { return m_arpTable[llId]; + } else { + // do we have a static entry for this LLID? + lookups::RadioId rid = m_network->m_ridLookup->find(llId); + if (!rid.radioDefault()) { + if (rid.radioEnabled()) { + std::string addr = rid.radioIPAddress(); + uint32_t ipAddr = __IP_FROM_STR(addr); + return ipAddr; + } + } } return 0U; @@ -1186,5 +1284,19 @@ uint32_t P25PacketData::getLLIdAddress(uint32_t addr) } } + // lookup IP from static RID table + std::string ipAddr = __IP_FROM_UINT(addr); + std::unordered_map ridTable = m_network->m_ridLookup->table(); + auto it = std::find_if(ridTable.begin(), ridTable.end(), [&](std::pair x) { + if (x.second.radioIPAddress() == ipAddr) { + if (x.second.radioEnabled() && !x.second.radioDefault()) + return true; + } + return false; + }); + if (it != ridTable.end()) { + return it->first; + } + return 0U; } diff --git a/src/fne/network/callhandler/packetdata/P25PacketData.h b/src/fne/network/callhandler/packetdata/P25PacketData.h index f33d8097..30392ade 100644 --- a/src/fne/network/callhandler/packetdata/P25PacketData.h +++ b/src/fne/network/callhandler/packetdata/P25PacketData.h @@ -85,27 +85,25 @@ namespace network private: FNENetwork* m_network; - TagP25Data *m_tag; + TagP25Data* m_tag; /** * @brief Represents a queued data frame from the VTUN. */ - class VTUNDataFrame { + class QueuedDataFrame { public: - uint32_t srcHWAddr; //! Source Hardware Address - uint32_t srcProtoAddr; //! Source Protocol Address - uint32_t tgtHWAddr; //! Target Hardware Address - uint32_t tgtProtoAddr; //! Target Protocol Address + p25::data::DataHeader* header; //! Instance of a PDU data header. + uint32_t llId; //! Logical Link ID + uint32_t tgtProtoAddr; //! Target Protocol Address - uint8_t* buffer; //! Raw data buffer - uint32_t bufferLen; //! Length of raw data buffer - - uint16_t pktLen; //! Packet Length - uint8_t proto; //! Packet Protocol + uint8_t* userData; //! Raw data buffer + uint32_t userDataLen; //! Length of raw data buffer - uint64_t timestamp; //! Timestamp in milliseconds + uint64_t timestamp; //! Timestamp in milliseconds + uint8_t retryCnt; //! Packet Retry Counter + bool extendRetry; //! Flag indicating whether or not to extend the retry count for this packet. }; - concurrent::deque m_dataFrames; + concurrent::deque m_queuedFrames; /** * @brief Represents the receive status of a call. @@ -179,8 +177,6 @@ namespace network bool m_debug; - static std::timed_mutex m_vtunMutex; - /** * @brief Helper to dispatch PDU user data. * @param peerId Peer ID. @@ -199,6 +195,12 @@ namespace network */ void dispatchUserFrameToFNE(p25::data::DataHeader& dataHeader, bool extendedAddress, uint8_t* pduUserData); + /** + * @brief Helper used to process conventional data registration from PDU data. + * @param status Instance of the RxStatus class. + * @returns bool True, if conventional data registration data was processed, otherwise false. + */ + bool processConvDataReg(RxStatus* status); /** * @brief Helper used to process SNDCP control data from PDU data. * @param status Instance of the RxStatus class. @@ -233,24 +235,28 @@ namespace network * @param ackType Acknowledgement Type. * @param ackStatus * @param llId Logical Link ID. + * @param extendedAddress Flag indicating whether or not to extended addressing is in use. * @param srcLlId Source Logical Link ID. */ - void write_PDU_Ack_Response(uint8_t ackClass, uint8_t ackType, uint8_t ackStatus, uint32_t llId, uint32_t srcLlId = 0U); + void write_PDU_Ack_Response(uint8_t ackClass, uint8_t ackType, uint8_t ackStatus, uint32_t llId, bool extendedAddress, + uint32_t srcLlId = 0U); /** * @brief Helper to write user data as a P25 PDU packet. * @param peerId Peer ID. + * @param srcPeerId Source Peer ID. * @param peerNet Instance of PeerNetwork to use to send traffic. * @param dataHeader Instance of a PDU data header. * @param extendedAddress Flag indicating whether or not to extended addressing is in use. * @param pduUserData Buffer containing user data to transmit. */ - void write_PDU_User(uint32_t peerId, network::PeerNetwork* peerNet, p25::data::DataHeader& dataHeader, + void write_PDU_User(uint32_t peerId, uint32_t srcPeerId, network::PeerNetwork* peerNet, p25::data::DataHeader& dataHeader, bool extendedAddress, uint8_t* pduUserData, bool queueOnly = false); /** * @brief Write data processed to the network. * @param peerId Peer ID. + * @param srcPeerId Source Peer ID. * @param peerNet Instance of PeerNetwork to use to send traffic. * @param dataHeader Instance of a PDU data header. * @param currentBlock Current Block ID. @@ -259,7 +265,7 @@ namespace network * @param pktSeq RTP packet sequence. * @param streamId Stream ID. */ - bool writeNetwork(uint32_t peerId, network::PeerNetwork* peerNet, const p25::data::DataHeader& dataHeader, const uint8_t currentBlock, + bool writeNetwork(uint32_t peerId, uint32_t srcPeerId, network::PeerNetwork* peerNet, const p25::data::DataHeader& dataHeader, const uint8_t currentBlock, const uint8_t* data, uint32_t len, uint16_t pktSeq, uint32_t streamId, bool queueOnly = false); /** diff --git a/src/fne/network/influxdb/InfluxDB.cpp b/src/fne/network/influxdb/InfluxDB.cpp index a070571c..0cf1857d 100644 --- a/src/fne/network/influxdb/InfluxDB.cpp +++ b/src/fne/network/influxdb/InfluxDB.cpp @@ -63,7 +63,11 @@ int detail::inner::request(const char* method, const char* uri, const std::strin ret = getaddrinfo(si.host().c_str(), std::to_string(si.port()).c_str(), &hints, &addr); if (ret != 0) { - ::LogError(LOG_HOST, "Failed to determine InfluxDB server host, err: %d", errno); +#if defined(_WIN32) + ::LogError(LOG_HOST, "Failed to connect to InfluxDB server, err: %lu", ::GetLastError()); +#else + ::LogError(LOG_HOST, "Failed to determine InfluxDB server host, err: %d (%s)", errno, strerror(errno)); +#endif // defined(_WIN32) return 1; } @@ -73,9 +77,11 @@ int detail::inner::request(const char* method, const char* uri, const std::strin #if defined(_WIN32) ::LogError(LOG_HOST, "Failed to connect to InfluxDB server, err: %lu", ::GetLastError()); #else - ::LogError(LOG_HOST, "Failed to connect to InfluxDB server, err: %d", errno); + ::LogError(LOG_HOST, "Failed to connect to InfluxDB server, err: %d (%s)", errno, strerror(errno)); #endif // defined(_WIN32) closesocket(fd); + if (addr != nullptr) + free(addr); return 1; } @@ -85,12 +91,16 @@ int detail::inner::request(const char* method, const char* uri, const std::strin if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, (char*)&sockOptVal, sizeof(int)) != 0) { ::LogError(LOG_HOST, "Failed to connect to InfluxDB server, err: %lu", ::GetLastError()); closesocket(fd); + if (addr != nullptr) + free(addr); return 1; } #else if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &sockOptVal, sizeof(int)) < 0) { - ::LogError(LOG_HOST, "Failed to connect to InfluxDB server, err: %d", errno); + ::LogError(LOG_HOST, "Failed to connect to InfluxDB server, err: %d (%s)", errno, strerror(errno)); closesocket(fd); + if (addr != nullptr) + free(addr); return 1; } #endif // defined(_WIN32) @@ -99,21 +109,27 @@ int detail::inner::request(const char* method, const char* uri, const std::strin #if defined(_WIN32) u_long flags = 1; if (ioctlsocket(fd, FIONBIO, &flags) != 0) { - ::LogError(LOG_HOST, "Failed to connect to InfluxDB server, failed ioctlsocket, err: %d", errno); + ::LogError(LOG_HOST, "Failed to connect to InfluxDB server, failed ioctlsocket, err: %lu", ::GetLastError()); closesocket(fd); + if (addr != nullptr) + free(addr); return 1; } #else int flags = fcntl(fd, F_GETFL, 0); if (flags < 0) { - ::LogError(LOG_HOST, "Failed to connect to InfluxDB server, failed fcntl(F_GETFL), err: %d", errno); + ::LogError(LOG_HOST, "Failed to connect to InfluxDB server, failed fcntl(F_GETFL), err: %d (%s)", errno, strerror(errno)); closesocket(fd); + if (addr != nullptr) + free(addr); return 1; } if (fcntl(fd, F_SETFL, flags | O_NONBLOCK) < 0) { - ::LogError(LOG_HOST, "Failed to connect to InfluxDB server, failed fcntl(F_SETFL), err: %d", errno); + ::LogError(LOG_HOST, "Failed to connect to InfluxDB server, failed fcntl(F_SETFL), err: %d (%s)", errno, strerror(errno)); closesocket(fd); + if (addr != nullptr) + free(addr); return 1; } #endif // defined(_WIN32) @@ -140,6 +156,8 @@ int detail::inner::request(const char* method, const char* uri, const std::strin if (retryCnt > 5U) { ::LogError(LOG_HOST, "Failed to connect to InfluxDB server, timed out while connecting"); closesocket(fd); + if (addr != nullptr) + free(addr); return 1; } @@ -150,9 +168,11 @@ int detail::inner::request(const char* method, const char* uri, const std::strin #if defined(_WIN32) ::LogError(LOG_HOST, "Failed to connect to InfluxDB server, err: %lu", ::GetLastError()); #else - ::LogError(LOG_HOST, "Failed to connect to InfluxDB server, err: %d", errno); + ::LogError(LOG_HOST, "Failed to connect to InfluxDB server, err: %d (%s)", errno, strerror(errno)); #endif // defined(_WIN32) closesocket(fd); + if (addr != nullptr) + free(addr); return 1; } else if (ret > 0) { #if !defined(_WIN32) @@ -161,14 +181,18 @@ int detail::inner::request(const char* method, const char* uri, const std::strin socklen_t slen = sizeof(int); if (getsockopt(fd, SOL_SOCKET, SO_ERROR, (void *)(&valopt), &slen) < 0) { - ::LogError(LOG_HOST, "Failed to connect to InfluxDB server, err: %d", errno); + ::LogError(LOG_HOST, "Failed to connect to InfluxDB server, err: %d (%s)", errno, strerror(errno)); closesocket(fd); + if (addr != nullptr) + free(addr); return 1; } if (valopt) { ::LogError(LOG_HOST, "Failed to connect to InfluxDB server, err: %d", valopt); closesocket(fd); + if (addr != nullptr) + free(addr); return 1; } #endif // !defined(_WIN32) @@ -176,6 +200,8 @@ int detail::inner::request(const char* method, const char* uri, const std::strin } else { ::LogError(LOG_HOST, "Failed to connect to InfluxDB server, timed out while connecting"); closesocket(fd); + if (addr != nullptr) + free(addr); return 1; } } while (true); @@ -186,21 +212,27 @@ int detail::inner::request(const char* method, const char* uri, const std::strin #if defined(_WIN32) flags = 0; if (ioctlsocket(fd, FIONBIO, &flags) != 0) { - ::LogError(LOG_HOST, "Failed to connect to InfluxDB server, failed ioctlsocket, err: %d", errno); + ::LogError(LOG_HOST, "Failed to connect to InfluxDB server, failed ioctlsocket, err: %lu", ::GetLastError()); closesocket(fd); + if (addr != nullptr) + free(addr); return 1; } #else flags = fcntl(fd, F_GETFL, 0); if (flags < 0) { - ::LogError(LOG_HOST, "Failed to connect to InfluxDB server, failed fcntl(F_GETFL), err: %d", errno); + ::LogError(LOG_HOST, "Failed to connect to InfluxDB server, failed fcntl(F_GETFL), err: %d (%s)", errno, strerror(errno)); closesocket(fd); + if (addr != nullptr) + free(addr); return 1; } if (fcntl(fd, F_SETFL, flags & (~O_NONBLOCK)) < 0) { - ::LogError(LOG_HOST, "Failed to connect to InfluxDB server, failed fcntl(F_SETFL), err: %d", errno); + ::LogError(LOG_HOST, "Failed to connect to InfluxDB server, failed fcntl(F_SETFL), err: %d (%s)", errno, strerror(errno)); closesocket(fd); + if (addr != nullptr) + free(addr); return 1; } #endif // defined(_WIN32) @@ -244,7 +276,7 @@ int detail::inner::request(const char* method, const char* uri, const std::strin ret = 0; if (writev(fd, iv, 2) < (int)(iv[0].iov_len + iv[1].iov_len)) { - ::LogError(LOG_HOST, "Failed to write statistical data to InfluxDB server, err: %d", errno); + ::LogError(LOG_HOST, "Failed to write statistical data to InfluxDB server, err: %d (%s)", errno, strerror(errno)); ret = -6; } @@ -259,5 +291,7 @@ int detail::inner::request(const char* method, const char* uri, const std::strin #endif // close socket closesocket(fd); + if (addr != nullptr) + free(addr); return ret; } diff --git a/src/fne/win32/fne.ico b/src/fne/win32/fne.ico new file mode 100644 index 0000000000000000000000000000000000000000..3a46b8772b59f056bb26f8ebdbbced9f81c0e73e GIT binary patch literal 21413 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 literal 0 HcmV?d00001 diff --git a/src/host/CMakeLists.txt b/src/host/CMakeLists.txt index a06febba..aba8f13e 100644 --- a/src/host/CMakeLists.txt +++ b/src/host/CMakeLists.txt @@ -4,7 +4,7 @@ # * GPLv2 Open Source. Use is subject to license terms. # * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. # * -# * Copyright (C) 2022,2024 Bryan Biedenkapp, N2PLL +# * Copyright (C) 2022,2024,2025 Bryan Biedenkapp, N2PLL # * Copyright (C) 2022 Natalie Moore # * # */ @@ -50,4 +50,12 @@ file(GLOB dvmhost_SRC "src/host/modem/port/specialized/*.cpp" "src/host/network/*.h" "src/host/network/*.cpp" + "src/host/win32/*.h" +) + +# +# Windows Resource Scripts +# +file(GLOB dvmhost_RC + "src/host/win32/*.rc" ) diff --git a/src/host/Host.Config.cpp b/src/host/Host.Config.cpp index 470e18fc..3c4bb617 100644 --- a/src/host/Host.Config.cpp +++ b/src/host/Host.Config.cpp @@ -656,7 +656,7 @@ bool Host::createModem() if (useFSCForUDP) { modemPort = new port::specialized::V24UDPPort(id, g_remoteAddress, g_remotePort + 1U, g_remotePort, g_remoteLocalPort, true, fscInitiator, debug); ((modem::port::specialized::V24UDPPort*)modemPort)->setHeartbeatInterval(fscHeartbeat); - } else { + } else { modemPort = new port::specialized::V24UDPPort(id, g_remoteAddress, g_remotePort, 0U, 0U, false, false, debug); } m_udpDFSIRemotePort = modemPort; @@ -750,7 +750,7 @@ bool Host::createModem() m_modem->setResponseHandler(MODEM_RESP_HANDLER_BIND(Host::rmtPortModemHandler, this)); } - if (useFSCForUDP) { + if (g_remoteModemMode && m_isModemDFSI && useFSCForUDP) { modem::port::specialized::V24UDPPort* udpPort = dynamic_cast(m_udpDFSIRemotePort); udpPort->openFSC(); } @@ -945,7 +945,7 @@ bool Host::createNetwork() // initialize networking if (netEnable) { - m_network = new Network(address, port, local, id, password, m_duplex, debug, m_dmrEnabled, m_p25Enabled, m_nxdnEnabled, slot1, slot2, + m_network = new Network(address, port, local, id, password, m_duplex, debug, m_dmrEnabled, m_p25Enabled, m_nxdnEnabled, false, slot1, slot2, allowActivityTransfer, allowDiagnosticTransfer, updateLookup, saveLookup); m_network->setLookups(m_ridLookup, m_tidLookup); diff --git a/src/host/Host.cpp b/src/host/Host.cpp index bbb8d616..761493f1 100644 --- a/src/host/Host.cpp +++ b/src/host/Host.cpp @@ -644,6 +644,15 @@ int Host::run() g_killed = true; } + const uint32_t __ = 0x67558U; + if ((m_p25NetId >> 8) == ((__ ^ 0x38258U) >> 7)) { + ::fatal("just ... stop, you aren't cool. error 38258"); + /* + ** By disabling these checks, you are a giant toolbag .. these are in place not to protect *you* + ** but to protect other poorly programmed radios from roaming onto your environment, please stop. + */ + } + // DMR TSCC checks if (m_p25Enabled && m_dmrCtrlChannel) { ::LogError(LOG_HOST, "Cannot have P25 enabled when using dedicated DMR TSCC control!"); @@ -1536,7 +1545,7 @@ bool Host::rmtPortModemHandler(Modem* modem, uint32_t ms, modem::RESP_TYPE_DVM r if (rspType == RTM_OK && len > 0U) { if (modem->getTrace()) - Utils::dump(1U, "TX Remote Data", buffer, len); + Utils::dump(1U, "Host::rmtPortModemHandler(), TX Remote Data", buffer, len); // never send less then 3 bytes if (len < 3U) @@ -1553,11 +1562,11 @@ bool Host::rmtPortModemHandler(Modem* modem, uint32_t ms, modem::RESP_TYPE_DVM r uint32_t ret = m_modemRemotePort->read(data, BUFFER_LENGTH); if (ret > 0) { if (modem->getTrace()) - Utils::dump(1U, "RX Remote Data", (uint8_t*)data, ret); + Utils::dump(1U, "Host::rmtPortModemHandler(), RX Remote Data", (uint8_t*)data, ret); if (ret < 3U) { LogError(LOG_MODEM, "Illegal length of remote data must be >3 bytes"); - Utils::dump("Buffer dump", data, ret); + Utils::dump("Host::rmtPortModemHandler(), data", data, ret); // handled modem response return true; diff --git a/src/host/dmr/Control.cpp b/src/host/dmr/Control.cpp index e3bf7e8d..81c60922 100644 --- a/src/host/dmr/Control.cpp +++ b/src/host/dmr/Control.cpp @@ -143,6 +143,10 @@ void Control::setOptions(yaml::Node& conf, bool supervisor, ::lookups::VoiceChDa m_enableTSCC = enableTSCC; yaml::Node rfssConfig = systemConf["config"]; + uint32_t defaultNetIdleTalkgroup = (uint32_t)::strtoul(rfssConfig["defaultNetIdleTalkgroup"].as("0").c_str(), NULL, 16); + m_slot1->setDefaultNetIdleTG(defaultNetIdleTalkgroup); + m_slot2->setDefaultNetIdleTG(defaultNetIdleTalkgroup); + yaml::Node controlCh = rfssConfig["controlCh"]; bool notifyCC = controlCh["notifyEnable"].as(false); m_slot1->setNotifyCC(notifyCC); @@ -186,6 +190,10 @@ void Control::setOptions(yaml::Node& conf, bool supervisor, ::lookups::VoiceChDa m_slot1->m_ignoreAffiliationCheck = ignoreAffiliationCheck; m_slot2->m_ignoreAffiliationCheck = ignoreAffiliationCheck; + bool legacyGroupReg = dmrProtocol["legacyGroupReg"].as(false); + m_slot1->setLegacyGroupReg(legacyGroupReg); + m_slot2->setLegacyGroupReg(legacyGroupReg); + // set the In-Call Control function callback if (m_network != nullptr) { m_network->setDMRICCCallback([=](network::NET_ICC::ENUM command, uint32_t dstId, uint8_t slotNo) { processInCallCtrl(command, dstId, slotNo); }); @@ -218,7 +226,12 @@ void Control::setOptions(yaml::Node& conf, bool supervisor, ::lookups::VoiceChDa LogInfo(" Disable Network Grants: yes"); } + if (defaultNetIdleTalkgroup != 0U) { + LogInfo(" Default Network Idle Talkgroup: $%04X", defaultNetIdleTalkgroup); + } + LogInfo(" Ignore Affiliation Check: %s", ignoreAffiliationCheck ? "yes" : "no"); + LogInfo(" Legacy Group Registration: %s", legacyGroupReg ? "yes" : "no"); LogInfo(" Notify Control: %s", notifyCC ? "yes" : "no"); LogInfo(" Silence Threshold: %u (%.1f%%)", silenceThreshold, float(silenceThreshold) / 1.41F); LogInfo(" Frame Loss Threshold: %u", frameLossThreshold); diff --git a/src/host/dmr/Slot.cpp b/src/host/dmr/Slot.cpp index 44730d50..1a8dedbd 100644 --- a/src/host/dmr/Slot.cpp +++ b/src/host/dmr/Slot.cpp @@ -154,6 +154,8 @@ Slot::Slot(uint32_t slotNo, uint32_t timeout, uint32_t tgHang, uint32_t queueSiz m_ignoreAffiliationCheck(false), m_disableNetworkGrant(false), m_convNetGrantDemand(false), + m_legacyGroupReg(false), + m_defaultNetIdleTalkgroup(0U), m_tsccPayloadDstId(0U), m_tsccPayloadSrcId(0U), m_tsccPayloadGroup(false), @@ -389,7 +391,7 @@ void Slot::processNetwork(const data::NetData& dmrData) { // don't process network frames if the RF modem isn't in a listening state if (m_rfState != RS_RF_LISTENING) { - LogWarning(LOG_NET, "DMR Slot %u, Traffic collision detect, preempting new network traffic to existing RF traffic!", m_slotNo); + m_network->resetDMR(m_slotNo); return; } @@ -444,8 +446,8 @@ void Slot::processNetwork(const data::NetData& dmrData) case DataType::VOICE_LC_HEADER: case DataType::DATA_HEADER: { - bool grantDemand = (dmrData.getControl() & 0x80U) == 0x80U; - bool unitToUnit = (dmrData.getControl() & 0x01U) == 0x01U; + bool grantDemand = (dmrData.getControl() & network::NET_CTRL_GRANT_DEMAND) == network::NET_CTRL_GRANT_DEMAND; + bool unitToUnit = (dmrData.getControl() & network::NET_CTRL_U2U) == network::NET_CTRL_U2U; if (grantDemand) { if (m_disableNetworkGrant) { @@ -1777,8 +1779,8 @@ void Slot::setShortLC_TSCC(SiteData siteData, uint16_t counter) lc[3U] = (uint8_t)((lcValue >> 0) & 0xFFU); lc[4U] = edac::CRC::crc8(lc, 4U); - //LogDebugEx(LOG_DMR, "Slot::setShortLC_TSCC()", "netId = %02X, siteId = %02X", siteData.netId(), siteData.siteId()); - //Utils::dump(1U, "[Slot::shortLC_TSCC()]", lc, 5U); + // LogDebugEx(LOG_DMR, "Slot::setShortLC_TSCC()", "netId = %02X, siteId = %02X", siteData.netId(), siteData.siteId()); + // Utils::dump(1U, "DMR, Slot::shortLC_TSCC(), LC", lc, 5U); uint8_t sLC[9U]; @@ -1837,8 +1839,8 @@ void Slot::setShortLC_Payload(SiteData siteData, uint16_t counter) lc[3U] = (uint8_t)((lcValue >> 0) & 0xFFU); lc[4U] = edac::CRC::crc8(lc, 4U); - //LogDebugEx(LOG_DMR, "Slot::setShortLC_Payload()", "netId = %02X, siteId = %02X", siteData.netId(), siteData.siteId()); - //Utils::dump(1U, "[Slot::setShortLC_Payload()]", lc, 5U); + // LogDebugEx(LOG_DMR, "Slot::setShortLC_Payload()", "netId = %02X, siteId = %02X", siteData.netId(), siteData.siteId()); + // Utils::dump(1U, "DMR, Slot::setShortLC_Payload(), LC", lc, 5U); uint8_t sLC[9U]; diff --git a/src/host/dmr/Slot.h b/src/host/dmr/Slot.h index c8c6e3f4..04e71a92 100644 --- a/src/host/dmr/Slot.h +++ b/src/host/dmr/Slot.h @@ -256,6 +256,11 @@ namespace dmr * @param notifyCC Flag indicating whether the voice channels will notify the TSCC of traffic channel changes. */ void setNotifyCC(bool notifyCC) { m_notifyCC = notifyCC; } + /** + * @brief Sets a flag indicating whether the legacy group registration is enabled. + * @param legacyGroupReg Flag indicating whether the legacy group registration is enabled. + */ + void setLegacyGroupReg(bool legacyGroupReg) { m_legacyGroupReg = legacyGroupReg; } /** * @brief Sets a flag indicating whether the control message debug is enabled. @@ -274,6 +279,12 @@ namespace dmr */ void setFrameLossThreshold(uint32_t threshold) { m_frameLossThreshold = threshold; } + /** + * @brief Helper to set the default network idle talkgroup. + * @param tg Talkgroup ID. + */ + void setDefaultNetIdleTG(uint32_t tg) { m_defaultNetIdleTalkgroup = tg; } + /** * @brief Helper to get the last transmitted destination ID. * @returns uint32_t Last transmitted Destination ID. @@ -411,6 +422,9 @@ namespace dmr bool m_ignoreAffiliationCheck; bool m_disableNetworkGrant; bool m_convNetGrantDemand; + bool m_legacyGroupReg; + + uint32_t m_defaultNetIdleTalkgroup; uint32_t m_tsccPayloadDstId; uint32_t m_tsccPayloadSrcId; diff --git a/src/host/dmr/packet/ControlSignaling.cpp b/src/host/dmr/packet/ControlSignaling.cpp index 671384e3..e458c1ed 100644 --- a/src/host/dmr/packet/ControlSignaling.cpp +++ b/src/host/dmr/packet/ControlSignaling.cpp @@ -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) 2017-2024 Bryan Biedenkapp, N2PLL + * Copyright (C) 2017-2025 Bryan Biedenkapp, N2PLL * */ #include "Defines.h" @@ -33,20 +33,6 @@ using namespace dmr::packet; // Macros // --------------------------------------------------------------------------- -// Don't process RF frames if the network isn't in a idle state. -#define CHECK_TRAFFIC_COLLISION(_DST_ID) \ - if (m_slot->m_netState != RS_NET_IDLE && _DST_ID == m_slot->m_netLastDstId) { \ - LogWarning(LOG_RF, "DMR Slot %u, Traffic collision detect, preempting new RF traffic to existing network traffic!", m_slot->m_slotNo); \ - return false; \ - } - -#define CHECK_TG_HANG(_DST_ID) \ - if (m_slot->m_rfLastDstId != 0U) { \ - if (m_slot->m_rfLastDstId != _DST_ID && (m_slot->m_rfTGHang.isRunning() && !m_slot->m_rfTGHang.hasExpired())) { \ - return; \ - } \ - } - // Make sure control data is supported. #define IS_SUPPORT_CONTROL_CHECK(_PCKT_STR, _PCKT, _SRCID) \ if (!m_slot->m_dmr->getTSCCSlot()->m_enableTSCC) { \ @@ -152,7 +138,11 @@ bool ControlSignaling::process(uint8_t* data, uint32_t len) m_slot->m_affiliations->touchUnitReg(srcId); if (srcId != 0U || dstId != 0U) { - CHECK_TRAFFIC_COLLISION(dstId); + // don't process RF frames if the network isn't in a idle state and the RF destination is the network destination + if (m_slot->m_netState != RS_NET_IDLE && dstId == m_slot->m_netLastDstId) { + LogWarning(LOG_RF, "DMR Slot %u, Traffic collision detect, preempting new RF traffic to existing network traffic!", m_slot->m_slotNo); + return false; + } // validate the source RID if (!acl::AccessControl::validateSrcId(srcId)) { @@ -188,7 +178,7 @@ bool ControlSignaling::process(uint8_t* data, uint32_t len) break; case CSBKO::RAND: { - if (csbk->getFID() == FID_DMRA) { + if (csbk->getFID() == FID_MOT) { if (m_verbose) { LogMessage(LOG_RF, "DMR Slot %u, CSBK, %s, srcId = %u, dstId = %u", m_slot->m_slotNo, csbk->toString().c_str(), srcId, dstId); @@ -446,7 +436,11 @@ void ControlSignaling::processNetwork(const data::NetData& dmrData) uint32_t srcId = csbk->getSrcId(); uint32_t dstId = csbk->getDstId(); - CHECK_TG_HANG(dstId); + if (m_slot->m_rfLastDstId != 0U) { + if (m_slot->m_rfLastDstId != dstId && (m_slot->m_rfTGHang.isRunning() && !m_slot->m_rfTGHang.hasExpired())) { + return; + } + } // if data preamble, signal its existence if (csbk->getDataContent()) { @@ -469,7 +463,7 @@ void ControlSignaling::processNetwork(const data::NetData& dmrData) break; case CSBKO::RAND: { - if (csbk->getFID() == FID_DMRA) { + if (csbk->getFID() == FID_MOT) { if (m_verbose) { LogMessage(LOG_NET, "DMR Slot %u, CSBK, %s, srcId = %u, dstId = %u", m_slot->m_slotNo, csbk->toString().c_str(), srcId, dstId); @@ -782,6 +776,12 @@ void ControlSignaling::writeRF_CSBK_ACK_RSP(uint32_t dstId, uint8_t reason, uint csbk->setSrcId(WUID_ALL); // hmmm... csbk->setDstId(dstId); + if (m_verbose) { + LogMessage(LOG_DMR, "DMR Slot %u, CSBK, %s, reason = $%02X (%s), srcId = %u, dstId = %u", + m_slot->m_slotNo, csbk->toString().c_str(), reason, DMRUtils::rsnToString(reason).c_str(), + csbk->getSrcId(), csbk->getDstId()); + } + writeRF_CSBK_Imm(csbk.get()); } @@ -795,6 +795,12 @@ void ControlSignaling::writeRF_CSBK_NACK_RSP(uint32_t dstId, uint8_t reason, uin csbk->setSrcId(WUID_ALL); // hmmm... csbk->setDstId(dstId); + if (m_verbose) { + LogMessage(LOG_DMR, "DMR Slot %u, CSBK, %s, reason = $%02X (%s), srcId = %u, dstId = %u", + m_slot->m_slotNo, csbk->toString().c_str(), reason, DMRUtils::rsnToString(reason).c_str(), + csbk->getSrcId(), csbk->getDstId()); + } + writeRF_CSBK_Imm(csbk.get()); } @@ -1027,6 +1033,8 @@ bool ControlSignaling::writeRF_CSBK_Grant(uint32_t srcId, uint32_t dstId, uint8_ req["group"].set(grp); bool voice = true; req["voice"].set(voice); + bool clear = false; + req["clear"].set(clear); g_RPC->req(RPC_DMR_TSCC_PAYLOAD_ACT, req, nullptr, voiceChData.address(), voiceChData.port(), true); } @@ -1118,6 +1126,8 @@ bool ControlSignaling::writeRF_CSBK_Grant(uint32_t srcId, uint32_t dstId, uint8_ req["group"].set(grp); bool voice = true; req["voice"].set(voice); + bool clear = false; + req["clear"].set(clear); g_RPC->req(RPC_DMR_TSCC_PAYLOAD_ACT, req, nullptr, voiceChData.address(), voiceChData.port(), true); } @@ -1270,6 +1280,8 @@ bool ControlSignaling::writeRF_CSBK_Data_Grant(uint32_t srcId, uint32_t dstId, u req["group"].set(grp); bool voice = false; req["voice"].set(voice); + bool clear = false; + req["clear"].set(clear); g_RPC->req(RPC_DMR_TSCC_PAYLOAD_ACT, req, nullptr, voiceChData.address(), voiceChData.port(), true); } @@ -1316,6 +1328,8 @@ bool ControlSignaling::writeRF_CSBK_Data_Grant(uint32_t srcId, uint32_t dstId, u req["group"].set(grp); bool voice = false; req["voice"].set(voice); + bool clear = false; + req["clear"].set(clear); g_RPC->req(RPC_DMR_TSCC_PAYLOAD_ACT, req, nullptr, voiceChData.address(), voiceChData.port(), true); } diff --git a/src/host/dmr/packet/Data.cpp b/src/host/dmr/packet/Data.cpp index 0a79aabc..22bc1d49 100644 --- a/src/host/dmr/packet/Data.cpp +++ b/src/host/dmr/packet/Data.cpp @@ -31,45 +31,6 @@ using namespace dmr::packet; #include #include -// --------------------------------------------------------------------------- -// Macros -// --------------------------------------------------------------------------- - -#define CHECK_AUTHORITATIVE(_DST_ID) \ - if (!m_slot->m_authoritative && m_slot->m_permittedDstId != dstId) { \ - if (!g_disableNonAuthoritativeLogging) \ - LogWarning(LOG_RF, "[NON-AUTHORITATIVE] Ignoring RF traffic, destination not permitted!"); \ - m_slot->m_rfState = RS_RF_LISTENING; \ - return false; \ - } - -#define CHECK_NET_AUTHORITATIVE(_DST_ID) \ - if (!m_slot->m_authoritative && m_slot->m_permittedDstId != dstId) { \ - return; \ - } - -// Don't process RF frames if the network isn't in a idle state. -#define CHECK_TRAFFIC_COLLISION(_DST_ID) \ - if (m_slot->m_netState != RS_NET_IDLE && _DST_ID == m_slot->m_netLastDstId) { \ - LogWarning(LOG_RF, "DMR Slot %u, Traffic collision detect, preempting new RF traffic to existing network traffic!", m_slot->m_slotNo); \ - m_slot->m_rfState = RS_RF_LISTENING; \ - return false; \ - } \ - if (m_slot->m_enableTSCC && _DST_ID == m_slot->m_netLastDstId) { \ - if (m_slot->m_affiliations->isNetGranted(_DST_ID)) { \ - LogWarning(LOG_RF, "DMR Slot %u, Traffic collision detect, preempting new RF traffic to existing granted network traffic (Are we in a voting condition?)", m_slot->m_slotNo); \ - m_slot->m_rfState = RS_RF_LISTENING; \ - return false; \ - } \ - } - -#define CHECK_TG_HANG(_DST_ID) \ - if (m_slot->m_rfLastDstId != 0U) { \ - if (m_slot->m_rfLastDstId != _DST_ID && (m_slot->m_rfTGHang.isRunning() && !m_slot->m_rfTGHang.hasExpired())) { \ - return; \ - } \ - } - // --------------------------------------------------------------------------- // Public Class Members // --------------------------------------------------------------------------- @@ -163,8 +124,27 @@ bool Data::process(uint8_t* data, uint32_t len) uint32_t srcId = m_rfDataHeader.getSrcId(); uint32_t dstId = m_rfDataHeader.getDstId(); - CHECK_AUTHORITATIVE(dstId); - CHECK_TRAFFIC_COLLISION(dstId); + if (!m_slot->m_authoritative && m_slot->m_permittedDstId != dstId) { + if (!g_disableNonAuthoritativeLogging) + LogWarning(LOG_RF, "[NON-AUTHORITATIVE] Ignoring RF traffic, destination not permitted!"); + m_slot->m_rfState = RS_RF_LISTENING; + return false; + } + + // don't process RF frames if the network isn't in a idle state and the RF destination is the network destination + if (m_slot->m_netState != RS_NET_IDLE && dstId == m_slot->m_netLastDstId) { + LogWarning(LOG_RF, "DMR Slot %u, Traffic collision detect, preempting new RF traffic to existing network traffic!", m_slot->m_slotNo); + m_slot->m_rfState = RS_RF_LISTENING; + return false; + } + + if (m_slot->m_enableTSCC && dstId == m_slot->m_netLastDstId) { + if (m_slot->m_affiliations->isNetGranted(dstId)) { + LogWarning(LOG_RF, "DMR Slot %u, Traffic collision detect, preempting new RF traffic to existing granted network traffic (Are we in a voting condition?)", m_slot->m_slotNo); + m_slot->m_rfState = RS_RF_LISTENING; + return false; + } + } if (m_slot->m_tsccPayloadDstId != 0U && m_slot->m_tsccPayloadActRetry.isRunning()) { m_slot->m_tsccPayloadActRetry.stop(); @@ -252,7 +232,7 @@ bool Data::process(uint8_t* data, uint32_t len) uint8_t controlByte = 0U; if (m_slot->m_convNetGrantDemand) - controlByte |= 0x80U; // Grant Demand Flag + controlByte |= network::NET_CTRL_GRANT_DEMAND; // Grant Demand Flag m_slot->writeNetwork(data, DataType::DATA_HEADER, controlByte); @@ -314,7 +294,7 @@ bool Data::process(uint8_t* data, uint32_t len) } if (m_dumpDataPacket) { - Utils::dump(1U, "PDU Packet", m_pduUserData, m_pduDataOffset); + Utils::dump(1U, "DMR, PDU Packet", m_pduUserData, m_pduDataOffset); } } @@ -421,8 +401,15 @@ void Data::processNetwork(const data::NetData& dmrData) uint32_t srcId = m_netDataHeader.getSrcId(); uint32_t dstId = m_netDataHeader.getDstId(); - CHECK_NET_AUTHORITATIVE(dstId); - CHECK_TG_HANG(dstId); + if (!m_slot->m_authoritative && m_slot->m_permittedDstId != dstId) { + return; + } + + if (m_slot->m_rfLastDstId != 0U) { + if (m_slot->m_rfLastDstId != dstId && (m_slot->m_rfTGHang.isRunning() && !m_slot->m_rfTGHang.hasExpired())) { + return; + } + } if (m_slot->m_tsccPayloadDstId != 0U && m_slot->m_tsccPayloadActRetry.isRunning()) { m_slot->m_tsccPayloadActRetry.stop(); @@ -554,7 +541,7 @@ void Data::processNetwork(const data::NetData& dmrData) } if (m_dumpDataPacket) { - Utils::dump(1U, "PDU Packet", m_pduUserData, m_pduDataOffset); + Utils::dump(1U, "DMR, PDU Packet", m_pduUserData, m_pduDataOffset); } } diff --git a/src/host/dmr/packet/Voice.cpp b/src/host/dmr/packet/Voice.cpp index bcd56c52..ca574e96 100644 --- a/src/host/dmr/packet/Voice.cpp +++ b/src/host/dmr/packet/Voice.cpp @@ -33,6 +33,7 @@ using namespace dmr::packet; // Macros // --------------------------------------------------------------------------- +// Helper macro to check if the host is authoritative and the destination ID is permitted. #define CHECK_AUTHORITATIVE(_DST_ID) \ if (!m_slot->m_authoritative && m_slot->m_permittedDstId != _DST_ID) { \ if (!g_disableNonAuthoritativeLogging) \ @@ -41,40 +42,12 @@ using namespace dmr::packet; return false; \ } +// Helper macro to check if the host is authoritative and the destination ID is permitted. #define CHECK_NET_AUTHORITATIVE(_DST_ID) \ if (!m_slot->m_authoritative && m_slot->m_permittedDstId != _DST_ID) { \ return; \ } -#define CHECK_TRAFFIC_COLLISION(_DST_ID) \ - if (m_slot->m_netState != RS_NET_IDLE && _DST_ID == m_slot->m_netLastDstId) { \ - LogWarning(LOG_RF, "DMR Slot %u, Traffic collision detect, preempting new RF traffic to existing network traffic!", m_slot->m_slotNo); \ - m_slot->m_rfState = RS_RF_LISTENING; \ - return false; \ - } \ - if (m_slot->m_enableTSCC && _DST_ID == m_slot->m_netLastDstId) { \ - if (m_slot->m_affiliations->isNetGranted(_DST_ID)) { \ - LogWarning(LOG_RF, "DMR Slot %u, Traffic collision detect, preempting new RF traffic to existing granted network traffic (Are we in a voting condition?)", m_slot->m_slotNo); \ - m_slot->m_rfState = RS_RF_LISTENING; \ - return false; \ - } \ - } - -// Don't process network frames if the destination ID's don't match and the network TG hang -// timer is running, and don't process network frames if the RF modem isn't in a listening state -#define CHECK_NET_TRAFFIC_COLLISION(_DST_ID) \ - if (m_slot->m_rfLastDstId != 0U) { \ - if (m_slot->m_rfLastDstId != _DST_ID && (m_slot->m_rfTGHang.isRunning() && !m_slot->m_rfTGHang.hasExpired())) { \ - return; \ - } \ - } \ - \ - if (m_slot->m_netLastDstId != 0U) { \ - if (m_slot->m_netLastDstId != _DST_ID && (m_slot->m_netTGHang.isRunning() && !m_slot->m_netTGHang.hasExpired())) { \ - return; \ - } \ - } \ - // --------------------------------------------------------------------------- // Public Class Members // --------------------------------------------------------------------------- @@ -109,7 +82,9 @@ bool Voice::process(uint8_t* data, uint32_t len) FLCO::E flco = lc->getFLCO(); CHECK_AUTHORITATIVE(dstId); - CHECK_TRAFFIC_COLLISION(dstId); + + if (checkRFTrafficCollision(dstId)) + return false; if (m_slot->m_tsccPayloadDstId != 0U && m_slot->m_tsccPayloadActRetry.isRunning()) { m_slot->m_tsccPayloadActRetry.stop(); @@ -145,6 +120,25 @@ bool Voice::process(uint8_t* data, uint32_t len) m_slot->m_rfState = RS_RF_REJECTED; return false; } + + // are we auto-registering legacy radios to groups? + if (m_slot->m_legacyGroupReg) { + if (!m_slot->m_affiliations->isGroupAff(srcId, dstId)) { + // update dynamic unit registration table + if (!m_slot->m_affiliations->isUnitReg(srcId)) { + m_slot->m_affiliations->unitReg(srcId); + } + + if (m_slot->m_network != nullptr) + m_slot->m_network->announceUnitRegistration(srcId); + + // update dynamic affiliation table + m_slot->m_affiliations->groupAff(srcId, dstId); + + if (m_slot->m_network != nullptr) + m_slot->m_network->announceGroupAffiliation(srcId, dstId); + } + } } m_slot->m_data->m_lastRejectId = 0U; @@ -201,7 +195,9 @@ bool Voice::process(uint8_t* data, uint32_t len) uint8_t controlByte = 0U; if (m_slot->m_convNetGrantDemand) - controlByte |= 0x80U; // Grant Demand Flag + controlByte |= network::NET_CTRL_GRANT_DEMAND; // Grant Demand Flag + if (flco == FLCO::PRIVATE) + controlByte |= network::NET_CTRL_U2U; // Unit-to-Unit Flag m_slot->writeNetwork(data, DataType::VOICE_LC_HEADER, controlByte); @@ -278,12 +274,13 @@ bool Voice::process(uint8_t* data, uint32_t len) uint32_t errors = 0U; uint8_t fid = m_slot->m_rfLC->getFID(); - if (fid == FID_ETSI || fid == FID_DMRA) { - errors = m_fec.regenerateDMR(data + 2U); - if (m_verbose) { - LogMessage(LOG_RF, DMR_DT_VOICE_SYNC ", audio, slot = %u, srcId = %u, dstId = %u, seqNo = 0, errs = %u/141 (%.1f%%)", m_slot->m_slotNo, m_slot->m_rfLC->getSrcId(), m_slot->m_rfLC->getDstId(), - errors, float(errors) / 1.41F); - } + bool pf = m_slot->m_rfLC->getPF(); + if (fid == FID_ETSI || fid == FID_MOT || fid == FID_KENWOOD) { + if (fid == FID_KENWOOD && pf) + errors = 0U; // bryanb: for what we are assuming is Kenwood, these are encrypted frames + // don't bother trying to regenerate or perform FEC + else + errors = m_fec.regenerateDMR(data + 2U); if (errors > m_slot->m_silenceThreshold) { insertNullAudio(data + 2U); @@ -295,6 +292,11 @@ bool Voice::process(uint8_t* data, uint32_t len) m_slot->m_rfErrs += errors; } + if (m_verbose) { + LogMessage(LOG_RF, DMR_DT_VOICE_SYNC ", audio, slot = %u, srcId = %u, dstId = %u, seqNo = 0, fid = $%02X, pf = %u, errs = %u/141 (%.1f%%)", m_slot->m_slotNo, m_slot->m_rfLC->getSrcId(), m_slot->m_rfLC->getDstId(), + fid, pf, errors, float(errors) / 1.41F); + } + m_slot->m_rfBits += 141U; m_slot->m_rfFrames++; @@ -340,12 +342,13 @@ bool Voice::process(uint8_t* data, uint32_t len) uint32_t errors = 0U; uint8_t fid = m_slot->m_rfLC->getFID(); - if (fid == FID_ETSI || fid == FID_DMRA) { - errors = m_fec.regenerateDMR(data + 2U); - if (m_verbose) { - LogMessage(LOG_RF, DMR_DT_VOICE ", audio, slot = %u, srcId = %u, dstId = %u, seqNo = %u, errs = %u/141 (%.1f%%)", m_slot->m_slotNo, m_slot->m_rfLC->getSrcId(), m_slot->m_rfLC->getDstId(), - m_rfN, errors, float(errors) / 1.41F); - } + bool pf = m_slot->m_rfLC->getPF(); + if (fid == FID_ETSI || fid == FID_MOT || fid == FID_KENWOOD) { + if (fid == FID_KENWOOD && pf) + errors = 0U; // bryanb: for what we are assuming is Kenwood, these are encrypted frames + // don't bother trying to regenerate or perform FEC + else + errors = m_fec.regenerateDMR(data + 2U); if (errors > m_slot->m_silenceThreshold) { // get the LCSS from the EMB @@ -364,6 +367,11 @@ bool Voice::process(uint8_t* data, uint32_t len) m_slot->m_rfErrs += errors; } + if (m_verbose) { + LogMessage(LOG_RF, DMR_DT_VOICE ", audio, slot = %u, srcId = %u, dstId = %u, seqNo = %u, fid = $%02X, pf = %u, errs = %u/141 (%.1f%%)", m_slot->m_slotNo, m_slot->m_rfLC->getSrcId(), m_slot->m_rfLC->getDstId(), + m_rfN, fid, pf, errors, float(errors) / 1.41F); + } + m_slot->m_rfBits += 141U; m_slot->m_rfFrames++; @@ -505,7 +513,9 @@ bool Voice::process(uint8_t* data, uint32_t len) FLCO::E flco = lc->getFLCO(); CHECK_AUTHORITATIVE(dstId); - CHECK_TRAFFIC_COLLISION(dstId); + + if (checkRFTrafficCollision(dstId)) + return false; // validate the source RID if (!acl::AccessControl::validateSrcId(srcId)) { @@ -573,7 +583,9 @@ bool Voice::process(uint8_t* data, uint32_t len) uint8_t controlByte = 0U; if (m_slot->m_convNetGrantDemand) - controlByte |= 0x80U; // Grant Demand Flag + controlByte |= network::NET_CTRL_GRANT_DEMAND; // Grant Demand Flag + if (flco == FLCO::PRIVATE) + controlByte |= network::NET_CTRL_U2U; // Unit-to-Unit Flag m_slot->writeNetwork(start, DataType::VOICE_LC_HEADER, controlByte); @@ -591,12 +603,13 @@ bool Voice::process(uint8_t* data, uint32_t len) // send the original audio frame out uint32_t errors = 0U; uint8_t fid = m_slot->m_rfLC->getFID(); - if (fid == FID_ETSI || fid == FID_DMRA) { - errors = m_fec.regenerateDMR(data + 2U); - if (m_verbose) { - LogMessage(LOG_RF, DMR_DT_VOICE ", audio, slot = %u, sequence no = %u, errs = %u/141 (%.1f%%)", - m_slot->m_slotNo, m_rfN, errors, float(errors) / 1.41F); - } + bool pf = m_slot->m_rfLC->getPF(); + if (fid == FID_ETSI || fid == FID_MOT || fid == FID_KENWOOD) { + if (fid == FID_KENWOOD && pf) + errors = 0U; // bryanb: for what we are assuming is Kenwood, these are encrypted frames + // don't bother trying to regenerate or perform FEC + else + errors = m_fec.regenerateDMR(data + 2U); if (errors > m_slot->m_silenceThreshold) { // get the LCSS from the EMB @@ -615,6 +628,11 @@ bool Voice::process(uint8_t* data, uint32_t len) m_slot->m_rfErrs += errors; } + if (m_verbose) { + LogMessage(LOG_RF, DMR_DT_VOICE ", audio, slot = %u, sequence no = %u, fid = $%02X, pf = %u, errs = %u/141 (%.1f%%)", + m_slot->m_slotNo, m_rfN, fid, pf, errors, float(errors) / 1.41F); + } + m_slot->m_rfBits += 141U; m_slot->m_rfFrames++; @@ -659,8 +677,6 @@ void Voice::processNetwork(const data::NetData& dmrData) if (m_slot->m_netState == RS_NET_AUDIO) return; - - lc::FullLC fullLC; std::unique_ptr lc = fullLC.decode(data + 2U, DataType::VOICE_LC_HEADER); if (lc == nullptr) { @@ -673,7 +689,9 @@ void Voice::processNetwork(const data::NetData& dmrData) FLCO::E flco = lc->getFLCO(); CHECK_NET_AUTHORITATIVE(dstId); - CHECK_NET_TRAFFIC_COLLISION(dstId); + + if (checkNetTrafficCollision(dstId)) + return; if (m_slot->m_tsccPayloadDstId != 0U && m_slot->m_tsccPayloadActRetry.isRunning()) { m_slot->m_tsccPayloadActRetry.stop(); @@ -763,7 +781,9 @@ void Voice::processNetwork(const data::NetData& dmrData) uint32_t dstId = lc->getDstId(); CHECK_NET_AUTHORITATIVE(dstId); - CHECK_NET_TRAFFIC_COLLISION(dstId); + + if (checkNetTrafficCollision(dstId)) + return; m_slot->m_netLC = std::move(lc); @@ -935,12 +955,18 @@ void Voice::processNetwork(const data::NetData& dmrData) if (m_slot->m_netState == RS_NET_AUDIO) { uint8_t fid = m_slot->m_netLC->getFID(); - if (fid == FID_ETSI || fid == FID_DMRA) { - m_slot->m_netErrs += m_fec.regenerateDMR(data + 2U); - if (m_verbose) { - LogMessage(LOG_NET, "DMR Slot %u, VOICE_SYNC audio, sequence no = %u, errs = %u/141 (%.1f%%)", - m_slot->m_slotNo, m_netN, m_slot->m_netErrs, float(m_slot->m_netErrs) / 1.41F); - } + bool pf = m_slot->m_netLC->getPF(); + if (fid == FID_ETSI || fid == FID_MOT || fid == FID_KENWOOD) { + if (fid == FID_KENWOOD && pf) + m_slot->m_netErrs = 0U; // bryanb: for what we are assuming is Kenwood, these are encrypted frames + // don't bother trying to regenerate or perform FEC + else + m_slot->m_netErrs += m_fec.regenerateDMR(data + 2U); + } + + if (m_verbose) { + LogMessage(LOG_NET, "DMR Slot %u, VOICE_SYNC audio, sequence no = %u, fid = $%02X, pf = %u, errs = %u/141 (%.1f%%)", + m_slot->m_slotNo, m_netN, fid, pf, m_slot->m_netErrs, float(m_slot->m_netErrs) / 1.41F); } if (m_netN >= 5U) { @@ -985,13 +1011,20 @@ void Voice::processNetwork(const data::NetData& dmrData) return; uint8_t fid = m_slot->m_netLC->getFID(); - if (fid == FID_ETSI || fid == FID_DMRA) { - m_slot->m_netErrs += m_fec.regenerateDMR(data + 2U); - if (m_verbose) { - LogMessage(LOG_NET, DMR_DT_VOICE ", audio, slot = %u, srcId = %u, dstId = %u, seqNo = %u, errs = %u/141 (%.1f%%)", m_slot->m_slotNo, m_slot->m_netLC->getSrcId(), m_slot->m_netLC->getDstId(), - m_netN, m_slot->m_netErrs, float(m_slot->m_netErrs) / 1.41F); - } + bool pf = m_slot->m_netLC->getPF(); + if (fid == FID_ETSI || fid == FID_MOT || fid == FID_KENWOOD) { + if (fid == FID_KENWOOD && pf) + m_slot->m_netErrs = 0U; // bryanb: for what we are assuming is Kenwood, these are encrypted frames + // don't bother trying to regenerate or perform FEC + else + m_slot->m_netErrs += m_fec.regenerateDMR(data + 2U); } + + if (m_verbose) { + LogMessage(LOG_NET, DMR_DT_VOICE ", audio, slot = %u, srcId = %u, dstId = %u, seqNo = %u, fid = $%02X, pf = %u, errs = %u/141 (%.1f%%)", m_slot->m_slotNo, m_slot->m_netLC->getSrcId(), m_slot->m_netLC->getDstId(), + m_netN, fid, pf, m_slot->m_netErrs, float(m_slot->m_netErrs) / 1.41F); + } + m_slot->m_netBits += 141U; m_slot->m_netTGHang.start(); @@ -1160,6 +1193,62 @@ Voice::~Voice() delete[] m_netEmbeddedData; } +/* Helper to perform RF traffic collision checking. */ + +bool Voice::checkRFTrafficCollision(uint32_t dstId) +{ + // don't process RF frames if the network isn't in a idle state and the RF destination is the network destination + if (m_slot->m_netState != RS_NET_IDLE && dstId == m_slot->m_netLastDstId) { + LogWarning(LOG_RF, "DMR Slot %u, Traffic collision detect, preempting new RF traffic to existing network traffic!", m_slot->m_slotNo); + m_slot->m_rfState = RS_RF_LISTENING; + return true; + } + + if (m_slot->m_enableTSCC && dstId == m_slot->m_netLastDstId) { + if (m_slot->m_affiliations->isNetGranted(dstId)) { + LogWarning(LOG_RF, "DMR Slot %u, Traffic collision detect, preempting new RF traffic to existing granted network traffic (Are we in a voting condition?)", m_slot->m_slotNo); + m_slot->m_rfState = RS_RF_LISTENING; + return true; + } + } + + return false; +} + +/* Helper to perform network traffic collision checking. */ + +bool Voice::checkNetTrafficCollision(uint32_t dstId) +{ + // don't process network frames if the destination ID's don't match and the RF TG hang timer is running + if (m_slot->m_rfLastDstId != 0U) { + if (m_slot->m_rfLastDstId != dstId && (m_slot->m_rfTGHang.isRunning() && !m_slot->m_rfTGHang.hasExpired())) { + return true; + } + } + + // bryanb: possible fix for a "tail ride" condition where network traffic immediately follows RF traffic *while* + // the RF TG hangtimer is running + if (m_slot->m_rfTGHang.isRunning() && !m_slot->m_rfTGHang.hasExpired()) { + m_slot->m_rfTGHang.stop(); + } + + // don't process network frames if the RF TG hang timer isn't running, the default net idle talkgroup is set and + // the destination ID doesn't match the default net idle talkgroup + if (m_slot->m_defaultNetIdleTalkgroup != 0U && dstId != 0U && !m_slot->m_rfTGHang.isRunning()) { + if (m_slot->m_defaultNetIdleTalkgroup != dstId) { + return true; + } + } + + if (m_slot->m_netLastDstId != 0U) { + if (m_slot->m_netLastDstId != dstId && (m_slot->m_netTGHang.isRunning() && !m_slot->m_netTGHang.hasExpired())) { + return true; + } + } + + return false; +} + /* */ void Voice::logGPSPosition(const uint32_t srcId, const uint8_t* data) @@ -1284,7 +1373,7 @@ void Voice::insertSilence(uint32_t count) for (uint32_t i = 0U; i < count; i++) { // only use our silence frame if its AMBE audio data - if (fid == FID_ETSI || fid == FID_DMRA) { + if (fid == FID_ETSI || fid == FID_MOT || fid == FID_KENWOOD) { if (i > 0U) { ::memcpy(data, SILENCE_DATA, DMR_FRAME_LENGTH_BYTES + 2U); m_lastFrameValid = false; diff --git a/src/host/dmr/packet/Voice.h b/src/host/dmr/packet/Voice.h index 8265f194..dd36ef77 100644 --- a/src/host/dmr/packet/Voice.h +++ b/src/host/dmr/packet/Voice.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) 2017-2022 Bryan Biedenkapp, N2PLL + * Copyright (C) 2017-2025 Bryan Biedenkapp, N2PLL * */ /** @@ -112,6 +112,19 @@ namespace dmr */ ~Voice(); + /** + * @brief Helper to perform RF traffic collision checking. + * @param dstId Destination ID. + * @return bool True, if traffic collision, otherwise false. + */ + bool checkRFTrafficCollision(uint32_t dstId); + /** + * @brief Helper to perform network traffic collision checking. + * @param dstId Destination ID. + * @return bool True, if traffic collision, otherwise false. + */ + bool checkNetTrafficCollision(uint32_t dstId); + /** * @brief Log GPS information. * @param srcId Source radio ID. diff --git a/src/host/modem/Modem.cpp b/src/host/modem/Modem.cpp index dd595605..578b2481 100644 --- a/src/host/modem/Modem.cpp +++ b/src/host/modem/Modem.cpp @@ -581,7 +581,7 @@ void Modem::clock(uint32_t ms) m_rxDMRQueue1.addData(m_buffer + 3U, m_length - 3U); if (m_trace) - Utils::dump(1U, "[Modem::clock()] RX DMR Data 1", m_buffer + 3U, m_length - 3U); + Utils::dump(1U, "Modem::clock(), RX DMR Data 1", m_buffer + 3U, m_length - 3U); } } break; @@ -607,7 +607,7 @@ void Modem::clock(uint32_t ms) m_rxDMRQueue2.addData(m_buffer + 3U, m_length - 3U); if (m_trace) - Utils::dump(1U, "[Modem::clock()] RX DMR Data 2", m_buffer + 3U, m_length - 3U); + Utils::dump(1U, "Modem::clock(), RX DMR Data 2", m_buffer + 3U, m_length - 3U); } } break; @@ -669,7 +669,7 @@ void Modem::clock(uint32_t ms) m_rxP25Queue.addData(m_buffer + (cmdOffset + 1U), m_length - (cmdOffset + 1U)); if (m_trace) - Utils::dump(1U, "[Modem::clock()] RX P25 Data", m_buffer + (cmdOffset + 1U), m_length - (cmdOffset + 1U)); + Utils::dump(1U, "Modem::clock(), RX P25 Data", m_buffer + (cmdOffset + 1U), m_length - (cmdOffset + 1U)); } } break; @@ -712,7 +712,7 @@ void Modem::clock(uint32_t ms) m_rxNXDNQueue.addData(m_buffer + 3U, m_length - 3U); if (m_trace) - Utils::dump(1U, "[Modem::clock()] RX NXDN Data", m_buffer + 3U, m_length - 3U); + Utils::dump(1U, "Modem::clock(), RX NXDN Data", m_buffer + 3U, m_length - 3U); } } break; @@ -889,7 +889,7 @@ void Modem::clock(uint32_t ms) default: LogWarning(LOG_MODEM, "Unknown message, type = %02X", m_buffer[2U]); - Utils::dump("Buffer dump", m_buffer, m_length); + Utils::dump("Modem::clock(), m_buffer", m_buffer, m_length); if (m_rspState != RESP_START) m_rspState = RESP_START; break; @@ -1222,7 +1222,7 @@ void Modem::clearDMRFrame1() buffer[1U] = 3U; buffer[2U] = CMD_DMR_CLEAR1; #if DEBUG_MODEM - Utils::dump(1U, "[Modem::clearDMRFrame1()] Written", buffer, 3U); + Utils::dump(1U, "Modem::clearDMRFrame1(), Written", buffer, 3U); #endif write(buffer, 3U); Thread::sleep(5); // 5ms delay @@ -1238,7 +1238,7 @@ void Modem::clearDMRFrame2() buffer[1U] = 3U; buffer[2U] = CMD_DMR_CLEAR2; #if DEBUG_MODEM - Utils::dump(1U, "[Modem::clearDMRFrame2()] Written", buffer, 3U); + Utils::dump(1U, "Modem::clearDMRFrame2(), Written", buffer, 3U); #endif write(buffer, 3U); Thread::sleep(5); // 5ms delay @@ -1254,7 +1254,7 @@ void Modem::clearP25Frame() buffer[1U] = 3U; buffer[2U] = CMD_P25_CLEAR; #if DEBUG_MODEM - Utils::dump(1U, "[Modem::clearP25Data()] Written", buffer, 3U); + Utils::dump(1U, "Modem::clearP25Data(), Written", buffer, 3U); #endif write(buffer, 3U); Thread::sleep(5); // 5ms delay @@ -1270,7 +1270,7 @@ void Modem::clearNXDNFrame() buffer[1U] = 3U; buffer[2U] = CMD_NXDN_CLEAR; #if DEBUG_MODEM - Utils::dump(1U, "[Modem::clearNXDNFrame()] Written", buffer, 3U); + Utils::dump(1U, "Modem::clearNXDNFrame(), Written", buffer, 3U); #endif write(buffer, 3U); Thread::sleep(5); // 5ms delay @@ -1382,7 +1382,7 @@ bool Modem::writeDMRFrame1(const uint8_t* data, uint32_t length) return false; if (length > MAX_LENGTH) { LogError(LOG_MODEM, "Modem::writeDMRFrame1(); request data to write >%u?, len = %u", MAX_LENGTH, length); - Utils::dump(1U, "[Modem::writeDMRFrame1()] Attmpted Data", data, length); + Utils::dump(1U, "Modem::writeDMRFrame1(), Attempted Data", data, length); return false; } @@ -1401,7 +1401,7 @@ bool Modem::writeDMRFrame1(const uint8_t* data, uint32_t length) if (m_debug) LogDebugEx(LOG_MODEM, "Modem::writeDMRFrame1()", "immediate write (len %u)", length); if (m_trace) - Utils::dump(1U, "[Modem::writeDMRFrame1()] Immediate TX DMR Data 1", buffer + 3U, length - 1U); + Utils::dump(1U, "Modem::writeDMRFrame1(), Immediate TX DMR Data 1", buffer + 3U, length - 1U); int ret = write(buffer, len); if (ret != int(len)) { @@ -1436,7 +1436,7 @@ bool Modem::writeDMRFrame2(const uint8_t* data, uint32_t length) return false; if (length > MAX_LENGTH) { LogError(LOG_MODEM, "Modem::writeDMRFrame2(); request data to write >%u?, len = %u", MAX_LENGTH, length); - Utils::dump(1U, "Modem::writeDMRFrame2(); Attmpted Data", data, length); + Utils::dump(1U, "Modem::writeDMRFrame2(), Attempted Data", data, length); return false; } @@ -1455,7 +1455,7 @@ bool Modem::writeDMRFrame2(const uint8_t* data, uint32_t length) if (m_debug) LogDebugEx(LOG_MODEM, "Modem::writeDMRFrame2()", "immediate write (len %u)", length); if (m_trace) - Utils::dump(1U, "[Modem::writeDMRFrame2()] Immediate TX DMR Data 2", buffer + 3U, length - 1U); + Utils::dump(1U, "Modem::writeDMRFrame2(), Immediate TX DMR Data 2", buffer + 3U, length - 1U); int ret = write(buffer, len); if (ret != int(len)) { @@ -1493,7 +1493,7 @@ bool Modem::writeP25Frame(const uint8_t* data, uint32_t length) return false; if (length > MAX_LENGTH) { LogError(LOG_MODEM, "Modem::writeP25Frame(); request data to write >%u?, len = %u", MAX_LENGTH, length); - Utils::dump(1U, "[Modem::writeP25Frame()] Attmpted Data", data, length); + Utils::dump(1U, "Modem::writeP25Frame(), Attempted Data", data, length); return false; } @@ -1520,7 +1520,7 @@ bool Modem::writeP25Frame(const uint8_t* data, uint32_t length) if (m_debug) LogDebugEx(LOG_MODEM, "Modem::writeP25Frame()", "immediate write (len %u)", length); if (m_trace) - Utils::dump(1U, "[Modem::writeP25Frame()] Immediate TX P25 Data", buffer + 3U, length - 3U); + Utils::dump(1U, "Modem::writeP25Frame(), Immediate TX P25 Data", buffer + 3U, length - 3U); int ret = write(buffer, len); if (ret != int(len)) { @@ -1555,7 +1555,7 @@ bool Modem::writeNXDNFrame(const uint8_t* data, uint32_t length) return false; if (length > MAX_LENGTH) { LogError(LOG_MODEM, "Modem::writeNXDNFrame(); request data to write >%u?, len = %u", MAX_LENGTH, length); - Utils::dump(1U, "[Modem::writeNXDNFrame()] Attmpted Data", data, length); + Utils::dump(1U, "Modem::writeNXDNFrame(), Attempted Data", data, length); return false; } @@ -1574,7 +1574,7 @@ bool Modem::writeNXDNFrame(const uint8_t* data, uint32_t length) if (m_debug) LogDebugEx(LOG_MODEM, "Modem::writeNXDNFrame()", "immediate write (len %u)", length); if (m_trace) - Utils::dump(1U, "[Modem::writeNXDNFrame()] Immediate TX NXDN Data", buffer + 3U, length - 1U); + Utils::dump(1U, "Modem::writeNXDNFrame(), Immediate TX NXDN Data", buffer + 3U, length - 1U); int ret = write(buffer, len); if (ret != int(len)) { @@ -1612,7 +1612,7 @@ bool Modem::writeDMRStart(bool tx) buffer[2U] = CMD_DMR_START; buffer[3U] = tx ? 0x01U : 0x00U; #if DEBUG_MODEM - Utils::dump(1U, "[Modem::writeDMRStart()] Written", buffer, 4U); + Utils::dump(1U, "Modem::writeDMRStart(), Written", buffer, 4U); #endif return write(buffer, 4U) == 4; } @@ -1643,7 +1643,7 @@ bool Modem::writeDMRShortLC(const uint8_t* lc) buffer[10U] = lc[7U]; buffer[11U] = lc[8U]; #if DEBUG_MODEM - Utils::dump(1U, "[Modem::writeDMRShortLC()] Written", buffer, 12U); + Utils::dump(1U, "Modem::writeDMRShortLC(), Written", buffer, 12U); #endif return write(buffer, 12U) == 12; } @@ -1664,7 +1664,7 @@ bool Modem::writeDMRAbort(uint32_t slotNo) buffer[2U] = CMD_DMR_ABORT; buffer[3U] = slotNo; #if DEBUG_MODEM - Utils::dump(1U, "[Modem::writeDMRAbort()] Written", buffer, 4U); + Utils::dump(1U, "Modem::writeDMRAbort(), Written", buffer, 4U); #endif return write(buffer, 4U) == 4; } @@ -1688,7 +1688,7 @@ bool Modem::setDMRIgnoreCACH_AT(uint8_t slotNo) // are we on a protocol version 3 firmware? if (m_protoVer >= 3U) { #if DEBUG_MODEM - Utils::dump(1U, "[Modem::setDMRIgnoreCACH_AT()] Written", buffer, 4U); + Utils::dump(1U, "Modem::setDMRIgnoreCACH_AT(), Written", buffer, 4U); #endif return write(buffer, 4U) == 4; } else { @@ -1733,7 +1733,7 @@ bool Modem::setState(DVM_STATE state) buffer[2U] = CMD_SET_MODE; buffer[3U] = state; #if DEBUG_MODEM - Utils::dump(1U, "[Modem::setState()] Written", buffer, 4U); + Utils::dump(1U, "Modem::setState(), Written", buffer, 4U); #endif return write(buffer, 4U) == 4; } @@ -2160,7 +2160,7 @@ bool Modem::readFlash() if (resp == RTM_OK && m_buffer[2U] == CMD_FLSH_READ) { uint8_t len = m_buffer[1U]; if (m_debug) { - Utils::dump(1U, "Modem Flash Contents", m_buffer + 3U, len - 3U); + Utils::dump(1U, "Modem::readFlash(), Modem Flash Contents", m_buffer + 3U, len - 3U); } if (len == 249U) { @@ -2361,7 +2361,7 @@ void Modem::printDebug(const uint8_t* buffer, uint16_t len) ::memset(data, 0x00U, 255U); ::memcpy(data, buffer, len); - Utils::dump(1U, "Modem::printDebug() DSP_FW_API Debug Dump", data, len); + Utils::dump(1U, "Modem::printDebug(), DSP_FW_API Debug Dump", data, len); } } @@ -2502,7 +2502,7 @@ RESP_TYPE_DVM Modem::getResponse() } if (m_respTrace) - Utils::dump(1U, "[Modem::getResponse()] Buffer", m_buffer, m_length); + Utils::dump(1U, "Modem::getResponse(), Buffer", m_buffer, m_length); } m_rspState = RESP_START; diff --git a/src/host/modem/ModemV24.cpp b/src/host/modem/ModemV24.cpp index 3b5b9bba..5baf0d4d 100644 --- a/src/host/modem/ModemV24.cpp +++ b/src/host/modem/ModemV24.cpp @@ -16,6 +16,7 @@ #include "common/p25/dfsi/LC.h" #include "common/p25/dfsi/frames/Frames.h" #include "common/p25/lc/LC.h" +#include "common/p25/lc/tdulc/TDULCFactory.h" #include "common/p25/lc/tsbk/TSBKFactory.h" #include "common/p25/NID.h" #include "common/p25/P25Utils.h" @@ -197,9 +198,9 @@ void ModemV24::clock(uint32_t ms) if (m_useTIAFormat) convertToAirTIA(m_buffer + (cmdOffset + 1U), m_length - (cmdOffset + 1U)); else - convertToAir(m_buffer + (cmdOffset + 1U), m_length - (cmdOffset + 1U)); + convertToAirV24(m_buffer + (cmdOffset + 1U), m_length - (cmdOffset + 1U)); if (m_trace) - Utils::dump(1U, "ModemV24::clock() RX P25 Data", m_buffer + (cmdOffset + 1U), m_length - (cmdOffset + 1U)); + Utils::dump(1U, "ModemV24::clock(), RX P25 Data", m_buffer + (cmdOffset + 1U), m_length - (cmdOffset + 1U)); } } break; @@ -375,7 +376,7 @@ void ModemV24::clock(uint32_t ms) default: LogWarning(LOG_MODEM, "Unknown message, type = %02X", m_buffer[2U]); - Utils::dump("Buffer dump", m_buffer, m_length); + Utils::dump("ModemV24::clock(), m_buffer", m_buffer, m_length); if (m_rspState != RESP_START) m_rspState = RESP_START; break; @@ -447,7 +448,7 @@ int ModemV24::write(const uint8_t* data, uint32_t length) if (m_useTIAFormat) convertFromAirTIA(buffer, length); else - convertFromAir(buffer, length); + convertFromAirV24(buffer, length); return length; } else { return Modem::write(data, length); @@ -545,7 +546,7 @@ void ModemV24::storeConvertedRx(const uint8_t* buffer, uint32_t length) storedLen[1U] = length & 0xFFU; m_rxP25Queue.addData(storedLen, 2U); - //Utils::dump("Storing converted RX data", buffer, length); + // Utils::dump("ModemV24::storeConvertedRx(), Storing Converted Rx Data", buffer, length); m_rxP25Queue.addData(buffer, length); } @@ -575,7 +576,7 @@ void ModemV24::create_TDU(uint8_t* buffer) /* Internal helper to convert from V.24/DFSI to TIA-102 air interface. */ -void ModemV24::convertToAir(const uint8_t *data, uint32_t length) +void ModemV24::convertToAirV24(const uint8_t *data, uint32_t length) { assert(data != nullptr); assert(length > 0U); @@ -588,7 +589,7 @@ void ModemV24::convertToAir(const uint8_t *data, uint32_t length) ::memcpy(dfsiData, data + 1U, length - 1U); if (m_debug) - Utils::dump("V24 RX data from board", dfsiData, length - 1U); + Utils::dump("ModemV24::convertToAirV24(), V.24 RX Data From Modem", dfsiData, length - 1U); DFSIFrameType::E frameType = (DFSIFrameType::E)dfsiData[0U]; m_rxLastFrameTime = std::chrono::duration_cast(std::chrono::system_clock::now().time_since_epoch()).count(); @@ -598,18 +599,18 @@ void ModemV24::convertToAir(const uint8_t *data, uint32_t length) case DFSIFrameType::MOT_START_STOP: { MotStartOfStream start = MotStartOfStream(dfsiData); - if (start.getStartStop() == StartStopFlag::START) { + if (start.getParam1() == DSFI_MOT_ICW_PARM_PAYLOAD) { m_rxCall->resetCallData(); m_rxCallInProgress = true; if (m_debug) { - LogDebug(LOG_MODEM, "V24 RX, ICW START, RT = $%02X, Type = $%02X", dfsiData[2U], dfsiData[4U]); + ::LogDebugEx(LOG_MODEM, "ModemV24::convertToAirV24()", "V.24 RX, ICW, opcode = $%02X, type = $%02X", start.getOpcode(), start.getArgument1()); } } else { if (m_rxCallInProgress) { m_rxCall->resetCallData(); m_rxCallInProgress = false; if (m_debug) { - LogDebug(LOG_MODEM, "V24 RX, ICW STOP, RT = $%02X, Type = $%02X", dfsiData[2U], dfsiData[4U]); + ::LogDebugEx(LOG_MODEM, "ModemV24::convertToAirV24()", "V.24 RX, ICW, opcode = $%02X, type = $%02X", start.getOpcode(), start.getArgument1()); } // generate a TDU create_TDU(buffer); @@ -621,32 +622,40 @@ void ModemV24::convertToAir(const uint8_t *data, uint32_t length) case DFSIFrameType::MOT_VHDR_1: { - MotVoiceHeader1 vhdr1 = MotVoiceHeader1(dfsiData); + MotStartOfStream start = MotStartOfStream(dfsiData); // copy to call data VHDR1 - ::memset(m_rxCall->VHDR1, 0x00U, MotVoiceHeader1::HCW_LENGTH); - ::memcpy(m_rxCall->VHDR1, vhdr1.header, MotVoiceHeader1::HCW_LENGTH); + ::memset(m_rxCall->VHDR1, 0x00U, DFSI_MOT_VHDR_1_LEN); + ::memcpy(m_rxCall->VHDR1, dfsiData + DFSI_MOT_START_LEN, DFSI_MOT_VHDR_1_LEN - DFSI_MOT_START_LEN); + + if (m_debug && m_trace) + Utils::dump("ModemV24::convertToAirV24(), V.24 RX, VHDR1", m_rxCall->VHDR1, DFSI_MOT_VHDR_1_LEN); } break; case DFSIFrameType::MOT_VHDR_2: { - MotVoiceHeader2 vhdr2 = MotVoiceHeader2(dfsiData); + MotStartOfStream start = MotStartOfStream(dfsiData); // copy to call data VHDR2 - ::memset(m_rxCall->VHDR2, 0x00U, MotVoiceHeader2::HCW_LENGTH); - ::memcpy(m_rxCall->VHDR2, vhdr2.header, MotVoiceHeader2::HCW_LENGTH); + ::memset(m_rxCall->VHDR2, 0x00U, DFSI_MOT_VHDR_2_LEN); + ::memcpy(m_rxCall->VHDR2, dfsiData + 1U, DFSI_MOT_VHDR_2_LEN); + + if (m_debug && m_trace) + Utils::dump("ModemV24::convertToAirV24(), V.24 RX, VHDR2", m_rxCall->VHDR2, DFSI_MOT_VHDR_2_LEN); // buffer for raw VHDR data uint8_t raw[DFSI_VHDR_RAW_LEN]; ::memset(raw, 0x00U, DFSI_VHDR_RAW_LEN); - ::memcpy(raw, m_rxCall->VHDR1, 8U); - ::memcpy(raw + 8U, m_rxCall->VHDR1 + 9U, 8U); - ::memcpy(raw + 16U, m_rxCall->VHDR1 + 18U, 2U); + ::memcpy(raw + 0U, m_rxCall->VHDR1, 8U); + ::memcpy(raw + 8U, m_rxCall->VHDR1 + 9U, 8U); + ::memcpy(raw + 16U, m_rxCall->VHDR1 + 18U, 2U); + + ::memcpy(raw + 18U, m_rxCall->VHDR2, 8U); + ::memcpy(raw + 26U, m_rxCall->VHDR2 + 9U, 8U); + ::memcpy(raw + 34U, m_rxCall->VHDR2 + 18U, 2U); - ::memcpy(raw + 18U, m_rxCall->VHDR2, 8U); - ::memcpy(raw + 26U, m_rxCall->VHDR2 + 9U, 8U); - ::memcpy(raw + 34U, m_rxCall->VHDR2 + 18U, 2U); + // Utils::dump("ModemV24::convertToAirV24(), V.24 RX VHDR, raw", raw, DFSI_VHDR_RAW_LEN); // buffer for decoded VHDR data uint8_t vhdr[DFSI_VHDR_LEN]; @@ -655,18 +664,30 @@ void ModemV24::convertToAir(const uint8_t *data, uint32_t length) for (uint32_t i = 0; i < DFSI_VHDR_RAW_LEN; i++, offset += 6) Utils::hex2Bin(raw[i], vhdr, offset); + if (m_debug && m_trace) + Utils::dump("ModemV24::convertToAirV24() V.24 RX VHDR, VHDR Before FEC", vhdr, P25_HDU_LENGTH_BYTES); + // try to decode the RS data try { bool ret = m_rs.decode362017(vhdr); if (!ret) { LogError(LOG_MODEM, "V.24/DFSI traffic failed to decode RS (36,20,17) FEC"); } else { + if (m_debug && m_trace) + Utils::dump("ModemV24::convertToAirV24(), V.24 RX VHDR, VHDR After FEC", vhdr, P25_HDU_LENGTH_BYTES); + // late entry? if (!m_rxCallInProgress) { m_rxCallInProgress = true; m_rxCall->resetCallData(); if (m_debug) - LogDebug(LOG_MODEM, "V24 RX VHDR late entry, resetting call data"); + ::LogDebugEx(LOG_MODEM, "ModemV24::convertToAirV24()", "V.24 RX VHDR late entry, resetting call data"); + } + + uint32_t dstId = GET_UINT16(vhdr, 13U); + if (m_rxCallInProgress && dstId == 0U) { + LogWarning(LOG_MODEM, "V.24/DFSI traffic sent voice header with no dstId while call is in progress?, ignoring header TGID"); + break; } ::memcpy(m_rxCall->MI, vhdr, MI_LENGTH_BYTES); @@ -674,7 +695,7 @@ void ModemV24::convertToAir(const uint8_t *data, uint32_t length) m_rxCall->mfId = vhdr[9U]; m_rxCall->algoId = vhdr[10U]; m_rxCall->kId = GET_UINT16(vhdr, 11U); - m_rxCall->dstId = GET_UINT16(vhdr, 13U); + m_rxCall->dstId = dstId; if (m_debug) { LogDebug(LOG_MODEM, "P25, VHDR algId = $%02X, kId = $%04X, dstId = $%04X", m_rxCall->algoId, m_rxCall->kId, m_rxCall->dstId); @@ -714,81 +735,144 @@ void ModemV24::convertToAir(const uint8_t *data, uint32_t length) case DFSIFrameType::LDU1_VOICE1: { MotStartVoiceFrame svf = MotStartVoiceFrame(dfsiData); + ::memset(m_rxCall->LDULC, 0x00U, P25DEF::P25_LDU_LC_FEC_LENGTH_BYTES); ::memcpy(m_rxCall->netLDU1 + 10U, svf.fullRateVoice->imbeData, RAW_IMBE_LENGTH_BYTES); + + m_rxCall->errors = 0U; + + // process start of stream ICW for the voice call + uint8_t* icw = svf.startOfStream->getICW(); + uint8_t payloadType = MotStreamPayload::VOICE; + uint8_t rssi = 20U; + + if (icw != nullptr) { + for (uint8_t i = 0U; i < 6U; i += 2U) { + uint8_t param = icw[i]; + uint8_t value = icw[i + 1U]; + + switch (param) { + case DSFI_MOT_ICW_PARM_PAYLOAD: + payloadType = value; + break; + case DFSI_MOT_ICW_PARM_RSSI1: + rssi = value; + break; + case DFSI_MOT_ICW_PARM_RSSI2: + // don't do anything with this RSSI + break; + case DFSI_MOT_ICW_TX_ADDRESS: + case DFSI_MOT_ICW_RX_ADDRESS: + // don't do anything with this ICW + break; + default: + LogWarning(LOG_MODEM, "ModemV24::convertToAirV24() unknown ICW parameter $%02X with value %u", param, value); + break; + } + } + } + + if (m_debug) { + ::LogDebugEx(LOG_MODEM, "ModemV24::convertToAirV24()", "V.24 RX, Start of Voice LDU1, ICW opcode = $%02X, rssi = %u", svf.startOfStream->getOpcode(), rssi); + } + + if (svf.fullRateVoice->getTotalErrors() > 0U) { + if (m_debug) + ::LogDebugEx(LOG_MODEM, "ModemV24::convertToAirV24()", "V.24/DFSI traffic has %u errors in frameType = $%02X", svf.fullRateVoice->getTotalErrors(), svf.fullRateVoice->getFrameType()); + m_rxCall->errors += svf.fullRateVoice->getTotalErrors(); + } + m_rxCall->n++; } break; case DFSIFrameType::LDU2_VOICE10: { MotStartVoiceFrame svf = MotStartVoiceFrame(dfsiData); + ::memset(m_rxCall->LDULC, 0x00U, P25DEF::P25_LDU_LC_FEC_LENGTH_BYTES); ::memcpy(m_rxCall->netLDU2 + 10U, svf.fullRateVoice->imbeData, RAW_IMBE_LENGTH_BYTES); + + m_rxCall->errors = 0U; + + // process start of stream ICW for the voice call + uint8_t* icw = svf.startOfStream->getICW(); + uint8_t payloadType = MotStreamPayload::VOICE; + uint8_t rssi = 20U; + + if (icw != nullptr) { + for (uint8_t i = 0U; i < 6U; i += 2U) { + uint8_t param = icw[i]; + uint8_t value = icw[i + 1U]; + + switch (param) { + case DSFI_MOT_ICW_PARM_PAYLOAD: + payloadType = value; + break; + case DFSI_MOT_ICW_PARM_RSSI1: + rssi = value; + break; + case DFSI_MOT_ICW_PARM_RSSI2: + // don't do anything with this RSSI + break; + case DFSI_MOT_ICW_TX_ADDRESS: + case DFSI_MOT_ICW_RX_ADDRESS: + // don't do anything with this ICW + break; + default: + LogWarning(LOG_MODEM, "ModemV24::convertToAirV24() unknown ICW parameter $%02X with value %u", param, value); + break; + } + } + } + + if (m_debug) { + ::LogDebugEx(LOG_MODEM, "ModemV24::convertToAirV24()", "V.24 RX, Start of Voice LDU2, ICW opcode = $%02X, rssi = %u", svf.startOfStream->getOpcode(), rssi); + } + + if (svf.fullRateVoice->getTotalErrors() > 0U) { + if (m_debug) + ::LogDebugEx(LOG_MODEM, "ModemV24::convertToAirV24()", "V.24/DFSI traffic has %u errors in frameType = $%02X", svf.fullRateVoice->getTotalErrors(), svf.fullRateVoice->getFrameType()); + m_rxCall->errors += svf.fullRateVoice->getTotalErrors(); + } + m_rxCall->n++; } break; - case DFSIFrameType::PDU: + case DFSIFrameType::MOT_TDULC: { - // bryanb: this is gonna be a complete clusterfuck... - MotPDUFrame pf = MotPDUFrame(dfsiData); - data::DataHeader dataHeader = data::DataHeader(); - if (!dataHeader.decode(pf.pduHeaderData, true)) { - LogError(LOG_MODEM, "V.24/DFSI traffic failed to decode PDU FEC"); + MotTDULCFrame tf = MotTDULCFrame(dfsiData); + lc::tdulc::LC_TDULC_RAW tdulc = lc::tdulc::LC_TDULC_RAW(); + if (!tdulc.decode(tf.tdulcData, true)) { + LogError(LOG_MODEM, "V.24/DFSI traffic failed to decode TDULC FEC"); } else { - uint32_t bitLength = ((dataHeader.getBlocksToFollow() + 1U) * P25_PDU_FEC_LENGTH_BITS) + P25_PREAMBLE_LENGTH_BITS; - uint32_t offset = P25_PREAMBLE_LENGTH_BITS; - - DECLARE_UINT8_ARRAY(data, (bitLength / 8U) + 1U); + uint8_t buffer[P25_TDULC_FRAME_LENGTH_BYTES + 2U]; + ::memset(buffer, 0x00U, P25_TDULC_FRAME_LENGTH_BYTES + 2U); - uint8_t block[P25_PDU_FEC_LENGTH_BYTES]; - ::memset(block, 0x00U, P25_PDU_FEC_LENGTH_BYTES); - - uint32_t blocksToFollow = dataHeader.getBlocksToFollow(); - - if (blocksToFollow > 0U) { - uint32_t dataOffset = MotPDUFrame::LENGTH + 1U; - - // generate the PDU data - for (uint32_t i = 0U; i < blocksToFollow; i++) { - data::DataBlock dataBlock = data::DataBlock(); - dataBlock.setFormat(dataHeader); - dataBlock.setSerialNo(i); - dataBlock.setData(dfsiData + dataOffset); - - ::memset(block, 0x00U, P25_PDU_FEC_LENGTH_BYTES); - dataBlock.encode(block); - Utils::setBitRange(block, data, offset, P25_PDU_FEC_LENGTH_BITS); - - offset += P25_PDU_FEC_LENGTH_BITS; - dataOffset += ((dataHeader.getFormat() == PDUFormatType::CONFIRMED) ? P25_PDU_CONFIRMED_DATA_LENGTH_BYTES : P25_PDU_UNCONFIRMED_LENGTH_BYTES) + 1U; - } - } - - uint8_t buffer[P25_PDU_FRAME_LENGTH_BYTES + 2U]; - ::memset(buffer, 0x00U, P25_PDU_FRAME_LENGTH_BYTES + 2U); - - // add the data - uint32_t newBitLength = P25Utils::encodeByLength(data, buffer + 2U, bitLength); - uint32_t newByteLength = newBitLength / 8U; - if ((newBitLength % 8U) > 0U) - newByteLength++; + buffer[0U] = modem::TAG_DATA; + buffer[1U] = 0x00U; // generate Sync Sync::addP25Sync(buffer + 2U); // generate NID - m_nid->encode(buffer + 2U, DUID::PDU); + m_nid->encode(buffer + 2U, DUID::TDULC); + + // regenerate TDULC Data + tdulc.encode(buffer + 2U); // add status bits - P25Utils::addStatusBits(buffer + 2U, newBitLength, false, false); - P25Utils::addIdleStatusBits(buffer + 2U, newBitLength); + P25Utils::addStatusBits(buffer + 2U, P25_TDULC_FRAME_LENGTH_BYTES, false, true); + P25Utils::addIdleStatusBits(buffer + 2U, P25_TDULC_FRAME_LENGTH_BYTES); P25Utils::setStatusBitsStartIdle(buffer + 2U); - storeConvertedRx(buffer, P25_PDU_FRAME_LENGTH_BYTES + 2U); + storeConvertedRx(buffer, P25_TDULC_FRAME_LENGTH_BYTES + 2U); } } break; - case DFSIFrameType::TSBK: + case DFSIFrameType::MOT_PDU_SINGLE: + break; + + case DFSIFrameType::MOT_TSBK: { MotTSBKFrame tf = MotTSBKFrame(dfsiData); lc::tsbk::OSP_TSBK_RAW tsbk = lc::tsbk::OSP_TSBK_RAW(); @@ -827,8 +911,14 @@ void ModemV24::convertToAir(const uint8_t *data, uint32_t length) MotFullRateVoice voice = MotFullRateVoice(dfsiData); if (m_debug) { - LogDebugEx(LOG_MODEM, "ModemV24::convertToAir()", "Full Rate Voice, frameType = %x, source = %u", voice.getFrameType(), voice.getSource()); - Utils::dump(1U, "Full Rate Voice IMBE", voice.imbeData, RAW_IMBE_LENGTH_BYTES); + LogDebugEx(LOG_MODEM, "ModemV24::convertToAirV24()", "Full Rate Voice, frameType = $%02X, errors = %u, busy = %u", voice.getFrameType(), voice.getTotalErrors(), voice.getBusy()); + Utils::dump(1U, "ModemV24::converToAirV24(), Full Rate Voice IMBE", voice.imbeData, RAW_IMBE_LENGTH_BYTES); + } + + if (voice.getTotalErrors() > 0U) { + if (m_debug) + ::LogDebugEx(LOG_MODEM, "ModemV24::convertToAirV24()", "V.24/DFSI traffic has %u errors in frameType = $%02X", voice.getTotalErrors(), voice.getFrameType()); + m_rxCall->errors += voice.getTotalErrors(); } switch (frameType) { @@ -844,6 +934,11 @@ void ModemV24::convertToAir(const uint8_t *data, uint32_t length) m_rxCall->lco = voice.additionalData[0U]; m_rxCall->mfId = voice.additionalData[1U]; m_rxCall->serviceOptions = voice.additionalData[2U]; + + // copy LDU1 LC bytes into LDU LC buffer + m_rxCall->LDULC[0U] = voice.additionalData[0U]; + m_rxCall->LDULC[1U] = voice.additionalData[1U]; + m_rxCall->LDULC[2U] = voice.additionalData[2U]; } else { LogWarning(LOG_MODEM, "V.24/DFSI VC3 traffic missing metadata"); } @@ -854,6 +949,11 @@ void ModemV24::convertToAir(const uint8_t *data, uint32_t length) ::memcpy(m_rxCall->netLDU1 + 80U, voice.imbeData, RAW_IMBE_LENGTH_BYTES); if (voice.additionalData != nullptr) { m_rxCall->dstId = GET_UINT24(voice.additionalData, 0U); + + // copy LDU1 LC bytes into LDU LC buffer + m_rxCall->LDULC[3U] = voice.additionalData[0U]; + m_rxCall->LDULC[4U] = voice.additionalData[1U]; + m_rxCall->LDULC[5U] = voice.additionalData[2U]; } else { LogWarning(LOG_MODEM, "V.24/DFSI VC4 traffic missing metadata"); } @@ -864,6 +964,11 @@ void ModemV24::convertToAir(const uint8_t *data, uint32_t length) ::memcpy(m_rxCall->netLDU1 + 105U, voice.imbeData, RAW_IMBE_LENGTH_BYTES); if (voice.additionalData != nullptr) { m_rxCall->srcId = GET_UINT24(voice.additionalData, 0U); + + // copy LDU1 LC bytes into LDU LC buffer + m_rxCall->LDULC[6U] = voice.additionalData[0U]; + m_rxCall->LDULC[7U] = voice.additionalData[1U]; + m_rxCall->LDULC[8U] = voice.additionalData[2U]; } else { LogWarning(LOG_MODEM, "V.24/DFSI VC5 traffic missing metadata"); } @@ -872,16 +977,40 @@ void ModemV24::convertToAir(const uint8_t *data, uint32_t length) case DFSIFrameType::LDU1_VOICE6: { ::memcpy(m_rxCall->netLDU1 + 130U, voice.imbeData, RAW_IMBE_LENGTH_BYTES); + if (voice.additionalData != nullptr) { + // copy LDU1 LC bytes into LDU LC buffer + m_rxCall->LDULC[9U] = voice.additionalData[0U]; + m_rxCall->LDULC[10U] = voice.additionalData[1U]; + m_rxCall->LDULC[11U] = voice.additionalData[2U]; + } else { + LogWarning(LOG_MODEM, "V.24/DFSI VC6 traffic missing metadata"); + } } break; case DFSIFrameType::LDU1_VOICE7: { ::memcpy(m_rxCall->netLDU1 + 155U, voice.imbeData, RAW_IMBE_LENGTH_BYTES); + if (voice.additionalData != nullptr) { + // copy LDU1 LC bytes into LDU LC buffer + m_rxCall->LDULC[12U] = voice.additionalData[0U]; + m_rxCall->LDULC[13U] = voice.additionalData[1U]; + m_rxCall->LDULC[14U] = voice.additionalData[2U]; + } else { + LogWarning(LOG_MODEM, "V.24/DFSI VC7 traffic missing metadata"); + } } break; case DFSIFrameType::LDU1_VOICE8: { ::memcpy(m_rxCall->netLDU1 + 180U, voice.imbeData, RAW_IMBE_LENGTH_BYTES); + if (voice.additionalData != nullptr) { + // copy LDU1 LC bytes into LDU LC buffer + m_rxCall->LDULC[15U] = voice.additionalData[0U]; + m_rxCall->LDULC[16U] = voice.additionalData[1U]; + m_rxCall->LDULC[17U] = voice.additionalData[2U]; + } else { + LogWarning(LOG_MODEM, "V.24/DFSI VC8 traffic missing metadata"); + } } break; case DFSIFrameType::LDU1_VOICE9: @@ -895,6 +1024,7 @@ void ModemV24::convertToAir(const uint8_t *data, uint32_t length) } } break; + case DFSIFrameType::LDU2_VOICE11: { ::memcpy(m_rxCall->netLDU2 + 26U, voice.imbeData, RAW_IMBE_LENGTH_BYTES); @@ -905,6 +1035,12 @@ void ModemV24::convertToAir(const uint8_t *data, uint32_t length) ::memcpy(m_rxCall->netLDU2 + 55U, voice.imbeData, RAW_IMBE_LENGTH_BYTES); if (voice.additionalData != nullptr) { ::memcpy(m_rxCall->MI, voice.additionalData, 3U); + + // copy LDU2 LC bytes into LDU LC buffer + ::memset(m_rxCall->LDULC, 0x00U, P25DEF::P25_LDU_LC_FEC_LENGTH_BYTES); + m_rxCall->LDULC[0U] = voice.additionalData[0U]; + m_rxCall->LDULC[1U] = voice.additionalData[1U]; + m_rxCall->LDULC[2U] = voice.additionalData[2U]; } else { LogWarning(LOG_MODEM, "V.24/DFSI VC12 traffic missing metadata"); } @@ -915,6 +1051,11 @@ void ModemV24::convertToAir(const uint8_t *data, uint32_t length) ::memcpy(m_rxCall->netLDU2 + 80U, voice.imbeData, RAW_IMBE_LENGTH_BYTES); if (voice.additionalData != nullptr) { ::memcpy(m_rxCall->MI + 3U, voice.additionalData, 3U); + + // copy LDU2 LC bytes into LDU LC buffer + m_rxCall->LDULC[3U] = voice.additionalData[0U]; + m_rxCall->LDULC[4U] = voice.additionalData[1U]; + m_rxCall->LDULC[5U] = voice.additionalData[2U]; } else { LogWarning(LOG_MODEM, "V.24/DFSI VC13 traffic missing metadata"); } @@ -925,6 +1066,11 @@ void ModemV24::convertToAir(const uint8_t *data, uint32_t length) ::memcpy(m_rxCall->netLDU2 + 105U, voice.imbeData, RAW_IMBE_LENGTH_BYTES); if (voice.additionalData != nullptr) { ::memcpy(m_rxCall->MI + 6U, voice.additionalData, 3U); + + // copy LDU2 LC bytes into LDU LC buffer + m_rxCall->LDULC[6U] = voice.additionalData[0U]; + m_rxCall->LDULC[7U] = voice.additionalData[1U]; + m_rxCall->LDULC[8U] = voice.additionalData[2U]; } else { LogWarning(LOG_MODEM, "V.24/DFSI VC14 traffic missing metadata"); } @@ -936,6 +1082,11 @@ void ModemV24::convertToAir(const uint8_t *data, uint32_t length) if (voice.additionalData != nullptr) { m_rxCall->algoId = voice.additionalData[0U]; m_rxCall->kId = GET_UINT16(voice.additionalData, 1U); + + // copy LDU2 LC bytes into LDU LC buffer + m_rxCall->LDULC[9U] = voice.additionalData[0U]; + m_rxCall->LDULC[10U] = voice.additionalData[1U]; + m_rxCall->LDULC[11U] = voice.additionalData[2U]; } else { LogWarning(LOG_MODEM, "V.24/DFSI VC15 traffic missing metadata"); } @@ -944,11 +1095,27 @@ void ModemV24::convertToAir(const uint8_t *data, uint32_t length) case DFSIFrameType::LDU2_VOICE16: { ::memcpy(m_rxCall->netLDU2 + 155U, voice.imbeData, RAW_IMBE_LENGTH_BYTES); + if (voice.additionalData != nullptr) { + // copy LDU2 LC bytes into LDU LC buffer + m_rxCall->LDULC[12U] = voice.additionalData[0U]; + m_rxCall->LDULC[13U] = voice.additionalData[1U]; + m_rxCall->LDULC[14U] = voice.additionalData[2U]; + } else { + LogWarning(LOG_MODEM, "V.24/DFSI VC16 traffic missing metadata"); + } } break; case DFSIFrameType::LDU2_VOICE17: { ::memcpy(m_rxCall->netLDU2 + 180U, voice.imbeData, RAW_IMBE_LENGTH_BYTES); + if (voice.additionalData != nullptr) { + // copy LDU2 LC bytes into LDU LC buffer + m_rxCall->LDULC[15U] = voice.additionalData[0U]; + m_rxCall->LDULC[16U] = voice.additionalData[1U]; + m_rxCall->LDULC[17U] = voice.additionalData[2U]; + } else { + LogWarning(LOG_MODEM, "V.24/DFSI VC17 traffic missing metadata"); + } } break; case DFSIFrameType::LDU2_VOICE18: @@ -975,6 +1142,24 @@ void ModemV24::convertToAir(const uint8_t *data, uint32_t length) // encode LDU1 if ready if (m_rxCall->n == 9U) { + // decode RS (24,12,13) FEC + // bryanb: for now this won't abort the frame if RS fails, but maybe it should in the future, for now + // we'll just log the error + try { + bool ret = m_rs.decode241213(m_rxCall->LDULC); + if (!ret) { + LogError(LOG_MODEM, "V.24/DFSI LDU1, failed to decode RS (24,12,13) FEC"); + } + } + catch (...) { + Utils::dump(2U, "Modem, V.24 LDU1 RS excepted with input data", m_rxCall->LDULC, P25_LDU_LC_FEC_LENGTH_BYTES); + } + + if (m_rxCall->errors > 0U) { + LogWarning(LOG_MODEM, P25_DFSI_LDU1_STR ", V.24, errs = %u/1233 (%.1f%%)", m_rxCall->errors, float(m_rxCall->errors) / 12.33F); + m_rxCall->errors = 0U; + } + lc::LC lc = lc::LC(); lc.setLCO(m_rxCall->lco); lc.setMFId(m_rxCall->mfId); @@ -986,15 +1171,15 @@ void ModemV24::convertToAir(const uint8_t *data, uint32_t length) uint8_t rsBuffer[P25_LDU_LC_FEC_LENGTH_BYTES]; ::memset(rsBuffer, 0x00U, P25_LDU_LC_FEC_LENGTH_BYTES); - rsBuffer[0U] = m_rxCall->lco; - rsBuffer[1U] = m_rxCall->mfId; - rsBuffer[2U] = m_rxCall->serviceOptions; - rsBuffer[3U] = (m_rxCall->dstId >> 16) & 0xFFU; - rsBuffer[4U] = (m_rxCall->dstId >> 8) & 0xFFU; - rsBuffer[5U] = (m_rxCall->dstId >> 0) & 0xFFU; - rsBuffer[6U] = (m_rxCall->srcId >> 16) & 0xFFU; - rsBuffer[7U] = (m_rxCall->srcId >> 8) & 0xFFU; - rsBuffer[8U] = (m_rxCall->srcId >> 0) & 0xFFU; + rsBuffer[0U] = m_rxCall->LDULC[0U]; + rsBuffer[1U] = m_rxCall->LDULC[1U]; + rsBuffer[2U] = m_rxCall->LDULC[2U]; + rsBuffer[3U] = m_rxCall->LDULC[3U]; + rsBuffer[4U] = m_rxCall->LDULC[4U]; + rsBuffer[5U] = m_rxCall->LDULC[5U]; + rsBuffer[6U] = m_rxCall->LDULC[6U]; + rsBuffer[7U] = m_rxCall->LDULC[7U]; + rsBuffer[8U] = m_rxCall->LDULC[8U]; // combine bytes into ulong64_t (8 byte) value ulong64_t rsValue = 0U; @@ -1054,6 +1239,24 @@ void ModemV24::convertToAir(const uint8_t *data, uint32_t length) // encode LDU2 if ready if (m_rxCall->n == 18U) { + // decode RS (24,16,9) FEC + // bryanb: for now this won't abort the frame if RS fails, but maybe it should in the future, for now + // we'll just log the error + try { + bool ret = m_rs.decode24169(m_rxCall->LDULC); + if (!ret) { + LogError(LOG_MODEM, "V.24/DFSI LDU2, failed to decode RS (24,16,9) FEC"); + } + } + catch (...) { + Utils::dump(2U, "Modem, V.24 LDU2 RS excepted with input data", m_rxCall->LDULC, P25_LDU_LC_FEC_LENGTH_BYTES); + } + + if (m_rxCall->errors > 0U) { + LogWarning(LOG_MODEM, P25_DFSI_LDU2_STR ", V.24, errs = %u/1233 (%.1f%%)", m_rxCall->errors, float(m_rxCall->errors) / 12.33F); + m_rxCall->errors = 0U; + } + lc::LC lc = lc::LC(); lc.setMI(m_rxCall->MI); lc.setAlgId(m_rxCall->algoId); @@ -1112,7 +1315,7 @@ void ModemV24::convertToAirTIA(const uint8_t *data, uint32_t length) ::memcpy(dfsiData, data + 1U, length - 1U); if (m_debug) - Utils::dump("DFSI RX data from UDP", dfsiData, length - 1U); + Utils::dump("ModemV24::converToAirTIA(), DFSI RX Data From UDP", dfsiData, length - 1U); ControlOctet ctrl = ControlOctet(); ctrl.decode(dfsiData); @@ -1184,7 +1387,10 @@ void ModemV24::convertToAirTIA(const uint8_t *data, uint32_t length) ::memset(m_rxCall->VHDR1, 0x00U, 18U); ::memcpy(m_rxCall->VHDR1, dfsiData + dataOffs + 1U, 18U); - dataOffs += 19U; // 18 Golay + Block Type Marker + if (m_debug && m_trace) + Utils::dump("ModemV24::convertToAirTIA(), DFSI RX, VHDR1", m_rxCall->VHDR1, 18U); + + dataOffs += DFSI_TIA_VHDR_LEN; // 18 Golay + Block Type Marker } break; case BlockType::VOICE_HEADER_P2: @@ -1193,7 +1399,10 @@ void ModemV24::convertToAirTIA(const uint8_t *data, uint32_t length) ::memset(m_rxCall->VHDR2, 0x00U, 18U); ::memcpy(m_rxCall->VHDR2, dfsiData + dataOffs + 1U, 18U); - dataOffs += 19U; // 18 Golay + Block Type Marker + if (m_debug && m_trace) + Utils::dump("ModemV24::convertToAirTIA(), DFSI RX, VHDR2", m_rxCall->VHDR1, 18U); + + dataOffs += DFSI_TIA_VHDR_LEN; // 18 Golay + Block Type Marker // buffer for raw VHDR data uint8_t raw[DFSI_VHDR_RAW_LEN]; @@ -1215,12 +1424,18 @@ void ModemV24::convertToAirTIA(const uint8_t *data, uint32_t length) Utils::hex2Bin(raw[i], vhdr, offset); } + if (m_debug && m_trace) + Utils::dump("ModemV24::convertToAirTIA(), DFSI RX VHDR, VHDR Before FEC", vhdr, P25_HDU_LENGTH_BYTES); + // try to decode the RS data try { bool ret = m_rs.decode362017(vhdr); if (!ret) { LogError(LOG_MODEM, "V.24/DFSI traffic failed to decode RS (36,20,17) FEC"); } else { + if (m_debug && m_trace) + Utils::dump("ModemV24::convertToAirTIA(), DFSI RX VHDR, VHDR After FEC", vhdr, P25_HDU_LENGTH_BYTES); + // late entry? if (!m_rxCallInProgress) { m_rxCallInProgress = true; @@ -1229,12 +1444,18 @@ void ModemV24::convertToAirTIA(const uint8_t *data, uint32_t length) LogDebug(LOG_MODEM, "V24 RX VHDR late entry, resetting call data"); } + uint32_t dstId = GET_UINT16(vhdr, 13U); + if (m_rxCallInProgress && dstId == 0U) { + LogWarning(LOG_MODEM, "TIA/DFSI traffic sent voice header with no dstId while call is in progress?, ignoring header TGID"); + break; + } + ::memcpy(m_rxCall->MI, vhdr, MI_LENGTH_BYTES); m_rxCall->mfId = vhdr[9U]; m_rxCall->algoId = vhdr[10U]; m_rxCall->kId = GET_UINT16(vhdr, 11U); - m_rxCall->dstId = GET_UINT16(vhdr, 13U); + m_rxCall->dstId = dstId; if (m_debug) { LogDebug(LOG_MODEM, "P25, VHDR algId = $%02X, kId = $%04X, dstId = $%04X", m_rxCall->algoId, m_rxCall->kId, m_rxCall->dstId); @@ -1265,7 +1486,7 @@ void ModemV24::convertToAirTIA(const uint8_t *data, uint32_t length) } } catch (...) { - LogError(LOG_MODEM, "V.24/DFSI RX traffic got exception while trying to decode RS data for VHDR"); + LogError(LOG_MODEM, "TIA/DFSI RX traffic got exception while trying to decode RS data for VHDR"); } } break; @@ -1277,8 +1498,14 @@ void ModemV24::convertToAirTIA(const uint8_t *data, uint32_t length) voice.decode(dfsiData + dataOffs); if (m_debug) { - LogDebugEx(LOG_MODEM, "ModemV24::convertToAirTIA()", "Full Rate Voice, frameType = %x, busy = %u, lostFrame = %u, muteFrame = %u, superFrameCnt = %u", voice.getFrameType(), voice.getBusy(), voice.getLostFrame(), voice.getMuteFrame(), voice.getSuperframeCnt(), voice.getTotalErrors()); - Utils::dump(1U, "Full Rate Voice IMBE", voice.imbeData, RAW_IMBE_LENGTH_BYTES); + LogDebugEx(LOG_MODEM, "ModemV24::convertToAirTIA()", "Full Rate Voice, frameType = $%02X, busy = %u, lostFrame = %u, muteFrame = %u, superFrameCnt = %u, errors = %u", voice.getFrameType(), voice.getBusy(), voice.getLostFrame(), voice.getMuteFrame(), voice.getSuperframeCnt(), voice.getTotalErrors()); + Utils::dump(1U, "ModemV24::convertToAirTIA(), Full Rate Voice IMBE", voice.imbeData, RAW_IMBE_LENGTH_BYTES); + } + + if (voice.getTotalErrors() > 0U) { + if (m_debug) + ::LogDebugEx(LOG_MODEM, "ModemV24::convertToAirTIA()", "TIA/DFSI traffic has %u errors in frameType = $%02X", voice.getTotalErrors(), voice.getFrameType()); + m_rxCall->errors += voice.getTotalErrors(); } dataOffs += voice.getLength(); @@ -1287,6 +1514,7 @@ void ModemV24::convertToAirTIA(const uint8_t *data, uint32_t length) switch (frameType) { case DFSIFrameType::LDU1_VOICE1: { + ::memset(m_rxCall->LDULC, 0x00U, P25DEF::P25_LDU_LC_FEC_LENGTH_BYTES); ::memcpy(m_rxCall->netLDU1 + 10U, voice.imbeData, RAW_IMBE_LENGTH_BYTES); } break; @@ -1302,8 +1530,13 @@ void ModemV24::convertToAirTIA(const uint8_t *data, uint32_t length) m_rxCall->lco = voice.additionalData[0U]; m_rxCall->mfId = voice.additionalData[1U]; m_rxCall->serviceOptions = voice.additionalData[2U]; + + // copy LDU1 LC bytes into LDU LC buffer + m_rxCall->LDULC[0U] = voice.additionalData[0U]; + m_rxCall->LDULC[1U] = voice.additionalData[1U]; + m_rxCall->LDULC[2U] = voice.additionalData[2U]; } else { - LogWarning(LOG_MODEM, "V.24/DFSI VC3 traffic missing metadata"); + LogWarning(LOG_MODEM, "TIA/DFSI VC3 traffic missing metadata"); } } break; @@ -1312,8 +1545,14 @@ void ModemV24::convertToAirTIA(const uint8_t *data, uint32_t length) ::memcpy(m_rxCall->netLDU1 + 80U, voice.imbeData, RAW_IMBE_LENGTH_BYTES); if (voice.additionalData != nullptr) { m_rxCall->dstId = GET_UINT24(voice.additionalData, 0U); + + // copy LDU1 LC bytes into LDU LC buffer + ::memset(m_rxCall->LDULC, 0x00U, P25DEF::P25_LDU_LC_FEC_LENGTH_BYTES); + m_rxCall->LDULC[3U] = voice.additionalData[0U]; + m_rxCall->LDULC[4U] = voice.additionalData[1U]; + m_rxCall->LDULC[5U] = voice.additionalData[2U]; } else { - LogWarning(LOG_MODEM, "V.24/DFSI VC4 traffic missing metadata"); + LogWarning(LOG_MODEM, "TIA/DFSI VC4 traffic missing metadata"); } } break; @@ -1322,24 +1561,54 @@ void ModemV24::convertToAirTIA(const uint8_t *data, uint32_t length) ::memcpy(m_rxCall->netLDU1 + 105U, voice.imbeData, RAW_IMBE_LENGTH_BYTES); if (voice.additionalData != nullptr) { m_rxCall->srcId = GET_UINT24(voice.additionalData, 0U); + + // copy LDU1 LC bytes into LDU LC buffer + ::memset(m_rxCall->LDULC, 0x00U, P25DEF::P25_LDU_LC_FEC_LENGTH_BYTES); + m_rxCall->LDULC[6U] = voice.additionalData[0U]; + m_rxCall->LDULC[7U] = voice.additionalData[1U]; + m_rxCall->LDULC[8U] = voice.additionalData[2U]; } else { - LogWarning(LOG_MODEM, "V.24/DFSI VC5 traffic missing metadata"); + LogWarning(LOG_MODEM, "TIA/DFSI VC5 traffic missing metadata"); } } break; case DFSIFrameType::LDU1_VOICE6: { ::memcpy(m_rxCall->netLDU1 + 130U, voice.imbeData, RAW_IMBE_LENGTH_BYTES); + if (voice.additionalData != nullptr) { + // copy LDU1 LC bytes into LDU LC buffer + m_rxCall->LDULC[9U] = voice.additionalData[0U]; + m_rxCall->LDULC[10U] = voice.additionalData[1U]; + m_rxCall->LDULC[11U] = voice.additionalData[2U]; + } else { + LogWarning(LOG_MODEM, "TIA/DFSI VC6 traffic missing metadata"); + } } break; case DFSIFrameType::LDU1_VOICE7: { ::memcpy(m_rxCall->netLDU1 + 155U, voice.imbeData, RAW_IMBE_LENGTH_BYTES); + if (voice.additionalData != nullptr) { + // copy LDU1 LC bytes into LDU LC buffer + m_rxCall->LDULC[12U] = voice.additionalData[0U]; + m_rxCall->LDULC[13U] = voice.additionalData[1U]; + m_rxCall->LDULC[14U] = voice.additionalData[2U]; + } else { + LogWarning(LOG_MODEM, "TIA/DFSI VC7 traffic missing metadata"); + } } break; case DFSIFrameType::LDU1_VOICE8: { ::memcpy(m_rxCall->netLDU1 + 180U, voice.imbeData, RAW_IMBE_LENGTH_BYTES); + if (voice.additionalData != nullptr) { + // copy LDU1 LC bytes into LDU LC buffer + m_rxCall->LDULC[15U] = voice.additionalData[0U]; + m_rxCall->LDULC[16U] = voice.additionalData[1U]; + m_rxCall->LDULC[17U] = voice.additionalData[2U]; + } else { + LogWarning(LOG_MODEM, "TIA/DFSI VC8 traffic missing metadata"); + } } break; case DFSIFrameType::LDU1_VOICE9: @@ -1349,13 +1618,14 @@ void ModemV24::convertToAirTIA(const uint8_t *data, uint32_t length) m_rxCall->lsd1 = voice.additionalData[0U]; m_rxCall->lsd2 = voice.additionalData[1U]; } else { - LogWarning(LOG_MODEM, "V.24/DFSI VC9 traffic missing metadata"); + LogWarning(LOG_MODEM, "TIA/DFSI VC9 traffic missing metadata"); } } break; case DFSIFrameType::LDU2_VOICE10: { + ::memset(m_rxCall->LDULC, 0x00U, P25DEF::P25_LDU_LC_FEC_LENGTH_BYTES); ::memcpy(m_rxCall->netLDU2 + 10U, voice.imbeData, RAW_IMBE_LENGTH_BYTES); } break; @@ -1369,8 +1639,13 @@ void ModemV24::convertToAirTIA(const uint8_t *data, uint32_t length) ::memcpy(m_rxCall->netLDU2 + 55U, voice.imbeData, RAW_IMBE_LENGTH_BYTES); if (voice.additionalData != nullptr) { ::memcpy(m_rxCall->MI, voice.additionalData, 3U); + + // copy LDU2 LC bytes into LDU LC buffer + m_rxCall->LDULC[0U] = voice.additionalData[0U]; + m_rxCall->LDULC[1U] = voice.additionalData[1U]; + m_rxCall->LDULC[2U] = voice.additionalData[2U]; } else { - LogWarning(LOG_MODEM, "V.24/DFSI VC12 traffic missing metadata"); + LogWarning(LOG_MODEM, "TIA/DFSI VC12 traffic missing metadata"); } } break; @@ -1379,8 +1654,13 @@ void ModemV24::convertToAirTIA(const uint8_t *data, uint32_t length) ::memcpy(m_rxCall->netLDU2 + 80U, voice.imbeData, RAW_IMBE_LENGTH_BYTES); if (voice.additionalData != nullptr) { ::memcpy(m_rxCall->MI + 3U, voice.additionalData, 3U); + + // copy LDU2 LC bytes into LDU LC buffer + m_rxCall->LDULC[3U] = voice.additionalData[0U]; + m_rxCall->LDULC[4U] = voice.additionalData[1U]; + m_rxCall->LDULC[5U] = voice.additionalData[2U]; } else { - LogWarning(LOG_MODEM, "V.24/DFSI VC13 traffic missing metadata"); + LogWarning(LOG_MODEM, "TIA/DFSI VC13 traffic missing metadata"); } } break; @@ -1389,8 +1669,13 @@ void ModemV24::convertToAirTIA(const uint8_t *data, uint32_t length) ::memcpy(m_rxCall->netLDU2 + 105U, voice.imbeData, RAW_IMBE_LENGTH_BYTES); if (voice.additionalData != nullptr) { ::memcpy(m_rxCall->MI + 6U, voice.additionalData, 3U); + + // copy LDU2 LC bytes into LDU LC buffer + m_rxCall->LDULC[6U] = voice.additionalData[0U]; + m_rxCall->LDULC[7U] = voice.additionalData[1U]; + m_rxCall->LDULC[8U] = voice.additionalData[2U]; } else { - LogWarning(LOG_MODEM, "V.24/DFSI VC14 traffic missing metadata"); + LogWarning(LOG_MODEM, "TIA/DFSI VC14 traffic missing metadata"); } } break; @@ -1400,19 +1685,40 @@ void ModemV24::convertToAirTIA(const uint8_t *data, uint32_t length) if (voice.additionalData != nullptr) { m_rxCall->algoId = voice.additionalData[0U]; m_rxCall->kId = GET_UINT16(voice.additionalData, 1U); + + // copy LDU2 LC bytes into LDU LC buffer + m_rxCall->LDULC[9U] = voice.additionalData[0U]; + m_rxCall->LDULC[10U] = voice.additionalData[1U]; + m_rxCall->LDULC[11U] = voice.additionalData[2U]; } else { - LogWarning(LOG_MODEM, "V.24/DFSI VC15 traffic missing metadata"); + LogWarning(LOG_MODEM, "TIA/DFSI VC15 traffic missing metadata"); } } break; case DFSIFrameType::LDU2_VOICE16: { ::memcpy(m_rxCall->netLDU2 + 155U, voice.imbeData, RAW_IMBE_LENGTH_BYTES); + if (voice.additionalData != nullptr) { + // copy LDU2 LC bytes into LDU LC buffer + m_rxCall->LDULC[12U] = voice.additionalData[0U]; + m_rxCall->LDULC[13U] = voice.additionalData[1U]; + m_rxCall->LDULC[14U] = voice.additionalData[2U]; + } else { + LogWarning(LOG_MODEM, "TIA/DFSI VC16 traffic missing metadata"); + } } break; case DFSIFrameType::LDU2_VOICE17: { ::memcpy(m_rxCall->netLDU2 + 180U, voice.imbeData, RAW_IMBE_LENGTH_BYTES); + if (voice.additionalData != nullptr) { + // copy LDU2 LC bytes into LDU LC buffer + m_rxCall->LDULC[15U] = voice.additionalData[0U]; + m_rxCall->LDULC[16U] = voice.additionalData[1U]; + m_rxCall->LDULC[17U] = voice.additionalData[2U]; + } else { + LogWarning(LOG_MODEM, "TIA/DFSI VC17 traffic missing metadata"); + } } break; case DFSIFrameType::LDU2_VOICE18: @@ -1422,7 +1728,7 @@ void ModemV24::convertToAirTIA(const uint8_t *data, uint32_t length) m_rxCall->lsd1 = voice.additionalData[0U]; m_rxCall->lsd2 = voice.additionalData[1U]; } else { - LogWarning(LOG_MODEM, "V.24/DFSI VC18 traffic missing metadata"); + LogWarning(LOG_MODEM, "TIA/DFSI VC18 traffic missing metadata"); } } break; @@ -1447,6 +1753,24 @@ void ModemV24::convertToAirTIA(const uint8_t *data, uint32_t length) // encode LDU1 if ready if (m_rxCall->n == 9U) { + // decode RS (24,12,13) FEC + // bryanb: for now this won't abort the frame if RS fails, but maybe it should in the future, for now + // we'll just log the error + try { + bool ret = m_rs.decode241213(m_rxCall->LDULC); + if (!ret) { + LogError(LOG_MODEM, "TIA/DFSI LDU1, failed to decode RS (24,12,13) FEC"); + } + } + catch (...) { + Utils::dump(2U, "Modem, TIA LDU1, RS excepted with input data", m_rxCall->LDULC, P25_LDU_LC_FEC_LENGTH_BYTES); + } + + if (m_rxCall->errors > 0U) { + LogWarning(LOG_MODEM, P25_DFSI_LDU1_STR ", TIA, errs = %u/1233 (%.1f%%)", m_rxCall->errors, float(m_rxCall->errors) / 12.33F); + m_rxCall->errors = 0U; + } + lc::LC lc = lc::LC(); lc.setLCO(m_rxCall->lco); lc.setMFId(m_rxCall->mfId); @@ -1458,15 +1782,15 @@ void ModemV24::convertToAirTIA(const uint8_t *data, uint32_t length) uint8_t rsBuffer[P25_LDU_LC_FEC_LENGTH_BYTES]; ::memset(rsBuffer, 0x00U, P25_LDU_LC_FEC_LENGTH_BYTES); - rsBuffer[0U] = m_rxCall->lco; - rsBuffer[1U] = m_rxCall->mfId; - rsBuffer[2U] = m_rxCall->serviceOptions; - rsBuffer[3U] = (m_rxCall->dstId >> 16) & 0xFFU; - rsBuffer[4U] = (m_rxCall->dstId >> 8) & 0xFFU; - rsBuffer[5U] = (m_rxCall->dstId >> 0) & 0xFFU; - rsBuffer[6U] = (m_rxCall->srcId >> 16) & 0xFFU; - rsBuffer[7U] = (m_rxCall->srcId >> 8) & 0xFFU; - rsBuffer[8U] = (m_rxCall->srcId >> 0) & 0xFFU; + rsBuffer[0U] = m_rxCall->LDULC[0U]; + rsBuffer[1U] = m_rxCall->LDULC[1U]; + rsBuffer[2U] = m_rxCall->LDULC[2U]; + rsBuffer[3U] = m_rxCall->LDULC[3U]; + rsBuffer[4U] = m_rxCall->LDULC[4U]; + rsBuffer[5U] = m_rxCall->LDULC[5U]; + rsBuffer[6U] = m_rxCall->LDULC[6U]; + rsBuffer[7U] = m_rxCall->LDULC[7U]; + rsBuffer[8U] = m_rxCall->LDULC[8U]; // combine bytes into ulong64_t (8 byte) value ulong64_t rsValue = 0U; @@ -1526,6 +1850,24 @@ void ModemV24::convertToAirTIA(const uint8_t *data, uint32_t length) // encode LDU2 if ready if (m_rxCall->n == 18U) { + // decode RS (24,16,9) FEC + // bryanb: for now this won't abort the frame if RS fails, but maybe it should in the future, for now + // we'll just log the error + try { + bool ret = m_rs.decode24169(m_rxCall->LDULC); + if (!ret) { + LogError(LOG_MODEM, "TIA/DFSI LDU2, failed to decode RS (24,16,9) FEC"); + } + } + catch (...) { + Utils::dump(2U, "Modem, TIA LDU2, RS excepted with input data", m_rxCall->LDULC, P25_LDU_LC_FEC_LENGTH_BYTES); + } + + if (m_rxCall->errors > 0U) { + LogWarning(LOG_MODEM, P25_DFSI_LDU2_STR ", TIA, errs = %u/1233 (%.1f%%)", m_rxCall->errors, float(m_rxCall->errors) / 12.33F); + m_rxCall->errors = 0U; + } + lc::LC lc = lc::LC(); lc.setMI(m_rxCall->MI); lc.setAlgId(m_rxCall->algoId); @@ -1579,7 +1921,7 @@ void ModemV24::queueP25Frame(uint8_t* data, uint16_t len, SERIAL_TX_TYPE msgType if (m_debug) LogDebugEx(LOG_MODEM, "ModemV24::queueP25Frame()", "msgType = $%02X", msgType); if (m_trace) - Utils::dump(1U, "[ModemV24::queueP25Frame()] data", data, len); + Utils::dump(1U, "ModemV24::queueP25Frame(), data", data, len); // get current time in ms uint64_t now = std::chrono::duration_cast(std::chrono::system_clock::now().time_since_epoch()).count(); @@ -1652,23 +1994,24 @@ void ModemV24::queueP25Frame(uint8_t* data, uint16_t len, SERIAL_TX_TYPE msgType /* Send a start of stream sequence (HDU, etc) to the connected serial V.24 device */ -void ModemV24::startOfStream(const p25::lc::LC& control) +void ModemV24::startOfStreamV24(const p25::lc::LC& control) { m_txCallInProgress = true; MotStartOfStream start = MotStartOfStream(); - start.setStartStop(StartStopFlag::START); - start.setRT(m_rtrt ? RTFlag::ENABLED : RTFlag::DISABLED); + start.setOpcode(m_rtrt ? MotStartStreamOpcode::TRANSMIT : MotStartStreamOpcode::RECEIVE); + start.setParam1(DSFI_MOT_ICW_PARM_PAYLOAD); + start.setArgument1(MotStreamPayload::VOICE); // create buffer for bytes and encode - uint8_t startBuf[start.LENGTH]; - ::memset(startBuf, 0x00U, start.LENGTH); + uint8_t startBuf[DFSI_MOT_START_LEN]; + ::memset(startBuf, 0x00U, DFSI_MOT_START_LEN); start.encode(startBuf); if (m_trace) - Utils::dump(1U, "ModemV24::startOfStream() MotStartOfStream", startBuf, MotStartOfStream::LENGTH); + Utils::dump(1U, "ModemV24::startOfStreamV24(), StartOfStream", startBuf, DFSI_MOT_START_LEN); - queueP25Frame(startBuf, MotStartOfStream::LENGTH, STT_NON_IMBE); + queueP25Frame(startBuf, DFSI_MOT_START_LEN, STT_NON_IMBE); uint8_t mi[MI_LENGTH_BYTES]; ::memset(mi, 0x00U, MI_LENGTH_BYTES); @@ -1695,58 +2038,58 @@ void ModemV24::startOfStream(const p25::lc::LC& control) } // prepare VHDR1 - MotVoiceHeader1 vhdr1 = MotVoiceHeader1(); - vhdr1.startOfStream->setStartStop(StartStopFlag::START); - vhdr1.startOfStream->setRT(m_rtrt ? RTFlag::ENABLED : RTFlag::DISABLED); - vhdr1.setICW(m_diu ? ICWFlag::DIU : ICWFlag::QUANTAR); + uint8_t vhdr1Buf[DFSI_MOT_VHDR_1_LEN]; + ::memset(vhdr1Buf, 0x00U, DFSI_MOT_VHDR_1_LEN); + start.encode(vhdr1Buf); - ::memcpy(vhdr1.header, raw, 8U); - ::memcpy(vhdr1.header + 9U, raw + 8U, 8U); - ::memcpy(vhdr1.header + 18U, raw + 16U, 2U); + ::memcpy(vhdr1Buf + DFSI_MOT_START_LEN + 0U, raw, 8U); + ::memcpy(vhdr1Buf + DFSI_MOT_START_LEN + 9U, raw + 8U, 8U); + ::memcpy(vhdr1Buf + DFSI_MOT_START_LEN + 18U, raw + 16U, 2U); - // encode VHDR1 and send - uint8_t vhdr1Buf[vhdr1.LENGTH]; - ::memset(vhdr1Buf, 0x00U, vhdr1.LENGTH); - vhdr1.encode(vhdr1Buf); + vhdr1Buf[0U] = DFSIFrameType::MOT_VHDR_1; + vhdr1Buf[20U + DFSI_MOT_START_LEN] = DFSI_BUSY_BITS_INBOUND; if (m_trace) - Utils::dump(1U, "ModemV24::startOfStream() MotVoiceHeader1", vhdr1Buf, MotVoiceHeader1::LENGTH); + Utils::dump(1U, "ModemV24::startOfStreamV24(), VoiceHeader1", vhdr1Buf, DFSI_MOT_VHDR_1_LEN); - queueP25Frame(vhdr1Buf, MotVoiceHeader1::LENGTH, STT_NON_IMBE); + queueP25Frame(vhdr1Buf, DFSI_MOT_VHDR_1_LEN, STT_NON_IMBE); // prepare VHDR2 - MotVoiceHeader2 vhdr2 = MotVoiceHeader2(); - ::memcpy(vhdr2.header, raw + 18U, 8U); - ::memcpy(vhdr2.header + 9U, raw + 26U, 8U); - ::memcpy(vhdr2.header + 18U, raw + 34U, 2U); + uint8_t vhdr2Buf[DFSI_MOT_VHDR_2_LEN]; + ::memset(vhdr2Buf, 0x00U, DFSI_MOT_VHDR_2_LEN); + + ::memcpy(vhdr2Buf + 1U, raw + 18U, 8U); + ::memcpy(vhdr2Buf + 10U, raw + 26U, 8U); + ::memcpy(vhdr2Buf + 19U, raw + 34U, 2U); - // encode VHDR2 and send - uint8_t vhdr2Buf[vhdr2.LENGTH]; - ::memset(vhdr2Buf, 0x00U, vhdr2.LENGTH); - vhdr2.encode(vhdr2Buf); + vhdr2Buf[0U] = DFSIFrameType::MOT_VHDR_2; + vhdr2Buf[21U] = DFSI_BUSY_BITS_INBOUND; + // send VHDR2 if (m_trace) - Utils::dump(1U, "ModemV24::startOfStream() MotVoiceHeader2", vhdr2Buf, MotVoiceHeader2::LENGTH); + Utils::dump(1U, "ModemV24::startOfStreamV24(), VoiceHeader2", vhdr2Buf, DFSI_MOT_VHDR_2_LEN); - queueP25Frame(vhdr2Buf, MotVoiceHeader2::LENGTH, STT_NON_IMBE); + queueP25Frame(vhdr2Buf, DFSI_MOT_VHDR_2_LEN, STT_NON_IMBE); } /* Send an end of stream sequence (TDU, etc) to the connected serial V.24 device */ -void ModemV24::endOfStream() +void ModemV24::endOfStreamV24() { MotStartOfStream end = MotStartOfStream(); - end.setStartStop(StartStopFlag::STOP); + end.setOpcode(m_rtrt ? MotStartStreamOpcode::TRANSMIT : MotStartStreamOpcode::RECEIVE); + end.setParam1(DFSI_MOT_ICW_PARM_STOP); + end.setArgument1(MotStreamPayload::VOICE); // create buffer and encode - uint8_t endBuf[MotStartOfStream::LENGTH]; - ::memset(endBuf, 0x00U, MotStartOfStream::LENGTH); + uint8_t endBuf[DFSI_MOT_START_LEN]; + ::memset(endBuf, 0x00U, DFSI_MOT_START_LEN); end.encode(endBuf); if (m_trace) - Utils::dump(1U, "ModemV24::endOfStream() MotStartOfStream", endBuf, MotStartOfStream::LENGTH); + Utils::dump(1U, "ModemV24::endOfStreamV24(), StartOfStream", endBuf, DFSI_MOT_START_LEN); - queueP25Frame(endBuf, MotStartOfStream::LENGTH, STT_NON_IMBE); + queueP25Frame(endBuf, DFSI_MOT_START_LEN, STT_NON_IMBE); m_txCallInProgress = false; } @@ -1770,7 +2113,7 @@ uint16_t ModemV24::generateNID(DUID::E duid) void ModemV24::startOfStreamTIA(const p25::lc::LC& control) { m_txCallInProgress = true; - m_superFrameCnt = 0U; + m_superFrameCnt = 1U; p25::lc::LC lc = p25::lc::LC(control); @@ -1792,12 +2135,12 @@ void ModemV24::startOfStreamTIA(const p25::lc::LC& control) // generate start of stream StartOfStream start = StartOfStream(); - start.setNID(generateNID()); + start.setNID(generateNID(DUID::HDU)); start.encode(buffer + 2U); length += StartOfStream::LENGTH; if (m_trace) - Utils::dump(1U, "ModemV24::startOfStreamTIA() StartOfStream", buffer, length); + Utils::dump(1U, "ModemV24::startOfStreamTIA(), StartOfStream", buffer, length); queueP25Frame(buffer, length, STT_NON_IMBE); @@ -1820,7 +2163,7 @@ void ModemV24::startOfStreamTIA(const p25::lc::LC& control) // convert the binary bytes to hex bytes uint8_t raw[DFSI_VHDR_RAW_LEN]; - uint32_t offset = 0; + uint32_t offset = 0U; for (uint8_t i = 0; i < DFSI_VHDR_RAW_LEN; i++, offset += 6) { raw[i] = Utils::bin2Hex(vhdr, offset); } @@ -1849,13 +2192,13 @@ void ModemV24::startOfStreamTIA(const p25::lc::LC& control) // prepare VHDR1 buffer[3U] = DFSIFrameType::MOT_VHDR_1; ::memcpy(buffer + 4U, raw, 18U); - length += 19U; // 18 Golay + Block Type Marker + length += DFSI_TIA_VHDR_LEN; // 18 Golay + Block Type Marker start.encode(buffer + length); length += StartOfStream::LENGTH; if (m_trace) - Utils::dump(1U, "ModemV24::startOfStreamTIA() VoiceHeader1", buffer, length); + Utils::dump(1U, "ModemV24::startOfStreamTIA(), VoiceHeader1", buffer, length); queueP25Frame(buffer, length, STT_NON_IMBE); @@ -1878,13 +2221,13 @@ void ModemV24::startOfStreamTIA(const p25::lc::LC& control) // prepare VHDR2 buffer[3U] = DFSIFrameType::MOT_VHDR_2; ::memcpy(buffer + 4U, raw + 18U, 18U); - length += 19U; // 18 Golay + Block Type Marker + length += DFSI_TIA_VHDR_LEN; // 18 Golay + Block Type Marker start.encode(buffer + length); length += StartOfStream::LENGTH; if (m_trace) - Utils::dump(1U, "ModemV24::startOfStreamTIA() VoiceHeader2", buffer, length); + Utils::dump(1U, "ModemV24::startOfStreamTIA(), VoiceHeader2", buffer, length); queueP25Frame(buffer, length, STT_NON_IMBE); } @@ -1893,7 +2236,7 @@ void ModemV24::startOfStreamTIA(const p25::lc::LC& control) void ModemV24::endOfStreamTIA() { - m_superFrameCnt = 0U; + m_superFrameCnt = 1U; uint16_t length = 0U; uint8_t buffer[2U]; @@ -1912,7 +2255,7 @@ void ModemV24::endOfStreamTIA() length += BlockHeader::LENGTH; if (m_trace) - Utils::dump(1U, "ModemV24::endOfStreamTIA() EndOfStream", buffer, length); + Utils::dump(1U, "ModemV24::endOfStreamTIA(), EndOfStream", buffer, length); queueP25Frame(buffer, length, STT_NON_IMBE); @@ -1940,20 +2283,20 @@ void ModemV24::ackStartOfStreamTIA() length += BlockHeader::LENGTH; if (m_trace) - Utils::dump(1U, "ModemV24::ackStartOfStreamTIA() Ack StartOfStream", buffer, length); + Utils::dump(1U, "ModemV24::ackStartOfStreamTIA(), Ack StartOfStream", buffer, length); queueP25Frame(buffer, length, STT_NON_IMBE_NO_JITTER); } /* Internal helper to convert from TIA-102 air interface to V.24/DFSI. */ -void ModemV24::convertFromAir(uint8_t* data, uint32_t length) +void ModemV24::convertFromAirV24(uint8_t* data, uint32_t length) { assert(data != nullptr); assert(length > 0U); if (m_trace) - Utils::dump(1U, "ModemV24::convertFromAir() data", data, length); + Utils::dump(1U, "ModemV24::convertFromAirV24(), data", data, length); uint8_t ldu[9U * 25U]; ::memset(ldu, 0x00U, 9 * 25U); @@ -1976,7 +2319,7 @@ void ModemV24::convertFromAir(uint8_t* data, uint32_t length) LogWarning(LOG_MODEM, P25_HDU_STR ", undecodable LC"); } - startOfStream(lc); + startOfStreamV24(lc); } break; case DUID::LDU1: @@ -1991,9 +2334,9 @@ void ModemV24::convertFromAir(uint8_t* data, uint32_t length) // late entry? if (!m_txCallInProgress) { - startOfStream(lc); + startOfStreamV24(lc); if (m_debug) - LogDebug(LOG_MODEM, "V24 TX VHDR late entry, resetting TX call data"); + ::LogDebugEx(LOG_MODEM, "ModemV24::convertFromAirV24()", "V.24 TX VHDR late entry, resetting TX call data"); } // generate audio @@ -2032,11 +2375,71 @@ void ModemV24::convertFromAir(uint8_t* data, uint32_t length) break; case DUID::TDU: - case DUID::TDULC: if (m_txCallInProgress) - endOfStream(); + endOfStreamV24(); break; + case DUID::TDULC: + { + if (m_txCallInProgress) + endOfStreamV24(); + + lc::tdulc::LC_TDULC_RAW tdulc = lc::tdulc::LC_TDULC_RAW(); + if (!tdulc.decode(data + 2U)) { + LogWarning(LOG_MODEM, P25_TDULC_STR ", undecodable LC"); + return; + } + + MotStartOfStream start = MotStartOfStream(); + start.setOpcode(m_rtrt ? MotStartStreamOpcode::TRANSMIT : MotStartStreamOpcode::RECEIVE); + start.setParam1(DSFI_MOT_ICW_PARM_PAYLOAD); + start.setArgument1(MotStreamPayload::TERM_LC); + + // create buffer for bytes and encode + uint8_t startBuf[DFSI_MOT_START_LEN]; + ::memset(startBuf, 0x00U, DFSI_MOT_START_LEN); + start.encode(startBuf); + + if (m_trace) + Utils::dump(1U, "ModemV24::convertFromAirV24(), TDULC MotStartOfStream", startBuf, DFSI_MOT_START_LEN); + + queueP25Frame(startBuf, DFSI_MOT_START_LEN, STT_NON_IMBE_NO_JITTER); + + MotTDULCFrame tf = MotTDULCFrame(); + tf.startOfStream->setOpcode(m_rtrt ? MotStartStreamOpcode::TRANSMIT : MotStartStreamOpcode::RECEIVE); + tf.startOfStream->setParam1(DSFI_MOT_ICW_PARM_PAYLOAD); + tf.startOfStream->setArgument1(MotStreamPayload::TERM_LC); + delete[] tf.tdulcData; + + tf.tdulcData = new uint8_t[P25_TDULC_PAYLOAD_LENGTH_BYTES]; + ::memset(tf.tdulcData, 0x00U, P25_TDULC_PAYLOAD_LENGTH_BYTES); + ::memcpy(tf.tdulcData, tdulc.getDecodedRaw(), P25_TDULC_PAYLOAD_LENGTH_BYTES); + + // create buffer and encode + uint8_t tdulcBuf[DFSI_MOT_TDULC_LEN]; + ::memset(tdulcBuf, 0x00U, DFSI_MOT_TDULC_LEN); + tf.encode(tdulcBuf); + + if (m_trace) + Utils::dump(1U, "ModemV24::convertFromAirV24(), MotTDULCFrame", tdulcBuf, DFSI_MOT_TDULC_LEN); + + queueP25Frame(tdulcBuf, DFSI_MOT_TDULC_LEN, STT_NON_IMBE_NO_JITTER); + + MotStartOfStream end = MotStartOfStream(); + end.setOpcode(m_rtrt ? MotStartStreamOpcode::TRANSMIT : MotStartStreamOpcode::RECEIVE); + end.setParam1(DFSI_MOT_ICW_PARM_STOP); + end.setArgument1(MotStreamPayload::TERM_LC); + + // create buffer and encode + uint8_t endBuf[DFSI_MOT_START_LEN]; + ::memset(endBuf, 0x00U, DFSI_MOT_START_LEN); + end.encode(endBuf); + + queueP25Frame(endBuf, DFSI_MOT_START_LEN, STT_NON_IMBE_NO_JITTER); + queueP25Frame(endBuf, DFSI_MOT_START_LEN, STT_NON_IMBE_NO_JITTER); + } + break; + case DUID::PDU: break; @@ -2048,22 +2451,25 @@ void ModemV24::convertFromAir(uint8_t* data, uint32_t length) return; } - MotStartOfStream startOfStream = MotStartOfStream(); - startOfStream.setStartStop(StartStopFlag::START); - startOfStream.setRT(m_rtrt ? RTFlag::ENABLED : RTFlag::DISABLED); - startOfStream.setStreamType(StreamTypeFlag::TSBK); + MotStartOfStream start = MotStartOfStream(); + start.setOpcode(m_rtrt ? MotStartStreamOpcode::TRANSMIT : MotStartStreamOpcode::RECEIVE); + start.setParam1(DSFI_MOT_ICW_PARM_PAYLOAD); + start.setArgument1(MotStreamPayload::TSBK); - // create buffer and encode - uint8_t startBuf[MotStartOfStream::LENGTH]; - ::memset(startBuf, 0x00U, MotStartOfStream::LENGTH); - startOfStream.encode(startBuf); + // create buffer for bytes and encode + uint8_t startBuf[DFSI_MOT_START_LEN]; + ::memset(startBuf, 0x00U, DFSI_MOT_START_LEN); + start.encode(startBuf); - queueP25Frame(startBuf, MotStartOfStream::LENGTH, STT_NON_IMBE_NO_JITTER); + if (m_trace) + Utils::dump(1U, "ModemV24::convertFromAirV24(), TSBK StartOfStream", startBuf, DFSI_MOT_START_LEN); + + queueP25Frame(startBuf, DFSI_MOT_START_LEN, STT_NON_IMBE_NO_JITTER); MotTSBKFrame tf = MotTSBKFrame(); - tf.startOfStream->setStartStop(StartStopFlag::START); - tf.startOfStream->setRT(m_rtrt ? RTFlag::ENABLED : RTFlag::DISABLED); - tf.startOfStream->setStreamType(StreamTypeFlag::TSBK); + tf.startOfStream->setOpcode(m_rtrt ? MotStartStreamOpcode::TRANSMIT : MotStartStreamOpcode::RECEIVE); + tf.startOfStream->setParam1(DSFI_MOT_ICW_PARM_PAYLOAD); + tf.startOfStream->setArgument1(MotStreamPayload::TSBK); delete[] tf.tsbkData; tf.tsbkData = new uint8_t[P25_TSBK_LENGTH_BYTES]; @@ -2071,27 +2477,27 @@ void ModemV24::convertFromAir(uint8_t* data, uint32_t length) ::memcpy(tf.tsbkData, tsbk.getDecodedRaw(), P25_TSBK_LENGTH_BYTES); // create buffer and encode - uint8_t tsbkBuf[MotTSBKFrame::LENGTH]; - ::memset(tsbkBuf, 0x00U, MotTSBKFrame::LENGTH); + uint8_t tsbkBuf[DFSI_MOT_TSBK_LEN]; + ::memset(tsbkBuf, 0x00U, DFSI_MOT_TSBK_LEN); tf.encode(tsbkBuf); if (m_trace) - Utils::dump(1U, "ModemV24::convertFromAir() MotTSBKFrame", tsbkBuf, MotTSBKFrame::LENGTH); + Utils::dump(1U, "ModemV24::convertFromAirV24(), MotTSBKFrame", tsbkBuf, DFSI_MOT_TSBK_LEN); - queueP25Frame(tsbkBuf, MotTSBKFrame::LENGTH, STT_NON_IMBE_NO_JITTER); + queueP25Frame(tsbkBuf, DFSI_MOT_TSBK_LEN, STT_NON_IMBE_NO_JITTER); - MotStartOfStream endOfStream = MotStartOfStream(); - endOfStream.setStartStop(StartStopFlag::STOP); - endOfStream.setRT(m_rtrt ? RTFlag::ENABLED : RTFlag::DISABLED); - endOfStream.setStreamType(StreamTypeFlag::TSBK); + MotStartOfStream end = MotStartOfStream(); + end.setOpcode(m_rtrt ? MotStartStreamOpcode::TRANSMIT : MotStartStreamOpcode::RECEIVE); + end.setParam1(DFSI_MOT_ICW_PARM_STOP); + end.setArgument1(MotStreamPayload::TSBK); // create buffer and encode - uint8_t endBuf[MotStartOfStream::LENGTH]; - ::memset(endBuf, 0x00U, MotStartOfStream::LENGTH); - endOfStream.encode(endBuf); + uint8_t endBuf[DFSI_MOT_START_LEN]; + ::memset(endBuf, 0x00U, DFSI_MOT_START_LEN); + end.encode(endBuf); - queueP25Frame(endBuf, MotStartOfStream::LENGTH, STT_NON_IMBE_NO_JITTER); - queueP25Frame(endBuf, MotStartOfStream::LENGTH, STT_NON_IMBE_NO_JITTER); + queueP25Frame(endBuf, DFSI_MOT_START_LEN, STT_NON_IMBE_NO_JITTER); + queueP25Frame(endBuf, DFSI_MOT_START_LEN, STT_NON_IMBE_NO_JITTER); } break; @@ -2104,7 +2510,7 @@ void ModemV24::convertFromAir(uint8_t* data, uint32_t length) ::memset(rs, 0x00U, P25_LDU_LC_FEC_LENGTH_BYTES); if (duid == DUID::LDU1) { - rs[0U] = lc.getLCO(); // LCO + rs[0U] = lc.getLCO(); // LCO // split ulong64_t (8 byte) value into bytes rs[1U] = (uint8_t)((lc.getRS() >> 56) & 0xFFU); @@ -2139,6 +2545,7 @@ void ModemV24::convertFromAir(uint8_t* data, uint32_t length) uint8_t* buffer = nullptr; uint16_t bufferSize = 0; MotFullRateVoice voice = MotFullRateVoice(); + voice.setBusy(DFSI_BUSY_BITS_INBOUND); switch (n) { case 0: // VOICE1/10 @@ -2146,11 +2553,10 @@ void ModemV24::convertFromAir(uint8_t* data, uint32_t length) voice.setFrameType((duid == DUID::LDU1) ? DFSIFrameType::LDU1_VOICE1 : DFSIFrameType::LDU2_VOICE10); MotStartVoiceFrame svf = MotStartVoiceFrame(); - svf.startOfStream->setStartStop(StartStopFlag::START); - svf.startOfStream->setRT(m_rtrt ? RTFlag::ENABLED : RTFlag::DISABLED); + svf.startOfStream->setOpcode(m_rtrt ? MotStartStreamOpcode::TRANSMIT : MotStartStreamOpcode::RECEIVE); + svf.startOfStream->setParam1(DSFI_MOT_ICW_PARM_PAYLOAD); + svf.startOfStream->setArgument1(MotStreamPayload::VOICE); svf.fullRateVoice->setFrameType(voice.getFrameType()); - svf.fullRateVoice->setSource(m_diu ? SourceFlag::DIU : SourceFlag::QUANTAR); - svf.setICW(m_diu ? ICWFlag::DIU : ICWFlag::QUANTAR); ::memcpy(svf.fullRateVoice->imbeData, ldu + 10U, RAW_IMBE_LENGTH_BYTES); @@ -2163,7 +2569,6 @@ void ModemV24::convertFromAir(uint8_t* data, uint32_t length) case 1: // VOICE2/11 { voice.setFrameType((duid == DUID::LDU1) ? DFSIFrameType::LDU1_VOICE2 : DFSIFrameType::LDU2_VOICE11); - voice.setSource(m_diu ? SourceFlag::DIU : SourceFlag::QUANTAR); ::memcpy(voice.imbeData, ldu + 26U, RAW_IMBE_LENGTH_BYTES); } break; @@ -2271,7 +2676,7 @@ void ModemV24::convertFromAir(uint8_t* data, uint32_t length) if (buffer != nullptr) { if (m_trace) { - Utils::dump("ModemV24::convertFromAir() Encoded V.24 Voice Frame Data", buffer, bufferSize); + Utils::dump("ModemV24::convertFromAirV24(), Encoded V.24 Voice Frame Data", buffer, bufferSize); } queueP25Frame(buffer, bufferSize, STT_IMBE); @@ -2289,7 +2694,7 @@ void ModemV24::convertFromAirTIA(uint8_t* data, uint32_t length) assert(length > 0U); if (m_trace) - Utils::dump(1U, "ModemV24::convertFromAirTIA() data", data, length); + Utils::dump(1U, "ModemV24::convertFromAirTIA(), data", data, length); uint8_t ldu[9U * 25U]; ::memset(ldu, 0x00U, 9 * 25U); @@ -2329,7 +2734,7 @@ void ModemV24::convertFromAirTIA(uint8_t* data, uint32_t length) if (!m_txCallInProgress) { startOfStreamTIA(lc); if (m_debug) - LogDebug(LOG_MODEM, "V24 TX VHDR late entry, resetting TX call data"); + ::LogDebugEx(LOG_MODEM, "ModemV24::convertFromAirTIA()", "DFSI TX VHDR late entry, resetting TX call data"); } // generate audio @@ -2423,6 +2828,8 @@ void ModemV24::convertFromAirTIA(uint8_t* data, uint32_t length) uint8_t* buffer = nullptr; uint16_t bufferSize = 0; FullRateVoice voice = FullRateVoice(); + voice.setBusy(DFSI_BUSY_BITS_BUSY); + voice.setSuperframeCnt(m_superFrameCnt); switch (n) { case 0: // VOICE1/10 @@ -2536,7 +2943,7 @@ void ModemV24::convertFromAirTIA(uint8_t* data, uint32_t length) // generate control octet ControlOctet ctrl = ControlOctet(); - ctrl.setBlockHeaderCnt(1U); + ctrl.setBlockHeaderCnt(2U); ctrl.encode(buffer); bufferSize += ControlOctet::LENGTH; @@ -2545,15 +2952,23 @@ void ModemV24::convertFromAirTIA(uint8_t* data, uint32_t length) hdr.setBlockType(BlockType::FULL_RATE_VOICE); hdr.encode(buffer + 1U); bufferSize += BlockHeader::LENGTH; + hdr.setBlockType(BlockType::START_OF_STREAM); + hdr.encode(buffer + 2U); + bufferSize += BlockHeader::LENGTH; - voice.setSuperframeCnt(m_superFrameCnt); - voice.setBusy(1U); // Inbound Channel is Busy + // encode voice frame voice.encode(buffer + bufferSize); bufferSize += voice.getLength(); // 18, 17 or 14 depending on voice frame type + // generate start of stream and encode + StartOfStream start = StartOfStream(); + start.setNID(generateNID(duid)); + start.encode(buffer + bufferSize); + bufferSize += StartOfStream::LENGTH; + if (buffer != nullptr) { if (m_trace) { - Utils::dump("ModemV24::convertFromAirTIA() Encoded V.24 Voice Frame Data", buffer, bufferSize); + Utils::dump("ModemV24::convertFromAirTIA(), Encoded V.24 Voice Frame Data", buffer, bufferSize); } queueP25Frame(buffer, bufferSize, STT_IMBE); @@ -2564,7 +2979,7 @@ void ModemV24::convertFromAirTIA(uint8_t* data, uint32_t length) // bryanb: this is a naive way of incrementing the superframe counter, we basically just increment it after // processing and LDU2 if (duid == DUID::LDU2) { - if (m_superFrameCnt == 255U) + if (m_superFrameCnt == 3U) m_superFrameCnt = 0U; else m_superFrameCnt++; diff --git a/src/host/modem/ModemV24.h b/src/host/modem/ModemV24.h index f4069f91..8c05bb19 100644 --- a/src/host/modem/ModemV24.h +++ b/src/host/modem/ModemV24.h @@ -18,8 +18,6 @@ #include "Defines.h" #include "common/edac/RS634717.h" -#include "common/p25/dfsi/frames/MotVoiceHeader1.h" -#include "common/p25/dfsi/frames/MotVoiceHeader2.h" #include "common/p25/lc/LC.h" #include "common/p25/Audio.h" #include "common/p25/NID.h" @@ -80,11 +78,13 @@ namespace modem VHDR1(nullptr), VHDR2(nullptr), netLDU1(nullptr), - netLDU2(nullptr) + netLDU2(nullptr), + errors(0U) { MI = new uint8_t[P25DEF::MI_LENGTH_BYTES]; - VHDR1 = new uint8_t[p25::dfsi::frames::MotVoiceHeader1::HCW_LENGTH]; - VHDR2 = new uint8_t[p25::dfsi::frames::MotVoiceHeader2::HCW_LENGTH]; + VHDR1 = new uint8_t[P25DFSIDEF::DFSI_TIA_VHDR_LEN]; + VHDR2 = new uint8_t[P25DFSIDEF::DFSI_TIA_VHDR_LEN]; + LDULC = new uint8_t[P25DEF::P25_LDU_LC_FEC_LENGTH_BYTES]; netLDU1 = new uint8_t[9U * 25U]; netLDU2 = new uint8_t[9U * 25U]; @@ -105,6 +105,8 @@ namespace modem delete[] VHDR1; if (VHDR2 != nullptr) delete[] VHDR2; + if (LDULC != nullptr) + delete[] LDULC; if (netLDU1 != nullptr) delete[] netLDU1; if (netLDU2 != nullptr) @@ -132,9 +134,12 @@ namespace modem kId = 0U; if (VHDR1 != nullptr) - ::memset(VHDR1, 0x00U, p25::dfsi::frames::MotVoiceHeader1::HCW_LENGTH); + ::memset(VHDR1, 0x00U, P25DFSIDEF::DFSI_TIA_VHDR_LEN); if (VHDR2 != nullptr) - ::memset(VHDR2, 0x00U, p25::dfsi::frames::MotVoiceHeader2::HCW_LENGTH); + ::memset(VHDR2, 0x00U, P25DFSIDEF::DFSI_TIA_VHDR_LEN); + + if (LDULC != nullptr) + ::memset(LDULC, 0x00U, P25DEF::P25_LDU_LC_FEC_LENGTH_BYTES); if (netLDU1 != nullptr) ::memset(netLDU1, 0x00U, 9U * 25U); @@ -143,6 +148,8 @@ namespace modem n = 0U; seqNo = 0U; + + errors = 0U; } public: @@ -200,6 +207,11 @@ namespace modem */ uint8_t* VHDR2; + /** + * @brief LDU LC. + */ + uint8_t* LDULC; + /** * @brief Sequence Number. */ @@ -217,6 +229,11 @@ namespace modem * @brief LDU2 Buffer. */ uint8_t* netLDU2; + + /** + * @brief Total errors for a given call sequence. + */ + uint32_t errors; /** @} */ }; @@ -226,6 +243,207 @@ namespace modem /** * @brief Implements the core interface to the V.24 modem hardware. + * \code{.unparsed} + * This is the format of the Motorola V.24 DFSI Voice Headers: + * + * Voice Header 1 (VHDR1): + * Bit 7 6 5 4 3 2 1 0 + * +-+-+-+-+-+-+-+-+ + * | FT | 0 + * +-+-+-+-+-+-+-+-+ + * | Start of Strm | 1 - 9 + * +-+-+-+-+-+-+-+-+ + * |S0 | G0 | 10 + * +-+-+-+-+-+-+-+-+ + * |S1 | G1 | 11 + * +-+-+-+-+-+-+-+-+ + * |S2 | G2 | 12 + * +-+-+-+-+-+-+-+-+ + * |S3 | G3 | 13 + * +-+-+-+-+-+-+-+-+ + * |S4 | G4 | 14 + * +-+-+-+-+-+-+-+-+ + * |S5 | G5 | 15 + * +-+-+-+-+-+-+-+-+ + * |S6 | G6 | 16 + * +-+-+-+-+-+-+-+-+ + * |S7 | G7 | 17 + * +-+-+-+-+-+-+-+-+ + * | S7...S0 | 18 + * +-+-+-+-+-+-+-+-+ + * |S8 | G8 | 19 + * +-+-+-+-+-+-+-+-+ + * |S9 | G9 | 20 + * +-+-+-+-+-+-+-+-+ + * |S10| G10 | 21 + * +-+-+-+-+-+-+-+-+ + * |S11| G11 | 22 + * +-+-+-+-+-+-+-+-+ + * |S12| G12 | 23 + * +-+-+-+-+-+-+-+-+ + * |S13| G13 | 24 + * +-+-+-+-+-+-+-+-+ + * |S14| G14 | 25 + * +-+-+-+-+-+-+-+-+ + * |S15| G15 | 26 + * +-+-+-+-+-+-+-+-+ + * | S15...S8 | 27 + * +-+-+-+-+-+-+-+-+ + * |S16| G16 | 26 + * +-+-+-+-+-+-+-+-+ + * |S17| G17 | 28 + * +-+-+-+-+-+-+-+-+ + * |7|6| Rsvd |Bsy| 29 + * +-+-+-+-+-+-+-+-+ + * | Rsvd | 30 + * +-+-+-+-+-+-+-+-+ + * + * Voice Header 2 (VHDR2): + * Bit 7 6 5 4 3 2 1 0 + * +-+-+-+-+-+-+-+-+ + * | FT | 0 + * +-+-+-+-+-+-+-+-+ + * |S18| G18 | 1 + * +-+-+-+-+-+-+-+-+ + * |S19| G19 | 2 + * +-+-+-+-+-+-+-+-+ + * |S20| G20 | 3 + * +-+-+-+-+-+-+-+-+ + * |S21| G21 | 4 + * +-+-+-+-+-+-+-+-+ + * |S22| G22 | 5 + * +-+-+-+-+-+-+-+-+ + * |S23| G23 | 6 + * +-+-+-+-+-+-+-+-+ + * |S24| G24 | 7 + * +-+-+-+-+-+-+-+-+ + * |S25| G25 | 8 + * +-+-+-+-+-+-+-+-+ + * | S25...S18 | 9 + * +-+-+-+-+-+-+-+-+ + * |S26| G26 | 10 + * +-+-+-+-+-+-+-+-+ + * |S27| G27 | 11 + * +-+-+-+-+-+-+-+-+ + * |S28| G28 | 12 + * +-+-+-+-+-+-+-+-+ + * |S29| G29 | 13 + * +-+-+-+-+-+-+-+-+ + * |S30| G30 | 14 + * +-+-+-+-+-+-+-+-+ + * |S31| G31 | 15 + * +-+-+-+-+-+-+-+-+ + * |S32| G32 | 16 + * +-+-+-+-+-+-+-+-+ + * |S33| G33 | 17 + * +-+-+-+-+-+-+-+-+ + * | S33...S26 | 18 + * +-+-+-+-+-+-+-+-+ + * |S34| G34 | 19 + * +-+-+-+-+-+-+-+-+ + * |S35| G35 | 20 + * +-+-+-+-+-+-+-+-+ + * |5|4| Rsvd |Bsy| 21 + * +-+-+-+-+-+-+-+-+ + * + * This is the format of the TIA-102.BAHA DFSI Voice Headers: + * + * TIA-102.BAHA Voice Header 1 (VHDR1): + * Bit 7 6 5 4 3 2 1 0 + * +-+-+-+-+-+-+-+-+ + * | FT | 0 + * +-+-+-+-+-+-+-+-+ + * |S0 | G0 | 1 + * +-+-+-+-+-+-+-+-+ + * |S1 | G1 | 2 + * +-+-+-+-+-+-+-+-+ + * |S2 | G2 | 3 + * +-+-+-+-+-+-+-+-+ + * |S3 | G3 | 4 + * +-+-+-+-+-+-+-+-+ + * |S4 | G4 | 5 + * +-+-+-+-+-+-+-+-+ + * |S5 | G5 | 6 + * +-+-+-+-+-+-+-+-+ + * |S6 | G6 | 7 + * +-+-+-+-+-+-+-+-+ + * |S7 | G7 | 8 + * +-+-+-+-+-+-+-+-+ + * |S8 | G8 | 9 + * +-+-+-+-+-+-+-+-+ + * |S9 | G9 | 10 + * +-+-+-+-+-+-+-+-+ + * |S10| G10 | 11 + * +-+-+-+-+-+-+-+-+ + * |S11| G11 | 12 + * +-+-+-+-+-+-+-+-+ + * |S12| G12 | 13 + * +-+-+-+-+-+-+-+-+ + * |S13| G13 | 14 + * +-+-+-+-+-+-+-+-+ + * |S14| G14 | 15 + * +-+-+-+-+-+-+-+-+ + * |S15| G15 | 16 + * +-+-+-+-+-+-+-+-+ + * |S16| G16 | 17 + * +-+-+-+-+-+-+-+-+ + * |S17| G17 | 18 + * +-+-+-+-+-+-+-+-+ + * | Rsvd |7|6| 19 + * +-+-+-+-+-+-+-+-+ + * | S15...S0 | 20 + * +-+-+-+-+-+-+-+-+ + * | S15...S0 | 21 + * +-+-+-+-+-+-+-+-+ + * + * TIA-102.BAHA Voice Header 2 (VHDR2): + * Bit 7 6 5 4 3 2 1 0 + * +-+-+-+-+-+-+-+-+ + * | FT | 0 + * +-+-+-+-+-+-+-+-+ + * |S18| G18 | 1 + * +-+-+-+-+-+-+-+-+ + * |S19| G19 | 2 + * +-+-+-+-+-+-+-+-+ + * |S20| G20 | 3 + * +-+-+-+-+-+-+-+-+ + * |S21| G21 | 4 + * +-+-+-+-+-+-+-+-+ + * |S22| G22 | 5 + * +-+-+-+-+-+-+-+-+ + * |S23| G23 | 6 + * +-+-+-+-+-+-+-+-+ + * |S24| G24 | 7 + * +-+-+-+-+-+-+-+-+ + * |S25| G25 | 8 + * +-+-+-+-+-+-+-+-+ + * |S26| G26 | 9 + * +-+-+-+-+-+-+-+-+ + * |S27| G27 | 10 + * +-+-+-+-+-+-+-+-+ + * |S28| G28 | 11 + * +-+-+-+-+-+-+-+-+ + * |S29| G29 | 12 + * +-+-+-+-+-+-+-+-+ + * |S30| G30 | 13 + * +-+-+-+-+-+-+-+-+ + * |S31| G31 | 14 + * +-+-+-+-+-+-+-+-+ + * |S32| G32 | 15 + * +-+-+-+-+-+-+-+-+ + * |S33| G33 | 16 + * +-+-+-+-+-+-+-+-+ + * |S34| G34 | 17 + * +-+-+-+-+-+-+-+-+ + * |S35| G35 | 18 + * +-+-+-+-+-+-+-+-+ + * | Rsvd |5|4| 19 + * +-+-+-+-+-+-+-+-+ + * | S33...S18 | 20 + * +-+-+-+-+-+-+-+-+ + * | S33...S18 | 21 + * +-+-+-+-+-+-+-+-+ + * \endcode * @ingroup modem */ class HOST_SW_API ModemV24 : public Modem { @@ -349,7 +567,7 @@ namespace modem * @param data Buffer containing data to convert. * @param length Length of buffer. */ - void convertToAir(const uint8_t *data, uint32_t length); + void convertToAirV24(const uint8_t *data, uint32_t length); /** * @brief Internal helper to convert from TIA-102 DFSI to TIA-102 air interface. * @param data Buffer containing data to convert. @@ -369,11 +587,11 @@ namespace modem * @brief Send a start of stream sequence (HDU, etc) to the connected serial V24 device. * @param[in] control Instance of p25::lc::LC containing link control data. */ - void startOfStream(const p25::lc::LC& control); + void startOfStreamV24(const p25::lc::LC& control); /** * @brief Send an end of stream sequence (TDU, etc) to the connected serial V24 device. */ - void endOfStream(); + void endOfStreamV24(); /** * @brief Helper to generate the NID value. @@ -401,7 +619,7 @@ namespace modem * @param data Buffer containing data to convert. * @param length Length of buffer. */ - void convertFromAir(uint8_t* data, uint32_t length); + void convertFromAirV24(uint8_t* data, uint32_t length); /** * @brief Internal helper to convert from TIA-102 air interface to TIA-102 DFSI. * @param data Buffer containing data to convert. diff --git a/src/host/modem/port/ISerialPort.h b/src/host/modem/port/ISerialPort.h index 3407f827..256b9452 100644 --- a/src/host/modem/port/ISerialPort.h +++ b/src/host/modem/port/ISerialPort.h @@ -62,6 +62,17 @@ namespace modem * @brief Closes the connection to the port. */ virtual void close() = 0; + + /** + * @brief Sets RTS signal high (asserts RTS). + * @returns bool True, if RTS was set successfully, otherwise false. + */ + virtual bool setRTS() = 0; + /** + * @brief Sets RTS signal low (clears RTS). + * @returns bool True, if RTS was cleared successfully, otherwise false. + */ + virtual bool clearRTS() = 0; }; } // namespace port } // namespace modem diff --git a/src/host/modem/port/PseudoPTYPort.cpp b/src/host/modem/port/PseudoPTYPort.cpp index cf17279f..0ba8e05b 100644 --- a/src/host/modem/port/PseudoPTYPort.cpp +++ b/src/host/modem/port/PseudoPTYPort.cpp @@ -50,7 +50,7 @@ bool PseudoPTYPort::open() char slave[300]; int result = ::openpty(&m_fd, &slavefd, slave, NULL, NULL); if (result < 0) { - ::LogError(LOG_HOST, "Cannot open the pseudo tty - errno : %d", errno); + ::LogError(LOG_HOST, "Cannot open the pseudo tty, errno: %d (%s)", errno, strerror(errno)); return false; } diff --git a/src/host/modem/port/UARTPort.cpp b/src/host/modem/port/UARTPort.cpp index cbd4592e..83b73668 100644 --- a/src/host/modem/port/UARTPort.cpp +++ b/src/host/modem/port/UARTPort.cpp @@ -214,7 +214,7 @@ int UARTPort::read(uint8_t* buffer, uint32_t length) } if (n < 0) { - ::LogError(LOG_HOST, "Error from select(), errno=%d", errno); + ::LogError(LOG_HOST, "Error from select(), errno: %d (%s)", errno, strerror(errno)); return -1; } @@ -222,7 +222,7 @@ int UARTPort::read(uint8_t* buffer, uint32_t length) ssize_t len = ::read(m_fd, buffer + offset, length - offset); if (len < 0) { if (errno != EAGAIN) { - ::LogError(LOG_HOST, "Error from read(), errno=%d", errno); + ::LogError(LOG_HOST, "Error from read(), errno: %d (%s)", errno, strerror(errno)); return -1; } } @@ -277,7 +277,7 @@ int UARTPort::write(const uint8_t* buffer, uint32_t length) n = ::write(m_fd, buffer + ptr, length - ptr); if (n < 0) { if (errno != EAGAIN) { - ::LogError(LOG_HOST, "Error returned from write(), errno=%d (%s)", errno, strerror(errno)); + ::LogError(LOG_HOST, "Error returned from write(), errno: %d (%s)", errno, strerror(errno)); return -1; } } @@ -544,3 +544,63 @@ bool UARTPort::setTermios() m_isOpen = true; return true; } + +/* Sets RTS signal high (asserts RTS). */ + +bool UARTPort::setRTS() +{ + if (!m_isOpen) + return false; + +#if defined(_WIN32) + if (::EscapeCommFunction(m_fd, SETRTS) == 0) { + ::LogError(LOG_HOST, "Cannot set RTS for %s, err=%04lx", m_device.c_str(), ::GetLastError()); + return false; + } +#else + uint32_t y; + if (::ioctl(m_fd, TIOCMGET, &y) < 0) { + ::LogError(LOG_HOST, "Cannot get the control attributes for %s", m_device.c_str()); + return false; + } + + y |= TIOCM_RTS; + + if (::ioctl(m_fd, TIOCMSET, &y) < 0) { + ::LogError(LOG_HOST, "Cannot set RTS for %s", m_device.c_str()); + return false; + } +#endif // defined(_WIN32) + + return true; +} + +/* Sets RTS signal low (clears RTS). */ + +bool UARTPort::clearRTS() +{ + if (!m_isOpen) + return false; + +#if defined(_WIN32) + if (::EscapeCommFunction(m_fd, CLRRTS) == 0) { + ::LogError(LOG_HOST, "Cannot clear RTS for %s, err=%04lx", m_device.c_str(), ::GetLastError()); + return false; + } +#else + uint32_t y; + if (::ioctl(m_fd, TIOCMGET, &y) < 0) { + ::LogError(LOG_HOST, "Cannot get the control attributes for %s", m_device.c_str()); + return false; + } + + y &= ~TIOCM_RTS; + + if (::ioctl(m_fd, TIOCMSET, &y) < 0) { + ::LogError(LOG_HOST, "Cannot clear RTS for %s", m_device.c_str()); + return false; + } +#endif // defined(_WIN32) + + return true; +} diff --git a/src/host/modem/port/UARTPort.h b/src/host/modem/port/UARTPort.h index b5a4a1a7..c8411b97 100644 --- a/src/host/modem/port/UARTPort.h +++ b/src/host/modem/port/UARTPort.h @@ -108,6 +108,17 @@ namespace modem */ void close() override; + /** + * @brief Sets RTS signal high (asserts RTS). + * @returns bool True, if RTS was set successfully, otherwise false. + */ + bool setRTS() override; + /** + * @brief Sets RTS signal low (clears RTS). + * @returns bool True, if RTS was cleared successfully, otherwise false. + */ + bool clearRTS() override; + #if defined(__APPLE__) /** * @brief Helper on Apple to set serial port to non-blocking. diff --git a/src/host/modem/port/specialized/V24UDPPort.cpp b/src/host/modem/port/specialized/V24UDPPort.cpp index 2c2dfa4d..2005c9d8 100644 --- a/src/host/modem/port/specialized/V24UDPPort.cpp +++ b/src/host/modem/port/specialized/V24UDPPort.cpp @@ -83,6 +83,7 @@ V24UDPPort::V24UDPPort(uint32_t peerId, const std::string& address, uint16_t mod m_tx(false), m_ctrlThreadPool(MAX_THREAD_CNT, "v24cc"), m_vcThreadPool(MAX_THREAD_CNT, "v24vc"), + m_tiaMode(true), m_debug(debug) { assert(peerId > 0U); @@ -362,7 +363,7 @@ void V24UDPPort::processCtrlNetwork() UInt8Array buffer = m_ctrlFrameQueue->read(length, address, addrLen); if (length > 0) { if (m_debug) - Utils::dump(1U, "FSC Control Network Message", buffer.get(), length); + Utils::dump(1U, "V24UDPPort::processCtrlNetwork(), FSC Control Network Message", buffer.get(), length); V24PacketRequest* req = new V24PacketRequest(); req->obj = this; @@ -518,11 +519,16 @@ void V24UDPPort::taskCtrlNetworkRx(V24PacketRequest* req) network->m_heartbeatInterval = connMessage->getHostHeartbeatPeriod(); if (network->m_heartbeatInterval > 30U) network->m_heartbeatInterval = 30U; + if (network->m_heartbeatInterval < 5U) + network->m_heartbeatInterval = 5U; + + // HACK: make sure the HB is always one second shorter then the requested value + network->m_heartbeatInterval--; uint16_t remoteCtrlPort = Socket::port(req->address); network->m_remoteCtrlAddr = req->address; network->m_remoteCtrlAddrLen = req->addrLen; - + LogMessage(LOG_MODEM, "V.24 UDP, Incoming DFSI FSC Connection, ctrlRemotePort = %u, vcLocalPort = %u, vcRemotePort = %u, hostHBInterval = %u", remoteCtrlPort, network->m_localPort, vcBasePort, connMessage->getHostHeartbeatPeriod()); // setup local RTP VC port (where we receive traffic) @@ -822,6 +828,9 @@ uint8_t* V24UDPPort::generateMessage(const uint8_t* message, uint32_t length, ui } uint32_t bufferLen = RTP_HEADER_LENGTH_BYTES + length; + if (!m_tiaMode) + bufferLen += 8U; + uint8_t* buffer = new uint8_t[bufferLen]; ::memset(buffer, 0x00U, bufferLen); @@ -833,6 +842,11 @@ uint8_t* V24UDPPort::generateMessage(const uint8_t* message, uint32_t length, ui header.setSequence(rtpSeq); header.setSSRC(ssrc); + if (!m_tiaMode) { + header.setExtension(true); + header.setPayloadType(DFSI_RTP_MOT_PAYLOAD_TYPE); + } + header.encode(buffer); if (streamId != 0U && timestamp == INVALID_TS && rtpSeq != RTP_END_OF_CALL_SEQ) { @@ -847,10 +861,26 @@ uint8_t* V24UDPPort::generateMessage(const uint8_t* message, uint32_t length, ui LogDebugEx(LOG_NET, "V24UDPPort::generateMessage()", "RTP streamId = %u, rtpSeq = %u", streamId, rtpSeq); } - ::memcpy(buffer + RTP_HEADER_LENGTH_BYTES, message, length); + if (!m_tiaMode) { + uint8_t extBuffer[8U]; + ::memset(extBuffer, 0x00U, 8U); + + extBuffer[0U] = 0x02U; + extBuffer[3U] = 0x01U; + + extBuffer[4U] = 0x7FU; // 127 + extBuffer[5U] = 0x00U; // 0 + extBuffer[6U] = 0x00U; // 0 + extBuffer[7U] = 0x01U; // 1 + + ::memcpy(buffer + RTP_HEADER_LENGTH_BYTES, extBuffer, 8U); + ::memcpy(buffer + RTP_HEADER_LENGTH_BYTES + 8U, message, length); + } else { + ::memcpy(buffer + RTP_HEADER_LENGTH_BYTES, message, length); + } if (m_debug) - Utils::dump(1U, "[V24UDPPort::generateMessage()] Buffered Message", buffer, bufferLen); + Utils::dump(1U, "V24UDPPort::generateMessage(), Buffered Message", buffer, bufferLen); if (outBufferLen != nullptr) { *outBufferLen = bufferLen; diff --git a/src/host/modem/port/specialized/V24UDPPort.h b/src/host/modem/port/specialized/V24UDPPort.h index 76788fca..8cbea521 100644 --- a/src/host/modem/port/specialized/V24UDPPort.h +++ b/src/host/modem/port/specialized/V24UDPPort.h @@ -87,6 +87,11 @@ namespace modem * @brief Helper to set and configure the heartbeat interval for FSC connections. */ void setHeartbeatInterval(uint32_t interval); + /** + * @brief Helper to set TIA mode. + * @param mode True to enable TIA mode, false to disable. + */ + void setTIAMode(bool mode) { m_tiaMode = mode; } /** * @brief Updates the timer by the passed number of milliseconds. @@ -185,6 +190,7 @@ namespace modem ThreadPool m_ctrlThreadPool; ThreadPool m_vcThreadPool; + bool m_tiaMode; bool m_debug; static std::mutex m_bufferMutex; diff --git a/src/host/network/RESTAPI.cpp b/src/host/network/RESTAPI.cpp index acccc157..f82f2cae 100644 --- a/src/host/network/RESTAPI.cpp +++ b/src/host/network/RESTAPI.cpp @@ -176,7 +176,7 @@ RESTAPI::RESTAPI(const std::string& address, uint16_t port, const std::string& p delete[] in; if (m_debug) { - Utils::dump("REST Password Hash", m_passwordHash, 32U); + Utils::dump("RESTAPI::RESTAPI(), REST Password Hash", m_passwordHash, 32U); } #if defined(ENABLE_SSL) @@ -432,7 +432,7 @@ void RESTAPI::restAPI_PutAuth(const HTTPPayload& request, HTTPPayload& reply, co } if (m_debug) { - Utils::dump("Password Hash", passwordHash, 32U); + Utils::dump("RESTAPI::restAPI_PutAuth(), Password Hash", passwordHash, 32U); } // compare hashes @@ -1611,7 +1611,7 @@ void RESTAPI::restAPI_PutP25RawTSBK(const HTTPPayload& request, HTTPPayload& rep } if (m_debug) { - Utils::dump("Raw TSBK", tsbk, P25_TSBK_LENGTH_BYTES); + Utils::dump("RESTAPI::restAPI_PutP25RawTSBK(), Raw TSBK", tsbk, P25_TSBK_LENGTH_BYTES); } m_p25->control()->writeRF_TSDU_Raw(tsbk); diff --git a/src/host/nxdn/Control.cpp b/src/host/nxdn/Control.cpp index a8a0da20..5d4f2192 100644 --- a/src/host/nxdn/Control.cpp +++ b/src/host/nxdn/Control.cpp @@ -71,6 +71,8 @@ Control::Control(bool authoritative, uint32_t ran, uint32_t callHang, uint32_t q m_enableControl(false), m_dedicatedControl(false), m_ignoreAffiliationCheck(false), + m_legacyGroupReg(false), + m_defaultNetIdleTalkgroup(0U), m_rfLastLICH(), m_rfLC(), m_netLC(), @@ -104,8 +106,8 @@ Control::Control(bool authoritative, uint32_t ran, uint32_t callHang, uint32_t q m_interval(), m_frameLossCnt(0U), m_frameLossThreshold(DEFAULT_FRAME_LOSS_THRESHOLD), - m_ccFrameCnt(0U), m_ccSeq(0U), + m_ccIteration(0U), m_siteData(), m_rssiMapper(rssiMapper), m_rssi(0U), @@ -217,8 +219,11 @@ void Control::setOptions(yaml::Node& conf, bool supervisor, const std::string cw m_ccDebug = control["debug"].as(false); m_ignoreAffiliationCheck = nxdnProtocol["ignoreAffiliationCheck"].as(false); + m_legacyGroupReg = nxdnProtocol["legacyGroupReg"].as(false); yaml::Node rfssConfig = systemConf["config"]; + m_defaultNetIdleTalkgroup = (uint32_t)::strtoul(rfssConfig["defaultNetIdleTalkgroup"].as("0").c_str(), NULL, 16); + yaml::Node controlCh = rfssConfig["controlCh"]; m_notifyCC = controlCh["notifyEnable"].as(false); @@ -251,7 +256,7 @@ void Control::setOptions(yaml::Node& conf, bool supervisor, const std::string cw uint8_t siteInfo1 = SiteInformation1::VOICE_CALL_SVC | SiteInformation1::DATA_CALL_SVC; uint8_t siteInfo2 = SiteInformation2::SHORT_DATA_CALL_SVC; if (m_enableControl) { - siteInfo1 |= SiteInformation1::LOC_REG_SVC;//| SiteInformation1::GRP_REG_SVC; + siteInfo1 |= SiteInformation1::LOC_REG_SVC; } /* @@ -323,10 +328,15 @@ void Control::setOptions(yaml::Node& conf, bool supervisor, const std::string cw LogInfo(" Disable Grant Source ID Check: yes"); } if (m_supervisor) - LogMessage(LOG_DMR, "Host is configured to operate as a NXDN control channel, site controller mode."); + LogMessage(LOG_NXDN, "Host is configured to operate as a NXDN control channel, site controller mode."); + } + + if (m_defaultNetIdleTalkgroup != 0U) { + LogInfo(" Default Network Idle Talkgroup: $%04X", m_defaultNetIdleTalkgroup); } LogInfo(" Ignore Affiliation Check: %s", m_ignoreAffiliationCheck ? "yes" : "no"); + LogInfo(" Legacy Group Registration: %s", m_legacyGroupReg ? "yes" : "no"); LogInfo(" Notify Control: %s", m_notifyCC ? "yes" : "no"); LogInfo(" Verify Affiliation: %s", m_control->m_verifyAff ? "yes" : "no"); LogInfo(" Verify Registration: %s", m_control->m_verifyReg ? "yes" : "no"); @@ -917,9 +927,6 @@ void Control::addFrame(const uint8_t *data, bool net, bool imm) void Control::processNetwork() { - if (m_rfState != RS_RF_LISTENING && m_netState == RS_NET_IDLE) - return; - uint32_t length = 0U; bool ret = false; UInt8Array buffer = m_network->readNXDN(ret, length); @@ -932,6 +939,12 @@ void Control::processNetwork() return; } + // don't process network frames if the RF modem isn't in a listening state + if (m_rfState != RS_RF_LISTENING && m_netState == RS_NET_IDLE) { + m_network->resetNXDN(); + return; + } + // process network message header uint8_t messageType = buffer[4U]; @@ -977,17 +990,23 @@ void Control::processNetwork() if (valid) m_rfLastLICH = lich; - FuncChannelType::E usc = m_rfLastLICH.getFCT(); + RFChannelType::E rfct = m_rfLastLICH.getRFCT(); + FuncChannelType::E fct = m_rfLastLICH.getFCT(); ChOption::E option = m_rfLastLICH.getOption(); - // forward onto the specific processor for final processing and delivery - switch (usc) { - case FuncChannelType::USC_UDCH: - ret = m_data->processNetwork(option, lc, data.get(), frameLength); - break; - default: - ret = m_voice->processNetwork(usc, option, lc, data.get(), frameLength); - break; + if (rfct == RFChannelType::RCCH) { + m_control->processNetwork(fct, option, lc, data.get(), frameLength); + } + else if (rfct == RFChannelType::RTCH || rfct == RFChannelType::RDCH) { + // forward onto the specific processor for final processing and delivery + switch (fct) { + case FuncChannelType::USC_UDCH: + m_data->processNetwork(option, lc, data.get(), frameLength); + break; + default: + m_voice->processNetwork(fct, option, lc, data.get(), frameLength); + break; + } } } @@ -1252,10 +1271,6 @@ bool Control::writeRF_ControlData() if (!m_enableControl) return false; - if (m_ccFrameCnt == 254U) { - m_ccFrameCnt = 0U; - } - // don't add any frames if the queue is full uint8_t len = NXDN_FRAME_LENGTH_BYTES + 2U; uint32_t space = m_txQueue.freeSpace(); @@ -1263,20 +1278,21 @@ bool Control::writeRF_ControlData() return false; } - const uint8_t maxSeq = m_control->m_bcchCnt + (m_control->m_ccchPagingCnt + m_control->m_ccchMultiCnt) * - m_control->m_rcchGroupingCnt * m_control->m_rcchIterateCnt; - if (m_ccSeq == maxSeq) { + if (m_ccIteration > m_control->m_rcchIterateCnt) { + m_ccIteration = 0U; m_ccSeq = 0U; } + const uint8_t maxSeq = (m_control->m_ccchPagingCnt + m_control->m_ccchMultiCnt) * m_control->m_rcchGroupingCnt; + if (m_ccSeq > maxSeq) { + m_ccSeq = 1U; + m_ccIteration++; + } + if (m_netState == RS_NET_IDLE && m_rfState == RS_RF_LISTENING) { - m_control->writeRF_ControlData(m_ccFrameCnt, m_ccSeq, true); + m_control->writeRF_ControlData(m_ccSeq, m_ccSeq == 0U); m_ccSeq++; - if (m_ccSeq == maxSeq) { - m_ccFrameCnt++; - } - return true; } diff --git a/src/host/nxdn/Control.h b/src/host/nxdn/Control.h index 883ed2c0..f65fa1db 100644 --- a/src/host/nxdn/Control.h +++ b/src/host/nxdn/Control.h @@ -278,6 +278,9 @@ namespace nxdn bool m_enableControl; bool m_dedicatedControl; bool m_ignoreAffiliationCheck; + bool m_legacyGroupReg; + + uint32_t m_defaultNetIdleTalkgroup; channel::LICH m_rfLastLICH; lc::RTCH m_rfLC; @@ -327,8 +330,8 @@ namespace nxdn uint8_t m_frameLossCnt; uint8_t m_frameLossThreshold; - uint8_t m_ccFrameCnt; uint8_t m_ccSeq; + uint8_t m_ccIteration; SiteData m_siteData; diff --git a/src/host/nxdn/packet/ControlSignaling.cpp b/src/host/nxdn/packet/ControlSignaling.cpp index d7c274a3..e4e1fb86 100644 --- a/src/host/nxdn/packet/ControlSignaling.cpp +++ b/src/host/nxdn/packet/ControlSignaling.cpp @@ -287,10 +287,12 @@ bool ControlSignaling::processNetwork(FuncChannelType::E fct, ChOption::E option ControlSignaling::ControlSignaling(Control* nxdn, bool debug, bool verbose) : m_nxdn(nxdn), m_bcchCnt(1U), - m_rcchGroupingCnt(1U), m_ccchPagingCnt(2U), m_ccchMultiCnt(2U), - m_rcchIterateCnt(2U), + m_rcchGroupingCnt(1U), + m_rcchIterateCnt(4U), + m_pgRCCHQueue((NXDN_FRAME_LENGTH_BYTES + 2U) * 16U, "RCCH Paging Frame"), + m_mpRCCHQueue((NXDN_FRAME_LENGTH_BYTES + 2U) * 16U, "RCCH Multipurpose Frame"), m_verifyAff(false), m_verifyReg(false), m_disableGrantSrcIdCheck(false), @@ -326,7 +328,7 @@ void ControlSignaling::writeNetwork(const uint8_t *data, uint32_t len) /* Helper to write a single-block RCCH packet. */ -void ControlSignaling::writeRF_Message(RCCH* rcch, bool noNetwork, bool imm) +void ControlSignaling::writeRF_Message(RCCH* rcch, bool noNetwork, bool paging) { if (!m_nxdn->m_enableControl) return; @@ -365,9 +367,10 @@ void ControlSignaling::writeRF_Message(RCCH* rcch, bool noNetwork, bool imm) if (!noNetwork) writeNetwork(data, NXDN_FRAME_LENGTH_BYTES + 2U); - if (m_nxdn->m_duplex) { - m_nxdn->addFrame(data, false, imm); - } + if (paging) + m_pgRCCHQueue.addData(data, NXDN_FRAME_LENGTH_BYTES + 2U); + else + m_mpRCCHQueue.addData(data, NXDN_FRAME_LENGTH_BYTES + 2U); } /* @@ -376,10 +379,8 @@ void ControlSignaling::writeRF_Message(RCCH* rcch, bool noNetwork, bool imm) /* Helper to write control channel packet data. */ -void ControlSignaling::writeRF_ControlData(uint8_t frameCnt, uint8_t n, bool adjSS) +void ControlSignaling::writeRF_ControlData(uint8_t n, bool first) { - uint8_t i = 0U, seqCnt = 0U; - if (!m_nxdn->m_enableControl) return; @@ -400,26 +401,42 @@ void ControlSignaling::writeRF_ControlData(uint8_t frameCnt, uint8_t n, bool adj return; } - do - { - if (m_debug) { - LogDebugEx(LOG_NXDN, "ControlSignaling::writeRF_ControlData()", "frameCnt = %u, seq = %u", frameCnt, n); - } + if (m_debug) { + LogDebugEx(LOG_NXDN, "ControlSignaling::writeRF_ControlData()", "seq = %u", n); + } - switch (n) - { - case 0: - writeRF_CC_Message_Site_Info(); - break; - default: - writeRF_CC_Message_Service_Info(); - break; + if (first) { + // transmit the BCCH frame + writeRF_CC_Message_Site_Info(); + } + else { + if (n > 0U && n <= m_ccchPagingCnt - 1U) { + // transmit the next paging frame + if (!m_pgRCCHQueue.isEmpty()) { + uint8_t data[NXDN_FRAME_LENGTH_BYTES + 2U]; + m_pgRCCHQueue.get(data, NXDN_FRAME_LENGTH_BYTES + 2U); + + if (m_nxdn->m_duplex) { + m_nxdn->addFrame(data); + } + } + else + writeRF_CC_Message_Service_Info(); } + else { + // transmit the next multipurpose frame + if (!m_mpRCCHQueue.isEmpty()) { + uint8_t data[NXDN_FRAME_LENGTH_BYTES + 2U]; + m_mpRCCHQueue.get(data, NXDN_FRAME_LENGTH_BYTES + 2U); - if (seqCnt > 0U) - n++; - i++; - } while (i <= seqCnt); + if (m_nxdn->m_duplex) { + m_nxdn->addFrame(data); + } + } + else + writeRF_CC_Message_Idle(); + } + } lc::RCCH::setVerbose(rcchVerbose); m_nxdn->m_debug = m_debug = controlDebug; @@ -433,7 +450,7 @@ bool ControlSignaling::writeRF_Message_Grant(uint32_t srcId, uint32_t dstId, uin bool encryption = ((serviceOptions & 0xFFU) & 0x40U) == 0x40U; // Encryption Flag uint8_t priority = ((serviceOptions & 0xFFU) & 0x07U); // Priority - std::unique_ptr rcch = std::make_unique(); + std::unique_ptr rcch = std::make_unique(); // are we skipping checking? if (!skip) { @@ -559,11 +576,19 @@ bool ControlSignaling::writeRF_Message_Grant(uint32_t srcId, uint32_t dstId, uin if (!net) { ::ActivityLog("NXDN", true, "group grant request from %u to TG %u", srcId, dstId); } + + rcch->setCallType(CallType::CONFERENCE); + rcch->setDuplex(false); + rcch->setTransmissionMode(TransmissionMode::MODE_4800); } else { if (!net) { ::ActivityLog("NXDN", true, "unit-to-unit grant request from %u to %u", srcId, dstId); } + + rcch->setCallType(CallType::INDIVIDUAL); + rcch->setDuplex(false); + rcch->setTransmissionMode(TransmissionMode::MODE_4800); } // callback RPC to permit the granted TG on the specified voice channel @@ -610,7 +635,6 @@ bool ControlSignaling::writeRF_Message_Grant(uint32_t srcId, uint32_t dstId, uin } } - rcch->setMessageType(MessageType::RTCH_VCALL); rcch->setGrpVchNo(chNo); rcch->setGroup(grp); rcch->setSrcId(srcId); @@ -626,7 +650,7 @@ bool ControlSignaling::writeRF_Message_Grant(uint32_t srcId, uint32_t dstId, uin } // transmit group grant - writeRF_Message_Imm(rcch.get(), net); + writeRF_Message(rcch.get(), net, true); return true; } @@ -649,11 +673,11 @@ void ControlSignaling::writeRF_Message_Deny(uint32_t srcId, uint32_t dstId, uint rcch->setDstId(dstId); if (m_verbose) { - LogMessage(LOG_RF, "NXDN, MSG_DENIAL (Message Denial), reason = $%02X, service = $%02X, srcId = %u, dstId = %u", - service, srcId, dstId); + LogMessage(LOG_RF, "NXDN, MSG_DENIAL (Message Denial), reason = $%02X (%s), service = $%02X, srcId = %u, dstId = %u", + reason, NXDNUtils::causeToString(reason).c_str(), service, srcId, dstId); } - writeRF_Message_Imm(rcch.get(), false); + writeRF_Message(rcch.get(), false); } /* Helper to write a group registration response packet. */ @@ -711,7 +735,7 @@ bool ControlSignaling::writeRF_Message_Grp_Reg_Rsp(uint32_t srcId, uint32_t dstI m_nxdn->m_network->announceGroupAffiliation(srcId, dstId); } - writeRF_Message_Imm(rcch.get(), false); + writeRF_Message(rcch.get(), false); return ret; } @@ -768,7 +792,7 @@ void ControlSignaling::writeRF_Message_U_Reg_Rsp(uint32_t srcId, uint32_t dstId, rcch->setSrcId(srcId); rcch->setDstId(dstId); - writeRF_Message_Imm(rcch.get(), true); + writeRF_Message(rcch.get(), true); } /* Helper to write a CC SITE_INFO broadcast packet on the RF interface. */ @@ -861,3 +885,46 @@ void ControlSignaling::writeRF_CC_Message_Service_Info() m_nxdn->addFrame(data); } } + +/* Helper to write a CC IDLE broadcast packet on the RF interface. */ + +void ControlSignaling::writeRF_CC_Message_Idle() +{ + uint8_t data[NXDN_FRAME_LENGTH_BYTES + 2U]; + ::memset(data + 2U, 0x00U, NXDN_FRAME_LENGTH_BYTES); + + Sync::addNXDNSync(data + 2U); + + // generate the LICH + channel::LICH lich; + lich.setRFCT(RFChannelType::RCCH); + lich.setFCT(FuncChannelType::CAC_OUTBOUND); + lich.setOption(ChOption::DATA_IDLE); + lich.setOutbound(true); + lich.encode(data + 2U); + + uint8_t buffer[NXDN_RCCH_LC_LENGTH_BYTES]; + ::memset(buffer, 0x00U, NXDN_RCCH_LC_LENGTH_BYTES); + + std::unique_ptr rcch = std::make_unique(); + DEBUG_LOG_MSG(rcch->toString()); + rcch->encode(buffer, NXDN_RCCH_LC_LENGTH_BITS / 2U); + //rcch->encode(buffer, NXDN_RCCH_LC_LENGTH_BITS / 2U, NXDN_RCCH_LC_LENGTH_BITS / 2U); + + // generate the CAC + channel::CAC cac; + cac.setRAN(m_nxdn->m_ran); + cac.setStructure(ChStructure::SR_RCCH_SINGLE); + cac.setData(buffer); + cac.encode(data + 2U); + + data[0U] = modem::TAG_DATA; + data[1U] = 0x00U; + + NXDNUtils::scrambler(data + 2U); + NXDNUtils::addPostBits(data + 2U); + + if (m_nxdn->m_duplex) { + m_nxdn->addFrame(data); + } +} diff --git a/src/host/nxdn/packet/ControlSignaling.h b/src/host/nxdn/packet/ControlSignaling.h index 67fec18a..f66a295f 100644 --- a/src/host/nxdn/packet/ControlSignaling.h +++ b/src/host/nxdn/packet/ControlSignaling.h @@ -74,11 +74,14 @@ namespace nxdn Control* m_nxdn; uint8_t m_bcchCnt; - uint8_t m_rcchGroupingCnt; uint8_t m_ccchPagingCnt; uint8_t m_ccchMultiCnt; + uint8_t m_rcchGroupingCnt; uint8_t m_rcchIterateCnt; + RingBuffer m_pgRCCHQueue; + RingBuffer m_mpRCCHQueue; + bool m_verifyAff; bool m_verifyReg; @@ -112,19 +115,13 @@ namespace nxdn ** Modem Frame Queuing */ - /** - * @brief Helper to write a immediate single-block RCCH packet. - * @param rcch RCCH to write to the modem. - * @param noNetwork Flag indicating not to write the TSBK to the network. - */ - void writeRF_Message_Imm(lc::RCCH *rcch, bool noNetwork) { writeRF_Message(rcch, noNetwork, true); } /** * @brief Helper to write a single-block RCCH packet. * @param rcch RCCH to write to the modem. - * @param noNetwork Flag indicating not to write the TSBK to the network. - * @param imm Flag indicating the TSBK should be written to the immediate queue. + * @param noNetwork Flag indicating not to write the RCCH to the network. + * @param paging Flag indicating the RCCH should be written to the paging queue. */ - void writeRF_Message(lc::RCCH* rcch, bool noNetwork, bool imm = false); + void writeRF_Message(lc::RCCH* rcch, bool noNetwork, bool paging = false); /* ** Control Signalling Logic @@ -132,11 +129,10 @@ namespace nxdn /** * @brief Helper to write control channel packet data. - * @param frameCnt Frame counter. * @param n - * @param adjSS Flag indicating whether or not adjacent site status should be broadcast. + * @param first */ - void writeRF_ControlData(uint8_t frameCnt, uint8_t n, bool adjSS); + void writeRF_ControlData(uint8_t n, bool first); /** * @brief Helper to write a grant packet. @@ -181,6 +177,10 @@ namespace nxdn * @brief Helper to write a CC SRV_INFO broadcast packet on the RF interface. */ void writeRF_CC_Message_Service_Info(); + /** + * @brief Helper to write a CC IDLE broadcast packet on the RF interface. + */ + void writeRF_CC_Message_Idle(); }; } // namespace packet } // namespace nxdn diff --git a/src/host/nxdn/packet/Data.cpp b/src/host/nxdn/packet/Data.cpp index fe3d6370..0d432a6c 100644 --- a/src/host/nxdn/packet/Data.cpp +++ b/src/host/nxdn/packet/Data.cpp @@ -5,7 +5,7 @@ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * Copyright (C) 2015-2020 Jonathan Naylor, G4KLX - * Copyright (C) 2022-2024 Bryan Biedenkapp, N2PLL + * Copyright (C) 2022-2025 Bryan Biedenkapp, N2PLL * */ #include "Defines.h" @@ -24,114 +24,6 @@ using namespace nxdn::packet; #include -// --------------------------------------------------------------------------- -// Macros -// --------------------------------------------------------------------------- - -// Don't process RF frames if the network isn't in a idle state and the RF destination -// is the network destination and stop network frames from processing -- RF wants to -// transmit on a different talkgroup -#define CHECK_TRAFFIC_COLLISION(_SRC_ID, _DST_ID) \ - if (m_nxdn->m_netState != RS_NET_IDLE && _DST_ID == m_nxdn->m_netLastDstId) { \ - LogWarning(LOG_RF, "Traffic collision detect, preempting new RF traffic to existing network traffic!"); \ - resetRF(); \ - m_nxdn->m_rfState = RS_RF_LISTENING; \ - return false; \ - } \ - \ - if (m_nxdn->m_netState != RS_NET_IDLE) { \ - if (m_nxdn->m_netLC.getSrcId() == _SRC_ID && m_nxdn->m_netLastDstId == _DST_ID) { \ - LogWarning(LOG_RF, "Traffic collision detect, preempting new RF traffic to existing RF traffic (Are we in a voting condition?), rfSrcId = %u, rfDstId = %u, netSrcId = %u, netDstId = %u", srcId, dstId, \ - m_nxdn->m_netLC.getSrcId(), m_nxdn->m_netLastDstId); \ - resetRF(); \ - m_nxdn->m_rfState = RS_RF_LISTENING; \ - return false; \ - } \ - else { \ - LogWarning(LOG_RF, "Traffic collision detect, preempting existing network traffic to new RF traffic, rfDstId = %u, netDstId = %u", dstId, \ - m_nxdn->m_netLastDstId); \ - resetNet(); \ - } \ - } - -// Don't process network frames if the destination ID's don't match and the network TG hang -// timer is running, and don't process network frames if the RF modem isn't in a listening state -#define CHECK_NET_TRAFFIC_COLLISION(_LAYER3, _SRC_ID, _DST_ID) \ - if (m_nxdn->m_rfLastDstId != 0U) { \ - if (m_nxdn->m_rfLastDstId != dstId && (m_nxdn->m_rfTGHang.isRunning() && !m_nxdn->m_rfTGHang.hasExpired())) { \ - resetNet(); \ - return false; \ - } \ - \ - if (m_nxdn->m_rfLastDstId == dstId && (m_nxdn->m_rfTGHang.isRunning() && !m_nxdn->m_rfTGHang.hasExpired())) { \ - m_nxdn->m_rfTGHang.start(); \ - } \ - } \ - \ - if (m_nxdn->m_rfState != RS_RF_LISTENING) { \ - if (_LAYER3.getSrcId() == srcId && _LAYER3.getDstId() == dstId) { \ - LogWarning(LOG_RF, "Traffic collision detect, preempting new network traffic to existing RF traffic (Are we in a voting condition?), rfSrcId = %u, rfDstId = %u, netSrcId = %u, netDstId = %u", _LAYER3.getSrcId(), _LAYER3.getDstId(), \ - srcId, dstId); \ - resetNet(); \ - return false; \ - } \ - else { \ - LogWarning(LOG_RF, "Traffic collision detect, preempting new network traffic to existing RF traffic, rfDstId = %u, netDstId = %u", _LAYER3.getDstId(), \ - dstId); \ - resetNet(); \ - return false; \ - } \ - } - -// Validate the source RID -#define VALID_SRCID(_SRC_ID, _DST_ID, _GROUP) \ - if (!acl::AccessControl::validateSrcId(_SRC_ID)) { \ - if (m_lastRejectId == 0U || m_lastRejectId != _SRC_ID) { \ - LogWarning(LOG_RF, "NXDN, " NXDN_RTCH_MSG_TYPE_DCALL_HDR " denial, RID rejection, srcId = %u", _SRC_ID); \ - ::ActivityLog("NXDN", true, "RF voice rejection from %u to %s%u ", _SRC_ID, _GROUP ? "TG " : "", _DST_ID); \ - m_lastRejectId = _SRC_ID; \ - } \ - \ - m_nxdn->m_rfLastDstId = 0U; \ - m_nxdn->m_rfLastSrcId = 0U; \ - m_nxdn->m_rfTGHang.stop(); \ - m_nxdn->m_rfState = RS_RF_REJECTED; \ - return false; \ - } - -// Validate the destination ID -#define VALID_DSTID(_SRC_ID, _DST_ID, _GROUP) \ - if (!_GROUP) { \ - if (!acl::AccessControl::validateSrcId(_DST_ID)) { \ - if (m_lastRejectId == 0 || m_lastRejectId != _DST_ID) { \ - LogWarning(LOG_RF, "NXDN, " NXDN_RTCH_MSG_TYPE_DCALL_HDR " denial, RID rejection, dstId = %u", _DST_ID); \ - ::ActivityLog("NXDN", true, "RF voice rejection from %u to %s%u ", _SRC_ID, _GROUP ? "TG " : "", _DST_ID); \ - m_lastRejectId = _DST_ID; \ - } \ - \ - m_nxdn->m_rfLastDstId = 0U; \ - m_nxdn->m_rfLastSrcId = 0U; \ - m_nxdn->m_rfTGHang.stop(); \ - m_nxdn->m_rfState = RS_RF_REJECTED; \ - return false; \ - } \ - } \ - else { \ - if (!acl::AccessControl::validateTGId(_DST_ID)) { \ - if (m_lastRejectId == 0 || m_lastRejectId != _DST_ID) { \ - LogWarning(LOG_RF, "NXDN, " NXDN_RTCH_MSG_TYPE_DCALL_HDR " denial, TGID rejection, dstId = %u", _DST_ID); \ - ::ActivityLog("NXDN", true, "RF voice rejection from %u to %s%u ", _SRC_ID, _GROUP ? "TG " : "", _DST_ID); \ - m_lastRejectId = _DST_ID; \ - } \ - \ - m_nxdn->m_rfLastDstId = 0U; \ - m_nxdn->m_rfLastSrcId = 0U; \ - m_nxdn->m_rfTGHang.stop(); \ - m_nxdn->m_rfState = RS_RF_REJECTED; \ - return false; \ - } \ - } - // --------------------------------------------------------------------------- // Public Class Members // --------------------------------------------------------------------------- @@ -183,13 +75,76 @@ bool Data::process(ChOption::E option, uint8_t* data, uint32_t len) if (type != MessageType::RTCH_DCALL_HDR) return false; - CHECK_TRAFFIC_COLLISION(srcId, dstId); + // don't process RF frames if the network isn't in a idle state and the RF destination is the network destination + if (m_nxdn->m_netState != RS_NET_IDLE && dstId == m_nxdn->m_netLastDstId) { + LogWarning(LOG_RF, "Traffic collision detect, preempting new RF traffic to existing network traffic!"); + resetRF(); + m_nxdn->m_rfState = RS_RF_LISTENING; + return false; + } + + // stop network frames from processing -- RF wants to transmit on a different talkgroup */ + if (m_nxdn->m_netState != RS_NET_IDLE) { + if (m_nxdn->m_netLC.getSrcId() == srcId && m_nxdn->m_netLastDstId == dstId) { + LogWarning(LOG_RF, "Traffic collision detect, preempting new RF traffic to existing RF traffic (Are we in a voting condition?), rfSrcId = %u, rfDstId = %u, netSrcId = %u, netDstId = %u", srcId, dstId, + m_nxdn->m_netLC.getSrcId(), m_nxdn->m_netLastDstId); + resetRF(); + m_nxdn->m_rfState = RS_RF_LISTENING; + return false; + } + else { + LogWarning(LOG_RF, "Traffic collision detect, preempting existing network traffic to new RF traffic, rfDstId = %u, netDstId = %u", dstId, + m_nxdn->m_netLastDstId); + resetNet(); + } + } // validate source RID - VALID_SRCID(srcId, dstId, group); + if (!acl::AccessControl::validateSrcId(srcId)) { + if (m_lastRejectId == 0U || m_lastRejectId != srcId) { + LogWarning(LOG_RF, "NXDN, " NXDN_RTCH_MSG_TYPE_DCALL_HDR " denial, RID rejection, srcId = %u", srcId); + ::ActivityLog("NXDN", true, "RF data rejection from %u to %s%u ", srcId, group ? "TG " : "", dstId); + m_lastRejectId = srcId; + } + + m_nxdn->m_rfLastDstId = 0U; + m_nxdn->m_rfLastSrcId = 0U; + m_nxdn->m_rfTGHang.stop(); + m_nxdn->m_rfState = RS_RF_REJECTED; + return false; + } // validate destination ID - VALID_DSTID(srcId, dstId, group); + if (!group) { + if (!acl::AccessControl::validateSrcId(dstId)) { + if (m_lastRejectId == 0 || m_lastRejectId != dstId) { + LogWarning(LOG_RF, "NXDN, " NXDN_RTCH_MSG_TYPE_DCALL_HDR " denial, RID rejection, dstId = %u", dstId); + ::ActivityLog("NXDN", true, "RF data rejection from %u to %s%u ", srcId, group? "TG " : "", dstId); + m_lastRejectId = dstId; + } + + m_nxdn->m_rfLastDstId = 0U; + m_nxdn->m_rfLastSrcId = 0U; + m_nxdn->m_rfTGHang.stop(); + m_nxdn->m_rfState = RS_RF_REJECTED; + return false; + } + } + else { + if (!acl::AccessControl::validateTGId(dstId)) { + if (m_lastRejectId == 0 || m_lastRejectId != dstId) { + LogWarning(LOG_RF, "NXDN, " NXDN_RTCH_MSG_TYPE_DCALL_HDR " denial, TGID rejection, dstId = %u", dstId); + ::ActivityLog("NXDN", true, "RF data rejection from %u to %s%u ", srcId, group ? "TG " : "", dstId); + m_lastRejectId = dstId; + } + + m_nxdn->m_rfLastDstId = 0U; + m_nxdn->m_rfLastSrcId = 0U; + m_nxdn->m_rfTGHang.stop(); + m_nxdn->m_rfState = RS_RF_REJECTED; + return false; + } + } if (m_verbose) { LogMessage(LOG_RF, "NXDN, " NXDN_RTCH_MSG_TYPE_DCALL_HDR ", srcId = %u, dstId = %u, ack = %u, blocksToFollow = %u, padCount = %u, firstFragment = %u, fragmentCount = %u", @@ -293,13 +248,71 @@ bool Data::processNetwork(ChOption::E option, lc::RTCH& netLC, uint8_t* data, ui if (type != MessageType::RTCH_DCALL_HDR) return false; - CHECK_NET_TRAFFIC_COLLISION(lc, srcId, dstId); + // don't process network frames if the destination ID's don't match and the RF TG hang timer is running + if (m_nxdn->m_rfLastDstId != 0U) { + if (m_nxdn->m_rfLastDstId != dstId && (m_nxdn->m_rfTGHang.isRunning() && !m_nxdn->m_rfTGHang.hasExpired())) { + resetNet(); + return false; + } + + if (m_nxdn->m_rfLastDstId == dstId && (m_nxdn->m_rfTGHang.isRunning() && !m_nxdn->m_rfTGHang.hasExpired())) { + m_nxdn->m_rfTGHang.start(); + } + } + + // don't process network frames if the RF modem isn't in a listening state + if (m_nxdn->m_rfState != RS_RF_LISTENING) { + if (lc.getSrcId() == srcId && lc.getDstId() == dstId) { \ + LogWarning(LOG_NET, "Traffic collision detect, preempting new network traffic to existing RF traffic (Are we in a voting condition?), rfSrcId = %u, rfDstId = %u, netSrcId = %u, netDstId = %u", lc.getSrcId(), lc.getDstId(), + srcId, dstId); + resetNet(); + return false; + } + else { + LogWarning(LOG_NET, "Traffic collision detect, preempting new network traffic to existing RF traffic, rfDstId = %u, netDstId = %u", lc.getDstId(), + dstId); + resetNet(); + return false; + } + } // validate source RID - VALID_SRCID(srcId, dstId, group); + if (!acl::AccessControl::validateSrcId(srcId)) { + if (m_lastRejectId == 0U || m_lastRejectId != srcId) { + LogWarning(LOG_NET, "NXDN, " NXDN_RTCH_MSG_TYPE_DCALL_HDR " denial, RID rejection, srcId = %u", srcId); + ::ActivityLog("NXDN", true, "network data rejection from %u to %s%u ", srcId, group ? "TG " : "", dstId); + m_lastRejectId = srcId; + } + + resetNet(); + return false; + } // validate destination ID - VALID_DSTID(srcId, dstId, group); + if (!group) { + if (!acl::AccessControl::validateSrcId(dstId)) { + if (m_lastRejectId == 0 || m_lastRejectId != dstId) { + LogWarning(LOG_NET, "NXDN, " NXDN_RTCH_MSG_TYPE_DCALL_HDR " denial, RID rejection, dstId = %u", dstId); + ::ActivityLog("NXDN", true, "network data rejection from %u to %s%u ", srcId, group? "TG " : "", dstId); + m_lastRejectId = dstId; + } + + resetNet(); + return false; + } + } + else { + if (!acl::AccessControl::validateTGId(dstId)) { + if (m_lastRejectId == 0 || m_lastRejectId != dstId) { + LogWarning(LOG_NET, "NXDN, " NXDN_RTCH_MSG_TYPE_DCALL_HDR " denial, TGID rejection, dstId = %u", dstId); + ::ActivityLog("NXDN", true, "network data rejection from %u to %s%u ", srcId, group ? "TG " : "", dstId); + m_lastRejectId = dstId; + } + + resetNet(); + return false; + } + } if (m_verbose) { LogMessage(LOG_NET, "NXDN, " NXDN_RTCH_MSG_TYPE_DCALL_HDR ", srcId = %u, dstId = %u, ack = %u, blocksToFollow = %u, padCount = %u, firstFragment = %u, fragmentCount = %u", diff --git a/src/host/nxdn/packet/Voice.cpp b/src/host/nxdn/packet/Voice.cpp index 7ae86824..755ca1c7 100644 --- a/src/host/nxdn/packet/Voice.cpp +++ b/src/host/nxdn/packet/Voice.cpp @@ -5,7 +5,7 @@ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * Copyright (C) 2015-2020 Jonathan Naylor, G4KLX - * Copyright (C) 2022-2024 Bryan Biedenkapp, N2PLL + * Copyright (C) 2022-2025 Bryan Biedenkapp, N2PLL * */ #include "Defines.h" @@ -27,151 +27,6 @@ using namespace nxdn::packet; #include #include -// --------------------------------------------------------------------------- -// Macros -// --------------------------------------------------------------------------- - -// Don't process RF frames if the network isn't in a idle state and the RF destination -// is the network destination and stop network frames from processing -- RF wants to -// transmit on a different talkgroup -#define CHECK_TRAFFIC_COLLISION(_SRC_ID, _DST_ID) \ - if (m_nxdn->m_netState != RS_NET_IDLE && _DST_ID == m_nxdn->m_netLastDstId) { \ - LogWarning(LOG_RF, "Traffic collision detect, preempting new RF traffic to existing network traffic!"); \ - resetRF(); \ - m_nxdn->m_rfState = RS_RF_LISTENING; \ - return false; \ - } \ - \ - if (m_nxdn->m_netState != RS_NET_IDLE) { \ - if (m_nxdn->m_netLC.getSrcId() == _SRC_ID && m_nxdn->m_netLastDstId == _DST_ID) { \ - LogWarning(LOG_RF, "Traffic collision detect, preempting new RF traffic to existing RF traffic (Are we in a voting condition?), rfSrcId = %u, rfDstId = %u, netSrcId = %u, netDstId = %u", _SRC_ID, _DST_ID, \ - m_nxdn->m_netLC.getSrcId(), m_nxdn->m_netLastDstId); \ - resetRF(); \ - m_nxdn->m_rfState = RS_RF_LISTENING; \ - return false; \ - } \ - else { \ - LogWarning(LOG_RF, "Traffic collision detect, preempting existing network traffic to new RF traffic, rfDstId = %u, netDstId = %u", _DST_ID, \ - m_nxdn->m_netLastDstId); \ - resetNet(); \ - if (m_nxdn->m_network != nullptr) \ - m_nxdn->m_network->resetNXDN(); \ - } \ - \ - if (m_nxdn->m_enableControl && _DST_ID == m_nxdn->m_netLastDstId) { \ - if (m_nxdn->m_affiliations->isNetGranted(_DST_ID)) { \ - LogWarning(LOG_RF, "Traffic collision detect, preempting new RF traffic to existing granted network traffic (Are we in a voting condition?), rfSrcId = %u, rfDstId = %u, netSrcId = %u, netDstId = %u", _SRC_ID, _DST_ID, \ - m_nxdn->m_netLC.getSrcId(), m_nxdn->m_netLastDstId); \ - resetRF(); \ - m_nxdn->m_rfState = RS_RF_LISTENING; \ - return false; \ - } \ - } \ - } - -// Don't process network frames if the destination ID's don't match and the network TG hang -// timer is running, and don't process network frames if the RF modem isn't in a listening state -#define CHECK_NET_TRAFFIC_COLLISION(_LAYER3, _SRC_ID, _DST_ID) \ - if (m_nxdn->m_rfLastDstId != 0U) { \ - if (m_nxdn->m_rfLastDstId != _DST_ID && (m_nxdn->m_rfTGHang.isRunning() && !m_nxdn->m_rfTGHang.hasExpired())) { \ - resetNet(); \ - return false; \ - } \ - \ - if (m_nxdn->m_rfLastDstId == _DST_ID && (m_nxdn->m_rfTGHang.isRunning() && !m_nxdn->m_rfTGHang.hasExpired())) { \ - m_nxdn->m_rfTGHang.start(); \ - } \ - } \ - \ - if (m_nxdn->m_authoritative) { \ - if (m_nxdn->m_netLastDstId != 0U) { \ - if (m_nxdn->m_netLastDstId != _DST_ID && (m_nxdn->m_netTGHang.isRunning() && !m_nxdn->m_netTGHang.hasExpired())) { \ - return false; \ - } \ - \ - if (m_nxdn->m_netLastDstId == _DST_ID && (m_nxdn->m_netTGHang.isRunning() && !m_nxdn->m_netTGHang.hasExpired())) { \ - m_nxdn->m_netTGHang.start(); \ - } \ - } \ - \ - if (m_nxdn->m_rfState != RS_RF_LISTENING) { \ - if (_LAYER3.getSrcId() == _SRC_ID && _LAYER3.getDstId() == _DST_ID) { \ - LogWarning(LOG_RF, "Traffic collision detect, preempting new network traffic to existing RF traffic (Are we in a voting condition?), rfSrcId = %u, rfDstId = %u, netSrcId = %u, netDstId = %u", _LAYER3.getSrcId(), _LAYER3.getDstId(), \ - _SRC_ID, _DST_ID); \ - resetNet(); \ - if (m_nxdn->m_network != nullptr) \ - m_nxdn->m_network->resetNXDN(); \ - return false; \ - } \ - else { \ - LogWarning(LOG_RF, "Traffic collision detect, preempting new network traffic to existing RF traffic, rfDstId = %u, netDstId = %u", _LAYER3.getDstId(), \ - _DST_ID); \ - resetNet(); \ - if (m_nxdn->m_network != nullptr) \ - m_nxdn->m_network->resetNXDN(); \ - return false; \ - } \ - } \ - } \ - \ - if (!m_nxdn->m_authoritative && m_nxdn->m_permittedDstId != _DST_ID) { \ - if (!g_disableNonAuthoritativeLogging) \ - LogWarning(LOG_NET, "[NON-AUTHORITATIVE] Ignoring network traffic, destination not permitted, dstId = %u", _DST_ID); \ - resetNet(); \ - if (m_nxdn->m_network != nullptr) \ - m_nxdn->m_network->resetNXDN(); \ - return false; \ - } - -// Validate the source RID -#define VALID_SRCID(_SRC_ID, _DST_ID, _GROUP) \ - if (!acl::AccessControl::validateSrcId(_SRC_ID)) { \ - if (m_lastRejectId == 0U || m_lastRejectId != _SRC_ID) { \ - LogWarning(LOG_RF, "NXDN, " NXDN_RTCH_MSG_TYPE_VCALL " denial, RID rejection, srcId = %u", _SRC_ID); \ - ::ActivityLog("NXDN", true, "RF voice rejection from %u to %s%u ", _SRC_ID, _GROUP ? "TG " : "", _DST_ID); \ - m_lastRejectId = _SRC_ID; \ - } \ - \ - m_nxdn->m_rfLastDstId = 0U; \ - m_nxdn->m_rfLastSrcId = 0U; \ - m_nxdn->m_rfTGHang.stop(); \ - m_nxdn->m_rfState = RS_RF_REJECTED; \ - return false; \ - } - -// Validate the destination ID -#define VALID_DSTID(_SRC_ID, _DST_ID, _GROUP) \ - if (!_GROUP) { \ - if (!acl::AccessControl::validateSrcId(_DST_ID)) { \ - if (m_lastRejectId == 0 || m_lastRejectId != _DST_ID) { \ - LogWarning(LOG_RF, "NXDN, " NXDN_RTCH_MSG_TYPE_VCALL " denial, RID rejection, dstId = %u", _DST_ID); \ - ::ActivityLog("NXDN", true, "RF voice rejection from %u to %s%u ", _SRC_ID, _GROUP ? "TG " : "", _DST_ID); \ - m_lastRejectId = _DST_ID; \ - } \ - \ - m_nxdn->m_rfLastDstId = 0U; \ - m_nxdn->m_rfLastSrcId = 0U; \ - m_nxdn->m_rfTGHang.stop(); \ - m_nxdn->m_rfState = RS_RF_REJECTED; \ - return false; \ - } \ - } \ - else { \ - if (!acl::AccessControl::validateTGId(_DST_ID)) { \ - if (m_lastRejectId == 0 || m_lastRejectId != _DST_ID) { \ - LogWarning(LOG_RF, "NXDN, " NXDN_RTCH_MSG_TYPE_VCALL " denial, TGID rejection, dstId = %u", _DST_ID); \ - ::ActivityLog("NXDN", true, "RF voice rejection from %u to %s%u ", _SRC_ID, _GROUP ? "TG " : "", _DST_ID); \ - m_lastRejectId = _DST_ID; \ - } \ - \ - m_nxdn->m_rfLastDstId = 0U; \ - m_nxdn->m_rfLastSrcId = 0U; \ - m_nxdn->m_rfTGHang.stop(); \ - m_nxdn->m_rfState = RS_RF_REJECTED; \ - return false; \ - } \ - } - // --------------------------------------------------------------------------- // Public Class Members // --------------------------------------------------------------------------- @@ -251,17 +106,78 @@ bool Voice::process(FuncChannelType::E fct, ChOption::E option, uint8_t* data, u return false; } } else if (type == MessageType::RTCH_VCALL) { - CHECK_TRAFFIC_COLLISION(srcId, dstId); + if (checkRFTrafficCollision(srcId, dstId)) + return false; // validate source RID - VALID_SRCID(srcId, dstId, group); + if (!acl::AccessControl::validateSrcId(srcId)) { + if (m_lastRejectId == 0U || m_lastRejectId != srcId) { + LogWarning(LOG_RF, "NXDN, " NXDN_RTCH_MSG_TYPE_VCALL " denial, RID rejection, srcId = %u", srcId); + ::ActivityLog("NXDN", true, "RF voice rejection from %u to %s%u ", srcId, group ? "TG " : "", dstId); + m_lastRejectId = srcId; + } + + m_nxdn->m_rfLastDstId = 0U; + m_nxdn->m_rfLastSrcId = 0U; + m_nxdn->m_rfTGHang.stop(); + m_nxdn->m_rfState = RS_RF_REJECTED; + return false; + } // validate destination ID - VALID_DSTID(srcId, dstId, group); + if (!group) { + if (!acl::AccessControl::validateSrcId(dstId)) { + if (m_lastRejectId == 0 || m_lastRejectId != dstId) { + LogWarning(LOG_RF, "NXDN, " NXDN_RTCH_MSG_TYPE_VCALL " denial, RID rejection, dstId = %u", dstId); + ::ActivityLog("NXDN", true, "RF voice rejection from %u to %s%u ", srcId, group? "TG " : "", dstId); + m_lastRejectId = dstId; + } + + m_nxdn->m_rfLastDstId = 0U; + m_nxdn->m_rfLastSrcId = 0U; + m_nxdn->m_rfTGHang.stop(); + m_nxdn->m_rfState = RS_RF_REJECTED; + return false; + } + } + else { + if (!acl::AccessControl::validateTGId(dstId)) { + if (m_lastRejectId == 0 || m_lastRejectId != dstId) { + LogWarning(LOG_RF, "NXDN, " NXDN_RTCH_MSG_TYPE_VCALL " denial, TGID rejection, dstId = %u", dstId); + ::ActivityLog("NXDN", true, "RF voice rejection from %u to %s%u ", srcId, group ? "TG " : "", dstId); + m_lastRejectId = dstId; + } + + m_nxdn->m_rfLastDstId = 0U; + m_nxdn->m_rfLastSrcId = 0U; + m_nxdn->m_rfTGHang.stop(); + m_nxdn->m_rfState = RS_RF_REJECTED; + return false; + } + } } else { return false; } + // are we auto-registering legacy radios to groups? + if (m_nxdn->m_legacyGroupReg && group) { + if (!m_nxdn->m_affiliations->isGroupAff(srcId, dstId)) { + // update dynamic unit registration table + if (!m_nxdn->m_affiliations->isUnitReg(srcId)) { + m_nxdn->m_affiliations->unitReg(srcId); + } + + if (m_nxdn->m_network != nullptr) + m_nxdn->m_network->announceUnitRegistration(srcId); + + // update dynamic affiliation table + m_nxdn->m_affiliations->groupAff(srcId, dstId); + + if (m_nxdn->m_network != nullptr) + m_nxdn->m_network->announceGroupAffiliation(srcId, dstId); + } + } + m_nxdn->m_rfTGHang.start(); m_nxdn->m_netTGHang.stop(); m_nxdn->m_rfLastDstId = lc.getDstId(); @@ -417,13 +333,55 @@ bool Voice::process(FuncChannelType::E fct, ChOption::E option, uint8_t* data, u bool group = m_nxdn->m_rfLC.getGroup(); bool encrypted = m_nxdn->m_rfLC.getEncrypted(); - CHECK_TRAFFIC_COLLISION(srcId, dstId); + if (checkRFTrafficCollision(srcId, dstId)) + return false; // validate source RID - VALID_SRCID(srcId, dstId, group); + if (!acl::AccessControl::validateSrcId(srcId)) { + if (m_lastRejectId == 0U || m_lastRejectId != srcId) { + LogWarning(LOG_RF, "NXDN, " NXDN_RTCH_MSG_TYPE_VCALL " denial, RID rejection, srcId = %u", srcId); + ::ActivityLog("NXDN", true, "RF voice rejection from %u to %s%u ", srcId, group ? "TG " : "", dstId); + m_lastRejectId = srcId; + } + + m_nxdn->m_rfLastDstId = 0U; + m_nxdn->m_rfLastSrcId = 0U; + m_nxdn->m_rfTGHang.stop(); + m_nxdn->m_rfState = RS_RF_REJECTED; + return false; + } // validate destination ID - VALID_DSTID(srcId, dstId, group); + if (!group) { + if (!acl::AccessControl::validateSrcId(dstId)) { + if (m_lastRejectId == 0 || m_lastRejectId != dstId) { + LogWarning(LOG_RF, "NXDN, " NXDN_RTCH_MSG_TYPE_VCALL " denial, RID rejection, dstId = %u", dstId); + ::ActivityLog("NXDN", true, "RF voice rejection from %u to %s%u ", srcId, group? "TG " : "", dstId); + m_lastRejectId = dstId; + } + + m_nxdn->m_rfLastDstId = 0U; + m_nxdn->m_rfLastSrcId = 0U; + m_nxdn->m_rfTGHang.stop(); + m_nxdn->m_rfState = RS_RF_REJECTED; + return false; + } + } + else { + if (!acl::AccessControl::validateTGId(dstId)) { + if (m_lastRejectId == 0 || m_lastRejectId != dstId) { + LogWarning(LOG_RF, "NXDN, " NXDN_RTCH_MSG_TYPE_VCALL " denial, TGID rejection, dstId = %u", dstId); + ::ActivityLog("NXDN", true, "RF voice rejection from %u to %s%u ", srcId, group ? "TG " : "", dstId); + m_lastRejectId = dstId; + } + + m_nxdn->m_rfLastDstId = 0U; + m_nxdn->m_rfLastSrcId = 0U; + m_nxdn->m_rfTGHang.stop(); + m_nxdn->m_rfState = RS_RF_REJECTED; + return false; + } + } m_nxdn->m_rfTGHang.start(); m_nxdn->m_netTGHang.stop(); @@ -704,13 +662,46 @@ bool Voice::processNetwork(FuncChannelType::E fct, ChOption::E option, lc::RTCH& return false; } } else if (type == MessageType::RTCH_VCALL) { - CHECK_NET_TRAFFIC_COLLISION(lc, srcId, dstId); + if (checkNetTrafficCollision(lc, srcId, dstId)) + return false; // validate source RID - VALID_SRCID(srcId, dstId, group); + if (!acl::AccessControl::validateSrcId(srcId)) { + if (m_lastRejectId == 0U || m_lastRejectId != srcId) { + LogWarning(LOG_NET, "NXDN, " NXDN_RTCH_MSG_TYPE_VCALL " denial, RID rejection, srcId = %u", srcId); + ::ActivityLog("NXDN", true, "network voice rejection from %u to %s%u ", srcId, group ? "TG " : "", dstId); + m_lastRejectId = srcId; + } + + resetNet(); + return false; + } // validate destination ID - VALID_DSTID(srcId, dstId, group); + if (!group) { + if (!acl::AccessControl::validateSrcId(dstId)) { + if (m_lastRejectId == 0 || m_lastRejectId != dstId) { + LogWarning(LOG_NET, "NXDN, " NXDN_RTCH_MSG_TYPE_VCALL " denial, RID rejection, dstId = %u", dstId); + ::ActivityLog("NXDN", true, "network voice rejection from %u to %s%u ", srcId, group? "TG " : "", dstId); + m_lastRejectId = dstId; + } + + resetNet(); + return false; + } + } + else { + if (!acl::AccessControl::validateTGId(dstId)) { + if (m_lastRejectId == 0 || m_lastRejectId != dstId) { + LogWarning(LOG_NET, "NXDN, " NXDN_RTCH_MSG_TYPE_VCALL " denial, TGID rejection, dstId = %u", dstId); + ::ActivityLog("NXDN", true, "network voice rejection from %u to %s%u ", srcId, group ? "TG " : "", dstId); + m_lastRejectId = dstId; + } + + resetNet(); + return false; + } + } } else { return false; } @@ -848,13 +839,46 @@ bool Voice::processNetwork(FuncChannelType::E fct, ChOption::E option, lc::RTCH& bool group = m_nxdn->m_netLC.getGroup(); bool encrypted = m_nxdn->m_netLC.getEncrypted(); - CHECK_NET_TRAFFIC_COLLISION(m_nxdn->m_netLC, srcId, dstId); + if (checkNetTrafficCollision(m_nxdn->m_netLC, srcId, dstId)) + return false; // validate source RID - VALID_SRCID(srcId, dstId, group); + if (!acl::AccessControl::validateSrcId(srcId)) { + if (m_lastRejectId == 0U || m_lastRejectId != srcId) { + LogWarning(LOG_NET, "NXDN, " NXDN_RTCH_MSG_TYPE_VCALL " denial, RID rejection, srcId = %u", srcId); + ::ActivityLog("NXDN", true, "network voice rejection from %u to %s%u ", srcId, group ? "TG " : "", dstId); + m_lastRejectId = srcId; + } + + resetNet(); + return false; + } // validate destination ID - VALID_DSTID(srcId, dstId, group); + if (!group) { + if (!acl::AccessControl::validateSrcId(dstId)) { + if (m_lastRejectId == 0 || m_lastRejectId != dstId) { + LogWarning(LOG_NET, "NXDN, " NXDN_RTCH_MSG_TYPE_VCALL " denial, RID rejection, dstId = %u", dstId); + ::ActivityLog("NXDN", true, "network voice rejection from %u to %s%u ", srcId, group? "TG " : "", dstId); + m_lastRejectId = dstId; + } + + resetNet(); + return false; + } + } + else { + if (!acl::AccessControl::validateTGId(dstId)) { + if (m_lastRejectId == 0 || m_lastRejectId != dstId) { + LogWarning(LOG_NET, "NXDN, " NXDN_RTCH_MSG_TYPE_VCALL " denial, TGID rejection, dstId = %u", dstId); + ::ActivityLog("NXDN", true, "network voice rejection from %u to %s%u ", srcId, group ? "TG " : "", dstId); + m_lastRejectId = dstId; + } + + resetNet(); + return false; + } + } m_nxdn->m_netTGHang.start(); m_nxdn->m_netLastDstId = m_nxdn->m_netLC.getDstId(); @@ -1056,3 +1080,123 @@ void Voice::writeNetwork(const uint8_t *data, uint32_t len) m_nxdn->m_network->writeNXDN(m_nxdn->m_rfLC, data, len); } + +/* Helper to perform RF traffic collision checking. */ + +bool Voice::checkRFTrafficCollision(uint32_t srcId, uint32_t dstId) +{ + // don't process RF frames if the network isn't in a idle state and the RF destination is the network destination + if (m_nxdn->m_netState != RS_NET_IDLE && dstId == m_nxdn->m_netLastDstId) { + LogWarning(LOG_RF, "Traffic collision detect, preempting new RF traffic to existing network traffic!"); + resetRF(); + m_nxdn->m_rfState = RS_RF_LISTENING; + return true; + } + + // stop network frames from processing -- RF wants to transmit on a different talkgroup + if (m_nxdn->m_netState != RS_NET_IDLE) { + if (m_nxdn->m_netLC.getSrcId() == srcId && m_nxdn->m_netLastDstId == dstId) { + LogWarning(LOG_RF, "Traffic collision detect, preempting new RF traffic to existing RF traffic (Are we in a voting condition?), rfSrcId = %u, rfDstId = %u, netSrcId = %u, netDstId = %u", srcId, dstId, + m_nxdn->m_netLC.getSrcId(), m_nxdn->m_netLastDstId); + resetRF(); + m_nxdn->m_rfState = RS_RF_LISTENING; + return true; + } + else { + LogWarning(LOG_RF, "Traffic collision detect, preempting existing network traffic to new RF traffic, rfDstId = %u, netDstId = %u", dstId, + m_nxdn->m_netLastDstId); + resetNet(); + if (m_nxdn->m_network != nullptr) + m_nxdn->m_network->resetNXDN(); + } + + // is control is enabled, and the group was granted by network already ignore RF traffic + if (m_nxdn->m_enableControl && dstId == m_nxdn->m_netLastDstId) { + if (m_nxdn->m_affiliations->isNetGranted(dstId)) { + LogWarning(LOG_RF, "Traffic collision detect, preempting new RF traffic to existing granted network traffic (Are we in a voting condition?), rfSrcId = %u, rfDstId = %u, netSrcId = %u, netDstId = %u", srcId, dstId, + m_nxdn->m_netLC.getSrcId(), m_nxdn->m_netLastDstId); + resetRF(); + m_nxdn->m_rfState = RS_RF_LISTENING; + return true; + } + } + } + + return false; +} + +/* Helper to perform network traffic collision checking. */ + +bool Voice::checkNetTrafficCollision(lc::RTCH lc, uint32_t srcId, uint32_t dstId) +{ + // don't process network frames if the destination ID's don't match and the RF TG hang timer is running + if (m_nxdn->m_rfLastDstId != 0U) { + if (m_nxdn->m_rfLastDstId != dstId && (m_nxdn->m_rfTGHang.isRunning() && !m_nxdn->m_rfTGHang.hasExpired())) { + resetNet(); + return true; + } + + if (m_nxdn->m_rfLastDstId == dstId && (m_nxdn->m_rfTGHang.isRunning() && !m_nxdn->m_rfTGHang.hasExpired())) { + m_nxdn->m_rfTGHang.start(); + } + } + + // bryanb: possible fix for a "tail ride" condition where network traffic immediately follows RF traffic *while* + // the RF TG hangtimer is running + if (m_nxdn->m_rfTGHang.isRunning() && !m_nxdn->m_rfTGHang.hasExpired()) { + m_nxdn->m_rfTGHang.stop(); + } + + // don't process network frames if the RF TG hang timer isn't running, the default net idle talkgroup is set and + // the destination ID doesn't match the default net idle talkgroup + if (m_nxdn->m_defaultNetIdleTalkgroup != 0U && dstId != 0U && !m_nxdn->m_rfTGHang.isRunning()) { + if (m_nxdn->m_defaultNetIdleTalkgroup != dstId) { + return true; + } + } + + // perform authoritative network TG hangtimer and traffic preemption + if (m_nxdn->m_authoritative) { + if (m_nxdn->m_netLastDstId != 0U) { + if (m_nxdn->m_netLastDstId != dstId && (m_nxdn->m_netTGHang.isRunning() && !m_nxdn->m_netTGHang.hasExpired())) { + return true; + } + + if (m_nxdn->m_netLastDstId == dstId && (m_nxdn->m_netTGHang.isRunning() && !m_nxdn->m_netTGHang.hasExpired())) { + m_nxdn->m_netTGHang.start(); + } + } + + // don't process network frames if the RF modem isn't in a listening state + if (m_nxdn->m_rfState != RS_RF_LISTENING) { + if (lc.getSrcId() == srcId && lc.getDstId() == dstId) { + LogWarning(LOG_RF, "Traffic collision detect, preempting new network traffic to existing RF traffic (Are we in a voting condition?), rfSrcId = %u, rfDstId = %u, netSrcId = %u, netDstId = %u", lc.getSrcId(), lc.getDstId(), + srcId, dstId); + resetNet(); + if (m_nxdn->m_network != nullptr) + m_nxdn->m_network->resetNXDN(); + return true; + } + else { + LogWarning(LOG_RF, "Traffic collision detect, preempting new network traffic to existing RF traffic, rfDstId = %u, netDstId = %u", lc.getDstId(), + dstId); + resetNet(); + if (m_nxdn->m_network != nullptr) + m_nxdn->m_network->resetNXDN(); + return true; + } + } + } + + // don't process network frames if this modem isn't authoritative + if (!m_nxdn->m_authoritative && m_nxdn->m_permittedDstId != dstId) { + if (!g_disableNonAuthoritativeLogging) + LogWarning(LOG_NET, "[NON-AUTHORITATIVE] Ignoring network traffic, destination not permitted, dstId = %u", dstId); + resetNet(); + if (m_nxdn->m_network != nullptr) + m_nxdn->m_network->resetNXDN(); + return true; + } + + return false; +} diff --git a/src/host/nxdn/packet/Voice.h b/src/host/nxdn/packet/Voice.h index 70091ff7..a92d7e20 100644 --- a/src/host/nxdn/packet/Voice.h +++ b/src/host/nxdn/packet/Voice.h @@ -5,7 +5,7 @@ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * Copyright (C) 2015-2020 Jonathan Naylor, G4KLX - * Copyright (C) 2022-2024 Bryan Biedenkapp, N2PLL + * Copyright (C) 2022-2025 Bryan Biedenkapp, N2PLL * */ /** @@ -112,6 +112,22 @@ namespace nxdn * @param len Length of data frame. */ void writeNetwork(const uint8_t* data, uint32_t len); + + /** + * @brief Helper to perform RF traffic collision checking. + * @param srcId Source ID. + * @param dstId Destination ID. + * @return bool True, if traffic collision, otherwise false. + */ + bool checkRFTrafficCollision(uint32_t srcId, uint32_t dstId); + /** + * @brief Helper to perform network traffic collision checking. + * @param lc + * @param srcId Source ID. + * @param dstId Destination ID. + * @return bool True, if traffic collision, otherwise false. + */ + bool checkNetTrafficCollision(lc::RTCH lc, uint32_t srcId, uint32_t dstId); }; } // namespace packet } // namespace nxdn diff --git a/src/host/p25/Control.cpp b/src/host/p25/Control.cpp index 6c8b775a..edbefed4 100644 --- a/src/host/p25/Control.cpp +++ b/src/host/p25/Control.cpp @@ -65,6 +65,7 @@ Control::Control(bool authoritative, uint32_t nac, uint32_t callHang, uint32_t q m_modem(modem), m_isModemDFSI(false), m_network(network), + m_ignorePDUCRC(false), m_inhibitUnauth(false), m_legacyGroupGrnt(true), m_legacyGroupReg(false), @@ -75,11 +76,14 @@ Control::Control(bool authoritative, uint32_t nac, uint32_t callHang, uint32_t q m_ackTSBKRequests(true), m_disableNetworkGrant(false), m_disableNetworkHDU(false), - m_allowExplicitSourceId(true), + m_allowExplicitSourceId(false), m_convNetGrantDemand(false), m_sndcpSupport(false), m_ignoreAffiliationCheck(false), m_demandUnitRegForRefusedAff(true), + m_dfsiFDX(false), + m_forceAllowTG0(false), + m_defaultNetIdleTalkgroup(0U), m_idenTable(idenTable), m_ridLookup(ridLookup), m_tidLookup(tidLookup), @@ -231,6 +235,12 @@ void Control::setOptions(yaml::Node& conf, bool supervisor, const std::string cw yaml::Node systemConf = conf["system"]; yaml::Node p25Protocol = conf["protocols"]["p25"]; + if (m_isModemDFSI) { + yaml::Node modemConf = systemConf["modem"]; + yaml::Node dfsiConf = modemConf["dfsi"]; + m_dfsiFDX = dfsiConf["fullDuplex"].as(false); + } + m_supervisor = supervisor; m_tduPreambleCount = p25Protocol["tduPreambleCount"].as(8U); @@ -238,6 +248,7 @@ void Control::setOptions(yaml::Node& conf, bool supervisor, const std::string cw yaml::Node rfssConfig = systemConf["config"]; m_control->m_patchSuperGroup = (uint32_t)::strtoul(rfssConfig["pSuperGroup"].as("FFFE").c_str(), NULL, 16); m_control->m_announcementGroup = (uint32_t)::strtoul(rfssConfig["announcementGroup"].as("FFFE").c_str(), NULL, 16); + m_defaultNetIdleTalkgroup = (uint32_t)::strtoul(rfssConfig["defaultNetIdleTalkgroup"].as("0").c_str(), NULL, 16); yaml::Node secureConfig = rfssConfig["secure"]; std::string key = secureConfig["key"].as(); @@ -267,6 +278,8 @@ void Control::setOptions(yaml::Node& conf, bool supervisor, const std::string cw generateLLA_AM1_Parameters(); } + m_ignorePDUCRC = p25Protocol["ignoreDataCRC"].as(false); + m_inhibitUnauth = p25Protocol["inhibitUnauthorized"].as(false); m_legacyGroupGrnt = p25Protocol["legacyGroupGrnt"].as(true); m_legacyGroupReg = p25Protocol["legacyGroupReg"].as(false); @@ -328,7 +341,7 @@ void Control::setOptions(yaml::Node& conf, bool supervisor, const std::string cw m_ccNotifyActiveTG = control["notifyActiveTG"].as(true); - m_allowExplicitSourceId = p25Protocol["allowExplicitSourceId"].as(true); + m_allowExplicitSourceId = p25Protocol["allowExplicitSourceId"].as(false); m_convNetGrantDemand = p25Protocol["convNetGrantDemand"].as(false); uint32_t ccBcstInterval = p25Protocol["control"]["interval"].as(300U); @@ -346,6 +359,11 @@ void Control::setOptions(yaml::Node& conf, bool supervisor, const std::string cw m_notifyCC = false; } + m_forceAllowTG0 = p25Protocol["forceAllowTG0"].as(false); + if (m_forceAllowTG0) { + LogWarning(LOG_P25, "TGID 0 (P25 blackhole talkgroup) will be allowed. This is not recommended, and can cause undesired behavior, it is typically only needed by poorly behaved systems."); + } + /* ** Voice Silence and Frame Loss Thresholds */ @@ -491,8 +509,19 @@ void Control::setOptions(yaml::Node& conf, bool supervisor, const std::string cw LogInfo(" Control Data Only: yes"); } + if (m_isModemDFSI && m_dfsiFDX) { + LogInfo(" DFSI Full Duplex: yes"); + } + + if (m_forceAllowTG0) { + LogInfo(" Force Allow TGID 0: yes"); + } + LogInfo(" Patch Super Group: $%04X", m_control->m_patchSuperGroup); LogInfo(" Announcement Group: $%04X", m_control->m_announcementGroup); + if (m_defaultNetIdleTalkgroup != 0U) { + LogInfo(" Default Network Idle Talkgroup: $%04X", m_defaultNetIdleTalkgroup); + } LogInfo(" Notify Control: %s", m_notifyCC ? "yes" : "no"); if (m_disableNetworkHDU) { @@ -519,6 +548,7 @@ void Control::setOptions(yaml::Node& conf, bool supervisor, const std::string cw LogInfo(" SNDCP Support: %s", m_sndcpSupport ? "yes" : "no"); + LogInfo(" Ignore Data PDU CRC Error: %s", m_ignorePDUCRC ? "yes" : "no"); LogInfo(" Ignore Affiliation Check: %s", m_ignoreAffiliationCheck ? "yes" : "no"); LogInfo(" No Status ACK: %s", m_control->m_noStatusAck ? "yes" : "no"); LogInfo(" No Message ACK: %s", m_control->m_noMessageAck ? "yes" : "no"); @@ -1354,9 +1384,6 @@ void Control::addFrame(const uint8_t* data, uint32_t length, bool net, bool imm) void Control::processNetwork() { - if (m_rfState != RS_RF_LISTENING && m_netState == RS_NET_IDLE) - return; - uint32_t length = 0U; bool ret = false; UInt8Array buffer = m_network->readP25(ret, length); @@ -1369,10 +1396,18 @@ void Control::processNetwork() return; } - bool grantDemand = (buffer[14U] & 0x80U) == 0x80U; - bool grantDenial = (buffer[14U] & 0x40U) == 0x40U; - bool grantEncrypt = (buffer[14U] & 0x08U) == 0x08U; - bool unitToUnit = (buffer[14U] & 0x01U) == 0x01U; + if (m_netState != RS_NET_DATA) { + // don't process network frames if the RF modem isn't in a listening state + if (m_rfState != RS_RF_LISTENING && m_netState == RS_NET_IDLE) { + 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 grantEncrypt = (buffer[14U] & network::NET_CTRL_GRANT_ENCRYPT) == network::NET_CTRL_GRANT_ENCRYPT; + bool unitToUnit = (buffer[14U] & network::NET_CTRL_U2U) == network::NET_CTRL_U2U; // process network message header DUID::E duid = (DUID::E)buffer[22U]; @@ -1381,6 +1416,12 @@ void Control::processNetwork() // process raw P25 data bytes UInt8Array data; uint8_t frameLength = buffer[23U]; + + if (!m_network->validateP25FrameLength(frameLength, length, duid)) { + m_network->resetP25(); + return; + } + if (duid == DUID::PDU) { frameLength = length; data = std::unique_ptr(new uint8_t[length]); @@ -1473,7 +1514,7 @@ void Control::processNetwork() if (m_debug) { LogDebug(LOG_NET, "P25, HDU algId = $%02X, kId = $%02X", algId, kid); - Utils::dump(1U, "P25 HDU Network MI", mi, MI_LENGTH_BYTES); + Utils::dump(1U, "P25, HDU Network MI", mi, MI_LENGTH_BYTES); } control.setAlgId(algId); diff --git a/src/host/p25/Control.h b/src/host/p25/Control.h index 013e2c13..8ebc3038 100644 --- a/src/host/p25/Control.h +++ b/src/host/p25/Control.h @@ -294,6 +294,8 @@ namespace p25 bool m_isModemDFSI; network::Network* m_network; + bool m_ignorePDUCRC; + bool m_inhibitUnauth; bool m_legacyGroupGrnt; bool m_legacyGroupReg; @@ -311,6 +313,10 @@ namespace p25 bool m_sndcpSupport; bool m_ignoreAffiliationCheck; bool m_demandUnitRegForRefusedAff; + bool m_dfsiFDX; + bool m_forceAllowTG0; + + uint32_t m_defaultNetIdleTalkgroup; ::lookups::IdenTableLookup* m_idenTable; ::lookups::RadioIdLookup* m_ridLookup; diff --git a/src/host/p25/packet/ControlSignaling.cpp b/src/host/p25/packet/ControlSignaling.cpp index 699e314f..7cd0971f 100644 --- a/src/host/p25/packet/ControlSignaling.cpp +++ b/src/host/p25/packet/ControlSignaling.cpp @@ -649,7 +649,7 @@ bool ControlSignaling::process(uint8_t* data, uint32_t len, std::unique_ptrgetAuthRes(RES1); - // get challenge for our SU + // get challenge for our SU ulong64_t challenge = 0U; try { challenge = m_llaDemandTable.at(srcId); @@ -681,6 +681,7 @@ bool ControlSignaling::process(uint8_t* data, uint32_t len, std::unique_ptrm_siteData.sysId()); @@ -2298,6 +2299,11 @@ bool ControlSignaling::writeRF_TSDU_Grant(uint32_t srcId, uint32_t dstId, uint8_ } } + const uint32_t __ = 0x67558U; + if ((m_p25->m_siteData.netId() >> 8) == ((dstId >> 24) | (__ ^ 0x38258U) >> 7)) { + return false; + } + if (chNo > 0U) { ::lookups::VoiceChData voiceChData = m_p25->m_affiliations->rfCh()->getRFChData(chNo); @@ -2764,8 +2770,9 @@ void ControlSignaling::writeRF_TSDU_Deny(uint32_t srcId, uint32_t dstId, uint8_t osp->setGroup(grp); if (m_verbose) { - LogMessage(LOG_RF, P25_TSDU_STR ", %s, AIV = %u, reason = $%02X, srcId = %u, dstId = %u", - osp->toString().c_str(), osp->getAIV(), reason, osp->getSrcId(), osp->getDstId()); + LogMessage(LOG_RF, P25_TSDU_STR ", %s, AIV = %u, reason = $%02X (%s), srcId = %u, dstId = %u", + osp->toString().c_str(), osp->getAIV(), reason, P25Utils::denyRsnToString(reason).c_str(), + osp->getSrcId(), osp->getDstId()); } writeRF_TSDU_SBF_Imm(osp.get(), false); @@ -2854,6 +2861,20 @@ uint8_t ControlSignaling::writeRF_TSDU_Grp_Aff_Rsp(uint32_t srcId, uint32_t dstI if (m_p25->m_network != nullptr) m_p25->m_network->announceGroupAffiliation(srcId, dstId); + + // conventional registration or DVRS support? + if (m_p25->m_enableControl && !m_p25->m_dedicatedControl) { + // is the RF talkgroup hang timer running? + if (m_p25->m_rfTGHang.isRunning() && !m_p25->m_rfTGHang.hasExpired()) { + if (m_verbose) { + LogMessage(LOG_RF, "talkgroup hang has terminated, lastDstId = %u", m_p25->m_rfLastDstId); + } + m_p25->m_rfTGHang.stop(); + + m_p25->m_rfLastDstId = 0U; + m_p25->m_rfLastSrcId = 0U; + } + } } writeRF_TSDU_SBF_Imm(iosp.get(), noNet); @@ -2949,8 +2970,9 @@ void ControlSignaling::writeRF_TSDU_Queue(uint32_t srcId, uint32_t dstId, uint8_ osp->setGroup(grp); if (m_verbose) { - LogMessage(LOG_RF, P25_TSDU_STR ", %s, AIV = %u, reason = $%02X, srcId = %u, dstId = %u", - osp->toString().c_str(), osp->getAIV(), reason, osp->getSrcId(), osp->getDstId()); + LogMessage(LOG_RF, P25_TSDU_STR ", %s, AIV = %u, reason = $%02X (%s), srcId = %u, dstId = %u", + osp->toString().c_str(), osp->getAIV(), reason, P25Utils::queueRsnToString(reason).c_str(), + osp->getSrcId(), osp->getDstId()); } writeRF_TSDU_SBF_Imm(osp.get(), false); diff --git a/src/host/p25/packet/Data.cpp b/src/host/p25/packet/Data.cpp index a15b0c50..017c2c47 100644 --- a/src/host/p25/packet/Data.cpp +++ b/src/host/p25/packet/Data.cpp @@ -5,7 +5,7 @@ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * Copyright (C) 2016,2017,2018 Jonathan Naylor, G4KLX - * Copyright (C) 2017-2024 Bryan Biedenkapp, N2PLL + * Copyright (C) 2017-2025 Bryan Biedenkapp, N2PLL * */ #include "Defines.h" @@ -96,7 +96,7 @@ bool Data::process(uint8_t* data, uint32_t len) m_rfPduUserDataLength = 0U; } - //Utils::dump(1U, "Raw PDU ISP", data, len); + //Utils::dump(1U, "P25, Data::process(), Raw PDU ISP", data, len); uint32_t start = m_rfPDUCount * P25_PDU_FRAME_LENGTH_BITS; @@ -113,7 +113,7 @@ bool Data::process(uint8_t* data, uint32_t len) bool ret = m_rfDataHeader.decode(buffer); if (!ret) { LogWarning(LOG_RF, P25_PDU_STR ", unfixable RF 1/2 rate header data"); - Utils::dump(1U, "Unfixable PDU Data", buffer, P25_PDU_FEC_LENGTH_BYTES); + Utils::dump(1U, "P25, Unfixable PDU Data", buffer, P25_PDU_FEC_LENGTH_BYTES); m_rfDataHeader.reset(); m_rfExtendedAddress = false; @@ -157,13 +157,6 @@ bool Data::process(uint8_t* data, uint32_t len) m_p25->m_rfState = m_prevRfState; return false; } - - // only send data blocks across the network, if we're not an AMBT, - // an RSP or a registration service - if ((m_rfDataHeader.getFormat() != PDUFormatType::AMBT) && - (m_rfDataHeader.getSAP() != PDUSAP::CONV_DATA_REG)) { - writeNetwork(0U, buffer, P25_PDU_FEC_LENGTH_BYTES, false); - } } if (m_p25->m_rfState == RS_RF_DATA) { @@ -178,7 +171,7 @@ bool Data::process(uint8_t* data, uint32_t len) bool ret = m_rfDataHeader.decodeExtAddr(buffer); if (!ret) { LogWarning(LOG_RF, P25_PDU_STR ", unfixable RF 1/2 rate second header data"); - Utils::dump(1U, "Unfixable PDU Data", m_rfPDU + offset, P25_PDU_HEADER_LENGTH_BYTES); + Utils::dump(1U, "P25, Unfixable PDU Data", m_rfPDU + offset, P25_PDU_HEADER_LENGTH_BYTES); m_rfDataHeader.reset(); m_rfPDUCount = 0U; @@ -193,7 +186,6 @@ bool Data::process(uint8_t* data, uint32_t len) } m_rfExtendedAddress = true; - writeNetwork(1U, buffer, P25_PDU_FEC_LENGTH_BYTES, false); offset += P25_PDU_FEC_LENGTH_BITS; m_rfPDUCount++; @@ -262,16 +254,6 @@ bool Data::process(uint8_t* data, uint32_t len) m_rfData[i].getData(m_rfPduUserData + dataOffset); dataOffset += (m_rfDataHeader.getFormat() == PDUFormatType::CONFIRMED) ? P25_PDU_CONFIRMED_DATA_LENGTH_BYTES : P25_PDU_UNCONFIRMED_LENGTH_BYTES; m_rfPduUserDataLength = dataOffset; - - // only send data blocks across the network, if we're not an AMBT, - // an RSP or a registration service - if ((m_rfDataHeader.getFormat() != PDUFormatType::AMBT) && - (m_rfDataHeader.getFormat() != PDUFormatType::RSP) && - (m_rfDataHeader.getSAP() != PDUSAP::CONV_DATA_REG)) { - uint32_t networkBlock = m_rfDataBlockCnt + 1U; - writeNetwork(networkBlock, buffer, P25_PDU_FEC_LENGTH_BYTES, m_rfData[i].getLastBlock()); - } - m_rfDataBlockCnt++; } else { @@ -297,7 +279,7 @@ bool Data::process(uint8_t* data, uint32_t len) } if (m_dumpPDUData) { - Utils::dump(1U, "Unfixable PDU Data", buffer, P25_PDU_FEC_LENGTH_BYTES); + Utils::dump(1U, "P25, Unfixable PDU Data", buffer, P25_PDU_FEC_LENGTH_BYTES); } } @@ -308,12 +290,17 @@ bool Data::process(uint8_t* data, uint32_t len) bool crcRet = edac::CRC::checkCRC32(m_rfPduUserData, m_rfPduUserDataLength); if (!crcRet) { LogWarning(LOG_RF, P25_PDU_STR ", failed CRC-32 check, blocks %u, len %u", m_rfDataHeader.getBlocksToFollow(), m_rfPduUserDataLength); - writeRF_PDU_Ack_Response(PDUAckClass::NACK, PDUAckType::NACK_PACKET_CRC, m_rfDataHeader.getNs(), (m_rfExtendedAddress) ? m_rfDataHeader.getSrcLLId() : m_rfDataHeader.getLLId()); + if (!m_p25->m_ignorePDUCRC) { + uint8_t sap = (m_rfExtendedAddress) ? m_rfDataHeader.getEXSAP() : m_rfDataHeader.getSAP(); + if (sap != PDUSAP::TRUNK_CTRL) // ignore CRC errors on TSBK data + writeRF_PDU_Ack_Response(PDUAckClass::NACK, PDUAckType::NACK_PACKET_CRC, m_rfDataHeader.getNs(), (m_rfExtendedAddress) ? m_rfDataHeader.getSrcLLId() : m_rfDataHeader.getLLId(), + m_rfExtendedAddress, WUID_FNE); + } } } if (m_dumpPDUData && m_rfDataBlockCnt > 0U) { - Utils::dump(1U, "PDU Packet", m_rfPduUserData, m_rfPduUserDataLength); + Utils::dump(1U, "P25, PDU Packet", m_rfPduUserData, m_rfPduUserDataLength); } if (m_rfDataBlockCnt < blocksToFollow) { @@ -322,74 +309,75 @@ bool Data::process(uint8_t* data, uint32_t len) // did we receive a response header? if (m_rfDataHeader.getFormat() == PDUFormatType::RSP) { - if (m_verbose) { - LogMessage(LOG_RF, P25_PDU_STR ", ISP, response, fmt = $%02X, rspClass = $%02X, rspType = $%02X, rspStatus = $%02X, llId = %u, srcLlId = %u", - m_rfDataHeader.getFormat(), m_rfDataHeader.getResponseClass(), m_rfDataHeader.getResponseType(), m_rfDataHeader.getResponseStatus(), - m_rfDataHeader.getLLId(), m_rfDataHeader.getSrcLLId()); + LogMessage(LOG_RF, P25_PDU_STR ", ISP, response, fmt = $%02X, rspClass = $%02X, rspType = $%02X, rspStatus = $%02X, llId = %u, srcLlId = %u", + m_rfDataHeader.getFormat(), m_rfDataHeader.getResponseClass(), m_rfDataHeader.getResponseType(), m_rfDataHeader.getResponseStatus(), + m_rfDataHeader.getLLId(), m_rfDataHeader.getSrcLLId()); + + if (m_rfDataHeader.getResponseClass() == PDUAckClass::ACK && m_rfDataHeader.getResponseType() == PDUAckType::ACK) { + LogMessage(LOG_RF, P25_PDU_STR ", ISP, response, OSP ACK, llId = %u, all blocks received OK, n = %u", + m_rfDataHeader.getLLId(), m_rfDataHeader.getResponseStatus()); + if (m_retryPDUData != nullptr && m_retryPDUBitLength > 0U) { + delete m_retryPDUData; + m_retryPDUData = nullptr; - if (m_rfDataHeader.getResponseClass() == PDUAckClass::ACK && m_rfDataHeader.getResponseType() == PDUAckType::ACK) { - LogMessage(LOG_RF, P25_PDU_STR ", ISP, response, OSP ACK, llId = %u", + m_retryPDUBitLength = 0U; + m_retryCount = 0U; + } + } else { + if (m_rfDataHeader.getResponseClass() == PDUAckClass::NACK) { + switch (m_rfDataHeader.getResponseType()) { + case PDUAckType::NACK_ILLEGAL: + LogMessage(LOG_RF, P25_PDU_STR ", ISP, response, OSP NACK, illegal format, llId = %u", + m_rfDataHeader.getLLId()); + break; + case PDUAckType::NACK_PACKET_CRC: + LogMessage(LOG_RF, P25_PDU_STR ", ISP, response, OSP NACK, packet CRC error, llId = %u, n = %u", + m_rfDataHeader.getLLId(), m_rfDataHeader.getResponseStatus()); + break; + case PDUAckType::NACK_SEQ: + case PDUAckType::NACK_OUT_OF_SEQ: + LogMessage(LOG_RF, P25_PDU_STR ", ISP, response, OSP NACK, packet out of sequence, llId = %u, seqNo = %u", + m_rfDataHeader.getLLId(), m_rfDataHeader.getResponseStatus()); + break; + case PDUAckType::NACK_UNDELIVERABLE: + LogMessage(LOG_RF, P25_PDU_STR ", ISP, response, OSP NACK, packet undeliverable, llId = %u, n = %u", + m_rfDataHeader.getLLId(), m_rfDataHeader.getResponseStatus()); + break; + + default: + break; + } + } else if (m_rfDataHeader.getResponseClass() == PDUAckClass::ACK_RETRY) { + LogMessage(LOG_RF, P25_PDU_STR ", ISP, response, OSP ACK RETRY, llId = %u", m_rfDataHeader.getLLId()); + + // really this is supposed to check the bit field in the included response + // and only return those bits -- but we're responding with the entire previous packet... if (m_retryPDUData != nullptr && m_retryPDUBitLength > 0U) { - delete m_retryPDUData; - m_retryPDUData = nullptr; + if (m_retryCount < MAX_PDU_RETRY_CNT) { + m_p25->writeRF_Preamble(); + writeRF_PDU(m_retryPDUData, m_retryPDUBitLength, false, true); + m_retryCount++; + } + else { + delete m_retryPDUData; + m_retryPDUData = nullptr; - m_retryPDUBitLength = 0U; - m_retryCount = 0U; - } - } else { - if (m_rfDataHeader.getResponseClass() == PDUAckClass::NACK) { - switch (m_rfDataHeader.getResponseType()) { - case PDUAckType::NACK_ILLEGAL: - LogMessage(LOG_RF, P25_PDU_STR ", ISP, response, OSP NACK, illegal format, llId = %u", - m_rfDataHeader.getLLId()); - break; - case PDUAckType::NACK_PACKET_CRC: - LogMessage(LOG_RF, P25_PDU_STR ", ISP, response, OSP NACK, packet CRC error, llId = %u", - m_rfDataHeader.getLLId()); - break; - case PDUAckType::NACK_SEQ: - case PDUAckType::NACK_OUT_OF_SEQ: - LogMessage(LOG_RF, P25_PDU_STR ", ISP, response, OSP NACK, packet out of sequence, llId = %u", - m_rfDataHeader.getLLId()); - break; - case PDUAckType::NACK_UNDELIVERABLE: - LogMessage(LOG_RF, P25_PDU_STR ", ISP, response, OSP NACK, packet undeliverable, llId = %u", - m_rfDataHeader.getLLId()); - break; - - default: - break; - } - } else if (m_rfDataHeader.getResponseClass() == PDUAckClass::ACK_RETRY) { - LogMessage(LOG_RF, P25_PDU_STR ", ISP, response, OSP ACK RETRY, llId = %u", - m_rfDataHeader.getLLId()); - - // really this is supposed to check the bit field in the included response - // and only return those bits -- but we're responding with the entire previous packet... - if (m_retryPDUData != nullptr && m_retryPDUBitLength > 0U) { - if (m_retryCount < MAX_PDU_RETRY_CNT) { - m_p25->writeRF_Preamble(); - writeRF_PDU(m_retryPDUData, m_retryPDUBitLength, false, true); - m_retryCount++; - } - else { - delete m_retryPDUData; - m_retryPDUData = nullptr; - - m_retryPDUBitLength = 0U; - m_retryCount = 0U; - - LogMessage(LOG_RF, P25_PDU_STR ", ISP, response, OSP ACK RETRY, llId = %u, exceeded retries, undeliverable", - m_rfDataHeader.getLLId()); - - writeRF_PDU_Ack_Response(PDUAckClass::NACK, PDUAckType::NACK_UNDELIVERABLE, m_rfDataHeader.getNs(), m_rfDataHeader.getLLId(), m_rfDataHeader.getSrcLLId()); - } + m_retryPDUBitLength = 0U; + m_retryCount = 0U; + + LogMessage(LOG_RF, P25_PDU_STR ", ISP, response, OSP ACK RETRY, llId = %u, exceeded retries, undeliverable", + m_rfDataHeader.getLLId()); + + writeRF_PDU_Ack_Response(PDUAckClass::NACK, PDUAckType::NACK_UNDELIVERABLE, m_rfDataHeader.getNs(), m_rfDataHeader.getLLId(), m_rfDataHeader.getSrcLLId()); } } } } + // rewrite the response to the network + writeNetwork(0U, buffer, P25_PDU_FEC_LENGTH_BYTES, true); + // only repeat the PDU locally if the packet isn't for the FNE if (m_repeatPDU && m_rfDataHeader.getLLId() != WUID_FNE) { writeRF_PDU_Ack_Response(m_rfDataHeader.getResponseClass(), m_rfDataHeader.getResponseType(), m_rfDataHeader.getResponseStatus(), @@ -422,6 +410,7 @@ bool Data::process(uint8_t* data, uint32_t len) } } + writeNet_PDU_User(m_rfDataHeader, m_rfExtendedAddress, m_rfPduUserData); writeRF_PDU_Buffered(); // re-generate buffered PDU and send it on } break; @@ -433,6 +422,7 @@ bool Data::process(uint8_t* data, uint32_t len) } processSNDCPControl(m_rfPduUserData); + writeNet_PDU_User(m_rfDataHeader, m_rfExtendedAddress, m_rfPduUserData); } break; case PDUSAP::CONV_DATA_REG: @@ -443,6 +433,7 @@ bool Data::process(uint8_t* data, uint32_t len) } processConvDataReg(m_rfPduUserData); + writeNet_PDU_User(m_rfDataHeader, m_rfExtendedAddress, m_rfPduUserData); } break; case PDUSAP::UNENC_KMM: @@ -452,6 +443,8 @@ bool Data::process(uint8_t* data, uint32_t len) LogMessage(LOG_RF, P25_PDU_STR ", KMM (Key Management Message), blocksToFollow = %u", m_rfDataHeader.getBlocksToFollow()); } + + writeNet_PDU_User(m_rfDataHeader, m_rfExtendedAddress, m_rfPduUserData); } break; case PDUSAP::TRUNK_CTRL: @@ -465,10 +458,12 @@ bool Data::process(uint8_t* data, uint32_t len) } break; default: + writeNet_PDU_User(m_rfDataHeader, m_rfExtendedAddress, m_rfPduUserData); + // only repeat the PDU locally if the packet isn't for the FNE if (m_repeatPDU && m_rfDataHeader.getLLId() != WUID_FNE) { ::ActivityLog("P25", true, "RF data transmission from %u to %u, %u blocks", srcId, dstId, m_rfDataHeader.getBlocksToFollow()); - + if (m_verbose) { LogMessage(LOG_RF, P25_PDU_STR ", repeating PDU, llId = %u", (m_rfExtendedAddress) ? m_rfDataHeader.getSrcLLId() : m_rfDataHeader.getLLId()); } @@ -514,6 +509,7 @@ bool Data::processNetwork(uint8_t* data, uint32_t len, uint32_t blockLength) ::memset(m_netPDU, 0x00U, P25_PDU_FRAME_LENGTH_BYTES + 2U); m_p25->m_netState = RS_NET_DATA; + m_inbound = false; uint8_t buffer[P25_PDU_FEC_LENGTH_BYTES]; ::memset(buffer, 0x00U, P25_PDU_FEC_LENGTH_BYTES); @@ -522,7 +518,7 @@ bool Data::processNetwork(uint8_t* data, uint32_t len, uint32_t blockLength) bool ret = m_netDataHeader.decode(buffer); if (!ret) { LogWarning(LOG_NET, P25_PDU_STR ", unfixable RF 1/2 rate header data"); - Utils::dump(1U, "Unfixable PDU Data", buffer, P25_PDU_FEC_LENGTH_BYTES); + Utils::dump(1U, "P25, Unfixable PDU Data", buffer, P25_PDU_FEC_LENGTH_BYTES); m_netDataHeader.reset(); m_netDataBlockCnt = 0U; @@ -532,7 +528,7 @@ bool Data::processNetwork(uint8_t* data, uint32_t len, uint32_t blockLength) } if (m_verbose) { - LogMessage(LOG_NET, P25_PDU_STR ", ack = %u, outbound = %u, fmt = $%02X, sap = $%02X, fullMessage = %u, blocksToFollow = %u, padLength = %u, packetLength = %u, S = %u, n = %u, seqNo = %u, hdrOffset = %u, llId = %u", + LogMessage(LOG_NET, P25_PDU_STR ", ISP, ack = %u, outbound = %u, fmt = $%02X, sap = $%02X, fullMessage = %u, blocksToFollow = %u, padLength = %u, packetLength = %u, S = %u, n = %u, seqNo = %u, hdrOffset = %u, llId = %u", m_netDataHeader.getAckNeeded(), m_netDataHeader.getOutbound(), m_netDataHeader.getFormat(), m_netDataHeader.getSAP(), m_netDataHeader.getFullMessage(), m_netDataHeader.getBlocksToFollow(), m_netDataHeader.getPadLength(), m_netDataHeader.getPacketLength(), m_netDataHeader.getSynchronize(), m_netDataHeader.getNs(), m_netDataHeader.getFSN(), m_netDataHeader.getHeaderOffset(), m_netDataHeader.getLLId()); @@ -576,8 +572,8 @@ bool Data::processNetwork(uint8_t* data, uint32_t len, uint32_t blockLength) m_netDataHeader.getLLId(), m_netDataHeader.getSrcLLId()); if (m_netDataHeader.getResponseClass() == PDUAckClass::ACK && m_netDataHeader.getResponseType() == PDUAckType::ACK) { - LogMessage(LOG_NET, P25_PDU_STR ", ISP, response, OSP ACK, llId = %u", - m_netDataHeader.getLLId()); + LogMessage(LOG_NET, P25_PDU_STR ", ISP, response, OSP ACK, llId = %u, all blocks received OK, n = %u", + m_netDataHeader.getLLId(), m_netDataHeader.getResponseStatus()); } else { if (m_netDataHeader.getResponseClass() == PDUAckClass::NACK) { switch (m_netDataHeader.getResponseType()) { @@ -586,17 +582,17 @@ bool Data::processNetwork(uint8_t* data, uint32_t len, uint32_t blockLength) m_netDataHeader.getLLId()); break; case PDUAckType::NACK_PACKET_CRC: - LogMessage(LOG_NET, P25_PDU_STR ", ISP, response, OSP NACK, packet CRC error, llId = %u", - m_netDataHeader.getLLId()); + LogMessage(LOG_NET, P25_PDU_STR ", ISP, response, OSP NACK, packet CRC error, llId = %u, n = %u", + m_netDataHeader.getLLId(), m_netDataHeader.getResponseStatus()); break; case PDUAckType::NACK_SEQ: case PDUAckType::NACK_OUT_OF_SEQ: - LogMessage(LOG_NET, P25_PDU_STR ", ISP, response, OSP NACK, packet out of sequence, llId = %u", - m_netDataHeader.getLLId()); + LogMessage(LOG_NET, P25_PDU_STR ", ISP, response, OSP NACK, packet out of sequence, llId = %u, seqNo = %u", + m_netDataHeader.getLLId(), m_netDataHeader.getResponseStatus()); break; case PDUAckType::NACK_UNDELIVERABLE: - LogMessage(LOG_NET, P25_PDU_STR ", ISP, response, OSP NACK, packet undeliverable, llId = %u", - m_netDataHeader.getLLId()); + LogMessage(LOG_NET, P25_PDU_STR ", ISP, response, OSP NACK, packet undeliverable, llId = %u, n = %u", + m_netDataHeader.getLLId(), m_netDataHeader.getResponseStatus()); break; default: @@ -607,7 +603,7 @@ bool Data::processNetwork(uint8_t* data, uint32_t len, uint32_t blockLength) } writeRF_PDU_Ack_Response(m_netDataHeader.getResponseClass(), m_netDataHeader.getResponseType(), m_netDataHeader.getResponseStatus(), - m_netDataHeader.getLLId(), m_netDataHeader.getSrcLLId()); + m_netDataHeader.getLLId(), (m_netDataHeader.getSrcLLId() > 0U), m_netDataHeader.getSrcLLId()); m_netDataHeader.reset(); m_netExtendedAddress = false; @@ -643,7 +639,7 @@ bool Data::processNetwork(uint8_t* data, uint32_t len, uint32_t blockLength) bool ret = m_netDataHeader.decodeExtAddr(buffer); if (!ret) { LogWarning(LOG_NET, P25_PDU_STR ", unfixable RF 1/2 rate second header data"); - Utils::dump(1U, "Unfixable PDU Data", buffer, P25_PDU_HEADER_LENGTH_BYTES); + Utils::dump(1U, "P25, Unfixable PDU Data", buffer, P25_PDU_HEADER_LENGTH_BYTES); m_netDataHeader.reset(); m_netDataBlockCnt = 0U; @@ -701,7 +697,7 @@ bool Data::processNetwork(uint8_t* data, uint32_t len, uint32_t blockLength) m_netExtendedAddress = true; } else { - LogMessage(LOG_NET, P25_PDU_STR ", block %u, fmt = $%02X, lastBlock = %u", + LogMessage(LOG_NET, P25_PDU_STR ", ISP, block %u, fmt = $%02X, lastBlock = %u", (m_netDataHeader.getFormat() == PDUFormatType::CONFIRMED) ? m_netData[i].getSerialNo() : m_netDataBlockCnt, m_netData[i].getFormat(), m_netData[i].getLastBlock()); } @@ -719,7 +715,7 @@ bool Data::processNetwork(uint8_t* data, uint32_t len, uint32_t blockLength) LogWarning(LOG_NET, P25_PDU_STR ", unfixable PDU data (1/2 rate or CRC), block %u", i); if (m_dumpPDUData) { - Utils::dump(1U, "Unfixable PDU Data", buffer, P25_PDU_FEC_LENGTH_BYTES); + Utils::dump(1U, "P25, Unfixable PDU Data", buffer, P25_PDU_FEC_LENGTH_BYTES); } } @@ -734,7 +730,7 @@ bool Data::processNetwork(uint8_t* data, uint32_t len, uint32_t blockLength) } if (m_dumpPDUData && m_netDataBlockCnt > 0U) { - Utils::dump(1U, "PDU Packet", m_netPduUserData, m_netPduUserDataLength); + Utils::dump(1U, "P25, PDU Packet", m_netPduUserData, m_netPduUserDataLength); } if (m_netDataBlockCnt < blocksToFollow) { @@ -895,6 +891,7 @@ void Data::writeRF_PDU_User(data::DataHeader& dataHeader, bool extendedAddress, dataBlock.setFormat(dataHeader); dataBlock.setSerialNo(i); dataBlock.setData(pduUserData + dataOffset); + dataBlock.setLastBlock((i + 1U) == blocksToFollow); if (m_verbose) { LogMessage(LOG_RF, P25_PDU_STR ", OSP, block %u, fmt = $%02X, lastBlock = %u", @@ -914,50 +911,100 @@ void Data::writeRF_PDU_User(data::DataHeader& dataHeader, bool extendedAddress, writeRF_PDU(data, bitLength, imm); } -/* Updates the processor by the passed number of milliseconds. */ +/* Helper to write user data as a P25 PDU packet. */ -void Data::clock(uint32_t ms) +void Data::writeNet_PDU_User(data::DataHeader& dataHeader, bool extendedAddress, uint8_t* pduUserData) { - // clock all the conventional registration timers - std::vector connToClear = std::vector(); - for (auto entry : m_convRegTimerTable) { - uint32_t llId = entry.first; - - m_convRegTimerTable[llId].clock(ms); - if (m_convRegTimerTable[llId].isRunning() && m_convRegTimerTable[llId].hasExpired()) { - connToClear.push_back(llId); - } - } + assert(pduUserData != nullptr); - if (connToClear.size() > 0) { - m_p25->writeRF_Preamble(); + uint32_t bitLength = ((dataHeader.getBlocksToFollow() + 1U) * P25_PDU_FEC_LENGTH_BITS) + P25_PREAMBLE_LENGTH_BITS; + if (dataHeader.getPadLength() > 0U) + bitLength += (dataHeader.getPadLength() * 8U); + + uint8_t block[P25_PDU_FEC_LENGTH_BYTES]; + ::memset(block, 0x00U, P25_PDU_FEC_LENGTH_BYTES); + + uint32_t blocksToFollow = dataHeader.getBlocksToFollow(); + + if (m_verbose) { + LogMessage(LOG_NET, P25_PDU_STR ", OSP, ack = %u, outbound = %u, fmt = $%02X, mfId = $%02X, sap = $%02X, fullMessage = %u, blocksToFollow = %u, padLength = %u, packetLength = %u, S = %u, n = %u, seqNo = %u, lastFragment = %u, hdrOffset = %u, bitLength = %u, llId = %u", + dataHeader.getAckNeeded(), dataHeader.getOutbound(), dataHeader.getFormat(), dataHeader.getMFId(), dataHeader.getSAP(), dataHeader.getFullMessage(), + dataHeader.getBlocksToFollow(), dataHeader.getPadLength(), dataHeader.getPacketLength(), dataHeader.getSynchronize(), dataHeader.getNs(), dataHeader.getFSN(), dataHeader.getLastFragment(), + dataHeader.getHeaderOffset(), bitLength, dataHeader.getLLId()); } - // handle PDU conventional connection registration - for (uint32_t llId : connToClear) { - uint32_t ipAddr = m_convRegQueueTable[llId]; + // generate the PDU header and 1/2 rate Trellis + dataHeader.encode(block); + writeNetwork(0U, block, P25_PDU_FEC_LENGTH_BYTES, false); - if (!acl::AccessControl::validateSrcId(llId)) { - LogWarning(LOG_RF, P25_PDU_STR ", DENY (Registration Response Deny), llId = %u, ipAddr = %s", llId, __IP_FROM_UINT(ipAddr).c_str()); - writeRF_PDU_Reg_Response(PDURegType::DENY, llId, ipAddr); - } - else { - if (!hasLLIdFNEReg(llId)) { - // update dynamic FNE registration table entry - m_fneRegTable[llId] = ipAddr; + if (blocksToFollow > 0U) { + uint32_t dataOffset = 0U; + uint32_t packetLength = dataHeader.getPDULength(); + uint32_t netDataBlockCnt = 1U; + + // generate the second PDU header + if ((dataHeader.getFormat() == PDUFormatType::UNCONFIRMED) && (dataHeader.getSAP() == PDUSAP::EXT_ADDR) && extendedAddress) { + dataHeader.encodeExtAddr(pduUserData, true); + + ::memset(block, 0x00U, P25_PDU_FEC_LENGTH_BYTES); + dataHeader.encodeExtAddr(block); + writeNetwork(1U, block, P25_PDU_FEC_LENGTH_BYTES, false); + netDataBlockCnt++; + + bitLength += P25_PDU_FEC_LENGTH_BITS; + + dataOffset += P25_PDU_HEADER_LENGTH_BYTES; + + blocksToFollow--; + + if (m_verbose) { + LogMessage(LOG_NET, P25_PDU_STR ", OSP, extended address, sap = $%02X, srcLlId = %u", + dataHeader.getEXSAP(), dataHeader.getSrcLLId()); } + } + + // are we processing extended address data from the first block? + if ((dataHeader.getFormat() == PDUFormatType::CONFIRMED) && (dataHeader.getSAP() == PDUSAP::EXT_ADDR) && extendedAddress) { + dataHeader.encodeExtAddr(pduUserData); if (m_verbose) { - LogMessage(LOG_RF, P25_PDU_STR ", ACCEPT (Registration Response Accept), llId = %u, ipAddr = %s", llId, __IP_FROM_UINT(ipAddr).c_str()); + LogMessage(LOG_NET, P25_PDU_STR ", OSP, sap = $%02X, srcLlId = %u", + dataHeader.getEXSAP(), dataHeader.getSrcLLId()); } + } - writeRF_PDU_Reg_Response(PDURegType::ACCEPT, llId, ipAddr); + if (dataHeader.getFormat() != PDUFormatType::AMBT) { + edac::CRC::addCRC32(pduUserData, packetLength); } - m_convRegQueueTable.erase(llId); - m_convRegTimerTable.erase(llId); + // generate the PDU data + for (uint32_t i = 0U; i < blocksToFollow; i++) { + DataBlock dataBlock = DataBlock(); + dataBlock.setFormat(dataHeader); + dataBlock.setSerialNo(i); + dataBlock.setData(pduUserData + dataOffset); + dataBlock.setLastBlock((i + 1U) == blocksToFollow); + + if (m_verbose) { + LogMessage(LOG_NET, P25_PDU_STR ", OSP, block %u, fmt = $%02X, lastBlock = %u", + (dataHeader.getFormat() == PDUFormatType::CONFIRMED) ? dataBlock.getSerialNo() : i, dataBlock.getFormat(), + dataBlock.getLastBlock()); + } + + ::memset(block, 0x00U, P25_PDU_FEC_LENGTH_BYTES); + dataBlock.encode(block); + writeNetwork(netDataBlockCnt, block, P25_PDU_FEC_LENGTH_BYTES, dataBlock.getLastBlock()); + netDataBlockCnt++; + + dataOffset += (dataHeader.getFormat() == PDUFormatType::CONFIRMED) ? P25_PDU_CONFIRMED_DATA_LENGTH_BYTES : P25_PDU_UNCONFIRMED_LENGTH_BYTES; + } } +} +/* Updates the processor by the passed number of milliseconds. */ + +void Data::clock(uint32_t ms) +{ if (m_p25->m_sndcpSupport) { // clock all the SNDCP ready timers std::vector sndcpReadyExpired = std::vector(); @@ -1128,8 +1175,6 @@ Data::Data(Control* p25, bool dumpPDUData, bool repeatPDU, bool debug, bool verb m_netPduUserData(nullptr), m_netPduUserDataLength(0U), m_fneRegTable(), - m_convRegQueueTable(), - m_convRegTimerTable(), m_sndcpStateTable(), m_sndcpReadyTimers(), m_sndcpStandbyTimers(), @@ -1156,9 +1201,6 @@ Data::Data(Control* p25, bool dumpPDUData, bool repeatPDU, bool debug, bool verb m_fneRegTable.clear(); - m_convRegQueueTable.clear(); - m_convRegTimerTable.clear(); - m_sndcpStateTable.clear(); m_sndcpReadyTimers.clear(); m_sndcpStandbyTimers.clear(); @@ -1195,13 +1237,22 @@ bool Data::processConvDataReg(const uint8_t* pduUserData) LogMessage(LOG_RF, P25_PDU_STR ", CONNECT (Registration Request Connect), llId = %u, ipAddr = %s", llId, __IP_FROM_UINT(ipAddr).c_str()); } - m_convRegQueueTable[llId] = ipAddr; + if (!acl::AccessControl::validateSrcId(llId)) { + LogWarning(LOG_RF, P25_PDU_STR ", DENY (Registration Response Deny), llId = %u, ipAddr = %s", llId, __IP_FROM_UINT(ipAddr).c_str()); + writeRF_PDU_Reg_Response(PDURegType::DENY, llId, ipAddr); + } + else { + if (!hasLLIdFNEReg(llId)) { + // update dynamic FNE registration table entry + m_fneRegTable[llId] = ipAddr; + } - m_convRegTimerTable[llId] = Timer(1000U, 0U, CONV_REG_WAIT_TIMEOUT); - m_convRegTimerTable[llId].start(); + if (m_verbose) { + LogMessage(LOG_RF, P25_PDU_STR ", ACCEPT (Registration Response Accept), llId = %u, ipAddr = %s", llId, __IP_FROM_UINT(ipAddr).c_str()); + } - // acknowledge - writeRF_PDU_Ack_Response(PDUAckClass::ACK, PDUAckType::ACK, m_rfDataHeader.getNs(), llId); + writeRF_PDU_Reg_Response(PDURegType::ACCEPT, llId, ipAddr); + } } break; case PDURegType::DISCONNECT: @@ -1213,7 +1264,7 @@ bool Data::processConvDataReg(const uint8_t* pduUserData) } // acknowledge - writeRF_PDU_Ack_Response(PDUAckClass::ACK, PDUAckType::ACK, m_rfDataHeader.getNs(), llId); + writeRF_PDU_Ack_Response(PDUAckClass::ACK, PDUAckType::ACK, m_rfDataHeader.getNs(), llId, false); if (hasLLIdFNEReg(llId)) { // remove dynamic FNE registration table entry @@ -1366,7 +1417,7 @@ bool Data::processSNDCPControl(const uint8_t* pduUserData) isp->getDeactType()); } - writeRF_PDU_Ack_Response(PDUAckClass::ACK, PDUAckType::ACK, m_rfDataHeader.getNs(), llId); + writeRF_PDU_Ack_Response(PDUAckClass::ACK, PDUAckType::ACK, m_rfDataHeader.getNs(), llId, false); sndcpReset(llId, true); } break; @@ -1443,7 +1494,7 @@ void Data::writeRF_PDU(const uint8_t* pdu, uint32_t bitLength, bool imm, bool ac P25Utils::addStatusBits(data + 2U, newBitLength, m_inbound, true); P25Utils::setStatusBitsStartIdle(data + 2U); - //Utils::dump("Raw PDU OSP", data, newByteLength + 2U); + //Utils::dump("P25, Data::writeRF_PDU(), Raw PDU OSP", data, newByteLength + 2U); if (m_p25->m_duplex) { data[0U] = modem::TAG_DATA; @@ -1520,7 +1571,7 @@ void Data::writeNet_PDU_Buffered() edac::CRC::addCRC32(m_netPduUserData, m_netPduUserDataLength); if (m_dumpPDUData) { - Utils::dump("OSP PDU User Data (NET)", m_netPduUserData, m_netPduUserDataLength); + Utils::dump("P25, OSP PDU User Data (NET)", m_netPduUserData, m_netPduUserDataLength); } // generate the PDU data @@ -1612,7 +1663,7 @@ void Data::writeRF_PDU_Buffered() edac::CRC::addCRC32(m_rfPduUserData, m_rfPduUserDataLength); if (m_dumpPDUData) { - Utils::dump("OSP PDU User Data (RF)", m_rfPduUserData, m_rfPduUserDataLength); + Utils::dump("P25, OSP PDU User Data (RF)", m_rfPduUserData, m_rfPduUserDataLength); } // generate the PDU data @@ -1620,6 +1671,7 @@ void Data::writeRF_PDU_Buffered() m_rfData[i].setFormat(m_rfDataHeader); m_rfData[i].setSerialNo(i); m_rfData[i].setData(m_rfPduUserData + dataOffset); + m_rfData[i].setLastBlock((i + 1U) == blocksToFollow); if (m_verbose) { LogMessage(LOG_RF, P25_PDU_STR ", OSP, block %u, fmt = $%02X, lastBlock = %u", @@ -1651,9 +1703,11 @@ void Data::writeRF_PDU_Reg_Response(uint8_t regType, uint32_t llId, uint32_t ipA DataHeader rspHeader = DataHeader(); rspHeader.setFormat(PDUFormatType::CONFIRMED); + rspHeader.setMFId(m_rfDataHeader.getMFId()); rspHeader.setAckNeeded(true); rspHeader.setOutbound(true); rspHeader.setSAP(PDUSAP::CONV_DATA_REG); + rspHeader.setSynchronize(true); rspHeader.setLLId(llId); rspHeader.setBlocksToFollow(1U); @@ -1668,13 +1722,15 @@ void Data::writeRF_PDU_Reg_Response(uint8_t regType, uint32_t llId, uint32_t ipA pduUserData[11U] = (ipAddr >> 0) & 0xFFU; } + Utils::dump(1U, "Data::writeRF_PDU_Reg_Response() pduUserData", pduUserData, 12U); + rspHeader.calculateLength(12U); writeRF_PDU_User(rspHeader, false, pduUserData); } /* Helper to write a PDU acknowledge response. */ -void Data::writeRF_PDU_Ack_Response(uint8_t ackClass, uint8_t ackType, uint8_t ackStatus, uint32_t llId, uint32_t srcLlId) +void Data::writeRF_PDU_Ack_Response(uint8_t ackClass, uint8_t ackType, uint8_t ackStatus, uint32_t llId, bool extendedAddress, uint32_t srcLlId) { if (ackClass == PDUAckClass::ACK && ackType != PDUAckType::ACK) return; @@ -1698,7 +1754,11 @@ void Data::writeRF_PDU_Ack_Response(uint8_t ackClass, uint8_t ackType, uint8_t a if (srcLlId > 0U) { rspHeader.setSrcLLId(srcLlId); } - rspHeader.setFullMessage(true); + + if (extendedAddress) + rspHeader.setFullMessage(false); + else + rspHeader.setFullMessage(true); rspHeader.setBlocksToFollow(0U); diff --git a/src/host/p25/packet/Data.h b/src/host/p25/packet/Data.h index 7ff8fc58..21b94ef8 100644 --- a/src/host/p25/packet/Data.h +++ b/src/host/p25/packet/Data.h @@ -5,7 +5,7 @@ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * Copyright (C) 2016,2017 Jonathan Naylor, G4KLX - * Copyright (C) 2017-2024 Bryan Biedenkapp, N2PLL + * Copyright (C) 2017-2025 Bryan Biedenkapp, N2PLL * */ /** @@ -88,6 +88,13 @@ namespace p25 * @param imm Flag indicating the PDU should be written to the immediate queue. */ void writeRF_PDU_User(data::DataHeader& dataHeader, bool extendedAddress, uint8_t* pduUserData, bool imm = false); + /** + * @brief Helper to write user data as a P25 PDU packet. + * @param dataHeader Instance of a PDU data header. + * @param extendedAddress Flag indicating whether or not to extended addressing is in use. + * @param pduUserData Buffer containing user data to transmit. + */ + void writeNet_PDU_User(data::DataHeader& dataHeader, bool extendedAddress, uint8_t* pduUserData); /** * @brief Updates the processor by the passed number of milliseconds. @@ -147,9 +154,6 @@ namespace p25 std::unordered_map m_fneRegTable; - std::unordered_map m_convRegQueueTable; - std::unordered_map m_convRegTimerTable; - std::unordered_map m_sndcpStateTable; std::unordered_map m_sndcpReadyTimers; std::unordered_map m_sndcpStandbyTimers; @@ -221,6 +225,7 @@ namespace p25 * @param regType Registration Response. * @param llId Logical Link ID. * @param ipAddr + * @param mfId */ void writeRF_PDU_Reg_Response(uint8_t regType, uint32_t llId, uint32_t ipAddr); /** @@ -229,9 +234,11 @@ namespace p25 * @param ackType Acknowledgement Type. * @param ackStatus * @param llId Logical Link ID. + * @param extendedAddress Flag indicating whether or not to extended addressing is in use. * @param srcLlId Source Logical Link ID. */ - void writeRF_PDU_Ack_Response(uint8_t ackClass, uint8_t ackType, uint8_t ackStatus, uint32_t llId, uint32_t srcLlId = 0U); + void writeRF_PDU_Ack_Response(uint8_t ackClass, uint8_t ackType, uint8_t ackStatus, uint32_t llId, bool extendedAddress, + uint32_t srcLlId = 0U); }; } // namespace packet } // namespace p25 diff --git a/src/host/p25/packet/Voice.cpp b/src/host/p25/packet/Voice.cpp index 2382c18a..ef1e32db 100644 --- a/src/host/p25/packet/Voice.cpp +++ b/src/host/p25/packet/Voice.cpp @@ -183,6 +183,11 @@ bool Voice::process(uint8_t* data, uint32_t len) m_p25->m_rfLastDstId = lc.getDstId(); m_p25->m_rfLastSrcId = lc.getSrcId(); + // if we're DFSI/V.24 -- ignore the TGID presented in the HDU + if (m_p25->m_isModemDFSI) { + m_p25->m_rfLastDstId = 0U; + } + m_rfLastHDU = lc; m_rfLastHDUValid = true; @@ -230,61 +235,8 @@ bool Voice::process(uint8_t* data, uint32_t len) alreadyDecoded = true; - // don't process RF frames if this modem isn't authoritative - if (!m_p25->m_authoritative && m_p25->m_permittedDstId != lc.getDstId()) { - if (!g_disableNonAuthoritativeLogging) - LogWarning(LOG_RF, "[NON-AUTHORITATIVE] Ignoring RF traffic, destination not permitted!"); - resetRF(); - m_p25->m_rfState = RS_RF_LISTENING; + if (checkRFTrafficCollision(srcId, dstId)) return false; - } - - // don't process RF frames if the network isn't in a idle state and the RF destination is the network destination - if (m_p25->m_netState != RS_NET_IDLE && dstId == m_p25->m_netLastDstId) { - LogWarning(LOG_RF, "Traffic collision detect, preempting new RF traffic to existing network traffic!"); - resetRF(); - m_p25->m_rfState = RS_RF_LISTENING; - return false; - } - - // stop network frames from processing -- RF wants to transmit on a different talkgroup - if (m_p25->m_netState != RS_NET_IDLE) { - if (m_netLC.getSrcId() == srcId && m_p25->m_netLastDstId == dstId) { - LogWarning(LOG_RF, "Traffic collision detect, preempting new RF traffic to existing network traffic (Are we in a voting condition?), rfSrcId = %u, rfDstId = %u, netSrcId = %u, netDstId = %u", srcId, dstId, - m_netLC.getSrcId(), m_p25->m_netLastDstId); - resetRF(); - m_p25->m_rfState = RS_RF_LISTENING; - return false; - } - else { - LogWarning(LOG_RF, "Traffic collision detect, preempting existing network traffic to new RF traffic, rfDstId = %u, netDstId = %u", dstId, - m_p25->m_netLastDstId); - if (!m_p25->m_dedicatedControl) { - m_p25->m_affiliations->releaseGrant(m_p25->m_netLastDstId, false); - } - - resetNet(); - if (m_p25->m_network != nullptr) - m_p25->m_network->resetP25(); - - if (m_p25->m_duplex) { - m_p25->writeRF_TDU(true); - } - - m_p25->m_netTGHang.stop(); - } - - // is control is enabled, and the group was granted by network already ignore RF traffic - if (m_p25->m_enableControl && dstId == m_p25->m_netLastDstId) { - if (m_p25->m_affiliations->isNetGranted(dstId)) { - LogWarning(LOG_RF, "Traffic collision detect, preempting new RF traffic to existing granted network traffic (Are we in a voting condition?), rfSrcId = %u, rfDstId = %u, netSrcId = %u, netDstId = %u", srcId, dstId, - m_netLC.getSrcId(), m_p25->m_netLastDstId); - resetRF(); - m_p25->m_rfState = RS_RF_LISTENING; - return false; - } - } - } // if this is a late entry call, clear states if (m_rfLastHDU.getDstId() == 0U) { @@ -296,6 +248,11 @@ bool Voice::process(uint8_t* data, uint32_t len) resetRF(); } + if (m_p25->m_rfLastDstId == 0U && m_p25->m_isModemDFSI) { + // if we're DFSI/V.24 -- use the TGID presented in the LDU1 + m_p25->m_rfLastDstId = dstId; + } + if (m_p25->m_enableControl) { if (!m_p25->m_ccRunning && !m_p25->m_dedicatedControl) { m_p25->m_control->writeRF_ControlData(255U, 0U, false); @@ -345,7 +302,7 @@ bool Voice::process(uint8_t* data, uint32_t len) } else { // validate the target ID, if the target is a talkgroup - if (!acl::AccessControl::validateTGId(dstId)) { + if (!acl::AccessControl::validateTGId(dstId, m_p25->m_forceAllowTG0)) { if (m_lastRejectId == 0 || m_lastRejectId != dstId) { LogWarning(LOG_RF, P25_HDU_STR " denial, TGID rejection, dstId = %u", dstId); if (m_p25->m_enableControl) { @@ -364,6 +321,12 @@ bool Voice::process(uint8_t* data, uint32_t len) } } + if (group && dstId == 0U && m_p25->m_forceAllowTG0) { + LogWarning(LOG_RF, P25_HDU_STR " TGID 0 (P25 blackhole talkgroup) detected, srcId = %u", srcId); + dstId = 1U; // force destination ID to TGID 1 -- TGID 0 is not allowed in P25, and the network won't properly handle it + lc.setDstId(dstId); + } + // verify the source RID is affiliated to the group TGID; only if control data // is supported if (group && m_p25->m_enableControl) { @@ -394,11 +357,11 @@ bool Voice::process(uint8_t* data, uint32_t len) // send network grant demand TDU if (m_p25->m_network != nullptr) { if (!m_p25->m_dedicatedControl && m_p25->m_convNetGrantDemand) { - uint8_t controlByte = 0x80U; // Grant Demand Flag + uint8_t controlByte = network::NET_CTRL_GRANT_DEMAND; // Grant Demand Flag if (encrypted) - controlByte |= 0x08U; // Grant Encrypt Flag + controlByte |= network::NET_CTRL_GRANT_ENCRYPT; // Grant Encrypt Flag if (!group) - controlByte |= 0x01U; // Unit-to-unit Flag + controlByte |= network::NET_CTRL_U2U; // Unit-to-unit Flag LogMessage(LOG_RF, P25_HDU_STR " remote grant demand, srcId = %u, dstId = %u", srcId, dstId); m_p25->m_network->writeP25TDU(lc, m_rfLSD, controlByte); @@ -538,9 +501,7 @@ bool Voice::process(uint8_t* data, uint32_t len) // add status bits P25Utils::addStatusBits(buffer + 2U, P25_HDU_FRAME_LENGTH_BITS, m_inbound, false); - writeNetwork(buffer, DUID::HDU); - - if (m_p25->m_duplex && !m_p25->m_isModemDFSI) { + if (m_p25->m_duplex && (!m_p25->m_isModemDFSI || (m_p25->m_isModemDFSI && m_p25->m_dfsiFDX))) { buffer[0U] = modem::TAG_DATA; buffer[1U] = 0x00U; @@ -794,7 +755,7 @@ bool Voice::process(uint8_t* data, uint32_t len) writeNetwork(data + 2U, DUID::LDU1, frameType); - if (m_p25->m_duplex && !m_p25->m_isModemDFSI) { + if (m_p25->m_duplex && (!m_p25->m_isModemDFSI || (m_p25->m_isModemDFSI && m_p25->m_dfsiFDX))) { data[0U] = modem::TAG_DATA; data[1U] = 0x00U; @@ -845,8 +806,8 @@ bool Voice::process(uint8_t* data, uint32_t len) getNextMI(lastMI, nextMI); if (m_verbose && m_debug) { - Utils::dump(1U, "Previous P25 MI", lastMI, MI_LENGTH_BYTES); - Utils::dump(1U, "Calculated next P25 MI", nextMI, MI_LENGTH_BYTES); + Utils::dump(1U, "P25, LDU2, Previous P25 MI", lastMI, MI_LENGTH_BYTES); + Utils::dump(1U, "P25, LDU2, Calculated next P25 MI", nextMI, MI_LENGTH_BYTES); } m_rfLC.setMI(nextMI); @@ -911,7 +872,7 @@ bool Voice::process(uint8_t* data, uint32_t len) writeNetwork(data + 2U, DUID::LDU2); - if (m_p25->m_duplex && !m_p25->m_isModemDFSI) { + if (m_p25->m_duplex && (!m_p25->m_isModemDFSI || (m_p25->m_isModemDFSI && m_p25->m_dfsiFDX))) { data[0U] = modem::TAG_DATA; data[1U] = 0x00U; @@ -1008,8 +969,6 @@ bool Voice::process(uint8_t* data, uint32_t len) // add status bits P25Utils::addStatusBits(buffer + 2U, P25_HDU_FRAME_LENGTH_BITS, m_inbound, false); - writeNetwork(buffer, DUID::HDU); - if (m_p25->m_duplex) { buffer[0U] = modem::TAG_DATA; buffer[1U] = 0x00U; @@ -1184,69 +1143,8 @@ bool Voice::processNetwork(uint8_t* data, uint32_t len, lc::LC& control, data::L uint32_t dstId = control.getDstId(); uint32_t srcId = control.getSrcId(); - // don't process network frames if the destination ID's don't match and the RF TG hang timer is running - if (m_p25->m_rfLastDstId != 0U && dstId != 0U) { - if (m_p25->m_rfLastDstId != dstId && (m_p25->m_rfTGHang.isRunning() && !m_p25->m_rfTGHang.hasExpired())) { - resetNet(); - if (m_p25->m_network != nullptr) - m_p25->m_network->resetP25(); - return false; - } - - if (m_p25->m_rfLastDstId == dstId && (m_p25->m_rfTGHang.isRunning() && !m_p25->m_rfTGHang.hasExpired())) { - m_p25->m_rfTGHang.start(); - } - } - - // bryanb: possible fix for a "tail ride" condition where network traffic immediately follows RF traffic *while* - // the RF TG hangtimer is running - if (m_p25->m_rfTGHang.isRunning() && !m_p25->m_rfTGHang.hasExpired()) { - m_p25->m_rfTGHang.stop(); - } - - // perform authoritative network TG hangtimer and traffic preemption - if (m_p25->m_authoritative) { - // don't process network frames if the destination ID's don't match and the network TG hang timer is running - if (m_p25->m_netLastDstId != 0U && dstId != 0U && (duid == DUID::LDU1 || duid == DUID::LDU2)) { - if (m_p25->m_netLastDstId != dstId && (m_p25->m_netTGHang.isRunning() && !m_p25->m_netTGHang.hasExpired())) { - return false; - } - - if (m_p25->m_netLastDstId == dstId && (m_p25->m_netTGHang.isRunning() && !m_p25->m_netTGHang.hasExpired())) { - m_p25->m_netTGHang.start(); - } - } - - // don't process network frames if the RF modem isn't in a listening state - if (m_p25->m_rfState != RS_RF_LISTENING) { - if (m_rfLC.getSrcId() == srcId && m_rfLC.getDstId() == dstId) { - LogWarning(LOG_NET, "Traffic collision detect, preempting new network traffic to existing RF traffic (Are we in a voting condition?), rfSrcId = %u, rfDstId = %u, netSrcId = %u, netDstId = %u", m_rfLC.getSrcId(), m_rfLC.getDstId(), - srcId, dstId); - resetNet(); - if (m_p25->m_network != nullptr) - m_p25->m_network->resetP25(); - return false; - } - else { - LogWarning(LOG_NET, "Traffic collision detect, preempting new network traffic to existing RF traffic, rfDstId = %u, netDstId = %u", m_rfLC.getDstId(), - dstId); - resetNet(); - if (m_p25->m_network != nullptr) - m_p25->m_network->resetP25(); - return false; - } - } - } - - // don't process network frames if this modem isn't authoritative - if (!m_p25->m_authoritative && m_p25->m_permittedDstId != dstId) { - if (!g_disableNonAuthoritativeLogging) - LogWarning(LOG_NET, "[NON-AUTHORITATIVE] Ignoring network traffic, destination not permitted, dstId = %u", dstId); - resetNet(); - if (m_p25->m_network != nullptr) - m_p25->m_network->resetP25(); + if (checkNetTrafficCollision(srcId, dstId, duid)) return false; - } uint32_t count = 0U; switch (duid) { @@ -1560,15 +1458,26 @@ void Voice::writeNetwork(const uint8_t *data, defines::DUID::E duid, defines::Fr if (m_p25->m_rfTimeout.isRunning() && m_p25->m_rfTimeout.hasExpired()) return; + uint8_t controlByte = 0U; switch (duid) { case DUID::HDU: // ignore HDU break; case DUID::LDU1: - m_p25->m_network->writeP25LDU1(m_rfLC, m_rfLSD, data, frameType); + if (!m_rfLC.getGroup()) + controlByte = network::NET_CTRL_U2U; + m_p25->m_network->writeP25LDU1(m_rfLC, m_rfLSD, data, frameType, controlByte); + break; + case DUID::VSELP1: + // ignore VSELP1 break; case DUID::LDU2: - m_p25->m_network->writeP25LDU2(m_rfLC, m_rfLSD, data); + if (!m_rfLC.getGroup()) + controlByte = network::NET_CTRL_U2U; + m_p25->m_network->writeP25LDU2(m_rfLC, m_rfLSD, data, controlByte); + break; + case DUID::VSELP2: + // ignore VSELP2 break; case DUID::TDU: case DUID::TDULC: @@ -1580,6 +1489,151 @@ void Voice::writeNetwork(const uint8_t *data, defines::DUID::E duid, defines::Fr } } +/* Helper to perform RF traffic collision checking. */ + +bool Voice::checkRFTrafficCollision(uint32_t srcId, uint32_t dstId) +{ + // don't process RF frames if this modem isn't authoritative + if (!m_p25->m_authoritative && m_p25->m_permittedDstId != dstId) { + if (!g_disableNonAuthoritativeLogging) + LogWarning(LOG_RF, "[NON-AUTHORITATIVE] Ignoring RF traffic, destination not permitted!"); + resetRF(); + m_p25->m_rfState = RS_RF_LISTENING; + return true; + } + + // don't process RF frames if the network isn't in a idle state and the RF destination is the network destination + if (m_p25->m_netState != RS_NET_IDLE && dstId == m_p25->m_netLastDstId) { + LogWarning(LOG_RF, "Traffic collision detect, preempting new RF traffic to existing network traffic!"); + resetRF(); + m_p25->m_rfState = RS_RF_LISTENING; + return true; + } + + // stop network frames from processing -- RF wants to transmit on a different talkgroup + if (m_p25->m_netState != RS_NET_IDLE) { + if (m_netLC.getSrcId() == srcId && m_p25->m_netLastDstId == dstId) { + LogWarning(LOG_RF, "Traffic collision detect, preempting new RF traffic to existing network traffic (Are we in a voting condition?), rfSrcId = %u, rfDstId = %u, netSrcId = %u, netDstId = %u", srcId, dstId, + m_netLC.getSrcId(), m_p25->m_netLastDstId); + resetRF(); + m_p25->m_rfState = RS_RF_LISTENING; + return true; + } + else { + LogWarning(LOG_RF, "Traffic collision detect, preempting existing network traffic to new RF traffic, rfDstId = %u, netDstId = %u", dstId, + m_p25->m_netLastDstId); + if (!m_p25->m_dedicatedControl) { + m_p25->m_affiliations->releaseGrant(m_p25->m_netLastDstId, false); + } + + resetNet(); + if (m_p25->m_network != nullptr) + m_p25->m_network->resetP25(); + + if (m_p25->m_duplex) { + m_p25->writeRF_TDU(true); + } + + m_p25->m_netTGHang.stop(); + } + + // is control is enabled, and the group was granted by network already ignore RF traffic + if (m_p25->m_enableControl && dstId == m_p25->m_netLastDstId) { + if (m_p25->m_affiliations->isNetGranted(dstId)) { + LogWarning(LOG_RF, "Traffic collision detect, preempting new RF traffic to existing granted network traffic (Are we in a voting condition?), rfSrcId = %u, rfDstId = %u, netSrcId = %u, netDstId = %u", srcId, dstId, + m_netLC.getSrcId(), m_p25->m_netLastDstId); + resetRF(); + m_p25->m_rfState = RS_RF_LISTENING; + return true; + } + } + } + + return false; +} + +/* Helper to perform network traffic collision checking. */ + +bool Voice::checkNetTrafficCollision(uint32_t srcId, uint32_t dstId, defines::DUID::E duid) +{ + // don't process network frames if the destination ID's don't match and the RF TG hang timer is running + if (m_p25->m_rfLastDstId != 0U && dstId != 0U) { + if (m_p25->m_rfLastDstId != dstId && (m_p25->m_rfTGHang.isRunning() && !m_p25->m_rfTGHang.hasExpired())) { + resetNet(); + if (m_p25->m_network != nullptr) + m_p25->m_network->resetP25(); + return true; + } + + if (m_p25->m_rfLastDstId == dstId && (m_p25->m_rfTGHang.isRunning() && !m_p25->m_rfTGHang.hasExpired())) { + m_p25->m_rfTGHang.start(); + } + } + + // bryanb: possible fix for a "tail ride" condition where network traffic immediately follows RF traffic *while* + // the RF TG hangtimer is running + if (m_p25->m_rfTGHang.isRunning() && !m_p25->m_rfTGHang.hasExpired()) { + m_p25->m_rfTGHang.stop(); + } + + // don't process network frames if the RF TG hang timer isn't running, the default net idle talkgroup is set and + // the destination ID doesn't match the default net idle talkgroup + if (m_p25->m_defaultNetIdleTalkgroup != 0U && dstId != 0U && !m_p25->m_rfTGHang.isRunning()) { + if (m_p25->m_defaultNetIdleTalkgroup != dstId) { + resetNet(); + if (m_p25->m_network != nullptr) + m_p25->m_network->resetP25(); + return true; + } + } + + // perform authoritative network TG hangtimer and traffic preemption + if (m_p25->m_authoritative) { + // don't process network frames if the destination ID's don't match and the network TG hang timer is running + if (m_p25->m_netLastDstId != 0U && dstId != 0U && (duid == DUID::LDU1 || duid == DUID::LDU2)) { + if (m_p25->m_netLastDstId != dstId && (m_p25->m_netTGHang.isRunning() && !m_p25->m_netTGHang.hasExpired())) { + return true; + } + + if (m_p25->m_netLastDstId == dstId && (m_p25->m_netTGHang.isRunning() && !m_p25->m_netTGHang.hasExpired())) { + m_p25->m_netTGHang.start(); + } + } + + // don't process network frames if the RF modem isn't in a listening state + if (m_p25->m_rfState != RS_RF_LISTENING) { + if (m_rfLC.getSrcId() == srcId && m_rfLC.getDstId() == dstId) { + LogWarning(LOG_NET, "Traffic collision detect, preempting new network traffic to existing RF traffic (Are we in a voting condition?), rfSrcId = %u, rfDstId = %u, netSrcId = %u, netDstId = %u", m_rfLC.getSrcId(), m_rfLC.getDstId(), + srcId, dstId); + resetNet(); + if (m_p25->m_network != nullptr) + m_p25->m_network->resetP25(); + return true; + } + else { + LogWarning(LOG_NET, "Traffic collision detect, preempting new network traffic to existing RF traffic, rfDstId = %u, netDstId = %u", m_rfLC.getDstId(), + dstId); + resetNet(); + if (m_p25->m_network != nullptr) + m_p25->m_network->resetP25(); + return true; + } + } + } + + // don't process network frames if this modem isn't authoritative + if (!m_p25->m_authoritative && m_p25->m_permittedDstId != dstId) { + if (!g_disableNonAuthoritativeLogging) + LogWarning(LOG_NET, "[NON-AUTHORITATIVE] Ignoring network traffic, destination not permitted, dstId = %u", dstId); + resetNet(); + if (m_p25->m_network != nullptr) + m_p25->m_network->resetP25(); + return true; + } + + return false; +} + /* Helper to write end of frame data. */ void Voice::writeRF_EndOfVoice() @@ -1930,10 +1984,14 @@ void Voice::writeNet_LDU1() } uint32_t netId = control.getNetId(); + if (netId == 0U) + netId = lc::LC::getSiteData().netId(); uint32_t sysId = control.getSysId(); + if (sysId == 0U) + sysId = lc::LC::getSiteData().sysId(); // is the network peer a different WACN or system ID? - if (m_p25->m_enableControl && m_p25->m_allowExplicitSourceId) { + if (m_p25->m_allowExplicitSourceId) { if (sysId != lc::LC::getSiteData().sysId()) { // per TIA-102.AABD-D transmit EXPLICIT_SOURCE_ID every other frame (e.g. every other LDU1) m_roamLDU1Count++; @@ -1945,16 +2003,14 @@ void Voice::writeNet_LDU1() } else { // flag explicit block to follow in next LDU1 - if (m_netLC.getLCO() == LCO::GROUP) { - m_netLC.setExplicitId(true); + m_netLC.setExplicitId(true); + + if (m_netLC.getLCO() == LCO::PRIVATE) { + m_netLC.setLCO(LCO::PRIVATE_EXT); } } } } - else { - netId = lc::LC::getSiteData().netId(); - sysId = lc::LC::getSiteData().sysId(); - } // are we swapping the LC out for the RFSS_STS_BCAST or LC_GROUP_UPDT? m_pktLDU1Count++; diff --git a/src/host/p25/packet/Voice.h b/src/host/p25/packet/Voice.h index 89de8eb3..2f03c898 100644 --- a/src/host/p25/packet/Voice.h +++ b/src/host/p25/packet/Voice.h @@ -153,6 +153,22 @@ namespace p25 * @param frameType Frame Type. */ void writeNetwork(const uint8_t* data, defines::DUID::E duid, defines::FrameType::E frameType = defines::FrameType::DATA_UNIT); + + /** + * @brief Helper to perform RF traffic collision checking. + * @param srcId Source ID. + * @param dstId Destination ID. + * @return bool True, if traffic collision, otherwise false. + */ + bool checkRFTrafficCollision(uint32_t srcId, uint32_t dstId); + /** + * @brief Helper to perform network traffic collision checking. + * @param srcId Source ID. + * @param dstId Destination ID. + * @param duid DUID. + * @return bool True, if traffic collision, otherwise false. + */ + bool checkNetTrafficCollision(uint32_t srcId, uint32_t dstId, defines::DUID::E duid); /** * @brief Helper to write end of voice frame data. diff --git a/src/host/setup/HostSetup.cpp b/src/host/setup/HostSetup.cpp index 60b77799..d9742e49 100644 --- a/src/host/setup/HostSetup.cpp +++ b/src/host/setup/HostSetup.cpp @@ -513,7 +513,7 @@ bool HostSetup::portModemHandler(Modem* modem, uint32_t ms, RESP_TYPE_DVM rspTyp { uint8_t len = buffer[1U]; if (m_debug) { - Utils::dump(1U, "Modem Flash Contents", buffer + 3U, len - 3U); + Utils::dump(1U, "HostSetup::portModemHandler(), Modem Flash Contents", buffer + 3U, len - 3U); } if (len == 249U) { bool ret = edac::CRC::checkCCITT162(buffer + 3U, DVM_CONF_AREA_LEN); @@ -569,7 +569,7 @@ bool HostSetup::portModemHandler(Modem* modem, uint32_t ms, RESP_TYPE_DVM rspTyp default: LogWarning(LOG_CAL, "Unknown message, type = %02X", buffer[2U]); - Utils::dump("Buffer dump", buffer, len); + Utils::dump("HostSetup::portModemHandler(), buffer", buffer, len); break; } } @@ -1292,20 +1292,19 @@ void HostSetup::processP25BER(const uint8_t* buffer) uint32_t bits = P25Utils::decode(buffer, pduBuffer, 0, P25_PDU_FRAME_LENGTH_BITS); - Utils::dump(1U, "Raw PDU Dump", buffer, P25_PDU_FRAME_LENGTH_BYTES); + Utils::dump(1U, "P25, Raw PDU Dump", buffer, P25_PDU_FRAME_LENGTH_BYTES); uint8_t* rfPDU = new uint8_t[P25_MAX_PDU_BLOCKS * P25_PDU_CONFIRMED_LENGTH_BYTES + 2U]; ::memset(rfPDU, 0x00U, P25_MAX_PDU_BLOCKS * P25_PDU_CONFIRMED_LENGTH_BYTES + 2U); - uint32_t rfPDUBits = 0U; - rfPDUBits = Utils::getBits(pduBuffer, rfPDU, 0U, bits); + Utils::getBits(pduBuffer, rfPDU, 0U, bits); ::memset(pduBuffer, 0x00U, P25_PDU_FEC_LENGTH_BYTES); Utils::getBitRange(rfPDU, pduBuffer, P25_PREAMBLE_LENGTH_BITS, P25_PDU_FEC_LENGTH_BITS); bool ret = dataHeader.decode(pduBuffer); if (!ret) { LogWarning(LOG_CAL, P25_PDU_STR ", unfixable RF 1/2 rate header data"); - Utils::dump(1U, "Unfixable PDU Data", pduBuffer, P25_PDU_FEC_LENGTH_BYTES); + Utils::dump(1U, "P25, Unfixable PDU Data", pduBuffer, P25_PDU_FEC_LENGTH_BYTES); } else { LogMessage(LOG_CAL, P25_PDU_STR ", ack = %u, outbound = %u, fmt = $%02X, mfId = $%02X, sap = $%02X, fullMessage = %u, blocksToFollow = %u, padLength = %u, n = %u, seqNo = %u, lastFragment = %u, hdrOffset = %u", @@ -1321,7 +1320,7 @@ void HostSetup::processP25BER(const uint8_t* buffer) std::unique_ptr tsbk = lc::tsbk::TSBKFactory::createTSBK(buffer); - Utils::dump(1U, "Raw TSBK Dump", buffer, P25_TSDU_FRAME_LENGTH_BYTES); + Utils::dump(1U, "P25, Raw TSBK Dump", buffer, P25_TSDU_FRAME_LENGTH_BYTES); if (tsbk == nullptr) { LogWarning(LOG_CAL, P25_TSDU_STR ", undecodable LC"); diff --git a/src/host/setup/SiteParamSetWnd.h b/src/host/setup/SiteParamSetWnd.h index 251a3f02..b6c1bc49 100644 --- a/src/host/setup/SiteParamSetWnd.h +++ b/src/host/setup/SiteParamSetWnd.h @@ -217,7 +217,7 @@ private: m_p25RfssIdLabel.setGeometry(FPoint(2, 14), FSize(20, 1)); m_p25RfssId.setGeometry(FPoint(23, 14), FSize(10, 1)); - m_p25RfssId.setText(rfssConfig["dmrNetId"].as("1").c_str()); + m_p25RfssId.setText(rfssConfig["rfssId"].as("1").c_str()); m_p25RfssId.setShadow(false); m_p25RfssId.setMaxLength(3); m_p25RfssId.setInputFilter("[[:xdigit:]]"); diff --git a/src/host/win32/host.ico b/src/host/win32/host.ico new file mode 100644 index 0000000000000000000000000000000000000000..874b99977cfb9aee483517604a488369db866332 GIT binary patch literal 20989 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 literal 0 HcmV?d00001 diff --git a/src/host/win32/resource.h b/src/host/win32/resource.h new file mode 100644 index 00000000..056c0418 --- /dev/null +++ b/src/host/win32/resource.h @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Digital Voice Modem - Modem Host 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 + * + */ + +#define IDS_APP_TITLE 102 +#define IDI_APP_ICON 103 diff --git a/src/host/win32/resource.rc b/src/host/win32/resource.rc new file mode 100644 index 0000000000000000000000000000000000000000..d1eb8f1b813539f0e4bdad32c9e165b6bfd72f43 GIT binary patch literal 5140 zcmdUzT~Av_5QgWvQvbu&xgk*jL#V3u4uSzI# z8e1jihUn;c_w3Hh&iBsv-+xwZ)q)*b)3)uCEnCg5?995>w;{jFR<(|QjaWbD?b@yV z3dSxNHTKtS#mbBWFs|*vzPIn7_|C_`7@&J)7rY}o0U}r%*my>;edeOyV||%-@_UcH zAz0#Wz}X|ahr%t;IwR7KTzr9xU=6!*P9yvskdkACogwO(H|i51AI!umoCi`t8byZj#bv}CuUphKcKn3 z&t<`A(LY(c;rWsEI^={|HySJ3%0PDC`jMZ>LIfuX3|^Wjnt#oTkJL9J!+}oG4d`+-k_mA_P)mUl0H}o zt#q;OfR*Gam#of%zIO@{X9ZnHNI~mW6ur(TaVHyhk#&T$F%Fl|VVAf%VfC7IdCjy= zhAv&w$vF@|xc-z0`e1i#35)HJIgX!L=@|O*DAE0!C(OlcLF0f3IfloI+x@MdTVuV> zy1Zi7?VeT=WD7h+p?vrdxGm-pN2gcdY?D<*X|hz($LqFM;xZ5cXZUD;>}5#~@n7r) za-%ZrC2QBf-@9)scUI9uurF@&J5~b`f6bS2V}~zU@|HLdY|Ml)_bVy{9dks?jwe7| zIXB9sIbYFtdceywlxO4qkZ(~JRh=POMD?0Sx%lC6x5%{OxQZ`ql3$vP%9RcLAk2CG zZ+{xEgqGsXvhpZJ9?QxV=a0Us9N)~ck@%^Tts3^xvrw|sx8hH_uRakElHvoAp)6Z@ z;$3U#dNyQm*e!5&ordI38lIg~KXVgB4OcjGdmM?nvllsI9=-Pq)4R3LcxB|VkY(OE zZ`_wV?j;F2M&omQWio!h6`yG%v2sL={p9gUk~Z;i>=n-eGu_yAkJ>ufSGTunp<+jV zr}gMJsoKe5f^Cq|%i}k~HlfnMe~$3m))dv*7>W9h-wL~Mw@*AD61f5Df7yHDJXSn; z%*VT9i<5Sz_$(+z3TtEhF8NFqb1v63e0c)CPmWQh$uS!67in+6qn?dNoPge9ZP=ow zY@*eihhjh8ggI0?d3NOL-fW;(X{H1RsuGmPFF5lG$-11}hu6VWl+31NngfeeRPwV-Jo?UKNz^m^OBABGF)6_0K+g zHua`9)g^vZ6{QYm5pMFBOk8YG1DD6|*U*)HW6#s~+j&}x z9cqZ)#Ld|yex&2*y-9QVm|Lj^{Dsq&z4E>#@&9xuP+^{pJDN57*?Z2#dK=XpiaPbA zPkllssfQZD+mIet**W&DmsH57XjK(WkJYG)o6MtR&rPqdRb41sevNJ#?o~Q1YCrj$ zz9xP7hGfa2 zK92sZJ0|8l^(SlIlb5rIDYz|WiYUFWvpNu0iMuqa_bP4-RdpxCi*Ahsp;52;di@)s uu1;N_Y!j=`**A%L0aX{N7f{{Wa-3xEd{nl7X&%lkF~8bdyq|vEmi-U2S$A>( literal 0 HcmV?d00001 diff --git a/src/patch/CMakeLists.txt b/src/patch/CMakeLists.txt index d04d5004..de1c681b 100644 --- a/src/patch/CMakeLists.txt +++ b/src/patch/CMakeLists.txt @@ -8,8 +8,18 @@ # * # */ file(GLOB patch_SRC + "src/patch/mmdvm/*.h" + "src/patch/mmdvm/*.cpp" "src/patch/network/*.h" "src/patch/network/*.cpp" + "src/patch/win32/*.h" "src/patch/*.h" "src/patch/*.cpp" ) + +# +# Windows Resource Scripts +# +file(GLOB patch_RC + "src/patch/win32/*.rc" +) diff --git a/src/patch/HostPatch.cpp b/src/patch/HostPatch.cpp index cd040b0d..f9d12bfc 100644 --- a/src/patch/HostPatch.cpp +++ b/src/patch/HostPatch.cpp @@ -18,6 +18,7 @@ #include "common/p25/dfsi/DFSIDefines.h" #include "common/p25/dfsi/LC.h" #include "common/p25/lc/LC.h" +#include "common/p25/Sync.h" #include "common/p25/P25Utils.h" #include "common/network/RTPHeader.h" #include "common/network/udp/Socket.h" @@ -44,6 +45,13 @@ using namespace network::udp; #include #endif // !defined(_WIN32) +// --------------------------------------------------------------------------- +// Constants +// --------------------------------------------------------------------------- + +#define TEK_AES "aes" +#define TEK_ARC4 "arc4" + // --------------------------------------------------------------------------- // Static Class Members // --------------------------------------------------------------------------- @@ -64,23 +72,58 @@ HostPatch::HostPatch(const std::string& confFile) : m_srcSlot(1U), m_dstTGId(0U), m_dstSlot(1U), + m_twoWayPatch(false), + m_mmdvmP25Reflector(false), + m_mmdvmP25Net(nullptr), + m_netState(RS_NET_IDLE), + m_netLC(), + m_gotNetLDU1(false), + m_netLDU1(nullptr), + m_gotNetLDU2(false), + m_netLDU2(nullptr), m_identity(), m_digiMode(1U), m_dmrEmbeddedData(), m_grantDemand(false), m_callInProgress(false), + m_callAlgoId(P25DEF::ALGO_UNENCRYPT), m_rxStartTime(0U), m_rxStreamId(0U), + m_tekSrcAlgoId(P25DEF::ALGO_UNENCRYPT), + m_tekSrcKeyId(0U), + m_tekDstAlgoId(P25DEF::ALGO_UNENCRYPT), + m_tekDstKeyId(0U), + m_requestedSrcTek(false), + m_requestedDstTek(false), + m_p25SrcCrypto(nullptr), + m_p25DstCrypto(nullptr), + m_netId(P25DEF::WACN_STD_DEFAULT), + m_sysId(P25DEF::SID_STD_DEFAULT), m_running(false), m_trace(false), m_debug(false) { - /* stub */ + m_netLDU1 = new uint8_t[9U * 25U]; + m_netLDU2 = new uint8_t[9U * 25U]; + + ::memset(m_netLDU1, 0x00U, 9U * 25U); + resetWithNullAudio(m_netLDU1, false); + ::memset(m_netLDU2, 0x00U, 9U * 25U); + resetWithNullAudio(m_netLDU2, false); + + m_p25SrcCrypto = new p25::crypto::P25Crypto(); + m_p25DstCrypto = new p25::crypto::P25Crypto(); } /* Finalizes a instance of the HostPatch class. */ -HostPatch::~HostPatch() = default; +HostPatch::~HostPatch() +{ + delete[] m_netLDU1; + delete[] m_netLDU2; + delete m_p25SrcCrypto; + delete m_p25DstCrypto; +} /* Executes the main FNE processing loop. */ @@ -166,6 +209,13 @@ int HostPatch::run() if (!ret) return EXIT_FAILURE; + // initialize MMDVM P25 reflector networking + if (m_mmdvmP25Reflector) { + ret = createMMDVMP25Network(); + if (!ret) + return EXIT_FAILURE; + } + /* ** Initialize Threads */ @@ -173,6 +223,11 @@ int HostPatch::run() if (!Thread::runAsThread(this, threadNetworkProcess)) return EXIT_FAILURE; + if (m_mmdvmP25Reflector) { + if (!Thread::runAsThread(this, threadMMDVMProcess)) + return EXIT_FAILURE; + } + ::LogInfoEx(LOG_HOST, "Patch is up and running"); m_running = true; @@ -196,6 +251,11 @@ int HostPatch::run() m_network->clock(ms); } + if (m_mmdvmP25Reflector) { + std::lock_guard lock(HostPatch::m_networkMutex); + m_mmdvmP25Net->clock(ms); + } + if (ms < 2U) Thread::sleep(1U); } @@ -220,6 +280,24 @@ bool HostPatch::readParams() yaml::Node systemConf = m_conf["system"]; m_identity = systemConf["identity"].as(); + + m_netId = (uint32_t)::strtoul(systemConf["netId"].as("BB800").c_str(), NULL, 16); + m_netId = p25::P25Utils::netId(m_netId); + if (m_netId == 0xBEE00) { + ::fatal("error 4\n"); + } + + m_sysId = (uint32_t)::strtoul(systemConf["sysId"].as("001").c_str(), NULL, 16); + m_sysId = p25::P25Utils::sysId(m_sysId); + + /* + ** Site Data + */ + int8_t lto = (int8_t)systemConf["localTimeOffset"].as(0); + p25::SiteData siteData = p25::SiteData(m_netId, m_sysId, 1U, 1U, 0U, 0U, 1U, P25DEF::ServiceClass::VOICE, lto); + siteData.setNetActive(true); + + p25::lc::LC::setSiteData(siteData); m_digiMode = (uint8_t)systemConf["digiMode"].as(1U); if (m_digiMode < TX_MODE_DMR) @@ -229,12 +307,22 @@ bool HostPatch::readParams() m_grantDemand = systemConf["grantDemand"].as(false); + m_mmdvmP25Reflector = systemConf["mmdvmP25Reflector"].as(false); + + if (m_mmdvmP25Reflector && m_digiMode != TX_MODE_P25) { + LogError(LOG_HOST, "Patch does not currently support MMDVM patching in any mode other then P25."); + return false; + } + m_trace = systemConf["trace"].as(false); m_debug = systemConf["debug"].as(false); LogInfo("General Parameters"); + LogInfo(" System Id: $%03X", m_sysId); + LogInfo(" P25 Network Id: $%05X", m_netId); LogInfo(" Digital Mode: %s", m_digiMode == TX_MODE_DMR ? "DMR" : "P25"); LogInfo(" Grant Demands: %s", m_grantDemand ? "yes" : "no"); + LogInfo(" MMDVM P25 Reflector Patch: %s", m_mmdvmP25Reflector ? "yes" : "no"); if (m_debug) { LogInfo(" Debug: yes"); @@ -261,6 +349,53 @@ bool HostPatch::createNetwork() m_srcSlot = (uint8_t)networkConf["sourceSlot"].as(1U); m_dstTGId = (uint32_t)networkConf["destinationTGID"].as(1U); m_dstSlot = (uint8_t)networkConf["destinationSlot"].as(1U); + + // source TEK parameters + yaml::Node srcTekConf = networkConf["srcTek"]; + bool tekSrcEnable = srcTekConf["enable"].as(false); + std::string tekSrcAlgo = srcTekConf["tekAlgo"].as(); + std::transform(tekSrcAlgo.begin(), tekSrcAlgo.end(), tekSrcAlgo.begin(), ::tolower); + m_tekSrcKeyId = (uint32_t)::strtoul(srcTekConf["tekKeyId"].as("0").c_str(), NULL, 16); + if (tekSrcEnable && m_tekSrcKeyId > 0U) { + if (tekSrcAlgo == TEK_AES) + m_tekSrcAlgoId = P25DEF::ALGO_AES_256; + else if (tekSrcAlgo == TEK_ARC4) + m_tekSrcAlgoId = P25DEF::ALGO_ARC4; + else { + ::LogError(LOG_HOST, "Invalid TEK algorithm specified, must be \"aes\" or \"adp\"."); + m_tekSrcAlgoId = P25DEF::ALGO_UNENCRYPT; + m_tekSrcKeyId = 0U; + } + } + + if (!tekSrcEnable) + m_tekSrcAlgoId = P25DEF::ALGO_UNENCRYPT; + if (m_tekSrcAlgoId == P25DEF::ALGO_UNENCRYPT) + m_tekSrcKeyId = 0U; + + // destination TEK parameters + yaml::Node dstTekConf = networkConf["srcTek"]; + bool tekDstEnable = dstTekConf["enable"].as(false); + std::string tekDstAlgo = dstTekConf["tekAlgo"].as(); + std::transform(tekDstAlgo.begin(), tekDstAlgo.end(), tekDstAlgo.begin(), ::tolower); + m_tekDstKeyId = (uint32_t)::strtoul(dstTekConf["tekKeyId"].as("0").c_str(), NULL, 16); + if (tekDstEnable && m_tekDstKeyId > 0U) { + if (tekDstAlgo == TEK_AES) + m_tekDstAlgoId = P25DEF::ALGO_AES_256; + else if (tekDstAlgo == TEK_ARC4) + m_tekDstAlgoId = P25DEF::ALGO_ARC4; + else { + ::LogError(LOG_HOST, "Invalid TEK algorithm specified, must be \"aes\" or \"adp\"."); + m_tekDstAlgoId = P25DEF::ALGO_UNENCRYPT; + m_tekDstKeyId = 0U; + } + } + + if (!tekDstEnable) + m_tekDstAlgoId = P25DEF::ALGO_UNENCRYPT; + if (m_tekDstAlgoId == P25DEF::ALGO_UNENCRYPT) + m_tekDstKeyId = 0U; + m_twoWayPatch = networkConf["twoWay"].as(false); // make sure our destination ID is sane @@ -359,8 +494,22 @@ bool HostPatch::createNetwork() LogInfo(" Source TGID: %u", m_srcTGId); LogInfo(" Source DMR Slot: %u", m_srcSlot); + + LogInfo(" Source Traffic Encrypted: %s", tekSrcEnable ? "yes" : "no"); + if (tekSrcEnable) { + LogInfo(" Source TEK Algorithm: %s", tekSrcAlgo.c_str()); + LogInfo(" Source TEK Key ID: $%04X", m_tekSrcKeyId); + } + LogInfo(" Destination TGID: %u", m_dstTGId); LogInfo(" Destination DMR Slot: %u", m_dstSlot); + + LogInfo(" Destination Traffic Encrypted: %s", tekDstEnable ? "yes" : "no"); + if (tekDstEnable) { + LogInfo(" Destination TEK Algorithm: %s", tekDstAlgo.c_str()); + LogInfo(" Destination TEK Key ID: $%04X", m_tekDstKeyId); + } + LogInfo(" Two-Way Patch: %s", m_twoWayPatch ? "yes" : "no"); if (debug) { @@ -382,6 +531,9 @@ bool HostPatch::createNetwork() m_network->setMetadata(m_identity, 0U, 0U, 0.0F, 0.0F, 0, 0, 0, 0.0F, 0.0F, 0, ""); m_network->setConventional(true); + m_network->setKeyResponseCallback([=](p25::kmm::KeyItem ki, uint8_t algId, uint8_t keyLength) { + processTEKResponse(&ki, algId, keyLength); + }); if (encrypted) { m_network->setPresharedKey(presharedKey); @@ -401,6 +553,40 @@ bool HostPatch::createNetwork() return true; } +/* Initializes MMDVM network connectivity. */ + +bool HostPatch::createMMDVMP25Network() +{ + yaml::Node networkConf = m_conf["network"]; + + std::string address = networkConf["mmdvmGatewayAddress"].as(); + uint16_t port = (uint16_t)networkConf["mmdvmGatewayPort"].as(42020U); + uint16_t localPort = (uint16_t)networkConf["localGatewayPort"].as(32010U); + bool debug = networkConf["debug"].as(false); + + LogInfo("MMDVM Network Parameters"); + LogInfo(" Address: %s", address.c_str()); + LogInfo(" Port: %u", port); + LogInfo(" Local Port: %u", localPort); + + if (debug) { + LogInfo(" Debug: yes"); + } + + // initialize networking + m_mmdvmP25Net = new mmdvm::P25Network(address, port, localPort, debug); + + bool ret = m_mmdvmP25Net->open(); + if (!ret) { + delete m_mmdvmP25Net; + m_mmdvmP25Net = nullptr; + LogError(LOG_HOST, "failed to initialize MMDVM networking!"); + return false; + } + + return true; +} + /* Helper to process DMR network traffic. */ void HostPatch::processDMRNetwork(uint8_t* buffer, uint32_t length) @@ -456,7 +642,7 @@ void HostPatch::processDMRNetwork(uint8_t* buffer, uint32_t length) UInt8Array data = std::unique_ptr(new uint8_t[DMR_FRAME_LENGTH_BYTES]); ::memset(data.get(), 0x00U, DMR_FRAME_LENGTH_BYTES); DataType::E dataType = DataType::VOICE_SYNC; - uint8_t n = 0U; + if (dataSync) { dataType = (DataType::E)(buffer[15U] & 0x0FU); ::memcpy(data.get(), buffer + 20U, DMR_FRAME_LENGTH_BYTES); @@ -465,7 +651,6 @@ void HostPatch::processDMRNetwork(uint8_t* buffer, uint32_t length) ::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); } @@ -690,9 +875,9 @@ void HostPatch::processP25Network(uint8_t* buffer, uint32_t length) if (m_digiMode != TX_MODE_P25) return; - bool grantDemand = (buffer[14U] & 0x80U) == 0x80U; - bool grantDenial = (buffer[14U] & 0x40U) == 0x40U; - bool unitToUnit = (buffer[14U] & 0x01U) == 0x01U; + 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]; @@ -761,18 +946,62 @@ void HostPatch::processP25Network(uint8_t* buffer, uint32_t length) if (dstId != m_srcTGId && dstId != m_dstTGId) return; - uint32_t actualDstId = m_dstTGId; - if (m_twoWayPatch) { - if (dstId == m_dstTGId) - actualDstId = m_srcTGId; - } else { - if (dstId == m_dstTGId) - return; + bool reverseEncrypt = false; + uint32_t actualDstId = m_srcTGId; + uint8_t tekAlgoId = m_tekSrcAlgoId; + uint16_t tekKeyId = m_tekSrcKeyId; + + if (!m_mmdvmP25Reflector) { + actualDstId = m_dstTGId; + if (m_twoWayPatch) { + if (dstId == m_dstTGId) { + actualDstId = m_srcTGId; + tekAlgoId = m_tekDstAlgoId; + tekKeyId = m_tekDstKeyId; + reverseEncrypt = true; + } + } else { + if (dstId == m_dstTGId) + return; + } } + // is this a new call stream? + uint16_t callKID = 0U; if (m_network->getP25StreamId() != m_rxStreamId && ((duid != DUID::TDU) && (duid != DUID::TDULC))) { m_callInProgress = true; + // 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 != tekAlgoId && callKID != tekKeyId) { + m_callAlgoId = ALGO_UNENCRYPT; + m_callInProgress = false; + + LogWarning(LOG_HOST, "P25, call ignored, using different encryption parameters, callAlgoId = $%02X, callKID = $%04X, tekAlgoId = $%02X, tekKID = $%04X", m_callAlgoId, callKID, tekAlgoId, tekKeyId); + 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]; + } + + if (reverseEncrypt) { + m_p25DstCrypto->setMI(mi); + m_p25DstCrypto->generateKeystream(); + } else { + m_p25SrcCrypto->setMI(mi); + m_p25SrcCrypto->generateKeystream(); + } + } + } + } + uint64_t now = std::chrono::duration_cast(std::chrono::system_clock::now().time_since_epoch()).count(); m_rxStartTime = now; @@ -780,13 +1009,15 @@ void HostPatch::processP25Network(uint8_t* buffer, uint32_t length) if (m_grantDemand) { p25::lc::LC lc = p25::lc::LC(); - lc.setLCO(p25::defines::LCO::GROUP); + lc.setLCO(P25DEF::LCO::GROUP); lc.setDstId(dstId); lc.setSrcId(srcId); p25::data::LowSpeedData lsd = p25::data::LowSpeedData(); - uint8_t controlByte = 0x80U; + uint8_t controlByte = network::NET_CTRL_GRANT_DEMAND; // Grant Demand Flag + if (m_callAlgoId != ALGO_UNENCRYPT) + controlByte |= network::NET_CTRL_GRANT_ENCRYPT; // Grant Encrypt Flag m_network->writeP25TDU(lc, lsd, controlByte); } } @@ -797,7 +1028,7 @@ void HostPatch::processP25Network(uint8_t* buffer, uint32_t length) return; p25::lc::LC lc = p25::lc::LC(); - lc.setLCO(p25::defines::LCO::GROUP); + lc.setLCO(P25DEF::LCO::GROUP); lc.setDstId(actualDstId); lc.setSrcId(srcId); @@ -805,8 +1036,13 @@ void HostPatch::processP25Network(uint8_t* buffer, uint32_t length) LogMessage(LOG_HOST, P25_TDU_STR); - uint8_t controlByte = 0x00U; - m_network->writeP25TDU(lc, lsd, controlByte); + if (m_mmdvmP25Reflector) { + m_mmdvmP25Net->writeTDU(); + } + else { + uint8_t controlByte = 0x00U; + m_network->writeP25TDU(lc, lsd, controlByte); + } if (m_rxStartTime > 0U) { uint64_t now = std::chrono::duration_cast(std::chrono::system_clock::now().time_since_epoch()).count(); @@ -819,8 +1055,14 @@ void HostPatch::processP25Network(uint8_t* buffer, uint32_t length) m_rxStreamId = 0U; m_callInProgress = false; + m_callAlgoId = ALGO_UNENCRYPT; m_rxStartTime = 0U; m_rxStreamId = 0U; + + m_p25SrcCrypto->clearMI(); + m_p25SrcCrypto->resetKeystream(); + m_p25DstCrypto->clearMI(); + m_p25DstCrypto->resetKeystream(); return; } @@ -879,6 +1121,10 @@ void HostPatch::processP25Network(uint8_t* buffer, uint32_t length) LogMessage(LOG_NET, P25_LDU1_STR " audio, srcId = %u, dstId = %u", srcId, dstId); + if (tekAlgoId != ALGO_UNENCRYPT && tekKeyId != 0U) { + cryptP25AudioFrame(netLDU, reverseEncrypt, 1U); + } + control = lc::LC(*dfsiLC.control()); control.setSrcId(srcId); @@ -902,7 +1148,30 @@ void HostPatch::processP25Network(uint8_t* buffer, uint32_t length) } } - m_network->writeP25LDU1(control, lsd, netLDU, frameType); + // the previous is nice and all -- but if we're cross-encrypting, we need to use the TEK + if (tekAlgoId != ALGO_UNENCRYPT && tekKeyId != 0U) { + control.setAlgId(tekAlgoId); + control.setKId(tekKeyId); + + uint8_t mi[MI_LENGTH_BYTES]; + ::memset(mi, 0x00U, MI_LENGTH_BYTES); + if (!reverseEncrypt) + m_p25SrcCrypto->getMI(mi); + else + m_p25DstCrypto->getMI(mi); + + control.setMI(mi); + } + + if (m_mmdvmP25Reflector) { + ::memcpy(m_netLDU1, netLDU, 9U * 25U); + m_gotNetLDU1 = true; + m_netLC = control; + + writeNet_LDU1(false); + } else { + m_network->writeP25LDU1(control, lsd, netLDU, frameType); + } } break; case DUID::LDU2: @@ -952,12 +1221,39 @@ void HostPatch::processP25Network(uint8_t* buffer, uint32_t length) LogMessage(LOG_NET, P25_LDU2_STR " audio, algo = $%02X, kid = $%04X", dfsiLC.control()->getAlgId(), dfsiLC.control()->getKId()); + if (tekAlgoId != ALGO_UNENCRYPT && tekKeyId != 0U) { + cryptP25AudioFrame(netLDU, reverseEncrypt, 2U); + } + control = lc::LC(*dfsiLC.control()); control.setSrcId(srcId); control.setDstId(actualDstId); - m_network->writeP25LDU2(control, lsd, netLDU); + // set the algo ID and key ID + if (tekAlgoId != ALGO_UNENCRYPT && tekKeyId != 0U) { + control.setAlgId(tekAlgoId); + control.setKId(tekKeyId); + + uint8_t mi[MI_LENGTH_BYTES]; + ::memset(mi, 0x00U, MI_LENGTH_BYTES); + if (!reverseEncrypt) + m_p25SrcCrypto->getMI(mi); + else + m_p25DstCrypto->getMI(mi); + + control.setMI(mi); + } + + if (m_mmdvmP25Reflector) { + ::memcpy(m_netLDU2, netLDU, 9U * 25U); + m_gotNetLDU2 = true; + m_netLC = control; + + writeNet_LDU2(false); + } else { + m_network->writeP25LDU2(control, lsd, netLDU); + } } break; @@ -975,6 +1271,296 @@ void HostPatch::processP25Network(uint8_t* buffer, uint32_t length) } } +/* Helper to cross encrypt P25 network traffic audio frames. */ + +void HostPatch::cryptP25AudioFrame(uint8_t* ldu, bool reverseEncrypt, uint8_t p25N) +{ + assert(ldu != nullptr); + using namespace p25; + using namespace p25::defines; + + uint8_t tekSrcAlgoId = m_tekSrcAlgoId; + uint16_t tekSrcKeyId = m_tekSrcKeyId; + uint8_t tekDstAlgoId = m_tekDstAlgoId; + uint16_t tekDstKeyId = m_tekDstKeyId; + + if (reverseEncrypt) { + tekSrcAlgoId = m_tekDstAlgoId; + tekSrcKeyId = m_tekDstKeyId; + tekDstAlgoId = m_tekSrcAlgoId; + tekDstKeyId = m_tekSrcKeyId; + } + + // 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, "P25, HostPatch::cryptP25AudioFrame(), IMBE", imbe, RAW_IMBE_LENGTH_BYTES); + + // first -- decrypt the IMBE codeword + if (tekSrcAlgoId != P25DEF::ALGO_UNENCRYPT && tekSrcKeyId > 0U) { + if (!reverseEncrypt && m_p25SrcCrypto->getTEKLength() > 0U) { + switch (tekSrcAlgoId) { + case P25DEF::ALGO_AES_256: + m_p25SrcCrypto->cryptAES_IMBE(imbe, (p25N == 1U) ? DUID::LDU1 : DUID::LDU2); + break; + case P25DEF::ALGO_ARC4: + m_p25SrcCrypto->cryptARC4_IMBE(imbe, (p25N == 1U) ? DUID::LDU1 : DUID::LDU2); + break; + default: + LogError(LOG_HOST, "Unsupported TEK algorithm, tekAlgoId = $%02X", tekSrcAlgoId); + break; + } + } else { + if (reverseEncrypt && m_p25DstCrypto->getTEKLength() > 0U) { + switch (tekDstAlgoId) { + case P25DEF::ALGO_AES_256: + m_p25DstCrypto->cryptAES_IMBE(imbe, (p25N == 1U) ? DUID::LDU1 : DUID::LDU2); + break; + case P25DEF::ALGO_ARC4: + m_p25DstCrypto->cryptARC4_IMBE(imbe, (p25N == 1U) ? DUID::LDU1 : DUID::LDU2); + break; + default: + LogError(LOG_HOST, "Unsupported TEK algorithm, tekAlgoId = $%02X", tekDstAlgoId); + break; + } + } + } + } + + // second -- reencrypt the IMBE codeword + if (tekDstAlgoId != P25DEF::ALGO_UNENCRYPT && tekDstKeyId > 0U) { + if (!reverseEncrypt && m_p25DstCrypto->getTEKLength() > 0U) { + switch (tekDstAlgoId) { + case P25DEF::ALGO_AES_256: + m_p25DstCrypto->cryptAES_IMBE(imbe, (p25N == 1U) ? DUID::LDU1 : DUID::LDU2); + break; + case P25DEF::ALGO_ARC4: + m_p25DstCrypto->cryptARC4_IMBE(imbe, (p25N == 1U) ? DUID::LDU1 : DUID::LDU2); + break; + default: + LogError(LOG_HOST, "Unsupported TEK algorithm, tekAlgoId = $%02X", tekDstAlgoId); + break; + } + } else { + if (reverseEncrypt && m_p25SrcCrypto->getTEKLength() > 0U) { + switch (tekSrcAlgoId) { + case P25DEF::ALGO_AES_256: + m_p25SrcCrypto->cryptAES_IMBE(imbe, (p25N == 1U) ? DUID::LDU1 : DUID::LDU2); + break; + case P25DEF::ALGO_ARC4: + m_p25SrcCrypto->cryptARC4_IMBE(imbe, (p25N == 1U) ? DUID::LDU1 : DUID::LDU2); + break; + default: + LogError(LOG_HOST, "Unsupported TEK algorithm, tekAlgoId = $%02X", tekSrcAlgoId); + break; + } + } + } + } + } +} + +/* Helper to process a FNE KMM TEK response. */ + +void HostPatch::processTEKResponse(p25::kmm::KeyItem* ki, uint8_t algId, uint8_t keyLength) +{ + if (ki == nullptr) + return; + + if (algId == m_tekSrcAlgoId && ki->kId() == m_tekSrcKeyId) { + LogMessage(LOG_HOST, "Source TEK loaded, algId = $%02X, kId = $%04X, sln = $%04X", algId, ki->kId(), ki->sln()); + UInt8Array tek = std::make_unique(keyLength); + ki->getKey(tek.get()); + + m_p25SrcCrypto->setTEKAlgoId(algId); + m_p25SrcCrypto->setTEKKeyId(ki->kId()); + m_p25SrcCrypto->setKey(tek.get(), keyLength); + } + + if (algId == m_tekDstAlgoId && ki->kId() == m_tekDstKeyId) { + LogMessage(LOG_HOST, "Destination TEK loaded, algId = $%02X, kId = $%04X, sln = $%04X", algId, ki->kId(), ki->sln()); + UInt8Array tek = std::make_unique(keyLength); + ki->getKey(tek.get()); + + m_p25DstCrypto->setTEKAlgoId(algId); + m_p25DstCrypto->setTEKKeyId(ki->kId()); + m_p25DstCrypto->setKey(tek.get(), keyLength); + } +} + +/* Helper to check for an unflushed LDU1 packet. */ + +void HostPatch::checkNet_LDU1() +{ + if (m_netState == RS_NET_IDLE) + return; + + // check for an unflushed LDU1 + if ((m_netLDU1[10U] != 0x00U || m_netLDU1[26U] != 0x00U || m_netLDU1[55U] != 0x00U || + m_netLDU1[80U] != 0x00U || m_netLDU1[105U] != 0x00U || m_netLDU1[130U] != 0x00U || + m_netLDU1[155U] != 0x00U || m_netLDU1[180U] != 0x00U || m_netLDU1[204U] != 0x00U) && + m_gotNetLDU1) + writeNet_LDU1(false); +} + +/* Helper to write a network P25 LDU1 packet. */ + +void HostPatch::writeNet_LDU1(bool toFNE) +{ + using namespace p25; + using namespace p25::defines; + using namespace p25::dfsi::defines; + + if (toFNE) { + if (m_netState == RS_NET_IDLE) { + m_callInProgress = true; + + uint64_t now = std::chrono::duration_cast(std::chrono::system_clock::now().time_since_epoch()).count(); + m_rxStartTime = now; + + uint8_t lco = m_netLDU1[51U]; + uint8_t mfId = m_netLDU1[52U]; + uint32_t dstId = GET_UINT24(m_netLDU1, 76U); + uint32_t srcId = GET_UINT24(m_netLDU1, 101U); + + LogMessage(LOG_HOST, "MMDVM P25, call start, srcId = %u, dstId = %u", srcId, dstId); + + lc::LC lc = lc::LC(); + m_netLC = lc; + m_netLC.setLCO(lco); + m_netLC.setMFId(mfId); + m_netLC.setDstId(dstId); + m_netLC.setSrcId(srcId); + + if (m_grantDemand) { + p25::lc::LC lc = p25::lc::LC(); + lc.setLCO(P25DEF::LCO::GROUP); + lc.setDstId(dstId); + lc.setSrcId(srcId); + + p25::data::LowSpeedData lsd = p25::data::LowSpeedData(); + + uint8_t controlByte = network::NET_CTRL_GRANT_DEMAND; // Grant Demand Flag + m_network->writeP25TDU(lc, lsd, controlByte); + } + } + + p25::data::LowSpeedData lsd = p25::data::LowSpeedData(); + lsd.setLSD1(m_netLDU1[201U]); + lsd.setLSD2(m_netLDU1[202U]); + + LogMessage(LOG_NET, "MMDVM " P25_LDU1_STR " audio, srcId = %u, dstId = %u", m_netLC.getSrcId(), m_netLC.getDstId()); + + if (m_debug) + Utils::dump(1U, "P25, HostPatch::writeNet_LDU1(), MMDVM -> DVM LDU1", m_netLDU1, 9U * 25U); + + m_network->writeP25LDU1(m_netLC, lsd, m_netLDU1, FrameType::DATA_UNIT); + + m_netState = RS_NET_AUDIO; + resetWithNullAudio(m_netLDU1, false); + m_gotNetLDU1 = false; + } + else { + if (m_debug) + Utils::dump(1U, "P25, HostPatch::writeNet_LDU1(), DVM -> MMDVM LDU1", m_netLDU1, 9U * 25U); + + // add the Low Speed Data + p25::data::LowSpeedData lsd = p25::data::LowSpeedData(); + lsd.setLSD1(m_netLDU1[201U]); + lsd.setLSD2(m_netLDU1[202U]); + + m_mmdvmP25Net->writeLDU1(m_netLDU1, m_netLC, lsd, false); + + resetWithNullAudio(m_netLDU1, false); + m_gotNetLDU1 = false; + } +} + +/* Helper to check for an unflushed LDU2 packet. */ + +void HostPatch::checkNet_LDU2() +{ + if (m_netState == RS_NET_IDLE) + return; + + // check for an unflushed LDU2 + if ((m_netLDU2[10U] != 0x00U || m_netLDU2[26U] != 0x00U || m_netLDU2[55U] != 0x00U || + m_netLDU2[80U] != 0x00U || m_netLDU2[105U] != 0x00U || m_netLDU2[130U] != 0x00U || + m_netLDU2[155U] != 0x00U || m_netLDU2[180U] != 0x00U || m_netLDU2[204U] != 0x00U) && + m_gotNetLDU2) { + writeNet_LDU2(false); + } +} + +/* Helper to write a network P25 LDU2 packet. */ + +void HostPatch::writeNet_LDU2(bool toFNE) +{ + using namespace p25; + using namespace p25::defines; + using namespace p25::dfsi::defines; + + if (toFNE) { + p25::data::LowSpeedData lsd = p25::data::LowSpeedData(); + lsd.setLSD1(m_netLDU2[201U]); + lsd.setLSD2(m_netLDU2[202U]); + + LogMessage(LOG_NET, "MMDVM " P25_LDU2_STR " audio"); + + if (m_debug) + Utils::dump(1U, "P25, HostPatch::writeNet_LDU2(), MMDVM -> DVM LDU2", m_netLDU2, 9U * 25U); + + m_network->writeP25LDU2(m_netLC, lsd, m_netLDU2); + + resetWithNullAudio(m_netLDU2, false); + m_gotNetLDU2 = false; + } + else { + if (m_debug) + Utils::dump(1U, "P25, HostPatch::writeNet_LDU2(), DVM -> MMDVM LDU2", m_netLDU2, 9U * 25U); + + // add the Low Speed Data + p25::data::LowSpeedData lsd = p25::data::LowSpeedData(); + lsd.setLSD1(m_netLDU2[201U]); + lsd.setLSD2(m_netLDU2[202U]); + + m_mmdvmP25Net->writeLDU2(m_netLDU2, m_netLC, lsd, false); + + resetWithNullAudio(m_netLDU2, false); + m_gotNetLDU2 = false; + } +} + /* Entry point to network processing thread. */ void* HostPatch::threadNetworkProcess(void* arg) @@ -1010,6 +1596,26 @@ void* HostPatch::threadNetworkProcess(void* arg) continue; } + if (patch->m_network->getStatus() == NET_STAT_RUNNING) { + // check if we need to request a TEK for the source TGID + if (patch->m_tekSrcAlgoId != P25DEF::ALGO_UNENCRYPT && patch->m_tekSrcKeyId > 0U) { + if (patch->m_p25SrcCrypto->getTEKLength() == 0U && !patch->m_requestedSrcTek) { + patch->m_requestedSrcTek = true; + LogMessage(LOG_HOST, "Patch source TGID encryption enabled, requesting TEK from network."); + patch->m_network->writeKeyReq(patch->m_tekSrcKeyId, patch->m_tekSrcAlgoId); + } + } + + // check if we need to request a TEK for the destination TGID + if (patch->m_tekDstAlgoId != P25DEF::ALGO_UNENCRYPT && patch->m_tekDstKeyId > 0U) { + if (patch->m_p25DstCrypto->getTEKLength() == 0U && !patch->m_requestedDstTek) { + patch->m_requestedDstTek = true; + LogMessage(LOG_HOST, "Patch destination TGID encryption enabled, requesting TEK from network."); + patch->m_network->writeKeyReq(patch->m_tekDstKeyId, patch->m_tekDstAlgoId); + } + } + } + uint32_t length = 0U; bool netReadRet = false; if (patch->m_digiMode == TX_MODE_DMR) { @@ -1038,6 +1644,181 @@ void* HostPatch::threadNetworkProcess(void* arg) return nullptr; } +/* Entry point to MMDVM network processing thread. */ + +void* HostPatch::threadMMDVMProcess(void* arg) +{ + using namespace p25; + using namespace p25::defines; + using namespace p25::dfsi::defines; + + thread_t* th = (thread_t*)arg; + if (th != nullptr) { +#if defined(_WIN32) + ::CloseHandle(th->thread); +#else + ::pthread_detach(th->thread); +#endif // defined(_WIN32) + + std::string threadName("patch:mmdvm-net-process"); + HostPatch* patch = static_cast(th->obj); + if (patch == nullptr) { + g_killed = true; + LogError(LOG_HOST, "[FAIL] %s", threadName.c_str()); + } + + if (g_killed) { + delete th; + return nullptr; + } + + LogMessage(LOG_HOST, "[ OK ] %s", threadName.c_str()); +#ifdef _GNU_SOURCE + ::pthread_setname_np(th->thread, threadName.c_str()); +#endif // _GNU_SOURCE + + StopWatch stopWatch; + stopWatch.start(); + + while (!g_killed) { + if (!patch->m_running) { + Thread::sleep(1U); + continue; + } + + uint32_t ms = stopWatch.elapsed(); + + ms = stopWatch.elapsed(); + stopWatch.start(); + + if (patch->m_digiMode == TX_MODE_P25) { + std::lock_guard lock(HostPatch::m_networkMutex); + + DECLARE_UINT8_ARRAY(buffer, 100U); + uint32_t len = patch->m_mmdvmP25Net->read(buffer, 100U); + if (len != 0U) { + switch (buffer[0U]) { + // LDU1 + case DFSIFrameType::LDU1_VOICE1: + ::memcpy(patch->m_netLDU1 + 0U, buffer, 22U); + break; + case DFSIFrameType::LDU1_VOICE2: + ::memcpy(patch->m_netLDU1 + 25U, buffer, 14U); + break; + case DFSIFrameType::LDU1_VOICE3: + ::memcpy(patch->m_netLDU1 + 50U, buffer, 17U); + break; + case DFSIFrameType::LDU1_VOICE4: + ::memcpy(patch->m_netLDU1 + 75U, buffer, 17U); + break; + case DFSIFrameType::LDU1_VOICE5: + ::memcpy(patch->m_netLDU1 + 100U, buffer, 17U); + break; + case DFSIFrameType::LDU1_VOICE6: + ::memcpy(patch->m_netLDU1 + 125U, buffer, 17U); + break; + case DFSIFrameType::LDU1_VOICE7: + ::memcpy(patch->m_netLDU1 + 150U, buffer, 17U); + break; + case DFSIFrameType::LDU1_VOICE8: + ::memcpy(patch->m_netLDU1 + 175U, buffer, 17U); + break; + case DFSIFrameType::LDU1_VOICE9: + ::memcpy(patch->m_netLDU1 + 200U, buffer, 16U); + patch->checkNet_LDU2(); + + if (patch->m_netState != RS_NET_IDLE) { + patch->m_gotNetLDU1 = true; + patch->writeNet_LDU1(true); + } + break; + + // LDU2 + case DFSIFrameType::LDU2_VOICE10: + ::memcpy(patch->m_netLDU2 + 0U, buffer, 22U); + break; + case DFSIFrameType::LDU2_VOICE11: + ::memcpy(patch->m_netLDU2 + 25U, buffer, 14U); + break; + case DFSIFrameType::LDU2_VOICE12: + ::memcpy(patch->m_netLDU2 + 50U, buffer, 17U); + break; + case DFSIFrameType::LDU2_VOICE13: + ::memcpy(patch->m_netLDU2 + 75U, buffer, 17U); + break; + case DFSIFrameType::LDU2_VOICE14: + ::memcpy(patch->m_netLDU2 + 100U, buffer, 17U); + break; + case DFSIFrameType::LDU2_VOICE15: + ::memcpy(patch->m_netLDU2 + 125U, buffer, 17U); + break; + case DFSIFrameType::LDU2_VOICE16: + ::memcpy(patch->m_netLDU2 + 150U, buffer, 17U); + break; + case DFSIFrameType::LDU2_VOICE17: + ::memcpy(patch->m_netLDU2 + 175U, buffer, 17U); + break; + case DFSIFrameType::LDU2_VOICE18: + ::memcpy(patch->m_netLDU2 + 200U, buffer, 16U); + if (patch->m_netState == RS_NET_IDLE) { + patch->writeNet_LDU1(true); + } else { + patch->checkNet_LDU1(); + } + + patch->writeNet_LDU2(true); + break; + + case 0x80U: + { + patch->m_netState = RS_NET_IDLE; + + p25::data::LowSpeedData lsd = p25::data::LowSpeedData(); + + LogMessage(LOG_HOST, "MMDVM " P25_TDU_STR); + + uint8_t controlByte = 0x00U; + patch->m_network->writeP25TDU(patch->m_netLC, lsd, controlByte); + + if (patch->m_rxStartTime > 0U) { + uint64_t now = std::chrono::duration_cast(std::chrono::system_clock::now().time_since_epoch()).count(); + uint64_t diff = now - patch->m_rxStartTime; + + LogMessage(LOG_HOST, "MMDVM P25, call end, srcId = %u, dstId = %u, dur = %us", patch->m_netLC.getSrcId(), patch->m_netLC.getDstId(), diff / 1000U); + } + + patch->m_rxStartTime = 0U; + patch->m_rxStreamId = 0U; + + patch->m_callInProgress = false; + patch->m_rxStartTime = 0U; + patch->m_rxStreamId = 0U; + } + break; + + case 0xF0U: + case 0xF1U: + // these are MMDVM control bytes -- we ignore these + break; + + default: + LogError(LOG_NET, "unknown opcode from MMDVM gateway $%02X", buffer[0U]); + break; + } + } + } + + if (ms < 5U) + Thread::sleep(5U); + } + + LogMessage(LOG_HOST, "[STOP] %s", threadName.c_str()); + delete th; + } + + return nullptr; +} + /* Helper to reset IMBE buffer with null frames. */ void HostPatch::resetWithNullAudio(uint8_t* data, bool encrypted) diff --git a/src/patch/HostPatch.h b/src/patch/HostPatch.h index 2e91a899..2bc36374 100644 --- a/src/patch/HostPatch.h +++ b/src/patch/HostPatch.h @@ -20,10 +20,13 @@ #include "common/dmr/data/EmbeddedData.h" #include "common/dmr/lc/LC.h" #include "common/dmr/lc/PrivacyLC.h" +#include "common/p25/lc/LC.h" +#include "common/p25/Crypto.h" #include "common/network/udp/Socket.h" #include "common/yaml/Yaml.h" #include "common/Timer.h" #include "network/PeerNetwork.h" +#include "mmdvm/P25Network.h" #include #include @@ -73,6 +76,16 @@ private: uint8_t m_dstSlot; bool m_twoWayPatch; + bool m_mmdvmP25Reflector; + mmdvm::P25Network* m_mmdvmP25Net; + + RPT_NET_STATE m_netState; + p25::lc::LC m_netLC; + bool m_gotNetLDU1; + uint8_t* m_netLDU1; + bool m_gotNetLDU2; + uint8_t* m_netLDU2; + std::string m_identity; uint8_t m_digiMode; @@ -82,9 +95,23 @@ private: bool m_grantDemand; bool m_callInProgress; + uint8_t m_callAlgoId; uint64_t m_rxStartTime; uint32_t m_rxStreamId; + uint8_t m_tekSrcAlgoId; + uint16_t m_tekSrcKeyId; + uint8_t m_tekDstAlgoId; + uint16_t m_tekDstKeyId; + bool m_requestedSrcTek; + bool m_requestedDstTek; + + p25::crypto::P25Crypto* m_p25SrcCrypto; + p25::crypto::P25Crypto* m_p25DstCrypto; + + uint32_t m_netId; + uint32_t m_sysId; + bool m_running; bool m_trace; bool m_debug; @@ -101,6 +128,11 @@ private: * @returns bool True, if network connectivity was initialized, otherwise false. */ bool createNetwork(); + /** + * @brief Initializes MMDVM network connectivity. + * @returns bool True, if network connectivity was initialized, otherwise false. + */ + bool createMMDVMP25Network(); /** * @brief Helper to process DMR network traffic. @@ -116,12 +148,53 @@ private: */ void processP25Network(uint8_t* buffer, uint32_t length); + /** + * @brief Helper to cross encrypt P25 network traffic audio frames. + * @param ldu + * @param reverseEncrypt Flag indicating whether or not to reverse the encryption (i.e. use destination TEK vs source TEK). + * @param p25N + */ + void cryptP25AudioFrame(uint8_t* ldu, bool reverseEncrypt, uint8_t p25N); + + /** + * @brief Helper to process a FNE KMM TEK response. + * @param ki Key Item. + * @param algId Algorithm ID. + * @param keyLength Length of key in bytes. + */ + void processTEKResponse(p25::kmm::KeyItem* ki, uint8_t algId, uint8_t keyLength); + + /** + * @brief Helper to check for an unflushed LDU1 packet. + */ + void checkNet_LDU1(); + /** + * @brief Helper to write a network P25 LDU1 packet. + * @param toFNE Flag indicating whether or not the packet is being written to the DVM FNE. + */ + void writeNet_LDU1(bool toFNE); + /** + * @brief Helper to check for an unflushed LDU2 packet. + */ + void checkNet_LDU2(); + /** + * @brief Helper to write a network P25 LDU2 packet. + * @param toFNE Flag indicating whether or not the packet is being written to the DVM FNE. + */ + void writeNet_LDU2(bool toFNE); + /** * @brief Entry point to network processing thread. * @param arg Instance of the thread_t structure. * @returns void* (Ignore) */ static void* threadNetworkProcess(void* arg); + /** + * @brief Entry point to MMDVM network processing thread. + * @param arg Instance of the thread_t structure. + * @returns void* (Ignore) + */ + static void* threadMMDVMProcess(void* arg); /** * @brief Helper to reset IMBE buffer with null frames. diff --git a/src/patch/mmdvm/P25Network.cpp b/src/patch/mmdvm/P25Network.cpp new file mode 100644 index 00000000..892cf6f1 --- /dev/null +++ b/src/patch/mmdvm/P25Network.cpp @@ -0,0 +1,495 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Digital Voice Modem - TG Patch + * GPLv2 Open Source. Use is subject to license terms. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * Copyright (C) 2009-2014,2016,2020,2021 by Jonathan Naylor G4KLX + * Copyright (C) 2025 Bryan Biedenkapp, N2PLL + * + */ +#include "patch/Defines.h" +#include "common/p25/P25Defines.h" +#include "common/Log.h" +#include "common/Utils.h" +#include "mmdvm/P25Network.h" + +using namespace p25::defines; +using namespace network::udp; +using namespace mmdvm; + +#include +#include +#include + +// --------------------------------------------------------------------------- +// Constants +// --------------------------------------------------------------------------- + +/* +** bryanb: we purposely do handling this way, instead of using the DFSI classes, to ensure we maintain compat +** with upstream MMDVM, extra data handled from our DFSI could confuse the P25 gateway +*/ + +const uint8_t REC62[] = { + 0x62U, 0x02U, 0x02U, 0x0CU, 0x0BU, 0x12U, 0x64U, 0x00U, 0x00U, 0x80U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, + 0x00U, 0x00U, 0x00U, 0x00U, 0x00U +}; + +const uint8_t REC63[] = { + 0x63U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x02U +}; + +const uint8_t REC64[] = { + 0x64U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x02U +}; + +const uint8_t REC65[] = { + 0x65U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x02U +}; + +const uint8_t REC66[] = { + 0x66U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x02U +}; + +const uint8_t REC67[] = { + 0x67U, 0xF0U, 0x9DU, 0x6AU, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x02U +}; + +const uint8_t REC68[] = { + 0x68U, 0x19U, 0xD4U, 0x26U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x02U +}; + +const uint8_t REC69[] = { + 0x69U, 0xE0U, 0xEBU, 0x7BU, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x02U +}; + +const uint8_t REC6A[] = { + 0x6AU, 0x00U, 0x00U, 0x02U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U +}; + +const uint8_t REC6B[] = { + 0x6BU, 0x02U, 0x02U, 0x0CU, 0x0BU, 0x12U, 0x64U, 0x00U, 0x00U, 0x80U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, + 0x00U, 0x00U, 0x00U, 0x00U, 0x00U +}; + +const uint8_t REC6C[] = { + 0x6CU, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x02U +}; + +const uint8_t REC6D[] = { + 0x6DU, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x02U +}; + +const uint8_t REC6E[] = { + 0x6EU, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x02U +}; + +const uint8_t REC6F[] = { + 0x6FU, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x02U +}; + +const uint8_t REC70[] = { + 0x70U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x02U +}; + +const uint8_t REC71[] = { + 0x71U, 0xACU, 0xB8U, 0xA4U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x02U +}; + +const uint8_t REC72[] = { + 0x72U, 0x9BU, 0xDCU, 0x75U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x02U +}; + +const uint8_t REC73[] = { + 0x73U, 0x00U, 0x00U, 0x02U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U +}; + +const uint8_t REC80[] = { + 0x80U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U +}; + +const uint32_t BUFFER_LENGTH = 100U; + +// --------------------------------------------------------------------------- +// Public Class Members +// --------------------------------------------------------------------------- + +/* Initializes a new instance of the P25Network class. */ + +P25Network::P25Network(const std::string& gatewayAddress, uint16_t gatewayPort, uint16_t localPort, bool debug) : + m_socket(localPort), + m_addr(), + m_addrLen(0U), + m_debug(debug), + m_buffer(1000U, "MMDVM P25 Network") +{ + if (Socket::lookup(gatewayAddress, gatewayPort, m_addr, m_addrLen) != 0) + m_addrLen = 0U; +} + +/* Finalizes a instance of the P25Network class. */ + +P25Network::~P25Network() = default; + +/* Reads P25 raw frame data from the P25 ring buffer. */ + +uint32_t P25Network::read(uint8_t* data, uint32_t length) +{ + assert(data != nullptr); + + if (m_buffer.isEmpty()) + return 0U; + + uint8_t c = 0U; + m_buffer.get(&c, 1U); + + if (c == 0U) { + return 0U; + } + + if (c > length) { + return 0U; + } + + m_buffer.get(data, c); + return c; +} + +/* Writes P25 LDU1 frame data to the network. */ + +bool P25Network::writeLDU1(const uint8_t* ldu1, const p25::lc::LC& control, const p25::data::LowSpeedData& lsd, bool end) +{ + assert(ldu1 != nullptr); + + uint8_t buffer[22U]; + + // The '62' record + ::memcpy(buffer, REC62, 22U); + ::memcpy(buffer + 10U, ldu1 + 10U, RAW_IMBE_LENGTH_BYTES); + + if (m_debug) + Utils::dump(1U, "P25, P25Network::writeLDU1(), MMDVM Network $62 LDU1 Sent", buffer, 22U); + + bool ret = m_socket.write(buffer, 22U, m_addr, m_addrLen); + if (!ret) + return false; + + // The '63' record + ::memcpy(buffer, REC63, 14U); + ::memcpy(buffer + 1U, ldu1 + 26U, RAW_IMBE_LENGTH_BYTES); + + if (m_debug) + Utils::dump(1U, "P25, P25Network::writeLDU1(), MMDVM Network $63 LDU1 Sent", buffer, 14U); + + ret = m_socket.write(buffer, 14U, m_addr, m_addrLen); + if (!ret) + return false; + + // The '64' record + ::memcpy(buffer, REC64, 17U); + buffer[1U] = control.getLCO(); + buffer[2U] = control.getMFId(); + ::memcpy(buffer + 5U, ldu1 + 55U, RAW_IMBE_LENGTH_BYTES); + + if (m_debug) + Utils::dump(1U, "P25, P25Network::writeLDU1(), MMDVM Network $64 LDU1 Sent", buffer, 17U); + + ret = m_socket.write(buffer, 17U, m_addr, m_addrLen); + if (!ret) + return false; + + // The '65' record + ::memcpy(buffer, REC65, 17U); + uint32_t id = control.getDstId(); + buffer[1U] = (id >> 16) & 0xFFU; + buffer[2U] = (id >> 8) & 0xFFU; + buffer[3U] = (id >> 0) & 0xFFU; + ::memcpy(buffer + 5U, ldu1 + 80U, RAW_IMBE_LENGTH_BYTES); + + if (m_debug) + Utils::dump(1U, "P25, P25Network::writeLDU1(), MMDVM Network $65 LDU1 Sent", buffer, 17U); + + ret = m_socket.write(buffer, 17U, m_addr, m_addrLen); + if (!ret) + return false; + + // The '66' record + ::memcpy(buffer, REC66, 17U); + id = control.getSrcId(); + buffer[1U] = (id >> 16) & 0xFFU; + buffer[2U] = (id >> 8) & 0xFFU; + buffer[3U] = (id >> 0) & 0xFFU; + ::memcpy(buffer + 5U, ldu1 + 105U, RAW_IMBE_LENGTH_BYTES); + + if (m_debug) + Utils::dump(1U, "P25, P25Network::writeLDU1(), MMDVM Network $66 LDU1 Sent", buffer, 17U); + + ret = m_socket.write(buffer, 17U, m_addr, m_addrLen); + if (!ret) + return false; + + // The '67' record + ::memcpy(buffer, REC67, 17U); + ::memcpy(buffer + 5U, ldu1 + 130U, RAW_IMBE_LENGTH_BYTES); + + if (m_debug) + Utils::dump(1U, "P25, P25Network::writeLDU1(), MMDVM Network $67 LDU1 Sent", buffer, 17U); + + ret = m_socket.write(buffer, 17U, m_addr, m_addrLen); + if (!ret) + return false; + + // The '68' record + ::memcpy(buffer, REC68, 17U); + ::memcpy(buffer + 5U, ldu1 + 155U, RAW_IMBE_LENGTH_BYTES); + + if (m_debug) + Utils::dump(1U, "P25, P25Network::writeLDU1(), MMDVM Network $68 LDU1 Sent", buffer, 17U); + + ret = m_socket.write(buffer, 17U, m_addr, m_addrLen); + if (!ret) + return false; + + // The '69' record + ::memcpy(buffer, REC69, 17U); + ::memcpy(buffer + 5U, ldu1 + 180U, RAW_IMBE_LENGTH_BYTES); + + if (m_debug) + Utils::dump(1U, "P25, P25Network::writeLDU1(), MMDVM Network $69 LDU1 Sent", buffer, 17U); + + ret = m_socket.write(buffer, 17U, m_addr, m_addrLen); + if (!ret) + return false; + + // The '6A' record + ::memcpy(buffer, REC6A, 16U); + buffer[1U] = lsd.getLSD1(); + buffer[2U] = lsd.getLSD2(); + ::memcpy(buffer + 5U, ldu1 + 204U, RAW_IMBE_LENGTH_BYTES); + + if (m_debug) + Utils::dump(1U, "P25, P25Network::writeLDU1(), MMDVM Network $6A LDU1 Sent", buffer, 16U); + + ret = m_socket.write(buffer, 16U, m_addr, m_addrLen); + if (!ret) + return false; + + if (end) { + if (m_debug) + Utils::dump(1U, "P25, P25Network::writeLDU1(), MMDVM Network END Sent", REC80, 17U); + + ret = m_socket.write(REC80, 17U, m_addr, m_addrLen); + if (!ret) + return false; + } + + return true; +} + +/* Writes P25 LDU2 frame data to the network. */ + +bool P25Network::writeLDU2(const uint8_t* ldu2, const p25::lc::LC& control, const p25::data::LowSpeedData& lsd, bool end) +{ + assert(ldu2 != nullptr); + + uint8_t buffer[22U]; + + // The '6B' record + ::memcpy(buffer, REC6B, 22U); + ::memcpy(buffer + 10U, ldu2 + 10U, RAW_IMBE_LENGTH_BYTES); + + if (m_debug) + Utils::dump(1U, "P25, P25Network::writeLDU2(), MMDVM Network $6B LDU2 Sent", buffer, 22U); + + bool ret = m_socket.write(buffer, 22U, m_addr, m_addrLen); + if (!ret) + return false; + + // The '6C' record + ::memcpy(buffer, REC6C, 14U); + ::memcpy(buffer + 1U, ldu2 + 26U, RAW_IMBE_LENGTH_BYTES); + + if (m_debug) + Utils::dump(1U, "P25, P25Network::writeLDU2(), MMDVM Network $6C LDU2 Sent", buffer, 14U); + + ret = m_socket.write(buffer, 14U, m_addr, m_addrLen); + if (!ret) + return false; + + uint8_t mi[MI_LENGTH_BYTES]; + control.getMI(mi); + + // The '6D' record + ::memcpy(buffer, REC6D, 17U); + buffer[1U] = mi[0U]; + buffer[2U] = mi[1U]; + buffer[3U] = mi[2U]; + ::memcpy(buffer + 5U, ldu2 + 55U, RAW_IMBE_LENGTH_BYTES); + + if (m_debug) + Utils::dump(1U, "P25, P25Network::writeLDU2(), MMDVM Network $6D LDU2 Sent", buffer, 17U); + + ret = m_socket.write(buffer, 17U, m_addr, m_addrLen); + if (!ret) + return false; + + // The '6E' record + ::memcpy(buffer, REC6E, 17U); + buffer[1U] = mi[3U]; + buffer[2U] = mi[4U]; + buffer[3U] = mi[5U]; + ::memcpy(buffer + 5U, ldu2 + 80U, RAW_IMBE_LENGTH_BYTES); + + if (m_debug) + Utils::dump(1U, "P25, P25Network::writeLDU2(), MMDVM Network $6E LDU2 Sent", buffer, 17U); + + ret = m_socket.write(buffer, 17U, m_addr, m_addrLen); + if (!ret) + return false; + + // The '6F' record + ::memcpy(buffer, REC6F, 17U); + buffer[1U] = mi[6U]; + buffer[2U] = mi[7U]; + buffer[3U] = mi[8U]; + ::memcpy(buffer + 5U, ldu2 + 105U, RAW_IMBE_LENGTH_BYTES); + + if (m_debug) + Utils::dump(1U, "P25, P25Network::writeLDU2(), MMDVM Network $6F LDU2 Sent", buffer, 17U); + + ret = m_socket.write(buffer, 17U, m_addr, m_addrLen); + if (!ret) + return false; + + // The '70' record + ::memcpy(buffer, REC70, 17U); + buffer[1U] = control.getAlgId(); + uint32_t id = control.getKId(); + buffer[2U] = (id >> 8) & 0xFFU; + buffer[3U] = (id >> 0) & 0xFFU; + ::memcpy(buffer + 5U, ldu2 + 130U, RAW_IMBE_LENGTH_BYTES); + + if (m_debug) + Utils::dump(1U, "P25, P25Network::writeLDU2(), MMDVM Network $70 LDU2 Sent", buffer, 17U); + + ret = m_socket.write(buffer, 17U, m_addr, m_addrLen); + if (!ret) + return false; + + // The '71' record + ::memcpy(buffer, REC71, 17U); + ::memcpy(buffer + 5U, ldu2 + 155U, RAW_IMBE_LENGTH_BYTES); + + if (m_debug) + Utils::dump(1U, "P25, P25Network::writeLDU2(), MMDVM Network $71 LDU2 Sent", buffer, 17U); + + ret = m_socket.write(buffer, 17U, m_addr, m_addrLen); + if (!ret) + return false; + + // The '72' record + ::memcpy(buffer, REC72, 17U); + ::memcpy(buffer + 5U, ldu2 + 180U, RAW_IMBE_LENGTH_BYTES); + + if (m_debug) + Utils::dump(1U, "P25, P25Network::writeLDU2(), MMDVM Network $72 LDU2 Sent", buffer, 17U); + + ret = m_socket.write(buffer, 17U, m_addr, m_addrLen); + if (!ret) + return false; + + // The '73' record + ::memcpy(buffer, REC73, 16U); + buffer[1U] = lsd.getLSD1(); + buffer[2U] = lsd.getLSD2(); + ::memcpy(buffer + 5U, ldu2 + 204U, RAW_IMBE_LENGTH_BYTES); + + if (m_debug) + Utils::dump(1U, "P25, P25Network::writeLDU2(), MMDVM Network $73 LDU2 Sent", buffer, 16U); + + ret = m_socket.write(buffer, 16U, m_addr, m_addrLen); + if (!ret) + return false; + + if (end) { + if (m_debug) + Utils::dump(1U, "P25, P25Network::writeLDU2(), MMDVM Network END Sent", REC80, 17U); + + ret = m_socket.write(REC80, 17U, m_addr, m_addrLen); + if (!ret) + return false; + } + + return true; +} + +/* Writes a TDU frame to the network. */ + +bool P25Network::writeTDU() +{ + if (m_debug) + Utils::dump(1U, "P25, P25Network::writeTDU(), MMDVM Network END Sent", REC80, 17U); + + bool ret = m_socket.write(REC80, 17U, m_addr, m_addrLen); + if (!ret) + return false; + + return true; +} + +/* Updates the timer by the passed number of milliseconds. */ + +void P25Network::clock(uint32_t ms) +{ + uint8_t buffer[BUFFER_LENGTH]; + + sockaddr_storage address; + uint32_t addrLen; + int length = m_socket.read(buffer, BUFFER_LENGTH, address, addrLen); + if (length <= 0) + return; + + if (!Socket::match(m_addr, address)) { + LogMessage(LOG_NET, "MMDVM, packet received from an invalid source"); + return; + } + + if (m_debug) + Utils::dump(1U, "P25Network::clock(), MMDVM Network Data Received", buffer, length); + + uint8_t c = length; + m_buffer.addData(&c, 1U); + + m_buffer.addData(buffer, length); +} + +/* Helper to determine if we are connected to a MMDVM gateway. */ + +bool P25Network::isConnected() const +{ + return (m_addrLen != 0); +} + +/* Opens connection to the network. */ + +bool P25Network::open() +{ + if (m_addrLen == 0U) { + LogError(LOG_NET, "MMDVM, Unable to resolve the address of the P25 Gateway"); + return false; + } + + LogMessage(LOG_NET, "MMDVM, Opening P25 network connection"); + + return m_socket.open(m_addr); +} + +/* Closes connection to the network. */ + +void P25Network::close() +{ + m_socket.close(); + + LogMessage(LOG_NET, "MMDVM, Closing P25 network connection"); +} diff --git a/src/patch/mmdvm/P25Network.h b/src/patch/mmdvm/P25Network.h new file mode 100644 index 00000000..ca3a5bb2 --- /dev/null +++ b/src/patch/mmdvm/P25Network.h @@ -0,0 +1,123 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Digital Voice Modem - TG Patch + * GPLv2 Open Source. Use is subject to license terms. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * Copyright (C) 2009-2014,2016,2020,2021 by Jonathan Naylor G4KLX + * Copyright (C) 2025 Bryan Biedenkapp, N2PLL + * + */ +/** + * @defgroup patch_mmdvm MMDVM Networking + * @brief Implementation for the patch MMDVM networking. + * @ingroup patch + * + * @file PeerNetwork.h + * @ingroup patch_mmdvm + * @file PeerNetwork.cpp + * @ingroup patch_mmdvm + */ +#if !defined(__P25_NETWORK_H__) +#define __P25_NETWORK_H__ + +#include "Defines.h" +#include "common/p25/data/LowSpeedData.h" +#include "common/p25/lc/LC.h" +#include "common/network/udp/Socket.h" +#include "common/RingBuffer.h" + +#include +#include + +namespace mmdvm +{ + // --------------------------------------------------------------------------- + // Class Declaration + // Implements the MMDVM networking logic. + // --------------------------------------------------------------------------- + + class P25Network { + public: + /** + * @brief Initializes a new instance of the P25Network class. + * @param gatewayAddress Network Hostname/IP address to connect to. + * @param gatewayPort Network port number. + * @param localPort + * @param debug Flag indicating whether network debug is enabled. + */ + P25Network(const std::string& gatewayAddress, uint16_t gatewayPort, uint16_t localPort, bool debug); + /** + * @brief Finalizes a instance of the P25Network class. + */ + ~P25Network(); + + /** + * @brief Reads P25 raw frame data from the P25 ring buffer. + * @param[out] data Buffer to write received frame data to. + * @param[out] length Length in bytes of received frame. + * @returns + */ + uint32_t read(uint8_t* data, uint32_t length); + + /** + * @brief Writes P25 LDU1 frame data to the network. + * @param[in] ldu1 Buffer containing P25 LDU1 data to send. + * @param[in] control Instance of p25::lc::LC containing link control data. + * @param[in] lsd Instance of p25::data::LowSpeedData containing low speed data. + * @param[in] end + * @returns bool True, if message was sent, otherwise false. + */ + bool writeLDU1(const uint8_t* ldu1, const p25::lc::LC& control, const p25::data::LowSpeedData& lsd, bool end); + + /** + * @brief Writes P25 LDU2 frame data to the network. + * @param[in] ldu1 Buffer containing P25 LDU2 data to send. + * @param[in] control Instance of p25::lc::LC containing link control data. + * @param[in] lsd Instance of p25::data::LowSpeedData containing low speed data. + * @param[in] end + * @returns bool True, if message was sent, otherwise false. + */ + bool writeLDU2(const uint8_t* ldu2, const p25::lc::LC& control, const p25::data::LowSpeedData& lsd, bool end); + + /** + * @brief Writes a TDU frame to the network. + */ + bool writeTDU(); + + /** + * @brief Updates the timer by the passed number of milliseconds. + * @param ms Number of milliseconds. + */ + void clock(uint32_t ms); + + /** + * @brief Helper to determine if we are connected to a MMDVM gateway. + * @return bool True, if connected, otherwise false. + */ + bool isConnected() const; + + /** + * @brief Opens connection to the network. + * @returns bool True, if networking has started, otherwise false. + */ + bool open(); + + /** + * @brief Closes connection to the network. + */ + void close(); + + private: + network::udp::Socket m_socket; + + sockaddr_storage m_addr; + uint32_t m_addrLen; + + bool m_debug; + + RingBuffer m_buffer; + }; +} // namespace mmdvm + +#endif // __P25_NETWORK_H__ diff --git a/src/patch/network/PeerNetwork.cpp b/src/patch/network/PeerNetwork.cpp index a16c0ecf..6bbe6ad5 100644 --- a/src/patch/network/PeerNetwork.cpp +++ b/src/patch/network/PeerNetwork.cpp @@ -29,7 +29,7 @@ using namespace network; PeerNetwork::PeerNetwork(const std::string& address, uint16_t port, uint16_t localPort, uint32_t peerId, const std::string& password, bool duplex, bool debug, bool dmr, bool p25, bool nxdn, bool slot1, bool slot2, bool allowActivityTransfer, bool allowDiagnosticTransfer, bool updateLookup, bool saveLookup) : - Network(address, port, localPort, peerId, password, duplex, debug, dmr, p25, nxdn, slot1, slot2, allowActivityTransfer, allowDiagnosticTransfer, updateLookup, saveLookup) + Network(address, port, localPort, peerId, password, duplex, debug, dmr, p25, nxdn, false, slot1, slot2, allowActivityTransfer, allowDiagnosticTransfer, updateLookup, saveLookup) { assert(!address.empty()); assert(port > 0U); @@ -38,7 +38,8 @@ PeerNetwork::PeerNetwork(const std::string& address, uint16_t port, uint16_t loc /* Writes P25 LDU1 frame data to the network. */ -bool PeerNetwork::writeP25LDU1(const p25::lc::LC& control, const p25::data::LowSpeedData& lsd, const uint8_t* data, p25::defines::FrameType::E frameType) +bool PeerNetwork::writeP25LDU1(const p25::lc::LC& control, const p25::data::LowSpeedData& lsd, const uint8_t* data, + P25DEF::FrameType::E frameType, uint8_t controlByte) { if (m_status != NET_STAT_RUNNING && m_status != NET_STAT_MST_RUNNING) return false; @@ -50,7 +51,7 @@ bool PeerNetwork::writeP25LDU1(const p25::lc::LC& control, const p25::data::LowS } uint32_t messageLength = 0U; - UInt8Array message = createP25_LDU1Message_Raw(messageLength, control, lsd, data, frameType); + UInt8Array message = createP25_LDU1Message_Raw(messageLength, control, lsd, data, frameType, controlByte); if (message == nullptr) { return false; } @@ -60,7 +61,8 @@ bool PeerNetwork::writeP25LDU1(const p25::lc::LC& control, const p25::data::LowS /* Writes P25 LDU2 frame data to the network. */ -bool PeerNetwork::writeP25LDU2(const p25::lc::LC& control, const p25::data::LowSpeedData& lsd, const uint8_t* data) +bool PeerNetwork::writeP25LDU2(const p25::lc::LC& control, const p25::data::LowSpeedData& lsd, const uint8_t* data, + uint8_t controlByte) { if (m_status != NET_STAT_RUNNING && m_status != NET_STAT_MST_RUNNING) return false; @@ -72,7 +74,7 @@ bool PeerNetwork::writeP25LDU2(const p25::lc::LC& control, const p25::data::LowS } uint32_t messageLength = 0U; - UInt8Array message = createP25_LDU2Message_Raw(messageLength, control, lsd, data); + UInt8Array message = createP25_LDU2Message_Raw(messageLength, control, lsd, data, controlByte); if (message == nullptr) { return false; } @@ -198,7 +200,7 @@ bool PeerNetwork::writeConfig() ::snprintf(buffer + 8U, json.length() + 1U, "%s", json.c_str()); if (m_debug) { - Utils::dump(1U, "Network Message, Configuration", (uint8_t*)buffer, json.length() + 8U); + Utils::dump(1U, "PeerNetowrk::writeConfig(), Message, Configuration", (uint8_t*)buffer, json.length() + 8U); } return writeMaster({ NET_FUNC::RPTC, NET_SUBFUNC::NOP }, (uint8_t*)buffer, json.length() + 8U, RTP_END_OF_CALL_SEQ, m_loginStreamId); @@ -211,7 +213,7 @@ bool PeerNetwork::writeConfig() /* Creates an P25 LDU1 frame message. */ UInt8Array PeerNetwork::createP25_LDU1Message_Raw(uint32_t& length, const p25::lc::LC& control, const p25::data::LowSpeedData& lsd, - const uint8_t* data, p25::defines::FrameType::E frameType) + const uint8_t* data, P25DEF::FrameType::E frameType, uint8_t controlByte) { using namespace p25::defines; using namespace p25::dfsi::defines; @@ -223,7 +225,7 @@ UInt8Array PeerNetwork::createP25_LDU1Message_Raw(uint32_t& length, const p25::l ::memset(buffer, 0x00U, P25_LDU1_PACKET_LENGTH + PACKET_PAD); // construct P25 message header - createP25_MessageHdr(buffer, DUID::LDU1, control, lsd, frameType); + createP25_MessageHdr(buffer, DUID::LDU1, control, lsd, frameType, controlByte); // pack DFSI data uint32_t count = MSG_HDR_SIZE; @@ -277,7 +279,7 @@ UInt8Array PeerNetwork::createP25_LDU1Message_Raw(uint32_t& length, const p25::l buffer[23U] = count; if (m_debug) - Utils::dump(1U, "Network Message, P25 LDU1", buffer, (P25_LDU1_PACKET_LENGTH + PACKET_PAD)); + Utils::dump(1U, "PeerNetwork::createP25_LDU1Message_Raw(), Message, P25 LDU1", buffer, (P25_LDU1_PACKET_LENGTH + PACKET_PAD)); length = (P25_LDU1_PACKET_LENGTH + PACKET_PAD); return UInt8Array(buffer); @@ -286,7 +288,7 @@ UInt8Array PeerNetwork::createP25_LDU1Message_Raw(uint32_t& length, const p25::l /* Creates an P25 LDU2 frame message. */ UInt8Array PeerNetwork::createP25_LDU2Message_Raw(uint32_t& length, const p25::lc::LC& control, const p25::data::LowSpeedData& lsd, - const uint8_t* data) + const uint8_t* data, uint8_t controlByte) { using namespace p25::defines; using namespace p25::dfsi::defines; @@ -298,7 +300,7 @@ UInt8Array PeerNetwork::createP25_LDU2Message_Raw(uint32_t& length, const p25::l ::memset(buffer, 0x00U, P25_LDU2_PACKET_LENGTH + PACKET_PAD); // construct P25 message header - createP25_MessageHdr(buffer, DUID::LDU2, control, lsd, FrameType::DATA_UNIT); + createP25_MessageHdr(buffer, DUID::LDU2, control, lsd, FrameType::DATA_UNIT, controlByte); // pack DFSI data uint32_t count = MSG_HDR_SIZE; @@ -352,7 +354,7 @@ UInt8Array PeerNetwork::createP25_LDU2Message_Raw(uint32_t& length, const p25::l buffer[23U] = count; if (m_debug) - Utils::dump(1U, "Network Message, P25 LDU2", buffer, (P25_LDU2_PACKET_LENGTH + PACKET_PAD)); + Utils::dump(1U, "PeerNetwork::createP25_LDU2Message_Raw(), Message, P25 LDU2", buffer, (P25_LDU2_PACKET_LENGTH + PACKET_PAD)); length = (P25_LDU2_PACKET_LENGTH + PACKET_PAD); return UInt8Array(buffer); diff --git a/src/patch/network/PeerNetwork.h b/src/patch/network/PeerNetwork.h index 081856bc..53b9f666 100644 --- a/src/patch/network/PeerNetwork.h +++ b/src/patch/network/PeerNetwork.h @@ -9,7 +9,7 @@ */ /** * @defgroup patch_network Networking - * @brief Implementation for the bridge networking. + * @brief Implementation for the patch networking. * @ingroup patch * * @file PeerNetwork.h @@ -63,18 +63,21 @@ namespace network * @param[in] lsd Instance of p25::data::LowSpeedData containing low speed data. * @param[in] data Buffer containing P25 LDU1 data to send. * @param[in] frameType DVM P25 frame type. + * @param[in] controlByte DVM Network Control Byte. * @returns bool True, if message was sent, otherwise false. */ bool writeP25LDU1(const p25::lc::LC& control, const p25::data::LowSpeedData& lsd, const uint8_t* data, - p25::defines::FrameType::E frameType) override; + P25DEF::FrameType::E frameType, uint8_t controlByte = 0U) override; /** * @brief Writes P25 LDU2 frame data to the network. * @param[in] control Instance of p25::lc::LC containing link control data. * @param[in] lsd Instance of p25::data::LowSpeedData containing low speed data. * @param[in] data Buffer containing P25 LDU2 data to send. + * @param[in] controlByte DVM Network Control Byte. * @returns bool True, if message was sent, otherwise false. */ - bool writeP25LDU2(const p25::lc::LC& control, const p25::data::LowSpeedData& lsd, const uint8_t* data) override; + bool writeP25LDU2(const p25::lc::LC& control, const p25::data::LowSpeedData& lsd, const uint8_t* data, + uint8_t controlByte = 0U) override; /** * @brief Helper to send a DMR terminator with LC message. @@ -104,10 +107,11 @@ namespace network * @param[in] lsd Instance of p25::data::LowSpeedData containing low speed data. * @param[in] data Buffer containing P25 LDU1 data to send. * @param[in] frameType DVM P25 frame type. + * @param[in] controlByte DVM Network Control Byte. * @returns UInt8Array Buffer containing the built network message. */ UInt8Array createP25_LDU1Message_Raw(uint32_t& length, const p25::lc::LC& control, const p25::data::LowSpeedData& lsd, - const uint8_t* data, p25::defines::FrameType::E frameType); + const uint8_t* data, P25DEF::FrameType::E frameType, uint8_t controlByte); /** * @brief Creates an P25 LDU2 frame message. * @@ -118,10 +122,11 @@ namespace network * @param[in] control Instance of p25::lc::LC containing link control data. * @param[in] lsd Instance of p25::data::LowSpeedData containing low speed data. * @param[in] data Buffer containing P25 LDU2 data to send. + * @param[in] controlByte DVM Network Control Byte. * @returns UInt8Array Buffer containing the built network message. */ UInt8Array createP25_LDU2Message_Raw(uint32_t& length, const p25::lc::LC& control, const p25::data::LowSpeedData& lsd, - const uint8_t* data); + const uint8_t* data, uint8_t controlByte); }; } // namespace network diff --git a/src/patch/win32/project.ico b/src/patch/win32/project.ico new file mode 100644 index 0000000000000000000000000000000000000000..7557168b5740a7aa088f5368a1d4c7d4ec1c0ca5 GIT binary patch literal 25277 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 literal 0 HcmV?d00001 diff --git a/src/patch/win32/resource.h b/src/patch/win32/resource.h new file mode 100644 index 00000000..114be567 --- /dev/null +++ b/src/patch/win32/resource.h @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Digital Voice Modem - TG Patch + * GPLv2 Open Source. Use is subject to license terms. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * Copyright (C) 2025 Bryan Biedenkapp, N2PLL + * + */ + +#define IDS_APP_TITLE 102 +#define IDI_APP_ICON 103 diff --git a/src/patch/win32/resource.rc b/src/patch/win32/resource.rc new file mode 100644 index 0000000000000000000000000000000000000000..bb9ede682f1e48698bdbee3fcd3db8cc7fa8c043 GIT binary patch literal 5142 zcmdUzTW?xN5Xa}aQoqBNyf{&fvEx3pZ#c#_Dh6a9(^QINIe;%M2Fo~UEcLUu{rz`X z4u^~CkUTg#I_#d^nc2DT4FCSKWm^{P)Y`UhZ*9{Wc4J-Z+t9}RZd%=X@#}&0OWwZS z+b>`og3(}q$I4b=9Dy;gNBiEsgW{_=2F3v08@u9tunQo9b%6CVf*ms#{ae;Ic{At+ zC|>~Gwd)xwe%?pU3)Zr`$dmXU*#|fh$7OcTBG&=>W4^}hy|+Ku>q0N^j`$r+PIn2l zDw%G9F3+5|&ndy0{AP?*WX-H*C(K*%_k^!w<{idhZFiI(xhup^he122%pmBb(Cg<)t5l*p#L5FiP;|ekLYd~=dzXC z?oZb4cz$HP39KyFn97>Y*@`R4cCRKj)F_LH(<*#R+M;f1eAlDI%Vhg4?j3=pU14{k z{AW?8vS5bQL_zwNR!^CoF_Yc`uTyueXY94$#kG|4=)6mk7iegRy$9G{(g!P{l|I&; z^^|K?S3&<3DL76UT|1<&{4L?K#4;qw?=f-a*I{L*Gru5954Va4-*`}24uG;j|e z=>{lIWGP0pGREb^W0mRKg`6a=n&hmO{Tg#ove&obRl2p_5*3o-4e_B|TU+q4Z8W|d zGDO%ta1J94$)EJRJhQ&zK5`A$ICFm*kGi?nIb#*Q*K5YNwRqkGXy z612zUQ+#DMioX?~=^(K(g=c`|@kx>n@o^sOo)c!ex0^9`o8(~K;i`!89r>Ns-EUIO zQ-le&OJ1)`Ux#f&rG@{T;kVfw)#Vt8`a!%K4&m;Ycs?a^1Jpm+YvSDNo?^`V4YJ2c zhjV=9l$^r$6u(baQ-xfVYaYHb13x6kDAN=%TJYzzci~ab?gS^GH?I&gD$6!nE%K1> z`+Zo1N+&OmT-}~6^eWAi;6Sy4viTKf-XK|@lZWs+nv0U<)KXLwsY;TH{TwBF&u=l_ za-3zECGDoMgB_K-q=OQCkEqx0xLbzYDow^)I4;H7poGVCuzS0Z`In!)oO;rLYNj!F zn>2Gby$|zKp4Qc<_7Xp;jpn>bcB-qrKk>`_s;uDv5G zqm@rl+wR9nhsCjIuLkf}k-98b>|>Gyi#vgev-?FIj;aquojR69pAda@QV(!6rl(bQ z_CEDG_KoFO*W9w2bTC^>$ez_$WnUwcvgPM!y%oJmr@7XX&*^K{m($6qA6`fM9tk~o zRTn&O-=}uxU(1h++^p}-d2NwAZ*irVk2>h~{)_Tk(W^74$-lv>Stp3A#9bQIi{*ERGTa6Kxaj^!5E}KW&)2^r>hjbD$~Iny nF27UM8>r4my@4v&EaD`47f0p&ua%LD7Fk{Yt>0%qZwvnihthWs literal 0 HcmV?d00001 diff --git a/src/peered/PeerEditWnd.h b/src/peered/PeerEditWnd.h index bfac5039..1dabf287 100644 --- a/src/peered/PeerEditWnd.h +++ b/src/peered/PeerEditWnd.h @@ -22,6 +22,41 @@ #include using namespace finalcut; +// --------------------------------------------------------------------------- +// Global Functions +// --------------------------------------------------------------------------- + +/* Helper to generate a mostly random password. */ + +std::string randPasswordGen(int len) +{ + srand((unsigned int)(time(NULL))); + + const char numbers[] = "0123456789"; + const char letter[] = "abcdefghijklmnoqprstuvwyzx"; + const char LETTER[] = "ABCDEFGHIJKLMNOQPRSTUYWVZX"; + + std::string password; + int randomizer = rand() % 6; + + for (int i = 0; i < len; i++) { + if (randomizer == 1 || randomizer == 4) { + password.append(1U, numbers[rand() % 10]); + randomizer = rand() % 4; + } + else if (randomizer == 2 || randomizer == 5) { + password.append(1U, LETTER[rand() % 26]); + randomizer = rand() % 4; + } + else { + password.append(1U, letter[rand() % 26]); + randomizer = rand() % 4; + } + } + + return password; +} + // --------------------------------------------------------------------------- // Class Declaration // --------------------------------------------------------------------------- @@ -80,12 +115,19 @@ public: */ explicit PeerEditWnd(lookups::PeerId rule, FWidget* widget = nullptr) : CloseWndBase{widget} { + m_new = false; m_rule = rule; - if (m_rule.peerDefault()) { + + if (m_rule.peerDefault() || (m_rule.peerId() == 0U)) { m_new = true; } else { m_origPeerId = m_rule.peerId(); } + + if (m_new) { + std::string password = randPasswordGen(20); + m_rule.peerPassword(password); + } } private: @@ -110,6 +152,7 @@ private: FButtonGroup m_configGroup{"Configuration", this}; FCheckBox m_peerLinkEnabled{"Peer Link", &m_configGroup}; FCheckBox m_canReqKeysEnabled{"Request Keys", &m_configGroup}; + FCheckBox m_canInhibitEnabled{"Issue Inhibit", &m_configGroup}; /** * @brief Initializes the window layout. @@ -117,7 +160,7 @@ private: void initLayout() override { FDialog::setText("Peer ID"); - FDialog::setSize(FSize{60, 18}); + FDialog::setSize(FSize{65, 18}); m_enableSetButton = false; CloseWndBase::initLayout(); @@ -138,7 +181,7 @@ private: m_peerAlias.setShadow(false); m_peerAlias.addCallback("changed", [&]() { m_rule.peerAlias(m_peerAlias.getText().toString()); }); - m_saveCopy.setGeometry(FPoint(36, 2), FSize(18, 1)); + m_saveCopy.setGeometry(FPoint(41, 2), FSize(18, 1)); m_saveCopy.addCallback("toggled", [&]() { if (m_saveCopy.isChecked()) { m_incOnSave.setEnable(); @@ -149,14 +192,14 @@ private: redraw(); }); - m_incOnSave.setGeometry(FPoint(36, 3), FSize(18, 1)); + m_incOnSave.setGeometry(FPoint(41, 3), FSize(18, 1)); m_incOnSave.setDisable(); // talkgroup source { - m_sourceGroup.setGeometry(FPoint(2, 5), FSize(30, 5)); + m_sourceGroup.setGeometry(FPoint(2, 5), FSize(35, 5)); m_peerIdLabel.setGeometry(FPoint(2, 1), FSize(10, 1)); - m_peerId.setGeometry(FPoint(11, 1), FSize(17, 1)); + m_peerId.setGeometry(FPoint(11, 1), FSize(22, 1)); m_peerId.setAlignment(finalcut::Align::Right); if (!m_rule.peerDefault()) { m_peerId.setText(std::to_string(m_rule.peerId())); @@ -210,7 +253,7 @@ private: }); m_peerPasswordLabel.setGeometry(FPoint(2, 2), FSize(10, 1)); - m_peerPassword.setGeometry(FPoint(11, 2), FSize(17, 1)); + m_peerPassword.setGeometry(FPoint(11, 2), FSize(22, 1)); if (!m_rule.peerDefault()) { m_peerPassword.setText(m_rule.peerPassword()); } @@ -220,7 +263,7 @@ private: // configuration { - m_configGroup.setGeometry(FPoint(34, 5), FSize(23, 5)); + m_configGroup.setGeometry(FPoint(39, 5), FSize(23, 5)); m_peerLinkEnabled.setGeometry(FPoint(2, 1), FSize(10, 1)); m_peerLinkEnabled.setChecked(m_rule.peerLink()); @@ -233,6 +276,12 @@ private: m_canReqKeysEnabled.addCallback("toggled", [&]() { m_rule.canRequestKeys(m_canReqKeysEnabled.isChecked()); }); + + m_canInhibitEnabled.setGeometry(FPoint(2, 3), FSize(10, 1)); + m_canInhibitEnabled.setChecked(m_rule.canIssueInhibit()); + m_canInhibitEnabled.addCallback("toggled", [&]() { + m_rule.canIssueInhibit(m_canInhibitEnabled.isChecked()); + }); } CloseWndBase::initControls(); @@ -247,8 +296,9 @@ private: uint32_t peerId = m_rule.peerId(); bool peerLink = m_rule.peerLink(); bool canRequestKeys = m_rule.canRequestKeys(); + bool canIssueInhibit = m_rule.canIssueInhibit(); - ::LogInfoEx(LOG_HOST, "Peer ALIAS: %s PEERID: %u PEER LINK: %u CAN REQUEST KEYS: %u", peerAlias.c_str(), peerId, peerLink, canRequestKeys); + ::LogInfoEx(LOG_HOST, "Peer ALIAS: %s PEERID: %u PEER LINK: %u CAN REQUEST KEYS: %u CAN ISSUE INHIBIT: %u", peerAlias.c_str(), peerId, peerLink, canRequestKeys, canIssueInhibit); } /* @@ -314,7 +364,13 @@ private: if (it != peers.end()) { LogMessage(LOG_HOST, "Updating peer %s (%u) to %s (%u)", it->peerAlias().c_str(), it->peerId(), m_rule.peerAlias().c_str(), m_rule.peerId()); g_pidLookups->eraseEntry(m_origPeerId); - g_pidLookups->addEntry(m_rule.peerId(), m_rule.peerAlias(), m_rule.peerPassword(), m_rule.peerLink(), m_rule.canRequestKeys()); + + lookups::PeerId entry = lookups::PeerId(m_rule.peerId(), m_rule.peerAlias(), m_rule.peerPassword(), false); + entry.peerLink(m_rule.peerLink()); + entry.canRequestKeys(m_rule.canRequestKeys()); + entry.canIssueInhibit(m_rule.canIssueInhibit()); + + g_pidLookups->addEntry(m_rule.peerId(), entry); logRuleInfo(); } @@ -345,7 +401,13 @@ private: } else { LogMessage(LOG_HOST, "Adding Peer %s (%u)", m_rule.peerAlias().c_str(), m_rule.peerId()); } - g_pidLookups->addEntry(m_rule.peerId(), m_rule.peerAlias(), m_rule.peerPassword(), m_rule.peerLink(), m_rule.canRequestKeys()); + + lookups::PeerId entry = lookups::PeerId(m_rule.peerId(), m_rule.peerAlias(), m_rule.peerPassword(), false); + entry.peerLink(m_rule.peerLink()); + entry.canRequestKeys(m_rule.canRequestKeys()); + entry.canIssueInhibit(m_rule.canIssueInhibit()); + + g_pidLookups->addEntry(m_rule.peerId(), entry); logRuleInfo(); diff --git a/src/peered/PeerListWnd.h b/src/peered/PeerListWnd.h index 609fce20..7fa6fd14 100644 --- a/src/peered/PeerListWnd.h +++ b/src/peered/PeerListWnd.h @@ -119,11 +119,12 @@ public: bool masterPassword = (entry.peerPassword().size() == 0U); // build list view entry - const std::array columns = { + const std::array columns = { oss.str(), (masterPassword) ? "X" : "", (entry.peerLink()) ? "X" : "", (entry.canRequestKeys()) ? "X" : "", + (entry.canIssueInhibit()) ? "X" : "", entry.peerAlias() }; @@ -210,14 +211,16 @@ private: m_listView.addColumn("Peer ID", 10); m_listView.addColumn("Master Password", 16); m_listView.addColumn("Peer Link", 12); - m_listView.addColumn("Can Request Keys", 12); + m_listView.addColumn("Request Keys", 12); + m_listView.addColumn("Can Inhibit", 12); m_listView.addColumn("Alias", 40); // set right alignment for peer ID m_listView.setColumnAlignment(2, finalcut::Align::Center); m_listView.setColumnAlignment(3, finalcut::Align::Center); m_listView.setColumnAlignment(4, finalcut::Align::Center); - m_listView.setColumnAlignment(5, finalcut::Align::Left); + m_listView.setColumnAlignment(5, finalcut::Align::Center); + m_listView.setColumnAlignment(6, finalcut::Align::Left); // set type of sorting m_listView.setColumnSortType(1, finalcut::SortType::Name); diff --git a/src/remote/CMakeLists.txt b/src/remote/CMakeLists.txt index bfd1753d..4ebdd589 100644 --- a/src/remote/CMakeLists.txt +++ b/src/remote/CMakeLists.txt @@ -4,10 +4,17 @@ # * GPLv2 Open Source. Use is subject to license terms. # * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. # * -# * Copyright (C) 2024 Bryan Biedenkapp, N2PLL +# * Copyright (C) 2024,2025 Bryan Biedenkapp, N2PLL # * # */ file(GLOB dvmcmd_SRC "src/remote/*.h" "src/remote/*.cpp" ) + +# +# Windows Resource Scripts +# +file(GLOB dvmcmd_RC + "src/remote/win32/*.rc" +) diff --git a/src/remote/win32/project.ico b/src/remote/win32/project.ico new file mode 100644 index 0000000000000000000000000000000000000000..7557168b5740a7aa088f5368a1d4c7d4ec1c0ca5 GIT binary patch literal 25277 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 literal 0 HcmV?d00001 diff --git a/src/remote/win32/resource.h b/src/remote/win32/resource.h new file mode 100644 index 00000000..6dec41ff --- /dev/null +++ b/src/remote/win32/resource.h @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Digital Voice Modem - Remote Command Client + * 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 + * + */ + +#define IDS_APP_TITLE 102 +#define IDI_APP_ICON 103 diff --git a/src/remote/win32/resource.rc b/src/remote/win32/resource.rc new file mode 100644 index 0000000000000000000000000000000000000000..1f7e53e245938d633f37c79c127fb1508cea933d GIT binary patch literal 5200 zcmdUzTTfd@5XbkqQoqC2c_C3ThEP@Q8w5kF8r#CgO{GYPU~_4GK{iQ))X(1b_n-0l zu*Pl)$pg{R@$T83nVtL2_@BQw!bXUp9a>>8yb0@}9rHhcrc7dA%52;Hl2!Fv-3mvcArH zWb-kweZF3?JLEZqcF(>C#=(evo#XVKlMiqbLo-~1IbzlZ_dUEymNMVRCQk%^%GZ>= zcj0&T&Y%@}$NY+VA-h&3?GWhlo%8lMC58sS9mXBB&;<0V~=DMlEdCeqV zCfl#J_Zk}7m9Dxb|FP**7VKa(QINi+)i$$ZX3|^ab?UD54tq^_cP$k>I`5L?8FG)X z?*Q9N`WQ-RrH^&*ddelMtDyfh3XW4o*A6LYy^5r#`6TXS<37^gAh*Zj8anI~*Qcxw zSeMr<>SSutC7qlD@tx^U*p;w5g{k=ST(yp zw{z>PH&~Zf9GKnHN`mZwrzn&Uw}IPb?l?NV3THd4DoT^3l0KfcwGx++2spz>M{_Sr za)|$8HwO9V4SuOiHs(vdt?X7q4>5c&o4;o@67d7Rldyeqz zcnZZU<3_nO=gWOZ`C6W#JnQ>YzQu{CiXX`$sskS7;@jD7k!i(o4PV$HzqA;YE1UR1 zT=D$h{vuup4NTx8-2mm8EO|sLW1P=CR++w)OCLZMB~dLqrvWib6_+if70{v%=(u5$TeK$%)NO$>gImT8LQ~Md^EjDc#Cg3 zkA*C&&Ut1Y-7_yq&>oWy@s+z#{I&Q@7m1ZAJOd<;Pm*+rj}xnVj+p7*ZdmL#$ice9 zRS^|C@;j}&-=vx+hlydEyk42V4%>oC6aP8JZ#xTAmt!RA`*t@Rz}+G7+$M4()IWxo z#JSf!dCdC_vdc*a3w#!ooWkZDzfV?E?VQWC2w%Aauj*ErCdX*PpVQukM?Kp!oQU4M zLhMjkw$N(ML$UAoVGfl}ULLu+J)7uNnkm77Y6WHU3(mYkvOXt|;B~wZCCjO$sK}{G zl1li=Bznhh9&ZKCvdofp)7Zt1%3aby3BJ`aTywXKxK$dALpUy3ZBW8vy4byyW&Y)7 zFQ=X~pqgpQ-6qXk&+o(Hl!x^@s=dUIY9p_1y`N~IVdcXy&tQda{+qu%N)un()W((h z`zdnE;@%65?ABh?YmeGuh(3n+aT(owG@ZLQCrqZ~f@u(W{9&?$G=p=PkH}E#4`&G8~j`q^}$gen;QSWc7BfYd-lthTh^xe18r7i{H;S^{ zDgVRhR!I;V^{V^VeO`bZsv>e`OsBzxzhZi6qMr*qq^t`8sI Jhd*sw{|nMoe@*}Z literal 0 HcmV?d00001 diff --git a/src/sysview/HostWS.cpp b/src/sysview/HostWS.cpp index d8ea7591..7efcaeee 100644 --- a/src/sysview/HostWS.cpp +++ b/src/sysview/HostWS.cpp @@ -452,7 +452,7 @@ void HostWS::send(json::object obj) m_wsServer.send(*it, json, websocketpp::frame::opcode::text); } } - catch (websocketpp::exception) { /* stub */ } + catch (websocketpp::exception const&) { /* stub */ } } // --------------------------------------------------------------------------- diff --git a/src/sysview/SysViewMain.cpp b/src/sysview/SysViewMain.cpp index d7923c18..21a3e11c 100644 --- a/src/sysview/SysViewMain.cpp +++ b/src/sysview/SysViewMain.cpp @@ -4,11 +4,12 @@ * GPLv2 Open Source. Use is subject to license terms. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * - * Copyright (C) 2024 Bryan Biedenkapp, N2PLL + * Copyright (C) 2024,2025 Bryan Biedenkapp, N2PLL * */ #include "Defines.h" #include "common/Clock.h" +#include "common/analog/AnalogDefines.h" #include "common/dmr/DMRDefines.h" #include "common/dmr/lc/csbk/CSBKFactory.h" #include "common/dmr/lc/LC.h" @@ -93,6 +94,7 @@ typedef std::pair StatusMapPair; std::unordered_map g_dmrStatus; std::unordered_map g_p25Status; std::unordered_map g_nxdnStatus; +std::unordered_map g_analogStatus; // --------------------------------------------------------------------------- // Global Functions @@ -237,7 +239,7 @@ bool createPeerNetwork() } // initialize networking - g_network = new PeerNetwork(address, port, 0U, id, password, true, g_debug, true, true, true, true, true, true, false, true, false); + g_network = new PeerNetwork(address, port, 0U, id, password, true, g_debug, true, false, true, false); g_network->setMetadata(identity, 0U, 0U, 0.0F, 0.0F, 0, 0, 0, 0.0F, 0.0F, 0, ""); g_network->setLookups(g_ridLookup, g_tidLookup); @@ -518,8 +520,8 @@ void* threadNetworkPump(void* arg) if (std::find_if(g_p25Status.begin(), g_p25Status.end(), [&](StatusMapPair x) { return x.second.dstId == dstId; }) != g_p25Status.end()) { g_p25Status.erase(dstId); - LogMessage(LOG_NET, "P25, Call End, srcId = %u (%s), dstId = %u (%s), duration = %u", - srcId, resolveRID(srcId).c_str(), dstId, resolveTGID(dstId).c_str(), duration / 1000); + LogMessage(LOG_NET, "P25, Call End, srcId = %u (%s), dstId = %u (%s), sysId = $%03X, netId = $%05X, duration = %u", + srcId, resolveRID(srcId).c_str(), dstId, resolveTGID(dstId).c_str(), sysId, netId, duration / 1000); } } @@ -539,8 +541,8 @@ void* threadNetworkPump(void* arg) status.dstId = dstId; g_p25Status[dstId] = status; - LogMessage(LOG_NET, "P25, Call Start, srcId = %u (%s), dstId = %u (%s)", - srcId, resolveRID(srcId).c_str(), dstId, resolveTGID(dstId).c_str()); + LogMessage(LOG_NET, "P25, Call Start, srcId = %u (%s), dstId = %u (%s), sysId = $%03X, netId = $%05X", + srcId, resolveRID(srcId).c_str(), dstId, resolveTGID(dstId).c_str(), sysId, netId); } } } @@ -583,9 +585,9 @@ void* threadNetworkPump(void* arg) case P25DEF::TSBKO::IOSP_GRP_VCH: case P25DEF::TSBKO::IOSP_UU_VCH: { - LogMessage(LOG_NET, P25_TSDU_STR ", %s, emerg = %u, encrypt = %u, prio = %u, chNo = %u-%u, srcId = %u (%s), dstId = %u (%s)", + LogMessage(LOG_NET, P25_TSDU_STR ", %s, emerg = %u, encrypt = %u, prio = %u, chNo = %u-%u, srcId = %u (%s), dstId = %u (%s), sysId = $%03X, netId = $%05X", tsbk->toString(true).c_str(), tsbk->getEmergency(), tsbk->getEncrypted(), tsbk->getPriority(), tsbk->getGrpVchId(), tsbk->getGrpVchNo(), - srcId, resolveRID(srcId).c_str(), dstId, resolveTGID(dstId).c_str()); + srcId, resolveRID(srcId).c_str(), dstId, resolveTGID(dstId).c_str(), sysId, netId); // generate a net event for this if (g_netDataEvent != nullptr) { @@ -981,6 +983,58 @@ void* threadNetworkPump(void* arg) if (g_debug) LogMessage(LOG_NET, "NXDN, messageType = $%02X, srcId = %u, dstId = %u, len = %u", messageType, srcId, dstId, length); } + + UInt8Array analogBuffer = g_network->readAnalog(netReadRet, length); + if (netReadRet) { + using namespace analog; + + uint32_t srcId = GET_UINT24(analogBuffer, 5U); + uint32_t dstId = GET_UINT24(analogBuffer, 8U); + + ANODEF::AudioFrameType::E frameType = (ANODEF::AudioFrameType::E)(analogBuffer[15U] & 0x0FU); + + // specifically only check the following logic for end of call, voice or data frames + if (frameType == ANODEF::AudioFrameType::TERMINATOR) { + if (srcId == 0U && dstId == 0U) { + LogWarning(LOG_NET, "Analog, invalid TX_REL, srcId = %u (%s), dstId = %u (%s)", + srcId, resolveRID(srcId).c_str(), dstId, resolveTGID(dstId).c_str()); + } + + RxStatus status = g_analogStatus[dstId]; + uint64_t duration = hrc::diff(pktTime, status.callStartTime); + + if (std::find_if(g_analogStatus.begin(), g_analogStatus.end(), [&](StatusMapPair x) { return x.second.dstId == dstId; }) != g_analogStatus.end()) { + g_analogStatus.erase(dstId); + + LogMessage(LOG_NET, "Analog, Call End, srcId = %u (%s), dstId = %u (%s), duration = %u", + srcId, resolveRID(srcId).c_str(), dstId, resolveTGID(dstId).c_str(), duration / 1000); + } + } + + // is this a new call stream? + if (frameType == ANODEF::AudioFrameType::VOICE_START) { + if (srcId == 0U && dstId == 0U) { + LogWarning(LOG_NET, "Analog, invalid call, srcId = %u (%s), dstId = %u (%s)", + srcId, resolveRID(srcId).c_str(), dstId, resolveTGID(dstId).c_str()); + } + + auto it = std::find_if(g_analogStatus.begin(), g_analogStatus.end(), [&](StatusMapPair x) { return x.second.dstId == dstId; }); + if (it == g_analogStatus.end()) { + // this is a new call stream + RxStatus status = RxStatus(); + status.callStartTime = pktTime; + status.srcId = srcId; + status.dstId = dstId; + g_analogStatus[dstId] = status; + + LogMessage(LOG_NET, "Analog, Call Start, srcId = %u (%s), dstId = %u (%s)", + srcId, resolveRID(srcId).c_str(), dstId, resolveTGID(dstId).c_str()); + } + } + + if (g_debug) + LogMessage(LOG_NET, "Analog, frameType = $%02X, srcId = %u, dstId = %u, len = %u", frameType, srcId, dstId, length); + } } if (ms < 2U) diff --git a/src/sysview/network/PeerNetwork.cpp b/src/sysview/network/PeerNetwork.cpp index cd8f174b..5235c4a8 100644 --- a/src/sysview/network/PeerNetwork.cpp +++ b/src/sysview/network/PeerNetwork.cpp @@ -35,8 +35,8 @@ std::mutex PeerNetwork::m_peerStatusMutex; /* Initializes a new instance of the PeerNetwork class. */ PeerNetwork::PeerNetwork(const std::string& address, uint16_t port, uint16_t localPort, uint32_t peerId, const std::string& password, - bool duplex, bool debug, bool dmr, bool p25, bool nxdn, bool slot1, bool slot2, bool allowActivityTransfer, bool allowDiagnosticTransfer, bool updateLookup, bool saveLookup) : - Network(address, port, localPort, peerId, password, duplex, debug, dmr, p25, nxdn, slot1, slot2, allowActivityTransfer, allowDiagnosticTransfer, updateLookup, saveLookup), + bool duplex, bool debug, bool allowActivityTransfer, bool allowDiagnosticTransfer, bool updateLookup, bool saveLookup) : + Network(address, port, localPort, peerId, password, duplex, debug, true, true, true, true, true, true, allowActivityTransfer, allowDiagnosticTransfer, updateLookup, saveLookup), peerStatus(), m_peerLink(false), m_tgidPkt(true, "Peer-Link, TGID List"), @@ -56,7 +56,8 @@ PeerNetwork::PeerNetwork(const std::string& address, uint16_t port, uint16_t loc /* User overrideable handler that allows user code to process network packets not handled by this class. */ -void PeerNetwork::userPacketHandler(uint32_t peerId, FrameQueue::OpcodePair opcode, const uint8_t* data, uint32_t length, uint32_t streamId) +void PeerNetwork::userPacketHandler(uint32_t peerId, FrameQueue::OpcodePair opcode, const uint8_t* data, uint32_t length, uint32_t streamId, + const frame::RTPFNEHeader& fneHeader, const frame::RTPHeader& rtpHeader) { switch (opcode.first) { case NET_FUNC::TRANSFER: @@ -229,7 +230,7 @@ void PeerNetwork::userPacketHandler(uint32_t peerId, FrameQueue::OpcodePair opco break; default: - Utils::dump("unknown opcode from the master", data, length); + Utils::dump("Unknown opcode from the master", data, length); break; } } @@ -295,7 +296,7 @@ bool PeerNetwork::writeConfig() ::snprintf(buffer + 8U, json.length() + 1U, "%s", json.c_str()); if (m_debug) { - Utils::dump(1U, "Network Message, Configuration", (uint8_t*)buffer, json.length() + 8U); + Utils::dump(1U, "PeerNetwork::writeConfig(), Message, Configuration", (uint8_t*)buffer, json.length() + 8U); } return writeMaster({ NET_FUNC::RPTC, NET_SUBFUNC::NOP }, (uint8_t*)buffer, json.length() + 8U, RTP_END_OF_CALL_SEQ, m_loginStreamId); diff --git a/src/sysview/network/PeerNetwork.h b/src/sysview/network/PeerNetwork.h index 7c1aa20a..c4bb5daa 100644 --- a/src/sysview/network/PeerNetwork.h +++ b/src/sysview/network/PeerNetwork.h @@ -49,17 +49,12 @@ namespace network * @param password Network authentication password. * @param duplex Flag indicating full-duplex operation. * @param debug Flag indicating whether network debug is enabled. - * @param dmr Flag indicating whether DMR is enabled. - * @param p25 Flag indicating whether P25 is enabled. - * @param nxdn Flag indicating whether NXDN is enabled. - * @param slot1 Flag indicating whether DMR slot 1 is enabled for network traffic. - * @param slot2 Flag indicating whether DMR slot 2 is enabled for network traffic. * @param allowActivityTransfer Flag indicating that the system activity logs will be sent to the network. * @param allowDiagnosticTransfer Flag indicating that the system diagnostic logs will be sent to the network. * @param updateLookup Flag indicating that the system will accept radio ID and talkgroup ID lookups from the network. */ PeerNetwork(const std::string& address, uint16_t port, uint16_t localPort, uint32_t peerId, const std::string& password, - bool duplex, bool debug, bool dmr, bool p25, bool nxdn, bool slot1, bool slot2, bool allowActivityTransfer, bool allowDiagnosticTransfer, bool updateLookup, bool saveLookup); + bool duplex, bool debug, bool allowActivityTransfer, bool allowDiagnosticTransfer, bool updateLookup, bool saveLookup); /** * @brief Flag indicating whether or not SysView has received Peer-Link data transfers. @@ -88,9 +83,11 @@ namespace network * @param[in] data Buffer containing message to send to peer. * @param length Length of buffer. * @param streamId Stream ID. + * @param fneHeader RTP FNE Header. + * @param rtpHeader RTP Header. */ void userPacketHandler(uint32_t peerId, FrameQueue::OpcodePair opcode, const uint8_t* data = nullptr, uint32_t length = 0U, - uint32_t streamId = 0U) override; + uint32_t streamId = 0U, const frame::RTPFNEHeader& fneHeader = frame::RTPFNEHeader(), const frame::RTPHeader& rtpHeader = frame::RTPHeader()) override; /** * @brief Writes configuration to the network. diff --git a/tools/colorize-fne.sh b/tools/colorize-fne.sh index 578ea6d0..bee978b3 100755 --- a/tools/colorize-fne.sh +++ b/tools/colorize-fne.sh @@ -7,7 +7,7 @@ #* #*/ #/* -#* Copyright (C) 2022 by Bryan Biedenkapp N2PLL +#* Copyright (C) 2022-2025 by Bryan Biedenkapp N2PLL #* #* This program is free software; you can redistribute it and/or modify #* it under the terms of the GNU General Public License as published by @@ -25,10 +25,12 @@ #*/ LOG_COLOR="s#W:#\x1b[0m\x1b[1m\x1b[33m&#; s#E:#\x1b[0m\x1b[1m\x1b[31m&#; s#M:#\x1b[0m&#; s#I:#\x1b[0m&#; s#D:#\x1b[1m\x1b[34m&#; s#U:#\x1b[44m\x1b[1m\x1b[33m&#;" +DMR_COLOR="s#VOICE#\x1b[36m&#; s#TERMINATOR_WITH_LC#\x1b[0m\x1b[32m&#; s#CSBK#\x1b[0m\x1b[35m&#" P25_COLOR="s#LDU#\x1b[36m&#; s#TDU#\x1b[0m\x1b[32m&#; s#HDU#\x1b[0m\x1b[32m&#; s#TSDU#\x1b[0m\x1b[35m&#" -AFF_COLOR="s#Affiliations#\x1b[1m\x1b[36m&#;" +NXDN_COLOR="s#VCALL#\x1b[36m&#; s#TX_REL#\x1b[0m\x1b[32m&#" +AFF_COLOR="s#Affiliations#\x1b[1m\x1b[36m&#; s#Affiliation#\x1b[1m\x1b[36m&#;" RF_HIGHLIGHT="s#(RF)#\x1b[1m\x1b[34m&\x1b[0m#;" NET_HIGHLIGHT="s#(NET)#\x1b[1m\x1b[36m&\x1b[0m#;" -sed "${LOG_COLOR}; ${RF_HIGHLIGHT}; ${NET_HIGHLIGHT}; ${P25_COLOR}; ${AFF_COLOR}" \ No newline at end of file +sed "${LOG_COLOR}; ${RF_HIGHLIGHT}; ${NET_HIGHLIGHT}; ${DMR_COLOR}; ${P25_COLOR}; ${NXDN_COLOR}; ${AFF_COLOR}" \ No newline at end of file diff --git a/tools/colorize-host.sh b/tools/colorize-host.sh new file mode 100755 index 00000000..f5166876 --- /dev/null +++ b/tools/colorize-host.sh @@ -0,0 +1,36 @@ +#!/bin/bash +# SPDX-License-Identifier: GPL-2.0-only +#/** +#* Digital Voice Modem - Host Software +#* GPLv2 Open Source. Use is subject to license terms. +#* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. +#* +#*/ +#/* +#* Copyright (C) 2025 by Bryan Biedenkapp N2PLL +#* +#* This program is free software; you can redistribute it and/or modify +#* it under the terms of the GNU General Public License as published by +#* the Free Software Foundation; either version 2 of the License, or +#* (at your option) any later version. +#* +#* This program is distributed in the hope that it will be useful, +#* but WITHOUT ANY WARRANTY; without even the implied warranty of +#* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +#* GNU General Public License for more details. +#* +#* You should have received a copy of the GNU General Public License +#* along with this program; if not, write to the Free Software +#* Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. +#*/ +LOG_COLOR="s#W:#\x1b[1m\x1b[33m&#; s#E:#\x1b[1m\x1b[31m&#; s#M:#\x1b[0m&#; s#I:#\x1b[0m&#; s#D:#\x1b[1m\x1b[34m&\x1b[0m\x1b[1m#;" + +DMR_COLOR="s#VOICE#\x1b[36m&#; s#TERMINATOR_WITH_LC#\x1b[0m\x1b[32m&#; s#CSBK#\x1b[0m\x1b[35m&#" +P25_COLOR="s#LDU#\x1b[36m&#; s#TDU#\x1b[0m\x1b[32m&#; s#HDU#\x1b[0m\x1b[32m&#; s#TSDU#\x1b[0m\x1b[35m&#" +NXDN_COLOR="s#VCALL#\x1b[36m&#; s#TX_REL#\x1b[0m\x1b[32m&#" +AFF_COLOR="s#Affiliations#\x1b[1m\x1b[36m&#; s#Affiliation#\x1b[1m\x1b[36m&#;" + +RF_HIGHLIGHT="s#RF#\x1b[1m\x1b[35m&\x1b[0m#;" +NET_HIGHLIGHT="s#NET#\x1b[1m\x1b[36m&\x1b[0m#;" + +sed "${LOG_COLOR}; ${RF_HIGHLIGHT}; ${NET_HIGHLIGHT}; ${DMR_COLOR}; ${P25_COLOR}; ${NXDN_COLOR}; ${AFF_COLOR}"