diff --git a/FprimeZephyrReference/Components/ModeManager/CMakeLists.txt b/FprimeZephyrReference/Components/ModeManager/CMakeLists.txt index 5ae91fc3..65b13bae 100644 --- a/FprimeZephyrReference/Components/ModeManager/CMakeLists.txt +++ b/FprimeZephyrReference/Components/ModeManager/CMakeLists.txt @@ -19,6 +19,7 @@ register_fprime_library( "${CMAKE_CURRENT_LIST_DIR}/ModeManager.fpp" SOURCES "${CMAKE_CURRENT_LIST_DIR}/ModeManager.cpp" + "${CMAKE_CURRENT_LIST_DIR}/PicoSleep.cpp" # DEPENDS # MyPackage_MyOtherModule ) diff --git a/FprimeZephyrReference/Components/ModeManager/ModeManager.cpp b/FprimeZephyrReference/Components/ModeManager/ModeManager.cpp index e697db4f..17b902b3 100644 --- a/FprimeZephyrReference/Components/ModeManager/ModeManager.cpp +++ b/FprimeZephyrReference/Components/ModeManager/ModeManager.cpp @@ -9,6 +9,7 @@ #include #include +#include "FprimeZephyrReference/Components/ModeManager/PicoSleep.hpp" #include "Fw/Types/Assert.hpp" namespace Components { @@ -18,7 +19,17 @@ 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), + m_inHibernationWakeWindow(false), + m_wakeWindowCounter(0), + m_hibernationCycleCount(0), + m_hibernationTotalSeconds(0), + m_sleepDurationSec(3600), // Default 60 minutes + m_wakeDurationSec(60) {} // Default 1 minute ModeManager ::~ModeManager() {} @@ -35,18 +46,23 @@ void ModeManager ::run_handler(FwIndexType portNum, U32 context) { // Increment run counter (1Hz tick counter) this->m_runCounter++; + // Handle hibernation wake window timing + if (this->m_mode == SystemMode::HIBERNATION_MODE && this->m_inHibernationWakeWindow) { + this->handleWakeWindowTick(); + } + // Update telemetry this->tlmWrite_CurrentMode(static_cast(this->m_mode)); } 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 - this->log_WARNING_HI_ExternalFaultDetected(); - + // Only allowed from NORMAL (sequential +1/-1 transitions) 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 } Components::SystemMode ModeManager ::getMode_handler(FwIndexType portNum) { @@ -60,13 +76,36 @@ 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) - if (this->m_mode == SystemMode::NORMAL) { - this->enterSafeMode("Ground command"); + // Reject if in hibernation mode - must use EXIT_HIBERNATION instead + // This ensures proper cleanup of hibernation state (m_inHibernationWakeWindow, counters) + if (this->m_mode == SystemMode::HIBERNATION_MODE) { + Fw::LogStringArg cmdNameStr("FORCE_SAFE_MODE"); + Fw::LogStringArg reasonStr("Use EXIT_HIBERNATION to exit hibernation mode"); + this->log_WARNING_LO_CommandValidationFailed(cmdNameStr, reasonStr); + this->cmdResponse_out(opCode, cmdSeq, Fw::CmdResponse::VALIDATION_ERROR); + return; + } + + // 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); } @@ -87,6 +126,100 @@ 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); +} + +void ModeManager ::ENTER_HIBERNATION_cmdHandler(FwOpcodeType opCode, + U32 cmdSeq, + U32 sleepDurationSec, + U32 wakeDurationSec) { + // Command to enter hibernation mode - only allowed from SAFE_MODE + + // Validate: must be in SAFE_MODE + if (this->m_mode != SystemMode::SAFE_MODE) { + Fw::LogStringArg cmdNameStr("ENTER_HIBERNATION"); + Fw::LogStringArg reasonStr("Can only enter hibernation from safe mode"); + this->log_WARNING_LO_CommandValidationFailed(cmdNameStr, reasonStr); + this->cmdResponse_out(opCode, cmdSeq, Fw::CmdResponse::VALIDATION_ERROR); + return; + } + + // Use defaults if zero passed + U32 sleepSec = (sleepDurationSec == 0) ? 3600 : sleepDurationSec; // Default 60 min + U32 wakeSec = (wakeDurationSec == 0) ? 60 : wakeDurationSec; // Default 1 min + + // Send command response BEFORE entering hibernation + // enterHibernation -> enterDormantSleep -> sys_reboot() does not return on success + this->cmdResponse_out(opCode, cmdSeq, Fw::CmdResponse::OK); + + // Enter hibernation mode (does not return on success - system reboots) + this->enterHibernation(sleepSec, wakeSec, "Ground command"); +} + +void ModeManager ::EXIT_HIBERNATION_cmdHandler(FwOpcodeType opCode, U32 cmdSeq) { + // Command to exit hibernation mode + + // Validate: must be in HIBERNATION_MODE and in wake window + if (this->m_mode != SystemMode::HIBERNATION_MODE) { + Fw::LogStringArg cmdNameStr("EXIT_HIBERNATION"); + Fw::LogStringArg reasonStr("Not currently in hibernation mode"); + this->log_WARNING_LO_CommandValidationFailed(cmdNameStr, reasonStr); + this->cmdResponse_out(opCode, cmdSeq, Fw::CmdResponse::VALIDATION_ERROR); + return; + } + + if (!this->m_inHibernationWakeWindow) { + Fw::LogStringArg cmdNameStr("EXIT_HIBERNATION"); + Fw::LogStringArg reasonStr("Not in wake window"); + this->log_WARNING_LO_CommandValidationFailed(cmdNameStr, reasonStr); + this->cmdResponse_out(opCode, cmdSeq, Fw::CmdResponse::VALIDATION_ERROR); + return; + } + + // Exit hibernation mode + this->exitHibernation(); + this->cmdResponse_out(opCode, cmdSeq, Fw::CmdResponse::OK); +} + // ---------------------------------------------------------------------- // Private helper methods // ---------------------------------------------------------------------- @@ -102,38 +235,94 @@ 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::SAFE_MODE)) { + // Validate state data before restoring (valid range: 0-3 for HIBERNATION, SAFE, NORMAL, PAYLOAD) + 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; + this->m_hibernationCycleCount = state.hibernationCycleCount; + this->m_hibernationTotalSeconds = state.hibernationTotalSeconds; + this->m_sleepDurationSec = state.sleepDurationSec; + this->m_wakeDurationSec = state.wakeDurationSec; // Restore physical hardware state to match loaded mode - if (this->m_mode == SystemMode::SAFE_MODE) { + if (this->m_mode == SystemMode::HIBERNATION_MODE) { + // Woke from dormant sleep - enter wake window + // Note: Hibernation counters (cycleCount, totalSeconds) were already + // incremented in enterDormantSleep() BEFORE the dormant sleep and saved + // to persistent storage. We're restoring those pre-incremented values. + // Keep all load switches OFF - we're in minimal power mode + this->turnOffNonCriticalComponents(); + + // Start wake window (radio is already initializing in Main.cpp) + // startWakeWindow() only logs an event and sets up state, no counter changes + this->startWakeWindow(); + } else if (this->m_mode == SystemMode::SAFE_MODE) { // Turn off non-critical components to match safe mode state this->turnOffNonCriticalComponents(); // Log that we're restoring safe mode (not entering it fresh) Fw::LogStringArg reasonStr("State restored from persistent storage"); this->log_WARNING_HI_EnteringSafeMode(reasonStr); + } else if (this->m_mode == SystemMode::PAYLOAD_MODE) { + // PAYLOAD_MODE - turn on face switches AND payload switches + this->turnOnComponents(); // Face switches (0-5) + this->turnOnPayload(); // Payload switches (6-7) + + // Log that we're restoring payload mode + Fw::LogStringArg reasonStr("State restored from persistent storage"); + this->log_ACTIVITY_HI_EnteringPayloadMode(reasonStr); } else { // NORMAL mode - ensure components are turned on this->turnOnComponents(); } } else { - // Corrupted state - use defaults - this->m_mode = SystemMode::NORMAL; - this->m_safeModeEntryCount = 0; - this->turnOnComponents(); + // Corrupted state (invalid mode value) - default to SAFE_MODE + // This prevents violating power constraints if system was in hibernation + Fw::LogStringArg reasonStr("Corrupted state file (invalid mode value)"); + this->log_WARNING_HI_StateRestorationFailed(reasonStr); + + this->m_mode = SystemMode::SAFE_MODE; + this->m_safeModeEntryCount = 1; // Count this as a safe mode entry + this->m_payloadModeEntryCount = 0; + this->m_hibernationCycleCount = 0; + this->m_hibernationTotalSeconds = 0; + this->m_sleepDurationSec = 3600; + this->m_wakeDurationSec = 60; + this->turnOffNonCriticalComponents(); } + } else { + // Read failed or size mismatch (possibly old struct version) + // Default to SAFE_MODE to maintain conservative power profile + Fw::LogStringArg reasonStr("State file read failed or size mismatch"); + this->log_WARNING_HI_StateRestorationFailed(reasonStr); + + this->m_mode = SystemMode::SAFE_MODE; + this->m_safeModeEntryCount = 1; // Count this as a safe mode entry + this->m_payloadModeEntryCount = 0; + this->m_hibernationCycleCount = 0; + this->m_hibernationTotalSeconds = 0; + this->m_sleepDurationSec = 3600; + this->m_wakeDurationSec = 60; + this->turnOffNonCriticalComponents(); } file.close(); } else { - // File doesn't exist or can't be opened - initialize to default state - this->m_mode = SystemMode::NORMAL; - this->m_safeModeEntryCount = 0; - this->turnOnComponents(); + // File doesn't exist or can't be opened - default to SAFE_MODE + // This is appropriate for first boot and maintains conservative power profile + Fw::LogStringArg reasonStr("State file not found or cannot be opened"); + this->log_WARNING_HI_StateRestorationFailed(reasonStr); + + this->m_mode = SystemMode::SAFE_MODE; + this->m_safeModeEntryCount = 1; // Count this as a safe mode entry + this->m_payloadModeEntryCount = 0; + this->m_hibernationCycleCount = 0; + this->m_hibernationTotalSeconds = 0; + this->m_sleepDurationSec = 3600; + this->m_wakeDurationSec = 60; + this->turnOffNonCriticalComponents(); } } @@ -151,6 +340,11 @@ void ModeManager ::saveState() { PersistentState state; state.mode = static_cast(this->m_mode); state.safeModeEntryCount = this->m_safeModeEntryCount; + state.payloadModeEntryCount = this->m_payloadModeEntryCount; + state.hibernationCycleCount = this->m_hibernationCycleCount; + state.hibernationTotalSeconds = this->m_hibernationTotalSeconds; + state.sleepDurationSec = this->m_sleepDurationSec; + state.wakeDurationSec = this->m_wakeDurationSec; FwSizeType bytesToWrite = sizeof(PersistentState); FwSizeType bytesWritten = bytesToWrite; @@ -223,6 +417,66 @@ 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(); + + // 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)); + + // 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 +494,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)) { @@ -264,4 +539,136 @@ F32 ModeManager ::getCurrentVoltage(bool& valid) { return 0.0f; } +void ModeManager ::enterHibernation(U32 sleepDurationSec, U32 wakeDurationSec, const char* reason) { + // Transition to hibernation mode + this->m_mode = SystemMode::HIBERNATION_MODE; + this->m_inHibernationWakeWindow = false; + this->m_sleepDurationSec = sleepDurationSec; + this->m_wakeDurationSec = wakeDurationSec; + + // Build reason string + Fw::LogStringArg reasonStr; + if (reason != nullptr) { + reasonStr = reason; + } else { + reasonStr = "Unknown"; + } + + // Log entering hibernation with parameters + this->log_WARNING_HI_EnteringHibernation(reasonStr, sleepDurationSec, wakeDurationSec); + + // Turn off ALL load switches (0-7) - minimal power mode + this->turnOffNonCriticalComponents(); + + // Update telemetry + this->tlmWrite_CurrentMode(static_cast(this->m_mode)); + this->tlmWrite_HibernationCycleCount(this->m_hibernationCycleCount); + this->tlmWrite_HibernationTotalSeconds(this->m_hibernationTotalSeconds); + + // Notify other components of mode change + if (this->isConnected_modeChanged_OutputPort(0)) { + Components::SystemMode fppMode = static_cast(this->m_mode); + this->modeChanged_out(0, fppMode); + } + + // Save state before dormant sleep (includes sleep/wake durations for resume) + this->saveState(); + + // Enter dormant sleep - this does NOT return on success + this->enterDormantSleep(); +} + +void ModeManager ::exitHibernation() { + // Transition from hibernation to safe mode + this->m_mode = SystemMode::SAFE_MODE; + this->m_inHibernationWakeWindow = false; + + // Log exit with statistics + this->log_ACTIVITY_HI_ExitingHibernation(this->m_hibernationCycleCount, this->m_hibernationTotalSeconds); + + // Update telemetry + this->tlmWrite_CurrentMode(static_cast(this->m_mode)); + + // Notify other components of mode change + if (this->isConnected_modeChanged_OutputPort(0)) { + Components::SystemMode fppMode = static_cast(this->m_mode); + this->modeChanged_out(0, fppMode); + } + + // Save state (now in SAFE_MODE) + this->saveState(); +} + +void ModeManager ::enterDormantSleep() { + // Increment counters BEFORE sleep (in case of unexpected wake) + this->m_hibernationCycleCount++; + this->m_hibernationTotalSeconds += this->m_sleepDurationSec; + + // Log that we're starting a sleep cycle + this->log_ACTIVITY_LO_HibernationSleepCycle(this->m_hibernationCycleCount); + + // Update telemetry before sleep + this->tlmWrite_HibernationCycleCount(this->m_hibernationCycleCount); + this->tlmWrite_HibernationTotalSeconds(this->m_hibernationTotalSeconds); + + // Save updated counters + this->saveState(); + + // Use Pico SDK to enter dormant mode with AON timer alarm + // On RP2350: + // - Returns true: Successfully woke from AON timer alarm + // - Returns false: Dormant mode entry failed (native/sim or hardware error) + // - Does not return: sys_reboot fallback was used (loadState handles wake) + bool success = PicoSleep::sleepForSeconds(this->m_sleepDurationSec); + + if (success) { + // Successfully woke from AON timer dormant mode! + // Start the wake window - same behavior as reboot-based wake + this->startWakeWindow(); + } else { + // Dormant mode entry failed + // Roll back counters since sleep didn't actually occur + this->m_hibernationCycleCount--; + this->m_hibernationTotalSeconds -= this->m_sleepDurationSec; + + // Update telemetry with corrected values + this->tlmWrite_HibernationCycleCount(this->m_hibernationCycleCount); + this->tlmWrite_HibernationTotalSeconds(this->m_hibernationTotalSeconds); + + // Log failure with HIGH severity - ground already saw OK response! + // This is the only way ground knows the command actually failed + Fw::LogStringArg reasonStr("Dormant mode entry failed - hardware or AON timer error"); + this->log_WARNING_HI_HibernationEntryFailed(reasonStr); + + // Exit hibernation mode since we couldn't enter dormant + // (this will save state with corrected counters) + this->exitHibernation(); + } +} + +void ModeManager ::startWakeWindow() { + this->m_inHibernationWakeWindow = true; + this->m_wakeWindowCounter = 0; + + // Log that we're in a wake window + this->log_ACTIVITY_LO_HibernationWakeWindow(this->m_hibernationCycleCount); + + // Update telemetry + this->tlmWrite_HibernationCycleCount(this->m_hibernationCycleCount); + this->tlmWrite_HibernationTotalSeconds(this->m_hibernationTotalSeconds); +} + +void ModeManager ::handleWakeWindowTick() { + // Called from run_handler at 1Hz when in hibernation wake window + this->m_wakeWindowCounter++; + + if (this->m_wakeWindowCounter >= this->m_wakeDurationSec) { + // Wake window elapsed, no EXIT_HIBERNATION received + // Go back to dormant sleep + this->m_inHibernationWakeWindow = false; + this->enterDormantSleep(); + } + // Otherwise, continue listening for EXIT_HIBERNATION command +} + } // namespace Components diff --git a/FprimeZephyrReference/Components/ModeManager/ModeManager.fpp b/FprimeZephyrReference/Components/ModeManager/ModeManager.fpp index 5f9fe612..41f0661a 100644 --- a/FprimeZephyrReference/Components/ModeManager/ModeManager.fpp +++ b/FprimeZephyrReference/Components/ModeManager/ModeManager.fpp @@ -2,8 +2,10 @@ module Components { @ System mode enumeration enum SystemMode { - NORMAL = 0 @< Normal operational mode + HIBERNATION_MODE = 0 @< Ultra-low-power hibernation with periodic wake windows SAFE_MODE = 1 @< Safe mode with non-critical components powered off + NORMAL = 2 @< Normal operational mode + PAYLOAD_MODE = 3 @< Payload mode with payload power and battery enabled } @ Port for notifying about mode changes @@ -57,6 +59,28 @@ 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() + + @ Command to enter hibernation mode (only from SAFE_MODE) + @ Uses RP2350 dormant mode with RTC alarm wake + @ sleepDurationSec: Duration of each sleep cycle in seconds (default 3600 = 60 min) + @ wakeDurationSec: Duration of each wake window in seconds (default 60 = 1 min) + sync command ENTER_HIBERNATION( + sleepDurationSec: U32 @< Sleep cycle duration in seconds (0 = default 3600) + wakeDurationSec: U32 @< Wake window duration in seconds (0 = default 60) + ) + + @ Command to exit hibernation mode + @ Only succeeds if currently in hibernation mode wake window + @ Transitions to SAFE_MODE + sync command EXIT_HIBERNATION() + # ---------------------------------------------------------------------- # Events # ---------------------------------------------------------------------- @@ -83,6 +107,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( @@ -100,6 +140,55 @@ module Components { severity warning low \ format "State persistence {} failed with status {}" + @ Event emitted when entering hibernation mode + event EnteringHibernation( + reason: string size 100 @< Reason for entering hibernation + sleepDurationSec: U32 @< Sleep cycle duration in seconds + wakeDurationSec: U32 @< Wake window duration in seconds + ) \ + severity warning high \ + format "ENTERING HIBERNATION: {} (sleep={}s, wake={}s)" + + @ Event emitted when exiting hibernation mode + event ExitingHibernation( + cycleCount: U32 @< Total hibernation cycles completed + totalSeconds: U32 @< Total time spent in hibernation + ) \ + severity activity high \ + format "Exiting hibernation after {} cycles ({}s total)" + + @ Event emitted when hibernation wake window starts + event HibernationWakeWindow( + cycleNumber: U32 @< Current wake cycle number + ) \ + severity activity low \ + format "Hibernation wake window #{}" + + @ Event emitted when starting a new hibernation sleep cycle + event HibernationSleepCycle( + cycleNumber: U32 @< Sleep cycle number starting + ) \ + severity activity low \ + format "Hibernation sleep cycle #{} starting" + + @ Event emitted when hibernation dormant sleep entry fails + @ CRITICAL: Ground already received OK response before this failure + @ Mode reverts to SAFE_MODE, counters are rolled back + event HibernationEntryFailed( + reason: string size 100 @< Reason for failure + ) \ + severity warning high \ + format "HIBERNATION ENTRY FAILED (command ack'd OK but dormant failed): {}" + + @ Event emitted when state restoration fails on boot + @ CRITICAL: System defaults to SAFE_MODE to maintain conservative power profile + @ This prevents violating power constraints if system was in hibernation + event StateRestorationFailed( + reason: string size 100 @< Description of the failure + ) \ + severity warning high \ + format "STATE RESTORATION FAILED - defaulting to SAFE_MODE: {}" + # ---------------------------------------------------------------------- # Telemetry # ---------------------------------------------------------------------- @@ -110,6 +199,15 @@ module Components { @ Number of times safe mode has been entered telemetry SafeModeEntryCount: U32 + @ Number of times payload mode has been entered + telemetry PayloadModeEntryCount: U32 + + @ Number of hibernation sleep/wake cycles completed + telemetry HibernationCycleCount: U32 + + @ Total time spent in hibernation (seconds) + telemetry HibernationTotalSeconds: 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..5ffb22b2 100644 --- a/FprimeZephyrReference/Components/ModeManager/ModeManager.hpp +++ b/FprimeZephyrReference/Components/ModeManager/ModeManager.hpp @@ -70,6 +70,28 @@ 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; + + //! Handler implementation for command ENTER_HIBERNATION + void ENTER_HIBERNATION_cmdHandler(FwOpcodeType opCode, //!< The opcode + U32 cmdSeq, //!< The command sequence number + U32 sleepDurationSec, //!< Sleep cycle duration in seconds + U32 wakeDurationSec //!< Wake window duration in seconds + ) override; + + //! Handler implementation for command EXIT_HIBERNATION + void EXIT_HIBERNATION_cmdHandler(FwOpcodeType opCode, //!< The opcode + U32 cmdSeq //!< The command sequence number + ) override; + private: // ---------------------------------------------------------------------- // Private helper methods @@ -87,9 +109,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(); @@ -99,26 +133,63 @@ class ModeManager : public ModeManagerComponentBase { //! \return Current voltage (only valid if valid parameter is set to true) F32 getCurrentVoltage(bool& valid); + //! Enter hibernation mode with configurable durations + //! \param sleepDurationSec Duration of each sleep cycle in seconds + //! \param wakeDurationSec Duration of each wake window in seconds + //! \param reason Reason for entering hibernation + void enterHibernation(U32 sleepDurationSec, U32 wakeDurationSec, const char* reason = nullptr); + + //! Exit hibernation mode (transitions to SAFE_MODE) + void exitHibernation(); + + //! Enter dormant sleep (calls PicoSleep, does not return on success) + void enterDormantSleep(); + + //! Start wake window after dormant wake + void startWakeWindow(); + + //! Handle wake window tick (called from run_handler at 1Hz) + void handleWakeWindowTick(); + // ---------------------------------------------------------------------- // Private enums and types // ---------------------------------------------------------------------- //! System mode enumeration - enum class SystemMode : U8 { NORMAL = 0, SAFE_MODE = 1 }; + enum class SystemMode : U8 { + HIBERNATION_MODE = 0, //!< Ultra-low-power hibernation with periodic wake windows + SAFE_MODE = 1, //!< Safe mode with non-critical components powered off + NORMAL = 2, //!< Normal operational mode + PAYLOAD_MODE = 3 //!< Payload mode with payload power and battery enabled + }; - //! Persistent state structure + //! Persistent state structure (version 2 with hibernation support) 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 + U32 hibernationCycleCount; //!< Number of hibernation sleep/wake cycles + U32 hibernationTotalSeconds; //!< Total time spent in hibernation (seconds) + U32 sleepDurationSec; //!< Configured sleep cycle duration (for resume) + U32 wakeDurationSec; //!< Configured wake window duration (for resume) }; // ---------------------------------------------------------------------- // 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) + + // Hibernation state variables + bool m_inHibernationWakeWindow; //!< True if currently in wake window + U32 m_wakeWindowCounter; //!< Seconds elapsed in current wake window + U32 m_hibernationCycleCount; //!< Total hibernation cycles completed + U32 m_hibernationTotalSeconds; //!< Total seconds spent in hibernation + U32 m_sleepDurationSec; //!< Configured sleep cycle duration + U32 m_wakeDurationSec; //!< Configured wake window duration static constexpr const char* STATE_FILE_PATH = "/mode_state.bin"; //!< State file path }; diff --git a/FprimeZephyrReference/Components/ModeManager/PicoSleep.cpp b/FprimeZephyrReference/Components/ModeManager/PicoSleep.cpp new file mode 100644 index 00000000..55b9dccb --- /dev/null +++ b/FprimeZephyrReference/Components/ModeManager/PicoSleep.cpp @@ -0,0 +1,264 @@ +// ====================================================================== +// \title PicoSleep.cpp +// \brief Implementation of RP2350 dormant mode using AON Timer and POWMAN +// +// The RP2350 uses the Always-On (AON) Timer and Power Manager (POWMAN) +// for low-power dormant mode, unlike the RP2040 which uses the RTC. +// The AON timer runs from the Low Power Oscillator (LPOSC) which stays +// active during dormant mode. +// +// References: +// - Pico SDK pico_aon_timer library +// - Pico SDK hardware_powman library +// - pico-extras pico_sleep library +// ====================================================================== + +#include "FprimeZephyrReference/Components/ModeManager/PicoSleep.hpp" + +#include +#include + +// RP2350 Pico SDK includes for AON timer and power management +#if defined(CONFIG_SOC_RP2350) || defined(CONFIG_SOC_SERIES_RP2XXX) +#include +#include +#include +#include +#include +#include +#include +#include +#include +#endif + +namespace Components { + +// Flag to track if we should use dormant mode or fallback to reboot +// Known issue: RP2350 can halt after multiple wake-ups (pico-sdk #2376) +// Set to false to use safer sys_reboot fallback +static constexpr bool USE_DORMANT_MODE = true; + +// Track if AON timer has been initialized +static bool s_aonTimerInitialized = false; + +#if defined(CONFIG_SOC_RP2350) || defined(CONFIG_SOC_SERIES_RP2XXX) + +// Callback for AON timer alarm - does nothing, wakeup is automatic +static void aon_timer_alarm_callback(void) { + // Empty callback - the alarm firing wakes the processor from dormant +} + +// Ensure AON timer is initialized and running +// For continuous timing across reboots, check POWMAN_TIMER_RUN first +static bool ensureAonTimerRunning(void) { + // Check if POWMAN timer is already running (e.g., from previous boot) + // This is the "required operating procedure when you want continuous timing" +#if defined(POWMAN_TIMER_RUN_BITS) && defined(POWMAN_BASE) + if (powman_hw->timer & POWMAN_TIMER_RUN_BITS) { + s_aonTimerInitialized = true; + return true; + } +#endif + + // Timer not running, need to initialize it + // Start the AON timer with current time (0 for simplicity) + struct timespec ts = {0, 0}; +#ifdef PICO_AON_TIMER_H + aon_timer_start(&ts); + // Wait a bit for timer to stabilize (per pico-sdk #2148) + k_busy_wait(100); +#else + // AON timer not available; cannot start timer + return false; +#endif + + s_aonTimerInitialized = true; + return true; +} + +// Switch clocks to run from ROSC for dormant mode +// ROSC stays on during dormant, XOSC is stopped +static void sleep_run_from_rosc(void) { + // Switch clk_ref to use ROSC (Ring Oscillator) + // ROSC runs at ~6.5MHz and stays on during dormant + hw_write_masked(&clocks_hw->clk[clk_ref].ctrl, + CLOCKS_CLK_REF_CTRL_SRC_VALUE_ROSC_CLKSRC_PH << CLOCKS_CLK_REF_CTRL_SRC_LSB, + CLOCKS_CLK_REF_CTRL_SRC_BITS); + + // Wait for clock switch to complete + while (!(clocks_hw->clk[clk_ref].selected & (1u << CLOCKS_CLK_REF_CTRL_SRC_VALUE_ROSC_CLKSRC_PH))) { + tight_loop_contents(); + } + + // Switch clk_sys to use clk_ref (which is now ROSC) + hw_write_masked(&clocks_hw->clk[clk_sys].ctrl, CLOCKS_CLK_SYS_CTRL_SRC_VALUE_CLK_REF << CLOCKS_CLK_SYS_CTRL_SRC_LSB, + CLOCKS_CLK_SYS_CTRL_SRC_BITS); + + // Wait for clock switch to complete + while (!(clocks_hw->clk[clk_sys].selected & (1u << CLOCKS_CLK_SYS_CTRL_SRC_VALUE_CLK_REF))) { + tight_loop_contents(); + } +} + +// Restore clocks after waking from dormant +static void sleep_power_up(void) { + // Re-enable the XOSC + xosc_init(); + + // Switch clk_ref back to XOSC + hw_write_masked(&clocks_hw->clk[clk_ref].ctrl, + CLOCKS_CLK_REF_CTRL_SRC_VALUE_XOSC_CLKSRC << CLOCKS_CLK_REF_CTRL_SRC_LSB, + CLOCKS_CLK_REF_CTRL_SRC_BITS); + + // Wait for clock switch to complete + while (!(clocks_hw->clk[clk_ref].selected & (1u << CLOCKS_CLK_REF_CTRL_SRC_VALUE_XOSC_CLKSRC))) { + tight_loop_contents(); + } + + // Note: Full PLL/clock restoration is handled by Zephyr or requires + // calling clocks_init() which may conflict with Zephyr's clock setup. + // For now, we do a system reboot after wake to ensure clean state. +} + +// Enter dormant mode - processor will halt until AON timer alarm +static void go_dormant(void) { + // Enable deep sleep in the processor (Cortex-M33) + scb_hw->scr |= M33_SCR_SLEEPDEEP_BITS; + + // Disable all clocks except those needed for dormant wake + // On RP2350, CLK_REF_POWMAN needs to stay enabled for AON timer +#ifdef PICO_SDK_PRESENT + clocks_hw->sleep_en0 = CLOCKS_SLEEP_EN0_CLK_REF_POWMAN_BITS; + clocks_hw->sleep_en1 = 0; +#else + // Zephyr build: clocks_hw not available, skipping direct register access +#endif + + // Wait for interrupt - processor enters dormant mode + // Will wake when AON timer alarm fires + __wfi(); + + // Restore clocks after wakeup +#ifdef PICO_SDK_PRESENT + clocks_hw->sleep_en0 = 0xFFFFFFFF; + clocks_hw->sleep_en1 = 0xFFFFFFFF; +#else + // Zephyr build: clocks_hw not available, skipping direct register access +#endif + + // Clear deep sleep bit + scb_hw->scr &= ~M33_SCR_SLEEPDEEP_BITS; +} + +#endif // CONFIG_SOC_RP2350 + +bool PicoSleep::sleepForSeconds(U32 seconds) { +#if defined(CONFIG_BOARD_NATIVE_POSIX) || defined(CONFIG_BOARD_NATIVE_SIM) || defined(CONFIG_ARCH_POSIX) + // Native/simulation builds: Return false to exercise the failure handling path + // This allows CI to test HibernationEntryFailed event emission, counter rollback, + // and mode reversion to SAFE_MODE without actually rebooting. + (void)seconds; + return false; + +#elif defined(CONFIG_SOC_RP2350) || defined(CONFIG_SOC_SERIES_RP2XXX) + // RP2350: Use AON timer for proper dormant mode with timer wakeup + // + // The AON timer uses the Low Power Oscillator (LPOSC) which runs at ~32kHz + // and stays active during dormant mode. When the alarm fires, the processor + // wakes and execution continues after __wfi(). + // + // NOTE: There's a known issue (pico-sdk #2376) where RP2350 can halt after + // multiple dormant wake cycles. If USE_DORMANT_MODE is false, we fall back + // to sys_reboot which is more reliable but uses more power. + + if (!USE_DORMANT_MODE) { + // Fallback: Use cold reboot instead of dormant + // State file will indicate hibernation mode on next boot + sys_reboot(SYS_REBOOT_COLD); + return false; // Never reached + } + + // Ensure AON timer is initialized and running + if (!ensureAonTimerRunning()) { + // Failed to initialize AON timer - fall back to reboot + sys_reboot(SYS_REBOOT_COLD); + return false; + } + + // Get current time from AON timer + struct timespec ts; +#ifdef PICO_SDK_PRESENT + if (!aon_timer_get_time(&ts)) { + // AON timer not working - fall back to reboot + sys_reboot(SYS_REBOOT_COLD); + return false; + } +#else + // Pico SDK not available - cannot get AON timer time, fall back to reboot + sys_reboot(SYS_REBOOT_COLD); + return false; +#endif + + // Set wakeup time + ts.tv_sec += seconds; + + // Configure power manager timer to use LPOSC (stays on during dormant) + uint64_t current_ms = powman_timer_get_ms(); + powman_timer_set_1khz_tick_source_lposc(); + powman_timer_set_ms(current_ms); + + // Switch to running from ROSC (stays on during dormant, XOSC will stop) + sleep_run_from_rosc(); + + // Set AON timer alarm for wakeup + // The alarm will fire and wake the processor from dormant +#if defined(aon_timer_enable_alarm) + aon_timer_enable_alarm(&ts, aon_timer_alarm_callback, true); +#else + // AON timer alarm not available; fall back to reboot + sys_reboot(SYS_REBOOT_COLD); + return false; +#endif + + // Enter dormant mode - execution stops here until alarm + go_dormant(); + + // If we get here, we woke up successfully from dormant! +#if defined(aon_timer_disable_alarm) + aon_timer_disable_alarm(); +#endif + + // Restore clocks (partial - PLLs require full init) + sleep_power_up(); + + // Due to complexity of restoring full Zephyr clock state, + // we do a warm reboot to ensure clean state + // The state file still has HIBERNATION_MODE, so loadState() will + // detect this and start the wake window + sys_reboot(SYS_REBOOT_WARM); + + return false; // Never reached (reboot above) + +#else + // Unknown platform - use sys_reboot as fallback + (void)seconds; + sys_reboot(SYS_REBOOT_COLD); + return false; + +#endif +} + +bool PicoSleep::isSupported() { +#if defined(CONFIG_BOARD_NATIVE_POSIX) || defined(CONFIG_BOARD_NATIVE_SIM) || defined(CONFIG_ARCH_POSIX) + // Native/simulation: dormant mode is not supported (returns false to exercise failure path) + return false; +#elif defined(CONFIG_SOC_RP2350) || defined(CONFIG_SOC_SERIES_RP2XXX) + // RP2350 supports dormant mode via AON timer and POWMAN + return USE_DORMANT_MODE; +#else + // Unknown platform + return false; +#endif +} + +} // namespace Components diff --git a/FprimeZephyrReference/Components/ModeManager/PicoSleep.hpp b/FprimeZephyrReference/Components/ModeManager/PicoSleep.hpp new file mode 100644 index 00000000..49f7463e --- /dev/null +++ b/FprimeZephyrReference/Components/ModeManager/PicoSleep.hpp @@ -0,0 +1,68 @@ +// ====================================================================== +// \title PicoSleep.hpp +// \brief Wrapper for RP2350 dormant mode using AON Timer and POWMAN +// ====================================================================== + +#ifndef Components_PicoSleep_HPP +#define Components_PicoSleep_HPP + +#include "Fw/Types/BasicTypes.hpp" + +namespace Components { + +/** + * @brief Wrapper class for RP2350 Pico SDK dormant mode functionality + * + * The RP2350 uses the Always-On (AON) Timer and Power Manager (POWMAN) + * for ultra-low-power dormant mode. Unlike the RP2040's RTC-based sleep, + * the RP2350's AON timer runs from the Low Power Oscillator (LPOSC) which + * stays active during dormant mode (~32kHz, less precise than XOSC). + * + * Power consumption in dormant mode: + * - Dormant with AON timer: ~3mA + * - POWMAN deep sleep: ~0.65-0.85mA + * + * IMPORTANT: On RP2350, sleepForSeconds() CAN return (unlike RP2040). + * When the AON timer alarm fires, the processor wakes and execution + * continues. If dormant entry fails or is disabled, falls back to + * sys_reboot() for reliability. + * + * Known issue: RP2350 can halt after multiple dormant wake cycles + * (see pico-sdk issue #2376). Set USE_DORMANT_MODE to false in + * PicoSleep.cpp to use safer sys_reboot fallback. + */ +class PicoSleep { + public: + /** + * @brief Enter dormant mode for specified duration + * + * Configures the AON timer alarm and enters RP2350 dormant mode. + * The processor halts with only LPOSC running. When the alarm fires, + * execution resumes and the function returns true. + * + * If dormant mode is not available or fails, falls back to sys_reboot() + * which does not return. + * + * @param seconds Duration to sleep in seconds + * @return true if woke from dormant successfully + * @return false if dormant mode entry failed (native/sim builds) + * @note On sys_reboot fallback, function does not return + */ + static bool sleepForSeconds(U32 seconds); + + /** + * @brief Check if dormant mode is supported on this platform + * + * @return true if AON timer dormant mode is available (RP2350) + * @return false on native/sim builds or if USE_DORMANT_MODE is disabled + */ + static bool isSupported(); + + private: + // Prevent instantiation + PicoSleep() = delete; +}; + +} // namespace Components + +#endif diff --git a/FprimeZephyrReference/Components/ModeManager/docs/sdd.md b/FprimeZephyrReference/Components/ModeManager/docs/sdd.md index d3233d6e..ba7ac78f 100644 --- a/FprimeZephyrReference/Components/ModeManager/docs/sdd.md +++ b/FprimeZephyrReference/Components/ModeManager/docs/sdd.md @@ -1,30 +1,39 @@ # 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. - -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. +The ModeManager component manages system operational modes and orchestrates transitions across HIBERNATION_MODE, SAFE_MODE, NORMAL, 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. ## Requirements | Name | Description | Validation | |---|---|---| -| MM0001 | The ModeManager shall maintain two distinct operational modes: NORMAL and SAFE_MODE | Integration Testing | +| MM0001 | The ModeManager shall maintain four operational modes: HIBERNATION_MODE, SAFE_MODE, NORMAL, 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 | | 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 | | MM0011 | The ModeManager shall allow downstream components to query the current mode via getMode port | Unit Testing | | MM0012 | The ModeManager shall notify downstream components of mode changes with the new mode value | Unit Testing | +| MM0013 | The ModeManager shall enter payload mode when commanded via ENTER_PAYLOAD_MODE while in NORMAL and reject entry from SAFE_MODE | Integration Testing | +| MM0014 | The ModeManager shall exit payload mode only via explicit EXIT_PAYLOAD_MODE command and reject exit when not in payload mode | Integration Testing | +| MM0016 | The ModeManager shall turn on payload load switches (indices 6 and 7) when entering payload mode and turn them off when exiting payload mode | Integration Testing | +| MM0017 | The ModeManager shall track and report the number of times payload mode has been entered | Integration Testing | +| MM0018 | The ModeManager shall persist payload mode state and payload mode entry count to non-volatile storage and restore them on initialization | Integration Testing | +| MM0019 | The ModeManager shall reject FORCE_SAFE_MODE from payload mode (must exit payload mode first for sequential transitions) | Integration Testing | +| MM0020 | The ModeManager shall enter hibernation mode only from SAFE_MODE via ENTER_HIBERNATION command | Integration Testing | +| MM0021 | The ModeManager shall use RP2350 dormant mode with RTC alarm for ultra-low-power hibernation sleep cycles | Hardware Testing | +| MM0022 | The ModeManager shall support configurable sleep duration (default 3600s/60min) and wake window duration (default 60s/1min) | Integration Testing | +| MM0023 | The ModeManager shall track hibernation cycle count and total hibernation seconds (persisted across reboots) | Integration Testing | +| MM0024 | The ModeManager shall exit hibernation only via EXIT_HIBERNATION command during a wake window | Integration Testing | +| MM0025 | The ModeManager shall transition to SAFE_MODE when exiting hibernation | Integration Testing | +| MM0026 | The ModeManager shall emit HibernationWakeWindow event at start of each wake window | Integration Testing | +| MM0027 | The ModeManager shall emit HibernationSleepCycle event before entering each dormant sleep | Integration Testing | +| MM0028 | The ModeManager shall persist hibernation configuration (sleep/wake durations) across reboots to resume hibernation | Integration Testing | +| MM0029 | The ModeManager shall roll back hibernation counters if dormant sleep entry fails | Unit Testing | +| MM0030 | The ModeManager shall send command response BEFORE entering dormant sleep (since reboot prevents response) | Integration Testing | ## Usage Examples @@ -35,15 +44,17 @@ The ModeManager component operates as an active component that manages system-wi 1. **System Initialization** - Component is instantiated during system startup - Loads previous mode state from `/mode_state.bin` + - If resuming from hibernation, automatically starts wake window - Configures load switches to match restored mode - Begins 1Hz periodic execution via rate group 2. **Normal Operation** - - Updates telemetry channels (CurrentMode, SafeModeEntryCount) + - Updates telemetry channels (CurrentMode, SafeModeEntryCount, PayloadModeEntryCount, HibernationCycleCount, HibernationTotalSeconds) - 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: + - Can be triggered by (only from NORMAL mode - sequential transitions enforced): - Ground command: `FORCE_SAFE_MODE` - External component request via `forceSafeMode` port - Actions performed: @@ -54,19 +65,73 @@ 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** + - Triggered by ground command: `ENTER_PAYLOAD_MODE` (only allowed from NORMAL) + - Actions performed: + - Transitions mode to PAYLOAD_MODE + - Increments payload mode entry counter + - Emits `EnteringPayloadMode` event and, for commands, `ManualPayloadModeEntry` + - Turns on payload load switches (indices 6 and 7) + - Notifies downstream components via `modeChanged` port + - Updates telemetry (CurrentMode, PayloadModeEntryCount) + - Persists state to flash storage + +5. **Payload Mode Exit** + - Triggered by ground command: `EXIT_PAYLOAD_MODE` + - Validates currently in payload mode before allowing exit + - 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: - 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 -5. **Mode Queries** - - Downstream components can call `getMode` port to query current mode - - Returns immediate synchronous response with current mode +7. **Hibernation Mode Entry** + - Triggered by ground command: `ENTER_HIBERNATION sleepDurationSec wakeDurationSec` + - Only allowed from SAFE_MODE + - Actions performed: + - Validates currently in SAFE_MODE + - Sends command response OK (BEFORE entering dormant - critical since reboot prevents response) + - Emits `EnteringHibernation` event with reason and durations + - Sets mode to HIBERNATION_MODE + - Saves state to flash (including sleep/wake durations for resume) + - Emits `HibernationSleepCycle` event + - Enters RP2350 dormant mode with RTC alarm set for sleepDurationSec + - System reboots when RTC alarm fires + +8. **Hibernation Wake Window** + - On reboot after dormant wake: + - Loads state from flash, detects HIBERNATION_MODE + - Increments hibernation cycle count and total seconds + - Emits `HibernationWakeWindow` event + - Starts wake window counter (1Hz ticks) + - Only LoRa radio active, listens for EXIT_HIBERNATION command + - At end of wake window (counter reaches wakeDurationSec): + - Automatically re-enters dormant sleep for next cycle + +9. **Hibernation Mode Exit** + - Triggered by ground command: `EXIT_HIBERNATION` during wake window + - Actions performed: + - Validates currently in HIBERNATION_MODE wake window + - Emits `ExitingHibernation` event with cycle count and total time + - Transitions to SAFE_MODE + - Persists state to flash storage + - Ground can then issue EXIT_SAFE_MODE to return to NORMAL + +10. **Mode Queries** + - Downstream components can call `getMode` port to query current mode + - Returns immediate synchronous response with current mode ## Class Diagram @@ -80,7 +145,14 @@ classDiagram <> - m_mode: SystemMode - m_safeModeEntryCount: U32 + - m_payloadModeEntryCount: U32 - m_runCounter: U32 + - m_inHibernationWakeWindow: bool + - m_wakeWindowCounter: U32 + - m_hibernationCycleCount: U32 + - m_hibernationTotalSeconds: U32 + - m_sleepDurationSec: U32 + - m_wakeDurationSec: U32 - STATE_FILE_PATH: const char* + ModeManager(const char* compName) + ~ModeManager() @@ -90,18 +162,33 @@ classDiagram - 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) + - ENTER_HIBERNATION_cmdHandler(FwOpcodeType opCode, U32 cmdSeq, U32 sleepDurationSec, U32 wakeDurationSec) + - EXIT_HIBERNATION_cmdHandler(FwOpcodeType opCode, U32 cmdSeq) - loadState() - saveState() - enterSafeMode(const char* reason) - exitSafeMode() + - enterPayloadMode(const char* reason) + - exitPayloadMode() + - enterHibernation(U32 sleepDurationSec, U32 wakeDurationSec, const char* reason) + - exitHibernation() + - enterDormantSleep() + - startWakeWindow() + - handleWakeWindowTick() - turnOffNonCriticalComponents() - turnOnComponents() + - turnOnPayload() + - turnOffPayload() - getCurrentVoltage(bool& valid): F32 } class SystemMode { <> - NORMAL = 0 + HIBERNATION_MODE = 0 SAFE_MODE = 1 + NORMAL = 2 + PAYLOAD_MODE = 3 } } ModeManagerComponentBase <|-- ModeManager : inherits @@ -113,7 +200,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 wake window countdown | | 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 | @@ -129,16 +216,28 @@ classDiagram | Name | Type | Description | |---|---|---| -| m_mode | SystemMode | Current operational mode (NORMAL or SAFE_MODE) | +| m_mode | SystemMode | Current operational mode (HIBERNATION_MODE, SAFE_MODE, NORMAL, 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 | +| m_inHibernationWakeWindow | bool | True when in hibernation wake window, false otherwise | +| m_wakeWindowCounter | U32 | Seconds elapsed in current wake window (counts up to wakeDurationSec) | +| m_hibernationCycleCount | U32 | Total number of hibernation sleep/wake cycles completed | +| m_hibernationTotalSeconds | U32 | Total seconds spent in hibernation sleep | +| m_sleepDurationSec | U32 | Configured sleep cycle duration (default 3600s = 60min) | +| m_wakeDurationSec | U32 | Configured wake window duration (default 60s = 1min) | ### 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) +- Hibernation cycle count (U32) +- Hibernation total seconds (U32) +- Sleep duration seconds (U32) - for hibernation resume +- Wake duration seconds (U32) - for hibernation resume -This state is loaded on initialization and saved on every mode transition. +This state is loaded on initialization and saved on every mode transition. The hibernation configuration (sleep/wake durations) are persisted to enable resumption of hibernation cycles after each dormant wake. ## Sequence Diagrams @@ -196,13 +295,127 @@ 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 + 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 ``` +### Hibernation Mode Entry (Command) +```mermaid +sequenceDiagram + participant Ground + participant ModeManager + participant FlashStorage + participant PicoSleep + participant RP2350 + + Ground->>ModeManager: ENTER_HIBERNATION(sleepSec, wakeSec) + ModeManager->>ModeManager: Validate currently in SAFE_MODE + ModeManager->>Ground: Command response OK (before dormant!) + ModeManager->>ModeManager: Emit EnteringHibernation event + ModeManager->>ModeManager: Set m_mode = HIBERNATION_MODE + ModeManager->>FlashStorage: Save state (mode, cycle count, durations) + ModeManager->>ModeManager: Emit HibernationSleepCycle event + ModeManager->>PicoSleep: sleepForSeconds(sleepDurationSec) + PicoSleep->>RP2350: Enter dormant mode with RTC alarm + Note over RP2350: System in ultra-low-power state + RP2350-->>RP2350: RTC alarm fires + RP2350->>RP2350: sys_reboot() - full system restart +``` + +### Hibernation Wake Window +```mermaid +sequenceDiagram + participant RP2350 + participant ModeManager + participant FlashStorage + participant RateGroup + + RP2350->>ModeManager: System boot (after dormant wake) + ModeManager->>FlashStorage: loadState() + FlashStorage-->>ModeManager: mode=HIBERNATION_MODE, durations + ModeManager->>ModeManager: Detect hibernation resume + ModeManager->>ModeManager: Increment m_hibernationCycleCount + ModeManager->>ModeManager: Add sleepDurationSec to m_hibernationTotalSeconds + ModeManager->>ModeManager: Emit HibernationWakeWindow event + ModeManager->>ModeManager: Set m_inHibernationWakeWindow = true + ModeManager->>ModeManager: Reset m_wakeWindowCounter = 0 + + loop Every 1 second (wake window) + RateGroup->>ModeManager: run_handler() + ModeManager->>ModeManager: Increment m_wakeWindowCounter + alt Counter >= wakeDurationSec + ModeManager->>ModeManager: enterDormantSleep() + Note over ModeManager: Re-enter dormant for next cycle + end + end +``` + +### Hibernation Mode Exit (Command) +```mermaid +sequenceDiagram + participant Ground + participant ModeManager + participant FlashStorage + participant DownstreamComponents + + Note over ModeManager: In wake window, listening for commands + Ground->>ModeManager: EXIT_HIBERNATION command + ModeManager->>ModeManager: Validate in HIBERNATION_MODE wake window + ModeManager->>ModeManager: Emit ExitingHibernation event (cycles, total time) + ModeManager->>ModeManager: Set m_mode = SAFE_MODE + ModeManager->>ModeManager: Set m_inHibernationWakeWindow = false + ModeManager->>ModeManager: Update telemetry + ModeManager->>DownstreamComponents: modeChanged_out(SAFE_MODE) + ModeManager->>FlashStorage: Save state to /mode_state.bin + ModeManager->>Ground: Command response OK + Note over Ground: Can now issue EXIT_SAFE_MODE +``` + ### Mode Query ```mermaid sequenceDiagram @@ -211,7 +424,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 (HIBERNATION, SAFE, NORMAL, or PAYLOAD) ``` ### Periodic Execution (1Hz) @@ -223,14 +436,25 @@ sequenceDiagram RateGroup->>ModeManager: run(portNum, context) ModeManager->>ModeManager: Increment m_runCounter ModeManager->>ModeManager: Write CurrentMode telemetry + alt In hibernation wake window + ModeManager->>ModeManager: handleWakeWindowTick() + ModeManager->>ModeManager: Increment m_wakeWindowCounter + alt Wake window expired + ModeManager->>ModeManager: enterDormantSleep() + end + end ``` ## Commands | 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. | +| ENTER_HIBERNATION | sleepDurationSec: U32
wakeDurationSec: U32 | Enters hibernation mode from SAFE_MODE. Uses RP2350 dormant mode with RTC alarm. sleepDurationSec=0 uses default 3600s (60min). wakeDurationSec=0 uses default 60s. Command response sent BEFORE entering dormant (since system reboots). Fails with CommandValidationFailed if not in SAFE_MODE. | +| EXIT_HIBERNATION | None | Exits hibernation mode during a wake window. Transitions to SAFE_MODE. Fails with CommandValidationFailed if not currently in hibernation wake window. | ## Events @@ -240,46 +464,107 @@ 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") | +| 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 | +| EnteringHibernation | WARNING_HI | reason: string size 100
sleepDurationSec: U32
wakeDurationSec: U32 | Emitted when entering hibernation mode, includes reason and configured durations | +| ExitingHibernation | ACTIVITY_HI | cycleCount: U32
totalSeconds: U32 | Emitted when exiting hibernation, includes total cycles completed and time spent | +| HibernationWakeWindow | ACTIVITY_LO | cycleNumber: U32 | Emitted at start of each wake window after dormant wake | +| HibernationSleepCycle | ACTIVITY_LO | cycleNumber: U32 | Emitted before entering each dormant sleep cycle | +| HibernationEntryFailed | WARNING_HI | reason: string size 100 | **CRITICAL:** Emitted when dormant sleep entry fails AFTER command was acked OK. Ground sees OK response but system is actually in SAFE_MODE. Counters rolled back. | +| StateRestorationFailed | WARNING_HI | reason: string size 100 | **CRITICAL:** Emitted when state file cannot be read or is corrupted on boot. System defaults to SAFE_MODE to maintain conservative power profile. | ## Telemetry | Name | Type | Update Rate | Description | |---|---|---|---| -| CurrentMode | U8 | 1Hz | Current system mode (0 = NORMAL, 1 = SAFE_MODE) | +| CurrentMode | U8 | 1Hz | Current system mode (0=HIBERNATION, 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) | +| HibernationCycleCount | U32 | On change | Number of hibernation sleep/wake cycles completed (persists across reboots) | +| HibernationTotalSeconds | U32 | On change | Total time spent in hibernation sleep in seconds (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 | HIBERNATION State | SAFE_MODE State | NORMAL State | PAYLOAD_MODE State | +|---|---|---|---|---|---| +| 0 | Satellite Face 0 | OFF | OFF | ON | ON | +| 1 | Satellite Face 1 | OFF | OFF | ON | ON | +| 2 | Satellite Face 2 | OFF | OFF | ON | ON | +| 3 | Satellite Face 3 | OFF | OFF | ON | ON | +| 4 | Satellite Face 4 | OFF | OFF | ON | ON | +| 5 | Satellite Face 5 | OFF | OFF | ON | ON | +| 6 | Payload Power | OFF | OFF | OFF | ON | +| 7 | Payload Battery | OFF | OFF | OFF | ON | + +> **Note:** HIBERNATION_MODE can only be entered from SAFE_MODE. PAYLOAD_MODE can only be entered from NORMAL 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. + +## Mode State Machine + +```mermaid +stateDiagram-v2 + [*] --> SAFE_MODE: Initial boot (no saved state) + [*] --> SAFE_MODE: State file corrupted/unreadable + [*] --> SAFE_MODE: Restore from flash + [*] --> NORMAL: Restore from flash + [*] --> PAYLOAD_MODE: Restore from flash + [*] --> HIBERNATION_MODE: Restore from flash (wake window) + + NORMAL --> SAFE_MODE: FORCE_SAFE_MODE + NORMAL --> PAYLOAD_MODE: ENTER_PAYLOAD_MODE + + SAFE_MODE --> NORMAL: EXIT_SAFE_MODE + SAFE_MODE --> HIBERNATION_MODE: ENTER_HIBERNATION + + PAYLOAD_MODE --> NORMAL: EXIT_PAYLOAD_MODE + + HIBERNATION_MODE --> SAFE_MODE: EXIT_HIBERNATION + HIBERNATION_MODE --> HIBERNATION_MODE: Wake window timeout (re-enter dormant) +``` ## Integration Tests -See `FprimeZephyrReference/test/int/mode_manager_test.py` for comprehensive integration tests covering: +See `FprimeZephyrReference/test/int/mode_manager_test.py`, `FprimeZephyrReference/test/int/payload_mode_test.py`, and `FprimeZephyrReference/test/int/hibernation_mode_test.py` for comprehensive integration tests covering: + +### Safe Mode & Normal Mode Tests (mode_manager_test.py) | 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 | + +### Payload Mode Tests (payload_mode_test.py) + +| Test | Description | Coverage | +|---|---|---| +| test_payload_01_enter_exit_payload_mode | Validates payload mode entry/exit, events, telemetry, payload load switches | Payload mode entry/exit | +| test_payload_02_cannot_enter_from_safe_mode | Ensures ENTER_PAYLOAD_MODE fails from SAFE_MODE | Command validation | +| test_payload_03_safe_mode_rejected_from_payload | Ensures FORCE_SAFE_MODE is rejected from payload mode (sequential transitions) | Command validation | +| test_payload_04_state_persists | Verifies payload mode and counters persist | Payload persistence | + +### Hibernation Mode Tests (hibernation_mode_test.py) + +| Test | Description | CI Coverage | +|---|---|---| +| test_hibernation_01_enter_fails_from_normal | Verifies ENTER_HIBERNATION fails from NORMAL mode | Normal CI | +| test_hibernation_02_enter_fails_from_payload | Verifies ENTER_HIBERNATION fails from PAYLOAD_MODE | Normal CI | +| test_hibernation_03_exit_fails_from_normal | Verifies EXIT_HIBERNATION fails when not in hibernation | Normal CI | +| test_hibernation_04_exit_fails_from_safe | Verifies EXIT_HIBERNATION fails from SAFE_MODE | Normal CI | +| test_hibernation_05_telemetry_channels_exist | Verifies hibernation telemetry channels exist and are readable | Normal CI | +| test_hibernation_06_dormant_entry_failure_handling | Verifies failure path: HibernationEntryFailed event, counter rollback, mode reversion | Normal CI (native/sim) | +| test_hibernation_07_enter_reboot_success | Verifies successful hibernation entry on real hardware | --run-reboot | +| test_hibernation_08_exit_during_wake_window | Verifies EXIT_HIBERNATION during wake window | --run-reboot | +| test_hibernation_09_wake_window_timeout | Verifies automatic re-sleep when wake window expires | --run-reboot | +| test_hibernation_10_force_safe_mode_rejected | Verifies FORCE_SAFE_MODE is rejected from HIBERNATION_MODE | --run-reboot | + +> **Testing Strategy:** On native/simulation builds, `PicoSleep::sleepForSeconds()` returns false (dormant not supported), which exercises the failure handling path in normal CI. This tests the HibernationEntryFailed event emission, counter rollback, and mode reversion to SAFE_MODE. Tests requiring actual reboots (07-10) need the `--run-reboot` flag for hardware testing. ## Design Decisions @@ -303,13 +588,51 @@ Mode state is persisted to `/mode_state.bin` to maintain operational context acr - Intentional reboots - Watchdog resets - Power cycles +- **Hibernation dormant wake cycles** (critical for hibernation resumption) + +This ensures the system resumes in the correct mode after recovery. For hibernation, the sleep/wake durations are also persisted to enable automatic continuation of hibernation cycles. + +**Default Mode on State Restoration Failure:** If the state file is missing, corrupted, or unreadable on boot, the system defaults to SAFE_MODE (not NORMAL). This conservative approach ensures power constraints are not violated if the system was in hibernation before the state file became corrupted. A WARNING_HI `StateRestorationFailed` event is emitted to alert ground operators of the issue. + +### Sequential Mode Transitions +Mode transitions follow a sequential pattern: HIBERNATION_MODE(0) ↔ SAFE_MODE(1) ↔ NORMAL(2) ↔ PAYLOAD_MODE(3). Direct jumps (e.g., PAYLOAD→SAFE, NORMAL→HIBERNATION) are not allowed - users must follow the transition paths: +- To enter hibernation: Must be in SAFE_MODE +- To exit hibernation: Transitions to SAFE_MODE (then EXIT_SAFE_MODE for NORMAL) +- To enter payload: Must be in NORMAL +- To enter safe: Must be in NORMAL (not from PAYLOAD_MODE directly) + +### Hibernation Implementation Strategy +The hibernation mode uses the RP2350's dormant mode with AON (Always-On) Timer and POWMAN (Power Manager): + +1. **Sleep Entry**: When entering dormant sleep, the system saves all state to flash (including cycle counts and durations), then calls `PicoSleep::sleepForSeconds()` which puts the RP2350 into ultra-low-power dormant state with an AON timer alarm configured. + +2. **AON Timer vs RTC**: Unlike the RP2040 which uses an RTC, the RP2350 uses the AON Timer running from the Low Power Oscillator (LPOSC, ~32kHz). The LPOSC stays active during dormant mode while the main XOSC is powered off. Note: LPOSC is less precise (can drift a few percent) but is suitable for hibernation periods. + +3. **Wake Mechanism**: When the AON timer alarm fires, the processor wakes and execution continues (unlike RP2040 which reboots). If `USE_DORMANT_MODE` is disabled or AON timer is unavailable, falls back to `sys_reboot()` for reliability. + +4. **State Resumption**: On successful wake from AON timer, `startWakeWindow()` is called directly. On reboot-based wake (fallback), `loadState()` detects HIBERNATION_MODE and starts the wake window. + +5. **Command Response Timing**: The `ENTER_HIBERNATION` command sends its OK response BEFORE entering dormant sleep. This is critical because the subsequent sleep would prevent the response from ever being sent, causing ground to see a timeout. + +6. **Counter Rollback**: If dormant entry fails (e.g., hardware error), the pre-incremented counters are rolled back to maintain accurate statistics. + +7. **Dormant Entry Failure Notification**: Because the OK response is sent before attempting dormant entry, ground cannot rely on command response to detect failure. If `PicoSleep::sleepForSeconds()` returns false (hardware/AON timer error), a WARNING_HI `HibernationEntryFailed` event is emitted. Ground must monitor for this event after any ENTER_HIBERNATION command - if seen, the command actually failed despite the OK response, and the system has reverted to SAFE_MODE. + +8. **Wake Window**: During the wake window, only essential subsystems (LoRa radio) are active. The 1Hz run handler counts down the wake window and automatically re-enters dormant sleep when expired. -This ensures the system resumes in the correct mode after recovery. +9. **Power Consumption**: Dormant mode with AON timer: ~3mA. POWMAN deep sleep: ~0.65-0.85mA. For reference, the Pico 2W datasheet claims ~1.4-1.6mA in dormant mode. -### 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. +10. **Known Issue**: There's a known issue (pico-sdk #2376) where RP2350 can halt after multiple dormant wake cycles. Set `USE_DORMANT_MODE = false` in PicoSleep.cpp to use the safer `sys_reboot()` fallback if this issue is encountered. ## Change Log | Date | Description | |---|---| +| 2025-11-29 | State restoration failure now defaults to SAFE_MODE instead of NORMAL; added StateRestorationFailed (WARNING_HI) event | +| 2025-11-29 | Fixed FORCE_SAFE_MODE to reject from HIBERNATION_MODE (must use EXIT_HIBERNATION); added test_hibernation_10 | +| 2025-11-29 | Replaced RTC-based dormant with AON Timer + POWMAN for proper RP2350 dormant mode; added power consumption docs, known issue (pico-sdk #2376), and USE_DORMANT_MODE fallback | +| 2025-11-29 | Added HibernationEntryFailed (WARNING_HI) event for when dormant entry fails after OK response sent | +| 2025-11-29 | Added HIBERNATION_MODE with RP2350 dormant mode, configurable sleep/wake durations, cycle tracking, and integration tests | +| 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/ReferenceDeployment/Top/ReferenceDeploymentPackets.fppi b/FprimeZephyrReference/ReferenceDeployment/Top/ReferenceDeploymentPackets.fppi index 6055131c..687ed374 100644 --- a/FprimeZephyrReference/ReferenceDeployment/Top/ReferenceDeploymentPackets.fppi +++ b/FprimeZephyrReference/ReferenceDeployment/Top/ReferenceDeploymentPackets.fppi @@ -13,6 +13,9 @@ telemetry packets ReferenceDeploymentPackets { ReferenceDeployment.startupManager.QuiescenceEndTime ReferenceDeployment.modeManager.CurrentMode ReferenceDeployment.modeManager.SafeModeEntryCount + ReferenceDeployment.modeManager.PayloadModeEntryCount + ReferenceDeployment.modeManager.HibernationCycleCount + ReferenceDeployment.modeManager.HibernationTotalSeconds } packet HealthWarnings id 2 group 1 { diff --git a/FprimeZephyrReference/test/int/conftest.py b/FprimeZephyrReference/test/int/conftest.py index 6b1b8540..d023113f 100644 --- a/FprimeZephyrReference/test/int/conftest.py +++ b/FprimeZephyrReference/test/int/conftest.py @@ -14,6 +14,35 @@ from fprime_gds.common.testing_fw.api import IntegrationTestAPI +def pytest_addoption(parser): + """Add custom command-line options for integration tests.""" + parser.addoption( + "--run-reboot", + action="store_true", + default=False, + help="Run tests that trigger system reboot (hibernation tests)", + ) + + +def pytest_configure(config): + """Register custom markers.""" + config.addinivalue_line( + "markers", + "reboot: marks tests that trigger system reboot (use --run-reboot to run)", + ) + + +def pytest_collection_modifyitems(config, items): + """Skip reboot tests unless --run-reboot is specified.""" + if config.getoption("--run-reboot"): + # --run-reboot given: do not skip reboot tests + return + skip_reboot = pytest.mark.skip(reason="need --run-reboot option to run") + for item in items: + if "reboot" in item.keywords: + item.add_marker(skip_reboot) + + @pytest.fixture(scope="session") def start_gds(fprime_test_api_session: IntegrationTestAPI): """Fixture to start GDS before tests and stop after tests diff --git a/FprimeZephyrReference/test/int/hibernation_mode_test.py b/FprimeZephyrReference/test/int/hibernation_mode_test.py new file mode 100644 index 00000000..c742acd1 --- /dev/null +++ b/FprimeZephyrReference/test/int/hibernation_mode_test.py @@ -0,0 +1,655 @@ +""" +hibernation_mode_test.py: + +Integration tests for the ModeManager component (hibernation mode). + +Tests cover: +- ENTER_HIBERNATION command validation (only from SAFE_MODE) +- EXIT_HIBERNATION command validation +- Hibernation telemetry (HibernationCycleCount, HibernationTotalSeconds) +- Dormant entry failure handling (HibernationEntryFailed event, counter rollback) +- Command response ordering (response sent before reboot/dormant attempt) +- Wake window behavior (reboot tests only) + +Total: 10 tests (6 run in normal CI, 4 require --run-reboot for real hardware) + +Mode enum values: HIBERNATION_MODE=0, SAFE_MODE=1, NORMAL=2, PAYLOAD_MODE=3 + +Test 06 (dormant entry failure handling) runs in normal CI on native/sim builds +where PicoSleep::sleepForSeconds() returns false. This exercises the failure +path: HibernationEntryFailed event, counter rollback, mode reversion to SAFE_MODE. + +Tests 07-10 require the --run-reboot flag and will cause actual system reboots: + pytest hibernation_mode_test.py --run-reboot +""" + +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" +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 + # Note: EXIT_HIBERNATION only works during wake window, which we won't be in + # during normal test setup + + # 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 + + +# ============================================================================== +# ENTER_HIBERNATION Command Validation Tests +# ============================================================================== + + +def test_hibernation_01_enter_fails_from_normal_mode( + fprime_test_api: IntegrationTestAPI, start_gds +): + """ + Test that ENTER_HIBERNATION fails when in NORMAL mode. + Must be in SAFE_MODE to enter hibernation. + """ + # Verify we're in NORMAL 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 + ) + + if mode_result.get_val() != 2: + # Not in NORMAL mode - try to get there + try: + proves_send_and_assert_command( + fprime_test_api, f"{component}.EXIT_SAFE_MODE" + ) + except Exception: + pass + + # Clear histories before the test command + fprime_test_api.clear_histories() + + # Try to enter hibernation from NORMAL mode - should fail + with pytest.raises(Exception): + proves_send_and_assert_command( + fprime_test_api, f"{component}.ENTER_HIBERNATION", ["0", "0"] + ) + + # Verify CommandValidationFailed event + fprime_test_api.assert_event(f"{component}.CommandValidationFailed", timeout=3) + + # Verify still in NORMAL 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 still be in NORMAL mode" + + +def test_hibernation_02_enter_fails_from_payload_mode( + fprime_test_api: IntegrationTestAPI, start_gds +): + """ + Test that ENTER_HIBERNATION fails when in PAYLOAD_MODE. + Must be in SAFE_MODE to enter hibernation. + """ + # Enter payload mode first + proves_send_and_assert_command(fprime_test_api, f"{component}.ENTER_PAYLOAD_MODE") + time.sleep(2) + + # Verify in payload mode + proves_send_and_assert_command(fprime_test_api, "CdhCore.tlmSend.SEND_PKT", ["1"]) + mode_result: ChData = fprime_test_api.assert_telemetry( + f"{component}.CurrentMode", timeout=5 + ) + assert mode_result.get_val() == 3, "Should be in PAYLOAD_MODE" + + # Clear histories + fprime_test_api.clear_histories() + + # Try to enter hibernation from PAYLOAD_MODE - should fail + with pytest.raises(Exception): + proves_send_and_assert_command( + fprime_test_api, f"{component}.ENTER_HIBERNATION", ["0", "0"] + ) + + # Verify CommandValidationFailed event + fprime_test_api.assert_event(f"{component}.CommandValidationFailed", timeout=3) + + # Verify still in PAYLOAD_MODE (3) + proves_send_and_assert_command(fprime_test_api, "CdhCore.tlmSend.SEND_PKT", ["1"]) + mode_result: ChData = fprime_test_api.assert_telemetry( + f"{component}.CurrentMode", timeout=5 + ) + assert mode_result.get_val() == 3, "Should still be in PAYLOAD_MODE" + + +# ============================================================================== +# EXIT_HIBERNATION Command Validation Tests +# ============================================================================== + + +def test_hibernation_03_exit_fails_from_normal_mode( + fprime_test_api: IntegrationTestAPI, start_gds +): + """ + Test that EXIT_HIBERNATION fails when not in hibernation mode. + """ + # Verify we're in NORMAL mode (or get there) + 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 + ) + + if mode_result.get_val() != 2: + try: + proves_send_and_assert_command( + fprime_test_api, f"{component}.EXIT_SAFE_MODE" + ) + except Exception: + pass + + # Clear histories + fprime_test_api.clear_histories() + + # Try to exit hibernation when not in it - should fail + with pytest.raises(Exception): + proves_send_and_assert_command(fprime_test_api, f"{component}.EXIT_HIBERNATION") + + # Verify CommandValidationFailed event with "Not currently in hibernation mode" + fprime_test_api.assert_event(f"{component}.CommandValidationFailed", timeout=3) + + +def test_hibernation_04_exit_fails_from_safe_mode( + fprime_test_api: IntegrationTestAPI, start_gds +): + """ + Test that EXIT_HIBERNATION fails when in SAFE_MODE (not hibernation). + """ + # Enter safe mode + 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" + + # Clear histories + fprime_test_api.clear_histories() + + # Try to exit hibernation when in safe mode - should fail + with pytest.raises(Exception): + proves_send_and_assert_command(fprime_test_api, f"{component}.EXIT_HIBERNATION") + + # Verify CommandValidationFailed event + fprime_test_api.assert_event(f"{component}.CommandValidationFailed", timeout=3) + + # Verify still in 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 still be in SAFE_MODE" + + +# ============================================================================== +# Telemetry Tests +# ============================================================================== + + +def test_hibernation_05_telemetry_channels_exist( + fprime_test_api: IntegrationTestAPI, start_gds +): + """ + Test that hibernation telemetry channels exist and can be read. + Verifies HibernationCycleCount and HibernationTotalSeconds are available. + """ + # Trigger telemetry update by sending Health packet (ID 1) + proves_send_and_assert_command(fprime_test_api, "CdhCore.tlmSend.SEND_PKT", ["1"]) + + # Read HibernationCycleCount telemetry + cycle_result: ChData = fprime_test_api.assert_telemetry( + f"{component}.HibernationCycleCount", timeout=5 + ) + cycle_count = cycle_result.get_val() + assert cycle_count >= 0, f"Invalid cycle count: {cycle_count}" + + # Read HibernationTotalSeconds telemetry + seconds_result: ChData = fprime_test_api.assert_telemetry( + f"{component}.HibernationTotalSeconds", timeout=5 + ) + total_seconds = seconds_result.get_val() + assert total_seconds >= 0, f"Invalid total seconds: {total_seconds}" + + +# ============================================================================== +# Dormant Entry Failure Handling Test (runs in normal CI on native/sim builds) +# ============================================================================== + + +def test_hibernation_06_dormant_entry_failure_handling( + fprime_test_api: IntegrationTestAPI, start_gds +): + """ + Test that dormant entry failure is handled correctly. + + On native/simulation builds, PicoSleep::sleepForSeconds() returns false + (dormant mode not supported), which exercises the failure handling path. + This test runs in normal CI without --run-reboot. + + Verifies: + - Command response is OK (sent before dormant attempt - critical!) + - EnteringHibernation event is emitted + - HibernationSleepCycle event is emitted + - HibernationEntryFailed event is emitted (WARNING_HI) + - ExitingHibernation event is emitted (transitions to SAFE_MODE) + - Counters are rolled back (same as before attempt) + - Mode reverts to SAFE_MODE (not stuck in HIBERNATION_MODE) + + NOTE: On real hardware with --run-reboot, this test will cause a reboot + and these assertions won't be reached. Use test_hibernation_07 for + hardware-only reboot testing. + """ + # 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" + + # Record initial hibernation counters + initial_cycle: ChData = fprime_test_api.assert_telemetry( + f"{component}.HibernationCycleCount", timeout=5 + ) + initial_cycle_count = initial_cycle.get_val() + + initial_seconds: ChData = fprime_test_api.assert_telemetry( + f"{component}.HibernationTotalSeconds", timeout=5 + ) + initial_seconds_count = initial_seconds.get_val() + + # Clear histories before the critical command + fprime_test_api.clear_histories() + + # Enter hibernation mode - command should return OK before dormant attempt + # CRITICAL: This command MUST return OK even if dormant fails + proves_send_and_assert_command( + fprime_test_api, + f"{component}.ENTER_HIBERNATION", + ["60", "10"], # 60s sleep, 10s wake + ) + + # Verify EnteringHibernation event was emitted + fprime_test_api.assert_event(f"{component}.EnteringHibernation", timeout=3) + + # Verify HibernationSleepCycle event (logged just before dormant attempt) + fprime_test_api.assert_event(f"{component}.HibernationSleepCycle", timeout=3) + + # On native/sim builds: dormant fails, so we should see failure handling + # Wait a moment for the failure path to complete + time.sleep(2) + + # Try to detect if we're on native/sim (dormant failed) or real hardware (rebooted) + # by checking if we can still communicate and what mode we're in + try: + 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 + ) + current_mode = mode_result.get_val() + except Exception: + # If we can't communicate, system rebooted (real hardware) + pytest.skip( + "System rebooted (real hardware). " + "Use --run-reboot tests for hardware hibernation testing." + ) + return + + # If we get here, we're on native/sim and dormant entry failed + # Verify HibernationEntryFailed event (WARNING_HI) was emitted + fprime_test_api.assert_event(f"{component}.HibernationEntryFailed", timeout=3) + + # Verify ExitingHibernation event was emitted (mode reverted to SAFE_MODE) + fprime_test_api.assert_event(f"{component}.ExitingHibernation", timeout=3) + + # Verify mode reverted to SAFE_MODE (1), not stuck in HIBERNATION_MODE (0) + assert current_mode == 1, ( + f"Mode should revert to SAFE_MODE (1) after dormant failure, got {current_mode}" + ) + + # Verify counters were rolled back + cycle_result: ChData = fprime_test_api.assert_telemetry( + f"{component}.HibernationCycleCount", timeout=5 + ) + assert cycle_result.get_val() == initial_cycle_count, ( + f"HibernationCycleCount should be rolled back to {initial_cycle_count}, " + f"got {cycle_result.get_val()}" + ) + + seconds_result: ChData = fprime_test_api.assert_telemetry( + f"{component}.HibernationTotalSeconds", timeout=5 + ) + assert seconds_result.get_val() == initial_seconds_count, ( + f"HibernationTotalSeconds should be rolled back to {initial_seconds_count}, " + f"got {seconds_result.get_val()}" + ) + + +# ============================================================================== +# Reboot Tests (run with --run-reboot flag on real hardware) +# ============================================================================== + + +@pytest.mark.reboot +def test_hibernation_07_enter_reboot_success( + fprime_test_api: IntegrationTestAPI, start_gds +): + """ + Test that ENTER_HIBERNATION from SAFE_MODE succeeds on real hardware. + + WARNING: This test will cause the RP2350 to reboot! + Only runs with --run-reboot flag. + + Verifies the success path BEFORE reboot: + - Command response is received (critical: response must be sent before reboot) + - EnteringHibernation event is emitted with correct parameters + - HibernationSleepCycle event is emitted + - Mode transitions to HIBERNATION_MODE (0) + + After reboot, the system will be in hibernation wake window. + Run test_hibernation_08 within the wake window to verify exit behavior. + """ + # 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" + + # Record initial hibernation counters + proves_send_and_assert_command(fprime_test_api, "CdhCore.tlmSend.SEND_PKT", ["1"]) + initial_cycle: ChData = fprime_test_api.assert_telemetry( + f"{component}.HibernationCycleCount", timeout=5 + ) + initial_cycle_count = initial_cycle.get_val() + + # Clear histories before the critical command + fprime_test_api.clear_histories() + + # Enter hibernation mode with short durations for testing + # sleepDurationSec=10, wakeDurationSec=60 + # CRITICAL: This command MUST return OK before the system reboots + # If this times out, the command response ordering is broken + proves_send_and_assert_command( + fprime_test_api, + f"{component}.ENTER_HIBERNATION", + ["10", "60"], + ) + + # Verify EnteringHibernation event was emitted with correct parameters + fprime_test_api.assert_event(f"{component}.EnteringHibernation", timeout=3) + + # Verify HibernationSleepCycle event (logged just before dormant entry) + fprime_test_api.assert_event(f"{component}.HibernationSleepCycle", timeout=3) + + # Verify mode changed to HIBERNATION_MODE (0) before reboot + 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 HIBERNATION_MODE" + + # Verify cycle count incremented + cycle_result: ChData = fprime_test_api.assert_telemetry( + f"{component}.HibernationCycleCount", timeout=5 + ) + assert cycle_result.get_val() == initial_cycle_count + 1, ( + f"HibernationCycleCount should increment (was {initial_cycle_count})" + ) + + # SUCCESS: If we reach here, the command response was sent before reboot + # The system will now reboot into dormant mode + # After wake, it will enter the 60-second wake window + + +@pytest.mark.reboot +def test_hibernation_08_exit_during_wake_window( + fprime_test_api: IntegrationTestAPI, start_gds +): + """ + Test EXIT_HIBERNATION during wake window succeeds. + + PREREQUISITE: System must be in hibernation mode wake window. + This can be achieved by: + 1. Running test_hibernation_07 to enter hibernation (causes reboot) + 2. Waiting for system to reboot and enter wake window + 3. Running this test within the wake window duration (default 60s) + + Verifies: + - System is in HIBERNATION_MODE (0) - skips if not + - EXIT_HIBERNATION command succeeds + - ExitingHibernation event is emitted with cycle stats + - Mode transitions to SAFE_MODE (1) + - Hibernation counters are preserved + """ + # Check if we're in HIBERNATION_MODE (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 + ) + + if mode_result.get_val() != 0: + pytest.skip( + f"Not in HIBERNATION_MODE (mode={mode_result.get_val()}). " + "Run test_hibernation_07 first, wait for reboot, then run this test." + ) + + # Record counters before exit + cycle_result: ChData = fprime_test_api.assert_telemetry( + f"{component}.HibernationCycleCount", timeout=5 + ) + cycle_count_before = cycle_result.get_val() + + seconds_result: ChData = fprime_test_api.assert_telemetry( + f"{component}.HibernationTotalSeconds", timeout=5 + ) + seconds_before = seconds_result.get_val() + + # Clear histories + fprime_test_api.clear_histories() + + # Exit hibernation during wake window + proves_send_and_assert_command( + fprime_test_api, + f"{component}.EXIT_HIBERNATION", + ) + + # Verify ExitingHibernation event was emitted + fprime_test_api.assert_event(f"{component}.ExitingHibernation", timeout=3) + + 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 after exiting hibernation" + ) + + # Verify hibernation counters were preserved (not reset) + cycle_result: ChData = fprime_test_api.assert_telemetry( + f"{component}.HibernationCycleCount", timeout=5 + ) + assert cycle_result.get_val() == cycle_count_before, ( + f"HibernationCycleCount should be preserved ({cycle_count_before})" + ) + + seconds_result: ChData = fprime_test_api.assert_telemetry( + f"{component}.HibernationTotalSeconds", timeout=5 + ) + assert seconds_result.get_val() == seconds_before, ( + f"HibernationTotalSeconds should be preserved ({seconds_before})" + ) + + +@pytest.mark.reboot +def test_hibernation_09_wake_window_timeout_causes_resleep( + fprime_test_api: IntegrationTestAPI, start_gds +): + """ + Test that wake window timeout causes system to re-enter dormant sleep. + + PREREQUISITE: System must be in hibernation mode wake window. + + This test verifies that if EXIT_HIBERNATION is NOT sent during the wake + window, the system will automatically re-enter dormant sleep when the + window expires. + + WARNING: This test will cause the RP2350 to reboot again! + + Verifies: + - System is in HIBERNATION_MODE (0) and wake window + - Wait for wake window to expire (requires short wake duration) + - System should reboot into next sleep cycle + """ + # Check if we're in HIBERNATION_MODE (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 + ) + + if mode_result.get_val() != 0: + pytest.skip( + f"Not in HIBERNATION_MODE (mode={mode_result.get_val()}). " + "Run test_hibernation_07 first with short wake duration." + ) + + # Wait for wake window to expire + # Note: This requires the wake duration to be short (e.g., 10-30 seconds) + # The system will emit HibernationSleepCycle event and reboot + fprime_test_api.clear_histories() + + # Wait up to 120 seconds for the sleep cycle event + # (adjust based on configured wake duration) + try: + fprime_test_api.assert_event( + f"{component}.HibernationSleepCycle", + timeout=120, + ) + # If we see this event, the wake window expired and system is going to sleep + # The system will reboot shortly after + except Exception: + pytest.fail( + "Did not see HibernationSleepCycle event - wake window may not have expired " + "or wake duration is too long for this test" + ) + + +@pytest.mark.reboot +def test_hibernation_10_force_safe_mode_rejected_from_hibernation( + fprime_test_api: IntegrationTestAPI, start_gds +): + """ + Test that FORCE_SAFE_MODE is rejected from HIBERNATION_MODE. + + PREREQUISITE: System must be in hibernation mode wake window. + + FORCE_SAFE_MODE must be rejected from HIBERNATION_MODE to ensure proper + hibernation cleanup. Using FORCE_SAFE_MODE would bypass exitHibernation(), + leaving m_inHibernationWakeWindow and counters in inconsistent state. + + The correct way to exit hibernation is via EXIT_HIBERNATION command. + + Verifies: + - System is in HIBERNATION_MODE (0) - skips if not + - FORCE_SAFE_MODE command is rejected with VALIDATION_ERROR + - CommandValidationFailed event is emitted with "Use EXIT_HIBERNATION" + - Mode remains HIBERNATION_MODE (not changed to SAFE_MODE) + """ + # Check if we're in HIBERNATION_MODE (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 + ) + + if mode_result.get_val() != 0: + pytest.skip( + f"Not in HIBERNATION_MODE (mode={mode_result.get_val()}). " + "Run test_hibernation_07 first, wait for reboot, then run this test." + ) + + # Clear histories before the command + fprime_test_api.clear_histories() + + # Try to force safe mode - should be rejected + with pytest.raises(Exception): + proves_send_and_assert_command(fprime_test_api, f"{component}.FORCE_SAFE_MODE") + + # Verify CommandValidationFailed event with correct message + fprime_test_api.assert_event(f"{component}.CommandValidationFailed", timeout=3) + + # Verify still in HIBERNATION_MODE (0) - mode should NOT have changed + 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, ( + f"Should still be in HIBERNATION_MODE (0), but got mode={mode_result.get_val()}" + ) diff --git a/FprimeZephyrReference/test/int/payload_mode_test.py b/FprimeZephyrReference/test/int/payload_mode_test.py new file mode 100644 index 00000000..1f4474f8 --- /dev/null +++ b/FprimeZephyrReference/test/int/payload_mode_test.py @@ -0,0 +1,239 @@ +""" +payload_mode_test.py: + +Integration tests for the ModeManager component (payload mode). + +Tests cover: +- Payload mode entry and exit +- Mode transition validation (cannot enter from safe mode) +- Sequential transitions (FORCE_SAFE_MODE rejected from payload mode) +- State persistence + +Total: 4 tests + +Mode enum values: HIBERNATION_MODE=0, SAFE_MODE=1, NORMAL=2, PAYLOAD_MODE=3 +""" + +import time + +import pytest +from common import proves_send_and_assert_command +from fprime_gds.common.data_types.ch_data import ChData +from fprime_gds.common.testing_fw.api import IntegrationTestAPI + +component = "ReferenceDeployment.modeManager" +payload_load_switch_channels = [ + "ReferenceDeployment.payloadPowerLoadSwitch.IsOn", + "ReferenceDeployment.payloadBatteryLoadSwitch.IsOn", +] +all_load_switch_channels = [ + "ReferenceDeployment.face4LoadSwitch.IsOn", + "ReferenceDeployment.face0LoadSwitch.IsOn", + "ReferenceDeployment.face1LoadSwitch.IsOn", + "ReferenceDeployment.face2LoadSwitch.IsOn", + "ReferenceDeployment.face3LoadSwitch.IsOn", + "ReferenceDeployment.face5LoadSwitch.IsOn", + "ReferenceDeployment.payloadPowerLoadSwitch.IsOn", + "ReferenceDeployment.payloadBatteryLoadSwitch.IsOn", +] + + +@pytest.fixture(autouse=True) +def setup_and_teardown(fprime_test_api: IntegrationTestAPI, start_gds): + """ + Setup before each test and cleanup after. + Ensures clean state by exiting any mode back to NORMAL. + """ + # Setup: Try to get to a clean NORMAL state + try: + proves_send_and_assert_command(fprime_test_api, f"{component}.EXIT_SAFE_MODE") + except Exception: + pass + try: + proves_send_and_assert_command( + fprime_test_api, f"{component}.EXIT_PAYLOAD_MODE" + ) + except Exception: + pass + + # Clear event and telemetry history before test + fprime_test_api.clear_histories() + + yield + + # Teardown: Return to clean NORMAL state for next test + try: + proves_send_and_assert_command(fprime_test_api, f"{component}.EXIT_SAFE_MODE") + except Exception: + pass + try: + proves_send_and_assert_command( + fprime_test_api, f"{component}.EXIT_PAYLOAD_MODE" + ) + except Exception: + pass + + +# ============================================================================== +# Payload Mode Tests +# ============================================================================== + + +def test_payload_01_enter_exit_payload_mode( + fprime_test_api: IntegrationTestAPI, start_gds +): + """ + Test ENTER_PAYLOAD_MODE and EXIT_PAYLOAD_MODE commands. + Verifies: + - EnteringPayloadMode event is emitted + - CurrentMode telemetry = 3 (PAYLOAD_MODE) + - Payload load switches (6 & 7) are ON + - ExitingPayloadMode event is emitted on exit + - CurrentMode returns to 2 (NORMAL) + """ + # Enter payload mode + proves_send_and_assert_command( + fprime_test_api, + f"{component}.ENTER_PAYLOAD_MODE", + events=[f"{component}.ManualPayloadModeEntry"], + ) + + time.sleep(2) + + # Verify mode is PAYLOAD_MODE (3) + proves_send_and_assert_command(fprime_test_api, "CdhCore.tlmSend.SEND_PKT", ["1"]) + mode_result: ChData = fprime_test_api.assert_telemetry( + f"{component}.CurrentMode", timeout=5 + ) + assert mode_result.get_val() == 3, "Should be in PAYLOAD_MODE" + + # Verify payload load switches are ON + proves_send_and_assert_command(fprime_test_api, "CdhCore.tlmSend.SEND_PKT", ["9"]) + for channel in payload_load_switch_channels: + value = fprime_test_api.assert_telemetry(channel, timeout=5).get_val() + if isinstance(value, str): + assert value.upper() == "ON", f"{channel} should be ON in payload mode" + else: + assert value == 1, f"{channel} should be ON in payload mode" + + # Exit payload mode + proves_send_and_assert_command( + fprime_test_api, + f"{component}.EXIT_PAYLOAD_MODE", + events=[f"{component}.ExitingPayloadMode"], + ) + + time.sleep(2) + + # Verify mode is NORMAL (2) + proves_send_and_assert_command(fprime_test_api, "CdhCore.tlmSend.SEND_PKT", ["1"]) + mode_result: ChData = fprime_test_api.assert_telemetry( + f"{component}.CurrentMode", timeout=5 + ) + assert mode_result.get_val() == 2, "Should be in NORMAL mode" + + # Verify payload load switches are OFF in NORMAL mode + proves_send_and_assert_command(fprime_test_api, "CdhCore.tlmSend.SEND_PKT", ["9"]) + for channel in payload_load_switch_channels: + value = fprime_test_api.assert_telemetry(channel, timeout=5).get_val() + if isinstance(value, str): + assert value.upper() == "OFF", f"{channel} should be OFF in NORMAL mode" + else: + assert value == 0, f"{channel} should be OFF in NORMAL mode" + + +def test_payload_02_cannot_enter_from_safe_mode( + fprime_test_api: IntegrationTestAPI, start_gds +): + """ + Test that ENTER_PAYLOAD_MODE fails when in safe mode. + Must exit safe mode to NORMAL first. + """ + # Enter safe mode first + proves_send_and_assert_command(fprime_test_api, f"{component}.FORCE_SAFE_MODE") + time.sleep(2) + + # Verify in safe mode + proves_send_and_assert_command(fprime_test_api, "CdhCore.tlmSend.SEND_PKT", ["1"]) + mode_result: ChData = fprime_test_api.assert_telemetry( + f"{component}.CurrentMode", timeout=5 + ) + assert mode_result.get_val() == 1, "Should be in SAFE_MODE" + + # Try to enter payload mode - should fail + fprime_test_api.clear_histories() + with pytest.raises(Exception): + proves_send_and_assert_command( + fprime_test_api, f"{component}.ENTER_PAYLOAD_MODE" + ) + + # Verify CommandValidationFailed event + fprime_test_api.assert_event(f"{component}.CommandValidationFailed", timeout=3) + + # Verify still in safe mode + proves_send_and_assert_command(fprime_test_api, "CdhCore.tlmSend.SEND_PKT", ["1"]) + mode_result: ChData = fprime_test_api.assert_telemetry( + f"{component}.CurrentMode", timeout=5 + ) + assert mode_result.get_val() == 1, "Should still be in SAFE_MODE" + + +def test_payload_03_safe_mode_rejected_from_payload( + fprime_test_api: IntegrationTestAPI, start_gds +): + """ + Test that FORCE_SAFE_MODE is rejected from PAYLOAD_MODE (sequential transitions). + Must exit payload mode first before entering safe mode. + Verifies: + - FORCE_SAFE_MODE fails with validation error from payload mode + - CommandValidationFailed event is emitted + - Remains in PAYLOAD_MODE (3) + """ + # Enter payload mode first + proves_send_and_assert_command(fprime_test_api, f"{component}.ENTER_PAYLOAD_MODE") + time.sleep(2) + + # Verify in payload mode + proves_send_and_assert_command(fprime_test_api, "CdhCore.tlmSend.SEND_PKT", ["1"]) + mode_result: ChData = fprime_test_api.assert_telemetry( + f"{component}.CurrentMode", timeout=5 + ) + assert mode_result.get_val() == 3, "Should be in PAYLOAD_MODE" + + # Try to force safe mode - should fail (sequential transitions required) + fprime_test_api.clear_histories() + with pytest.raises(Exception): + proves_send_and_assert_command(fprime_test_api, f"{component}.FORCE_SAFE_MODE") + + # Verify CommandValidationFailed event + fprime_test_api.assert_event(f"{component}.CommandValidationFailed", timeout=3) + + # Verify still in payload mode + proves_send_and_assert_command(fprime_test_api, "CdhCore.tlmSend.SEND_PKT", ["1"]) + mode_result: ChData = fprime_test_api.assert_telemetry( + f"{component}.CurrentMode", timeout=5 + ) + assert mode_result.get_val() == 3, "Should still be in PAYLOAD_MODE" + + +def test_payload_04_state_persists(fprime_test_api: IntegrationTestAPI, start_gds): + """ + Test that payload mode state persists to /mode_state.bin file. + Note: Full persistence test would require reboot, this verifies state is saved. + """ + # Enter payload mode to trigger state save + proves_send_and_assert_command(fprime_test_api, f"{component}.ENTER_PAYLOAD_MODE") + time.sleep(2) + + # Verify mode is saved as PAYLOAD_MODE (3) + proves_send_and_assert_command(fprime_test_api, "CdhCore.tlmSend.SEND_PKT", ["1"]) + mode_result: ChData = fprime_test_api.assert_telemetry( + f"{component}.CurrentMode", timeout=5 + ) + assert mode_result.get_val() == 3, "Mode should be saved as PAYLOAD_MODE" + + # Verify PayloadModeEntryCount is tracking + count_result: ChData = fprime_test_api.assert_telemetry( + f"{component}.PayloadModeEntryCount", timeout=5 + ) + assert count_result.get_val() >= 1, "PayloadModeEntryCount should be at least 1" diff --git a/FprimeZephyrReference/test/long/mode_manager_test.py b/FprimeZephyrReference/test/long/mode_manager_test.py index 6bdd0429..ad476401 100644 --- a/FprimeZephyrReference/test/long/mode_manager_test.py +++ b/FprimeZephyrReference/test/long/mode_manager_test.py @@ -11,6 +11,8 @@ - Edge cases Total: 9 tests + +Mode enum values: HIBERNATION_MODE=0, 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/Makefile b/Makefile index b200288a..8c87b9fd 100644 --- a/Makefile +++ b/Makefile @@ -68,8 +68,17 @@ build: submodules zephyr fprime-venv generate-spi-dict generate-if-needed ## Bui @$(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.