diff --git a/radio/src/CMakeLists.txt b/radio/src/CMakeLists.txt index 8c66a614752..2184214bece 100644 --- a/radio/src/CMakeLists.txt +++ b/radio/src/CMakeLists.txt @@ -451,6 +451,7 @@ endif() set(SRC ${SRC} edgetx.cpp + warning_checks.cpp datastructs_model.cpp datastructs_radio.cpp functions.cpp diff --git a/radio/src/edgetx.cpp b/radio/src/edgetx.cpp index c269d438c77..e57e31c461b 100644 --- a/radio/src/edgetx.cpp +++ b/radio/src/edgetx.cpp @@ -42,6 +42,7 @@ #include "timers_driver.h" #include "switches.h" +#include "warning_checks.h" #include "inactivity_timer.h" #include "input_mapping.h" #include "trainer.h" @@ -675,257 +676,6 @@ void resetBacklightTimeout() } -#if defined(MULTIMODULE) -void checkMultiLowPower() -{ - bool low_power_warning = false; - for (uint8_t i = 0; i < MAX_MODULES; i++) { - if (isModuleMultimodule(i) && - g_model.moduleData[i].multi.lowPowerMode) { - low_power_warning = true; - } - } - - if (low_power_warning) { - ALERT("MULTI", STR_WARN_MULTI_LOWPOWER, AU_ERROR); - } -} -#endif - -static void checkRTCBattery() -{ - if (!mixerTaskRunning()) getADC(); - if (getRTCBatteryVoltage() < 200) { - ALERT(STR_BATTERY, STR_WARN_RTC_BATTERY_LOW, AU_ERROR); - } -} - -void checkSDfreeStorage() { - if(sdIsFull()) { - ALERT(STR_SD_CARD, STR_SDCARD_FULL, AU_ERROR); - } -} - -static void checkFailsafe() -{ - for (int i=0; isetMessage(strKeys.c_str()); - MainWindow::instance()->blockUntilClose(true, [=]() { - if (dlg->deleted()) return true; - if ((tgtime < get_tmr10ms()) || !keyDown()) { - dlg->deleteLater(); - return true; - } - return false; - }); - LED_ERROR_END(); - } -#else - if (!waitKeysReleased()) { - showMessageBox(STR_KEYSTUCK); - tmr10ms_t tgtime = get_tmr10ms() + 500; - while (tgtime != get_tmr10ms()) { - sleep_ms(1); - WDG_RESET(); - } - } -#endif - -#if defined(EXTERNAL_ANTENNA) && defined(INTERNAL_MODULE_PXX1) - checkExternalAntenna(); -#endif - - START_SILENCE_PERIOD(); -} -#endif // GUI - -bool isThrottleWarningAlertNeeded() -{ - if (g_model.disableThrottleWarning) { - return false; - } - - uint8_t thr_src = throttleSource2Source(g_model.thrTraceSrc); - - // in case an output channel is choosen as throttle source - // we assume the throttle stick is the input (no computed channels yet) - if (thr_src >= MIXSRC_FIRST_CH) { - thr_src = throttleSource2Source(0); - } - - if (!mixerTaskRunning()) getADC(); - evalInputs(e_perout_mode_notrainer); // let do evalInputs do the job - - int16_t v = getValue(thr_src); - - // TODO: this looks fishy.... - if (g_model.thrTraceSrc && g_model.throttleReversed) { - v = -v; - } - - if (g_model.enableCustomThrottleWarning) { - int16_t idleValue = (int32_t)RESX * - (int32_t)g_model.customThrottleWarningPosition / - (int32_t)100; - return abs(v - idleValue) > THRCHK_DEADBAND; - } else { -#if defined(SURFACE_RADIO) // surface radio, stick centered - return v > THRCHK_DEADBAND; -#else - return v > THRCHK_DEADBAND - RESX; -#endif - } -} - -#if defined(COLORLCD) -void checkThrottleStick() -{ - char throttleNotIdle[strlen(STR_THROTTLE_NOT_IDLE) + 9]; - if (isThrottleWarningAlertNeeded()) { - if (g_model.enableCustomThrottleWarning) { - sprintf(throttleNotIdle, "%s (%d%%)", STR_THROTTLE_NOT_IDLE, g_model.customThrottleWarningPosition); - } - else { - strcpy(throttleNotIdle, STR_THROTTLE_NOT_IDLE); - } - LED_ERROR_BEGIN(); - auto dialog = new ThrottleWarnDialog(throttleNotIdle); - MainWindow::instance()->blockUntilClose(true, [=]() { - return dialog->deleted(); - }); - LED_ERROR_END(); - } -} -#else -void checkThrottleStick() -{ - char throttleNotIdle[strlen(STR_THROTTLE_NOT_IDLE) + 9]; - if (!isThrottleWarningAlertNeeded()) { - return; - } - if (g_model.enableCustomThrottleWarning) { - sprintf(throttleNotIdle, "%s (%d%%)", STR_THROTTLE_NOT_IDLE, g_model.customThrottleWarningPosition); - } - else { - strcpy(throttleNotIdle, STR_THROTTLE_NOT_IDLE); - } - // first - display warning; also deletes inputs if any have been before - LED_ERROR_BEGIN(); - RAISE_ALERT(STR_THROTTLE_UPPERCASE, throttleNotIdle, STR_PRESS_ANY_KEY_TO_SKIP, AU_THROTTLE_ALERT); - -#if defined(PWR_BUTTON_PRESS) - bool refresh = false; -#endif - - while (!keyDown()) { - if (!isThrottleWarningAlertNeeded()) { - return; - } - -#if defined(PWR_BUTTON_PRESS) - uint32_t power = pwrCheck(); - if (power == e_power_off) { - drawSleepBitmap(); - boardOff(); - break; - } - else if (power == e_power_press) { - refresh = true; - } - else if (power == e_power_on && refresh) { - RAISE_ALERT(STR_THROTTLE_UPPERCASE, throttleNotIdle, STR_PRESS_ANY_KEY_TO_SKIP, AU_NONE); - refresh = false; - } -#else - if (pwrCheck() == e_power_off) { - break; - } -#endif - - checkBacklight(); - - WDG_RESET(); - sleep_ms(10); - } - - LED_ERROR_END(); -} -#endif - -void checkAlarm() // added by Gohst -{ - if (g_eeGeneral.disableAlarmWarning) { - return; - } - - if (IS_SOUND_OFF()) { - ALERT(STR_ALARMSWARN, STR_ALARMSDISABLED, AU_ERROR); - } -} - #if !defined(COLORLCD) void alert(const char * title, const char * msg , uint8_t sound) { @@ -1133,7 +883,12 @@ void flightReset(uint8_t check) logicalSwitchesReset(); if (check) { - checkAll(); + // Run the warning checks through the model-load state machine (pumped by + // perMain), non-blocking. Pulses are left running (WCC_FLIGHT_RESET terminal + // does not touch them, matching the old behaviour). The REQUEST_FLIGHT_RESET + // handler gates this on warningChecksIdle() so it can't clobber a sequence already + // in progress. + warningChecksStart(WCC_FLIGHT_RESET); } } @@ -1626,8 +1381,11 @@ void edgeTxInit() } else if (!(startOptions & OPENTX_START_NO_CHECKS)) { checkAlarm(); - checkAll(true); - PLAY_MODEL_NAME(); + checkBootSpecificWarnings(); + // Arm the model-load state machine for the post-load warning sequence; it is + // pumped by perMain() once the main loop starts. It announces the model and + // starts pulses (WCC_BOOT terminal), so pulses stay held until checks clear. + warningChecksStart(WCC_BOOT); } #endif // defined(GUI) @@ -1646,7 +1404,11 @@ void edgeTxInit() LED_ERROR_END(); - pulsesStart(); + // The normal-checks branch armed the model-load state machine (warningChecksIdle() + // is then false); it starts pulses at its terminal state once perMain() has + // pumped it, keeping RF silent until the boot checks clear. The calibration and + // OPENTX_START_NO_CHECKS paths don't arm it, so start pulses immediately here. + if (warningChecksIdle()) pulsesStart(); WDG_ENABLE(WDG_DURATION); } diff --git a/radio/src/edgetx.h b/radio/src/edgetx.h index 3e84b3bedff..d7738102e4e 100644 --- a/radio/src/edgetx.h +++ b/radio/src/edgetx.h @@ -323,12 +323,6 @@ void flightReset(uint8_t check=true); #define RESET_THR_TRACE() s_timeCum16ThrP = s_timeCumThr = 0 #endif -void checkLowEEPROM(); -void checkThrottleStick(); -void checkSwitches(); -void checkAlarm(); -void checkAll(bool isBootCheck = false); - void getADC(); #include "sbus.h" diff --git a/radio/src/gui/128x64/radio_setup.cpp b/radio/src/gui/128x64/radio_setup.cpp index 8fb4112afd6..62385a25088 100644 --- a/radio/src/gui/128x64/radio_setup.cpp +++ b/radio/src/gui/128x64/radio_setup.cpp @@ -22,6 +22,7 @@ #define LANGUAGE_PACKS_DEFINITION #include "edgetx.h" +#include "warning_checks.h" #include "tasks/mixer_task.h" #include "hal/adc_driver.h" #include "hal/usb_driver.h" @@ -832,9 +833,9 @@ void menuRadioSetup(event_t event) g_eeGeneral.stickMode) { mixerTaskStop(); g_eeGeneral.stickMode = reusableBuffer.generalSettings.stickMode; - checkThrottleStick(); - mixerTaskStart(); - waitKeysReleased(); + // Throttle warning runs through the model-load state machine (pumped by + // perMain); its terminal restarts the mixer we just stopped. + warningChecksStart(WCC_STICK_MODE); } break; #endif diff --git a/radio/src/gui/212x64/radio_setup.cpp b/radio/src/gui/212x64/radio_setup.cpp index b373c082f69..9b2f452c6a7 100644 --- a/radio/src/gui/212x64/radio_setup.cpp +++ b/radio/src/gui/212x64/radio_setup.cpp @@ -26,6 +26,7 @@ #define LANGUAGE_PACKS_DEFINITION #include "edgetx.h" +#include "warning_checks.h" #include "tasks/mixer_task.h" #include "input_mapping.h" @@ -811,9 +812,9 @@ void menuRadioSetup(event_t event) else if (mode != g_eeGeneral.stickMode) { mixerTaskStop(); g_eeGeneral.stickMode = mode; - checkThrottleStick(); - mixerTaskStart(); - waitKeysReleased(); + // Throttle warning runs through the model-load state machine (pumped + // by perMain); its terminal restarts the mixer we just stopped. + warningChecksStart(WCC_STICK_MODE); } } break; diff --git a/radio/src/gui/colorlcd/CMakeLists.txt b/radio/src/gui/colorlcd/CMakeLists.txt index a6c1970aa4d..2f52e9e7676 100644 --- a/radio/src/gui/colorlcd/CMakeLists.txt +++ b/radio/src/gui/colorlcd/CMakeLists.txt @@ -55,6 +55,7 @@ set(GUI_SRC lcd.cpp LvglWrapper.cpp startup_shutdown.cpp + warning_checks_view.cpp model/curveedit.cpp model/input_edit.cpp diff --git a/radio/src/gui/colorlcd/controls/switch_warn_dialog.cpp b/radio/src/gui/colorlcd/controls/switch_warn_dialog.cpp index e8301e1d03e..eaf91694134 100644 --- a/radio/src/gui/colorlcd/controls/switch_warn_dialog.cpp +++ b/radio/src/gui/colorlcd/controls/switch_warn_dialog.cpp @@ -36,12 +36,9 @@ void SwitchWarnDialog::checkEvents() { FullScreenDialog::checkEvents(); - uint16_t bad_pots; - if (!isSwitchWarningRequired(bad_pots)) { - deleteLater(); - return; - }; - + // Display only — the driver (boot/flightReset loop or the model-load state + // machine) has already refreshed the inputs and owns the close decision. Here + // we just rebuild the list of switches/pots still in the wrong position. std::string warn_txt; for (int i = 0; i < switchGetMaxAllSwitches(); ++i) { if (SWITCH_WARNING_ALLOWED(i)) { @@ -85,12 +82,3 @@ ThrottleWarnDialog::ThrottleWarnDialog(const char* msg) : lv_label_set_long_mode(messageLabel->getLvObj(), LV_LABEL_LONG_WRAP); AUDIO_ERROR_MESSAGE(AU_THROTTLE_ALERT); } - -void ThrottleWarnDialog::checkEvents() -{ - FullScreenDialog::checkEvents(); - - extern bool isThrottleWarningAlertNeeded(); - if (!isThrottleWarningAlertNeeded()) - deleteLater(); -} diff --git a/radio/src/gui/colorlcd/controls/switch_warn_dialog.h b/radio/src/gui/colorlcd/controls/switch_warn_dialog.h index cf0bed8b7d4..a0eade041e7 100644 --- a/radio/src/gui/colorlcd/controls/switch_warn_dialog.h +++ b/radio/src/gui/colorlcd/controls/switch_warn_dialog.h @@ -25,6 +25,11 @@ #include "mainwindow.h" #include "edgetx.h" +// Dumb display-only dialogs. Their driver (the boot/flightReset blocking loops +// or the model-load state machine) owns the input refresh, the warning +// predicate and the close decision. Closing on a key press self-deletes as +// usual (default FullScreenDialog behaviour); for the state machine the +// model-load view reports that close as an acknowledge. class SwitchWarnDialog : public FullScreenDialog { public: @@ -35,6 +40,7 @@ class SwitchWarnDialog : public FullScreenDialog #endif protected: + // rebuilds the bad-switch/pot list text each frame (display only) void checkEvents() override; }; @@ -46,7 +52,4 @@ class ThrottleWarnDialog : public FullScreenDialog #if defined(DEBUG_WINDOWS) std::string getName() const override { return "ThrottleWarnDialog"; } #endif - - protected: - void checkEvents() override; }; diff --git a/radio/src/gui/colorlcd/libui/view_text.cpp b/radio/src/gui/colorlcd/libui/view_text.cpp index 3605ea37c53..581849753aa 100644 --- a/radio/src/gui/colorlcd/libui/view_text.cpp +++ b/radio/src/gui/colorlcd/libui/view_text.cpp @@ -506,17 +506,10 @@ void readModelNotes(bool fromMenu) _readModelNotes(fromMenu); } -// Blocking version of readModelNotes. -void readChecklist() +// Non-blocking version, driven by the model-load state machine view. +Window* showModelChecklist() { - auto dialog = _readModelNotes(false); - if (dialog) { - LED_ERROR_BEGIN(); - MainWindow::instance()->blockUntilClose(true, [=]() { - return dialog->deleted(); - }); - LED_ERROR_END(); - } + return _readModelNotes(false); } ModelNotesPage::ModelNotesPage(const PageDef& pageDef) : PageGroupItem(pageDef, PAD_ZERO) diff --git a/radio/src/gui/colorlcd/libui/view_text.h b/radio/src/gui/colorlcd/libui/view_text.h index 901e67c9640..87969e0a19b 100644 --- a/radio/src/gui/colorlcd/libui/view_text.h +++ b/radio/src/gui/colorlcd/libui/view_text.h @@ -66,4 +66,7 @@ class ModelNotesPage : public PageGroupItem }; void readModelNotes(bool fromMenu = false); -void readChecklist(); + +// Opens the model notes/checklist window and returns it (or nullptr if there is +// none) without spinning a UI loop. Driven by the model-load state machine view. +Window* showModelChecklist(); diff --git a/radio/src/gui/colorlcd/radio/radio_setup.cpp b/radio/src/gui/colorlcd/radio/radio_setup.cpp index d10caa4c7b5..39282c65002 100644 --- a/radio/src/gui/colorlcd/radio/radio_setup.cpp +++ b/radio/src/gui/colorlcd/radio/radio_setup.cpp @@ -26,6 +26,7 @@ #include "choice.h" #include "dialog.h" #include "edgetx.h" +#include "warning_checks.h" #include "getset_helpers.h" #include "hal/adc_driver.h" #include "hal/audio_driver.h" @@ -1016,8 +1017,10 @@ const static SetupLineDef setupLines[] = { mixerTaskStop(); g_eeGeneral.stickMode = newValue; SET_DIRTY(); - checkThrottleStick(); - mixerTaskStart(); + // Throttle warning runs through the model-load state + // machine (pumped by perMain, flat stack — no nested + // LVGL run); its terminal restarts the mixer. + warningChecksStart(WCC_STICK_MODE); }); choice->setTextHandler([](uint8_t value) { auto stick0 = inputMappingConvertMode(value, 0); diff --git a/radio/src/gui/colorlcd/warning_checks_view.cpp b/radio/src/gui/colorlcd/warning_checks_view.cpp new file mode 100644 index 00000000000..f68211e57c5 --- /dev/null +++ b/radio/src/gui/colorlcd/warning_checks_view.cpp @@ -0,0 +1,126 @@ +/* + * Copyright (C) EdgeTX + * + * Based on code named + * opentx - https://github.com/opentx/opentx + * th9x - http://code.google.com/p/th9x + * er9x - http://code.google.com/p/er9x + * gruvin9x - http://code.google.com/p/gruvin9x + * + * License GPLv2: http://www.gnu.org/licenses/gpl-2.0.html + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + */ + +#include "warning_checks_view.h" + +#include "edgetx.h" +#include "warning_checks.h" +#include "controls/switch_warn_dialog.h" +#include "libui/fullscreen_dialog.h" +#include "libui/view_text.h" + +// The single warning dialog currently displayed for the active machine state, +// and the state it corresponds to (so we only (re)build on state changes). +static Window* s_shown = nullptr; +static WarningCheckState s_shownState = WCS_IDLE; +// set while we tear a dialog down ourselves, to distinguish a view-initiated +// close (no acknowledge) from a user/condition close (acknowledge). +static bool s_tearingDown = false; + +// Invoked from Window::deleteLater() (before the window is trashed/freed) when +// the dialog we are showing closes — either because its warning condition +// cleared, or the user pressed a key. We must react here rather than polling +// s_shown->deleted() afterwards, since perMain's MainWindow::run() empties the +// trash (freeing the window) before the next warningChecksViewSync(). +static void onShownClosed() +{ + s_shown = nullptr; + if (!s_tearingDown) acknowledgeWarningCheck(); +} + +static FullScreenDialog* makeAckAlert(const char* title, const char* msg) +{ + AUDIO_ERROR_MESSAGE(AU_ERROR); + return new FullScreenDialog(WARNING_TYPE_ALERT, title, msg, + STR_PRESS_ANY_KEY_TO_SKIP); +} + +void warningChecksViewUpdateRefresh() +{ + // Suspend main-view widget refresh for the whole model-load sequence: the + // model is being swapped and the warning view covers the main view anyway. + // Called before MainWindow::run() so the widgets are never repainted for a + // tick while a sequence is active. Edge-triggered so we don't fight other + // users of the flag (standalone Lua). + static bool s_seqActive = false; + bool active = !warningChecksIdle(); + if (active != s_seqActive) { + s_seqActive = active; + MainWindow::instance()->enableWidgetRefresh(!active); + } +} + +void warningChecksViewSync() +{ + WarningCheckState want = activeWarningCheck(); + if (want == s_shownState) return; + + // Tear down a dialog that is still up (e.g. the sequence was restarted by a + // new model switch). Suppress the acknowledge for this view-initiated close. + if (s_shown) { + s_tearingDown = true; + s_shown->deleteLater(); // fires onShownClosed -> s_shown = nullptr + s_tearingDown = false; + } + s_shownState = want; + + Window* dlg = nullptr; + switch (want) { + case WCS_CHECK_THROTTLE: + LED_ERROR_BEGIN(); + dlg = new ThrottleWarnDialog(warningCheckText()); + break; + case WCS_CHECK_SWITCHES: + LED_ERROR_BEGIN(); + dlg = new SwitchWarnDialog(); + break; + case WCS_CHECK_SD: + LED_ERROR_BEGIN(); + dlg = makeAckAlert(STR_SD_CARD, STR_SDCARD_FULL); + break; + case WCS_CHECK_FAILSAFE: + LED_ERROR_BEGIN(); + dlg = makeAckAlert(STR_FAILSAFEWARN, STR_NO_FAILSAFE); + break; +#if defined(MULTIMODULE) + case WCS_CHECK_MULTI: + LED_ERROR_BEGIN(); + dlg = makeAckAlert("MULTI", STR_WARN_MULTI_LOWPOWER); + break; +#endif + case WCS_CHECK_CHECKLIST: + cancelSplash(); + dlg = showModelChecklist(); + // no notes window could be opened: don't stall the sequence + if (!dlg) acknowledgeWarningCheck(); + break; + case WCS_IDLE: + default: + // sequence finished: clear the error indication + LED_ERROR_END(); + break; + } + + if (dlg) { + dlg->setCloseHandler(onShownClosed); + s_shown = dlg; + } +} diff --git a/radio/src/gui/colorlcd/warning_checks_view.h b/radio/src/gui/colorlcd/warning_checks_view.h new file mode 100644 index 00000000000..9ae84a239b4 --- /dev/null +++ b/radio/src/gui/colorlcd/warning_checks_view.h @@ -0,0 +1,33 @@ +/* + * Copyright (C) EdgeTX + * + * Based on code named + * opentx - https://github.com/opentx/opentx + * th9x - http://code.google.com/p/th9x + * er9x - http://code.google.com/p/er9x + * gruvin9x - http://code.google.com/p/gruvin9x + * + * License GPLv2: http://www.gnu.org/licenses/gpl-2.0.html + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + */ + +#pragma once + +// COLORLCD view for the model-load state machine. Polled once per perMain() +// iteration (after MainWindow::run): it mirrors the machine's active warning +// into the matching full-screen dialog and reports user "skip" key presses back +// to the machine. The machine owns all the logic; this is display only. +void warningChecksViewSync(); + +// Suspend/restore main-view widget refresh for the whole warning sequence. Must +// be called *before* MainWindow::run() in perMain() so widgets are never +// repainted for a tick while a sequence is active (edge-triggered). +void warningChecksViewUpdateRefresh(); diff --git a/radio/src/gui/common/stdlcd/CMakeLists.txt b/radio/src/gui/common/stdlcd/CMakeLists.txt index a9e4328e232..8c296490fbd 100644 --- a/radio/src/gui/common/stdlcd/CMakeLists.txt +++ b/radio/src/gui/common/stdlcd/CMakeLists.txt @@ -11,6 +11,7 @@ set(GUI_SRC ../common/stdlcd/model_mixes.cpp ../common/stdlcd/view_text.cpp ../common/stdlcd/model_notes.cpp + ../common/stdlcd/warning_checks_view.cpp ../common/stdlcd/model_curves.cpp ../common/stdlcd/radio_hardware.cpp ../common/stdlcd/radio_version.cpp diff --git a/radio/src/gui/common/stdlcd/menus_common.h b/radio/src/gui/common/stdlcd/menus_common.h index dff3e5f0f01..d114824d7b3 100644 --- a/radio/src/gui/common/stdlcd/menus_common.h +++ b/radio/src/gui/common/stdlcd/menus_common.h @@ -190,7 +190,7 @@ extern uint8_t noHighlightCounter; void menuTextView(event_t event); void pushMenuTextView(const char *filename); void pushModelNotes(); -void readModelNotes(); +void setModelChecklistFilename(); uint8_t getExposCount(); void insertExpo(uint8_t idx); diff --git a/radio/src/gui/common/stdlcd/view_text.cpp b/radio/src/gui/common/stdlcd/view_text.cpp index b41ba85b026..944346b62b6 100644 --- a/radio/src/gui/common/stdlcd/view_text.cpp +++ b/radio/src/gui/common/stdlcd/view_text.cpp @@ -94,10 +94,11 @@ static void sdReadTextFile(const char * filename, char lines[TEXT_VIEWER_LINES][ } } -void readModelNotes() +// Build the model checklist/notes filename into reusableBuffer.viewText.filename. +// Tries ".txt" first, then " .txt", then (modelslist) the stored +// model filename with a .txt extension. Shared by the model-load checklist view. +void setModelChecklistFilename() { - LED_ERROR_BEGIN(); - strcpy(reusableBuffer.viewText.filename, MODELS_PATH "/"); char *buf = strcat_currentmodelname( &reusableBuffer.viewText.filename[sizeof(MODELS_PATH)], 0); @@ -111,35 +112,12 @@ void readModelNotes() #if defined(STORAGE_MODELSLIST) if (!isFileAvailable(reusableBuffer.viewText.filename)) { buf = strAppendFilename( - &reusableBuffer.viewText.filenam[sizeof(MODELS_PATH)], + &reusableBuffer.viewText.filename[sizeof(MODELS_PATH)], g_eeGeneral.currModelFilename, LEN_MODEL_FILENAME); strcpy(buf, TEXT_EXT); } #endif } - - waitKeysReleased(); - event_t event = EVT_ENTRY; - reusableBuffer.viewText.pushMenu = false; - while (true) { - uint32_t power = pwrCheck(); - if (power != e_power_press) { - lcdRefreshWait(); - lcdClear(); - menuTextView(event); - lcdRefresh(); - } - if (power == e_power_off){ - drawSleepBitmap(); - boardOff(); - break; - } - event = getEvent(); - WDG_RESET(); - if (reusableBuffer.viewText.checklistComplete) break; - } - - LED_ERROR_END(); } void menuTextView(event_t event) diff --git a/radio/src/gui/common/stdlcd/warning_checks_view.cpp b/radio/src/gui/common/stdlcd/warning_checks_view.cpp new file mode 100644 index 00000000000..f66b297729d --- /dev/null +++ b/radio/src/gui/common/stdlcd/warning_checks_view.cpp @@ -0,0 +1,172 @@ +/* + * Copyright (C) EdgeTX + * + * Based on code named + * opentx - https://github.com/opentx/opentx + * th9x - http://code.google.com/p/th9x + * er9x - http://code.google.com/p/er9x + * gruvin9x - http://code.google.com/p/gruvin9x + * + * License GPLv2: http://www.gnu.org/licenses/gpl-2.0.html + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + */ + +#include "warning_checks_view.h" + +#include "edgetx.h" +#include "warning_checks.h" +#include "menus_common.h" +#include "strhelpers.h" + +#if LCD_W >= 212 + #define SWITCH_WARNING_LIST_X 60 + #define SWITCH_WARNING_LIST_Y 4*FH+4 +#else + #define SWITCH_WARNING_LIST_X 4 + #define SWITCH_WARNING_LIST_Y 4*FH+4 +#endif + +// Draw the full-screen switch/pot warning (one frame). Called once per main-loop +// tick by warningChecksViewSync(); refresh, audio and key handling are the +// view's job (see below). +static void drawSwitchWarningScreen() +{ + drawAlertBox(STR_SWITCHWARN, nullptr, STR_PRESS_ANY_KEY_TO_SKIP); + + int x = SWITCH_WARNING_LIST_X; + int y = SWITCH_WARNING_LIST_Y; + int numWarnings = 0; + for (int i = 0; i < switchGetMaxAllSwitches(); ++i) { + if (SWITCH_WARNING_ALLOWED(i)) { + uint8_t warnState = g_model.getSwitchWarning(i); + if (warnState) { + swarnstate_t swState = g_model.getSwitchStateForWarning(i); + if (warnState != swState) { + if (++numWarnings < 6) { + const char* s = getSwitchWarnSymbol(warnState); + drawSource(x, y, MIXSRC_FIRST_SWITCH + i, INVERS); + lcdDrawText(lcdNextPos, y, s, INVERS); + x = lcdNextPos + 3; + } + } + } + } + } + + if (g_model.potsWarnMode) { + for (int i = 0; i < MAX_POTS; i++) { + if (!IS_POT_SLIDER_AVAILABLE(i)) continue; + if (g_model.potsWarnEnabled & (1 << i)) { + if (abs(g_model.potsWarnPosition[i] - GET_LOWRES_POT_POSITION(i)) > 1) { + if (++numWarnings < 6) { + drawSource(x, y, MIXSRC_FIRST_POT + i, INVERS); + const char* symbol; + auto warn_pos = g_model.potsWarnPosition[i]; + if (IS_SLIDER(i)) { + symbol = warn_pos > GET_LOWRES_POT_POSITION(i) + ? CHAR_UP + : CHAR_DOWN; + } else { + symbol = warn_pos > GET_LOWRES_POT_POSITION(i) + ? CHAR_RIGHT + : CHAR_LEFT; + } + lcdDrawText(lcdNextPos, y, symbol, INVERS); + x = lcdNextPos + 3; + } + } + } + } + } + + if (numWarnings >= 6) { + lcdDrawText(x, y, "...", 0); + } +} + +// Plays the per-warning alert sound on the entering edge (the state just became +// active). The state machine reads fresh inputs every tick, so the warning may +// auto-clear without a key press; we only want one sound per warning. +static void playEnterSound(WarningCheckState st) +{ + switch (st) { + case WCS_CHECK_THROTTLE: AUDIO_ERROR_MESSAGE(AU_THROTTLE_ALERT); break; + case WCS_CHECK_SWITCHES: AUDIO_ERROR_MESSAGE(AU_SWITCH_ALERT); break; + default: AUDIO_ERROR_MESSAGE(AU_ERROR); break; + } +} + +void warningChecksViewSync(event_t evt) +{ + static WarningCheckState s_shownState = WCS_IDLE; + + WarningCheckState st = activeWarningCheck(); + + if (st == WCS_IDLE) { + // sequence finished (or no warning needed): clear the error indication once + if (s_shownState != WCS_IDLE) { + LED_ERROR_END(); + s_shownState = WCS_IDLE; + } + return; + } + + bool entering = (st != s_shownState); + if (entering) { + cancelSplash(); + LED_ERROR_BEGIN(); + s_shownState = st; + if (st == WCS_CHECK_CHECKLIST) setModelChecklistFilename(); + } + + // The checklist is interactive: drive the text viewer with the tick's event + // and report completion (rather than consuming the event as a plain "skip"). + if (st == WCS_CHECK_CHECKLIST) { + reusableBuffer.viewText.pushMenu = false; + lcdRefreshWait(); + lcdClear(); + menuTextView(entering ? EVT_ENTRY : evt); + lcdRefresh(); + if (reusableBuffer.viewText.checklistComplete) acknowledgeWarningCheck(); + return; + } + + if (entering) playEnterSound(st); + + lcdRefreshWait(); + switch (st) { + case WCS_CHECK_THROTTLE: + drawAlertBox(STR_THROTTLE_UPPERCASE, warningCheckText(), + STR_PRESS_ANY_KEY_TO_SKIP); + break; + case WCS_CHECK_SWITCHES: + drawSwitchWarningScreen(); + break; + case WCS_CHECK_SD: + drawAlertBox(STR_SD_CARD, STR_SDCARD_FULL, STR_PRESS_ANY_KEY_TO_SKIP); + break; + case WCS_CHECK_FAILSAFE: + drawAlertBox(STR_FAILSAFEWARN, STR_NO_FAILSAFE, STR_PRESS_ANY_KEY_TO_SKIP); + break; +#if defined(MULTIMODULE) + case WCS_CHECK_MULTI: + drawAlertBox("MULTI", STR_WARN_MULTI_LOWPOWER, STR_PRESS_ANY_KEY_TO_SKIP); + break; +#endif + default: + break; + } + lcdRefresh(); + + // Any key acknowledges the warning ("press any key to skip"). The machine + // consumes the acknowledge on the next tick's warningChecksRun(). + if (evt) acknowledgeWarningCheck(); +} diff --git a/radio/src/gui/common/stdlcd/warning_checks_view.h b/radio/src/gui/common/stdlcd/warning_checks_view.h new file mode 100644 index 00000000000..6aa043b0524 --- /dev/null +++ b/radio/src/gui/common/stdlcd/warning_checks_view.h @@ -0,0 +1,35 @@ +/* + * Copyright (C) EdgeTX + * + * Based on code named + * opentx - https://github.com/opentx/opentx + * th9x - http://code.google.com/p/th9x + * er9x - http://code.google.com/p/er9x + * gruvin9x - http://code.google.com/p/gruvin9x + * + * License GPLv2: http://www.gnu.org/licenses/gpl-2.0.html + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + */ + +#pragma once + +#include "edgetx_types.h" + +// B&W view over the model-load state machine (model_load_sm.{h,cpp}). The +// COLORLCD counterpart lives in gui/colorlcd/warning_checks_view.cpp; both render the +// machine's active warning and feed key presses back as an acknowledge, so the +// warning sequence is shared by both GUIs and every entry point (model switch, +// boot, flight reset, stick-mode change). +// +// Called once per main-loop iteration from perMain(), after warningChecksRun(). +// When a warning is active it draws it full-screen (so guiMain must be skipped for +// that tick) and turns the tick's key event into acknowledgeWarningCheck(). +void warningChecksViewSync(event_t evt); diff --git a/radio/src/main.cpp b/radio/src/main.cpp index 8abf1cff2f0..6ac1c2c993a 100644 --- a/radio/src/main.cpp +++ b/radio/src/main.cpp @@ -27,6 +27,7 @@ #include "edgetx.h" #include "lua/lua_states.h" +#include "warning_checks.h" #if defined(COLORLCD) #include "view_main.h" @@ -35,6 +36,9 @@ #include "etx_lv_theme.h" #include "menu.h" #include "mainwindow.h" +#include "warning_checks_view.h" +#elif defined(GUI) +#include "gui/common/stdlcd/warning_checks_view.h" #endif #if defined(CLI) @@ -537,7 +541,9 @@ void perMain() periodicTick(); DEBUG_TIMER_STOP(debugTimerPerMain1); - if (mainRequestFlags & (1u << REQUEST_FLIGHT_RESET)) { + // flightReset arms the model-load state machine for its warning checks; defer + // the request while a sequence is already mid-flight so it isn't clobbered. + if ((mainRequestFlags & (1u << REQUEST_FLIGHT_RESET)) && warningChecksIdle()) { TRACE("Executing requested Flight Reset"); flightReset(); mainRequestFlags &= ~(1u << REQUEST_FLIGHT_RESET); @@ -550,7 +556,15 @@ void perMain() #endif #if defined(COLORLCD) + // Advance the post-model-load checks before driving the UI, so any state + // change happens outside lv_timer_handler (flat stack, no re-entrancy). + warningChecksRun(); + // Suspend widget refresh before run() so the main view isn't repainted for a + // tick on the edge where a sequence becomes active. + warningChecksViewUpdateRefresh(); MainWindow::instance()->run(); + // Reflect the machine's current state in the warning dialogs. + warningChecksViewSync(); #endif #if defined(RTC_BACKUP_RAM) @@ -598,12 +612,21 @@ void perMain() bindButtonHandler(evt); #endif +#if defined(GUI) && !defined(COLORLCD) + // Advance the post-model-load checks before driving the menus (flat stack), then + // render the active warning. While a warning is up it covers the screen and + // consumes the key, so guiMain must not also run for this tick. + warningChecksRun(); + bool mlWarn = (activeWarningCheck() != WCS_IDLE); + warningChecksViewSync(evt); +#endif + #if defined(GUI) DEBUG_TIMER_START(debugTimerGuiMain); #if defined(COLORLCD) guiMain(0); #else - guiMain(evt); + if (!mlWarn) guiMain(evt); #endif DEBUG_TIMER_STOP(debugTimerGuiMain); #endif diff --git a/radio/src/storage/storage.h b/radio/src/storage/storage.h index e7c4d33b7d3..c48572fcc5c 100644 --- a/radio/src/storage/storage.h +++ b/radio/src/storage/storage.h @@ -61,6 +61,11 @@ void postRadioSettingsLoad(); void preModelLoad(); void postModelLoad(bool alarms); +// Final part of model load (mixer/pulses start + post-start setup). Run directly +// for loads without warning checks (alarms=false), or deferred by the model-load +// state machine (MLC_MODEL_SWITCH terminal) once all warnings have been cleared. +void postModelLoadFinish(); + #if !defined(STORAGE_MODELSLIST) extern ModelHeader modelHeaders[MAX_MODELS]; diff --git a/radio/src/storage/storage_common.cpp b/radio/src/storage/storage_common.cpp index ab9c77a2953..debc4866bb1 100644 --- a/radio/src/storage/storage_common.cpp +++ b/radio/src/storage/storage_common.cpp @@ -25,6 +25,7 @@ #include "tasks/mixer_task.h" #include "mixes.h" #include "switches.h" +#include "warning_checks.h" #if defined(FUNCTION_SWITCHES_RGB_LEDS) #include "hal/rgbleds.h" @@ -295,12 +296,23 @@ if(g_model.rssiSource) { sanitizeMixerLines(); #if defined(GUI) + // Runtime model switch (alarms): the mixer was already running, so pulses were + // stopped in preModelLoad. Hand the post-load warning checks to the polled, + // core-owned state machine (driven once per perMain() by both GUIs) instead of + // a blocking UI loop. Pulses stay stopped (RX holds failsafe) until the machine + // reaches its terminal state, which then runs postModelLoadFinish(). + // Boot loads pass alarms=false (boot arms the machine itself in edgeTxInit). if (alarms) { - checkAll(); - PLAY_MODEL_NAME(); + warningChecksStart(); + return; } #endif + postModelLoadFinish(); +} + +void postModelLoadFinish() +{ // Mixer should only be restarted // if we are switching between models, // not on first boot (started later on) diff --git a/radio/src/switches.cpp b/radio/src/switches.cpp index 8f7327ef73d..19ede1ddda7 100644 --- a/radio/src/switches.cpp +++ b/radio/src/switches.cpp @@ -38,16 +38,6 @@ #define CS_LAST_VALUE_INIT -32768 -#if defined(COLORLCD) - #define SWITCH_WARNING_LIST_X WARNING_LINE_X - #define SWITCH_WARNING_LIST_Y WARNING_LINE_Y+3*FH -#elif LCD_W >= 212 - #define SWITCH_WARNING_LIST_X 60 - #define SWITCH_WARNING_LIST_Y 4*FH+4 -#else - #define SWITCH_WARNING_LIST_X 4 - #define SWITCH_WARNING_LIST_Y 4*FH+4 -#endif enum LogicalSwitchContextState { SWITCH_START, @@ -877,12 +867,12 @@ swsrc_t getMovedSwitch() return result; } +// Pure predicate: reads the current switch/pot state. The caller is responsible +// for refreshing inputs first (refreshInputsForWarnings()). bool isSwitchWarningRequired(uint16_t &bad_pots) { - if (!mixerTaskRunning()) getADC(); - getMovedSwitch(); - bool warn = false; + bad_pots = 0; for (int i = 0; i < switchGetMaxAllSwitches(); i++) { if (SWITCH_WARNING_ALLOWED(i)) { uint8_t warnState = g_model.getSwitchWarning(i); @@ -896,8 +886,6 @@ bool isSwitchWarningRequired(uint16_t &bad_pots) } if (g_model.potsWarnMode) { - evalFlightModeMixes(e_perout_mode_normal, 0); - bad_pots = 0; for (int i = 0; i < adcGetMaxInputs(ADC_INPUT_FLEX); i++) { if (!IS_POT_SLIDER_AVAILABLE(i)) continue; if ((g_model.potsWarnEnabled & (1 << i)) && @@ -911,142 +899,6 @@ bool isSwitchWarningRequired(uint16_t &bad_pots) return warn; } -#if defined(COLORLCD) -#include "switch_warn_dialog.h" -void checkSwitches() -{ - uint16_t bad_pots = 0; - if (!isSwitchWarningRequired(bad_pots)) - return; - - LED_ERROR_BEGIN(); - auto dialog = new SwitchWarnDialog(); - MainWindow::instance()->blockUntilClose(true, [=]() { - return dialog->deleted(); - }); - LED_ERROR_END(); -} -#elif defined(GUI) - -void checkSwitches() -{ - swarnstate_t last_bad_switches = 0xff; - uint16_t bad_pots = 0, last_bad_pots = 0xff; - -#if defined(PWR_BUTTON_PRESS) - bool refresh = false; -#endif - - while (true) { -#if defined(RADIO_GX12) - _poll_switches(); -#endif - if (!isSwitchWarningRequired(bad_pots)) - break; - - cancelSplash(); - LED_ERROR_BEGIN(); - resetBacklightTimeout(); - - // first - display warning - if (last_bad_switches != switches_states || last_bad_pots != bad_pots) { - drawAlertBox(STR_SWITCHWARN, nullptr, STR_PRESS_ANY_KEY_TO_SKIP); - if (last_bad_switches == 0xff || last_bad_pots == 0xff) { - AUDIO_ERROR_MESSAGE(AU_SWITCH_ALERT); - } - int x = SWITCH_WARNING_LIST_X; - int y = SWITCH_WARNING_LIST_Y; - int numWarnings = 0; - for (int i = 0; i < switchGetMaxAllSwitches(); ++i) { - if (SWITCH_WARNING_ALLOWED(i)) { - uint8_t warnState = g_model.getSwitchWarning(i); - if (warnState) { - swarnstate_t swState = g_model.getSwitchStateForWarning(i); - if (warnState != swState) { - if (++numWarnings < 6) { - const char* s = getSwitchWarnSymbol(warnState); - drawSource(x, y, MIXSRC_FIRST_SWITCH + i, INVERS); - lcdDrawText(lcdNextPos, y, s, INVERS); - x = lcdNextPos + 3; - } - } - } - } - } - - if (g_model.potsWarnMode) { - for (int i = 0; i < MAX_POTS; i++) { - if (!IS_POT_SLIDER_AVAILABLE(i)) continue; - if (g_model.potsWarnEnabled & (1 << i)) { - if (abs(g_model.potsWarnPosition[i] - GET_LOWRES_POT_POSITION(i)) > 1) { - if (++numWarnings < 6) { - drawSource(x, y, MIXSRC_FIRST_POT + i, INVERS); - const char* symbol; - auto warn_pos = g_model.potsWarnPosition[i]; - if (IS_SLIDER(i)) { - symbol = warn_pos > GET_LOWRES_POT_POSITION(i) - ? CHAR_UP - : CHAR_DOWN; - } else { - symbol = warn_pos > GET_LOWRES_POT_POSITION(i) - ? CHAR_RIGHT - : CHAR_LEFT; - } - lcdDrawText(lcdNextPos, y, symbol, INVERS); - x = lcdNextPos + 3; - } - } - } - } - } - - if (numWarnings >= 6) { - lcdDrawText(x, y, "...", 0); - } - - last_bad_pots = bad_pots; - - lcdRefresh(); - lcdSetContrast(); - waitKeysReleased(); - - last_bad_switches = switches_states; - } - - if (keyDown()) - break; - -#if defined(PWR_BUTTON_PRESS) - uint32_t power = pwrCheck(); - if (power == e_power_off) { - drawSleepBitmap(); - boardOff(); - break; - } - else if (power == e_power_press) { - refresh = true; - } - else if (power == e_power_on && refresh) { - last_bad_switches = 0xff; - last_bad_pots = 0xff; - refresh = false; - } -#else - if (pwrCheck() == e_power_off) { - break; - } -#endif - - checkBacklight(); - - WDG_RESET(); - sleep_ms(10); - } - - LED_ERROR_END(); -} -#endif // GUI - void logicalSwitchesTimerTick() { #if (MAX_LOGICAL_SWITCHES != 64) diff --git a/radio/src/warning_checks.cpp b/radio/src/warning_checks.cpp new file mode 100644 index 00000000000..6f9b491d162 --- /dev/null +++ b/radio/src/warning_checks.cpp @@ -0,0 +1,382 @@ +/* + * Copyright (C) EdgeTX + * + * Based on code named + * opentx - https://github.com/opentx/opentx + * th9x - http://code.google.com/p/th9x + * er9x - http://code.google.com/p/er9x + * gruvin9x - http://code.google.com/p/gruvin9x + * + * License GPLv2: http://www.gnu.org/licenses/gpl-2.0.html + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + */ + +#include "warning_checks.h" + +#include "edgetx.h" +#include "switches.h" +#include "sdcard.h" +#include "audio.h" +#include "pulses/pulses.h" +#include "tasks/mixer_task.h" +#include "storage/storage.h" +#include "hal/adc_driver.h" +#include "os/sleep.h" + +#if defined(GUI) +#include "gui/gui_common.h" +#endif + +#if defined(COLORLCD) +#include "libui/fullscreen_dialog.h" +#include "libui/mainwindow.h" +#include "libui/popups.h" +#endif + +#include +#include + +// Warning predicates and the input refresh they rely on -------------------- + +// Refresh the input readings the warning predicates rely on. Kept separate from +// the predicates themselves so they stay pure: the driver (the state machine, or +// the boot/flightReset blocking loops) refreshes once per tick, then queries the +// predicates and the warning views read the same fresh state. +void refreshInputsForWarnings() +{ + if (!mixerTaskRunning()) getADC(); + evalInputs(e_perout_mode_notrainer); + getMovedSwitch(); +} + +bool isThrottleWarningAlertNeeded() +{ + if (g_model.disableThrottleWarning) { + return false; + } + + uint8_t thr_src = throttleSource2Source(g_model.thrTraceSrc); + + // in case an output channel is choosen as throttle source + // we assume the throttle stick is the input (no computed channels yet) + if (thr_src >= MIXSRC_FIRST_CH) { + thr_src = throttleSource2Source(0); + } + + int16_t v = getValue(thr_src); + + // TODO: this looks fishy.... + if (g_model.thrTraceSrc && g_model.throttleReversed) { + v = -v; + } + + if (g_model.enableCustomThrottleWarning) { + int16_t idleValue = (int32_t)RESX * + (int32_t)g_model.customThrottleWarningPosition / + (int32_t)100; + return abs(v - idleValue) > THRCHK_DEADBAND; + } else { +#if defined(SURFACE_RADIO) // surface radio, stick centered + return v > THRCHK_DEADBAND; +#else + return v > THRCHK_DEADBAND - RESX; +#endif + } +} + +bool isFailsafeWarningRequired() +{ + for (int i=0; isetMessage(strKeys.c_str()); + MainWindow::instance()->blockUntilClose(true, [=]() { + if (dlg->deleted()) return true; + if ((tgtime < get_tmr10ms()) || !keyDown()) { + dlg->deleteLater(); + return true; + } + return false; + }); + LED_ERROR_END(); + } +#else + if (!waitKeysReleased()) { + showMessageBox(STR_KEYSTUCK); + tmr10ms_t tgtime = get_tmr10ms() + 500; + while (tgtime != get_tmr10ms()) { + sleep_ms(1); + WDG_RESET(); + } + } +#endif + + if (!g_eeGeneral.disableRtcWarning) { + enableVBatBridge(); + checkRTCBattery(); + } + disableVBatBridge(); + +#if defined(EXTERNAL_ANTENNA) && defined(INTERNAL_MODULE_PXX1) + checkExternalAntenna(); +#endif +} +#endif // GUI + +static WarningCheckState s_state = WCS_IDLE; +static WarningCheckContext s_ctx = WCC_MODEL_SWITCH; // what armed the machine +static bool s_entered = false; // entry evaluation done for the current state +static bool s_warnActive = false; // a warning dialog should be shown for the current state +static bool s_ack = false; // view reported a "press any key to skip" + +static char s_warnText[64]; + +bool warningChecksIdle() { return s_state == WCS_IDLE; } + +WarningCheckState activeWarningCheck() +{ + return s_warnActive ? s_state : WCS_IDLE; +} + +const char* warningCheckText() { return s_warnText; } + +void acknowledgeWarningCheck() { s_ack = true; } + +void warningChecksStart(WarningCheckContext ctx) +{ + s_ctx = ctx; + // The stick-mode change only needs the throttle check; every other context + // runs the full sequence from the top. + s_state = (ctx == WCC_STICK_MODE) ? WCS_CHECK_THROTTLE : WCS_CHECK_SD; + s_entered = false; + s_warnActive = false; + s_ack = false; +} + +static void buildThrottleText() +{ + if (g_model.enableCustomThrottleWarning) { + snprintf(s_warnText, sizeof(s_warnText), "%s (%d%%)", STR_THROTTLE_NOT_IDLE, + g_model.customThrottleWarningPosition); + } else { + strncpy(s_warnText, STR_THROTTLE_NOT_IDLE, sizeof(s_warnText) - 1); + s_warnText[sizeof(s_warnText) - 1] = '\0'; + } +} + +static void gotoNextState() +{ + s_entered = false; + s_warnActive = false; + // The stick-mode context only runs the throttle check, then terminates. + if (s_ctx == WCC_STICK_MODE) { + s_state = WCS_COMPLETE; + return; + } + switch (s_state) { + case WCS_CHECK_SD: s_state = WCS_CHECK_THROTTLE; break; + case WCS_CHECK_THROTTLE: s_state = WCS_CHECK_SWITCHES; break; + case WCS_CHECK_SWITCHES: s_state = WCS_CHECK_FAILSAFE; break; + case WCS_CHECK_FAILSAFE: s_state = WCS_CHECK_MULTI; break; + case WCS_CHECK_MULTI: s_state = WCS_CHECK_CHECKLIST; break; + case WCS_CHECK_CHECKLIST: s_state = WCS_COMPLETE; break; + default: s_state = WCS_COMPLETE; break; + } +} + +void warningChecksRun() +{ + if (s_state == WCS_IDLE) return; + + // Refresh the input readings once per tick, then query the pure predicates + // below; the warning views read the same fresh state. + refreshInputsForWarnings(); + + // Loop so that any run of states that don't need a warning collapses into a + // single tick (the no-warning case reaches WCS_COMPLETE immediately). + for (;;) { + switch (s_state) { + case WCS_IDLE: + return; + + case WCS_CHECK_SD: + if (!s_entered) { s_entered = true; s_warnActive = sdIsFull(); } + if (!s_warnActive) { gotoNextState(); continue; } + if (s_ack) { s_ack = false; gotoNextState(); continue; } + return; + + case WCS_CHECK_THROTTLE: + if (!s_entered) { + s_entered = true; + // Don't check the throttle stick if the radio is not calibrated; the + // stick-mode change is exempt (its old direct call never gated on it). + s_warnActive = (s_ctx == WCC_STICK_MODE || + g_eeGeneral.chkSum == evalChkSum()) && + isThrottleWarningAlertNeeded(); + if (s_warnActive) buildThrottleText(); + } + if (!s_warnActive) { gotoNextState(); continue; } + // Auto-advance when the stick is lowered (pure predicate, evaluated on + // this tick's fresh inputs), or skip on key press (view acknowledge). + if (s_ack || !isThrottleWarningAlertNeeded()) { + s_ack = false; gotoNextState(); continue; + } + return; + + case WCS_CHECK_SWITCHES: { + uint16_t bad_pots = 0; + if (!s_entered) { + s_entered = true; + s_warnActive = isSwitchWarningRequired(bad_pots); + } + if (!s_warnActive) { gotoNextState(); continue; } + // Auto-advance when the switches/pots are corrected, or skip on key + // press (view acknowledge). + if (s_ack || !isSwitchWarningRequired(bad_pots)) { + s_ack = false; gotoNextState(); continue; + } + return; + } + + case WCS_CHECK_FAILSAFE: + if (!s_entered) { s_entered = true; s_warnActive = isFailsafeWarningRequired(); } + if (!s_warnActive) { gotoNextState(); continue; } + if (s_ack) { s_ack = false; gotoNextState(); continue; } + return; + + case WCS_CHECK_MULTI: +#if defined(MULTIMODULE) + if (!s_entered) { s_entered = true; s_warnActive = isMultiLowPowerWarningRequired(); } + if (!s_warnActive) { gotoNextState(); continue; } + if (s_ack) { s_ack = false; gotoNextState(); continue; } + return; +#else + gotoNextState(); + continue; +#endif + + case WCS_CHECK_CHECKLIST: +#if defined(GUI) + if (!s_entered) { + s_entered = true; + s_warnActive = (g_model.displayChecklist && modelHasNotes()); + } + if (!s_warnActive) { gotoNextState(); continue; } + if (s_ack) { s_ack = false; gotoNextState(); continue; } + return; +#else + gotoNextState(); + continue; +#endif + + case WCS_COMPLETE: + // All checks cleared. Mirror the old blocking order: settle into the + // silence period, announce the model, then run the context-specific + // terminal action (restart pulses/mixer and/or the model-load tail). + // The silence period is started before PLAY_MODEL_NAME() and the tail to + // match the pre-refactor sequence (checkAll() ended with + // START_SILENCE_PERIOD(), then the caller did PLAY_MODEL_NAME() + restart). + if (s_ctx != WCC_STICK_MODE) START_SILENCE_PERIOD(); + if (s_ctx == WCC_MODEL_SWITCH || s_ctx == WCC_BOOT) PLAY_MODEL_NAME(); + switch (s_ctx) { + case WCC_MODEL_SWITCH: + // runtime switch: pulses were stopped in preModelLoad; restart them + // and run the rest of the model-load tail (audio refs, LUA, failsafe) + postModelLoadFinish(); + break; + case WCC_BOOT: + // boot: postModelLoadFinish() already ran at load time (with the + // mixer not yet started, so it skipped pulses); just start pulses now + pulsesStart(); + break; + case WCC_STICK_MODE: + // stick-mode change: the caller stopped the mixer; restart it + mixerTaskStart(); + break; + case WCC_FLIGHT_RESET: + // flight reset never stopped pulses and must not reload the model + break; + } + s_state = WCS_IDLE; + s_warnActive = false; + s_entered = false; + return; + } + } +} diff --git a/radio/src/warning_checks.h b/radio/src/warning_checks.h new file mode 100644 index 00000000000..85561b11f22 --- /dev/null +++ b/radio/src/warning_checks.h @@ -0,0 +1,112 @@ +/* + * Copyright (C) EdgeTX + * + * Based on code named + * opentx - https://github.com/opentx/opentx + * th9x - http://code.google.com/p/th9x + * er9x - http://code.google.com/p/er9x + * gruvin9x - http://code.google.com/p/gruvin9x + * + * License GPLv2: http://www.gnu.org/licenses/gpl-2.0.html + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + */ + +#pragma once + +#include + +// Startup/safety warning check sequence, modelled as a GUI-agnostic state +// machine. Shared by every entry point that has to run the throttle/switch/ +// failsafe/etc. warnings: a runtime model switch, boot, a flight reset, and the +// radio-setup stick-mode change (see WarningCheckContext). +// +// For the contexts that stop the mixer/pulses first (a model switch stops them in +// preModelLoad, the stick-mode change stops the mixer), the RF module is silent +// and the receiver holds failsafe meanwhile. The machine progresses through the +// checks one at a time, and only when it reaches WCS_COMPLETE (all cleared) does +// it run the context-specific terminal action (restart pulses/mixer, model-load +// tail, ...). It is polled once per main loop iteration, so no blocking/nested UI +// loop is needed. +// +// This module owns the truth (which check is active, when to advance, what the +// terminal does). The GUI is only a *view*: it renders activeWarningCheck() and +// feeds key presses back via acknowledgeWarningCheck(), so the sequence is +// independent of any particular GUI (COLORLCD and B&W each have a thin view). + +enum WarningCheckState : uint8_t { + WCS_IDLE = 0, // NORMAL: nothing pending, mixer running + WCS_CHECK_SD, // ack-alert: SD card full + WCS_CHECK_THROTTLE, // position warning: throttle not idle + WCS_CHECK_SWITCHES, // position warning: switches/pots not in warning position + WCS_CHECK_FAILSAFE, // ack-alert: failsafe not set + WCS_CHECK_MULTI, // ack-alert: MULTI module low power (skipped if !MULTIMODULE) + WCS_CHECK_CHECKLIST, // model checklist / notes + WCS_COMPLETE, // terminal: run the context-specific tail, then -> WCS_IDLE +}; + +// What armed the machine. The set of checks and the terminal action both depend +// on this, so every warning entry point (model switch, boot, flight reset, the +// radio-setup stick-mode change) routes through the one machine instead of its +// own blocking loop. +enum WarningCheckContext : uint8_t { + WCC_MODEL_SWITCH, // full sequence; terminal: postModelLoadFinish() (restart pulses + model init) + WCC_BOOT, // full sequence; terminal: pulsesStart() (postModelLoadFinish already ran at load) + WCC_FLIGHT_RESET, // full sequence; terminal: nothing (pulses were never stopped) + WCC_STICK_MODE, // throttle-only; terminal: mixerTaskStart() (caller did mixerTaskStop()) +}; + +// Arm the machine for the given context. Resets any in-progress sequence. +// WCC_MODEL_SWITCH / WCC_BOOT / WCC_FLIGHT_RESET run the full check sequence; +// WCC_STICK_MODE runs only the throttle check. +void warningChecksStart(WarningCheckContext ctx = WCC_MODEL_SWITCH); + +// Advance the machine. Polled once per perMain() iteration, before the UI loop. +void warningChecksRun(); + +// View contract ------------------------------------------------------------- + +// The check the view should currently display a warning dialog for, or WCS_IDLE +// if no dialog should be shown. +WarningCheckState activeWarningCheck(); + +// Message text for the currently active warning (currently only the throttle +// warning uses a dynamic string; valid while activeWarningCheck() returns +// WCS_CHECK_THROTTLE). +const char* warningCheckText(); + +// Report a user "press any key to skip" acknowledge to the machine. +void acknowledgeWarningCheck(); + +// True when the machine is idle (no model-load sequence pending). Used to gate a +// deferred flight-reset request so it can't restart a sequence already running, +// and at boot to decide whether pulses are started directly or by the terminal. +bool warningChecksIdle(); + +// Warning predicates (pure reads) and the input refresh they rely on. The state +// machine calls refreshInputsForWarnings() once per tick, then queries the +// predicates; the warning views read the same fresh state. +void refreshInputsForWarnings(); +bool isThrottleWarningAlertNeeded(); +bool isFailsafeWarningRequired(); +#if defined(MULTIMODULE) +bool isMultiLowPowerWarningRequired(); +#endif + +// Sound-disabled alarm, shown once at boot. +void checkAlarm(); + +#if defined(GUI) +// Boot-only warning checks that are not part of the runtime warning sequence +// (stuck keys, low RTC battery, external-antenna configuration). Run once, +// blocking, in edgeTxInit() before the main loop and before arming the state +// machine for the post-load checks. +void checkBootSpecificWarnings(); +#endif