add rudimentary experimental support for NXDN to dvmbridge; correct a few issues with trunked voice handling for NXDN, when a VC is a trunked participant the outgoing voice frames are RTCH vs RDCH;

pull/121/merge
Bryan Biedenkapp 2 weeks ago
parent 3d22282606
commit e16685fc4a

@ -106,7 +106,7 @@ system:
# Flag indicating whether a network grant demand packet will be sent before audio. # Flag indicating whether a network grant demand packet will be sent before audio.
grantDemand: false grantDemand: false
# Audio transmit mode (1 - DMR, 2 - P25, 3 - Analog). # Audio transmit mode (1 - DMR, 2 - P25, 3 - Analog, 4 - NXDN).
txMode: 1 txMode: 1
# PCM audio gain for received (from digital network) audio frames. # PCM audio gain for received (from digital network) audio frames.

@ -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 <cstdio>
#include <algorithm>
#include <functional>
#include <random>
// ---------------------------------------------------------------------------
// 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::milliseconds>(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::milliseconds>(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);
}

@ -16,6 +16,7 @@
#include "common/network/udp/Socket.h" #include "common/network/udp/Socket.h"
#include "common/dmr/DMRDefines.h" #include "common/dmr/DMRDefines.h"
#include "common/dmr/data/EMB.h" #include "common/dmr/data/EMB.h"
#include "common/nxdn/NXDNDefines.h"
#include "common/p25/P25Defines.h" #include "common/p25/P25Defines.h"
#include "common/p25/data/LowSpeedData.h" #include "common/p25/data/LowSpeedData.h"
#include "common/p25/dfsi/DFSIDefines.h" #include "common/p25/dfsi/DFSIDefines.h"
@ -231,6 +232,10 @@ HostBridge::HostBridge(const std::string& confFile) :
m_netLDU2(nullptr), m_netLDU2(nullptr),
m_p25SeqNo(0U), m_p25SeqNo(0U),
m_p25N(0U), m_p25N(0U),
m_rxNXDNLC(),
m_nxdnAMBE(nullptr),
m_nxdnSeqNo(0U),
m_nxdnN(0U),
m_analogN(0U), m_analogN(0U),
m_rtsPttEnable(false), m_rtsPttEnable(false),
m_rtsPttPort(), m_rtsPttPort(),
@ -287,6 +292,9 @@ HostBridge::HostBridge(const std::string& confFile) :
m_ambeBuffer = new uint8_t[27U]; m_ambeBuffer = new uint8_t[27U];
::memset(m_ambeBuffer, 0x00U, 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_netLDU1 = new uint8_t[9U * 25U];
m_netLDU2 = new uint8_t[9U * 25U]; m_netLDU2 = new uint8_t[9U * 25U];
@ -317,6 +325,7 @@ HostBridge::~HostBridge()
} }
delete[] m_ambeBuffer; delete[] m_ambeBuffer;
delete[] m_nxdnAMBE;
delete[] m_netLDU1; delete[] m_netLDU1;
delete[] m_netLDU2; delete[] m_netLDU2;
delete m_p25Crypto; delete m_p25Crypto;
@ -491,7 +500,7 @@ int HostBridge::run()
mdc_decoder_set_callback(m_mdcDecoder, mdcPacketDetected, this); mdc_decoder_set_callback(m_mdcDecoder, mdcPacketDetected, this);
// initialize vocoders // initialize vocoders
if (m_txMode == TX_MODE_DMR) { if (m_txMode == TX_MODE_DMR || m_txMode == TX_MODE_NXDN) {
// initialize DMR vocoders // initialize DMR vocoders
m_decoder = new vocoder::MBEDecoder(vocoder::DECODE_DMR_AMBE); m_decoder = new vocoder::MBEDecoder(vocoder::DECODE_DMR_AMBE);
m_encoder = new vocoder::MBEEncoder(vocoder::ENCODE_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); }); 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) { if (m_txMode == TX_MODE_ANALOG) {
m_network->setAnalogICCCallback([=](network::NET_ICC::ENUM command, uint32_t dstId, m_network->setAnalogICCCallback([=](network::NET_ICC::ENUM command, uint32_t dstId,
uint32_t peerId, uint32_t ssrc, uint32_t streamId) { processInCallCtrl(command, dstId, 0U); }); uint32_t peerId, uint32_t ssrc, uint32_t streamId) { processInCallCtrl(command, dstId, 0U); });
@ -669,6 +683,8 @@ int HostBridge::run()
m_dmrN = 0U; m_dmrN = 0U;
m_p25SeqNo = 0U; m_p25SeqNo = 0U;
m_p25N = 0U; m_p25N = 0U;
m_nxdnSeqNo = 0U;
m_nxdnN = 0U;
m_analogN = 0U; m_analogN = 0U;
if (!m_udpRTPContinuousSeq) { if (!m_udpRTPContinuousSeq) {
@ -685,6 +701,8 @@ int HostBridge::run()
m_network->resetP25(); m_network->resetP25();
m_network->resetNXDN();
m_network->resetAnalog(); m_network->resetAnalog();
} }
} }
@ -825,7 +843,7 @@ int HostBridge::ambeDecode(const uint8_t* codeword, uint32_t codewordLength, sho
::memcpy(cw.get(), codeword, codewordLength); ::memcpy(cw.get(), codeword, codewordLength);
// is this a DMR codeword? // 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) codewordLength == 9)
{ {
// use the vocoder to retrieve the un-ECC'ed and uninterleaved AMBE bits // 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); 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? // 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<uint8_t[]>(49); UInt8Array bits = std::make_unique<uint8_t[]>(49);
for (int i = 0; i < 49; i++) for (int i = 0; i < 49; i++)
bits[i] = (uint8_t)codewordBits[i]; bits[i] = (uint8_t)codewordBits[i];
@ -1002,8 +1020,8 @@ bool HostBridge::readParams()
m_txMode = (uint8_t)systemConf["txMode"].as<uint32_t>(1U); m_txMode = (uint8_t)systemConf["txMode"].as<uint32_t>(1U);
if (m_txMode < TX_MODE_DMR) if (m_txMode < TX_MODE_DMR)
m_txMode = TX_MODE_DMR; m_txMode = TX_MODE_DMR;
if (m_txMode > TX_MODE_ANALOG) if (m_txMode > TX_MODE_NXDN)
m_txMode = TX_MODE_ANALOG; m_txMode = TX_MODE_NXDN;
m_voxSampleLevel = systemConf["voxSampleLevel"].as<float>(30.0f); m_voxSampleLevel = systemConf["voxSampleLevel"].as<float>(30.0f);
m_dropTimeMS = (uint16_t)systemConf["dropTimeMs"].as<uint32_t>(180U); m_dropTimeMS = (uint16_t)systemConf["dropTimeMs"].as<uint32_t>(180U);
@ -1022,6 +1040,8 @@ bool HostBridge::readParams()
} }
} }
break; break;
case TX_MODE_NXDN:
break;
case TX_MODE_ANALOG: case TX_MODE_ANALOG:
break; break;
} }
@ -1105,6 +1125,13 @@ bool HostBridge::readParams()
m_tekKeyId = 0U; 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) // ensure encryption is currently disabled for analog (its not supported)
if (m_txMode == TX_MODE_ANALOG && m_tekAlgoId != P25DEF::ALGO_UNENCRYPT && m_tekKeyId > 0U) { if (m_txMode == TX_MODE_ANALOG && m_tekAlgoId != P25DEF::ALGO_UNENCRYPT && m_tekKeyId > 0U) {
::LogError(LOG_HOST, "Encryption is not supported for Analog. Disabling."); ::LogError(LOG_HOST, "Encryption is not supported for Analog. Disabling.");
@ -1126,6 +1153,8 @@ bool HostBridge::readParams()
std::string txModeStr = "DMR"; std::string txModeStr = "DMR";
if (m_txMode == TX_MODE_P25) if (m_txMode == TX_MODE_P25)
txModeStr = "P25"; txModeStr = "P25";
if (m_txMode == TX_MODE_NXDN)
txModeStr = "NXDN";
if (m_txMode == TX_MODE_ANALOG) if (m_txMode == TX_MODE_ANALOG)
txModeStr = "Analog"; txModeStr = "Analog";
@ -1232,6 +1261,7 @@ bool HostBridge::createNetwork()
} }
break; break;
case TX_MODE_P25: case TX_MODE_P25:
case TX_MODE_NXDN:
case TX_MODE_ANALOG: case TX_MODE_ANALOG:
{ {
if (m_dstId > 65535) { if (m_dstId > 65535) {
@ -1311,7 +1341,7 @@ bool HostBridge::createNetwork()
LogInfo(" Debug: yes"); LogInfo(" Debug: yes");
} }
bool dmr = false, p25 = false, analog = false; bool dmr = false, p25 = false, nxdnMode = false, analog = false;
switch (m_txMode) { switch (m_txMode) {
case TX_MODE_DMR: case TX_MODE_DMR:
dmr = true; dmr = true;
@ -1319,13 +1349,16 @@ bool HostBridge::createNetwork()
case TX_MODE_P25: case TX_MODE_P25:
p25 = true; p25 = true;
break; break;
case TX_MODE_NXDN:
nxdnMode = true;
break;
case TX_MODE_ANALOG: case TX_MODE_ANALOG:
analog = true; analog = true;
break; break;
} }
// initialize networking // 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->setPacketDump(packetDump);
m_network->setMetadata(m_identity, 0U, 0U, 0.0F, 0.0F, 0, 0, 0, 0.0F, 0.0F, 0, ""); 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); m_network->writeP25TDU(lc, lsd, controlByte);
} }
break; 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: case TX_MODE_ANALOG:
{ {
LogInfoEx(LOG_HOST, ANO_TERMINATOR); LogInfoEx(LOG_HOST, ANO_TERMINATOR);
@ -1802,6 +1852,8 @@ void HostBridge::callEnd(uint32_t srcId, uint32_t dstId)
m_dmrN = 0U; m_dmrN = 0U;
m_p25SeqNo = 0U; m_p25SeqNo = 0U;
m_p25N = 0U; m_p25N = 0U;
m_nxdnSeqNo = 0U;
m_nxdnN = 0U;
m_analogN = 0U; m_analogN = 0U;
if (!m_udpRTPContinuousSeq) { if (!m_udpRTPContinuousSeq) {
@ -1815,6 +1867,7 @@ void HostBridge::callEnd(uint32_t srcId, uint32_t dstId)
m_network->resetDMR(m_slot); m_network->resetDMR(m_slot);
m_network->resetP25(); m_network->resetP25();
m_network->resetNXDN();
m_network->resetAnalog(); m_network->resetAnalog();
} }
@ -2011,6 +2064,9 @@ void* HostBridge::threadAudioProcess(void* arg)
case TX_MODE_P25: case TX_MODE_P25:
bridge->encodeP25AudioFrame(pcm); bridge->encodeP25AudioFrame(pcm);
break; break;
case TX_MODE_NXDN:
bridge->encodeNXDNAudioFrame(pcm);
break;
case TX_MODE_ANALOG: case TX_MODE_ANALOG:
bridge->encodeAnalogAudioFrame(pcm); bridge->encodeAnalogAudioFrame(pcm);
break; break;
@ -2036,6 +2092,9 @@ void* HostBridge::threadAudioProcess(void* arg)
case TX_MODE_P25: case TX_MODE_P25:
bridge->encodeP25AudioFrame(pcm); bridge->encodeP25AudioFrame(pcm);
break; break;
case TX_MODE_NXDN:
bridge->encodeNXDNAudioFrame(pcm);
break;
case TX_MODE_ANALOG: case TX_MODE_ANALOG:
bridge->encodeAnalogAudioFrame(pcm); bridge->encodeAnalogAudioFrame(pcm);
break; break;
@ -2469,6 +2528,9 @@ void* HostBridge::threadUDPAudioProcess(void* arg)
case TX_MODE_P25: case TX_MODE_P25:
bridge->encodeP25AudioFrame(pcm, bridge->m_udpSrcId); bridge->encodeP25AudioFrame(pcm, bridge->m_udpSrcId);
break; break;
case TX_MODE_NXDN:
bridge->encodeNXDNAudioFrame(pcm, bridge->m_udpSrcId);
break;
case TX_MODE_ANALOG: case TX_MODE_ANALOG:
bridge->encodeAnalogAudioFrame(pcm, bridge->m_udpSrcId); bridge->encodeAnalogAudioFrame(pcm, bridge->m_udpSrcId);
break; 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<std::mutex> 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? // is the bridge in analog mode?
if (bridge->m_txMode == TX_MODE_ANALOG) { if (bridge->m_txMode == TX_MODE_ANALOG) {
UInt8Array analogBuffer = nullptr; UInt8Array analogBuffer = nullptr;
@ -2836,6 +2913,34 @@ void HostBridge::padSilenceAudio(uint32_t srcId, uint32_t dstId)
} }
} }
break; 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;
} }
} }

@ -4,7 +4,7 @@
* GPLv2 Open Source. Use is subject to license terms. * GPLv2 Open Source. Use is subject to license terms.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * 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/data/EmbeddedData.h"
#include "common/dmr/lc/LC.h" #include "common/dmr/lc/LC.h"
#include "common/dmr/lc/PrivacyLC.h" #include "common/dmr/lc/PrivacyLC.h"
#include "common/nxdn/lc/RTCH.h"
#include "common/p25/Crypto.h" #include "common/p25/Crypto.h"
#include "common/network/udp/Socket.h" #include "common/network/udp/Socket.h"
#include "common/network/RTPHeader.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_DMR = 1U;
const uint8_t TX_MODE_P25 = 2U; const uint8_t TX_MODE_P25 = 2U;
const uint8_t TX_MODE_ANALOG = 3U; const uint8_t TX_MODE_ANALOG = 3U;
const uint8_t TX_MODE_NXDN = 4U;
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@ -254,6 +256,15 @@ private:
uint32_t m_p25SeqNo; uint32_t m_p25SeqNo;
uint8_t m_p25N; uint8_t m_p25N;
/*
** NXDN
*/
nxdn::lc::RTCH m_rxNXDNLC;
uint8_t* m_nxdnAMBE;
uint32_t m_nxdnSeqNo;
uint8_t m_nxdnN;
/* /*
** Analog ** Analog
*/ */
@ -616,6 +627,29 @@ private:
*/ */
void encodeP25AudioFrame(uint8_t* pcm, uint32_t forcedSrcId = 0U, uint32_t forcedDstId = 0U); 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) // Analog (HostBridge.Analog.cpp)
/** /**
* @brief Helper to process analog network traffic. * @brief Helper to process analog network traffic.

@ -115,6 +115,7 @@ namespace nxdn
const uint8_t CALLSIGN_LENGTH_BYTES = 8U; const uint8_t CALLSIGN_LENGTH_BYTES = 8U;
const uint32_t MI_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; const uint32_t PCKT_INFO_LENGTH_BYTES = 3U;
/** @} */ /** @} */

@ -5,7 +5,7 @@
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
* *
* Copyright (C) 2015-2020 Jonathan Naylor, G4KLX * Copyright (C) 2015-2020 Jonathan Naylor, G4KLX
* Copyright (C) 2022-2025 Bryan Biedenkapp, N2PLL * Copyright (C) 2022-2026 Bryan Biedenkapp, N2PLL
* *
*/ */
#include "Defines.h" #include "Defines.h"
@ -205,7 +205,7 @@ bool Voice::process(FuncChannelType::E fct, ChOption::E option, uint8_t* data, u
// generate the LICH // generate the LICH
channel::LICH lich; channel::LICH lich;
lich.setRFCT(RFChannelType::RDCH); lich.setRFCT(getVoiceRFChannelType());
lich.setFCT(FuncChannelType::USC_SACCH_NS); lich.setFCT(FuncChannelType::USC_SACCH_NS);
lich.setOption(ChOption::STEAL_FACCH); lich.setOption(ChOption::STEAL_FACCH);
lich.setOutbound(!m_nxdn->m_duplex ? false : true); 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 // generate the LICH
channel::LICH lich; channel::LICH lich;
lich.setRFCT(RFChannelType::RDCH); lich.setRFCT(getVoiceRFChannelType());
lich.setFCT(FuncChannelType::USC_SACCH_NS); lich.setFCT(FuncChannelType::USC_SACCH_NS);
lich.setOption(ChOption::STEAL_FACCH); lich.setOption(ChOption::STEAL_FACCH);
lich.setOutbound(!m_nxdn->m_duplex ? false : true); 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 // regenerate the LICH
channel::LICH lich; channel::LICH lich;
lich.setRFCT(RFChannelType::RDCH); lich.setRFCT(getVoiceRFChannelType());
lich.setFCT(FuncChannelType::USC_SACCH_SS); lich.setFCT(FuncChannelType::USC_SACCH_SS);
lich.setOption(option); lich.setOption(option);
lich.setOutbound(!m_nxdn->m_duplex ? false : true); 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 // generate the LICH
channel::LICH lich; channel::LICH lich;
lich.setRFCT(RFChannelType::RDCH); lich.setRFCT(getVoiceRFChannelType());
lich.setFCT(FuncChannelType::USC_SACCH_NS); lich.setFCT(FuncChannelType::USC_SACCH_NS);
lich.setOption(ChOption::STEAL_FACCH); lich.setOption(ChOption::STEAL_FACCH);
lich.setOutbound(true); lich.setOutbound(true);
@ -932,7 +932,7 @@ bool Voice::processNetwork(FuncChannelType::E fct, ChOption::E option, lc::RTCH&
// generate the LICH // generate the LICH
channel::LICH lich; channel::LICH lich;
lich.setRFCT(RFChannelType::RDCH); lich.setRFCT(getVoiceRFChannelType());
lich.setFCT(FuncChannelType::USC_SACCH_NS); lich.setFCT(FuncChannelType::USC_SACCH_NS);
lich.setOption(ChOption::STEAL_FACCH); lich.setOption(ChOption::STEAL_FACCH);
lich.setOutbound(true); lich.setOutbound(true);
@ -969,7 +969,7 @@ bool Voice::processNetwork(FuncChannelType::E fct, ChOption::E option, lc::RTCH&
// regenerate the LICH // regenerate the LICH
channel::LICH lich; channel::LICH lich;
lich.setRFCT(RFChannelType::RDCH); lich.setRFCT(getVoiceRFChannelType());
lich.setFCT(FuncChannelType::USC_SACCH_SS); lich.setFCT(FuncChannelType::USC_SACCH_SS);
lich.setOption(option); lich.setOption(option);
lich.setOutbound(true); lich.setOutbound(true);
@ -1239,3 +1239,10 @@ bool Voice::checkNetTrafficCollision(lc::RTCH lc, uint32_t srcId, uint32_t dstId
return false; 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;
}

@ -5,7 +5,7 @@
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
* *
* Copyright (C) 2015-2020 Jonathan Naylor, G4KLX * 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. * @return bool True, if traffic collision, otherwise false.
*/ */
bool checkNetTrafficCollision(lc::RTCH lc, uint32_t srcId, uint32_t dstId); 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 packet
} // namespace nxdn } // namespace nxdn

Loading…
Cancel
Save

Powered by TurnKey Linux.