// 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. * * Copyright (C) 2026 Bryan Biedenkapp, N2PLL * */ #include "host/p25/phase2/Control.h" #include "host/p25/phase2/Slot.h" #include "host/HostTestHooks.h" #include "common/BitManipulation.h" #include "common/edac/RS634717.h" #include "common/p25/P25Defines.h" #include "common/p25/lc/mac/MACFactory.h" #include "host/modem/Modem.h" #include "host/modem/port/IModemPort.h" #include #include #include #include using namespace p25::phase2; // --------------------------------------------------------------------------- // Global Functions // --------------------------------------------------------------------------- namespace { /** * @brief Creates an inbound MAC frame for testing purposes. * @param duid The DUID of the MAC frame. * @param opcode The MAC PDU opcode. * @param srcId The source ID for the MAC frame. * @param dstId The destination ID for the MAC frame. * @return An array representing the inbound MAC frame. */ std::array makeInboundMAC( P25DEF::P2_DUID::E duid, uint8_t opcode, uint32_t srcId = 0x123456U, uint32_t dstId = 0x2345U, uint16_t colorCode = 0U) { p25::lc::LC control; control.setGroup(true); control.setLCO(P25DEF::P2_MAC_MCO::GROUP); control.setSrcId(srcId); control.setDstId(dstId); control.setColorCode(colorCode); control.setP2DUID(static_cast(duid)); control.setMACPDUOpcode(opcode); std::array frame{}; frame[0U] = modem::TAG_DATA; frame[1U] = static_cast(duid); control.encodeVCH_MACPDU_IEMI(frame.data() + 2U, duid == P25DEF::P2_DUID::FACCH_UNSCRAMBLED); return frame; } TEST_CASE("P25 Phase 2 ignores END_PTT for another call", "[p25][p2][end-ptt]") { p25::phase2::Control control(true, 1U, 20U, 20U, nullptr, nullptr, nullptr, nullptr, nullptr, 4096U, false, false); p25::phase2::Slot& slot = HostTestHooks::p25P2Slot(control, 0U); auto ptt = makeInboundMAC(P25DEF::P2_DUID::FACCH_UNSCRAMBLED, P25DEF::P2_MAC_HEADER_OPCODE::PTT); REQUIRE(control.processFrame(0U, ptt.data(), ptt.size())); auto wrongColor = makeInboundMAC(P25DEF::P2_DUID::FACCH_UNSCRAMBLED, P25DEF::P2_MAC_HEADER_OPCODE::END_PTT, 0xFFFFFFU, 0x2345U, 1U); REQUIRE(control.processFrame(0U, wrongColor.data(), wrongColor.size())); REQUIRE(control.processFrame(0U, wrongColor.data(), wrongColor.size())); REQUIRE(HostTestHooks::p25P2RFVCHState(slot) == p25::phase2::Slot::VCH_STATE::PTT); auto wrongAddress = makeInboundMAC(P25DEF::P2_DUID::FACCH_UNSCRAMBLED, P25DEF::P2_MAC_HEADER_OPCODE::END_PTT, 0xFFFFFFU, 0x3456U); REQUIRE(control.processFrame(0U, wrongAddress.data(), wrongAddress.size())); REQUIRE(control.processFrame(0U, wrongAddress.data(), wrongAddress.size())); REQUIRE(HostTestHooks::p25P2RFVCHState(slot) == p25::phase2::Slot::VCH_STATE::PTT); auto matching = makeInboundMAC(P25DEF::P2_DUID::FACCH_UNSCRAMBLED, P25DEF::P2_MAC_HEADER_OPCODE::END_PTT, 0xFFFFFFU); REQUIRE(control.processFrame(0U, matching.data(), matching.size())); REQUIRE(control.processFrame(0U, matching.data(), matching.size())); REQUIRE(HostTestHooks::p25P2RFVCHState(slot) == p25::phase2::Slot::VCH_STATE::HANGTIME); } /** * @brief Creates a P25 Phase 2 voice burst frame for testing purposes. * @param duid The DUID of the voice burst. * @param ess The encoded speech segment data. * @param essBNo The block number within the ESS. * @return An array representing the voice burst frame. */ std::array makeVoiceBurst( P25DEF::P2_DUID::E duid, const std::array& ess, uint32_t essBNo) { std::array frame{}; frame[0U] = modem::TAG_DATA; frame[1U] = static_cast(duid); uint8_t* burst = frame.data() + 2U; if (duid == P25DEF::P2_DUID::VTCH_4V) { for (uint32_t bit = 0U; bit < 24U; ++bit) WRITE_BIT(burst, 148U + bit, READ_BIT(ess.data(), essBNo * 24U + bit)); } else { for (uint32_t bit = 0U; bit < 168U; ++bit) WRITE_BIT(burst, 148U + bit, READ_BIT(ess.data(), 96U + bit)); } return frame; } /** * @brief Decodes the MAC PDU opcode from a queued P25 Phase 2 frame. * @param frame The queued P25 Phase 2 frame. * @return The decoded MAC PDU opcode. */ uint8_t decodeQueuedMACOpcode(const std::array& frame) { p25::lc::LC decoded; const auto duid = static_cast(frame[1U]); const bool sync = duid == P25DEF::P2_DUID::FACCH_UNSCRAMBLED; REQUIRE(decoded.decodeVCH_MACPDU_OEMI(frame.data() + 2U, sync)); return decoded.getMACPDUOpcode(); } } // namespace // --------------------------------------------------------------------------- // Class Declaration // --------------------------------------------------------------------------- /** * @brief A mock implementation of the P25AffiliationLookup for testing grant behavior. */ class P2GrantLookup final : public p25::lookups::P25AffiliationLookup { public: /** * @brief Initializes a new P2GrantLookup instance. */ P2GrantLookup() : P25AffiliationLookup(nullptr, nullptr, false), grantedDstId(0U), touches(0U), releases(0U) { } /** * @brief Checks if a grant is currently active for the specified destination ID. * @param dstId The destination ID to check. * @return True if the grant is active, false otherwise. */ bool isGranted(uint32_t dstId) const override { return dstId != 0U && dstId == grantedDstId; } /** * @brief Marks the grant for the specified destination ID as touched. * @param dstId The destination ID whose grant is being touched. */ void touchGrant(uint32_t dstId) override { if (isGranted(dstId)) ++touches; } /** * @brief Releases the grant for the specified destination ID. * @param dstId The destination ID whose grant is being released. * @param releaseAll Indicates whether all grants should be released. * @return True if the grant was successfully released, false otherwise. */ bool releaseGrant(uint32_t dstId, bool releaseAll) override { (void)releaseAll; if (!isGranted(dstId)) return false; ++releases; grantedDstId = 0U; return true; } uint32_t grantedDstId; uint32_t touches; uint32_t releases; }; // --------------------------------------------------------------------------- // Class Declaration // --------------------------------------------------------------------------- /** * @brief A loopback port implementation for testing P25 Phase 2 modems. */ class P2LoopbackPort final : public modem::port::IModemPort { public: /** * @brief Opens the loopback port. * @return Always returns true. */ bool open() override { return true; } /** * @brief Reads data from the loopback port. * @param buffer The buffer to store the read data. * @param length The maximum number of bytes to read. * @return Always returns 0, as no data is available. */ int read(uint8_t* buffer, uint32_t length) override { (void)buffer; (void)length; return 0; } /** * @brief Writes data to the loopback port. * @param buffer The buffer containing the data to write. * @param length The number of bytes to write. * @return The number of bytes written. */ int write(const uint8_t* buffer, uint32_t length) override { writes.emplace_back(buffer, buffer + length); return static_cast(length); } /** * @brief Closes the loopback port. */ void close() override { } std::vector> writes; }; // --------------------------------------------------------------------------- // Class Declaration // --------------------------------------------------------------------------- /** * @brief A loopback modem implementation for testing P25 Phase 2 modems. */ class P2LoopbackModem final : public modem::Modem { public: /** * @brief Constructs a loopback modem with the specified loopback port. * @param port The loopback port to use with the modem. */ explicit P2LoopbackModem(P2LoopbackPort* port) : modem::Modem(port, false, false, false, false, false, false, 0U, 0U, 0U, 1024U, 4096U, 1024U, true, true, false, false, false, false) { setModeParams(false, true, false); m_p25P2Capable = true; m_p25P2Space1 = 4096U; m_p25P2Space2 = 4096U; } }; TEST_CASE("P25 Phase 2 control isolates slot queues", "[p25][p2][control]") { p25::phase2::Control control(true, 1U, 1U, 1U, nullptr, nullptr, nullptr, nullptr, nullptr, 4096U, false, false); std::array slot0Frame{}; std::array slot1Frame{}; slot0Frame[0U] = 0x10U; slot1Frame[0U] = 0x20U; REQUIRE(control.processFrame(0U, slot0Frame.data(), slot0Frame.size())); REQUIRE(control.processFrame(1U, slot1Frame.data(), slot1Frame.size())); REQUIRE(control.peekFrameLength(0U) == P25DEF::P25_P2_HOST_FRAME_LENGTH_BYTES); REQUIRE(control.peekFrameLength(1U) == P25DEF::P25_P2_HOST_FRAME_LENGTH_BYTES); std::array output{}; REQUIRE(control.getFrame(0U, output.data()) == P25DEF::P25_P2_HOST_FRAME_LENGTH_BYTES); auto slot0Output = output; REQUIRE(slot0Output[0U] == modem::TAG_DATA); REQUIRE(slot0Output[1U] == P25DEF::P2_DUID::VTCH_4V); REQUIRE(control.getFrame(1U, output.data()) == P25DEF::P25_P2_HOST_FRAME_LENGTH_BYTES); auto slot1Output = output; REQUIRE(slot1Output[0U] == modem::TAG_DATA); REQUIRE(slot1Output[1U] == P25DEF::P2_DUID::VTCH_4V); REQUIRE(slot0Output.size() == slot1Output.size()); REQUIRE(control.getFrame(2U, output.data()) == 0U); } TEST_CASE("P25 Phase 2 RF processing forwards to network and RF", "[p25][p2][repeater]") { p25::phase2::Control control(true, 1000U, 1000U, 1000U, nullptr, nullptr, nullptr, nullptr, nullptr, 4096U, false, false); std::array frame{}; frame[0U] = 0x5AU; REQUIRE(control.processFrame(1U, frame.data(), frame.size())); REQUIRE(HostTestHooks::p25P2RFState(HostTestHooks::p25P2Slot(control, 1U)) == RS_RF_LATE_ENTRY); REQUIRE(control.peekFrameLength(1U) == P25DEF::P25_P2_HOST_FRAME_LENGTH_BYTES); } TEST_CASE("P25 Phase 2 rejects invalid frame lengths", "[p25][p2][validation]") { p25::phase2::Control control(true, 1000U, 1000U, 1000U, nullptr, nullptr, nullptr, nullptr, nullptr, 4096U, false, false); std::array frame{}; REQUIRE_FALSE(control.processFrame(0U, frame.data(), frame.size())); REQUIRE_FALSE(control.processFrame(0U, frame.data(), frame.size())); REQUIRE(control.peekFrameLength(0U) == 0U); REQUIRE(control.isQueueFull(2U)); } TEST_CASE("P25 Phase 2 slot returns to listening after RF timeout", "[p25][p2][lifecycle]") { p25::phase2::Control control(true, 1U, 1U, 1U, nullptr, nullptr, nullptr, nullptr, nullptr, 4096U, false, false); std::array frame{}; REQUIRE(control.processFrame(0U, frame.data(), frame.size())); REQUIRE(HostTestHooks::p25P2RFState(HostTestHooks::p25P2Slot(control, 0U)) == RS_RF_LATE_ENTRY); control.clock(1001U); REQUIRE(HostTestHooks::p25P2RFState(HostTestHooks::p25P2Slot(control, 0U)) == RS_RF_LISTENING); REQUIRE(HostTestHooks::p25P2RFState(HostTestHooks::p25P2Slot(control, 0U)) == RS_RF_LISTENING); } TEST_CASE("P25 Phase 2 RF processing populates slot call state", "[p25][p2][rf][metadata]") { p25::phase2::Control control(true, 1U, 1U, 1U, nullptr, nullptr, nullptr, nullptr, nullptr, 4096U, false, false); std::array frame{}; REQUIRE(control.processFrame(1U, frame.data(), frame.size())); REQUIRE(HostTestHooks::p25P2LastDstId(HostTestHooks::p25P2Slot(control, 1U)) == 0U); REQUIRE(HostTestHooks::p25P2LastSrcId(HostTestHooks::p25P2Slot(control, 1U)) == 0U); REQUIRE(HostTestHooks::p25P2RFState(HostTestHooks::p25P2Slot(control, 1U)) == RS_RF_LATE_ENTRY); } TEST_CASE("P25 Phase 2 tracks burst scrambler offsets", "[p25][p2][scrambler]") { p25::phase2::Control control(true, 1000U, 1000U, 1000U, nullptr, nullptr, nullptr, nullptr, nullptr, 4096U, false, false); std::array frame{}; REQUIRE(control.processFrame(0U, frame.data(), frame.size())); REQUIRE(HostTestHooks::p25P2RFScrambleOffset(HostTestHooks::p25P2Slot(control, 0U)) == 0U); REQUIRE(control.processFrame(0U, frame.data(), frame.size())); REQUIRE(HostTestHooks::p25P2RFScrambleOffset(HostTestHooks::p25P2Slot(control, 0U)) == P25DEF::P25_P2_BURST_LENGTH_BITS * 2U); REQUIRE(control.processFrame(1U, frame.data(), frame.size())); REQUIRE(HostTestHooks::p25P2RFScrambleOffset(HostTestHooks::p25P2Slot(control, 1U)) == P25DEF::P25_P2_BURST_LENGTH_BITS); control.reset(); REQUIRE(HostTestHooks::p25P2Slot(control, 0U).processNetwork(frame.data(), frame.size(), p25::lc::LC(), P25DEF::P2_DUID::VTCH_4V, P25DEF::P25_P2_BURST_LENGTH_BITS / 2U, 0U)); REQUIRE(HostTestHooks::p25P2NetScrambleOffset(HostTestHooks::p25P2Slot(control, 0U)) == P25DEF::P25_P2_BURST_LENGTH_BITS / 2U); } TEST_CASE("P25 Phase 2 tagged modem frame decodes FACCH PTT before dispatch", "[p25][p2][vch]") { p25::phase2::Control control(true, 2U, 10U, 2U, nullptr, nullptr, nullptr, nullptr, nullptr, 4096U, false, false); p25::lc::LC mac; mac.setP2DUID(P25DEF::P2_DUID::FACCH_UNSCRAMBLED); mac.setMACPDUOpcode(P25DEF::P2_MAC_HEADER_OPCODE::PTT); mac.setSrcId(0x123456U); mac.setDstId(0x2345U); std::array frame{}; frame[0U] = modem::TAG_DATA; frame[1U] = P25DEF::P2_DUID::FACCH_UNSCRAMBLED; mac.encodeVCH_MACPDU_IEMI(frame.data() + 2U, true); REQUIRE(control.processFrame(0U, frame.data(), frame.size())); const p25::phase2::Slot& slot = HostTestHooks::p25P2Slot(control, 0U); REQUIRE(HostTestHooks::p25P2DUID(slot) == P25DEF::P2_DUID::FACCH_UNSCRAMBLED); REQUIRE(HostTestHooks::p25P2RFVCHState(slot) == p25::phase2::Slot::VCH_STATE::PTT); REQUIRE(HostTestHooks::p25P2RFPTTCount(slot) == 1U); std::array output{}; bool imm = false; REQUIRE(control.getFrame(0U, output.data(), &imm) == output.size()); REQUIRE(imm); REQUIRE(output[0U] == modem::TAG_DATA); REQUIRE(output[1U] == P25DEF::P2_DUID::FACCH_UNSCRAMBLED); p25::lc::LC decoded; REQUIRE(decoded.decodeVCH_MACPDU_OEMI(output.data() + 2U, true)); REQUIRE(decoded.getMACPDUOpcode() == P25DEF::P2_MAC_HEADER_OPCODE::PTT); } TEST_CASE("P25 Phase 2 VCH processor rejects typed LCCH bursts", "[p25][p2][lcch]") { p25::phase2::Control control(true, 2U, 10U, 2U, nullptr, nullptr, nullptr, nullptr, nullptr, 4096U, false, false); std::array frame{}; frame[0U] = modem::TAG_DATA; frame[1U] = P25DEF::P2_DUID::LCCH_UNSCRAMBLED; REQUIRE_FALSE(control.processFrame(0U, frame.data(), frame.size())); REQUIRE(control.peekFrameLength(0U) == 0U); } TEST_CASE("P25 Phase 2 lost modem frame terminates the active slot", "[p25][p2][loss]") { p25::phase2::Control control(true, 0U, 10U, 2U, nullptr, nullptr, nullptr, nullptr, nullptr, 4096U, false, false); auto ptt = makeInboundMAC(P25DEF::P2_DUID::FACCH_UNSCRAMBLED, P25DEF::P2_MAC_HEADER_OPCODE::PTT); REQUIRE(control.processFrame(1U, ptt.data(), ptt.size())); std::array voice{}; REQUIRE(control.processFrame(1U, voice.data(), voice.size())); uint8_t lost = modem::TAG_LOST; REQUIRE_FALSE(control.processFrame(1U, &lost, 1U)); control.clock(1501U); REQUIRE(HostTestHooks::p25P2RFVCHState(HostTestHooks::p25P2Slot(control, 1U)) == p25::phase2::Slot::VCH_STATE::TERMINATING); } TEST_CASE("P25 Phase 2 public state management covers late entry collision and reset", "[p25][p2][state]") { p25::phase2::Control control(true, 0U, 20U, 20U, nullptr, nullptr, nullptr, nullptr, nullptr, 4096U, false, false); p25::phase2::Slot& slot = control.slot(0U); REQUIRE(control.getRFState(0U) == RS_RF_LISTENING); REQUIRE(control.getNetState(0U) == RS_NET_IDLE); REQUIRE(control.getRFState(2U) == RS_RF_INVALID); REQUIRE_FALSE(control.isBusy()); std::array voice{}; voice[0U] = modem::TAG_DATA; voice[1U] = P25DEF::P2_DUID::VTCH_4V; REQUIRE(control.processFrame(0U, voice.data(), voice.size())); REQUIRE(control.getRFState(0U) == RS_RF_LATE_ENTRY); REQUIRE(control.isBusy()); auto active = makeInboundMAC(P25DEF::P2_DUID::SACCH_UNSCRAMBLED, P25DEF::P2_MAC_HEADER_OPCODE::ACTIVE); REQUIRE(control.processFrame(0U, active.data(), active.size())); REQUIRE(control.getRFState(0U) == RS_RF_AUDIO); REQUIRE(HostTestHooks::p25P2RFVCHState(slot) == p25::phase2::Slot::VCH_STATE::ACTIVE); control.reset(); REQUIRE(control.getRFState(0U) == RS_RF_LISTENING); REQUIRE(control.getNetState(0U) == RS_NET_IDLE); REQUIRE_FALSE(control.isBusy()); p25::lc::LC call; call.setSrcId(0x123456U); call.setDstId(0x2345U); std::array networkVoice{}; REQUIRE(slot.processNetwork(networkVoice.data(), networkVoice.size(), call, P25DEF::P2_DUID::VTCH_4V, 0U, 0U)); REQUIRE(control.getNetState(0U) == RS_NET_AUDIO); auto ptt = makeInboundMAC(P25DEF::P2_DUID::FACCH_UNSCRAMBLED, P25DEF::P2_MAC_HEADER_OPCODE::PTT); REQUIRE_FALSE(control.processFrame(0U, ptt.data(), ptt.size())); REQUIRE(control.getRFState(0U) == RS_RF_LISTENING); REQUIRE(control.getNetState(0U) == RS_NET_AUDIO); control.clock(1501U); std::array queued{}; while (control.getFrame(0U, queued.data()) != 0U) { } REQUIRE(control.getNetState(0U) == RS_NET_IDLE); REQUIRE(control.getRFState(0U) == RS_RF_LISTENING); REQUIRE_FALSE(control.isBusy()); } TEST_CASE("P25 Phase 2 non-authoritative RF state requires a permitted destination", "[p25][p2][state][authoritative]") { p25::phase2::Control control(false, 0U, 20U, 20U, nullptr, nullptr, nullptr, nullptr, nullptr, 4096U, false, false); auto ptt = makeInboundMAC(P25DEF::P2_DUID::FACCH_UNSCRAMBLED, P25DEF::P2_MAC_HEADER_OPCODE::PTT); REQUIRE_FALSE(control.processFrame(0U, ptt.data(), ptt.size())); REQUIRE(control.getRFState(0U) == RS_RF_LISTENING); REQUIRE_FALSE(control.isBusy()); control.slot(0U).permittedTG(0x2345U); REQUIRE(control.processFrame(0U, ptt.data(), ptt.size())); REQUIRE(control.getRFState(0U) == RS_RF_AUDIO); REQUIRE(control.isBusy()); } TEST_CASE("P25 Phase 2 rejected RF state is explicitly recoverable", "[p25][p2][state][rejection]") { p25::phase2::Control control(true, 0U, 20U, 20U, nullptr, nullptr, nullptr, nullptr, nullptr, 4096U, false, false); p25::phase2::Slot& slot = control.slot(1U); HostTestHooks::p25P2SetRFState(slot, RS_RF_REJECTED); REQUIRE(control.getRFState(1U) == RS_RF_REJECTED); REQUIRE(control.isBusy()); control.clearRFReject(1U); REQUIRE(control.getRFState(1U) == RS_RF_LISTENING); REQUIRE_FALSE(control.isBusy()); } TEST_CASE("P25 Phase 2 network timeout is clocked once and honors authorization", "[p25][p2][state][network][timeout]") { p25::phase2::Control control(false, 0U, 2U, 20U, nullptr, nullptr, nullptr, nullptr, nullptr, 4096U, false, false); p25::phase2::Slot& slot = control.slot(0U); p25::lc::LC call; call.setSrcId(0x123456U); call.setDstId(0x2345U); std::array voice{}; REQUIRE_FALSE(slot.processNetwork(voice.data(), voice.size(), call, P25DEF::P2_DUID::VTCH_4V, 0U, 0U)); REQUIRE(control.getNetState(0U) == RS_NET_IDLE); slot.permittedTG(0x2345U); REQUIRE(slot.processNetwork(voice.data(), voice.size(), call, P25DEF::P2_DUID::VTCH_4V, 0U, 0U)); REQUIRE(control.getNetState(0U) == RS_NET_AUDIO); REQUIRE(HostTestHooks::p25P2NetVCHState(slot) == p25::phase2::Slot::VCH_STATE::IDLE); control.clock(1001U); REQUIRE(control.getNetState(0U) == RS_NET_AUDIO); REQUIRE(HostTestHooks::p25P2NetVCHState(slot) != p25::phase2::Slot::VCH_STATE::TERMINATING); } TEST_CASE("P25 Phase 2 permits are slot specific", "[p25][p2][permit][authoritative]") { p25::phase2::Control control(false, 0U, 20U, 20U, nullptr, nullptr, nullptr, nullptr, nullptr, 4096U, false, false); auto ptt = makeInboundMAC(P25DEF::P2_DUID::FACCH_UNSCRAMBLED, P25DEF::P2_MAC_HEADER_OPCODE::PTT); control.permittedTG(0x2345U, 0U); REQUIRE(control.processFrame(0U, ptt.data(), ptt.size())); REQUIRE_FALSE(control.processFrame(1U, ptt.data(), ptt.size())); REQUIRE(control.getRFState(0U) == RS_RF_AUDIO); REQUIRE(control.getRFState(1U) == RS_RF_LISTENING); } TEST_CASE("P25 Phase 2 accepted traffic touches and releases its CC grant", "[p25][p2][grant]") { P2GrantLookup grants; grants.grantedDstId = 0x2345U; p25::phase2::Control control(true, 0U, 20U, 20U, nullptr, nullptr, &grants, nullptr, nullptr, 4096U, false, false); auto ptt = makeInboundMAC(P25DEF::P2_DUID::FACCH_UNSCRAMBLED, P25DEF::P2_MAC_HEADER_OPCODE::PTT); REQUIRE(control.processFrame(0U, ptt.data(), ptt.size())); REQUIRE(grants.touches == 1U); std::array voice{}; voice[0U] = modem::TAG_DATA; voice[1U] = P25DEF::P2_DUID::VTCH_4V; REQUIRE(control.processFrame(0U, voice.data(), voice.size())); REQUIRE(grants.touches == 2U); control.touchGrantTG(0x2345U, 0U); REQUIRE(grants.touches == 3U); control.releaseGrantTG(0x2345U, 0U); REQUIRE(grants.releases == 1U); REQUIRE(grants.grantedDstId == 0U); } TEST_CASE("P25 Phase 2 RF call establishment enforces RID and TGID ACLs", "[p25][p2][acl]") { ::lookups::RadioIdLookup ridLookup("", 0U, true, false); ::lookups::TalkgroupRulesLookup tidLookup("", 0U, true, false); ridLookup.addEntry(0x123456U, true, "allowed source"); tidLookup.addEntry(0x2345U, 0U, true); p25::phase2::Control control(true, 0U, 20U, 20U, nullptr, nullptr, nullptr, &ridLookup, &tidLookup, 4096U, false, false); auto groupPTT = makeInboundMAC(P25DEF::P2_DUID::FACCH_UNSCRAMBLED, P25DEF::P2_MAC_HEADER_OPCODE::PTT); REQUIRE(control.processFrame(0U, groupPTT.data(), groupPTT.size())); REQUIRE(control.getRFState(0U) == RS_RF_AUDIO); control.reset(); auto deniedSource = makeInboundMAC(P25DEF::P2_DUID::FACCH_UNSCRAMBLED, P25DEF::P2_MAC_HEADER_OPCODE::PTT, 0x654321U, 0x2345U); REQUIRE_FALSE(control.processFrame(0U, deniedSource.data(), deniedSource.size())); REQUIRE(control.getRFState(0U) == RS_RF_REJECTED); control.clearRFReject(0U); auto deniedTG = makeInboundMAC(P25DEF::P2_DUID::FACCH_UNSCRAMBLED, P25DEF::P2_MAC_HEADER_OPCODE::PTT, 0x123456U, 0x3456U); REQUIRE_FALSE(control.processFrame(0U, deniedTG.data(), deniedTG.size())); REQUIRE(control.getRFState(0U) == RS_RF_REJECTED); control.clearRFReject(0U); p25::lc::LC privateCall; privateCall.setGroup(false); privateCall.setMACPartition(P25DEF::P2_MAC_MCO_PARTITION::UNIQUE); privateCall.setLCO(P25DEF::P2_MAC_MCO::PRIVATE); privateCall.setSrcId(0x123456U); privateCall.setDstId(0x456789U); privateCall.setP2DUID(P25DEF::P2_DUID::SACCH_UNSCRAMBLED); privateCall.setMACPDUOpcode(P25DEF::P2_MAC_HEADER_OPCODE::ACTIVE); std::array privatePTT{}; privatePTT[0U] = modem::TAG_DATA; privatePTT[1U] = P25DEF::P2_DUID::SACCH_UNSCRAMBLED; privateCall.encodeVCH_MACPDU_IEMI(privatePTT.data() + 2U, false); std::array lateVoice{}; lateVoice[0U] = modem::TAG_DATA; lateVoice[1U] = P25DEF::P2_DUID::VTCH_4V; REQUIRE(control.processFrame(0U, lateVoice.data(), lateVoice.size())); REQUIRE_FALSE(control.processFrame(0U, privatePTT.data(), privatePTT.size())); REQUIRE(control.getRFState(0U) == RS_RF_REJECTED); control.clearRFReject(0U); ridLookup.addEntry(0x456789U, true, "allowed target"); REQUIRE(control.processFrame(0U, lateVoice.data(), lateVoice.size())); REQUIRE(control.processFrame(0U, privatePTT.data(), privatePTT.size())); REQUIRE(control.getRFState(0U) == RS_RF_AUDIO); } TEST_CASE("P25 Phase 2 host modem logical loopback preserves both slots", "[p25][p2][e2e][modem]") { p25::phase2::Control transmitter(true, 1000U, 1000U, 1000U, nullptr, nullptr, nullptr, nullptr, nullptr, 4096U, false, false); p25::phase2::Control receiver(true, 1000U, 1000U, 1000U, nullptr, nullptr, nullptr, nullptr, nullptr, 4096U, false, false); auto* port = new P2LoopbackPort(); P2LoopbackModem airModem(port); for (uint32_t slotNo = 0U; slotNo < p25::phase2::Control::SLOT_COUNT; ++slotNo) { std::array input{}; input[0U] = modem::TAG_DATA; input[1U] = P25DEF::P2_DUID::VTCH_4V; input[2U] = static_cast(0x20U + slotNo); REQUIRE(transmitter.processFrame(slotNo, input.data(), input.size())); std::array hostFrame{}; REQUIRE(transmitter.getFrame(slotNo, hostFrame.data()) == hostFrame.size()); REQUIRE(hostFrame[0U] == modem::TAG_DATA); REQUIRE(hostFrame[1U] == P25DEF::P2_DUID::VTCH_4V); const bool written = slotNo == 0U ? airModem.writeP25P2Frame1(hostFrame.data(), hostFrame.size()) : airModem.writeP25P2Frame2(hostFrame.data(), hostFrame.size()); REQUIRE(written); REQUIRE(port->writes.size() == slotNo + 1U); const std::vector& serialFrame = port->writes.back(); REQUIRE(serialFrame.size() == P25DEF::P25_P2_FRAME_LENGTH_BYTES + 4U); REQUIRE(serialFrame[0U] == modem::DVM_SHORT_FRAME_START); REQUIRE(serialFrame[1U] == serialFrame.size()); REQUIRE(serialFrame[2U] == (slotNo == 0U ? modem::CMD_P25_P2_DATA1 : modem::CMD_P25_P2_DATA2)); REQUIRE(serialFrame[3U] == P25DEF::P2_DUID::VTCH_4V); REQUIRE(std::memcmp(serialFrame.data() + 4U, hostFrame.data() + 2U, P25DEF::P25_P2_FRAME_LENGTH_BYTES) == 0); std::array received{}; received[0U] = modem::TAG_DATA; std::memcpy(received.data() + 1U, serialFrame.data() + 3U, serialFrame.size() - 3U); REQUIRE(receiver.processFrame(slotNo, received.data(), received.size())); std::array repeated{}; REQUIRE(receiver.getFrame(slotNo, repeated.data()) == repeated.size()); REQUIRE(repeated == hostFrame); REQUIRE(receiver.peekFrameLength(1U - slotNo) == 0U); } } TEST_CASE("P25 Phase 2 deterministic full RF call lifecycle", "[p25][p2][e2e][call]") { p25::phase2::Control control(true, 1U, 20U, 20U, nullptr, nullptr, nullptr, nullptr, nullptr, 8192U, false, false); p25::phase2::Slot& slot = HostTestHooks::p25P2Slot(control, 0U); std::array queued{}; bool immediate = false; auto ptt = makeInboundMAC(P25DEF::P2_DUID::FACCH_UNSCRAMBLED, P25DEF::P2_MAC_HEADER_OPCODE::PTT); REQUIRE(control.processFrame(0U, ptt.data(), ptt.size())); REQUIRE(HostTestHooks::p25P2RFVCHState(slot) == p25::phase2::Slot::VCH_STATE::PTT); REQUIRE(control.getFrame(0U, queued.data(), &immediate) == queued.size()); REQUIRE(immediate); REQUIRE(queued[1U] == P25DEF::P2_DUID::FACCH_UNSCRAMBLED); REQUIRE(decodeQueuedMACOpcode(queued) == P25DEF::P2_MAC_HEADER_OPCODE::PTT); std::array ess{}; ess[0U] = 0x80U; ess[1U] = 0x12U; ess[2U] = 0x34U; for (uint32_t i = 0U; i < 9U; ++i) ess[3U + i] = static_cast(0xA0U + i); edac::RS634717().encode441629(ess.data()); auto active = makeInboundMAC(P25DEF::P2_DUID::SACCH_UNSCRAMBLED, P25DEF::P2_MAC_HEADER_OPCODE::ACTIVE); p25::lc::LC decodedActive; REQUIRE(decodedActive.decodeVCH_MACPDU_IEMI(active.data() + 2U, false)); REQUIRE(decodedActive.getP2DUID() == P25DEF::P2_DUID::SACCH_UNSCRAMBLED); REQUIRE(decodedActive.getMACPDUOpcode() == P25DEF::P2_MAC_HEADER_OPCODE::ACTIVE); REQUIRE(decodedActive.getLCO() == P25DEF::P2_MAC_MCO::GROUP); REQUIRE(p25::lc::mac::MACFactory::createMACPDU(decodedActive) != nullptr); for (uint32_t essBNo = 0U; essBNo < 4U; ++essBNo) { auto voice4V = makeVoiceBurst(P25DEF::P2_DUID::VTCH_4V, ess, essBNo); REQUIRE(control.processFrame(0U, voice4V.data(), voice4V.size())); REQUIRE(control.processFrame(0U, active.data(), active.size())); REQUIRE(HostTestHooks::p25P2RFVCHState(slot) == p25::phase2::Slot::VCH_STATE::ACTIVE); auto voice2V = makeVoiceBurst(P25DEF::P2_DUID::VTCH_2V, ess, essBNo); REQUIRE(control.processFrame(0U, voice2V.data(), voice2V.size())); } REQUIRE(HostTestHooks::p25P2ESSComplete(slot)); REQUIRE(HostTestHooks::p25P2ESSAlgId(slot) == 0x80U); REQUIRE(HostTestHooks::p25P2ESSKeyId(slot) == 0x1234U); REQUIRE(HostTestHooks::p25P2RFFrames(slot) == 8U); REQUIRE(HostTestHooks::p25P2RFBits(slot) == 1729U); REQUIRE(HostTestHooks::p25P2RFErrs(slot) > 0U); REQUIRE(HostTestHooks::p25P2RFErrs(slot) <= HostTestHooks::p25P2RFBits(slot)); auto endPTT = makeInboundMAC(P25DEF::P2_DUID::FACCH_UNSCRAMBLED, P25DEF::P2_MAC_HEADER_OPCODE::END_PTT); REQUIRE(control.processFrame(0U, endPTT.data(), endPTT.size())); REQUIRE(HostTestHooks::p25P2RFVCHState(slot) == p25::phase2::Slot::VCH_STATE::ACTIVE); REQUIRE(control.processFrame(0U, endPTT.data(), endPTT.size())); REQUIRE(HostTestHooks::p25P2RFVCHState(slot) == p25::phase2::Slot::VCH_STATE::HANGTIME); REQUIRE(control.getFrame(0U, queued.data(), &immediate) == queued.size()); REQUIRE(immediate); REQUIRE(queued[1U] == P25DEF::P2_DUID::SACCH_UNSCRAMBLED); REQUIRE(decodeQueuedMACOpcode(queued) == P25DEF::P2_MAC_HEADER_OPCODE::HANGTIME); control.clock(360U); control.clock(641U); REQUIRE(HostTestHooks::p25P2RFVCHState(slot) == p25::phase2::Slot::VCH_STATE::TERMINATING); for (uint32_t i = 0U; i < 2U; ++i) { REQUIRE(control.getFrame(0U, queued.data(), &immediate) == queued.size()); REQUIRE(immediate); REQUIRE(queued[1U] == P25DEF::P2_DUID::FACCH_UNSCRAMBLED); REQUIRE(decodeQueuedMACOpcode(queued) == P25DEF::P2_MAC_HEADER_OPCODE::END_PTT); } bool saw4V = false; bool saw2V = false; uint32_t sacchCount = 0U; while (control.getFrame(0U, queued.data(), &immediate) != 0U) { REQUIRE_FALSE(immediate); saw4V |= queued[1U] == P25DEF::P2_DUID::VTCH_4V; saw2V |= queued[1U] == P25DEF::P2_DUID::VTCH_2V; if (queued[1U] == P25DEF::P2_DUID::SACCH_UNSCRAMBLED) ++sacchCount; } REQUIRE(saw4V); REQUIRE(saw2V); // Four inbound ACTIVE SACCH bursts are repeated, and the 1,001 ms // hangtime interval inserts two additional 360 ms cadence SACCH bursts. REQUIRE(sacchCount == 6U); REQUIRE(HostTestHooks::p25P2RFVCHState(slot) == p25::phase2::Slot::VCH_STATE::IDLE); REQUIRE(HostTestHooks::p25P2RFState(slot) == RS_RF_LISTENING); REQUIRE(HostTestHooks::p25P2RFFrames(slot) == 0U); REQUIRE(HostTestHooks::p25P2RFBits(slot) == 1U); REQUIRE(HostTestHooks::p25P2RFErrs(slot) == 0U); } TEST_CASE("P25 Phase 2 slot accounts network frames loss misses and BER", "[p25][p2][statistics]") { p25::phase2::Control control(true, 1U, 20U, 20U, nullptr, nullptr, nullptr, nullptr, nullptr, 4096U, false, false); p25::phase2::Slot& slot = HostTestHooks::p25P2Slot(control, 0U); p25::lc::LC call; call.setSrcId(0x123456U); call.setDstId(0x2345U); std::array voice4V{}; voice4V[0U] = 0x20U; // one error in the first outbound AMBE+2 codeword REQUIRE(slot.processNetwork(voice4V.data(), voice4V.size(), call, P25DEF::P2_DUID::VTCH_4V, 0U, 0U)); REQUIRE(HostTestHooks::p25P2NetFrames(slot) == 1U); REQUIRE(HostTestHooks::p25P2NetBits(slot) == 289U); REQUIRE(HostTestHooks::p25P2NetErrs(slot) > 0U); std::array voice2V{}; REQUIRE(slot.processNetwork(voice2V.data(), voice2V.size(), call, P25DEF::P2_DUID::VTCH_2V, 0U, 0U)); REQUIRE(HostTestHooks::p25P2NetFrames(slot) == 2U); REQUIRE(HostTestHooks::p25P2NetBits(slot) == 433U); p25::lc::LC competing(call); competing.setDstId(0x3456U); REQUIRE_FALSE(slot.processNetwork(voice4V.data(), voice4V.size(), competing, P25DEF::P2_DUID::VTCH_4V, 0U, 0U)); REQUIRE(HostTestHooks::p25P2NetMissed(slot) == 1U); control.clock(1501U); REQUIRE(HostTestHooks::p25P2NetLost(slot) == 1U); slot.reset(); REQUIRE(HostTestHooks::p25P2NetFrames(slot) == 0U); REQUIRE(HostTestHooks::p25P2NetLost(slot) == 0U); REQUIRE(HostTestHooks::p25P2NetMissed(slot) == 0U); REQUIRE(HostTestHooks::p25P2NetBits(slot) == 1U); REQUIRE(HostTestHooks::p25P2NetErrs(slot) == 0U); } TEST_CASE("P25 Phase 2 deterministic call recovers from missing termination", "[p25][p2][e2e][call][loss]") { p25::phase2::Control control(true, 1U, 20U, 20U, nullptr, nullptr, nullptr, nullptr, nullptr, 4096U, false, false); p25::phase2::Slot& slot = HostTestHooks::p25P2Slot(control, 1U); auto ptt = makeInboundMAC(P25DEF::P2_DUID::FACCH_UNSCRAMBLED, P25DEF::P2_MAC_HEADER_OPCODE::PTT); REQUIRE(control.processFrame(1U, ptt.data(), ptt.size())); std::array queued{}; bool immediate = false; REQUIRE(control.getFrame(1U, queued.data(), &immediate) == queued.size()); REQUIRE(immediate); REQUIRE(decodeQueuedMACOpcode(queued) == P25DEF::P2_MAC_HEADER_OPCODE::PTT); std::array emptyESS{}; auto voice = makeVoiceBurst(P25DEF::P2_DUID::VTCH_4V, emptyESS, 0U); REQUIRE(control.processFrame(1U, voice.data(), voice.size())); uint8_t lost = modem::TAG_LOST; REQUIRE_FALSE(control.processFrame(1U, &lost, 1U)); control.clock(1501U); REQUIRE(HostTestHooks::p25P2RFVCHState(slot) == p25::phase2::Slot::VCH_STATE::TERMINATING); for (uint32_t i = 0U; i < 2U; ++i) { REQUIRE(control.getFrame(1U, queued.data(), &immediate) == queued.size()); REQUIRE(immediate); REQUIRE(decodeQueuedMACOpcode(queued) == P25DEF::P2_MAC_HEADER_OPCODE::END_PTT); } while (control.getFrame(1U, queued.data(), &immediate) != 0U) { } REQUIRE(HostTestHooks::p25P2RFVCHState(slot) == p25::phase2::Slot::VCH_STATE::IDLE); REQUIRE(HostTestHooks::p25P2RFState(slot) == RS_RF_LISTENING); }