Skip to content

Commit fddb10f

Browse files
committed
change mode enum values
1 parent 05ff516 commit fddb10f

File tree

6 files changed

+79
-78
lines changed

6 files changed

+79
-78
lines changed

FprimeZephyrReference/Components/ModeManager/ModeManager.cpp

Lines changed: 24 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -45,13 +45,14 @@ void ModeManager ::run_handler(FwIndexType portNum, U32 context) {
4545

4646
void ModeManager ::forceSafeMode_handler(FwIndexType portNum) {
4747
// Force entry into safe mode (called by other components)
48-
// Provides immediate safe mode entry for critical component-detected faults
48+
// Only allowed from NORMAL (sequential +1/-1 transitions)
4949
this->log_WARNING_HI_ExternalFaultDetected();
5050

51-
// Allow safe mode entry from NORMAL or PAYLOAD_MODE (emergency override)
52-
if (this->m_mode == SystemMode::NORMAL || this->m_mode == SystemMode::PAYLOAD_MODE) {
51+
// Only enter safe mode from NORMAL - payload mode must be exited first
52+
if (this->m_mode == SystemMode::NORMAL) {
5353
this->enterSafeMode("External component request");
5454
}
55+
// Note: Request ignored if in PAYLOAD_MODE or already in SAFE_MODE
5556
}
5657

5758
Components::SystemMode ModeManager ::getMode_handler(FwIndexType portNum) {
@@ -65,14 +66,26 @@ Components::SystemMode ModeManager ::getMode_handler(FwIndexType portNum) {
6566
// ----------------------------------------------------------------------
6667

6768
void ModeManager ::FORCE_SAFE_MODE_cmdHandler(FwOpcodeType opCode, U32 cmdSeq) {
68-
// Force entry into safe mode
69-
this->log_ACTIVITY_HI_ManualSafeModeEntry();
69+
// Force entry into safe mode - only allowed from NORMAL (sequential +1/-1 transitions)
7070

71-
// Allow safe mode entry from NORMAL or PAYLOAD_MODE (emergency override)
72-
if (this->m_mode == SystemMode::NORMAL || this->m_mode == SystemMode::PAYLOAD_MODE) {
73-
this->enterSafeMode("Ground command");
71+
// Reject if in payload mode - must exit payload mode first
72+
if (this->m_mode == SystemMode::PAYLOAD_MODE) {
73+
Fw::LogStringArg cmdNameStr("FORCE_SAFE_MODE");
74+
Fw::LogStringArg reasonStr("Must exit payload mode first");
75+
this->log_WARNING_LO_CommandValidationFailed(cmdNameStr, reasonStr);
76+
this->cmdResponse_out(opCode, cmdSeq, Fw::CmdResponse::VALIDATION_ERROR);
77+
return;
7478
}
7579

80+
// Already in safe mode - idempotent success
81+
if (this->m_mode == SystemMode::SAFE_MODE) {
82+
this->cmdResponse_out(opCode, cmdSeq, Fw::CmdResponse::OK);
83+
return;
84+
}
85+
86+
// Enter safe mode from NORMAL
87+
this->log_ACTIVITY_HI_ManualSafeModeEntry();
88+
this->enterSafeMode("Ground command");
7689
this->cmdResponse_out(opCode, cmdSeq, Fw::CmdResponse::OK);
7790
}
7891

@@ -150,8 +163,9 @@ void ModeManager ::loadState() {
150163
status = file.read(reinterpret_cast<U8*>(&state), bytesRead, Os::File::WaitType::WAIT);
151164

152165
if (status == Os::File::OP_OK && bytesRead == sizeof(PersistentState)) {
153-
// Validate state data before restoring
154-
if (state.mode <= static_cast<U8>(SystemMode::PAYLOAD_MODE)) {
166+
// Validate state data before restoring (valid range: 1-3 for SAFE, NORMAL, PAYLOAD)
167+
if (state.mode >= static_cast<U8>(SystemMode::SAFE_MODE) &&
168+
state.mode <= static_cast<U8>(SystemMode::PAYLOAD_MODE)) {
155169
// Valid mode value - restore state
156170
this->m_mode = static_cast<SystemMode>(state.mode);
157171
this->m_safeModeEntryCount = state.safeModeEntryCount;

FprimeZephyrReference/Components/ModeManager/ModeManager.fpp

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
module Components {
22

3-
@ System mode enumeration
3+
@ System mode enumeration (values ordered for +1/-1 sequential transitions)
44
enum SystemMode {
5-
NORMAL = 0 @< Normal operational mode
65
SAFE_MODE = 1 @< Safe mode with non-critical components powered off
7-
PAYLOAD_MODE = 2 @< Payload mode with payload power and battery enabled
6+
NORMAL = 2 @< Normal operational mode
7+
PAYLOAD_MODE = 3 @< Payload mode with payload power and battery enabled
88
}
99

1010
@ Port for notifying about mode changes

FprimeZephyrReference/Components/ModeManager/ModeManager.hpp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -125,8 +125,8 @@ class ModeManager : public ModeManagerComponentBase {
125125
// Private enums and types
126126
// ----------------------------------------------------------------------
127127

128-
//! System mode enumeration
129-
enum class SystemMode : U8 { NORMAL = 0, SAFE_MODE = 1, PAYLOAD_MODE = 2 };
128+
//! System mode enumeration (values ordered for +1/-1 sequential transitions)
129+
enum class SystemMode : U8 { SAFE_MODE = 1, NORMAL = 2, PAYLOAD_MODE = 3 };
130130

131131
//! Persistent state structure
132132
struct PersistentState {

FprimeZephyrReference/Components/ModeManager/docs/sdd.md

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ Future work: a HIBERNATION mode remains planned; it will follow the same persist
2424
| 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 |
2525
| MM0017 | The ModeManager shall track and report the number of times payload mode has been entered | Integration Testing |
2626
| MM0018 | The ModeManager shall persist payload mode state and payload mode entry count to non-volatile storage and restore them on initialization | Integration Testing |
27-
| MM0019 | The ModeManager shall allow FORCE_SAFE_MODE to override payload mode and transition to safe mode | Integration Testing |
27+
| MM0019 | The ModeManager shall reject FORCE_SAFE_MODE from payload mode (must exit payload mode first for sequential transitions) | Integration Testing |
2828

2929
## Usage Examples
3030

@@ -44,7 +44,7 @@ The ModeManager component operates as an active component that manages system-wi
4444
- Keeps payload load switches (indices 6 and 7) off unless payload mode is explicitly entered
4545

4646
3. **Safe Mode Entry**
47-
- Can be triggered by:
47+
- Can be triggered by (only from NORMAL mode - sequential transitions enforced):
4848
- Ground command: `FORCE_SAFE_MODE`
4949
- External component request via `forceSafeMode` port
5050
- Actions performed:
@@ -130,9 +130,9 @@ classDiagram
130130
}
131131
class SystemMode {
132132
<<enumeration>>
133-
NORMAL = 0
134133
SAFE_MODE = 1
135-
PAYLOAD_MODE = 2
134+
NORMAL = 2
135+
PAYLOAD_MODE = 3
136136
}
137137
}
138138
ModeManagerComponentBase <|-- ModeManager : inherits
@@ -304,7 +304,7 @@ sequenceDiagram
304304

305305
| Name | Arguments | Description |
306306
|---|---|---|
307-
| FORCE_SAFE_MODE | None | Forces the system into safe mode immediately. Emits ManualSafeModeEntry event. Can be called from any mode (idempotent). |
307+
| 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. |
308308
| EXIT_SAFE_MODE | None | Exits safe mode and returns to normal operation. Fails with CommandValidationFailed if not currently in safe mode. |
309309
| 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. |
310310
| EXIT_PAYLOAD_MODE | None | Exits payload mode and returns to normal operation. Fails with CommandValidationFailed if not currently in payload mode. |
@@ -327,7 +327,7 @@ sequenceDiagram
327327

328328
| Name | Type | Update Rate | Description |
329329
|---|---|---|---|
330-
| CurrentMode | U8 | 1Hz | Current system mode (0 = NORMAL, 1 = SAFE_MODE, 2 = PAYLOAD_MODE) |
330+
| CurrentMode | U8 | 1Hz | Current system mode (1 = SAFE_MODE, 2 = NORMAL, 3 = PAYLOAD_MODE) |
331331
| SafeModeEntryCount | U32 | On change | Number of times safe mode has been entered (persists across reboots) |
332332
| PayloadModeEntryCount | U32 | On change | Number of times payload mode has been entered (persists across reboots) |
333333

@@ -360,7 +360,7 @@ See `FprimeZephyrReference/test/int/mode_manager_test.py` and `FprimeZephyrRefer
360360
| test_19_safe_mode_state_persists | Verifies safe mode persistence to flash | State persistence |
361361
| test_payload_01_enter_exit_payload_mode | Validates payload mode entry/exit, events, telemetry, payload load switches | Payload mode entry/exit |
362362
| test_payload_02_cannot_enter_from_safe_mode | Ensures ENTER_PAYLOAD_MODE fails from SAFE_MODE | Command validation |
363-
| test_payload_03_safe_mode_override_from_payload | Ensures FORCE_SAFE_MODE overrides payload mode | Emergency override |
363+
| test_payload_03_safe_mode_rejected_from_payload | Ensures FORCE_SAFE_MODE is rejected from payload mode (sequential transitions) | Command validation |
364364
| test_payload_04_state_persists | Verifies payload mode and counters persist | Payload persistence |
365365

366366
## Design Decisions
@@ -388,12 +388,13 @@ Mode state is persisted to `/mode_state.bin` to maintain operational context acr
388388

389389
This ensures the system resumes in the correct mode after recovery.
390390

391-
### Idempotent Safe Mode Entry
392-
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.
391+
### Sequential Mode Transitions
392+
Mode transitions follow a +1/-1 sequential pattern: SAFE_MODE(1) ↔ NORMAL(2) ↔ PAYLOAD_MODE(3). Direct jumps (e.g., PAYLOAD→SAFE) are not allowed - users must exit payload mode first before entering safe mode. FORCE_SAFE_MODE is idempotent when already in safe mode.
393393

394394
## Change Log
395395
| Date | Description |
396396
|---|---|
397+
| 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 |
397398
| 2025-11-26 | Removed forcePayloadMode port - payload mode now only entered via ENTER_PAYLOAD_MODE ground command |
398399
| 2025-11-25 | Added PAYLOAD_MODE (commands, events, telemetry, persistence, payload load switch control) and documented payload integration tests |
399400
| 2025-11-19 | Added getMode query port and enhanced modeChanged to carry mode value |

FprimeZephyrReference/test/int/mode_manager_test.py

Lines changed: 15 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
- Edge cases
1212
1313
Total: 9 tests
14+
15+
Mode enum values: SAFE_MODE=1, NORMAL=2, PAYLOAD_MODE=3
1416
"""
1517

1618
import time
@@ -87,12 +89,12 @@ def test_01_initial_telemetry(fprime_test_api: IntegrationTestAPI, start_gds):
8789
# Trigger telemetry update by sending Health packet (ID 1)
8890
proves_send_and_assert_command(fprime_test_api, "CdhCore.tlmSend.SEND_PKT", ["1"])
8991

90-
# Read CurrentMode telemetry (0 = NORMAL, 1 = SAFE_MODE)
92+
# Read CurrentMode telemetry (1 = SAFE_MODE, 2 = NORMAL, 3 = PAYLOAD_MODE)
9193
mode_result: ChData = fprime_test_api.assert_telemetry(
9294
f"{component}.CurrentMode", start="NOW", timeout=3
9395
)
9496
current_mode = mode_result.get_val()
95-
assert current_mode in [0, 1], f"Invalid mode value: {current_mode}"
97+
assert current_mode in [1, 2, 3], f"Invalid mode value: {current_mode}"
9698

9799

98100
# ==============================================================================
@@ -103,13 +105,11 @@ def test_01_initial_telemetry(fprime_test_api: IntegrationTestAPI, start_gds):
103105
def test_04_force_safe_mode_command(fprime_test_api: IntegrationTestAPI, start_gds):
104106
"""
105107
Test FORCE_SAFE_MODE command enters safe mode by checking telemetry.
108+
Note: With idempotent behavior, ManualSafeModeEntry is only emitted when
109+
transitioning from NORMAL, not when already in SAFE_MODE.
106110
"""
107-
# Send FORCE_SAFE_MODE command - still expect ManualSafeModeEntry for logging
108-
proves_send_and_assert_command(
109-
fprime_test_api,
110-
f"{component}.FORCE_SAFE_MODE",
111-
events=[f"{component}.ManualSafeModeEntry"],
112-
)
111+
# Send FORCE_SAFE_MODE command (idempotent - succeeds even if already in safe mode)
112+
proves_send_and_assert_command(fprime_test_api, f"{component}.FORCE_SAFE_MODE")
113113

114114
# Wait for mode transition (happens in 1Hz rate group)
115115
time.sleep(3)
@@ -201,7 +201,7 @@ def test_13_exit_safe_mode_fails_not_in_safe_mode(
201201
f"{component}.CurrentMode", start="NOW", timeout=3
202202
)
203203

204-
if mode_result.get_val() != 0:
204+
if mode_result.get_val() != 2:
205205
pytest.skip("Not in NORMAL mode - cannot test this scenario")
206206

207207
# 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
219219
Test EXIT_SAFE_MODE succeeds.
220220
Verifies:
221221
- ExitingSafeMode event is emitted
222-
- CurrentMode returns to NORMAL (0)
222+
- CurrentMode returns to NORMAL (2)
223223
"""
224224
# Enter safe mode
225225
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
234234

235235
time.sleep(2)
236236

237-
# Verify mode is NORMAL (0)
237+
# Verify mode is NORMAL (2)
238238
proves_send_and_assert_command(fprime_test_api, "CdhCore.tlmSend.SEND_PKT", ["1"])
239239
mode_result: ChData = fprime_test_api.assert_telemetry(
240240
f"{component}.CurrentMode", start="NOW", timeout=3
241241
)
242-
assert mode_result.get_val() == 0, "Should be in NORMAL mode"
242+
assert mode_result.get_val() == 2, "Should be in NORMAL mode"
243243

244244

245245
# ==============================================================================
@@ -250,7 +250,7 @@ def test_14_exit_safe_mode_success(fprime_test_api: IntegrationTestAPI, start_gd
250250
def test_18_force_safe_mode_idempotent(fprime_test_api: IntegrationTestAPI, start_gds):
251251
"""
252252
Test that calling FORCE_SAFE_MODE while already in safe mode is idempotent.
253-
Should succeed and not cause issues.
253+
Should succeed without emitting events (no re-entry).
254254
"""
255255
# Enter safe mode first time
256256
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
259259
# Clear event history
260260
fprime_test_api.clear_histories()
261261

262-
# Force safe mode again - should succeed without EnteringSafeMode event
263-
# But ManualSafeModeEntry event is still emitted (logging)
264-
proves_send_and_assert_command(
265-
fprime_test_api,
266-
f"{component}.FORCE_SAFE_MODE",
267-
events=[f"{component}.ManualSafeModeEntry"],
268-
)
262+
# Force safe mode again - should succeed silently (no events, no re-entry)
263+
proves_send_and_assert_command(fprime_test_api, f"{component}.FORCE_SAFE_MODE")
269264

270265
# Verify still in safe mode
271266
time.sleep(1)

FprimeZephyrReference/test/int/payload_mode_test.py

Lines changed: 25 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,12 @@
66
Tests cover:
77
- Payload mode entry and exit
88
- Mode transition validation (cannot enter from safe mode)
9-
- Emergency safe mode override from payload mode
9+
- Sequential transitions (FORCE_SAFE_MODE rejected from payload mode)
1010
- State persistence
1111
1212
Total: 4 tests
13+
14+
Mode enum values: SAFE_MODE=1, NORMAL=2, PAYLOAD_MODE=3
1315
"""
1416

1517
import time
@@ -84,10 +86,10 @@ def test_payload_01_enter_exit_payload_mode(
8486
Test ENTER_PAYLOAD_MODE and EXIT_PAYLOAD_MODE commands.
8587
Verifies:
8688
- EnteringPayloadMode event is emitted
87-
- CurrentMode telemetry = 2 (PAYLOAD_MODE)
89+
- CurrentMode telemetry = 3 (PAYLOAD_MODE)
8890
- Payload load switches (6 & 7) are ON
8991
- ExitingPayloadMode event is emitted on exit
90-
- CurrentMode returns to 0 (NORMAL)
92+
- CurrentMode returns to 2 (NORMAL)
9193
"""
9294
# Enter payload mode
9395
proves_send_and_assert_command(
@@ -98,12 +100,12 @@ def test_payload_01_enter_exit_payload_mode(
98100

99101
time.sleep(2)
100102

101-
# Verify mode is PAYLOAD_MODE (2)
103+
# Verify mode is PAYLOAD_MODE (3)
102104
proves_send_and_assert_command(fprime_test_api, "CdhCore.tlmSend.SEND_PKT", ["1"])
103105
mode_result: ChData = fprime_test_api.assert_telemetry(
104106
f"{component}.CurrentMode", timeout=5
105107
)
106-
assert mode_result.get_val() == 2, "Should be in PAYLOAD_MODE"
108+
assert mode_result.get_val() == 3, "Should be in PAYLOAD_MODE"
107109

108110
# Verify payload load switches are ON
109111
proves_send_and_assert_command(fprime_test_api, "CdhCore.tlmSend.SEND_PKT", ["9"])
@@ -123,12 +125,12 @@ def test_payload_01_enter_exit_payload_mode(
123125

124126
time.sleep(2)
125127

126-
# Verify mode is NORMAL (0)
128+
# Verify mode is NORMAL (2)
127129
proves_send_and_assert_command(fprime_test_api, "CdhCore.tlmSend.SEND_PKT", ["1"])
128130
mode_result: ChData = fprime_test_api.assert_telemetry(
129131
f"{component}.CurrentMode", timeout=5
130132
)
131-
assert mode_result.get_val() == 0, "Should be in NORMAL mode"
133+
assert mode_result.get_val() == 2, "Should be in NORMAL mode"
132134

133135
# Verify payload load switches are OFF in NORMAL mode
134136
proves_send_and_assert_command(fprime_test_api, "CdhCore.tlmSend.SEND_PKT", ["9"])
@@ -176,16 +178,16 @@ def test_payload_02_cannot_enter_from_safe_mode(
176178
assert mode_result.get_val() == 1, "Should still be in SAFE_MODE"
177179

178180

179-
def test_payload_03_safe_mode_override_from_payload(
181+
def test_payload_03_safe_mode_rejected_from_payload(
180182
fprime_test_api: IntegrationTestAPI, start_gds
181183
):
182184
"""
183-
Test that FORCE_SAFE_MODE works from PAYLOAD_MODE (emergency override).
185+
Test that FORCE_SAFE_MODE is rejected from PAYLOAD_MODE (sequential transitions).
186+
Must exit payload mode first before entering safe mode.
184187
Verifies:
185-
- Can enter safe mode from payload mode
186-
- EnteringSafeMode event is emitted
187-
- CurrentMode = 1 (SAFE_MODE)
188-
- All load switches are OFF
188+
- FORCE_SAFE_MODE fails with validation error from payload mode
189+
- CommandValidationFailed event is emitted
190+
- Remains in PAYLOAD_MODE (3)
189191
"""
190192
# Enter payload mode first
191193
proves_send_and_assert_command(fprime_test_api, f"{component}.ENTER_PAYLOAD_MODE")
@@ -196,33 +198,22 @@ def test_payload_03_safe_mode_override_from_payload(
196198
mode_result: ChData = fprime_test_api.assert_telemetry(
197199
f"{component}.CurrentMode", timeout=5
198200
)
199-
assert mode_result.get_val() == 2, "Should be in PAYLOAD_MODE"
201+
assert mode_result.get_val() == 3, "Should be in PAYLOAD_MODE"
200202

201-
# Force safe mode (emergency override)
203+
# Try to force safe mode - should fail (sequential transitions required)
202204
fprime_test_api.clear_histories()
203-
proves_send_and_assert_command(
204-
fprime_test_api,
205-
f"{component}.FORCE_SAFE_MODE",
206-
events=[f"{component}.ManualSafeModeEntry"],
207-
)
205+
with pytest.raises(Exception):
206+
proves_send_and_assert_command(fprime_test_api, f"{component}.FORCE_SAFE_MODE")
208207

209-
time.sleep(2)
208+
# Verify CommandValidationFailed event
209+
fprime_test_api.assert_event(f"{component}.CommandValidationFailed", timeout=3)
210210

211-
# Verify mode is SAFE_MODE (1)
211+
# Verify still in payload mode
212212
proves_send_and_assert_command(fprime_test_api, "CdhCore.tlmSend.SEND_PKT", ["1"])
213213
mode_result: ChData = fprime_test_api.assert_telemetry(
214214
f"{component}.CurrentMode", timeout=5
215215
)
216-
assert mode_result.get_val() == 1, "Should be in SAFE_MODE"
217-
218-
# Verify all load switches are OFF
219-
proves_send_and_assert_command(fprime_test_api, "CdhCore.tlmSend.SEND_PKT", ["9"])
220-
for channel in all_load_switch_channels:
221-
value = fprime_test_api.assert_telemetry(channel, timeout=5).get_val()
222-
if isinstance(value, str):
223-
assert value.upper() == "OFF", f"{channel} should be OFF in safe mode"
224-
else:
225-
assert value == 0, f"{channel} should be OFF in safe mode"
216+
assert mode_result.get_val() == 3, "Should still be in PAYLOAD_MODE"
226217

227218

228219
def test_payload_04_state_persists(fprime_test_api: IntegrationTestAPI, start_gds):
@@ -234,12 +225,12 @@ def test_payload_04_state_persists(fprime_test_api: IntegrationTestAPI, start_gd
234225
proves_send_and_assert_command(fprime_test_api, f"{component}.ENTER_PAYLOAD_MODE")
235226
time.sleep(2)
236227

237-
# Verify mode is saved as PAYLOAD_MODE
228+
# Verify mode is saved as PAYLOAD_MODE (3)
238229
proves_send_and_assert_command(fprime_test_api, "CdhCore.tlmSend.SEND_PKT", ["1"])
239230
mode_result: ChData = fprime_test_api.assert_telemetry(
240231
f"{component}.CurrentMode", timeout=5
241232
)
242-
assert mode_result.get_val() == 2, "Mode should be saved as PAYLOAD_MODE"
233+
assert mode_result.get_val() == 3, "Mode should be saved as PAYLOAD_MODE"
243234

244235
# Verify PayloadModeEntryCount is tracking
245236
count_result: ChData = fprime_test_api.assert_telemetry(

0 commit comments

Comments
 (0)