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"
25#include "cdc_core/UsbManager.h"
26#include "cdc_core/Raii.h"
27
34
35#include "cdc_hal/IDisplay.h"
36#include "cdc_hal/IKeypad.h"
42#include "cdc_hal/IRtc.h"
43
45#include "nvs.h"
46
47#include <atomic>
48#include <cstdio>
49#include <cstring>
50#include <ctime>
51#include "freertos/FreeRTOS.h"
52#include "freertos/task.h"
53#include "freertos/semphr.h"
54#include "esp_timer.h"
55
56namespace cdc::ui {
57
59
60static constexpr uint8_t MAIN_MENU_MAX_ITEMS = 16;
61// Fixed main-menu entries appended after the dynamic module items; the
62// trailing MAIN_MENU_FIXED_COUNT is their number, derived automatically.
64static constexpr uint8_t TOOLS_MAX_ITEMS = 16;
65
66static constexpr uint32_t INACTIVITY_TIMEOUT_MS = 5 * 60 * 1000;
67
69
81
83static constexpr uint16_t MAX_LANGUAGES = 16;
84
86static LockScreenView* s_lockScreen = nullptr;
87static PinEntryView* s_pinEntry = nullptr;
88static ListView* s_mainMenu = nullptr;
89static ListView* s_toolsMenu = nullptr;
90static ListView* s_settingsMenu = nullptr;
92static SliderView* s_sleepSlider = nullptr;
93static SliderView* s_timezoneSlider = nullptr;
94static ListView* s_languageMenu = nullptr;
95static DateInputView* s_dateInput = nullptr;
96static TimeInputView* s_timeInput = nullptr;
101
102// Numeric-comparison pairing is requested from the nimble_host task; the prompt
103// must be shown from the main/UI task. The request is parked here under a mutex
104// and the UI work is deferred via BLE_PAIRING_REQUEST.
106 bool valid = false;
107 uint16_t connHandle = 0xFFFF;
108 uint32_t passkey = 0;
109};
111static SemaphoreHandle_t s_blePendingMutex = nullptr;
112
116static void (*s_transferUnlockCb)(void*) = nullptr;
117static void* s_transferUnlockUd = nullptr;
119static uint16_t s_blePairingPendingHandle = 0xFFFF;
120
122static UiDeps s_deps = {};
123
127static uint8_t s_mainMenuPluginCount = 0;
128
132static uint8_t s_toolsModuleCount = 0;
133
136
140static uint16_t s_languageCount = 0;
141
143static int8_t s_lastMinute = -1;
144
146static bool s_lastUsbConnected = false;
147static bool s_lastCharging = false;
148static bool s_lastWifiConnected = false;
149static bool s_lastBleEnabled = false;
150static bool s_lastBackgroundPlugin = false;
151static bool s_lastBatteryPresent = false;
152
153// Throttle for the battery-percent ADC sample on the lockscreen. The BQ25895
154// ADC step is 20 mV, mapped over 1000 mV (3200-4200 mV) the linear curve gives
155// ~2 %/step; an unrestricted per-tick sample makes ADC jitter flip the
156// rendered percentage, marking the view dirty and triggering a partial EPD
157// refresh on essentially every tick.
158static constexpr uint32_t BATTERY_SAMPLE_INTERVAL_MS = 30000;
159static uint32_t s_lastBatterySampleMs = 0;
160
162static bool s_ignoreKeyUntilRelease = false;
163
164// Set from the keypad task when the N+Y rescue chord is held; consumed by
165// ui_process on the UI task, which performs the anti-block instant lock.
166static std::atomic<bool> s_antiBlockLockRequested{false};
167
169static inline uint8_t getPluginsIndex() { return s_mainMenuPluginCount + MM_PLUGINS; }
171static inline uint8_t getToolsIndex() { return s_mainMenuPluginCount + MM_TOOLS; }
173static inline uint8_t getSettingsIndex() { return s_mainMenuPluginCount + MM_SETTINGS; }
176
178static void onUnlockRequested();
179
181static void onBlePairingLockedAccept(void* userData);
183static bool onPinVerify(const char* pin);
185static void onPinSuccess();
187static void onMainMenuSelect(uint16_t index, void* userData);
189static void onToolsSelect(uint16_t index, void* userData);
191static void onSettingsSelect(uint16_t index, void* userData);
193static void onLanguageSelect(uint16_t index, void* userData);
195static void rebuildLanguageMenu();
197static void rebuildMenuLabels();
199static void onInactivityTimeout();
201static void clearKeypadBuffer();
202
211void drawSignalBars(Gdey029T94* gfx, int x, int y, int8_t rssi, bool inverted) {
212 if (!gfx) return;
213
214 int bars;
215 if (rssi > -50) bars = 4;
216 else if (rssi > -60) bars = 3;
217 else if (rssi > -70) bars = 2;
218 else bars = 1;
219
220 uint16_t fg = inverted ? EPD_WHITE : EPD_BLACK;
221
222 int barWidth = 3;
223 int gap = 1;
224 int baseY = y + 13;
225
226 for (int i = 0; i < 4; i++) {
227 int barHeight = 4 + i * 3;
228 int bx = x + i * (barWidth + gap);
229 int by = baseY - barHeight;
230
231 if (i < bars) {
232 gfx->fillRect(bx, by, barWidth, barHeight, fg);
233 } else {
234 gfx->drawRect(bx, by, barWidth, barHeight, fg);
235 }
236 }
237}
238
250static void updateStatusIcon(StatusIcon icon, bool active, bool& last) {
251 if (active == last) return;
252 if (active) {
253 s_lockScreen->addStatusIcon(icon);
254 } else {
255 s_lockScreen->removeStatusIcon(icon);
256 }
257 last = active;
258}
259
267 bool present = s_deps.power->isBatteryPresent();
268 uint32_t nowMs = static_cast<uint32_t>(esp_timer_get_time() / 1000ULL);
269
270 if (present != s_lastBatteryPresent) {
271 if (present) {
272 s_lockScreen->removeStatusIcon(StatusIcon::NO_BATTERY);
273 s_lockScreen->setBatteryPercent(s_deps.power->getBatteryPercent());
274 } else {
275 s_lockScreen->addStatusIcon(StatusIcon::NO_BATTERY);
276 s_lockScreen->setBatteryPercent(0);
277 }
278 s_lastBatteryPresent = present;
279 s_lastBatterySampleMs = nowMs;
280 return;
281 }
282
283 if (!present) return;
284
285 if (s_lastBatterySampleMs != 0 &&
287 return;
288 }
289 s_lastBatterySampleMs = nowMs;
290 s_lockScreen->setBatteryPercent(s_deps.power->getBatteryPercent());
291}
292
297 if (!s_lockScreen || !s_deps.power) return;
298
299 const bool usbConnected = s_deps.power->isUsbConnected();
300 const hal::ChargeStatus chargeStatus = s_deps.power->getChargeStatus();
301 const bool charging = (chargeStatus == hal::ChargeStatus::FAST_CHARGE ||
302 chargeStatus == hal::ChargeStatus::PRE_CHARGE);
303
304 auto* wifi = hal::getWifiControllerInstance();
305 const bool wifiConnected = wifi && wifi->isConnected();
306
308 const bool bleEnabled = ble && ble->isEnabled();
309
310 const bool backgroundPlugin =
312
318
320}
321
325static void clearKeypadBuffer() {
326 if (!s_deps.keypad) return;
327 while (s_deps.keypad->getNextKey() != hal::Key::KEY_NONE) {}
328}
329
334 if (!s_lockScreen) return;
335 if (ViewStack::instance().current() != s_lockScreen) return;
336
337 time_t now = time(nullptr);
338 struct tm* t = localtime(&now);
339 if (!t || t->tm_min == s_lastMinute) return;
340
341 s_lastMinute = t->tm_min;
342 char buf[40];
343 snprintf(buf, sizeof(buf), "%02d:%02d", t->tm_hour, t->tm_min);
344 s_lockScreen->setClock(buf);
345 snprintf(buf, sizeof(buf), "%02d.%02d.%04d", t->tm_mday, t->tm_mon + 1, t->tm_year + 1900);
346 s_lockScreen->setDate(buf);
347}
348
352static void onUnlockRequested() {
353 s_transferUnlockCb = nullptr; // a plain unlock answers no pending transfer
354 s_transferUnlockUd = nullptr;
357
358 if (s_pinEntry) {
359 s_pinEntry->clear();
361 }
362}
363
369static bool onPinVerify(const char* pin) {
371 // Duress check runs before the badge-PIN verify. The duress PIN is forced
372 // distinct from the badge PIN at set time, so the order is unambiguous.
373 // selfDestruct() does not return; from the user's perspective entry is
374 // indistinguishable from any other PIN attempt (no UI/log/timing tell).
375 if (pm.hasDuressPin() && pm.isDuressPin(pin)) {
377 }
378 return pm.verifyBadgePin(pin);
379}
380
392static bool onDuressPinSet(const char* currentPin, const char* newPin) {
393 (void)currentPin;
395}
396
408
412static void onPinSuccess() {
415 if (s_deps.display) s_deps.display->backlightOn();
416
417 // A BLE transfer accepted on the lock screen (pairing or consent) is answered
418 // only now, so it continues on the freshly unlocked badge.
419 if (s_transferUnlockCb) {
420 auto cb = s_transferUnlockCb;
421 void* ud = s_transferUnlockUd;
422 s_transferUnlockCb = nullptr;
423 s_transferUnlockUd = nullptr;
424 cb(ud);
425 }
426}
427
436void requestUnlockForTransfer(void (*onUnlocked)(void*), void* userData) {
437 if (s_deps.display) s_deps.display->backlightOn();
438 onUnlockRequested(); // clears any stale callback, pushes PIN entry
439 s_transferUnlockCb = onUnlocked;
440 s_transferUnlockUd = userData;
441}
442
446static void onInactivityTimeout() {
448 // No auto-lock while a prevent_sleep plugin holds the foreground.
449 if (pm.activePluginPreventsSleep()) {
451 return;
452 }
453 // A non-background foreground plugin is unloaded on lock; a background
454 // plugin is demoted and keeps running.
455 pm.requestStopActivePlugin();
456 while (ViewStack::instance().depth() > 1) {
458 }
461}
462
471static void performAntiBlockLock() {
473
474 auto& stack = ViewStack::instance();
475 while (stack.hasModal()) stack.hideModal();
476 while (stack.depth() > 1) stack.pop();
477
479
482 if (s_lockScreen) s_lockScreen->markDirty();
483}
484
489 auto& moduleReg = core::ModuleRegistry::instance();
490
491 s_mainMenuPluginCount = moduleReg.getMenuItems(
495 );
496
497 for (uint8_t i = 0; i < s_mainMenuPluginCount; i++) {
498 s_mainMenuItems[i] = {s_mainMenuModuleItems[i].label, 0, false, nullptr};
499 }
500
501 s_mainMenuItems[getPluginsIndex()] = {ui::tr("core.plugins"), 0, false, nullptr};
502 s_mainMenuItems[getToolsIndex()] = {ui::tr("core.tools"), 0, false, nullptr};
503 s_mainMenuItems[getSettingsIndex()] = {ui::tr("core.settings"), 0, false, nullptr};
504
505 if (s_mainMenu) {
506 s_mainMenu->init(ui::tr("core.main_menu"), s_mainMenuItems, getMainMenuCount());
507 }
508}
509
513 const char* labelKey; // i18n key
514 uint8_t (*icon)(); // optional status icon getter (nullptr -> none)
515 void (*action)(); // invoked when the entry is selected
516};
517
518static uint8_t toolsBluetoothIcon() {
520 return static_cast<uint8_t>(ble && ble->isEnabled() ? '*' : 0);
521}
522
523static const FixedMenuEntry kToolsFixed[] = {
524 {"core.wifi_menu", nullptr, showWifiMainMenu},
525 {"core.bluetooth", toolsBluetoothIcon, showBluetoothMenu},
526 {"core.msg_beacon", nullptr, showBeaconMenu},
527 {"core.expert", nullptr, showExpertMenu},
528};
529static constexpr uint8_t TOOLS_FIXED_COUNT =
530 static_cast<uint8_t>(sizeof(kToolsFixed) / sizeof(kToolsFixed[0]));
531
536 for (uint8_t i = 0; i < TOOLS_FIXED_COUNT; i++) {
537 s_toolsItems[i] = {ui::tr(kToolsFixed[i].labelKey),
538 kToolsFixed[i].icon ? kToolsFixed[i].icon() : uint8_t{0},
539 false, nullptr};
540 }
541
542 auto& moduleReg = core::ModuleRegistry::instance();
543 s_toolsModuleCount = moduleReg.getMenuItems(
547 );
548
549 for (uint8_t i = 0; i < s_toolsModuleCount; i++) {
550 s_toolsItems[TOOLS_FIXED_COUNT + i] = {s_toolsModuleItems[i].label, 0, false, nullptr};
551 }
552
553 if (s_toolsMenu) {
555 }
556}
557
561static void rebuildMenuLabels() {
562 // Settings items
563 s_settingsItems[SETTINGS_IDX_BRIGHTNESS] = {ui::tr("core.brightness"), 0, false, nullptr};
564 s_settingsItems[SETTINGS_IDX_LANGUAGE] = {ui::tr("core.language"), 0, false, nullptr};
565 s_settingsItems[SETTINGS_IDX_TIMEZONE] = {ui::tr("core.timezone"), 0, false, nullptr};
566 s_settingsItems[SETTINGS_IDX_AUTO_SLEEP] = {ui::tr("core.auto_sleep"), 0, false, nullptr};
567 s_settingsItems[SETTINGS_IDX_BADGE_TEXT] = {ui::tr("core.badge_text"), 0, false, nullptr};
568 s_settingsItems[SETTINGS_IDX_SET_DATE] = {ui::tr("core.set_date"), 0, false, nullptr};
569 s_settingsItems[SETTINGS_IDX_SET_TIME] = {ui::tr("core.set_time"), 0, false, nullptr};
570 s_settingsItems[SETTINGS_IDX_CHANGE_PIN] = {ui::tr("core.change_pin"), 0, false, nullptr};
571
572 if (s_settingsMenu) {
574 }
575
578}
579
585static void onMainMenuSelect(uint16_t index, void* userData) {
586 (void)userData;
587
588 // Module items first
589 if (index < s_mainMenuPluginCount) {
590 auto& item = s_mainMenuModuleItems[index];
591 if (item.getView) {
592 IView* view = item.getView();
593 if (view) ViewStack::instance().push(view);
594 }
595 return;
596 }
597
598 if (index == getPluginsIndex()) {
601 } else if (index == getToolsIndex()) {
603 } else if (index == getSettingsIndex()) {
605 }
606}
607
613static void onToolsSelect(uint16_t index, void* userData) {
614 (void)userData;
615
616 switch (index) {
617 case 0: showWifiMainMenu(); return;
618 case 1: showBluetoothMenu(); return;
619 case 2: showBeaconMenu(); return;
620 case 3: showExpertMenu(); return;
621 }
622
623 uint8_t moduleIdx = index - TOOLS_FIXED_COUNT;
624 if (moduleIdx < s_toolsModuleCount) {
625 auto& item = s_toolsModuleItems[moduleIdx];
626 if (item.getView) {
627 IView* view = item.getView();
628 if (view) ViewStack::instance().push(view);
629 }
630 }
631}
632
638static void onSettingsSelect(uint16_t index, void* userData) {
639 (void)userData;
640
641 switch (index) {
644 break;
648 break;
651 break;
654 break;
657 break;
660 break;
663 break;
665 if (s_pinChangeView) {
668 }
669 break;
670 }
671}
672
680static void rebuildLanguageMenu() {
681 if (!s_languageMenu) return;
682 auto& i18n = I18n::instance();
683
684 s_languageCount = 0;
685 auto addLanguage = [&](const char* code) {
686 if (s_languageCount >= MAX_LANGUAGES) return;
687 std::strncpy(s_languageCodes[s_languageCount], code,
688 sizeof(s_languageCodes[0]) - 1);
689 s_languageCodes[s_languageCount][sizeof(s_languageCodes[0]) - 1] = '\0';
690 s_languageItems[s_languageCount] = {i18n.languageName(code), 0, false, nullptr};
692 };
693
694 addLanguage("en");
695 for (const auto& lang : i18n.availableOverlayLanguages()) {
696 addLanguage(lang.code.c_str());
697 }
698
699 s_languageMenu->init(ui::tr("core.language"), s_languageItems, s_languageCount);
700 s_languageMenu->setOnSelect(onLanguageSelect);
701
702 for (uint16_t i = 0; i < s_languageCount; ++i) {
703 if (i18n.getLanguageCode() == s_languageCodes[i]) {
704 s_languageMenu->setSelection(i);
705 break;
706 }
707 }
708}
709
715static void onLanguageSelect(uint16_t index, void* userData) {
716 (void)userData;
717 if (index >= s_languageCount) return;
718
722}
723
729 return ViewStack::instance().depth() <= 1 &&
731}
732
742static void onBleNumericComparison(uint16_t connHandle, uint32_t passkey) {
744
745 bool dropped = false;
746 {
748 if (s_pendingPairing.valid) {
749 dropped = true;
750 } else {
751 s_pendingPairing.connHandle = connHandle;
752 s_pendingPairing.passkey = passkey;
753 s_pendingPairing.valid = true;
754 }
755 }
756 if (dropped) {
757 if (ble) ble->respondToNumericComparison(connHandle, false);
758 return;
759 }
760
762 {
764 s_pendingPairing.valid = false;
765 }
766 if (ble) ble->respondToNumericComparison(connHandle, false);
767 }
768}
769
777static void onBlePairingRequestEvent(const core::Event& evt) {
778 (void)evt;
779 PendingPairing req;
780 {
782 req = s_pendingPairing;
783 s_pendingPairing.valid = false;
784 }
785 if (!req.valid) return;
786
787 if (!s_pairingPrompt) {
789 }
790 s_pairingPrompt->prepare(req.connHandle, req.passkey);
791
792 if (isBadgeLocked()) {
793 // Mirror the FIDO2 locked flow: wake the screen and show the request, but
794 // gate acceptance behind the badge PIN. Accepting drives the unlock flow;
795 // the pairing is answered (and the transfer continues) only after unlock.
796 if (s_deps.display) s_deps.display->backlightOn();
798 s_pairingPrompt->setOnLockedAccept(onBlePairingLockedAccept, nullptr);
799 }
801}
802
811static void onBlePairingUnlocked(void* /*userData*/) {
813 if (ble) ble->respondToNumericComparison(s_blePairingPendingHandle, true);
814}
815
816static void onBlePairingLockedAccept(void* /*userData*/) {
818}
819
824void ui_init(const UiDeps& deps) {
825 s_deps = deps;
826
827 // Initialize I18n
829
830 // Refresh menu labels whenever the active translation table changes,
831 // so item label pointers stay in sync with the loaded overlay.
834 });
835
836 // Create LockScreen
838 s_lockScreen->init();
839
840 if (s_deps.keypad) {
841 s_deps.keypad->setLongPressEnabled(true, 800);
842 s_deps.keypad->setLongPressCallback([](hal::Key key) {
843 char keyChar = static_cast<char>(key);
845#if FEATURE_EPD_LONGPRESS_FULL_REFRESH
846 // Global anti-ghosting gesture: long-press '5' forces a manual FULL
847 // refresh when no view claimed the press (T9 input etc. keep priority).
848 if (keyChar == '5' && result == ui::InputResult::IGNORED) {
850 }
851#else
852 (void)result;
853#endif
854 });
855 s_deps.keypad->setPanicChordCallback([]() {
856 s_antiBlockLockRequested.store(true, std::memory_order_relaxed);
857 });
858 }
859
860 // Load display texts from NVS
861 {
862 nvs_handle_t nvs;
863 char buf[64];
864 size_t len;
865
866 if (nvs_open("display", NVS_READONLY, &nvs) == ESP_OK) {
867 len = sizeof(buf);
868 if (nvs_get_str(nvs, "name", buf, &len) == ESP_OK && len > 1) {
869 s_lockScreen->setDisplayName(buf);
870 } else {
871 s_lockScreen->setDisplayName(ui::tr("core.default_name"));
872 }
873
874 len = sizeof(buf);
875 if (nvs_get_str(nvs, "info", buf, &len) == ESP_OK && len > 1) {
876 s_lockScreen->setInfo(buf);
877 } else {
878 s_lockScreen->setInfo(ui::tr("core.default_info"));
879 }
880
881 len = sizeof(buf);
882 if (nvs_get_str(nvs, "info2", buf, &len) == ESP_OK && len > 1) {
883 s_lockScreen->setInfo2(buf);
884 }
885
886 nvs_close(nvs);
887 } else {
888 s_lockScreen->setDisplayName(ui::tr("core.default_name"));
889 s_lockScreen->setInfo(ui::tr("core.default_info"));
890 }
891 }
892
893 s_lockScreen->setOnUnlock(onUnlockRequested);
894 s_lockScreen->setPreRenderCallback([]() {
895 if (s_deps.power) {
896 s_deps.power->refresh();
897 }
899 });
900
901 if (s_deps.power) {
902 s_deps.power->setPreShipModeCallback([]() {
903 if (s_lockScreen) {
904 s_lockScreen->renderShipModeScreen();
905 }
906 });
907 }
908
909 if (!core::TropicSlotMap::instance().isValid()) {
910 const char* msg = core::TropicSlotMap::instance().errorMessage();
911 showToastAlertSticky(msg ? msg : "Slot map invalid");
912 }
913
914 // Set initial clock/date from RTC
915 {
916 time_t now = time(nullptr);
917 struct tm* t = localtime(&now);
918 if (t) {
919 char buf[40];
920 snprintf(buf, sizeof(buf), "%02d:%02d", t->tm_hour, t->tm_min);
921 s_lockScreen->setClock(buf);
922 snprintf(buf, sizeof(buf), "%02d.%02d.%04d", t->tm_mday, t->tm_mon + 1, t->tm_year + 1900);
923 s_lockScreen->setDate(buf);
924 s_lastMinute = t->tm_min;
925 }
926 }
927
928 // Set initial battery/charging status
929 if (s_deps.power) {
930 s_lockScreen->setBatteryPercent(s_deps.power->getBatteryPercent());
931 if (s_deps.power->getChargeStatus() == hal::ChargeStatus::FAST_CHARGE ||
932 s_deps.power->getChargeStatus() == hal::ChargeStatus::PRE_CHARGE) {
933 s_lockScreen->addStatusIcon(StatusIcon::CHARGING);
934 }
935 if (s_deps.power->isUsbConnected()) {
936 s_lockScreen->addStatusIcon(StatusIcon::USB);
937 }
938 s_lastUsbConnected = s_deps.power->isUsbConnected();
939 s_lastCharging = (s_deps.power->getChargeStatus() == hal::ChargeStatus::FAST_CHARGE ||
940 s_deps.power->getChargeStatus() == hal::ChargeStatus::PRE_CHARGE);
941 }
942
943 // Create PinEntryView
944 s_pinEntry = new PinEntryView();
945 s_pinEntry->init(ui::tr("core.enter_pin"), 8, 3);
946 s_pinEntry->setOnVerify(onPinVerify);
947 s_pinEntry->setOnSuccess(onPinSuccess);
948
949 // Create Main Menu
950 s_mainMenu = new ListView();
951 s_mainMenu->setOnSelect(onMainMenuSelect);
952
953 // Tools Menu
954 s_toolsMenu = new ListView();
955 s_toolsMenu->setOnSelect(onToolsSelect);
956
957 // Build menus with module items
960
961 // Settings Menu
962 s_settingsItems[SETTINGS_IDX_BRIGHTNESS] = {ui::tr("core.brightness"), 0, false, nullptr};
963 s_settingsItems[SETTINGS_IDX_LANGUAGE] = {ui::tr("core.language"), 0, false, nullptr};
964 s_settingsItems[SETTINGS_IDX_TIMEZONE] = {ui::tr("core.timezone"), 0, false, nullptr};
965 s_settingsItems[SETTINGS_IDX_AUTO_SLEEP] = {ui::tr("core.auto_sleep"), 0, false, nullptr};
966 s_settingsItems[SETTINGS_IDX_BADGE_TEXT] = {ui::tr("core.badge_text"), 0, false, nullptr};
967 s_settingsItems[SETTINGS_IDX_SET_DATE] = {ui::tr("core.set_date"), 0, false, nullptr};
968 s_settingsItems[SETTINGS_IDX_SET_TIME] = {ui::tr("core.set_time"), 0, false, nullptr};
969 s_settingsItems[SETTINGS_IDX_CHANGE_PIN] = {ui::tr("core.change_pin"), 0, false, nullptr};
970 s_settingsMenu = new ListView();
972 s_settingsMenu->setOnSelect(onSettingsSelect);
973
974 // Initialize settings handlers
975 settings::init(s_deps.display, s_deps.sleep, s_lockScreen);
976
977 // Brightness Slider
979 uint16_t currentBrightness = s_deps.display ? s_deps.display->getBacklight() / 10 : 50;
980 s_brightnessSlider->init(ui::tr("core.brightness"), 0, 100, currentBrightness, 1, "%");
984
985 // Auto Sleep Slider
987 uint16_t currentSleepMin = 0;
988 if (s_deps.sleep) {
989 currentSleepMin = static_cast<uint16_t>(s_deps.sleep->getLightSleepInterval() / 60);
990 }
991 s_sleepSlider->init(ui::tr("core.auto_sleep"), 0, 60, currentSleepMin, 1, ui::tr("core.minutes"));
992 s_sleepSlider->setZeroLabel(ui::tr("core.never"));
994
995 // Timezone Slider
997 {
998 auto* rtcTz = hal::getRtcInstance();
999 int8_t currentTz = rtcTz ? rtcTz->getTimezoneOffset() : 0;
1000 uint16_t tzSliderValue = static_cast<uint16_t>(currentTz + 12);
1001 s_timezoneSlider->init(ui::tr("core.timezone"), 0, 26, tzSliderValue, 1, "h");
1002 s_timezoneSlider->setDisplayOffset(-12);
1004 }
1005
1006 // Language Menu - populated from the overlay files present (English only
1007 // until the plugins FAT is mounted and the overlay scan runs at boot).
1008 s_languageMenu = new ListView();
1010
1011 // Date/Time Input Views
1012 time_t now = time(nullptr);
1013 struct tm* tm = localtime(&now);
1014 s_dateInput = new DateInputView();
1015 s_dateInput->init(ui::tr("core.set_date"),
1016 tm ? tm->tm_mday : 1,
1017 tm ? tm->tm_mon + 1 : 1,
1018 tm ? tm->tm_year + 1900 : 2026);
1019 s_dateInput->setOnConfirm(settings::onDateConfirm);
1020
1021 s_timeInput = new TimeInputView();
1022 s_timeInput->init(ui::tr("core.set_time"),
1023 tm ? tm->tm_hour : 12,
1024 tm ? tm->tm_min : 0);
1025 s_timeInput->setOnConfirm(settings::onTimeConfirm);
1026
1027 // PIN Change View
1031
1032 // Duress / self-destruct PIN setup: same wizard, step 1 verifies the badge
1033 // PIN, steps 2-3 set the duress PIN (armed via onDuressPinSet).
1036 s_duressPinView->setTitle(ui::tr("core.set_duress_pin"));
1037 s_duressPinView->setChangeCallback(onDuressPinSet);
1039
1040 // Initialize PinManager
1042
1043 // Initialize SleepManager
1045
1046 // Push to ViewStack
1048
1049 // Configure inactivity timeout
1051
1052 // Subscribe to module error events
1055
1056 // Central numeric-comparison pairing prompt: the request arrives on the
1057 // nimble_host task and is deferred to the main task for UI work.
1058 if (!s_blePendingMutex) {
1059 s_blePendingMutex = xSemaphoreCreateMutex();
1060 }
1063 if (auto* ble = hal::getBluetoothControllerInstance()) {
1064 ble->addNumericComparisonCallback(onBleNumericComparison);
1065 }
1066
1067 // Badge-to-badge message transfer: consent prompt, peer picker, progress.
1069
1070 // Serial callbacks
1072 serial::SerialCmd::setTextCallback([](const char* field, const char* value) {
1073 if (!s_lockScreen) return;
1074 // Serial text arrives already as CP437 (matches T9 and the display
1075 // pipeline), so it is stored verbatim.
1076 if (strcmp(field, "name") == 0) {
1077 s_lockScreen->setDisplayName(value);
1078 settings::saveDisplayField("name", value);
1079 } else if (strcmp(field, "info") == 0) {
1080 s_lockScreen->setInfo(value);
1081 settings::saveDisplayField("info", value);
1082 } else if (strcmp(field, "info2") == 0) {
1083 s_lockScreen->setInfo2(value);
1084 settings::saveDisplayField("info2", value);
1085 }
1086 });
1087
1089 if (!s_lockScreen) return;
1090 time_t now = time(nullptr);
1091 struct tm* tm = localtime(&now);
1092 if (tm) {
1093 char buf[32];
1094 snprintf(buf, sizeof(buf), "%02d:%02d", tm->tm_hour, tm->tm_min);
1095 s_lockScreen->setClock(buf);
1096 snprintf(buf, sizeof(buf), "%02d.%02d.%d", tm->tm_mday, tm->tm_mon + 1, tm->tm_year + 1900);
1097 s_lockScreen->setDate(buf);
1098 }
1099 });
1100}
1101
1109
1120
1130 if (s_lockScreen) {
1131 s_lockScreen->setDisplayName("BOOTLOADER MODE");
1132 s_lockScreen->setInfo("Awaiting flash...");
1133 s_lockScreen->setInfo2("Press RESET to resume");
1134 s_lockScreen->markDirty();
1135 }
1136
1137 auto& stack = ViewStack::instance();
1138 while (stack.hasModal()) {
1139 stack.hideModal();
1140 }
1141 while (stack.depth() > 1) {
1142 stack.pop();
1143 }
1144
1145 if (s_lockScreen) {
1146 s_lockScreen->render(false);
1147 if (s_deps.display) {
1148 s_deps.display->flushSync(hal::RefreshMode::FULL);
1149 // Defensive wait in case the driver returns from flushSync()
1150 // before the panel busy line has fully deasserted.
1151 for (int i = 0; i < 50 && s_deps.display->isBusy(); ++i) {
1152 vTaskDelay(pdMS_TO_TICKS(20));
1153 }
1154 s_deps.display->backlightOff();
1155 }
1156 }
1157}
1158
1163void ui_process(uint32_t nowMs) {
1164 // Rescue chord (N+Y) requested an anti-block lock: bypass the current view
1165 // entirely and force a clean locked state before any key dispatch.
1166 if (s_antiBlockLockRequested.exchange(false, std::memory_order_relaxed)) {
1168 }
1169
1170 // Update status icons when on lock screen
1171 if (s_lockScreen && ViewStack::instance().current() == s_lockScreen) {
1173 }
1174
1175 // Update clock at minute change
1177
1178 // Keypad input
1179 if (s_deps.keypad) {
1182 if (!s_deps.keypad->anyKeyDown()) {
1184 }
1185 } else {
1186 hal::Key key = s_deps.keypad->getNextKey();
1187 if (key != hal::Key::KEY_NONE) {
1188 char keyChar = static_cast<char>(key);
1191 }
1192 }
1193 }
1194
1196 msgTransferUiProcess(nowMs);
1197
1198 nowMs = static_cast<uint32_t>(esp_timer_get_time() / 1000ULL);
1200 if (ViewStack::instance().depth() > 1) {
1202 } else {
1203 // On lock screen: check for light sleep
1205 }
1206
1207 // Render if needed
1208 if (ViewStack::instance().needsRender()) {
1210 }
1211}
1212
1213} // 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:139
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:306
void setOnLanguageChanged(LanguageChangedCallback cb)
Definition I18n.h:173
bool setLanguageCode(const char *code)
Set the active language by code.
Definition I18n.cpp:389
bool init()
Initialize and load persisted language code from NVS.
Definition I18n.cpp:317
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 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:53
void showModal(IView *modal)
InputResult dispatchLongPress(char key)
void checkInactivity(uint32_t nowMs)
void push(IView *view, void *context=nullptr)
uint8_t depth() const
Definition ViewStack.h:83
void forceFullRefresh()
Definition ViewStack.h:166
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:146
const char * tr(const char *key)
Look up a translation by string key.
Definition I18n.h:209
MainMenuFixed
Definition AppUi.cpp:63
@ MM_PLUGINS
Definition AppUi.cpp:63
@ MM_SETTINGS
Definition AppUi.cpp:63
@ MM_TOOLS
Definition AppUi.cpp:63
@ MAIN_MENU_FIXED_COUNT
Definition AppUi.cpp:63
static uint16_t s_languageCount
Definition AppUi.cpp:140
static void performAntiBlockLock()
Anti-block instant lock: force the badge into a clean locked state.
Definition AppUi.cpp:471
static void updateStatusIcon(StatusIcon icon, bool active, bool &last)
Updates a single boolean-driven status icon on the lock screen.
Definition AppUi.cpp:250
static constexpr uint8_t TOOLS_FIXED_COUNT
Definition AppUi.cpp:529
void showBeaconMenu()
static std::atomic< bool > s_antiBlockLockRequested
Definition AppUi.cpp:166
static const FixedMenuEntry kToolsFixed[]
Definition AppUi.cpp:523
static constexpr uint16_t MAX_LANGUAGES
Upper bound on languages shown in the picker (English + overlay files).
Definition AppUi.cpp:83
static LockScreenView * s_lockScreen
Static UI state and lazily constructed view pointers.
Definition AppUi.cpp:86
static uint8_t s_mainMenuPluginCount
Definition AppUi.cpp:127
static ListItem s_languageItems[MAX_LANGUAGES]
Language menu backing storage (filled dynamically from overlay files).
Definition AppUi.cpp:138
static BlePairingPromptView * s_pairingPrompt
Definition AppUi.cpp:99
void showDuressPinSetup()
Opens the duress / self-destruct PIN setup wizard.
Definition AppUi.cpp:403
static ListView * s_languageMenu
Definition AppUi.cpp:94
static ListView * s_toolsMenu
Definition AppUi.cpp:89
void requestUnlockForTransfer(void(*onUnlocked)(void *), void *userData)
Wake the screen and start the PIN-unlock flow for a transfer accepted on the lock screen.
Definition AppUi.cpp:436
void prepareForBootloaderReset()
Puts the badge into a quiet pre-reset state.
Definition AppUi.cpp:1129
static UiDeps s_deps
Runtime dependencies provided during ui_init.
Definition AppUi.cpp:122
static uint8_t getSettingsIndex()
Returns main-menu index of the fixed "Settings" item.
Definition AppUi.cpp:173
static bool s_lastBatteryPresent
Definition AppUi.cpp:151
static bool s_lastWifiConnected
Definition AppUi.cpp:148
static ListItem s_mainMenuItems[MAIN_MENU_MAX_ITEMS]
Main-menu backing storage for module and fixed menu entries.
Definition AppUi.cpp:125
static PinChangeView * s_pinChangeView
Definition AppUi.cpp:97
static void(* s_transferUnlockCb)(void *)
Definition AppUi.cpp:116
static bool s_lastBackgroundPlugin
Definition AppUi.cpp:150
static void onLanguageSelect(uint16_t index, void *userData)
Handles language-menu item selection.
Definition AppUi.cpp:715
static void clearKeypadBuffer()
Drains buffered keypad events.
Definition AppUi.cpp:325
static constexpr uint8_t TOOLS_MAX_ITEMS
Definition AppUi.cpp:64
static uint32_t s_lastBatterySampleMs
Definition AppUi.cpp:159
static void onSettingsSelect(uint16_t index, void *userData)
Handles settings-menu item selection.
Definition AppUi.cpp:638
static constexpr uint32_t INACTIVITY_TIMEOUT_MS
Definition AppUi.cpp:66
static int8_t s_lastMinute
Last rendered minute for lock-screen clock throttling.
Definition AppUi.cpp:143
void rebuildToolsMenu()
Rebuilds tools menu entries including dynamic module tools.
Definition AppUi.cpp:535
SettingsMenuIdx
Index enum for the fixed settings menu.
Definition AppUi.cpp:70
@ SETTINGS_IDX_LANGUAGE
Definition AppUi.cpp:72
@ SETTINGS_IDX_AUTO_SLEEP
Definition AppUi.cpp:74
@ SETTINGS_IDX_SET_DATE
Definition AppUi.cpp:76
@ SETTINGS_IDX_SET_TIME
Definition AppUi.cpp:77
@ SETTINGS_IDX_TIMEZONE
Definition AppUi.cpp:73
@ SETTINGS_IDX_COUNT
Definition AppUi.cpp:79
@ SETTINGS_IDX_BRIGHTNESS
Definition AppUi.cpp:71
@ SETTINGS_IDX_CHANGE_PIN
Definition AppUi.cpp:78
@ SETTINGS_IDX_BADGE_TEXT
Definition AppUi.cpp:75
static void onBlePairingUnlocked(void *)
Lock-screen accept of a numeric-comparison pairing.
Definition AppUi.cpp:811
static cdc::plugin_manager::PluginListView * s_pluginListView
Definition AppUi.cpp:100
static void onBlePairingLockedAccept(void *userData)
Lock-screen accept of a BLE pairing: drives PIN unlock, then pairs.
Definition AppUi.cpp:816
void ui_rebuild_menus()
Rebuilds dynamic UI menus.
Definition AppUi.cpp:1117
static char s_languageCodes[MAX_LANGUAGES][8]
Definition AppUi.cpp:139
static ListView * s_mainMenu
Definition AppUi.cpp:88
void msgTransferUiInit()
void rebuildMainMenu()
Rebuilds main menu entries including dynamically provided modules.
Definition AppUi.cpp:488
static bool onDuressPinSet(const char *currentPin, const char *newPin)
PinChangeView change-callback for the duress-PIN setup flow.
Definition AppUi.cpp:392
static SemaphoreHandle_t s_blePendingMutex
Definition AppUi.cpp:111
void onModuleErrorEvent(const core::Event &evt)
Displays toast notification for module error events.
static PinEntryView * s_pinEntry
Definition AppUi.cpp:87
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:680
static PinChangeView * s_duressPinView
Definition AppUi.cpp:98
static PendingPairing s_pendingPairing
Definition AppUi.cpp:110
static void updateBatteryIndicator()
Updates the battery percentage indicator on the lock screen.
Definition AppUi.cpp:266
static void updateLockScreenClock()
Updates lock-screen clock/date once per minute while lock screen is visible.
Definition AppUi.cpp:333
InputResult
Definition IView.h:11
static bool onPinVerify(const char *pin)
Verifies entered PIN via PinManager.
Definition AppUi.cpp:369
static void onUnlockRequested()
Starts unlock flow from lock screen.
Definition AppUi.cpp:352
void registerBackupSerialCommand()
Registers the AUTH-gated BACKUP serial command.
static bool s_ignoreKeyUntilRelease
Prevents stale key events directly after unlock transition.
Definition AppUi.cpp:162
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:175
static void onBlePairingRequestEvent(const core::Event &evt)
Main-task handler for a deferred numeric-comparison pairing request.
Definition AppUi.cpp:777
void ui_process(uint32_t nowMs)
Main UI tick: input processing, timeouts, status updates, and rendering.
Definition AppUi.cpp:1163
static void onMainMenuSelect(uint16_t index, void *userData)
Handles main-menu item selection.
Definition AppUi.cpp:585
static void onToolsSelect(uint16_t index, void *userData)
Handles tools-menu item selection.
Definition AppUi.cpp:613
static ListView * s_settingsMenu
Definition AppUi.cpp:90
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:211
static bool s_lastBleEnabled
Definition AppUi.cpp:149
static void onBleNumericComparison(uint16_t connHandle, uint32_t passkey)
Numeric-comparison pairing request, invoked on the nimble_host task.
Definition AppUi.cpp:742
static SliderView * s_timezoneSlider
Definition AppUi.cpp:93
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:824
static void rebuildMenuLabels()
Rebuilds labels for translatable menus after language change.
Definition AppUi.cpp:561
void updatePowerStatusIcons()
Synchronizes lock-screen status icons with current hardware state.
Definition AppUi.cpp:296
static SliderView * s_brightnessSlider
Definition AppUi.cpp:91
static uint8_t toolsBluetoothIcon()
Definition AppUi.cpp:518
static core::ModuleMenuItem s_mainMenuModuleItems[MAIN_MENU_MAX_ITEMS]
Definition AppUi.cpp:126
static ListItem s_settingsItems[SETTINGS_IDX_COUNT]
Settings menu backing storage.
Definition AppUi.cpp:135
static TimeInputView * s_timeInput
Definition AppUi.cpp:96
static void onPinSuccess()
Handles successful unlock and transitions to main menu.
Definition AppUi.cpp:412
static constexpr uint32_t BATTERY_SAMPLE_INTERVAL_MS
Definition AppUi.cpp:158
static SliderView * s_sleepSlider
Definition AppUi.cpp:92
static uint8_t getToolsIndex()
Returns main-menu index of the fixed "Tools" item.
Definition AppUi.cpp:171
static core::ModuleMenuItem s_toolsModuleItems[TOOLS_MAX_ITEMS]
Definition AppUi.cpp:131
void ui_on_modules_ready()
Refreshes module-backed menus once module startup is complete.
Definition AppUi.cpp:1105
static void onInactivityTimeout()
Callback invoked when inactivity timeout is reached.
Definition AppUi.cpp:446
static ListItem s_toolsItems[TOOLS_MAX_ITEMS]
Tools-menu backing storage for fixed and module entries.
Definition AppUi.cpp:130
static uint8_t s_toolsModuleCount
Definition AppUi.cpp:132
static DateInputView * s_dateInput
Definition AppUi.cpp:95
static void * s_transferUnlockUd
Definition AppUi.cpp:117
static uint8_t getPluginsIndex()
Returns main-menu index of the fixed "Plugins" item.
Definition AppUi.cpp:169
bool isBadgeLocked()
Returns whether the badge is currently locked (showing lock screen with no menu above).
Definition AppUi.cpp:728
static bool s_lastCharging
Definition AppUi.cpp:147
static uint16_t s_blePairingPendingHandle
Connection handle of a numeric-comparison pairing accepted on the lock screen.
Definition AppUi.cpp:119
static constexpr uint8_t MAIN_MENU_MAX_ITEMS
Menu sizing and inactivity timeout constants.
Definition AppUi.cpp:60
Menu item registered by a module.
Definition IModule.h:29
const char * labelKey
Definition AppUi.cpp:513
uint8_t(* icon)()
Definition AppUi.cpp:514