From d651352a18c8016385a7d964de32b4dbf4115859 Mon Sep 17 00:00:00 2001 From: Michael Pham <61564344+Mikefly123@users.noreply.github.com> Date: Mon, 13 Jul 2026 01:56:32 -0700 Subject: [PATCH 1/8] feat(sband): add SBandFaultPolicy pure-logic fault state machine, TDD Implements decision D3 from S-BAND-REINTEGRATION-PLAN.md: N=5 consecutive radio-operation failures request one nRST reset; M=3 resets without an intervening success latch FAULTED; ground can re-arm from FAULTED via groundResetRequested(). Host-compilable pure class (no Zephyr/F' deps), five RED->GREEN behaviors pinned in test_SBand_FaultPolicy.cpp. The SBand component will wire this in as a thin adapter in a follow-up commit. Co-Authored-By: Claude Fable 5 --- .../Components/SBand/SBandFaultPolicy.cpp | 60 ++++++++++ .../Components/SBand/SBandFaultPolicy.hpp | 82 +++++++++++++ .../test/unit-tests/CMakeLists.txt | 9 ++ .../unit-tests/test_SBand_FaultPolicy.cpp | 113 ++++++++++++++++++ 4 files changed, 264 insertions(+) create mode 100644 PROVESFlightControllerReference/Components/SBand/SBandFaultPolicy.cpp create mode 100644 PROVESFlightControllerReference/Components/SBand/SBandFaultPolicy.hpp create mode 100644 PROVESFlightControllerReference/test/unit-tests/test_SBand_FaultPolicy.cpp diff --git a/PROVESFlightControllerReference/Components/SBand/SBandFaultPolicy.cpp b/PROVESFlightControllerReference/Components/SBand/SBandFaultPolicy.cpp new file mode 100644 index 00000000..4981a78d --- /dev/null +++ b/PROVESFlightControllerReference/Components/SBand/SBandFaultPolicy.cpp @@ -0,0 +1,60 @@ +// ====================================================================== +// \title SBandFaultPolicy.cpp +// \brief cpp file for SBandFaultPolicy pure-logic class +// ====================================================================== + +#include "PROVESFlightControllerReference/Components/SBand/SBandFaultPolicy.hpp" + +namespace Components { + +SBandFaultPolicy::SBandFaultPolicy() + : m_consecutiveFailures(0), m_resetsSinceSuccess(0), m_resetPending(false), m_faulted(false) {} + +void SBandFaultPolicy::operationSucceeded() { + if (m_faulted) { + return; + } + m_consecutiveFailures = 0; + m_resetsSinceSuccess = 0; +} + +void SBandFaultPolicy::operationFailed() { + if (m_faulted) { + return; + } + m_consecutiveFailures++; + if ((m_consecutiveFailures >= CONSECUTIVE_FAILURE_LIMIT) && !m_resetPending) { + m_resetPending = true; + } +} + +void SBandFaultPolicy::resetCompleted() { + if (m_faulted || !m_resetPending) { + return; + } + m_resetPending = false; + m_consecutiveFailures = 0; + m_resetsSinceSuccess++; + if (m_resetsSinceSuccess >= RESET_ATTEMPT_LIMIT) { + m_faulted = true; + } +} + +void SBandFaultPolicy::groundResetRequested() { + m_faulted = false; + m_consecutiveFailures = 0; + m_resetsSinceSuccess = 0; + m_resetPending = false; +} + +SBandFaultPolicy::Decision SBandFaultPolicy::decision() const { + if (m_faulted) { + return Decision::FAULTED; + } + if (m_resetPending) { + return Decision::REQUEST_RESET; + } + return Decision::NONE; +} + +} // namespace Components diff --git a/PROVESFlightControllerReference/Components/SBand/SBandFaultPolicy.hpp b/PROVESFlightControllerReference/Components/SBand/SBandFaultPolicy.hpp new file mode 100644 index 00000000..24d4ad96 --- /dev/null +++ b/PROVESFlightControllerReference/Components/SBand/SBandFaultPolicy.hpp @@ -0,0 +1,82 @@ +// ====================================================================== +// \title SBandFaultPolicy.hpp +// \brief hpp file for SBandFaultPolicy pure-logic class +// ====================================================================== + +#pragma once + +#include + +namespace Components { + +//! Pure logic core implementing the S-Band fault-management state machine +//! (S-BAND-REINTEGRATION-PLAN.md decision D3). Host-compilable: no Zephyr or +//! F Prime autocode dependencies, matching the pattern used by +//! DetumbleManager::BDot and StackMonitorCore. All state is fixed-size; +//! zero heap allocation. +//! +//! Invariant: S-Band failure degrades to "no S-Band", never to backpressure, +//! hang, or spacecraft reset. The component feeds this class the outcome of +//! every radio operation and queries decision() to learn what, if anything, +//! it should do in response. +class SBandFaultPolicy { + public: + //! Number of consecutive operation failures that triggers a nRST reset + //! request. Tunable later; chosen to tolerate a handful of transient + //! SPI/RF glitches without masking a real fault. + static constexpr std::uint32_t CONSECUTIVE_FAILURE_LIMIT = 5; // N + + //! Number of resets performed without an intervening success that + //! latches FAULTED. Tunable later; chosen so a truly wedged radio stops + //! consuming reset attempts (and the nRST line) after a bounded number + //! of tries. + static constexpr std::uint32_t RESET_ATTEMPT_LIMIT = 3; // M + + //! Decision the component should act on, as of the most recent event. + enum class Decision { + NONE, //!< Nothing to do; keep operating normally. + REQUEST_RESET, //!< Consecutive failures crossed the limit; perform a nRST reset. + FAULTED //!< Latched: stop all radio-interface calls until ground intervenes. + }; + + //! Construct a fresh, healthy policy. + SBandFaultPolicy(); + + //! Report that a radio operation succeeded. Clears the consecutive- + //! failure count and the reset-without-success count. No-op once + //! latched FAULTED. + void operationSucceeded(); + + //! Report that a radio operation failed. Advances the consecutive- + //! failure count; once it reaches CONSECUTIVE_FAILURE_LIMIT, decision() + //! reports REQUEST_RESET until resetCompleted() is called (further + //! failures in the meantime do not issue additional requests). No-op + //! once latched FAULTED. + void operationFailed(); + + //! Report that the component finished a nRST reset attempt (regardless + //! of whether the following re-init succeeds -- that outcome arrives via + //! a later operationSucceeded()/operationFailed()). Clears the pending + //! reset request and the consecutive-failure count, and advances the + //! reset-without-success count; latches FAULTED once that count reaches + //! RESET_ATTEMPT_LIMIT. No-op once latched FAULTED. + void resetCompleted(); + + //! Ground has requested a reset (e.g. the RESET_RADIO command). Re-arms + //! the policy to a clean state -- including clearing a FAULTED latch -- + //! so a re-init can be attempted. Unconditional: safe to call even when + //! not FAULTED. + void groundResetRequested(); + + //! The current decision. Cheap, side-effect-free; safe to query as + //! often as needed. + Decision decision() const; + + private: + std::uint32_t m_consecutiveFailures; + std::uint32_t m_resetsSinceSuccess; + bool m_resetPending; //!< A REQUEST_RESET has been issued and not yet completed. + bool m_faulted; //!< Latched fault state. +}; + +} // namespace Components diff --git a/PROVESFlightControllerReference/test/unit-tests/CMakeLists.txt b/PROVESFlightControllerReference/test/unit-tests/CMakeLists.txt index 4302a604..b0e64f51 100644 --- a/PROVESFlightControllerReference/test/unit-tests/CMakeLists.txt +++ b/PROVESFlightControllerReference/test/unit-tests/CMakeLists.txt @@ -47,6 +47,14 @@ target_include_directories(stack_monitor_core PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/../../.. ) +# SBand FaultPolicy +add_library(sband_fault_policy STATIC + ${CMAKE_CURRENT_SOURCE_DIR}/../../../PROVESFlightControllerReference/Components/SBand/SBandFaultPolicy.cpp +) +target_include_directories(sband_fault_policy PUBLIC + ${CMAKE_CURRENT_SOURCE_DIR}/../../.. +) + # --- Auto-discover and build tests --- file(GLOB TEST_SOURCES "${CMAKE_CURRENT_SOURCE_DIR}/test_*.cpp") @@ -62,6 +70,7 @@ foreach(test_src ${TEST_SOURCES}) detumble_manager_bdot rtc_manager_rtc_helper stack_monitor_core + sband_fault_policy ) add_test(NAME ${test_name} COMMAND ${test_name}) diff --git a/PROVESFlightControllerReference/test/unit-tests/test_SBand_FaultPolicy.cpp b/PROVESFlightControllerReference/test/unit-tests/test_SBand_FaultPolicy.cpp new file mode 100644 index 00000000..88852cf3 --- /dev/null +++ b/PROVESFlightControllerReference/test/unit-tests/test_SBand_FaultPolicy.cpp @@ -0,0 +1,113 @@ +#include + +#include "PROVESFlightControllerReference/Components/SBand/SBandFaultPolicy.hpp" + +using Components::SBandFaultPolicy; + +TEST(SBandFaultPolicyTest, FreshPolicyReportsNoneAndStaysNoneBelowThreshold) { + SBandFaultPolicy policy; + + EXPECT_EQ(policy.decision(), SBandFaultPolicy::Decision::NONE); + + // One less than the consecutive-failure limit: still NONE. + for (std::uint32_t i = 0; i < SBandFaultPolicy::CONSECUTIVE_FAILURE_LIMIT - 1; i++) { + policy.operationFailed(); + } + EXPECT_EQ(policy.decision(), SBandFaultPolicy::Decision::NONE); +} + +TEST(SBandFaultPolicyTest, ConsecutiveFailureLimitRequestsResetExactlyOnce) { + SBandFaultPolicy policy; + + for (std::uint32_t i = 0; i < SBandFaultPolicy::CONSECUTIVE_FAILURE_LIMIT; i++) { + policy.operationFailed(); + } + EXPECT_EQ(policy.decision(), SBandFaultPolicy::Decision::REQUEST_RESET); + + // Further failures while the reset is pending must not re-trigger or + // otherwise change the decision -- the reset is requested exactly once + // until resetCompleted() is observed. + policy.operationFailed(); + policy.operationFailed(); + EXPECT_EQ(policy.decision(), SBandFaultPolicy::Decision::REQUEST_RESET); +} + +TEST(SBandFaultPolicyTest, SuccessResetsConsecutiveFailureCount) { + SBandFaultPolicy policy; + + for (std::uint32_t i = 0; i < SBandFaultPolicy::CONSECUTIVE_FAILURE_LIMIT - 1; i++) { + policy.operationFailed(); + } + EXPECT_EQ(policy.decision(), SBandFaultPolicy::Decision::NONE); + + // A success anywhere clears the count; it now takes a fresh run of + // CONSECUTIVE_FAILURE_LIMIT failures to request a reset. + policy.operationSucceeded(); + + for (std::uint32_t i = 0; i < SBandFaultPolicy::CONSECUTIVE_FAILURE_LIMIT - 1; i++) { + policy.operationFailed(); + } + EXPECT_EQ(policy.decision(), SBandFaultPolicy::Decision::NONE); + + policy.operationFailed(); + EXPECT_EQ(policy.decision(), SBandFaultPolicy::Decision::REQUEST_RESET); +} + +//! Drive the policy through one full failure/reset escalation cycle: +//! CONSECUTIVE_FAILURE_LIMIT failures (REQUEST_RESET), then resetCompleted() +//! with no intervening success. +void driveOneResetCycle(SBandFaultPolicy& policy) { + for (std::uint32_t i = 0; i < SBandFaultPolicy::CONSECUTIVE_FAILURE_LIMIT; i++) { + policy.operationFailed(); + } + policy.resetCompleted(); +} + +TEST(SBandFaultPolicyTest, ResetsWithoutInterveningSuccessLatchFaulted) { + SBandFaultPolicy policy; + + for (std::uint32_t i = 0; i < SBandFaultPolicy::RESET_ATTEMPT_LIMIT - 1; i++) { + driveOneResetCycle(policy); + EXPECT_EQ(policy.decision(), SBandFaultPolicy::Decision::NONE); + } + + // The Mth reset without an intervening success latches FAULTED. + driveOneResetCycle(policy); + EXPECT_EQ(policy.decision(), SBandFaultPolicy::Decision::FAULTED); + + // Latched: further failures and successes do not change the state. + policy.operationFailed(); + EXPECT_EQ(policy.decision(), SBandFaultPolicy::Decision::FAULTED); + policy.operationSucceeded(); + EXPECT_EQ(policy.decision(), SBandFaultPolicy::Decision::FAULTED); + policy.resetCompleted(); + EXPECT_EQ(policy.decision(), SBandFaultPolicy::Decision::FAULTED); +} + +TEST(SBandFaultPolicyTest, GroundResetReArmsFromFaultedAndCanReLatch) { + SBandFaultPolicy policy; + + // Drive to FAULTED. + for (std::uint32_t i = 0; i < SBandFaultPolicy::RESET_ATTEMPT_LIMIT; i++) { + driveOneResetCycle(policy); + } + ASSERT_EQ(policy.decision(), SBandFaultPolicy::Decision::FAULTED); + + // Ground commands a reset: policy re-arms to a clean state. + policy.groundResetRequested(); + EXPECT_EQ(policy.decision(), SBandFaultPolicy::Decision::NONE); + + // Re-init fails N more times: escalates to REQUEST_RESET again, exactly + // as if starting fresh. + for (std::uint32_t i = 0; i < SBandFaultPolicy::CONSECUTIVE_FAILURE_LIMIT; i++) { + policy.operationFailed(); + } + EXPECT_EQ(policy.decision(), SBandFaultPolicy::Decision::REQUEST_RESET); + + // M more failed resets re-latch FAULTED. + policy.resetCompleted(); + for (std::uint32_t i = 0; i < SBandFaultPolicy::RESET_ATTEMPT_LIMIT - 1; i++) { + driveOneResetCycle(policy); + } + EXPECT_EQ(policy.decision(), SBandFaultPolicy::Decision::FAULTED); +} From 5e059011c2a2c125ea063da668f4c181326114c0 Mon Sep 17 00:00:00 2001 From: Michael Pham <61564344+Mikefly123@users.noreply.github.com> Date: Mon, 13 Jul 2026 02:07:20 -0700 Subject: [PATCH 2/8] feat(sband): add SBandRadioIf test seam (decision D4) Abstract interface over the exact 13 SX1280 (RadioLib) calls SBand uses (begin, setPacketParamsLoRa, transmit, readData, getPacketLength, getIrqStatus, startReceive, standby, setSpreadingFactor, setCodingRate, setBandwidth, getRSSI, getSNR). No Zephyr/F' dependency -- just stdint/cstddef -- so both the production RadioLib-backed implementation and test fakes can implement it. Co-Authored-By: Claude Fable 5 --- .../Components/SBand/SBandRadioIf.hpp | 79 +++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 PROVESFlightControllerReference/Components/SBand/SBandRadioIf.hpp diff --git a/PROVESFlightControllerReference/Components/SBand/SBandRadioIf.hpp b/PROVESFlightControllerReference/Components/SBand/SBandRadioIf.hpp new file mode 100644 index 00000000..90543004 --- /dev/null +++ b/PROVESFlightControllerReference/Components/SBand/SBandRadioIf.hpp @@ -0,0 +1,79 @@ +// ====================================================================== +// \title SBandRadioIf.hpp +// \brief Abstract seam over the SX1280 (RadioLib) surface used by SBand +// ====================================================================== + +#ifndef Components_SBandRadioIf_HPP +#define Components_SBandRadioIf_HPP + +#include +#include + +namespace Components { + +//! Abstract interface covering exactly the SX1280 (RadioLib) surface the +//! SBand component uses (S-BAND-REINTEGRATION-PLAN.md decision D4). No +//! Zephyr or F Prime autocode dependencies -- just stdint/cstddef -- so it +//! compiles anywhere: production code (RadioLibSBandRadio, which owns +//! FprimeHal/Module/SX1280 and implements this 1:1) and test fakes alike. +//! +//! The component holds a pointer to this interface rather than the RadioLib +//! types directly, so its handlers can be exercised against a scripted fake +//! without any RadioLib/Zephyr headers in the mix. +class SBandRadioIf { + public: + virtual ~SBandRadioIf() = default; + + //! Configure and start the radio. Mirrors SX128x::begin. + virtual int16_t begin(float freqMHz, + float bandwidthKHz, + uint8_t spreadingFactor, + uint8_t codingRate, + uint8_t syncWord, + int8_t outputPowerDbm, + uint16_t preambleLength) = 0; + + //! Mirrors SX128x::setPacketParamsLoRa. + virtual int16_t setPacketParamsLoRa(uint8_t preambleLen, + uint8_t hdrType, + uint8_t payLen, + uint8_t crc, + uint8_t invIQ) = 0; + + //! Mirrors SX128x::transmit. + virtual int16_t transmit(const uint8_t* data, size_t len) = 0; + + //! Mirrors SX128x::readData. + virtual int16_t readData(uint8_t* data, size_t len) = 0; + + //! Mirrors SX128x::getPacketLength. + virtual size_t getPacketLength() = 0; + + //! Mirrors SX128x::getIrqStatus. + virtual uint16_t getIrqStatus() = 0; + + //! Mirrors SX128x::startReceive (no-arg overload: continuous Rx mode). + virtual int16_t startReceive() = 0; + + //! Mirrors SX128x::standby. + virtual int16_t standby() = 0; + + //! Mirrors SX128x::setSpreadingFactor. + virtual int16_t setSpreadingFactor(uint8_t sf) = 0; + + //! Mirrors SX128x::setCodingRate. + virtual int16_t setCodingRate(uint8_t cr) = 0; + + //! Mirrors SX128x::setBandwidth. + virtual int16_t setBandwidth(float bwKHz) = 0; + + //! Mirrors SX128x::getRSSI. + virtual float getRSSI() = 0; + + //! Mirrors SX128x::getSNR. + virtual float getSNR() = 0; +}; + +} // namespace Components + +#endif From c869a9b38dfdac82f8c1b419e24683a4ce511f0a Mon Sep 17 00:00:00 2001 From: Michael Pham <61564344+Mikefly123@users.noreply.github.com> Date: Mon, 13 Jul 2026 02:07:46 -0700 Subject: [PATCH 3/8] feat(sband): add SBandFaultPolicy counter getters for telemetry consecutiveFailures()/resetsSinceSuccess() expose the fault policy's internal counts read-only, so the SBand component can surface them as telemetry (ConsecutiveRadioFailures/RadioResetCount) and EVR arguments without changing the state machine's semantics. Pinned with a test. Co-Authored-By: Claude Fable 5 --- .../Components/SBand/SBandFaultPolicy.cpp | 8 ++++++++ .../Components/SBand/SBandFaultPolicy.hpp | 8 ++++++++ .../test/unit-tests/test_SBand_FaultPolicy.cpp | 18 ++++++++++++++++++ 3 files changed, 34 insertions(+) diff --git a/PROVESFlightControllerReference/Components/SBand/SBandFaultPolicy.cpp b/PROVESFlightControllerReference/Components/SBand/SBandFaultPolicy.cpp index 4981a78d..f35303ff 100644 --- a/PROVESFlightControllerReference/Components/SBand/SBandFaultPolicy.cpp +++ b/PROVESFlightControllerReference/Components/SBand/SBandFaultPolicy.cpp @@ -57,4 +57,12 @@ SBandFaultPolicy::Decision SBandFaultPolicy::decision() const { return Decision::NONE; } +std::uint32_t SBandFaultPolicy::consecutiveFailures() const { + return m_consecutiveFailures; +} + +std::uint32_t SBandFaultPolicy::resetsSinceSuccess() const { + return m_resetsSinceSuccess; +} + } // namespace Components diff --git a/PROVESFlightControllerReference/Components/SBand/SBandFaultPolicy.hpp b/PROVESFlightControllerReference/Components/SBand/SBandFaultPolicy.hpp index 24d4ad96..35439b3e 100644 --- a/PROVESFlightControllerReference/Components/SBand/SBandFaultPolicy.hpp +++ b/PROVESFlightControllerReference/Components/SBand/SBandFaultPolicy.hpp @@ -72,6 +72,14 @@ class SBandFaultPolicy { //! often as needed. Decision decision() const; + //! Current consecutive-failure count (for telemetry/EVR args only; not + //! part of the state-machine's semantics). + std::uint32_t consecutiveFailures() const; + + //! Current resets-since-last-success count (for telemetry/EVR args + //! only; not part of the state-machine's semantics). + std::uint32_t resetsSinceSuccess() const; + private: std::uint32_t m_consecutiveFailures; std::uint32_t m_resetsSinceSuccess; diff --git a/PROVESFlightControllerReference/test/unit-tests/test_SBand_FaultPolicy.cpp b/PROVESFlightControllerReference/test/unit-tests/test_SBand_FaultPolicy.cpp index 88852cf3..a3836730 100644 --- a/PROVESFlightControllerReference/test/unit-tests/test_SBand_FaultPolicy.cpp +++ b/PROVESFlightControllerReference/test/unit-tests/test_SBand_FaultPolicy.cpp @@ -111,3 +111,21 @@ TEST(SBandFaultPolicyTest, GroundResetReArmsFromFaultedAndCanReLatch) { } EXPECT_EQ(policy.decision(), SBandFaultPolicy::Decision::FAULTED); } + +TEST(SBandFaultPolicyTest, CountersReflectFailuresAndResetsForTelemetry) { + SBandFaultPolicy policy; + + EXPECT_EQ(policy.consecutiveFailures(), 0u); + EXPECT_EQ(policy.resetsSinceSuccess(), 0u); + + policy.operationFailed(); + policy.operationFailed(); + EXPECT_EQ(policy.consecutiveFailures(), 2u); + + policy.operationSucceeded(); + EXPECT_EQ(policy.consecutiveFailures(), 0u); + + driveOneResetCycle(policy); + EXPECT_EQ(policy.resetsSinceSuccess(), 1u); + EXPECT_EQ(policy.consecutiveFailures(), 0u); +} From e5ec7dcff99780553b03d5c8c5548d4bcfff23dc Mon Sep 17 00:00:00 2001 From: Michael Pham <61564344+Mikefly123@users.noreply.github.com> Date: Mon, 13 Jul 2026 02:08:09 -0700 Subject: [PATCH 4/8] feat(sband): add RadioLibSBandRadio, the production SBandRadioIf impl Owns the RadioLib HAL/Module/SX1280 stack the SBand component used to hold directly, and forwards every SBandRadioIf call 1:1 to it. Decision D4: production wiring uses this class; host/component tests point the same SBandRadioIf* at a fake instead. No behavior change -- pure delegation. Co-Authored-By: Claude Fable 5 --- .../Components/SBand/RadioLibSBandRadio.cpp | 79 +++++++++++++++++++ .../Components/SBand/RadioLibSBandRadio.hpp | 63 +++++++++++++++ 2 files changed, 142 insertions(+) create mode 100644 PROVESFlightControllerReference/Components/SBand/RadioLibSBandRadio.cpp create mode 100644 PROVESFlightControllerReference/Components/SBand/RadioLibSBandRadio.hpp diff --git a/PROVESFlightControllerReference/Components/SBand/RadioLibSBandRadio.cpp b/PROVESFlightControllerReference/Components/SBand/RadioLibSBandRadio.cpp new file mode 100644 index 00000000..c787a4ec --- /dev/null +++ b/PROVESFlightControllerReference/Components/SBand/RadioLibSBandRadio.cpp @@ -0,0 +1,79 @@ +// ====================================================================== +// \title RadioLibSBandRadio.cpp +// \brief Production SBandRadioIf implementation backed by RadioLib +// ====================================================================== + +#define RADIOLIB_LOW_LEVEL 1 + +#include "PROVESFlightControllerReference/Components/SBand/RadioLibSBandRadio.hpp" + +namespace Components { + +RadioLibSBandRadio::RadioLibSBandRadio(SBand* component) + : m_hal(component), + m_module(&m_hal, SBAND_PIN_CS, SBAND_PIN_IRQ, SBAND_PIN_RST, SBAND_PIN_BUSY), + m_radio(&m_module) {} + +int16_t RadioLibSBandRadio::begin(float freqMHz, + float bandwidthKHz, + uint8_t spreadingFactor, + uint8_t codingRate, + uint8_t syncWord, + int8_t outputPowerDbm, + uint16_t preambleLength) { + return m_radio.begin(freqMHz, bandwidthKHz, spreadingFactor, codingRate, syncWord, outputPowerDbm, preambleLength); +} + +int16_t RadioLibSBandRadio::setPacketParamsLoRa(uint8_t preambleLen, + uint8_t hdrType, + uint8_t payLen, + uint8_t crc, + uint8_t invIQ) { + return m_radio.setPacketParamsLoRa(preambleLen, hdrType, payLen, crc, invIQ); +} + +int16_t RadioLibSBandRadio::transmit(const uint8_t* data, size_t len) { + return m_radio.transmit(data, len); +} + +int16_t RadioLibSBandRadio::readData(uint8_t* data, size_t len) { + return m_radio.readData(data, len); +} + +size_t RadioLibSBandRadio::getPacketLength() { + return m_radio.getPacketLength(); +} + +uint16_t RadioLibSBandRadio::getIrqStatus() { + return m_radio.getIrqStatus(); +} + +int16_t RadioLibSBandRadio::startReceive() { + return m_radio.startReceive(); +} + +int16_t RadioLibSBandRadio::standby() { + return m_radio.standby(); +} + +int16_t RadioLibSBandRadio::setSpreadingFactor(uint8_t sf) { + return m_radio.setSpreadingFactor(sf); +} + +int16_t RadioLibSBandRadio::setCodingRate(uint8_t cr) { + return m_radio.setCodingRate(cr); +} + +int16_t RadioLibSBandRadio::setBandwidth(float bwKHz) { + return m_radio.setBandwidth(bwKHz); +} + +float RadioLibSBandRadio::getRSSI() { + return m_radio.getRSSI(); +} + +float RadioLibSBandRadio::getSNR() { + return m_radio.getSNR(); +} + +} // namespace Components diff --git a/PROVESFlightControllerReference/Components/SBand/RadioLibSBandRadio.hpp b/PROVESFlightControllerReference/Components/SBand/RadioLibSBandRadio.hpp new file mode 100644 index 00000000..c0bdf52b --- /dev/null +++ b/PROVESFlightControllerReference/Components/SBand/RadioLibSBandRadio.hpp @@ -0,0 +1,63 @@ +// ====================================================================== +// \title RadioLibSBandRadio.hpp +// \brief Production SBandRadioIf implementation backed by RadioLib +// ====================================================================== + +#ifndef Components_RadioLibSBandRadio_HPP +#define Components_RadioLibSBandRadio_HPP + +#include + +#include "PROVESFlightControllerReference/Components/SBand/FprimeHal.hpp" +#include "PROVESFlightControllerReference/Components/SBand/SBandRadioIf.hpp" + +namespace Components { + +class SBand; + +//! Production implementation of SBandRadioIf: owns the RadioLib HAL/Module/ +//! SX1280 stack and forwards every call 1:1 (S-BAND-REINTEGRATION-PLAN.md +//! decision D4). The SBand component holds this by value and points its +//! SBandRadioIf* at it in production; tests point the same pointer at a +//! fake instead. +class RadioLibSBandRadio final : public SBandRadioIf { + public: + //! \param component back-pointer used by the owned FprimeHal to reach + //! the component's GPIO/SPI output ports. + explicit RadioLibSBandRadio(SBand* component); + + int16_t begin(float freqMHz, + float bandwidthKHz, + uint8_t spreadingFactor, + uint8_t codingRate, + uint8_t syncWord, + int8_t outputPowerDbm, + uint16_t preambleLength) override; + + int16_t setPacketParamsLoRa(uint8_t preambleLen, + uint8_t hdrType, + uint8_t payLen, + uint8_t crc, + uint8_t invIQ) override; + + int16_t transmit(const uint8_t* data, size_t len) override; + int16_t readData(uint8_t* data, size_t len) override; + size_t getPacketLength() override; + uint16_t getIrqStatus() override; + int16_t startReceive() override; + int16_t standby() override; + int16_t setSpreadingFactor(uint8_t sf) override; + int16_t setCodingRate(uint8_t cr) override; + int16_t setBandwidth(float bwKHz) override; + float getRSSI() override; + float getSNR() override; + + private: + FprimeHal m_hal; //!< RadioLib HAL instance + Module m_module; //!< RadioLib Module instance + SX1280 m_radio; //!< RadioLib SX1280 radio instance +}; + +} // namespace Components + +#endif From caa68490cd7b296bdac9b910492422179b56d465 Mon Sep 17 00:00:00 2001 From: Michael Pham <61564344+Mikefly123@users.noreply.github.com> Date: Mon, 13 Jul 2026 02:08:36 -0700 Subject: [PATCH 5/8] build(sband): re-enable the SBand component in the firmware build Un-comments add_fprime_subdirectory for Components/SBand -- the library and its sources now build again (topology stays untouched: the sband instance remains commented out until PR 3, so no flight-behavior change). Co-Authored-By: Claude Fable 5 --- PROVESFlightControllerReference/Components/CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PROVESFlightControllerReference/Components/CMakeLists.txt b/PROVESFlightControllerReference/Components/CMakeLists.txt index f2ddc660..2ae1b608 100644 --- a/PROVESFlightControllerReference/Components/CMakeLists.txt +++ b/PROVESFlightControllerReference/Components/CMakeLists.txt @@ -22,7 +22,7 @@ add_fprime_subdirectory("${CMAKE_CURRENT_LIST_DIR}/NullPrmDb/") add_fprime_subdirectory("${CMAKE_CURRENT_LIST_DIR}/PayloadCom/") add_fprime_subdirectory("${CMAKE_CURRENT_LIST_DIR}/PowerMonitor/") add_fprime_subdirectory("${CMAKE_CURRENT_LIST_DIR}/ResetManager/") -#add_fprime_subdirectory("${CMAKE_CURRENT_LIST_DIR}/SBand/") +add_fprime_subdirectory("${CMAKE_CURRENT_LIST_DIR}/SBand/") add_fprime_subdirectory("${CMAKE_CURRENT_LIST_DIR}/StackMonitor/") add_fprime_subdirectory("${CMAKE_CURRENT_LIST_DIR}/StartupManager/") add_fprime_subdirectory("${CMAKE_CURRENT_LIST_DIR}/ThermalManager/") From 72bc582f013e45563e6544ca4c29b7864c0b5aff Mon Sep 17 00:00:00 2001 From: Michael Pham <61564344+Mikefly123@users.noreply.github.com> Date: Mon, 13 Jul 2026 02:08:46 -0700 Subject: [PATCH 6/8] fix(sband): wire the SX1280 BUSY line into RadioLib via FprimeHal Ports s-band-speedup 363101e + 7c473f4 (hand-applied: this repo was renamed FprimeZephyrReference -> PROVESFlightControllerReference in #331, so the original commits don't cherry-pick cleanly). Adds SBAND_PIN_BUSY and FprimeHal::digitalRead's BUSY branch; RadioLibSBandRadio's Module is already constructed with the BUSY pin (this commit makes that pin do something). The topology-level GPIO connection (gpioSbandBusy) is deferred to PR 3 along with the rest of the sband instance wiring. Co-Authored-By: Claude Fable 5 --- .../Components/SBand/FprimeHal.cpp | 9 +++++++++ .../Components/SBand/FprimeHal.hpp | 1 + 2 files changed, 10 insertions(+) diff --git a/PROVESFlightControllerReference/Components/SBand/FprimeHal.cpp b/PROVESFlightControllerReference/Components/SBand/FprimeHal.cpp index 9ef1c4b0..04f9ca10 100644 --- a/PROVESFlightControllerReference/Components/SBand/FprimeHal.cpp +++ b/PROVESFlightControllerReference/Components/SBand/FprimeHal.cpp @@ -38,6 +38,15 @@ uint32_t FprimeHal::digitalRead(uint32_t pin) { else return FPRIME_HAL_GPIO_LEVEL_LOW; } + if (pin == SBAND_PIN_BUSY) { + Fw::Logic busyState; + Drv::GpioStatus state = this->m_component->getBusyLine_out(0, busyState); + FW_ASSERT(state == Drv::GpioStatus::OP_OK); + if (busyState == Fw::Logic::HIGH) + return FPRIME_HAL_GPIO_LEVEL_HIGH; + else + return FPRIME_HAL_GPIO_LEVEL_LOW; + } return FPRIME_HAL_GPIO_LEVEL_LOW; } diff --git a/PROVESFlightControllerReference/Components/SBand/FprimeHal.hpp b/PROVESFlightControllerReference/Components/SBand/FprimeHal.hpp index 3a56cc6b..4eba0286 100644 --- a/PROVESFlightControllerReference/Components/SBand/FprimeHal.hpp +++ b/PROVESFlightControllerReference/Components/SBand/FprimeHal.hpp @@ -11,6 +11,7 @@ #define SBAND_PIN_CS 0 #define SBAND_PIN_IRQ 5 #define SBAND_PIN_RST 6 +#define SBAND_PIN_BUSY 7 namespace Components { class SBand; From d2136ce9e1a52e68a10efd2a17c5df25abc98b61 Mon Sep 17 00:00:00 2001 From: Michael Pham <61564344+Mikefly123@users.noreply.github.com> Date: Mon, 13 Jul 2026 02:09:13 -0700 Subject: [PATCH 7/8] feat(sband): wire SBandFaultPolicy + SBandRadioIf into the component Thin-adapter refactor (D3+D4), verified by firmware build now / HWIL in PR 3 -- logic is covered by the SBandFaultPolicy host UTs: - Component holds a SBandRadioIf* (default: &m_rlb_radio, a RadioLibSBandRadio) instead of the RadioLib types directly; every begin/transmit/readData/etc. call goes through m_radio. - Every radio-call outcome feeds SBandFaultPolicy via handleRadioResult(); applyFaultDecision() reacts to REQUEST_RESET (nRST reset + re-init attempt, existing resetSend_out path) and FAULTED (stop all radio-interface calls, WARNING_HI EVR once, RadioFaulted telemetry). - New RESET_RADIO command: groundResetRequested() + full re-init attempt. - FAULTED and !configured both still return dataIn buffers and emit comStatus immediately so the com queue never starves; run ticks become no-ops while FAULTED. - deferredRxHandler allocates from getPacketLength() and reads RadioLib data directly into the Fw::Buffer -- the 256-byte stack array is gone (this was implicated in the #299 SBand-thread stack overflow). - New telemetry: RadioFaulted (bool), ConsecutiveRadioFailures, RadioResetCount. New events: RadioResetRequested, RadioFaultLatched, RadioFaultCleared. Also hand-ports the remaining s-band-speedup correctness fixes (cherry-pick doesn't apply post-#331 rename): - 8aeee2f: dataOut_out only after all SPI ops for the RX'd packet (readData/RSSI/SNR/enableRx) -- avoids contending with flash on the SPI bus while dataOut_out's synchronous downstream chain runs. - 9b240d0: m_rxHandlerQueued is now std::atomic, exchanged atomically in run_handler -- it's genuinely shared between the rate-group thread and SBand's own thread. - 17df3ec: enableRx/enableTx return the RadioLib int16_t state instead of Status; re-arming RX goes through enableRx() (not a raw startReceive()) so RF params are reapplied on every re-arm. Trivial compile fix against current RadioLib: startReceive() no longer takes a timeout arg (the no-arg overload is already continuous-Rx mode). SBand/CMakeLists.txt: adds SBandFaultPolicy.cpp/RadioLibSBandRadio.cpp to the library, and defines RADIOLIB_STATIC_ONLY=1 PUBLIC on the RadioLib target so it applies to RadioLib's own TUs and everything that includes its headers (no heap allocation after boot). The sband instance stays commented out of topology.fpp; no ReferenceDeploymentPackets.fppi changes (its channels aren't in the dictionary until PR 3). Co-Authored-By: Claude Fable 5 --- .../Components/SBand/CMakeLists.txt | 15 + .../Components/SBand/SBand.cpp | 315 ++++++++++++------ .../Components/SBand/SBand.fpp | 27 ++ .../Components/SBand/SBand.hpp | 53 ++- 4 files changed, 296 insertions(+), 114 deletions(-) diff --git a/PROVESFlightControllerReference/Components/SBand/CMakeLists.txt b/PROVESFlightControllerReference/Components/SBand/CMakeLists.txt index 24968493..6a6124d8 100644 --- a/PROVESFlightControllerReference/Components/SBand/CMakeLists.txt +++ b/PROVESFlightControllerReference/Components/SBand/CMakeLists.txt @@ -20,6 +20,8 @@ register_fprime_library( SOURCES "${CMAKE_CURRENT_LIST_DIR}/SBand.cpp" "${CMAKE_CURRENT_LIST_DIR}/FprimeHal.cpp" + "${CMAKE_CURRENT_LIST_DIR}/SBandFaultPolicy.cpp" + "${CMAKE_CURRENT_LIST_DIR}/RadioLibSBandRadio.cpp" DEPENDS RadioLib ) @@ -29,7 +31,20 @@ add_subdirectory("${FPRIME_PROJECT_ROOT}/lib/RadioLib" "${CMAKE_BINARY_DIR}/Radi # Disable compile warnings for RadioLib as they pollute the log and we can't do anything about them. target_compile_options(RadioLib PRIVATE -w) +# RadioLib defaults to dynamic (heap) allocation; flight code must not +# heap-allocate after boot (S-BAND-REINTEGRATION-PLAN.md root cause #3). +# PUBLIC so it also applies to any TU that includes RadioLib headers +# (SBand.cpp, FprimeHal.cpp, RadioLibSBandRadio.cpp) via the RadioLib +# target dependency above, not just RadioLib's own compilation units. +target_compile_definitions(RadioLib PUBLIC RADIOLIB_STATIC_ONLY=1) + ### Unit Tests ### +# F Prime component-level UTs are intentionally not used in this repo: host +# unit tests live under test/unit-tests/ (see test_SBand_FaultPolicy.cpp), +# driving the pure-logic SBandFaultPolicy class directly with no Zephyr/F' +# autocode dependency. The SBand component itself is a thin adapter over +# SBandRadioIf/SBandFaultPolicy, verified by this firmware build and by +# HWIL in PR 3. # register_fprime_ut( # AUTOCODER_INPUTS # "${CMAKE_CURRENT_LIST_DIR}/SBand.fpp" diff --git a/PROVESFlightControllerReference/Components/SBand/SBand.cpp b/PROVESFlightControllerReference/Components/SBand/SBand.cpp index dddca577..f701b8b4 100644 --- a/PROVESFlightControllerReference/Components/SBand/SBand.cpp +++ b/PROVESFlightControllerReference/Components/SBand/SBand.cpp @@ -12,6 +12,8 @@ #include #include +#include +#include #include "FprimeHal.hpp" @@ -38,105 +40,194 @@ static float bandwidthEnumToKHz(SBandBandwidth bw) { // ---------------------------------------------------------------------- SBand ::SBand(const char* const compName) - : SBandComponentBase(compName), - m_rlb_hal(this), - m_rlb_module(&m_rlb_hal, SBAND_PIN_CS, SBAND_PIN_IRQ, SBAND_PIN_RST), - m_rlb_radio(&m_rlb_module) {} + : SBandComponentBase(compName), m_rlb_radio(this), m_radio(&m_rlb_radio), m_faultPolicy() {} SBand ::~SBand() {} +void SBand ::setRadioIf(SBandRadioIf* radio) { + m_radio = (radio != nullptr) ? radio : &m_rlb_radio; +} + +// ---------------------------------------------------------------------- +// Fault management (S-BAND-REINTEGRATION-PLAN.md decision D3) +// ---------------------------------------------------------------------- + +bool SBand ::isFaulted() const { + return this->m_faultPolicy.decision() == SBandFaultPolicy::Decision::FAULTED; +} + +SBand::Status SBand ::handleRadioResult(int16_t state) { + if (state == RADIOLIB_ERR_NONE) { + this->m_faultPolicy.operationSucceeded(); + this->log_WARNING_HI_RadioLibFailed_ThrottleClear(); + this->tlmWrite_ConsecutiveRadioFailures(0); + return Status::SUCCESS; + } + + this->log_WARNING_HI_RadioLibFailed(state); + this->m_faultPolicy.operationFailed(); + this->tlmWrite_ConsecutiveRadioFailures(this->m_faultPolicy.consecutiveFailures()); + return Status::ERROR; +} + +void SBand ::performHardwareReset() { + // Toggle the SX1280's nRST line directly: nRST is a component-level GPIO + // output port (resetSend_out / FprimeHal's SBAND_PIN_RST digitalWrite), + // not part of the SBandRadioIf seam, which only covers SX1280 driver + // calls. Pulse width/settle time are placeholders pending PR 3 HWIL + // bench tuning. + this->resetSend_out(0, Fw::Logic::LOW); + Os::Task::delay(Fw::TimeInterval(0, 1000)); // 1 ms nRST pulse + this->resetSend_out(0, Fw::Logic::HIGH); + Os::Task::delay(Fw::TimeInterval(0, 1000)); // allow the SX1280 to boot before further SPI +} + +void SBand ::applyFaultDecision() { + // Bounded: each REQUEST_RESET branch below calls resetCompleted(), and + // SBandFaultPolicy::RESET_ATTEMPT_LIMIT resets without an intervening + // success latches FAULTED (terminating the loop). The iteration cap is + // a defensive backstop only -- see SBandFaultPolicy.hpp. + for (std::uint32_t guard = 0; guard <= SBandFaultPolicy::RESET_ATTEMPT_LIMIT; guard++) { + SBandFaultPolicy::Decision decision = this->m_faultPolicy.decision(); + + if (decision == SBandFaultPolicy::Decision::NONE) { + return; + } + + if (decision == SBandFaultPolicy::Decision::FAULTED) { + this->tlmWrite_RadioFaulted(true); + if (!this->m_faultEvrEmitted) { + this->log_WARNING_HI_RadioFaultLatched(this->m_faultPolicy.resetsSinceSuccess()); + this->m_faultEvrEmitted = true; + } + return; + } + + // REQUEST_RESET: perform the nRST reset, then attempt a fresh + // re-init. The re-init's own radio calls flow back through + // handleRadioResult(), which may push the decision to + // REQUEST_RESET or FAULTED again -- re-checked at the top of the + // next iteration. + this->log_WARNING_LO_RadioResetRequested(this->m_faultPolicy.consecutiveFailures()); + this->performHardwareReset(); + this->m_faultPolicy.resetCompleted(); + this->tlmWrite_ConsecutiveRadioFailures(0); + this->tlmWrite_RadioResetCount(this->m_faultPolicy.resetsSinceSuccess()); + + if (this->m_faultPolicy.decision() != SBandFaultPolicy::Decision::FAULTED) { + (void)this->configureRadio(); + } + } +} + // ---------------------------------------------------------------------- // Handler implementations for typed input ports // ---------------------------------------------------------------------- void SBand ::run_handler(FwIndexType portNum, U32 context) { - // Only process if radio is configured - if (!m_configured) { + // Only process if radio is configured and not latched FAULTED (D3: a + // FAULTED radio makes run ticks no-ops -- no further radio-interface + // calls are made until a ground RESET_RADIO succeeds). + if (!m_configured || this->isFaulted()) { return; } - // Queue RX handler only if not already queued - if (!m_rxHandlerQueued) { - m_rxHandlerQueued = true; + // Queue RX handler only if not already queued. m_rxHandlerQueued is + // exchanged atomically (ported from s-band-speedup 9b240d0): run_handler + // executes on the rate-group thread while deferredRxHandler runs on + // SBand's own thread, so a plain read-then-write here would race. + if (!m_rxHandlerQueued.exchange(true)) { this->deferredRxHandler_internalInterfaceInvoke(); } } void SBand ::deferredRxHandler_internalInterfaceHandler() { + if (this->isFaulted()) { + m_rxHandlerQueued = false; + return; + } + // Check IRQ status - uint16_t irqStatus = this->m_rlb_radio.getIrqStatus(); + uint16_t irqStatus = m_radio->getIrqStatus(); // Only process if RX_DONE if (irqStatus & RADIOLIB_SX128X_IRQ_RX_DONE) { - // Process received data - SX1280* radio = &this->m_rlb_radio; - uint8_t data[256] = {0}; - size_t len = radio->getPacketLength(); - int16_t state = radio->readData(data, len); - - if (state != RADIOLIB_ERR_NONE) { - this->log_WARNING_HI_RadioLibFailed(state); + bool haveData = false; + Fw::Buffer buffer; + + // Allocate directly from the packet length, then read RadioLib data + // straight into the allocated Fw::Buffer -- no intermediate stack + // array (the 256-byte array this replaced was implicated in the + // #299 SBand-thread stack overflow). + size_t len = m_radio->getPacketLength(); + buffer = this->allocate_out(0, static_cast(len)); + + if (!buffer.isValid()) { + this->log_WARNING_HI_AllocationFailed(static_cast(len)); } else { - Fw::Buffer buffer = this->allocate_out(0, static_cast(len)); - if (buffer.isValid()) { - (void)::memcpy(buffer.getData(), data, len); - ComCfg::FrameContext frameContext; - this->dataOut_out(0, buffer, frameContext); + int16_t state = m_radio->readData(buffer.getData(), len); + if (this->handleRadioResult(state) == Status::SUCCESS) { + this->log_WARNING_HI_AllocationFailed_ThrottleClear(); // Log RSSI and SNR for received packet - float rssi = radio->getRSSI(); - float snr = radio->getSNR(); - this->tlmWrite_LastRssi(rssi); - this->tlmWrite_LastSnr(snr); - - // Clear throttled warnings on success - this->log_WARNING_HI_RadioLibFailed_ThrottleClear(); - this->log_WARNING_HI_AllocationFailed_ThrottleClear(); + this->tlmWrite_LastRssi(m_radio->getRSSI()); + this->tlmWrite_LastSnr(m_radio->getSNR()); + haveData = true; } else { - this->log_WARNING_HI_AllocationFailed(static_cast(len)); + this->deallocate_out(0, buffer); } } - // Re-enable receive mode - state = radio->startReceive(RADIOLIB_SX128X_RX_TIMEOUT_INF); - if (state != RADIOLIB_ERR_NONE) { - this->log_WARNING_HI_RadioLibFailed(state); + // Re-enable receive mode. Ported from s-band-speedup 17df3ec: go + // through enableRx() (not a raw startReceive()) so RF params are + // reapplied on every re-arm, not just at configureRadio() time. + this->enableRx(); + + // Ported from s-band-speedup 8aeee2f: send the frame out only after + // every SPI operation for this packet is done (readData, RSSI/SNR, + // enableRx) -- RadioLib and the flash filesystem both use the SPI + // bus, and dataOut_out's synchronous downstream call chain held the + // bus contended with flash when it ran before those SPI ops. + if (haveData) { + ComCfg::FrameContext frameContext; + this->dataOut_out(0, buffer, frameContext); } } // Clear the queued flag m_rxHandlerQueued = false; + + this->applyFaultDecision(); } void SBand ::deferredTxHandler_internalInterfaceHandler(const Fw::Buffer& data, const ComCfg::FrameContext& context) { Fw::Success returnStatus = Fw::Success::FAILURE; Fw::Buffer mutableData = data; // hack to get around const-ness - if (this->m_transmit_enabled != SBandTransmitState::ENABLED) { + if ((this->m_transmit_enabled != SBandTransmitState::ENABLED) || this->isFaulted()) { this->dataReturnOut_out(0, mutableData, context); this->comStatusOut_out(0, returnStatus); return; } // Enable transmit mode - Status status = this->enableTx(); - if (status == Status::SUCCESS) { + int16_t state = this->enableTx(); + if (state == RADIOLIB_ERR_NONE) { // Transmit data - int16_t state = this->m_rlb_radio.transmit(data.getData(), data.getSize()); - if (state != RADIOLIB_ERR_NONE) { - this->log_WARNING_HI_RadioLibFailed(state); - returnStatus = Fw::Success::FAILURE; - } else { + state = m_radio->transmit(data.getData(), data.getSize()); + if (this->handleRadioResult(state) == Status::SUCCESS) { returnStatus = Fw::Success::SUCCESS; - // Clear throttled warnings on success - this->log_WARNING_HI_RadioLibFailed_ThrottleClear(); } } this->dataReturnOut_out(0, mutableData, context); this->comStatusOut_out(0, returnStatus); - status = this->enableRx(); + if (!this->isFaulted()) { + this->enableRx(); + } + + this->applyFaultDecision(); } // ---------------------------------------------------------------------- @@ -144,8 +235,13 @@ void SBand ::deferredTxHandler_internalInterfaceHandler(const Fw::Buffer& data, // ---------------------------------------------------------------------- void SBand ::dataIn_handler(FwIndexType portNum, Fw::Buffer& data, const ComCfg::FrameContext& context) { - if (!m_configured) { - this->log_WARNING_HI_RadioNotConfigured(); + // FAULTED and !configured both degrade to "no S-Band": the buffer is + // returned and comStatus is still emitted immediately so the com queue + // never starves, but the radio itself is never touched. + if (!m_configured || this->isFaulted()) { + if (!m_configured) { + this->log_WARNING_HI_RadioNotConfigured(); + } Fw::Success failureStatus = Fw::Success::FAILURE; this->dataReturnOut_out(0, data, context); this->comStatusOut_out(0, failureStatus); @@ -161,7 +257,7 @@ void SBand ::dataReturnIn_handler(FwIndexType portNum, Fw::Buffer& data, const C this->deallocate_out(0, data); } -SBand::Status SBand ::enableRx() { +int16_t SBand ::enableRx() { Fw::ParamValid isValid = Fw::ParamValid::INVALID; const SBandDataRate dataRate = this->paramGet_DATA_RATE(isValid); FW_ASSERT((isValid == Fw::ParamValid::VALID) || (isValid == Fw::ParamValid::DEFAULT), @@ -176,41 +272,32 @@ SBand::Status SBand ::enableRx() { this->txEnable_out(0, Fw::Logic::LOW); this->rxEnable_out(0, Fw::Logic::HIGH); - SX1280* radio = &this->m_rlb_radio; - - int16_t state = radio->standby(); - if (state != RADIOLIB_ERR_NONE) { - this->log_WARNING_HI_RadioLibFailed(state); - return Status::ERROR; + int16_t state = m_radio->standby(); + if (this->handleRadioResult(state) != Status::SUCCESS) { + return state; } - state = radio->setSpreadingFactor(static_cast(dataRate.e)); - if (state != RADIOLIB_ERR_NONE) { - this->log_WARNING_HI_RadioLibFailed(state); - return Status::ERROR; + state = m_radio->setSpreadingFactor(static_cast(dataRate.e)); + if (this->handleRadioResult(state) != Status::SUCCESS) { + return state; } - state = radio->setCodingRate(static_cast(codingRate.e)); - if (state != RADIOLIB_ERR_NONE) { - this->log_WARNING_HI_RadioLibFailed(state); - return Status::ERROR; + state = m_radio->setCodingRate(static_cast(codingRate.e)); + if (this->handleRadioResult(state) != Status::SUCCESS) { + return state; } - state = radio->setBandwidth(bandwidthEnumToKHz(bandwidth)); - if (state != RADIOLIB_ERR_NONE) { - this->log_WARNING_HI_RadioLibFailed(state); - return Status::ERROR; + state = m_radio->setBandwidth(bandwidthEnumToKHz(bandwidth)); + if (this->handleRadioResult(state) != Status::SUCCESS) { + return state; } - state = radio->startReceive(RADIOLIB_SX128X_RX_TIMEOUT_INF); - if (state != RADIOLIB_ERR_NONE) { - this->log_WARNING_HI_RadioLibFailed(state); - return Status::ERROR; - } - return Status::SUCCESS; + state = m_radio->startReceive(); + (void)this->handleRadioResult(state); + return state; } -SBand::Status SBand ::enableTx() { +int16_t SBand ::enableTx() { Fw::ParamValid isValid = Fw::ParamValid::INVALID; const SBandDataRate dataRate = this->paramGet_DATA_RATE(isValid); FW_ASSERT((isValid == Fw::ParamValid::VALID) || (isValid == Fw::ParamValid::DEFAULT), @@ -225,33 +312,24 @@ SBand::Status SBand ::enableTx() { this->rxEnable_out(0, Fw::Logic::LOW); this->txEnable_out(0, Fw::Logic::HIGH); - SX1280* radio = &this->m_rlb_radio; - - int16_t state = radio->standby(); - if (state != RADIOLIB_ERR_NONE) { - this->log_WARNING_HI_RadioLibFailed(state); - return Status::ERROR; + int16_t state = m_radio->standby(); + if (this->handleRadioResult(state) != Status::SUCCESS) { + return state; } - state = radio->setSpreadingFactor(static_cast(dataRate.e)); - if (state != RADIOLIB_ERR_NONE) { - this->log_WARNING_HI_RadioLibFailed(state); - return Status::ERROR; + state = m_radio->setSpreadingFactor(static_cast(dataRate.e)); + if (this->handleRadioResult(state) != Status::SUCCESS) { + return state; } - state = radio->setCodingRate(static_cast(codingRate.e)); - if (state != RADIOLIB_ERR_NONE) { - this->log_WARNING_HI_RadioLibFailed(state); - return Status::ERROR; + state = m_radio->setCodingRate(static_cast(codingRate.e)); + if (this->handleRadioResult(state) != Status::SUCCESS) { + return state; } - state = radio->setBandwidth(bandwidthEnumToKHz(bandwidth)); - if (state != RADIOLIB_ERR_NONE) { - this->log_WARNING_HI_RadioLibFailed(state); - return Status::ERROR; - } - - return Status::SUCCESS; + state = m_radio->setBandwidth(bandwidthEnumToKHz(bandwidth)); + (void)this->handleRadioResult(state); + return state; } SBand::Status SBand ::configureRadio() { @@ -274,22 +352,20 @@ SBand::Status SBand ::configureRadio() { int8_t outputPowerDbm = 13; // 13 dBm is max uint16_t preambleLength = 12; - int16_t state = this->m_rlb_radio.begin(frequencyMHz, bandwidthKHz, spreadingFactor, codingRateValue, syncWord, - outputPowerDbm, preambleLength); - if (state != RADIOLIB_ERR_NONE) { - this->log_WARNING_HI_RadioLibFailed(state); + int16_t state = m_radio->begin(frequencyMHz, bandwidthKHz, spreadingFactor, codingRateValue, syncWord, + outputPowerDbm, preambleLength); + if (this->handleRadioResult(state) != Status::SUCCESS) { return Status::ERROR; } - state = this->m_rlb_radio.setPacketParamsLoRa(preambleLength, RADIOLIB_SX128X_LORA_HEADER_EXPLICIT, 255, - RADIOLIB_SX128X_LORA_CRC_ON, RADIOLIB_SX128X_LORA_IQ_STANDARD); - if (state != RADIOLIB_ERR_NONE) { - this->log_WARNING_HI_RadioLibFailed(state); + state = m_radio->setPacketParamsLoRa(preambleLength, RADIOLIB_SX128X_LORA_HEADER_EXPLICIT, 255, + RADIOLIB_SX128X_LORA_CRC_ON, RADIOLIB_SX128X_LORA_IQ_STANDARD); + if (this->handleRadioResult(state) != Status::SUCCESS) { return Status::ERROR; } - Status rx_status = this->enableRx(); - if (rx_status != Status::SUCCESS) { + int16_t rx_state = this->enableRx(); + if (rx_state != RADIOLIB_ERR_NONE) { return Status::ERROR; } @@ -329,4 +405,31 @@ void SBand ::deferredTransmitCmd_internalInterfaceHandler(const SBandTransmitSta } } +void SBand ::RESET_RADIO_cmdHandler(FwOpcodeType opCode, U32 cmdSeq) { + bool wasFaulted = this->isFaulted(); + + // Re-arm the fault policy (clears a FAULTED latch, if any) and attempt + // a full re-init. + this->m_faultPolicy.groundResetRequested(); + this->m_faultEvrEmitted = false; + this->tlmWrite_RadioFaulted(false); + this->tlmWrite_ConsecutiveRadioFailures(0); + this->tlmWrite_RadioResetCount(0); + + Status status = this->configureRadio(); + + // The re-init attempt above flows through handleRadioResult(); apply + // whatever decision that produced (it may immediately re-escalate to + // REQUEST_RESET/FAULTED if the underlying hardware fault persists). + this->applyFaultDecision(); + + if (wasFaulted && !this->isFaulted()) { + this->log_ACTIVITY_HI_RadioFaultCleared(); + } + + Fw::CmdResponse response = + ((status == Status::SUCCESS) && !this->isFaulted()) ? Fw::CmdResponse::OK : Fw::CmdResponse::EXECUTION_ERROR; + this->cmdResponse_out(opCode, cmdSeq, response); +} + } // namespace Components diff --git a/PROVESFlightControllerReference/Components/SBand/SBand.fpp b/PROVESFlightControllerReference/Components/SBand/SBand.fpp index 3a0eddba..0b329b95 100644 --- a/PROVESFlightControllerReference/Components/SBand/SBand.fpp +++ b/PROVESFlightControllerReference/Components/SBand/SBand.fpp @@ -81,6 +81,9 @@ module Components { @ S-Band IRQ Line output port getIRQLine: Drv.GpioRead + @ S-Band Busy Line + output port getBusyLine: Drv.GpioRead + @ Event to indicate RadioLib call failure event RadioLibFailed(error: I16) severity warning high \ format "SBand RadioLib call failed, error: {}" throttle 2 @@ -93,12 +96,33 @@ module Components { event RadioNotConfigured() severity warning high \ format "Radio not configured, operation ignored" throttle 3 + @ Event: consecutive RadioLib failures crossed the auto-reset threshold; a nRST reset was requested + event RadioResetRequested(consecutiveFailures: U32) severity warning low \ + format "SBand: {} consecutive radio failures, requesting nRST reset" + + @ Event: repeated resets without an intervening success latched the radio FAULTED + event RadioFaultLatched(resetCount: U32) severity warning high \ + format "SBand radio FAULTED after {} failed reset attempts; ground RESET_RADIO required to recover" + + @ Event: ground RESET_RADIO command cleared a FAULTED latch and re-initialized the radio + event RadioFaultCleared() severity activity high \ + format "SBand radio fault cleared by ground RESET_RADIO; radio re-initialized" + @ Last received RSSI (if available) telemetry LastRssi: F32 update on change @ Last received SNR (if available) telemetry LastSnr: F32 update on change + @ True when the radio is latched FAULTED (see SBandFaultPolicy); cleared only by a successful RESET_RADIO + telemetry RadioFaulted: bool update on change + + @ Current consecutive-failure count tracked by SBandFaultPolicy + telemetry ConsecutiveRadioFailures: U32 update on change + + @ Number of nRST resets performed since the last successful radio operation + telemetry RadioResetCount: U32 update on change + ############################################################################### # Parameters # ############################################################################### @@ -122,6 +146,9 @@ module Components { @ Start/stop transmission on the S-Band module sync command TRANSMIT(enabled: SBandTransmitState) + @ Ground-commanded radio reset: clears a FAULTED latch (if any) and attempts a full re-init + sync command RESET_RADIO() + ############################################################################### # Standard AC Ports: Required for Channels, Events, Commands, and Parameters # ############################################################################### diff --git a/PROVESFlightControllerReference/Components/SBand/SBand.hpp b/PROVESFlightControllerReference/Components/SBand/SBand.hpp index a60c01fe..d98138c7 100644 --- a/PROVESFlightControllerReference/Components/SBand/SBand.hpp +++ b/PROVESFlightControllerReference/Components/SBand/SBand.hpp @@ -7,8 +7,12 @@ #ifndef Components_SBand_HPP #define Components_SBand_HPP -#include "FprimeHal.hpp" +#include + +#include "PROVESFlightControllerReference/Components/SBand/RadioLibSBandRadio.hpp" #include "PROVESFlightControllerReference/Components/SBand/SBandComponentAc.hpp" +#include "PROVESFlightControllerReference/Components/SBand/SBandFaultPolicy.hpp" +#include "PROVESFlightControllerReference/Components/SBand/SBandRadioIf.hpp" namespace Components { @@ -31,6 +35,12 @@ class SBand final : public SBandComponentBase { //! Configure the radio and start operation Status configureRadio(); + //! Test seam: point the component at a different SBandRadioIf (e.g. a + //! fake). Not used in flight; production wiring always uses the + //! RadioLibSBandRadio owned by this object (m_rlb_radio). + void setRadioIf(SBandRadioIf* radio); + + using SBandComponentBase::getBusyLine_out; using SBandComponentBase::getIRQLine_out; using SBandComponentBase::getTime; using SBandComponentBase::resetSend_out; @@ -83,19 +93,46 @@ class SBand final : public SBandComponentBase { //! Start/stop transmission on the S-Band module void TRANSMIT_cmdHandler(FwOpcodeType opCode, U32 cmdSeq, SBandTransmitState enabled) override; + //! Handler implementation for command RESET_RADIO + //! + //! Ground-commanded radio reset: groundResetRequested() + full re-init attempt + void RESET_RADIO_cmdHandler(FwOpcodeType opCode, U32 cmdSeq) override; + private: //! Enable receive mode - Status enableRx(); + //! \return RadioLib state (RADIOLIB_ERR_NONE on success) + int16_t enableRx(); //! Enable transmit mode - Status enableTx(); + //! \return RadioLib state (RADIOLIB_ERR_NONE on success) + int16_t enableTx(); + + //! True while the fault policy is latched FAULTED: no further + //! radio-interface calls are made until a ground RESET_RADIO succeeds. + bool isFaulted() const; + + //! Feed one radio-call outcome into the fault policy, update the + //! associated telemetry, and emit RadioLibFailed on failure. Does not + //! itself act on the resulting decision -- callers follow up with + //! applyFaultDecision(). + Status handleRadioResult(int16_t state); + + //! React to the fault policy's current decision: perform a nRST reset + //! and re-init attempt on REQUEST_RESET, latch telemetry/EVR on + //! FAULTED. No-op when the decision is NONE. + void applyFaultDecision(); + + //! Toggle the radio's nRST line (the existing reset path already wired + //! through resetSend_out/FprimeHal's SBAND_PIN_RST digitalWrite). + void performHardwareReset(); private: - FprimeHal m_rlb_hal; //!< RadioLib HAL instance - Module m_rlb_module; //!< RadioLib Module instance - SX1280 m_rlb_radio; //!< RadioLib SX1280 radio instance - bool m_configured = false; //!< Flag indicating radio is configured - bool m_rxHandlerQueued = false; //!< Flag indicating RX handler is queued + RadioLibSBandRadio m_rlb_radio; //!< Production RadioLib-backed radio implementation + SBandRadioIf* m_radio; //!< Interface used by all handlers; defaults to &m_rlb_radio + SBandFaultPolicy m_faultPolicy; //!< Fault state machine (decision D3); see SBandFaultPolicy.hpp + bool m_configured = false; //!< Flag indicating radio is configured + bool m_faultEvrEmitted = false; //!< RadioFaultLatched is emitted once per FAULTED latch, not every tick + std::atomic m_rxHandlerQueued{false}; //!< Flag indicating RX handler is queued SBandTransmitState m_transmit_enabled = SBandTransmitState::DISABLED; //!< Transmit state }; From f11d4bab7b6e78bf9192927f1289f84200a6afca Mon Sep 17 00:00:00 2001 From: Michael Pham <61564344+Mikefly123@users.noreply.github.com> Date: Mon, 13 Jul 2026 02:15:14 -0700 Subject: [PATCH 8/8] docs(plan): record Slice 1.2 resolution and #109 correction MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Slice 1.2 resolved via investigation: assert path is loud for late-started tasks; fprime-zephyr gets a stack-alloc-failure log (separate PR); the early-component half-alive gap is pre-existing and filed separately. Also corrects the plan's assumption that issue #109 fault management was on main — it was never merged; PR 2 built the fault surface fresh. Co-Authored-By: Claude Fable 5 --- S-BAND-REINTEGRATION-PLAN.md | 34 +++++++++++++++++++++++++--------- 1 file changed, 25 insertions(+), 9 deletions(-) diff --git a/S-BAND-REINTEGRATION-PLAN.md b/S-BAND-REINTEGRATION-PLAN.md index b242e6be..da313188 100644 --- a/S-BAND-REINTEGRATION-PLAN.md +++ b/S-BAND-REINTEGRATION-PLAN.md @@ -5,8 +5,14 @@ that got it removed in PR #256 ("Remove SBand to protect RAM"), with every fix p by a test written first. **Related:** issue #122 (library can lock up / RAM concerns), issue #299 (stack -overflow analysis), PR #175 (original MVP), PR #256 (removal), PR #109 (radio fault -management / nRST reset), branch `s-band-speedup` (post-removal correctness fixes). +overflow analysis), PR #175 (original MVP), PR #256 (removal), branch +`s-band-speedup` (post-removal correctness fixes). + +> Correction (found during PR 2): issue #109's radio fault management +> (RESET_RADIO + auto-reset) was never merged to main — it lives only on the +> unmerged `radio-sensor-fault-manage` branch (2273dc3). PR 2 built the fault +> surface fresh per D3; that branch remains reference material for PR 3 bench +> work only. > Naming note: #122 says "RadioHead" but the flight code uses **RadioLib** > (`lib/RadioLib`, jgromes). Correct this when updating the issues. @@ -37,7 +43,7 @@ management / nRST reset), branch `s-band-speedup` (post-removal correctness fixe |---|----------| | D1 | Resurrect the existing RadioLib driver in place (no Zephyr-native rewrite, no com-chain async rework). Cherry-pick only *correctness* commits from `s-band-speedup`. | | D2 | SBand thread gets a dedicated 8 KB stack via `CONFIG_DYNAMIC_THREAD_ALLOC=y` fallback; boot-failure is loud (assert/FATAL); stack usage is *measured*, not assumed. | -| D3 | Failure semantics: bounded call → N consecutive errors/timeouts → auto nRST reset (reuse #109 path) → M failed resets → **FAULTED, latched until ground command**. Invariant: S-Band failure degrades to "no S-Band", never to backpressure, hang, or spacecraft reset. UHF unaffected. | +| D3 | Failure semantics: bounded call → N consecutive errors/timeouts → auto nRST reset → M failed resets → **FAULTED, latched until ground command**. Invariant: S-Band failure degrades to "no S-Band", never to backpressure, hang, or spacecraft reset. UHF unaffected. | | D4 | Test seam: new `SBandRadioIf` abstract interface over the 13 SX1280 calls the component uses; production impl wraps SX1280 and owns the bounded-timeout logic; F´ UTs drive the component against a scripted fake. | | D5 | Three staged PRs (infra → component+UTs → topology re-enable). PR 3 gated on ≥24 h dual-radio HWIL soak + end-to-end functional pass. | | D6 | Program-wide `StackMonitor` component (all threads, 1 Hz, `k_thread_foreach` + `k_thread_stack_space_get`, needs `CONFIG_THREAD_MONITOR=y`). | @@ -61,11 +67,21 @@ New passive component `Components/StackMonitor`, seam: `ThreadInfoProviderIf` 3. Handles thread count changing between ticks without asserting. ### Slice 1.2 — Loud boot failure on task-start error -Verify what `ActiveComponentBase`/`Os::Task` does today when -`k_thread_stack_alloc` returns null (`ERROR_RESOURCES`). If it can silently limp, -add an assert/FATAL. Behavior to pin (host UT where the Os layer permits, else -HWIL check in PR 3): a deployment whose thread can't get its stack never runs -half-alive. +**RESOLVED 2026-07-13 (investigated):** `ActiveComponentBase::start()` already +FW_ASSERTs on failure, but the assert hook (`AssertFatalAdapter`) logs a FATAL +event and *continues*; the reboot arrives indirectly via FatalHandler → +stop-watchdog-feed → external WDT starvation. For SBand (started late, event +machinery alive) this path is loud. Resolution: +- No change to global assert semantics (FATAL→watchdog-starve was chosen + deliberately in PR #259). +- fprime-zephyr PR (branch `fix/task-start-stack-alloc-logging`, 015c34a): + `ZephyrTask::start()` logs task name + requested size on stack-alloc failure + (compile-verified against this project). Submodule bump after upstream merge. +- PR 3 HWIL fault injection includes: impossible sband stack size → expect + FATAL EVR + watchdog reset, not a silent limp. +- Known pre-existing gap, filed separately (out of scope): if one of the *first* + components (`cmdDisp`, `events`) fails task-start, the FATAL is queued but + never dispatched → genuine half-alive boot. ### Slice 1.3 — Kconfig flips `CONFIG_DYNAMIC_THREAD_ALLOC=y`, `CONFIG_THREAD_MONITOR=y`. No new tests (config @@ -108,7 +124,7 @@ Fake returns an error / simulated timeout → call completes promptly, EVR emitt consecutive-error counter advances; one success resets the counter. ### Slice 2.6 — Auto-reset escalation -N consecutive failures → exactly one nRST reset request via the #109 path, EVR, +N consecutive failures → exactly one nRST reset request via the nRST GPIO, EVR, counters observable in telemetry. ### Slice 2.7 — FAULTED latch