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;

pull/134/head
Bryan Biedenkapp 3 days ago
parent 93e38ee2c7
commit b8db35ef30

@ -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.

@ -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 |

@ -128,6 +128,11 @@ std::unique_ptr<CSBK> 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);

@ -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;
}

@ -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;
}

@ -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)

@ -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.

@ -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)

@ -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.

@ -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

@ -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)

@ -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.

@ -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<fne_lookups::AffiliationLookup> 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<fne_lookups::AffiliationLookup> 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;
}
}
}

@ -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.

@ -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<lc::rcch::MESSAGE_TYPE_VCALL_ASSGN> rcch = std::make_unique<lc::rcch::MESSAGE_TYPE_VCALL_ASSGN>();
// check the affiliations for this peer to see if we can grant traffic
std::shared_ptr<fne_lookups::AffiliationLookup> 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<fne_lookups::AffiliationLookup> 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;
}
}
}

@ -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.

@ -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<fne_lookups::AffiliationLookup> aff = m_network->getPeerAffiliations(peerId);
if (aff == nullptr) {

@ -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.

@ -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);
}

@ -154,9 +154,6 @@ bool Host::readParams()
*/
yaml::Node rfssConfig = systemConf["config"];
m_authoritative = rfssConfig["authoritative"].as<bool>(true);
if (m_authoritative) {
m_supervisor = rfssConfig["supervisor"].as<bool>(false);
}
m_channelLookup = new ChannelLookup();
m_channelId = (uint8_t)rfssConfig["channelId"].as<uint32_t>(0U);
@ -291,9 +288,8 @@ bool Host::readParams()
uint16_t rpcApiPort = (uint16_t)channel["rpcPort"].as<uint32_t>(RPC_DEFAULT_PORT);
std::string rpcApiPassword = channel["rpcPassword"].as<std::string>();
// 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!");
}

@ -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<dmr::Control>(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<p25::Control>(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<nxdn::Control>(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) {

@ -187,7 +187,6 @@ private:
uint32_t m_nxdnQueueSizeBytes;
bool m_authoritative;
bool m_supervisor;
uint32_t m_dmr1RejCnt;
uint32_t m_dmr2RejCnt;

@ -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<bool>(false);
uint8_t nRandWait = (uint8_t)dmrProtocol["nRandWait"].as<uint32_t>(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)

@ -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.

@ -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<uint32_t>(dstId);
req["slot"].set<uint8_t>(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<uint32_t>(dstId);
req["slot"].set<uint8_t>(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);
}
}
});

@ -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;

@ -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();

@ -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<bool>(false);
m_control->m_verifyReg = nxdnProtocol["verifyReg"].as<bool>(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<uint32_t>(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<uint32_t>(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)

@ -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.

@ -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);
}
}

@ -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())) {

@ -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<bool>(false);
}
m_supervisor = supervisor;
m_tduPreambleCount = p25Protocol["tduPreambleCount"].as<uint32_t>(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<uint32_t>(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<uint32_t>(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)

@ -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.

@ -255,12 +255,10 @@ bool ControlSignaling::process(uint8_t* data, uint32_t len, std::unique_ptr<lc::
(tsbk->getEncrypted() ? 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_ptr<lc::
// restart target availability checking
m_u2uAvailabilityTransactions.erase(u2uTransactionKey(srcId, dstId));
m_u2uAvailabilityTransactions.erase(u2uTransactionKey(dstId, srcId));
if (m_p25->m_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_ptr<lc::
// U2U request immediately; retain correlation state so a later PROCEED can
// reissue a grant that expired while the called unit was ringing
startU2UAvailabilityTransaction(srcId, dstId, serviceOptions, true, false);
if (m_p25->m_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_ptr<lc::
// handle the response for the unit-to-unit availability request
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)
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);
}
}

@ -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.

@ -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()) {

@ -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.

@ -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.

@ -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<int>()) {
errorPayload(reply, "state was not a valid integer");
return;
}
DVM_STATE state = (DVM_STATE)req["state"].get<int>();
// validate destination ID is a integer within the JSON blob
if (!req["enable"].is<bool>()) {
errorPayload(reply, "enable was not a boolean");
return;
}
bool enable = req["enable"].get<bool>();
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)

@ -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.

@ -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"

@ -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));
}
}

@ -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<uint32_t>{ TARGET_PEER });
std::sort(writes.begin(), writes.end());
CHECK(writes == std::vector<uint32_t>{ 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<FNETestHooks::ParrotProtocol> protocols = {
FNETestHooks::ParrotProtocol::DMR,
FNETestHooks::ParrotProtocol::P25,
FNETestHooks::ParrotProtocol::NXDN
};
for (FNETestHooks::ParrotProtocol protocol : protocols) {
CAPTURE(static_cast<int>(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<uint32_t> 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<uint32_t>{ SOURCE_PEER, TARGET_PEER });
writes.clear();
REQUIRE(request(TARGET_PEER, DESTINATION_ID, SOURCE_ID));
std::sort(writes.begin(), writes.end());
CHECK(writes == std::vector<uint32_t>{ 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<uint32_t>{ ALLOWED_PEER_A, ALLOWED_PEER_B });
CHECK(writes == std::vector<uint32_t>{ 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<uint32_t>{ TARGET_SITE });
std::sort(writes.begin(), writes.end());
CHECK(writes == std::vector<uint32_t>{ 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<uint32_t>{ SITE_A, SITE_B });
CHECK(writes == std::vector<uint32_t>{ 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<uint32_t>{ TARGET_SITE });
std::sort(writes.begin(), writes.end());
CHECK(writes == std::vector<uint32_t>{ 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<uint32_t>{ SITE_A, SITE_B });
CHECK(writes == std::vector<uint32_t>{ SOURCE_PEER, SITE_A, SITE_B });
}
TEST_CASE("FNE call handlers validate source radios and group state", "[fne][callhandler][validation]")

@ -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)

@ -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.

@ -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 <cstring>
#include <memory>
#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<dmr::lc::CSBK> grant;
if (group)
grant = std::make_unique<dmr::lc::csbk::CSBK_TV_GRANT>();
else
grant = std::make_unique<dmr::lc::csbk::CSBK_PV_GRANT>();
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)

@ -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.

@ -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 <catch2/catch_test_macros.hpp>

@ -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());
}

@ -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());
}

Loading…
Cancel
Save

Powered by TurnKey Linux.