diff --git a/configs/config.example.yml b/configs/config.example.yml index c156a096..42bf7f4e 100644 --- a/configs/config.example.yml +++ b/configs/config.example.yml @@ -485,10 +485,32 @@ system: # Site Configuration # config: - # Flag indicating whether or not this host is authoritative (i.e. will automatically handle channel grant) for TGID operations. + # Controls how traffic is authorized and, for trunking control channels, + # which who owns channel-grant transactions (either the local host or the FNE). + # + # Role / operating mode Authoritative + # Standalone conventional repeater true + # Standalone DVRS true + # Locally managed trunking control channel true + # FNE-managed trunking control channel false + # Trunked voice channel slaved to its CC false + # + # true: This host makes local traffic-admission decisions. A trunking + # control channel also allocates and grants its own voice channels. + # false: Traffic must be explicitly permitted by the managing system. A + # trunking control channel delegates grant transactions to the FNE + # while the FNE connection is active, and falls back to local grant + # handling when that connection is unavailable. + # + # IMPORTANT FOR TRUNKED VOICE CHANNELS: + # When controlCh.rpcAddress and controlCh.rpcPort identify a control channel + # and authoritative is false, this voice channel is slaved to that control + # channel for traffic permits. It accepts only destinations the control + # channel has permitted for an active grant, and clears the permit when the + # grant is released. The control channel automatically permits the selected + # remote voice channel before emitting a grant; no separate permit-control + # flag is required. authoritative: true - # Flag indicating whether or not this host acts as a supervisor (i.e. will automatically handle permission) for voice channel TGID permission. - supervisor: false # Channel Identity (corresponds to the appropriate entry in the iden_table file). # Note: When using explicit channels, this represents the transmit channel number. diff --git a/docs/TN.1101 - DVMHost REST API Documentation.md b/docs/TN.1101 - DVMHost REST API Documentation.md index f2b3d765..bd050bba 100644 --- a/docs/TN.1101 - DVMHost REST API Documentation.md +++ b/docs/TN.1101 - DVMHost REST API Documentation.md @@ -14,7 +14,7 @@ AI WARNING: This document was mainly generated using AI assistance. As such, the 2. [Authentication](#2-authentication) 3. [System Endpoints](#3-system-endpoints) 4. [Modem Control](#4-modem-control) -5. [Trunking and Supervisory Control](#5-trunking-and-supervisory-control) +5. [Trunking and Traffic Permit Control](#5-trunking-and-traffic-permit-control) 6. [Radio ID Lookup](#6-radio-id-lookup) 7. [DMR Protocol Endpoints](#7-dmr-protocol-endpoints) 8. [P25 Protocol Endpoints](#8-p25-protocol-endpoints) @@ -533,99 +533,9 @@ Content-Type: application/json --- -## 5. Trunking and Supervisory Control +## 5. Trunking and Traffic Permit Control -### 5.1 Endpoint: PUT /set-supervisor - -**Method:** `PUT` - -**Description:** Enable or disable supervisory (trunking) mode for the host. - -**Request Headers:** -``` -X-DVM-Auth-Token: {token} -Content-Type: application/json -``` - -**Request Body:** -```json -{ - "state": 4, - "enable": true -} -``` - -**Request Fields:** -- `state` (required, integer): Protocol state value - - `4` = DMR - - `5` = P25 - - `6` = NXDN -- `enable` (required, boolean): Enable (`true`) or disable (`false`) supervisory mode - -**Response (Success):** -```json -{ - "status": 200, - "message": "OK" -} -``` - -**Response Fields:** -- `status`: HTTP status code (200) -- `message`: Always `"OK"` - -**Error Responses:** - -**Host Not Authoritative:** -```json -{ - "status": 400, - "message": "Host is not authoritative, cannot set supervisory state" -} -``` - -**Invalid State Parameter:** -```json -{ - "status": 400, - "message": "state was not a valid integer" -} -``` - -**Invalid Enable Parameter:** -```json -{ - "status": 400, - "message": "enable was not a boolean" -} -``` - -**Invalid State Value:** -```json -{ - "status": 400, - "message": "invalid mode" -} -``` - -**Protocol Not Enabled:** -```json -{ - "status": 503, - "message": "DMR mode is not enabled" -} -``` -*Similar messages for P25 and NXDN protocols* - -**Notes:** -- Only available when host is configured as authoritative (`authoritative: true` in config) -- Supervisory mode enables trunking control features for the specified protocol -- The host must have the requested protocol enabled in its configuration -- Each protocol (DMR, P25, NXDN) has independent supervisory mode settings - ---- - -### 5.2 Endpoint: PUT /permit-tg +### 5.1 Endpoint: PUT /permit-tg **Method:** `PUT` @@ -756,7 +666,7 @@ Content-Type: application/json --- -### 5.3 Endpoint: PUT /grant-tg +### 5.2 Endpoint: PUT /grant-tg **Method:** `PUT` @@ -922,7 +832,7 @@ Content-Type: application/json --- -### 5.4 Endpoint: GET /release-grants +### 5.3 Endpoint: GET /release-grants **Method:** `GET` @@ -961,7 +871,7 @@ X-DVM-Auth-Token: {token} --- -### 5.5 Endpoint: GET /release-affs +### 5.4 Endpoint: GET /release-affs **Method:** `GET` @@ -2995,14 +2905,6 @@ or } ``` -**Unauthorized Operation:** -```json -{ - "status": 400, - "message": "Host is not authoritative, cannot set supervisory state" -} -``` - **Protocol Conflicts:** ```json { @@ -3108,7 +3010,7 @@ or - **Mode changes:** Use caution when changing modes during active traffic - **Kill command:** Restricted to authorized administrators -- **Supervisory mode:** Only enable on authoritative/master hosts +- **Traffic permits:** Restrict permit operations to trusted control channels and management systems - **Raw TSBK:** Advanced feature requiring protocol knowledge --- @@ -3805,7 +3707,6 @@ if __name__ == "__main__": | GET | /voice-ch | Get voice channel states | Yes | N/A | | PUT | /mdm/mode | Set modem mode | Yes | N/A | | PUT | /mdm/kill | Shutdown host | Yes | N/A | -| PUT | /set-supervisor | Set supervisory mode | Yes | N/A | | PUT | /permit-tg | Permit talkgroup | Yes | All | | PUT | /grant-tg | Grant voice channel | Yes | All | | GET | /release-grants | Release all grants | Yes | All | diff --git a/src/common/dmr/lc/csbk/CSBKFactory.cpp b/src/common/dmr/lc/csbk/CSBKFactory.cpp index 3a03edb3..14002803 100644 --- a/src/common/dmr/lc/csbk/CSBKFactory.cpp +++ b/src/common/dmr/lc/csbk/CSBKFactory.cpp @@ -128,6 +128,11 @@ std::unique_ptr CSBKFactory::createCSBK(const uint8_t* data, DataType::E d return decode(new CSBK_BROADCAST(), data, dataType); case CSBKO::MAINT: return decode(new CSBK_MAINT(), data, dataType); + case CSBKO::TV_GRANT: + case CSBKO::BTV_GRANT: + return decode(new CSBK_TV_GRANT(), data, dataType); + case CSBKO::PV_GRANT: + return decode(new CSBK_PV_GRANT(), data, dataType); default: LogError(LOG_DMR, "CSBKFactory::create(), unknown CSBK type, csbko = $%02X", CSBKO); diff --git a/src/common/dmr/lc/csbk/CSBK_PV_GRANT.cpp b/src/common/dmr/lc/csbk/CSBK_PV_GRANT.cpp index 1ed6d272..14507206 100644 --- a/src/common/dmr/lc/csbk/CSBK_PV_GRANT.cpp +++ b/src/common/dmr/lc/csbk/CSBK_PV_GRANT.cpp @@ -4,7 +4,7 @@ * GPLv2 Open Source. Use is subject to license terms. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * - * Copyright (C) 2022,2024 Bryan Biedenkapp, N2PLL + * Copyright (C) 2022,2024,2026 Bryan Biedenkapp, N2PLL * */ #include "Defines.h" @@ -34,7 +34,20 @@ bool CSBK_PV_GRANT::decode(const uint8_t* data) { assert(data != nullptr); - /* stub */ + uint8_t csbk[DMR_CSBK_LENGTH_BYTES]; + ::memset(csbk, 0x00U, DMR_CSBK_LENGTH_BYTES); + + if (!CSBK::decode(data, csbk)) + return false; + + const ulong64_t csbkValue = CSBK::toValue(csbk); + m_logicalCh1 = (uint32_t)((csbkValue >> 52) & 0xFFFU); // Logical Physical Channel 1 + m_slotNo = ((csbkValue >> 51) & 0x01U) == 0x01U ? 2U : 1U; // Logical Slot Number + m_emergency = ((csbkValue >> 49) & 0x01U) == 0x01U; // Emergency + m_siteOffsetTiming = ((csbkValue >> 48) & 0x01U) == 0x01U; // Site Timing: Aligned or Offset + m_dstId = (uint32_t)((csbkValue >> 24) & 0xFFFFFFU); // Talkgroup ID + m_srcId = (uint32_t)(csbkValue & 0xFFFFFFU); // Source Radio Address + m_GI = false; return true; } diff --git a/src/common/dmr/lc/csbk/CSBK_TV_GRANT.cpp b/src/common/dmr/lc/csbk/CSBK_TV_GRANT.cpp index 9564e6b4..1a5830f0 100644 --- a/src/common/dmr/lc/csbk/CSBK_TV_GRANT.cpp +++ b/src/common/dmr/lc/csbk/CSBK_TV_GRANT.cpp @@ -27,6 +27,7 @@ CSBK_TV_GRANT::CSBK_TV_GRANT() : CSBK(), m_lateEntry(false) { m_CSBKO = CSBKO::TV_GRANT; + m_GI = true; } /* Decode a control signalling block. */ @@ -35,7 +36,21 @@ bool CSBK_TV_GRANT::decode(const uint8_t* data) { assert(data != nullptr); - /* stub */ + uint8_t csbk[DMR_CSBK_LENGTH_BYTES]; + ::memset(csbk, 0x00U, DMR_CSBK_LENGTH_BYTES); + + if (!CSBK::decode(data, csbk)) + return false; + + const ulong64_t csbkValue = CSBK::toValue(csbk); + m_logicalCh1 = (uint32_t)((csbkValue >> 52) & 0xFFFU); // Logical Physical Channel 1 + m_slotNo = ((csbkValue >> 51) & 0x01U) == 0x01U ? 2U : 1U; // Logical Slot Number + m_lateEntry = ((csbkValue >> 50) & 0x01U) == 0x01U; // Late Entry + m_emergency = ((csbkValue >> 49) & 0x01U) == 0x01U; // Emergency + m_siteOffsetTiming = ((csbkValue >> 48) & 0x01U) == 0x01U; // Site Timing: Aligned or Offset + m_dstId = (uint32_t)((csbkValue >> 24) & 0xFFFFFFU); // Talkgroup ID + m_srcId = (uint32_t)(csbkValue & 0xFFFFFFU); // Source Radio Address + m_GI = true; return true; } diff --git a/src/common/network/BaseNetwork.cpp b/src/common/network/BaseNetwork.cpp index d124a5b2..a160a4fd 100644 --- a/src/common/network/BaseNetwork.cpp +++ b/src/common/network/BaseNetwork.cpp @@ -111,8 +111,8 @@ bool BaseNetwork::writeGrantReq(const uint8_t mode, const uint32_t srcId, const uint8_t buffer[MSG_HDR_SIZE]; ::memset(buffer, 0x00U, MSG_HDR_SIZE); - SET_UINT32(srcId, buffer, 11U); // Source Address - SET_UINT32(dstId, buffer, 15U); // Destination Address + SET_UINT24(srcId, buffer, 11U); // Source Address + SET_UINT24(dstId, buffer, 15U); // Destination Address buffer[19U] = slot; // Slot Number if (unitToUnit) @@ -124,6 +124,32 @@ bool BaseNetwork::writeGrantReq(const uint8_t mode, const uint32_t srcId, const return writeMaster({ NET_FUNC::GRANT_REQ, NET_SUBFUNC::NOP }, buffer, MSG_HDR_SIZE, RTP_END_OF_CALL_SEQ, 0U); } +/* Writes a grant release to the network. */ + +bool BaseNetwork::writeGrantRelease(const uint8_t mode, const uint32_t srcId, const uint32_t dstId, + const uint8_t slot, const bool unitToUnit, const bool failed) +{ + if (m_status != NET_STAT_RUNNING && m_status != NET_STAT_MST_RUNNING) + return false; + + uint8_t buffer[MSG_HDR_SIZE]; + ::memset(buffer, 0x00U, MSG_HDR_SIZE); + + SET_UINT24(srcId, buffer, 11U); + SET_UINT24(dstId, buffer, 15U); + buffer[19U] = slot; + + if (unitToUnit) + buffer[19U] |= 0x80U; + + buffer[20U] = mode; + + if (failed) + buffer[21U] |= NET_GRANT_RELEASE_FAILED; + + return writeMaster({ NET_FUNC::GRANT_REL, NET_SUBFUNC::NOP }, buffer, MSG_HDR_SIZE, RTP_END_OF_CALL_SEQ, 0U); +} + /* Writes enc. key request to the network. */ bool BaseNetwork::writeKeyReq(const uint16_t kId, const uint8_t algId, const uint32_t srcId) diff --git a/src/common/network/BaseNetwork.h b/src/common/network/BaseNetwork.h index 7a4a7570..a724f163 100644 --- a/src/common/network/BaseNetwork.h +++ b/src/common/network/BaseNetwork.h @@ -159,6 +159,15 @@ namespace network NET_CTRL_U2U = 0x01U, //!< Unit-to-Unit }; + /** + * @brief Grant-release status flags. + * @note These flags indicate the status of a grant release operation. + * @ingroup network_core + */ + enum GRANT_RELEASE_FLAGS : uint8_t { + NET_GRANT_RELEASE_FAILED = 0x01U, //!< Local RF grant commit failed + }; + /** * @brief Peer Connection Class Enumerations * @note These values define which connection class a peer connection belongs to. This is used for determining how @@ -464,6 +473,19 @@ namespace network bool writeGrantReq(const uint8_t mode, const uint32_t srcId, const uint32_t dstId, const uint8_t slot, const bool unitToUnit, const uint8_t serviceOptions); + /** + * @brief Writes a release for a grant previously assigned by the network. + * @param mode Digital mode owning the grant. + * @param srcId Source radio ID associated with the grant. + * @param dstId Destination radio or talkgroup ID associated with the grant. + * @param slot DMR or P25 Phase 2 slot number, or zero when not applicable. + * @param unitToUnit Flag indicating an individual-call grant. + * @param failed Flag indicating local RF grant commit failed. + * @returns bool True if the grant release was sent; otherwise false. + */ + bool writeGrantRelease(const uint8_t mode, const uint32_t srcId, const uint32_t dstId, + const uint8_t slot, const bool unitToUnit, const bool failed = false); + /** * @brief Writes a enc. key request to the network. * @param kId Key ID. diff --git a/src/common/network/Network.cpp b/src/common/network/Network.cpp index 9074ac0f..9435e94b 100644 --- a/src/common/network/Network.cpp +++ b/src/common/network/Network.cpp @@ -128,6 +128,21 @@ bool Network::writeGrantReq(const uint8_t mode, const uint32_t srcId, const uint return BaseNetwork::writeGrantReq(mode, srcId, dstId, slot, unitToUnit, serviceOptions); } +/* Releases a grant previously assigned by the connected FNE. */ + +bool Network::writeGrantRelease(const uint8_t mode, const uint32_t srcId, const uint32_t dstId, + const uint8_t slot, const bool unitToUnit, const bool failed) +{ + return BaseNetwork::writeGrantRelease(mode, srcId, dstId, slot, unitToUnit, failed); +} + +/* Determines whether the connected FNE can currently own grant decisions. */ + +bool Network::hasGrantAuthority() const +{ + return m_status == NET_STAT_RUNNING || m_status == NET_STAT_MST_RUNNING; +} + /* Resets the DMR ring buffer for the given slot. */ void Network::resetDMR(uint32_t slotNo) diff --git a/src/common/network/Network.h b/src/common/network/Network.h index 4dbbb689..b90bb56c 100644 --- a/src/common/network/Network.h +++ b/src/common/network/Network.h @@ -186,6 +186,25 @@ namespace network bool writeGrantReq(const uint8_t mode, const uint32_t srcId, const uint32_t dstId, const uint8_t slot, const bool unitToUnit, const uint8_t serviceOptions); + /** + * @brief Releases a grant previously assigned by the connected FNE. + * @param mode DVM protocol mode owning the grant. + * @param srcId Source subscriber ID. + * @param dstId Destination subscriber or talkgroup ID. + * @param slot DMR or P25 Phase 2 slot number, or zero when not applicable. + * @param unitToUnit Flag indicating an individual-call grant. + * @param failed Flag indicating local RF grant commit failed. + * @return True if the release was written; otherwise false. + */ + bool writeGrantRelease(const uint8_t mode, const uint32_t srcId, const uint32_t dstId, + const uint8_t slot, const bool unitToUnit, const bool failed = false); + + /** + * @brief Determines whether the connected FNE can currently own grant decisions. + * @return True when the authenticated network session is running; otherwise false. + */ + bool hasGrantAuthority() const; + /** * @brief Resets the DMR ring buffer for the given slot. * @param slotNo DMR slot ring buffer to reset. diff --git a/src/common/network/RTPFNEHeader.h b/src/common/network/RTPFNEHeader.h index d5a7111c..d66428bb 100644 --- a/src/common/network/RTPFNEHeader.h +++ b/src/common/network/RTPFNEHeader.h @@ -60,6 +60,7 @@ namespace network PING = 0x74U, //!< Ping PONG = 0x75U, //!< Pong + GRANT_REL = 0x79U, //!< Grant Release GRANT_REQ = 0x7AU, //!< Grant Request INCALL_CTRL = 0x7BU, //!< In-Call Control KEY_REQ = 0x7CU, //!< Encryption Key TEK Request diff --git a/src/fne/network/TrafficNetwork.cpp b/src/fne/network/TrafficNetwork.cpp index 2054e029..2a4ed458 100644 --- a/src/fne/network/TrafficNetwork.cpp +++ b/src/fne/network/TrafficNetwork.cpp @@ -1536,6 +1536,7 @@ void TrafficNetwork::processNetworkRx(NetPacketRequest* req) { NET_FUNC::PING, &TrafficNetwork::PacketHandler::ping }, + { NET_FUNC::GRANT_REL, &TrafficNetwork::PacketHandler::grantRelease }, { NET_FUNC::GRANT_REQ, &TrafficNetwork::PacketHandler::grantRequest }, { NET_FUNC::INCALL_CTRL, &TrafficNetwork::PacketHandler::inCallControl }, @@ -2340,6 +2341,38 @@ void TrafficNetwork::processInCallCtrl(network::NET_ICC::ENUM command, network:: } } +/* Releases a global grant when its owner and call parties match. */ + +bool TrafficNetwork::processGrantRelease(uint32_t srcId, uint32_t dstId, bool unitToUnit, uint32_t peerId, + bool failed) +{ + uint32_t grantOwner = dstId; + if (unitToUnit && (!m_globalAff->isGranted(grantOwner) || m_globalAff->isGroup(grantOwner) || m_globalAff->getGrantedSrcId(grantOwner) != srcId)) { + grantOwner = m_globalAff->getGrantedBySrcId(dstId); + } + + if (!m_globalAff->isGranted(grantOwner)) + return false; + if (m_globalAff->getSSRCByGrant(grantOwner) != peerId) + return false; + if (m_globalAff->isGroup(grantOwner) == unitToUnit) + return false; + + const uint32_t grantedSrcId = m_globalAff->getGrantedSrcId(grantOwner); + const bool matchingDirection = grantedSrcId == srcId && grantOwner == dstId; + const bool matchingReverseDirection = unitToUnit && grantedSrcId == dstId && grantOwner == srcId; + if (!matchingDirection && !matchingReverseDirection) + return false; + + if (failed) { + LogWarning(LOG_MASTER, "PEER %u failed to commit grant locally, releasing, srcId = %u, dstId = %u", + peerId, srcId, dstId); + } + + m_globalAff->releaseGrant(grantOwner, false); + return true; +} + /* Helper to send the network metadata to the specified peer in a separate thread. */ void TrafficNetwork::peerMetadataUpdate(uint32_t peerId) diff --git a/src/fne/network/TrafficNetwork.h b/src/fne/network/TrafficNetwork.h index a9970047..d90f30c7 100644 --- a/src/fne/network/TrafficNetwork.h +++ b/src/fne/network/TrafficNetwork.h @@ -690,6 +690,17 @@ namespace network */ static void grantRequest(TrafficNetwork* network, NetPacketRequest* req, uint32_t peerId, uint32_t ssrc, uint32_t streamId, uint64_t now); + /** + * @brief Handles NET_FUNC::GRANT_REL packets. + * @param network Instance of the TrafficNetwork class. + * @param req Instance of the NetPacketRequest structure. + * @param peerId Peer ID. + * @param ssrc RTP synchronization source ID. + * @param streamId Stream ID. + * @param now Current time in milliseconds. + */ + static void grantRelease(TrafficNetwork* network, NetPacketRequest* req, uint32_t peerId, uint32_t ssrc, uint32_t streamId, uint64_t now); + /** * @brief Handles NET_FUNC::INCALL_CTRL packets. * @param network Instance of the TrafficNetwork class. @@ -955,6 +966,18 @@ namespace network void processInCallCtrl(network::NET_ICC::ENUM command, network::NET_SUBFUNC::ENUM subFunc, uint32_t dstId, uint8_t slotNo, uint32_t peerId, uint32_t ssrc, uint32_t streamId); + /** + * @brief Releases a global grant when its originating peer and call parties match. + * @param srcId Source subscriber ID. + * @param dstId Destination subscriber or talkgroup ID. + * @param unitToUnit Flag indicating an individual-call grant. + * @param peerId Authenticated peer requesting the release. + * @param failed Flag indicating local RF grant commit failed. + * @return True if a matching active grant was released; otherwise false. + */ + bool processGrantRelease(uint32_t srcId, uint32_t dstId, bool unitToUnit, uint32_t peerId, + bool failed = false); + /** * @brief Helper to send the network metadata to the specified peer in a separate thread. * @param peerId Peer ID. diff --git a/src/fne/network/callhandler/TagDMRData.cpp b/src/fne/network/callhandler/TagDMRData.cpp index 4092b0e7..7e43a59d 100644 --- a/src/fne/network/callhandler/TagDMRData.cpp +++ b/src/fne/network/callhandler/TagDMRData.cpp @@ -786,29 +786,37 @@ bool TagDMRData::processGrantReq(uint32_t srcId, uint32_t dstId, uint8_t slot, b } } + // a private call remains one grant when either participant retries or + // becomes the next talker; group calls retain destination-only ownership + uint32_t grantOwner = dstId; + bool existingU2U = unitToUnit && m_network->m_globalAff->isGranted(dstId) && !m_network->m_globalAff->isGroup(dstId) && m_network->m_globalAff->getGrantedSrcId(dstId) == srcId; + if (unitToUnit && !existingU2U) { + grantOwner = m_network->m_globalAff->getGrantedBySrcId(dstId); + existingU2U = grantOwner == srcId && m_network->m_globalAff->isGranted(grantOwner) && !m_network->m_globalAff->isGroup(grantOwner); + } + + if (existingU2U) { + m_network->m_globalAff->touchGrant(grantOwner); + } + else if (m_network->m_globalAff->isGranted(dstId) || !m_network->m_globalAff->grantCh(dstId, srcId, peerId, fne_lookups::GRANT_TIMER_TIMEOUT, !unitToUnit)) { + return false; + } + // repeat traffic to the connected peers if (m_network->peerCount() > 0U) { const uint32_t registeredPeerId = unitToUnit ? m_network->findPeerUnitReg(dstId) : 0U; for (const auto& peer : m_network->peersSnapshot()) { - if (peerId != peer.first) { - if (registeredPeerId != 0U && peer.first != registeredPeerId) - continue; - if (unitToUnit && !m_network->isUnitToUnitPeerAllowed(peer.first)) - continue; + if (registeredPeerId != 0U && peer.first != registeredPeerId && peer.first != peerId) + continue; + if (unitToUnit && !m_network->isUnitToUnitPeerAllowed(peer.first)) + continue; - write_CSBK_Grant(peer.first, srcId, dstId, serviceOptions, !unitToUnit); - } + write_CSBK_Grant(peer.first, srcId, dstId, serviceOptions, !unitToUnit, + peer.first == peerId); } } - // check if the destination has been granted globally - if (!m_network->m_globalAff->isGranted(dstId)) { - if (m_network->m_globalAff->grantCh(dstId, srcId, peerId, fne_lookups::GRANT_TIMER_TIMEOUT, !unitToUnit)) { - return true; - } - } - - return false; + return true; } /* Helper to trigger a call takeover from a In-Call control event. */ @@ -1496,7 +1504,8 @@ bool TagDMRData::validate(uint32_t peerId, data::NetData& data, lc::CSBK* csbk, /* Helper to write a grant packet. */ -bool TagDMRData::write_CSBK_Grant(uint32_t peerId, uint32_t srcId, uint32_t dstId, uint8_t serviceOptions, bool grp) +bool TagDMRData::write_CSBK_Grant(uint32_t peerId, uint32_t srcId, uint32_t dstId, + uint8_t serviceOptions, bool grp, bool requestingPeer) { uint8_t slot = 0U; @@ -1509,21 +1518,23 @@ bool TagDMRData::write_CSBK_Grant(uint32_t peerId, uint32_t srcId, uint32_t dstI return true; // do not generate grant packets for $FFFF (All Call) TGID } - // check the affiliations for this peer to see if we can grant traffic - std::shared_ptr aff = m_network->getPeerAffiliations(peerId); - if (aff == nullptr) { - std::string peerIdentity = m_network->resolvePeerIdentity(peerId); - LogError(LOG_MASTER, "PEER %u (%s) has an invalid affiliations lookup? This shouldn't happen BUGBUG.", peerId, peerIdentity.c_str()); - return false; // this will cause no traffic to pass for this peer now...I'm not sure this is good behavior - } - else { - if (grp && !aff->hasGroupAff(dstId)) { - return false; + if (!requestingPeer) { + // check the affiliations for this peer to see if we can grant traffic + std::shared_ptr aff = m_network->getPeerAffiliations(peerId); + if (aff == nullptr) { + std::string peerIdentity = m_network->resolvePeerIdentity(peerId); + LogError(LOG_MASTER, "PEER %u (%s) has an invalid affiliations lookup? This shouldn't happen BUGBUG.", peerId, peerIdentity.c_str()); + return false; // this will cause no traffic to pass for this peer now...I'm not sure this is good behavior } - if (!grp && !aff->isUnitReg(dstId) && !m_network->shouldFloodUnitToUnit(dstId)) { - TrafficNetwork::PeerConnectionPtr connection = m_network->findPeer(peerId); - if (connection == nullptr || !connection->isConventional()) + else { + if (grp && !aff->hasGroupAff(dstId)) { return false; + } + if (!grp && !aff->isUnitReg(dstId) && !m_network->shouldFloodUnitToUnit(dstId)) { + TrafficNetwork::PeerConnectionPtr connection = m_network->findPeer(peerId); + if (connection == nullptr || !connection->isConventional()) + return false; + } } } diff --git a/src/fne/network/callhandler/TagDMRData.h b/src/fne/network/callhandler/TagDMRData.h index 8cb5ec27..59d92025 100644 --- a/src/fne/network/callhandler/TagDMRData.h +++ b/src/fne/network/callhandler/TagDMRData.h @@ -352,9 +352,11 @@ namespace network * @param dstId Destination ID. * @param serviceOptions Service Options. * @param grp Flag indicating the destination ID is a talkgroup. + * @param requestingPeer Flag indicating routing checks were already satisfied by the grant request. * @returns True, if granted, otherwise false. */ - bool write_CSBK_Grant(uint32_t peerId, uint32_t srcId, uint32_t dstId, uint8_t serviceOptions, bool grp); + bool write_CSBK_Grant(uint32_t peerId, uint32_t srcId, uint32_t dstId, + uint8_t serviceOptions, bool grp, bool requestingPeer = false); /** * @brief Helper to write a NACK RSP packet. * @param peerId Peer ID. diff --git a/src/fne/network/callhandler/TagNXDNData.cpp b/src/fne/network/callhandler/TagNXDNData.cpp index 1f07c723..47f5f5d4 100644 --- a/src/fne/network/callhandler/TagNXDNData.cpp +++ b/src/fne/network/callhandler/TagNXDNData.cpp @@ -796,29 +796,37 @@ bool TagNXDNData::processGrantReq(uint32_t srcId, uint32_t dstId, bool unitToUni } } + // a private call remains one grant when either participant retries or + // becomes the next talker; group calls retain destination-only ownership + uint32_t grantOwner = dstId; + bool existingU2U = unitToUnit && m_network->m_globalAff->isGranted(dstId) && !m_network->m_globalAff->isGroup(dstId) && m_network->m_globalAff->getGrantedSrcId(dstId) == srcId; + if (unitToUnit && !existingU2U) { + grantOwner = m_network->m_globalAff->getGrantedBySrcId(dstId); + existingU2U = grantOwner == srcId && m_network->m_globalAff->isGranted(grantOwner) && !m_network->m_globalAff->isGroup(grantOwner); + } + + if (existingU2U) { + m_network->m_globalAff->touchGrant(grantOwner); + } + else if (m_network->m_globalAff->isGranted(dstId) || !m_network->m_globalAff->grantCh(dstId, srcId, peerId, fne_lookups::GRANT_TIMER_TIMEOUT, !unitToUnit)) { + return false; + } + // repeat traffic to the connected peers if (m_network->peerCount() > 0U) { const uint32_t registeredPeerId = unitToUnit ? m_network->findPeerUnitReg(dstId) : 0U; for (const auto& peer : m_network->peersSnapshot()) { - if (peerId != peer.first) { - if (registeredPeerId != 0U && peer.first != registeredPeerId) - continue; - if (unitToUnit && !m_network->isUnitToUnitPeerAllowed(peer.first)) - continue; - - write_Message_Grant(peer.first, srcId, dstId, serviceOptions, !unitToUnit); - } - } - } + if (registeredPeerId != 0U && peer.first != registeredPeerId && peer.first != peerId) + continue; + if (unitToUnit && !m_network->isUnitToUnitPeerAllowed(peer.first)) + continue; - // check if the destination has been granted globally - if (!m_network->m_globalAff->isGranted(dstId)) { - if (m_network->m_globalAff->grantCh(dstId, srcId, peerId, fne_lookups::GRANT_TIMER_TIMEOUT, !unitToUnit)) { - return true; + write_Message_Grant(peer.first, srcId, dstId, serviceOptions, !unitToUnit, + peer.first == peerId); } } - return false; + return true; } /* Helper to trigger a call takeover from a In-Call control event. */ @@ -1316,7 +1324,8 @@ bool TagNXDNData::validate(uint32_t peerId, lc::RTCH& lc, uint8_t messageType, u /* Helper to write a grant packet. */ -bool TagNXDNData::write_Message_Grant(uint32_t peerId, uint32_t srcId, uint32_t dstId, uint8_t serviceOptions, bool grp) +bool TagNXDNData::write_Message_Grant(uint32_t peerId, uint32_t srcId, uint32_t dstId, + uint8_t serviceOptions, bool grp, bool requestingPeer) { bool emergency = ((serviceOptions & 0xFFU) & 0x80U) == 0x80U; // Emergency Flag bool encryption = ((serviceOptions & 0xFFU) & 0x40U) == 0x40U; // Encryption Flag @@ -1324,21 +1333,23 @@ bool TagNXDNData::write_Message_Grant(uint32_t peerId, uint32_t srcId, uint32_t std::unique_ptr rcch = std::make_unique(); - // check the affiliations for this peer to see if we can grant traffic - std::shared_ptr aff = m_network->getPeerAffiliations(peerId); - if (aff == nullptr) { - std::string peerIdentity = m_network->resolvePeerIdentity(peerId); - LogError(LOG_MASTER, "PEER %u (%s) has an invalid affiliations lookup? This shouldn't happen BUGBUG.", peerId, peerIdentity.c_str()); - return false; // this will cause no traffic to pass for this peer now...I'm not sure this is good behavior - } - else { - if (grp && !aff->hasGroupAff(dstId)) { - return false; + if (!requestingPeer) { + // check the affiliations for this peer to see if we can grant traffic + std::shared_ptr aff = m_network->getPeerAffiliations(peerId); + if (aff == nullptr) { + std::string peerIdentity = m_network->resolvePeerIdentity(peerId); + LogError(LOG_MASTER, "PEER %u (%s) has an invalid affiliations lookup? This shouldn't happen BUGBUG.", peerId, peerIdentity.c_str()); + return false; // this will cause no traffic to pass for this peer now...I'm not sure this is good behavior } - if (!grp && !aff->isUnitReg(dstId) && !m_network->shouldFloodUnitToUnit(dstId)) { - TrafficNetwork::PeerConnectionPtr connection = m_network->findPeer(peerId); - if (connection == nullptr || !connection->isConventional()) + else { + if (grp && !aff->hasGroupAff(dstId)) { return false; + } + if (!grp && !aff->isUnitReg(dstId) && !m_network->shouldFloodUnitToUnit(dstId)) { + TrafficNetwork::PeerConnectionPtr connection = m_network->findPeer(peerId); + if (connection == nullptr || !connection->isConventional()) + return false; + } } } diff --git a/src/fne/network/callhandler/TagNXDNData.h b/src/fne/network/callhandler/TagNXDNData.h index e269060c..0e32707a 100644 --- a/src/fne/network/callhandler/TagNXDNData.h +++ b/src/fne/network/callhandler/TagNXDNData.h @@ -303,9 +303,11 @@ namespace network * @param dstId Destination ID. * @param serviceOptions Service Options. * @param grp Flag indicating the destination ID is a talkgroup. + * @param requestingPeer Flag indicating routing checks were already satisfied by the grant request. * @returns True, if granted, otherwise false. */ - bool write_Message_Grant(uint32_t peerId, uint32_t srcId, uint32_t dstId, uint8_t serviceOptions, bool grp); + bool write_Message_Grant(uint32_t peerId, uint32_t srcId, uint32_t dstId, + uint8_t serviceOptions, bool grp, bool requestingPeer = false); /** * @brief Helper to write a deny packet. * @param peerId Peer ID. diff --git a/src/fne/network/callhandler/TagP25Data.cpp b/src/fne/network/callhandler/TagP25Data.cpp index f934699b..0685c499 100644 --- a/src/fne/network/callhandler/TagP25Data.cpp +++ b/src/fne/network/callhandler/TagP25Data.cpp @@ -922,29 +922,37 @@ bool TagP25Data::processGrantReq(uint32_t srcId, uint32_t dstId, bool unitToUnit } } + // a private call remains one grant when either participant retries or + // becomes the next talker; group calls retain destination-only ownership + uint32_t grantOwner = dstId; + bool existingU2U = unitToUnit && m_network->m_globalAff->isGranted(dstId) && !m_network->m_globalAff->isGroup(dstId) && m_network->m_globalAff->getGrantedSrcId(dstId) == srcId; + if (unitToUnit && !existingU2U) { + grantOwner = m_network->m_globalAff->getGrantedBySrcId(dstId); + existingU2U = grantOwner == srcId && m_network->m_globalAff->isGranted(grantOwner) && !m_network->m_globalAff->isGroup(grantOwner); + } + + if (existingU2U) { + m_network->m_globalAff->touchGrant(grantOwner); + } + else if (m_network->m_globalAff->isGranted(dstId) || !m_network->m_globalAff->grantCh(dstId, srcId, peerId, fne_lookups::GRANT_TIMER_TIMEOUT, !unitToUnit)) { + return false; + } + // repeat traffic to the connected peers if (m_network->peerCount() > 0U) { const uint32_t registeredPeerId = unitToUnit ? m_network->findPeerUnitReg(dstId) : 0U; for (const auto& peer : m_network->peersSnapshot()) { - if (peerId != peer.first) { - if (registeredPeerId != 0U && peer.first != registeredPeerId) - continue; - if (unitToUnit && !m_network->isUnitToUnitPeerAllowed(peer.first)) - continue; + if (registeredPeerId != 0U && peer.first != registeredPeerId && peer.first != peerId) + continue; + if (unitToUnit && !m_network->isUnitToUnitPeerAllowed(peer.first)) + continue; - write_TSDU_Grant(peer.first, srcId, dstId, serviceOptions, !unitToUnit); - } - } - } - - // check if the destination has been granted globally - if (!m_network->m_globalAff->isGranted(dstId)) { - if (m_network->m_globalAff->grantCh(dstId, srcId, peerId, fne_lookups::GRANT_TIMER_TIMEOUT, !unitToUnit)) { - return true; + write_TSDU_Grant(peer.first, srcId, dstId, serviceOptions, !unitToUnit, + peer.first == peerId); } } - return false; + return true; } /* Helper to trigger a call takeover from a In-Call control event. */ @@ -2140,7 +2148,8 @@ bool TagP25Data::validate(uint32_t peerId, lc::LC& control, DUID::E duid, const /* Helper to write a grant packet. */ -bool TagP25Data::write_TSDU_Grant(uint32_t peerId, uint32_t srcId, uint32_t dstId, uint8_t serviceOptions, bool grp) +bool TagP25Data::write_TSDU_Grant(uint32_t peerId, uint32_t srcId, uint32_t dstId, + uint8_t serviceOptions, bool grp, bool requestingPeer) { bool emergency = ((serviceOptions & 0xFFU) & 0x80U) == 0x80U; // Emergency Flag bool encryption = ((serviceOptions & 0xFFU) & 0x40U) == 0x40U; // Encryption Flag @@ -2150,7 +2159,7 @@ bool TagP25Data::write_TSDU_Grant(uint32_t peerId, uint32_t srcId, uint32_t dstI return true; // do not generate grant packets for $FFFF (All Call) TGID } - if (grp) { + if (grp && !requestingPeer) { // check the affiliations for this peer to see if we can grant traffic std::shared_ptr aff = m_network->getPeerAffiliations(peerId); if (aff == nullptr) { diff --git a/src/fne/network/callhandler/TagP25Data.h b/src/fne/network/callhandler/TagP25Data.h index bf50cf8a..86b14321 100644 --- a/src/fne/network/callhandler/TagP25Data.h +++ b/src/fne/network/callhandler/TagP25Data.h @@ -410,9 +410,11 @@ namespace network * @param dstId Destination ID. * @param serviceOptions Service Options. * @param grp Flag indicating the destination ID is a talkgroup. + * @param requestingPeer Flag indicating routing checks were already satisfied by the grant request. * @returns True, if granted, otherwise false. */ - bool write_TSDU_Grant(uint32_t peerId, uint32_t srcId, uint32_t dstId, uint8_t serviceOptions, bool grp); + bool write_TSDU_Grant(uint32_t peerId, uint32_t srcId, uint32_t dstId, + uint8_t serviceOptions, bool grp, bool requestingPeer = false); /** * @brief Helper to write a deny packet. * @param peerId Peer ID. diff --git a/src/fne/network/packet/GrantRequest.cpp b/src/fne/network/packet/GrantRequest.cpp index 5c854ff1..b8593960 100644 --- a/src/fne/network/packet/GrantRequest.cpp +++ b/src/fne/network/packet/GrantRequest.cpp @@ -81,3 +81,23 @@ void TrafficNetwork::PacketHandler::grantRequest(TrafficNetwork* network, NetPac } } } + +/* Handles NET_FUNC::GRANT_REL packets. */ + +void TrafficNetwork::PacketHandler::grantRelease(TrafficNetwork* network, NetPacketRequest* req, uint32_t peerId, uint32_t ssrc, uint32_t streamId, uint64_t now) +{ + TrafficNetwork::PeerConnectionPtr connection = network->findPeer(peerId); + if (peerId == 0U || connection == nullptr) + return; + + const std::string ip = udp::Socket::address(req->address); + if (!connection->connected() || connection->address() != ip) + return; + + const uint32_t srcId = GET_UINT24(req->buffer, 11U); + const uint32_t dstId = GET_UINT24(req->buffer, 15U); + const bool unitToUnit = (req->buffer[19U] & 0x80U) == 0x80U; + const bool failed = (req->buffer[21U] & NET_GRANT_RELEASE_FAILED) == NET_GRANT_RELEASE_FAILED; + + network->processGrantRelease(srcId, dstId, unitToUnit, peerId, failed); +} diff --git a/src/host/Host.Config.cpp b/src/host/Host.Config.cpp index b18ae41f..e7ab38a9 100644 --- a/src/host/Host.Config.cpp +++ b/src/host/Host.Config.cpp @@ -154,9 +154,6 @@ bool Host::readParams() */ yaml::Node rfssConfig = systemConf["config"]; m_authoritative = rfssConfig["authoritative"].as(true); - if (m_authoritative) { - m_supervisor = rfssConfig["supervisor"].as(false); - } m_channelLookup = new ChannelLookup(); m_channelId = (uint8_t)rfssConfig["channelId"].as(0U); @@ -291,9 +288,8 @@ bool Host::readParams() uint16_t rpcApiPort = (uint16_t)channel["rpcPort"].as(RPC_DEFAULT_PORT); std::string rpcApiPassword = channel["rpcPassword"].as(); - // if we are both non-authoritative and non-supervisory then check if the voice channel - // configuration seems sane - if (!m_authoritative && !m_supervisor) { + // A non-authoritative traffic channel must not point its voice-channel entry at its own RPC listener. + if (!m_authoritative) { if ((rpcApiAddress == m_rpcAddress) && rpcApiPort == m_rpcPort) { ::LogWarning(LOG_HOST, "Detected possible misconfiguration! Voice Channel RPC Address/Port and the configured RPC Address/Port are the same!"); } @@ -378,9 +374,6 @@ bool Host::readParams() LogInfo("System Config Parameters"); LogInfo(" Authoritative: %s", m_authoritative ? "yes" : "no"); - if (m_authoritative) { - LogInfo(" Supervisor: %s", m_supervisor ? "yes" : "no"); - } LogInfo(" RX Frequency: %uHz", m_rxFrequency); LogInfo(" TX Frequency: %uHz", m_txFrequency); LogInfo(" Base Frequency: %uHz", entry.baseFrequency()); @@ -405,7 +398,6 @@ bool Host::readParams() LogInfo(" NXDN RAN: %u", m_nxdnRAN); if (!m_authoritative) { - m_supervisor = false; LogWarning(LOG_HOST, "Host is non-authoritative! This requires a second instance configured in site controller mode."); LogWarning(LOG_HOST, "RPC is required to handle permit TG for VCs!"); } diff --git a/src/host/Host.cpp b/src/host/Host.cpp index fca020c3..76001041 100644 --- a/src/host/Host.cpp +++ b/src/host/Host.cpp @@ -129,7 +129,6 @@ Host::Host(const std::string& confFile) : m_p25QueueSizeBytes(2592U), // 12 frames m_nxdnQueueSizeBytes(1488U), // 31 frames m_authoritative(true), - m_supervisor(false), m_dmr1RejCnt(0U), m_dmr2RejCnt(0U), m_p25RejCnt(0U), @@ -461,7 +460,7 @@ int Host::run() m_dmr = std::make_unique(m_authoritative, m_dmrColorCode, callHang, m_dmrQueueSizeBytes, embeddedLCOnly, dumpTAData, m_timeout, m_rfTalkgroupHang, m_modem, m_network, m_duplex, m_channelLookup, m_ridLookup, m_tidLookup, m_idenTable, rssi, jitter, dmrDumpDataPacket, dmrRepeatDataPacket, dmrDumpCsbkData, dmrDebug, dmrVerbose); - m_dmr->setOptions(m_conf, m_supervisor, m_controlChData, m_dmrNetId, m_siteId, m_channelId, + m_dmr->setOptions(m_conf, m_controlChData, m_dmrNetId, m_siteId, m_channelId, m_channelNo, true); if (dmrCtrlChannel) { @@ -530,7 +529,7 @@ int Host::run() m_p25 = std::make_unique(m_authoritative, m_p25NAC, callHang, m_p25QueueSizeBytes, m_modem, m_network, m_timeout, m_rfTalkgroupHang, m_duplex, m_channelLookup, m_ridLookup, m_tidLookup, m_idenTable, rssi, p25DumpDataPacket, p25RepeatDataPacket, p25DumpTsbkData, p25Debug, p25Verbose); - m_p25->setOptions(m_conf, m_supervisor, m_cwCallsign, m_controlChData, + m_p25->setOptions(m_conf, m_cwCallsign, m_controlChData, m_p25NetId, m_sysId, m_p25RfssId, m_siteId, m_channelId, m_channelNo, true); if (p25CtrlChannel) { @@ -590,7 +589,7 @@ int Host::run() m_nxdn = std::make_unique(m_authoritative, m_nxdnRAN, callHang, m_nxdnQueueSizeBytes, m_timeout, m_rfTalkgroupHang, m_modem, m_network, m_duplex, m_channelLookup, m_ridLookup, m_tidLookup, m_idenTable, rssi, nxdnDumpRcchData, nxdnDebug, nxdnVerbose); - m_nxdn->setOptions(m_conf, m_supervisor, m_cwCallsign, m_controlChData, m_siteId, + m_nxdn->setOptions(m_conf, m_cwCallsign, m_controlChData, m_siteId, m_sysId, m_channelId, m_channelNo, true); if (nxdnCtrlChannel) { diff --git a/src/host/Host.h b/src/host/Host.h index dcf6230c..198317e6 100644 --- a/src/host/Host.h +++ b/src/host/Host.h @@ -187,7 +187,6 @@ private: uint32_t m_nxdnQueueSizeBytes; bool m_authoritative; - bool m_supervisor; uint32_t m_dmr1RejCnt; uint32_t m_dmr2RejCnt; diff --git a/src/host/dmr/Control.cpp b/src/host/dmr/Control.cpp index f33dccc7..b2797076 100644 --- a/src/host/dmr/Control.cpp +++ b/src/host/dmr/Control.cpp @@ -60,7 +60,6 @@ Control::Control(bool authoritative, uint32_t colorCode, uint32_t callHang, uint ::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_supervisor(false), m_colorCode(colorCode), m_modem(modem), m_network(network), @@ -114,14 +113,12 @@ Control::~Control() /* Helper to set DMR configuration options. */ -void Control::setOptions(yaml::Node& conf, bool supervisor, ::lookups::VoiceChData controlChData, +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"]; - m_supervisor = supervisor; - Slot::s_verifyReg = dmrProtocol["verifyReg"].as(false); uint8_t nRandWait = (uint8_t)dmrProtocol["nRandWait"].as(DEFAULT_NRAND_WAIT); @@ -163,13 +160,11 @@ void Control::setOptions(yaml::Node& conf, bool supervisor, ::lookups::VoiceChDa switch (m_tsccSlotNo) { case 1U: m_slot1->setTSCC(enableTSCC, dedicatedTSCC); - m_slot1->setSupervisor(m_supervisor); m_slot1->setDisableSourceIDGrantCheck(disableGrantSourceIdCheck); m_slot1->setCCDebug(ccDebug); break; case 2U: m_slot2->setTSCC(enableTSCC, dedicatedTSCC); - m_slot2->setSupervisor(m_supervisor); m_slot2->setDisableSourceIDGrantCheck(disableGrantSourceIdCheck); m_slot2->setCCDebug(ccDebug); break; @@ -255,8 +250,7 @@ void Control::setOptions(yaml::Node& conf, bool supervisor, ::lookups::VoiceChDa if (disableGrantSourceIdCheck) { LogInfo(" TSCC Disable Grant Source ID Check: yes"); } - if (m_supervisor) - LogInfoEx(LOG_DMR, "Host is configured to operate as a DMR TSCC, site controller mode."); + LogInfoEx(LOG_DMR, "Host is configured to operate as a DMR TSCC."); } if (defaultNetIdleTalkgroup != 0U) { @@ -447,27 +441,6 @@ void Control::clockSiteData(uint32_t ms) m_slot2->clockSiteData(ms); } -/* Sets a flag indicating whether DMR has supervisory functions and can send permit TG to voice channels. */ - -void Control::setSupervisor(bool supervisor) -{ - if (!m_enableTSCC) { - return; - } - - switch (m_tsccSlotNo) { - case 1U: - m_slot1->setSupervisor(supervisor); - break; - case 2U: - m_slot2->setSupervisor(supervisor); - break; - default: - LogError(LOG_DMR, "DMR, invalid slot, slotNo = %u", m_tsccSlotNo); - break; - } -} - /* Permits a TGID on a non-authoritative host. */ void Control::permittedTG(uint32_t dstId, uint8_t slot) diff --git a/src/host/dmr/Control.h b/src/host/dmr/Control.h index 669a4743..0bf02d64 100644 --- a/src/host/dmr/Control.h +++ b/src/host/dmr/Control.h @@ -104,7 +104,6 @@ namespace dmr /** * @brief Helper to set DMR configuration options. * @param conf Instance of the ConfigINI class. - * @param supervisor Flag indicating whether the DMR has supervisory functions. * @param controlChData Control Channel data. * @param netId DMR Network ID. * @param siteId DMR Site ID. @@ -112,7 +111,7 @@ namespace dmr * @param channelNo Channel Number. * @param printOptions Flag indicating whether or not options should be printed to log. */ - void setOptions(yaml::Node& conf, bool supervisor, ::lookups::VoiceChData controlChData, + void setOptions(yaml::Node& conf, ::lookups::VoiceChData controlChData, uint32_t netId, uint8_t siteId, uint8_t channelId, uint32_t channelNo, bool printOptions); /** @name CC Control */ @@ -186,12 +185,6 @@ namespace dmr void clockSiteData(uint32_t ms); /** @} */ - /** @name Supervisory Control */ - /** - * @brief Sets a flag indicating whether control has supervisory functions and can send permit TG to voice channels. - * @param supervisor Flag indicating whether control has supervisory functions. - */ - void setSupervisor(bool supervisor); /** * @brief Permits a TGID on a non-authoritative host. * @param dstId Destination ID. @@ -315,7 +308,6 @@ namespace dmr #endif bool m_authoritative; - bool m_supervisor; uint32_t m_colorCode; @@ -364,7 +356,7 @@ namespace dmr */ void processInCallCtrl(network::NET_ICC::ENUM command, uint32_t dstId, uint8_t slotNo); - /** @name Supervisory Control */ + /** @name Traffic Permit Control */ /** * @brief (RPC Handler) Permits a TGID on a non-authoritative host. * @param req JSON request. diff --git a/src/host/dmr/Slot.cpp b/src/host/dmr/Slot.cpp index 263848ed..f6b2a038 100644 --- a/src/host/dmr/Slot.cpp +++ b/src/host/dmr/Slot.cpp @@ -165,7 +165,6 @@ Slot::Slot(uint32_t slotNo, uint32_t timeout, uint32_t tgHang, uint32_t queueSiz m_tsccAdjSSCnt(0U), m_disableGrantSrcIdCheck(false), m_lastLateEntry(0U), - m_supervisor(false), m_notifyCC(true), m_ccDebug(debug), m_verbose(verbose), @@ -399,6 +398,13 @@ uint32_t Slot::getFrame(uint8_t* data, bool* imm) void Slot::processNetwork(const data::NetData& dmrData) { + const DataType::E dataType = dmrData.getDataType(); + Slot* tscc = s_dmr->getTSCCSlot(); + const bool fneGrantControl = !s_authoritative && s_network != nullptr && + s_network->hasGrantAuthority() && tscc != nullptr && tscc->m_enableTSCC && + tscc->m_dedicatedTSCC && (dataType == DataType::CSBK || + dataType == DataType::MBC_HEADER || dataType == DataType::MBC_DATA); + // don't process network frames if the RF modem isn't in a listening state if (m_rfState != RS_RF_LISTENING && m_netState == RS_NET_IDLE) { return; @@ -428,19 +434,6 @@ void Slot::processNetwork(const data::NetData& dmrData) } } - // don't process network frames if this modem isn't authoritative - if (!s_authoritative && m_permittedDstId != dmrData.getDstId()) { - if (!g_disableNonAuthoritativeLogging) - LogWarning(LOG_NET, "DMR Slot %u, [NON-AUTHORITATIVE] Ignoring network traffic, destination not permitted!", m_slotNo); - return; - } - - m_networkWatchdog.start(); - - DataType::E dataType = dmrData.getDataType(); - - Slot* tscc = s_dmr->getTSCCSlot(); - bool enableTSCC = false; if (tscc != nullptr) enableTSCC = tscc->m_enableTSCC; @@ -506,6 +499,15 @@ void Slot::processNetwork(const data::NetData& dmrData) } } + // don't process network frames if this modem isn't authoritative + if (!s_authoritative && !fneGrantControl && m_permittedDstId != dmrData.getDstId()) { + if (!g_disableNonAuthoritativeLogging) + LogWarning(LOG_NET, "DMR Slot %u, [NON-AUTHORITATIVE] Ignoring network traffic, destination not permitted!", m_slotNo); + return; + } + + m_networkWatchdog.start(); + switch (dataType) { case DataType::CSBK: @@ -934,6 +936,13 @@ void Slot::permittedTG(uint32_t dstId) m_permittedDstId = dstId; } +/* Determines whether this control channel currently owns grant decisions. */ + +bool Slot::isGrantAuthoritative() const +{ + return s_authoritative || s_network == nullptr || !s_network->hasGrantAuthority(); +} + /* Grants a TGID on a non-authoritative host. */ void Slot::grantTG(uint32_t srcId, uint32_t dstId, bool grp) @@ -1176,6 +1185,12 @@ void Slot::init(Control* dmr, bool authoritative, uint32_t colorCode, SiteData s // set the grant release callback s_affiliations->setReleaseGrantCallback([=](uint32_t chNo, uint32_t srcId, uint32_t dstId, uint8_t slot) { + const bool networkGrant = s_affiliations->isNetGranted(dstId); + const bool unitToUnit = !s_affiliations->isGroup(dstId); + if (!s_authoritative && networkGrant && s_network != nullptr) { + s_network->writeGrantRelease(modem::DVM_STATE::STATE_DMR, srcId, dstId, slot, unitToUnit); + } + Slot* tscc = s_dmr->getTSCCSlot(); if (tscc != nullptr) { if (chNo == tscc->s_channelNo) { @@ -1196,19 +1211,17 @@ void Slot::init(Control* dmr, bool authoritative, uint32_t colorCode, SiteData s ::LogError(LOG_DMR, "DMR Slot %u, CSBK, RAND (Random Access), failed to clear payload channel, chNo = %u, slot = %u", tscc->m_slotNo, chNo, slot); } - // callback REST API to clear TG permit for the granted TG on the specified voice channel - if (s_authoritative && s_dmr->m_supervisor) { - if (voiceChData.isValidCh() && !voiceChData.address().empty() && voiceChData.port() > 0) { - json::object req = json::object(); - dstId = 0U; // clear TG value - req["dstId"].set(dstId); - req["slot"].set(slot); + // clear the TG permit on the selected remote voice channel + if (voiceChData.isValidCh() && !voiceChData.address().empty() && voiceChData.port() > 0) { + json::object req = json::object(); + dstId = 0U; // clear TG value + req["dstId"].set(dstId); + req["slot"].set(slot); - g_RPC->req(RPC_PERMIT_DMR_TG, req, nullptr, voiceChData.address(), voiceChData.port()); - } - else { - ::LogError(LOG_DMR, "DMR Slot %u, CSBK, RAND (Random Access), failed to clear TG permit, chNo = %u, slot = %u", tscc->m_slotNo, chNo, slot); - } + g_RPC->req(RPC_PERMIT_DMR_TG, req, nullptr, voiceChData.address(), voiceChData.port()); + } + else { + ::LogError(LOG_DMR, "DMR Slot %u, CSBK, RAND (Random Access), failed to clear TG permit, chNo = %u, slot = %u", tscc->m_slotNo, chNo, slot); } } }); diff --git a/src/host/dmr/Slot.h b/src/host/dmr/Slot.h index 9d7e8e74..c9ddae01 100644 --- a/src/host/dmr/Slot.h +++ b/src/host/dmr/Slot.h @@ -190,17 +190,16 @@ namespace dmr void clockSiteData(uint32_t ms); /** @} */ - /** @name Supervisory Control */ - /** - * @brief Sets a flag indicating whether control has supervisory functions and can send permit TG to voice channels. - * @param supervisor Flag indicating whether control has supervisory functions. - */ - void setSupervisor(bool supervisor) { m_supervisor = supervisor; } /** * @brief Permits a TGID on a non-authoritative host. * @param dstId Destination ID. */ void permittedTG(uint32_t dstId); + /** + * @brief Determines whether this control channel currently owns grant decisions. + * @return True for configured authority or disconnected-network fallback. + */ + bool isGrantAuthoritative() const; /** * @brief Grants a TGID on a non-authoritative host. * @param srcId Source Radio ID. @@ -498,7 +497,6 @@ namespace dmr uint32_t m_lastLateEntry; - bool m_supervisor; bool m_notifyCC; bool m_ccDebug; diff --git a/src/host/dmr/packet/ControlSignaling.cpp b/src/host/dmr/packet/ControlSignaling.cpp index b6f5d9f8..46085a72 100644 --- a/src/host/dmr/packet/ControlSignaling.cpp +++ b/src/host/dmr/packet/ControlSignaling.cpp @@ -208,13 +208,11 @@ bool ControlSignaling::process(uint8_t* data, uint32_t len) // verify the source RID is registered VERIFY_SRCID_REG(csbk->toString(), ServiceKind::IND_VOICE_CALL, srcId); - if (m_slot->s_authoritative) { + if (m_slot->isGrantAuthoritative()) { m_slot->beginU2UOACSU(srcId, dstId, isp->getServiceOptions()); } else { writeRF_CSBK_ACK_RSP(srcId, ReasonCode::TS_WAIT_RSN, 0U); - if (m_slot->s_network != nullptr) - m_slot->s_network->writeGrantReq(modem::DVM_STATE::STATE_DMR, - srcId, dstId, m_slot->m_slotNo, true, isp->getServiceOptions()); + m_slot->s_network->writeGrantReq(modem::DVM_STATE::STATE_DMR, srcId, dstId, m_slot->m_slotNo, true, isp->getServiceOptions()); } break; case ServiceKind::CANCEL_CALL: @@ -235,12 +233,10 @@ bool ControlSignaling::process(uint8_t* data, uint32_t len) writeRF_CSBK_ACK_RSP(srcId, ReasonCode::TS_WAIT_RSN, 1U); - if (m_slot->s_authoritative) { + if (m_slot->isGrantAuthoritative()) { writeRF_CSBK_Grant(srcId, dstId, isp->getServiceOptions(), true); } else { - if (m_slot->s_network != nullptr) - m_slot->s_network->writeGrantReq(modem::DVM_STATE::STATE_DMR, - srcId, dstId, m_slot->m_slotNo, false, isp->getServiceOptions()); + m_slot->s_network->writeGrantReq(modem::DVM_STATE::STATE_DMR, srcId, dstId, m_slot->m_slotNo, false, isp->getServiceOptions()); } break; case ServiceKind::IND_DATA_CALL: @@ -537,6 +533,26 @@ void ControlSignaling::processNetwork(const data::NetData& dmrData) VERBOSE_LOG_CSBK_NET(csbk->toString(), srcId, dstId); } break; + case CSBKO::TV_GRANT: + case CSBKO::BTV_GRANT: + case CSBKO::PV_GRANT: + { + const bool group = csbko != CSBKO::PV_GRANT; + const uint8_t serviceOptions = (csbk->getEmergency() ? 0x80U : 0x00U) | 0x04U; + + Slot* tscc = m_slot->s_dmr->getTSCCSlot(); + if (tscc != nullptr && tscc->m_enableTSCC && tscc->m_dedicatedTSCC && !tscc->s_affiliations->isGranted(dstId)) { + if (!tscc->m_control->writeRF_CSBK_Grant(srcId, dstId, serviceOptions, group, true) && + tscc->s_network != nullptr) { + tscc->s_network->writeGrantRelease(modem::DVM_STATE::STATE_DMR, srcId, dstId, 0U, !group, true); + } + } + + // The FNE grant carries no site-specific channel assignment. The + // TSCC has either rebuilt it with local RF data or rejected it. + handled = true; + } + break; case CSBKO::RAND: { if (csbk->getFID() == FID_MOT) { @@ -1115,8 +1131,7 @@ bool ControlSignaling::writeRF_CSBK_Grant(uint32_t srcId, uint32_t dstId, uint8_ } // callback RPC to permit the granted TG on the specified voice channel - if (tscc->s_authoritative && tscc->m_supervisor && - tscc->s_channelNo != chNo) { + if (tscc->s_channelNo != chNo) { ::lookups::VoiceChData voiceChData = tscc->s_affiliations->rfCh()->getRFChData(chNo); if (voiceChData.isValidCh() && !voiceChData.address().empty() && voiceChData.port() > 0) { json::object req = json::object(); @@ -1210,8 +1225,7 @@ bool ControlSignaling::writeRF_CSBK_Grant(uint32_t srcId, uint32_t dstId, uint8_ } // callback RPC to permit the granted TG on the specified voice channel - if (tscc->s_authoritative && tscc->m_supervisor && - tscc->s_channelNo != chNo) { + if (tscc->s_channelNo != chNo) { ::lookups::VoiceChData voiceChData = tscc->s_affiliations->rfCh()->getRFChData(chNo); if (voiceChData.isValidCh() && !voiceChData.address().empty() && voiceChData.port() > 0) { json::object req = json::object(); diff --git a/src/host/nxdn/Control.cpp b/src/host/nxdn/Control.cpp index abb3209b..82705eba 100644 --- a/src/host/nxdn/Control.cpp +++ b/src/host/nxdn/Control.cpp @@ -62,7 +62,6 @@ Control::Control(bool authoritative, uint32_t ran, uint32_t callHang, uint32_t q m_data(nullptr), m_control(nullptr), m_authoritative(authoritative), - m_supervisor(false), m_ran(ran), m_timeout(timeout), m_modem(modem), @@ -198,14 +197,12 @@ void Control::reset() /* Helper to set NXDN configuration options. */ -void Control::setOptions(yaml::Node& conf, bool supervisor, const std::string cwCallsign, lookups::VoiceChData controlChData, +void Control::setOptions(yaml::Node& conf, const std::string cwCallsign, 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_control->m_verifyAff = nxdnProtocol["verifyAff"].as(false); m_control->m_verifyReg = nxdnProtocol["verifyReg"].as(false); @@ -285,20 +282,24 @@ void Control::setOptions(yaml::Node& conf, bool supervisor, const std::string cw // set the grant release callback m_affiliations->setReleaseGrantCallback([=](uint32_t chNo, uint32_t srcId, 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->rfCh()->getRFChData(chNo); - if (voiceChData.isValidCh() && !voiceChData.address().empty() && voiceChData.port() > 0 && - chNo != m_siteData.channelNo()) { - json::object req = json::object(); - dstId = 0U; // clear TG value - req["dstId"].set(dstId); - - g_RPC->req(RPC_PERMIT_NXDN_TG, req, nullptr, voiceChData.address(), voiceChData.port()); - } - else { - ::LogError(LOG_NXDN, "NXDN, " NXDN_RTCH_MSG_TYPE_VCALL_RESP ", failed to clear TG permit, chNo = %u", chNo); - } + const bool networkGrant = m_affiliations->isNetGranted(dstId); + const bool unitToUnit = !m_affiliations->isGroup(dstId); + if (!m_authoritative && networkGrant && m_network != nullptr) { + m_network->writeGrantRelease(modem::DVM_STATE::STATE_NXDN, srcId, dstId, slot, unitToUnit); + } + + // clear the TG permit on the selected remote voice channel + ::lookups::VoiceChData voiceChData = m_affiliations->rfCh()->getRFChData(chNo); + if (voiceChData.isValidCh() && !voiceChData.address().empty() && voiceChData.port() > 0 && + chNo != m_siteData.channelNo()) { + json::object req = json::object(); + dstId = 0U; // clear TG value + req["dstId"].set(dstId); + + g_RPC->req(RPC_PERMIT_NXDN_TG, req, nullptr, voiceChData.address(), voiceChData.port()); + } + else if (chNo != m_siteData.channelNo()) { + ::LogError(LOG_NXDN, "NXDN, " NXDN_RTCH_MSG_TYPE_VCALL_RESP ", failed to clear TG permit, chNo = %u", chNo); } }); @@ -334,8 +335,7 @@ void Control::setOptions(yaml::Node& conf, bool supervisor, const std::string cw if (m_control->m_disableGrantSrcIdCheck) { LogInfo(" Disable Grant Source ID Check: yes"); } - if (m_supervisor) - LogInfoEx(LOG_NXDN, "Host is configured to operate as a NXDN control channel, site controller mode."); + LogInfoEx(LOG_NXDN, "Host is configured to operate as a NXDN control channel."); } if (m_defaultNetIdleTalkgroup != 0U) { @@ -819,6 +819,13 @@ void Control::permittedTG(uint32_t dstId) m_permittedDstId = dstId; } +/* Determines whether this control channel currently owns grant decisions. */ + +bool Control::isGrantAuthoritative() const +{ + return m_authoritative || m_network == nullptr || !m_network->hasGrantAuthority(); +} + /* Grants a TGID on a non-authoritative host. */ void Control::grantTG(uint32_t srcId, uint32_t dstId, bool grp) diff --git a/src/host/nxdn/Control.h b/src/host/nxdn/Control.h index 01069013..ecd1a913 100644 --- a/src/host/nxdn/Control.h +++ b/src/host/nxdn/Control.h @@ -112,7 +112,6 @@ namespace nxdn /** * @brief Helper to set NXDN configuration options. * @param conf Instance of the yaml::Node class. - * @param supervisor Flag indicating whether the DMR has supervisory functions. * @param cwCallsign CW callsign of this host. * @param controlChData Control Channel data. * @param siteId NXDN Site Code. @@ -121,7 +120,7 @@ namespace nxdn * @param channelNo Channel Number. * @param printOptions Flag indicating whether or not options should be printed to log. */ - void setOptions(yaml::Node& conf, bool supervisor, const std::string cwCallsign, lookups::VoiceChData controlChData, + void setOptions(yaml::Node& conf, const std::string cwCallsign, lookups::VoiceChData controlChData, uint16_t siteId, uint32_t sysId, uint8_t channelId, uint32_t channelNo, bool printOptions); /** @name CC Control */ @@ -186,17 +185,16 @@ namespace nxdn void clockSiteData(uint32_t ms); /** @} */ - /** @name Supervisory Control */ - /** - * @brief Sets a flag indicating whether control has supervisory functions and can send permit TG to voice channels. - * @param supervisor Flag indicating whether control has supervisory functions. - */ - void setSupervisor(bool supervisor) { m_supervisor = supervisor; } /** * @brief Permits a TGID on a non-authoritative host. * @param dstId Destination ID. */ void permittedTG(uint32_t dstId); + /** + * @brief Determines whether this control channel currently owns grant decisions. + * @return True for configured authority or disconnected-network fallback. + */ + bool isGrantAuthoritative() const; /** * @brief Grants a TGID on a non-authoritative host. * @param srcId Source Radio ID. @@ -278,7 +276,6 @@ namespace nxdn packet::ControlSignaling* m_control; bool m_authoritative; - bool m_supervisor; uint32_t m_ran; uint32_t m_timeout; @@ -402,7 +399,7 @@ namespace nxdn */ void notifyCC_TouchGrant(uint32_t dstId); - /** @name Supervisory Control */ + /** @name Traffic Permit Control */ /** * @brief (RPC Handler) Permits a TGID on a non-authoritative host. * @param req JSON request. diff --git a/src/host/nxdn/packet/ControlSignaling.cpp b/src/host/nxdn/packet/ControlSignaling.cpp index 99fadb7d..68b1e536 100644 --- a/src/host/nxdn/packet/ControlSignaling.cpp +++ b/src/host/nxdn/packet/ControlSignaling.cpp @@ -185,14 +185,13 @@ bool ControlSignaling::process(FuncChannelType::E fct, ChOption::E option, uint8 (rcch->getEncrypted() ? 0x40U : 0x00U) + // Encrypted Flag (rcch->getPriority() & 0x07U); // Priority - if (m_nxdn->m_authoritative) { + if (m_nxdn->isGrantAuthoritative()) { if (!group && m_nxdn->m_unitToUnitAvailCheck) beginU2UAvailability(srcId, dstId, serviceOptions); else writeRF_Message_Grant(srcId, dstId, serviceOptions, group); } else { - if (m_nxdn->m_network != nullptr) - m_nxdn->m_network->writeGrantReq(modem::DVM_STATE::STATE_NXDN, srcId, dstId, 0U, !group, serviceOptions); + m_nxdn->m_network->writeGrantReq(modem::DVM_STATE::STATE_NXDN, srcId, dstId, 0U, !group, serviceOptions); } } break; @@ -321,7 +320,11 @@ bool ControlSignaling::processNetwork(FuncChannelType::E fct, ChOption::E option (rcch->getEncrypted() ? 0x40U : 0x00U) + // Encrypted Flag (rcch->getPriority() & 0x07U); // Priority - writeRF_Message_Grant(srcId, dstId, serviceOptions, rcch->getGroup(), true); + const bool group = rcch->getGroup(); + if (!writeRF_Message_Grant(srcId, dstId, serviceOptions, group, true) && + m_nxdn->m_network != nullptr) { + m_nxdn->m_network->writeGrantRelease(modem::DVM_STATE::STATE_NXDN, srcId, dstId, 0U, !group, true); + } } } } @@ -753,7 +756,7 @@ bool ControlSignaling::writeRF_Message_Grant(uint32_t srcId, uint32_t dstId, } // callback RPC to permit the granted TG on the specified voice channel - if (m_nxdn->m_authoritative && m_nxdn->m_supervisor) { + { ::lookups::VoiceChData voiceChData = m_nxdn->m_affiliations->rfCh()->getRFChData(chNo); if (voiceChData.isValidCh() && !voiceChData.address().empty() && voiceChData.port() > 0 && chNo != m_nxdn->m_siteData.channelNo()) { @@ -791,7 +794,7 @@ bool ControlSignaling::writeRF_Message_Grant(uint32_t srcId, uint32_t dstId, return false; } } - else { + else if (chNo != m_nxdn->m_siteData.channelNo()) { ::LogError((net) ? LOG_NET : LOG_RF, "NXDN, %s, failed to permit TG for use, chNo = %u", rcch->toString().c_str(), chNo); } } diff --git a/src/host/nxdn/packet/Voice.cpp b/src/host/nxdn/packet/Voice.cpp index e873c0f8..59b7095f 100644 --- a/src/host/nxdn/packet/Voice.cpp +++ b/src/host/nxdn/packet/Voice.cpp @@ -1257,6 +1257,11 @@ bool Voice::checkRFTrafficCollision(uint32_t srcId, uint32_t dstId) bool Voice::checkNetTrafficCollision(lc::RTCH lc, uint32_t srcId, uint32_t dstId) { + // a dedicated control channel does not carry network voice + if (m_nxdn->m_dedicatedControl) { + return true; + } + // don't process network frames if the destination ID's don't match and the RF TG hang timer is running if (m_nxdn->m_rfLastDstId != 0U) { if (m_nxdn->m_rfLastDstId != dstId && (m_nxdn->m_rfTGHang.isRunning() && !m_nxdn->m_rfTGHang.hasExpired())) { diff --git a/src/host/p25/Control.cpp b/src/host/p25/Control.cpp index d96cf813..d33b01a8 100644 --- a/src/host/p25/Control.cpp +++ b/src/host/p25/Control.cpp @@ -61,7 +61,6 @@ Control::Control(bool authoritative, uint32_t nac, uint32_t callHang, uint32_t q m_data(nullptr), m_control(nullptr), m_authoritative(authoritative), - m_supervisor(false), m_nac(nac), m_txNAC(nac), m_timeout(timeout), @@ -234,7 +233,7 @@ void Control::reset() /* Helper to set P25 configuration options. */ -void Control::setOptions(yaml::Node& conf, bool supervisor, const std::string cwCallsign, const ::lookups::VoiceChData controlChData, +void Control::setOptions(yaml::Node& conf, const std::string cwCallsign, const ::lookups::VoiceChData controlChData, uint32_t netId, uint32_t sysId, uint8_t rfssId, uint8_t siteId, uint8_t channelId, uint32_t channelNo, bool printOptions) { yaml::Node systemConf = conf["system"]; @@ -246,8 +245,6 @@ void Control::setOptions(yaml::Node& conf, bool supervisor, const std::string cw m_dfsiFDX = dfsiConf["fullDuplex"].as(false); } - m_supervisor = supervisor; - m_tduPreambleCount = p25Protocol["tduPreambleCount"].as(8U); yaml::Node rfssConfig = systemConf["config"]; @@ -442,22 +439,26 @@ void Control::setOptions(yaml::Node& conf, bool supervisor, const std::string cw m_affiliations->setDisableGrpAffTimeout(disableGrpAffTimeout); // set the grant release callback m_affiliations->setReleaseGrantCallback([=](uint32_t chNo, uint32_t srcId, uint32_t dstId, uint8_t slot) { + const bool networkGrant = m_affiliations->isNetGranted(dstId); + const bool unitToUnit = !m_affiliations->isGroup(dstId); + if (!m_authoritative && networkGrant && m_network != nullptr) { + m_network->writeGrantRelease(modem::DVM_STATE::STATE_P25, srcId, dstId, slot, unitToUnit); + } + m_control->m_telIntGrants.erase(dstId); - // 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->rfCh()->getRFChData(chNo); - if (voiceChData.isValidCh() && !voiceChData.address().empty() && voiceChData.port() > 0 && - chNo != m_siteData.channelNo()) { - json::object req = json::object(); - dstId = 0U; // clear TG value - req["dstId"].set(dstId); + // clear the TG permit on the selected remote voice channel + ::lookups::VoiceChData voiceChData = m_affiliations->rfCh()->getRFChData(chNo); + if (voiceChData.isValidCh() && !voiceChData.address().empty() && voiceChData.port() > 0 && + chNo != m_siteData.channelNo()) { + json::object req = json::object(); + dstId = 0U; // clear TG value + req["dstId"].set(dstId); - g_RPC->req(RPC_PERMIT_P25_TG, req, nullptr, voiceChData.address(), voiceChData.port()); - } - else { - ::LogError(LOG_P25, P25_TSDU_STR ", TSBKO, IOSP_GRP_VCH (Group Voice Channel Grant), failed to clear TG permit, chNo = %u", chNo); - } + g_RPC->req(RPC_PERMIT_P25_TG, req, nullptr, voiceChData.address(), voiceChData.port()); + } + else if (chNo != m_siteData.channelNo()) { + ::LogError(LOG_P25, P25_TSDU_STR ", TSBKO, IOSP_GRP_VCH (Group Voice Channel Grant), failed to clear TG permit, chNo = %u", chNo); } }); @@ -493,8 +494,7 @@ void Control::setOptions(yaml::Node& conf, bool supervisor, const std::string cw if (m_control->m_disableGrantSrcIdCheck) { LogInfo(" Disable Grant Source ID Check: yes"); } - if (m_supervisor) - LogInfoEx(LOG_P25, "Host is configured to operate as a P25 control channel, site controller mode."); + LogInfoEx(LOG_P25, "Host is configured to operate as a P25 control channel."); } if (m_controlOnly) { @@ -1331,6 +1331,14 @@ bool Control::isPermitted(uint32_t srcId, uint32_t dstId, bool group) const return dstId == m_permittedDstId; } +/* Determines whether this control channel currently owns grant decisions. */ + +bool Control::isGrantAuthoritative() const +{ + // A disconnected delegated controller resumes the existing local path. + return m_authoritative || m_network == nullptr || !m_network->hasGrantAuthority(); +} + /* Grants a TGID on a non-authoritative host. */ void Control::grantTG(uint32_t srcId, uint32_t dstId, bool grp) diff --git a/src/host/p25/Control.h b/src/host/p25/Control.h index 4dde5bf9..5b4452f9 100644 --- a/src/host/p25/Control.h +++ b/src/host/p25/Control.h @@ -120,7 +120,6 @@ namespace p25 /** * @brief Helper to set P25 configuration options. * @param conf Instance of the yaml::Node class. - * @param supervisor Flag indicating whether the DMR has supervisory functions. * @param cwCallsign CW callsign of this host. * @param controlChData Control Channel data. * @param pSuperGroup @@ -132,7 +131,7 @@ namespace p25 * @param channelNo Channel Number. * @param printOptions Flag indicating whether or not options should be printed to log. */ - void setOptions(yaml::Node& conf, bool supervisor, const std::string cwCallsign, const ::lookups::VoiceChData controlChData, + void setOptions(yaml::Node& conf, const std::string cwCallsign, const ::lookups::VoiceChData controlChData, uint32_t netId, uint32_t sysId, uint8_t rfssId, uint8_t siteId, uint8_t channelId, uint32_t channelNo, bool printOptions); /** @name CC Control */ @@ -203,12 +202,6 @@ namespace p25 void clockSiteData(uint32_t ms); /** @} */ - /** @name Supervisory Control */ - /** - * @brief Sets a flag indicating whether control has supervisory functions and can send permit TG to voice channels. - * @param supervisor Flag indicating whether control has supervisory functions. - */ - void setSupervisor(bool supervisor) { m_supervisor = supervisor; } /** * @brief Permits a TGID on a non-authoritative host. * @param dstId Destination ID. @@ -225,6 +218,11 @@ namespace p25 * @returns bool True when the traffic matches the active permit. */ bool isPermitted(uint32_t srcId, uint32_t dstId, bool group) const; + /** + * @brief Determines whether this control channel currently owns grant decisions. + * @return True for configured authority or disconnected-network fallback. + */ + bool isGrantAuthoritative() const; /** * @brief Grants a TGID on a non-authoritative host. * @param srcId Source Radio ID. @@ -307,7 +305,6 @@ namespace p25 friend class lookups::P25AffiliationLookup; bool m_authoritative; - bool m_supervisor; uint32_t m_nac; uint32_t m_txNAC; @@ -506,7 +503,7 @@ namespace p25 */ void writeRF_TDU(bool noNetwork, bool imm = false); - /** @name Supervisory Control */ + /** @name Traffic Permit Control */ /** * @brief (RPC Handler) Permits a TGID on a non-authoritative host. * @param req JSON request. diff --git a/src/host/p25/packet/ControlSignaling.cpp b/src/host/p25/packet/ControlSignaling.cpp index 91ab483e..bffc2a32 100644 --- a/src/host/p25/packet/ControlSignaling.cpp +++ b/src/host/p25/packet/ControlSignaling.cpp @@ -255,12 +255,10 @@ bool ControlSignaling::process(uint8_t* data, uint32_t len, std::unique_ptrgetEncrypted() ? 0x40U : 0x00U) + // Encrypted Flag (tsbk->getPriority() & 0x07U); // Priority - if (m_p25->m_authoritative) { + if (m_p25->isGrantAuthoritative()) { writeRF_TSDU_Grant(srcId, dstId, serviceOptions, true); } else { - if (m_p25->m_network != nullptr) - m_p25->m_network->writeGrantReq(modem::DVM_STATE::STATE_P25, - srcId, dstId, 0U, false, serviceOptions); + m_p25->m_network->writeGrantReq(modem::DVM_STATE::STATE_P25, srcId, dstId, 0U, false, serviceOptions); } } break; @@ -289,15 +287,13 @@ bool ControlSignaling::process(uint8_t* data, uint32_t len, std::unique_ptrm_authoritative) { + if (m_p25->isGrantAuthoritative()) { // TIA-102.AABD-B 4.2.1.2.2 and 7.3.6: the requesting SU // is first to talk and therefore occupies the grant source field - writeRF_TSDU_Grant(srcId, dstId, serviceOptions, false, - false, false, 0U, false, srcId); + writeRF_TSDU_Grant(srcId, dstId, serviceOptions, false, false, false, 0U, false, srcId); } - else if (m_p25->m_network != nullptr) { - m_p25->m_network->writeGrantReq(modem::DVM_STATE::STATE_P25, - srcId, dstId, 0U, true, serviceOptions); + else { + m_p25->m_network->writeGrantReq(modem::DVM_STATE::STATE_P25, srcId, dstId, 0U, true, serviceOptions); } break; } @@ -311,12 +307,10 @@ bool ControlSignaling::process(uint8_t* data, uint32_t len, std::unique_ptrm_authoritative) { + if (m_p25->isGrantAuthoritative()) { writeRF_TSDU_Grant(srcId, dstId, serviceOptions, false); } else { - if (m_p25->m_network != nullptr) - m_p25->m_network->writeGrantReq(modem::DVM_STATE::STATE_P25, - srcId, dstId, 0U, true, serviceOptions); + m_p25->m_network->writeGrantReq(modem::DVM_STATE::STATE_P25, srcId, dstId, 0U, true, serviceOptions); } } } @@ -361,13 +355,12 @@ bool ControlSignaling::process(uint8_t* data, uint32_t len, std::unique_ptrgetResponse() == ResponseCode::ANS_PROCEED) { - if (m_p25->m_authoritative) { + if (m_p25->isGrantAuthoritative()) { // select the answering unit as first talker and address the grant // to the original caller so both subscribers follow the assignment writeRF_TSDU_Grant(srcId, dstId, transaction.serviceOptions, false, false, false, 0U, false, srcId); } else { - if (m_p25->m_network != nullptr) - m_p25->m_network->writeGrantReq(modem::DVM_STATE::STATE_P25, srcId, dstId, 0U, true, transaction.serviceOptions); + m_p25->m_network->writeGrantReq(modem::DVM_STATE::STATE_P25, srcId, dstId, 0U, true, transaction.serviceOptions); } m_u2uAvailabilityTransactions.erase(transactionIt); @@ -1120,7 +1113,10 @@ bool ControlSignaling::processNetwork(uint8_t* data, uint32_t len, lc::LC& contr (tsbk->getEncrypted() ? 0x40U : 0x00U) + // Encrypted Flag (tsbk->getPriority() & 0x07U); // Priority - writeRF_TSDU_Grant(srcId, dstId, serviceOptions, (tsbk->getLCO() == TSBKO::IOSP_GRP_VCH), true); + const bool group = tsbk->getLCO() == TSBKO::IOSP_GRP_VCH; + if (!writeRF_TSDU_Grant(srcId, dstId, serviceOptions, group, true) && m_p25->m_network != nullptr) { + m_p25->m_network->writeGrantRelease(modem::DVM_STATE::STATE_P25, srcId, dstId, 0U, !group, true); + } } } } @@ -1156,12 +1152,12 @@ bool ControlSignaling::processNetwork(uint8_t* data, uint32_t len, lc::LC& contr // handle the response based on the ISP code if (iosp->getResponse() == ResponseCode::ANS_PROCEED) { - if (m_p25->m_authoritative) { + if (m_p25->isGrantAuthoritative()) { // select the answering unit as first talker and address the grant // to the original caller so both subscribers follow the assignment writeRF_TSDU_Grant(srcId, dstId, transaction.serviceOptions, false, false, false, 0U, false, srcId); } - else if (m_p25->m_network != nullptr) { + else { m_p25->m_network->writeGrantReq(modem::DVM_STATE::STATE_P25, srcId, dstId, 0U, true, transaction.serviceOptions); } @@ -1761,7 +1757,7 @@ void ControlSignaling::resetGroupAffQueryTransactions() /* Write data processed from RF to the network. */ -void ControlSignaling::writeNetworkRF(lc::TSBK* tsbk, const uint8_t* data, bool autoReset) +void ControlSignaling::writeNetworkRF(lc::TSBK* tsbk, const uint8_t* data, bool autoReset, bool inbound) { assert(tsbk != nullptr); assert(data != nullptr); @@ -1789,7 +1785,8 @@ void ControlSignaling::writeNetworkRF(lc::TSBK* tsbk, const uint8_t* data, bool lc.setSrcId(tsbk->getSrcId()); lc.setDstId(tsbk->getDstId()); - m_p25->m_network->writeP25TSDU(lc, data, network::NET_CTRL_ISP); + const uint8_t controlByte = inbound ? network::NET_CTRL_ISP : 0U; + m_p25->m_network->writeP25TSDU(lc, data, controlByte); if (autoReset) m_p25->m_network->resetP25(); } @@ -1945,7 +1942,7 @@ void ControlSignaling::writeRF_TSDU_SBF(lc::TSBK* tsbk, bool noNetwork, bool for P25Utils::setStatusBitsStartIdle(data + 2U); if (!noNetwork) - writeNetworkRF(tsbk, data + 2U, true); + writeNetworkRF(tsbk, data + 2U, true, false); // we always force any immediate TSDUs as single-block if (imm) { @@ -2862,7 +2859,7 @@ bool ControlSignaling::writeRF_TSDU_Grant(uint32_t srcId, uint32_t dstId, uint8_ if (grp) { // callback RPC to permit the granted TG on the specified voice channel - if (m_p25->m_authoritative && m_p25->m_supervisor) { + { if (voiceChData.isValidCh() && !voiceChData.address().empty() && voiceChData.port() > 0 && chNo != m_p25->m_siteData.channelNo()) { json::object req = json::object(); @@ -2899,7 +2896,7 @@ bool ControlSignaling::writeRF_TSDU_Grant(uint32_t srcId, uint32_t dstId, uint8_ return false; } } - else { + else if (chNo != m_p25->m_siteData.channelNo()) { ::LogError((net) ? LOG_NET : LOG_RF, P25_TSDU_STR ", TSBKO, IOSP_GRP_VCH (Group Voice Channel Request), failed to permit TG for use, chNo = %u-%u", voiceChData.chId(), chNo); } } @@ -2966,7 +2963,7 @@ bool ControlSignaling::writeRF_TSDU_Grant(uint32_t srcId, uint32_t dstId, uint8_ } else { // callback RPC to permit the granted TG on the specified voice channel - if (m_p25->m_authoritative && m_p25->m_supervisor) { + { if (voiceChData.isValidCh() && !voiceChData.address().empty() && voiceChData.port() > 0 && chNo != m_p25->m_siteData.channelNo()) { json::object req = json::object(); @@ -3009,7 +3006,7 @@ bool ControlSignaling::writeRF_TSDU_Grant(uint32_t srcId, uint32_t dstId, uint8_ return false; } } - else { + else if (chNo != m_p25->m_siteData.channelNo()) { ::LogError((net) ? LOG_NET : LOG_RF, P25_TSDU_STR ", TSBKO, IOSP_UU_VCH (Unit-to-Unit Voice Channel Request), failed to permit TG for use, chNo = %u-%u", voiceChData.chId(), chNo); } } @@ -3297,7 +3294,7 @@ bool ControlSignaling::writeRF_TSDU_SNDCP_Grant(uint32_t srcId, bool skip, uint3 ::lookups::VoiceChData voiceChData = m_p25->m_affiliations->rfCh()->getRFChData(chNo); // callback RPC to permit the granted TG on the specified voice channel - if (m_p25->m_authoritative && m_p25->m_supervisor) { + { if (voiceChData.isValidCh() && !voiceChData.address().empty() && voiceChData.port() > 0 && chNo != m_p25->m_siteData.channelNo()) { json::object req = json::object(); @@ -3336,7 +3333,7 @@ bool ControlSignaling::writeRF_TSDU_SNDCP_Grant(uint32_t srcId, bool skip, uint3 return false; } } - else { + else if (chNo != m_p25->m_siteData.channelNo()) { ::LogError(LOG_RF, P25_TSDU_STR ", TSBKO, ISP_SNDCP_CH_REQ (SNDCP Data Channel Request), failed to permit for use, chNo = %u-%u", voiceChData.chId(), chNo); } } diff --git a/src/host/p25/packet/ControlSignaling.h b/src/host/p25/packet/ControlSignaling.h index b1d01894..8df21b1f 100644 --- a/src/host/p25/packet/ControlSignaling.h +++ b/src/host/p25/packet/ControlSignaling.h @@ -147,6 +147,7 @@ namespace p25 */ void setLastMFId(uint8_t mfId) { m_lastMFID = mfId; } + /** * @brief Flag indicating whether P25 TSBK verbosity is enabled or not. * @returns bool Flag indicating whether TSBK verbosity is enabled. @@ -322,8 +323,9 @@ namespace p25 * @param tsbk TSBK to write to network. * @param data Buffer containing encoded TSBK. * @param autoReset After writing TSBK to network, reset P25 network state. + * @param inbound Flag indicating the TSBK originated on the inbound RF path. */ - void writeNetworkRF(lc::TSBK* tsbk, const uint8_t* data, bool autoReset); + void writeNetworkRF(lc::TSBK* tsbk, const uint8_t* data, bool autoReset, bool inbound = true); /** * @brief Write data processed from RF to the network. * @param tduLc TDULC to write to network. diff --git a/src/host/p25/packet/Voice.cpp b/src/host/p25/packet/Voice.cpp index a7974a3e..511abaa3 100644 --- a/src/host/p25/packet/Voice.cpp +++ b/src/host/p25/packet/Voice.cpp @@ -1594,6 +1594,11 @@ bool Voice::checkRFTrafficCollision(uint32_t srcId, uint32_t dstId, bool group) bool Voice::checkNetTrafficCollision(uint32_t srcId, uint32_t dstId, bool group, defines::DUID::E duid) { + // a dedicated control channel does not carry network voice + if (m_p25->m_dedicatedControl && !m_p25->m_voiceOnControl) { + return true; + } + // safety check: if the network is marked as not idle, but the last network destination ID is 0 *and* the // network watchdog isn't running, then reset the network state to idle if (m_p25->m_netState != RS_NET_IDLE && m_p25->m_netLastDstId == 0U && !m_p25->m_networkWatchdog.isRunning()) { diff --git a/src/host/p25/phase2/Control.h b/src/host/p25/phase2/Control.h index 28999bee..ee46665d 100644 --- a/src/host/p25/phase2/Control.h +++ b/src/host/p25/phase2/Control.h @@ -155,7 +155,7 @@ namespace p25 */ bool isBusy() const; - /** @name Supervisory Control */ + /** @name Traffic Permit Control */ /** * @brief Permits a destination on a non-authoritative Phase 2 slot. * @param dstId Destination ID. diff --git a/src/host/p25/phase2/Slot.h b/src/host/p25/phase2/Slot.h index 5968c510..4383921d 100644 --- a/src/host/p25/phase2/Slot.h +++ b/src/host/p25/phase2/Slot.h @@ -122,7 +122,7 @@ namespace p25 void clock(uint32_t ms); /** @} */ - /** @name Supervisory Control */ + /** @name Traffic Permit Control */ /** * @brief Permits a TGID on a non-authoritative host. * @param dstId Destination ID. diff --git a/src/host/restapi/RESTAPI.cpp b/src/host/restapi/RESTAPI.cpp index 5b7ff630..2f646c89 100644 --- a/src/host/restapi/RESTAPI.cpp +++ b/src/host/restapi/RESTAPI.cpp @@ -291,7 +291,6 @@ void RESTAPI::initializeEndpoints() m_dispatcher.match(PUT_MDM_MODE).put(REST_API_BIND(RESTAPI::restAPI_PutModemMode, this)); m_dispatcher.match(PUT_MDM_KILL).put(REST_API_BIND(RESTAPI::restAPI_PutModemKill, this)); - m_dispatcher.match(PUT_SET_SUPERVISOR).put(REST_API_BIND(RESTAPI::restAPI_PutSetSupervisor, this)); m_dispatcher.match(PUT_PERMIT_TG).put(REST_API_BIND(RESTAPI::restAPI_PutPermitTG, this)); m_dispatcher.match(PUT_GRANT_TG).put(REST_API_BIND(RESTAPI::restAPI_PutGrantTG, this)); m_dispatcher.match(GET_RELEASE_GRNTS).get(REST_API_BIND(RESTAPI::restAPI_GetReleaseGrants, this)); @@ -657,79 +656,6 @@ void RESTAPI::restAPI_PutModemKill(const HTTPPayload& request, HTTPPayload& repl } } -/* REST API endpoint; implements set supervisory mode request. */ - -void RESTAPI::restAPI_PutSetSupervisor(const HTTPPayload& request, HTTPPayload& reply, const RequestMatch& match) -{ - if (!validateAuth(request, reply)) { - return; - } - - json::object req = json::object(); - if (!parseRequestBody(request, reply, req)) { - return; - } - - errorPayload(reply, "OK", HTTPPayload::OK); - - if (!m_host->m_authoritative) { - errorPayload(reply, "Host is not authoritative, cannot set supervisory state"); - return; - } - - // validate state is a string within the JSON blob - if (!req["state"].is()) { - errorPayload(reply, "state was not a valid integer"); - return; - } - - DVM_STATE state = (DVM_STATE)req["state"].get(); - - // validate destination ID is a integer within the JSON blob - if (!req["enable"].is()) { - errorPayload(reply, "enable was not a boolean"); - return; - } - - bool enable = req["enable"].get(); - - switch (state) { - case STATE_DMR: - { - if (m_dmr != nullptr) { - m_dmr->setSupervisor(enable); - } - else { - errorPayload(reply, "DMR mode is not enabled", HTTPPayload::SERVICE_UNAVAILABLE); - } - } - break; - case STATE_P25: - { - if (m_p25 != nullptr) { - m_p25->setSupervisor(enable); - } - else { - errorPayload(reply, "P25 mode is not enabled", HTTPPayload::SERVICE_UNAVAILABLE); - } - } - break; - case STATE_NXDN: - { - if (m_nxdn != nullptr) { - m_nxdn->setSupervisor(enable); - } - else { - errorPayload(reply, "NXDN mode is not enabled", HTTPPayload::SERVICE_UNAVAILABLE); - } - } - break; - default: - errorPayload(reply, "invalid mode"); - break; - } -} - /* REST API endpoint; implements permit TG request. */ void RESTAPI::restAPI_PutPermitTG(const HTTPPayload& request, HTTPPayload& reply, const RequestMatch& match) diff --git a/src/host/restapi/RESTAPI.h b/src/host/restapi/RESTAPI.h index e4cd6d57..43d0f7c2 100644 --- a/src/host/restapi/RESTAPI.h +++ b/src/host/restapi/RESTAPI.h @@ -191,13 +191,6 @@ namespace host_restapi */ void restAPI_PutModemKill(const HTTPPayload& request, HTTPPayload& reply, const restapi::RequestMatch& match); - /** - * @brief REST API endpoint; implements set supervisory mode request. - * @param request HTTP request. - * @param reply HTTP reply. - * @param match HTTP request matcher. - */ - void restAPI_PutSetSupervisor(const HTTPPayload& request, HTTPPayload& reply, const restapi::RequestMatch& match); /** * @brief REST API endpoint; implements permit TG request. * @param request HTTP request. diff --git a/src/host/restapi/RESTDefines.h b/src/host/restapi/RESTDefines.h index 4659ffdf..ce254a4f 100644 --- a/src/host/restapi/RESTDefines.h +++ b/src/host/restapi/RESTDefines.h @@ -59,7 +59,6 @@ #define PUT_MDM_KILL "/mdm/kill" -#define PUT_SET_SUPERVISOR "/set-supervisor" #define PUT_PERMIT_TG "/permit-tg" #define PUT_GRANT_TG "/grant-tg" #define GET_RELEASE_GRNTS "/release-grants" diff --git a/tests/dmr/HostControl_Tests.cpp b/tests/dmr/HostControl_Tests.cpp index 9c567467..5ce99513 100644 --- a/tests/dmr/HostControl_Tests.cpp +++ b/tests/dmr/HostControl_Tests.cpp @@ -1016,3 +1016,66 @@ TEST_CASE("DMR rfTGHang zero fallback arms rfLossWatchdog and recovers RF state" REQUIRE(HostTestHooks::dmrRFState(*slot) == RS_RF_LISTENING); REQUIRE_FALSE(HostTestHooks::dmrRFLossWatchdog(*slot).isRunning()); } + +TEST_CASE("DMR grant authority follows configuration and FNE availability", + "[dmr][host][control][grant-authority]") +{ + DMRHostHarness delegated(false, true); + dmr::Slot* delegatedSlot = HostTestHooks::dmrSlot1(*delegated.m_control); + REQUIRE(delegatedSlot->isGrantAuthoritative()); + + delegated.network()->activateForTest(); + REQUIRE_FALSE(delegatedSlot->isGrantAuthoritative()); + + DMRHostHarness local(true, true); + local.network()->activateForTest(); + dmr::Slot* localSlot = HostTestHooks::dmrSlot1(*local.m_control); + REQUIRE(localSlot->isGrantAuthoritative()); +} + +TEST_CASE("DMR FNE grant responses are rebuilt with local channel assignments", + "[dmr][host][control][grant-response]") +{ + constexpr uint32_t CHANNEL_NO = 101U; + constexpr uint32_t SOURCE_ID = 1001U; + constexpr uint32_t DESTINATION_ID = 2001U; + + SECTION("group grant") { + DMRHostHarness harness(false, true); + harness.network()->activateForTest(); + REQUIRE(harness.m_chLookup.initializeRFCh(CHANNEL_NO)); + harness.m_tidLookup.addEntry(admissionRule(DESTINATION_ID, 2U, { SOURCE_ID })); + HostTestHooks::dmrEnableTier3(*harness.m_control, CHANNEL_NO, 1U, true); + dmr::Slot* tscc = HostTestHooks::dmrSlot1(*harness.m_control); + + HostTestHooks::dmrProcessNetworkGrant(*tscc, SOURCE_ID, DESTINATION_ID, true, true); + + CHECK(HostTestHooks::dmrIsGranted(*tscc, DESTINATION_ID)); + CHECK(HostTestHooks::dmrIsGroupGrant(*tscc, DESTINATION_ID)); + } + + SECTION("private grant") { + DMRHostHarness harness(false, true); + harness.network()->activateForTest(); + REQUIRE(harness.m_chLookup.initializeRFCh(CHANNEL_NO)); + HostTestHooks::dmrEnableTier3(*harness.m_control, CHANNEL_NO, 1U, true); + dmr::Slot* tscc = HostTestHooks::dmrSlot1(*harness.m_control); + HostTestHooks::dmrRegisterUnit(*tscc, DESTINATION_ID); + + HostTestHooks::dmrProcessNetworkGrant(*tscc, SOURCE_ID, DESTINATION_ID, false); + + CHECK(HostTestHooks::dmrIsGranted(*tscc, DESTINATION_ID)); + CHECK_FALSE(HostTestHooks::dmrIsGroupGrant(*tscc, DESTINATION_ID)); + } + + SECTION("allocation failure releases the FNE grant") { + DMRHostHarness harness(false, true); + harness.network()->activateForTest(); + harness.m_tidLookup.addEntry(admissionRule(DESTINATION_ID, 2U, { SOURCE_ID })); + HostTestHooks::dmrEnableTier3(*harness.m_control, 0U, 1U, true); + dmr::Slot* tscc = HostTestHooks::dmrSlot1(*harness.m_control); + HostTestHooks::dmrProcessNetworkGrant(*tscc, SOURCE_ID, DESTINATION_ID, true); + + CHECK_FALSE(HostTestHooks::dmrIsGranted(*tscc, DESTINATION_ID)); + } +} diff --git a/tests/fne/FNENetwork_Tests.cpp b/tests/fne/FNENetwork_Tests.cpp index bed252fc..3ccd492c 100644 --- a/tests/fne/FNENetwork_Tests.cpp +++ b/tests/fne/FNENetwork_Tests.cpp @@ -798,7 +798,61 @@ TEST_CASE("FNE private-call grants target only the registered next hop", harness.traffic, SOURCE_PEER, SOURCE_ID, DESTINATION_ID, true); } REQUIRE(accepted); - CHECK(writes == std::vector{ TARGET_PEER }); + std::sort(writes.begin(), writes.end()); + CHECK(writes == std::vector{ SOURCE_PEER, TARGET_PEER }); + } +} + +TEST_CASE("FNE private-call grants support retries and reverse talker direction", + "[fne][callhandler][routing][u2u][grants][continuation]") +{ + constexpr uint32_t SOURCE_PEER = 1001U; + constexpr uint32_t TARGET_PEER = 1002U; + constexpr uint32_t SOURCE_ID = 2001U; + constexpr uint32_t DESTINATION_ID = 2002U; + + const std::vector protocols = { + FNETestHooks::ParrotProtocol::DMR, + FNETestHooks::ParrotProtocol::P25, + FNETestHooks::ParrotProtocol::NXDN + }; + + for (FNETestHooks::ParrotProtocol protocol : protocols) { + CAPTURE(static_cast(protocol)); + FNEHarness harness; + FNETestHooks::addPeer(harness.traffic, SOURCE_PEER, network::NET_STAT_RUNNING, true); + FNETestHooks::addPeer(harness.traffic, TARGET_PEER, network::NET_STAT_RUNNING, true); + harness.radios.addEntry(SOURCE_ID, true, "test source"); + harness.radios.addEntry(DESTINATION_ID, true, "test destination"); + FNETestHooks::registerP25Unit(harness.traffic, SOURCE_PEER, SOURCE_ID); + FNETestHooks::registerP25Unit(harness.traffic, TARGET_PEER, DESTINATION_ID); + + auto request = [&](uint32_t peerId, uint32_t sourceId, uint32_t destinationId) { + if (protocol == FNETestHooks::ParrotProtocol::DMR) { + return FNETestHooks::processDMRGrantRequest( + harness.traffic, peerId, sourceId, destinationId, true); + } + if (protocol == FNETestHooks::ParrotProtocol::P25) { + return FNETestHooks::processP25GrantRequest( + harness.traffic, peerId, sourceId, destinationId, true); + } + return FNETestHooks::processNXDNGrantRequest( + harness.traffic, peerId, sourceId, destinationId, true); + }; + + std::vector writes; + FNETestHooks::capturePeerWrites(harness.traffic, writes); + REQUIRE(request(SOURCE_PEER, SOURCE_ID, DESTINATION_ID)); + + writes.clear(); + REQUIRE(request(SOURCE_PEER, SOURCE_ID, DESTINATION_ID)); + std::sort(writes.begin(), writes.end()); + CHECK(writes == std::vector{ SOURCE_PEER, TARGET_PEER }); + + writes.clear(); + REQUIRE(request(TARGET_PEER, DESTINATION_ID, SOURCE_ID)); + std::sort(writes.begin(), writes.end()); + CHECK(writes == std::vector{ SOURCE_PEER, TARGET_PEER }); } } @@ -860,7 +914,7 @@ TEST_CASE("FNE private-call flood reaches every authorized transit peer", REQUIRE(accepted); std::sort(writes.begin(), writes.end()); - CHECK(writes == std::vector{ ALLOWED_PEER_A, ALLOWED_PEER_B }); + CHECK(writes == std::vector{ SOURCE_PEER, ALLOWED_PEER_A, ALLOWED_PEER_B }); } } @@ -983,7 +1037,75 @@ TEST_CASE("FNE DMR individual grant demand targets the called unit site", FNETestHooks::capturePeerWrites(harness.traffic, writes); REQUIRE(FNETestHooks::processDMRGrantRequest( harness.traffic, SOURCE_PEER, SOURCE_ID, DESTINATION_ID, true)); - CHECK(writes == std::vector{ TARGET_SITE }); + std::sort(writes.begin(), writes.end()); + CHECK(writes == std::vector{ SOURCE_PEER, TARGET_SITE }); +} + +TEST_CASE("FNE grant release requires the originating peer and matching call", + "[fne][callhandler][grant][release]") +{ + constexpr uint32_t OWNER_PEER = 1001U; + constexpr uint32_t OTHER_PEER = 1002U; + constexpr uint32_t SOURCE_ID = 2001U; + constexpr uint32_t TALK_GROUP = 3100U; + + FNEHarness harness; + FNETestHooks::addPeer(harness.traffic, OWNER_PEER, network::NET_STAT_RUNNING, true); + FNETestHooks::addPeer(harness.traffic, OTHER_PEER, network::NET_STAT_RUNNING, true); + harness.radios.addEntry(SOURCE_ID, true, "test source"); + harness.talkgroups.addEntry(TALK_GROUP, 1U, true, false); + + REQUIRE(FNETestHooks::processP25GrantRequest( + harness.traffic, OWNER_PEER, SOURCE_ID, TALK_GROUP, false)); + CHECK_FALSE(FNETestHooks::processGrantRelease( + harness.traffic, OTHER_PEER, SOURCE_ID, TALK_GROUP, false)); + CHECK_FALSE(FNETestHooks::processGrantRelease( + harness.traffic, OWNER_PEER, SOURCE_ID + 1U, TALK_GROUP, false)); + CHECK_FALSE(FNETestHooks::processGrantRelease( + harness.traffic, OWNER_PEER, SOURCE_ID, TALK_GROUP, true)); + CHECK(FNETestHooks::processGrantRelease( + harness.traffic, OWNER_PEER, SOURCE_ID, TALK_GROUP, false)); + CHECK_FALSE(FNETestHooks::processGrantRelease( + harness.traffic, OWNER_PEER, SOURCE_ID, TALK_GROUP, false)); +} + +TEST_CASE("FNE grant release resolves reverse U2U direction", + "[fne][callhandler][grant][release][u2u]") +{ + constexpr uint32_t OWNER_PEER = 1001U; + constexpr uint32_t SOURCE_ID = 2001U; + constexpr uint32_t DESTINATION_ID = 2002U; + + FNEHarness harness; + FNETestHooks::addPeer(harness.traffic, OWNER_PEER, network::NET_STAT_RUNNING, true); + harness.radios.addEntry(SOURCE_ID, true, "test source"); + harness.radios.addEntry(DESTINATION_ID, true, "test destination"); + FNETestHooks::registerP25Unit(harness.traffic, OWNER_PEER, DESTINATION_ID); + + REQUIRE(FNETestHooks::processP25GrantRequest( + harness.traffic, OWNER_PEER, SOURCE_ID, DESTINATION_ID, true)); + CHECK(FNETestHooks::processGrantRelease( + harness.traffic, OWNER_PEER, DESTINATION_ID, SOURCE_ID, true)); +} + +TEST_CASE("FNE failed grant commit releases the reserved grant", + "[fne][callhandler][grant][release][failed]") +{ + constexpr uint32_t OWNER_PEER = 1001U; + constexpr uint32_t SOURCE_ID = 2001U; + constexpr uint32_t TALK_GROUP = 3100U; + + FNEHarness harness; + FNETestHooks::addPeer(harness.traffic, OWNER_PEER, network::NET_STAT_RUNNING, true); + harness.radios.addEntry(SOURCE_ID, true, "test source"); + harness.talkgroups.addEntry(TALK_GROUP, 1U, true, false); + + REQUIRE(FNETestHooks::processP25GrantRequest( + harness.traffic, OWNER_PEER, SOURCE_ID, TALK_GROUP, false)); + CHECK(FNETestHooks::processGrantRelease( + harness.traffic, OWNER_PEER, SOURCE_ID, TALK_GROUP, false, true)); + CHECK_FALSE(FNETestHooks::processGrantRelease( + harness.traffic, OWNER_PEER, SOURCE_ID, TALK_GROUP, false, true)); } TEST_CASE("FNE DMR group grant demand retains group peer routing", @@ -1009,7 +1131,7 @@ TEST_CASE("FNE DMR group grant demand retains group peer routing", REQUIRE(FNETestHooks::processDMRGrantRequest( harness.traffic, SOURCE_PEER, SOURCE_ID, TALK_GROUP, false)); std::sort(writes.begin(), writes.end()); - CHECK(writes == std::vector{ SITE_A, SITE_B }); + CHECK(writes == std::vector{ SOURCE_PEER, SITE_A, SITE_B }); } TEST_CASE("FNE NXDN individual grant demand targets the called unit site", @@ -1036,7 +1158,8 @@ TEST_CASE("FNE NXDN individual grant demand targets the called unit site", FNETestHooks::capturePeerWrites(harness.traffic, writes); REQUIRE(FNETestHooks::processNXDNGrantRequest( harness.traffic, SOURCE_PEER, SOURCE_ID, DESTINATION_ID, true)); - CHECK(writes == std::vector{ TARGET_SITE }); + std::sort(writes.begin(), writes.end()); + CHECK(writes == std::vector{ SOURCE_PEER, TARGET_SITE }); } TEST_CASE("FNE NXDN group grant demand retains group peer routing", @@ -1062,7 +1185,7 @@ TEST_CASE("FNE NXDN group grant demand retains group peer routing", REQUIRE(FNETestHooks::processNXDNGrantRequest( harness.traffic, SOURCE_PEER, SOURCE_ID, TALK_GROUP, false)); std::sort(writes.begin(), writes.end()); - CHECK(writes == std::vector{ SITE_A, SITE_B }); + CHECK(writes == std::vector{ SOURCE_PEER, SITE_A, SITE_B }); } TEST_CASE("FNE call handlers validate source radios and group state", "[fne][callhandler][validation]") diff --git a/tests/fne/FNETestHooks.cpp b/tests/fne/FNETestHooks.cpp index 99d4599e..fa035874 100644 --- a/tests/fne/FNETestHooks.cpp +++ b/tests/fne/FNETestHooks.cpp @@ -342,6 +342,14 @@ bool FNETestHooks::processNXDNGrantRequest(TrafficNetwork& network, uint32_t pee sourceId, destinationId, unitToUnit, serviceOptions, peerId, 1U, 106U); } +/* Processes a synthetic FNE grant release. */ + +bool FNETestHooks::processGrantRelease(TrafficNetwork& network, uint32_t peerId, + uint32_t sourceId, uint32_t destinationId, bool unitToUnit, bool failed) +{ + return network.processGrantRelease(sourceId, destinationId, unitToUnit, peerId, failed); +} + /* Sets whether unit-to-unit traffic is globally disabled. */ void FNETestHooks::disallowU2U(TrafficNetwork& network, bool disabled) diff --git a/tests/fne/FNETestHooks.h b/tests/fne/FNETestHooks.h index a1824415..dcf8ce1a 100644 --- a/tests/fne/FNETestHooks.h +++ b/tests/fne/FNETestHooks.h @@ -221,6 +221,18 @@ public: */ static bool processNXDNGrantRequest(network::TrafficNetwork& network, uint32_t peerId, uint32_t sourceId, uint32_t destinationId, bool unitToUnit, uint8_t serviceOptions = 4U); + /** + * @brief Processes a synthetic FNE grant release. + * @param network The TrafficNetwork instance. + * @param peerId Peer requesting the release. + * @param sourceId Calling subscriber ID. + * @param destinationId Called subscriber or talkgroup ID. + * @param unitToUnit Whether the grant is an individual call. + * @param failed Whether the owning site failed to commit the grant locally. + * @return True if a matching grant was released. + */ + static bool processGrantRelease(network::TrafficNetwork& network, uint32_t peerId, + uint32_t sourceId, uint32_t destinationId, bool unitToUnit, bool failed = false); /** * @brief Sets whether unit-to-unit traffic is globally disabled. * @param network The TrafficNetwork instance. diff --git a/tests/host/HostTestHooks.cpp b/tests/host/HostTestHooks.cpp index 22cab11c..c568f683 100644 --- a/tests/host/HostTestHooks.cpp +++ b/tests/host/HostTestHooks.cpp @@ -11,6 +11,8 @@ #include "common/dmr/SlotType.h" #include "common/dmr/Sync.h" #include "common/dmr/lc/FullLC.h" +#include "common/dmr/lc/csbk/CSBK_PV_GRANT.h" +#include "common/dmr/lc/csbk/CSBK_TV_GRANT.h" #include "common/nxdn/NXDNDefines.h" #include "common/nxdn/NXDNUtils.h" #include "common/nxdn/Sync.h" @@ -25,6 +27,7 @@ #include "host/p25/packet/Voice.h" #include +#include #if defined(CATCH2_TEST_COMPILATION) @@ -152,6 +155,43 @@ void HostTestHooks::dmrStartNetVoiceCall(dmr::Slot& slot, uint32_t srcId, uint32 slot.processNetwork(dmrData); } +/* Injects an FNE-generated DMR voice grant response. */ + +void HostTestHooks::dmrProcessNetworkGrant(dmr::Slot& slot, uint32_t srcId, uint32_t dstId, + bool group, bool emergency) +{ + std::unique_ptr grant; + if (group) + grant = std::make_unique(); + else + grant = std::make_unique(); + + grant->setSrcId(srcId); + grant->setDstId(dstId); + grant->setLogicalCh1(0U); + grant->setSlotNo(0U); + grant->setEmergency(emergency); + + uint8_t data[dmr::defines::DMR_FRAME_LENGTH_BYTES]; + ::memset(data, 0x00U, sizeof(data)); + grant->encode(data); + + dmr::SlotType slotType; + slotType.setColorCode(0U); + slotType.setDataType(dmr::defines::DataType::CSBK); + slotType.encode(data); + dmr::Sync::addDMRDataSync(data, true); + + dmr::data::NetData dmrData; + dmrData.setSlotNo(slot.m_slotNo); + dmrData.setDataType(dmr::defines::DataType::CSBK); + dmrData.setSrcId(srcId); + dmrData.setDstId(dstId); + dmrData.setFLCO(group ? dmr::defines::FLCO::GROUP : dmr::defines::FLCO::PRIVATE); + dmrData.setData(data); + slot.processNetwork(dmrData); +} + /* Injects synthetic DMR RF voice call frame. */ bool HostTestHooks::dmrStartRFVoiceCall(dmr::Slot& slot, uint32_t srcId, uint32_t dstId, bool group) @@ -187,14 +227,15 @@ void HostTestHooks::dmrDetachNetwork() /* Configures a co-located DMR Tier III control channel. */ -void HostTestHooks::dmrEnableTier3(dmr::Control& control, uint32_t channelNo, uint8_t tsccSlot) +void HostTestHooks::dmrEnableTier3(dmr::Control& control, uint32_t channelNo, uint8_t tsccSlot, + bool dedicated) { control.m_enableTSCC = true; control.m_tsccSlotNo = tsccSlot; dmr::Slot::s_channelNo = channelNo; dmr::Slot* slot = tsccSlot == 1U ? control.m_slot1 : control.m_slot2; - slot->setTSCC(true, false); + slot->setTSCC(true, dedicated); } /* Gets the active DMR Tier III payload destination. */ @@ -426,6 +467,20 @@ void HostTestHooks::p25ConfigureRFGrantSteering(p25::Control& control, bool enab control.m_allowRFGrantTGSteering = enabled; } +/* Configures whether a dedicated P25 control channel carries voice. */ + +void HostTestHooks::p25SetVoiceOnControl(p25::Control& control, bool enabled) +{ + control.m_voiceOnControl = enabled; +} + +/* Runs P25 network-voice admission for a test call. */ + +bool HostTestHooks::p25CheckNetworkVoiceAdmission(p25::Control& control, uint32_t srcId, uint32_t dstId) +{ + return control.m_voice->checkNetTrafficCollision(srcId, dstId, true, p25::defines::DUID::LDU1); +} + /* Tests whether the P25 affiliation table contains a grant. */ bool HostTestHooks::p25IsGranted(const p25::Control& control, uint32_t dstId) @@ -492,6 +547,16 @@ bool HostTestHooks::p25ProcessNetworkRawTSBK(p25::Control& control, const uint8_ return control.m_control->processNetwork(encoded, sizeof(encoded), lc, lsd, duid); } +/* Emits a P25 deny response through the RF control-signaling path. */ + +void HostTestHooks::p25WriteDenyResponse(p25::Control& control, uint32_t srcId, uint32_t dstId) +{ + control.m_enableControl = true; + control.m_control->writeRF_TSDU_Deny(srcId, dstId, + p25::defines::ReasonCode::DENY_TGT_GROUP_NOT_VALID, + p25::defines::TSBKO::IOSP_GRP_VCH, true, true); +} + /* Tests whether one P25 private grant owns the specified RID pair. */ bool HostTestHooks::p25IsU2UGranted(const p25::Control& control, uint32_t srcId, uint32_t dstId) diff --git a/tests/host/HostTestHooks.h b/tests/host/HostTestHooks.h index 74983e40..5cb924bf 100644 --- a/tests/host/HostTestHooks.h +++ b/tests/host/HostTestHooks.h @@ -119,6 +119,16 @@ public: * @param group True for group call, false for private call. */ static void dmrStartNetVoiceCall(dmr::Slot& slot, uint32_t srcId, uint32_t dstId, bool group = true); + /** + * @brief Injects an FNE-generated DMR voice grant response. + * @param slot Slot receiving the network CSBK. + * @param srcId Calling subscriber ID. + * @param dstId Called subscriber or talkgroup ID. + * @param group True for a talkgroup grant; false for a private grant. + * @param emergency Emergency service flag carried by the grant. + */ + static void dmrProcessNetworkGrant(dmr::Slot& slot, uint32_t srcId, uint32_t dstId, + bool group, bool emergency = false); /** * @brief Injects a synthetic DMR RF voice call frame into a slot. * @param slot DMR slot instance. @@ -133,9 +143,14 @@ public: */ static void dmrDetachNetwork(); /** - * @brief Configures one slot as a co-located Tier III control channel for tests. + * @brief Configures one slot as a Tier III control channel for tests. + * @param control DMR controller to configure. + * @param channelNo Control-channel number. + * @param tsccSlot Control-channel slot number. + * @param dedicated True to configure a dedicated TSCC. */ - static void dmrEnableTier3(dmr::Control& control, uint32_t channelNo, uint8_t tsccSlot); + static void dmrEnableTier3(dmr::Control& control, uint32_t channelNo, uint8_t tsccSlot, + bool dedicated = false); /** * @brief Gets the active Tier III payload destination for a slot. */ @@ -266,6 +281,20 @@ public: * @param dedicated Flag indicating whether the control channel is dedicated. */ static void p25ConfigureRFGrantSteering(p25::Control& control, bool enabled, bool dedicated); + /** + * @brief Configures whether a dedicated P25 control channel carries voice. + * @param control P25 control instance. + * @param enabled Flag indicating voice on the control channel is enabled. + */ + static void p25SetVoiceOnControl(p25::Control& control, bool enabled); + /** + * @brief Runs P25 network-voice admission for a test call. + * @param control P25 control instance. + * @param srcId Source radio ID. + * @param dstId Destination ID. + * @returns bool True when the network voice frame must be discarded. + */ + static bool p25CheckNetworkVoiceAdmission(p25::Control& control, uint32_t srcId, uint32_t dstId); /** * @brief Tests whether the P25 affiliation table contains a grant. * @param control P25 control instance. @@ -316,6 +345,13 @@ public: * @returns bool True if the network TSBK was accepted; otherwise, false. */ static bool p25ProcessNetworkRawTSBK(p25::Control& control, const uint8_t* raw); + /** + * @brief Emits a P25 deny response through the RF control-signaling path. + * @param control P25 control instance. + * @param srcId Source radio ID. + * @param dstId Destination ID. + */ + static void p25WriteDenyResponse(p25::Control& control, uint32_t srcId, uint32_t dstId); /** * @brief Tests whether one P25 private grant owns the specified RID pair. * @param control P25 control instance. diff --git a/tests/network/Network_Tests.cpp b/tests/network/Network_Tests.cpp index 3531969a..98f6159c 100644 --- a/tests/network/Network_Tests.cpp +++ b/tests/network/Network_Tests.cpp @@ -13,6 +13,7 @@ #include "common/network/RTPFNEHeader.h" #include "common/network/RTPHeader.h" #include "common/network/udp/Socket.h" +#include "host/modem/Modem.h" #include diff --git a/tests/nxdn/HostControl_Tests.cpp b/tests/nxdn/HostControl_Tests.cpp index 59b11662..6308cfcb 100644 --- a/tests/nxdn/HostControl_Tests.cpp +++ b/tests/nxdn/HostControl_Tests.cpp @@ -1052,3 +1052,17 @@ TEST_CASE("NXDN rfTGHang zero fallback arms rfLossWatchdog and recovers RF state REQUIRE(HostTestHooks::nxdnRFState(*harness.m_control) == RS_RF_LISTENING); REQUIRE_FALSE(HostTestHooks::nxdnRFLossWatchdog(*harness.m_control).isRunning()); } + +TEST_CASE("NXDN grant authority follows configuration and FNE availability", + "[nxdn][host][control][grant-authority]") +{ + NXDNHostHarness delegated(false, true); + REQUIRE(delegated.m_control->isGrantAuthoritative()); + + delegated.network()->activateForTest(); + REQUIRE_FALSE(delegated.m_control->isGrantAuthoritative()); + + NXDNHostHarness local(true, true); + local.network()->activateForTest(); + REQUIRE(local.m_control->isGrantAuthoritative()); +} diff --git a/tests/p25/HostControl_Tests.cpp b/tests/p25/HostControl_Tests.cpp index 9c765155..93d8cbfb 100644 --- a/tests/p25/HostControl_Tests.cpp +++ b/tests/p25/HostControl_Tests.cpp @@ -339,7 +339,10 @@ public: */ P25TestNetwork(uint16_t localPort = 0U, uint32_t peerId = 1U, bool disallowUnitToUnit = false) : network::Network("127.0.0.1", 1U, localPort, peerId, "test", true, true, false, true, false, false, true, true, false, false, false, false, disallowUnitToUnit), - m_resetP25Count(0U) + m_resetP25Count(0U), + m_lastTSDUControlByte(0U), + m_lastTSDULCO(0U), + m_tsduWriteCount(0U) { // keep protocol gates deterministic for this P25-focused harness m_dmrEnabled = false; @@ -518,8 +521,36 @@ public: return m_rxP25StreamId; } + /** + * @brief Records a P25 TSDU write without using the UDP transport. + * @param control Link control for the TSDU. + * @param data Encoded TSDU data. + * @param controlByte Network direction flags. + * @returns bool Always true. + */ + bool writeP25TSDU(const p25::lc::LC& control, const uint8_t* data, const uint8_t controlByte = 0U) override + { + REQUIRE(data != nullptr); + m_lastTSDUControlByte = controlByte; + m_lastTSDULCO = control.getLCO(); + ++m_tsduWriteCount; + return true; + } + + /** @brief Gets the direction flags from the last P25 TSDU write. */ + uint8_t lastTSDUControlByte() const { return m_lastTSDUControlByte; } + + /** @brief Gets the LCO from the last P25 TSDU write. */ + uint8_t lastTSDULCO() const { return m_lastTSDULCO; } + + /** @brief Gets the number of P25 TSDU writes. */ + uint32_t tsduWriteCount() const { return m_tsduWriteCount; } + private: uint32_t m_resetP25Count; + uint8_t m_lastTSDUControlByte; + uint8_t m_lastTSDULCO; + uint32_t m_tsduWriteCount; }; // --------------------------------------------------------------------------- @@ -600,6 +631,31 @@ public: p25::Control* m_control; }; +TEST_CASE("P25 deny responses retain outbound direction when mirrored to the network", "[p25][host][control][deny][direction]") +{ + P25HostHarness harness(true, true); + + HostTestHooks::p25WriteDenyResponse(*harness.m_control, 1001U, 2001U); + + REQUIRE(harness.network()->tsduWriteCount() == 1U); + REQUIRE(harness.network()->lastTSDULCO() == p25::defines::TSBKO::OSP_DENY_RSP); + REQUIRE((harness.network()->lastTSDUControlByte() & network::NET_CTRL_ISP) == 0U); +} + +TEST_CASE("P25 dedicated control channels silently discard network voice unless voice on control is enabled", + "[p25][host][control][dedicated][network-voice]") +{ + P25HostHarness harness(false, true); + harness.network()->activateForTest(); + HostTestHooks::p25ConfigureRFGrantSteering(*harness.m_control, true, true); + + REQUIRE(HostTestHooks::p25CheckNetworkVoiceAdmission(*harness.m_control, 1001U, 2001U)); + + HostTestHooks::p25SetVoiceOnControl(*harness.m_control, true); + harness.m_control->permittedTG(2001U); + REQUIRE_FALSE(HostTestHooks::p25CheckNetworkVoiceAdmission(*harness.m_control, 1001U, 2001U)); +} + TEST_CASE("P25 blocks inbound and outbound unit-to-unit network voice", "[p25][host][network][u2u][u2u-block]") { P25HostHarness harness(true, true, 0U, 1U, true); @@ -1844,3 +1900,17 @@ TEST_CASE("P25 rfTGHang zero fallback arms rfLossWatchdog and recovers RF state" REQUIRE(HostTestHooks::p25RFState(*harness.m_control) == RS_RF_LISTENING); REQUIRE_FALSE(HostTestHooks::p25RFLossWatchdog(*harness.m_control).isRunning()); } + +TEST_CASE("P25 grant authority follows configuration and FNE availability", + "[p25][host][control][grant-authority]") +{ + P25HostHarness delegated(false, true); + REQUIRE(delegated.m_control->isGrantAuthoritative()); + + delegated.network()->activateForTest(); + REQUIRE_FALSE(delegated.m_control->isGrantAuthoritative()); + + P25HostHarness local(true, true); + local.network()->activateForTest(); + REQUIRE(local.m_control->isGrantAuthoritative()); +}