From b8db35ef307e0660f89f04a7511e51d064ffce44 Mon Sep 17 00:00:00 2001 From: Bryan Biedenkapp Date: Tue, 10 Nov 2026 15:21:35 -0500 Subject: [PATCH] implement FNE authoritative channel grant transactions for P25, DMR and NXDN, add U2U grant handling, local channel assignment, traffic channel permit and grant release cleanup, correct dedicated control channel traffic handling and P25 deny response decoding, remove supervisor configuration in favor of topology-driven traffic permits, document authoritative operating modes and add grant lifecycle regression tests; --- configs/config.example.yml | 28 +++- ...N.1101 - DVMHost REST API Documentation.md | 113 +-------------- src/common/dmr/lc/csbk/CSBKFactory.cpp | 5 + src/common/dmr/lc/csbk/CSBK_PV_GRANT.cpp | 17 ++- src/common/dmr/lc/csbk/CSBK_TV_GRANT.cpp | 17 ++- src/common/network/BaseNetwork.cpp | 30 +++- src/common/network/BaseNetwork.h | 22 +++ src/common/network/Network.cpp | 15 ++ src/common/network/Network.h | 19 +++ src/common/network/RTPFNEHeader.h | 1 + src/fne/network/TrafficNetwork.cpp | 33 +++++ src/fne/network/TrafficNetwork.h | 23 +++ src/fne/network/callhandler/TagDMRData.cpp | 69 +++++---- src/fne/network/callhandler/TagDMRData.h | 4 +- src/fne/network/callhandler/TagNXDNData.cpp | 69 +++++---- src/fne/network/callhandler/TagNXDNData.h | 4 +- src/fne/network/callhandler/TagP25Data.cpp | 43 +++--- src/fne/network/callhandler/TagP25Data.h | 4 +- src/fne/network/packet/GrantRequest.cpp | 20 +++ src/host/Host.Config.cpp | 12 +- src/host/Host.cpp | 7 +- src/host/Host.h | 1 - src/host/dmr/Control.cpp | 31 +--- src/host/dmr/Control.h | 12 +- src/host/dmr/Slot.cpp | 65 +++++---- src/host/dmr/Slot.h | 12 +- src/host/dmr/packet/ControlSignaling.cpp | 38 +++-- src/host/nxdn/Control.cpp | 47 +++--- src/host/nxdn/Control.h | 17 +-- src/host/nxdn/packet/ControlSignaling.cpp | 15 +- src/host/nxdn/packet/Voice.cpp | 5 + src/host/p25/Control.cpp | 46 +++--- src/host/p25/Control.h | 17 +-- src/host/p25/packet/ControlSignaling.cpp | 55 ++++--- src/host/p25/packet/ControlSignaling.h | 4 +- src/host/p25/packet/Voice.cpp | 5 + src/host/p25/phase2/Control.h | 2 +- src/host/p25/phase2/Slot.h | 2 +- src/host/restapi/RESTAPI.cpp | 74 ---------- src/host/restapi/RESTAPI.h | 7 - src/host/restapi/RESTDefines.h | 1 - tests/dmr/HostControl_Tests.cpp | 63 ++++++++ tests/fne/FNENetwork_Tests.cpp | 135 +++++++++++++++++- tests/fne/FNETestHooks.cpp | 8 ++ tests/fne/FNETestHooks.h | 12 ++ tests/host/HostTestHooks.cpp | 69 ++++++++- tests/host/HostTestHooks.h | 40 +++++- tests/network/Network_Tests.cpp | 1 + tests/nxdn/HostControl_Tests.cpp | 14 ++ tests/p25/HostControl_Tests.cpp | 72 +++++++++- 50 files changed, 944 insertions(+), 481 deletions(-) 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()); +}