Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
206 changes: 193 additions & 13 deletions src/main/cms/cms.c
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@
#include "fc/rc_controls.h"
#include "fc/runtime_config.h"
#include "fc/settings.h"
#include "fc/rc_modes.h"

#include "flight/mixer.h"
#include "flight/servos.h"
Expand Down Expand Up @@ -183,6 +184,42 @@ static uint8_t linesPerMenuItem;
static cms_key_e externKey = CMS_KEY_NONE;

bool cmsInMenu = false;
static bool cmsOpenedInFlight = false; // true when menu was opened via BOXUSER4 while armed
static bool cmsMenuSwitchLatched = false;
static uint32_t cmsOpenCountdownStartTime = 0;
static timeMs_t cmsLastInputMs = 0;

uint32_t cmsGetOpenCountdownRemaining(void)
{
if (cmsOpenCountdownStartTime == 0) {
return 0;
}
uint32_t elapsed = millis() - cmsOpenCountdownStartTime;
if (elapsed >= 3000) {
return 0;
}
return 3000 - elapsed;
}

uint32_t cmsGetInactivityCloseCountdownRemaining(void)
{
if (!cmsOpenedInFlight || cmsLastInputMs == 0) {
return 0;
}
uint32_t elapsed = millis() - cmsLastInputMs;
if (elapsed < 5000) {
return 0;
}
if (elapsed >= 15000) {
return 0;
}
return 15000 - elapsed;
}

bool cmsIsMenuSwitchLatched(void)
{
return cmsMenuSwitchLatched;
}

typedef struct cmsCtx_s {
const CMS_Menu *menu; // menu for this context
Expand Down Expand Up @@ -794,13 +831,21 @@ void cmsMenuOpen(void)
{
if (!cmsInMenu) {
// New open
setServoOutputEnabled(false);
if (!ARMING_FLAG(ARMED)) {
setServoOutputEnabled(false);
}
pCurrentDisplay = cmsDisplayPortSelectCurrent();
if (!pCurrentDisplay)
return;
cmsInMenu = true;
currentCtx = (cmsCtx_t){ &menuMain, 0, 0 };
ENABLE_ARMING_FLAG(ARMING_DISABLED_CMS_MENU);
cmsOpenedInFlight = ARMING_FLAG(ARMED);
cmsLastInputMs = millis();
if (cmsOpenedInFlight) {
currentCtx = (cmsCtx_t){ &menuMainInFlight, 0, 0 };
} else {
currentCtx = (cmsCtx_t){ &menuMain, 0, 0 };
ENABLE_ARMING_FLAG(ARMING_DISABLED_CMS_MENU);
}
} else {
// Switch display
displayPort_t *pNextDisplay = cmsDisplayPortSelectNext();
Expand Down Expand Up @@ -872,6 +917,10 @@ long cmsMenuExit(displayPort_t *pDisplay, const void *ptr)
case CMS_EXIT_SAVEREBOOT:
case CMS_POPUP_SAVE:
case CMS_POPUP_SAVEREBOOT:
if (cmsOpenedInFlight) {
// Save and reboot are not allowed while armed - treat as simple exit
break;
}

cmsTraverseGlobalExit(&menuMain);

Expand All @@ -891,6 +940,16 @@ long cmsMenuExit(displayPort_t *pDisplay, const void *ptr)
break;

case CMS_EXIT:
if (cmsOpenedInFlight) {
if (currentCtx.menu && currentCtx.menu->onExit) {
currentCtx.menu->onExit((OSD_Entry *)NULL);
}
for (int i = menuStackIdx - 1; i >= 0; i--) {
if (menuStack[i].menu && menuStack[i].menu->onExit) {
menuStack[i].menu->onExit((OSD_Entry *)NULL);
}
}
}
break;
}

Expand All @@ -899,19 +958,26 @@ long cmsMenuExit(displayPort_t *pDisplay, const void *ptr)
displayRelease(pDisplay);
currentCtx.menu = NULL;

setServoOutputEnabled(true);
if (!cmsOpenedInFlight) {
setServoOutputEnabled(true);

if ((exitType == CMS_EXIT_SAVEREBOOT) || (exitType == CMS_POPUP_SAVEREBOOT)) {
processDelayedSave();
displayClearScreen(pDisplay);
displayWrite(pDisplay, 5, 3, "REBOOTING...");
if ((exitType == CMS_EXIT_SAVEREBOOT) || (exitType == CMS_POPUP_SAVEREBOOT)) {
processDelayedSave();
displayClearScreen(pDisplay);
displayWrite(pDisplay, 5, 3, "REBOOTING...");

displayResync(pDisplay); // Was max7456RefreshAll(); why at this timing?
displayResync(pDisplay); // Was max7456RefreshAll(); why at this timing?

fcReboot(false);
}

fcReboot(false);
DISABLE_ARMING_FLAG(ARMING_DISABLED_CMS_MENU);
} else {
// Latch the switch so it doesn't reopen immediately if still ON
cmsMenuSwitchLatched = IS_RC_MODE_ACTIVE(BOXUSER4);
}

DISABLE_ARMING_FLAG(ARMING_DISABLED_CMS_MENU);
cmsOpenedInFlight = false;

return 0;
}
Expand Down Expand Up @@ -1262,9 +1328,9 @@ static uint16_t cmsScanKeys(timeMs_t currentTimeMs, timeMs_t lastCalledMs, int16
key = CMS_KEY_LEFT;
} else if (IS_HI(ROLL)) {
key = CMS_KEY_RIGHT;
} else if (IS_LO(YAW)) {
} else if (IS_LO(YAW) && !cmsOpenedInFlight) {
key = CMS_KEY_ESC;
} else if (IS_HI(YAW)) {
} else if (IS_HI(YAW) && !cmsOpenedInFlight) {
key = CMS_KEY_SAVEMENU;
}

Expand All @@ -1276,6 +1342,7 @@ static uint16_t cmsScanKeys(timeMs_t currentTimeMs, timeMs_t lastCalledMs, int16
} else {
// The 'key' is being pressed; keep counting
++holdCount;
cmsLastInputMs = currentTimeMs;
}

if (rcDelayMs > 0) {
Expand Down Expand Up @@ -1330,6 +1397,57 @@ static uint16_t cmsScanKeys(timeMs_t currentTimeMs, timeMs_t lastCalledMs, int16
return rcDelayMs;
}

static bool cmsIsNavModeActive(void)
{
return FLIGHT_MODE(NAV_POSHOLD_MODE) ||
FLIGHT_MODE(NAV_RTH_MODE) ||
FLIGHT_MODE(NAV_WP_MODE) ||
FLIGHT_MODE(NAV_ALTHOLD_MODE);
Comment on lines +1402 to +1405

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

2. Course-hold nav gate missing 🐞 Bug ≡ Correctness

cmsIsNavModeActive() is used to allow opening/keeping the in-flight CMS, but it omits
NAV_COURSE_HOLD_MODE. As a result, COURSE_HOLD will be treated as unsafe: the menu cannot be opened
in COURSE_HOLD and will also be force-closed if COURSE_HOLD is the only active nav mode.
Agent Prompt
### Issue description
`cmsIsNavModeActive()` does not consider `NAV_COURSE_HOLD_MODE` as an allowed stabilized mode for in-flight CMS. This blocks the feature (and triggers emergency close) in COURSE_HOLD.

### Issue Context
`NAV_COURSE_HOLD_MODE` is a first-class flight mode flag.

### Fix Focus Areas
- Add `FLIGHT_MODE(NAV_COURSE_HOLD_MODE)` to `cmsIsNavModeActive()`.
- Re-check both open and forced-close conditions that depend on this helper.

- src/main/cms/cms.c[1390-1396]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

}

static bool cmsDetectPanicStickMovement(timeMs_t currentTimeMs)
{
// Detect panicking pilot by checking for simultaneous multi-axis stick deflection.
//
// Normal CMS menu navigation is strictly single-axis:
// - Pitch only for scrolling items (Roll stays near center, max ~65 PWM crosstalk)
// - Roll only for changing values (Pitch stays near center)
// - Spring bounce after release is single-axis only
//
// A panicking pilot grabs the stick and moves it erratically, which always
// deflects both Roll AND Pitch simultaneously with significant force.
//
// Trigger: both Roll and Pitch deflected >100 PWM from center for 3 consecutive
// samples at 50ms intervals (150ms sustained). This gives zero false positives
// on real navigation data while catching all panic patterns within ~200ms.

static uint8_t dualAxisCount = 0;
static timeMs_t lastCheckMs = 0;

// Sample at ~20 Hz
if (currentTimeMs - lastCheckMs < 50) {
return false;
}
lastCheckMs = currentTimeMs;

const int16_t rollDev = ABS((int16_t)rxGetChannelValue(ROLL) - 1500);
const int16_t pitchDev = ABS((int16_t)rxGetChannelValue(PITCH) - 1500);

#define PANIC_DUAL_AXIS_THRESHOLD 100 // PWM deviation from center

if (rollDev > PANIC_DUAL_AXIS_THRESHOLD && pitchDev > PANIC_DUAL_AXIS_THRESHOLD) {
dualAxisCount++;
if (dualAxisCount >= 3) {
dualAxisCount = 0;
return true;
}
} else {
dualAxisCount = 0;
}

return false;
}

void cmsUpdate(uint32_t currentTimeUs)
{
#ifdef USE_RCDEVICE
Expand All @@ -1345,13 +1463,54 @@ void cmsUpdate(uint32_t currentTimeUs)

const timeMs_t currentTimeMs = currentTimeUs / 1000;

if (!IS_RC_MODE_ACTIVE(BOXUSER4)) {
cmsMenuSwitchLatched = false;
cmsOpenCountdownStartTime = 0;
}

if (!cmsInMenu) {
// Detect menu invocation
if (IS_MID(THROTTLE) && IS_LO(YAW) && IS_HI(PITCH) && !ARMING_FLAG(ARMED)) {
cmsMenuOpen();
rcDelayMs = BUTTON_PAUSE; // Tends to overshoot if BUTTON_TIME
}
// In-flight menu via BOXUSER4 mode - requires armed state, a NAV mode,
// and no active failsafe. Without a NAV mode the stick override would
// leave the aircraft with zeroed control inputs.
else if (IS_RC_MODE_ACTIVE(BOXUSER4) && ARMING_FLAG(ARMED)
&& cmsIsNavModeActive() && !FLIGHT_MODE(FAILSAFE_MODE)) {

if (!cmsMenuSwitchLatched) {
if (cmsOpenCountdownStartTime == 0) {
cmsOpenCountdownStartTime = millis();
} else if (millis() - cmsOpenCountdownStartTime >= 3000) {
cmsMenuOpen();
cmsOpenCountdownStartTime = 0;
rcDelayMs = BUTTON_PAUSE;
}
}
} else {
cmsOpenCountdownStartTime = 0;
}
} else {
// Close menu immediately if opened in-flight and any safety condition is lost:
// - BOXUSER4 switch deactivated (user wants to exit)
// - Aircraft disarmed
// - Failsafe activated (pilot must regain situational awareness)
// - NAV mode lost (stick override would leave aircraft without stabilization)
// - Panic / rapid / multi-axis stick movement detected (immediate evasive override)
if (cmsOpenedInFlight && (!IS_RC_MODE_ACTIVE(BOXUSER4) || !ARMING_FLAG(ARMED)
|| FLIGHT_MODE(FAILSAFE_MODE) || !cmsIsNavModeActive()
|| cmsDetectPanicStickMovement(currentTimeMs))) {
cmsMenuExit(pCurrentDisplay, (void *)CMS_EXIT);
return;
Comment thread
qodo-free-for-open-source-projects[bot] marked this conversation as resolved.
}

if (cmsOpenedInFlight && (currentTimeMs - cmsLastInputMs >= 15000)) {
cmsMenuExit(pCurrentDisplay, (void *)CMS_EXIT);
return;
}

displayBeginTransaction(pCurrentDisplay, DISPLAY_TRANSACTION_OPT_RESET_DRAWING);

// Check if we're yielding and its's time to stop it
Expand All @@ -1369,6 +1528,27 @@ void cmsUpdate(uint32_t currentTimeUs)
// Check again, the keypress might have produced a yield
if (cmsYieldUntil == 0) {
cmsDrawMenu(pCurrentDisplay, currentTimeUs);

static bool wasDrawingCountdown = false;
if (cmsOpenedInFlight) {
uint32_t elapsed = currentTimeMs - cmsLastInputMs;
if (elapsed >= 5000) {
uint32_t remaining = 15000 - elapsed;
unsigned sec = remaining / 1000;
char buf[22];
tfp_sprintf(buf, " CLOSING IN %u ", sec);
int col = (pCurrentDisplay->cols - strlen(buf)) / 2;
if (col < 0) col = 0;
displayWrite(pCurrentDisplay, col, pCurrentDisplay->rows - 1, buf);
wasDrawingCountdown = true;
} else if (wasDrawingCountdown) {
// Clear the bottom row
char buf[32] = " ";
buf[pCurrentDisplay->cols > 0 && pCurrentDisplay->cols < 32 ? pCurrentDisplay->cols : 30] = '\0';
displayWrite(pCurrentDisplay, 0, pCurrentDisplay->rows - 1, buf);
wasDrawingCountdown = false;
}
}
}
}

Expand Down
5 changes: 5 additions & 0 deletions src/main/cms/cms.h
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,11 @@ displayPort_t *cmsDisplayPortGetCurrent(void);
void cmsMenuOpen(void);
long cmsMenuChange(displayPort_t *pPort, const CMS_Menu *menu, const OSD_Entry *from);
long cmsMenuExit(displayPort_t *pPort, const void *ptr);

uint32_t cmsGetOpenCountdownRemaining(void);
uint32_t cmsGetInactivityCloseCountdownRemaining(void);
bool cmsIsMenuSwitchLatched(void);

void cmsYieldDisplay(displayPort_t *pPort, timeMs_t duration);
void cmsUpdate(uint32_t currentTimeUs);
void cmsSetExternKey(cms_key_e extKey);
Expand Down
38 changes: 37 additions & 1 deletion src/main/cms/cms_menu_battery.c
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@

#include "fc/config.h"
#include "fc/rc_controls.h"
#include "fc/runtime_config.h"
#include "fc/settings.h"

#include "sensors/battery.h"
Expand Down Expand Up @@ -62,6 +63,9 @@ static long cmsx_menuBattery_onExit(const OSD_Entry *self)

setConfigBatteryProfile(battProfileIndex);
activateBatteryProfile();
if (ARMING_FLAG(ARMED)) {
batteryUpdateThresholdsAndCells();
}
Comment thread
qodo-free-for-open-source-projects[bot] marked this conversation as resolved.

if (featureProfAutoswitchEnabled) {
featureSet(FEATURE_BAT_PROFILE_AUTOSWITCH);
Expand Down Expand Up @@ -93,6 +97,17 @@ static long cmsx_menuBattSettings_onEnter(const OSD_Entry *from)
return 0;
}

static long cmsx_menuBattSettings_onExit(const OSD_Entry *self)
{
UNUSED(self);

if (ARMING_FLAG(ARMED)) {
batteryUpdateThresholdsAndCells();
}

return 0;
}

static const OSD_Entry menuBattSettingsEntries[]=
{
OSD_LABEL_DATA_ENTRY("-- BATT SETTINGS --", battProfileIndexString),
Expand All @@ -118,7 +133,7 @@ static CMS_Menu cmsx_menuBattSettings = {
.GUARD_type = OME_MENU,
#endif
.onEnter = cmsx_menuBattSettings_onEnter,
.onExit = NULL,
.onExit = cmsx_menuBattSettings_onExit,
.onGlobalExit = NULL,
.entries = menuBattSettingsEntries
};
Expand Down Expand Up @@ -147,4 +162,25 @@ CMS_Menu cmsx_menuBattery = {
.entries = menuBatteryEntries
};

static OSD_Entry menuBatteryInFlightEntries[]=
{
OSD_LABEL_ENTRY("-- BATTERY --"),

OSD_UINT8_CALLBACK_ENTRY("PROF", cmsx_onBatteryProfileIndexChange, (&(const OSD_UINT8_t){ &battDispProfileIndex, 1, MAX_BATTERY_PROFILE_COUNT, 1})),
OSD_SUBMENU_ENTRY("SETTINGS", &cmsx_menuBattSettings),

OSD_BACK_AND_END_ENTRY,
};

const CMS_Menu cmsx_menuBatteryInFlight = {
#ifdef CMS_MENU_DEBUG
.GUARD_text = "XBATT_IF",
.GUARD_type = OME_MENU,
#endif
.onEnter = cmsx_menuBattery_onEnter,
.onExit = cmsx_menuBattery_onExit,
.onGlobalExit = NULL,
.entries = menuBatteryInFlightEntries
};

#endif // CMS
1 change: 1 addition & 0 deletions src/main/cms/cms_menu_battery.h
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,4 @@
#pragma once

extern CMS_Menu cmsx_menuBattery;
extern const CMS_Menu cmsx_menuBatteryInFlight;
Loading