From 93bac53d7fa051c2dffac26556b86641c4bc297e Mon Sep 17 00:00:00 2001 From: "Sam S. Yu" <25761223+yudataguy@users.noreply.github.com> Date: Tue, 25 Nov 2025 22:18:18 -0800 Subject: [PATCH 01/14] add payload mode --- .../Components/ModeManager/ModeManager.cpp | 158 ++++++++++- .../Components/ModeManager/ModeManager.fpp | 31 +++ .../Components/ModeManager/ModeManager.hpp | 42 ++- .../Top/ReferenceDeploymentPackets.fppi | 1 + .../test/int/payload_mode_test.py | 248 ++++++++++++++++++ Makefile | 13 +- README.md | 6 + 7 files changed, 484 insertions(+), 15 deletions(-) create mode 100644 FprimeZephyrReference/test/int/payload_mode_test.py diff --git a/FprimeZephyrReference/Components/ModeManager/ModeManager.cpp b/FprimeZephyrReference/Components/ModeManager/ModeManager.cpp index e697db4f..7a030858 100644 --- a/FprimeZephyrReference/Components/ModeManager/ModeManager.cpp +++ b/FprimeZephyrReference/Components/ModeManager/ModeManager.cpp @@ -18,7 +18,11 @@ namespace Components { // ---------------------------------------------------------------------- ModeManager ::ModeManager(const char* const compName) - : ModeManagerComponentBase(compName), m_mode(SystemMode::NORMAL), m_safeModeEntryCount(0), m_runCounter(0) {} + : ModeManagerComponentBase(compName), + m_mode(SystemMode::NORMAL), + m_safeModeEntryCount(0), + m_payloadModeEntryCount(0), + m_runCounter(0) {} ModeManager ::~ModeManager() {} @@ -44,11 +48,20 @@ void ModeManager ::forceSafeMode_handler(FwIndexType portNum) { // Provides immediate safe mode entry for critical component-detected faults this->log_WARNING_HI_ExternalFaultDetected(); - if (this->m_mode == SystemMode::NORMAL) { + // Allow safe mode entry from NORMAL or PAYLOAD_MODE (emergency override) + if (this->m_mode == SystemMode::NORMAL || this->m_mode == SystemMode::PAYLOAD_MODE) { this->enterSafeMode("External component request"); } } +void ModeManager ::forcePayloadMode_handler(FwIndexType portNum) { + // Force entry into payload mode (called by other components) + // Only allowed from NORMAL mode (not from SAFE_MODE) + if (this->m_mode == SystemMode::NORMAL) { + this->enterPayloadMode("External component request"); + } +} + Components::SystemMode ModeManager ::getMode_handler(FwIndexType portNum) { // Return the current system mode // Convert internal C++ enum to FPP-generated enum type @@ -63,7 +76,8 @@ void ModeManager ::FORCE_SAFE_MODE_cmdHandler(FwOpcodeType opCode, U32 cmdSeq) { // Force entry into safe mode this->log_ACTIVITY_HI_ManualSafeModeEntry(); - if (this->m_mode == SystemMode::NORMAL) { + // Allow safe mode entry from NORMAL or PAYLOAD_MODE (emergency override) + if (this->m_mode == SystemMode::NORMAL || this->m_mode == SystemMode::PAYLOAD_MODE) { this->enterSafeMode("Ground command"); } @@ -87,6 +101,48 @@ void ModeManager ::EXIT_SAFE_MODE_cmdHandler(FwOpcodeType opCode, U32 cmdSeq) { this->cmdResponse_out(opCode, cmdSeq, Fw::CmdResponse::OK); } +void ModeManager ::ENTER_PAYLOAD_MODE_cmdHandler(FwOpcodeType opCode, U32 cmdSeq) { + // Command to enter payload mode - only allowed from NORMAL mode + + // Check if currently in safe mode (not allowed) + if (this->m_mode == SystemMode::SAFE_MODE) { + Fw::LogStringArg cmdNameStr("ENTER_PAYLOAD_MODE"); + Fw::LogStringArg reasonStr("Cannot enter payload mode from safe mode"); + this->log_WARNING_LO_CommandValidationFailed(cmdNameStr, reasonStr); + this->cmdResponse_out(opCode, cmdSeq, Fw::CmdResponse::VALIDATION_ERROR); + return; + } + + // Check if already in payload mode + if (this->m_mode == SystemMode::PAYLOAD_MODE) { + // Already in payload mode - success (idempotent) + this->cmdResponse_out(opCode, cmdSeq, Fw::CmdResponse::OK); + return; + } + + // Enter payload mode + this->log_ACTIVITY_HI_ManualPayloadModeEntry(); + this->enterPayloadMode("Ground command"); + this->cmdResponse_out(opCode, cmdSeq, Fw::CmdResponse::OK); +} + +void ModeManager ::EXIT_PAYLOAD_MODE_cmdHandler(FwOpcodeType opCode, U32 cmdSeq) { + // Command to exit payload mode + + // Check if currently in payload mode + if (this->m_mode != SystemMode::PAYLOAD_MODE) { + Fw::LogStringArg cmdNameStr("EXIT_PAYLOAD_MODE"); + Fw::LogStringArg reasonStr("Not currently in payload mode"); + this->log_WARNING_LO_CommandValidationFailed(cmdNameStr, reasonStr); + this->cmdResponse_out(opCode, cmdSeq, Fw::CmdResponse::VALIDATION_ERROR); + return; + } + + // Exit payload mode + this->exitPayloadMode(); + this->cmdResponse_out(opCode, cmdSeq, Fw::CmdResponse::OK); +} + // ---------------------------------------------------------------------- // Private helper methods // ---------------------------------------------------------------------- @@ -103,10 +159,11 @@ void ModeManager ::loadState() { if (status == Os::File::OP_OK && bytesRead == sizeof(PersistentState)) { // Validate state data before restoring - if (state.mode <= static_cast(SystemMode::SAFE_MODE)) { + if (state.mode <= static_cast(SystemMode::PAYLOAD_MODE)) { // Valid mode value - restore state this->m_mode = static_cast(state.mode); this->m_safeModeEntryCount = state.safeModeEntryCount; + this->m_payloadModeEntryCount = state.payloadModeEntryCount; // Restore physical hardware state to match loaded mode if (this->m_mode == SystemMode::SAFE_MODE) { @@ -116,6 +173,13 @@ void ModeManager ::loadState() { // Log that we're restoring safe mode (not entering it fresh) Fw::LogStringArg reasonStr("State restored from persistent storage"); this->log_WARNING_HI_EnteringSafeMode(reasonStr); + } else if (this->m_mode == SystemMode::PAYLOAD_MODE) { + // PAYLOAD_MODE - turn on payload switches + this->turnOnPayload(); + + // Log that we're restoring payload mode + Fw::LogStringArg reasonStr("State restored from persistent storage"); + this->log_ACTIVITY_HI_EnteringPayloadMode(reasonStr); } else { // NORMAL mode - ensure components are turned on this->turnOnComponents(); @@ -124,6 +188,7 @@ void ModeManager ::loadState() { // Corrupted state - use defaults this->m_mode = SystemMode::NORMAL; this->m_safeModeEntryCount = 0; + this->m_payloadModeEntryCount = 0; this->turnOnComponents(); } } @@ -133,6 +198,7 @@ void ModeManager ::loadState() { // File doesn't exist or can't be opened - initialize to default state this->m_mode = SystemMode::NORMAL; this->m_safeModeEntryCount = 0; + this->m_payloadModeEntryCount = 0; this->turnOnComponents(); } } @@ -151,6 +217,7 @@ void ModeManager ::saveState() { PersistentState state; state.mode = static_cast(this->m_mode); state.safeModeEntryCount = this->m_safeModeEntryCount; + state.payloadModeEntryCount = this->m_payloadModeEntryCount; FwSizeType bytesToWrite = sizeof(PersistentState); FwSizeType bytesWritten = bytesToWrite; @@ -223,6 +290,62 @@ void ModeManager ::exitSafeMode() { this->saveState(); } +void ModeManager ::enterPayloadMode(const char* reasonOverride) { + // Transition to payload mode + this->m_mode = SystemMode::PAYLOAD_MODE; + this->m_payloadModeEntryCount++; + + // Build reason string + Fw::LogStringArg reasonStr; + char reasonBuf[100]; + if (reasonOverride != nullptr) { + reasonStr = reasonOverride; + } else { + snprintf(reasonBuf, sizeof(reasonBuf), "Unknown"); + reasonStr = reasonBuf; + } + + this->log_ACTIVITY_HI_EnteringPayloadMode(reasonStr); + + // Turn on payload switches (6 & 7) + this->turnOnPayload(); + + // Update telemetry + this->tlmWrite_CurrentMode(static_cast(this->m_mode)); + this->tlmWrite_PayloadModeEntryCount(this->m_payloadModeEntryCount); + + // Notify other components of mode change with new mode value + if (this->isConnected_modeChanged_OutputPort(0)) { + Components::SystemMode fppMode = static_cast(this->m_mode); + this->modeChanged_out(0, fppMode); + } + + // Save state + this->saveState(); +} + +void ModeManager ::exitPayloadMode() { + // Transition back to normal mode + this->m_mode = SystemMode::NORMAL; + + this->log_ACTIVITY_HI_ExitingPayloadMode(); + + // Turn off payload switches + this->turnOffPayload(); + + // Update telemetry + this->tlmWrite_CurrentMode(static_cast(this->m_mode)); + + // Notify other components of mode change with new mode value + if (this->isConnected_modeChanged_OutputPort(0)) { + Components::SystemMode fppMode = static_cast(this->m_mode); + this->modeChanged_out(0, fppMode); + } + + // Save state + this->saveState(); +} + void ModeManager ::turnOffNonCriticalComponents() { // Turn OFF: // - Satellite faces 0-5 (LoadSwitch instances 0-5) @@ -240,16 +363,37 @@ void ModeManager ::turnOffNonCriticalComponents() { } void ModeManager ::turnOnComponents() { - // Turn ON all load switches to restore normal operation + // Turn ON face load switches (0-5) to restore normal operation + // Note: Payload switches (6-7) are NOT turned on here - they require PAYLOAD_MODE - // Send turn on signal to all 8 load switches - for (FwIndexType i = 0; i < 8; i++) { + // Send turn on signal to face load switches only (indices 0-5) + for (FwIndexType i = 0; i < 6; i++) { if (this->isConnected_loadSwitchTurnOn_OutputPort(i)) { this->loadSwitchTurnOn_out(i); } } } +void ModeManager ::turnOnPayload() { + // Turn ON payload load switches (6 = payload power, 7 = payload battery) + if (this->isConnected_loadSwitchTurnOn_OutputPort(6)) { + this->loadSwitchTurnOn_out(6); + } + if (this->isConnected_loadSwitchTurnOn_OutputPort(7)) { + this->loadSwitchTurnOn_out(7); + } +} + +void ModeManager ::turnOffPayload() { + // Turn OFF payload load switches (6 = payload power, 7 = payload battery) + if (this->isConnected_loadSwitchTurnOff_OutputPort(6)) { + this->loadSwitchTurnOff_out(6); + } + if (this->isConnected_loadSwitchTurnOff_OutputPort(7)) { + this->loadSwitchTurnOff_out(7); + } +} + F32 ModeManager ::getCurrentVoltage(bool& valid) { // Call the voltage get port to get current system voltage if (this->isConnected_voltageGet_OutputPort(0)) { diff --git a/FprimeZephyrReference/Components/ModeManager/ModeManager.fpp b/FprimeZephyrReference/Components/ModeManager/ModeManager.fpp index 5f9fe612..f1fe6420 100644 --- a/FprimeZephyrReference/Components/ModeManager/ModeManager.fpp +++ b/FprimeZephyrReference/Components/ModeManager/ModeManager.fpp @@ -4,6 +4,7 @@ module Components { enum SystemMode { NORMAL = 0 @< Normal operational mode SAFE_MODE = 1 @< Safe mode with non-critical components powered off + PAYLOAD_MODE = 2 @< Payload mode with payload power and battery enabled } @ Port for notifying about mode changes @@ -26,6 +27,9 @@ module Components { @ Port to force safe mode entry (callable by other components) async input port forceSafeMode: Fw.Signal + @ Port to force payload mode entry (callable by other components) + async input port forcePayloadMode: Fw.Signal + @ Port to query the current system mode sync input port getMode: Components.GetSystemMode @@ -57,6 +61,14 @@ module Components { @ Only succeeds if currently in safe mode sync command EXIT_SAFE_MODE() + @ Command to enter payload mode + @ Only succeeds if currently in normal mode + sync command ENTER_PAYLOAD_MODE() + + @ Command to exit payload mode + @ Only succeeds if currently in payload mode + sync command EXIT_PAYLOAD_MODE() + # ---------------------------------------------------------------------- # Events # ---------------------------------------------------------------------- @@ -83,6 +95,22 @@ module Components { severity warning high \ format "External fault detected - external component forced safe mode" + @ Event emitted when entering payload mode + event EnteringPayloadMode( + reason: string size 100 @< Reason for entering payload mode + ) \ + severity activity high \ + format "ENTERING PAYLOAD MODE: {}" + + @ Event emitted when exiting payload mode + event ExitingPayloadMode() \ + severity activity high \ + format "Exiting payload mode" + + @ Event emitted when payload mode is manually commanded + event ManualPayloadModeEntry() \ + severity activity high \ + format "Payload mode entry commanded manually" @ Event emitted when command validation fails event CommandValidationFailed( @@ -110,6 +138,9 @@ module Components { @ Number of times safe mode has been entered telemetry SafeModeEntryCount: U32 + @ Number of times payload mode has been entered + telemetry PayloadModeEntryCount: U32 + ############################################################################### # Standard AC Ports: Required for Channels, Events, Commands, and Parameters # diff --git a/FprimeZephyrReference/Components/ModeManager/ModeManager.hpp b/FprimeZephyrReference/Components/ModeManager/ModeManager.hpp index a32878cb..73f4103d 100644 --- a/FprimeZephyrReference/Components/ModeManager/ModeManager.hpp +++ b/FprimeZephyrReference/Components/ModeManager/ModeManager.hpp @@ -50,6 +50,12 @@ class ModeManager : public ModeManagerComponentBase { void forceSafeMode_handler(FwIndexType portNum //!< The port number ) override; + //! Handler implementation for forcePayloadMode + //! + //! Port to force payload mode entry (callable by other components) + void forcePayloadMode_handler(FwIndexType portNum //!< The port number + ) override; + //! Handler implementation for getMode //! //! Port to query the current system mode @@ -70,6 +76,16 @@ class ModeManager : public ModeManagerComponentBase { U32 cmdSeq //!< The command sequence number ) override; + //! Handler implementation for command ENTER_PAYLOAD_MODE + void ENTER_PAYLOAD_MODE_cmdHandler(FwOpcodeType opCode, //!< The opcode + U32 cmdSeq //!< The command sequence number + ) override; + + //! Handler implementation for command EXIT_PAYLOAD_MODE + void EXIT_PAYLOAD_MODE_cmdHandler(FwOpcodeType opCode, //!< The opcode + U32 cmdSeq //!< The command sequence number + ) override; + private: // ---------------------------------------------------------------------- // Private helper methods @@ -87,9 +103,21 @@ class ModeManager : public ModeManagerComponentBase { //! Exit safe mode void exitSafeMode(); + //! Enter payload mode with optional reason override + void enterPayloadMode(const char* reason = nullptr); + + //! Exit payload mode + void exitPayloadMode(); + //! Turn off non-critical components void turnOffNonCriticalComponents(); + //! Turn on payload (load switches 6 & 7) + void turnOnPayload(); + + //! Turn off payload (load switches 6 & 7) + void turnOffPayload(); + //! Turn on components (restore normal operation) void turnOnComponents(); @@ -104,21 +132,23 @@ class ModeManager : public ModeManagerComponentBase { // ---------------------------------------------------------------------- //! System mode enumeration - enum class SystemMode : U8 { NORMAL = 0, SAFE_MODE = 1 }; + enum class SystemMode : U8 { NORMAL = 0, SAFE_MODE = 1, PAYLOAD_MODE = 2 }; //! Persistent state structure struct PersistentState { - U8 mode; //!< Current mode (SystemMode) - U32 safeModeEntryCount; //!< Number of times safe mode entered + U8 mode; //!< Current mode (SystemMode) + U32 safeModeEntryCount; //!< Number of times safe mode entered + U32 payloadModeEntryCount; //!< Number of times payload mode entered }; // ---------------------------------------------------------------------- // Private member variables // ---------------------------------------------------------------------- - SystemMode m_mode; //!< Current system mode - U32 m_safeModeEntryCount; //!< Counter for safe mode entries - U32 m_runCounter; //!< Counter for run handler calls (1Hz) + SystemMode m_mode; //!< Current system mode + U32 m_safeModeEntryCount; //!< Counter for safe mode entries + U32 m_payloadModeEntryCount; //!< Counter for payload mode entries + U32 m_runCounter; //!< Counter for run handler calls (1Hz) static constexpr const char* STATE_FILE_PATH = "/mode_state.bin"; //!< State file path }; diff --git a/FprimeZephyrReference/ReferenceDeployment/Top/ReferenceDeploymentPackets.fppi b/FprimeZephyrReference/ReferenceDeployment/Top/ReferenceDeploymentPackets.fppi index 472d9d6b..eaede325 100644 --- a/FprimeZephyrReference/ReferenceDeployment/Top/ReferenceDeploymentPackets.fppi +++ b/FprimeZephyrReference/ReferenceDeployment/Top/ReferenceDeploymentPackets.fppi @@ -13,6 +13,7 @@ telemetry packets ReferenceDeploymentPackets { ReferenceDeployment.startupManager.QuiescenceEndTime ReferenceDeployment.modeManager.CurrentMode ReferenceDeployment.modeManager.SafeModeEntryCount + ReferenceDeployment.modeManager.PayloadModeEntryCount } packet HealthWarnings id 2 group 1 { diff --git a/FprimeZephyrReference/test/int/payload_mode_test.py b/FprimeZephyrReference/test/int/payload_mode_test.py new file mode 100644 index 00000000..365ceb3c --- /dev/null +++ b/FprimeZephyrReference/test/int/payload_mode_test.py @@ -0,0 +1,248 @@ +""" +payload_mode_test.py: + +Integration tests for the ModeManager component (payload mode). + +Tests cover: +- Payload mode entry and exit +- Mode transition validation (cannot enter from safe mode) +- Emergency safe mode override from payload mode +- State persistence + +Total: 4 tests +""" + +import time + +import pytest +from common import proves_send_and_assert_command +from fprime_gds.common.data_types.ch_data import ChData +from fprime_gds.common.testing_fw.api import IntegrationTestAPI + +component = "ReferenceDeployment.modeManager" +payload_load_switch_channels = [ + "ReferenceDeployment.payloadPowerLoadSwitch.IsOn", + "ReferenceDeployment.payloadBatteryLoadSwitch.IsOn", +] +all_load_switch_channels = [ + "ReferenceDeployment.face4LoadSwitch.IsOn", + "ReferenceDeployment.face0LoadSwitch.IsOn", + "ReferenceDeployment.face1LoadSwitch.IsOn", + "ReferenceDeployment.face2LoadSwitch.IsOn", + "ReferenceDeployment.face3LoadSwitch.IsOn", + "ReferenceDeployment.face5LoadSwitch.IsOn", + "ReferenceDeployment.payloadPowerLoadSwitch.IsOn", + "ReferenceDeployment.payloadBatteryLoadSwitch.IsOn", +] + + +@pytest.fixture(autouse=True) +def setup_and_teardown(fprime_test_api: IntegrationTestAPI, start_gds): + """ + Setup before each test and cleanup after. + Ensures clean state by exiting any mode back to NORMAL. + """ + # Setup: Try to get to a clean NORMAL state + try: + proves_send_and_assert_command(fprime_test_api, f"{component}.EXIT_SAFE_MODE") + except Exception: + pass + try: + proves_send_and_assert_command( + fprime_test_api, f"{component}.EXIT_PAYLOAD_MODE" + ) + except Exception: + pass + + # Clear event and telemetry history before test + fprime_test_api.clear_histories() + + yield + + # Teardown: Return to clean NORMAL state for next test + try: + proves_send_and_assert_command(fprime_test_api, f"{component}.EXIT_SAFE_MODE") + except Exception: + pass + try: + proves_send_and_assert_command( + fprime_test_api, f"{component}.EXIT_PAYLOAD_MODE" + ) + except Exception: + pass + + +# ============================================================================== +# Payload Mode Tests +# ============================================================================== + + +def test_payload_01_enter_exit_payload_mode( + fprime_test_api: IntegrationTestAPI, start_gds +): + """ + Test ENTER_PAYLOAD_MODE and EXIT_PAYLOAD_MODE commands. + Verifies: + - EnteringPayloadMode event is emitted + - CurrentMode telemetry = 2 (PAYLOAD_MODE) + - Payload load switches (6 & 7) are ON + - ExitingPayloadMode event is emitted on exit + - CurrentMode returns to 0 (NORMAL) + """ + # Enter payload mode + proves_send_and_assert_command( + fprime_test_api, + f"{component}.ENTER_PAYLOAD_MODE", + events=[f"{component}.ManualPayloadModeEntry"], + ) + + time.sleep(2) + + # Verify mode is PAYLOAD_MODE (2) + proves_send_and_assert_command(fprime_test_api, "CdhCore.tlmSend.SEND_PKT", ["1"]) + mode_result: ChData = fprime_test_api.assert_telemetry( + f"{component}.CurrentMode", timeout=5 + ) + assert mode_result.get_val() == 2, "Should be in PAYLOAD_MODE" + + # Verify payload load switches are ON + proves_send_and_assert_command(fprime_test_api, "CdhCore.tlmSend.SEND_PKT", ["9"]) + for channel in payload_load_switch_channels: + value = fprime_test_api.assert_telemetry(channel, timeout=5).get_val() + if isinstance(value, str): + assert value.upper() == "ON", f"{channel} should be ON in payload mode" + else: + assert value == 1, f"{channel} should be ON in payload mode" + + # Exit payload mode + proves_send_and_assert_command( + fprime_test_api, + f"{component}.EXIT_PAYLOAD_MODE", + events=[f"{component}.ExitingPayloadMode"], + ) + + time.sleep(2) + + # Verify mode is NORMAL (0) + proves_send_and_assert_command(fprime_test_api, "CdhCore.tlmSend.SEND_PKT", ["1"]) + mode_result: ChData = fprime_test_api.assert_telemetry( + f"{component}.CurrentMode", timeout=5 + ) + assert mode_result.get_val() == 0, "Should be in NORMAL mode" + + # Verify payload load switches are OFF in NORMAL mode + proves_send_and_assert_command(fprime_test_api, "CdhCore.tlmSend.SEND_PKT", ["9"]) + for channel in payload_load_switch_channels: + value = fprime_test_api.assert_telemetry(channel, timeout=5).get_val() + if isinstance(value, str): + assert value.upper() == "OFF", f"{channel} should be OFF in NORMAL mode" + else: + assert value == 0, f"{channel} should be OFF in NORMAL mode" + + +def test_payload_02_cannot_enter_from_safe_mode( + fprime_test_api: IntegrationTestAPI, start_gds +): + """ + Test that ENTER_PAYLOAD_MODE fails when in safe mode. + Must exit safe mode to NORMAL first. + """ + # Enter safe mode first + proves_send_and_assert_command(fprime_test_api, f"{component}.FORCE_SAFE_MODE") + time.sleep(2) + + # Verify in safe mode + proves_send_and_assert_command(fprime_test_api, "CdhCore.tlmSend.SEND_PKT", ["1"]) + mode_result: ChData = fprime_test_api.assert_telemetry( + f"{component}.CurrentMode", timeout=5 + ) + assert mode_result.get_val() == 1, "Should be in SAFE_MODE" + + # Try to enter payload mode - should fail + fprime_test_api.clear_histories() + with pytest.raises(Exception): + proves_send_and_assert_command( + fprime_test_api, f"{component}.ENTER_PAYLOAD_MODE" + ) + + # Verify CommandValidationFailed event + fprime_test_api.assert_event(f"{component}.CommandValidationFailed", timeout=3) + + # Verify still in safe mode + proves_send_and_assert_command(fprime_test_api, "CdhCore.tlmSend.SEND_PKT", ["1"]) + mode_result: ChData = fprime_test_api.assert_telemetry( + f"{component}.CurrentMode", timeout=5 + ) + assert mode_result.get_val() == 1, "Should still be in SAFE_MODE" + + +def test_payload_03_safe_mode_override_from_payload( + fprime_test_api: IntegrationTestAPI, start_gds +): + """ + Test that FORCE_SAFE_MODE works from PAYLOAD_MODE (emergency override). + Verifies: + - Can enter safe mode from payload mode + - EnteringSafeMode event is emitted + - CurrentMode = 1 (SAFE_MODE) + - All load switches are OFF + """ + # Enter payload mode first + proves_send_and_assert_command(fprime_test_api, f"{component}.ENTER_PAYLOAD_MODE") + time.sleep(2) + + # Verify in payload mode + proves_send_and_assert_command(fprime_test_api, "CdhCore.tlmSend.SEND_PKT", ["1"]) + mode_result: ChData = fprime_test_api.assert_telemetry( + f"{component}.CurrentMode", timeout=5 + ) + assert mode_result.get_val() == 2, "Should be in PAYLOAD_MODE" + + # Force safe mode (emergency override) + fprime_test_api.clear_histories() + proves_send_and_assert_command( + fprime_test_api, + f"{component}.FORCE_SAFE_MODE", + events=[f"{component}.ManualSafeModeEntry"], + ) + + time.sleep(2) + + # Verify mode is SAFE_MODE (1) + proves_send_and_assert_command(fprime_test_api, "CdhCore.tlmSend.SEND_PKT", ["1"]) + mode_result: ChData = fprime_test_api.assert_telemetry( + f"{component}.CurrentMode", timeout=5 + ) + assert mode_result.get_val() == 1, "Should be in SAFE_MODE" + + # Verify all load switches are OFF + proves_send_and_assert_command(fprime_test_api, "CdhCore.tlmSend.SEND_PKT", ["9"]) + for channel in all_load_switch_channels: + value = fprime_test_api.assert_telemetry(channel, timeout=5).get_val() + if isinstance(value, str): + assert value.upper() == "OFF", f"{channel} should be OFF in safe mode" + else: + assert value == 0, f"{channel} should be OFF in safe mode" + + +def test_payload_04_state_persists(fprime_test_api: IntegrationTestAPI, start_gds): + """ + Test that payload mode state persists to /mode_state.bin file. + Note: Full persistence test would require reboot, this verifies state is saved. + """ + # Enter payload mode to trigger state save + proves_send_and_assert_command(fprime_test_api, f"{component}.ENTER_PAYLOAD_MODE") + time.sleep(2) + + # Verify mode is saved as PAYLOAD_MODE + proves_send_and_assert_command(fprime_test_api, "CdhCore.tlmSend.SEND_PKT", ["1"]) + mode_result: ChData = fprime_test_api.assert_telemetry( + f"{component}.CurrentMode", timeout=5 + ) + assert mode_result.get_val() == 2, "Mode should be saved as PAYLOAD_MODE" + + # Verify PayloadModeEntryCount is tracking + count_result: ChData = fprime_test_api.assert_telemetry( + f"{component}.PayloadModeEntryCount", timeout=5 + ) + assert count_result.get_val() >= 1, "PayloadModeEntryCount should be at least 1" diff --git a/Makefile b/Makefile index 1574e41a..9afd5561 100644 --- a/Makefile +++ b/Makefile @@ -53,8 +53,17 @@ build: submodules zephyr fprime-venv generate-if-needed ## Build FPrime-Zephyr P @$(UV_RUN) fprime-util build .PHONY: test-integration -test-integration: uv - @$(UV_RUN) pytest FprimeZephyrReference/test/int --deployment build-artifacts/zephyr/fprime-zephyr-deployment +test-integration: uv ## Run integration tests (set TEST= to run a single file) + @TARGET="FprimeZephyrReference/test/int"; \ + if [ -n "$(TEST)" ]; then \ + case "$(TEST)" in \ + *.py) TARGET="FprimeZephyrReference/test/int/$(TEST)" ;; \ + *) TARGET="FprimeZephyrReference/test/int/$(TEST).py" ;; \ + esac; \ + [ -e "$$TARGET" ] || { echo "Specified test file $$TARGET not found"; exit 1; }; \ + fi; \ + echo "Running integration tests at $$TARGET"; \ + $(UV_RUN) pytest $$TARGET --deployment build-artifacts/zephyr/fprime-zephyr-deployment .PHONY: bootloader bootloader: uv diff --git a/README.md b/README.md index d70a075e..82276357 100644 --- a/README.md +++ b/README.md @@ -73,6 +73,12 @@ Then, in another terminal, run the following command to execute the integration make test-integration ``` +To run a single integration test file, set `TEST` to the filename (with or without `.py`): +```sh +make test-integration TEST=mode_manager_test +make test-integration TEST=mode_manager_test.py +``` + ## Running The Radio With CircuitPython To test the radio setup easily, you can use CircuitPython code on one board and fprime-zephyr on another. This provides a simple client/server setup and lets you observe what data is being sent through the radio. From e02705520f971ce821af32cfd1bd2b8dca6a9ef2 Mon Sep 17 00:00:00 2001 From: "Sam S. Yu" <25761223+yudataguy@users.noreply.github.com> Date: Tue, 25 Nov 2025 22:25:16 -0800 Subject: [PATCH 02/14] update sdd --- .../Components/ModeManager/docs/sdd.md | 148 ++++++++++++++---- 1 file changed, 117 insertions(+), 31 deletions(-) diff --git a/FprimeZephyrReference/Components/ModeManager/docs/sdd.md b/FprimeZephyrReference/Components/ModeManager/docs/sdd.md index d3233d6e..4acd7ca9 100644 --- a/FprimeZephyrReference/Components/ModeManager/docs/sdd.md +++ b/FprimeZephyrReference/Components/ModeManager/docs/sdd.md @@ -1,19 +1,13 @@ # Components::ModeManager -The ModeManager component manages system operational modes and orchestrates transitions (NORMAL, SAFE_MODE, and planned PAYLOAD and HIBERNATION modes). It evaluates watchdog faults and communication timeouts to make mode decisions, controls power to non‑critical subsystems during transitions, and maintains/persists mode state across reboots to ensure consistent post‑recovery behavior. +The ModeManager component manages system operational modes and orchestrates transitions across NORMAL, SAFE_MODE, and PAYLOAD_MODE. It evaluates watchdog faults and communication timeouts to make mode decisions, controls power to non‑critical subsystems during transitions, and maintains/persists mode state across reboots to ensure consistent post‑recovery behavior. -Planned additions: -- PAYLOAD mode — A mid‑power operational mode that prioritizes payload activity while limiting non‑critical subsystems; intended for mission operations when power is constrained but payload operation must continue. -- HIBERNATION mode — A deep low‑power state for long‑term battery preservation, with strict wake conditions (e.g., RTC alarm, hardware wake interrupt); minimal subsystems remain active. -- ModeManager will extend its API and telemetry: new commands (ENTER/EXIT_PAYLOAD, ENTER/EXIT_HIBERNATION), an expanded SystemMode enumeration, modeChanged notifications reflecting new modes, and getMode support for queries. -- Persistence and validation: new modes will be persisted like existing modes, restored on boot, and have explicit validation for entry/exit (e.g., exit from HIBERNATION requires approved wake condition). - -These mode additions will be integrated incrementally with corresponding telemetry, events, commands, unit tests, and integration tests to verify correct behavior, power control, and persistence semantics. +Future work: a HIBERNATION mode remains planned; it will follow the same persistence and validation patterns once implemented. ## Requirements | Name | Description | Validation | |---|---|---| -| MM0001 | The ModeManager shall maintain two distinct operational modes: NORMAL and SAFE_MODE | Integration Testing | +| MM0001 | The ModeManager shall maintain three operational modes: NORMAL, SAFE_MODE, and PAYLOAD_MODE | Integration Testing | | MM0002 | The ModeManager shall enter safe mode when commanded manually via FORCE_SAFE_MODE command | Integration Testing | | MM0003 | The ModeManager shall enter safe mode when requested by external components via forceSafeMode port | Integration Testing | | MM0004 | The ModeManager shall exit safe mode only via explicit EXIT_SAFE_MODE command | Integration Testing | @@ -25,6 +19,13 @@ These mode additions will be integrated incrementally with corresponding telemet | MM0010 | The ModeManager shall track and report the number of times safe mode has been entered | Integration Testing | | MM0011 | The ModeManager shall allow downstream components to query the current mode via getMode port | Unit Testing | | MM0012 | The ModeManager shall notify downstream components of mode changes with the new mode value | Unit Testing | +| MM0013 | The ModeManager shall enter payload mode when commanded via ENTER_PAYLOAD_MODE while in NORMAL and reject entry from SAFE_MODE | Integration Testing | +| MM0014 | The ModeManager shall enter payload mode when requested by external components via forcePayloadMode port while in NORMAL | Integration Testing | +| MM0015 | The ModeManager shall exit payload mode only via explicit EXIT_PAYLOAD_MODE command and reject exit when not in payload mode | Integration Testing | +| MM0016 | The ModeManager shall turn on payload load switches (indices 6 and 7) when entering payload mode and turn them off when exiting payload mode | Integration Testing | +| MM0017 | The ModeManager shall track and report the number of times payload mode has been entered | Integration Testing | +| MM0018 | The ModeManager shall persist payload mode state and payload mode entry count to non-volatile storage and restore them on initialization | Integration Testing | +| MM0019 | The ModeManager shall allow FORCE_SAFE_MODE to override payload mode and transition to safe mode | Integration Testing | ## Usage Examples @@ -39,7 +40,7 @@ The ModeManager component operates as an active component that manages system-wi - Begins 1Hz periodic execution via rate group 2. **Normal Operation** - - Updates telemetry channels (CurrentMode, SafeModeEntryCount) + - Updates telemetry channels (CurrentMode, SafeModeEntryCount, PayloadModeEntryCount) - Responds to mode query requests from downstream components 3. **Safe Mode Entry** @@ -54,7 +55,31 @@ The ModeManager component operates as an active component that manages system-wi - Notifies downstream components via `modeChanged` port - Persists state to flash storage -4. **Safe Mode Exit** +4. **Payload Mode Entry** + - Can be triggered by: + - Ground command: `ENTER_PAYLOAD_MODE` (only allowed from NORMAL) + - External component request via `forcePayloadMode` port (only allowed from NORMAL) + - Actions performed: + - Transitions mode to PAYLOAD_MODE + - Increments payload mode entry counter + - Emits `EnteringPayloadMode` event and, for commands, `ManualPayloadModeEntry` + - Turns on payload load switches (indices 6 and 7) + - Notifies downstream components via `modeChanged` port + - Updates telemetry (CurrentMode, PayloadModeEntryCount) + - Persists state to flash storage + +5. **Payload Mode Exit** + - Triggered by ground command: `EXIT_PAYLOAD_MODE` + - Validates currently in payload mode before allowing exit + - Actions performed: + - Transitions mode to NORMAL + - Emits `ExitingPayloadMode` event + - Turns off payload load switches (indices 6 and 7) + - Notifies downstream components via `modeChanged` port + - Updates telemetry + - Persists state to flash storage + +6. **Safe Mode Exit** - Triggered only by ground command: `EXIT_SAFE_MODE` - Validates currently in safe mode before allowing exit - Actions performed: @@ -64,7 +89,7 @@ The ModeManager component operates as an active component that manages system-wi - Notifies downstream components via `modeChanged` port - Persists state to flash storage -5. **Mode Queries** +7. **Mode Queries** - Downstream components can call `getMode` port to query current mode - Returns immediate synchronous response with current mode @@ -80,6 +105,7 @@ classDiagram <> - m_mode: SystemMode - m_safeModeEntryCount: U32 + - m_payloadModeEntryCount: U32 - m_runCounter: U32 - STATE_FILE_PATH: const char* + ModeManager(const char* compName) @@ -87,21 +113,29 @@ classDiagram + init(FwSizeType queueDepth, FwEnumStoreType instance) - run_handler(FwIndexType portNum, U32 context) - forceSafeMode_handler(FwIndexType portNum) + - forcePayloadMode_handler(FwIndexType portNum) - getMode_handler(FwIndexType portNum): SystemMode - FORCE_SAFE_MODE_cmdHandler(FwOpcodeType opCode, U32 cmdSeq) - EXIT_SAFE_MODE_cmdHandler(FwOpcodeType opCode, U32 cmdSeq) + - ENTER_PAYLOAD_MODE_cmdHandler(FwOpcodeType opCode, U32 cmdSeq) + - EXIT_PAYLOAD_MODE_cmdHandler(FwOpcodeType opCode, U32 cmdSeq) - loadState() - saveState() - enterSafeMode(const char* reason) - exitSafeMode() + - enterPayloadMode(const char* reason) + - exitPayloadMode() - turnOffNonCriticalComponents() - turnOnComponents() + - turnOnPayload() + - turnOffPayload() - getCurrentVoltage(bool& valid): F32 } class SystemMode { <> NORMAL = 0 SAFE_MODE = 1 + PAYLOAD_MODE = 2 } } ModeManagerComponentBase <|-- ModeManager : inherits @@ -115,6 +149,7 @@ classDiagram |---|---|---|---| | run | Svc.Sched | sync | Receives periodic calls from rate group (1Hz) for telemetry updates | | forceSafeMode | Fw.Signal | async | Receives safe mode requests from external components detecting faults | +| forcePayloadMode | Fw.Signal | async | Receives payload mode requests from external components while in NORMAL | | getMode | Components.GetSystemMode | sync | Allows downstream components to query current system mode | ### Output Ports @@ -129,14 +164,16 @@ classDiagram | Name | Type | Description | |---|---|---| -| m_mode | SystemMode | Current operational mode (NORMAL or SAFE_MODE) | +| m_mode | SystemMode | Current operational mode (NORMAL, SAFE_MODE, or PAYLOAD_MODE) | | m_safeModeEntryCount | U32 | Number of times safe mode has been entered since initial deployment | +| m_payloadModeEntryCount | U32 | Number of times payload mode has been entered since initial deployment | | m_runCounter | U32 | Counter for 1Hz run handler calls | ### Persistent State The component persists the following state to `/mode_state.bin`: - Current mode (U8) - Safe mode entry count (U32) +- Payload mode entry count (U32) This state is loaded on initialization and saved on every mode transition. @@ -203,6 +240,48 @@ sequenceDiagram ModeManager->>Ground: Command response OK ``` +### Payload Mode Entry (Command) +```mermaid +sequenceDiagram + participant Ground + participant ModeManager + participant LoadSwitches + participant DownstreamComponents + participant FlashStorage + + Ground->>ModeManager: ENTER_PAYLOAD_MODE command + ModeManager->>ModeManager: Validate currently in NORMAL + ModeManager->>ModeManager: Emit ManualPayloadModeEntry event + ModeManager->>ModeManager: Set m_mode = PAYLOAD_MODE + ModeManager->>ModeManager: Increment m_payloadModeEntryCount + ModeManager->>ModeManager: Emit EnteringPayloadMode event + ModeManager->>LoadSwitches: Turn on payload switches (6 & 7) + ModeManager->>ModeManager: Update telemetry + ModeManager->>DownstreamComponents: modeChanged_out(PAYLOAD_MODE) + ModeManager->>FlashStorage: Save state to /mode_state.bin + ModeManager->>Ground: Command response OK +``` + +### Payload Mode Exit (Command) +```mermaid +sequenceDiagram + participant Ground + participant ModeManager + participant LoadSwitches + participant DownstreamComponents + participant FlashStorage + + Ground->>ModeManager: EXIT_PAYLOAD_MODE command + ModeManager->>ModeManager: Validate currently in PAYLOAD_MODE + ModeManager->>ModeManager: Set m_mode = NORMAL + ModeManager->>ModeManager: Emit ExitingPayloadMode event + ModeManager->>LoadSwitches: Turn off payload switches (6 & 7) + ModeManager->>ModeManager: Update telemetry + ModeManager->>DownstreamComponents: modeChanged_out(NORMAL) + ModeManager->>FlashStorage: Save state to /mode_state.bin + ModeManager->>Ground: Command response OK +``` + ### Mode Query ```mermaid sequenceDiagram @@ -211,7 +290,7 @@ sequenceDiagram DownstreamComponent->>ModeManager: getMode() port call ModeManager->>ModeManager: Read m_mode - ModeManager-->>DownstreamComponent: Return current mode (NORMAL or SAFE_MODE) + ModeManager-->>DownstreamComponent: Return current mode (NORMAL, SAFE_MODE, or PAYLOAD_MODE) ``` ### Periodic Execution (1Hz) @@ -231,6 +310,8 @@ sequenceDiagram |---|---|---| | FORCE_SAFE_MODE | None | Forces the system into safe mode immediately. Emits ManualSafeModeEntry event. Can be called from any mode (idempotent). | | EXIT_SAFE_MODE | None | Exits safe mode and returns to normal operation. Fails with CommandValidationFailed if not currently in safe mode. | +| ENTER_PAYLOAD_MODE | None | Enters payload mode from NORMAL. Fails with CommandValidationFailed if issued from SAFE_MODE or if already in payload mode (idempotent success when already in payload). Emits ManualPayloadModeEntry event. | +| EXIT_PAYLOAD_MODE | None | Exits payload mode and returns to normal operation. Fails with CommandValidationFailed if not currently in payload mode. | ## Events @@ -240,6 +321,9 @@ sequenceDiagram | ExitingSafeMode | ACTIVITY_HI | None | Emitted when exiting safe mode and returning to normal operation | | ManualSafeModeEntry | ACTIVITY_HI | None | Emitted when safe mode is manually commanded via FORCE_SAFE_MODE | | ExternalFaultDetected | WARNING_HI | None | Emitted when an external component triggers safe mode via forceSafeMode port | +| EnteringPayloadMode | ACTIVITY_HI | reason: string size 100 | Emitted when entering payload mode, includes reason (e.g., "Ground command", "External component request") | +| ExitingPayloadMode | ACTIVITY_HI | None | Emitted when exiting payload mode and returning to normal operation | +| ManualPayloadModeEntry | ACTIVITY_HI | None | Emitted when payload mode is manually commanded via ENTER_PAYLOAD_MODE | | CommandValidationFailed | WARNING_LO | cmdName: string size 50
reason: string size 100 | Emitted when a command fails validation (e.g., EXIT_SAFE_MODE when not in safe mode) | | StatePersistenceFailure | WARNING_LO | operation: string size 20
status: I32 | Emitted when state save/load operations fail | @@ -247,39 +331,40 @@ sequenceDiagram | Name | Type | Update Rate | Description | |---|---|---|---| -| CurrentMode | U8 | 1Hz | Current system mode (0 = NORMAL, 1 = SAFE_MODE) | +| CurrentMode | U8 | 1Hz | Current system mode (0 = NORMAL, 1 = SAFE_MODE, 2 = PAYLOAD_MODE) | | SafeModeEntryCount | U32 | On change | Number of times safe mode has been entered (persists across reboots) | +| PayloadModeEntryCount | U32 | On change | Number of times payload mode has been entered (persists across reboots) | ## Load Switch Mapping The ModeManager controls 8 load switches that power non-critical satellite subsystems: -| Index | Subsystem | Safe Mode State | -|---|---|---| -| 0 | Satellite Face 0 | OFF | -| 1 | Satellite Face 1 | OFF | -| 2 | Satellite Face 2 | OFF | -| 3 | Satellite Face 3 | OFF | -| 4 | Satellite Face 4 | OFF | -| 5 | Satellite Face 5 | OFF | -| 6 | Payload Power | OFF | -| 7 | Payload Battery | OFF | +| Index | Subsystem | Safe Mode State | Payload Mode State | +|---|---|---|---| +| 0 | Satellite Face 0 | OFF | ON | +| 1 | Satellite Face 1 | OFF | ON | +| 2 | Satellite Face 2 | OFF | ON | +| 3 | Satellite Face 3 | OFF | ON | +| 4 | Satellite Face 4 | OFF | ON | +| 5 | Satellite Face 5 | OFF | ON | +| 6 | Payload Power | OFF | ON | +| 7 | Payload Battery | OFF | ON | ## Integration Tests -See `FprimeZephyrReference/test/int/mode_manager_test.py` for comprehensive integration tests covering: +See `FprimeZephyrReference/test/int/mode_manager_test.py` and `FprimeZephyrReference/test/int/payload_mode_test.py` for comprehensive integration tests covering: | Test | Description | Coverage | |---|---|---| | test_01_initial_telemetry | Verifies initial telemetry can be read | Basic functionality | | test_04_force_safe_mode_command | Tests FORCE_SAFE_MODE command enters safe mode | Safe mode entry | -| test_05_safe_mode_increments_counter | Verifies SafeModeEntryCount increments | State tracking | | test_06_safe_mode_turns_off_load_switches | Verifies all load switches turn off in safe mode | Power management | -| test_07_safe_mode_emits_event | Verifies EnteringSafeMode event with correct reason | Event emission | -| test_13_exit_safe_mode_fails_not_in_safe_mode | Tests EXIT_SAFE_MODE validation | Error handling | | test_14_exit_safe_mode_success | Tests successful safe mode exit | Safe mode exit | -| test_18_force_safe_mode_idempotent | Tests FORCE_SAFE_MODE is idempotent | Edge cases | -| test_19_safe_mode_state_persists | Verifies mode persistence to flash | State persistence | +| test_19_safe_mode_state_persists | Verifies safe mode persistence to flash | State persistence | +| test_payload_01_enter_exit_payload_mode | Validates payload mode entry/exit, events, telemetry, payload load switches | Payload mode entry/exit | +| test_payload_02_cannot_enter_from_safe_mode | Ensures ENTER_PAYLOAD_MODE fails from SAFE_MODE | Command validation | +| test_payload_03_safe_mode_override_from_payload | Ensures FORCE_SAFE_MODE overrides payload mode | Emergency override | +| test_payload_04_state_persists | Verifies payload mode and counters persist | Payload persistence | ## Design Decisions @@ -312,4 +397,5 @@ The FORCE_SAFE_MODE command can be called from any mode without error. If alread ## Change Log | Date | Description | |---|---| +| 2026-02-18 | Added PAYLOAD_MODE (commands, events, telemetry, persistence, payload load switch control) and documented payload integration tests | | 2025-11-19 | Added getMode query port and enhanced modeChanged to carry mode value | From f8336e0d7e76ab4f7f14870ff190208c9a43e6c8 Mon Sep 17 00:00:00 2001 From: Sam Yu <25761223+yudataguy@users.noreply.github.com> Date: Tue, 25 Nov 2025 22:45:39 -0800 Subject: [PATCH 03/14] Update FprimeZephyrReference/Components/ModeManager/docs/sdd.md Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../Components/ModeManager/docs/sdd.md | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/FprimeZephyrReference/Components/ModeManager/docs/sdd.md b/FprimeZephyrReference/Components/ModeManager/docs/sdd.md index 4acd7ca9..e8ac389f 100644 --- a/FprimeZephyrReference/Components/ModeManager/docs/sdd.md +++ b/FprimeZephyrReference/Components/ModeManager/docs/sdd.md @@ -341,15 +341,16 @@ The ModeManager controls 8 load switches that power non-critical satellite subsy | Index | Subsystem | Safe Mode State | Payload Mode State | |---|---|---|---| -| 0 | Satellite Face 0 | OFF | ON | -| 1 | Satellite Face 1 | OFF | ON | -| 2 | Satellite Face 2 | OFF | ON | -| 3 | Satellite Face 3 | OFF | ON | -| 4 | Satellite Face 4 | OFF | ON | -| 5 | Satellite Face 5 | OFF | ON | +| 0 | Satellite Face 0 | OFF | PREV/OFF | +| 1 | Satellite Face 1 | OFF | PREV/OFF | +| 2 | Satellite Face 2 | OFF | PREV/OFF | +| 3 | Satellite Face 3 | OFF | PREV/OFF | +| 4 | Satellite Face 4 | OFF | PREV/OFF | +| 5 | Satellite Face 5 | OFF | PREV/OFF | | 6 | Payload Power | OFF | ON | | 7 | Payload Battery | OFF | ON | +> **Note:** In payload mode, only the payload switches (indices 6 & 7) are explicitly turned ON. The state of face switches (0-5) depends on their state prior to entering payload mode (typically OFF after safe mode). See requirement MM0016 and implementation for details. ## Integration Tests See `FprimeZephyrReference/test/int/mode_manager_test.py` and `FprimeZephyrReference/test/int/payload_mode_test.py` for comprehensive integration tests covering: From 2809b15fe581f6747285b568cf3c6356c42b7bf4 Mon Sep 17 00:00:00 2001 From: Sam Yu <25761223+yudataguy@users.noreply.github.com> Date: Tue, 25 Nov 2025 22:50:24 -0800 Subject: [PATCH 04/14] Update FprimeZephyrReference/Components/ModeManager/docs/sdd.md Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- FprimeZephyrReference/Components/ModeManager/docs/sdd.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/FprimeZephyrReference/Components/ModeManager/docs/sdd.md b/FprimeZephyrReference/Components/ModeManager/docs/sdd.md index e8ac389f..6eb03bbb 100644 --- a/FprimeZephyrReference/Components/ModeManager/docs/sdd.md +++ b/FprimeZephyrReference/Components/ModeManager/docs/sdd.md @@ -398,5 +398,5 @@ The FORCE_SAFE_MODE command can be called from any mode without error. If alread ## Change Log | Date | Description | |---|---| -| 2026-02-18 | Added PAYLOAD_MODE (commands, events, telemetry, persistence, payload load switch control) and documented payload integration tests | +| 2025-11-20 | Added PAYLOAD_MODE (commands, events, telemetry, persistence, payload load switch control) and documented payload integration tests | | 2025-11-19 | Added getMode query port and enhanced modeChanged to carry mode value | From 1e8d449b088c770f1640b6c47834ae587d5b2a81 Mon Sep 17 00:00:00 2001 From: "Sam S. Yu" <25761223+yudataguy@users.noreply.github.com> Date: Tue, 25 Nov 2025 22:51:29 -0800 Subject: [PATCH 05/14] fix restore payload - face issues, update sdd --- .../Components/ModeManager/ModeManager.cpp | 9 +++++-- .../Components/ModeManager/docs/sdd.md | 27 ++++++++++--------- 2 files changed, 21 insertions(+), 15 deletions(-) diff --git a/FprimeZephyrReference/Components/ModeManager/ModeManager.cpp b/FprimeZephyrReference/Components/ModeManager/ModeManager.cpp index 7a030858..2d1b1014 100644 --- a/FprimeZephyrReference/Components/ModeManager/ModeManager.cpp +++ b/FprimeZephyrReference/Components/ModeManager/ModeManager.cpp @@ -174,8 +174,9 @@ void ModeManager ::loadState() { Fw::LogStringArg reasonStr("State restored from persistent storage"); this->log_WARNING_HI_EnteringSafeMode(reasonStr); } else if (this->m_mode == SystemMode::PAYLOAD_MODE) { - // PAYLOAD_MODE - turn on payload switches - this->turnOnPayload(); + // PAYLOAD_MODE - turn on face switches AND payload switches + this->turnOnComponents(); // Face switches (0-5) + this->turnOnPayload(); // Payload switches (6-7) // Log that we're restoring payload mode Fw::LogStringArg reasonStr("State restored from persistent storage"); @@ -333,6 +334,10 @@ void ModeManager ::exitPayloadMode() { // Turn off payload switches this->turnOffPayload(); + // Ensure face switches (0-5) are ON for NORMAL mode + // This guarantees consistent state regardless of transition path + this->turnOnComponents(); + // Update telemetry this->tlmWrite_CurrentMode(static_cast(this->m_mode)); diff --git a/FprimeZephyrReference/Components/ModeManager/docs/sdd.md b/FprimeZephyrReference/Components/ModeManager/docs/sdd.md index 4acd7ca9..f19d500e 100644 --- a/FprimeZephyrReference/Components/ModeManager/docs/sdd.md +++ b/FprimeZephyrReference/Components/ModeManager/docs/sdd.md @@ -13,7 +13,7 @@ Future work: a HIBERNATION mode remains planned; it will follow the same persist | MM0004 | The ModeManager shall exit safe mode only via explicit EXIT_SAFE_MODE command | Integration Testing | | MM0005 | The ModeManager shall prevent exit from safe mode when not currently in safe mode | Integration Testing | | MM0006 | The ModeManager shall turn off all 8 load switches when entering safe mode | Integration Testing | -| MM0007 | The ModeManager shall turn on all 8 load switches when exiting safe mode | Integration Testing | +| MM0007 | The ModeManager shall turn on face load switches (indices 0-5) when exiting safe mode; payload switches remain off until payload mode | Integration Testing | | MM0008 | The ModeManager shall persist current mode and safe mode entry count to non-volatile storage | Integration Testing | | MM0009 | The ModeManager shall restore mode state from persistent storage on initialization | Integration Testing | | MM0010 | The ModeManager shall track and report the number of times safe mode has been entered | Integration Testing | @@ -42,6 +42,7 @@ The ModeManager component operates as an active component that manages system-wi 2. **Normal Operation** - Updates telemetry channels (CurrentMode, SafeModeEntryCount, PayloadModeEntryCount) - Responds to mode query requests from downstream components + - Keeps payload load switches (indices 6 and 7) off unless payload mode is explicitly entered 3. **Safe Mode Entry** - Can be triggered by: @@ -85,7 +86,7 @@ The ModeManager component operates as an active component that manages system-wi - Actions performed: - Transitions mode to NORMAL - Emits `ExitingSafeMode` event - - Turns on all 8 load switches + - Turns on face load switches (indices 0-5); payload switches remain off until explicitly entering payload mode - Notifies downstream components via `modeChanged` port - Persists state to flash storage @@ -233,7 +234,7 @@ sequenceDiagram ModeManager->>ModeManager: Validate currently in SAFE_MODE ModeManager->>ModeManager: Set m_mode = NORMAL ModeManager->>ModeManager: Emit ExitingSafeMode event - ModeManager->>LoadSwitches: Turn on all 8 switches + ModeManager->>LoadSwitches: Turn on face switches (indices 0-5) ModeManager->>ModeManager: Update telemetry ModeManager->>DownstreamComponents: modeChanged_out(NORMAL) ModeManager->>FlashStorage: Save state to /mode_state.bin @@ -339,16 +340,16 @@ sequenceDiagram The ModeManager controls 8 load switches that power non-critical satellite subsystems: -| Index | Subsystem | Safe Mode State | Payload Mode State | -|---|---|---|---| -| 0 | Satellite Face 0 | OFF | ON | -| 1 | Satellite Face 1 | OFF | ON | -| 2 | Satellite Face 2 | OFF | ON | -| 3 | Satellite Face 3 | OFF | ON | -| 4 | Satellite Face 4 | OFF | ON | -| 5 | Satellite Face 5 | OFF | ON | -| 6 | Payload Power | OFF | ON | -| 7 | Payload Battery | OFF | ON | +| Index | Subsystem | Normal State | Safe Mode State | Payload Mode State | +|---|---|---|---|---| +| 0 | Satellite Face 0 | ON | OFF | ON | +| 1 | Satellite Face 1 | ON | OFF | ON | +| 2 | Satellite Face 2 | ON | OFF | ON | +| 3 | Satellite Face 3 | ON | OFF | ON | +| 4 | Satellite Face 4 | ON | OFF | ON | +| 5 | Satellite Face 5 | ON | OFF | ON | +| 6 | Payload Power | OFF | OFF | ON | +| 7 | Payload Battery | OFF | OFF | ON | ## Integration Tests From 62a100c45e8249b5f26344d74327d60d635b3af0 Mon Sep 17 00:00:00 2001 From: "Sam S. Yu" <25761223+yudataguy@users.noreply.github.com> Date: Tue, 25 Nov 2025 22:55:26 -0800 Subject: [PATCH 06/14] consistency --- .../Components/ModeManager/docs/sdd.md | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/FprimeZephyrReference/Components/ModeManager/docs/sdd.md b/FprimeZephyrReference/Components/ModeManager/docs/sdd.md index 5b95481e..5bd0c267 100644 --- a/FprimeZephyrReference/Components/ModeManager/docs/sdd.md +++ b/FprimeZephyrReference/Components/ModeManager/docs/sdd.md @@ -340,18 +340,18 @@ sequenceDiagram The ModeManager controls 8 load switches that power non-critical satellite subsystems: -| Index | Subsystem | Safe Mode State | Payload Mode State | -|---|---|---|---| -| 0 | Satellite Face 0 | OFF | PREV/OFF | -| 1 | Satellite Face 1 | OFF | PREV/OFF | -| 2 | Satellite Face 2 | OFF | PREV/OFF | -| 3 | Satellite Face 3 | OFF | PREV/OFF | -| 4 | Satellite Face 4 | OFF | PREV/OFF | -| 5 | Satellite Face 5 | OFF | PREV/OFF | -| 6 | Payload Power | OFF | ON | -| 7 | Payload Battery | OFF | ON | - -> **Note:** In payload mode, only the payload switches (indices 6 & 7) are explicitly turned ON. The state of face switches (0-5) depends on their state prior to entering payload mode (typically OFF after safe mode). See requirement MM0016 and implementation for details. +| Index | Subsystem | NORMAL State | SAFE_MODE State | PAYLOAD_MODE State | +|---|---|---|---|---| +| 0 | Satellite Face 0 | ON | OFF | ON | +| 1 | Satellite Face 1 | ON | OFF | ON | +| 2 | Satellite Face 2 | ON | OFF | ON | +| 3 | Satellite Face 3 | ON | OFF | ON | +| 4 | Satellite Face 4 | ON | OFF | ON | +| 5 | Satellite Face 5 | ON | OFF | ON | +| 6 | Payload Power | OFF | OFF | ON | +| 7 | Payload Battery | OFF | OFF | ON | + +> **Note:** PAYLOAD_MODE can only be entered from NORMAL mode (not from SAFE_MODE). When restoring PAYLOAD_MODE from persistent storage after a reboot, both face switches (0-5) and payload switches (6-7) are explicitly turned ON to ensure consistent state. ## Integration Tests See `FprimeZephyrReference/test/int/mode_manager_test.py` and `FprimeZephyrReference/test/int/payload_mode_test.py` for comprehensive integration tests covering: @@ -399,5 +399,5 @@ The FORCE_SAFE_MODE command can be called from any mode without error. If alread ## Change Log | Date | Description | |---|---| -| 2025-11-20 | Added PAYLOAD_MODE (commands, events, telemetry, persistence, payload load switch control) and documented payload integration tests | +| 2025-11-25 | Added PAYLOAD_MODE (commands, events, telemetry, persistence, payload load switch control) and documented payload integration tests | | 2025-11-19 | Added getMode query port and enhanced modeChanged to carry mode value | From 05ff516f88cb393a35d5db454a6501ea0c609c25 Mon Sep 17 00:00:00 2001 From: "Sam S. Yu" <25761223+yudataguy@users.noreply.github.com> Date: Wed, 26 Nov 2025 14:53:51 -0800 Subject: [PATCH 07/14] remove component calling safe mode --- .../Components/ModeManager/ModeManager.cpp | 8 -------- .../Components/ModeManager/ModeManager.fpp | 3 --- .../Components/ModeManager/ModeManager.hpp | 6 ------ .../Components/ModeManager/docs/sdd.md | 12 ++++-------- 4 files changed, 4 insertions(+), 25 deletions(-) diff --git a/FprimeZephyrReference/Components/ModeManager/ModeManager.cpp b/FprimeZephyrReference/Components/ModeManager/ModeManager.cpp index 2d1b1014..ca672a2b 100644 --- a/FprimeZephyrReference/Components/ModeManager/ModeManager.cpp +++ b/FprimeZephyrReference/Components/ModeManager/ModeManager.cpp @@ -54,14 +54,6 @@ void ModeManager ::forceSafeMode_handler(FwIndexType portNum) { } } -void ModeManager ::forcePayloadMode_handler(FwIndexType portNum) { - // Force entry into payload mode (called by other components) - // Only allowed from NORMAL mode (not from SAFE_MODE) - if (this->m_mode == SystemMode::NORMAL) { - this->enterPayloadMode("External component request"); - } -} - Components::SystemMode ModeManager ::getMode_handler(FwIndexType portNum) { // Return the current system mode // Convert internal C++ enum to FPP-generated enum type diff --git a/FprimeZephyrReference/Components/ModeManager/ModeManager.fpp b/FprimeZephyrReference/Components/ModeManager/ModeManager.fpp index f1fe6420..c65dd493 100644 --- a/FprimeZephyrReference/Components/ModeManager/ModeManager.fpp +++ b/FprimeZephyrReference/Components/ModeManager/ModeManager.fpp @@ -27,9 +27,6 @@ module Components { @ Port to force safe mode entry (callable by other components) async input port forceSafeMode: Fw.Signal - @ Port to force payload mode entry (callable by other components) - async input port forcePayloadMode: Fw.Signal - @ Port to query the current system mode sync input port getMode: Components.GetSystemMode diff --git a/FprimeZephyrReference/Components/ModeManager/ModeManager.hpp b/FprimeZephyrReference/Components/ModeManager/ModeManager.hpp index 73f4103d..79862387 100644 --- a/FprimeZephyrReference/Components/ModeManager/ModeManager.hpp +++ b/FprimeZephyrReference/Components/ModeManager/ModeManager.hpp @@ -50,12 +50,6 @@ class ModeManager : public ModeManagerComponentBase { void forceSafeMode_handler(FwIndexType portNum //!< The port number ) override; - //! Handler implementation for forcePayloadMode - //! - //! Port to force payload mode entry (callable by other components) - void forcePayloadMode_handler(FwIndexType portNum //!< The port number - ) override; - //! Handler implementation for getMode //! //! Port to query the current system mode diff --git a/FprimeZephyrReference/Components/ModeManager/docs/sdd.md b/FprimeZephyrReference/Components/ModeManager/docs/sdd.md index 5bd0c267..3e9d83d5 100644 --- a/FprimeZephyrReference/Components/ModeManager/docs/sdd.md +++ b/FprimeZephyrReference/Components/ModeManager/docs/sdd.md @@ -20,8 +20,7 @@ Future work: a HIBERNATION mode remains planned; it will follow the same persist | MM0011 | The ModeManager shall allow downstream components to query the current mode via getMode port | Unit Testing | | MM0012 | The ModeManager shall notify downstream components of mode changes with the new mode value | Unit Testing | | MM0013 | The ModeManager shall enter payload mode when commanded via ENTER_PAYLOAD_MODE while in NORMAL and reject entry from SAFE_MODE | Integration Testing | -| MM0014 | The ModeManager shall enter payload mode when requested by external components via forcePayloadMode port while in NORMAL | Integration Testing | -| MM0015 | The ModeManager shall exit payload mode only via explicit EXIT_PAYLOAD_MODE command and reject exit when not in payload mode | Integration Testing | +| MM0014 | The ModeManager shall exit payload mode only via explicit EXIT_PAYLOAD_MODE command and reject exit when not in payload mode | Integration Testing | | MM0016 | The ModeManager shall turn on payload load switches (indices 6 and 7) when entering payload mode and turn them off when exiting payload mode | Integration Testing | | MM0017 | The ModeManager shall track and report the number of times payload mode has been entered | Integration Testing | | MM0018 | The ModeManager shall persist payload mode state and payload mode entry count to non-volatile storage and restore them on initialization | Integration Testing | @@ -57,9 +56,7 @@ The ModeManager component operates as an active component that manages system-wi - Persists state to flash storage 4. **Payload Mode Entry** - - Can be triggered by: - - Ground command: `ENTER_PAYLOAD_MODE` (only allowed from NORMAL) - - External component request via `forcePayloadMode` port (only allowed from NORMAL) + - Triggered by ground command: `ENTER_PAYLOAD_MODE` (only allowed from NORMAL) - Actions performed: - Transitions mode to PAYLOAD_MODE - Increments payload mode entry counter @@ -114,7 +111,6 @@ classDiagram + init(FwSizeType queueDepth, FwEnumStoreType instance) - run_handler(FwIndexType portNum, U32 context) - forceSafeMode_handler(FwIndexType portNum) - - forcePayloadMode_handler(FwIndexType portNum) - getMode_handler(FwIndexType portNum): SystemMode - FORCE_SAFE_MODE_cmdHandler(FwOpcodeType opCode, U32 cmdSeq) - EXIT_SAFE_MODE_cmdHandler(FwOpcodeType opCode, U32 cmdSeq) @@ -150,7 +146,6 @@ classDiagram |---|---|---|---| | run | Svc.Sched | sync | Receives periodic calls from rate group (1Hz) for telemetry updates | | forceSafeMode | Fw.Signal | async | Receives safe mode requests from external components detecting faults | -| forcePayloadMode | Fw.Signal | async | Receives payload mode requests from external components while in NORMAL | | getMode | Components.GetSystemMode | sync | Allows downstream components to query current system mode | ### Output Ports @@ -322,7 +317,7 @@ sequenceDiagram | ExitingSafeMode | ACTIVITY_HI | None | Emitted when exiting safe mode and returning to normal operation | | ManualSafeModeEntry | ACTIVITY_HI | None | Emitted when safe mode is manually commanded via FORCE_SAFE_MODE | | ExternalFaultDetected | WARNING_HI | None | Emitted when an external component triggers safe mode via forceSafeMode port | -| EnteringPayloadMode | ACTIVITY_HI | reason: string size 100 | Emitted when entering payload mode, includes reason (e.g., "Ground command", "External component request") | +| EnteringPayloadMode | ACTIVITY_HI | reason: string size 100 | Emitted when entering payload mode, includes reason (e.g., "Ground command") | | ExitingPayloadMode | ACTIVITY_HI | None | Emitted when exiting payload mode and returning to normal operation | | ManualPayloadModeEntry | ACTIVITY_HI | None | Emitted when payload mode is manually commanded via ENTER_PAYLOAD_MODE | | CommandValidationFailed | WARNING_LO | cmdName: string size 50
reason: string size 100 | Emitted when a command fails validation (e.g., EXIT_SAFE_MODE when not in safe mode) | @@ -399,5 +394,6 @@ The FORCE_SAFE_MODE command can be called from any mode without error. If alread ## Change Log | Date | Description | |---|---| +| 2025-11-26 | Removed forcePayloadMode port - payload mode now only entered via ENTER_PAYLOAD_MODE ground command | | 2025-11-25 | Added PAYLOAD_MODE (commands, events, telemetry, persistence, payload load switch control) and documented payload integration tests | | 2025-11-19 | Added getMode query port and enhanced modeChanged to carry mode value | From fddb10f8348db23114dbe5751b7a226c874d0042 Mon Sep 17 00:00:00 2001 From: "Sam S. Yu" <25761223+yudataguy@users.noreply.github.com> Date: Wed, 26 Nov 2025 21:35:43 -0800 Subject: [PATCH 08/14] change mode enum values --- .../Components/ModeManager/ModeManager.cpp | 34 +++++++---- .../Components/ModeManager/ModeManager.fpp | 6 +- .../Components/ModeManager/ModeManager.hpp | 4 +- .../Components/ModeManager/docs/sdd.md | 19 +++--- .../test/int/mode_manager_test.py | 35 +++++------ .../test/int/payload_mode_test.py | 59 ++++++++----------- 6 files changed, 79 insertions(+), 78 deletions(-) diff --git a/FprimeZephyrReference/Components/ModeManager/ModeManager.cpp b/FprimeZephyrReference/Components/ModeManager/ModeManager.cpp index ca672a2b..8295fe25 100644 --- a/FprimeZephyrReference/Components/ModeManager/ModeManager.cpp +++ b/FprimeZephyrReference/Components/ModeManager/ModeManager.cpp @@ -45,13 +45,14 @@ void ModeManager ::run_handler(FwIndexType portNum, U32 context) { void ModeManager ::forceSafeMode_handler(FwIndexType portNum) { // Force entry into safe mode (called by other components) - // Provides immediate safe mode entry for critical component-detected faults + // Only allowed from NORMAL (sequential +1/-1 transitions) this->log_WARNING_HI_ExternalFaultDetected(); - // Allow safe mode entry from NORMAL or PAYLOAD_MODE (emergency override) - if (this->m_mode == SystemMode::NORMAL || this->m_mode == SystemMode::PAYLOAD_MODE) { + // Only enter safe mode from NORMAL - payload mode must be exited first + if (this->m_mode == SystemMode::NORMAL) { this->enterSafeMode("External component request"); } + // Note: Request ignored if in PAYLOAD_MODE or already in SAFE_MODE } Components::SystemMode ModeManager ::getMode_handler(FwIndexType portNum) { @@ -65,14 +66,26 @@ Components::SystemMode ModeManager ::getMode_handler(FwIndexType portNum) { // ---------------------------------------------------------------------- void ModeManager ::FORCE_SAFE_MODE_cmdHandler(FwOpcodeType opCode, U32 cmdSeq) { - // Force entry into safe mode - this->log_ACTIVITY_HI_ManualSafeModeEntry(); + // Force entry into safe mode - only allowed from NORMAL (sequential +1/-1 transitions) - // Allow safe mode entry from NORMAL or PAYLOAD_MODE (emergency override) - if (this->m_mode == SystemMode::NORMAL || this->m_mode == SystemMode::PAYLOAD_MODE) { - this->enterSafeMode("Ground command"); + // Reject if in payload mode - must exit payload mode first + if (this->m_mode == SystemMode::PAYLOAD_MODE) { + Fw::LogStringArg cmdNameStr("FORCE_SAFE_MODE"); + Fw::LogStringArg reasonStr("Must exit payload mode first"); + this->log_WARNING_LO_CommandValidationFailed(cmdNameStr, reasonStr); + this->cmdResponse_out(opCode, cmdSeq, Fw::CmdResponse::VALIDATION_ERROR); + return; } + // Already in safe mode - idempotent success + if (this->m_mode == SystemMode::SAFE_MODE) { + this->cmdResponse_out(opCode, cmdSeq, Fw::CmdResponse::OK); + return; + } + + // Enter safe mode from NORMAL + this->log_ACTIVITY_HI_ManualSafeModeEntry(); + this->enterSafeMode("Ground command"); this->cmdResponse_out(opCode, cmdSeq, Fw::CmdResponse::OK); } @@ -150,8 +163,9 @@ void ModeManager ::loadState() { status = file.read(reinterpret_cast(&state), bytesRead, Os::File::WaitType::WAIT); if (status == Os::File::OP_OK && bytesRead == sizeof(PersistentState)) { - // Validate state data before restoring - if (state.mode <= static_cast(SystemMode::PAYLOAD_MODE)) { + // Validate state data before restoring (valid range: 1-3 for SAFE, NORMAL, PAYLOAD) + if (state.mode >= static_cast(SystemMode::SAFE_MODE) && + state.mode <= static_cast(SystemMode::PAYLOAD_MODE)) { // Valid mode value - restore state this->m_mode = static_cast(state.mode); this->m_safeModeEntryCount = state.safeModeEntryCount; diff --git a/FprimeZephyrReference/Components/ModeManager/ModeManager.fpp b/FprimeZephyrReference/Components/ModeManager/ModeManager.fpp index c65dd493..09e73743 100644 --- a/FprimeZephyrReference/Components/ModeManager/ModeManager.fpp +++ b/FprimeZephyrReference/Components/ModeManager/ModeManager.fpp @@ -1,10 +1,10 @@ module Components { - @ System mode enumeration + @ System mode enumeration (values ordered for +1/-1 sequential transitions) enum SystemMode { - NORMAL = 0 @< Normal operational mode SAFE_MODE = 1 @< Safe mode with non-critical components powered off - PAYLOAD_MODE = 2 @< Payload mode with payload power and battery enabled + NORMAL = 2 @< Normal operational mode + PAYLOAD_MODE = 3 @< Payload mode with payload power and battery enabled } @ Port for notifying about mode changes diff --git a/FprimeZephyrReference/Components/ModeManager/ModeManager.hpp b/FprimeZephyrReference/Components/ModeManager/ModeManager.hpp index 79862387..a4b9b9b5 100644 --- a/FprimeZephyrReference/Components/ModeManager/ModeManager.hpp +++ b/FprimeZephyrReference/Components/ModeManager/ModeManager.hpp @@ -125,8 +125,8 @@ class ModeManager : public ModeManagerComponentBase { // Private enums and types // ---------------------------------------------------------------------- - //! System mode enumeration - enum class SystemMode : U8 { NORMAL = 0, SAFE_MODE = 1, PAYLOAD_MODE = 2 }; + //! System mode enumeration (values ordered for +1/-1 sequential transitions) + enum class SystemMode : U8 { SAFE_MODE = 1, NORMAL = 2, PAYLOAD_MODE = 3 }; //! Persistent state structure struct PersistentState { diff --git a/FprimeZephyrReference/Components/ModeManager/docs/sdd.md b/FprimeZephyrReference/Components/ModeManager/docs/sdd.md index 3e9d83d5..22d80144 100644 --- a/FprimeZephyrReference/Components/ModeManager/docs/sdd.md +++ b/FprimeZephyrReference/Components/ModeManager/docs/sdd.md @@ -24,7 +24,7 @@ Future work: a HIBERNATION mode remains planned; it will follow the same persist | MM0016 | The ModeManager shall turn on payload load switches (indices 6 and 7) when entering payload mode and turn them off when exiting payload mode | Integration Testing | | MM0017 | The ModeManager shall track and report the number of times payload mode has been entered | Integration Testing | | MM0018 | The ModeManager shall persist payload mode state and payload mode entry count to non-volatile storage and restore them on initialization | Integration Testing | -| MM0019 | The ModeManager shall allow FORCE_SAFE_MODE to override payload mode and transition to safe mode | Integration Testing | +| MM0019 | The ModeManager shall reject FORCE_SAFE_MODE from payload mode (must exit payload mode first for sequential transitions) | Integration Testing | ## Usage Examples @@ -44,7 +44,7 @@ The ModeManager component operates as an active component that manages system-wi - Keeps payload load switches (indices 6 and 7) off unless payload mode is explicitly entered 3. **Safe Mode Entry** - - Can be triggered by: + - Can be triggered by (only from NORMAL mode - sequential transitions enforced): - Ground command: `FORCE_SAFE_MODE` - External component request via `forceSafeMode` port - Actions performed: @@ -130,9 +130,9 @@ classDiagram } class SystemMode { <> - NORMAL = 0 SAFE_MODE = 1 - PAYLOAD_MODE = 2 + NORMAL = 2 + PAYLOAD_MODE = 3 } } ModeManagerComponentBase <|-- ModeManager : inherits @@ -304,7 +304,7 @@ sequenceDiagram | Name | Arguments | Description | |---|---|---| -| FORCE_SAFE_MODE | None | Forces the system into safe mode immediately. Emits ManualSafeModeEntry event. Can be called from any mode (idempotent). | +| FORCE_SAFE_MODE | None | Forces the system into safe mode. Only allowed from NORMAL mode (rejects from PAYLOAD_MODE with validation error). Emits ManualSafeModeEntry event. Idempotent when already in safe mode. | | EXIT_SAFE_MODE | None | Exits safe mode and returns to normal operation. Fails with CommandValidationFailed if not currently in safe mode. | | ENTER_PAYLOAD_MODE | None | Enters payload mode from NORMAL. Fails with CommandValidationFailed if issued from SAFE_MODE or if already in payload mode (idempotent success when already in payload). Emits ManualPayloadModeEntry event. | | EXIT_PAYLOAD_MODE | None | Exits payload mode and returns to normal operation. Fails with CommandValidationFailed if not currently in payload mode. | @@ -327,7 +327,7 @@ sequenceDiagram | Name | Type | Update Rate | Description | |---|---|---|---| -| CurrentMode | U8 | 1Hz | Current system mode (0 = NORMAL, 1 = SAFE_MODE, 2 = PAYLOAD_MODE) | +| CurrentMode | U8 | 1Hz | Current system mode (1 = SAFE_MODE, 2 = NORMAL, 3 = PAYLOAD_MODE) | | SafeModeEntryCount | U32 | On change | Number of times safe mode has been entered (persists across reboots) | | PayloadModeEntryCount | U32 | On change | Number of times payload mode has been entered (persists across reboots) | @@ -360,7 +360,7 @@ See `FprimeZephyrReference/test/int/mode_manager_test.py` and `FprimeZephyrRefer | test_19_safe_mode_state_persists | Verifies safe mode persistence to flash | State persistence | | test_payload_01_enter_exit_payload_mode | Validates payload mode entry/exit, events, telemetry, payload load switches | Payload mode entry/exit | | test_payload_02_cannot_enter_from_safe_mode | Ensures ENTER_PAYLOAD_MODE fails from SAFE_MODE | Command validation | -| test_payload_03_safe_mode_override_from_payload | Ensures FORCE_SAFE_MODE overrides payload mode | Emergency override | +| test_payload_03_safe_mode_rejected_from_payload | Ensures FORCE_SAFE_MODE is rejected from payload mode (sequential transitions) | Command validation | | test_payload_04_state_persists | Verifies payload mode and counters persist | Payload persistence | ## Design Decisions @@ -388,12 +388,13 @@ Mode state is persisted to `/mode_state.bin` to maintain operational context acr This ensures the system resumes in the correct mode after recovery. -### Idempotent Safe Mode Entry -The FORCE_SAFE_MODE command can be called from any mode without error. If already in safe mode, it succeeds without re-entering. This simplifies fault handling logic in external components. +### Sequential Mode Transitions +Mode transitions follow a +1/-1 sequential pattern: SAFE_MODE(1) ↔ NORMAL(2) ↔ PAYLOAD_MODE(3). Direct jumps (e.g., PAYLOAD→SAFE) are not allowed - users must exit payload mode first before entering safe mode. FORCE_SAFE_MODE is idempotent when already in safe mode. ## Change Log | Date | Description | |---|---| +| 2025-11-26 | Reordered enum values (SAFE=1, NORMAL=2, PAYLOAD=3) for sequential +1/-1 transitions; FORCE_SAFE_MODE now rejected from payload mode | | 2025-11-26 | Removed forcePayloadMode port - payload mode now only entered via ENTER_PAYLOAD_MODE ground command | | 2025-11-25 | Added PAYLOAD_MODE (commands, events, telemetry, persistence, payload load switch control) and documented payload integration tests | | 2025-11-19 | Added getMode query port and enhanced modeChanged to carry mode value | diff --git a/FprimeZephyrReference/test/int/mode_manager_test.py b/FprimeZephyrReference/test/int/mode_manager_test.py index 6bdd0429..7f7d11b3 100644 --- a/FprimeZephyrReference/test/int/mode_manager_test.py +++ b/FprimeZephyrReference/test/int/mode_manager_test.py @@ -11,6 +11,8 @@ - Edge cases Total: 9 tests + +Mode enum values: SAFE_MODE=1, NORMAL=2, PAYLOAD_MODE=3 """ import time @@ -87,12 +89,12 @@ def test_01_initial_telemetry(fprime_test_api: IntegrationTestAPI, start_gds): # Trigger telemetry update by sending Health packet (ID 1) proves_send_and_assert_command(fprime_test_api, "CdhCore.tlmSend.SEND_PKT", ["1"]) - # Read CurrentMode telemetry (0 = NORMAL, 1 = SAFE_MODE) + # Read CurrentMode telemetry (1 = SAFE_MODE, 2 = NORMAL, 3 = PAYLOAD_MODE) mode_result: ChData = fprime_test_api.assert_telemetry( f"{component}.CurrentMode", start="NOW", timeout=3 ) current_mode = mode_result.get_val() - assert current_mode in [0, 1], f"Invalid mode value: {current_mode}" + assert current_mode in [1, 2, 3], f"Invalid mode value: {current_mode}" # ============================================================================== @@ -103,13 +105,11 @@ def test_01_initial_telemetry(fprime_test_api: IntegrationTestAPI, start_gds): def test_04_force_safe_mode_command(fprime_test_api: IntegrationTestAPI, start_gds): """ Test FORCE_SAFE_MODE command enters safe mode by checking telemetry. + Note: With idempotent behavior, ManualSafeModeEntry is only emitted when + transitioning from NORMAL, not when already in SAFE_MODE. """ - # Send FORCE_SAFE_MODE command - still expect ManualSafeModeEntry for logging - proves_send_and_assert_command( - fprime_test_api, - f"{component}.FORCE_SAFE_MODE", - events=[f"{component}.ManualSafeModeEntry"], - ) + # Send FORCE_SAFE_MODE command (idempotent - succeeds even if already in safe mode) + proves_send_and_assert_command(fprime_test_api, f"{component}.FORCE_SAFE_MODE") # Wait for mode transition (happens in 1Hz rate group) time.sleep(3) @@ -201,7 +201,7 @@ def test_13_exit_safe_mode_fails_not_in_safe_mode( f"{component}.CurrentMode", start="NOW", timeout=3 ) - if mode_result.get_val() != 0: + if mode_result.get_val() != 2: pytest.skip("Not in NORMAL mode - cannot test this scenario") # Try to exit safe mode when not in it - should fail @@ -219,7 +219,7 @@ def test_14_exit_safe_mode_success(fprime_test_api: IntegrationTestAPI, start_gd Test EXIT_SAFE_MODE succeeds. Verifies: - ExitingSafeMode event is emitted - - CurrentMode returns to NORMAL (0) + - CurrentMode returns to NORMAL (2) """ # Enter safe mode proves_send_and_assert_command(fprime_test_api, f"{component}.FORCE_SAFE_MODE") @@ -234,12 +234,12 @@ def test_14_exit_safe_mode_success(fprime_test_api: IntegrationTestAPI, start_gd time.sleep(2) - # Verify mode is NORMAL (0) + # Verify mode is NORMAL (2) proves_send_and_assert_command(fprime_test_api, "CdhCore.tlmSend.SEND_PKT", ["1"]) mode_result: ChData = fprime_test_api.assert_telemetry( f"{component}.CurrentMode", start="NOW", timeout=3 ) - assert mode_result.get_val() == 0, "Should be in NORMAL mode" + assert mode_result.get_val() == 2, "Should be in NORMAL mode" # ============================================================================== @@ -250,7 +250,7 @@ def test_14_exit_safe_mode_success(fprime_test_api: IntegrationTestAPI, start_gd def test_18_force_safe_mode_idempotent(fprime_test_api: IntegrationTestAPI, start_gds): """ Test that calling FORCE_SAFE_MODE while already in safe mode is idempotent. - Should succeed and not cause issues. + Should succeed without emitting events (no re-entry). """ # Enter safe mode first time proves_send_and_assert_command(fprime_test_api, f"{component}.FORCE_SAFE_MODE") @@ -259,13 +259,8 @@ def test_18_force_safe_mode_idempotent(fprime_test_api: IntegrationTestAPI, star # Clear event history fprime_test_api.clear_histories() - # Force safe mode again - should succeed without EnteringSafeMode event - # But ManualSafeModeEntry event is still emitted (logging) - proves_send_and_assert_command( - fprime_test_api, - f"{component}.FORCE_SAFE_MODE", - events=[f"{component}.ManualSafeModeEntry"], - ) + # Force safe mode again - should succeed silently (no events, no re-entry) + proves_send_and_assert_command(fprime_test_api, f"{component}.FORCE_SAFE_MODE") # Verify still in safe mode time.sleep(1) diff --git a/FprimeZephyrReference/test/int/payload_mode_test.py b/FprimeZephyrReference/test/int/payload_mode_test.py index 365ceb3c..b8b76aa3 100644 --- a/FprimeZephyrReference/test/int/payload_mode_test.py +++ b/FprimeZephyrReference/test/int/payload_mode_test.py @@ -6,10 +6,12 @@ Tests cover: - Payload mode entry and exit - Mode transition validation (cannot enter from safe mode) -- Emergency safe mode override from payload mode +- Sequential transitions (FORCE_SAFE_MODE rejected from payload mode) - State persistence Total: 4 tests + +Mode enum values: SAFE_MODE=1, NORMAL=2, PAYLOAD_MODE=3 """ import time @@ -84,10 +86,10 @@ def test_payload_01_enter_exit_payload_mode( Test ENTER_PAYLOAD_MODE and EXIT_PAYLOAD_MODE commands. Verifies: - EnteringPayloadMode event is emitted - - CurrentMode telemetry = 2 (PAYLOAD_MODE) + - CurrentMode telemetry = 3 (PAYLOAD_MODE) - Payload load switches (6 & 7) are ON - ExitingPayloadMode event is emitted on exit - - CurrentMode returns to 0 (NORMAL) + - CurrentMode returns to 2 (NORMAL) """ # Enter payload mode proves_send_and_assert_command( @@ -98,12 +100,12 @@ def test_payload_01_enter_exit_payload_mode( time.sleep(2) - # Verify mode is PAYLOAD_MODE (2) + # Verify mode is PAYLOAD_MODE (3) proves_send_and_assert_command(fprime_test_api, "CdhCore.tlmSend.SEND_PKT", ["1"]) mode_result: ChData = fprime_test_api.assert_telemetry( f"{component}.CurrentMode", timeout=5 ) - assert mode_result.get_val() == 2, "Should be in PAYLOAD_MODE" + assert mode_result.get_val() == 3, "Should be in PAYLOAD_MODE" # Verify payload load switches are ON proves_send_and_assert_command(fprime_test_api, "CdhCore.tlmSend.SEND_PKT", ["9"]) @@ -123,12 +125,12 @@ def test_payload_01_enter_exit_payload_mode( time.sleep(2) - # Verify mode is NORMAL (0) + # Verify mode is NORMAL (2) proves_send_and_assert_command(fprime_test_api, "CdhCore.tlmSend.SEND_PKT", ["1"]) mode_result: ChData = fprime_test_api.assert_telemetry( f"{component}.CurrentMode", timeout=5 ) - assert mode_result.get_val() == 0, "Should be in NORMAL mode" + assert mode_result.get_val() == 2, "Should be in NORMAL mode" # Verify payload load switches are OFF in NORMAL mode proves_send_and_assert_command(fprime_test_api, "CdhCore.tlmSend.SEND_PKT", ["9"]) @@ -176,16 +178,16 @@ def test_payload_02_cannot_enter_from_safe_mode( assert mode_result.get_val() == 1, "Should still be in SAFE_MODE" -def test_payload_03_safe_mode_override_from_payload( +def test_payload_03_safe_mode_rejected_from_payload( fprime_test_api: IntegrationTestAPI, start_gds ): """ - Test that FORCE_SAFE_MODE works from PAYLOAD_MODE (emergency override). + Test that FORCE_SAFE_MODE is rejected from PAYLOAD_MODE (sequential transitions). + Must exit payload mode first before entering safe mode. Verifies: - - Can enter safe mode from payload mode - - EnteringSafeMode event is emitted - - CurrentMode = 1 (SAFE_MODE) - - All load switches are OFF + - FORCE_SAFE_MODE fails with validation error from payload mode + - CommandValidationFailed event is emitted + - Remains in PAYLOAD_MODE (3) """ # Enter payload mode first proves_send_and_assert_command(fprime_test_api, f"{component}.ENTER_PAYLOAD_MODE") @@ -196,33 +198,22 @@ def test_payload_03_safe_mode_override_from_payload( mode_result: ChData = fprime_test_api.assert_telemetry( f"{component}.CurrentMode", timeout=5 ) - assert mode_result.get_val() == 2, "Should be in PAYLOAD_MODE" + assert mode_result.get_val() == 3, "Should be in PAYLOAD_MODE" - # Force safe mode (emergency override) + # Try to force safe mode - should fail (sequential transitions required) fprime_test_api.clear_histories() - proves_send_and_assert_command( - fprime_test_api, - f"{component}.FORCE_SAFE_MODE", - events=[f"{component}.ManualSafeModeEntry"], - ) + with pytest.raises(Exception): + proves_send_and_assert_command(fprime_test_api, f"{component}.FORCE_SAFE_MODE") - time.sleep(2) + # Verify CommandValidationFailed event + fprime_test_api.assert_event(f"{component}.CommandValidationFailed", timeout=3) - # Verify mode is SAFE_MODE (1) + # Verify still in payload mode proves_send_and_assert_command(fprime_test_api, "CdhCore.tlmSend.SEND_PKT", ["1"]) mode_result: ChData = fprime_test_api.assert_telemetry( f"{component}.CurrentMode", timeout=5 ) - assert mode_result.get_val() == 1, "Should be in SAFE_MODE" - - # Verify all load switches are OFF - proves_send_and_assert_command(fprime_test_api, "CdhCore.tlmSend.SEND_PKT", ["9"]) - for channel in all_load_switch_channels: - value = fprime_test_api.assert_telemetry(channel, timeout=5).get_val() - if isinstance(value, str): - assert value.upper() == "OFF", f"{channel} should be OFF in safe mode" - else: - assert value == 0, f"{channel} should be OFF in safe mode" + assert mode_result.get_val() == 3, "Should still be in PAYLOAD_MODE" def test_payload_04_state_persists(fprime_test_api: IntegrationTestAPI, start_gds): @@ -234,12 +225,12 @@ def test_payload_04_state_persists(fprime_test_api: IntegrationTestAPI, start_gd proves_send_and_assert_command(fprime_test_api, f"{component}.ENTER_PAYLOAD_MODE") time.sleep(2) - # Verify mode is saved as PAYLOAD_MODE + # Verify mode is saved as PAYLOAD_MODE (3) proves_send_and_assert_command(fprime_test_api, "CdhCore.tlmSend.SEND_PKT", ["1"]) mode_result: ChData = fprime_test_api.assert_telemetry( f"{component}.CurrentMode", timeout=5 ) - assert mode_result.get_val() == 2, "Mode should be saved as PAYLOAD_MODE" + assert mode_result.get_val() == 3, "Mode should be saved as PAYLOAD_MODE" # Verify PayloadModeEntryCount is tracking count_result: ChData = fprime_test_api.assert_telemetry( From a7d55082e5a35ac9aa2044e2eff2ae6ee81ee929 Mon Sep 17 00:00:00 2001 From: "Sam S. Yu" <25761223+yudataguy@users.noreply.github.com> Date: Wed, 26 Nov 2025 21:45:57 -0800 Subject: [PATCH 09/14] fix the safe mode emit spam --- FprimeZephyrReference/Components/ModeManager/ModeManager.cpp | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/FprimeZephyrReference/Components/ModeManager/ModeManager.cpp b/FprimeZephyrReference/Components/ModeManager/ModeManager.cpp index 8295fe25..616a0a9f 100644 --- a/FprimeZephyrReference/Components/ModeManager/ModeManager.cpp +++ b/FprimeZephyrReference/Components/ModeManager/ModeManager.cpp @@ -46,10 +46,8 @@ void ModeManager ::run_handler(FwIndexType portNum, U32 context) { void ModeManager ::forceSafeMode_handler(FwIndexType portNum) { // Force entry into safe mode (called by other components) // Only allowed from NORMAL (sequential +1/-1 transitions) - this->log_WARNING_HI_ExternalFaultDetected(); - - // Only enter safe mode from NORMAL - payload mode must be exited first if (this->m_mode == SystemMode::NORMAL) { + this->log_WARNING_HI_ExternalFaultDetected(); this->enterSafeMode("External component request"); } // Note: Request ignored if in PAYLOAD_MODE or already in SAFE_MODE From 5b0cc7e385fc00b6b597b1ca4f4b2607d78c3880 Mon Sep 17 00:00:00 2001 From: "Sam S. Yu" <25761223+yudataguy@users.noreply.github.com> Date: Fri, 28 Nov 2025 18:32:39 -0800 Subject: [PATCH 10/14] update ci run group to sam (temp) --- .github/workflows/ci.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index b6a683a0..ea7fa53e 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -17,7 +17,8 @@ jobs: make fmt build: - runs-on: deathstar + runs-on: + group: sam steps: - uses: actions/checkout@v4 From 286cd57f29bbaeaab1498f77ec6361cb40f6864f Mon Sep 17 00:00:00 2001 From: "Sam S. Yu" <25761223+yudataguy@users.noreply.github.com> Date: Fri, 28 Nov 2025 22:50:20 -0800 Subject: [PATCH 11/14] rever runner group --- .github/workflows/ci.yaml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index ea7fa53e..b6a683a0 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -17,8 +17,7 @@ jobs: make fmt build: - runs-on: - group: sam + runs-on: deathstar steps: - uses: actions/checkout@v4 From 37a29a241539c2444264849b5f8ae9c515dccf4d Mon Sep 17 00:00:00 2001 From: "Sam S. Yu" <25761223+yudataguy@users.noreply.github.com> Date: Sun, 30 Nov 2025 23:28:07 -0800 Subject: [PATCH 12/14] re-implement safe and normal mode auto switch --- .../Components/ModeManager/ModeManager.cpp | 343 ++++++----- .../Components/ModeManager/ModeManager.fpp | 81 ++- .../Components/ModeManager/ModeManager.hpp | 67 ++- .../Components/ModeManager/docs/sdd.md | 360 ++++++------ .../Components/ResetManager/ResetManager.cpp | 12 + .../Components/ResetManager/ResetManager.fpp | 3 + .../Top/ReferenceDeploymentPackets.fppi | 2 +- .../ReferenceDeployment/Top/topology.fpp | 4 + .../test/int/payload_mode_test.py | 239 -------- .../test/int/safe_mode_test.py | 554 ++++++++++++++++++ 10 files changed, 1044 insertions(+), 621 deletions(-) delete mode 100644 FprimeZephyrReference/test/int/payload_mode_test.py create mode 100644 FprimeZephyrReference/test/int/safe_mode_test.py diff --git a/FprimeZephyrReference/Components/ModeManager/ModeManager.cpp b/FprimeZephyrReference/Components/ModeManager/ModeManager.cpp index 616a0a9f..9baf3537 100644 --- a/FprimeZephyrReference/Components/ModeManager/ModeManager.cpp +++ b/FprimeZephyrReference/Components/ModeManager/ModeManager.cpp @@ -21,8 +21,16 @@ ModeManager ::ModeManager(const char* const compName) : ModeManagerComponentBase(compName), m_mode(SystemMode::NORMAL), m_safeModeEntryCount(0), - m_payloadModeEntryCount(0), - m_runCounter(0) {} + m_runCounter(0), + m_safeModeReason(Components::SafeModeReason::NONE), + m_safeModeVoltageCounter(0), + m_recoveryVoltageCounter(0) { + // Compile-time verification that internal SystemMode enum matches FPP-generated enum + static_assert(static_cast(SystemMode::SAFE_MODE) == static_cast(Components::SystemMode::SAFE_MODE), + "Internal SAFE_MODE value must match FPP enum"); + static_assert(static_cast(SystemMode::NORMAL) == static_cast(Components::SystemMode::NORMAL), + "Internal NORMAL value must match FPP enum"); +} ModeManager ::~ModeManager() {} @@ -39,18 +47,79 @@ void ModeManager ::run_handler(FwIndexType portNum, U32 context) { // Increment run counter (1Hz tick counter) this->m_runCounter++; + // Get current voltage (used by mode-specific voltage monitoring) + bool valid = false; + F32 voltage = this->getCurrentVoltage(valid); + + // Mode-specific voltage monitoring + if (this->m_mode == SystemMode::NORMAL) { + // Low-voltage protection for normal mode -> safe mode entry: + // - Threshold: 6.7V triggers safe mode entry + // - Debounce: 10 consecutive seconds below threshold + bool isFault = !valid || (voltage < SAFE_MODE_ENTRY_VOLTAGE); + + if (isFault) { + this->m_safeModeVoltageCounter++; + + if (this->m_safeModeVoltageCounter >= SAFE_MODE_DEBOUNCE_SECONDS) { + // Trigger automatic entry into safe mode + this->log_WARNING_HI_AutoSafeModeEntry(Components::SafeModeReason::LOW_BATTERY, valid ? voltage : 0.0f); + this->enterSafeMode(Components::SafeModeReason::LOW_BATTERY); + this->m_safeModeVoltageCounter = 0; // Reset counter + } + } else { + // Voltage OK and valid - reset counter + this->m_safeModeVoltageCounter = 0; + } + + // Reset recovery counter when in normal mode + this->m_recoveryVoltageCounter = 0; + + } else if (this->m_mode == SystemMode::SAFE_MODE) { + // Auto-recovery from safe mode (only if reason is LOW_BATTERY): + // - Threshold: Voltage > 8.0V triggers auto-recovery + // - Debounce: 10 consecutive seconds above threshold + // - SYSTEM_FAULT or GROUND_COMMAND require manual EXIT_SAFE_MODE command + if (this->m_safeModeReason == Components::SafeModeReason::LOW_BATTERY) { + if (valid && voltage > SAFE_MODE_RECOVERY_VOLTAGE) { + this->m_recoveryVoltageCounter++; + + if (this->m_recoveryVoltageCounter >= SAFE_MODE_DEBOUNCE_SECONDS) { + // Trigger automatic exit from safe mode + this->exitSafeModeAutomatic(voltage); + this->m_recoveryVoltageCounter = 0; // Reset counter + } + } else { + // Voltage not recovered yet - reset counter + this->m_recoveryVoltageCounter = 0; + } + } + // Note: If reason is SYSTEM_FAULT, GROUND_COMMAND, or EXTERNAL_REQUEST, no auto-recovery + + // Reset safe mode entry counter when in safe mode + this->m_safeModeVoltageCounter = 0; + } + // Update telemetry this->tlmWrite_CurrentMode(static_cast(this->m_mode)); + this->tlmWrite_CurrentSafeModeReason(this->m_safeModeReason); } -void ModeManager ::forceSafeMode_handler(FwIndexType portNum) { +void ModeManager ::forceSafeMode_handler(FwIndexType portNum, const Components::SafeModeReason& reason) { // Force entry into safe mode (called by other components) // Only allowed from NORMAL (sequential +1/-1 transitions) if (this->m_mode == SystemMode::NORMAL) { this->log_WARNING_HI_ExternalFaultDetected(); - this->enterSafeMode("External component request"); + + // Use provided reason, defaulting to EXTERNAL_REQUEST if NONE is passed + Components::SafeModeReason effectiveReason = reason; + if (reason == Components::SafeModeReason::NONE) { + effectiveReason = Components::SafeModeReason::EXTERNAL_REQUEST; + } + + this->enterSafeMode(effectiveReason); } - // Note: Request ignored if in PAYLOAD_MODE or already in SAFE_MODE + // Note: Request ignored if already in SAFE_MODE } Components::SystemMode ModeManager ::getMode_handler(FwIndexType portNum) { @@ -59,21 +128,49 @@ Components::SystemMode ModeManager ::getMode_handler(FwIndexType portNum) { return static_cast(this->m_mode); } +void ModeManager ::prepareForReboot_handler(FwIndexType portNum) { + // Called before intentional reboot to set clean shutdown flag + // This allows us to detect unintended reboots on next startup + this->log_ACTIVITY_HI_PreparingForReboot(); + + // Save state with clean shutdown flag set + // We directly write to file here to ensure the flag is persisted + Os::File file; + Os::File::Status status = file.open(STATE_FILE_PATH, Os::File::OPEN_CREATE); + + if (status != Os::File::OP_OK) { + // Log failure - next boot will be misclassified as unintended reboot + Fw::LogStringArg opStr("shutdown-open"); + this->log_WARNING_LO_StatePersistenceFailure(opStr, static_cast(status)); + return; + } + + PersistentState state; + state.mode = static_cast(this->m_mode); + state.safeModeEntryCount = this->m_safeModeEntryCount; + state.safeModeReason = static_cast(this->m_safeModeReason); + state.cleanShutdown = 1; // Mark as clean shutdown + + FwSizeType bytesToWrite = sizeof(PersistentState); + FwSizeType bytesWritten = bytesToWrite; + Os::File::Status writeStatus = file.write(reinterpret_cast(&state), bytesWritten, Os::File::WaitType::WAIT); + + // Check if write succeeded and correct number of bytes written + if (writeStatus != Os::File::OP_OK || bytesWritten != bytesToWrite) { + // Log failure - next boot will be misclassified as unintended reboot + Fw::LogStringArg opStr("shutdown-write"); + this->log_WARNING_LO_StatePersistenceFailure(opStr, static_cast(writeStatus)); + } + + file.close(); +} + // ---------------------------------------------------------------------- // Handler implementations for commands // ---------------------------------------------------------------------- void ModeManager ::FORCE_SAFE_MODE_cmdHandler(FwOpcodeType opCode, U32 cmdSeq) { - // Force entry into safe mode - only allowed from NORMAL (sequential +1/-1 transitions) - - // Reject if in payload mode - must exit payload mode first - if (this->m_mode == SystemMode::PAYLOAD_MODE) { - Fw::LogStringArg cmdNameStr("FORCE_SAFE_MODE"); - Fw::LogStringArg reasonStr("Must exit payload mode first"); - this->log_WARNING_LO_CommandValidationFailed(cmdNameStr, reasonStr); - this->cmdResponse_out(opCode, cmdSeq, Fw::CmdResponse::VALIDATION_ERROR); - return; - } + // Force entry into safe mode // Already in safe mode - idempotent success if (this->m_mode == SystemMode::SAFE_MODE) { @@ -83,7 +180,7 @@ void ModeManager ::FORCE_SAFE_MODE_cmdHandler(FwOpcodeType opCode, U32 cmdSeq) { // Enter safe mode from NORMAL this->log_ACTIVITY_HI_ManualSafeModeEntry(); - this->enterSafeMode("Ground command"); + this->enterSafeMode(Components::SafeModeReason::GROUND_COMMAND); this->cmdResponse_out(opCode, cmdSeq, Fw::CmdResponse::OK); } @@ -104,48 +201,6 @@ void ModeManager ::EXIT_SAFE_MODE_cmdHandler(FwOpcodeType opCode, U32 cmdSeq) { this->cmdResponse_out(opCode, cmdSeq, Fw::CmdResponse::OK); } -void ModeManager ::ENTER_PAYLOAD_MODE_cmdHandler(FwOpcodeType opCode, U32 cmdSeq) { - // Command to enter payload mode - only allowed from NORMAL mode - - // Check if currently in safe mode (not allowed) - if (this->m_mode == SystemMode::SAFE_MODE) { - Fw::LogStringArg cmdNameStr("ENTER_PAYLOAD_MODE"); - Fw::LogStringArg reasonStr("Cannot enter payload mode from safe mode"); - this->log_WARNING_LO_CommandValidationFailed(cmdNameStr, reasonStr); - this->cmdResponse_out(opCode, cmdSeq, Fw::CmdResponse::VALIDATION_ERROR); - return; - } - - // Check if already in payload mode - if (this->m_mode == SystemMode::PAYLOAD_MODE) { - // Already in payload mode - success (idempotent) - this->cmdResponse_out(opCode, cmdSeq, Fw::CmdResponse::OK); - return; - } - - // Enter payload mode - this->log_ACTIVITY_HI_ManualPayloadModeEntry(); - this->enterPayloadMode("Ground command"); - this->cmdResponse_out(opCode, cmdSeq, Fw::CmdResponse::OK); -} - -void ModeManager ::EXIT_PAYLOAD_MODE_cmdHandler(FwOpcodeType opCode, U32 cmdSeq) { - // Command to exit payload mode - - // Check if currently in payload mode - if (this->m_mode != SystemMode::PAYLOAD_MODE) { - Fw::LogStringArg cmdNameStr("EXIT_PAYLOAD_MODE"); - Fw::LogStringArg reasonStr("Not currently in payload mode"); - this->log_WARNING_LO_CommandValidationFailed(cmdNameStr, reasonStr); - this->cmdResponse_out(opCode, cmdSeq, Fw::CmdResponse::VALIDATION_ERROR); - return; - } - - // Exit payload mode - this->exitPayloadMode(); - this->cmdResponse_out(opCode, cmdSeq, Fw::CmdResponse::OK); -} - // ---------------------------------------------------------------------- // Private helper methods // ---------------------------------------------------------------------- @@ -154,6 +209,8 @@ void ModeManager ::loadState() { Os::File file; Os::File::Status status = file.open(STATE_FILE_PATH, Os::File::OPEN_READ); + bool unintendedReboot = false; + if (status == Os::File::OP_OK) { PersistentState state; FwSizeType size = sizeof(PersistentState); @@ -161,13 +218,20 @@ void ModeManager ::loadState() { status = file.read(reinterpret_cast(&state), bytesRead, Os::File::WaitType::WAIT); if (status == Os::File::OP_OK && bytesRead == sizeof(PersistentState)) { - // Validate state data before restoring (valid range: 1-3 for SAFE, NORMAL, PAYLOAD) + // Validate state data before restoring (valid range: 1-2 for SAFE, NORMAL) if (state.mode >= static_cast(SystemMode::SAFE_MODE) && - state.mode <= static_cast(SystemMode::PAYLOAD_MODE)) { + state.mode <= static_cast(SystemMode::NORMAL)) { // Valid mode value - restore state this->m_mode = static_cast(state.mode); this->m_safeModeEntryCount = state.safeModeEntryCount; - this->m_payloadModeEntryCount = state.payloadModeEntryCount; + this->m_safeModeReason = static_cast(state.safeModeReason); + + // Check for unintended reboot: + // If cleanShutdown flag is NOT set (0) and we were in NORMAL mode, + // this indicates an unintended reboot (crash, watchdog, power loss, etc.) + if (state.cleanShutdown == 0 && this->m_mode == SystemMode::NORMAL) { + unintendedReboot = true; + } // Restore physical hardware state to match loaded mode if (this->m_mode == SystemMode::SAFE_MODE) { @@ -177,35 +241,56 @@ void ModeManager ::loadState() { // Log that we're restoring safe mode (not entering it fresh) Fw::LogStringArg reasonStr("State restored from persistent storage"); this->log_WARNING_HI_EnteringSafeMode(reasonStr); - } else if (this->m_mode == SystemMode::PAYLOAD_MODE) { - // PAYLOAD_MODE - turn on face switches AND payload switches - this->turnOnComponents(); // Face switches (0-5) - this->turnOnPayload(); // Payload switches (6-7) - - // Log that we're restoring payload mode - Fw::LogStringArg reasonStr("State restored from persistent storage"); - this->log_ACTIVITY_HI_EnteringPayloadMode(reasonStr); } else { // NORMAL mode - ensure components are turned on this->turnOnComponents(); } } else { - // Corrupted state - use defaults + // Corrupted state (invalid mode value) - use defaults + Fw::LogStringArg opStr("load-corrupt"); + this->log_WARNING_LO_StatePersistenceFailure(opStr, static_cast(state.mode)); this->m_mode = SystemMode::NORMAL; this->m_safeModeEntryCount = 0; - this->m_payloadModeEntryCount = 0; + this->m_safeModeReason = Components::SafeModeReason::NONE; this->turnOnComponents(); } + } else { + // Read failed or file truncated - use defaults + Fw::LogStringArg opStr("load-read"); + this->log_WARNING_LO_StatePersistenceFailure(opStr, static_cast(status)); + this->m_mode = SystemMode::NORMAL; + this->m_safeModeEntryCount = 0; + this->m_safeModeReason = Components::SafeModeReason::NONE; + this->turnOnComponents(); } file.close(); } else { - // File doesn't exist or can't be opened - initialize to default state + // Initialize to default state this->m_mode = SystemMode::NORMAL; this->m_safeModeEntryCount = 0; - this->m_payloadModeEntryCount = 0; + this->m_safeModeReason = Components::SafeModeReason::NONE; this->turnOnComponents(); + + // Only log warning for unexpected errors, not for expected "file not found" on first boot + if (status != Os::File::DOESNT_EXIST) { + Fw::LogStringArg opStr("load-open"); + this->log_WARNING_LO_StatePersistenceFailure(opStr, static_cast(status)); + } + // Note: DOESNT_EXIST is expected on first boot - no warning needed + } + + // Handle unintended reboot detection AFTER basic state restoration + // This ensures we enter safe mode due to system fault + if (unintendedReboot) { + this->log_WARNING_HI_UnintendedRebootDetected(); + this->enterSafeMode(Components::SafeModeReason::SYSTEM_FAULT); } + + // Clear clean shutdown flag for next boot detection + // This ensures that if the system crashes before the next intentional reboot, + // we'll detect it as an unintended reboot + this->saveState(); } void ModeManager ::saveState() { @@ -222,7 +307,8 @@ void ModeManager ::saveState() { PersistentState state; state.mode = static_cast(this->m_mode); state.safeModeEntryCount = this->m_safeModeEntryCount; - state.payloadModeEntryCount = this->m_payloadModeEntryCount; + state.safeModeReason = static_cast(this->m_safeModeReason); + state.cleanShutdown = 0; // Default to unclean - only prepareForReboot sets this to 1 FwSizeType bytesToWrite = sizeof(PersistentState); FwSizeType bytesWritten = bytesToWrite; @@ -239,19 +325,33 @@ void ModeManager ::saveState() { file.close(); } -void ModeManager ::enterSafeMode(const char* reasonOverride) { +void ModeManager ::enterSafeMode(Components::SafeModeReason reason) { // Transition to safe mode this->m_mode = SystemMode::SAFE_MODE; this->m_safeModeEntryCount++; + this->m_safeModeReason = reason; - // Build reason string + // Build reason string for event log Fw::LogStringArg reasonStr; - char reasonBuf[100]; - if (reasonOverride != nullptr) { - reasonStr = reasonOverride; - } else { - snprintf(reasonBuf, sizeof(reasonBuf), "Unknown"); - reasonStr = reasonBuf; + switch (reason) { + case Components::SafeModeReason::LOW_BATTERY: + reasonStr = "Low battery voltage"; + break; + case Components::SafeModeReason::SYSTEM_FAULT: + reasonStr = "System fault (unintended reboot)"; + break; + case Components::SafeModeReason::GROUND_COMMAND: + reasonStr = "Ground command"; + break; + case Components::SafeModeReason::EXTERNAL_REQUEST: + reasonStr = "External component request"; + break; + case Components::SafeModeReason::LORA: + reasonStr = "LoRa communication fault"; + break; + default: + reasonStr = "Unknown"; + break; } this->log_WARNING_HI_EnteringSafeMode(reasonStr); @@ -262,6 +362,7 @@ void ModeManager ::enterSafeMode(const char* reasonOverride) { // Update telemetry this->tlmWrite_CurrentMode(static_cast(this->m_mode)); this->tlmWrite_SafeModeEntryCount(this->m_safeModeEntryCount); + this->tlmWrite_CurrentSafeModeReason(this->m_safeModeReason); // Notify other components of mode change with new mode value if (this->isConnected_modeChanged_OutputPort(0)) { @@ -274,8 +375,9 @@ void ModeManager ::enterSafeMode(const char* reasonOverride) { } void ModeManager ::exitSafeMode() { - // Transition back to normal mode + // Transition back to normal mode (manual command) this->m_mode = SystemMode::NORMAL; + this->m_safeModeReason = Components::SafeModeReason::NONE; // Clear reason on exit this->log_ACTIVITY_HI_ExitingSafeMode(); @@ -284,6 +386,7 @@ void ModeManager ::exitSafeMode() { // Update telemetry this->tlmWrite_CurrentMode(static_cast(this->m_mode)); + this->tlmWrite_CurrentSafeModeReason(this->m_safeModeReason); // Notify other components of mode change with new mode value if (this->isConnected_modeChanged_OutputPort(0)) { @@ -295,55 +398,20 @@ void ModeManager ::exitSafeMode() { this->saveState(); } -void ModeManager ::enterPayloadMode(const char* reasonOverride) { - // Transition to payload mode - this->m_mode = SystemMode::PAYLOAD_MODE; - this->m_payloadModeEntryCount++; - - // Build reason string - Fw::LogStringArg reasonStr; - char reasonBuf[100]; - if (reasonOverride != nullptr) { - reasonStr = reasonOverride; - } else { - snprintf(reasonBuf, sizeof(reasonBuf), "Unknown"); - reasonStr = reasonBuf; - } - - this->log_ACTIVITY_HI_EnteringPayloadMode(reasonStr); - - // Turn on payload switches (6 & 7) - this->turnOnPayload(); - - // Update telemetry - this->tlmWrite_CurrentMode(static_cast(this->m_mode)); - this->tlmWrite_PayloadModeEntryCount(this->m_payloadModeEntryCount); - - // Notify other components of mode change with new mode value - if (this->isConnected_modeChanged_OutputPort(0)) { - Components::SystemMode fppMode = static_cast(this->m_mode); - this->modeChanged_out(0, fppMode); - } - - // Save state - this->saveState(); -} - -void ModeManager ::exitPayloadMode() { - // Transition back to normal mode +void ModeManager ::exitSafeModeAutomatic(F32 voltage) { + // Automatic exit from safe mode due to voltage recovery + // Only called when safe mode reason is LOW_BATTERY and voltage > 8.0V this->m_mode = SystemMode::NORMAL; + this->m_safeModeReason = Components::SafeModeReason::NONE; // Clear reason on exit - this->log_ACTIVITY_HI_ExitingPayloadMode(); - - // Turn off payload switches - this->turnOffPayload(); + this->log_ACTIVITY_HI_AutoSafeModeExit(voltage); - // Ensure face switches (0-5) are ON for NORMAL mode - // This guarantees consistent state regardless of transition path + // Turn on components (restore normal operation) this->turnOnComponents(); // Update telemetry this->tlmWrite_CurrentMode(static_cast(this->m_mode)); + this->tlmWrite_CurrentSafeModeReason(this->m_safeModeReason); // Notify other components of mode change with new mode value if (this->isConnected_modeChanged_OutputPort(0)) { @@ -356,7 +424,7 @@ void ModeManager ::exitPayloadMode() { } void ModeManager ::turnOffNonCriticalComponents() { - // Turn OFF: + // Turn OFF all load switches: // - Satellite faces 0-5 (LoadSwitch instances 0-5) // - Payload power (LoadSwitch instance 6) // - Payload battery (LoadSwitch instance 7) @@ -373,9 +441,8 @@ void ModeManager ::turnOffNonCriticalComponents() { void ModeManager ::turnOnComponents() { // Turn ON face load switches (0-5) to restore normal operation - // Note: Payload switches (6-7) are NOT turned on here - they require PAYLOAD_MODE - // Send turn on signal to face load switches only (indices 0-5) + // Send turn on signal to face load switches (indices 0-5) for (FwIndexType i = 0; i < 6; i++) { if (this->isConnected_loadSwitchTurnOn_OutputPort(i)) { this->loadSwitchTurnOn_out(i); @@ -383,26 +450,6 @@ void ModeManager ::turnOnComponents() { } } -void ModeManager ::turnOnPayload() { - // Turn ON payload load switches (6 = payload power, 7 = payload battery) - if (this->isConnected_loadSwitchTurnOn_OutputPort(6)) { - this->loadSwitchTurnOn_out(6); - } - if (this->isConnected_loadSwitchTurnOn_OutputPort(7)) { - this->loadSwitchTurnOn_out(7); - } -} - -void ModeManager ::turnOffPayload() { - // Turn OFF payload load switches (6 = payload power, 7 = payload battery) - if (this->isConnected_loadSwitchTurnOff_OutputPort(6)) { - this->loadSwitchTurnOff_out(6); - } - if (this->isConnected_loadSwitchTurnOff_OutputPort(7)) { - this->loadSwitchTurnOff_out(7); - } -} - F32 ModeManager ::getCurrentVoltage(bool& valid) { // Call the voltage get port to get current system voltage if (this->isConnected_voltageGet_OutputPort(0)) { diff --git a/FprimeZephyrReference/Components/ModeManager/ModeManager.fpp b/FprimeZephyrReference/Components/ModeManager/ModeManager.fpp index 09e73743..f8777f94 100644 --- a/FprimeZephyrReference/Components/ModeManager/ModeManager.fpp +++ b/FprimeZephyrReference/Components/ModeManager/ModeManager.fpp @@ -1,10 +1,19 @@ module Components { - @ System mode enumeration (values ordered for +1/-1 sequential transitions) + @ System mode enumeration enum SystemMode { SAFE_MODE = 1 @< Safe mode with non-critical components powered off NORMAL = 2 @< Normal operational mode - PAYLOAD_MODE = 3 @< Payload mode with payload power and battery enabled + } + + @ Reason for entering safe mode (used for recovery decision logic) + enum SafeModeReason { + NONE = 0 @< Not in safe mode or reason cleared + LOW_BATTERY = 1 @< Entered due to low voltage condition + SYSTEM_FAULT = 2 @< Entered due to unintended reboot/system fault + GROUND_COMMAND = 3 @< Entered via ground command + EXTERNAL_REQUEST = 4 @< Entered via external component request + LORA = 5 @< Entered due to LoRa communication timeout or fault } @ Port for notifying about mode changes @@ -13,6 +22,10 @@ module Components { @ Port for querying the current system mode port GetSystemMode -> SystemMode + @ Port for forcing safe mode with a reason + @ Pass NONE as reason to default to EXTERNAL_REQUEST + port ForceSafeModeWithReason(reason: SafeModeReason) + @ Component to manage system modes and orchestrate safe mode transitions @ based on voltage, watchdog faults, and communication timeouts active component ModeManager { @@ -25,11 +38,15 @@ module Components { sync input port run: Svc.Sched @ Port to force safe mode entry (callable by other components) - async input port forceSafeMode: Fw.Signal + @ Accepts SafeModeReason - pass NONE to default to EXTERNAL_REQUEST + async input port forceSafeMode: Components.ForceSafeModeWithReason @ Port to query the current system mode sync input port getMode: Components.GetSystemMode + @ Port called before intentional reboot to set clean shutdown flag + sync input port prepareForReboot: Fw.Signal + # ---------------------------------------------------------------------- # Output Ports # ---------------------------------------------------------------------- @@ -37,10 +54,10 @@ module Components { @ Port to notify other components of mode changes (with current mode) output port modeChanged: Components.SystemModeChanged - @ Ports to turn on LoadSwitch instances (8 total) + @ Ports to turn on LoadSwitch instances (6 face switches + 2 payload switches) output port loadSwitchTurnOn: [8] Fw.Signal - @ Ports to turn off LoadSwitch instances (8 total) + @ Ports to turn off LoadSwitch instances (6 face switches + 2 payload switches) output port loadSwitchTurnOff: [8] Fw.Signal @ Port to get system voltage from INA219 manager @@ -58,14 +75,6 @@ module Components { @ Only succeeds if currently in safe mode sync command EXIT_SAFE_MODE() - @ Command to enter payload mode - @ Only succeeds if currently in normal mode - sync command ENTER_PAYLOAD_MODE() - - @ Command to exit payload mode - @ Only succeeds if currently in payload mode - sync command EXIT_PAYLOAD_MODE() - # ---------------------------------------------------------------------- # Events # ---------------------------------------------------------------------- @@ -92,23 +101,6 @@ module Components { severity warning high \ format "External fault detected - external component forced safe mode" - @ Event emitted when entering payload mode - event EnteringPayloadMode( - reason: string size 100 @< Reason for entering payload mode - ) \ - severity activity high \ - format "ENTERING PAYLOAD MODE: {}" - - @ Event emitted when exiting payload mode - event ExitingPayloadMode() \ - severity activity high \ - format "Exiting payload mode" - - @ Event emitted when payload mode is manually commanded - event ManualPayloadModeEntry() \ - severity activity high \ - format "Payload mode entry commanded manually" - @ Event emitted when command validation fails event CommandValidationFailed( cmdName: string size 50 @< Command that failed validation @@ -125,6 +117,31 @@ module Components { severity warning low \ format "State persistence {} failed with status {}" + @ Event emitted when automatically entering safe mode due to low voltage + event AutoSafeModeEntry( + reason: SafeModeReason @< Reason for entering safe mode + voltage: F32 @< Voltage that triggered the entry (0 if N/A) + ) \ + severity warning high \ + format "AUTO SAFE MODE ENTRY: reason={} voltage={}V" + + @ Event emitted when automatically exiting safe mode due to voltage recovery + event AutoSafeModeExit( + voltage: F32 @< Voltage that triggered recovery + ) \ + severity activity high \ + format "AUTO SAFE MODE EXIT: Voltage recovered to {}V" + + @ Event emitted when unintended reboot is detected + event UnintendedRebootDetected() \ + severity warning high \ + format "UNINTENDED REBOOT DETECTED: Entering safe mode" + + @ Event emitted when preparing for intentional reboot + event PreparingForReboot() \ + severity activity high \ + format "Preparing for intentional reboot - setting clean shutdown flag" + # ---------------------------------------------------------------------- # Telemetry # ---------------------------------------------------------------------- @@ -135,8 +152,8 @@ module Components { @ Number of times safe mode has been entered telemetry SafeModeEntryCount: U32 - @ Number of times payload mode has been entered - telemetry PayloadModeEntryCount: U32 + @ Current safe mode reason (NONE if not in safe mode) + telemetry CurrentSafeModeReason: SafeModeReason ############################################################################### diff --git a/FprimeZephyrReference/Components/ModeManager/ModeManager.hpp b/FprimeZephyrReference/Components/ModeManager/ModeManager.hpp index a4b9b9b5..7431d570 100644 --- a/FprimeZephyrReference/Components/ModeManager/ModeManager.hpp +++ b/FprimeZephyrReference/Components/ModeManager/ModeManager.hpp @@ -47,7 +47,9 @@ class ModeManager : public ModeManagerComponentBase { //! Handler implementation for forceSafeMode //! //! Port to force safe mode entry (callable by other components) - void forceSafeMode_handler(FwIndexType portNum //!< The port number + //! @param reason The reason for entering safe mode (NONE defaults to EXTERNAL_REQUEST) + void forceSafeMode_handler(FwIndexType portNum, //!< The port number + const Components::SafeModeReason& reason //!< The safe mode reason ) override; //! Handler implementation for getMode @@ -56,6 +58,12 @@ class ModeManager : public ModeManagerComponentBase { Components::SystemMode getMode_handler(FwIndexType portNum //!< The port number ) override; + //! Handler implementation for prepareForReboot + //! + //! Port called before intentional reboot to set clean shutdown flag + void prepareForReboot_handler(FwIndexType portNum //!< The port number + ) override; + // ---------------------------------------------------------------------- // Handler implementations for commands // ---------------------------------------------------------------------- @@ -70,16 +78,6 @@ class ModeManager : public ModeManagerComponentBase { U32 cmdSeq //!< The command sequence number ) override; - //! Handler implementation for command ENTER_PAYLOAD_MODE - void ENTER_PAYLOAD_MODE_cmdHandler(FwOpcodeType opCode, //!< The opcode - U32 cmdSeq //!< The command sequence number - ) override; - - //! Handler implementation for command EXIT_PAYLOAD_MODE - void EXIT_PAYLOAD_MODE_cmdHandler(FwOpcodeType opCode, //!< The opcode - U32 cmdSeq //!< The command sequence number - ) override; - private: // ---------------------------------------------------------------------- // Private helper methods @@ -91,27 +89,18 @@ class ModeManager : public ModeManagerComponentBase { //! Save persistent state to file void saveState(); - //! Enter safe mode with optional reason override - void enterSafeMode(const char* reason = nullptr); + //! Enter safe mode with specified reason + void enterSafeMode(Components::SafeModeReason reason); - //! Exit safe mode + //! Exit safe mode (manual command) void exitSafeMode(); - //! Enter payload mode with optional reason override - void enterPayloadMode(const char* reason = nullptr); - - //! Exit payload mode - void exitPayloadMode(); + //! Exit safe mode automatically due to voltage recovery + void exitSafeModeAutomatic(F32 voltage); //! Turn off non-critical components void turnOffNonCriticalComponents(); - //! Turn on payload (load switches 6 & 7) - void turnOnPayload(); - - //! Turn off payload (load switches 6 & 7) - void turnOffPayload(); - //! Turn on components (restore normal operation) void turnOnComponents(); @@ -125,26 +114,36 @@ class ModeManager : public ModeManagerComponentBase { // Private enums and types // ---------------------------------------------------------------------- - //! System mode enumeration (values ordered for +1/-1 sequential transitions) - enum class SystemMode : U8 { SAFE_MODE = 1, NORMAL = 2, PAYLOAD_MODE = 3 }; + //! System mode enumeration + enum class SystemMode : U8 { SAFE_MODE = 1, NORMAL = 2 }; //! Persistent state structure struct PersistentState { - U8 mode; //!< Current mode (SystemMode) - U32 safeModeEntryCount; //!< Number of times safe mode entered - U32 payloadModeEntryCount; //!< Number of times payload mode entered + U8 mode; //!< Current mode (SystemMode) + U32 safeModeEntryCount; //!< Number of times safe mode entered + U8 safeModeReason; //!< Reason for safe mode entry (SafeModeReason) + U8 cleanShutdown; //!< Clean shutdown flag (1 = clean, 0 = unclean) }; // ---------------------------------------------------------------------- // Private member variables // ---------------------------------------------------------------------- - SystemMode m_mode; //!< Current system mode - U32 m_safeModeEntryCount; //!< Counter for safe mode entries - U32 m_payloadModeEntryCount; //!< Counter for payload mode entries - U32 m_runCounter; //!< Counter for run handler calls (1Hz) + SystemMode m_mode; //!< Current system mode + U32 m_safeModeEntryCount; //!< Counter for safe mode entries + U32 m_runCounter; //!< Counter for run handler calls (1Hz) + Components::SafeModeReason m_safeModeReason; //!< Current safe mode reason + U32 m_safeModeVoltageCounter; //!< Counter for low voltage in NORMAL mode + U32 m_recoveryVoltageCounter; //!< Counter for voltage recovery in SAFE_MODE + + // ---------------------------------------------------------------------- + // Constants + // ---------------------------------------------------------------------- static constexpr const char* STATE_FILE_PATH = "/mode_state.bin"; //!< State file path + static constexpr F32 SAFE_MODE_ENTRY_VOLTAGE = 6.7f; //!< Voltage threshold for safe mode entry (V) + static constexpr F32 SAFE_MODE_RECOVERY_VOLTAGE = 8.0f; //!< Voltage threshold for safe mode recovery (V) + static constexpr U32 SAFE_MODE_DEBOUNCE_SECONDS = 10; //!< Debounce time for voltage transitions (s) }; } // namespace Components diff --git a/FprimeZephyrReference/Components/ModeManager/docs/sdd.md b/FprimeZephyrReference/Components/ModeManager/docs/sdd.md index 22d80144..18e9c8d7 100644 --- a/FprimeZephyrReference/Components/ModeManager/docs/sdd.md +++ b/FprimeZephyrReference/Components/ModeManager/docs/sdd.md @@ -1,93 +1,84 @@ # Components::ModeManager -The ModeManager component manages system operational modes and orchestrates transitions across NORMAL, SAFE_MODE, and PAYLOAD_MODE. It evaluates watchdog faults and communication timeouts to make mode decisions, controls power to non‑critical subsystems during transitions, and maintains/persists mode state across reboots to ensure consistent post‑recovery behavior. - -Future work: a HIBERNATION mode remains planned; it will follow the same persistence and validation patterns once implemented. +The ModeManager component manages system operational modes and orchestrates transitions between NORMAL and SAFE_MODE. It evaluates voltage conditions and detects unintended reboots to make mode decisions, controls power to non‑critical subsystems during transitions, and maintains/persists mode state across reboots to ensure consistent post‑recovery behavior. ## Requirements | Name | Description | Validation | |---|---|---| -| MM0001 | The ModeManager shall maintain three operational modes: NORMAL, SAFE_MODE, and PAYLOAD_MODE | Integration Testing | +| MM0001 | The ModeManager shall maintain two operational modes: NORMAL and SAFE_MODE | Integration Testing | | MM0002 | The ModeManager shall enter safe mode when commanded manually via FORCE_SAFE_MODE command | Integration Testing | | MM0003 | The ModeManager shall enter safe mode when requested by external components via forceSafeMode port | Integration Testing | -| MM0004 | The ModeManager shall exit safe mode only via explicit EXIT_SAFE_MODE command | Integration Testing | +| MM0004 | The ModeManager shall exit safe mode only via explicit EXIT_SAFE_MODE command or automatic voltage recovery | Integration Testing | | MM0005 | The ModeManager shall prevent exit from safe mode when not currently in safe mode | Integration Testing | -| MM0006 | The ModeManager shall turn off all 8 load switches when entering safe mode | Integration Testing | -| MM0007 | The ModeManager shall turn on face load switches (indices 0-5) when exiting safe mode; payload switches remain off until payload mode | Integration Testing | -| MM0008 | The ModeManager shall persist current mode and safe mode entry count to non-volatile storage | Integration Testing | +| MM0006 | The ModeManager shall turn off all 8 load switches (faces 0-5 + payload 6-7) when entering safe mode | Integration Testing | +| MM0007 | The ModeManager shall turn on face load switches (indices 0-5) when exiting safe mode; payload switches (6-7) remain off | Integration Testing | +| MM0008 | The ModeManager shall persist current mode, safe mode entry count, safe mode reason, and clean shutdown flag to non-volatile storage | Integration Testing | | MM0009 | The ModeManager shall restore mode state from persistent storage on initialization | Integration Testing | | MM0010 | The ModeManager shall track and report the number of times safe mode has been entered | Integration Testing | | MM0011 | The ModeManager shall allow downstream components to query the current mode via getMode port | Unit Testing | | MM0012 | The ModeManager shall notify downstream components of mode changes with the new mode value | Unit Testing | -| MM0013 | The ModeManager shall enter payload mode when commanded via ENTER_PAYLOAD_MODE while in NORMAL and reject entry from SAFE_MODE | Integration Testing | -| MM0014 | The ModeManager shall exit payload mode only via explicit EXIT_PAYLOAD_MODE command and reject exit when not in payload mode | Integration Testing | -| MM0016 | The ModeManager shall turn on payload load switches (indices 6 and 7) when entering payload mode and turn them off when exiting payload mode | Integration Testing | -| MM0017 | The ModeManager shall track and report the number of times payload mode has been entered | Integration Testing | -| MM0018 | The ModeManager shall persist payload mode state and payload mode entry count to non-volatile storage and restore them on initialization | Integration Testing | -| MM0019 | The ModeManager shall reject FORCE_SAFE_MODE from payload mode (must exit payload mode first for sequential transitions) | Integration Testing | +| MM0020 | The ModeManager shall automatically enter safe mode when voltage drops below 6.7V for 10 consecutive seconds | Integration Testing | +| MM0021 | The ModeManager shall automatically exit safe mode when voltage recovers above 8.0V for 10 consecutive seconds (only if reason is LOW_BATTERY) | Integration Testing | +| MM0022 | The ModeManager shall track safe mode reason (NONE, LOW_BATTERY, SYSTEM_FAULT, GROUND_COMMAND, EXTERNAL_REQUEST, LORA) | Integration Testing | +| MM0023 | The ModeManager shall detect unintended reboots and enter safe mode with reason SYSTEM_FAULT | Integration Testing | +| MM0024 | The ModeManager shall set clean shutdown flag when prepareForReboot port is called | Integration Testing | ## Usage Examples -The ModeManager component operates as an active component that manages system-wide operational modes. It runs at 1Hz via the rate group and responds to commands and external fault conditions. +The ModeManager component operates as an active component that manages system-wide operational modes. It runs at 1Hz via the rate group and responds to commands, voltage conditions, and external fault conditions. ### Typical Usage 1. **System Initialization** - Component is instantiated during system startup - Loads previous mode state from `/mode_state.bin` + - Detects unintended reboots via clean shutdown flag - Configures load switches to match restored mode - Begins 1Hz periodic execution via rate group 2. **Normal Operation** - - Updates telemetry channels (CurrentMode, SafeModeEntryCount, PayloadModeEntryCount) + - Updates telemetry channels (CurrentMode, SafeModeEntryCount, CurrentSafeModeReason) + - Monitors system voltage for automatic safe mode entry - Responds to mode query requests from downstream components - - Keeps payload load switches (indices 6 and 7) off unless payload mode is explicitly entered 3. **Safe Mode Entry** - - Can be triggered by (only from NORMAL mode - sequential transitions enforced): - - Ground command: `FORCE_SAFE_MODE` - - External component request via `forceSafeMode` port + - Can be triggered by: + - Ground command: `FORCE_SAFE_MODE` (reason: GROUND_COMMAND) + - External component request via `forceSafeMode` port with specified reason (e.g., LORA, EXTERNAL_REQUEST) + - Low voltage condition < 6.7V for 10 seconds (reason: LOW_BATTERY) + - Unintended reboot detection (reason: SYSTEM_FAULT) - Actions performed: - Transitions mode to SAFE_MODE + - Sets safe mode reason - Increments safe mode entry counter - - Emits `EnteringSafeMode` event with reason - - Turns off all 8 load switches + - Emits appropriate event (EnteringSafeMode, AutoSafeModeEntry, UnintendedRebootDetected) + - Turns off all 8 load switches (faces 0-5 + payload 6-7) - Notifies downstream components via `modeChanged` port - Persists state to flash storage -4. **Payload Mode Entry** - - Triggered by ground command: `ENTER_PAYLOAD_MODE` (only allowed from NORMAL) - - Actions performed: - - Transitions mode to PAYLOAD_MODE - - Increments payload mode entry counter - - Emits `EnteringPayloadMode` event and, for commands, `ManualPayloadModeEntry` - - Turns on payload load switches (indices 6 and 7) - - Notifies downstream components via `modeChanged` port - - Updates telemetry (CurrentMode, PayloadModeEntryCount) - - Persists state to flash storage - -5. **Payload Mode Exit** - - Triggered by ground command: `EXIT_PAYLOAD_MODE` - - Validates currently in payload mode before allowing exit +4. **Safe Mode Exit (Manual)** + - Triggered by ground command: `EXIT_SAFE_MODE` + - Validates currently in safe mode before allowing exit - Actions performed: - Transitions mode to NORMAL - - Emits `ExitingPayloadMode` event - - Turns off payload load switches (indices 6 and 7) + - Clears safe mode reason to NONE + - Emits `ExitingSafeMode` event + - Turns on face load switches (indices 0-5) - Notifies downstream components via `modeChanged` port - - Updates telemetry - Persists state to flash storage -6. **Safe Mode Exit** - - Triggered only by ground command: `EXIT_SAFE_MODE` - - Validates currently in safe mode before allowing exit +5. **Safe Mode Exit (Automatic)** + - Only triggers when safe mode reason is LOW_BATTERY + - Requires voltage > 8.0V for 10 consecutive seconds - Actions performed: - Transitions mode to NORMAL - - Emits `ExitingSafeMode` event - - Turns on face load switches (indices 0-5); payload switches remain off until explicitly entering payload mode + - Clears safe mode reason to NONE + - Emits `AutoSafeModeExit` event with recovered voltage + - Turns on face load switches (indices 0-5) - Notifies downstream components via `modeChanged` port - Persists state to flash storage -7. **Mode Queries** +6. **Mode Queries** - Downstream components can call `getMode` port to query current mode - Returns immediate synchronous response with current mode @@ -103,40 +94,50 @@ classDiagram <> - m_mode: SystemMode - m_safeModeEntryCount: U32 - - m_payloadModeEntryCount: U32 - m_runCounter: U32 + - m_safeModeReason: SafeModeReason + - m_safeModeVoltageCounter: U32 + - m_recoveryVoltageCounter: U32 - STATE_FILE_PATH: const char* + - SAFE_MODE_ENTRY_VOLTAGE: F32 = 6.7 + - SAFE_MODE_RECOVERY_VOLTAGE: F32 = 8.0 + - SAFE_MODE_DEBOUNCE_SECONDS: U32 = 10 + ModeManager(const char* compName) + ~ModeManager() + init(FwSizeType queueDepth, FwEnumStoreType instance) - run_handler(FwIndexType portNum, U32 context) - - forceSafeMode_handler(FwIndexType portNum) + - forceSafeMode_handler(FwIndexType portNum, SafeModeReason reason) - getMode_handler(FwIndexType portNum): SystemMode + - prepareForReboot_handler(FwIndexType portNum) - FORCE_SAFE_MODE_cmdHandler(FwOpcodeType opCode, U32 cmdSeq) - EXIT_SAFE_MODE_cmdHandler(FwOpcodeType opCode, U32 cmdSeq) - - ENTER_PAYLOAD_MODE_cmdHandler(FwOpcodeType opCode, U32 cmdSeq) - - EXIT_PAYLOAD_MODE_cmdHandler(FwOpcodeType opCode, U32 cmdSeq) - loadState() - saveState() - - enterSafeMode(const char* reason) + - enterSafeMode(SafeModeReason reason) - exitSafeMode() - - enterPayloadMode(const char* reason) - - exitPayloadMode() + - exitSafeModeAutomatic(F32 voltage) - turnOffNonCriticalComponents() - turnOnComponents() - - turnOnPayload() - - turnOffPayload() - getCurrentVoltage(bool& valid): F32 } class SystemMode { <> SAFE_MODE = 1 NORMAL = 2 - PAYLOAD_MODE = 3 + } + class SafeModeReason { + <> + NONE = 0 + LOW_BATTERY = 1 + SYSTEM_FAULT = 2 + GROUND_COMMAND = 3 + EXTERNAL_REQUEST = 4 + LORA = 5 } } ModeManagerComponentBase <|-- ModeManager : inherits ModeManager --> SystemMode : uses + ModeManager --> SafeModeReason : uses ``` ## Port Descriptions @@ -144,35 +145,47 @@ classDiagram ### Input Ports | Name | Type | Kind | Description | |---|---|---|---| -| run | Svc.Sched | sync | Receives periodic calls from rate group (1Hz) for telemetry updates | -| forceSafeMode | Fw.Signal | async | Receives safe mode requests from external components detecting faults | +| run | Svc.Sched | sync | Receives periodic calls from rate group (1Hz) for telemetry updates and voltage monitoring | +| forceSafeMode | Components.ForceSafeModeWithReason | async | Receives safe mode requests from external components with reason (NONE defaults to EXTERNAL_REQUEST) | | getMode | Components.GetSystemMode | sync | Allows downstream components to query current system mode | +| prepareForReboot | Fw.Signal | sync | Called by ResetManager before intentional reboot to set clean shutdown flag | ### Output Ports | Name | Type | Kind | Description | |---|---|---|---| | modeChanged | Components.SystemModeChanged | output | Notifies downstream components of mode changes with new mode value | -| loadSwitchTurnOn | Fw.Signal [8] | output | Signals to turn on each of 8 load switches (faces 0-5, payload power, payload battery) | -| loadSwitchTurnOff | Fw.Signal [8] | output | Signals to turn off each of 8 load switches | +| loadSwitchTurnOn | Fw.Signal [8] | output | Signals to turn on load switches (faces 0-5, payload power 6, payload battery 7) | +| loadSwitchTurnOff | Fw.Signal [8] | output | Signals to turn off load switches (faces 0-5, payload power 6, payload battery 7) | | voltageGet | Drv.VoltageGet | output | Queries INA219 manager for current system voltage | ## Component States | Name | Type | Description | |---|---|---| -| m_mode | SystemMode | Current operational mode (NORMAL, SAFE_MODE, or PAYLOAD_MODE) | +| m_mode | SystemMode | Current operational mode (NORMAL or SAFE_MODE) | | m_safeModeEntryCount | U32 | Number of times safe mode has been entered since initial deployment | -| m_payloadModeEntryCount | U32 | Number of times payload mode has been entered since initial deployment | +| m_safeModeReason | SafeModeReason | Current reason for being in safe mode (or NONE if not in safe mode) | | m_runCounter | U32 | Counter for 1Hz run handler calls | +| m_safeModeVoltageCounter | U32 | Debounce counter for low voltage detection (in NORMAL mode) | +| m_recoveryVoltageCounter | U32 | Debounce counter for voltage recovery detection (in SAFE_MODE) | ### Persistent State The component persists the following state to `/mode_state.bin`: - Current mode (U8) - Safe mode entry count (U32) -- Payload mode entry count (U32) +- Safe mode reason (U8) +- Clean shutdown flag (U8) This state is loaded on initialization and saved on every mode transition. +## Voltage Thresholds + +| Threshold | Value | Description | +|---|---|---| +| SAFE_MODE_ENTRY_VOLTAGE | 6.7V | Voltage below which safe mode is entered from NORMAL | +| SAFE_MODE_RECOVERY_VOLTAGE | 8.0V | Voltage above which safe mode can be exited automatically | +| SAFE_MODE_DEBOUNCE_SECONDS | 10 | Consecutive seconds required for voltage-based transitions | + ## Sequence Diagrams ### Safe Mode Entry (Command) @@ -187,95 +200,100 @@ sequenceDiagram Ground->>ModeManager: FORCE_SAFE_MODE command ModeManager->>ModeManager: Emit ManualSafeModeEntry event ModeManager->>ModeManager: Set m_mode = SAFE_MODE + ModeManager->>ModeManager: Set m_safeModeReason = GROUND_COMMAND ModeManager->>ModeManager: Increment m_safeModeEntryCount ModeManager->>ModeManager: Emit EnteringSafeMode event - ModeManager->>LoadSwitches: Turn off all 8 switches + ModeManager->>LoadSwitches: Turn off all 8 switches (faces + payload) ModeManager->>ModeManager: Update telemetry ModeManager->>DownstreamComponents: modeChanged_out(SAFE_MODE) ModeManager->>FlashStorage: Save state to /mode_state.bin ModeManager->>Ground: Command response OK ``` -### Safe Mode Entry (External Request) +### Safe Mode Entry (Low Voltage) ```mermaid sequenceDiagram - participant ExternalComponent + participant RateGroup participant ModeManager + participant INA219 participant LoadSwitches participant DownstreamComponents participant FlashStorage - ExternalComponent->>ModeManager: forceSafeMode port call - ModeManager->>ModeManager: Emit ExternalFaultDetected event + loop Every 1Hz + RateGroup->>ModeManager: run() + ModeManager->>INA219: voltageGet_out() + INA219-->>ModeManager: voltage < 6.7V + ModeManager->>ModeManager: Increment m_safeModeVoltageCounter + end + Note over ModeManager: After 10 consecutive low readings + ModeManager->>ModeManager: Emit AutoSafeModeEntry event (LOW_BATTERY) ModeManager->>ModeManager: Set m_mode = SAFE_MODE - ModeManager->>ModeManager: Increment m_safeModeEntryCount - ModeManager->>ModeManager: Emit EnteringSafeMode event - ModeManager->>LoadSwitches: Turn off all 8 switches - ModeManager->>ModeManager: Update telemetry + ModeManager->>ModeManager: Set m_safeModeReason = LOW_BATTERY + ModeManager->>LoadSwitches: Turn off all 8 switches (faces + payload) ModeManager->>DownstreamComponents: modeChanged_out(SAFE_MODE) - ModeManager->>FlashStorage: Save state to /mode_state.bin + ModeManager->>FlashStorage: Save state ``` -### Safe Mode Exit +### Safe Mode Exit (Automatic Voltage Recovery) ```mermaid sequenceDiagram - participant Ground + participant RateGroup participant ModeManager + participant INA219 participant LoadSwitches participant DownstreamComponents participant FlashStorage - Ground->>ModeManager: EXIT_SAFE_MODE command - ModeManager->>ModeManager: Validate currently in SAFE_MODE + Note over ModeManager: In SAFE_MODE with reason=LOW_BATTERY + loop Every 1Hz + RateGroup->>ModeManager: run() + ModeManager->>INA219: voltageGet_out() + INA219-->>ModeManager: voltage > 8.0V + ModeManager->>ModeManager: Increment m_recoveryVoltageCounter + end + Note over ModeManager: After 10 consecutive high readings + ModeManager->>ModeManager: Emit AutoSafeModeExit event (with voltage) ModeManager->>ModeManager: Set m_mode = NORMAL - ModeManager->>ModeManager: Emit ExitingSafeMode event - ModeManager->>LoadSwitches: Turn on face switches (indices 0-5) - ModeManager->>ModeManager: Update telemetry + ModeManager->>ModeManager: Set m_safeModeReason = NONE + ModeManager->>LoadSwitches: Turn on face switches (0-5) ModeManager->>DownstreamComponents: modeChanged_out(NORMAL) - ModeManager->>FlashStorage: Save state to /mode_state.bin - ModeManager->>Ground: Command response OK + ModeManager->>FlashStorage: Save state ``` -### Payload Mode Entry (Command) +### Unintended Reboot Detection ```mermaid sequenceDiagram - participant Ground + participant Boot participant ModeManager + participant FlashStorage participant LoadSwitches participant DownstreamComponents - participant FlashStorage - Ground->>ModeManager: ENTER_PAYLOAD_MODE command - ModeManager->>ModeManager: Validate currently in NORMAL - ModeManager->>ModeManager: Emit ManualPayloadModeEntry event - ModeManager->>ModeManager: Set m_mode = PAYLOAD_MODE - ModeManager->>ModeManager: Increment m_payloadModeEntryCount - ModeManager->>ModeManager: Emit EnteringPayloadMode event - ModeManager->>LoadSwitches: Turn on payload switches (6 & 7) - ModeManager->>ModeManager: Update telemetry - ModeManager->>DownstreamComponents: modeChanged_out(PAYLOAD_MODE) - ModeManager->>FlashStorage: Save state to /mode_state.bin - ModeManager->>Ground: Command response OK + Boot->>ModeManager: init() + ModeManager->>FlashStorage: Load /mode_state.bin + FlashStorage-->>ModeManager: state.cleanShutdown = 0, state.mode = NORMAL + ModeManager->>ModeManager: Detect unintended reboot + ModeManager->>ModeManager: Emit UnintendedRebootDetected event + ModeManager->>ModeManager: Set m_safeModeReason = SYSTEM_FAULT + ModeManager->>ModeManager: Set m_mode = SAFE_MODE + ModeManager->>LoadSwitches: Turn off all 8 switches (faces + payload) + ModeManager->>DownstreamComponents: modeChanged_out(SAFE_MODE) + ModeManager->>FlashStorage: Save state (cleanShutdown = 0) ``` -### Payload Mode Exit (Command) +### Prepare For Reboot (Clean Shutdown) ```mermaid sequenceDiagram - participant Ground + participant ResetManager participant ModeManager - participant LoadSwitches - participant DownstreamComponents participant FlashStorage - Ground->>ModeManager: EXIT_PAYLOAD_MODE command - ModeManager->>ModeManager: Validate currently in PAYLOAD_MODE - ModeManager->>ModeManager: Set m_mode = NORMAL - ModeManager->>ModeManager: Emit ExitingPayloadMode event - ModeManager->>LoadSwitches: Turn off payload switches (6 & 7) - ModeManager->>ModeManager: Update telemetry - ModeManager->>DownstreamComponents: modeChanged_out(NORMAL) - ModeManager->>FlashStorage: Save state to /mode_state.bin - ModeManager->>Ground: Command response OK + ResetManager->>ModeManager: prepareForReboot_out() + ModeManager->>ModeManager: Emit PreparingForReboot event + ModeManager->>FlashStorage: Save state with cleanShutdown = 1 + ModeManager-->>ResetManager: Return + ResetManager->>ResetManager: sys_reboot() ``` ### Mode Query @@ -286,82 +304,80 @@ sequenceDiagram DownstreamComponent->>ModeManager: getMode() port call ModeManager->>ModeManager: Read m_mode - ModeManager-->>DownstreamComponent: Return current mode (NORMAL, SAFE_MODE, or PAYLOAD_MODE) -``` - -### Periodic Execution (1Hz) -```mermaid -sequenceDiagram - participant RateGroup - participant ModeManager - - RateGroup->>ModeManager: run(portNum, context) - ModeManager->>ModeManager: Increment m_runCounter - ModeManager->>ModeManager: Write CurrentMode telemetry + ModeManager-->>DownstreamComponent: Return current mode (NORMAL or SAFE_MODE) ``` ## Commands | Name | Arguments | Description | |---|---|---| -| FORCE_SAFE_MODE | None | Forces the system into safe mode. Only allowed from NORMAL mode (rejects from PAYLOAD_MODE with validation error). Emits ManualSafeModeEntry event. Idempotent when already in safe mode. | -| EXIT_SAFE_MODE | None | Exits safe mode and returns to normal operation. Fails with CommandValidationFailed if not currently in safe mode. | -| ENTER_PAYLOAD_MODE | None | Enters payload mode from NORMAL. Fails with CommandValidationFailed if issued from SAFE_MODE or if already in payload mode (idempotent success when already in payload). Emits ManualPayloadModeEntry event. | -| EXIT_PAYLOAD_MODE | None | Exits payload mode and returns to normal operation. Fails with CommandValidationFailed if not currently in payload mode. | +| FORCE_SAFE_MODE | None | Forces the system into safe mode with reason GROUND_COMMAND. Emits ManualSafeModeEntry event. Idempotent when already in safe mode. | +| EXIT_SAFE_MODE | None | Exits safe mode and returns to normal operation. Fails with CommandValidationFailed if not currently in safe mode. Clears safe mode reason to NONE. | ## Events | Name | Severity | Arguments | Description | |---|---|---|---| -| EnteringSafeMode | WARNING_HI | reason: string size 100 | Emitted when entering safe mode, includes reason (e.g., "Ground command", "External component request") | -| ExitingSafeMode | ACTIVITY_HI | None | Emitted when exiting safe mode and returning to normal operation | +| EnteringSafeMode | WARNING_HI | reason: string size 100 | Emitted when entering safe mode, includes reason string | +| ExitingSafeMode | ACTIVITY_HI | None | Emitted when manually exiting safe mode | | ManualSafeModeEntry | ACTIVITY_HI | None | Emitted when safe mode is manually commanded via FORCE_SAFE_MODE | | ExternalFaultDetected | WARNING_HI | None | Emitted when an external component triggers safe mode via forceSafeMode port | -| EnteringPayloadMode | ACTIVITY_HI | reason: string size 100 | Emitted when entering payload mode, includes reason (e.g., "Ground command") | -| ExitingPayloadMode | ACTIVITY_HI | None | Emitted when exiting payload mode and returning to normal operation | -| ManualPayloadModeEntry | ACTIVITY_HI | None | Emitted when payload mode is manually commanded via ENTER_PAYLOAD_MODE | -| CommandValidationFailed | WARNING_LO | cmdName: string size 50
reason: string size 100 | Emitted when a command fails validation (e.g., EXIT_SAFE_MODE when not in safe mode) | -| StatePersistenceFailure | WARNING_LO | operation: string size 20
status: I32 | Emitted when state save/load operations fail | +| AutoSafeModeEntry | WARNING_HI | reason: SafeModeReason, voltage: F32 | Emitted when automatically entering safe mode due to low voltage | +| AutoSafeModeExit | ACTIVITY_HI | voltage: F32 | Emitted when automatically exiting safe mode due to voltage recovery | +| UnintendedRebootDetected | WARNING_HI | None | Emitted when an unintended reboot is detected on startup | +| PreparingForReboot | ACTIVITY_HI | None | Emitted when prepareForReboot is called before intentional reboot | +| CommandValidationFailed | WARNING_LO | cmdName: string size 50, reason: string size 100 | Emitted when a command fails validation | +| StatePersistenceFailure | WARNING_LO | operation: string size 20, status: I32 | Emitted when state save/load operations fail | ## Telemetry | Name | Type | Update Rate | Description | |---|---|---|---| -| CurrentMode | U8 | 1Hz | Current system mode (1 = SAFE_MODE, 2 = NORMAL, 3 = PAYLOAD_MODE) | +| CurrentMode | U8 | 1Hz | Current system mode (1 = SAFE_MODE, 2 = NORMAL) | | SafeModeEntryCount | U32 | On change | Number of times safe mode has been entered (persists across reboots) | -| PayloadModeEntryCount | U32 | On change | Number of times payload mode has been entered (persists across reboots) | +| CurrentSafeModeReason | SafeModeReason | 1Hz | Current reason for safe mode (NONE if not in safe mode) | ## Load Switch Mapping -The ModeManager controls 8 load switches that power non-critical satellite subsystems: +The ModeManager controls 8 load switches that power satellite subsystems: -| Index | Subsystem | NORMAL State | SAFE_MODE State | PAYLOAD_MODE State | -|---|---|---|---|---| -| 0 | Satellite Face 0 | ON | OFF | ON | -| 1 | Satellite Face 1 | ON | OFF | ON | -| 2 | Satellite Face 2 | ON | OFF | ON | -| 3 | Satellite Face 3 | ON | OFF | ON | -| 4 | Satellite Face 4 | ON | OFF | ON | -| 5 | Satellite Face 5 | ON | OFF | ON | -| 6 | Payload Power | OFF | OFF | ON | -| 7 | Payload Battery | OFF | OFF | ON | +| Index | Subsystem | NORMAL State | SAFE_MODE State | +|---|---|---|---| +| 0 | Satellite Face 4 | ON | OFF | +| 1 | Satellite Face 0 | ON | OFF | +| 2 | Satellite Face 1 | ON | OFF | +| 3 | Satellite Face 2 | ON | OFF | +| 4 | Satellite Face 3 | ON | OFF | +| 5 | Satellite Face 5 | ON | OFF | +| 6 | Payload Power | OFF | OFF | +| 7 | Payload Battery | OFF | OFF | + +> **Note:** When entering SAFE_MODE, all 8 switches are turned OFF. When exiting to NORMAL mode, only face switches (0-5) are turned ON. Payload switches (6-7) remain OFF and must be controlled separately. + +## Safe Mode Reason Logic + +| Reason | Trigger | Auto-Recovery | Manual Exit | +|---|---|---|---| +| LOW_BATTERY | Voltage < 6.7V for 10s | Yes (when voltage > 8.0V for 10s) | Yes | +| SYSTEM_FAULT | Unintended reboot detected | No | Yes | +| GROUND_COMMAND | FORCE_SAFE_MODE command | No | Yes | +| EXTERNAL_REQUEST | forceSafeMode port call | No | Yes | +| LORA | LoRa communication timeout or fault | No | Yes | -> **Note:** PAYLOAD_MODE can only be entered from NORMAL mode (not from SAFE_MODE). When restoring PAYLOAD_MODE from persistent storage after a reboot, both face switches (0-5) and payload switches (6-7) are explicitly turned ON to ensure consistent state. ## Integration Tests -See `FprimeZephyrReference/test/int/mode_manager_test.py` and `FprimeZephyrReference/test/int/payload_mode_test.py` for comprehensive integration tests covering: +See `FprimeZephyrReference/test/int/safe_mode_test.py` for comprehensive integration tests covering: | Test | Description | Coverage | |---|---|---| -| test_01_initial_telemetry | Verifies initial telemetry can be read | Basic functionality | -| test_04_force_safe_mode_command | Tests FORCE_SAFE_MODE command enters safe mode | Safe mode entry | -| test_06_safe_mode_turns_off_load_switches | Verifies all load switches turn off in safe mode | Power management | -| test_14_exit_safe_mode_success | Tests successful safe mode exit | Safe mode exit | -| test_19_safe_mode_state_persists | Verifies safe mode persistence to flash | State persistence | -| test_payload_01_enter_exit_payload_mode | Validates payload mode entry/exit, events, telemetry, payload load switches | Payload mode entry/exit | -| test_payload_02_cannot_enter_from_safe_mode | Ensures ENTER_PAYLOAD_MODE fails from SAFE_MODE | Command validation | -| test_payload_03_safe_mode_rejected_from_payload | Ensures FORCE_SAFE_MODE is rejected from payload mode (sequential transitions) | Command validation | -| test_payload_04_state_persists | Verifies payload mode and counters persist | Payload persistence | +| test_safe_01_initial_safe_mode_reason_is_none | Verifies CurrentSafeModeReason is NONE in NORMAL mode | Telemetry | +| test_safe_02_ground_command_sets_reason | Tests FORCE_SAFE_MODE sets reason to GROUND_COMMAND | Safe mode entry | +| test_safe_03_exit_clears_reason | Verifies EXIT_SAFE_MODE clears reason to NONE | Safe mode exit | +| test_safe_04_no_auto_recovery_for_ground_command | Ensures no auto-recovery when reason is GROUND_COMMAND | Auto-recovery logic | +| test_safe_05_auto_entry_low_voltage | Tests automatic safe mode entry due to low voltage | Voltage monitoring | +| test_safe_06_auto_recovery_voltage | Tests automatic safe mode exit due to voltage recovery | Voltage monitoring | +| test_safe_07_unintended_reboot_detection | Tests unintended reboot detection and SYSTEM_FAULT | Reboot detection | +| test_safe_08_clean_reboot_no_safe_mode | Verifies clean reboot does not trigger safe mode | Reboot handling | ## Design Decisions @@ -378,23 +394,33 @@ The component provides both pull-based (getMode port) and push-based (modeChange - Real-time mode tracking - Avoiding polling overhead -This dual approach ensures downstream components can reliably track system mode even if they miss a transition notification. - ### State Persistence Mode state is persisted to `/mode_state.bin` to maintain operational context across: -- Intentional reboots -- Watchdog resets -- Power cycles +- Intentional reboots (clean shutdown flag set) +- Watchdog resets (unintended reboot detection) +- Power cycles (unintended reboot detection) + +### Voltage-Based Auto-Transitions +The ModeManager implements hysteresis in voltage-based transitions: +- Entry threshold: 6.7V (lower) +- Recovery threshold: 8.0V (higher) +- Debounce: 10 consecutive 1Hz samples -This ensures the system resumes in the correct mode after recovery. +This prevents oscillation between modes when voltage is near thresholds. -### Sequential Mode Transitions -Mode transitions follow a +1/-1 sequential pattern: SAFE_MODE(1) ↔ NORMAL(2) ↔ PAYLOAD_MODE(3). Direct jumps (e.g., PAYLOAD→SAFE) are not allowed - users must exit payload mode first before entering safe mode. FORCE_SAFE_MODE is idempotent when already in safe mode. +### Safe Mode Reason Tracking +The reason for entering safe mode is tracked to enable intelligent recovery: +- LOW_BATTERY: Auto-recovery allowed when voltage recovers +- Other reasons: Manual EXIT_SAFE_MODE command required ## Change Log | Date | Description | |---|---| -| 2025-11-26 | Reordered enum values (SAFE=1, NORMAL=2, PAYLOAD=3) for sequential +1/-1 transitions; FORCE_SAFE_MODE now rejected from payload mode | -| 2025-11-26 | Removed forcePayloadMode port - payload mode now only entered via ENTER_PAYLOAD_MODE ground command | -| 2025-11-25 | Added PAYLOAD_MODE (commands, events, telemetry, persistence, payload load switch control) and documented payload integration tests | -| 2025-11-19 | Added getMode query port and enhanced modeChanged to carry mode value | +| 2025-11-30 | Changed forceSafeMode port to accept SafeModeReason parameter (defaults to EXTERNAL_REQUEST if NONE) | +| 2025-11-30 | Added LORA SafeModeReason for LoRa communication timeout/fault | +| 2025-11-30 | Removed payload mode (PAYLOAD_MODE enum, commands, events, telemetry, load switch connections) | +| 2025-11-30 | Added safe mode auto-entry/exit based on voltage thresholds (6.7V entry, 8.0V recovery) | +| 2025-11-30 | Added SafeModeReason tracking (LOW_BATTERY, SYSTEM_FAULT, GROUND_COMMAND, EXTERNAL_REQUEST) | +| 2025-11-30 | Added unintended reboot detection via clean shutdown flag | +| 2025-11-30 | Added prepareForReboot port for ResetManager integration | +| 2025-11-26 | Initial implementation with NORMAL and SAFE_MODE | diff --git a/FprimeZephyrReference/Components/ResetManager/ResetManager.cpp b/FprimeZephyrReference/Components/ResetManager/ResetManager.cpp index a4cb814a..b15bb362 100644 --- a/FprimeZephyrReference/Components/ResetManager/ResetManager.cpp +++ b/FprimeZephyrReference/Components/ResetManager/ResetManager.cpp @@ -56,6 +56,12 @@ void ResetManager ::handleColdReset() { // Log the cold reset event this->log_ACTIVITY_HI_INITIATE_COLD_RESET(); + // Notify ModeManager to set clean shutdown flag before rebooting + // This allows ModeManager to detect unintended reboots on next startup + if (this->isConnected_prepareForReboot_OutputPort(0)) { + this->prepareForReboot_out(0); + } + sys_reboot(SYS_REBOOT_COLD); } @@ -63,6 +69,12 @@ void ResetManager ::handleWarmReset() { // Log the warm reset event this->log_ACTIVITY_HI_INITIATE_WARM_RESET(); + // Notify ModeManager to set clean shutdown flag before rebooting + // This allows ModeManager to detect unintended reboots on next startup + if (this->isConnected_prepareForReboot_OutputPort(0)) { + this->prepareForReboot_out(0); + } + sys_reboot(SYS_REBOOT_WARM); } diff --git a/FprimeZephyrReference/Components/ResetManager/ResetManager.fpp b/FprimeZephyrReference/Components/ResetManager/ResetManager.fpp index 5ee3eb58..e73c4823 100644 --- a/FprimeZephyrReference/Components/ResetManager/ResetManager.fpp +++ b/FprimeZephyrReference/Components/ResetManager/ResetManager.fpp @@ -20,6 +20,9 @@ module Components { @ Port to invoke a warm reset sync input port warmReset: Fw.Signal + @ Port to notify ModeManager before reboot (sets clean shutdown flag) + output port prepareForReboot: Fw.Signal + ############################################################################### # Standard AC Ports: Required for Channels, Events, Commands, and Parameters # ############################################################################### diff --git a/FprimeZephyrReference/ReferenceDeployment/Top/ReferenceDeploymentPackets.fppi b/FprimeZephyrReference/ReferenceDeployment/Top/ReferenceDeploymentPackets.fppi index 6a04a110..70a586b9 100644 --- a/FprimeZephyrReference/ReferenceDeployment/Top/ReferenceDeploymentPackets.fppi +++ b/FprimeZephyrReference/ReferenceDeployment/Top/ReferenceDeploymentPackets.fppi @@ -13,7 +13,7 @@ telemetry packets ReferenceDeploymentPackets { ReferenceDeployment.startupManager.QuiescenceEndTime ReferenceDeployment.modeManager.CurrentMode ReferenceDeployment.modeManager.SafeModeEntryCount - ReferenceDeployment.modeManager.PayloadModeEntryCount + ReferenceDeployment.modeManager.CurrentSafeModeReason } packet HealthWarnings id 2 group 1 { diff --git a/FprimeZephyrReference/ReferenceDeployment/Top/topology.fpp b/FprimeZephyrReference/ReferenceDeployment/Top/topology.fpp index 0390534e..adb69798 100644 --- a/FprimeZephyrReference/ReferenceDeployment/Top/topology.fpp +++ b/FprimeZephyrReference/ReferenceDeployment/Top/topology.fpp @@ -233,6 +233,10 @@ module ReferenceDeployment { # Voltage monitoring from system power manager modeManager.voltageGet -> ina219SysManager.voltageGet + # Connection for clean shutdown notification from ResetManager + # Allows ModeManager to detect unintended reboots + resetManager.prepareForReboot -> modeManager.prepareForReboot + # Load switch control connections # The load switch index mapping below is non-sequential because it matches the physical board layout and wiring order. # This ordering ensures that software indices correspond to the hardware arrangement for easier maintenance and debugging. diff --git a/FprimeZephyrReference/test/int/payload_mode_test.py b/FprimeZephyrReference/test/int/payload_mode_test.py deleted file mode 100644 index b8b76aa3..00000000 --- a/FprimeZephyrReference/test/int/payload_mode_test.py +++ /dev/null @@ -1,239 +0,0 @@ -""" -payload_mode_test.py: - -Integration tests for the ModeManager component (payload mode). - -Tests cover: -- Payload mode entry and exit -- Mode transition validation (cannot enter from safe mode) -- Sequential transitions (FORCE_SAFE_MODE rejected from payload mode) -- State persistence - -Total: 4 tests - -Mode enum values: SAFE_MODE=1, NORMAL=2, PAYLOAD_MODE=3 -""" - -import time - -import pytest -from common import proves_send_and_assert_command -from fprime_gds.common.data_types.ch_data import ChData -from fprime_gds.common.testing_fw.api import IntegrationTestAPI - -component = "ReferenceDeployment.modeManager" -payload_load_switch_channels = [ - "ReferenceDeployment.payloadPowerLoadSwitch.IsOn", - "ReferenceDeployment.payloadBatteryLoadSwitch.IsOn", -] -all_load_switch_channels = [ - "ReferenceDeployment.face4LoadSwitch.IsOn", - "ReferenceDeployment.face0LoadSwitch.IsOn", - "ReferenceDeployment.face1LoadSwitch.IsOn", - "ReferenceDeployment.face2LoadSwitch.IsOn", - "ReferenceDeployment.face3LoadSwitch.IsOn", - "ReferenceDeployment.face5LoadSwitch.IsOn", - "ReferenceDeployment.payloadPowerLoadSwitch.IsOn", - "ReferenceDeployment.payloadBatteryLoadSwitch.IsOn", -] - - -@pytest.fixture(autouse=True) -def setup_and_teardown(fprime_test_api: IntegrationTestAPI, start_gds): - """ - Setup before each test and cleanup after. - Ensures clean state by exiting any mode back to NORMAL. - """ - # Setup: Try to get to a clean NORMAL state - try: - proves_send_and_assert_command(fprime_test_api, f"{component}.EXIT_SAFE_MODE") - except Exception: - pass - try: - proves_send_and_assert_command( - fprime_test_api, f"{component}.EXIT_PAYLOAD_MODE" - ) - except Exception: - pass - - # Clear event and telemetry history before test - fprime_test_api.clear_histories() - - yield - - # Teardown: Return to clean NORMAL state for next test - try: - proves_send_and_assert_command(fprime_test_api, f"{component}.EXIT_SAFE_MODE") - except Exception: - pass - try: - proves_send_and_assert_command( - fprime_test_api, f"{component}.EXIT_PAYLOAD_MODE" - ) - except Exception: - pass - - -# ============================================================================== -# Payload Mode Tests -# ============================================================================== - - -def test_payload_01_enter_exit_payload_mode( - fprime_test_api: IntegrationTestAPI, start_gds -): - """ - Test ENTER_PAYLOAD_MODE and EXIT_PAYLOAD_MODE commands. - Verifies: - - EnteringPayloadMode event is emitted - - CurrentMode telemetry = 3 (PAYLOAD_MODE) - - Payload load switches (6 & 7) are ON - - ExitingPayloadMode event is emitted on exit - - CurrentMode returns to 2 (NORMAL) - """ - # Enter payload mode - proves_send_and_assert_command( - fprime_test_api, - f"{component}.ENTER_PAYLOAD_MODE", - events=[f"{component}.ManualPayloadModeEntry"], - ) - - time.sleep(2) - - # Verify mode is PAYLOAD_MODE (3) - proves_send_and_assert_command(fprime_test_api, "CdhCore.tlmSend.SEND_PKT", ["1"]) - mode_result: ChData = fprime_test_api.assert_telemetry( - f"{component}.CurrentMode", timeout=5 - ) - assert mode_result.get_val() == 3, "Should be in PAYLOAD_MODE" - - # Verify payload load switches are ON - proves_send_and_assert_command(fprime_test_api, "CdhCore.tlmSend.SEND_PKT", ["9"]) - for channel in payload_load_switch_channels: - value = fprime_test_api.assert_telemetry(channel, timeout=5).get_val() - if isinstance(value, str): - assert value.upper() == "ON", f"{channel} should be ON in payload mode" - else: - assert value == 1, f"{channel} should be ON in payload mode" - - # Exit payload mode - proves_send_and_assert_command( - fprime_test_api, - f"{component}.EXIT_PAYLOAD_MODE", - events=[f"{component}.ExitingPayloadMode"], - ) - - time.sleep(2) - - # Verify mode is NORMAL (2) - proves_send_and_assert_command(fprime_test_api, "CdhCore.tlmSend.SEND_PKT", ["1"]) - mode_result: ChData = fprime_test_api.assert_telemetry( - f"{component}.CurrentMode", timeout=5 - ) - assert mode_result.get_val() == 2, "Should be in NORMAL mode" - - # Verify payload load switches are OFF in NORMAL mode - proves_send_and_assert_command(fprime_test_api, "CdhCore.tlmSend.SEND_PKT", ["9"]) - for channel in payload_load_switch_channels: - value = fprime_test_api.assert_telemetry(channel, timeout=5).get_val() - if isinstance(value, str): - assert value.upper() == "OFF", f"{channel} should be OFF in NORMAL mode" - else: - assert value == 0, f"{channel} should be OFF in NORMAL mode" - - -def test_payload_02_cannot_enter_from_safe_mode( - fprime_test_api: IntegrationTestAPI, start_gds -): - """ - Test that ENTER_PAYLOAD_MODE fails when in safe mode. - Must exit safe mode to NORMAL first. - """ - # Enter safe mode first - proves_send_and_assert_command(fprime_test_api, f"{component}.FORCE_SAFE_MODE") - time.sleep(2) - - # Verify in safe mode - proves_send_and_assert_command(fprime_test_api, "CdhCore.tlmSend.SEND_PKT", ["1"]) - mode_result: ChData = fprime_test_api.assert_telemetry( - f"{component}.CurrentMode", timeout=5 - ) - assert mode_result.get_val() == 1, "Should be in SAFE_MODE" - - # Try to enter payload mode - should fail - fprime_test_api.clear_histories() - with pytest.raises(Exception): - proves_send_and_assert_command( - fprime_test_api, f"{component}.ENTER_PAYLOAD_MODE" - ) - - # Verify CommandValidationFailed event - fprime_test_api.assert_event(f"{component}.CommandValidationFailed", timeout=3) - - # Verify still in safe mode - proves_send_and_assert_command(fprime_test_api, "CdhCore.tlmSend.SEND_PKT", ["1"]) - mode_result: ChData = fprime_test_api.assert_telemetry( - f"{component}.CurrentMode", timeout=5 - ) - assert mode_result.get_val() == 1, "Should still be in SAFE_MODE" - - -def test_payload_03_safe_mode_rejected_from_payload( - fprime_test_api: IntegrationTestAPI, start_gds -): - """ - Test that FORCE_SAFE_MODE is rejected from PAYLOAD_MODE (sequential transitions). - Must exit payload mode first before entering safe mode. - Verifies: - - FORCE_SAFE_MODE fails with validation error from payload mode - - CommandValidationFailed event is emitted - - Remains in PAYLOAD_MODE (3) - """ - # Enter payload mode first - proves_send_and_assert_command(fprime_test_api, f"{component}.ENTER_PAYLOAD_MODE") - time.sleep(2) - - # Verify in payload mode - proves_send_and_assert_command(fprime_test_api, "CdhCore.tlmSend.SEND_PKT", ["1"]) - mode_result: ChData = fprime_test_api.assert_telemetry( - f"{component}.CurrentMode", timeout=5 - ) - assert mode_result.get_val() == 3, "Should be in PAYLOAD_MODE" - - # Try to force safe mode - should fail (sequential transitions required) - fprime_test_api.clear_histories() - with pytest.raises(Exception): - proves_send_and_assert_command(fprime_test_api, f"{component}.FORCE_SAFE_MODE") - - # Verify CommandValidationFailed event - fprime_test_api.assert_event(f"{component}.CommandValidationFailed", timeout=3) - - # Verify still in payload mode - proves_send_and_assert_command(fprime_test_api, "CdhCore.tlmSend.SEND_PKT", ["1"]) - mode_result: ChData = fprime_test_api.assert_telemetry( - f"{component}.CurrentMode", timeout=5 - ) - assert mode_result.get_val() == 3, "Should still be in PAYLOAD_MODE" - - -def test_payload_04_state_persists(fprime_test_api: IntegrationTestAPI, start_gds): - """ - Test that payload mode state persists to /mode_state.bin file. - Note: Full persistence test would require reboot, this verifies state is saved. - """ - # Enter payload mode to trigger state save - proves_send_and_assert_command(fprime_test_api, f"{component}.ENTER_PAYLOAD_MODE") - time.sleep(2) - - # Verify mode is saved as PAYLOAD_MODE (3) - proves_send_and_assert_command(fprime_test_api, "CdhCore.tlmSend.SEND_PKT", ["1"]) - mode_result: ChData = fprime_test_api.assert_telemetry( - f"{component}.CurrentMode", timeout=5 - ) - assert mode_result.get_val() == 3, "Mode should be saved as PAYLOAD_MODE" - - # Verify PayloadModeEntryCount is tracking - count_result: ChData = fprime_test_api.assert_telemetry( - f"{component}.PayloadModeEntryCount", timeout=5 - ) - assert count_result.get_val() >= 1, "PayloadModeEntryCount should be at least 1" diff --git a/FprimeZephyrReference/test/int/safe_mode_test.py b/FprimeZephyrReference/test/int/safe_mode_test.py new file mode 100644 index 00000000..6bf92928 --- /dev/null +++ b/FprimeZephyrReference/test/int/safe_mode_test.py @@ -0,0 +1,554 @@ +""" +safe_mode_test.py: + +Integration tests for the ModeManager component (Normal <-> Safe mode transitions). + +Tests cover: +- CurrentSafeModeReason telemetry verification +- Safe mode reason tracking (GROUND_COMMAND, LOW_BATTERY, SYSTEM_FAULT) +- Auto safe mode entry due to low voltage (manual test - requires voltage control) +- Auto safe mode exit/recovery when voltage > 8V (manual test - requires voltage control) +- Unintended reboot detection (manual test - requires reboot) + +SafeModeReason Test Coverage: +| Reason | Value | Test | Status | +|------------------|-------|-------------------------|---------------------------------| +| NONE | 0 | test_safe_01, 03 | ✅ Automated | +| LOW_BATTERY | 1 | test_safe_05, 06 | ⏸️ Manual (voltage control) | +| SYSTEM_FAULT | 2 | test_safe_07 | ⏸️ Manual (power cycle) | +| GROUND_COMMAND | 3 | test_safe_02, 04 | ✅ Automated | +| EXTERNAL_REQUEST | 4 | N/A | ❌ Internal port only | +| LORA | 5 | N/A | ❌ Internal port only | + +Note: EXTERNAL_REQUEST is triggered via forceSafeMode port (used by other components). + LORA is triggered by LoRa driver when communication timeout/fault occurs. + These cannot be tested via ground commands in integration tests. + +Total: 8 tests (4 automated, 4 manual) + +SafeModeReason enum values: NONE=0, LOW_BATTERY=1, SYSTEM_FAULT=2, GROUND_COMMAND=3, EXTERNAL_REQUEST=4, LORA=5 +Mode enum values: SAFE_MODE=1, NORMAL=2 +""" + +import logging +import time + +import pytest +from common import proves_send_and_assert_command +from fprime_gds.common.data_types.ch_data import ChData +from fprime_gds.common.testing_fw.api import IntegrationTestAPI + +logger = logging.getLogger(__name__) + +component = "ReferenceDeployment.modeManager" + +# Telemetry packet IDs for CdhCore.tlmSend.SEND_PKT command +MODE_TELEMETRY_PACKET_ID = ( + "1" # ModeManager telemetry (CurrentMode, SafeModeReason, etc.) +) +VOLTAGE_TELEMETRY_PACKET_ID = "10" # PowerMonitor telemetry (includes INA219 voltage) + +# Voltage thresholds (must match ModeManager.hpp constants) +SAFE_MODE_ENTRY_VOLTAGE = 6.7 # Volts - threshold for entering safe mode from NORMAL +SAFE_MODE_RECOVERY_VOLTAGE = 8.0 # Volts - threshold for auto-recovery from safe mode +SAFE_MODE_DEBOUNCE_SECONDS = 10 # Consecutive seconds for safe mode transitions + +# SafeModeReason enum values (must match ModeManager.fpp) +SAFE_MODE_REASON_NONE = 0 +SAFE_MODE_REASON_LOW_BATTERY = 1 +SAFE_MODE_REASON_SYSTEM_FAULT = 2 +SAFE_MODE_REASON_GROUND_COMMAND = 3 +SAFE_MODE_REASON_EXTERNAL_REQUEST = 4 +SAFE_MODE_REASON_LORA = 5 + + +@pytest.fixture(autouse=True) +def setup_and_teardown(fprime_test_api: IntegrationTestAPI, start_gds): + """ + Setup before each test and cleanup after. + Ensures clean state by exiting safe mode if needed. + """ + # Setup: Try to get to a clean NORMAL state + try: + proves_send_and_assert_command(fprime_test_api, f"{component}.EXIT_SAFE_MODE") + except Exception as e: + logger.debug(f"Setup: EXIT_SAFE_MODE skipped (not in safe mode): {e}") + + # Clear event and telemetry history before test + fprime_test_api.clear_histories() + + yield + + # Teardown: Return to clean NORMAL state for next test + try: + proves_send_and_assert_command(fprime_test_api, f"{component}.EXIT_SAFE_MODE") + except Exception as e: + logger.debug(f"Teardown: EXIT_SAFE_MODE skipped (not in safe mode): {e}") + + +# ============================================================================== +# SafeModeReason Telemetry Tests +# ============================================================================== + + +def test_safe_01_initial_safe_mode_reason_is_none( + fprime_test_api: IntegrationTestAPI, start_gds +): + """ + Test that CurrentSafeModeReason telemetry is NONE when not in safe mode. + """ + # Ensure we're in NORMAL mode + proves_send_and_assert_command( + fprime_test_api, "CdhCore.tlmSend.SEND_PKT", [MODE_TELEMETRY_PACKET_ID] + ) + mode_result: ChData = fprime_test_api.assert_telemetry( + f"{component}.CurrentMode", timeout=5 + ) + + if mode_result.get_val() != 2: + # Not in NORMAL mode, skip test + pytest.skip("Not in NORMAL mode - cannot verify initial reason") + + # Verify safe mode reason is NONE + reason_result: ChData = fprime_test_api.assert_telemetry( + f"{component}.CurrentSafeModeReason", timeout=5 + ) + reason_val = reason_result.get_val() + + # Handle both numeric and string representations + if isinstance(reason_val, str): + assert "NONE" in reason_val.upper(), ( + f"Safe mode reason should be NONE, got {reason_val}" + ) + else: + assert reason_val == SAFE_MODE_REASON_NONE, ( + f"Safe mode reason should be NONE (0), got {reason_val}" + ) + + +def test_safe_02_ground_command_sets_reason( + fprime_test_api: IntegrationTestAPI, start_gds +): + """ + Test that FORCE_SAFE_MODE command sets reason to GROUND_COMMAND. + Verifies: + - CurrentSafeModeReason = GROUND_COMMAND after command + - EnteringSafeMode event contains "Ground command" + """ + # Enter safe mode via ground command + fprime_test_api.clear_histories() + proves_send_and_assert_command( + fprime_test_api, + f"{component}.FORCE_SAFE_MODE", + events=[f"{component}.ManualSafeModeEntry"], + ) + time.sleep(2) + + # Verify safe mode reason is GROUND_COMMAND + proves_send_and_assert_command( + fprime_test_api, "CdhCore.tlmSend.SEND_PKT", [MODE_TELEMETRY_PACKET_ID] + ) + reason_result: ChData = fprime_test_api.assert_telemetry( + f"{component}.CurrentSafeModeReason", timeout=5 + ) + reason_val = reason_result.get_val() + + if isinstance(reason_val, str): + assert "GROUND_COMMAND" in reason_val.upper(), ( + f"Safe mode reason should be GROUND_COMMAND, got {reason_val}" + ) + else: + assert reason_val == SAFE_MODE_REASON_GROUND_COMMAND, ( + f"Safe mode reason should be GROUND_COMMAND (3), got {reason_val}" + ) + + # Verify EnteringSafeMode event mentions ground command + events = fprime_test_api.get_event_test_history() + entering_events = [ + e for e in events if "EnteringSafeMode" in str(e.get_template().get_name()) + ] + assert len(entering_events) > 0, "Should have EnteringSafeMode event" + assert "Ground command" in entering_events[-1].get_display_text(), ( + "EnteringSafeMode reason should mention Ground command" + ) + + +def test_safe_03_exit_clears_reason(fprime_test_api: IntegrationTestAPI, start_gds): + """ + Test that EXIT_SAFE_MODE clears the safe mode reason to NONE. + """ + # Enter safe mode + proves_send_and_assert_command(fprime_test_api, f"{component}.FORCE_SAFE_MODE") + time.sleep(2) + + # Verify in safe mode with reason set + proves_send_and_assert_command( + fprime_test_api, "CdhCore.tlmSend.SEND_PKT", [MODE_TELEMETRY_PACKET_ID] + ) + mode_result: ChData = fprime_test_api.assert_telemetry( + f"{component}.CurrentMode", timeout=5 + ) + assert mode_result.get_val() == 1, "Should be in SAFE_MODE" + + # Exit safe mode + proves_send_and_assert_command( + fprime_test_api, + f"{component}.EXIT_SAFE_MODE", + events=[f"{component}.ExitingSafeMode"], + ) + time.sleep(2) + + # Verify reason is cleared to NONE + proves_send_and_assert_command( + fprime_test_api, "CdhCore.tlmSend.SEND_PKT", [MODE_TELEMETRY_PACKET_ID] + ) + reason_result: ChData = fprime_test_api.assert_telemetry( + f"{component}.CurrentSafeModeReason", timeout=5 + ) + reason_val = reason_result.get_val() + + if isinstance(reason_val, str): + assert "NONE" in reason_val.upper(), ( + f"Safe mode reason should be NONE after exit, got {reason_val}" + ) + else: + assert reason_val == SAFE_MODE_REASON_NONE, ( + f"Safe mode reason should be NONE (0) after exit, got {reason_val}" + ) + + +def test_safe_04_no_auto_recovery_for_ground_command( + fprime_test_api: IntegrationTestAPI, start_gds +): + """ + Test that safe mode entered via GROUND_COMMAND does NOT auto-recover. + Even if voltage is healthy, must use EXIT_SAFE_MODE command. + Verifies: + - Safe mode persists after debounce period + margin + - No AutoSafeModeExit event is emitted + """ + # Enter safe mode via ground command + proves_send_and_assert_command(fprime_test_api, f"{component}.FORCE_SAFE_MODE") + time.sleep(2) + + # Verify in safe mode with GROUND_COMMAND reason + proves_send_and_assert_command( + fprime_test_api, "CdhCore.tlmSend.SEND_PKT", [MODE_TELEMETRY_PACKET_ID] + ) + mode_result: ChData = fprime_test_api.assert_telemetry( + f"{component}.CurrentMode", timeout=5 + ) + assert mode_result.get_val() == 1, "Should be in SAFE_MODE" + + fprime_test_api.clear_histories() + + # Wait for debounce period + margin (should NOT auto-recover) + logger.info( + f"Waiting {SAFE_MODE_DEBOUNCE_SECONDS + 3} seconds to verify no auto-recovery..." + ) + time.sleep(SAFE_MODE_DEBOUNCE_SECONDS + 3) + + # Verify still in safe mode + proves_send_and_assert_command( + fprime_test_api, "CdhCore.tlmSend.SEND_PKT", [MODE_TELEMETRY_PACKET_ID] + ) + mode_result: ChData = fprime_test_api.assert_telemetry( + f"{component}.CurrentMode", timeout=5 + ) + assert mode_result.get_val() == 1, ( + "Should still be in SAFE_MODE (no auto-recovery for GROUND_COMMAND reason)" + ) + + # Verify no AutoSafeModeExit event + events = fprime_test_api.get_event_test_history() + auto_exit_events = [ + e for e in events if "AutoSafeModeExit" in str(e.get_template().get_name()) + ] + assert len(auto_exit_events) == 0, ( + "Should NOT have AutoSafeModeExit event for GROUND_COMMAND reason" + ) + + +# ============================================================================== +# Manual Tests - Require Voltage Control +# ============================================================================== + + +@pytest.mark.skip(reason="Requires voltage control - run manually with low battery") +def test_safe_05_auto_entry_low_voltage(fprime_test_api: IntegrationTestAPI, start_gds): + """ + Test automatic entry into safe mode due to low voltage. + + MANUAL TEST - requires ability to control battery voltage: + 1. Ensure in NORMAL mode + 2. Lower battery voltage below SAFE_MODE_ENTRY_VOLTAGE (6.7V) for SAFE_MODE_DEBOUNCE_SECONDS + 3. Verify AutoSafeModeEntry event is emitted with reason=LOW_BATTERY + 4. Verify mode changes to SAFE_MODE (1) + 5. Verify CurrentSafeModeReason = LOW_BATTERY + + Expected behavior: + - Voltage must be below 6.7V threshold for 10 consecutive seconds (1Hz checks) + - Invalid voltage readings also count as faults + - AutoSafeModeEntry event emitted with reason=LOW_BATTERY and voltage value + - Mode changes to SAFE_MODE + - All 8 load switches turned OFF + """ + # Ensure in NORMAL mode + proves_send_and_assert_command( + fprime_test_api, "CdhCore.tlmSend.SEND_PKT", [MODE_TELEMETRY_PACKET_ID] + ) + mode_result: ChData = fprime_test_api.assert_telemetry( + f"{component}.CurrentMode", timeout=5 + ) + assert mode_result.get_val() == 2, "Should start in NORMAL mode" + + fprime_test_api.clear_histories() + + # --- MANUAL STEP: Lower voltage below 6.7V for 10+ seconds --- + logger.info( + f"MANUAL: Lower voltage below {SAFE_MODE_ENTRY_VOLTAGE}V for {SAFE_MODE_DEBOUNCE_SECONDS}+ seconds" + ) + time.sleep(SAFE_MODE_DEBOUNCE_SECONDS + 5) + + # Verify AutoSafeModeEntry event + fprime_test_api.assert_event(f"{component}.AutoSafeModeEntry", timeout=5) + + # Verify mode is SAFE_MODE (1) + proves_send_and_assert_command( + fprime_test_api, "CdhCore.tlmSend.SEND_PKT", [MODE_TELEMETRY_PACKET_ID] + ) + mode_result: ChData = fprime_test_api.assert_telemetry( + f"{component}.CurrentMode", timeout=5 + ) + assert mode_result.get_val() == 1, "Should be in SAFE_MODE after low voltage" + + # Verify reason is LOW_BATTERY + reason_result: ChData = fprime_test_api.assert_telemetry( + f"{component}.CurrentSafeModeReason", timeout=5 + ) + reason_val = reason_result.get_val() + if isinstance(reason_val, str): + assert "LOW_BATTERY" in reason_val.upper(), ( + f"Safe mode reason should be LOW_BATTERY, got {reason_val}" + ) + else: + assert reason_val == SAFE_MODE_REASON_LOW_BATTERY, ( + f"Safe mode reason should be LOW_BATTERY (1), got {reason_val}" + ) + + +@pytest.mark.skip( + reason="Requires voltage control - run manually with battery recovery" +) +def test_safe_06_auto_recovery_voltage(fprime_test_api: IntegrationTestAPI, start_gds): + """ + Test automatic recovery from safe mode when voltage recovers. + + MANUAL TEST - requires ability to control battery voltage: + 1. Enter safe mode with LOW_BATTERY reason (use test_safe_05 first or simulate) + 2. Raise battery voltage above SAFE_MODE_RECOVERY_VOLTAGE (8.0V) for SAFE_MODE_DEBOUNCE_SECONDS + 3. Verify AutoSafeModeExit event is emitted with recovered voltage + 4. Verify mode changes to NORMAL (2) + 5. Verify CurrentSafeModeReason = NONE + + Expected behavior: + - Only works if reason is LOW_BATTERY (not SYSTEM_FAULT or GROUND_COMMAND) + - Voltage must be above 8.0V threshold for 10 consecutive seconds + - AutoSafeModeExit event emitted with voltage value + - Mode changes to NORMAL + - Face switches (0-5) turned ON + """ + # This test assumes we're in safe mode with LOW_BATTERY reason + # In real testing, run test_safe_05 first or manually set up the state + + # Verify in safe mode with LOW_BATTERY reason + proves_send_and_assert_command( + fprime_test_api, "CdhCore.tlmSend.SEND_PKT", [MODE_TELEMETRY_PACKET_ID] + ) + mode_result: ChData = fprime_test_api.assert_telemetry( + f"{component}.CurrentMode", timeout=5 + ) + assert mode_result.get_val() == 1, "Should be in SAFE_MODE" + + reason_result: ChData = fprime_test_api.assert_telemetry( + f"{component}.CurrentSafeModeReason", timeout=5 + ) + reason_val = reason_result.get_val() + # Skip if not LOW_BATTERY reason + if isinstance(reason_val, str): + if "LOW_BATTERY" not in reason_val.upper(): + pytest.skip( + "Safe mode reason is not LOW_BATTERY - auto-recovery won't work" + ) + else: + if reason_val != SAFE_MODE_REASON_LOW_BATTERY: + pytest.skip( + "Safe mode reason is not LOW_BATTERY - auto-recovery won't work" + ) + + fprime_test_api.clear_histories() + + # --- MANUAL STEP: Raise voltage above 8.0V for 10+ seconds --- + logger.info( + f"MANUAL: Raise voltage above {SAFE_MODE_RECOVERY_VOLTAGE}V for {SAFE_MODE_DEBOUNCE_SECONDS}+ seconds" + ) + time.sleep(SAFE_MODE_DEBOUNCE_SECONDS + 5) + + # Verify AutoSafeModeExit event + fprime_test_api.assert_event(f"{component}.AutoSafeModeExit", timeout=5) + + # Verify mode is NORMAL (2) + proves_send_and_assert_command( + fprime_test_api, "CdhCore.tlmSend.SEND_PKT", [MODE_TELEMETRY_PACKET_ID] + ) + mode_result: ChData = fprime_test_api.assert_telemetry( + f"{component}.CurrentMode", timeout=5 + ) + assert mode_result.get_val() == 2, "Should be in NORMAL mode after recovery" + + # Verify reason is cleared to NONE + reason_result: ChData = fprime_test_api.assert_telemetry( + f"{component}.CurrentSafeModeReason", timeout=5 + ) + reason_val = reason_result.get_val() + if isinstance(reason_val, str): + assert "NONE" in reason_val.upper(), ( + f"Safe mode reason should be NONE after recovery, got {reason_val}" + ) + else: + assert reason_val == SAFE_MODE_REASON_NONE, ( + f"Safe mode reason should be NONE (0) after recovery, got {reason_val}" + ) + + +@pytest.mark.skip(reason="Requires reboot - run manually with power cycle") +def test_safe_07_unintended_reboot_detection( + fprime_test_api: IntegrationTestAPI, start_gds +): + """ + Test unintended reboot detection and automatic safe mode entry. + + MANUAL TEST - requires power cycle without clean shutdown: + 1. Ensure in NORMAL mode + 2. Power cycle the board WITHOUT using COLD_RESET or WARM_RESET commands + 3. On boot, verify UnintendedRebootDetected event is emitted + 4. Verify mode is SAFE_MODE (1) + 5. Verify CurrentSafeModeReason = SYSTEM_FAULT + 6. Verify auto-recovery does NOT work (must use EXIT_SAFE_MODE) + + Expected behavior: + - cleanShutdown flag is 0 (not set by prepareForReboot) + - On boot, ModeManager detects unintended reboot + - Enters SAFE_MODE with reason = SYSTEM_FAULT + - Requires manual EXIT_SAFE_MODE to recover (no auto-recovery) + """ + # This test requires a power cycle, so we can only verify the aftermath + + # Check for UnintendedRebootDetected event (may have been emitted at boot) + events = fprime_test_api.get_event_test_history() + unintended_events = [ + e + for e in events + if "UnintendedRebootDetected" in str(e.get_template().get_name()) + ] + + if len(unintended_events) == 0: + pytest.skip("No UnintendedRebootDetected event - board may have booted cleanly") + + # Verify in safe mode + proves_send_and_assert_command( + fprime_test_api, "CdhCore.tlmSend.SEND_PKT", [MODE_TELEMETRY_PACKET_ID] + ) + mode_result: ChData = fprime_test_api.assert_telemetry( + f"{component}.CurrentMode", timeout=5 + ) + assert mode_result.get_val() == 1, "Should be in SAFE_MODE after unintended reboot" + + # Verify reason is SYSTEM_FAULT + reason_result: ChData = fprime_test_api.assert_telemetry( + f"{component}.CurrentSafeModeReason", timeout=5 + ) + reason_val = reason_result.get_val() + if isinstance(reason_val, str): + assert "SYSTEM_FAULT" in reason_val.upper(), ( + f"Safe mode reason should be SYSTEM_FAULT, got {reason_val}" + ) + else: + assert reason_val == SAFE_MODE_REASON_SYSTEM_FAULT, ( + f"Safe mode reason should be SYSTEM_FAULT (2), got {reason_val}" + ) + + +@pytest.mark.skip(reason="Requires reboot - run manually to verify clean shutdown") +def test_safe_08_clean_reboot_no_safe_mode( + fprime_test_api: IntegrationTestAPI, start_gds +): + """ + Test that intentional reboot (via COLD_RESET/WARM_RESET) does NOT trigger safe mode. + + MANUAL TEST - requires reboot via command: + 1. Ensure in NORMAL mode + 2. Issue COLD_RESET or WARM_RESET command + 3. After reboot, verify NO UnintendedRebootDetected event + 4. Verify mode restored to previous state (NORMAL) + 5. Verify CurrentSafeModeReason = NONE + + Expected behavior: + - prepareForReboot is called before sys_reboot() + - cleanShutdown flag is set to 1 + - On boot, ModeManager sees clean shutdown, no safe mode entry + - Mode restored from persistent state + """ + # Ensure in NORMAL mode before reboot + proves_send_and_assert_command( + fprime_test_api, "CdhCore.tlmSend.SEND_PKT", [MODE_TELEMETRY_PACKET_ID] + ) + mode_result: ChData = fprime_test_api.assert_telemetry( + f"{component}.CurrentMode", timeout=5 + ) + assert mode_result.get_val() == 2, "Should be in NORMAL mode before reboot" + + # Issue reboot command (this will disconnect GDS) + logger.info("Issuing COLD_RESET command - GDS will disconnect") + fprime_test_api.send_command("ReferenceDeployment.resetManager.COLD_RESET", []) + + # --- MANUAL STEP: Reconnect GDS after reboot --- + logger.info("MANUAL: Reconnect GDS after board reboots") + time.sleep(30) # Wait for reboot + + # After reconnect, verify no unintended reboot detection + fprime_test_api.clear_histories() + time.sleep(5) # Allow time for events to be received + + events = fprime_test_api.get_event_test_history() + unintended_events = [ + e + for e in events + if "UnintendedRebootDetected" in str(e.get_template().get_name()) + ] + assert len(unintended_events) == 0, ( + "Should NOT have UnintendedRebootDetected event after clean reboot" + ) + + # Verify in NORMAL mode + proves_send_and_assert_command( + fprime_test_api, "CdhCore.tlmSend.SEND_PKT", [MODE_TELEMETRY_PACKET_ID] + ) + mode_result: ChData = fprime_test_api.assert_telemetry( + f"{component}.CurrentMode", timeout=5 + ) + assert mode_result.get_val() == 2, "Should be in NORMAL mode after clean reboot" + + # Verify reason is NONE + reason_result: ChData = fprime_test_api.assert_telemetry( + f"{component}.CurrentSafeModeReason", timeout=5 + ) + reason_val = reason_result.get_val() + if isinstance(reason_val, str): + assert "NONE" in reason_val.upper(), ( + f"Safe mode reason should be NONE after clean reboot, got {reason_val}" + ) + else: + assert reason_val == SAFE_MODE_REASON_NONE, ( + f"Safe mode reason should be NONE (0) after clean reboot, got {reason_val}" + ) From e629051f4e2b1450f9f1be5ca8a9e3a0db940a6e Mon Sep 17 00:00:00 2001 From: "Sam S. Yu" <25761223+yudataguy@users.noreply.github.com> Date: Mon, 1 Dec 2025 10:18:24 -0800 Subject: [PATCH 13/14] minor update and fixes --- FprimeZephyrReference/Components/ModeManager/docs/sdd.md | 4 ++-- FprimeZephyrReference/test/long/mode_manager_test.py | 4 ++-- Makefile | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/FprimeZephyrReference/Components/ModeManager/docs/sdd.md b/FprimeZephyrReference/Components/ModeManager/docs/sdd.md index 18e9c8d7..c553da28 100644 --- a/FprimeZephyrReference/Components/ModeManager/docs/sdd.md +++ b/FprimeZephyrReference/Components/ModeManager/docs/sdd.md @@ -15,8 +15,8 @@ The ModeManager component manages system operational modes and orchestrates tran | MM0008 | The ModeManager shall persist current mode, safe mode entry count, safe mode reason, and clean shutdown flag to non-volatile storage | Integration Testing | | MM0009 | The ModeManager shall restore mode state from persistent storage on initialization | Integration Testing | | MM0010 | The ModeManager shall track and report the number of times safe mode has been entered | Integration Testing | -| MM0011 | The ModeManager shall allow downstream components to query the current mode via getMode port | Unit Testing | -| MM0012 | The ModeManager shall notify downstream components of mode changes with the new mode value | Unit Testing | +| MM0011 | The ModeManager shall allow downstream components to query the current mode via getMode port | Code Review | +| MM0012 | The ModeManager shall notify downstream components of mode changes with the new mode value | Code Review | | MM0020 | The ModeManager shall automatically enter safe mode when voltage drops below 6.7V for 10 consecutive seconds | Integration Testing | | MM0021 | The ModeManager shall automatically exit safe mode when voltage recovers above 8.0V for 10 consecutive seconds (only if reason is LOW_BATTERY) | Integration Testing | | MM0022 | The ModeManager shall track safe mode reason (NONE, LOW_BATTERY, SYSTEM_FAULT, GROUND_COMMAND, EXTERNAL_REQUEST, LORA) | Integration Testing | diff --git a/FprimeZephyrReference/test/long/mode_manager_test.py b/FprimeZephyrReference/test/long/mode_manager_test.py index 7f7d11b3..86f01917 100644 --- a/FprimeZephyrReference/test/long/mode_manager_test.py +++ b/FprimeZephyrReference/test/long/mode_manager_test.py @@ -12,7 +12,7 @@ Total: 9 tests -Mode enum values: SAFE_MODE=1, NORMAL=2, PAYLOAD_MODE=3 +Mode enum values: SAFE_MODE=1, NORMAL=2 """ import time @@ -94,7 +94,7 @@ def test_01_initial_telemetry(fprime_test_api: IntegrationTestAPI, start_gds): f"{component}.CurrentMode", start="NOW", timeout=3 ) current_mode = mode_result.get_val() - assert current_mode in [1, 2, 3], f"Invalid mode value: {current_mode}" + assert current_mode in [1, 2], f"Invalid mode value: {current_mode}" # ============================================================================== diff --git a/Makefile b/Makefile index 8c87b9fd..af7d81ae 100644 --- a/Makefile +++ b/Makefile @@ -78,7 +78,7 @@ test-integration: uv ## Run integration tests (set TEST= to run a [ -e "$$TARGET" ] || { echo "Specified test file $$TARGET not found"; exit 1; }; \ fi; \ echo "Running integration tests at $$TARGET"; \ - $(UV_RUN) pytest $$TARGET --deployment build-artifacts/zephyr/fprime-zephyr-deployment + $(UV_RUN) pytest -- "$$TARGET" --deployment build-artifacts/zephyr/fprime-zephyr-deployment .PHONY: bootloader bootloader: uv From f2b3bba962ac13f2d889e6a9034e6e9b742b385a Mon Sep 17 00:00:00 2001 From: "Sam S. Yu" <25761223+yudataguy@users.noreply.github.com> Date: Thu, 4 Dec 2025 18:09:43 -0800 Subject: [PATCH 14/14] update and fix --- .../AntennaDeployer/AntennaDeployer.cpp | 4 + .../Components/ModeManager/ModeManager.cpp | 27 +- .../Components/ModeManager/ModeManager.fpp | 12 + .../Components/ModeManager/ModeManager.hpp | 3 - .../Components/ModeManager/docs/sdd.md | 476 +++++------------- .../test/long/mode_manager_test.py | 127 ++++- 6 files changed, 274 insertions(+), 375 deletions(-) diff --git a/FprimeZephyrReference/Components/AntennaDeployer/AntennaDeployer.cpp b/FprimeZephyrReference/Components/AntennaDeployer/AntennaDeployer.cpp index 5e0b33d2..c02c619a 100644 --- a/FprimeZephyrReference/Components/AntennaDeployer/AntennaDeployer.cpp +++ b/FprimeZephyrReference/Components/AntennaDeployer/AntennaDeployer.cpp @@ -347,6 +347,10 @@ void AntennaDeployer ::writeDeploymentState() { U8 marker = 1; FwSizeType size = sizeof(marker); Os::File::Status write_status = file.write(&marker, size); + if (write_status != Os::File::OP_OK) { + printk("[AntennaDeployer] writeDeploymentState: write failed with status %d\n", + static_cast(write_status)); + } } (void)file.close(); diff --git a/FprimeZephyrReference/Components/ModeManager/ModeManager.cpp b/FprimeZephyrReference/Components/ModeManager/ModeManager.cpp index 9baf3537..bb53d876 100644 --- a/FprimeZephyrReference/Components/ModeManager/ModeManager.cpp +++ b/FprimeZephyrReference/Components/ModeManager/ModeManager.cpp @@ -51,17 +51,23 @@ void ModeManager ::run_handler(FwIndexType portNum, U32 context) { bool valid = false; F32 voltage = this->getCurrentVoltage(valid); + // Get configurable parameters + Fw::ParamValid paramValid; + F32 entryVoltage = this->paramGet_SafeModeEntryVoltage(paramValid); + F32 recoveryVoltage = this->paramGet_SafeModeRecoveryVoltage(paramValid); + U32 debounceSeconds = this->paramGet_SafeModeDebounceSeconds(paramValid); + // Mode-specific voltage monitoring if (this->m_mode == SystemMode::NORMAL) { // Low-voltage protection for normal mode -> safe mode entry: - // - Threshold: 6.7V triggers safe mode entry - // - Debounce: 10 consecutive seconds below threshold - bool isFault = !valid || (voltage < SAFE_MODE_ENTRY_VOLTAGE); + // - Threshold: configurable via SafeModeEntryVoltage parameter (default 6.7V) + // - Debounce: configurable via SafeModeDebounceSeconds parameter (default 10s) + bool isFault = !valid || (voltage < entryVoltage); if (isFault) { this->m_safeModeVoltageCounter++; - if (this->m_safeModeVoltageCounter >= SAFE_MODE_DEBOUNCE_SECONDS) { + if (this->m_safeModeVoltageCounter >= debounceSeconds) { // Trigger automatic entry into safe mode this->log_WARNING_HI_AutoSafeModeEntry(Components::SafeModeReason::LOW_BATTERY, valid ? voltage : 0.0f); this->enterSafeMode(Components::SafeModeReason::LOW_BATTERY); @@ -77,14 +83,14 @@ void ModeManager ::run_handler(FwIndexType portNum, U32 context) { } else if (this->m_mode == SystemMode::SAFE_MODE) { // Auto-recovery from safe mode (only if reason is LOW_BATTERY): - // - Threshold: Voltage > 8.0V triggers auto-recovery - // - Debounce: 10 consecutive seconds above threshold + // - Threshold: configurable via SafeModeRecoveryVoltage parameter (default 8.0V) + // - Debounce: configurable via SafeModeDebounceSeconds parameter (default 10s) // - SYSTEM_FAULT or GROUND_COMMAND require manual EXIT_SAFE_MODE command if (this->m_safeModeReason == Components::SafeModeReason::LOW_BATTERY) { - if (valid && voltage > SAFE_MODE_RECOVERY_VOLTAGE) { + if (valid && voltage > recoveryVoltage) { this->m_recoveryVoltageCounter++; - if (this->m_recoveryVoltageCounter >= SAFE_MODE_DEBOUNCE_SECONDS) { + if (this->m_recoveryVoltageCounter >= debounceSeconds) { // Trigger automatic exit from safe mode this->exitSafeModeAutomatic(voltage); this->m_recoveryVoltageCounter = 0; // Reset counter @@ -103,6 +109,7 @@ void ModeManager ::run_handler(FwIndexType portNum, U32 context) { // Update telemetry this->tlmWrite_CurrentMode(static_cast(this->m_mode)); this->tlmWrite_CurrentSafeModeReason(this->m_safeModeReason); + this->tlmWrite_SafeModeEntryCount(this->m_safeModeEntryCount); } void ModeManager ::forceSafeMode_handler(FwIndexType portNum, const Components::SafeModeReason& reason) { @@ -136,7 +143,7 @@ void ModeManager ::prepareForReboot_handler(FwIndexType portNum) { // Save state with clean shutdown flag set // We directly write to file here to ensure the flag is persisted Os::File file; - Os::File::Status status = file.open(STATE_FILE_PATH, Os::File::OPEN_CREATE); + Os::File::Status status = file.open(STATE_FILE_PATH, Os::File::OPEN_CREATE, Os::File::OVERWRITE); if (status != Os::File::OP_OK) { // Log failure - next boot will be misclassified as unintended reboot @@ -295,7 +302,7 @@ void ModeManager ::loadState() { void ModeManager ::saveState() { Os::File file; - Os::File::Status status = file.open(STATE_FILE_PATH, Os::File::OPEN_CREATE); + Os::File::Status status = file.open(STATE_FILE_PATH, Os::File::OPEN_CREATE, Os::File::OVERWRITE); if (status != Os::File::OP_OK) { // Log failure to open file, but allow component to continue diff --git a/FprimeZephyrReference/Components/ModeManager/ModeManager.fpp b/FprimeZephyrReference/Components/ModeManager/ModeManager.fpp index f8777f94..059f5999 100644 --- a/FprimeZephyrReference/Components/ModeManager/ModeManager.fpp +++ b/FprimeZephyrReference/Components/ModeManager/ModeManager.fpp @@ -155,6 +155,18 @@ module Components { @ Current safe mode reason (NONE if not in safe mode) telemetry CurrentSafeModeReason: SafeModeReason + # ---------------------------------------------------------------------- + # Parameters + # ---------------------------------------------------------------------- + + @ Voltage threshold for safe mode entry (V) + param SafeModeEntryVoltage: F32 default 6.7 + + @ Voltage threshold for safe mode recovery (V) + param SafeModeRecoveryVoltage: F32 default 8.0 + + @ Debounce time for voltage transitions (seconds) + param SafeModeDebounceSeconds: U32 default 10 ############################################################################### # Standard AC Ports: Required for Channels, Events, Commands, and Parameters # diff --git a/FprimeZephyrReference/Components/ModeManager/ModeManager.hpp b/FprimeZephyrReference/Components/ModeManager/ModeManager.hpp index 7431d570..72943a4e 100644 --- a/FprimeZephyrReference/Components/ModeManager/ModeManager.hpp +++ b/FprimeZephyrReference/Components/ModeManager/ModeManager.hpp @@ -141,9 +141,6 @@ class ModeManager : public ModeManagerComponentBase { // ---------------------------------------------------------------------- static constexpr const char* STATE_FILE_PATH = "/mode_state.bin"; //!< State file path - static constexpr F32 SAFE_MODE_ENTRY_VOLTAGE = 6.7f; //!< Voltage threshold for safe mode entry (V) - static constexpr F32 SAFE_MODE_RECOVERY_VOLTAGE = 8.0f; //!< Voltage threshold for safe mode recovery (V) - static constexpr U32 SAFE_MODE_DEBOUNCE_SECONDS = 10; //!< Debounce time for voltage transitions (s) }; } // namespace Components diff --git a/FprimeZephyrReference/Components/ModeManager/docs/sdd.md b/FprimeZephyrReference/Components/ModeManager/docs/sdd.md index c553da28..6b6f1f07 100644 --- a/FprimeZephyrReference/Components/ModeManager/docs/sdd.md +++ b/FprimeZephyrReference/Components/ModeManager/docs/sdd.md @@ -1,6 +1,6 @@ # Components::ModeManager -The ModeManager component manages system operational modes and orchestrates transitions between NORMAL and SAFE_MODE. It evaluates voltage conditions and detects unintended reboots to make mode decisions, controls power to non‑critical subsystems during transitions, and maintains/persists mode state across reboots to ensure consistent post‑recovery behavior. +The ModeManager component manages system operational modes and orchestrates transitions between NORMAL and SAFE_MODE. It evaluates voltage conditions and detects unintended reboots to make mode decisions, controls power to non-critical subsystems during transitions, and maintains/persists mode state across reboots. ## Requirements | Name | Description | Validation | @@ -9,256 +9,157 @@ The ModeManager component manages system operational modes and orchestrates tran | MM0002 | The ModeManager shall enter safe mode when commanded manually via FORCE_SAFE_MODE command | Integration Testing | | MM0003 | The ModeManager shall enter safe mode when requested by external components via forceSafeMode port | Integration Testing | | MM0004 | The ModeManager shall exit safe mode only via explicit EXIT_SAFE_MODE command or automatic voltage recovery | Integration Testing | -| MM0005 | The ModeManager shall prevent exit from safe mode when not currently in safe mode | Integration Testing | -| MM0006 | The ModeManager shall turn off all 8 load switches (faces 0-5 + payload 6-7) when entering safe mode | Integration Testing | -| MM0007 | The ModeManager shall turn on face load switches (indices 0-5) when exiting safe mode; payload switches (6-7) remain off | Integration Testing | -| MM0008 | The ModeManager shall persist current mode, safe mode entry count, safe mode reason, and clean shutdown flag to non-volatile storage | Integration Testing | -| MM0009 | The ModeManager shall restore mode state from persistent storage on initialization | Integration Testing | -| MM0010 | The ModeManager shall track and report the number of times safe mode has been entered | Integration Testing | -| MM0011 | The ModeManager shall allow downstream components to query the current mode via getMode port | Code Review | -| MM0012 | The ModeManager shall notify downstream components of mode changes with the new mode value | Code Review | -| MM0020 | The ModeManager shall automatically enter safe mode when voltage drops below 6.7V for 10 consecutive seconds | Integration Testing | -| MM0021 | The ModeManager shall automatically exit safe mode when voltage recovers above 8.0V for 10 consecutive seconds (only if reason is LOW_BATTERY) | Integration Testing | -| MM0022 | The ModeManager shall track safe mode reason (NONE, LOW_BATTERY, SYSTEM_FAULT, GROUND_COMMAND, EXTERNAL_REQUEST, LORA) | Integration Testing | -| MM0023 | The ModeManager shall detect unintended reboots and enter safe mode with reason SYSTEM_FAULT | Integration Testing | -| MM0024 | The ModeManager shall set clean shutdown flag when prepareForReboot port is called | Integration Testing | - -## Usage Examples - -The ModeManager component operates as an active component that manages system-wide operational modes. It runs at 1Hz via the rate group and responds to commands, voltage conditions, and external fault conditions. - -### Typical Usage - -1. **System Initialization** - - Component is instantiated during system startup - - Loads previous mode state from `/mode_state.bin` - - Detects unintended reboots via clean shutdown flag - - Configures load switches to match restored mode - - Begins 1Hz periodic execution via rate group - -2. **Normal Operation** - - Updates telemetry channels (CurrentMode, SafeModeEntryCount, CurrentSafeModeReason) - - Monitors system voltage for automatic safe mode entry - - Responds to mode query requests from downstream components - -3. **Safe Mode Entry** - - Can be triggered by: - - Ground command: `FORCE_SAFE_MODE` (reason: GROUND_COMMAND) - - External component request via `forceSafeMode` port with specified reason (e.g., LORA, EXTERNAL_REQUEST) - - Low voltage condition < 6.7V for 10 seconds (reason: LOW_BATTERY) - - Unintended reboot detection (reason: SYSTEM_FAULT) - - Actions performed: - - Transitions mode to SAFE_MODE - - Sets safe mode reason - - Increments safe mode entry counter - - Emits appropriate event (EnteringSafeMode, AutoSafeModeEntry, UnintendedRebootDetected) - - Turns off all 8 load switches (faces 0-5 + payload 6-7) - - Notifies downstream components via `modeChanged` port - - Persists state to flash storage - -4. **Safe Mode Exit (Manual)** - - Triggered by ground command: `EXIT_SAFE_MODE` - - Validates currently in safe mode before allowing exit - - Actions performed: - - Transitions mode to NORMAL - - Clears safe mode reason to NONE - - Emits `ExitingSafeMode` event - - Turns on face load switches (indices 0-5) - - Notifies downstream components via `modeChanged` port - - Persists state to flash storage - -5. **Safe Mode Exit (Automatic)** - - Only triggers when safe mode reason is LOW_BATTERY - - Requires voltage > 8.0V for 10 consecutive seconds - - Actions performed: - - Transitions mode to NORMAL - - Clears safe mode reason to NONE - - Emits `AutoSafeModeExit` event with recovered voltage - - Turns on face load switches (indices 0-5) - - Notifies downstream components via `modeChanged` port - - Persists state to flash storage - -6. **Mode Queries** - - Downstream components can call `getMode` port to query current mode - - Returns immediate synchronous response with current mode +| MM0005 | The ModeManager shall turn off all 8 load switches when entering safe mode | Integration Testing | +| MM0006 | The ModeManager shall turn on face load switches (0-5) when exiting safe mode; payload switches (6-7) remain off | Integration Testing | +| MM0007 | The ModeManager shall persist mode state to non-volatile storage and restore on initialization | Integration Testing | +| MM0008 | The ModeManager shall detect unintended reboots and enter safe mode with reason SYSTEM_FAULT | Integration Testing | +| MM0009 | The ModeManager shall automatically enter safe mode when voltage drops below configurable threshold | Integration Testing | +| MM0010 | The ModeManager shall automatically exit safe mode (LOW_BATTERY only) when voltage recovers above configurable threshold | Integration Testing | ## Class Diagram ```mermaid classDiagram - namespace Components { - class ModeManagerComponentBase { - <> - } - class ModeManager { - <> - - m_mode: SystemMode - - m_safeModeEntryCount: U32 - - m_runCounter: U32 - - m_safeModeReason: SafeModeReason - - m_safeModeVoltageCounter: U32 - - m_recoveryVoltageCounter: U32 - - STATE_FILE_PATH: const char* - - SAFE_MODE_ENTRY_VOLTAGE: F32 = 6.7 - - SAFE_MODE_RECOVERY_VOLTAGE: F32 = 8.0 - - SAFE_MODE_DEBOUNCE_SECONDS: U32 = 10 - + ModeManager(const char* compName) - + ~ModeManager() - + init(FwSizeType queueDepth, FwEnumStoreType instance) - - run_handler(FwIndexType portNum, U32 context) - - forceSafeMode_handler(FwIndexType portNum, SafeModeReason reason) - - getMode_handler(FwIndexType portNum): SystemMode - - prepareForReboot_handler(FwIndexType portNum) - - FORCE_SAFE_MODE_cmdHandler(FwOpcodeType opCode, U32 cmdSeq) - - EXIT_SAFE_MODE_cmdHandler(FwOpcodeType opCode, U32 cmdSeq) - - loadState() - - saveState() - - enterSafeMode(SafeModeReason reason) - - exitSafeMode() - - exitSafeModeAutomatic(F32 voltage) - - turnOffNonCriticalComponents() - - turnOnComponents() - - getCurrentVoltage(bool& valid): F32 - } - class SystemMode { - <> - SAFE_MODE = 1 - NORMAL = 2 - } - class SafeModeReason { - <> - NONE = 0 - LOW_BATTERY = 1 - SYSTEM_FAULT = 2 - GROUND_COMMAND = 3 - EXTERNAL_REQUEST = 4 - LORA = 5 - } + class ModeManager { + <> + - m_mode: SystemMode + - m_safeModeEntryCount: U32 + - m_safeModeReason: SafeModeReason + - m_safeModeVoltageCounter: U32 + - m_recoveryVoltageCounter: U32 + + init(queueDepth, instance) + - run_handler() + - forceSafeMode_handler(reason) + - getMode_handler(): SystemMode + - prepareForReboot_handler() + - enterSafeMode(reason) + - exitSafeMode() + - exitSafeModeAutomatic(voltage) + } + class SystemMode { + <> + SAFE_MODE = 1 + NORMAL = 2 } - ModeManagerComponentBase <|-- ModeManager : inherits - ModeManager --> SystemMode : uses - ModeManager --> SafeModeReason : uses + class SafeModeReason { + <> + NONE = 0 + LOW_BATTERY = 1 + SYSTEM_FAULT = 2 + GROUND_COMMAND = 3 + EXTERNAL_REQUEST = 4 + LORA = 5 + } + ModeManager --> SystemMode + ModeManager --> SafeModeReason ``` -## Port Descriptions +## Ports ### Input Ports | Name | Type | Kind | Description | |---|---|---|---| -| run | Svc.Sched | sync | Receives periodic calls from rate group (1Hz) for telemetry updates and voltage monitoring | -| forceSafeMode | Components.ForceSafeModeWithReason | async | Receives safe mode requests from external components with reason (NONE defaults to EXTERNAL_REQUEST) | -| getMode | Components.GetSystemMode | sync | Allows downstream components to query current system mode | -| prepareForReboot | Fw.Signal | sync | Called by ResetManager before intentional reboot to set clean shutdown flag | +| run | Svc.Sched | sync | 1Hz periodic calls for telemetry and voltage monitoring | +| forceSafeMode | ForceSafeModeWithReason | async | Safe mode requests from external components | +| getMode | GetSystemMode | sync | Query current system mode | +| prepareForReboot | Fw.Signal | sync | Set clean shutdown flag before intentional reboot | ### Output Ports -| Name | Type | Kind | Description | +| Name | Type | Description | +|---|---|---| +| modeChanged | SystemModeChanged | Notifies components of mode changes | +| loadSwitchTurnOn | Fw.Signal [8] | Turn on load switches | +| loadSwitchTurnOff | Fw.Signal [8] | Turn off load switches | +| voltageGet | Drv.VoltageGet | Query system voltage | + +## Commands + +| Name | Description | +|---|---| +| FORCE_SAFE_MODE | Forces safe mode with reason GROUND_COMMAND | +| EXIT_SAFE_MODE | Exits safe mode (fails if not in safe mode) | + +## Parameters + +Voltage thresholds are configurable via F-Prime parameters: + +| Parameter | Type | Default | Description | |---|---|---|---| -| modeChanged | Components.SystemModeChanged | output | Notifies downstream components of mode changes with new mode value | -| loadSwitchTurnOn | Fw.Signal [8] | output | Signals to turn on load switches (faces 0-5, payload power 6, payload battery 7) | -| loadSwitchTurnOff | Fw.Signal [8] | output | Signals to turn off load switches (faces 0-5, payload power 6, payload battery 7) | -| voltageGet | Drv.VoltageGet | output | Queries INA219 manager for current system voltage | +| SafeModeEntryVoltage | F32 | 6.7 | Voltage (V) below which safe mode is entered | +| SafeModeRecoveryVoltage | F32 | 8.0 | Voltage (V) above which safe mode can be exited | +| SafeModeDebounceSeconds | U32 | 10 | Consecutive seconds required for transitions | + +Parameters can be modified at runtime via `PRM_SET` commands. + +## Events + +| Name | Severity | Description | +|---|---|---| +| EnteringSafeMode | WARNING_HI | Entering safe mode with reason string | +| ExitingSafeMode | ACTIVITY_HI | Manually exiting safe mode | +| AutoSafeModeEntry | WARNING_HI | Auto-entry due to low voltage | +| AutoSafeModeExit | ACTIVITY_HI | Auto-exit due to voltage recovery | +| UnintendedRebootDetected | WARNING_HI | Unintended reboot detected on startup | +| ManualSafeModeEntry | ACTIVITY_HI | Safe mode commanded via FORCE_SAFE_MODE | +| ExternalFaultDetected | WARNING_HI | External component triggered safe mode | +| PreparingForReboot | ACTIVITY_HI | Clean shutdown flag being set | +| CommandValidationFailed | WARNING_LO | Command validation failed | +| StatePersistenceFailure | WARNING_LO | State save/load failed | -## Component States +## Telemetry | Name | Type | Description | |---|---|---| -| m_mode | SystemMode | Current operational mode (NORMAL or SAFE_MODE) | -| m_safeModeEntryCount | U32 | Number of times safe mode has been entered since initial deployment | -| m_safeModeReason | SafeModeReason | Current reason for being in safe mode (or NONE if not in safe mode) | -| m_runCounter | U32 | Counter for 1Hz run handler calls | -| m_safeModeVoltageCounter | U32 | Debounce counter for low voltage detection (in NORMAL mode) | -| m_recoveryVoltageCounter | U32 | Debounce counter for voltage recovery detection (in SAFE_MODE) | +| CurrentMode | U8 | Current mode (1=SAFE_MODE, 2=NORMAL) | +| SafeModeEntryCount | U32 | Times safe mode entered (persists across reboots) | +| CurrentSafeModeReason | SafeModeReason | Current reason (NONE if not in safe mode) | + +## State Persistence -### Persistent State -The component persists the following state to `/mode_state.bin`: +State is persisted to `/mode_state.bin`: - Current mode (U8) - Safe mode entry count (U32) - Safe mode reason (U8) - Clean shutdown flag (U8) -This state is loaded on initialization and saved on every mode transition. - -## Voltage Thresholds +## Safe Mode Reason Logic -| Threshold | Value | Description | +| Reason | Trigger | Auto-Recovery | |---|---|---| -| SAFE_MODE_ENTRY_VOLTAGE | 6.7V | Voltage below which safe mode is entered from NORMAL | -| SAFE_MODE_RECOVERY_VOLTAGE | 8.0V | Voltage above which safe mode can be exited automatically | -| SAFE_MODE_DEBOUNCE_SECONDS | 10 | Consecutive seconds required for voltage-based transitions | +| LOW_BATTERY | Voltage below threshold | Yes (when voltage recovers) | +| SYSTEM_FAULT | Unintended reboot detected | No | +| GROUND_COMMAND | FORCE_SAFE_MODE command | No | +| EXTERNAL_REQUEST | forceSafeMode port call | No | +| LORA | LoRa communication fault | No | -## Sequence Diagrams - -### Safe Mode Entry (Command) -```mermaid -sequenceDiagram - participant Ground - participant ModeManager - participant LoadSwitches - participant DownstreamComponents - participant FlashStorage +## Load Switch Mapping - Ground->>ModeManager: FORCE_SAFE_MODE command - ModeManager->>ModeManager: Emit ManualSafeModeEntry event - ModeManager->>ModeManager: Set m_mode = SAFE_MODE - ModeManager->>ModeManager: Set m_safeModeReason = GROUND_COMMAND - ModeManager->>ModeManager: Increment m_safeModeEntryCount - ModeManager->>ModeManager: Emit EnteringSafeMode event - ModeManager->>LoadSwitches: Turn off all 8 switches (faces + payload) - ModeManager->>ModeManager: Update telemetry - ModeManager->>DownstreamComponents: modeChanged_out(SAFE_MODE) - ModeManager->>FlashStorage: Save state to /mode_state.bin - ModeManager->>Ground: Command response OK -``` +| Index | Subsystem | NORMAL | SAFE_MODE | +|---|---|---|---| +| 0-5 | Satellite Faces | ON | OFF | +| 6-7 | Payload Power/Battery | OFF | OFF | -### Safe Mode Entry (Low Voltage) -```mermaid -sequenceDiagram - participant RateGroup - participant ModeManager - participant INA219 - participant LoadSwitches - participant DownstreamComponents - participant FlashStorage +> When exiting to NORMAL, only face switches (0-5) turn ON. Payload switches must be controlled separately. - loop Every 1Hz - RateGroup->>ModeManager: run() - ModeManager->>INA219: voltageGet_out() - INA219-->>ModeManager: voltage < 6.7V - ModeManager->>ModeManager: Increment m_safeModeVoltageCounter - end - Note over ModeManager: After 10 consecutive low readings - ModeManager->>ModeManager: Emit AutoSafeModeEntry event (LOW_BATTERY) - ModeManager->>ModeManager: Set m_mode = SAFE_MODE - ModeManager->>ModeManager: Set m_safeModeReason = LOW_BATTERY - ModeManager->>LoadSwitches: Turn off all 8 switches (faces + payload) - ModeManager->>DownstreamComponents: modeChanged_out(SAFE_MODE) - ModeManager->>FlashStorage: Save state -``` +## Sequence Diagrams -### Safe Mode Exit (Automatic Voltage Recovery) +### Safe Mode Entry (Low Voltage) ```mermaid sequenceDiagram participant RateGroup participant ModeManager participant INA219 participant LoadSwitches - participant DownstreamComponents - participant FlashStorage - Note over ModeManager: In SAFE_MODE with reason=LOW_BATTERY loop Every 1Hz RateGroup->>ModeManager: run() ModeManager->>INA219: voltageGet_out() - INA219-->>ModeManager: voltage > 8.0V - ModeManager->>ModeManager: Increment m_recoveryVoltageCounter + INA219-->>ModeManager: voltage < threshold + ModeManager->>ModeManager: Increment counter end - Note over ModeManager: After 10 consecutive high readings - ModeManager->>ModeManager: Emit AutoSafeModeExit event (with voltage) - ModeManager->>ModeManager: Set m_mode = NORMAL - ModeManager->>ModeManager: Set m_safeModeReason = NONE - ModeManager->>LoadSwitches: Turn on face switches (0-5) - ModeManager->>DownstreamComponents: modeChanged_out(NORMAL) - ModeManager->>FlashStorage: Save state + Note over ModeManager: After debounce period + ModeManager->>ModeManager: enterSafeMode(LOW_BATTERY) + ModeManager->>LoadSwitches: Turn off all 8 switches ``` ### Unintended Reboot Detection @@ -267,160 +168,17 @@ sequenceDiagram participant Boot participant ModeManager participant FlashStorage - participant LoadSwitches - participant DownstreamComponents Boot->>ModeManager: init() - ModeManager->>FlashStorage: Load /mode_state.bin - FlashStorage-->>ModeManager: state.cleanShutdown = 0, state.mode = NORMAL + ModeManager->>FlashStorage: Load state + FlashStorage-->>ModeManager: cleanShutdown=0, mode=NORMAL ModeManager->>ModeManager: Detect unintended reboot - ModeManager->>ModeManager: Emit UnintendedRebootDetected event - ModeManager->>ModeManager: Set m_safeModeReason = SYSTEM_FAULT - ModeManager->>ModeManager: Set m_mode = SAFE_MODE - ModeManager->>LoadSwitches: Turn off all 8 switches (faces + payload) - ModeManager->>DownstreamComponents: modeChanged_out(SAFE_MODE) - ModeManager->>FlashStorage: Save state (cleanShutdown = 0) -``` - -### Prepare For Reboot (Clean Shutdown) -```mermaid -sequenceDiagram - participant ResetManager - participant ModeManager - participant FlashStorage - - ResetManager->>ModeManager: prepareForReboot_out() - ModeManager->>ModeManager: Emit PreparingForReboot event - ModeManager->>FlashStorage: Save state with cleanShutdown = 1 - ModeManager-->>ResetManager: Return - ResetManager->>ResetManager: sys_reboot() -``` - -### Mode Query -```mermaid -sequenceDiagram - participant DownstreamComponent - participant ModeManager - - DownstreamComponent->>ModeManager: getMode() port call - ModeManager->>ModeManager: Read m_mode - ModeManager-->>DownstreamComponent: Return current mode (NORMAL or SAFE_MODE) + ModeManager->>ModeManager: enterSafeMode(SYSTEM_FAULT) ``` -## Commands - -| Name | Arguments | Description | -|---|---|---| -| FORCE_SAFE_MODE | None | Forces the system into safe mode with reason GROUND_COMMAND. Emits ManualSafeModeEntry event. Idempotent when already in safe mode. | -| EXIT_SAFE_MODE | None | Exits safe mode and returns to normal operation. Fails with CommandValidationFailed if not currently in safe mode. Clears safe mode reason to NONE. | - -## Events - -| Name | Severity | Arguments | Description | -|---|---|---|---| -| EnteringSafeMode | WARNING_HI | reason: string size 100 | Emitted when entering safe mode, includes reason string | -| ExitingSafeMode | ACTIVITY_HI | None | Emitted when manually exiting safe mode | -| ManualSafeModeEntry | ACTIVITY_HI | None | Emitted when safe mode is manually commanded via FORCE_SAFE_MODE | -| ExternalFaultDetected | WARNING_HI | None | Emitted when an external component triggers safe mode via forceSafeMode port | -| AutoSafeModeEntry | WARNING_HI | reason: SafeModeReason, voltage: F32 | Emitted when automatically entering safe mode due to low voltage | -| AutoSafeModeExit | ACTIVITY_HI | voltage: F32 | Emitted when automatically exiting safe mode due to voltage recovery | -| UnintendedRebootDetected | WARNING_HI | None | Emitted when an unintended reboot is detected on startup | -| PreparingForReboot | ACTIVITY_HI | None | Emitted when prepareForReboot is called before intentional reboot | -| CommandValidationFailed | WARNING_LO | cmdName: string size 50, reason: string size 100 | Emitted when a command fails validation | -| StatePersistenceFailure | WARNING_LO | operation: string size 20, status: I32 | Emitted when state save/load operations fail | - -## Telemetry - -| Name | Type | Update Rate | Description | -|---|---|---|---| -| CurrentMode | U8 | 1Hz | Current system mode (1 = SAFE_MODE, 2 = NORMAL) | -| SafeModeEntryCount | U32 | On change | Number of times safe mode has been entered (persists across reboots) | -| CurrentSafeModeReason | SafeModeReason | 1Hz | Current reason for safe mode (NONE if not in safe mode) | - -## Load Switch Mapping - -The ModeManager controls 8 load switches that power satellite subsystems: - -| Index | Subsystem | NORMAL State | SAFE_MODE State | -|---|---|---|---| -| 0 | Satellite Face 4 | ON | OFF | -| 1 | Satellite Face 0 | ON | OFF | -| 2 | Satellite Face 1 | ON | OFF | -| 3 | Satellite Face 2 | ON | OFF | -| 4 | Satellite Face 3 | ON | OFF | -| 5 | Satellite Face 5 | ON | OFF | -| 6 | Payload Power | OFF | OFF | -| 7 | Payload Battery | OFF | OFF | - -> **Note:** When entering SAFE_MODE, all 8 switches are turned OFF. When exiting to NORMAL mode, only face switches (0-5) are turned ON. Payload switches (6-7) remain OFF and must be controlled separately. - -## Safe Mode Reason Logic - -| Reason | Trigger | Auto-Recovery | Manual Exit | -|---|---|---|---| -| LOW_BATTERY | Voltage < 6.7V for 10s | Yes (when voltage > 8.0V for 10s) | Yes | -| SYSTEM_FAULT | Unintended reboot detected | No | Yes | -| GROUND_COMMAND | FORCE_SAFE_MODE command | No | Yes | -| EXTERNAL_REQUEST | forceSafeMode port call | No | Yes | -| LORA | LoRa communication timeout or fault | No | Yes | +## Design Notes -## Integration Tests - -See `FprimeZephyrReference/test/int/safe_mode_test.py` for comprehensive integration tests covering: - -| Test | Description | Coverage | -|---|---|---| -| test_safe_01_initial_safe_mode_reason_is_none | Verifies CurrentSafeModeReason is NONE in NORMAL mode | Telemetry | -| test_safe_02_ground_command_sets_reason | Tests FORCE_SAFE_MODE sets reason to GROUND_COMMAND | Safe mode entry | -| test_safe_03_exit_clears_reason | Verifies EXIT_SAFE_MODE clears reason to NONE | Safe mode exit | -| test_safe_04_no_auto_recovery_for_ground_command | Ensures no auto-recovery when reason is GROUND_COMMAND | Auto-recovery logic | -| test_safe_05_auto_entry_low_voltage | Tests automatic safe mode entry due to low voltage | Voltage monitoring | -| test_safe_06_auto_recovery_voltage | Tests automatic safe mode exit due to voltage recovery | Voltage monitoring | -| test_safe_07_unintended_reboot_detection | Tests unintended reboot detection and SYSTEM_FAULT | Reboot detection | -| test_safe_08_clean_reboot_no_safe_mode | Verifies clean reboot does not trigger safe mode | Reboot handling | - -## Design Decisions - -### Hybrid Mode Query/Notification Approach -The component provides both pull-based (getMode port) and push-based (modeChanged port) mechanisms for mode awareness: - -- **getMode port (pull)**: Allows components to query current mode on-demand, useful for: - - Component initialization - - Recovery from faults - - Periodic mode checks - -- **modeChanged port (push)**: Notifies components immediately when mode changes with the new mode value, useful for: - - Reactive behavior - - Real-time mode tracking - - Avoiding polling overhead - -### State Persistence -Mode state is persisted to `/mode_state.bin` to maintain operational context across: -- Intentional reboots (clean shutdown flag set) -- Watchdog resets (unintended reboot detection) -- Power cycles (unintended reboot detection) - -### Voltage-Based Auto-Transitions -The ModeManager implements hysteresis in voltage-based transitions: -- Entry threshold: 6.7V (lower) -- Recovery threshold: 8.0V (higher) -- Debounce: 10 consecutive 1Hz samples - -This prevents oscillation between modes when voltage is near thresholds. - -### Safe Mode Reason Tracking -The reason for entering safe mode is tracked to enable intelligent recovery: -- LOW_BATTERY: Auto-recovery allowed when voltage recovers -- Other reasons: Manual EXIT_SAFE_MODE command required - -## Change Log -| Date | Description | -|---|---| -| 2025-11-30 | Changed forceSafeMode port to accept SafeModeReason parameter (defaults to EXTERNAL_REQUEST if NONE) | -| 2025-11-30 | Added LORA SafeModeReason for LoRa communication timeout/fault | -| 2025-11-30 | Removed payload mode (PAYLOAD_MODE enum, commands, events, telemetry, load switch connections) | -| 2025-11-30 | Added safe mode auto-entry/exit based on voltage thresholds (6.7V entry, 8.0V recovery) | -| 2025-11-30 | Added SafeModeReason tracking (LOW_BATTERY, SYSTEM_FAULT, GROUND_COMMAND, EXTERNAL_REQUEST) | -| 2025-11-30 | Added unintended reboot detection via clean shutdown flag | -| 2025-11-30 | Added prepareForReboot port for ResetManager integration | -| 2025-11-26 | Initial implementation with NORMAL and SAFE_MODE | +- **Hysteresis**: Entry threshold (6.7V) < Recovery threshold (8.0V) prevents oscillation +- **Debounce**: Configurable consecutive samples prevent spurious transitions +- **Reason tracking**: Only LOW_BATTERY allows auto-recovery; other reasons require manual EXIT_SAFE_MODE +- **Mode query**: Both pull (getMode) and push (modeChanged) patterns supported diff --git a/FprimeZephyrReference/test/long/mode_manager_test.py b/FprimeZephyrReference/test/long/mode_manager_test.py index 86f01917..dd9ef674 100644 --- a/FprimeZephyrReference/test/long/mode_manager_test.py +++ b/FprimeZephyrReference/test/long/mode_manager_test.py @@ -7,12 +7,14 @@ - Basic functionality and telemetry - Safe mode entry (via command) - Safe mode exit -- State persistence +- State persistence (including repeated writes) +- SafeModeReason tracking - Edge cases -Total: 9 tests +Total: 13 tests Mode enum values: SAFE_MODE=1, NORMAL=2 +SafeModeReason enum: NONE=0, LOW_BATTERY=1, SYSTEM_FAULT=2, GROUND_COMMAND=3, EXTERNAL_REQUEST=4, LORA=5 """ import time @@ -89,7 +91,7 @@ def test_01_initial_telemetry(fprime_test_api: IntegrationTestAPI, start_gds): # Trigger telemetry update by sending Health packet (ID 1) proves_send_and_assert_command(fprime_test_api, "CdhCore.tlmSend.SEND_PKT", ["1"]) - # Read CurrentMode telemetry (1 = SAFE_MODE, 2 = NORMAL, 3 = PAYLOAD_MODE) + # Read CurrentMode telemetry (1 = SAFE_MODE, 2 = NORMAL) mode_result: ChData = fprime_test_api.assert_telemetry( f"{component}.CurrentMode", start="NOW", timeout=3 ) @@ -285,3 +287,122 @@ def test_19_safe_mode_state_persists(fprime_test_api: IntegrationTestAPI, start_ f"{component}.CurrentMode", start="NOW", timeout=3 ) assert mode_result.get_val() == 1, "Mode should be saved as SAFE_MODE" + + +def test_20_repeated_state_persistence(fprime_test_api: IntegrationTestAPI, start_gds): + """ + Test that repeated mode transitions correctly persist state. + This catches bugs like OPEN_CREATE without OVERWRITE flag. + Each transition triggers saveState() which must overwrite the existing file. + """ + # Get initial entry count + proves_send_and_assert_command(fprime_test_api, "CdhCore.tlmSend.SEND_PKT", ["1"]) + initial_count: ChData = fprime_test_api.assert_telemetry( + f"{component}.SafeModeEntryCount", timeout=5 + ) + initial_value = initial_count.get_val() + + # Cycle through safe mode multiple times + for _ in range(3): + # Enter safe mode + force_safe_mode_once(fprime_test_api, wait_for_entering=True) + time.sleep(2) + + # Exit safe mode + proves_send_and_assert_command( + fprime_test_api, + f"{component}.EXIT_SAFE_MODE", + events=[f"{component}.ExitingSafeMode"], + ) + time.sleep(2) + + # Verify count incremented correctly (3 entries) + proves_send_and_assert_command(fprime_test_api, "CdhCore.tlmSend.SEND_PKT", ["1"]) + final_count: ChData = fprime_test_api.assert_telemetry( + f"{component}.SafeModeEntryCount", timeout=5 + ) + final_value = final_count.get_val() + + assert final_value == initial_value + 3, ( + f"Count should increment by 3 after 3 cycles (was {initial_value}, now {final_value})" + ) + + +def test_21_safe_mode_reason_ground_command( + fprime_test_api: IntegrationTestAPI, start_gds +): + """ + Test that FORCE_SAFE_MODE sets reason to GROUND_COMMAND (3). + """ + # Enter safe mode via command + force_safe_mode_once(fprime_test_api, wait_for_entering=True) + time.sleep(2) + + # Check reason telemetry + proves_send_and_assert_command(fprime_test_api, "CdhCore.tlmSend.SEND_PKT", ["1"]) + reason_result: ChData = fprime_test_api.assert_telemetry( + f"{component}.CurrentSafeModeReason", timeout=5 + ) + reason_value = reason_result.get_val() + + # GROUND_COMMAND = 3 + assert reason_value == 3 or str(reason_value) == "GROUND_COMMAND", ( + f"Safe mode reason should be GROUND_COMMAND (3), got {reason_value}" + ) + + +def test_22_safe_mode_reason_cleared_on_exit( + fprime_test_api: IntegrationTestAPI, start_gds +): + """ + Test that EXIT_SAFE_MODE clears reason to NONE (0). + """ + # Enter safe mode + force_safe_mode_once(fprime_test_api, wait_for_entering=True) + time.sleep(2) + + # Exit safe mode + proves_send_and_assert_command( + fprime_test_api, + f"{component}.EXIT_SAFE_MODE", + events=[f"{component}.ExitingSafeMode"], + ) + time.sleep(2) + + # Check reason is cleared + proves_send_and_assert_command(fprime_test_api, "CdhCore.tlmSend.SEND_PKT", ["1"]) + reason_result: ChData = fprime_test_api.assert_telemetry( + f"{component}.CurrentSafeModeReason", timeout=5 + ) + reason_value = reason_result.get_val() + + # NONE = 0 + assert reason_value == 0 or str(reason_value) == "NONE", ( + f"Safe mode reason should be NONE (0) after exit, got {reason_value}" + ) + + +def test_23_no_persistence_failure_events( + fprime_test_api: IntegrationTestAPI, start_gds +): + """ + Test that state persistence doesn't emit failure events during normal operation. + This catches file system issues or bugs like missing OVERWRITE flag. + """ + fprime_test_api.clear_histories() + + # Trigger multiple state saves by cycling modes + for _ in range(2): + proves_send_and_assert_command(fprime_test_api, f"{component}.FORCE_SAFE_MODE") + time.sleep(2) + proves_send_and_assert_command(fprime_test_api, f"{component}.EXIT_SAFE_MODE") + time.sleep(2) + + # Check no StatePersistenceFailure events were emitted + history = fprime_test_api.get_event_test_history() + failure_events = [ + e for e in history if "StatePersistenceFailure" in str(e.get_id()) + ] + assert len(failure_events) == 0, ( + f"State persistence should not fail, but got {len(failure_events)} failure events" + )