/** * 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) 2022 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/channel/CAC.h" #include "nxdn/packet/Trunk.h" #include "nxdn/acl/AccessControl.h" #include "nxdn/Sync.h" #include "edac/CRC.h" #include "HostMain.h" #include "Log.h" #include "Utils.h" using namespace nxdn; using namespace nxdn::packet; #include #include #include #include // --------------------------------------------------------------------------- // Macros // --------------------------------------------------------------------------- // Make sure control data is supported. #define IS_SUPPORT_CONTROL_CHECK(_PCKT_STR, _PCKT, _SRCID) \ if (!m_nxdn->m_control) { \ LogWarning(LOG_RF, "NXDN, " _PCKT_STR " denial, unsupported service, srcId = %u", _SRCID); \ writeRF_Message_Deny(NXDN_CAUSE_SVC_UNAVAILABLE, _PCKT); \ m_nxdn->m_rfState = RS_RF_REJECTED; \ return false; \ } // Validate the source RID. #define VALID_SRCID(_PCKT_STR, _PCKT, _SRCID, _RSN) \ if (!acl::AccessControl::validateSrcId(_SRCID)) { \ LogWarning(LOG_RF, "NXDN, " _PCKT_STR " denial, RID rejection, srcId = %u", _SRCID); \ writeRF_Message_Deny(_RSN, _PCKT); \ m_nxdn->m_rfState = RS_RF_REJECTED; \ return false; \ } // Validate the target RID. #define VALID_DSTID(_PCKT_STR, _PCKT, _DSTID, _RSN) \ if (!acl::AccessControl::validateSrcId(_DSTID)) { \ LogWarning(LOG_RF, "NXDN, " _PCKT_STR " denial, RID rejection, dstId = %u", _DSTID); \ writeRF_Message_Deny(_RSN, _PCKT); \ m_nxdn->m_rfState = RS_RF_REJECTED; \ return false; \ } // Validate the talkgroup ID. #define VALID_TGID(_PCKT_STR, _PCKT, _DSTID, _RSN) \ if (!acl::AccessControl::validateTGId(_DSTID)) { \ LogWarning(LOG_RF, "NXDN, " _PCKT_STR " denial, TGID rejection, dstId = %u", _DSTID); \ writeRF_Message_Deny(_RSN, _PCKT); \ m_nxdn->m_rfState = RS_RF_REJECTED; \ return false; \ } // Verify the source RID is registered. #define VERIFY_SRCID_REG(_PCKT_STR, _PCKT, _SRCID, _RSN) \ if (!m_nxdn->m_affiliations.isUnitReg(_SRCID) && m_verifyReg) { \ LogWarning(LOG_RF, "NXDN, " _PCKT_STR " denial, RID not registered, srcId = %u", _SRCID); \ writeRF_Message_Deny(_RSN, _PCKT); \ m_nxdn->m_rfState = RS_RF_REJECTED; \ return false; \ } // Verify the source RID is affiliated. #define VERIFY_SRCID_AFF(_PCKT_STR, _PCKT, _SRCID, _DSTID, _RSN) \ if (!m_nxdn->m_affiliations.isGroupAff(_SRCID, _DSTID) && m_verifyAff) { \ LogWarning(LOG_RF, "NXDN, " _PCKT_STR " denial, RID not affiliated to TGID, srcId = %u, dstId = %u", _SRCID, _DSTID); \ writeRF_Message_Deny(_RSN, _PCKT); \ m_nxdn->m_rfState = RS_RF_REJECTED; \ return false; \ } // --------------------------------------------------------------------------- // Constants // --------------------------------------------------------------------------- const uint32_t GRANT_TIMER_TIMEOUT = 15U; // --------------------------------------------------------------------------- // Public Class Members // --------------------------------------------------------------------------- /// /// Resets the data states for the RF interface. /// void Trunk::resetRF() { lc::RCCH lc = lc::RCCH(m_nxdn->m_siteData, m_nxdn->m_idenEntry, m_dumpRCCH); m_rfLC = lc; } /// /// Resets the data states for the network. /// void Trunk::resetNet() { lc::RCCH lc = lc::RCCH(m_nxdn->m_siteData, m_nxdn->m_idenEntry, m_dumpRCCH); m_netLC = lc; } /// /// Process a data frame from the RF interface. /// /// Functional channel type. /// Channel options. /// Buffer containing data frame. /// Length of data frame. /// bool Trunk::process(uint8_t fct, uint8_t option, uint8_t* data, uint32_t len) { assert(data != NULL); channel::CAC cac; bool validCAC = cac.decode(data + 2U); if (m_nxdn->m_rfState == RS_RF_LISTENING && !validCAC) return false; if (validCAC) { uint8_t ran = cac.getRAN(); if (ran != m_nxdn->m_ran && ran != 0U) return false; } RPT_RF_STATE prevRfState = m_nxdn->m_rfState; if (m_nxdn->m_rfState != RS_RF_DATA) { m_nxdn->m_rfState = RS_RF_DATA; } m_nxdn->m_queue.clear(); // the layer3 data will only be correct if valid is true uint8_t buffer[NXDN_CAC_FRAME_LENGTH_BYTES]; cac.getData(buffer); m_rfLC.decode(buffer, NXDN_RCCH_CAC_LC_SHORT_LENGTH_BITS); uint16_t srcId = m_rfLC.getSrcId(); uint16_t dstId = m_rfLC.getDstId(); switch (m_rfLC.getMessageType()) { case RTCH_MESSAGE_TYPE_VCALL: // make sure control data is supported IS_SUPPORT_CONTROL_CHECK(NXDN_RTCH_MSG_TYPE_VCALL_REQ, RTCH_MESSAGE_TYPE_VCALL, srcId); // validate the source RID VALID_SRCID(NXDN_RTCH_MSG_TYPE_VCALL_REQ, RTCH_MESSAGE_TYPE_VCALL, srcId, NXDN_CAUSE_VD_REQ_UNIT_NOT_PERM); // validate the talkgroup ID VALID_TGID(NXDN_RTCH_MSG_TYPE_VCALL_REQ, RTCH_MESSAGE_TYPE_VCALL, dstId, NXDN_CAUSE_VD_TGT_UNIT_NOT_PERM); // verify the source RID is affiliated VERIFY_SRCID_AFF(NXDN_RTCH_MSG_TYPE_VCALL_REQ, RTCH_MESSAGE_TYPE_VCALL, srcId, dstId, NXDN_CAUSE_VD_REQ_UNIT_NOT_REG); if (m_verbose) { LogMessage(LOG_RF, "NXDN, " NXDN_RTCH_MSG_TYPE_VCALL_REQ ", srcId = %u, dstId = %u", srcId, dstId); } writeRF_Message_Grant(true); break; case RCCH_MESSAGE_TYPE_REG: // make sure control data is supported IS_SUPPORT_CONTROL_CHECK(NXDN_RCCH_MSG_TYPE_REG_REQ, RCCH_MESSAGE_TYPE_REG, srcId); if (m_verbose) { LogMessage(LOG_RF, "NXDN, " NXDN_RCCH_MSG_TYPE_REG_REQ ", srcId = %u", srcId); } writeRF_Message_U_Reg_Rsp(srcId); break; case RCCH_MESSAGE_TYPE_GRP_REG: // make sure control data is supported IS_SUPPORT_CONTROL_CHECK(NXDN_RCCH_MSG_TYPE_GRP_REG_REQ, RCCH_MESSAGE_TYPE_GRP_REG, srcId); if (m_verbose) { LogMessage(LOG_RF, "NXDN, " NXDN_RCCH_MSG_TYPE_GRP_REG_REQ ", srcId = %u, dstId = %u", srcId, dstId); } writeRF_Message_Grp_Reg_Rsp(srcId, dstId); break; default: LogError(LOG_RF, "NXDN, unhandled message type, messageType = $%02X", m_rfLC.getMessageType()); break; } m_nxdn->m_rfState = prevRfState; return true; } /// /// Process a data frame from the RF interface. /// /// Functional channel type. /// Channel options. /// /// Buffer containing data frame. /// Length of data frame. /// bool Trunk::processNetwork(uint8_t fct, uint8_t option, lc::RTCH& netLC, uint8_t* data, uint32_t len) { assert(data != NULL); if (m_nxdn->m_netState == RS_NET_IDLE) { m_nxdn->m_queue.clear(); resetRF(); resetNet(); } return true; } /// /// Updates the processor by the passed number of milliseconds. /// /// void Trunk::clock(uint32_t ms) { if (m_nxdn->m_control) { if (m_nxdn->m_network != NULL) { if (m_nxdn->m_network->isHandlingChGrants() && m_nxdn->m_siteData.netActive()) { bool grp = true; uint32_t srcId = 0U; uint32_t dstId = 0U; uint32_t grpVchNo = 0U; // do we have a grant response? if (m_nxdn->m_network->readGrantRsp(grp, srcId, dstId, grpVchNo)) { m_rfLC.setSrcId(srcId); m_rfLC.setDstId(dstId); m_rfLC.setGrpVchNo(grpVchNo); writeRF_Message_Grant(grp, true, true, true); } } } // clock all the grant timers m_nxdn->m_affiliations.clock(ms); } } // --------------------------------------------------------------------------- // Protected Class Members // --------------------------------------------------------------------------- /// /// Initializes a new instance of the Trunk class. /// /// Instance of the Control class. /// Instance of the BaseNetwork class. /// Flag indicating whether RCCH data is dumped to the log. /// Flag indicating whether NXDN debug is enabled. /// Flag indicating whether NXDN verbose logging is enabled. Trunk::Trunk(Control* nxdn, network::BaseNetwork* network, bool dumpRCCHData, bool debug, bool verbose) : m_nxdn(nxdn), m_network(network), m_verifyAff(false), m_verifyReg(false), m_rfLC(SiteData(), lookups::IdenTable()), m_netLC(SiteData(), lookups::IdenTable()), m_lastRejectId(0U), m_dumpRCCH(dumpRCCHData), m_verbose(verbose), m_debug(debug) { /* stub */ } /// /// Finalizes a instance of the Trunk class. /// Trunk::~Trunk() { /* stub */ } /// /// Write data processed from RF to the network. /// /// /// void Trunk::writeNetwork(const uint8_t *data, uint32_t len) { assert(data != NULL); if (m_network == NULL) return; if (m_nxdn->m_rfTimeout.isRunning() && m_nxdn->m_rfTimeout.hasExpired()) return; m_network->writeNXDN(m_nxdn->m_rfLC, data, len); } /// /// Helper to write control channel packet data. /// /// /// /// void Trunk::writeRF_ControlData(uint8_t frameCnt, uint8_t n, bool adjSS) { uint8_t i = 0U, seqCnt = 0U; if (!m_nxdn->m_control) return; // don't add any frames if the queue is full uint8_t len = NXDN_FRAME_LENGTH_BYTES + 2U; uint32_t space = m_nxdn->m_queue.freeSpace(); if (space < (len + 1U)) { return; } do { if (m_debug) { LogDebug(LOG_NXDN, "writeRF_ControlData, frameCnt = %u, seq = %u", frameCnt, n); } switch (n) { case 6: writeRF_CC_Message_Site_Info(); break; case 0: default: writeRF_CC_Message_Service_Info(); break; } if (seqCnt > 0U) n++; i++; } while (i <= seqCnt); } /// /// Helper to write a single-block RCCH packet. /// /// /// void Trunk::writeRF_Message(bool noNetwork, bool clearBeforeWrite) { if (!m_nxdn->m_control) return; uint8_t data[NXDN_FRAME_LENGTH_BYTES + 2U]; ::memset(data + 2U, 0x00U, NXDN_FRAME_LENGTH_BYTES); Sync::addNXDNSync(data + 2U); channel::LICH lich; lich.setRFCT(NXDN_LICH_RFCT_RCCH); lich.setFCT(NXDN_LICH_CAC_OUTBOUND); lich.setOption(NXDN_LICH_DATA_NORMAL); lich.setOutbound(true); lich.encode(data + 2U); uint8_t buffer[NXDN_RCCH_LC_LENGTH_BYTES]; ::memset(buffer, 0x00U, NXDN_RCCH_LC_LENGTH_BYTES); m_rfLC.encode(buffer, NXDN_RCCH_LC_LENGTH_BITS); channel::CAC cac; cac.setVerbose(m_dumpRCCH); cac.setRAN(m_nxdn->m_ran); cac.setData(buffer); cac.encode(data + 2U); data[0U] = modem::TAG_DATA; data[1U] = 0x00U; m_nxdn->scrambler(data + 2U); if (!noNetwork) writeNetwork(data, NXDN_FRAME_LENGTH_BYTES + 2U); if (clearBeforeWrite) { m_nxdn->m_modem->clearP25Data(); m_nxdn->m_queue.clear(); } if (m_nxdn->m_duplex) { m_nxdn->addFrame(data, NXDN_FRAME_LENGTH_BYTES + 2U); } } /// /// Helper to write a grant packet. /// /// /// /// /// /// bool Trunk::writeRF_Message_Grant(bool grp, bool skip, bool net, bool skipNetCheck) { uint8_t messageType = m_rfLC.getMessageType(); // do we have a network connection and are we handling grants at the network? if (m_nxdn->m_network != NULL) { if (m_nxdn->m_network->isHandlingChGrants() && m_nxdn->m_siteData.netActive() && !skipNetCheck) { return m_nxdn->m_network->writeGrantReq(grp, m_rfLC.getSrcId(), m_rfLC.getDstId()); } } // are we skipping checking? if (!skip) { if (m_nxdn->m_rfState != RS_RF_LISTENING && m_nxdn->m_rfState != RS_RF_DATA) { if (!net) { LogWarning(LOG_RF, "NXDN, " NXDN_RTCH_MSG_TYPE_VCALL_REQ " denied, traffic in progress, dstId = %u", m_rfLC.getDstId()); writeRF_Message_Deny(NXDN_CAUSE_VD_QUE_GRP_BUSY, RTCH_MESSAGE_TYPE_VCALL); ::ActivityLog("NXDN", true, "group grant request from %u to TG %u denied", m_rfLC.getSrcId(), m_rfLC.getDstId()); m_nxdn->m_rfState = RS_RF_REJECTED; } m_rfLC.setMessageType(messageType); return false; } if (m_nxdn->m_netState != RS_NET_IDLE && m_rfLC.getDstId() == m_nxdn->m_netLastDstId) { if (!net) { LogWarning(LOG_RF, "NXDN, " NXDN_RTCH_MSG_TYPE_VCALL_REQ " denied, traffic in progress, dstId = %u", m_rfLC.getDstId()); writeRF_Message_Deny(NXDN_CAUSE_VD_QUE_GRP_BUSY, RTCH_MESSAGE_TYPE_VCALL); ::ActivityLog("NXDN", true, "group grant request from %u to TG %u denied", m_rfLC.getSrcId(), m_rfLC.getDstId()); m_nxdn->m_rfState = RS_RF_REJECTED; } m_rfLC.setMessageType(messageType); return false; } // don't transmit grants if the destination ID's don't match and the network TG hang timer is running if (m_nxdn->m_rfLastDstId != 0U) { if (m_nxdn->m_rfLastDstId != m_rfLC.getDstId() && (m_nxdn->m_rfTGHang.isRunning() && !m_nxdn->m_rfTGHang.hasExpired())) { if (!net) { writeRF_Message_Deny(NXDN_CAUSE_VD_QUE_GRP_BUSY, RTCH_MESSAGE_TYPE_VCALL); m_nxdn->m_rfState = RS_RF_REJECTED; } m_rfLC.setMessageType(messageType); return false; } } if (!m_nxdn->m_affiliations.isGranted(m_rfLC.getDstId())) { if (!m_nxdn->m_affiliations.isRFChAvailable()) { if (grp) { if (!net) { LogWarning(LOG_RF, "NXDN, " NXDN_RTCH_MSG_TYPE_VCALL_REQ " queued, no channels available, dstId = %u", m_rfLC.getDstId()); writeRF_Message_Deny(NXDN_CAUSE_VD_QUE_CHN_RESOURCE_NOT_AVAIL, RTCH_MESSAGE_TYPE_VCALL); ::ActivityLog("NXDN", true, "group grant request from %u to TG %u queued", m_rfLC.getSrcId(), m_rfLC.getDstId()); m_nxdn->m_rfState = RS_RF_REJECTED; } m_rfLC.setMessageType(messageType); return false; } else { if (!net) { LogWarning(LOG_RF, "NXDN, " NXDN_RTCH_MSG_TYPE_VCALL_REQ " queued, no channels available, dstId = %u", m_rfLC.getDstId()); writeRF_Message_Deny(NXDN_CAUSE_VD_QUE_CHN_RESOURCE_NOT_AVAIL, RTCH_MESSAGE_TYPE_VCALL); ::ActivityLog("P25", true, "unit-to-unit grant request from %u to %u queued", m_rfLC.getSrcId(), m_rfLC.getDstId()); m_nxdn->m_rfState = RS_RF_REJECTED; } m_rfLC.setMessageType(messageType); return false; } } else { if (m_nxdn->m_affiliations.grantCh(m_rfLC.getDstId(), GRANT_TIMER_TIMEOUT)) { uint32_t chNo = m_nxdn->m_affiliations.getGrantedCh(m_rfLC.getDstId()); m_rfLC.setGrpVchNo(chNo); } } } else { uint32_t chNo = m_nxdn->m_affiliations.getGrantedCh(m_rfLC.getDstId()); m_rfLC.setGrpVchNo(chNo); } } if (grp) { if (!net) { ::ActivityLog("NXDN", true, "group grant request from %u to TG %u", m_rfLC.getSrcId(), m_rfLC.getDstId()); } } else { if (!net) { ::ActivityLog("NXDN", true, "unit-to-unit grant request from %u to %u", m_rfLC.getSrcId(), m_rfLC.getDstId()); } } if (m_verbose) { LogMessage((net) ? LOG_NET : LOG_RF, "NXDN, " NXDN_RTCH_MSG_TYPE_VCALL_RESP ", emerg = %u, encrypt = %u, prio = %u, chNo = %u, srcId = %u, dstId = %u", m_rfLC.getEmergency(), m_rfLC.getEncrypted(), m_rfLC.getPriority(), m_rfLC.getGrpVchNo(), m_rfLC.getSrcId(), m_rfLC.getDstId()); } // transmit group grant m_rfLC.setMessageType(RTCH_MESSAGE_TYPE_VCALL); writeRF_Message(false, true); m_rfLC.setMessageType(messageType); return true; } /// /// Helper to write a deny packet. /// /// /// void Trunk::writeRF_Message_Deny(uint8_t reason, uint8_t service) { uint8_t messageType = m_rfLC.getMessageType(); m_rfLC.setMessageType(service); m_rfLC.setCauseResponse(reason); if (m_verbose) { LogMessage(LOG_RF, "NXDN, MSG_DENIAL (Message Denial), reason = $%02X, service = $%02X, srcId = %u, dstId = %u", service, m_rfLC.getSrcId(), m_rfLC.getDstId()); } writeRF_Message(false); m_rfLC.setMessageType(messageType); } /// /// Helper to write a group registration response packet. /// /// /// bool Trunk::writeRF_Message_Grp_Reg_Rsp(uint32_t srcId, uint32_t dstId) { bool ret = false; m_rfLC.setMessageType(RCCH_MESSAGE_TYPE_GRP_REG); m_rfLC.setCauseResponse(NXDN_CAUSE_MM_REG_ACCEPTED); // validate the location ID if (m_rfLC.getLocId() != m_nxdn->m_siteData.locId()) { LogWarning(LOG_RF, "NXDN, " NXDN_RCCH_MSG_TYPE_GRP_REG_REQ " denial, LOCID rejection, locId = $%04X", m_rfLC.getLocId()); ::ActivityLog("NXDN", true, "group affiliation request from %u denied", srcId); m_rfLC.setCauseResponse(NXDN_CAUSE_MM_REG_FAILED); } // validate the source RID if (!acl::AccessControl::validateSrcId(srcId)) { LogWarning(LOG_RF, "NXDN, " NXDN_RCCH_MSG_TYPE_GRP_REG_REQ " denial, RID rejection, srcId = %u", srcId); ::ActivityLog("NXDN", true, "group affiliation request from %u to %s %u denied", srcId, "TG ", dstId); m_rfLC.setCauseResponse(NXDN_CAUSE_MM_REG_FAILED); } // validate the source RID is registered if (!m_nxdn->m_affiliations.isUnitReg(srcId) && m_verifyReg) { LogWarning(LOG_RF, "NXDN, " NXDN_RCCH_MSG_TYPE_GRP_REG_REQ " denial, RID not registered, srcId = %u", srcId); ::ActivityLog("NXDN", true, "group affiliation request from %u to %s %u denied", srcId, "TG ", dstId); m_rfLC.setCauseResponse(NXDN_CAUSE_MM_REG_REFUSED); } // validate the talkgroup ID if (m_rfLC.getGroup()) { if (dstId == 0U) { LogWarning(LOG_RF, "NXDN, " NXDN_RCCH_MSG_TYPE_GRP_REG_REQ ", TGID 0, dstId = %u", dstId); } else { if (!acl::AccessControl::validateTGId(dstId)) { LogWarning(LOG_RF, "NXDN, " NXDN_RCCH_MSG_TYPE_GRP_REG_REQ " denial, TGID rejection, dstId = %u", dstId); ::ActivityLog("NXDN", true, "group affiliation request from %u to %s %u denied", srcId, "TG ", dstId); m_rfLC.setCauseResponse(NXDN_CAUSE_MM_LOC_ACPT_GRP_REFUSE); } } } if (m_rfLC.getCauseResponse() == NXDN_CAUSE_MM_REG_ACCEPTED) { if (m_verbose) { LogMessage(LOG_RF, "NXDN, " NXDN_RCCH_MSG_TYPE_GRP_REG_REQ ", srcId = %u, dstId = %u", srcId, dstId); } ::ActivityLog("NXDN", true, "group affiliation request from %u to %s %u", srcId, "TG ", dstId); ret = true; // update dynamic affiliation table m_nxdn->m_affiliations.groupAff(srcId, dstId); } writeRF_Message(false); return ret; } /// /// Helper to write a unit registration response packet. /// /// void Trunk::writeRF_Message_U_Reg_Rsp(uint32_t srcId) { m_rfLC.setMessageType(RCCH_MESSAGE_TYPE_REG); m_rfLC.setCauseResponse(NXDN_CAUSE_MM_REG_ACCEPTED); // validate the location ID if (m_rfLC.getLocId() != m_nxdn->m_siteData.locId()) { LogWarning(LOG_RF, "NXDN, " NXDN_RCCH_MSG_TYPE_REG_REQ " denial, LOCID rejection, locId = $%04X", m_rfLC.getLocId()); ::ActivityLog("NXDN", true, "unit registration request from %u denied", srcId); m_rfLC.setCauseResponse(NXDN_CAUSE_MM_REG_FAILED); } // validate the source RID if (!acl::AccessControl::validateSrcId(srcId)) { LogWarning(LOG_RF, "NXDN, " NXDN_RCCH_MSG_TYPE_REG_REQ " denial, RID rejection, srcId = %u", srcId); ::ActivityLog("NXDN", true, "unit registration request from %u denied", srcId); m_rfLC.setCauseResponse(NXDN_CAUSE_MM_REG_FAILED); } if (m_rfLC.getCauseResponse() == NXDN_CAUSE_MM_REG_ACCEPTED) { if (m_verbose) { LogMessage(LOG_RF, "NXDN, " NXDN_RCCH_MSG_TYPE_REG_REQ ", srcId = %u, locId = %u", srcId, m_rfLC.getLocId()); } ::ActivityLog("NXDN", true, "unit registration request from %u", srcId); // update dynamic unit registration table if (!m_nxdn->m_affiliations.isUnitReg(srcId)) { m_nxdn->m_affiliations.unitReg(srcId); } } m_rfLC.setSrcId(srcId); m_rfLC.setDstId(srcId); writeRF_Message(true); } /// /// Helper to write a CC SITE_INFO broadcast packet on the RF interface. /// void Trunk::writeRF_CC_Message_Site_Info() { if (m_debug) { LogMessage(LOG_RF, "NXDN, SITE_INFO (Site Information)"); } uint8_t data[NXDN_FRAME_LENGTH_BYTES + 2U]; ::memset(data + 2U, 0x00U, NXDN_FRAME_LENGTH_BYTES); Sync::addNXDNSync(data + 2U); channel::LICH lich; lich.setRFCT(NXDN_LICH_RFCT_RCCH); lich.setFCT(NXDN_LICH_CAC_OUTBOUND); lich.setOption(NXDN_LICH_DATA_COMMON); lich.setOutbound(true); lich.encode(data + 2U); uint8_t buffer[NXDN_RCCH_LC_LENGTH_BYTES]; ::memset(buffer, 0x00U, NXDN_RCCH_LC_LENGTH_BYTES); m_rfLC.setMessageType(RCCH_MESSAGE_TYPE_SITE_INFO); m_rfLC.encode(buffer, NXDN_RCCH_LC_LENGTH_BITS); channel::CAC cac; cac.setVerbose(m_dumpRCCH); cac.setRAN(m_nxdn->m_ran); cac.setStructure(NXDN_SR_RCCH_HEAD_SINGLE); cac.setData(buffer); cac.encode(data + 2U); data[0U] = modem::TAG_DATA; data[1U] = 0x00U; m_nxdn->scrambler(data + 2U); if (m_nxdn->m_duplex) { m_nxdn->addFrame(data, NXDN_FRAME_LENGTH_BYTES + 2U); } } /// /// Helper to write a CC SRV_INFO broadcast packet on the RF interface. /// void Trunk::writeRF_CC_Message_Service_Info() { if (m_debug) { LogMessage(LOG_RF, "NXDN, SRV_INFO (Service Information)"); } uint8_t data[NXDN_FRAME_LENGTH_BYTES + 2U]; ::memset(data + 2U, 0x00U, NXDN_FRAME_LENGTH_BYTES); Sync::addNXDNSync(data + 2U); channel::LICH lich; lich.setRFCT(NXDN_LICH_RFCT_RCCH); lich.setFCT(NXDN_LICH_CAC_OUTBOUND); lich.setOption(NXDN_LICH_DATA_NORMAL); lich.setOutbound(true); lich.encode(data + 2U); uint8_t buffer[NXDN_RCCH_LC_LENGTH_BYTES]; ::memset(buffer, 0x00U, NXDN_RCCH_LC_LENGTH_BYTES); m_rfLC.setMessageType(MESSAGE_TYPE_SRV_INFO); m_rfLC.encode(buffer, NXDN_RCCH_LC_LENGTH_BITS); channel::CAC cac; cac.setVerbose(m_dumpRCCH); cac.setRAN(m_nxdn->m_ran); cac.setStructure(NXDN_SR_RCCH_SINGLE); cac.setData(buffer); cac.encode(data + 2U); data[0U] = modem::TAG_DATA; data[1U] = 0x00U; m_nxdn->scrambler(data + 2U); if (m_nxdn->m_duplex) { m_nxdn->addFrame(data, NXDN_FRAME_LENGTH_BYTES + 2U); } }