diff --git a/configs/bridge-config.example.yml b/configs/bridge-config.example.yml index 1ae63673..4d57001a 100644 --- a/configs/bridge-config.example.yml +++ b/configs/bridge-config.example.yml @@ -106,7 +106,7 @@ system: # Flag indicating whether a network grant demand packet will be sent before audio. grantDemand: false - # Audio transmit mode (1 - DMR, 2 - P25, 3 - Analog). + # Audio transmit mode (1 - DMR, 2 - P25, 3 - Analog, 4 - NXDN). txMode: 1 # PCM audio gain for received (from digital network) audio frames. diff --git a/src/bridge/HostBridge.NXDN.cpp b/src/bridge/HostBridge.NXDN.cpp new file mode 100644 index 00000000..1169b874 --- /dev/null +++ b/src/bridge/HostBridge.NXDN.cpp @@ -0,0 +1,447 @@ +// 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) 2026 Bryan Biedenkapp, N2PLL + * + */ +#include "Defines.h" +#include "common/analog/AnalogDefines.h" +#include "common/analog/AnalogAudio.h" +#include "common/dmr/DMRDefines.h" +#include "common/edac/AMBEFEC.h" +#include "common/nxdn/NXDNDefines.h" +#include "common/nxdn/NXDNUtils.h" +#include "common/nxdn/Audio.h" +#include "common/nxdn/Sync.h" +#include "common/nxdn/channel/FACCH1.h" +#include "common/nxdn/channel/LICH.h" +#include "common/nxdn/channel/SACCH.h" +#include "common/nxdn/lc/RTCH.h" +#include "common/Log.h" +#include "common/Utils.h" +#include "bridge/ActivityLog.h" +#include "HostBridge.h" +#include "BridgeMain.h" + +using namespace analog; +using namespace analog::defines; +using namespace network; +using namespace network::frame; +using namespace network::udp; + +#include +#include +#include +#include + +// --------------------------------------------------------------------------- +// Public Class Members +// --------------------------------------------------------------------------- + +/* +** NXDN +*/ + +/* Helper to process NXDN network traffic. */ + +void HostBridge::processNXDNNetwork(uint8_t* buffer, uint32_t length) +{ + assert(buffer != nullptr); + using namespace nxdn; + using namespace nxdn::defines; + + if (m_txMode != TX_MODE_NXDN) { + m_network->resetNXDN(); + return; + } + + uint8_t messageType = buffer[4U]; + uint32_t srcId = GET_UINT24(buffer, 5U); + uint32_t dstId = GET_UINT24(buffer, 8U); + + if (m_debug) { + LogDebug(LOG_NET, "NXDN, messageType = $%02X, srcId = %u, dstId = %u, len = %u", messageType, srcId, dstId, length); + } + + if (m_audioDetect || m_trafficFromUDP) + return; + + if (srcId == 0U) { + m_network->resetNXDN(); + return; + } + + if (dstId != m_dstId) { + m_network->resetNXDN(); + return; + } + + uint8_t frameLength = buffer[23U]; + if (frameLength == 0U) { + m_network->resetNXDN(); + return; + } + + uint32_t payloadLength = frameLength; + if (payloadLength > NXDN_FRAME_LENGTH_BYTES && payloadLength >= 24U) + payloadLength -= 24U; + + if (payloadLength < NXDN_FRAME_LENGTH_BYTES) { + m_network->resetNXDN(); + return; + } + + uint8_t frame[NXDN_FRAME_LENGTH_BYTES + 2U]; + ::memset(frame, 0x00U, NXDN_FRAME_LENGTH_BYTES + 2U); + ::memcpy(frame + 2U, buffer + 24U, NXDN_FRAME_LENGTH_BYTES); + + NXDNUtils::scrambler(frame + 2U); + + channel::LICH lich; + if (!lich.decode(frame + 2U)) { + m_network->resetNXDN(); + return; + } + + FuncChannelType::E fct = lich.getFCT(); + + // process non-superframe control signalling (VCALL/TX_REL) from FACCH + if (fct == FuncChannelType::USC_SACCH_NS) { + channel::FACCH1 facch; + bool valid = facch.decode(frame + 2U, NXDN_FSW_LENGTH_BITS + NXDN_LICH_LENGTH_BITS + NXDN_SACCH_FEC_LENGTH_BITS); + if (!valid) { + valid = facch.decode(frame + 2U, NXDN_FSW_LENGTH_BITS + NXDN_LICH_LENGTH_BITS + NXDN_SACCH_FEC_LENGTH_BITS + NXDN_FACCH1_FEC_LENGTH_BITS); + } + + if (valid) { + uint8_t lcBuffer[10U]; + ::memset(lcBuffer, 0x00U, 10U); + facch.getData(lcBuffer); + + lc::RTCH lc; + lc.decode(lcBuffer, NXDN_FACCH1_FEC_LENGTH_BITS); + + if (lc.getMessageType() == MessageType::RTCH_VCALL) { + m_rxNXDNLC = lc; + + m_networkWatchdog.start(); + + if (m_network->getNXDNStreamId() != m_rxStreamId && !m_callInProgress) { + 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; + + LogInfoEx(LOG_HOST, "NXDN, call start, srcId = %u, dstId = %u", srcId, dstId); + if (m_preambleLeaderTone) + generatePreambleTone(); + } + } + else if (lc.getMessageType() == MessageType::RTCH_TX_REL || lc.getMessageType() == MessageType::RTCH_TX_REL_EX) { + m_callInProgress = false; + m_networkWatchdog.stop(); + m_ignoreCall = false; + m_callAlgoId = 0U; + + if (m_rxStartTime > 0U) { + uint64_t now = std::chrono::duration_cast(std::chrono::system_clock::now().time_since_epoch()).count(); + uint64_t diff = now - m_rxStartTime; + + LogInfoEx(LOG_HOST, "NXDN, call end, srcId = %u, dstId = %u, dur = %us", srcId, dstId, diff / 1000U); + } + + m_rxNXDNLC = lc::RTCH(); + m_rxStartTime = 0U; + m_rxStreamId = 0U; + + if (!m_udpRTPContinuousSeq) { + m_rtpInitialFrame = false; + m_rtpSeqNo = 0U; + } + m_rtpTimestamp = INVALID_TS; + m_network->resetNXDN(); + return; + } + } + } + + if (m_ignoreCall) + return; + + LogInfoEx(LOG_NET, "NXDN, " NXDN_RTCH_MSG_TYPE_VCALL ", audio, srcId = %u, dstId = %u", srcId, dstId); + decodeNXDNAudioFrame(frame, srcId, dstId, m_nxdnSeqNo); + + m_nxdnSeqNo++; + m_rxStreamId = m_network->getNXDNStreamId(); +} + +/* Helper to decode NXDN network traffic audio frames. */ + +void HostBridge::decodeNXDNAudioFrame(uint8_t* frame, uint32_t srcId, uint32_t dstId, uint8_t nxdnN) +{ + assert(frame != nullptr); + using namespace nxdn; + using namespace nxdn::defines; + + channel::LICH lich; + if (!lich.decode(frame + 2U)) + return; + + ChOption::E option = lich.getOption(); + + nxdn::Audio nxdnAudio; + ::edac::AMBEFEC ambeFec; + + // ahhh yes fantastical C++ lambda functions... + auto decodePair = [&](uint32_t pairOffset, uint8_t vcBase) { + uint8_t nxdnAMBE[18U]; + ::memset(nxdnAMBE, 0x00U, 18U); + ::memcpy(nxdnAMBE, frame + 2U + NXDN_FSW_LICH_SACCH_LENGTH_BYTES + pairOffset, 18U); + + ambeFec.regenerateNXDN(nxdnAMBE + 0U); + ambeFec.regenerateNXDN(nxdnAMBE + 9U); + + uint8_t packedBits[13U]; + ::memset(packedBits, 0x00U, 13U); + nxdnAudio.decode(nxdnAMBE, packedBits); + + for (uint8_t half = 0U; half < 2U; half++) { + uint8_t rawBits[72U]; + ::memset(rawBits, 0x00U, 72U); + + for (uint32_t b = 0U; b < 49U; b++) { + rawBits[b] = READ_BIT(packedBits, (half * 49U) + b) ? 1U : 0U; + } + + // HACK: use the DMR AMBE handling to decode NXDN audio + uint8_t dmrAMBE[dmr::defines::DMR_AMBE_LENGTH_BYTES]; + ::memset(dmrAMBE, 0x00U, dmr::defines::DMR_AMBE_LENGTH_BYTES); + m_encoder->encodeBits(rawBits, dmrAMBE); + + short samples[AUDIO_SAMPLES_LENGTH]; + int errs = 0; +#if defined(_WIN32) + if (m_useExternalVocoder) { + ambeDecode(dmrAMBE, dmr::defines::DMR_AMBE_LENGTH_BYTES, samples); + } + else { +#endif // defined(_WIN32) + errs = m_decoder->decode(dmrAMBE, samples); +#if defined(_WIN32) + } +#endif // defined(_WIN32) + + if (m_debug) { + LogDebug(LOG_HOST, "NXDN, Frame, VC%u.%u, srcId = %u, dstId = %u, errs = %u", nxdnN, vcBase + half, srcId, dstId, errs); + } + + AnalogAudio::gain(samples, AUDIO_SAMPLES_LENGTH, m_rxAudioGain); + + if (m_localAudio) { + m_outputAudio.addData(samples, AUDIO_SAMPLES_LENGTH); + assertRtsPtt(); + } + + if (m_udpAudio) { + int pcmIdx = 0; + uint8_t pcm[AUDIO_SAMPLES_LENGTH * 2U]; + if (m_udpUseULaw) { + for (uint32_t smpIdx = 0; smpIdx < AUDIO_SAMPLES_LENGTH; smpIdx++) { + pcm[smpIdx] = AnalogAudio::encodeMuLaw(samples[smpIdx]); + } + + if (m_trace) + Utils::dump(1U, "HostBridge()::decodeNXDNAudioFrame(), Encoded uLaw Audio", pcm, AUDIO_SAMPLES_LENGTH); + + writeUDPAudio(srcId, dstId, pcm, AUDIO_SAMPLES_LENGTH_BYTES / 2U); + } + else { + for (uint32_t smpIdx = 0; smpIdx < AUDIO_SAMPLES_LENGTH; smpIdx++) { + pcm[pcmIdx + 0] = (uint8_t)(samples[smpIdx] & 0xFF); + pcm[pcmIdx + 1] = (uint8_t)((samples[smpIdx] >> 8) & 0xFF); + pcmIdx += 2; + } + + writeUDPAudio(srcId, dstId, pcm, AUDIO_SAMPLES_LENGTH_BYTES); + } + } + } + }; + + switch (option) { + case ChOption::STEAL_NONE: + decodePair(0U, 0U); + decodePair(18U, 2U); + break; + case ChOption::STEAL_FACCH1_1: + decodePair(18U, 2U); + break; + case ChOption::STEAL_FACCH1_2: + decodePair(0U, 0U); + break; + case ChOption::STEAL_FACCH: + default: + break; + } +} + +/* Helper to encode NXDN network traffic audio frames. */ + +void HostBridge::encodeNXDNAudioFrame(uint8_t* pcm, uint32_t forcedSrcId, uint32_t forcedDstId) +{ + assert(pcm != nullptr); + using namespace nxdn; + using namespace nxdn::defines; + + 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; + + if (srcId == 0U) + srcId = m_srcId; + + 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++; + } + + AnalogAudio::gain(samples, AUDIO_SAMPLES_LENGTH, m_txAudioGain); + + // HACK: use the DMR AMBE handling to encode NXDN audio + uint8_t dmrAMBE[dmr::defines::DMR_AMBE_LENGTH_BYTES]; + ::memset(dmrAMBE, 0x00U, dmr::defines::DMR_AMBE_LENGTH_BYTES); +#if defined(_WIN32) + if (m_useExternalVocoder) { + ambeEncode(samples, AUDIO_SAMPLES_LENGTH, dmrAMBE); + } + else { +#endif // defined(_WIN32) + m_encoder->encode(samples, dmrAMBE); +#if defined(_WIN32) + } +#endif // defined(_WIN32) + + if (m_nxdnN >= 4U) { + m_nxdnN = 0U; + } + + ::memcpy(m_nxdnAMBE + (m_nxdnN * dmr::defines::DMR_AMBE_LENGTH_BYTES), dmrAMBE, dmr::defines::DMR_AMBE_LENGTH_BYTES); + m_nxdnN++; + + if (m_nxdnN < 4U) { + return; + } + + nxdn::Audio nxdnAudio; + + uint8_t nxdnAudioPayload[36U]; + ::memset(nxdnAudioPayload, 0x00U, 36U); + + for (uint8_t pair = 0U; pair < 2U; pair++) { + uint8_t packedBits[13U]; + ::memset(packedBits, 0x00U, 13U); + + for (uint8_t half = 0U; half < 2U; half++) { + char mbeBits[49U]; + ::memset(mbeBits, 0x00U, 49U); + + uint8_t* ambe = m_nxdnAMBE + ((pair * 2U + half) * dmr::defines::DMR_AMBE_LENGTH_BYTES); + m_decoder->decodeBits(ambe, mbeBits); + + for (uint32_t b = 0U; b < 49U; b++) { + WRITE_BIT(packedBits, (half * 49U) + b, mbeBits[b] != 0); + } + } + + uint8_t encodedPair[18U]; + ::memset(encodedPair, 0x00U, 18U); + nxdnAudio.encode(packedBits, encodedPair); + ::memcpy(nxdnAudioPayload + (pair * 18U), encodedPair, 18U); + } + + lc::RTCH lc = lc::RTCH(); + lc.setMessageType(MessageType::RTCH_VCALL); + lc.setCallType(CallType::UNSPECIFIED); + lc.setGroup(true); + lc.setSrcId((uint16_t)srcId); + lc.setDstId((uint16_t)dstId); + lc.setTransmissionMode(TransmissionMode::MODE_4800); + + if (m_nxdnSeqNo == 0U) { + uint8_t controlFrame[NXDN_FRAME_LENGTH_BYTES + 2U]; + ::memset(controlFrame, 0x00U, NXDN_FRAME_LENGTH_BYTES + 2U); + + Sync::addNXDNSync(controlFrame + 2U); + + channel::LICH lich; + lich.setRFCT(RFChannelType::RDCH); + lich.setFCT(FuncChannelType::USC_SACCH_NS); + lich.setOption(ChOption::STEAL_FACCH); + lich.setOutbound(true); + lich.encode(controlFrame + 2U); + + channel::SACCH sacch; + sacch.setData(SACCH_IDLE); + sacch.setRAN(0U); + sacch.setStructure(ChStructure::SR_SINGLE); + sacch.encode(controlFrame + 2U); + + channel::FACCH1 facch; + uint8_t lcData[NXDN_RTCH_LC_LENGTH_BYTES]; + ::memset(lcData, 0x00U, NXDN_RTCH_LC_LENGTH_BYTES); + lc.encode(lcData, NXDN_RTCH_LC_LENGTH_BITS); + facch.setData(lcData); + facch.encode(controlFrame + 2U, NXDN_FSW_LENGTH_BITS + NXDN_LICH_LENGTH_BITS + NXDN_SACCH_FEC_LENGTH_BITS); + facch.encode(controlFrame + 2U, NXDN_FSW_LENGTH_BITS + NXDN_LICH_LENGTH_BITS + NXDN_SACCH_FEC_LENGTH_BITS + NXDN_FACCH1_FEC_LENGTH_BITS); + + NXDNUtils::scrambler(controlFrame + 2U); + + LogInfoEx(LOG_HOST, "NXDN, " NXDN_RTCH_MSG_TYPE_VCALL ", srcId = %u, dstId = %u", srcId, dstId); + m_network->writeNXDN(lc, controlFrame + 2U, NXDN_FRAME_LENGTH_BYTES); + m_txStreamId = m_network->getNXDNStreamId(); + } + + uint8_t voiceFrame[NXDN_FRAME_LENGTH_BYTES + 2U]; + ::memset(voiceFrame, 0x00U, NXDN_FRAME_LENGTH_BYTES + 2U); + + Sync::addNXDNSync(voiceFrame + 2U); + + channel::LICH lich; + lich.setRFCT(RFChannelType::RDCH); + lich.setFCT(FuncChannelType::USC_SACCH_SS); + lich.setOption(ChOption::STEAL_NONE); + lich.setOutbound(true); + lich.encode(voiceFrame + 2U); + + channel::SACCH sacch; + sacch.setData(SACCH_IDLE); + sacch.setRAN(0U); + sacch.setStructure(ChStructure::SR_SINGLE); + sacch.encode(voiceFrame + 2U); + + ::memcpy(voiceFrame + 2U + NXDN_FSW_LICH_SACCH_LENGTH_BYTES, nxdnAudioPayload, 36U); + + NXDNUtils::scrambler(voiceFrame + 2U); + + LogInfoEx(LOG_HOST, "NXDN, " NXDN_RTCH_MSG_TYPE_VCALL ", audio, srcId = %u, dstId = %u", srcId, dstId); + m_network->writeNXDN(lc, voiceFrame + 2U, NXDN_FRAME_LENGTH_BYTES); + m_txStreamId = m_network->getNXDNStreamId(); + + m_nxdnSeqNo++; + m_nxdnN = 0U; + ::memset(m_nxdnAMBE, 0x00U, 36U); +} diff --git a/src/bridge/HostBridge.cpp b/src/bridge/HostBridge.cpp index ec5f6e1a..afb4ca73 100644 --- a/src/bridge/HostBridge.cpp +++ b/src/bridge/HostBridge.cpp @@ -16,6 +16,7 @@ #include "common/network/udp/Socket.h" #include "common/dmr/DMRDefines.h" #include "common/dmr/data/EMB.h" +#include "common/nxdn/NXDNDefines.h" #include "common/p25/P25Defines.h" #include "common/p25/data/LowSpeedData.h" #include "common/p25/dfsi/DFSIDefines.h" @@ -231,6 +232,10 @@ HostBridge::HostBridge(const std::string& confFile) : m_netLDU2(nullptr), m_p25SeqNo(0U), m_p25N(0U), + m_rxNXDNLC(), + m_nxdnAMBE(nullptr), + m_nxdnSeqNo(0U), + m_nxdnN(0U), m_analogN(0U), m_rtsPttEnable(false), m_rtsPttPort(), @@ -287,6 +292,9 @@ HostBridge::HostBridge(const std::string& confFile) : m_ambeBuffer = new uint8_t[27U]; ::memset(m_ambeBuffer, 0x00U, 27U); + m_nxdnAMBE = new uint8_t[36U]; + ::memset(m_nxdnAMBE, 0x00U, 36U); + m_netLDU1 = new uint8_t[9U * 25U]; m_netLDU2 = new uint8_t[9U * 25U]; @@ -317,6 +325,7 @@ HostBridge::~HostBridge() } delete[] m_ambeBuffer; + delete[] m_nxdnAMBE; delete[] m_netLDU1; delete[] m_netLDU2; delete m_p25Crypto; @@ -491,7 +500,7 @@ int HostBridge::run() mdc_decoder_set_callback(m_mdcDecoder, mdcPacketDetected, this); // initialize vocoders - if (m_txMode == TX_MODE_DMR) { + if (m_txMode == TX_MODE_DMR || m_txMode == TX_MODE_NXDN) { // initialize DMR vocoders m_decoder = new vocoder::MBEDecoder(vocoder::DECODE_DMR_AMBE); m_encoder = new vocoder::MBEEncoder(vocoder::ENCODE_DMR_AMBE); @@ -548,6 +557,11 @@ int HostBridge::run() uint32_t peerId, uint32_t ssrc, uint32_t streamId) { processInCallCtrl(command, dstId, 0U); }); } + if (m_txMode == TX_MODE_NXDN) { + m_network->setNXDNICCCallback([=](network::NET_ICC::ENUM command, uint32_t dstId, + uint32_t peerId, uint32_t ssrc, uint32_t streamId) { processInCallCtrl(command, dstId, 0U); }); + } + if (m_txMode == TX_MODE_ANALOG) { m_network->setAnalogICCCallback([=](network::NET_ICC::ENUM command, uint32_t dstId, uint32_t peerId, uint32_t ssrc, uint32_t streamId) { processInCallCtrl(command, dstId, 0U); }); @@ -669,6 +683,8 @@ int HostBridge::run() m_dmrN = 0U; m_p25SeqNo = 0U; m_p25N = 0U; + m_nxdnSeqNo = 0U; + m_nxdnN = 0U; m_analogN = 0U; if (!m_udpRTPContinuousSeq) { @@ -685,6 +701,8 @@ int HostBridge::run() m_network->resetP25(); + m_network->resetNXDN(); + m_network->resetAnalog(); } } @@ -825,7 +843,7 @@ int HostBridge::ambeDecode(const uint8_t* codeword, uint32_t codewordLength, sho ::memcpy(cw.get(), codeword, codewordLength); // is this a DMR codeword? - if (codewordLength > m_frameLengthInBytes && m_txMode == TX_MODE_DMR && + if (codewordLength > m_frameLengthInBytes && (m_txMode == TX_MODE_DMR || m_txMode == TX_MODE_NXDN) && codewordLength == 9) { // use the vocoder to retrieve the un-ECC'ed and uninterleaved AMBE bits @@ -950,7 +968,7 @@ void HostBridge::ambeEncode(const short* samples, uint32_t sampleLength, uint8_t 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) { + if (m_txMode == TX_MODE_DMR || m_txMode == TX_MODE_NXDN) { UInt8Array bits = std::make_unique(49); for (int i = 0; i < 49; i++) bits[i] = (uint8_t)codewordBits[i]; @@ -1002,8 +1020,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_ANALOG) - m_txMode = TX_MODE_ANALOG; + if (m_txMode > TX_MODE_NXDN) + m_txMode = TX_MODE_NXDN; m_voxSampleLevel = systemConf["voxSampleLevel"].as(30.0f); m_dropTimeMS = (uint16_t)systemConf["dropTimeMs"].as(180U); @@ -1022,6 +1040,8 @@ bool HostBridge::readParams() } } break; + case TX_MODE_NXDN: + break; case TX_MODE_ANALOG: break; } @@ -1105,6 +1125,13 @@ bool HostBridge::readParams() m_tekKeyId = 0U; } + // ensure encryption is currently disabled for NXDN (its not supported) + if (m_txMode == TX_MODE_NXDN && m_tekAlgoId != P25DEF::ALGO_UNENCRYPT && m_tekKeyId > 0U) { + ::LogError(LOG_HOST, "Encryption is not supported for NXDN. Disabling."); + 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."); @@ -1126,6 +1153,8 @@ bool HostBridge::readParams() std::string txModeStr = "DMR"; if (m_txMode == TX_MODE_P25) txModeStr = "P25"; + if (m_txMode == TX_MODE_NXDN) + txModeStr = "NXDN"; if (m_txMode == TX_MODE_ANALOG) txModeStr = "Analog"; @@ -1232,6 +1261,7 @@ bool HostBridge::createNetwork() } break; case TX_MODE_P25: + case TX_MODE_NXDN: case TX_MODE_ANALOG: { if (m_dstId > 65535) { @@ -1311,7 +1341,7 @@ bool HostBridge::createNetwork() LogInfo(" Debug: yes"); } - bool dmr = false, p25 = false, analog = false; + bool dmr = false, p25 = false, nxdnMode = false, analog = false; switch (m_txMode) { case TX_MODE_DMR: dmr = true; @@ -1319,13 +1349,16 @@ bool HostBridge::createNetwork() case TX_MODE_P25: p25 = true; break; + case TX_MODE_NXDN: + nxdnMode = 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, analog, true, true, true, allowDiagnosticTransfer, true, false); + m_network = new PeerNetwork(address, port, local, id, password, true, debug, dmr, p25, nxdnMode, analog, true, true, true, allowDiagnosticTransfer, true, false); m_network->setPacketDump(packetDump); m_network->setMetadata(m_identity, 0U, 0U, 0.0F, 0.0F, 0, 0, 0, 0.0F, 0.0F, 0, ""); @@ -1761,6 +1794,23 @@ void HostBridge::callEnd(uint32_t srcId, uint32_t dstId) m_network->writeP25TDU(lc, lsd, controlByte); } break; + case TX_MODE_NXDN: + { + ::nxdn::lc::RTCH lc = ::nxdn::lc::RTCH(); + lc.setMessageType(NXDDEF::MessageType::RTCH_TX_REL); + lc.setCallType(NXDDEF::CallType::UNSPECIFIED); + lc.setGroup(true); + lc.setSrcId((uint16_t)srcId); + lc.setDstId((uint16_t)dstId); + lc.setTransmissionMode(NXDDEF::TransmissionMode::MODE_4800); + + uint8_t data[NXDDEF::NXDN_FRAME_LENGTH_BYTES]; + ::memset(data, 0x00U, NXDDEF::NXDN_FRAME_LENGTH_BYTES); + + LogInfoEx(LOG_HOST, "NXDN, " NXDN_RTCH_MSG_TYPE_TX_REL ", srcId = %u, dstId = %u", srcId, dstId); + m_network->writeNXDN(lc, data, NXDDEF::NXDN_FRAME_LENGTH_BYTES, true); + } + break; case TX_MODE_ANALOG: { LogInfoEx(LOG_HOST, ANO_TERMINATOR); @@ -1802,6 +1852,8 @@ void HostBridge::callEnd(uint32_t srcId, uint32_t dstId) m_dmrN = 0U; m_p25SeqNo = 0U; m_p25N = 0U; + m_nxdnSeqNo = 0U; + m_nxdnN = 0U; m_analogN = 0U; if (!m_udpRTPContinuousSeq) { @@ -1815,6 +1867,7 @@ void HostBridge::callEnd(uint32_t srcId, uint32_t dstId) m_network->resetDMR(m_slot); m_network->resetP25(); + m_network->resetNXDN(); m_network->resetAnalog(); } @@ -2011,6 +2064,9 @@ void* HostBridge::threadAudioProcess(void* arg) case TX_MODE_P25: bridge->encodeP25AudioFrame(pcm); break; + case TX_MODE_NXDN: + bridge->encodeNXDNAudioFrame(pcm); + break; case TX_MODE_ANALOG: bridge->encodeAnalogAudioFrame(pcm); break; @@ -2036,6 +2092,9 @@ void* HostBridge::threadAudioProcess(void* arg) case TX_MODE_P25: bridge->encodeP25AudioFrame(pcm); break; + case TX_MODE_NXDN: + bridge->encodeNXDNAudioFrame(pcm); + break; case TX_MODE_ANALOG: bridge->encodeAnalogAudioFrame(pcm); break; @@ -2469,6 +2528,9 @@ void* HostBridge::threadUDPAudioProcess(void* arg) case TX_MODE_P25: bridge->encodeP25AudioFrame(pcm, bridge->m_udpSrcId); break; + case TX_MODE_NXDN: + bridge->encodeNXDNAudioFrame(pcm, bridge->m_udpSrcId); + break; case TX_MODE_ANALOG: bridge->encodeAnalogAudioFrame(pcm, bridge->m_udpSrcId); break; @@ -2566,6 +2628,21 @@ void* HostBridge::threadNetworkProcess(void* arg) } } + // is the bridge in NXDN mode? + if (bridge->m_txMode == TX_MODE_NXDN) { + UInt8Array nxdnBuffer = nullptr; + + // scope is intentional to limit lock duration + { + std::lock_guard lock(HostBridge::s_networkMutex); + nxdnBuffer = bridge->m_network->readNXDN(netReadRet, length); + } + + if (netReadRet && nxdnBuffer != nullptr) { + bridge->processNXDNNetwork(nxdnBuffer.get(), length); + } + } + // is the bridge in analog mode? if (bridge->m_txMode == TX_MODE_ANALOG) { UInt8Array analogBuffer = nullptr; @@ -2836,6 +2913,34 @@ void HostBridge::padSilenceAudio(uint32_t srcId, uint32_t dstId) } } break; + case TX_MODE_NXDN: + { + using namespace nxdn; + using namespace nxdn::defines; + + if (m_nxdnN == 0U || m_nxdnN > 3U) { + ::memset(m_nxdnAMBE, 0x00U, 36U); + + ::memcpy(m_nxdnAMBE + 0U, NULL_AMBE, RAW_AMBE_LENGTH_BYTES); + ::memcpy(m_nxdnAMBE + 9U, NULL_AMBE, RAW_AMBE_LENGTH_BYTES); + ::memcpy(m_nxdnAMBE + 18U, NULL_AMBE, RAW_AMBE_LENGTH_BYTES); + ::memcpy(m_nxdnAMBE + 27U, NULL_AMBE, RAW_AMBE_LENGTH_BYTES); + + ::nxdn::lc::RTCH lc = ::nxdn::lc::RTCH(); + lc.setMessageType(NXDDEF::MessageType::RTCH_VCALL); + lc.setCallType(NXDDEF::CallType::UNSPECIFIED); + lc.setGroup(true); + lc.setSrcId((uint16_t)srcId); + lc.setDstId((uint16_t)dstId); + lc.setTransmissionMode(NXDDEF::TransmissionMode::MODE_4800); + + LogInfoEx(LOG_HOST, "NXDN, " NXDN_RTCH_MSG_TYPE_VCALL ", audio (silence), srcId = %u, dstId = %u", srcId, dstId); + + m_network->writeNXDN(lc, m_nxdnAMBE, 36U); + m_nxdnN = 1U; + } + } + break; } } diff --git a/src/bridge/HostBridge.h b/src/bridge/HostBridge.h index a0b52032..7a73ad6a 100644 --- a/src/bridge/HostBridge.h +++ b/src/bridge/HostBridge.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-2025 Bryan Biedenkapp, N2PLL + * Copyright (C) 2024-2026 Bryan Biedenkapp, N2PLL * */ /** @@ -21,6 +21,7 @@ #include "common/dmr/data/EmbeddedData.h" #include "common/dmr/lc/LC.h" #include "common/dmr/lc/PrivacyLC.h" +#include "common/nxdn/lc/RTCH.h" #include "common/p25/Crypto.h" #include "common/network/udp/Socket.h" #include "common/network/RTPHeader.h" @@ -67,6 +68,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; +const uint8_t TX_MODE_NXDN = 4U; // --------------------------------------------------------------------------- @@ -254,6 +256,15 @@ private: uint32_t m_p25SeqNo; uint8_t m_p25N; + /* + ** NXDN + */ + + nxdn::lc::RTCH m_rxNXDNLC; + uint8_t* m_nxdnAMBE; + uint32_t m_nxdnSeqNo; + uint8_t m_nxdnN; + /* ** Analog */ @@ -616,6 +627,29 @@ private: */ void encodeP25AudioFrame(uint8_t* pcm, uint32_t forcedSrcId = 0U, uint32_t forcedDstId = 0U); + // NXDN (HostBridge.NXDN.cpp) + /** + * @brief Helper to process NXDN network traffic. + * @param buffer + * @param length + */ + void processNXDNNetwork(uint8_t* buffer, uint32_t length); + /** + * @brief Helper to decode NXDN network traffic audio frames. + * @param frame + * @param srcId + * @param dstId + * @param nxdnN + */ + void decodeNXDNAudioFrame(uint8_t* frame, uint32_t srcId, uint32_t dstId, uint8_t nxdnN); + /** + * @brief Helper to encode NXDN network traffic audio frames. + * @param pcm + * @param forcedSrcId + * @param forcedDstId + */ + void encodeNXDNAudioFrame(uint8_t* pcm, uint32_t forcedSrcId = 0U, uint32_t forcedDstId = 0U); + // Analog (HostBridge.Analog.cpp) /** * @brief Helper to process analog network traffic. diff --git a/src/host/nxdn/Audio.cpp b/src/common/nxdn/Audio.cpp similarity index 100% rename from src/host/nxdn/Audio.cpp rename to src/common/nxdn/Audio.cpp diff --git a/src/host/nxdn/Audio.h b/src/common/nxdn/Audio.h similarity index 100% rename from src/host/nxdn/Audio.h rename to src/common/nxdn/Audio.h diff --git a/src/common/nxdn/NXDNDefines.h b/src/common/nxdn/NXDNDefines.h index 255ebc67..487d7da5 100644 --- a/src/common/nxdn/NXDNDefines.h +++ b/src/common/nxdn/NXDNDefines.h @@ -115,6 +115,7 @@ namespace nxdn const uint8_t CALLSIGN_LENGTH_BYTES = 8U; const uint32_t MI_LENGTH_BYTES = 8U; + const uint32_t RAW_AMBE_LENGTH_BYTES = 9U; const uint32_t PCKT_INFO_LENGTH_BYTES = 3U; /** @} */ diff --git a/src/host/nxdn/packet/Voice.cpp b/src/host/nxdn/packet/Voice.cpp index d7affa82..8ae0a5ba 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-2025 Bryan Biedenkapp, N2PLL + * Copyright (C) 2022-2026 Bryan Biedenkapp, N2PLL * */ #include "Defines.h" @@ -205,7 +205,7 @@ bool Voice::process(FuncChannelType::E fct, ChOption::E option, uint8_t* data, u // generate the LICH channel::LICH lich; - lich.setRFCT(RFChannelType::RDCH); + lich.setRFCT(getVoiceRFChannelType()); lich.setFCT(FuncChannelType::USC_SACCH_NS); lich.setOption(ChOption::STEAL_FACCH); lich.setOutbound(!m_nxdn->m_duplex ? false : true); @@ -433,7 +433,7 @@ bool Voice::process(FuncChannelType::E fct, ChOption::E option, uint8_t* data, u // generate the LICH channel::LICH lich; - lich.setRFCT(RFChannelType::RDCH); + lich.setRFCT(getVoiceRFChannelType()); lich.setFCT(FuncChannelType::USC_SACCH_NS); lich.setOption(ChOption::STEAL_FACCH); lich.setOutbound(!m_nxdn->m_duplex ? false : true); @@ -474,7 +474,7 @@ bool Voice::process(FuncChannelType::E fct, ChOption::E option, uint8_t* data, u // regenerate the LICH channel::LICH lich; - lich.setRFCT(RFChannelType::RDCH); + lich.setRFCT(getVoiceRFChannelType()); lich.setFCT(FuncChannelType::USC_SACCH_SS); lich.setOption(option); lich.setOutbound(!m_nxdn->m_duplex ? false : true); @@ -739,7 +739,7 @@ bool Voice::processNetwork(FuncChannelType::E fct, ChOption::E option, lc::RTCH& // generate the LICH channel::LICH lich; - lich.setRFCT(RFChannelType::RDCH); + lich.setRFCT(getVoiceRFChannelType()); lich.setFCT(FuncChannelType::USC_SACCH_NS); lich.setOption(ChOption::STEAL_FACCH); lich.setOutbound(true); @@ -932,7 +932,7 @@ bool Voice::processNetwork(FuncChannelType::E fct, ChOption::E option, lc::RTCH& // generate the LICH channel::LICH lich; - lich.setRFCT(RFChannelType::RDCH); + lich.setRFCT(getVoiceRFChannelType()); lich.setFCT(FuncChannelType::USC_SACCH_NS); lich.setOption(ChOption::STEAL_FACCH); lich.setOutbound(true); @@ -969,7 +969,7 @@ bool Voice::processNetwork(FuncChannelType::E fct, ChOption::E option, lc::RTCH& // regenerate the LICH channel::LICH lich; - lich.setRFCT(RFChannelType::RDCH); + lich.setRFCT(getVoiceRFChannelType()); lich.setFCT(FuncChannelType::USC_SACCH_SS); lich.setOption(option); lich.setOutbound(true); @@ -1239,3 +1239,10 @@ bool Voice::checkNetTrafficCollision(lc::RTCH lc, uint32_t srcId, uint32_t dstId return false; } + +/* Resolve the RF channel type to use for regenerated voice traffic. */ + +RFChannelType::E Voice::getVoiceRFChannelType() const +{ + return !m_nxdn->m_authoritative ? RFChannelType::RTCH : RFChannelType::RDCH; +} diff --git a/src/host/nxdn/packet/Voice.h b/src/host/nxdn/packet/Voice.h index a92d7e20..60fbd082 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-2025 Bryan Biedenkapp, N2PLL + * Copyright (C) 2022-2026 Bryan Biedenkapp, N2PLL * */ /** @@ -128,6 +128,12 @@ namespace nxdn * @return bool True, if traffic collision, otherwise false. */ bool checkNetTrafficCollision(lc::RTCH lc, uint32_t srcId, uint32_t dstId); + + /** + * @brief Resolve the RF channel type to use for regenerated voice traffic. + * @returns RTCH for non-authoritative hosts controlled by a trunked controller, otherwise RDCH. + */ + defines::RFChannelType::E getVoiceRFChannelType() const; }; } // namespace packet } // namespace nxdn