diff --git a/src/common/p25/P25Defines.h b/src/common/p25/P25Defines.h index b25b5a4a..d4f0fc52 100644 --- a/src/common/p25/P25Defines.h +++ b/src/common/p25/P25Defines.h @@ -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 diff --git a/src/common/p25/data/Assembler.cpp b/src/common/p25/data/Assembler.cpp index 8df3a9f5..e6688f49 100644 --- a/src/common/p25/data/Assembler.cpp +++ b/src/common/p25/data/Assembler.cpp @@ -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(); diff --git a/src/common/p25/lc/tsbk/ISP_SNDCP_DATA_PAGE_RES.cpp b/src/common/p25/lc/tsbk/ISP_SNDCP_DATA_PAGE_RES.cpp new file mode 100644 index 00000000..e880eef7 --- /dev/null +++ b/src/common/p25/lc/tsbk/ISP_SNDCP_DATA_PAGE_RES.cpp @@ -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 + +// --------------------------------------------------------------------------- +// 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; +} diff --git a/src/common/p25/lc/tsbk/ISP_SNDCP_DATA_PAGE_RES.h b/src/common/p25/lc/tsbk/ISP_SNDCP_DATA_PAGE_RES.h new file mode 100644 index 00000000..cc3ce1f6 --- /dev/null +++ b/src/common/p25/lc/tsbk/ISP_SNDCP_DATA_PAGE_RES.h @@ -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__ diff --git a/src/common/p25/lc/tsbk/OSP_SNDCP_CH_ANN.cpp b/src/common/p25/lc/tsbk/OSP_SNDCP_CH_ANN.cpp index 9d6b18fb..c59b0137 100644 --- a/src/common/p25/lc/tsbk/OSP_SNDCP_CH_ANN.cpp +++ b/src/common/p25/lc/tsbk/OSP_SNDCP_CH_ANN.cpp @@ -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) { diff --git a/src/common/p25/lc/tsbk/OSP_SNDCP_DATA_PAGE_REQ.cpp b/src/common/p25/lc/tsbk/OSP_SNDCP_DATA_PAGE_REQ.cpp new file mode 100644 index 00000000..240f674c --- /dev/null +++ b/src/common/p25/lc/tsbk/OSP_SNDCP_DATA_PAGE_REQ.cpp @@ -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 + +// --------------------------------------------------------------------------- +// 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 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; +} diff --git a/src/common/p25/lc/tsbk/OSP_SNDCP_DATA_PAGE_REQ.h b/src/common/p25/lc/tsbk/OSP_SNDCP_DATA_PAGE_REQ.h new file mode 100644 index 00000000..d9190911 --- /dev/null +++ b/src/common/p25/lc/tsbk/OSP_SNDCP_DATA_PAGE_REQ.h @@ -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__ diff --git a/src/common/p25/lc/tsbk/TSBKFactory.cpp b/src/common/p25/lc/tsbk/TSBKFactory.cpp index b1ffa2d7..6352f7a6 100644 --- a/src/common/p25/lc/tsbk/TSBKFactory.cpp +++ b/src/common/p25/lc/tsbk/TSBKFactory.cpp @@ -192,8 +192,12 @@ std::unique_ptr 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: diff --git a/src/common/p25/lc/tsbk/TSBKFactory.h b/src/common/p25/lc/tsbk/TSBKFactory.h index 3dfcefb4..3d8a7ea6 100644 --- a/src/common/p25/lc/tsbk/TSBKFactory.h +++ b/src/common/p25/lc/tsbk/TSBKFactory.h @@ -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" diff --git a/src/fne/network/callhandler/TagP25Data.cpp b/src/fne/network/callhandler/TagP25Data.cpp index ed2bd101..1ba3837e 100644 --- a/src/fne/network/callhandler/TagP25Data.cpp +++ b/src/fne/network/callhandler/TagP25Data.cpp @@ -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); diff --git a/src/fne/network/callhandler/packetdata/P25PacketData.cpp b/src/fne/network/callhandler/packetdata/P25PacketData.cpp index 701b1b02..d1397964 100644 --- a/src/fne/network/callhandler/packetdata/P25PacketData.cpp +++ b/src/fne/network/callhandler/packetdata/P25PacketData.cpp @@ -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 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 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 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 osp = std::make_unique(); + 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 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 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::system_clock::now().time_since_epoch()).count(); + { + std::lock_guard 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 pendingLlIds; + { + std::lock_guard 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 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 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 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 events; + std::vector resend; + + { + std::lock_guard 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::system_clock::now().time_since_epoch()).count(); + + std::lock_guard 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::system_clock::now().time_since_epoch()).count(); + + std::lock_guard 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 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 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 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, diff --git a/src/fne/network/callhandler/packetdata/P25PacketData.h b/src/fne/network/callhandler/packetdata/P25PacketData.h index 97102960..e6f5317e 100644 --- a/src/fne/network/callhandler/packetdata/P25PacketData.h +++ b/src/fne/network/callhandler/packetdata/P25PacketData.h @@ -32,6 +32,10 @@ #include "network/callhandler/packetdata/p25/IPConvergenceService.h" #include "network/callhandler/packetdata/p25/PacketDataState.h" +#include +#include +#include + 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 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 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 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. diff --git a/src/fne/network/callhandler/packetdata/P25PacketScheduler.h b/src/fne/network/callhandler/packetdata/P25PacketScheduler.h index c9351dea..801d43e1 100644 --- a/src/fne/network/callhandler/packetdata/P25PacketScheduler.h +++ b/src/fne/network/callhandler/packetdata/P25PacketScheduler.h @@ -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. }; // --------------------------------------------------------------------------- diff --git a/src/host/p25/packet/ControlSignaling.cpp b/src/host/p25/packet/ControlSignaling.cpp index 3ec2fede..f925be1a 100644 --- a/src/host/p25/packet/ControlSignaling.cpp +++ b/src/host/p25/packet/ControlSignaling.cpp @@ -470,13 +470,34 @@ bool ControlSignaling::process(uint8_t* data, uint32_t len, std::unique_ptrm_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(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_ptrtoString(true), TSBKO::ISP_SNDCP_CH_REQ, srcId); - ISP_SNDCP_CH_REQ* isp = static_cast(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(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); diff --git a/src/host/p25/packet/ControlSignaling.h b/src/host/p25/packet/ControlSignaling.h index 5436de5f..bb393735 100644 --- a/src/host/p25/packet/ControlSignaling.h +++ b/src/host/p25/packet/ControlSignaling.h @@ -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.