You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1062 lines
32 KiB
1062 lines
32 KiB
// SPDX-License-Identifier: GPL-2.0-only
|
|
/*
|
|
* Digital Voice Modem - Modem Host Software
|
|
* GPLv2 Open Source. Use is subject to license terms.
|
|
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
|
|
*
|
|
* Copyright (C) 2015,2016,2017 Jonathan Naylor, G4KLX
|
|
* Copyright (C) 2017-2025 Bryan Biedenkapp, N2PLL
|
|
*
|
|
*/
|
|
#include "Defines.h"
|
|
#include "common/dmr/acl/AccessControl.h"
|
|
#include "common/dmr/lc/CSBK.h"
|
|
#include "common/dmr/lc/csbk/CSBKFactory.h"
|
|
#include "common/RC4Crypto.h"
|
|
#include "common/Log.h"
|
|
#include "dmr/Control.h"
|
|
#include "Host.h"
|
|
|
|
using namespace dmr;
|
|
using namespace dmr::defines;
|
|
|
|
#include <cassert>
|
|
#include <algorithm>
|
|
#include <memory>
|
|
#include <random>
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Global Functions
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/**
|
|
* @brief Calculates the registration authentication response.
|
|
* @param challenge Registration challenge.
|
|
* @param key Authentication key.
|
|
* @return Calculated registration authentication response.
|
|
*/
|
|
uint32_t calculateRegistrationAuthResponse(uint32_t challenge, const uint8_t key[16U])
|
|
{
|
|
uint8_t rc4Key[19U];
|
|
rc4Key[0U] = (uint8_t)((challenge >> 16U) & 0xFFU);
|
|
rc4Key[1U] = (uint8_t)((challenge >> 8U) & 0xFFU);
|
|
rc4Key[2U] = (uint8_t)(challenge & 0xFFU);
|
|
::memcpy(rc4Key + 3U, key, 16U);
|
|
|
|
crypto::RC4 rc4;
|
|
std::unique_ptr<uint8_t[]> stream(rc4.keystream(259U, rc4Key, sizeof(rc4Key)));
|
|
return ((uint32_t)stream[256U] << 16U) |
|
|
((uint32_t)stream[257U] << 8U) | stream[258U];
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Public Class Members
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/* Initializes a new instance of the Control class. */
|
|
|
|
Control::Control(bool authoritative, uint32_t colorCode, uint32_t callHang, uint32_t queueSize, bool embeddedLCOnly,
|
|
bool dumpTAData, uint32_t timeout, uint32_t tgHang, modem::Modem* modem, network::Network* network, bool duplex, ::lookups::ChannelLookup* chLookup,
|
|
::lookups::RadioIdLookup* ridLookup, ::lookups::TalkgroupRulesLookup* tidLookup, ::lookups::IdenTableLookup* idenTable, ::lookups::RSSIInterpolator* rssiMapper,
|
|
uint32_t jitter, bool dumpDataPacket, bool repeatDataPacket, bool dumpCSBKData, bool debug, bool verbose) :
|
|
m_authoritative(authoritative),
|
|
m_colorCode(colorCode),
|
|
m_modem(modem),
|
|
m_network(network),
|
|
m_slot1(nullptr),
|
|
m_slot2(nullptr),
|
|
m_idenTable(idenTable),
|
|
m_ridLookup(ridLookup),
|
|
m_tidLookup(tidLookup),
|
|
m_enableTSCC(false),
|
|
m_requireAuthForReg(false),
|
|
m_tsccCnt(0U),
|
|
m_tsccCntInterval(1000U, 0U, DMR_SLOT_TIME / 2U),
|
|
m_tsccSlotNo(0U),
|
|
m_tsccPayloadActive(false),
|
|
m_ccRunning(false),
|
|
m_ccHalted(false),
|
|
m_dumpCSBKData(dumpCSBKData),
|
|
m_verbose(verbose),
|
|
m_debug(debug)
|
|
{
|
|
assert(modem != nullptr);
|
|
assert(chLookup != nullptr);
|
|
assert(ridLookup != nullptr);
|
|
assert(tidLookup != nullptr);
|
|
assert(idenTable != nullptr);
|
|
assert(rssiMapper != nullptr);
|
|
|
|
acl::AccessControl::init(m_ridLookup, m_tidLookup);
|
|
Slot::init(this, authoritative, colorCode, SiteData(), embeddedLCOnly, dumpTAData, callHang, modem, network, duplex, chLookup, m_ridLookup, m_tidLookup, m_idenTable, rssiMapper, jitter, verbose);
|
|
lc::CSBK::setVerbose(m_dumpCSBKData);
|
|
|
|
m_slot1 = new Slot(1U, timeout, tgHang, queueSize, dumpDataPacket, repeatDataPacket, dumpCSBKData, debug, verbose);
|
|
m_slot2 = new Slot(2U, timeout, tgHang, queueSize, dumpDataPacket, repeatDataPacket, dumpCSBKData, debug, verbose);
|
|
|
|
m_tsccCntInterval.start();
|
|
|
|
// register RPC handlers
|
|
g_RPC->registerHandler(RPC_PERMIT_DMR_TG, RPC_FUNC_BIND(Control::RPC_permittedTG, this));
|
|
g_RPC->registerHandler(RPC_RELEASE_DMR_TG, RPC_FUNC_BIND(Control::RPC_releaseGrantTG, this));
|
|
g_RPC->registerHandler(RPC_TOUCH_DMR_TG, RPC_FUNC_BIND(Control::RPC_touchGrantTG, this));
|
|
g_RPC->registerHandler(RPC_DMR_TSCC_PAYLOAD_ACT, RPC_FUNC_BIND(Control::RPC_tsccPayloadActivate, this));
|
|
}
|
|
|
|
/* Finalizes a instance of the Control class. */
|
|
|
|
Control::~Control()
|
|
{
|
|
delete m_slot2;
|
|
delete m_slot1;
|
|
}
|
|
|
|
/* Helper to set DMR configuration options. */
|
|
|
|
void Control::setOptions(yaml::Node& conf, ::lookups::VoiceChData controlChData,
|
|
uint32_t netId, uint8_t siteId, uint8_t channelId, uint32_t channelNo, bool printOptions)
|
|
{
|
|
yaml::Node systemConf = conf["system"];
|
|
yaml::Node dmrProtocol = conf["protocols"]["dmr"];
|
|
|
|
Slot::s_verifyReg = dmrProtocol["verifyReg"].as<bool>(false);
|
|
|
|
uint8_t nRandWait = (uint8_t)dmrProtocol["nRandWait"].as<uint32_t>(DEFAULT_NRAND_WAIT);
|
|
if (nRandWait > 15U)
|
|
nRandWait = 15U;
|
|
uint8_t backOff = (uint8_t)dmrProtocol["backOff"].as<uint32_t>(1U);
|
|
if (backOff > 15U)
|
|
backOff = 15U;
|
|
|
|
yaml::Node control = dmrProtocol["control"];
|
|
m_requireAuthForReg = control["requireAuthForReg"].as<bool>(false);
|
|
if (m_requireAuthForReg && m_network != nullptr) {
|
|
m_network->setKEKKeyResponseCallback([this](uint32_t srcId, p25::kmm::KeyItem key,
|
|
uint8_t algId, uint8_t keyLength) {
|
|
processRegistrationAuthKey(srcId, key, algId, keyLength);
|
|
});
|
|
} else if (m_requireAuthForReg) {
|
|
LogWarning(LOG_DMR, "DMR registration authentication disabled; no FNE network is configured");
|
|
m_requireAuthForReg = false;
|
|
}
|
|
|
|
bool enableTSCC = control["enable"].as<bool>(false);
|
|
bool dedicatedTSCC = false;
|
|
if (enableTSCC) {
|
|
dedicatedTSCC = control["dedicated"].as<bool>(false);
|
|
}
|
|
else {
|
|
dedicatedTSCC = false;
|
|
}
|
|
bool ccDebug = control["debug"].as<bool>(false);
|
|
|
|
Slot::setSiteData(controlChData, netId, siteId, channelId, channelNo, dedicatedTSCC);
|
|
Slot::setAlohaConfig(nRandWait, backOff);
|
|
|
|
bool disableGrantSourceIdCheck = control["disableGrantSourceIdCheck"].as<bool>(false);
|
|
|
|
if (enableTSCC) {
|
|
m_tsccSlotNo = (uint8_t)control["slot"].as<uint32_t>(0U);
|
|
switch (m_tsccSlotNo) {
|
|
case 1U:
|
|
m_slot1->setTSCC(enableTSCC, dedicatedTSCC);
|
|
m_slot1->setDisableSourceIDGrantCheck(disableGrantSourceIdCheck);
|
|
m_slot1->setCCDebug(ccDebug);
|
|
break;
|
|
case 2U:
|
|
m_slot2->setTSCC(enableTSCC, dedicatedTSCC);
|
|
m_slot2->setDisableSourceIDGrantCheck(disableGrantSourceIdCheck);
|
|
m_slot2->setCCDebug(ccDebug);
|
|
break;
|
|
default:
|
|
LogError(LOG_DMR, "DMR, invalid slot, TSCC disabled, slotNo = %u", m_tsccSlotNo);
|
|
break;
|
|
}
|
|
}
|
|
|
|
m_enableTSCC = enableTSCC;
|
|
|
|
yaml::Node rfssConfig = systemConf["config"];
|
|
uint32_t defaultNetIdleTalkgroup = (uint32_t)rfssConfig["defaultNetIdleTalkgroup"].as<uint32_t>(0U);
|
|
m_slot1->setDefaultNetIdleTG(defaultNetIdleTalkgroup);
|
|
m_slot2->setDefaultNetIdleTG(defaultNetIdleTalkgroup);
|
|
|
|
yaml::Node controlCh = rfssConfig["controlCh"];
|
|
bool notifyCC = controlCh["notifyEnable"].as<bool>(false);
|
|
m_slot1->setNotifyCC(notifyCC);
|
|
m_slot2->setNotifyCC(notifyCC);
|
|
|
|
bool disableGrpAffTimeout = dmrProtocol["disableGrpAffTimeout"].as<bool>(false);
|
|
m_slot1->s_affiliations->setDisableGrpAffTimeout(disableGrpAffTimeout);
|
|
m_slot2->s_affiliations->setDisableGrpAffTimeout(disableGrpAffTimeout);
|
|
bool disableUnitRegTimeout = dmrProtocol["disableUnitRegTimeout"].as<bool>(false);
|
|
m_slot1->s_affiliations->setDisableUnitRegTimeout(disableUnitRegTimeout);
|
|
m_slot2->s_affiliations->setDisableUnitRegTimeout(disableUnitRegTimeout);
|
|
|
|
/*
|
|
** Voice Silence and Frame Loss Thresholds
|
|
*/
|
|
uint32_t silenceThreshold = dmrProtocol["silenceThreshold"].as<uint32_t>(DEFAULT_SILENCE_THRESHOLD);
|
|
if (silenceThreshold > MAX_DMR_VOICE_ERRORS) {
|
|
LogWarning(LOG_DMR, "Silence threshold > %u, defaulting to %u", MAX_DMR_VOICE_ERRORS, DEFAULT_SILENCE_THRESHOLD);
|
|
silenceThreshold = DEFAULT_SILENCE_THRESHOLD;
|
|
}
|
|
|
|
// either MAX_DMR_VOICE_ERRORS or 0 will disable the threshold logic
|
|
if (silenceThreshold == 0U) {
|
|
LogWarning(LOG_DMR, "Silence threshold set to zero, defaulting to %u", MAX_DMR_VOICE_ERRORS);
|
|
silenceThreshold = MAX_DMR_VOICE_ERRORS;
|
|
}
|
|
|
|
m_slot1->setSilenceThreshold(silenceThreshold);
|
|
m_slot2->setSilenceThreshold(silenceThreshold);
|
|
|
|
uint8_t frameLossThreshold = (uint8_t)dmrProtocol["frameLossThreshold"].as<uint32_t>(DEFAULT_FRAME_LOSS_THRESHOLD);
|
|
if (frameLossThreshold == 0U) {
|
|
frameLossThreshold = 1U;
|
|
}
|
|
|
|
if (frameLossThreshold > DEFAULT_FRAME_LOSS_THRESHOLD * 2U) {
|
|
LogWarning(LOG_DMR, "Frame loss threshold may be excessive, default is %u, configured is %u", DEFAULT_FRAME_LOSS_THRESHOLD, frameLossThreshold);
|
|
}
|
|
|
|
m_slot1->setFrameLossThreshold(frameLossThreshold);
|
|
m_slot2->setFrameLossThreshold(frameLossThreshold);
|
|
|
|
bool ignoreAffiliationCheck = dmrProtocol["ignoreAffiliationCheck"].as<bool>(true);
|
|
m_slot1->m_ignoreAffiliationCheck = ignoreAffiliationCheck;
|
|
m_slot2->m_ignoreAffiliationCheck = ignoreAffiliationCheck;
|
|
|
|
bool legacyGroupReg = dmrProtocol["legacyGroupReg"].as<bool>(false);
|
|
m_slot1->setLegacyGroupReg(legacyGroupReg);
|
|
m_slot2->setLegacyGroupReg(legacyGroupReg);
|
|
|
|
// set the In-Call Control function callback
|
|
if (m_network != nullptr) {
|
|
m_network->setDMRICCCallback([=](network::NET_ICC::ENUM command, uint32_t dstId,
|
|
uint8_t slotNo, uint32_t peerId, uint32_t ssrc, uint32_t streamId) { processInCallCtrl(command, dstId, slotNo); });
|
|
}
|
|
|
|
bool convNetGrantDemand = dmrProtocol["convNetGrantDemand"].as<bool>(false);
|
|
m_slot1->m_convNetGrantDemand = convNetGrantDemand;
|
|
m_slot2->m_convNetGrantDemand = convNetGrantDemand;
|
|
|
|
|
|
if (printOptions) {
|
|
if (enableTSCC) {
|
|
LogInfo(" TSCC Slot: %u", m_tsccSlotNo);
|
|
LogInfo(" TSCC Aloha Random Access Wait: %u", nRandWait);
|
|
LogInfo(" TSCC Aloha Backoff: %u", backOff);
|
|
if (disableGrantSourceIdCheck) {
|
|
LogInfo(" TSCC Disable Grant Source ID Check: yes");
|
|
}
|
|
LogInfoEx(LOG_DMR, "Host is configured to operate as a DMR TSCC.");
|
|
}
|
|
|
|
if (defaultNetIdleTalkgroup != 0U) {
|
|
LogInfo(" Default Network Idle Talkgroup: %u", defaultNetIdleTalkgroup);
|
|
}
|
|
|
|
if (disableGrpAffTimeout) {
|
|
LogInfo(" Disable Group Affiliation Timeout: yes");
|
|
}
|
|
|
|
if (disableUnitRegTimeout) {
|
|
LogInfo(" Disable Unit Registration Timeout: yes");
|
|
}
|
|
|
|
LogInfo(" Ignore Affiliation Check: %s", ignoreAffiliationCheck ? "yes" : "no");
|
|
LogInfo(" Legacy Group Registration: %s", legacyGroupReg ? "yes" : "no");
|
|
LogInfo(" Notify Control: %s", notifyCC ? "yes" : "no");
|
|
LogInfo(" Silence Threshold: %u (%.1f%%)", silenceThreshold, float(silenceThreshold) / 1.41F);
|
|
LogInfo(" Frame Loss Threshold: %u", frameLossThreshold);
|
|
|
|
LogInfo(" Verify Registration: %s", Slot::s_verifyReg ? "yes" : "no");
|
|
LogInfo(" Conventional Network Grant Demand: %s", convNetGrantDemand ? "yes" : "no");
|
|
}
|
|
}
|
|
|
|
/* Sets a flag indicating whether the DMR control channel is running. */
|
|
|
|
void Control::setCCRunning(bool ccRunning)
|
|
{
|
|
if (!m_enableTSCC) {
|
|
m_ccRunning = false;
|
|
return;
|
|
}
|
|
|
|
m_ccRunning = ccRunning;
|
|
switch (m_tsccSlotNo) {
|
|
case 1U:
|
|
m_slot1->setCCRunning(ccRunning);
|
|
break;
|
|
case 2U:
|
|
m_slot2->setCCRunning(ccRunning);
|
|
break;
|
|
default:
|
|
LogError(LOG_DMR, "DMR, invalid slot, TSCC disabled, slotNo = %u", m_tsccSlotNo);
|
|
break;
|
|
}
|
|
}
|
|
|
|
/* Sets a flag indicating whether the DMR control channel is halted. */
|
|
|
|
void Control::setCCHalted(bool ccHalted)
|
|
{
|
|
if (!m_enableTSCC) {
|
|
m_ccHalted = true;
|
|
return;
|
|
}
|
|
|
|
m_ccHalted = ccHalted;
|
|
switch (m_tsccSlotNo) {
|
|
case 1U:
|
|
m_slot1->setCCHalted(ccHalted);
|
|
break;
|
|
case 2U:
|
|
m_slot2->setCCHalted(ccHalted);
|
|
break;
|
|
default:
|
|
LogError(LOG_DMR, "DMR, invalid slot, TSCC disabled, slotNo = %u", m_tsccSlotNo);
|
|
break;
|
|
}
|
|
}
|
|
|
|
/* Helper to process wakeup frames from the RF interface. */
|
|
|
|
bool Control::processWakeup(const uint8_t* data)
|
|
{
|
|
assert(data != nullptr);
|
|
|
|
// wakeups always come in on slot 1
|
|
if (data[0U] != modem::TAG_DATA || data[1U] != (IDLE_RX | SYNC_DATA | DataType::CSBK))
|
|
return false;
|
|
|
|
// generate a new CSBK and check validity
|
|
std::unique_ptr<lc::CSBK> csbk = lc::csbk::CSBKFactory::createCSBK(data + 2U, DataType::CSBK);
|
|
if (csbk == nullptr)
|
|
return false;
|
|
|
|
uint8_t csbko = csbk->getCSBKO();
|
|
if (csbko != CSBKO::BSDWNACT)
|
|
return false;
|
|
|
|
uint32_t srcId = csbk->getSrcId();
|
|
|
|
// check the srcId against the ACL control
|
|
bool ret = acl::AccessControl::validateSrcId(srcId);
|
|
if (!ret) {
|
|
LogError(LOG_RF, "DMR, invalid CSBKO, BSDWNACT, srcId = %u", srcId);
|
|
return false;
|
|
}
|
|
|
|
if (m_verbose) {
|
|
LogInfoEx(LOG_RF, "DMR, CSBKO, BSDWNACT, srcId = %u", srcId);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/* Process a data frame for slot, from the RF interface. */
|
|
|
|
bool Control::processFrame(uint32_t slotNo, uint8_t *data, uint32_t len)
|
|
{
|
|
assert(data != nullptr);
|
|
|
|
switch (slotNo) {
|
|
case 1U:
|
|
return m_slot1->processFrame(data, len);
|
|
case 2U:
|
|
return m_slot2->processFrame(data, len);
|
|
default:
|
|
LogError(LOG_DMR, "DMR, invalid slot, slotNo = %u", slotNo);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/* Get the frame data length for the next frame in the data ring buffer. */
|
|
|
|
uint32_t Control::peekFrameLength(uint32_t slotNo)
|
|
{
|
|
switch (slotNo) {
|
|
case 1U:
|
|
return m_slot1->peekFrameLength();
|
|
case 2U:
|
|
return m_slot2->peekFrameLength();
|
|
default:
|
|
LogError(LOG_DMR, "DMR, invalid slot, slotNo = %u", slotNo);
|
|
return 0U;
|
|
}
|
|
}
|
|
|
|
/* Helper to determine whether or not the internal frame queue is full. */
|
|
|
|
bool Control::isQueueFull(uint32_t slotNo)
|
|
{
|
|
switch (slotNo) {
|
|
case 1U:
|
|
return m_slot1->isQueueFull();
|
|
case 2U:
|
|
return m_slot2->isQueueFull();
|
|
default:
|
|
LogError(LOG_DMR, "DMR, invalid slot, slotNo = %u", slotNo);
|
|
return true;
|
|
}
|
|
}
|
|
|
|
/* Get a data frame for slot, from data ring buffer. */
|
|
|
|
uint32_t Control::getFrame(uint32_t slotNo, uint8_t* data, bool *imm)
|
|
{
|
|
assert(data != nullptr);
|
|
|
|
switch (slotNo) {
|
|
case 1U:
|
|
return m_slot1->getFrame(data, imm);
|
|
case 2U:
|
|
return m_slot2->getFrame(data, imm);
|
|
default:
|
|
LogError(LOG_DMR, "DMR, invalid slot, slotNo = %u", slotNo);
|
|
return 0U;
|
|
}
|
|
}
|
|
|
|
/* Updates the processor. */
|
|
|
|
void Control::clock()
|
|
{
|
|
if (m_network != nullptr) {
|
|
processNetwork();
|
|
}
|
|
|
|
m_slot1->clock();
|
|
m_slot2->clock();
|
|
}
|
|
|
|
/* Updates the adj. site tables. */
|
|
|
|
void Control::clockSiteData(uint32_t ms)
|
|
{
|
|
m_slot1->clockSiteData(ms);
|
|
m_slot2->clockSiteData(ms);
|
|
}
|
|
|
|
/* Permits a TGID on a non-authoritative host. */
|
|
|
|
void Control::permittedTG(uint32_t dstId, uint8_t slot)
|
|
{
|
|
switch (slot) {
|
|
case 1U:
|
|
m_slot1->permittedTG(dstId);
|
|
break;
|
|
case 2U:
|
|
m_slot2->permittedTG(dstId);
|
|
break;
|
|
default:
|
|
LogError(LOG_DMR, "DMR, invalid slot, slotNo = %u", slot);
|
|
break;
|
|
}
|
|
}
|
|
|
|
/* Grants a TGID on a non-authoritative host. */
|
|
|
|
void Control::grantTG(uint32_t srcId, uint32_t dstId, uint8_t slot, bool grp)
|
|
{
|
|
switch (slot) {
|
|
case 1U:
|
|
m_slot1->grantTG(srcId, dstId, grp);
|
|
break;
|
|
case 2U:
|
|
m_slot2->grantTG(srcId, dstId, grp);
|
|
break;
|
|
default:
|
|
LogError(LOG_DMR, "DMR, invalid slot, slotNo = %u", slot);
|
|
break;
|
|
}
|
|
}
|
|
|
|
/* Gets instance of the AffiliationLookup class. */
|
|
|
|
dmr::lookups::DMRAffiliationLookup* Control::affiliations()
|
|
{
|
|
switch (m_tsccSlotNo) {
|
|
case 1U:
|
|
return m_slot1->s_affiliations;
|
|
case 2U:
|
|
return m_slot2->s_affiliations;
|
|
default:
|
|
LogError(LOG_DMR, "DMR, invalid slot, slotNo = %u", m_tsccSlotNo);
|
|
break;
|
|
}
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
/* Returns the current operating RF state of the DMR controller. */
|
|
|
|
RPT_RF_STATE Control::getRFState(uint32_t slotNo) const
|
|
{
|
|
switch (slotNo) {
|
|
case 1U:
|
|
return m_slot1->getRFState();
|
|
case 2U:
|
|
return m_slot2->getRFState();
|
|
default:
|
|
LogError(LOG_DMR, "DMR, invalid slot, slotNo = %u", slotNo);
|
|
break;
|
|
}
|
|
|
|
return RS_RF_INVALID;
|
|
}
|
|
|
|
/* Clears the current operating RF state back to idle (with no data reset!). */
|
|
|
|
void Control::clearRFReject(uint32_t slotNo)
|
|
{
|
|
switch (slotNo) {
|
|
case 1U:
|
|
return m_slot1->clearRFReject();
|
|
case 2U:
|
|
return m_slot2->clearRFReject();
|
|
default:
|
|
LogError(LOG_DMR, "DMR, invalid slot, slotNo = %u", slotNo);
|
|
break;
|
|
}
|
|
}
|
|
|
|
/* Helper to return the slot carrying the TSCC. */
|
|
|
|
Slot* Control::getTSCCSlot() const
|
|
{
|
|
switch (m_tsccSlotNo) {
|
|
case 1U:
|
|
return m_slot1;
|
|
break;
|
|
case 2U:
|
|
return m_slot2;
|
|
break;
|
|
default:
|
|
return nullptr;
|
|
}
|
|
}
|
|
|
|
/* Helper to payload activate the slot carrying granted payload traffic. */
|
|
|
|
void Control::tsccActivateSlot(uint32_t slotNo, uint32_t dstId, uint32_t srcId, bool group, bool voice)
|
|
{
|
|
if (m_verbose) {
|
|
LogInfoEx(LOG_DMR, "DMR Slot %u, payload activation, srcId = %u, group = %u, dstId = %u",
|
|
slotNo, srcId, group, dstId);
|
|
}
|
|
|
|
// never allow the TSCC to become payload activated
|
|
if (m_tsccSlotNo == slotNo) {
|
|
LogError(LOG_DMR, "DMR, cowardly refusing to, TSCC payload activation, slotNo = %u", slotNo);
|
|
return;
|
|
}
|
|
|
|
switch (slotNo) {
|
|
case 1U:
|
|
m_tsccPayloadActive = true;
|
|
m_slot1->setTSCCActivated(dstId, srcId, group, voice);
|
|
break;
|
|
case 2U:
|
|
m_tsccPayloadActive = true;
|
|
m_slot2->setTSCCActivated(dstId, srcId, group, voice);
|
|
break;
|
|
default:
|
|
LogError(LOG_DMR, "DMR, invalid slot, TSCC payload activation, slotNo = %u", slotNo);
|
|
break;
|
|
}
|
|
}
|
|
|
|
/* Helper to clear an activated payload slot. */
|
|
|
|
void Control::tsccClearActivatedSlot(uint32_t slotNo)
|
|
{
|
|
if (m_verbose) {
|
|
LogInfoEx(LOG_DMR, "DMR Slot %u, payload activation clear", slotNo);
|
|
}
|
|
|
|
switch (slotNo) {
|
|
case 1U:
|
|
m_modem->clearDMRFrame1();
|
|
m_slot1->clearTSCCActivated();
|
|
break;
|
|
case 2U:
|
|
m_modem->clearDMRFrame2();
|
|
m_slot2->clearTSCCActivated();
|
|
break;
|
|
default:
|
|
LogError(LOG_DMR, "DMR, invalid slot, TSCC payload activation, slotNo = %u", slotNo);
|
|
break;
|
|
}
|
|
|
|
if (m_tsccPayloadActive && m_slot1->m_tsccPayloadDstId == 0U && m_slot2->m_tsccPayloadDstId == 0U) {
|
|
m_tsccPayloadActive = false;
|
|
}
|
|
}
|
|
|
|
/* Helper to write a DMR extended function packet on the RF interface. */
|
|
|
|
void Control::writeRF_Ext_Func(uint32_t slotNo, uint32_t func, uint32_t arg, uint32_t dstId)
|
|
{
|
|
switch (slotNo) {
|
|
case 1U:
|
|
m_slot1->control()->writeRF_Ext_Func(func, arg, dstId);
|
|
break;
|
|
case 2U:
|
|
m_slot2->control()->writeRF_Ext_Func(func, arg, dstId);
|
|
break;
|
|
default:
|
|
LogError(LOG_DMR, "DMR, invalid slot, slotNo = %u", slotNo);
|
|
break;
|
|
}
|
|
}
|
|
|
|
/* Helper to write a DMR call alert packet on the RF interface. */
|
|
|
|
void Control::writeRF_Call_Alrt(uint32_t slotNo, uint32_t srcId, uint32_t dstId)
|
|
{
|
|
switch (slotNo) {
|
|
case 1U:
|
|
m_slot1->control()->writeRF_Call_Alrt(srcId, dstId);
|
|
break;
|
|
case 2U:
|
|
m_slot2->control()->writeRF_Call_Alrt(srcId, dstId);
|
|
break;
|
|
default:
|
|
LogError(LOG_DMR, "DMR, invalid slot, slotNo = %u", slotNo);
|
|
break;
|
|
}
|
|
}
|
|
|
|
/* Flag indicating whether the processor or is busy or not. */
|
|
|
|
bool Control::isBusy() const
|
|
{
|
|
return (m_slot1->m_rfState != RS_RF_LISTENING || m_slot1->m_netState != RS_NET_IDLE) ||
|
|
(m_slot2->m_rfState != RS_RF_LISTENING || m_slot2->m_netState != RS_NET_IDLE);
|
|
}
|
|
|
|
/* Helper to change the debug and verbose state. */
|
|
|
|
void Control::setDebugVerbose(bool debug, bool verbose)
|
|
{
|
|
m_debug = debug;
|
|
m_verbose = verbose;
|
|
m_slot1->setDebugVerbose(debug, verbose);
|
|
m_slot2->setDebugVerbose(debug, verbose);
|
|
}
|
|
|
|
/* Helper to change the CSBK verbose state. */
|
|
|
|
void Control::setCSBKVerbose(bool verbose)
|
|
{
|
|
m_dumpCSBKData = verbose;
|
|
lc::CSBK::setVerbose(verbose);
|
|
}
|
|
|
|
/* Helper to get the last transmitted destination ID. */
|
|
|
|
uint32_t Control::getLastDstId(uint32_t slotNo) const
|
|
{
|
|
switch (slotNo) {
|
|
case 1U:
|
|
return m_slot1->getLastDstId();
|
|
case 2U:
|
|
return m_slot2->getLastDstId();
|
|
default:
|
|
LogError(LOG_DMR, "DMR, invalid slot, slotNo = %u", slotNo);
|
|
break;
|
|
}
|
|
|
|
return 0U;
|
|
}
|
|
|
|
/* Helper to get the last transmitted source ID. */
|
|
|
|
uint32_t Control::getLastSrcId(uint32_t slotNo) const
|
|
{
|
|
switch (slotNo) {
|
|
case 1U:
|
|
return m_slot1->getLastSrcId();
|
|
case 2U:
|
|
return m_slot2->getLastSrcId();
|
|
default:
|
|
LogError(LOG_DMR, "DMR, invalid slot, slotNo = %u", slotNo);
|
|
break;
|
|
}
|
|
|
|
return 0U;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Private Class Members
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/* Process a data frames from the network. */
|
|
|
|
void Control::processNetwork()
|
|
{
|
|
uint32_t length = 0U;
|
|
bool ret = false;
|
|
UInt8Array buffer = m_network->readDMR(ret, length);
|
|
if (!ret)
|
|
return;
|
|
if (length == 0U)
|
|
return;
|
|
if (buffer == nullptr) {
|
|
return;
|
|
}
|
|
|
|
data::NetData data;
|
|
|
|
// process network message header
|
|
uint8_t seqNo = buffer[4U];
|
|
|
|
uint32_t srcId = GET_UINT24(buffer, 5U);
|
|
uint32_t dstId = GET_UINT24(buffer, 8U);
|
|
|
|
uint8_t controlByte = buffer[14U];
|
|
|
|
FLCO::E flco = (buffer[15U] & 0x40U) == 0x40U ? FLCO::PRIVATE : FLCO::GROUP;
|
|
|
|
uint32_t slotNo = (buffer[15U] & 0x80U) == 0x80U ? 2U : 1U;
|
|
|
|
if (slotNo > 3U) {
|
|
LogError(LOG_DMR, "DMR, invalid slot, slotNo = %u", slotNo);
|
|
return;
|
|
}
|
|
|
|
// DMO mode slot disabling
|
|
if (slotNo == 1U && !m_network->getDuplex()) {
|
|
LogError(LOG_DMR, "DMR/DMO, invalid slot, slotNo = %u", slotNo);
|
|
return;
|
|
}
|
|
|
|
// Individual slot disabling
|
|
if (slotNo == 1U && !m_network->getSlot1()) {
|
|
LogError(LOG_DMR, "DMR, invalid slot, slot 1 disabled, slotNo = %u", slotNo);
|
|
return;
|
|
}
|
|
if (slotNo == 2U && !m_network->getSlot2()) {
|
|
LogError(LOG_DMR, "DMR, invalid slot, slot 2 disabled, slotNo = %u", slotNo);
|
|
return;
|
|
}
|
|
|
|
data.setSeqNo(seqNo);
|
|
data.setSlotNo(slotNo);
|
|
data.setSrcId(srcId);
|
|
data.setDstId(dstId);
|
|
data.setFLCO(flco);
|
|
|
|
data.setControl(controlByte);
|
|
|
|
bool dataSync = (buffer[15U] & 0x20U) == 0x20U;
|
|
bool voiceSync = (buffer[15U] & 0x10U) == 0x10U;
|
|
|
|
if (m_debug) {
|
|
LogDebug(LOG_NET, "DMR, seqNo = %u, srcId = %u, dstId = %u, flco = $%02X, slotNo = %u, len = %u", seqNo, srcId, dstId, flco, slotNo, length);
|
|
}
|
|
|
|
// process raw DMR data bytes
|
|
if (dataSync) {
|
|
DataType::E dataType = (DataType::E)(buffer[15U] & 0x0FU);
|
|
data.setData(buffer.get() + 20U);
|
|
data.setDataType(dataType);
|
|
data.setN(0U);
|
|
}
|
|
else if (voiceSync) {
|
|
data.setData(buffer.get() + 20U);
|
|
data.setDataType(DataType::VOICE_SYNC);
|
|
data.setN(0U);
|
|
}
|
|
else {
|
|
uint8_t n = buffer[15U] & 0x0FU;
|
|
data.setData(buffer.get() + 20U);
|
|
data.setDataType(DataType::VOICE);
|
|
data.setN(n);
|
|
}
|
|
|
|
// are we TSCC enabled?
|
|
if (m_enableTSCC) {
|
|
Slot* tscc = getTSCCSlot();
|
|
if (tscc != nullptr) {
|
|
if (!tscc->s_affiliations->isGranted(dstId)) {
|
|
tscc->m_control->writeRF_CSBK_Grant(srcId, dstId, 4U, (flco == FLCO::GROUP) ? true : false, true);
|
|
}
|
|
}
|
|
}
|
|
|
|
// forward onto the specific slot for final processing and delivery
|
|
switch (slotNo) {
|
|
case 1U:
|
|
m_slot1->processNetwork(data);
|
|
break;
|
|
case 2U:
|
|
m_slot2->processNetwork(data);
|
|
break;
|
|
}
|
|
}
|
|
|
|
/* Helper to process a registration authentication key. */
|
|
|
|
void Control::processRegistrationAuthKey(uint32_t srcId, const p25::kmm::KeyItem& key, uint8_t algId, uint8_t keyLength)
|
|
{
|
|
(void)algId; // The FNE labels this shared 16-byte key as the P25 AES-128 KEK type.
|
|
Slot* tscc = getTSCCSlot();
|
|
if (tscc == nullptr)
|
|
return;
|
|
|
|
auto it = tscc->m_authTransactions.find(srcId);
|
|
if (it == tscc->m_authTransactions.end())
|
|
return;
|
|
|
|
if (keyLength != 16U) {
|
|
tscc->m_authTransactions.erase(it);
|
|
tscc->m_control->writeRF_CSBK_NACK_RSP(srcId, ReasonCode::TS_DENY_RSN_REG_DENIED, ServiceKind::REG_SVC);
|
|
return;
|
|
}
|
|
|
|
uint8_t keyData[16U];
|
|
::memset(keyData, 0U, 16U);
|
|
key.getKey(keyData);
|
|
|
|
static std::random_device random;
|
|
std::uniform_int_distribution<uint32_t> distribution(0U, 0xFFFCDFU);
|
|
it->second.challenge = distribution(random);
|
|
it->second.expectedResponse = calculateRegistrationAuthResponse(it->second.challenge, keyData);
|
|
it->second.challengeSent = true;
|
|
it->second.expires = std::chrono::steady_clock::now() + std::chrono::seconds(30);
|
|
|
|
tscc->m_control->writeRF_CSBK_AHOY(srcId, ServiceKind::REG_SVC, it->second.challenge, false, 0U);
|
|
}
|
|
|
|
/* Helper to process an In-Call Control message. */
|
|
|
|
void Control::processInCallCtrl(network::NET_ICC::ENUM command, uint32_t dstId, uint8_t slotNo)
|
|
{
|
|
switch (slotNo) {
|
|
case 1U:
|
|
return m_slot1->processInCallCtrl(command, dstId);
|
|
case 2U:
|
|
return m_slot2->processInCallCtrl(command, dstId);
|
|
default:
|
|
LogError(LOG_DMR, "DMR, invalid slot, slotNo = %u", slotNo);
|
|
break;
|
|
}
|
|
}
|
|
|
|
/* (RPC Handler) Permits a TGID on a non-authoritative host. */
|
|
|
|
void Control::RPC_permittedTG(json::object& req, json::object& reply)
|
|
{
|
|
g_RPC->defaultResponse(reply, "OK", network::NetRPC::OK);
|
|
|
|
// validate destination ID is a integer within the JSON blob
|
|
if (!req["dstId"].is<int>()) {
|
|
g_RPC->defaultResponse(reply, "destination ID was not a valid integer", network::NetRPC::INVALID_ARGS);
|
|
return;
|
|
}
|
|
|
|
uint32_t dstId = req["dstId"].get<uint32_t>();
|
|
|
|
// validate slot is a integer within the JSON blob
|
|
if (!req["slot"].is<int>()) {
|
|
g_RPC->defaultResponse(reply, "slot was not a valid integer", network::NetRPC::INVALID_ARGS);
|
|
return;
|
|
}
|
|
|
|
uint8_t slot = (uint8_t)req["slot"].get<int>();
|
|
|
|
if (slot == 0U || slot > 2U) {
|
|
g_RPC->defaultResponse(reply, "illegal DMR slot");
|
|
return;
|
|
}
|
|
|
|
// LogDebugEx(LOG_DMR, "Control::RPC_permittedTG()", "callback, dstId = %u, slot = %u", dstId, slot);
|
|
|
|
permittedTG(dstId, slot);
|
|
}
|
|
|
|
/* (RPC Handler) Releases a granted TG. */
|
|
|
|
void Control::RPC_releaseGrantTG(json::object& req, json::object& reply)
|
|
{
|
|
g_RPC->defaultResponse(reply, "OK", network::NetRPC::OK);
|
|
|
|
// validate destination ID is a integer within the JSON blob
|
|
if (!req["dstId"].is<int>()) {
|
|
g_RPC->defaultResponse(reply, "destination ID was not a valid integer", network::NetRPC::INVALID_ARGS);
|
|
return;
|
|
}
|
|
|
|
uint32_t dstId = req["dstId"].get<uint32_t>();
|
|
|
|
if (dstId == 0U) {
|
|
g_RPC->defaultResponse(reply, "destination ID is an illegal TGID");
|
|
return;
|
|
}
|
|
|
|
// validate slot is a integer within the JSON blob
|
|
if (!req["slot"].is<int>()) {
|
|
g_RPC->defaultResponse(reply, "slot was not a valid integer", network::NetRPC::INVALID_ARGS);
|
|
return;
|
|
}
|
|
|
|
uint8_t slot = (uint8_t)req["slot"].get<int>();
|
|
|
|
if (slot == 0U || slot > 2U) {
|
|
g_RPC->defaultResponse(reply, "illegal DMR slot");
|
|
return;
|
|
}
|
|
|
|
// LogDebugEx(LOG_DMR, "Control::RPC_releaseGrantTG()", "callback, dstId = %u, slot = %u", dstId, slot);
|
|
|
|
switch (slot) {
|
|
case 1U:
|
|
m_slot1->releaseGrantTG(dstId);
|
|
break;
|
|
case 2U:
|
|
m_slot2->releaseGrantTG(dstId);
|
|
break;
|
|
default:
|
|
LogError(LOG_DMR, "DMR, invalid slot, slotNo = %u", slot);
|
|
break;
|
|
}
|
|
}
|
|
|
|
/* (RPC Handler) Touchs a granted TG to keep a channel grant alive. */
|
|
|
|
void Control::RPC_touchGrantTG(json::object& req, json::object& reply)
|
|
{
|
|
g_RPC->defaultResponse(reply, "OK", network::NetRPC::OK);
|
|
|
|
// validate destination ID is a integer within the JSON blob
|
|
if (!req["dstId"].is<int>()) {
|
|
g_RPC->defaultResponse(reply, "destination ID was not a valid integer", network::NetRPC::INVALID_ARGS);
|
|
return;
|
|
}
|
|
|
|
uint32_t dstId = req["dstId"].get<uint32_t>();
|
|
|
|
if (dstId == 0U) {
|
|
g_RPC->defaultResponse(reply, "destination ID is an illegal TGID");
|
|
return;
|
|
}
|
|
|
|
// validate slot is a integer within the JSON blob
|
|
if (!req["slot"].is<int>()) {
|
|
g_RPC->defaultResponse(reply, "slot was not a valid integer", network::NetRPC::INVALID_ARGS);
|
|
return;
|
|
}
|
|
|
|
uint8_t slot = (uint8_t)req["slot"].get<int>();
|
|
|
|
if (slot == 0U || slot > 2U) {
|
|
g_RPC->defaultResponse(reply, "illegal DMR slot");
|
|
return;
|
|
}
|
|
|
|
Slot* tscc = getTSCCSlot();
|
|
if (tscc != nullptr && tscc->m_enableTSCC && !tscc->s_affiliations->isGranted(dstId) &&
|
|
req["srcId"].is<int>() && req["group"].is<bool>() && req["chNo"].is<int>()) {
|
|
uint32_t srcId = req["srcId"].get<uint32_t>();
|
|
bool group = req["group"].get<bool>();
|
|
uint32_t chNo = req["chNo"].get<uint32_t>();
|
|
::lookups::VoiceChData voiceCh = tscc->s_affiliations->rfCh()->getRFChData(chNo);
|
|
if (!voiceCh.isValidCh()) {
|
|
g_RPC->defaultResponse(reply, "DMR payload channel is not configured", network::NetRPC::INVALID_ARGS);
|
|
return;
|
|
}
|
|
if (!tscc->m_control->writeRF_CSBK_Grant(srcId, dstId, 4U, group,
|
|
true, false, chNo, slot)) {
|
|
g_RPC->defaultResponse(reply, "failed to recreate DMR payload grant", network::NetRPC::BAD_REQUEST);
|
|
return;
|
|
}
|
|
}
|
|
|
|
// LogDebugEx(LOG_DMR, "Control::RPC_touchGrantTG()", "callback, dstId = %u, slot = %u", dstId, slot);
|
|
|
|
switch (slot) {
|
|
case 1U:
|
|
m_slot1->touchGrantTG(dstId);
|
|
break;
|
|
case 2U:
|
|
m_slot2->touchGrantTG(dstId);
|
|
break;
|
|
default:
|
|
LogError(LOG_DMR, "DMR, invalid slot, slotNo = %u", slot);
|
|
break;
|
|
}
|
|
}
|
|
|
|
/* (RPC Handler) Helper to payload activate the slot carrying granted payload traffic. */
|
|
|
|
void Control::RPC_tsccPayloadActivate(json::object& req, json::object& reply)
|
|
{
|
|
g_RPC->defaultResponse(reply, "OK", network::NetRPC::OK);
|
|
|
|
// validate destination ID is a integer within the JSON blob
|
|
if (!req["slot"].is<uint8_t>()) {
|
|
g_RPC->defaultResponse(reply, "slot was not valid", network::NetRPC::INVALID_ARGS);
|
|
return;
|
|
}
|
|
|
|
// validate clear flag is a boolean within the JSON blob
|
|
if (!req["clear"].is<bool>()) {
|
|
g_RPC->defaultResponse(reply, "clear flag was not valid", network::NetRPC::INVALID_ARGS);
|
|
return;
|
|
}
|
|
|
|
uint8_t slot = req["slot"].get<uint8_t>();
|
|
bool clear = req["clear"].get<bool>();
|
|
|
|
if (slot == 0U || slot >= 3U) {
|
|
g_RPC->defaultResponse(reply, "invalid DMR slot number (slot == 0 or slot > 3)");
|
|
return;
|
|
}
|
|
|
|
if (clear) {
|
|
tsccClearActivatedSlot(slot);
|
|
}
|
|
else {
|
|
// validate destination ID is a integer within the JSON blob
|
|
if (!req["dstId"].is<uint32_t>()) {
|
|
g_RPC->defaultResponse(reply, "destination ID was not valid", network::NetRPC::INVALID_ARGS);
|
|
return;
|
|
}
|
|
|
|
// validate destination ID is a integer within the JSON blob
|
|
if (!req["srcId"].is<uint32_t>()) {
|
|
g_RPC->defaultResponse(reply, "source ID was not valid", network::NetRPC::INVALID_ARGS);
|
|
return;
|
|
}
|
|
|
|
// validate group flag is a boolean within the JSON blob
|
|
if (!req["group"].is<bool>()) {
|
|
g_RPC->defaultResponse(reply, "group flag was not valid", network::NetRPC::INVALID_ARGS);
|
|
return;
|
|
}
|
|
|
|
// validate voice flag is a boolean within the JSON blob
|
|
if (!req["voice"].is<bool>()) {
|
|
g_RPC->defaultResponse(reply, "voice flag was not valid", network::NetRPC::INVALID_ARGS);
|
|
return;
|
|
}
|
|
|
|
uint32_t dstId = req["dstId"].get<uint32_t>();
|
|
uint32_t srcId = req["srcId"].get<uint32_t>();
|
|
bool group = req["group"].get<bool>();
|
|
bool voice = req["voice"].get<bool>();
|
|
|
|
if (dstId == 0U) {
|
|
g_RPC->defaultResponse(reply, "destination ID was not valid");
|
|
return;
|
|
}
|
|
|
|
tsccActivateSlot(slot, dstId, srcId, group, voice);
|
|
}
|
|
}
|