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/") 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/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; 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 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 }; diff --git a/PROVESFlightControllerReference/Components/SBand/SBandFaultPolicy.cpp b/PROVESFlightControllerReference/Components/SBand/SBandFaultPolicy.cpp new file mode 100644 index 00000000..f35303ff --- /dev/null +++ b/PROVESFlightControllerReference/Components/SBand/SBandFaultPolicy.cpp @@ -0,0 +1,68 @@ +// ====================================================================== +// \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; +} + +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 new file mode 100644 index 00000000..35439b3e --- /dev/null +++ b/PROVESFlightControllerReference/Components/SBand/SBandFaultPolicy.hpp @@ -0,0 +1,90 @@ +// ====================================================================== +// \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; + + //! 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; + 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/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 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..a3836730 --- /dev/null +++ b/PROVESFlightControllerReference/test/unit-tests/test_SBand_FaultPolicy.cpp @@ -0,0 +1,131 @@ +#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); +} + +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); +} 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