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/11] 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/11] 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/11] 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/11] 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/11] 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/11] 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/11] 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/11] 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/11] 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/11] 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/11] 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