From b555b2a4f0b95fde92b8deda860884eeb6360907 Mon Sep 17 00:00:00 2001 From: Bryan Biedenkapp Date: Sun, 8 Nov 2026 10:31:20 -0500 Subject: [PATCH] add synthetic U2U call tests; --- src/host/p25/packet/ControlSignaling.h | 3 + tests/host/HostTestHooks.cpp | 38 +++++++ tests/host/HostTestHooks.h | 27 +++++ tests/p25/HostControl_Tests.cpp | 134 +++++++++++++++++++++++++ tests/p25/TSBK_U2U_Tests.cpp | 69 +++++++++++++ 5 files changed, 271 insertions(+) create mode 100644 tests/p25/TSBK_U2U_Tests.cpp diff --git a/src/host/p25/packet/ControlSignaling.h b/src/host/p25/packet/ControlSignaling.h index 98cc0767..579a50d6 100644 --- a/src/host/p25/packet/ControlSignaling.h +++ b/src/host/p25/packet/ControlSignaling.h @@ -31,6 +31,8 @@ #include #include +class HostTestHooks; + namespace p25 { // --------------------------------------------------------------------------- @@ -155,6 +157,7 @@ namespace p25 void setTSBKVerbose(bool verbose); protected: + friend class ::HostTestHooks; friend class packet::Voice; friend class packet::Data; friend class p25::Control; diff --git a/tests/host/HostTestHooks.cpp b/tests/host/HostTestHooks.cpp index 47a3b89c..4cb6994f 100644 --- a/tests/host/HostTestHooks.cpp +++ b/tests/host/HostTestHooks.cpp @@ -18,6 +18,8 @@ #include "common/nxdn/channel/LICH.h" #include "common/nxdn/channel/SACCH.h" #include "common/nxdn/lc/RTCH.h" +#include "common/p25/lc/tsbk/TSBKFactory.h" +#include "host/p25/packet/ControlSignaling.h" #include @@ -384,6 +386,42 @@ bool HostTestHooks::p25SeedGrant(p25::Control& control, uint32_t srcId, uint32_t return control.m_affiliations->grantCh(dstId, srcId, 15U, true, netGranted); } +/* Seeds a P25 unit-to-unit grant for a call-procedure test. */ + +bool HostTestHooks::p25SeedU2UGrant(p25::Control& control, uint32_t srcId, uint32_t dstId) +{ + return control.m_affiliations->grantCh(dstId, srcId, 15U, false, false); +} + +/* Configures P25 unit-to-unit call-procedure policy for a test. */ + +void HostTestHooks::p25ConfigureU2U(p25::Control& control, bool availabilityCheck) +{ + control.m_enableControl = true; + control.m_ignoreAffiliationCheck = false; + control.m_control->m_unitToUnitAvailCheck = availabilityCheck; +} + +/* Registers a P25 unit for a call-procedure test. */ + +void HostTestHooks::p25RegisterUnit(p25::Control& control, uint32_t srcId) +{ + control.m_affiliations->unitReg(srcId); +} + +/* Injects an independently encoded raw TSBK into P25 control signaling. */ + +bool HostTestHooks::p25ProcessRawTSBK(p25::Control& control, const uint8_t* raw) +{ + std::unique_ptr tsbk = p25::lc::tsbk::TSBKFactory::createTSBK(raw, true); + if (tsbk == nullptr) { + return false; + } + + uint8_t frame[2U] = { 0U }; + return control.m_control->process(frame, sizeof(frame), std::move(tsbk)); +} + /* Releases a P25 grant for a steering-policy test. */ bool HostTestHooks::p25ReleaseGrant(p25::Control& control, uint32_t dstId) diff --git a/tests/host/HostTestHooks.h b/tests/host/HostTestHooks.h index 3d4419f5..b6bb63d4 100644 --- a/tests/host/HostTestHooks.h +++ b/tests/host/HostTestHooks.h @@ -232,6 +232,33 @@ public: * @returns bool True if the grant was created; otherwise, false. */ static bool p25SeedGrant(p25::Control& control, uint32_t srcId, uint32_t dstId, bool netGranted); + /** + * @brief Seeds a P25 unit-to-unit grant for a call-procedure test. + * @param control P25 control instance. + * @param srcId Source radio ID. + * @param dstId Destination radio ID. + * @returns bool True if the grant was created; otherwise, false. + */ + static bool p25SeedU2UGrant(p25::Control& control, uint32_t srcId, uint32_t dstId); + /** + * @brief Configures P25 unit-to-unit call-procedure policy for a test. + * @param control P25 control instance. + * @param availabilityCheck Flag indicating whether the availability check is enabled. + */ + static void p25ConfigureU2U(p25::Control& control, bool availabilityCheck); + /** + * @brief Registers a P25 unit for a call-procedure test. + * @param control P25 control instance. + * @param srcId Radio ID to register. + */ + static void p25RegisterUnit(p25::Control& control, uint32_t srcId); + /** + * @brief Injects an independently encoded raw TSBK into P25 control signaling. + * @param control P25 control instance. + * @param raw Raw 12-byte TSBK including CRC. + * @returns bool True if the TSBK was accepted; otherwise, false. + */ + static bool p25ProcessRawTSBK(p25::Control& control, const uint8_t* raw); /** * @brief Releases a P25 grant for a steering-policy test. * @param control P25 control instance. diff --git a/tests/p25/HostControl_Tests.cpp b/tests/p25/HostControl_Tests.cpp index 291bca27..48bfadc0 100644 --- a/tests/p25/HostControl_Tests.cpp +++ b/tests/p25/HostControl_Tests.cpp @@ -15,6 +15,7 @@ #include "common/lookups/TalkgroupRulesLookup.h" #include "common/network/NetRPC.h" #include "common/network/Network.h" +#include "common/edac/CRC.h" #include "common/p25/P25Defines.h" #include "common/p25/data/LowSpeedData.h" #include "common/p25/lc/LC.h" @@ -142,6 +143,42 @@ void buildP25RFFrame(const uint8_t* payload, uint8_t* frame) ::memcpy(frame + 2U, payload, p25::defines::P25_LDU_FRAME_LENGTH_BYTES); } +/** + * @brief Builds an independently encoded UU VCH request TSBK. + * @param srcId The ID of the source. + * @param dstId The ID of the destination. + * @param raw The buffer to store the constructed TSBK. + */ +void buildUUVchRequest(uint32_t srcId, uint32_t dstId, uint8_t* raw) +{ + ::memset(raw, 0x00U, p25::defines::P25_TSBK_LENGTH_BYTES); + raw[0U] = 0x80U | p25::defines::TSBKO::IOSP_UU_VCH; + raw[1U] = p25::defines::MFG_STANDARD; + raw[2U] = 0x04U; + SET_UINT24(dstId, raw, 4U); + SET_UINT24(srcId, raw, 7U); + edac::CRC::addCCITT162(raw, p25::defines::P25_TSBK_LENGTH_BYTES); +} + +/** + * @brief Builds an independently encoded UU ANS response ISP TSBK. + * @param responderId The ID of the responder. + * @param callerId The ID of the caller. + * @param response The response code. + * @param raw The buffer to store the constructed TSBK. + */ +void buildUUAnswerResponse(uint32_t responderId, uint32_t callerId, uint8_t response, uint8_t* raw) +{ + ::memset(raw, 0x00U, p25::defines::P25_TSBK_LENGTH_BYTES); + raw[0U] = 0x80U | p25::defines::TSBKO::IOSP_UU_ANS; + raw[1U] = p25::defines::MFG_STANDARD; + raw[2U] = 0x04U; + raw[3U] = response; + SET_UINT24(callerId, raw, 4U); + SET_UINT24(responderId, raw, 7U); + edac::CRC::addCCITT162(raw, p25::defines::P25_TSBK_LENGTH_BYTES); +} + } // namespace // --------------------------------------------------------------------------- @@ -591,6 +628,103 @@ TEST_CASE("P25 enhanced conventional RF grant permits only the owning source to REQUIRE(HostTestHooks::p25RFLastDstId(*harness.m_control) == 2001U); } +TEST_CASE("P25 unit-to-unit grant admits either participant as first talker", + "[p25][host][control][u2u]") +{ + P25HostHarness harness; + const uint32_t channelNo = 1U; + const uint32_t callingUnit = 5519008U; + const uint32_t calledUnit = 5519007U; + + harness.m_chLookup.setRFChData(channelNo, lookups::VoiceChData(1U, channelNo, "", 0U, "")); + REQUIRE(harness.m_chLookup.initializeRFCh(channelNo)); + REQUIRE(HostTestHooks::p25SeedU2UGrant(*harness.m_control, callingUnit, calledUnit)); + + // TIA-102.AABD permits either party to be the first talker after the + // availability-check procedure. Voice admission therefore must recognize + // the same private call when its direction is reversed. + CHECK(HostTestHooks::p25IsGranted(*harness.m_control, calledUnit)); + CHECK(HostTestHooks::p25IsGranted(*harness.m_control, callingUnit)); +} + +TEST_CASE("P25 unit-to-unit availability-check procedure grants only after proceed", + "[p25][host][control][u2u][procedure]") +{ + P25HostHarness harness; + const uint32_t channelNo = 1U; + const uint32_t caller = 5519008U; + const uint32_t called = 5519007U; + uint8_t raw[p25::defines::P25_TSBK_LENGTH_BYTES]; + + harness.m_chLookup.setRFChData(channelNo, lookups::VoiceChData(1U, channelNo, "", 0U, "")); + REQUIRE(harness.m_chLookup.initializeRFCh(channelNo)); + HostTestHooks::p25ConfigureU2U(*harness.m_control, true); + HostTestHooks::p25RegisterUnit(*harness.m_control, caller); + HostTestHooks::p25RegisterUnit(*harness.m_control, called); + + buildUUVchRequest(caller, called, raw); + REQUIRE(HostTestHooks::p25ProcessRawTSBK(*harness.m_control, raw)); + CHECK_FALSE(HostTestHooks::p25IsGranted(*harness.m_control, caller)); + CHECK_FALSE(HostTestHooks::p25IsGranted(*harness.m_control, called)); + + buildUUAnswerResponse(called, caller, p25::defines::ResponseCode::ANS_WAIT, raw); + REQUIRE(HostTestHooks::p25ProcessRawTSBK(*harness.m_control, raw)); + CHECK_FALSE(HostTestHooks::p25IsGranted(*harness.m_control, caller)); + CHECK_FALSE(HostTestHooks::p25IsGranted(*harness.m_control, called)); + + buildUUAnswerResponse(called, caller, p25::defines::ResponseCode::ANS_PROCEED, raw); + REQUIRE(HostTestHooks::p25ProcessRawTSBK(*harness.m_control, raw)); + CHECK(HostTestHooks::p25IsGranted(*harness.m_control, caller)); +} + +TEST_CASE("P25 unit-to-unit rejects unsolicited and mismatched answer responses", + "[p25][host][control][u2u][procedure]") +{ + P25HostHarness harness; + const uint32_t channelNo = 1U; + const uint32_t caller = 5519008U; + const uint32_t called = 5519007U; + const uint32_t unrelatedCaller = 5519006U; + uint8_t raw[p25::defines::P25_TSBK_LENGTH_BYTES]; + + harness.m_chLookup.setRFChData(channelNo, lookups::VoiceChData(1U, channelNo, "", 0U, "")); + REQUIRE(harness.m_chLookup.initializeRFCh(channelNo)); + HostTestHooks::p25ConfigureU2U(*harness.m_control, true); + HostTestHooks::p25RegisterUnit(*harness.m_control, caller); + HostTestHooks::p25RegisterUnit(*harness.m_control, called); + HostTestHooks::p25RegisterUnit(*harness.m_control, unrelatedCaller); + + buildUUAnswerResponse(called, caller, p25::defines::ResponseCode::ANS_PROCEED, raw); + REQUIRE(HostTestHooks::p25ProcessRawTSBK(*harness.m_control, raw)); + CHECK_FALSE(HostTestHooks::p25IsGranted(*harness.m_control, caller)); + REQUIRE(HostTestHooks::p25ReleaseGrant(*harness.m_control, caller)); + + buildUUVchRequest(caller, called, raw); + REQUIRE(HostTestHooks::p25ProcessRawTSBK(*harness.m_control, raw)); + buildUUAnswerResponse(called, unrelatedCaller, p25::defines::ResponseCode::ANS_PROCEED, raw); + REQUIRE(HostTestHooks::p25ProcessRawTSBK(*harness.m_control, raw)); + CHECK_FALSE(HostTestHooks::p25IsGranted(*harness.m_control, unrelatedCaller)); +} + +TEST_CASE("P25 unit-to-unit without availability check grants the request immediately", + "[p25][host][control][u2u][procedure]") +{ + P25HostHarness harness; + const uint32_t channelNo = 1U; + const uint32_t caller = 5519008U; + const uint32_t called = 5519007U; + uint8_t raw[p25::defines::P25_TSBK_LENGTH_BYTES]; + + harness.m_chLookup.setRFChData(channelNo, lookups::VoiceChData(1U, channelNo, "", 0U, "")); + REQUIRE(harness.m_chLookup.initializeRFCh(channelNo)); + HostTestHooks::p25ConfigureU2U(*harness.m_control, false); + HostTestHooks::p25RegisterUnit(*harness.m_control, caller); + + buildUUVchRequest(caller, called, raw); + REQUIRE(HostTestHooks::p25ProcessRawTSBK(*harness.m_control, raw)); + CHECK(HostTestHooks::p25IsGranted(*harness.m_control, called)); +} + TEST_CASE("P25 host preserves encrypted network call parameters", "[p25][host][control][crypto]") { P25HostHarness harness; diff --git a/tests/p25/TSBK_U2U_Tests.cpp b/tests/p25/TSBK_U2U_Tests.cpp new file mode 100644 index 00000000..5ce2a085 --- /dev/null +++ b/tests/p25/TSBK_U2U_Tests.cpp @@ -0,0 +1,69 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Digital Voice Modem - Test Suite + * GPLv2 Open Source. Use is subject to license terms. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + */ +#include "host/Defines.h" +#include "common/edac/CRC.h" +#include "common/p25/P25Defines.h" +#include "common/p25/lc/tsbk/TSBKFactory.h" + +using namespace p25; +using namespace p25::defines; +using namespace p25::lc; +using namespace p25::lc::tsbk; + +#include + +TEST_CASE("TSBK unit-to-unit answer request and response layouts", "[p25][tsbk][u2u]") +{ + const uint32_t callingUnit = 0x543210U; + const uint32_t calledUnit = 0x123456U; + + SECTION("OSP answer request omits the ISP answer-response octet") { + IOSP_UU_ANS request; + request.setPriority(4U); + request.setSrcId(callingUnit); + request.setDstId(calledUnit); + + uint8_t raw[P25_TSBK_LENGTH_BYTES]; + request.encode(raw, true, true); + + REQUIRE((raw[0U] & 0x3FU) == TSBKO::IOSP_UU_ANS); + REQUIRE(raw[1U] == MFG_STANDARD); + REQUIRE(raw[2U] == 0x04U); // Service options + REQUIRE(raw[3U] == 0x00U); // Reserved in the OSP request + REQUIRE(raw[4U] == 0x12U); // Target address + REQUIRE(raw[5U] == 0x34U); + REQUIRE(raw[6U] == 0x56U); + REQUIRE(raw[7U] == 0x54U); // Source address + REQUIRE(raw[8U] == 0x32U); + REQUIRE(raw[9U] == 0x10U); + } + + SECTION("ISP answer response decodes responder as source and caller as target") { + uint8_t raw[P25_TSBK_LENGTH_BYTES] = { 0U }; + raw[0U] = 0x80U | TSBKO::IOSP_UU_ANS; + raw[1U] = MFG_STANDARD; + raw[2U] = 0x04U; // Service options + raw[3U] = ResponseCode::ANS_PROCEED; + raw[4U] = 0x54U; // Target: original caller + raw[5U] = 0x32U; + raw[6U] = 0x10U; + raw[7U] = 0x12U; // Source: responding called unit + raw[8U] = 0x34U; + raw[9U] = 0x56U; + edac::CRC::addCCITT162(raw, P25_TSBK_LENGTH_BYTES); + + std::unique_ptr tsbk = TSBKFactory::createTSBK(raw, true); + REQUIRE(tsbk != nullptr); + + IOSP_UU_ANS* response = dynamic_cast(tsbk.get()); + REQUIRE(response != nullptr); + REQUIRE(response->getResponse() == ResponseCode::ANS_PROCEED); + REQUIRE(response->getPriority() == 4U); + REQUIRE(response->getSrcId() == calledUnit); + REQUIRE(response->getDstId() == callingUnit); + } +}