/** * 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. * * @package DVM / Host Software * */ // // Based on code from the MMDVMHost project. (https://github.com/g4klx/MMDVMHost) // Licensed under the GPLv2 License (https://opensource.org/licenses/GPL-2.0) // /* * Copyright (C) 2015-2020 by Jonathan Naylor G4KLX * Copyright (C) 2022-2023 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. */ #include "Defines.h" #include "nxdn/NXDNDefines.h" #include "nxdn/Control.h" #include "nxdn/acl/AccessControl.h" #include "nxdn/channel/SACCH.h" #include "nxdn/channel/FACCH1.h" #include "nxdn/lc/RCCH.h" #include "nxdn/lc/RTCH.h" #include "nxdn/Sync.h" #include "nxdn/NXDNUtils.h" #include "edac/AMBEFEC.h" #include "remote/RESTClient.h" #include "HostMain.h" #include "Log.h" #include "Utils.h" using namespace nxdn; using namespace nxdn::packet; #include #include #include #include // --------------------------------------------------------------------------- // Constants // --------------------------------------------------------------------------- const uint8_t MAX_SYNC_BYTES_ERRS = 0U; const uint8_t SCRAMBLER[] = { 0x00U, 0x00U, 0x00U, 0x82U, 0xA0U, 0x88U, 0x8AU, 0x00U, 0xA2U, 0xA8U, 0x82U, 0x8AU, 0x82U, 0x02U, 0x20U, 0x08U, 0x8AU, 0x20U, 0xAAU, 0xA2U, 0x82U, 0x08U, 0x22U, 0x8AU, 0xAAU, 0x08U, 0x28U, 0x88U, 0x28U, 0x28U, 0x00U, 0x0AU, 0x02U, 0x82U, 0x20U, 0x28U, 0x82U, 0x2AU, 0xAAU, 0x20U, 0x22U, 0x80U, 0xA8U, 0x8AU, 0x08U, 0xA0U, 0xAAU, 0x02U }; // --------------------------------------------------------------------------- // Public Class Members // --------------------------------------------------------------------------- /// /// Initializes a new instance of the Control class. /// /// Flag indicating whether or not the DVM is grant authoritative. /// NXDN Radio Access Number. /// Amount of hangtime for a NXDN call. /// Modem frame buffer queue size (bytes). /// Transmit timeout. /// Amount of time to hang on the last talkgroup mode from RF. /// Instance of the Modem class. /// Instance of the BaseNetwork class. /// Flag indicating full-duplex operation. /// Instance of the RadioIdLookup class. /// Instance of the TalkgroupRulesLookup class. /// Instance of the IdenTableLookup class. /// Instance of the RSSIInterpolator class. /// Flag indicating whether RCCH data is dumped to the log. /// Flag indicating whether P25 debug is enabled. /// Flag indicating whether P25 verbose logging is enabled. Control::Control(bool authoritative, uint32_t ran, uint32_t callHang, uint32_t queueSize, uint32_t timeout, uint32_t tgHang, modem::Modem* modem, network::Network* network, bool duplex, lookups::RadioIdLookup* ridLookup, lookups::TalkgroupRulesLookup* tidLookup, lookups::IdenTableLookup* idenTable, lookups::RSSIInterpolator* rssiMapper, bool dumpRCCHData, bool debug, bool verbose) : m_voice(nullptr), m_data(nullptr), m_authoritative(authoritative), m_supervisor(false), m_ran(ran), m_timeout(timeout), m_modem(modem), m_network(network), m_duplex(duplex), m_control(false), m_dedicatedControl(false), m_voiceOnControl(false), m_rfLastLICH(), m_rfLC(), m_netLC(), m_permittedDstId(0U), m_rfMask(0U), m_netMask(0U), m_idenTable(idenTable), m_ridLookup(ridLookup), m_tidLookup(tidLookup), m_affiliations("NXDN Affiliations", verbose), m_controlChData(), m_idenEntry(), m_txImmQueue(queueSize, "NXDN Imm Frame"), m_txQueue(queueSize, "NXDN Frame"), m_rfState(RS_RF_LISTENING), m_rfLastDstId(0U), m_netState(RS_NET_IDLE), m_netLastDstId(0U), m_ccRunning(false), m_ccPrevRunning(false), m_ccHalted(false), m_rfTimeout(1000U, timeout), m_rfTGHang(1000U, tgHang), m_netTimeout(1000U, timeout), m_networkWatchdog(1000U, 0U, 1500U), m_ccPacketInterval(1000U, 0U, 80U), m_ccFrameCnt(0U), m_ccSeq(0U), m_siteData(), m_rssiMapper(rssiMapper), m_rssi(0U), m_maxRSSI(0U), m_minRSSI(0U), m_aveRSSI(0U), m_rssiCount(0U), m_dumpRCCH(dumpRCCHData), m_verbose(verbose), m_debug(debug) { assert(ridLookup != nullptr); assert(tidLookup != nullptr); assert(idenTable != nullptr); assert(rssiMapper != nullptr); acl::AccessControl::init(m_ridLookup, m_tidLookup); m_voice = new Voice(this, network, debug, verbose); m_trunk = new Trunk(this, network, debug, verbose); m_data = new Data(this, network, debug, verbose); lc::RCCH::setVerbose(dumpRCCHData); lc::RTCH::setVerbose(dumpRCCHData); } /// /// Finalizes a instance of the Control class. /// Control::~Control() { if (m_voice != nullptr) { delete m_voice; } if (m_trunk != nullptr) { delete m_trunk; } if (m_data != nullptr) { delete m_data; } } /// /// Resets the data states for the RF interface. /// void Control::reset() { m_rfState = RS_RF_LISTENING; m_ccHalted = false; if (m_voice != nullptr) { m_voice->resetRF(); } if (m_data != nullptr) { m_data->resetRF(); } m_txQueue.clear(); m_rfMask = 0x00U; m_rfLC.reset(); m_netState = RS_NET_IDLE; m_netMask = 0x00U; m_netLC.reset(); } /// /// Helper to set NXDN configuration options. /// /// Instance of the yaml::Node class. /// Flag indicating whether the DMR has supervisory functions. /// CW callsign of this host. /// Voice Channel Number list. /// Voice Channel data map. /// Control Channel data. /// NXDN Site Code. /// NXDN System Code. /// Channel ID. /// Channel Number. /// void Control::setOptions(yaml::Node& conf, bool supervisor, const std::string cwCallsign, const std::vector voiceChNo, const std::unordered_map voiceChData, lookups::VoiceChData controlChData, uint16_t siteId, uint32_t sysId, uint8_t channelId, uint32_t channelNo, bool printOptions) { yaml::Node systemConf = conf["system"]; yaml::Node nxdnProtocol = conf["protocols"]["nxdn"]; m_supervisor = supervisor; m_trunk->m_verifyAff = nxdnProtocol["verifyAff"].as(false); m_trunk->m_verifyReg = nxdnProtocol["verifyReg"].as(false); yaml::Node control = nxdnProtocol["control"]; m_control = control["enable"].as(false); if (m_control) { m_dedicatedControl = control["dedicated"].as(false); } else { m_dedicatedControl = false; } m_voiceOnControl = nxdnProtocol["voiceOnControl"].as(false); m_voice->m_silenceThreshold = nxdnProtocol["silenceThreshold"].as(nxdn::DEFAULT_SILENCE_THRESHOLD); if (m_voice->m_silenceThreshold > MAX_NXDN_VOICE_ERRORS) { LogWarning(LOG_NXDN, "Silence threshold > %u, defaulting to %u", nxdn::MAX_NXDN_VOICE_ERRORS, nxdn::DEFAULT_SILENCE_THRESHOLD); m_voice->m_silenceThreshold = nxdn::DEFAULT_SILENCE_THRESHOLD; } // either MAX_NXDN_VOICE_ERRORS or 0 will disable the threshold logic if (m_voice->m_silenceThreshold == 0) { LogWarning(LOG_NXDN, "Silence threshold set to zero, defaulting to %u", nxdn::MAX_NXDN_VOICE_ERRORS); m_voice->m_silenceThreshold = nxdn::MAX_NXDN_VOICE_ERRORS; } bool disableCompositeFlag = nxdnProtocol["disableCompositeFlag"].as(false); uint8_t serviceClass = NXDN_SIF1_VOICE_CALL_SVC | NXDN_SIF1_DATA_CALL_SVC; if (m_control) { serviceClass |= NXDN_SIF1_GRP_REG_SVC; } if (m_voiceOnControl) { if (!disableCompositeFlag) { serviceClass |= NXDN_SIF1_COMPOSITE_CONTROL; } } // calculate the NXDN location ID uint32_t locId = NXDN_LOC_CAT_LOCAL; // DVM is currently fixed to "local" category locId = (locId << 17) + sysId; locId = (locId << 5) + (siteId & 0x1FU); m_siteData = SiteData(locId, channelId, channelNo, serviceClass, false); m_siteData.setCallsign(cwCallsign); for (uint32_t ch : voiceChNo) { m_affiliations.addRFCh(ch); } std::unordered_map chData = std::unordered_map(voiceChData); m_affiliations.setRFChData(chData); m_controlChData = controlChData; // set the grant release callback m_affiliations.setReleaseGrantCallback([=](uint32_t chNo, uint32_t dstId, uint8_t slot) { // callback REST API to clear TG permit for the granted TG on the specified voice channel if (m_authoritative && m_supervisor) { ::lookups::VoiceChData voiceChData = m_affiliations.getRFChData(chNo); if (voiceChData.isValidCh() && !voiceChData.address().empty() && voiceChData.port() > 0 && chNo != m_siteData.channelNo()) { json::object req = json::object(); int state = modem::DVM_STATE::STATE_NXDN; req["state"].set(state); dstId = 0U; // clear TG value req["dstId"].set(dstId); RESTClient::send(voiceChData.address(), voiceChData.port(), voiceChData.password(), HTTP_PUT, PUT_PERMIT_TG, req, m_debug); } else { ::LogError(LOG_NXDN, "NXDN, " NXDN_RTCH_MSG_TYPE_VCALL_RESP ", failed to clear TG permit, chNo = %u", chNo); } } }); lc::RCCH::setSiteData(m_siteData); lc::RCCH::setCallsign(cwCallsign); std::vector entries = m_idenTable->list(); for (auto entry : entries) { if (entry.channelId() == channelId) { m_idenEntry = entry; break; } } m_trunk->m_disableGrantSrcIdCheck = control["disableGrantSourceIdCheck"].as(false); if (printOptions) { LogInfo(" Silence Threshold: %u (%.1f%%)", m_voice->m_silenceThreshold, float(m_voice->m_silenceThreshold) / 12.33F); if (m_control) { LogInfo(" Voice on Control: %s", m_voiceOnControl ? "yes" : "no"); if (m_trunk->m_disableGrantSrcIdCheck) { LogInfo(" Disable Grant Source ID Check: yes"); } } LogInfo(" Verify Affiliation: %s", m_trunk->m_verifyAff ? "yes" : "no"); LogInfo(" Verify Registration: %s", m_trunk->m_verifyReg ? "yes" : "no"); } if (m_voice != nullptr) { m_voice->resetRF(); m_voice->resetNet(); } if (m_data != nullptr) { m_data->resetRF(); } } /// /// Process a data frame from the RF interface. /// /// Buffer containing data frame. /// Length of data frame. /// bool Control::processFrame(uint8_t* data, uint32_t len) { assert(data != nullptr); uint8_t type = data[0U]; bool sync = data[1U] == 0x01U; if (type == modem::TAG_LOST && m_rfState == RS_RF_AUDIO) { if (m_rssi != 0U) { ::ActivityLog("NXDN", true, "transmission lost, %.1f seconds, BER: %.1f%%, RSSI: -%u/-%u/-%u dBm", float(m_voice->m_rfFrames) / 12.5F, float(m_voice->m_rfErrs * 100U) / float(m_voice->m_rfBits), m_minRSSI, m_maxRSSI, m_aveRSSI / m_rssiCount); } else { ::ActivityLog("NXDN", true, "transmission lost, %.1f seconds, BER: %.1f%%", float(m_voice->m_rfFrames) / 12.5F, float(m_voice->m_rfErrs * 100U) / float(m_voice->m_rfBits)); } LogMessage(LOG_RF, "NXDN, " NXDN_RTCH_MSG_TYPE_TX_REL ", total frames: %d, bits: %d, undecodable LC: %d, errors: %d, BER: %.4f%%", m_voice->m_rfFrames, m_voice->m_rfBits, m_voice->m_rfUndecodableLC, m_voice->m_rfErrs, float(m_voice->m_rfErrs * 100U) / float(m_voice->m_rfBits)); m_affiliations.releaseGrant(m_rfLC.getDstId(), false); if (!m_control) { notifyCC_ReleaseGrant(m_rfLC.getDstId()); } writeEndRF(); return false; } if (type == modem::TAG_LOST && m_rfState == RS_RF_DATA) { writeEndRF(); return false; } if (type == modem::TAG_LOST) { m_rfState = RS_RF_LISTENING; m_rfMask = 0x00U; m_rfLC.reset(); return false; } // Have we got RSSI bytes on the end? if (len == (NXDN_FRAME_LENGTH_BYTES + 4U)) { uint16_t raw = 0U; raw |= (data[50U] << 8) & 0xFF00U; raw |= (data[51U] << 0) & 0x00FFU; // Convert the raw RSSI to dBm int rssi = m_rssiMapper->interpolate(raw); if (m_verbose) { LogMessage(LOG_RF, "NXDN, raw RSSI = %u, reported RSSI = %d dBm", raw, rssi); } // RSSI is always reported as positive m_rssi = (rssi >= 0) ? rssi : -rssi; if (m_rssi > m_minRSSI) m_minRSSI = m_rssi; if (m_rssi < m_maxRSSI) m_maxRSSI = m_rssi; m_aveRSSI += m_rssi; m_rssiCount++; } if (!sync && m_rfState == RS_RF_LISTENING) { uint8_t syncBytes[NXDN_FSW_BYTES_LENGTH]; ::memcpy(syncBytes, data + 2U, NXDN_FSW_BYTES_LENGTH); uint8_t errs = 0U; for (uint8_t i = 0U; i < NXDN_FSW_BYTES_LENGTH; i++) errs += Utils::countBits8(syncBytes[i] ^ NXDN_FSW_BYTES[i]); if (errs >= MAX_SYNC_BYTES_ERRS) { LogWarning(LOG_RF, "NXDN, possible sync word rejected, errs = %u, sync word = %02X %02X %02X", errs, syncBytes[0U], syncBytes[1U], syncBytes[2U]); return false; } else { LogWarning(LOG_RF, "NXDN, possible sync word, errs = %u, sync word = %02X %02X %02X", errs, syncBytes[0U], syncBytes[1U], syncBytes[2U]); sync = true; // we found a completly valid sync with no errors... } } if (sync && m_debug) { Utils::symbols("!!! *Rx NXDN", data + 2U, len - 2U); } NXDNUtils::scrambler(data + 2U); channel::LICH lich; bool valid = lich.decode(data + 2U); if (valid) m_rfLastLICH = lich; if (!valid && m_rfState == RS_RF_LISTENING) { if (m_debug) { LogDebug(LOG_RF, "NXDN, invalid LICH, rfct = %u fct = %u", lich.getRFCT(), lich.getFCT()); } return false; } uint8_t rfct = m_rfLastLICH.getRFCT(); uint8_t fct = m_rfLastLICH.getFCT(); uint8_t option = m_rfLastLICH.getOption(); if (m_debug) { LogDebug(LOG_RF, "NXDN, valid LICH, rfState = %u, netState = %u, rfct = %u, fct = %u", m_rfState, m_netState, rfct, fct); } // are we interrupting a running CC? if (m_ccRunning) { if ((fct != NXDN_LICH_CAC_INBOUND_SHORT) || (fct != NXDN_LICH_CAC_INBOUND_LONG)) { m_ccHalted = true; } } bool ret = false; if (rfct == NXDN_LICH_RFCT_RCCH) { ret = m_trunk->process(fct, option, data, len); } else if (rfct == NXDN_LICH_RFCT_RTCH || rfct == NXDN_LICH_RFCT_RDCH) { switch (fct) { case NXDN_LICH_USC_UDCH: if (!m_dedicatedControl) { ret = m_data->process(option, data, len); } else { if (m_voiceOnControl && m_affiliations.isChBusy(m_siteData.channelNo())) { ret = m_data->process(option, data, len); } } break; default: if (!m_dedicatedControl) { ret = m_voice->process(fct, option, data, len); } else { if (m_voiceOnControl && m_affiliations.isChBusy(m_siteData.channelNo())) { ret = m_voice->process(fct, option, data, len); } } break; } } return ret; } /// /// Get frame data from data ring buffer. /// /// Buffer to store frame data. /// Length of frame data retreived. uint32_t Control::getFrame(uint8_t* data) { assert(data != nullptr); if (m_txQueue.isEmpty() && m_txImmQueue.isEmpty()) return 0U; uint8_t len = 0U; // tx immediate queue takes priority if (!m_txImmQueue.isEmpty()) { m_txImmQueue.getData(&len, 1U); m_txImmQueue.getData(data, len); } else { m_txQueue.getData(&len, 1U); m_txQueue.getData(data, len); } return len; } /// /// Updates the processor by the passed number of milliseconds. /// /// void Control::clock(uint32_t ms) { if (m_network != nullptr) { processNetwork(); if (m_network->getStatus() == network::NET_STAT_RUNNING) { m_siteData.setNetActive(true); } else { m_siteData.setNetActive(false); } } // if we have control enabled; do clocking to generate a CC data stream if (m_control) { if (m_ccRunning && !m_ccPacketInterval.isRunning()) { m_ccPacketInterval.start(); } if (m_ccHalted) { if (!m_ccRunning) { m_ccHalted = false; m_ccPrevRunning = m_ccRunning; } } else { m_ccPacketInterval.clock(ms); if (!m_ccPacketInterval.isRunning()) { m_ccPacketInterval.start(); } if (m_ccPacketInterval.isRunning() && m_ccPacketInterval.hasExpired()) { if (m_ccRunning) { writeRF_ControlData(); } m_ccPacketInterval.start(); } } if (m_ccPrevRunning && !m_ccRunning) { m_txQueue.clear(); m_ccPacketInterval.stop(); m_ccPrevRunning = m_ccRunning; } } // handle timeouts and hang timers m_rfTimeout.clock(ms); m_netTimeout.clock(ms); if (m_rfTGHang.isRunning()) { m_rfTGHang.clock(ms); if (m_rfTGHang.hasExpired()) { m_rfTGHang.stop(); if (m_verbose) { LogMessage(LOG_RF, "talkgroup hang has expired, lastDstId = %u", m_rfLastDstId); } m_rfLastDstId = 0U; // reset permitted ID and clear permission state if (!m_authoritative && m_permittedDstId != 0U) { m_permittedDstId = 0U; } } } if (m_netState == RS_NET_AUDIO) { m_networkWatchdog.clock(ms); if (m_networkWatchdog.hasExpired()) { if (m_netState == RS_NET_AUDIO) { ::ActivityLog("NXDN", false, "network watchdog has expired, %.1f seconds, %u%% packet loss", float(m_voice->m_netFrames) / 50.0F, (m_voice->m_netLost * 100U) / m_voice->m_netFrames); } else { ::ActivityLog("NXDN", false, "network watchdog has expired"); } m_networkWatchdog.stop(); if (m_control) { m_affiliations.releaseGrant(m_netLC.getDstId(), false); } if (m_dedicatedControl) { if (m_network != nullptr) m_network->resetNXDN(); } m_netState = RS_NET_IDLE; m_netTimeout.stop(); writeEndNet(); } } // reset states if we're in a rejected state if (m_rfState == RS_RF_REJECTED) { m_txQueue.clear(); m_voice->resetRF(); m_voice->resetNet(); m_data->resetRF(); if (m_network != nullptr) m_network->resetNXDN(); m_rfState = RS_RF_LISTENING; } // clock data and trunking if (m_trunk != nullptr) { m_trunk->clock(ms); } } /// /// Permits a TGID on a non-authoritative host. /// /// void Control::permittedTG(uint32_t dstId) { if (m_authoritative) { return; } if (m_verbose) { LogMessage(LOG_P25, "non-authoritative TG permit, dstId = %u", dstId); } m_permittedDstId = dstId; } /// /// Releases a granted TG. /// /// void Control::releaseGrantTG(uint32_t dstId) { if (!m_control) { return; } if (m_verbose) { LogMessage(LOG_NXDN, "REST request, release TG grant, dstId = %u", dstId); } if (m_affiliations.isGranted(dstId)) { if (m_verbose) { LogMessage(LOG_NXDN, "REST request, TG grant released, dstId = %u", dstId); } m_affiliations.releaseGrant(dstId, false); } } /// /// Touchs a granted TG to keep a channel grant alive. /// /// void Control::touchGrantTG(uint32_t dstId) { if (!m_control) { return; } if (m_affiliations.isGranted(dstId)) { if (m_verbose) { LogMessage(LOG_NXDN, "REST request, touch TG grant, dstId = %u", dstId); } m_affiliations.touchGrant(dstId); } } /// /// Flag indicating whether the process or is busy or not. /// /// True, if processor is busy, otherwise false. bool Control::isBusy() const { return m_rfState != RS_RF_LISTENING || m_netState != RS_NET_IDLE; } /// /// Helper to change the debug and verbose state. /// /// Flag indicating whether NXDN debug is enabled. /// Flag indicating whether NXDN verbose logging is enabled. void Control::setDebugVerbose(bool debug, bool verbose) { m_debug = m_voice->m_debug = m_data->m_debug; m_verbose = m_voice->m_verbose = m_data->m_verbose; } /// /// Helper to change the RCCH verbose state. /// /// Flag indicating whether RCCH dumping is enabled. void Control::setRCCHVerbose(bool verbose) { m_dumpRCCH = verbose; lc::RCCH::setVerbose(verbose); lc::RTCH::setVerbose(verbose); } // --------------------------------------------------------------------------- // Private Class Members // --------------------------------------------------------------------------- /// /// Add data frame to the data ring buffer. /// /// Frame data to add to Tx queue. /// Flag indicating whether the data came from the network or not /// Flag indicating whether or not the data is priority and is added to the immediate queue. void Control::addFrame(const uint8_t *data, bool net, bool imm) { assert(data != nullptr); if (!net) { if (m_rfTimeout.isRunning() && m_rfTimeout.hasExpired()) return; } else { if (m_netTimeout.isRunning() && m_netTimeout.hasExpired()) return; } uint8_t len = NXDN_FRAME_LENGTH_BYTES + 2U; if (m_debug) { Utils::symbols("!!! *Tx NXDN", data + 2U, len - 2U); } // is this immediate data? if (imm) { // resize immediate queue if necessary (this shouldn't really ever happen) uint32_t space = m_txImmQueue.freeSpace(); if (space < (len + 1U)) { if (!net) { uint32_t queueLen = m_txImmQueue.length(); m_txImmQueue.resize(queueLen + len); LogError(LOG_P25, "overflow in the NXDN queue while writing imm data; queue free is %u, needed %u; resized was %u is %u", space, len, queueLen, m_txImmQueue.length()); return; } else { LogError(LOG_P25, "overflow in the NXDN queue while writing imm network data; queue free is %u, needed %u", space, len); return; } } m_txImmQueue.addData(&len, 1U); m_txImmQueue.addData(data, len); return; } uint32_t space = m_txQueue.freeSpace(); if (space < (len + 1U)) { if (!net) { uint32_t queueLen = m_txQueue.length(); m_txQueue.resize(queueLen + len); LogError(LOG_NXDN, "overflow in the NXDN queue while writing data; queue free is %u, needed %u; resized was %u is %u", space, len, queueLen, m_txQueue.length()); return; } else { LogError(LOG_NXDN, "overflow in the NXDN queue while writing network data; queue free is %u, needed %u", space, len); return; } } m_txQueue.addData(&len, 1U); m_txQueue.addData(data, len); } /// /// Process a data frames from the network. /// 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); if (!ret) return; if (length == 0U) return; if (buffer == nullptr) { m_network->resetNXDN(); return; } uint8_t messageType = buffer[4U]; uint32_t srcId = __GET_UINT16(buffer, 5U); uint32_t dstId = __GET_UINT16(buffer, 8U); if (m_debug) { LogDebug(LOG_NET, "NXDN, messageType = $%02X, srcId = %u, dstId = %u, len = %u", messageType, srcId, dstId, length); } lc::RTCH lc; lc.setMessageType(messageType); lc.setSrcId((uint16_t)srcId & 0xFFFFU); lc.setDstId((uint16_t)dstId & 0xFFFFU); bool group = (buffer[15U] & 0x40U) == 0x40U ? false : true; lc.setGroup(group); UInt8Array data; uint8_t frameLength = buffer[23U]; if (frameLength <= 24) { data = std::unique_ptr(new uint8_t[frameLength]); ::memset(data.get(), 0x00U, frameLength); } else { data = std::unique_ptr(new uint8_t[frameLength]); ::memset(data.get(), 0x00U, frameLength); ::memcpy(data.get(), buffer.get() + 24U, frameLength); } m_networkWatchdog.start(); if (m_debug) { Utils::dump(2U, "!!! *NXDN Network Frame", data.get(), frameLength); } NXDNUtils::scrambler(data.get() + 2U); channel::LICH lich; bool valid = lich.decode(data.get() + 2U); if (valid) m_rfLastLICH = lich; uint8_t usc = m_rfLastLICH.getFCT(); uint8_t option = m_rfLastLICH.getOption(); switch (usc) { case NXDN_LICH_USC_UDCH: ret = m_data->processNetwork(option, lc, data.get(), frameLength); break; default: ret = m_voice->processNetwork(usc, option, lc, data.get(), frameLength); break; } } /// /// Helper to send a REST API request to the CC to release a channel grant at the end of a call. /// /// void Control::notifyCC_ReleaseGrant(uint32_t dstId) { // callback REST API to release the granted TG on the specified control channel if (!m_controlChData.address().empty() && m_controlChData.port() > 0) { if (m_controlChData.address() == "127.0.0.1") { // cowardly ignore trying to send release grants to ourselves return; } json::object req = json::object(); int state = modem::DVM_STATE::STATE_NXDN; req["state"].set(state); req["dstId"].set(dstId); int ret = RESTClient::send(m_controlChData.address(), m_controlChData.port(), m_controlChData.password(), HTTP_PUT, PUT_RELEASE_TG, req, m_debug); if (ret != network::rest::http::HTTPPayload::StatusType::OK) { ::LogError(LOG_NXDN, "failed to notify the CC %s:%u of the release of, dstId = %u", m_controlChData.address().c_str(), m_controlChData.port(), dstId); } } } /// /// Helper to send a REST API request to the CC to "touch" a channel grant to refresh grant timers. /// /// void Control::notifyCC_TouchGrant(uint32_t dstId) { // callback REST API to touch the granted TG on the specified control channel if (!m_controlChData.address().empty() && m_controlChData.port() > 0) { if (m_controlChData.address() == "127.0.0.1") { // cowardly ignore trying to send touch grants to ourselves return; } json::object req = json::object(); int state = modem::DVM_STATE::STATE_NXDN; req["state"].set(state); req["dstId"].set(dstId); int ret = RESTClient::send(m_controlChData.address(), m_controlChData.port(), m_controlChData.password(), HTTP_PUT, PUT_TOUCH_TG, req, m_debug); if (ret != network::rest::http::HTTPPayload::StatusType::OK) { ::LogError(LOG_NXDN, "failed to notify the CC %s:%u of the touch of, dstId = %u", m_controlChData.address().c_str(), m_controlChData.port(), dstId); } } } /// /// Helper to write control channel frame data. /// /// bool Control::writeRF_ControlData() { if (!m_control) 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(); if (space < (len + 1U)) { return false; } const uint8_t maxSeq = m_trunk->m_bcchCnt + (m_trunk->m_ccchPagingCnt + m_trunk->m_ccchMultiCnt) * m_trunk->m_rcchGroupingCnt * m_trunk->m_rcchIterateCnt; if (m_ccSeq == maxSeq) { m_ccSeq = 0U; } if (m_netState == RS_NET_IDLE && m_rfState == RS_RF_LISTENING) { m_trunk->writeRF_ControlData(m_ccFrameCnt, m_ccSeq, true); m_ccSeq++; if (m_ccSeq == maxSeq) { m_ccFrameCnt++; } return true; } return false; } /// /// Helper to write a Tx release packet. /// /// void Control::writeRF_Message_Tx_Rel(bool noNetwork) { 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(NXDN_LICH_RFCT_RTCH); lich.setFCT(NXDN_LICH_USC_SACCH_NS); lich.setOption(NXDN_LICH_STEAL_FACCH); lich.setOutbound(true); lich.encode(data + 2U); uint8_t buffer[NXDN_RTCH_LC_LENGTH_BYTES]; ::memset(buffer, 0x00U, NXDN_RTCH_LC_LENGTH_BYTES); m_rfLC.setMessageType(RTCH_MESSAGE_TYPE_TX_REL); m_rfLC.encode(buffer, NXDN_UDCH_LENGTH_BITS); // generate the SACCH channel::SACCH sacch; sacch.setData(SACCH_IDLE); sacch.setRAN(m_ran); sacch.setStructure(NXDN_SR_SINGLE); sacch.encode(data + 2U); // generate the FACCH1 channel::FACCH1 facch; facch.setData(buffer); facch.encode(data + 2U, NXDN_FSW_LENGTH_BITS + NXDN_LICH_LENGTH_BITS + NXDN_SACCH_LENGTH_BITS); facch.encode(data + 2U, NXDN_FSW_LENGTH_BITS + NXDN_LICH_LENGTH_BITS + NXDN_SACCH_LENGTH_BITS + NXDN_FACCH1_LENGTH_BITS); data[0U] = modem::TAG_DATA; data[1U] = 0x00U; NXDNUtils::scrambler(data + 2U); if (!noNetwork) m_data->writeNetwork(data, NXDN_FRAME_LENGTH_BYTES + 2U); if (m_duplex) { addFrame(data, NXDN_FRAME_LENGTH_BYTES + 2U); } } /// /// Helper to write RF end of frame data. /// void Control::writeEndRF() { m_rfState = RS_RF_LISTENING; m_rfMask = 0x00U; m_rfLC.reset(); m_rfTimeout.stop(); //m_queue.clear(); if (m_network != nullptr) m_network->resetNXDN(); } /// /// Helper to write network end of frame data. /// void Control::writeEndNet() { m_netState = RS_NET_IDLE; m_netMask = 0x00U; m_netLC.reset(); m_netTimeout.stop(); m_networkWatchdog.stop(); if (m_network != nullptr) m_network->resetP25(); }