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/16] 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/16] 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/16] 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/16] 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/16] 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/16] 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/16] 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/16] 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/16] 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/16] 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/16] 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 a44949df56a4366f62a88bc463cc442084bff1c1 Mon Sep 17 00:00:00 2001 From: "Sam S. Yu" <25761223+yudataguy@users.noreply.github.com> Date: Sat, 29 Nov 2025 09:54:49 -0800 Subject: [PATCH 12/16] add normal <-> payload mode auto change --- .../Components/ModeManager/ModeManager.cpp | 66 +++++- .../Components/ModeManager/ModeManager.fpp | 7 + .../Components/ModeManager/ModeManager.hpp | 11 +- .../Components/ModeManager/docs/sdd.md | 65 +++++- .../test/int/payload_mode_test.py | 194 +++++++++++++++++- 5 files changed, 329 insertions(+), 14 deletions(-) diff --git a/FprimeZephyrReference/Components/ModeManager/ModeManager.cpp b/FprimeZephyrReference/Components/ModeManager/ModeManager.cpp index 616a0a9f..783e48b8 100644 --- a/FprimeZephyrReference/Components/ModeManager/ModeManager.cpp +++ b/FprimeZephyrReference/Components/ModeManager/ModeManager.cpp @@ -22,7 +22,8 @@ ModeManager ::ModeManager(const char* const compName) m_mode(SystemMode::NORMAL), m_safeModeEntryCount(0), m_payloadModeEntryCount(0), - m_runCounter(0) {} + m_runCounter(0), + m_lowVoltageCounter(0) {} ModeManager ::~ModeManager() {} @@ -39,6 +40,33 @@ void ModeManager ::run_handler(FwIndexType portNum, U32 context) { // Increment run counter (1Hz tick counter) this->m_runCounter++; + // Check for low voltage fault when in PAYLOAD_MODE + if (this->m_mode == SystemMode::PAYLOAD_MODE) { + bool valid = false; + F32 voltage = this->getCurrentVoltage(valid); + + // Treat both low voltage AND invalid readings as fault conditions + // Invalid readings (sensor failure/disconnection) should not mask brownout protection + bool isFault = !valid || (voltage < LOW_VOLTAGE_THRESHOLD); + + if (isFault) { + this->m_lowVoltageCounter++; + + if (this->m_lowVoltageCounter >= LOW_VOLTAGE_DEBOUNCE_SECONDS) { + // Trigger automatic exit from payload mode + // Use 0.0 for voltage if reading was invalid + this->exitPayloadModeAutomatic(valid ? voltage : 0.0f); + this->m_lowVoltageCounter = 0; // Reset counter + } + } else { + // Voltage OK and valid - reset counter + this->m_lowVoltageCounter = 0; + } + } else { + // Not in payload mode - reset counter + this->m_lowVoltageCounter = 0; + } + // Update telemetry this->tlmWrite_CurrentMode(static_cast(this->m_mode)); } @@ -299,6 +327,7 @@ void ModeManager ::enterPayloadMode(const char* reasonOverride) { // Transition to payload mode this->m_mode = SystemMode::PAYLOAD_MODE; this->m_payloadModeEntryCount++; + this->m_lowVoltageCounter = 0; // Reset low voltage counter on mode entry // Build reason string Fw::LogStringArg reasonStr; @@ -312,8 +341,10 @@ void ModeManager ::enterPayloadMode(const char* reasonOverride) { this->log_ACTIVITY_HI_EnteringPayloadMode(reasonStr); - // Turn on payload switches (6 & 7) - this->turnOnPayload(); + // Turn on ALL load switches (faces 0-5 AND payload 6-7) + // This ensures proper state even after automatic fault exit + this->turnOnComponents(); // Face switches (0-5) + this->turnOnPayload(); // Payload switches (6-7) // Update telemetry this->tlmWrite_CurrentMode(static_cast(this->m_mode)); @@ -330,7 +361,7 @@ void ModeManager ::enterPayloadMode(const char* reasonOverride) { } void ModeManager ::exitPayloadMode() { - // Transition back to normal mode + // Transition back to normal mode (manual exit) this->m_mode = SystemMode::NORMAL; this->log_ACTIVITY_HI_ExitingPayloadMode(); @@ -338,8 +369,8 @@ 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 + // Ensure face switches are ON for NORMAL mode + // This guarantees consistent state even if faces were turned off during payload mode this->turnOnComponents(); // Update telemetry @@ -355,6 +386,29 @@ void ModeManager ::exitPayloadMode() { this->saveState(); } +void ModeManager ::exitPayloadModeAutomatic(F32 voltage) { + // Automatic exit from payload mode due to fault condition (e.g., low voltage) + // More aggressive than manual exit - turns off ALL switches + this->m_mode = SystemMode::NORMAL; + + this->log_WARNING_HI_AutoPayloadModeExit(voltage); + + // Turn OFF all load switches (aggressive - includes faces 0-5 and payload 6-7) + this->turnOffNonCriticalComponents(); + + // 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) diff --git a/FprimeZephyrReference/Components/ModeManager/ModeManager.fpp b/FprimeZephyrReference/Components/ModeManager/ModeManager.fpp index 09e73743..8887b968 100644 --- a/FprimeZephyrReference/Components/ModeManager/ModeManager.fpp +++ b/FprimeZephyrReference/Components/ModeManager/ModeManager.fpp @@ -117,6 +117,13 @@ module Components { severity warning low \ format "Command {} failed: {}" + @ Event emitted when automatically exiting payload mode due to low voltage + event AutoPayloadModeExit( + voltage: F32 @< Voltage that triggered the exit + ) \ + severity warning high \ + format "AUTO EXIT PAYLOAD MODE: Low voltage detected ({}V)" + @ Event emitted when state persistence fails event StatePersistenceFailure( operation: string size 20 @< Operation that failed (save/load) diff --git a/FprimeZephyrReference/Components/ModeManager/ModeManager.hpp b/FprimeZephyrReference/Components/ModeManager/ModeManager.hpp index a4b9b9b5..1d3e65c8 100644 --- a/FprimeZephyrReference/Components/ModeManager/ModeManager.hpp +++ b/FprimeZephyrReference/Components/ModeManager/ModeManager.hpp @@ -100,9 +100,13 @@ class ModeManager : public ModeManagerComponentBase { //! Enter payload mode with optional reason override void enterPayloadMode(const char* reason = nullptr); - //! Exit payload mode + //! Exit payload mode (manual) void exitPayloadMode(); + //! Exit payload mode automatically due to fault condition + //! More aggressive than manual exit - turns off all switches + void exitPayloadModeAutomatic(F32 voltage); + //! Turn off non-critical components void turnOffNonCriticalComponents(); @@ -143,8 +147,13 @@ class ModeManager : public ModeManagerComponentBase { 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) + U32 m_lowVoltageCounter; //!< Counter for consecutive low voltage readings static constexpr const char* STATE_FILE_PATH = "/mode_state.bin"; //!< State file path + + // Voltage threshold constants for payload mode protection + static constexpr F32 LOW_VOLTAGE_THRESHOLD = 7.2f; //!< Voltage threshold in volts + static constexpr U32 LOW_VOLTAGE_DEBOUNCE_SECONDS = 10; //!< Consecutive seconds below threshold }; } // namespace Components diff --git a/FprimeZephyrReference/Components/ModeManager/docs/sdd.md b/FprimeZephyrReference/Components/ModeManager/docs/sdd.md index 22d80144..0ef25ac8 100644 --- a/FprimeZephyrReference/Components/ModeManager/docs/sdd.md +++ b/FprimeZephyrReference/Components/ModeManager/docs/sdd.md @@ -21,10 +21,12 @@ Future work: a HIBERNATION mode remains planned; it will follow the same persist | 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 | +| MM0016 | The ModeManager shall turn on all load switches (faces 0-5 and payload 6-7) when entering payload mode and turn off payload load switches 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 | While in payload mode, the ModeManager shall monitor bus voltage once per second and automatically exit payload mode after 10 consecutive seconds below 7.2V or upon invalid voltage readings | Integration Testing (manual + debounced behavior) | +| MM0021 | Automatic payload mode exit shall emit AutoPayloadModeExit and turn off all 8 load switches | Integration Testing (manual) | ## Usage Examples @@ -61,7 +63,7 @@ The ModeManager component operates as an active component that manages system-wi - 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) + - Turns on **all load switches** (faces 0-5 and payload 6-7) to ensure consistent power state even after fault exits - Notifies downstream components via `modeChanged` port - Updates telemetry (CurrentMode, PayloadModeEntryCount) - Persists state to flash storage @@ -73,11 +75,21 @@ The ModeManager component operates as an active component that manages system-wi - Transitions mode to NORMAL - Emits `ExitingPayloadMode` event - Turns off payload load switches (indices 6 and 7) + - Ensures face load switches (0-5) are turned ON for NORMAL mode - Notifies downstream components via `modeChanged` port - Updates telemetry - Persists state to flash storage -6. **Safe Mode Exit** +6. **Automatic Payload Mode Exit (Low Voltage)** + - Triggered when in PAYLOAD_MODE and bus voltage is below 7.2V (or invalid) for 10 consecutive 1Hz checks + - Actions performed: + - Emits `AutoPayloadModeExit` with measured/0.0V value + - Transitions mode to NORMAL + - Turns off **all 8 load switches** (faces and payload) aggressively + - Notifies downstream components via `modeChanged` port + - Updates telemetry and persists state + +7. **Safe Mode Exit** - Triggered only by ground command: `EXIT_SAFE_MODE` - Validates currently in safe mode before allowing exit - Actions performed: @@ -122,11 +134,15 @@ classDiagram - exitSafeMode() - enterPayloadMode(const char* reason) - exitPayloadMode() + - exitPayloadModeAutomatic(F32 voltage) - turnOffNonCriticalComponents() - turnOnComponents() - turnOnPayload() - turnOffPayload() - getCurrentVoltage(bool& valid): F32 + - m_lowVoltageCounter: U32 + - LOW_VOLTAGE_THRESHOLD: F32 + - LOW_VOLTAGE_DEBOUNCE_SECONDS: U32 } class SystemMode { <> @@ -144,7 +160,7 @@ classDiagram ### Input Ports | Name | Type | Kind | Description | |---|---|---|---| -| run | Svc.Sched | sync | Receives periodic calls from rate group (1Hz) for telemetry updates | +| run | Svc.Sched | sync | Receives periodic calls from rate group (1Hz) for telemetry updates and low-voltage monitoring while in PAYLOAD_MODE | | forceSafeMode | Fw.Signal | async | Receives safe mode requests from external components detecting faults | | getMode | Components.GetSystemMode | sync | Allows downstream components to query current system mode | @@ -164,6 +180,7 @@ classDiagram | 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 | +| m_lowVoltageCounter | U32 | Debounce counter for consecutive low/invalid voltage readings while in PAYLOAD_MODE | ### Persistent State The component persists the following state to `/mode_state.bin`: @@ -251,7 +268,7 @@ sequenceDiagram 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->>LoadSwitches: Turn on all load switches (faces 0-5 and payload 6 & 7) ModeManager->>ModeManager: Update telemetry ModeManager->>DownstreamComponents: modeChanged_out(PAYLOAD_MODE) ModeManager->>FlashStorage: Save state to /mode_state.bin @@ -272,12 +289,33 @@ sequenceDiagram ModeManager->>ModeManager: Set m_mode = NORMAL ModeManager->>ModeManager: Emit ExitingPayloadMode event ModeManager->>LoadSwitches: Turn off payload switches (6 & 7) + ModeManager->>LoadSwitches: Turn on face switches (0-5) to normalize NORMAL state ModeManager->>ModeManager: Update telemetry ModeManager->>DownstreamComponents: modeChanged_out(NORMAL) ModeManager->>FlashStorage: Save state to /mode_state.bin ModeManager->>Ground: Command response OK ``` +### Automatic Payload Mode Exit (Low Voltage) +```mermaid +sequenceDiagram + participant ModeManager + participant INA219 + participant LoadSwitches + participant DownstreamComponents + participant FlashStorage + + ModeManager->>INA219: voltageGet() + INA219-->>ModeManager: Voltage / invalid + ModeManager->>ModeManager: Debounce low-voltage/invalid readings (10 consecutive seconds) + ModeManager->>ModeManager: On debounce hit: Emit AutoPayloadModeExit(voltage) + ModeManager->>ModeManager: Set m_mode = NORMAL + ModeManager->>LoadSwitches: Turn off all 8 switches (faces and payload) + ModeManager->>ModeManager: Update telemetry + ModeManager->>DownstreamComponents: modeChanged_out(NORMAL) + ModeManager->>FlashStorage: Save state to /mode_state.bin +``` + ### Mode Query ```mermaid sequenceDiagram @@ -297,6 +335,8 @@ sequenceDiagram RateGroup->>ModeManager: run(portNum, context) ModeManager->>ModeManager: Increment m_runCounter + ModeManager->>ModeManager: If PAYLOAD_MODE: read voltage, debounce low-voltage counter (10s window) + ModeManager->>ModeManager: If sustained low/invalid voltage: exitPayloadModeAutomatic() ModeManager->>ModeManager: Write CurrentMode telemetry ``` @@ -320,6 +360,7 @@ sequenceDiagram | 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 | +| AutoPayloadModeExit | WARNING_HI | voltage: F32 | Emitted when automatically exiting payload mode due to low/invalid voltage (voltage value reported; 0.0 if invalid) | | 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 | @@ -346,7 +387,7 @@ The ModeManager controls 8 load switches that power non-critical satellite subsy | 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. +> **Notes:** 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. Automatic payload exits (low/invalid voltage) aggressively turn **all** switches OFF; manual payload exits leave faces ON and only shed payload loads. ## Integration Tests See `FprimeZephyrReference/test/int/mode_manager_test.py` and `FprimeZephyrReference/test/int/payload_mode_test.py` for comprehensive integration tests covering: @@ -362,6 +403,9 @@ See `FprimeZephyrReference/test/int/mode_manager_test.py` and `FprimeZephyrRefer | 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_payload_05_manual_exit_face_switches_remain_on | Verifies manual payload exit leaves faces ON and payload OFF | Payload exit power behavior | +| test_payload_06_voltage_monitoring_active | Verifies voltage telemetry is present in payload mode and no false auto-exit when voltage healthy | Low-voltage monitoring sanity | +| test_payload_07_auto_exit_low_voltage (manual) | Manual test to validate debounced low-voltage auto-exit, AutoPayloadModeExit event, and full load-shed | Low-voltage protection | ## Design Decisions @@ -388,12 +432,21 @@ Mode state is persisted to `/mode_state.bin` to maintain operational context acr This ensures the system resumes in the correct mode after recovery. +### Low-Voltage Payload Protection +Payload mode is protected by a debounced low-voltage monitor: +- Voltage sampled at 1Hz while in PAYLOAD_MODE via `voltageGet` +- Threshold: 7.2V; invalid readings are treated as faults to avoid masking brownouts +- Debounce: 10 consecutive low/invalid readings before acting +- Action: emit `AutoPayloadModeExit`, set mode to NORMAL, and turn off all 8 load switches +- Re-entering payload mode re-powers faces and payload switches to restore a consistent state + ### 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 | |---|---| +| 2026-11-29 | Added low-voltage monitoring with debounced automatic payload exit, AutoPayloadModeExit event, and documentation/tests for aggressive load shed and manual exit power state | | 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 | diff --git a/FprimeZephyrReference/test/int/payload_mode_test.py b/FprimeZephyrReference/test/int/payload_mode_test.py index b8b76aa3..29ff77c8 100644 --- a/FprimeZephyrReference/test/int/payload_mode_test.py +++ b/FprimeZephyrReference/test/int/payload_mode_test.py @@ -8,8 +8,11 @@ - Mode transition validation (cannot enter from safe mode) - Sequential transitions (FORCE_SAFE_MODE rejected from payload mode) - State persistence +- Manual exit behavior (face switches remain on) +- Voltage monitoring active in payload mode (no false triggers) +- Automatic exit due to low voltage (manual test - requires voltage control) -Total: 4 tests +Total: 7 tests (6 automated, 1 manual) Mode enum values: SAFE_MODE=1, NORMAL=2, PAYLOAD_MODE=3 """ @@ -22,6 +25,14 @@ from fprime_gds.common.testing_fw.api import IntegrationTestAPI component = "ReferenceDeployment.modeManager" +face_load_switch_channels = [ + "ReferenceDeployment.face4LoadSwitch.IsOn", + "ReferenceDeployment.face0LoadSwitch.IsOn", + "ReferenceDeployment.face1LoadSwitch.IsOn", + "ReferenceDeployment.face2LoadSwitch.IsOn", + "ReferenceDeployment.face3LoadSwitch.IsOn", + "ReferenceDeployment.face5LoadSwitch.IsOn", +] payload_load_switch_channels = [ "ReferenceDeployment.payloadPowerLoadSwitch.IsOn", "ReferenceDeployment.payloadBatteryLoadSwitch.IsOn", @@ -237,3 +248,184 @@ def test_payload_04_state_persists(fprime_test_api: IntegrationTestAPI, start_gd f"{component}.PayloadModeEntryCount", timeout=5 ) assert count_result.get_val() >= 1, "PayloadModeEntryCount should be at least 1" + + +def test_payload_05_manual_exit_face_switches_remain_on( + fprime_test_api: IntegrationTestAPI, start_gds +): + """ + Test that manual EXIT_PAYLOAD_MODE leaves face switches ON. + Verifies: + - Face switches (0-5) remain ON after manual exit + - Payload switches (6-7) are turned OFF + - Mode returns to NORMAL (2) + """ + # Enter payload mode + proves_send_and_assert_command(fprime_test_api, f"{component}.ENTER_PAYLOAD_MODE") + time.sleep(2) + + # Verify all switches are ON in payload mode + 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() == "ON", f"{channel} should be ON in payload mode" + else: + assert value == 1, f"{channel} should be ON in payload mode" + + # Exit payload mode manually + 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 face switches (0-5) are still ON + proves_send_and_assert_command(fprime_test_api, "CdhCore.tlmSend.SEND_PKT", ["9"]) + for channel in face_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 remain ON after manual exit" + ) + else: + assert value == 1, f"{channel} should remain ON after manual exit" + + # Verify payload switches (6-7) are OFF + 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 after manual exit" + else: + assert value == 0, f"{channel} should be OFF after manual exit" + + +def test_payload_06_voltage_monitoring_active( + fprime_test_api: IntegrationTestAPI, start_gds +): + """ + Test that voltage monitoring is active during payload mode. + Verifies: + - System voltage telemetry is available + - Voltage is above threshold (no auto-exit should occur) + - Mode remains in PAYLOAD_MODE after 5 seconds (no false triggers) + + This test verifies the infrastructure works without requiring + manual voltage control. The actual low-voltage exit is tested + in test_payload_07_auto_exit_low_voltage (manual test). + """ + # Enter payload mode + 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" + + # Verify system voltage telemetry is available and valid + proves_send_and_assert_command(fprime_test_api, "CdhCore.tlmSend.SEND_PKT", ["2"]) + voltage_result: ChData = fprime_test_api.assert_telemetry( + "ReferenceDeployment.ina219SysManager.Voltage", timeout=5 + ) + voltage = voltage_result.get_val() + assert voltage > 0, f"Voltage should be positive, got {voltage}" + + # Note: We don't assert voltage > 7.2V because board supply can fluctuate. + # If voltage is actually low, auto-exit is correct behavior. + print(f"Current system voltage: {voltage}V (threshold: 7.2V)") + + # Wait 5 seconds + time.sleep(5) + + # Check final 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 + ) + final_mode = mode_result.get_val() + + # Check for auto-exit events + events = fprime_test_api.get_event_test_history() + auto_exit_events = [ + e for e in events if "AutoPayloadModeExit" in str(e.get_template().get_name()) + ] + + if voltage >= 7.2: + # Voltage was healthy - should remain in payload mode + assert final_mode == 3, "Should still be in PAYLOAD_MODE (no false auto-exit)" + assert len(auto_exit_events) == 0, ( + "Should not have triggered AutoPayloadModeExit" + ) + else: + # Voltage was low - auto-exit may have triggered (correct behavior) + print( + "Note: Voltage was below threshold. Auto-exit may have occurred (expected)." + ) + # Don't fail the test - low voltage triggering exit is correct behavior + + +@pytest.mark.skip(reason="Requires voltage control - run manually with low battery") +def test_payload_07_auto_exit_low_voltage( + fprime_test_api: IntegrationTestAPI, start_gds +): + """ + Test automatic exit from payload mode due to low voltage. + + MANUAL TEST - requires ability to control battery voltage: + 1. Enter payload mode + 2. Lower battery voltage below 7.2V for 10+ seconds + 3. Verify AutoPayloadModeExit event is emitted + 4. Verify mode returns to NORMAL (2) + 5. Verify ALL switches (faces 0-5 AND payload 6-7) are OFF + + Expected behavior: + - Voltage must be below 7.2V for 10 consecutive seconds (1Hz checks) + - Invalid voltage readings also count as faults + - AutoPayloadModeExit event emitted with voltage value (0.0V if invalid) + - Mode changes to NORMAL + - All 8 load switches turned OFF (more aggressive than manual exit) + """ + # Enter payload mode + 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" + + # --- MANUAL STEP: Lower voltage below 7.2V for 10+ seconds --- + # Wait for automatic exit (would need voltage control here) + time.sleep(15) + + # Verify AutoPayloadModeExit event + fprime_test_api.assert_event(f"{component}.AutoPayloadModeExit", timeout=5) + + # 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 after auto exit" + + # Verify ALL switches are OFF (aggressive behavior) + 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 after auto exit" + else: + assert value == 0, f"{channel} should be OFF after auto exit" From 0e6cfed510684d813e29ec80fffe01d5170575fb Mon Sep 17 00:00:00 2001 From: "Sam S. Yu" <25761223+yudataguy@users.noreply.github.com> Date: Sat, 29 Nov 2025 10:30:15 -0800 Subject: [PATCH 13/16] fix review feedback --- .../Components/ModeManager/ModeManager.cpp | 23 ++++- .../Components/ModeManager/ModeManager.hpp | 3 + .../Components/ModeManager/docs/sdd.md | 2 +- .../test/int/payload_mode_test.py | 90 ++++++++++++++----- Makefile | 2 +- 5 files changed, 92 insertions(+), 28 deletions(-) diff --git a/FprimeZephyrReference/Components/ModeManager/ModeManager.cpp b/FprimeZephyrReference/Components/ModeManager/ModeManager.cpp index 783e48b8..a9903a74 100644 --- a/FprimeZephyrReference/Components/ModeManager/ModeManager.cpp +++ b/FprimeZephyrReference/Components/ModeManager/ModeManager.cpp @@ -23,7 +23,16 @@ ModeManager ::ModeManager(const char* const compName) m_safeModeEntryCount(0), m_payloadModeEntryCount(0), m_runCounter(0), - m_lowVoltageCounter(0) {} + m_lowVoltageCounter(0) { + // Compile-time verification that internal SystemMode enum matches FPP-generated enum + // This prevents silent mismatches when casting between enum types + 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"); + static_assert(static_cast(SystemMode::PAYLOAD_MODE) == static_cast(Components::SystemMode::PAYLOAD_MODE), + "Internal PAYLOAD_MODE value must match FPP enum"); +} ModeManager ::~ModeManager() {} @@ -40,6 +49,12 @@ void ModeManager ::run_handler(FwIndexType portNum, U32 context) { // Increment run counter (1Hz tick counter) this->m_runCounter++; + // Low-voltage protection for payload mode: + // - Debounce: Requires 10 consecutive fault readings (at 1Hz) to avoid spurious triggers + // from transient voltage dips during load switching or sensor noise + // - Invalid readings: Treated as faults (fail-safe) because sensor failure during payload + // operation could mask a real brownout condition + // - Threshold: 7.2V chosen as minimum safe operating voltage for payload components // Check for low voltage fault when in PAYLOAD_MODE if (this->m_mode == SystemMode::PAYLOAD_MODE) { bool valid = false; @@ -274,7 +289,7 @@ void ModeManager ::enterSafeMode(const char* reasonOverride) { // Build reason string Fw::LogStringArg reasonStr; - char reasonBuf[100]; + char reasonBuf[REASON_STRING_SIZE]; if (reasonOverride != nullptr) { reasonStr = reasonOverride; } else { @@ -331,7 +346,7 @@ void ModeManager ::enterPayloadMode(const char* reasonOverride) { // Build reason string Fw::LogStringArg reasonStr; - char reasonBuf[100]; + char reasonBuf[REASON_STRING_SIZE]; if (reasonOverride != nullptr) { reasonStr = reasonOverride; } else { @@ -363,6 +378,7 @@ void ModeManager ::enterPayloadMode(const char* reasonOverride) { void ModeManager ::exitPayloadMode() { // Transition back to normal mode (manual exit) this->m_mode = SystemMode::NORMAL; + this->m_lowVoltageCounter = 0; // Reset low voltage counter on mode exit this->log_ACTIVITY_HI_ExitingPayloadMode(); @@ -390,6 +406,7 @@ void ModeManager ::exitPayloadModeAutomatic(F32 voltage) { // Automatic exit from payload mode due to fault condition (e.g., low voltage) // More aggressive than manual exit - turns off ALL switches this->m_mode = SystemMode::NORMAL; + this->m_lowVoltageCounter = 0; // Reset low voltage counter on mode exit this->log_WARNING_HI_AutoPayloadModeExit(voltage); diff --git a/FprimeZephyrReference/Components/ModeManager/ModeManager.hpp b/FprimeZephyrReference/Components/ModeManager/ModeManager.hpp index 1d3e65c8..5674cd71 100644 --- a/FprimeZephyrReference/Components/ModeManager/ModeManager.hpp +++ b/FprimeZephyrReference/Components/ModeManager/ModeManager.hpp @@ -154,6 +154,9 @@ class ModeManager : public ModeManagerComponentBase { // Voltage threshold constants for payload mode protection static constexpr F32 LOW_VOLTAGE_THRESHOLD = 7.2f; //!< Voltage threshold in volts static constexpr U32 LOW_VOLTAGE_DEBOUNCE_SECONDS = 10; //!< Consecutive seconds below threshold + + // Buffer size for reason strings (must match FPP string size definitions) + static constexpr FwSizeType REASON_STRING_SIZE = 100; //!< Matches FPP reason: string size 100 }; } // namespace Components diff --git a/FprimeZephyrReference/Components/ModeManager/docs/sdd.md b/FprimeZephyrReference/Components/ModeManager/docs/sdd.md index 0ef25ac8..f1536064 100644 --- a/FprimeZephyrReference/Components/ModeManager/docs/sdd.md +++ b/FprimeZephyrReference/Components/ModeManager/docs/sdd.md @@ -446,7 +446,7 @@ Mode transitions follow a +1/-1 sequential pattern: SAFE_MODE(1) ↔ NORMAL(2) ## Change Log | Date | Description | |---|---| -| 2026-11-29 | Added low-voltage monitoring with debounced automatic payload exit, AutoPayloadModeExit event, and documentation/tests for aggressive load shed and manual exit power state | +| 2025-11-29 | Added low-voltage monitoring with debounced automatic payload exit, AutoPayloadModeExit event, and documentation/tests for aggressive load shed and manual exit power state | | 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 | diff --git a/FprimeZephyrReference/test/int/payload_mode_test.py b/FprimeZephyrReference/test/int/payload_mode_test.py index 29ff77c8..c7be87e8 100644 --- a/FprimeZephyrReference/test/int/payload_mode_test.py +++ b/FprimeZephyrReference/test/int/payload_mode_test.py @@ -48,6 +48,14 @@ "ReferenceDeployment.payloadBatteryLoadSwitch.IsOn", ] +# Telemetry packet IDs for CdhCore.tlmSend.SEND_PKT command +MODE_TELEMETRY_PACKET_ID = "1" # ModeManager telemetry (CurrentMode, etc.) +VOLTAGE_TELEMETRY_PACKET_ID = "2" # INA219 voltage telemetry +LOAD_SWITCH_TELEMETRY_PACKET_ID = "9" # Load switch telemetry (IsOn states) + +# Voltage threshold for payload mode auto-exit (must match LOW_VOLTAGE_THRESHOLD in ModeManager.hpp) +LOW_VOLTAGE_THRESHOLD = 7.2 # Volts + @pytest.fixture(autouse=True) def setup_and_teardown(fprime_test_api: IntegrationTestAPI, start_gds): @@ -112,14 +120,18 @@ def test_payload_01_enter_exit_payload_mode( time.sleep(2) # Verify mode is PAYLOAD_MODE (3) - proves_send_and_assert_command(fprime_test_api, "CdhCore.tlmSend.SEND_PKT", ["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() == 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"]) + proves_send_and_assert_command( + fprime_test_api, "CdhCore.tlmSend.SEND_PKT", [LOAD_SWITCH_TELEMETRY_PACKET_ID] + ) for channel in payload_load_switch_channels: value = fprime_test_api.assert_telemetry(channel, timeout=5).get_val() if isinstance(value, str): @@ -137,14 +149,18 @@ def test_payload_01_enter_exit_payload_mode( time.sleep(2) # Verify mode is NORMAL (2) - proves_send_and_assert_command(fprime_test_api, "CdhCore.tlmSend.SEND_PKT", ["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() == 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"]) + proves_send_and_assert_command( + fprime_test_api, "CdhCore.tlmSend.SEND_PKT", [LOAD_SWITCH_TELEMETRY_PACKET_ID] + ) for channel in payload_load_switch_channels: value = fprime_test_api.assert_telemetry(channel, timeout=5).get_val() if isinstance(value, str): @@ -165,7 +181,9 @@ def test_payload_02_cannot_enter_from_safe_mode( time.sleep(2) # Verify in safe mode - proves_send_and_assert_command(fprime_test_api, "CdhCore.tlmSend.SEND_PKT", ["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 ) @@ -182,7 +200,9 @@ def test_payload_02_cannot_enter_from_safe_mode( 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"]) + 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 ) @@ -205,7 +225,9 @@ def test_payload_03_safe_mode_rejected_from_payload( time.sleep(2) # Verify in payload mode - proves_send_and_assert_command(fprime_test_api, "CdhCore.tlmSend.SEND_PKT", ["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 ) @@ -220,7 +242,9 @@ def test_payload_03_safe_mode_rejected_from_payload( 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"]) + 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 ) @@ -237,7 +261,9 @@ def test_payload_04_state_persists(fprime_test_api: IntegrationTestAPI, start_gd time.sleep(2) # Verify mode is saved as PAYLOAD_MODE (3) - proves_send_and_assert_command(fprime_test_api, "CdhCore.tlmSend.SEND_PKT", ["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 ) @@ -265,7 +291,9 @@ def test_payload_05_manual_exit_face_switches_remain_on( time.sleep(2) # Verify all switches are ON in payload mode - proves_send_and_assert_command(fprime_test_api, "CdhCore.tlmSend.SEND_PKT", ["9"]) + proves_send_and_assert_command( + fprime_test_api, "CdhCore.tlmSend.SEND_PKT", [LOAD_SWITCH_TELEMETRY_PACKET_ID] + ) for channel in all_load_switch_channels: value = fprime_test_api.assert_telemetry(channel, timeout=5).get_val() if isinstance(value, str): @@ -282,14 +310,18 @@ def test_payload_05_manual_exit_face_switches_remain_on( time.sleep(2) # Verify mode is NORMAL (2) - proves_send_and_assert_command(fprime_test_api, "CdhCore.tlmSend.SEND_PKT", ["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() == 2, "Should be in NORMAL mode" # Verify face switches (0-5) are still ON - proves_send_and_assert_command(fprime_test_api, "CdhCore.tlmSend.SEND_PKT", ["9"]) + proves_send_and_assert_command( + fprime_test_api, "CdhCore.tlmSend.SEND_PKT", [LOAD_SWITCH_TELEMETRY_PACKET_ID] + ) for channel in face_load_switch_channels: value = fprime_test_api.assert_telemetry(channel, timeout=5).get_val() if isinstance(value, str): @@ -327,29 +359,35 @@ def test_payload_06_voltage_monitoring_active( time.sleep(2) # Verify in payload mode - proves_send_and_assert_command(fprime_test_api, "CdhCore.tlmSend.SEND_PKT", ["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() == 3, "Should be in PAYLOAD_MODE" # Verify system voltage telemetry is available and valid - proves_send_and_assert_command(fprime_test_api, "CdhCore.tlmSend.SEND_PKT", ["2"]) + proves_send_and_assert_command( + fprime_test_api, "CdhCore.tlmSend.SEND_PKT", [VOLTAGE_TELEMETRY_PACKET_ID] + ) voltage_result: ChData = fprime_test_api.assert_telemetry( "ReferenceDeployment.ina219SysManager.Voltage", timeout=5 ) voltage = voltage_result.get_val() assert voltage > 0, f"Voltage should be positive, got {voltage}" - # Note: We don't assert voltage > 7.2V because board supply can fluctuate. + # Note: We don't assert voltage > threshold because board supply can fluctuate. # If voltage is actually low, auto-exit is correct behavior. - print(f"Current system voltage: {voltage}V (threshold: 7.2V)") + print(f"Current system voltage: {voltage}V (threshold: {LOW_VOLTAGE_THRESHOLD}V)") - # Wait 5 seconds - time.sleep(5) + # Wait 11 seconds + time.sleep(11) # Check final mode - proves_send_and_assert_command(fprime_test_api, "CdhCore.tlmSend.SEND_PKT", ["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 ) @@ -361,7 +399,7 @@ def test_payload_06_voltage_monitoring_active( e for e in events if "AutoPayloadModeExit" in str(e.get_template().get_name()) ] - if voltage >= 7.2: + if voltage >= LOW_VOLTAGE_THRESHOLD: # Voltage was healthy - should remain in payload mode assert final_mode == 3, "Should still be in PAYLOAD_MODE (no false auto-exit)" assert len(auto_exit_events) == 0, ( @@ -401,7 +439,9 @@ def test_payload_07_auto_exit_low_voltage( time.sleep(2) # Verify in payload mode - proves_send_and_assert_command(fprime_test_api, "CdhCore.tlmSend.SEND_PKT", ["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 ) @@ -415,14 +455,18 @@ def test_payload_07_auto_exit_low_voltage( fprime_test_api.assert_event(f"{component}.AutoPayloadModeExit", timeout=5) # Verify mode is NORMAL (2) - proves_send_and_assert_command(fprime_test_api, "CdhCore.tlmSend.SEND_PKT", ["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() == 2, "Should be in NORMAL mode after auto exit" # Verify ALL switches are OFF (aggressive behavior) - proves_send_and_assert_command(fprime_test_api, "CdhCore.tlmSend.SEND_PKT", ["9"]) + proves_send_and_assert_command( + fprime_test_api, "CdhCore.tlmSend.SEND_PKT", [LOAD_SWITCH_TELEMETRY_PACKET_ID] + ) for channel in all_load_switch_channels: value = fprime_test_api.assert_telemetry(channel, timeout=5).get_val() if isinstance(value, str): diff --git a/Makefile b/Makefile index 9afd5561..6354c0b9 100644 --- a/Makefile +++ b/Makefile @@ -60,7 +60,7 @@ test-integration: uv ## Run integration tests (set TEST= to run a *.py) TARGET="FprimeZephyrReference/test/int/$(TEST)" ;; \ *) TARGET="FprimeZephyrReference/test/int/$(TEST).py" ;; \ esac; \ - [ -e "$$TARGET" ] || { echo "Specified test file $$TARGET not found"; exit 1; }; \ + [ -e "$$TARGET" ] || { echo "Error: Test file $$TARGET not found" >&2; exit 1; }; \ fi; \ echo "Running integration tests at $$TARGET"; \ $(UV_RUN) pytest $$TARGET --deployment build-artifacts/zephyr/fprime-zephyr-deployment From 79b620203452d2264787bae90d867641c7c52c32 Mon Sep 17 00:00:00 2001 From: "Sam S. Yu" <25761223+yudataguy@users.noreply.github.com> Date: Sat, 29 Nov 2025 10:41:44 -0800 Subject: [PATCH 14/16] improve based on review --- .../Components/ModeManager/ModeManager.cpp | 3 +++ .../test/int/payload_mode_test.py | 19 +++++++++++-------- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/FprimeZephyrReference/Components/ModeManager/ModeManager.cpp b/FprimeZephyrReference/Components/ModeManager/ModeManager.cpp index a9903a74..7e816f31 100644 --- a/FprimeZephyrReference/Components/ModeManager/ModeManager.cpp +++ b/FprimeZephyrReference/Components/ModeManager/ModeManager.cpp @@ -162,6 +162,9 @@ void ModeManager ::ENTER_PAYLOAD_MODE_cmdHandler(FwOpcodeType opCode, U32 cmdSeq // Check if already in payload mode if (this->m_mode == SystemMode::PAYLOAD_MODE) { // Already in payload mode - success (idempotent) + // Reset low voltage counter to ensure consistent state (operator may be + // re-sending command intentionally to reset any accumulated fault count) + this->m_lowVoltageCounter = 0; this->cmdResponse_out(opCode, cmdSeq, Fw::CmdResponse::OK); return; } diff --git a/FprimeZephyrReference/test/int/payload_mode_test.py b/FprimeZephyrReference/test/int/payload_mode_test.py index c7be87e8..0b506ec0 100644 --- a/FprimeZephyrReference/test/int/payload_mode_test.py +++ b/FprimeZephyrReference/test/int/payload_mode_test.py @@ -53,8 +53,11 @@ VOLTAGE_TELEMETRY_PACKET_ID = "2" # INA219 voltage telemetry LOAD_SWITCH_TELEMETRY_PACKET_ID = "9" # Load switch telemetry (IsOn states) -# Voltage threshold for payload mode auto-exit (must match LOW_VOLTAGE_THRESHOLD in ModeManager.hpp) +# Voltage threshold for payload mode auto-exit (must match ModeManager.hpp constants) LOW_VOLTAGE_THRESHOLD = 7.2 # Volts +LOW_VOLTAGE_DEBOUNCE_SECONDS = ( + 10 # Consecutive seconds below threshold before auto-exit +) @pytest.fixture(autouse=True) @@ -348,7 +351,7 @@ def test_payload_06_voltage_monitoring_active( Verifies: - System voltage telemetry is available - Voltage is above threshold (no auto-exit should occur) - - Mode remains in PAYLOAD_MODE after 5 seconds (no false triggers) + - Mode remains in PAYLOAD_MODE after 11 seconds (no false triggers) This test verifies the infrastructure works without requiring manual voltage control. The actual low-voltage exit is tested @@ -381,8 +384,8 @@ def test_payload_06_voltage_monitoring_active( # If voltage is actually low, auto-exit is correct behavior. print(f"Current system voltage: {voltage}V (threshold: {LOW_VOLTAGE_THRESHOLD}V)") - # Wait 11 seconds - time.sleep(11) + # Wait for debounce period + 1 second margin to verify no false triggers + time.sleep(LOW_VOLTAGE_DEBOUNCE_SECONDS + 1) # Check final mode proves_send_and_assert_command( @@ -422,13 +425,13 @@ def test_payload_07_auto_exit_low_voltage( MANUAL TEST - requires ability to control battery voltage: 1. Enter payload mode - 2. Lower battery voltage below 7.2V for 10+ seconds + 2. Lower battery voltage below LOW_VOLTAGE_THRESHOLD for LOW_VOLTAGE_DEBOUNCE_SECONDS 3. Verify AutoPayloadModeExit event is emitted 4. Verify mode returns to NORMAL (2) 5. Verify ALL switches (faces 0-5 AND payload 6-7) are OFF Expected behavior: - - Voltage must be below 7.2V for 10 consecutive seconds (1Hz checks) + - Voltage must be below threshold for debounce period (1Hz checks) - Invalid voltage readings also count as faults - AutoPayloadModeExit event emitted with voltage value (0.0V if invalid) - Mode changes to NORMAL @@ -447,9 +450,9 @@ def test_payload_07_auto_exit_low_voltage( ) assert mode_result.get_val() == 3, "Should be in PAYLOAD_MODE" - # --- MANUAL STEP: Lower voltage below 7.2V for 10+ seconds --- + # --- MANUAL STEP: Lower voltage below threshold for debounce period --- # Wait for automatic exit (would need voltage control here) - time.sleep(15) + time.sleep(LOW_VOLTAGE_DEBOUNCE_SECONDS + 5) # Extra margin for manual test # Verify AutoPayloadModeExit event fprime_test_api.assert_event(f"{component}.AutoPayloadModeExit", timeout=5) From f19e98e9a7bdaf6b97a6ef716a27d6cbe778cdaa Mon Sep 17 00:00:00 2001 From: "Sam S. Yu" <25761223+yudataguy@users.noreply.github.com> Date: Sat, 29 Nov 2025 10:50:54 -0800 Subject: [PATCH 15/16] improve test logging --- .../test/int/payload_mode_test.py | 24 ++++++++++++------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/FprimeZephyrReference/test/int/payload_mode_test.py b/FprimeZephyrReference/test/int/payload_mode_test.py index 0b506ec0..2652cb3e 100644 --- a/FprimeZephyrReference/test/int/payload_mode_test.py +++ b/FprimeZephyrReference/test/int/payload_mode_test.py @@ -17,6 +17,7 @@ Mode enum values: SAFE_MODE=1, NORMAL=2, PAYLOAD_MODE=3 """ +import logging import time import pytest @@ -24,6 +25,8 @@ 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" face_load_switch_channels = [ "ReferenceDeployment.face4LoadSwitch.IsOn", @@ -65,18 +68,21 @@ 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. + + Note: Exit commands fail with VALIDATION_ERROR if not in that mode, + which is expected behavior. We log these for debugging but don't fail. """ # 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 + except Exception as e: + logger.debug(f"Setup: EXIT_SAFE_MODE skipped (not in safe mode): {e}") try: proves_send_and_assert_command( fprime_test_api, f"{component}.EXIT_PAYLOAD_MODE" ) - except Exception: - pass + except Exception as e: + logger.debug(f"Setup: EXIT_PAYLOAD_MODE skipped (not in payload mode): {e}") # Clear event and telemetry history before test fprime_test_api.clear_histories() @@ -86,14 +92,14 @@ def setup_and_teardown(fprime_test_api: IntegrationTestAPI, start_gds): # 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 + except Exception as e: + logger.debug(f"Teardown: EXIT_SAFE_MODE skipped (not in safe mode): {e}") try: proves_send_and_assert_command( fprime_test_api, f"{component}.EXIT_PAYLOAD_MODE" ) - except Exception: - pass + except Exception as e: + logger.debug(f"Teardown: EXIT_PAYLOAD_MODE skipped (not in payload mode): {e}") # ============================================================================== @@ -351,7 +357,7 @@ def test_payload_06_voltage_monitoring_active( Verifies: - System voltage telemetry is available - Voltage is above threshold (no auto-exit should occur) - - Mode remains in PAYLOAD_MODE after 11 seconds (no false triggers) + - Mode remains in PAYLOAD_MODE after debounce period + 1 second (11 seconds total, no false triggers) This test verifies the infrastructure works without requiring manual voltage control. The actual low-voltage exit is tested From 54112b987eb3de9a12e8f233817ed3976379b5f0 Mon Sep 17 00:00:00 2001 From: "Sam S. Yu" <25761223+yudataguy@users.noreply.github.com> Date: Sat, 29 Nov 2025 11:00:25 -0800 Subject: [PATCH 16/16] improve test --- .../test/int/payload_mode_test.py | 21 +++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/FprimeZephyrReference/test/int/payload_mode_test.py b/FprimeZephyrReference/test/int/payload_mode_test.py index 2652cb3e..9ae3c272 100644 --- a/FprimeZephyrReference/test/int/payload_mode_test.py +++ b/FprimeZephyrReference/test/int/payload_mode_test.py @@ -388,11 +388,23 @@ def test_payload_06_voltage_monitoring_active( # Note: We don't assert voltage > threshold because board supply can fluctuate. # If voltage is actually low, auto-exit is correct behavior. - print(f"Current system voltage: {voltage}V (threshold: {LOW_VOLTAGE_THRESHOLD}V)") + print(f"Initial system voltage: {voltage}V (threshold: {LOW_VOLTAGE_THRESHOLD}V)") # Wait for debounce period + 1 second margin to verify no false triggers time.sleep(LOW_VOLTAGE_DEBOUNCE_SECONDS + 1) + # Sample voltage again after the wait - voltage may have changed during the period + proves_send_and_assert_command( + fprime_test_api, "CdhCore.tlmSend.SEND_PKT", [VOLTAGE_TELEMETRY_PACKET_ID] + ) + voltage_result_post: ChData = fprime_test_api.assert_telemetry( + "ReferenceDeployment.ina219SysManager.Voltage", timeout=5 + ) + voltage_post = voltage_result_post.get_val() + print( + f"Post-wait system voltage: {voltage_post}V (threshold: {LOW_VOLTAGE_THRESHOLD}V)" + ) + # Check final mode proves_send_and_assert_command( fprime_test_api, "CdhCore.tlmSend.SEND_PKT", [MODE_TELEMETRY_PACKET_ID] @@ -408,14 +420,15 @@ def test_payload_06_voltage_monitoring_active( e for e in events if "AutoPayloadModeExit" in str(e.get_template().get_name()) ] - if voltage >= LOW_VOLTAGE_THRESHOLD: - # Voltage was healthy - should remain in payload mode + # Use post-wait voltage for assertion (voltage may have changed during wait) + if voltage_post >= LOW_VOLTAGE_THRESHOLD: + # Voltage is healthy now - should remain in payload mode assert final_mode == 3, "Should still be in PAYLOAD_MODE (no false auto-exit)" assert len(auto_exit_events) == 0, ( "Should not have triggered AutoPayloadModeExit" ) else: - # Voltage was low - auto-exit may have triggered (correct behavior) + # Voltage is low - auto-exit may have triggered (correct behavior) print( "Note: Voltage was below threshold. Auto-exit may have occurred (expected)." )