SNDCPv1 Trunked Packet Data (#134)

* add SNDCP data page TSBKs, correct PDU disassembly with a header offset and clear autonomous access in the SNDCP channel announcement;

- add ISP_SNDCP_DATA_PAGE_RES ($13) and OSP_SNDCP_DATA_PAGE_REQ ($15)
- Assembler::disassemble no longer shifts the coded block stream by the
  data header offset; the offset counts octets of reassembled user data
- OSP_SNDCP_CH_ANN no longer sets autonomous access; the announced
  channel is the control channel, which receives no SN-Data

* echo data service options in SNDCP grants, handle SNDCP data pages and relay SNDCP grants to the FNE;

- the SNDCP data channel grant echoes the Data Service Options of the
  request; an SU ignores a grant whose options differ
- transmit an SNDCP data page request received from the FNE and answer
  a "proceed" page response with a data channel grant
- pass the first copy of each SNDCP grant to the network, so the FNE
  knows the SU has been directed to a data channel
- read the SNDCP reconnect request as an ISP_SNDCP_REC_REQ, the class
  the factory builds for it

* implement trunked SNDCP packet data in the FNE: activation ACK, SN-Data uplink and downlink, data paging and downlink retransmission;

- send the link-layer ACK for the SNDCP context activation request
- strip the SNDCP header from SN-Data received on the user data SAP and
  handle the IPv4 datagram as packet data
- frame downlink datagrams as SN-Data for an SU with an SNDCP context
- raise the standby timer in the context accept from 1 minute to 4 hours
- page an SU that has left the data channel through the control channel
  that last granted it one, and hold the PDU until the grant is reported
- retransmit a confirmed downlink the SU does not acknowledge, up to
  twice, with the same N(S)

---------

Co-authored-by: David Andrzejewski <181952+dandrzejewski@users.noreply.github.com>
pull/135/head
David Andrzejewski 2 days ago committed by GitHub
parent 080d4d5714
commit 50cf3b5e7e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -833,6 +833,7 @@ namespace p25
// TSBK Inbound Signalling Packet (ISP) Opcode(s)
ISP_TELE_INT_PSTN_REQ = 0x09U, //!< TELE INT PSTN REQ - Telephone Interconnect Request - Implicit
ISP_SNDCP_CH_REQ = 0x12U, //!< SNDCP CH REQ - SNDCP Data Channel Request
ISP_SNDCP_DATA_PAGE_RES = 0x13U, //!< SNDCP DATA PAGE RES - SNDCP Data Page Response
ISP_SNDCP_REC_REQ = 0x14U, //!< SNDCP REC REQ - SNDCP Reconnect Request
ISP_STS_Q_RSP = 0x19U, //!< STS Q RSP - Status Query Response
ISP_STS_Q_REQ = 0x1CU, //!< STS Q REQ - Status Query Request
@ -850,6 +851,7 @@ namespace p25
OSP_GRP_VCH_GRANT_UPD = 0x02U, //!< GRP VCH GRANT UPD - Group Voice Channel Grant Update
OSP_UU_VCH_GRANT_UPD = 0x06U, //!< UU VCH GRANT UPD - Unit-to-Unit Voice Channel Grant Update
OSP_SNDCP_CH_GNT = 0x14U, //!< SNDCP CH GNT - SNDCP Data Channel Grant
OSP_SNDCP_DATA_PAGE_REQ = 0x15U, //!< SNDCP DATA PAGE REQ - SNDCP Data Page Request
OSP_SNDCP_CH_ANN = 0x16U, //!< SNDCP CH ANN - SNDCP Data Channel Announcement
OSP_STS_Q = 0x1AU, //!< STS Q - Status Query
OSP_QUE_RSP = 0x21U, //!< QUE RSP - Queued Response

@ -162,10 +162,7 @@ bool Assembler::disassemble(const uint8_t* pduBlock, uint32_t blockLength, bool
m_dataBlockCnt = 0U;
// process all blocks in the data stream
// if the primary header has a header offset ensure data if offset by that amount
if (dataHeader.getHeaderOffset() > 0U) {
offset += dataHeader.getHeaderOffset();
}
// the header offset counts octets of reassembled user data, it does not shift the coded blocks
uint32_t packetLength = dataHeader.getPacketLength();
uint32_t padLength = dataHeader.getPadLength();

@ -0,0 +1,86 @@
// SPDX-License-Identifier: GPL-2.0-only
/*
* Digital Voice Modem - Common Library
* GPLv2 Open Source. Use is subject to license terms.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* Copyright (C) 2026 David Andrzejewski
*
*/
#include "Defines.h"
#include "p25/lc/tsbk/ISP_SNDCP_DATA_PAGE_RES.h"
using namespace p25;
using namespace p25::defines;
using namespace p25::lc;
using namespace p25::lc::tsbk;
#include <cassert>
// ---------------------------------------------------------------------------
// Public Class Members
// ---------------------------------------------------------------------------
/* Initializes a new instance of the ISP_SNDCP_DATA_PAGE_RES class. */
ISP_SNDCP_DATA_PAGE_RES::ISP_SNDCP_DATA_PAGE_RES() : TSBK(),
m_dataServiceOptions(0U),
m_answerResponse(0U),
m_dataAccessControl(0U)
{
m_lco = TSBKO::ISP_SNDCP_DATA_PAGE_RES;
}
/* Decode a trunking signalling block. */
bool ISP_SNDCP_DATA_PAGE_RES::decode(const uint8_t* data, bool rawTSBK)
{
assert(data != nullptr);
uint8_t tsbk[P25_TSBK_LENGTH_BYTES + 1U];
::memset(tsbk, 0x00U, P25_TSBK_LENGTH_BYTES);
bool ret = TSBK::decode(data, tsbk, rawTSBK);
if (!ret)
return false;
ulong64_t tsbkValue = TSBK::toValue(tsbk);
m_dataServiceOptions = (uint8_t)((tsbkValue >> 56) & 0xFFU); // Data Service Options
m_answerResponse = (uint8_t)((tsbkValue >> 48) & 0xFFU); // Answer Response
m_dataAccessControl = (uint16_t)((tsbkValue >> 32) & 0xFFFFU); // Data Access Control
m_srcId = (uint32_t)(tsbkValue & 0xFFFFFFU); // Source Radio Address
return true;
}
/* Encode a trunking signalling block. */
void ISP_SNDCP_DATA_PAGE_RES::encode(uint8_t* data, bool rawTSBK, bool noTrellis)
{
assert(data != nullptr);
/* stub -- inbound only; an SU sends this, the FNE and hosts never encode it */
}
/* Returns a string that represents the current TSBK. */
std::string ISP_SNDCP_DATA_PAGE_RES::toString(bool isp)
{
return std::string("TSBKO, ISP_SNDCP_DATA_PAGE_RES (SNDCP Data Page Response)");
}
// ---------------------------------------------------------------------------
// Private Class Members
// ---------------------------------------------------------------------------
/* Internal helper to copy the the class. */
void ISP_SNDCP_DATA_PAGE_RES::copy(const ISP_SNDCP_DATA_PAGE_RES& data)
{
TSBK::copy(data);
m_dataServiceOptions = data.m_dataServiceOptions;
m_answerResponse = data.m_answerResponse;
m_dataAccessControl = data.m_dataAccessControl;
}

@ -0,0 +1,84 @@
// SPDX-License-Identifier: GPL-2.0-only
/*
* Digital Voice Modem - Common Library
* GPLv2 Open Source. Use is subject to license terms.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* Copyright (C) 2026 David Andrzejewski
*
*/
/**
* @file ISP_SNDCP_DATA_PAGE_RES.h
* @ingroup p25_tsbk
* @file ISP_SNDCP_DATA_PAGE_RES.cpp
* @ingroup p25_tsbk
*/
#if !defined(__P25_LC_TSBK__ISP_SNDCP_DATA_PAGE_RES_H__)
#define __P25_LC_TSBK__ISP_SNDCP_DATA_PAGE_RES_H__
#include "common/Defines.h"
#include "common/p25/lc/TSBK.h"
namespace p25
{
namespace lc
{
namespace tsbk
{
// ---------------------------------------------------------------------------
// Class Declaration
// ---------------------------------------------------------------------------
/**
* @brief Implements SNDCP DATA PAGE RES - SNDCP Data Page Response (ISP)
* @ingroup p25_tsbk
*/
class DVM_COMMON_API ISP_SNDCP_DATA_PAGE_RES : public TSBK {
public:
/**
* @brief Initializes a new instance of the ISP_SNDCP_DATA_PAGE_RES class.
*/
ISP_SNDCP_DATA_PAGE_RES();
/**
* @brief Decode a trunking signalling block.
* @param[in] data Buffer containing a TSBK to decode.
* @param rawTSBK Flag indicating whether or not the passed buffer is raw.
* @returns bool True, if TSBK decoded, otherwise false.
*/
bool decode(const uint8_t* data, bool rawTSBK = false) override;
/**
* @brief Encode a trunking signalling block.
* @param[out] data Buffer to encode a TSBK.
* @param rawTSBK Flag indicating whether or not the output buffer is raw.
* @param noTrellis Flag indicating whether or not the encoded data should be Trellis encoded.
*/
void encode(uint8_t* data, bool rawTSBK = false, bool noTrellis = false) override;
/**
* @brief Returns a string that represents the current TSBK.
* @returns std::string String representation of the TSBK.
*/
std::string toString(bool isp = false) override;
public:
/**
* @brief SNDCP Data Service Options (the low nibble is the NSAPI)
*/
DECLARE_PROPERTY(uint8_t, dataServiceOptions, DataServiceOptions);
/**
* @brief Answer Response (proceed, deny or wait)
*/
DECLARE_PROPERTY(uint8_t, answerResponse, AnswerResponse);
/**
* @brief SNDCP Data Access Control
*/
DECLARE_PROPERTY(uint16_t, dataAccessControl, DataAccessControl);
DECLARE_COPY(ISP_SNDCP_DATA_PAGE_RES);
};
} // namespace tsbk
} // namespace lc
} // namespace p25
#endif // __P25_LC_TSBK__ISP_SNDCP_DATA_PAGE_RES_H__

@ -25,7 +25,7 @@ using namespace p25::lc::tsbk;
OSP_SNDCP_CH_ANN::OSP_SNDCP_CH_ANN() : TSBK(),
m_implicitChannel(false),
m_sndcpAutoAccess(true),
m_sndcpAutoAccess(false), // the announced channel is the control channel, which receives no SN-Data
m_sndcpRequestedAccess(true),
m_sndcpDAC(1U)
{

@ -0,0 +1,91 @@
// SPDX-License-Identifier: GPL-2.0-only
/*
* Digital Voice Modem - Common Library
* GPLv2 Open Source. Use is subject to license terms.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* Copyright (C) 2026 David Andrzejewski
*
*/
#include "Defines.h"
#include "p25/lc/tsbk/OSP_SNDCP_DATA_PAGE_REQ.h"
using namespace p25;
using namespace p25::defines;
using namespace p25::lc;
using namespace p25::lc::tsbk;
#include <cassert>
// ---------------------------------------------------------------------------
// Public Class Members
// ---------------------------------------------------------------------------
/* Initializes a new instance of the OSP_SNDCP_DATA_PAGE_REQ class. */
OSP_SNDCP_DATA_PAGE_REQ::OSP_SNDCP_DATA_PAGE_REQ() : TSBK(),
m_dataServiceOptions(0U),
m_dataAccessControl(1U)
{
m_lco = TSBKO::OSP_SNDCP_DATA_PAGE_REQ;
}
/* Decode a trunking signalling block. */
bool OSP_SNDCP_DATA_PAGE_REQ::decode(const uint8_t* data, bool rawTSBK)
{
assert(data != nullptr);
uint8_t tsbk[P25_TSBK_LENGTH_BYTES + 1U];
::memset(tsbk, 0x00U, P25_TSBK_LENGTH_BYTES);
bool ret = TSBK::decode(data, tsbk, rawTSBK);
if (!ret)
return false;
ulong64_t tsbkValue = TSBK::toValue(tsbk);
m_dataServiceOptions = (uint8_t)((tsbkValue >> 56) & 0xFFU); // Data Service Options
m_dataAccessControl = (uint16_t)((tsbkValue >> 24) & 0xFFFFU); // Data Access Control
m_dstId = (uint32_t)(tsbkValue & 0xFFFFFFU); // Target Radio Address
return true;
}
/* Encode a trunking signalling block. */
void OSP_SNDCP_DATA_PAGE_REQ::encode(uint8_t* data, bool rawTSBK, bool noTrellis)
{
assert(data != nullptr);
ulong64_t tsbkValue = 0U;
tsbkValue = m_dataServiceOptions; // Data Service Options
tsbkValue = (tsbkValue << 16); // Reserved
tsbkValue = (tsbkValue << 16) + m_dataAccessControl; // Data Access Control
tsbkValue = (tsbkValue << 24) + m_dstId; // Target Radio Address
std::unique_ptr<uint8_t[]> tsbk = TSBK::fromValue(tsbkValue);
TSBK::encode(data, tsbk.get(), rawTSBK, noTrellis);
}
/* Returns a string that represents the current TSBK. */
std::string OSP_SNDCP_DATA_PAGE_REQ::toString(bool isp)
{
return std::string("TSBKO, OSP_SNDCP_DATA_PAGE_REQ (SNDCP Data Page Request)");
}
// ---------------------------------------------------------------------------
// Private Class Members
// ---------------------------------------------------------------------------
/* Internal helper to copy the the class. */
void OSP_SNDCP_DATA_PAGE_REQ::copy(const OSP_SNDCP_DATA_PAGE_REQ& data)
{
TSBK::copy(data);
m_dataServiceOptions = data.m_dataServiceOptions;
m_dataAccessControl = data.m_dataAccessControl;
}

@ -0,0 +1,80 @@
// SPDX-License-Identifier: GPL-2.0-only
/*
* Digital Voice Modem - Common Library
* GPLv2 Open Source. Use is subject to license terms.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* Copyright (C) 2026 David Andrzejewski
*
*/
/**
* @file OSP_SNDCP_DATA_PAGE_REQ.h
* @ingroup p25_tsbk
* @file OSP_SNDCP_DATA_PAGE_REQ.cpp
* @ingroup p25_tsbk
*/
#if !defined(__P25_LC_TSBK__OSP_SNDCP_DATA_PAGE_REQ_H__)
#define __P25_LC_TSBK__OSP_SNDCP_DATA_PAGE_REQ_H__
#include "common/Defines.h"
#include "common/p25/lc/TSBK.h"
namespace p25
{
namespace lc
{
namespace tsbk
{
// ---------------------------------------------------------------------------
// Class Declaration
// ---------------------------------------------------------------------------
/**
* @brief Implements SNDCP DATA PAGE REQ - SNDCP Data Page Request (OSP)
* @ingroup p25_tsbk
*/
class DVM_COMMON_API OSP_SNDCP_DATA_PAGE_REQ : public TSBK {
public:
/**
* @brief Initializes a new instance of the OSP_SNDCP_DATA_PAGE_REQ class.
*/
OSP_SNDCP_DATA_PAGE_REQ();
/**
* @brief Decode a trunking signalling block.
* @param[in] data Buffer containing a TSBK to decode.
* @param rawTSBK Flag indicating whether or not the passed buffer is raw.
* @returns bool True, if TSBK decoded, otherwise false.
*/
bool decode(const uint8_t* data, bool rawTSBK = false) override;
/**
* @brief Encode a trunking signalling block.
* @param[out] data Buffer to encode a TSBK.
* @param rawTSBK Flag indicating whether or not the output buffer is raw.
* @param noTrellis Flag indicating whether or not the encoded data should be Trellis encoded.
*/
void encode(uint8_t* data, bool rawTSBK = false, bool noTrellis = false) override;
/**
* @brief Returns a string that represents the current TSBK.
* @returns std::string String representation of the TSBK.
*/
std::string toString(bool isp = false) override;
public:
/**
* @brief SNDCP Data Service Options (the low nibble is the NSAPI)
*/
DECLARE_PROPERTY(uint8_t, dataServiceOptions, DataServiceOptions);
/**
* @brief SNDCP Data Access Control
*/
DECLARE_PROPERTY(uint16_t, dataAccessControl, DataAccessControl);
DECLARE_COPY(OSP_SNDCP_DATA_PAGE_REQ);
};
} // namespace tsbk
} // namespace lc
} // namespace p25
#endif // __P25_LC_TSBK__OSP_SNDCP_DATA_PAGE_REQ_H__

@ -192,8 +192,12 @@ std::unique_ptr<TSBK> TSBKFactory::createTSBK(const uint8_t* data, bool rawTSBK)
return decode(new IOSP_TELE_INT_ANS(), data, rawTSBK);
case TSBKO::ISP_SNDCP_CH_REQ:
return decode(new ISP_SNDCP_CH_REQ(), data, rawTSBK);
case TSBKO::ISP_SNDCP_DATA_PAGE_RES:
return decode(new ISP_SNDCP_DATA_PAGE_RES(), data, rawTSBK);
case TSBKO::ISP_SNDCP_REC_REQ:
return decode(new ISP_SNDCP_REC_REQ(), data, rawTSBK);
case TSBKO::OSP_SNDCP_DATA_PAGE_REQ:
return decode(new OSP_SNDCP_DATA_PAGE_REQ(), data, rawTSBK);
case TSBKO::ISP_STS_Q_RSP:
return decode(new ISP_STS_Q_RSP(), data, rawTSBK);
case TSBKO::OSP_STS_Q:

@ -50,6 +50,7 @@
#include "common/p25/lc/tsbk/ISP_GRP_AFF_Q_RSP.h"
#include "common/p25/lc/tsbk/ISP_LOC_REG_REQ.h"
#include "common/p25/lc/tsbk/ISP_SNDCP_CH_REQ.h"
#include "common/p25/lc/tsbk/ISP_SNDCP_DATA_PAGE_RES.h"
#include "common/p25/lc/tsbk/ISP_SNDCP_REC_REQ.h"
#include "common/p25/lc/tsbk/ISP_STS_Q_RSP.h"
#include "common/p25/lc/tsbk/ISP_TELE_INT_PSTN_REQ.h"
@ -76,6 +77,7 @@
#include "common/p25/lc/tsbk/OSP_SCCB.h"
#include "common/p25/lc/tsbk/OSP_SNDCP_CH_ANN.h"
#include "common/p25/lc/tsbk/OSP_SNDCP_CH_GNT.h"
#include "common/p25/lc/tsbk/OSP_SNDCP_DATA_PAGE_REQ.h"
#include "common/p25/lc/tsbk/OSP_STS_Q.h"
#include "common/p25/lc/tsbk/OSP_SYNC_BCAST.h"
#include "common/p25/lc/tsbk/OSP_SYS_SRV_BCAST.h"

@ -115,6 +115,14 @@ bool TagP25Data::processFrame(const uint8_t* data, uint32_t len, uint32_t peerId
return m_packetData->processFrame(data, len, peerId, pktSeq, streamId, fromUpstream);
}
// a control channel relays the SNDCP data channel grants it issues, so packet data
// knows the SU is on a data channel; these are never repeated to other peers
if (duid == DUID::TSDU && lco == TSBKO::OSP_SNDCP_CH_GNT && MFId == MFG_STANDARD) {
if (!fromUpstream && !m_network->m_disablePacketData)
m_packetData->processSNDCPChannelGrant(peerId, srcId);
return true;
}
// perform TGID route rewrites if configured
routeRewrite(buffer, peerId, duid, dstId, false);
dstId = GET_UINT24(buffer, 8U);

@ -12,6 +12,7 @@
#include "common/p25/data/ConventionalRegistration.h"
#include "common/p25/data/IPv4Packet.h"
#include "common/p25/sndcp/SNDCPFactory.h"
#include "common/p25/lc/tsbk/TSBKFactory.h"
#include "common/p25/Sync.h"
#include "common/edac/CRC.h"
#include "common/Clock.h"
@ -55,6 +56,19 @@ const uint32_t ARP_RETRY_MS = 5000U; // milliseconds
const uint32_t SUBSCRIBER_READY_RETRY_MS = 1000U; // milliseconds
const uint32_t CONVENTIONAL_LOCATION_MAX_AGE_MS = 300000U; // 5 minutes
// trunked (SNDCP) downlink paging; kept shorter than the 10 second Ready Timer in the
// context accept, after which the SU returns to the control channel
const uint32_t SNDCP_ON_CHANNEL_MS = 8000U; // milliseconds an SU is taken to stay on the data channel
const uint32_t SNDCP_PDCH_SETTLE_MS = 1200U; // milliseconds from a grant to the first outbound PDU
const uint32_t SNDCP_PAGE_RETRY_MS = 4000U; // milliseconds between data pages (an SU answers in about 3 seconds)
const uint32_t SNDCP_PAGE_HOLD_MS = 200U; // milliseconds between checks while a page is outstanding
const uint8_t SNDCP_PAGE_MAX = 4U; // data pages sent before the packet is dropped
// trunked (SNDCP) confirmed downlink retransmission
const uint32_t SNDCP_LLC_ACK_TIMEOUT_MS = 4500U; // milliseconds to wait for the link-layer ACK (seen up to 3.1 seconds)
const uint8_t SNDCP_LLC_RETX_MAX = 2U; // retransmissions before the PDU is given up on
const uint32_t SNDCP_LLC_RESEND_STUCK_MS = 30000U; // milliseconds a queued retransmission may wait to leave
// ---------------------------------------------------------------------------
// Public Class Members
// ---------------------------------------------------------------------------
@ -371,25 +385,46 @@ void P25PacketData::processPacketFrame(const uint8_t* data, uint32_t len, bool a
llId, (llId == 0U) ? " (UNRESOLVED - will retry with ARP)" : "");
// assemble a P25 PDU frame header for transport...
// a subscriber with an active SNDCP context is sent SN-Data: user data SAP, confirmed,
// and a 2-octet SNDCP header (PDU type | NSAPI, PCOMP | DCOMP) ahead of the IPv4 datagram
bool sndcp = false;
uint8_t nsapi = 0U;
{
std::lock_guard<std::mutex> guard(m_sndcpContextLock);
auto ctx = m_sndcpContexts.find(llId);
if (ctx != m_sndcpContexts.end() && ctx->second.active) {
sndcp = true;
nsapi = ctx->second.nsapi;
}
}
const uint32_t sndcpHdrLen = sndcp ? 2U : 0U;
data::DataHeader pktHeader;
bool confirmed = route.delivery == DataDeliveryMode::CONFIRMED;
bool confirmed = sndcp || route.delivery == DataDeliveryMode::CONFIRMED;
pktHeader.setFormat(confirmed ? PDUFormatType::CONFIRMED : PDUFormatType::UNCONFIRMED);
pktHeader.setMFId(MFG_STANDARD);
pktHeader.setAckNeeded(confirmed);
pktHeader.setOutbound(true);
pktHeader.setSAP(PDUSAP::PACKET_DATA);
pktHeader.setSAP(sndcp ? PDUSAP::USER_DATA : PDUSAP::PACKET_DATA);
pktHeader.setHeaderOffset((uint8_t)sndcpHdrLen);
pktHeader.setLLId(llId);
pktHeader.setBlocksToFollow(1U);
pktHeader.calculateLength(pktLen);
pktHeader.calculateLength(pktLen + sndcpHdrLen);
uint32_t pduLength = pktHeader.getPDULength();
if (pduLength < pktLen) {
if (pduLength < pktLen + sndcpHdrLen) {
LogWarning(LOG_P25, "VTUN, data truncated!");
pktLen = pduLength; // don't overflow the buffer
pktLen = pduLength - sndcpHdrLen; // don't overflow the buffer
}
DECLARE_UINT8_ARRAY(pduUserData, pduLength);
::memcpy(pduUserData, data, pktLen);
if (sndcp) {
pduUserData[0U] = (uint8_t)((SNDCP_PDUType::RF_CONFIRMED << 4) | (nsapi & 0x0FU));
pduUserData[1U] = 0x00U; // no header or data compression
LogInfoEx(LOG_P25, "VTUN -> PDU SNDCP user data, llId = %u, nsapi = %u, length = %u", llId, nsapi, pktLen);
}
::memcpy(pduUserData + sndcpHdrLen, data, pktLen);
//#if DEBUG_P25_PDU_DATA
Utils::dump(1U, "P25, P25PacketData::processPacketFrame(), pduUserData", pduUserData, pduLength);
//#endif
@ -484,6 +519,9 @@ void P25PacketData::clock(uint32_t ms)
m_conventionalDataService.expire(now);
m_locationRegistry.expire(now);
// retransmit trunked downlinks the SU has not acknowledged (this may queue a packet)
processSNDCPRetransmit(now);
if (m_packetScheduler.empty()) {
return;
}
@ -493,8 +531,44 @@ void P25PacketData::clock(uint32_t ms)
return;
std::string targetIP = __IP_FROM_UINT(packet->targetIPAddress);
uint8_t protocol = packet->userData.size() >= 20U ? packet->userData[9U] : 0U;
if (packet->retryCount >= (packet->extendedRetry ? MAX_PKT_RETRY_CNT * 2U : MAX_PKT_RETRY_CNT)) {
const uint32_t ipOffset = packet->header.getHeaderOffset(); // SNDCP header, if any
uint8_t protocol = packet->userData.size() >= ipOffset + 20U ? packet->userData[ipOffset + 9U] : 0U;
// is this a queued retransmission, or a packet waiting behind a downlink still awaiting its ACK?
bool retx = false;
{
bool pending = false;
{
std::lock_guard<std::mutex> guard(m_sndcpPendingLock);
auto it = m_sndcpPending.find(packet->llId);
if (it != m_sndcpPending.end()) {
pending = true;
retx = packet->retransmit && it->second.resendDue &&
it->second.ns == packet->header.getNs() && it->second.userData == packet->userData;
}
}
if (packet->retransmit && !retx) {
// the ACK arrived (or the record was given up on) while the copy was queued
LogInfoEx(LOG_P25, P25_PDU_STR ", SNDCP downlink retransmit no longer needed, llId = %u", packet->llId);
m_packetScheduler.pop();
return;
}
if (pending && !retx) {
// not counted as a retry: the pending record bounds the wait
packet->dueAt = now + SUBSCRIBER_READY_RETRY_MS;
m_packetScheduler.rotate();
return;
}
}
// gives up on a queued retransmission that cannot be sent
auto abandonRetx = [&]() {
clearSNDCPPendingAck(packet->llId);
m_dataLinkManager.setReady(packet->llId, true);
};
if (!retx && packet->retryCount >= (packet->extendedRetry ? MAX_PKT_RETRY_CNT * 2U : MAX_PKT_RETRY_CNT)) {
LogWarning(LOG_P25, P25_PDU_STR ", max packet retry count exceeded, dropping packet, dstIp = %s",
targetIP.c_str());
m_dataLinkManager.setReady(packet->llId, true);
@ -502,12 +576,100 @@ void P25PacketData::clock(uint32_t ms)
return;
}
// trunked (SNDCP) downlink: an SU that has left the data channel is paged on the
// control channel, and the PDU is held until the site reports the grant
if (packet->llId != WUID_ALL && packet->header.getSAP() == PDUSAP::USER_DATA &&
packet->header.getHeaderOffset() == 2U) {
bool drop = false, noControlCh = false, page = false;
uint32_t ccPeerId = 0U, vcPeerId = 0U;
uint8_t nsapi = 0U, pageCount = 0U;
uint64_t holdUntil = 0U;
{
std::lock_guard<std::mutex> guard(m_sndcpContextLock);
auto it = m_sndcpContexts.find(packet->llId);
if (it != m_sndcpContexts.end()) {
SNDCPContext& ctx = it->second;
ccPeerId = ctx.ccPeerId;
vcPeerId = ctx.vcPeerId;
nsapi = ctx.nsapi;
if (now >= ctx.onChannelUntil) {
if (ctx.ccPeerId == 0U) {
// no control channel has reported a grant for this SU, so it cannot be paged
drop = true;
noControlCh = true;
} else if (now >= ctx.nextPageAt) {
if (ctx.pageCount >= SNDCP_PAGE_MAX) {
drop = true;
ctx.pageCount = 0U;
ctx.nextPageAt = 0U;
} else {
page = true;
ctx.pageCount++;
ctx.nextPageAt = now + SNDCP_PAGE_RETRY_MS;
}
}
pageCount = ctx.pageCount;
holdUntil = now + SNDCP_PAGE_HOLD_MS;
} else if (now < ctx.grantedAt + SNDCP_PDCH_SETTLE_MS) {
holdUntil = ctx.grantedAt + SNDCP_PDCH_SETTLE_MS; // let the SU arrive on the data channel
}
}
}
if (drop) {
if (noControlCh) {
LogWarning(LOG_P25, P25_PDU_STR ", SNDCP SU is off the data channel and no control channel is known to page it, dropping packet, dstIp = %s (%u)",
targetIP.c_str(), packet->llId);
} else {
LogWarning(LOG_P25, P25_PDU_STR ", SNDCP data page unanswered, dropping packet, dstIp = %s (%u)",
targetIP.c_str(), packet->llId);
}
if (retx)
abandonRetx();
m_packetScheduler.pop();
return;
}
if (page) {
std::unique_ptr<lc::tsbk::OSP_SNDCP_DATA_PAGE_REQ> osp = std::make_unique<lc::tsbk::OSP_SNDCP_DATA_PAGE_REQ>();
osp->setMFId(MFG_STANDARD);
osp->setSrcId(WUID_FNE);
osp->setDstId(packet->llId);
osp->setDataServiceOptions(nsapi & 0x0FU);
m_tag->write_TSDU(ccPeerId, osp.get());
LogInfoEx(LOG_P25, P25_PDU_STR ", SNDCP data page, llId = %u, nsapi = %u, peer = %u, attempt = %u",
packet->llId, nsapi, ccPeerId, pageCount);
}
if (holdUntil != 0U) {
packet->dueAt = holdUntil;
m_packetScheduler.rotate();
return;
}
// the SU may have been idle for longer than a learned location is kept; it
// is still reached through the data channel peer it was last heard on
if (vcPeerId != 0U) {
ConventionalLocation location;
location.peerId = vcPeerId;
location.lastSeen = now;
m_locationRegistry.updateConventional(packet->llId, location);
}
}
if (packet->llId != WUID_ALL) {
DataRoute route = m_locationRegistry.resolve(packet->llId, AccessMode::CONVENTIONAL, now);
if (!route.valid) {
LogWarning(LOG_P25, P25_PDU_STR ", subscriber location unknown, dstIp = %s (%u)",
targetIP.c_str(), packet->llId);
if (retx) {
abandonRetx();
m_packetScheduler.pop();
return;
}
UnknownLocationPolicy policy = m_locationRegistry.unknownLocationPolicy();
if (policy == UnknownLocationPolicy::DROP) {
m_packetScheduler.pop();
@ -523,7 +685,7 @@ void P25PacketData::clock(uint32_t ms)
}
}
if (m_dataLinkManager.hasState(packet->llId) &&
if (!retx && m_dataLinkManager.hasState(packet->llId) &&
!m_dataLinkManager.isReady(packet->llId)) {
LogWarning(LOG_P25, P25_PDU_STR ", subscriber not ready, dstIp = %s (%u), proto = %02X%s, will retry in %ums",
targetIP.c_str(), packet->llId, protocol, (protocol == 0x01U) ? " (ICMP)" : "",
@ -543,10 +705,48 @@ void P25PacketData::clock(uint32_t ms)
if (confirmed)
m_dataLinkManager.setReady(packet->llId, false);
if (dispatchUserFrameToFNE(packet->header, false, false, packet->userData.data())) {
// a retransmission keeps the N(S) its header was first sent with
if (dispatchUserFrameToFNE(packet->header, false, false, packet->userData.data(), retx)) {
if (retx) {
std::lock_guard<std::mutex> guard(m_sndcpPendingLock);
auto it = m_sndcpPending.find(packet->llId);
if (it != m_sndcpPending.end()) {
it->second.sentAt = now;
it->second.resendDue = false;
}
} else if (confirmed && packet->llId != WUID_ALL && packet->header.getSAP() == PDUSAP::USER_DATA &&
packet->header.getHeaderOffset() == 2U) {
// remember the PDU until the SU's link-layer ACK arrives
SNDCPPendingAck pending;
pending.header = packet->header; // carries the N(S) it was sent with
pending.userData = packet->userData;
pending.targetIPAddress = packet->targetIPAddress;
pending.ns = packet->header.getNs();
pending.sentAt = now;
bool replaced = false;
{
std::lock_guard<std::mutex> guard(m_sndcpPendingLock);
replaced = m_sndcpPending.find(packet->llId) != m_sndcpPending.end();
m_sndcpPending[packet->llId] = pending;
}
if (replaced) {
LogWarning(LOG_P25, P25_PDU_STR ", SNDCP downlink still unacknowledged, replacing pending record, llId = %u, n = %u",
packet->llId, uint32_t(pending.ns));
}
}
m_packetScheduler.pop();
}
else {
if (retx) {
LogWarning(LOG_P25, P25_PDU_STR ", SNDCP downlink retransmit rejected, dropping, llId = %u, n = %u",
packet->llId, uint32_t(packet->header.getNs()));
abandonRetx();
m_packetScheduler.pop();
return;
}
if (confirmed)
m_dataLinkManager.setReady(packet->llId, true);
@ -560,6 +760,27 @@ void P25PacketData::clock(uint32_t ms)
#endif // !defined(_WIN32)
}
/* Process a SNDCP data channel grant relayed by a site's control channel. */
void P25PacketData::processSNDCPChannelGrant(uint32_t peerId, uint32_t llId)
{
if (llId == 0U || llId == WUID_ALL)
return;
uint64_t now = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::system_clock::now().time_since_epoch()).count();
{
std::lock_guard<std::mutex> guard(m_sndcpContextLock);
SNDCPContext& ctx = m_sndcpContexts[llId];
ctx.ccPeerId = peerId;
ctx.grantedAt = now;
ctx.onChannelUntil = now + SNDCP_ON_CHANNEL_MS;
ctx.pageCount = 0U;
ctx.nextPageAt = 0U;
}
LogInfoEx(LOG_P25, P25_PDU_STR ", SNDCP data channel grant, llId = %u, peer = %u", llId, peerId);
}
/* Helper to cleanup any call's left in a dangling state without any further updates. */
void P25PacketData::cleanupStale()
@ -590,12 +811,143 @@ void P25PacketData::cleanupStale()
status = nullptr;
}
}
// forget downlinks pending an ACK from an SU that no longer has an SNDCP context
std::vector<uint32_t> pendingLlIds;
{
std::lock_guard<std::mutex> guard(m_sndcpPendingLock);
for (const auto& pending : m_sndcpPending)
pendingLlIds.push_back(pending.first);
}
for (uint32_t llId : pendingLlIds) {
bool active = false;
{
std::lock_guard<std::mutex> guard(m_sndcpContextLock);
auto ctx = m_sndcpContexts.find(llId);
active = ctx != m_sndcpContexts.end() && ctx->second.active;
}
if (!active)
clearSNDCPPendingAck(llId);
}
}
// ---------------------------------------------------------------------------
// Private Class Members
// ---------------------------------------------------------------------------
/* Helper to clear the downlink awaiting a link-layer ACK from a subscriber. */
void P25PacketData::clearSNDCPPendingAck(uint32_t llId)
{
std::lock_guard<std::mutex> guard(m_sndcpPendingLock);
m_sndcpPending.erase(llId);
}
/* Helper to process a link-layer ACK from a subscriber for a confirmed downlink. */
void P25PacketData::processSNDCPDownlinkAck(uint32_t llId, uint8_t ns)
{
std::lock_guard<std::mutex> guard(m_sndcpPendingLock);
auto it = m_sndcpPending.find(llId);
if (it != m_sndcpPending.end() && it->second.ns == ns)
m_sndcpPending.erase(it);
}
/* Helper to retransmit, or give up on, downlinks that were not acknowledged. */
void P25PacketData::processSNDCPRetransmit(uint64_t now)
{
#if !defined(_WIN32)
std::vector<SNDCPRetransmitEvent> events;
std::vector<ScheduledP25DataPacket> resend;
{
std::lock_guard<std::mutex> guard(m_sndcpPendingLock);
for (auto it = m_sndcpPending.begin(); it != m_sndcpPending.end(); ) {
SNDCPPendingAck& pending = it->second;
if (pending.resendDue) {
// the retransmission is still queued, waiting for the SU to reach a data channel
if (now >= pending.sentAt + SNDCP_LLC_RESEND_STUCK_MS) {
events.push_back({ it->first, pending.ns, pending.retxCount, SNDCPRetransmitResult::NEVER_SENT });
it = m_sndcpPending.erase(it);
} else {
++it;
}
continue;
}
if (now < pending.sentAt + SNDCP_LLC_ACK_TIMEOUT_MS) {
++it;
continue;
}
if (pending.retxCount >= SNDCP_LLC_RETX_MAX) {
events.push_back({ it->first, pending.ns, pending.retxCount, SNDCPRetransmitResult::UNACKNOWLEDGED });
it = m_sndcpPending.erase(it);
continue;
}
pending.retxCount++;
pending.sentAt = now;
pending.resendDue = true;
events.push_back({ it->first, pending.ns, pending.retxCount, SNDCPRetransmitResult::QUEUED });
// queue the copy, so the SU is paged first if it has left the data channel
ScheduledP25DataPacket packet;
packet.header = pending.header;
packet.userData = pending.userData;
packet.llId = it->first;
packet.targetIPAddress = pending.targetIPAddress;
packet.dueAt = 0U;
packet.retransmit = true;
resend.push_back(packet);
++it;
}
}
for (const SNDCPRetransmitEvent& event : events) {
switch (event.result) {
case SNDCPRetransmitResult::QUEUED:
LogInfoEx(LOG_P25, P25_PDU_STR ", SNDCP downlink retransmit, llId = %u, n = %u, attempt = %u",
event.llId, uint32_t(event.ns), uint32_t(event.attempt));
break;
case SNDCPRetransmitResult::UNACKNOWLEDGED:
LogWarning(LOG_P25, P25_PDU_STR ", SNDCP downlink unacknowledged, dropping, llId = %u, n = %u",
event.llId, uint32_t(event.ns));
m_dataLinkManager.setReady(event.llId, true);
break;
case SNDCPRetransmitResult::NEVER_SENT:
LogWarning(LOG_P25, P25_PDU_STR ", SNDCP downlink retransmit never sent, dropping, llId = %u, n = %u",
event.llId, uint32_t(event.ns));
m_dataLinkManager.setReady(event.llId, true);
break;
}
}
for (ScheduledP25DataPacket& packet : resend)
m_packetScheduler.enqueue(std::move(packet));
#endif // !defined(_WIN32)
}
/* Helper to refresh SNDCP state when a PDU is received from a subscriber. */
void P25PacketData::touchSNDCPContext(uint32_t llId, uint32_t peerId)
{
if (llId == 0U || llId == WUID_ALL)
return;
uint64_t now = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::system_clock::now().time_since_epoch()).count();
std::lock_guard<std::mutex> guard(m_sndcpContextLock);
auto it = m_sndcpContexts.find(llId);
if (it == m_sndcpContexts.end())
return; // not an SU with trunked packet data state
it->second.vcPeerId = peerId;
it->second.onChannelUntil = now + SNDCP_ON_CHANNEL_MS;
}
/* Helper to dispatch PDU user data. */
void P25PacketData::dispatch(uint32_t peerId)
@ -607,6 +959,10 @@ void P25PacketData::dispatch(uint32_t peerId)
return;
}
// anything received from an SU shows it is on the data channel right now
touchSNDCPContext(status->assembler.getExtendedAddress() ? status->assembler.dataHeader.getSrcLLId() :
status->assembler.dataHeader.getLLId(), peerId);
if (status->assembler.dataHeader.getFormat() == PDUFormatType::RSP) {
LogInfoEx(LOG_P25, P25_PDU_STR ", ISP, response, peer = %u, fmt = $%02X, rspClass = $%02X, rspType = $%02X, rspStatus = $%02X, llId = %u, srcLlId = %u",
peerId, status->assembler.dataHeader.getFormat(), status->assembler.dataHeader.getResponseClass(), status->assembler.dataHeader.getResponseType(), status->assembler.dataHeader.getResponseStatus(),
@ -617,6 +973,9 @@ void P25PacketData::dispatch(uint32_t peerId)
m_dataLinkManager.setReady(responseLlId, true);
if (status->assembler.dataHeader.getResponseClass() == PDUAckClass::ACK && status->assembler.dataHeader.getResponseType() == PDUAckType::ACK) {
if (responseLlId != 0U)
processSNDCPDownlinkAck(responseLlId, status->assembler.dataHeader.getResponseStatus());
LogInfoEx(LOG_P25, P25_PDU_STR ", ISP, response, OSP ACK, peer = %u, llId = %u, all blocks received OK, n = %u",
peerId, status->assembler.dataHeader.getLLId(), status->assembler.dataHeader.getResponseStatus());
} else {
@ -694,6 +1053,18 @@ void P25PacketData::dispatch(uint32_t peerId)
}
}
// SNDCP user data (SN-Data) is an IPv4 datagram behind a 2-octet SNDCP header on the
// user data SAP; strip the header and handle the datagram as packet data
if (sap == PDUSAP::USER_DATA && status->assembler.dataHeader.getHeaderOffset() == 2U &&
status->pduUserDataLength > 2U) {
const uint8_t sndcpType = (status->pduUserData[0U] >> 4) & 0x0FU;
if (sndcpType == SNDCP_PDUType::RF_CONFIRMED || sndcpType == SNDCP_PDUType::RF_UNCONFIRMED) {
if (!processSNDCPUserData(status, peerId))
return;
sap = PDUSAP::PACKET_DATA;
}
}
// handle standard P25 service access points
switch (sap) {
case PDUSAP::ARP:
@ -1002,7 +1373,7 @@ void P25PacketData::dispatchToFNE(uint32_t peerId)
/* Helper to dispatch PDU user data back to the local FNE network. (Will not transmit to neighbor FNE peers.) */
bool P25PacketData::dispatchUserFrameToFNE(DataHeader& dataHeader, bool extendedAddress, bool auxiliaryES, uint8_t* pduUserData)
bool P25PacketData::dispatchUserFrameToFNE(DataHeader& dataHeader, bool extendedAddress, bool auxiliaryES, uint8_t* pduUserData, bool retransmit)
{
uint32_t srcId = (extendedAddress) ? dataHeader.getSrcLLId() : dataHeader.getLLId();
uint32_t dstId = dataHeader.getLLId();
@ -1028,12 +1399,15 @@ bool P25PacketData::dispatchUserFrameToFNE(DataHeader& dataHeader, bool extended
return false;
}
// update V(S) only after a usable RF route has been selected
bool synchronize = false;
uint8_t sendSequence = m_dataLinkManager.nextSendSequence(srcId, synchronize);
if (synchronize)
dataHeader.setSynchronize(true);
dataHeader.setNs(sendSequence);
// update V(S) only after a usable RF route has been selected; a retransmission
// keeps the N(S) already in its header, so the SU can discard a duplicate
if (!retransmit) {
bool synchronize = false;
uint8_t sendSequence = m_dataLinkManager.nextSendSequence(srcId, synchronize);
if (synchronize)
dataHeader.setSynchronize(true);
dataHeader.setNs(sendSequence);
}
// dispatch the user frame to each resolved RF route
bool dispatched = false;
@ -1060,6 +1434,46 @@ bool P25PacketData::dispatchUserFrameToFNE(DataHeader& dataHeader, bool extended
return dispatched;
}
/* Helper used to process SNDCP user data (SN-Data) from PDU data. */
bool P25PacketData::processSNDCPUserData(RxStatus* status, uint32_t peerId)
{
const uint32_t llId = status->assembler.dataHeader.getLLId();
const uint8_t sndcpType = (status->pduUserData[0U] >> 4) & 0x0FU;
const uint8_t nsapi = status->pduUserData[0U] & 0x0FU;
// only uncompressed datagrams are supported; the context accept negotiates none
if (status->pduUserData[1U] != 0x00U) {
LogWarning(LOG_P25, P25_PDU_STR ", SNDCP user data with unsupported compression, llId = %u, pcomp/dcomp = $%02X",
llId, status->pduUserData[1U]);
return false;
}
LogInfoEx(LOG_P25, P25_PDU_STR ", SNDCP user data, llId = %u, pduType = $%02X, nsapi = %u, length = %u",
llId, sndcpType, nsapi, status->pduUserDataLength - 2U);
// adopt the context of an SU the FNE has no record of (the FNE restarted while the
// SU kept its context), so the reply is framed as SN-Data and can be routed
{
const uint64_t now = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::system_clock::now().time_since_epoch()).count();
std::lock_guard<std::mutex> guard(m_sndcpContextLock);
SNDCPContext& ctx = m_sndcpContexts[llId];
if (!ctx.active) {
ctx.active = true;
ctx.nsapi = nsapi;
ctx.vcPeerId = peerId;
ctx.onChannelUntil = now + SNDCP_ON_CHANNEL_MS;
}
}
// strip the SNDCP header, leaving a bare IPv4 datagram
::memmove(status->pduUserData, status->pduUserData + 2U, status->pduUserDataLength - 2U);
status->pduUserDataLength -= 2U;
return true;
}
/* Helper used to process conventional data registration from PDU data. */
bool P25PacketData::processConvDataReg(RxStatus* status)
@ -1149,6 +1563,11 @@ bool P25PacketData::processSNDCPControl(RxStatus* status)
LogInfoEx(LOG_P25, P25_PDU_STR ", SNDCP context activation request, llId = %u, nsapi = %u, ipAddr = %s, nat = $%02X, dsut = $%02X, mdpco = $%02X", llId,
isp->getNSAPI(), __IP_FROM_UINT(isp->getIPAddress()).c_str(), isp->getNAT(), isp->getDSUT(), isp->getMDPCO());
// the request is a confirmed PDU and needs its own ACK; the accept or reject
// that follows is a separate PDU and does not acknowledge it
write_PDU_Ack_Response(PDUAckClass::ACK, PDUAckType::ACK,
status->assembler.dataHeader.getNs(), llId, false);
// check if subscriber is provisioned (from RID table)
lookups::RadioId rid = m_network->m_ridLookup->find(llId);
if (rid.radioDefault() || !rid.radioEnabled()) {
@ -1222,7 +1641,7 @@ bool P25PacketData::processSNDCPControl(RxStatus* status)
osp->setNSAPI(isp->getNSAPI());
osp->setPriority(4U);
osp->setReadyTimer(SNDCPReadyTimer::TEN_SECONDS);
osp->setStandbyTimer(SNDCPStandbyTimer::ONE_MINUTE);
osp->setStandbyTimer(SNDCPStandbyTimer::FOUR_HOURS);
osp->setNAT(SNDCPNAT::IPV4_STATIC_ADDR);
osp->setIPAddress(staticIP);
osp->setMTU(SNDCP_MTU_510);
@ -1241,6 +1660,14 @@ bool P25PacketData::processSNDCPControl(RxStatus* status)
m_neighborCache.observe(llId, staticIP);
m_dataLinkManager.setReady(llId, true);
{
std::lock_guard<std::mutex> guard(m_sndcpContextLock);
SNDCPContext& ctx = m_sndcpContexts[llId];
ctx.active = true;
ctx.nsapi = isp->getNSAPI();
}
// a downlink still awaiting its ACK belongs to the context this one replaces
clearSNDCPPendingAck(llId);
dispatchUserFrameToFNE(rspHeader, false, false, txPduUserData);
@ -1287,7 +1714,7 @@ bool P25PacketData::processSNDCPControl(RxStatus* status)
osp->setNSAPI(isp->getNSAPI());
osp->setPriority(4U);
osp->setReadyTimer(SNDCPReadyTimer::TEN_SECONDS);
osp->setStandbyTimer(SNDCPStandbyTimer::ONE_MINUTE);
osp->setStandbyTimer(SNDCPStandbyTimer::FOUR_HOURS);
osp->setNAT(SNDCPNAT::IPV4_DYN_ADDR);
osp->setIPAddress(dynamicIP);
osp->setMTU(SNDCP_MTU_510);
@ -1306,6 +1733,14 @@ bool P25PacketData::processSNDCPControl(RxStatus* status)
m_neighborCache.observe(llId, dynamicIP);
m_dataLinkManager.setReady(llId, true);
{
std::lock_guard<std::mutex> guard(m_sndcpContextLock);
SNDCPContext& ctx = m_sndcpContexts[llId];
ctx.active = true;
ctx.nsapi = isp->getNSAPI();
}
// a downlink still awaiting its ACK belongs to the context this one replaces
clearSNDCPPendingAck(llId);
dispatchUserFrameToFNE(rspHeader, false, false, txPduUserData);
@ -1352,6 +1787,13 @@ bool P25PacketData::processSNDCPControl(RxStatus* status)
m_neighborCache.erase(llId);
m_dataLinkManager.erase(llId);
{
std::lock_guard<std::mutex> guard(m_sndcpContextLock);
auto ctx = m_sndcpContexts.find(llId);
if (ctx != m_sndcpContexts.end())
ctx->second.active = false;
}
clearSNDCPPendingAck(llId);
// send ACK response
write_PDU_Ack_Response(PDUAckClass::ACK, PDUAckType::ACK,

@ -32,6 +32,10 @@
#include "network/callhandler/packetdata/p25/IPConvergenceService.h"
#include "network/callhandler/packetdata/p25/PacketDataState.h"
#include <mutex>
#include <unordered_map>
#include <vector>
namespace network
{
namespace callhandler
@ -116,6 +120,13 @@ namespace network
*/
void cleanupStale();
/**
* @brief Process a SNDCP data channel grant relayed by a site's control channel.
* @param peerId Peer ID of the control channel that issued the grant.
* @param llId Logical Link ID of the SU.
*/
void processSNDCPChannelGrant(uint32_t peerId, uint32_t llId);
/**
* @brief Sets the debug flag.
* @param debug Debug flag.
@ -225,6 +236,81 @@ namespace network
p25data::ConventionalDataService m_conventionalDataService;
p25data::SCEPService m_scepService;
/**
* @brief Trunked (SNDCP) packet data state for a subscriber.
*/
struct SNDCPContext {
bool active = false; //!< Flag indicating a context has been accepted for the SU.
uint8_t nsapi = 0U; //!< NSAPI of the context.
uint32_t ccPeerId = 0U; //!< Control channel peer that last granted a data channel.
uint32_t vcPeerId = 0U; //!< Data channel peer the SU was last heard on.
uint64_t grantedAt = 0U; //!< Timestamp of the last data channel grant.
uint64_t onChannelUntil = 0U; //!< Timestamp until which the SU is taken to be on the data channel.
uint64_t nextPageAt = 0U; //!< Timestamp of the earliest next data page.
uint8_t pageCount = 0U; //!< Number of data pages sent for the packet being held.
};
std::mutex m_sndcpContextLock;
std::unordered_map<uint32_t, SNDCPContext> m_sndcpContexts;
/**
* @brief Confirmed trunked (SNDCP) downlink awaiting the subscriber's link-layer ACK.
*/
struct SNDCPPendingAck {
::p25::data::DataHeader header; //!< Header as sent, carrying the N(S).
std::vector<uint8_t> userData; //!< User data as sent.
uint32_t targetIPAddress = 0U; //!< Target IP address of the datagram.
uint8_t ns = 0U; //!< N(S) the PDU was sent with.
uint64_t sentAt = 0U; //!< Timestamp of the last send, or of queuing a retransmission.
uint8_t retxCount = 0U; //!< Number of retransmissions made.
bool resendDue = false; //!< Flag indicating a retransmission is queued.
};
/**
* @brief Result of checking a downlink that is awaiting its link-layer ACK.
*/
enum class SNDCPRetransmitResult : uint8_t {
QUEUED, //!< A retransmission was queued.
UNACKNOWLEDGED, //!< Retransmissions are exhausted, the PDU was dropped.
NEVER_SENT //!< A queued retransmission never left the scheduler.
};
/**
* @brief Outcome of a retransmission check for one subscriber.
*/
struct SNDCPRetransmitEvent {
uint32_t llId; //!< Logical Link ID of the SU.
uint8_t ns; //!< N(S) of the PDU.
uint8_t attempt; //!< Number of retransmissions made.
SNDCPRetransmitResult result; //!< Result of the check.
};
std::mutex m_sndcpPendingLock; // never nested with m_sndcpContextLock
std::unordered_map<uint32_t, SNDCPPendingAck> m_sndcpPending;
/**
* @brief Helper to refresh SNDCP state when a PDU is received from a subscriber.
* @param llId Logical Link ID of the SU.
* @param peerId Peer ID the PDU arrived on.
*/
void touchSNDCPContext(uint32_t llId, uint32_t peerId);
/**
* @brief Helper to clear the downlink awaiting a link-layer ACK from a subscriber.
* @param llId Logical Link ID of the SU.
*/
void clearSNDCPPendingAck(uint32_t llId);
/**
* @brief Helper to process a link-layer ACK from a subscriber for a confirmed downlink.
* @param llId Logical Link ID of the SU.
* @param ns N(S) the ACK acknowledges.
*/
void processSNDCPDownlinkAck(uint32_t llId, uint8_t ns);
/**
* @brief Helper to retransmit, or give up on, downlinks that were not acknowledged.
* @param now Current timestamp in milliseconds.
*/
void processSNDCPRetransmit(uint64_t now);
bool m_debug;
/**
@ -243,8 +329,9 @@ namespace network
* @param extendedAddress Flag indicating whether or not to extended addressing is in use.
* @param auxiliaryES Flag indicating whether or not an auxiliary ES is included.
* @param pduUserData Buffer containing user data to transmit.
* @param retransmit Flag indicating a retransmission, sent with the N(S) already in the header.
*/
bool dispatchUserFrameToFNE(::p25::data::DataHeader& dataHeader, bool extendedAddress, bool auxiliaryES, uint8_t* pduUserData);
bool dispatchUserFrameToFNE(::p25::data::DataHeader& dataHeader, bool extendedAddress, bool auxiliaryES, uint8_t* pduUserData, bool retransmit = false);
/**
* @brief Helper used to process conventional data registration from PDU data.
@ -258,6 +345,13 @@ namespace network
* @returns bool True, if SNDCP control data was processed, otherwise false.
*/
bool processSNDCPControl(RxStatus* status);
/**
* @brief Helper used to process SNDCP user data (SN-Data) from PDU data.
* @param status Instance of the RxStatus class.
* @param peerId Peer ID.
* @returns bool True, if the SNDCP header was stripped leaving an IPv4 datagram, otherwise false.
*/
bool processSNDCPUserData(RxStatus* status, uint32_t peerId);
/**
* @brief Helper write ARP request to the network.

@ -45,6 +45,7 @@ namespace network
uint64_t dueAt = 0U; //!< Timestamp indicating when the packet is due.
uint8_t retryCount = 0U; //!< Number of retry attempts for the packet.
bool extendedRetry = false; //!< Indicates if extended retry is enabled.
bool retransmit = false; //!< Link-layer retransmission of a confirmed packet already sent once.
};
// ---------------------------------------------------------------------------

@ -470,13 +470,34 @@ bool ControlSignaling::process(uint8_t* data, uint32_t len, std::unique_ptr<lc::
}
if (m_p25->m_sndcpSupport) {
writeRF_TSDU_SNDCP_Grant(srcId, false);
writeRF_TSDU_SNDCP_Grant(srcId, false, 0U, isp->getDataServiceOptions());
}
else {
writeRF_TSDU_Deny(srcId, WUID_FNE, ReasonCode::DENY_SYS_UNSUPPORTED_SVC, TSBKO::ISP_SNDCP_CH_REQ);
}
}
break;
case TSBKO::ISP_SNDCP_DATA_PAGE_RES:
{
// make sure control data is supported
IS_SUPPORT_CONTROL_CHECK(tsbk->toString(true), TSBKO::ISP_SNDCP_DATA_PAGE_RES, srcId);
// validate the source RID
VALID_SRCID(tsbk->toString(true), TSBKO::ISP_SNDCP_DATA_PAGE_RES, srcId);
ISP_SNDCP_DATA_PAGE_RES* isp = static_cast<ISP_SNDCP_DATA_PAGE_RES*>(tsbk.get());
if (m_verbose) {
LogInfoEx(LOG_RF, P25_TSDU_STR ", %s, dataServiceOptions = $%02X, answer = $%02X, dataAccessControl = $%04X, srcId = %u",
tsbk->toString(true).c_str(), isp->getDataServiceOptions(), isp->getAnswerResponse(), isp->getDataAccessControl(), srcId);
}
// the SU answered an FNE data page -- when it will proceed, direct it to
// a data channel exactly as for an SU request
if (m_p25->m_sndcpSupport && isp->getAnswerResponse() == ResponseCode::ANS_PROCEED) {
writeRF_TSDU_SNDCP_Grant(srcId, false, 0U, isp->getDataServiceOptions());
}
}
break;
case TSBKO::ISP_SNDCP_REC_REQ:
{
// make sure control data is supported
@ -485,14 +506,16 @@ bool ControlSignaling::process(uint8_t* data, uint32_t len, std::unique_ptr<lc::
// validate the source RID
VALID_SRCID(tsbk->toString(true), TSBKO::ISP_SNDCP_CH_REQ, srcId);
ISP_SNDCP_CH_REQ* isp = static_cast<ISP_SNDCP_CH_REQ*>(tsbk.get());
// the factory builds an ISP_SNDCP_REC_REQ for this opcode; it is not an
// ISP_SNDCP_CH_REQ, and read as one the Data Service Options come out wrong
ISP_SNDCP_REC_REQ* isp = static_cast<ISP_SNDCP_REC_REQ*>(tsbk.get());
if (m_verbose) {
LogInfoEx(LOG_RF, P25_TSDU_STR ", %s, dataServiceOptions = $%02X, dataAccessControl = %u, srcId = %u",
tsbk->toString(true).c_str(), isp->getDataServiceOptions(), isp->getDataAccessControl(), srcId);
}
if (m_p25->m_sndcpSupport) {
writeRF_TSDU_SNDCP_Grant(srcId, false);
writeRF_TSDU_SNDCP_Grant(srcId, false, 0U, isp->getDataServiceOptions());
}
else {
writeRF_TSDU_Deny(srcId, WUID_FNE, ReasonCode::DENY_SYS_UNSUPPORTED_SVC, TSBKO::ISP_SNDCP_REC_REQ);
@ -1397,6 +1420,11 @@ bool ControlSignaling::processNetwork(uint8_t* data, uint32_t len, lc::LC& contr
break; // the FNE may explicitly send these
case TSBKO::OSP_SNDCP_CH_GNT:
return true; // don't allow this to write to the air
case TSBKO::OSP_SNDCP_DATA_PAGE_REQ:
if (m_verbose) {
LogInfoEx(LOG_NET, P25_TSDU_STR ", %s, dstId = %u", tsbk->toString().c_str(), dstId);
}
break; // the FNE pages an SU for outbound packet data
default:
LogError(LOG_NET, P25_TSDU_STR ", unhandled LCO, mfId = $%02X, lco = $%02X", tsbk->getMFId(), tsbk->getLCO());
return false;
@ -3311,7 +3339,7 @@ void ControlSignaling::writeRF_TSDU_Grant_Update()
/* Helper to write a SNDCP grant packet. */
bool ControlSignaling::writeRF_TSDU_SNDCP_Grant(uint32_t srcId, bool skip, uint32_t chNo)
bool ControlSignaling::writeRF_TSDU_SNDCP_Grant(uint32_t srcId, bool skip, uint32_t chNo, uint8_t dataServiceOptions)
{
if (!m_p25->m_sndcpSupport)
return false;
@ -3322,6 +3350,10 @@ bool ControlSignaling::writeRF_TSDU_SNDCP_Grant(uint32_t srcId, bool skip, uint3
osp->setSrcId(srcId);
osp->setDstId(srcId);
// echo the requested Data Service Options; an SU ignores a grant whose
// options differ from the request it sent
osp->setDataServiceOptions(dataServiceOptions);
// are we skipping checking?
if (!skip) {
if (m_p25->m_rfState != RS_RF_LISTENING && m_p25->m_rfState != RS_RF_DATA) {
@ -3422,8 +3454,9 @@ bool ControlSignaling::writeRF_TSDU_SNDCP_Grant(uint32_t srcId, bool skip, uint3
osp->toString().c_str(), voiceChData.chId(), osp->getDataChnNo(), osp->getSrcId());
}
// transmit group grant
writeRF_TSDU_SBF_Imm(osp.get(), true);
// transmit group grant -- the first copy is also passed to the network so the
// FNE knows the SU has been directed to a data channel and may send to it
writeRF_TSDU_SBF_Imm(osp.get(), false);
if (m_redundantGrant) {
for (int i = 0; i < 3; i++)
writeRF_TSDU_SBF(osp.get(), true);

@ -453,9 +453,10 @@ namespace p25
* @param srcId Source Radio ID.
* @param skip Flag indicating normal grant checking is skipped.
* @param chNo Channel Number.
* @param dataServiceOptions Data Service Options of the request being answered, echoed in the grant.
* @returns True, if granted, otherwise false.
*/
bool writeRF_TSDU_SNDCP_Grant(uint32_t srcId, bool skip = false, uint32_t chNo = 0U);
bool writeRF_TSDU_SNDCP_Grant(uint32_t srcId, bool skip = false, uint32_t chNo = 0U, uint8_t dataServiceOptions = 0U);
/**
* @brief Helper to write a unit to unit answer request packet.
* @param srcId Source Radio ID.

Loading…
Cancel
Save

Powered by TurnKey Linux.