CDC Badge OS
Firmware for the CDC Badge v1.0 hardware security key
Loading...
Searching...
No Matches
AppUi.cpp
Go to the documentation of this file.
1
10
11#include "AppUiInternal.h"
12#include "cdc_os_ui/AppUi.h"
20#include "cdc_core/PinManager.h"
24#include "cdc_core/UsbManager.h"
25#include "cdc_core/Raii.h"
26
33
34#include "cdc_hal/IDisplay.h"
35#include "cdc_hal/IKeypad.h"
41#include "cdc_hal/IRtc.h"
42
44#include "nvs.h"
45
46#include <atomic>
47#include <cstdio>
48#include <cstring>
49#include <ctime>
50#include "freertos/FreeRTOS.h"
51#include "freertos/task.h"
52#include "freertos/semphr.h"
53#include "esp_timer.h"
54
55namespace cdc::ui {
56
58
59static constexpr uint8_t MAIN_MENU_MAX_ITEMS = 16;
60// Fixed main-menu entries appended after the dynamic module items; the
61// trailing MAIN_MENU_FIXED_COUNT is their number, derived automatically.
63static constexpr uint8_t TOOLS_MAX_ITEMS = 16;
64
65static constexpr uint32_t INACTIVITY_TIMEOUT_MS = 5 * 60 * 1000;
66
68
80
82static constexpr uint16_t MAX_LANGUAGES = 16;
83
85static LockScreenView* s_lockScreen = nullptr;
86static PinEntryView* s_pinEntry = nullptr;
87static ListView* s_mainMenu = nullptr;
88static ListView* s_toolsMenu = nullptr;
89static ListView* s_settingsMenu = nullptr;
91static SliderView* s_sleepSlider = nullptr;
92static SliderView* s_timezoneSlider = nullptr;
93static ListView* s_languageMenu = nullptr;
94static DateInputView* s_dateInput = nullptr;
95static TimeInputView* s_timeInput = nullptr;
100
101// Numeric-comparison pairing is requested from the nimble_host task; the prompt
102// must be shown from the main/UI task. The request is parked here under a mutex
103// and the UI work is deferred via BLE_PAIRING_REQUEST.
105 bool valid = false;
106 uint16_t connHandle = 0xFFFF;
107 uint32_t passkey = 0;
108};
110static SemaphoreHandle_t s_blePendingMutex = nullptr;
111
113static UiDeps s_deps = {};
114
118static uint8_t s_mainMenuPluginCount = 0;
119
123static uint8_t s_toolsModuleCount = 0;
124
127
131static uint16_t s_languageCount = 0;
132
134static int8_t s_lastMinute = -1;
135
137static bool s_lastUsbConnected = false;
138static bool s_lastCharging = false;
139static bool s_lastWifiConnected = false;
140static bool s_lastBleEnabled = false;
141static bool s_lastBackgroundPlugin = false;
142static bool s_lastBatteryPresent = false;
143
144// Throttle for the battery-percent ADC sample on the lockscreen. The BQ25895
145// ADC step is 20 mV, mapped over 1000 mV (3200-4200 mV) the linear curve gives
146// ~2 %/step; an unrestricted per-tick sample makes ADC jitter flip the
147// rendered percentage, marking the view dirty and triggering a partial EPD
148// refresh on essentially every tick.
149static constexpr uint32_t BATTERY_SAMPLE_INTERVAL_MS = 30000;
150static uint32_t s_lastBatterySampleMs = 0;
151
153static bool s_ignoreKeyUntilRelease = false;
154
155// Set from the keypad task when the N+Y rescue chord is held; consumed by
156// ui_process on the UI task, which performs the anti-block instant lock.
157static std::atomic<bool> s_antiBlockLockRequested{false};
158
160static inline uint8_t getPluginsIndex() { return s_mainMenuPluginCount + MM_PLUGINS; }
162static inline uint8_t getToolsIndex() { return s_mainMenuPluginCount + MM_TOOLS; }
164static inline uint8_t getSettingsIndex() { return s_mainMenuPluginCount + MM_SETTINGS; }
167
169static void onUnlockRequested();
171static bool onPinVerify(const char* pin);
173static void onPinSuccess();
175static void onMainMenuSelect(uint16_t index, void* userData);
177static void onToolsSelect(uint16_t index, void* userData);
179static void onSettingsSelect(uint16_t index, void* userData);
181static void onLanguageSelect(uint16_t index, void* userData);
183static void rebuildLanguageMenu();
185static void rebuildMenuLabels();
187static void onInactivityTimeout();
189static void clearKeypadBuffer();
190
199void drawSignalBars(Gdey029T94* gfx, int x, int y, int8_t rssi, bool inverted) {
200 if (!gfx) return;
201
202 int bars;
203 if (rssi > -50) bars = 4;
204 else if (rssi > -60) bars = 3;
205 else if (rssi > -70) bars = 2;
206 else bars = 1;
207
208 uint16_t fg = inverted ? EPD_WHITE : EPD_BLACK;
209
210 int barWidth = 3;
211 int gap = 1;
212 int baseY = y + 13;
213
214 for (int i = 0; i < 4; i++) {
215 int barHeight = 4 + i * 3;
216 int bx = x + i * (barWidth + gap);
217 int by = baseY - barHeight;
218
219 if (i < bars) {
220 gfx->fillRect(bx, by, barWidth, barHeight, fg);
221 } else {
222 gfx->drawRect(bx, by, barWidth, barHeight, fg);
223 }
224 }
225}
226
238static void updateStatusIcon(StatusIcon icon, bool active, bool& last) {
239 if (active == last) return;
240 if (active) {
241 s_lockScreen->addStatusIcon(icon);
242 } else {
243 s_lockScreen->removeStatusIcon(icon);
244 }
245 last = active;
246}
247
255 bool present = s_deps.power->isBatteryPresent();
256 uint32_t nowMs = static_cast<uint32_t>(esp_timer_get_time() / 1000ULL);
257
258 if (present != s_lastBatteryPresent) {
259 if (present) {
260 s_lockScreen->removeStatusIcon(StatusIcon::NO_BATTERY);
261 s_lockScreen->setBatteryPercent(s_deps.power->getBatteryPercent());
262 } else {
263 s_lockScreen->addStatusIcon(StatusIcon::NO_BATTERY);
264 s_lockScreen->setBatteryPercent(0);
265 }
266 s_lastBatteryPresent = present;
267 s_lastBatterySampleMs = nowMs;
268 return;
269 }
270
271 if (!present) return;
272
273 if (s_lastBatterySampleMs != 0 &&
275 return;
276 }
277 s_lastBatterySampleMs = nowMs;
278 s_lockScreen->setBatteryPercent(s_deps.power->getBatteryPercent());
279}
280
285 if (!s_lockScreen || !s_deps.power) return;
286
287 const bool usbConnected = s_deps.power->isUsbConnected();
288 const hal::ChargeStatus chargeStatus = s_deps.power->getChargeStatus();
289 const bool charging = (chargeStatus == hal::ChargeStatus::FAST_CHARGE ||
290 chargeStatus == hal::ChargeStatus::PRE_CHARGE);
291
292 auto* wifi = hal::getWifiControllerInstance();
293 const bool wifiConnected = wifi && wifi->isConnected();
294
296 const bool bleEnabled = ble && ble->isEnabled();
297
298 const bool backgroundPlugin =
300
306
308}
309
313static void clearKeypadBuffer() {
314 if (!s_deps.keypad) return;
315 while (s_deps.keypad->getNextKey() != hal::Key::KEY_NONE) {}
316}
317
322 if (!s_lockScreen) return;
323 if (ViewStack::instance().current() != s_lockScreen) return;
324
325 time_t now = time(nullptr);
326 struct tm* t = localtime(&now);
327 if (!t || t->tm_min == s_lastMinute) return;
328
329 s_lastMinute = t->tm_min;
330 char buf[40];
331 snprintf(buf, sizeof(buf), "%02d:%02d", t->tm_hour, t->tm_min);
332 s_lockScreen->setClock(buf);
333 snprintf(buf, sizeof(buf), "%02d.%02d.%04d", t->tm_mday, t->tm_mon + 1, t->tm_year + 1900);
334 s_lockScreen->setDate(buf);
335}
336
340static void onUnlockRequested() {
343
344 if (s_pinEntry) {
345 s_pinEntry->clear();
347 }
348}
349
355static bool onPinVerify(const char* pin) {
357 // Duress check runs before the badge-PIN verify. The duress PIN is forced
358 // distinct from the badge PIN at set time, so the order is unambiguous.
359 // selfDestruct() does not return; from the user's perspective entry is
360 // indistinguishable from any other PIN attempt (no UI/log/timing tell).
361 if (pm.hasDuressPin() && pm.isDuressPin(pin)) {
363 }
364 return pm.verifyBadgePin(pin);
365}
366
378static bool onDuressPinSet(const char* currentPin, const char* newPin) {
379 (void)currentPin;
381}
382
394
398static void onPinSuccess() {
401 if (s_deps.display) s_deps.display->backlightOn();
402}
403
407static void onInactivityTimeout() {
409 // No auto-lock while a prevent_sleep plugin holds the foreground.
410 if (pm.activePluginPreventsSleep()) {
412 return;
413 }
414 // A non-background foreground plugin is unloaded on lock; a background
415 // plugin is demoted and keeps running.
416 pm.requestStopActivePlugin();
417 while (ViewStack::instance().depth() > 1) {
419 }
422}
423
432static void performAntiBlockLock() {
434
435 auto& stack = ViewStack::instance();
436 while (stack.hasModal()) stack.hideModal();
437 while (stack.depth() > 1) stack.pop();
438
440
443 if (s_lockScreen) s_lockScreen->markDirty();
444}
445
450 auto& moduleReg = core::ModuleRegistry::instance();
451
452 s_mainMenuPluginCount = moduleReg.getMenuItems(
456 );
457
458 for (uint8_t i = 0; i < s_mainMenuPluginCount; i++) {
459 s_mainMenuItems[i] = {s_mainMenuModuleItems[i].label, 0, false, nullptr};
460 }
461
462 s_mainMenuItems[getPluginsIndex()] = {ui::tr("core.plugins"), 0, false, nullptr};
463 s_mainMenuItems[getToolsIndex()] = {ui::tr("core.tools"), 0, false, nullptr};
464 s_mainMenuItems[getSettingsIndex()] = {ui::tr("core.settings"), 0, false, nullptr};
465
466 if (s_mainMenu) {
467 s_mainMenu->init(ui::tr("core.main_menu"), s_mainMenuItems, getMainMenuCount());
468 }
469}
470
474 const char* labelKey; // i18n key
475 uint8_t (*icon)(); // optional status icon getter (nullptr -> none)
476 void (*action)(); // invoked when the entry is selected
477};
478
479static uint8_t toolsBluetoothIcon() {
481 return static_cast<uint8_t>(ble && ble->isEnabled() ? '*' : 0);
482}
483
484static const FixedMenuEntry kToolsFixed[] = {
485 {"core.wifi_menu", nullptr, showWifiMainMenu},
486 {"core.bluetooth", toolsBluetoothIcon, showBluetoothMenu},
487 {"core.msg_beacon", nullptr, showBeaconMenu},
488 {"core.expert", nullptr, showExpertMenu},
489};
490static constexpr uint8_t TOOLS_FIXED_COUNT =
491 static_cast<uint8_t>(sizeof(kToolsFixed) / sizeof(kToolsFixed[0]));
492
497 for (uint8_t i = 0; i < TOOLS_FIXED_COUNT; i++) {
498 s_toolsItems[i] = {ui::tr(kToolsFixed[i].labelKey),
499 kToolsFixed[i].icon ? kToolsFixed[i].icon() : uint8_t{0},
500 false, nullptr};
501 }
502
503 auto& moduleReg = core::ModuleRegistry::instance();
504 s_toolsModuleCount = moduleReg.getMenuItems(
508 );
509
510 for (uint8_t i = 0; i < s_toolsModuleCount; i++) {
511 s_toolsItems[TOOLS_FIXED_COUNT + i] = {s_toolsModuleItems[i].label, 0, false, nullptr};
512 }
513
514 if (s_toolsMenu) {
516 }
517}
518
522static void rebuildMenuLabels() {
523 // Settings items
524 s_settingsItems[SETTINGS_IDX_BRIGHTNESS] = {ui::tr("core.brightness"), 0, false, nullptr};
525 s_settingsItems[SETTINGS_IDX_LANGUAGE] = {ui::tr("core.language"), 0, false, nullptr};
526 s_settingsItems[SETTINGS_IDX_TIMEZONE] = {ui::tr("core.timezone"), 0, false, nullptr};
527 s_settingsItems[SETTINGS_IDX_AUTO_SLEEP] = {ui::tr("core.auto_sleep"), 0, false, nullptr};
528 s_settingsItems[SETTINGS_IDX_BADGE_TEXT] = {ui::tr("core.badge_text"), 0, false, nullptr};
529 s_settingsItems[SETTINGS_IDX_SET_DATE] = {ui::tr("core.set_date"), 0, false, nullptr};
530 s_settingsItems[SETTINGS_IDX_SET_TIME] = {ui::tr("core.set_time"), 0, false, nullptr};
531 s_settingsItems[SETTINGS_IDX_CHANGE_PIN] = {ui::tr("core.change_pin"), 0, false, nullptr};
532
533 if (s_settingsMenu) {
535 }
536
539}
540
546static void onMainMenuSelect(uint16_t index, void* userData) {
547 (void)userData;
548
549 // Module items first
550 if (index < s_mainMenuPluginCount) {
551 auto& item = s_mainMenuModuleItems[index];
552 if (item.getView) {
553 IView* view = item.getView();
554 if (view) ViewStack::instance().push(view);
555 }
556 return;
557 }
558
559 if (index == getPluginsIndex()) {
562 } else if (index == getToolsIndex()) {
564 } else if (index == getSettingsIndex()) {
566 }
567}
568
574static void onToolsSelect(uint16_t index, void* userData) {
575 (void)userData;
576
577 switch (index) {
578 case 0: showWifiMainMenu(); return;
579 case 1: showBluetoothMenu(); return;
580 case 2: showBeaconMenu(); return;
581 case 3: showExpertMenu(); return;
582 }
583
584 uint8_t moduleIdx = index - TOOLS_FIXED_COUNT;
585 if (moduleIdx < s_toolsModuleCount) {
586 auto& item = s_toolsModuleItems[moduleIdx];
587 if (item.getView) {
588 IView* view = item.getView();
589 if (view) ViewStack::instance().push(view);
590 }
591 }
592}
593
599static void onSettingsSelect(uint16_t index, void* userData) {
600 (void)userData;
601
602 switch (index) {
605 break;
609 break;
612 break;
615 break;
618 break;
621 break;
624 break;
626 if (s_pinChangeView) {
629 }
630 break;
631 }
632}
633
641static void rebuildLanguageMenu() {
642 if (!s_languageMenu) return;
643 auto& i18n = I18n::instance();
644
645 s_languageCount = 0;
646 auto addLanguage = [&](const char* code) {
647 if (s_languageCount >= MAX_LANGUAGES) return;
648 std::strncpy(s_languageCodes[s_languageCount], code,
649 sizeof(s_languageCodes[0]) - 1);
650 s_languageCodes[s_languageCount][sizeof(s_languageCodes[0]) - 1] = '\0';
651 s_languageItems[s_languageCount] = {i18n.languageName(code), 0, false, nullptr};
653 };
654
655 addLanguage("en");
656 for (const auto& lang : i18n.availableOverlayLanguages()) {
657 addLanguage(lang.code.c_str());
658 }
659
660 s_languageMenu->init(ui::tr("core.language"), s_languageItems, s_languageCount);
661 s_languageMenu->setOnSelect(onLanguageSelect);
662
663 for (uint16_t i = 0; i < s_languageCount; ++i) {
664 if (i18n.getLanguageCode() == s_languageCodes[i]) {
665 s_languageMenu->setSelection(i);
666 break;
667 }
668 }
669}
670
676static void onLanguageSelect(uint16_t index, void* userData) {
677 (void)userData;
678 if (index >= s_languageCount) return;
679
683}
684
690 return ViewStack::instance().depth() <= 1 &&
692}
693
703static void onBleNumericComparison(uint16_t connHandle, uint32_t passkey) {
705
706 bool dropped = false;
707 {
709 if (s_pendingPairing.valid) {
710 dropped = true;
711 } else {
712 s_pendingPairing.connHandle = connHandle;
713 s_pendingPairing.passkey = passkey;
714 s_pendingPairing.valid = true;
715 }
716 }
717 if (dropped) {
718 if (ble) ble->respondToNumericComparison(connHandle, false);
719 return;
720 }
721
723 {
725 s_pendingPairing.valid = false;
726 }
727 if (ble) ble->respondToNumericComparison(connHandle, false);
728 }
729}
730
738static void onBlePairingRequestEvent(const core::Event& evt) {
739 (void)evt;
740 PendingPairing req;
741 {
743 req = s_pendingPairing;
744 s_pendingPairing.valid = false;
745 }
746 if (!req.valid) return;
747
749 if (isBadgeLocked()) {
750 if (ble) ble->respondToNumericComparison(req.connHandle, false);
751 return;
752 }
753 if (!s_pairingPrompt) {
755 }
756 s_pairingPrompt->prepare(req.connHandle, req.passkey);
758}
759
764void ui_init(const UiDeps& deps) {
765 s_deps = deps;
766
767 // Initialize I18n
769
770 // Refresh menu labels whenever the active translation table changes,
771 // so item label pointers stay in sync with the loaded overlay.
774 });
775
776 // Create LockScreen
778 s_lockScreen->init();
779
780 if (s_deps.keypad) {
781 s_deps.keypad->setLongPressEnabled(true, 800);
782 s_deps.keypad->setLongPressCallback([](hal::Key key) {
783 char keyChar = static_cast<char>(key);
785 });
786 s_deps.keypad->setPanicChordCallback([]() {
787 s_antiBlockLockRequested.store(true, std::memory_order_relaxed);
788 });
789 }
790
791 // Load display texts from NVS
792 {
793 nvs_handle_t nvs;
794 char buf[64];
795 size_t len;
796
797 if (nvs_open("display", NVS_READONLY, &nvs) == ESP_OK) {
798 len = sizeof(buf);
799 if (nvs_get_str(nvs, "name", buf, &len) == ESP_OK && len > 1) {
800 s_lockScreen->setDisplayName(buf);
801 } else {
802 s_lockScreen->setDisplayName(ui::tr("core.default_name"));
803 }
804
805 len = sizeof(buf);
806 if (nvs_get_str(nvs, "info", buf, &len) == ESP_OK && len > 1) {
807 s_lockScreen->setInfo(buf);
808 } else {
809 s_lockScreen->setInfo(ui::tr("core.default_info"));
810 }
811
812 len = sizeof(buf);
813 if (nvs_get_str(nvs, "info2", buf, &len) == ESP_OK && len > 1) {
814 s_lockScreen->setInfo2(buf);
815 }
816
817 nvs_close(nvs);
818 } else {
819 s_lockScreen->setDisplayName(ui::tr("core.default_name"));
820 s_lockScreen->setInfo(ui::tr("core.default_info"));
821 }
822 }
823
824 s_lockScreen->setOnUnlock(onUnlockRequested);
825 s_lockScreen->setPreRenderCallback([]() {
826 if (s_deps.power) {
827 s_deps.power->refresh();
828 }
830 });
831
832 if (!core::TropicSlotMap::instance().isValid()) {
833 const char* msg = core::TropicSlotMap::instance().errorMessage();
834 showToastAlertSticky(msg ? msg : "Slot map invalid");
835 }
836
837 // Set initial clock/date from RTC
838 {
839 time_t now = time(nullptr);
840 struct tm* t = localtime(&now);
841 if (t) {
842 char buf[40];
843 snprintf(buf, sizeof(buf), "%02d:%02d", t->tm_hour, t->tm_min);
844 s_lockScreen->setClock(buf);
845 snprintf(buf, sizeof(buf), "%02d.%02d.%04d", t->tm_mday, t->tm_mon + 1, t->tm_year + 1900);
846 s_lockScreen->setDate(buf);
847 s_lastMinute = t->tm_min;
848 }
849 }
850
851 // Set initial battery/charging status
852 if (s_deps.power) {
853 s_lockScreen->setBatteryPercent(s_deps.power->getBatteryPercent());
854 if (s_deps.power->getChargeStatus() == hal::ChargeStatus::FAST_CHARGE ||
855 s_deps.power->getChargeStatus() == hal::ChargeStatus::PRE_CHARGE) {
856 s_lockScreen->addStatusIcon(StatusIcon::CHARGING);
857 }
858 if (s_deps.power->isUsbConnected()) {
859 s_lockScreen->addStatusIcon(StatusIcon::USB);
860 }
861 s_lastUsbConnected = s_deps.power->isUsbConnected();
862 s_lastCharging = (s_deps.power->getChargeStatus() == hal::ChargeStatus::FAST_CHARGE ||
863 s_deps.power->getChargeStatus() == hal::ChargeStatus::PRE_CHARGE);
864 }
865
866 // Create PinEntryView
867 s_pinEntry = new PinEntryView();
868 s_pinEntry->init(ui::tr("core.enter_pin"), 8, 3);
869 s_pinEntry->setOnVerify(onPinVerify);
870 s_pinEntry->setOnSuccess(onPinSuccess);
871
872 // Create Main Menu
873 s_mainMenu = new ListView();
874 s_mainMenu->setOnSelect(onMainMenuSelect);
875
876 // Tools Menu
877 s_toolsMenu = new ListView();
878 s_toolsMenu->setOnSelect(onToolsSelect);
879
880 // Build menus with module items
883
884 // Settings Menu
885 s_settingsItems[SETTINGS_IDX_BRIGHTNESS] = {ui::tr("core.brightness"), 0, false, nullptr};
886 s_settingsItems[SETTINGS_IDX_LANGUAGE] = {ui::tr("core.language"), 0, false, nullptr};
887 s_settingsItems[SETTINGS_IDX_TIMEZONE] = {ui::tr("core.timezone"), 0, false, nullptr};
888 s_settingsItems[SETTINGS_IDX_AUTO_SLEEP] = {ui::tr("core.auto_sleep"), 0, false, nullptr};
889 s_settingsItems[SETTINGS_IDX_BADGE_TEXT] = {ui::tr("core.badge_text"), 0, false, nullptr};
890 s_settingsItems[SETTINGS_IDX_SET_DATE] = {ui::tr("core.set_date"), 0, false, nullptr};
891 s_settingsItems[SETTINGS_IDX_SET_TIME] = {ui::tr("core.set_time"), 0, false, nullptr};
892 s_settingsItems[SETTINGS_IDX_CHANGE_PIN] = {ui::tr("core.change_pin"), 0, false, nullptr};
893 s_settingsMenu = new ListView();
895 s_settingsMenu->setOnSelect(onSettingsSelect);
896
897 // Initialize settings handlers
898 settings::init(s_deps.display, s_deps.sleep, s_lockScreen);
899
900 // Brightness Slider
902 uint16_t currentBrightness = s_deps.display ? s_deps.display->getBacklight() / 10 : 50;
903 s_brightnessSlider->init(ui::tr("core.brightness"), 0, 100, currentBrightness, 1, "%");
907
908 // Auto Sleep Slider
910 uint16_t currentSleepMin = 0;
911 if (s_deps.sleep) {
912 currentSleepMin = static_cast<uint16_t>(s_deps.sleep->getLightSleepInterval() / 60);
913 }
914 s_sleepSlider->init(ui::tr("core.auto_sleep"), 0, 60, currentSleepMin, 1, ui::tr("core.minutes"));
915 s_sleepSlider->setZeroLabel(ui::tr("core.never"));
917
918 // Timezone Slider
920 {
921 auto* rtcTz = hal::getRtcInstance();
922 int8_t currentTz = rtcTz ? rtcTz->getTimezoneOffset() : 0;
923 uint16_t tzSliderValue = static_cast<uint16_t>(currentTz + 12);
924 s_timezoneSlider->init(ui::tr("core.timezone"), 0, 26, tzSliderValue, 1, "h");
925 s_timezoneSlider->setDisplayOffset(-12);
927 }
928
929 // Language Menu - populated from the overlay files present (English only
930 // until the plugins FAT is mounted and the overlay scan runs at boot).
931 s_languageMenu = new ListView();
933
934 // Date/Time Input Views
935 time_t now = time(nullptr);
936 struct tm* tm = localtime(&now);
938 s_dateInput->init(ui::tr("core.set_date"),
939 tm ? tm->tm_mday : 1,
940 tm ? tm->tm_mon + 1 : 1,
941 tm ? tm->tm_year + 1900 : 2026);
943
945 s_timeInput->init(ui::tr("core.set_time"),
946 tm ? tm->tm_hour : 12,
947 tm ? tm->tm_min : 0);
949
950 // PIN Change View
954
955 // Duress / self-destruct PIN setup: same wizard, step 1 verifies the badge
956 // PIN, steps 2-3 set the duress PIN (armed via onDuressPinSet).
959 s_duressPinView->setTitle(ui::tr("core.set_duress_pin"));
960 s_duressPinView->setChangeCallback(onDuressPinSet);
962
963 // Initialize PinManager
965
966 // Initialize SleepManager
968
969 // Push to ViewStack
971
972 // Configure inactivity timeout
974
975 // Subscribe to module error events
978
979 // Central numeric-comparison pairing prompt: the request arrives on the
980 // nimble_host task and is deferred to the main task for UI work.
981 if (!s_blePendingMutex) {
982 s_blePendingMutex = xSemaphoreCreateMutex();
983 }
986 if (auto* ble = hal::getBluetoothControllerInstance()) {
987 ble->addNumericComparisonCallback(onBleNumericComparison);
988 }
989
990 // Badge-to-badge message transfer: consent prompt, peer picker, progress.
992
993 // Serial callbacks
995 serial::SerialCmd::setTextCallback([](const char* field, const char* value) {
996 if (!s_lockScreen) return;
997 // Serial text arrives already as CP437 (matches T9 and the display
998 // pipeline), so it is stored verbatim.
999 if (strcmp(field, "name") == 0) {
1000 s_lockScreen->setDisplayName(value);
1001 settings::saveDisplayField("name", value);
1002 } else if (strcmp(field, "info") == 0) {
1003 s_lockScreen->setInfo(value);
1004 settings::saveDisplayField("info", value);
1005 } else if (strcmp(field, "info2") == 0) {
1006 s_lockScreen->setInfo2(value);
1007 settings::saveDisplayField("info2", value);
1008 }
1009 });
1010
1012 if (!s_lockScreen) return;
1013 time_t now = time(nullptr);
1014 struct tm* tm = localtime(&now);
1015 if (tm) {
1016 char buf[32];
1017 snprintf(buf, sizeof(buf), "%02d:%02d", tm->tm_hour, tm->tm_min);
1018 s_lockScreen->setClock(buf);
1019 snprintf(buf, sizeof(buf), "%02d.%02d.%d", tm->tm_mday, tm->tm_mon + 1, tm->tm_year + 1900);
1020 s_lockScreen->setDate(buf);
1021 }
1022 });
1023}
1024
1032
1043
1053 if (s_lockScreen) {
1054 s_lockScreen->setDisplayName("BOOTLOADER MODE");
1055 s_lockScreen->setInfo("Awaiting flash...");
1056 s_lockScreen->setInfo2("Press RESET to resume");
1057 s_lockScreen->markDirty();
1058 }
1059
1060 auto& stack = ViewStack::instance();
1061 while (stack.hasModal()) {
1062 stack.hideModal();
1063 }
1064 while (stack.depth() > 1) {
1065 stack.pop();
1066 }
1067
1068 if (s_lockScreen) {
1069 s_lockScreen->render(false);
1070 if (s_deps.display) {
1071 s_deps.display->flushSync(hal::RefreshMode::FULL);
1072 // Defensive wait in case the driver returns from flushSync()
1073 // before the panel busy line has fully deasserted.
1074 for (int i = 0; i < 50 && s_deps.display->isBusy(); ++i) {
1075 vTaskDelay(pdMS_TO_TICKS(20));
1076 }
1077 s_deps.display->backlightOff();
1078 }
1079 }
1080}
1081
1086void ui_process(uint32_t nowMs) {
1087 // Rescue chord (N+Y) requested an anti-block lock: bypass the current view
1088 // entirely and force a clean locked state before any key dispatch.
1089 if (s_antiBlockLockRequested.exchange(false, std::memory_order_relaxed)) {
1091 }
1092
1093 // Update status icons when on lock screen
1094 if (s_lockScreen && ViewStack::instance().current() == s_lockScreen) {
1096 }
1097
1098 // Update clock at minute change
1100
1101 // Keypad input
1102 if (s_deps.keypad) {
1105 if (!s_deps.keypad->anyKeyDown()) {
1107 }
1108 } else {
1109 hal::Key key = s_deps.keypad->getNextKey();
1110 if (key != hal::Key::KEY_NONE) {
1111 char keyChar = static_cast<char>(key);
1114 }
1115 }
1116 }
1117
1119 msgTransferUiProcess(nowMs);
1120
1121 nowMs = static_cast<uint32_t>(esp_timer_get_time() / 1000ULL);
1123 if (ViewStack::instance().depth() > 1) {
1125 } else {
1126 // On lock screen: check for light sleep
1128 }
1129
1130 // Render if needed
1131 if (ViewStack::instance().needsRender()) {
1133 }
1134}
1135
1136} // namespace cdc::ui
Main-menu entry "Plugins" - lists all installed WASM plugins.
Discovers, loads, runs and unloads WASM plugins on the badge.
Shared RAII wrappers for firmware resources.
static EventBus & instance()
Returns singleton event-bus instance.
Definition EventBus.cpp:19
static constexpr uint32_t eventMask(EventType type)
Definition EventBus.h:127
uint8_t subscribe(EventHandler handler, uint32_t mask=0)
Subscribes an event handler with optional type mask.
Definition EventBus.cpp:52
void dispatchUnlock()
Dispatches unlock lifecycle event to started modules.
static ModuleRegistry & instance()
Returns the singleton module registry instance.
void dispatchLock()
Dispatches lock lifecycle event to started modules.
RAII wrapper for a FreeRTOS semaphore / mutex.
Definition Raii.h:181
static constexpr uint8_t BADGE_PIN_MAX
Definition PinManager.h:50
static constexpr uint8_t BADGE_PIN_MIN
Definition PinManager.h:49
static PinManager & instance()
Returns singleton PIN manager instance.
bool setDuressPin(const char *pin)
Sets the duress PIN, arming the self-destruct trigger.
bool init()
Initializes PIN state from secure storage or defaults.
static TropicSlotMap & instance()
Returns singleton Tropic slot-map instance.
const char * errorMessage() const
bool hasBackgroundPlugin() const noexcept
True if at least one plugin is currently resident in the background slot.
static PluginManager & instance() noexcept
static void setTextCallback(TextChangeCallback callback)
Sets the callback used by text-setting commands.
static void setTimeCallback(TimeChangeCallback callback)
Sets the callback invoked after successful date/time updates.
static I18n & instance()
Singleton accessor.
Definition I18n.cpp:287
void setOnLanguageChanged(LanguageChangedCallback cb)
Definition I18n.h:172
bool setLanguageCode(const char *code)
Set the active language by code.
Definition I18n.cpp:370
bool init()
Initialize and load persisted language code from NVS.
Definition I18n.cpp:298
void checkLockScreenSleep(uint32_t nowMs)
Evaluates whether lock-screen light sleep should be entered.
static SleepManager & instance()
Returns singleton sleep manager instance.
void resetTimer(uint32_t nowMs)
Resets lock-screen sleep timer using explicit timestamp.
void init(hal::ISleepController *sleep, hal::IPowerManager *power, LockScreenView *lockScreen)
Initializes sleep-manager dependencies and state.
void setInactivityTimeout(InactivityCallback callback, uint32_t timeoutMs)
IView * current() const
void dispatchLongPress(char key)
void replace(IView *view, void *context=nullptr)
void dispatchKey(char key)
void dispatchTick(uint32_t nowMs)
void render(bool synchronous=false)
Render current view (and modal if present) and flush to display.
static ViewStack & instance()
Returns singleton view-stack instance.
Definition ViewStack.cpp:34
void showModal(IView *modal)
void checkInactivity(uint32_t nowMs)
void push(IView *view, void *context=nullptr)
uint8_t depth() const
Definition ViewStack.h:83
void resetInactivityTimer()
void selfDestruct()
Triggers a full factory wipe on the next boot and restarts.
IWifiController * getWifiControllerInstance()
Returns the singleton Wi-Fi controller service instance.
IBluetoothController * getBluetoothControllerInstance()
Returns singleton Bluetooth stub when NimBLE is unavailable.
IRtc * getRtcInstance()
Returns the singleton RTC service instance.
Definition Rtc.cpp:304
void onPinChangeComplete(bool success)
Handles completion of PIN-change flow.
void saveDisplayField(const char *key, const char *value)
Saves one display text field to NVS.
void onBrightnessChange(uint16_t value)
Applies backlight preview without persisting.
uint16_t brightnessStepCallback(uint16_t current, bool increasing)
Returns adaptive brightness step size.
void onBrightnessSave(uint16_t value)
Persists and applies selected backlight value.
void processPendingBadgeText()
Processes the next pending badge-text wizard step.
void onDateConfirm(uint8_t day, uint8_t month, uint16_t year)
Applies confirmed date to system time.
void startBadgeTextEdit()
Starts badge-text editing wizard.
void onTimeConfirm(uint8_t hour, uint8_t minute)
Applies confirmed time to system clock.
void onTimezoneSave(uint16_t value)
Saves timezone offset and refreshes lock-screen clock.
void init(hal::IDisplay *display, hal::ISleepController *sleep, LockScreenView *lockScreen)
Initializes shared dependencies used by the settings handlers.
void onSleepIntervalSave(uint16_t value)
Saves lock-screen sleep interval in minutes.
Centralized key-code constants for cdc_views.
Definition IModule.h:8
static bool s_lastUsbConnected
Last known status-icon inputs to avoid redundant updates.
Definition AppUi.cpp:137
const char * tr(const char *key)
Look up a translation by string key.
Definition I18n.h:208
MainMenuFixed
Definition AppUi.cpp:62
@ MM_PLUGINS
Definition AppUi.cpp:62
@ MM_SETTINGS
Definition AppUi.cpp:62
@ MM_TOOLS
Definition AppUi.cpp:62
@ MAIN_MENU_FIXED_COUNT
Definition AppUi.cpp:62
static uint16_t s_languageCount
Definition AppUi.cpp:131
static void performAntiBlockLock()
Anti-block instant lock: force the badge into a clean locked state.
Definition AppUi.cpp:432
static void updateStatusIcon(StatusIcon icon, bool active, bool &last)
Updates a single boolean-driven status icon on the lock screen.
Definition AppUi.cpp:238
static constexpr uint8_t TOOLS_FIXED_COUNT
Definition AppUi.cpp:490
void showBeaconMenu()
static std::atomic< bool > s_antiBlockLockRequested
Definition AppUi.cpp:157
static const FixedMenuEntry kToolsFixed[]
Definition AppUi.cpp:484
static constexpr uint16_t MAX_LANGUAGES
Upper bound on languages shown in the picker (English + overlay files).
Definition AppUi.cpp:82
static LockScreenView * s_lockScreen
Static UI state and lazily constructed view pointers.
Definition AppUi.cpp:85
static uint8_t s_mainMenuPluginCount
Definition AppUi.cpp:118
static ListItem s_languageItems[MAX_LANGUAGES]
Language menu backing storage (filled dynamically from overlay files).
Definition AppUi.cpp:129
static BlePairingPromptView * s_pairingPrompt
Definition AppUi.cpp:98
void showDuressPinSetup()
Opens the duress / self-destruct PIN setup wizard.
Definition AppUi.cpp:389
static ListView * s_languageMenu
Definition AppUi.cpp:93
static ListView * s_toolsMenu
Definition AppUi.cpp:88
void prepareForBootloaderReset()
Puts the badge into a quiet pre-reset state.
Definition AppUi.cpp:1052
static UiDeps s_deps
Runtime dependencies provided during ui_init.
Definition AppUi.cpp:113
static uint8_t getSettingsIndex()
Returns main-menu index of the fixed "Settings" item.
Definition AppUi.cpp:164
static bool s_lastBatteryPresent
Definition AppUi.cpp:142
static bool s_lastWifiConnected
Definition AppUi.cpp:139
static ListItem s_mainMenuItems[MAIN_MENU_MAX_ITEMS]
Main-menu backing storage for module and fixed menu entries.
Definition AppUi.cpp:116
static PinChangeView * s_pinChangeView
Definition AppUi.cpp:96
static bool s_lastBackgroundPlugin
Definition AppUi.cpp:141
static void onLanguageSelect(uint16_t index, void *userData)
Handles language-menu item selection.
Definition AppUi.cpp:676
static void clearKeypadBuffer()
Drains buffered keypad events.
Definition AppUi.cpp:313
static constexpr uint8_t TOOLS_MAX_ITEMS
Definition AppUi.cpp:63
static uint32_t s_lastBatterySampleMs
Definition AppUi.cpp:150
static void onSettingsSelect(uint16_t index, void *userData)
Handles settings-menu item selection.
Definition AppUi.cpp:599
static constexpr uint32_t INACTIVITY_TIMEOUT_MS
Definition AppUi.cpp:65
static int8_t s_lastMinute
Last rendered minute for lock-screen clock throttling.
Definition AppUi.cpp:134
void rebuildToolsMenu()
Rebuilds tools menu entries including dynamic module tools.
Definition AppUi.cpp:496
SettingsMenuIdx
Index enum for the fixed settings menu.
Definition AppUi.cpp:69
@ SETTINGS_IDX_LANGUAGE
Definition AppUi.cpp:71
@ SETTINGS_IDX_AUTO_SLEEP
Definition AppUi.cpp:73
@ SETTINGS_IDX_SET_DATE
Definition AppUi.cpp:75
@ SETTINGS_IDX_SET_TIME
Definition AppUi.cpp:76
@ SETTINGS_IDX_TIMEZONE
Definition AppUi.cpp:72
@ SETTINGS_IDX_COUNT
Definition AppUi.cpp:78
@ SETTINGS_IDX_BRIGHTNESS
Definition AppUi.cpp:70
@ SETTINGS_IDX_CHANGE_PIN
Definition AppUi.cpp:77
@ SETTINGS_IDX_BADGE_TEXT
Definition AppUi.cpp:74
static cdc::plugin_manager::PluginListView * s_pluginListView
Definition AppUi.cpp:99
void ui_rebuild_menus()
Rebuilds dynamic UI menus.
Definition AppUi.cpp:1040
static char s_languageCodes[MAX_LANGUAGES][8]
Definition AppUi.cpp:130
static ListView * s_mainMenu
Definition AppUi.cpp:87
void msgTransferUiInit()
void rebuildMainMenu()
Rebuilds main menu entries including dynamically provided modules.
Definition AppUi.cpp:449
static bool onDuressPinSet(const char *currentPin, const char *newPin)
PinChangeView change-callback for the duress-PIN setup flow.
Definition AppUi.cpp:378
static SemaphoreHandle_t s_blePendingMutex
Definition AppUi.cpp:110
void onModuleErrorEvent(const core::Event &evt)
Displays toast notification for module error events.
static PinEntryView * s_pinEntry
Definition AppUi.cpp:86
void showToastAlertSticky(const char *message)
Shows a non-dismissible alert toast.
void showExpertMenu()
Shows expert menu and initial warning toast.
static void rebuildLanguageMenu()
Rebuilds the language picker from the overlay files present.
Definition AppUi.cpp:641
static PinChangeView * s_duressPinView
Definition AppUi.cpp:97
static PendingPairing s_pendingPairing
Definition AppUi.cpp:109
static void updateBatteryIndicator()
Updates the battery percentage indicator on the lock screen.
Definition AppUi.cpp:254
static void updateLockScreenClock()
Updates lock-screen clock/date once per minute while lock screen is visible.
Definition AppUi.cpp:321
static bool onPinVerify(const char *pin)
Verifies entered PIN via PinManager.
Definition AppUi.cpp:355
static void onUnlockRequested()
Starts unlock flow from lock screen.
Definition AppUi.cpp:340
void registerBackupSerialCommand()
Registers the AUTH-gated BACKUP serial command.
static bool s_ignoreKeyUntilRelease
Prevents stale key events directly after unlock transition.
Definition AppUi.cpp:153
void msgTransferUiProcess(uint32_t nowMs)
void showWifiMainMenu()
Shows top-level Wi-Fi menu and reloads stored configuration.
static uint8_t getMainMenuCount()
Returns effective main-menu item count including fixed entries.
Definition AppUi.cpp:166
static void onBlePairingRequestEvent(const core::Event &evt)
Main-task handler for a deferred numeric-comparison pairing request.
Definition AppUi.cpp:738
void ui_process(uint32_t nowMs)
Main UI tick: input processing, timeouts, status updates, and rendering.
Definition AppUi.cpp:1086
static void onMainMenuSelect(uint16_t index, void *userData)
Handles main-menu item selection.
Definition AppUi.cpp:546
static void onToolsSelect(uint16_t index, void *userData)
Handles tools-menu item selection.
Definition AppUi.cpp:574
static ListView * s_settingsMenu
Definition AppUi.cpp:89
void drawSignalBars(Gdey029T94 *gfx, int x, int y, int8_t rssi, bool inverted)
Draws RSSI signal bars using the shared lock-screen visual style.
Definition AppUi.cpp:199
static bool s_lastBleEnabled
Definition AppUi.cpp:140
static void onBleNumericComparison(uint16_t connHandle, uint32_t passkey)
Numeric-comparison pairing request, invoked on the nimble_host task.
Definition AppUi.cpp:703
static SliderView * s_timezoneSlider
Definition AppUi.cpp:92
void showBluetoothMenu()
Shows top-level Bluetooth menu.
void ui_init(const UiDeps &deps)
Initializes App UI, builds all core views, and wires callbacks.
Definition AppUi.cpp:764
static void rebuildMenuLabels()
Rebuilds labels for translatable menus after language change.
Definition AppUi.cpp:522
void updatePowerStatusIcons()
Synchronizes lock-screen status icons with current hardware state.
Definition AppUi.cpp:284
static SliderView * s_brightnessSlider
Definition AppUi.cpp:90
static uint8_t toolsBluetoothIcon()
Definition AppUi.cpp:479
static core::ModuleMenuItem s_mainMenuModuleItems[MAIN_MENU_MAX_ITEMS]
Definition AppUi.cpp:117
static ListItem s_settingsItems[SETTINGS_IDX_COUNT]
Settings menu backing storage.
Definition AppUi.cpp:126
static TimeInputView * s_timeInput
Definition AppUi.cpp:95
static void onPinSuccess()
Handles successful unlock and transitions to main menu.
Definition AppUi.cpp:398
static constexpr uint32_t BATTERY_SAMPLE_INTERVAL_MS
Definition AppUi.cpp:149
static SliderView * s_sleepSlider
Definition AppUi.cpp:91
static uint8_t getToolsIndex()
Returns main-menu index of the fixed "Tools" item.
Definition AppUi.cpp:162
static core::ModuleMenuItem s_toolsModuleItems[TOOLS_MAX_ITEMS]
Definition AppUi.cpp:122
void ui_on_modules_ready()
Refreshes module-backed menus once module startup is complete.
Definition AppUi.cpp:1028
static void onInactivityTimeout()
Callback invoked when inactivity timeout is reached.
Definition AppUi.cpp:407
static ListItem s_toolsItems[TOOLS_MAX_ITEMS]
Tools-menu backing storage for fixed and module entries.
Definition AppUi.cpp:121
static uint8_t s_toolsModuleCount
Definition AppUi.cpp:123
static DateInputView * s_dateInput
Definition AppUi.cpp:94
static uint8_t getPluginsIndex()
Returns main-menu index of the fixed "Plugins" item.
Definition AppUi.cpp:160
bool isBadgeLocked()
Returns whether the badge is currently locked (showing lock screen with no menu above).
Definition AppUi.cpp:689
static bool s_lastCharging
Definition AppUi.cpp:138
static constexpr uint8_t MAIN_MENU_MAX_ITEMS
Menu sizing and inactivity timeout constants.
Definition AppUi.cpp:59
Menu item registered by a module.
Definition IModule.h:29
const char * labelKey
Definition AppUi.cpp:474
uint8_t(* icon)()
Definition AppUi.cpp:475