Skip to content

Commit 1c8721b

Browse files
fix(color): run post-model-load warnings via a flat-stack state machine ((#7499))
On a model switch, postModelLoad() ran the throttle/switch warning checks synchronously from inside an LVGL event callback, each spinning MainWindow::blockUntilClose() which re-enters lv_timer_handler() from deep in the call chain. On hardware that re-entrant stack overflows the menus task and hard-faults (intermittent freeze on TX16S internal MPM); the simulator's larger stack hid it. Replace the nested blocking loop on COLORLCD with a core-owned, GUI-agnostic state machine (model_load_sm) polled once per perMain() (flat stack, no re-entrancy). It walks the post-load checks (SD / throttle / switches / failsafe / multi / checklist) and only once all are cleared runs the deferred model-load tail (postModelLoadFinish: pulsesStart + ...). Pulses stay stopped until then, preserving the failsafe-hold safety guarantee. A thin COLORLCD view (model_load_view) mirrors the active state into the existing warning dialogs and feeds key presses back as an acknowledge. Decouple the warning predicates from input refresh so they are pure reads: refreshInputsForWarnings() (getADC + evalInputs + getMovedSwitch) is now called by the drivers (state machine tick and the boot/flightReset blocking loops), and the warning dialogs become display-only views. Boot and flightReset keep their non-re-entrant blocking checkAll() and reuse the same predicates. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent eae6aa1 commit 1c8721b

16 files changed

Lines changed: 529 additions & 46 deletions

radio/src/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -451,6 +451,7 @@ endif()
451451
set(SRC
452452
${SRC}
453453
edgetx.cpp
454+
model_load_sm.cpp
454455
datastructs_model.cpp
455456
datastructs_radio.cpp
456457
functions.cpp

radio/src/edgetx.cpp

Lines changed: 47 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -676,17 +676,20 @@ void resetBacklightTimeout()
676676

677677

678678
#if defined(MULTIMODULE)
679-
void checkMultiLowPower()
679+
bool isMultiLowPowerWarningRequired()
680680
{
681-
bool low_power_warning = false;
682681
for (uint8_t i = 0; i < MAX_MODULES; i++) {
683682
if (isModuleMultimodule(i) &&
684683
g_model.moduleData[i].multi.lowPowerMode) {
685-
low_power_warning = true;
684+
return true;
686685
}
687686
}
687+
return false;
688+
}
688689

689-
if (low_power_warning) {
690+
void checkMultiLowPower()
691+
{
692+
if (isMultiLowPowerWarningRequired()) {
690693
ALERT("MULTI", STR_WARN_MULTI_LOWPOWER, AU_ERROR);
691694
}
692695
}
@@ -706,7 +709,7 @@ void checkSDfreeStorage() {
706709
}
707710
}
708711

709-
static void checkFailsafe()
712+
bool isFailsafeWarningRequired()
710713
{
711714
for (int i=0; i<NUM_MODULES; i++) {
712715
#if defined(MULTIMODULE)
@@ -716,11 +719,18 @@ static void checkFailsafe()
716719
if (isModuleFailsafeAvailable(i)) {
717720
ModuleData & moduleData = g_model.moduleData[i];
718721
if (moduleData.failsafeMode == FAILSAFE_NOT_SET) {
719-
ALERT(STR_FAILSAFEWARN, STR_NO_FAILSAFE, AU_ERROR);
720-
break;
722+
return true;
721723
}
722724
}
723725
}
726+
return false;
727+
}
728+
729+
static void checkFailsafe()
730+
{
731+
if (isFailsafeWarningRequired()) {
732+
ALERT(STR_FAILSAFEWARN, STR_NO_FAILSAFE, AU_ERROR);
733+
}
724734
}
725735

726736
#if defined(GUI)
@@ -802,6 +812,17 @@ void checkAll(bool isBootCheck)
802812
}
803813
#endif // GUI
804814

815+
// Refresh the input readings the warning predicates rely on. Kept separate from
816+
// the predicates themselves so they stay pure: the driver (the model-load state
817+
// machine, or the boot/flightReset blocking loops) refreshes once per tick, then
818+
// queries the predicates and the warning views read the same fresh state.
819+
void refreshInputsForWarnings()
820+
{
821+
if (!mixerTaskRunning()) getADC();
822+
evalInputs(e_perout_mode_notrainer);
823+
getMovedSwitch();
824+
}
825+
805826
bool isThrottleWarningAlertNeeded()
806827
{
807828
if (g_model.disableThrottleWarning) {
@@ -816,9 +837,6 @@ bool isThrottleWarningAlertNeeded()
816837
thr_src = throttleSource2Source(0);
817838
}
818839

819-
if (!mixerTaskRunning()) getADC();
820-
evalInputs(e_perout_mode_notrainer); // let do evalInputs do the job
821-
822840
int16_t v = getValue(thr_src);
823841

824842
// TODO: this looks fishy....
@@ -844,25 +862,31 @@ bool isThrottleWarningAlertNeeded()
844862
void checkThrottleStick()
845863
{
846864
char throttleNotIdle[strlen(STR_THROTTLE_NOT_IDLE) + 9];
847-
if (isThrottleWarningAlertNeeded()) {
848-
if (g_model.enableCustomThrottleWarning) {
865+
refreshInputsForWarnings();
866+
if (!isThrottleWarningAlertNeeded()) return;
867+
868+
if (g_model.enableCustomThrottleWarning) {
849869
sprintf(throttleNotIdle, "%s (%d%%)", STR_THROTTLE_NOT_IDLE, g_model.customThrottleWarningPosition);
850-
}
851-
else {
852-
strcpy(throttleNotIdle, STR_THROTTLE_NOT_IDLE);
853-
}
854-
LED_ERROR_BEGIN();
855-
auto dialog = new ThrottleWarnDialog(throttleNotIdle);
856-
MainWindow::instance()->blockUntilClose(true, [=]() {
857-
return dialog->deleted();
858-
});
859-
LED_ERROR_END();
870+
} else {
871+
strcpy(throttleNotIdle, STR_THROTTLE_NOT_IDLE);
860872
}
873+
874+
LED_ERROR_BEGIN();
875+
// The dialog is a dumb view; this loop owns the refresh + predicate and closes
876+
// when the stick is lowered (predicate clears) or a key is pressed (deleted).
877+
auto dialog = new ThrottleWarnDialog(throttleNotIdle);
878+
MainWindow::instance()->blockUntilClose(true, [=]() {
879+
refreshInputsForWarnings();
880+
return dialog->deleted() || !isThrottleWarningAlertNeeded();
881+
});
882+
if (!dialog->deleted()) dialog->deleteLater();
883+
LED_ERROR_END();
861884
}
862885
#else
863886
void checkThrottleStick()
864887
{
865888
char throttleNotIdle[strlen(STR_THROTTLE_NOT_IDLE) + 9];
889+
refreshInputsForWarnings();
866890
if (!isThrottleWarningAlertNeeded()) {
867891
return;
868892
}
@@ -881,6 +905,7 @@ void checkThrottleStick()
881905
#endif
882906

883907
while (!keyDown()) {
908+
refreshInputsForWarnings();
884909
if (!isThrottleWarningAlertNeeded()) {
885910
return;
886911
}

radio/src/edgetx.h

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -329,6 +329,17 @@ void checkSwitches();
329329
void checkAlarm();
330330
void checkAll(bool isBootCheck = false);
331331

332+
// Warning predicates (pure reads) and the input refresh they rely on. The
333+
// driver (boot/flightReset loops or the model-load state machine) calls
334+
// refreshInputsForWarnings() once per tick, then queries the predicates.
335+
void refreshInputsForWarnings();
336+
bool isThrottleWarningAlertNeeded();
337+
bool isFailsafeWarningRequired();
338+
void checkSDfreeStorage();
339+
#if defined(MULTIMODULE)
340+
bool isMultiLowPowerWarningRequired();
341+
#endif
342+
332343
void getADC();
333344

334345
#include "sbus.h"

radio/src/gui/colorlcd/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ set(GUI_SRC
5555
lcd.cpp
5656
LvglWrapper.cpp
5757
startup_shutdown.cpp
58+
model_load_view.cpp
5859

5960
model/curveedit.cpp
6061
model/input_edit.cpp

radio/src/gui/colorlcd/controls/switch_warn_dialog.cpp

Lines changed: 3 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -36,12 +36,9 @@ void SwitchWarnDialog::checkEvents()
3636
{
3737
FullScreenDialog::checkEvents();
3838

39-
uint16_t bad_pots;
40-
if (!isSwitchWarningRequired(bad_pots)) {
41-
deleteLater();
42-
return;
43-
};
44-
39+
// Display only — the driver (boot/flightReset loop or the model-load state
40+
// machine) has already refreshed the inputs and owns the close decision. Here
41+
// we just rebuild the list of switches/pots still in the wrong position.
4542
std::string warn_txt;
4643
for (int i = 0; i < switchGetMaxAllSwitches(); ++i) {
4744
if (SWITCH_WARNING_ALLOWED(i)) {
@@ -85,12 +82,3 @@ ThrottleWarnDialog::ThrottleWarnDialog(const char* msg) :
8582
lv_label_set_long_mode(messageLabel->getLvObj(), LV_LABEL_LONG_WRAP);
8683
AUDIO_ERROR_MESSAGE(AU_THROTTLE_ALERT);
8784
}
88-
89-
void ThrottleWarnDialog::checkEvents()
90-
{
91-
FullScreenDialog::checkEvents();
92-
93-
extern bool isThrottleWarningAlertNeeded();
94-
if (!isThrottleWarningAlertNeeded())
95-
deleteLater();
96-
}

radio/src/gui/colorlcd/controls/switch_warn_dialog.h

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,11 @@
2525
#include "mainwindow.h"
2626
#include "edgetx.h"
2727

28+
// Dumb display-only dialogs. Their driver (the boot/flightReset blocking loops
29+
// or the model-load state machine) owns the input refresh, the warning
30+
// predicate and the close decision. Closing on a key press self-deletes as
31+
// usual (default FullScreenDialog behaviour); for the state machine the
32+
// model-load view reports that close as an acknowledge.
2833
class SwitchWarnDialog : public FullScreenDialog
2934
{
3035
public:
@@ -35,6 +40,7 @@ class SwitchWarnDialog : public FullScreenDialog
3540
#endif
3641

3742
protected:
43+
// rebuilds the bad-switch/pot list text each frame (display only)
3844
void checkEvents() override;
3945
};
4046

@@ -46,7 +52,4 @@ class ThrottleWarnDialog : public FullScreenDialog
4652
#if defined(DEBUG_WINDOWS)
4753
std::string getName() const override { return "ThrottleWarnDialog"; }
4854
#endif
49-
50-
protected:
51-
void checkEvents() override;
5255
};

radio/src/gui/colorlcd/libui/view_text.cpp

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -519,6 +519,12 @@ void readChecklist()
519519
}
520520
}
521521

522+
// Non-blocking version, driven by the model-load state machine view.
523+
Window* showModelChecklist()
524+
{
525+
return _readModelNotes(false);
526+
}
527+
522528
ModelNotesPage::ModelNotesPage(const PageDef& pageDef) : PageGroupItem(pageDef, PAD_ZERO)
523529
{
524530
}

radio/src/gui/colorlcd/libui/view_text.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,3 +67,8 @@ class ModelNotesPage : public PageGroupItem
6767

6868
void readModelNotes(bool fromMenu = false);
6969
void readChecklist();
70+
71+
// Non-blocking variant of readChecklist(): opens the model notes/checklist
72+
// window and returns it (or nullptr if there is none) without spinning a UI
73+
// loop. Used by the model-load state machine view.
74+
Window* showModelChecklist();
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
/*
2+
* Copyright (C) EdgeTX
3+
*
4+
* Based on code named
5+
* opentx - https://github.com/opentx/opentx
6+
* th9x - http://code.google.com/p/th9x
7+
* er9x - http://code.google.com/p/er9x
8+
* gruvin9x - http://code.google.com/p/gruvin9x
9+
*
10+
* License GPLv2: http://www.gnu.org/licenses/gpl-2.0.html
11+
*
12+
* This program is free software; you can redistribute it and/or modify
13+
* it under the terms of the GNU General Public License version 2 as
14+
* published by the Free Software Foundation.
15+
*
16+
* This program is distributed in the hope that it will be useful,
17+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
18+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19+
* GNU General Public License for more details.
20+
*/
21+
22+
#include "model_load_view.h"
23+
24+
#include "edgetx.h"
25+
#include "model_load_sm.h"
26+
#include "controls/switch_warn_dialog.h"
27+
#include "libui/fullscreen_dialog.h"
28+
#include "libui/view_text.h"
29+
30+
// The single warning dialog currently displayed for the active machine state,
31+
// and the state it corresponds to (so we only (re)build on state changes).
32+
static Window* s_shown = nullptr;
33+
static ModelLoadState s_shownState = MLS_IDLE;
34+
// set while we tear a dialog down ourselves, to distinguish a view-initiated
35+
// close (no acknowledge) from a user/condition close (acknowledge).
36+
static bool s_tearingDown = false;
37+
38+
// Invoked from Window::deleteLater() (before the window is trashed/freed) when
39+
// the dialog we are showing closes — either because its warning condition
40+
// cleared, or the user pressed a key. We must react here rather than polling
41+
// s_shown->deleted() afterwards, since perMain's MainWindow::run() empties the
42+
// trash (freeing the window) before the next modelLoadViewSync().
43+
static void onShownClosed()
44+
{
45+
s_shown = nullptr;
46+
if (!s_tearingDown) modelLoadAcknowledge();
47+
}
48+
49+
static FullScreenDialog* makeAckAlert(const char* title, const char* msg)
50+
{
51+
AUDIO_ERROR_MESSAGE(AU_ERROR);
52+
return new FullScreenDialog(WARNING_TYPE_ALERT, title, msg,
53+
STR_PRESS_ANY_KEY_TO_SKIP);
54+
}
55+
56+
void modelLoadViewSync()
57+
{
58+
// Suspend main-view widget refresh for the whole model-load sequence: the
59+
// model is being swapped and the warning view covers the main view anyway.
60+
// Edge-triggered so we don't fight other users of the flag (standalone Lua).
61+
static bool s_seqActive = false;
62+
bool active = !modelLoadIdle();
63+
if (active != s_seqActive) {
64+
s_seqActive = active;
65+
MainWindow::instance()->enableWidgetRefresh(!active);
66+
}
67+
68+
ModelLoadState want = modelLoadActiveWarning();
69+
if (want == s_shownState) return;
70+
71+
// Tear down a dialog that is still up (e.g. the sequence was restarted by a
72+
// new model switch). Suppress the acknowledge for this view-initiated close.
73+
if (s_shown) {
74+
s_tearingDown = true;
75+
s_shown->deleteLater(); // fires onShownClosed -> s_shown = nullptr
76+
s_tearingDown = false;
77+
}
78+
s_shownState = want;
79+
80+
Window* dlg = nullptr;
81+
switch (want) {
82+
case MLS_CHECK_THROTTLE:
83+
LED_ERROR_BEGIN();
84+
dlg = new ThrottleWarnDialog(modelLoadWarningText());
85+
break;
86+
case MLS_CHECK_SWITCHES:
87+
LED_ERROR_BEGIN();
88+
dlg = new SwitchWarnDialog();
89+
break;
90+
case MLS_CHECK_SD:
91+
LED_ERROR_BEGIN();
92+
dlg = makeAckAlert(STR_SD_CARD, STR_SDCARD_FULL);
93+
break;
94+
case MLS_CHECK_FAILSAFE:
95+
LED_ERROR_BEGIN();
96+
dlg = makeAckAlert(STR_FAILSAFEWARN, STR_NO_FAILSAFE);
97+
break;
98+
#if defined(MULTIMODULE)
99+
case MLS_CHECK_MULTI:
100+
LED_ERROR_BEGIN();
101+
dlg = makeAckAlert("MULTI", STR_WARN_MULTI_LOWPOWER);
102+
break;
103+
#endif
104+
case MLS_CHECK_CHECKLIST:
105+
cancelSplash();
106+
dlg = showModelChecklist();
107+
// no notes window could be opened: don't stall the sequence
108+
if (!dlg) modelLoadAcknowledge();
109+
break;
110+
case MLS_IDLE:
111+
default:
112+
// sequence finished: clear the error indication
113+
LED_ERROR_END();
114+
break;
115+
}
116+
117+
if (dlg) {
118+
dlg->setCloseHandler(onShownClosed);
119+
s_shown = dlg;
120+
}
121+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
/*
2+
* Copyright (C) EdgeTX
3+
*
4+
* Based on code named
5+
* opentx - https://github.com/opentx/opentx
6+
* th9x - http://code.google.com/p/th9x
7+
* er9x - http://code.google.com/p/er9x
8+
* gruvin9x - http://code.google.com/p/gruvin9x
9+
*
10+
* License GPLv2: http://www.gnu.org/licenses/gpl-2.0.html
11+
*
12+
* This program is free software; you can redistribute it and/or modify
13+
* it under the terms of the GNU General Public License version 2 as
14+
* published by the Free Software Foundation.
15+
*
16+
* This program is distributed in the hope that it will be useful,
17+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
18+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19+
* GNU General Public License for more details.
20+
*/
21+
22+
#pragma once
23+
24+
// COLORLCD view for the model-load state machine. Polled once per perMain()
25+
// iteration (after MainWindow::run): it mirrors the machine's active warning
26+
// into the matching full-screen dialog and reports user "skip" key presses back
27+
// to the machine. The machine owns all the logic; this is display only.
28+
void modelLoadViewSync();

0 commit comments

Comments
 (0)