Files
MeshCore-Solo/examples/companion_radio/ui-lvgl/UITask.h
T
JakubandClaude Opus 5.5 e154a4e721 feat(l2): Gallery app (screenshots as thumbnails, full view with swipe and delete, the saved trails), the clock from NTP while WiFi is up (the router's server from DHCP first), tabs switch on release, Home apps out of Settings
- Gallery: thumbnails 30 to a page, built only while the screen is still (a decode held the loop and a scroll read as a tap)
- segmented(): CLICK_TRIG, so a swipe starting on a screen's tabs doesn't switch them
- Settings > Device > Home apps removed (hold an app on Home instead)
- platformio.ini: a note that the L2 has no microphone on GPIO15 (verified on hardware)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 15:39:56 +01:00

754 lines
30 KiB
C++

#pragma once
// ui-lvgl: the rich (colour + touch) frontend of the UI Core. Wio Tracker L2
// first. All application state lives in the Core (../ui-core); this class owns
// only LVGL screens, display power and input. See docs/solo/developer/ui-core.md.
#include <MeshCore.h>
#include <Arduino.h>
#include <helpers/ui/DisplayDriver.h>
#include <helpers/SensorManager.h>
#include <helpers/MultiSerialInterface.h>
#include <lvgl.h>
// A big zero-initialised static in PSRAM (ESP32): the internal RAM is what
// WiFi starts and joins on. Not for what DMA or an interrupt touches.
#if defined(ESP32)
#include <esp_attr.h>
#define PSRAM_BSS EXT_RAM_BSS_ATTR
#else
#define PSRAM_BSS
#endif
#ifdef PIN_BUZZER
#include <helpers/ui/buzzer.h>
#endif
#include "../AbstractUITask.h"
#include "../NodePrefs.h"
#include "../ui-core/UiCoreHost.h"
#include "../ui-core/ScreenLock.h"
// loop() waits on its own until something is due (lvport::idle, woken by a
// frame from the app): main.cpp adds no wait of its own.
#define UI_LOOP_IDLES 1
class UiCore;
class NearbyModel;
struct UiEvent;
namespace mapview { struct TileArea; }
// A popup that asks for one line of text: the field over the keyboard. `done`
// gets the keyboard's READY (OK) and CANCEL; the field is _nav_ta, the keyboard
// _nav_kb, until navClosePopup().
struct TextEntry {
const char* title;
lv_event_cb_t done;
const char* text = nullptr; // what the field starts with
const char* hint = nullptr; // placeholder
size_t max_bytes = 0; // UTF-8 bytes the field takes; 0 = no limit
bool password = false;
const char* accepted = nullptr; // only these characters (lv_textarea_set_accepted_chars)
bool symbols = false; // open on the digits / symbols page
bool bare = false; // no titled panel: the field straight on the dim, for the caller to add rows under it (returns the overlay)
};
class UITask : public UITaskBase, public UiCoreHost {
public:
UITask(mesh::MainBoard* board, MultiSerialInterface* serial) : UITaskBase(board, serial) {}
void begin(DisplayDriver* display, SensorManager* sensors, NodePrefs* node_prefs);
void loop() override;
void notify(UIEventType t = UIEventType::none) override;
void shutdown(bool restart = false) override;
MyMesh::Listener* meshListener() override;
// The screen as it is now: RGB565 (native, little-endian), row-major, w x h.
// For CMD_GET_SCREENSHOT (MyMesh, -D ENABLE_SCREENSHOT, display type 2);
// nullptr without the memory for it. The caller frees it with free().
uint16_t* captureFrame(int& w, int& h);
#define UI_SCREENSHOT_RGB565 1
// UiCoreHost
bool isViewingChannel(uint8_t channel_idx) override;
bool isViewingDM(const uint8_t* pub_key) override;
void onViewedHistoryGrew(bool channel) override { (void)channel; _thread_dirty = true; }
void botSetGPS(bool on) override;
void applySoundPrefs() override;
bool cliCommand(const char* command, char* reply, int n) override;
// ── Navigation (called from LVGL event callbacks) ────────────────────────
void showHome();
void setHomePage(int page);
void homeEdit(bool on); // Home's apps: drag to move, tap to hide (hold an app)
void homeAppDrop(int from, int to);
void homeAppToggle(int pos);
void goHome(); // the side button: Home's clock page
// The quick panel, pulled down from the status bar (QuickPanel.h)
void quickPanelBuild();
bool quickPanelBegin();
void quickPanelClose(bool animate);
bool quickPanelOpen() const;
void quickPanelRefresh();
void quickPanelTile(int t, bool hold);
void homeFieldSet(int slot, int field);
void showChats();
void showContacts();
void showSettings();
void showSchemaSettings(int page);
// Home > Admin: the repeaters / room servers to log in to (AdminScreen.h)
void showAdminPick();
// Settings > System > Firmware update (OtaScreen.h)
void showOta();
void otaAction();
void adminPick(int row);
void pruneContacts();
// Settings > Privacy: the position adverts carry with the GPS off, the
// Bluetooth PIN; Settings: factory reset, asked twice (stage 1, 2; 3 erases).
void ownPositionPopup();
void ownPositionPick(int code);
void ownPositionDone(bool ok);
void blePinPopup();
void blePinDone(bool ok);
void factoryResetStep(int stage);
void setBrightnessPct(uint8_t pct, bool save);
void applyDisplayPrefs() override;
void setSchemaValue(int idx, int v);
void setMsgBanner(bool on); // Settings > Display > Message banner
void showNearby();
void showMap(bool frame = true); // in its current mode; frame: on the target, if there is one
void openMap(bool nav); // the one map; false: from Nodes, with the nodes on it and Back to Nodes
void mapLongPress(int x, int y);
void mapZoom(int delta);
void mapCenterOnMe();
void mapPan(int dx, int dy);
void mapOpenMarker(int idx);
// Node detail > Route (RouteScreen.h) and picking its hops on the map
void showRoute(const uint8_t* pub_key);
void buildRoute();
void routeRebuild();
void routeStatus();
uint32_t routeSig();
void routePoll();
void routeDiscRebuild();
uint32_t routeDiscSig();
void routeDiscover();
void routeDiscTries(int sel);
void routeUseFound(int i);
void routeAct(uint8_t act);
void routeOp(int code);
void routeKeep();
void showRoutePick();
void buildRoutePick();
void routePick(int row);
void routeMapTap(const uint8_t* pub_key, uint8_t type);
void routeMapDone();
void routeShowDone();
bool routeShowing() const { return _route_show; }
void routeMapRefresh();
void routeMapUndo();
void routeMapFrame();
void buildRouteBar(lv_obj_t* body);
void mapFlowTick();
void flowHistoryPopup();
void flowReplay(int i); // a history index, -1 all
void mapFit(int32_t la0, int32_t la1, int32_t lo0, int32_t lo1);
void mapDownloadPopup();
// Map areas (MapAreas.h)
void areaBuildLayer(lv_obj_t* body);
void areaSelectBegin();
void areaSelectEnd();
bool areaSelecting() const;
void areaSelectDownload();
void areaSelectZmax(int d);
void areaHandleDrag(int corner, int dx, int dy);
void areaLayout();
bool areaStartDownload(const mapview::TileArea& box, bool force, bool trails);
void mapAreasPopup();
void mapAreaPopup(int idx);
void mapAreaAction(uint8_t act);
void mapRegionsPopup(); // Map tools > Vector regions (MapRegions.h)
void mapRegionPopup(int idx);
void mapRegionAction(uint8_t act);
void mapAreaRenameDone(bool ok);
void areaPreviewEnd(); // back to the view from before a map area's preview
int mapZoomLevel() const;
void setTapWake(bool on);
void mapDownloadClose();
void mapDownloadStart();
void mapDownloadStop();
void mapDownloadResume();
void mapDownloadDiscard();
void setLiveTiles(bool on);
void setTrails(bool on);
void setVectorMap(bool on);
void setMapLayer(int i);
void simMapAt(const char* spec); // Map tools > Live tiles
void simCompose(const char* text); // the open thread's field: this text, keyboard up
void simNavTarget(int32_t lat_1e6, int32_t lon_1e6); // a waypoint as the navigation target
void simDm(); // the first chat contact's thread
void simUnread(); // read and new messages to open on
void simSleep(bool on) { if (on) sleep(); else wake(); }
void simLock(bool on) { if (on) lockScreen(); else unlockScreen(); }
void simSend() { sendFromCompose(); } // the field, sent
// Navigation map (NavMap.h)
void navTargetsPopup();
void navClosePopup();
void navPick(int code);
void navWaypointMenu(int idx);
void navWaypointAction(uint8_t act);
void navRenameDone(bool ok);
void navMarkHere();
void navAveragingCancel();
void navToNode(const uint8_t* key, int32_t lat, int32_t lon, const char* name);
void navToolsPopup(bool back = false); // back: from a page it led to, as it was left
void navToolAction(uint8_t act);
void navSetShareTarget(int sel);
void navWaypointsPopup();
void navCoordsPopup();
void navCoordsDone(bool ok);
void shareToMessage(const char* text);
void messageLocationAction(int idx, bool save);
void showWifi();
void wifiSetAllowed(bool on);
void wifiScan();
void wifiPick(int idx);
void wifiOpenSaved(int idx); // a saved network into the fields, to change its password
void wifiTestShow(const char* msg, const char* ssid, const char* pass); // a password check's outcome
void wifiForget(int idx); // (its button asks first: WifiScreen.h)
void wifiSave();
void wifiEdit(lv_obj_t* ta);
void wifiKeyboardHide();
void setNearbyFilter(uint8_t f);
void setNearbySort(bool last_heard);
void startNearbyScan();
void closeScanPopup();
void openScanNode(int row);
void openNode(int row);
void nodeAction(uint8_t action);
void setKeyboardAlphabets(int main_idx, int alt_sel);
void openChannel(uint8_t channel_idx);
void openDM(const uint8_t* pub_key);
void bannerOpen(); // the message banner tapped: its conversation
void back();
void sendFromCompose();
int composeLimit() const; // bytes the open thread's message may take
int composeLen(const char* typed) const; // what `typed` takes once sent
void setKeyboardVisible(bool show);
void showToast(const char* text, uint32_t ms = 2500);
// Clock tools (ClockScreen.h)
void showClock();
void clockTab(int tab);
void clockAction(uint8_t act);
void setAlarm(int which, int v);
void dismissRing();
// Sound (SoundScreen.h): the speaker, Settings > Sound, the melody editor
void playMelody(const char* melody); // through mute (alarms, previews)
void stopMelody();
bool melodyPlaying();
int melodyNote(); // the note sounding now, -1 silent
void setSoundMode(int mode);
void setSoundVolume(int level);
void showMelodies(int slot);
void melodySlot(int slot);
void melodyPick(int idx);
void melodySet(uint8_t which, int v);
void melodyAction(uint8_t act);
void conversationMelody(int v);
void hearMelody(int slot);
#ifdef PIN_BUZZER
// For the sim page's Web Audio speaker (sim_buzzer_* in UITask.cpp).
bool isBuzzerPlaying() { return _buzzer.isPlaying(); }
uint16_t buzzerFreqHz() const;
uint8_t buzzerVolume() const { return _buzzer.getVolume(); }
#endif
// Quick messages, placeholders, advert, Bluetooth (QuickScreen.h)
void quickPopup(); // the compose bar's "+"
void quickSend(int slot);
void quickInsert(int ph);
void showQuickMsgs();
void quickEdit(int slot);
void quickEditDone(bool ok);
void quickEditInsert(int ph);
void advertPopup();
void advertSend(bool flood);
void setBluetooth(bool on);
void markAllRead();
// Settings > Radio (RadioScreen.h)
void showRadio();
void radioSet(int which, int v);
void radioFreqPopup(bool repeater); // the companion's frequency, or the repeater profile's
void radioFreqDone(bool ok);
// Settings > Radio: my presets, scopes (RadioExtras.h)
void presetMenu(int slot);
void presetAction(uint8_t act);
void radioNamePopup(uint8_t what);
void radioNameDone(bool ok);
void showScopes();
void scopeMenu(int idx);
void scopeAction(uint8_t act);
// Home > Repeater (RepeaterScreen.h)
void showRepeater();
void repeaterSet(int which, int v);
void repeaterScopesPopup();
void repeaterScopeSet(uint8_t i, bool on);
// Channels (ChannelScreen.h)
void channelMenu(int idx);
void channelSet(uint8_t which, int v);
void channelAction(uint8_t act);
void showChannelEdit(int idx);
void channelEditType(int type);
void channelEditHex(bool hex);
void channelEditField(lv_obj_t* ta);
void channelEditKbHide();
void channelEditSave();
// Remote admin (AdminScreen.h)
void openAdmin(const uint8_t* pub_key);
void adminTab(int tab);
void adminRow(int row);
void adminValue(uint8_t act);
void adminChoice(int idx);
void adminLogin(bool ok);
void adminTextDone(bool ok);
void adminCancelWait();
// Bot settings (BotScreen.h)
void showBot();
void botTab(int tab);
void botRow(int row);
void botToggle(int row, bool on);
void botPick(int idx);
void botHour(uint8_t act);
void botTextDone(bool ok);
// ConversationScreen.h
void openRoom(const uint8_t* pub_key);
void roomLoginPopup(const uint8_t* pub_key);
void roomLoginDone(bool ok);
void conversationMenu(const uint8_t* pub_key); // nullptr: the open thread's
void conversationAction(uint8_t act);
void conversationNotif(int v);
void messageMenu(int idx);
void messagePopupTick();
void messageAction(uint8_t act);
void chatFold(int which);
void toggleChatFilter(uint8_t which);
// DeviceScreen.h
void nodeNamePopup();
void nodeNameDone(bool ok);
void powerPopup(bool restart);
void unlockScreen();
void threadPage(int dir); // +1 older, -1 newer
// Saved trails on the SD card (NavMap.h)
bool saveTrailToCard(char* name_out, size_t n);
void savedTrailsPopup();
void savedTrailPopup(int idx); // -1 = the device's own slot
void savedTrailAction(uint8_t act);
void navFrameTrail();
// Settings > Storage (StorageScreen.h)
void showStorage();
void showGps(bool from_settings);
void buildGps();
void refreshGps();
void buildStorage();
void pollStorage();
void storageKeep(int idx);
void storageClearHistory();
void storageLiveCap(int idx);
void storageClearLive();
void storageFormatAsk(); // Storage > Format card: the popup (first confirmation)
void storageFormatTap(bool go); // its buttons: Format twice (the second confirmation), or Cancel
void usbPoll(); // a computer plugged in: offer the SD card as a USB drive
void usbTap(bool drive); // that popup's buttons
bool locked() const;
void pinKey(const char* key); // the lock screen's keypad
void pinSetupPopup(); // Settings > Display > Screen PIN
void pinSetupKey(const char* key);
void pinRemove();
void pinRowRefresh();
void accentRow(lv_obj_t* body); // Settings > Display > Accent colour
void setAccent(int idx);
void showDiag();
void diagTab(int tab);
void diagResetPopup();
void diagReset();
void diagNoiseRun();
void showCompass();
void showGallery();
void galleryTab(int tab);
void galleryPage(int page);
void galleryOpen(int i); // a screenshot on the whole screen
void galleryStep(int d);
void galleryViewAction(uint8_t act);
void galleryTrail(int idx); // -1 = the device's own slot
void showFlashlight(); // Flashlight.h
void flashlightClose();
bool flashlightOn() const;
void showBattCurve(); // Settings > Power > Battery curve (BatteryScreen.h)
void refreshBattCurve();
int battMv() const { return _batt_mv; } // smoothed cell voltage, 0 before the first read
NodePrefs* prefsMut() const { return _prefs; } // for the free helpers that change a setting
void favTap(int slot);
void favHold(int slot);
void favAction(uint8_t act);
void favPick(int code);
void pinPopup(bool channel, uint8_t ch_idx, const uint8_t* pub_key);
void pinTo(int slot);
void setGps(bool on);
bool ensureGps(); // true with a fix; else turns GPS on / says it's waiting
// Kept for main.cpp's sim hooks (ui-new API).
void openContactDM(const ContactInfo& ci) { openDM(ci.id.pub_key); }
void openAdminFor(const ContactInfo& ci, bool from_picker) { (void)from_picker; openAdmin(ci.id.pub_key); }
void openRouteFor(const uint8_t* pub_key) { showRoute(pub_key); }
void openMessagesList() { showChats(); } // sim harness
bool openNodesMap() { openMap(false); return true; } // sim harness
void statusBarSync(); // up or hidden, by the screen's title bar (UITask.cpp)
private:
enum Screen : uint8_t { SCR_HOME, SCR_CHATS, SCR_CONTACTS, SCR_THREAD, SCR_SETTINGS, SCR_NEARBY, SCR_NODE, SCR_MAP, SCR_WIFI, SCR_SETTINGS_NAV, SCR_CLOCK, SCR_RADIO, SCR_CHANNEL_EDIT, SCR_ADMIN, SCR_BOT, SCR_DIAG, SCR_COMPASS, SCR_SCOPES, SCR_REPEATER, SCR_MELODY, SCR_QUICK, SCR_ADMIN_PICK, SCR_OTA, SCR_STORAGE, SCR_GPS, SCR_BATT, SCR_ROUTE, SCR_ROUTE_PICK, SCR_GALLERY };
void buildStatusBar();
void refreshStatusBar();
void fitHeader();
lv_obj_t* newScreen(const char* title, bool with_back);
void showSplash(); // Splash.h
// The screen being shown: newScreen()'s, already while it slides in (popups
// and keyboards go on it, not on the one sliding out).
lv_obj_t* screen() const { return _scr ? _scr : lv_screen_active(); }
void buildHome();
void buildHomeClock(lv_obj_t* box);
void buildHomeMap(lv_obj_t* box);
void homeFieldText(uint8_t field, char* buf, int n);
bool homeMapFit();
#ifdef UI_PERF_TEST
void perfMap(bool reset = false);
void perfWalk(); // PerfTest.h: loop()'s self-driving walk through the screens
void perfNearbyAgain() { _nearby_sig = 0; refreshNearbyList(); }
#endif
void homeMapLayout();
void homeMapLoop();
void refreshHome();
void homeSwipePoll();
void buildChats();
void refreshChats();
void fillChats();
void buildContacts();
void fill(void (UITask::*fn)(int), int from, int to);
void then(void (UITask::*fn)(int), int arg);
void fillStart(int n, int first, void (UITask::*row)(int));
void fillStep();
void fillTick();
void fillFlush();
int fillPending() const;
void buildSettings();
static const int SETTINGS_GROUPS = 5;
void settingsGroup(int i);
void buildSchemaSettings();
void schemaRows(lv_obj_t* body, uint8_t page);
void schemaSection(int i);
void schemaExtras(int page);
void showMapOptions(uint8_t section);
void schemaRow(lv_obj_t* card, int idx);
void msgBannerRow(lv_obj_t* card);
void buildKeyboardPage(lv_obj_t* body);
void buildAboutPage(lv_obj_t* body);
void buildAdminPick();
void buildOta();
void refreshOta();
void otaTick();
void otaLeave();
bool otaBusy() const;
bool wifiInUse() const; // a map download or an update holds the WiFi
void buildNearby();
void refreshNearbyList();
uint32_t nearbySignature() const;
void showScanPopup();
void refreshScanPopup();
void buildMap();
void layoutMap();
void layoutMapMarks(double left, double top, int w, int h);
void mapLoop();
void rebuildMapMarkers();
void rebuildNodeMarkers();
void addMapMark(uint8_t kind, int idx, int32_t lat_e6, int32_t lon_e6, uint32_t col, const char* text);
void mapPointToLatLon(int x, int y, int32_t& lat_e6, int32_t& lon_e6) const;
double mapCenterBias() const;
int mapBottomH() const;
void mapPlaceBottom();
void buildNavLayers();
void buildNavControls(lv_obj_t* body);
void rebuildNavMarkers();
bool navTrailSync();
bool trailJournalTick(bool now = false); // NavMap.h: the live trail's copy on the card
void trailJournalRestore();
void layoutNav();
void refreshNavBar();
void navFrameTarget();
void navSetTarget(uint8_t kind, const uint8_t* key, int32_t lat, int32_t lon, const char* name);
lv_obj_t* navPopupPanel(const char* title, bool full, bool bottom = false);
lv_obj_t* navTextEntry(const TextEntry& t); // popup + one-line field + keyboard (NavMap.h)
void navRenamePopup(int idx);
void navAddWaypoint(int32_t lat, int32_t lon);
void navDropAt(int x, int y);
void navSpotPopup(int32_t lat, int32_t lon);
void placeCard(lv_obj_t* parent, int32_t lat, int32_t lon);
void refreshNavTools();
void navPollAveraging();
void navPollTrackBack();
bool navCurrentTarget(int32_t& lat, int32_t& lon, char* name, int n, bool& person, bool& set);
bool exportTrailGpx(char* name_out, size_t n);
void mapDownloadTick();
void wifiClockTick(); // the clock from NTP while WiFi is up (LvglPort.h ntpStart())
bool _ntp_on = false;
void mapLiveBegin();
void refreshDownloadPopup();
void buildWifi();
void pollWifiScan();
void buildNode();
void refreshNode();
void buildThread();
void threadOpenAt(int first);
void reopenThreadIfUnread();
bool threadInView() const; // a thread on a lit, unlocked screen
void refreshThread();
void refreshThreadAges();
uint32_t threadSignature() const;
void buildClock();
void buildRadio();
void rebuildRadio();
void rebuildSchemaSettings();
void rebuildSoon(void (UITask::*fn)()); // after the switch that asked for it has moved
void radioGroup(int i);
void buildRadioExtras(lv_obj_t* body);
void buildScopes();
void buildRepeater();
void rebuildRepeater();
void repeaterGroup(int i);
void repeaterRelays();
void buildSoundRows(lv_obj_t* body, bool top);
void buildMelodies();
void buildQuickMsgs();
bool sendThreadText(const char* text);
void refreshMelody();
void melodySave();
void radioCloseFreq();
void buildChannelEdit();
void buildAdmin();
void buildBot();
void refreshAdminStatus();
void adminLeave();
void adminPoll();
void roomPoll();
void lockScreen();
void refreshLock();
void lockPoll();
void favGrid(lv_obj_t* grid);
void favRefresh();
int unreadTotal(); // DMs, channels and rooms
void buildDiag();
void diagSection(int sec);
void refreshDiag();
void sampleHistory(); // DiagScreen.h: History's readings, from loop()
void fillHistory();
void buildCompass();
void buildGallery();
void galleryTick();
void galleryCloseView();
void galleryBack(); // the full view closes, else Home
void refreshCompass();
void favPickPopup(int slot);
void adminValuePopup();
void adminTextPopup(const char* text, bool digits);
void adminReplyPopup(const char* text);
bool radioPopupOpen() const;
void refreshClock();
void showRing(const char* text);
void hideRing();
void drainCoreEvents();
void onMessageArrived(const UiEvent& ev);
void wake();
void sleep();
void toggleMute();
void takeScreenshot();
uint32_t _next_wake_poll_ms = 0;
uint32_t _next_touch_poll_ms = 0; // tap to wake, while the screen is off
bool _wake_down = false;
uint32_t autoOffMillis() const;
uint32_t idleMillis(uint32_t lv_next);
bool cpuNeeded();
void checkLowBattery();
#ifdef PIN_BUZZER
genericBuzzer _buzzer;
char _notif_mel_buf[220]; // a user melody's RTTTL while it plays (the player copies it)
#endif
// Who the message being notified came from (SoundNotifier's overrides).
bool _notif_dm_valid = false;
uint8_t _notif_dm_prefix[4] = {0};
int _notif_ch_idx = -1;
DisplayDriver* _display = nullptr;
SensorManager* _sensors = nullptr;
NodePrefs* _prefs = nullptr;
bool _gps_from_settings = false; // GPS screen: Back returns there (else Home)
UiCore* _core = nullptr;
Screen _screen = SCR_HOME;
// What newScreen() last loaded, for the slide direction (Anim.h)
Screen _shown_screen = SCR_HOME;
char _shown_title[32] = "";
bool _nav_back = false; // set by back() around the screen it shows
bool _fade_next = false; // newScreen() cross-fades the next screen in (the side button)
lv_obj_t* _scr = nullptr; // the screen newScreen() built (active once its slide starts)
static int screenDepth(Screen s);
bool _asleep = false;
uint32_t _next_status_ms = 0;
uint32_t _next_thread_check_ms = 0;
uint32_t _next_thread_ages_ms = 0;
uint32_t _next_trackback_ms = 0;
uint32_t _next_clock_ms = 0;
uint8_t _settings_page = 0; // settings::Page shown in SCR_SETTINGS_NAV
lv_obj_t* _ota_status = nullptr;
lv_obj_t* _ota_bar = nullptr;
lv_obj_t* _ota_btn = nullptr;
lv_obj_t* _ota_btn_lbl = nullptr;
uint32_t _map_left_ms = 0; // when the map was left, for dropping live tiles' WiFi
bool _admin_from_pick = false; // Admin opened from its Home tile (back returns there)
uint16_t _batt_mv = 0; // smoothed, read every 8 s: the status bar and the low-battery shutdown
uint32_t _next_batt_ms = 0;
lv_obj_t* _prune_lbl = nullptr;
// Screen PIN (DeviceScreen.h): a salted hash in NodePrefs (ui-core/ScreenLock.h)
bool pinSet() const { return _prefs && screenlock::isSet(*_prefs); }
bool _tap_wake = true; // a touch wakes the dark screen (NVS)
char _pin_entry[9] = ""; // digits typed so far
char _pin_new[9] = ""; // setup: the first entry, waiting for its confirmation
screenlock::Attempts _pin_tries;
// Open conversation (SCR_THREAD)
bool _thread_is_channel = false;
int _thread_skip = 0; // newest messages above the page shown (Older / Newer)
bool _thread_scroll_top = false;
int _thread_new = -1; // on opening: newest-first index of the first unread (-1: none)
uint32_t _thread_new_key = 0; // and that message, marked with "New" while the thread is open
bool _thread_new_on = false;
int loadThreadPage(int& total);
uint8_t _thread_channel = 0;
uint8_t _thread_key[PUB_KEY_SIZE] = {0};
bool _thread_dirty = false;
uint32_t _thread_sig = 0;
// Widgets
lv_obj_t* _status_time = nullptr;
lv_obj_t* _status_icons = nullptr; // a row of icon labels, rebuilt with the state
lv_obj_t* _status_batt = nullptr;
lv_obj_t* _status_chg = nullptr; // the charging bolt, green, left of the battery
char _status_sig[128] = ""; // what the icon row shows, to skip unchanged rebuilds
lv_obj_t* _toast = nullptr;
lv_timer_t* _toast_timer = nullptr;
lv_obj_t* _home_clock = nullptr;
lv_obj_t* _home_date = nullptr;
lv_obj_t* _home_unread = nullptr;
lv_obj_t* _header = nullptr; // current screen's title bar (nullptr on Home)
lv_obj_t* _hdr_clock = nullptr; // its time and battery, at the right end
lv_obj_t* _hdr_title = nullptr; // its title, which the battery's level gives way to
lv_obj_t* _hdr_time = nullptr;
lv_obj_t* _hdr_batt = nullptr;
lv_obj_t* _body = nullptr;
lv_obj_t* _thread_list = nullptr;
lv_obj_t* _compose_ta = nullptr;
lv_obj_t* _keyboard = nullptr;
// Nearby (SCR_NEARBY) / node detail (SCR_NODE); list data in NearbyModel
NearbyModel* _nearby = nullptr;
NearbyModel* _scan = nullptr; // NODE_DISCOVER results: nodes in range now (popup over the list)
bool _scanning = false;
bool _node_from_scan = false;
uint32_t _scan_sig = 0;
lv_obj_t* _scan_overlay = nullptr;
lv_obj_t* _scan_list = nullptr;
lv_obj_t* _scan_status = nullptr;
uint32_t _scan_until_ms = 0;
uint32_t _nearby_sig = 0;
uint32_t _next_nearby_ms = 0;
bool _pinging = false;
uint32_t _ping_started_ms = 0;
uint32_t _ui_started_ms = 0; // begin(): the side button held soon after is the CLI rescue
lv_obj_t* _nearby_list = nullptr;
lv_obj_t* _nearby_status = nullptr;
lv_obj_t* _node_info = nullptr;
lv_obj_t* _node_ping = nullptr;
lv_obj_t* _node_delete_lbl = nullptr;
lv_obj_t* _node_path_lbl = nullptr;
bool _node_from_map = false;
lv_obj_t* _route_status = nullptr; // Route screen (RouteScreen.h)
lv_obj_t* _route_list = nullptr;
lv_obj_t* _route_trace_btn = nullptr;
lv_obj_t* _route_save_btn = nullptr;
lv_obj_t* _route_probe_row = nullptr; // "Find where it stops", after a trace with no reply
lv_obj_t* _route_note = nullptr;
lv_obj_t* _route_disc = nullptr; // Discover: the tries, its progress, the routes found
uint32_t _route_disc_shown = 0xFFFFFFFF; // what routeDiscRebuild() drew (routeDiscSig())
lv_obj_t* _route_keep_btn = nullptr; // header: Keep (accent while kept)
uint32_t _route_shown = 0xFFFFFFFF; // what routeRebuild() drew (routeSig())
uint32_t _next_route_ms = 0;
uint32_t _next_flow_ms = 0; // the map's packet flow repaint
uint8_t _flow_legend_tick = 0; // its legend, every 10th repaint
bool _route_pick = false; // the nodes map taps repeaters into the route
bool _route_show = false; // the nodes map shows a message's route (_core->hop_route)
lv_obj_t* _route_bar = nullptr;
lv_obj_t* _route_lbl = nullptr;
// Map (SCR_MAP): view centre in fractional tile coords at zoom _map_z
double _map_cx = 0, _map_cy = 0;
int _map_z = 0; // 0 = not initialised yet
bool _map_follow = true; // keep centred on own position until panned
bool _map_pending = false; // visible tiles still to decode
uint32_t _next_map_marks_ms = 0;
lv_obj_t* _map_area = nullptr;
lv_obj_t* _map_tiles[6] = {nullptr};
lv_obj_t* _map_marks = nullptr;
lv_obj_t* _map_me = nullptr;
lv_obj_t* _map_zoom_lbl = nullptr;
lv_obj_t* _map_center_btn = nullptr; // accent while the map follows you
uint32_t _next_follow_ms = 0;
lv_obj_t* _map_hint = nullptr;
lv_obj_t* _map_dl_pill = nullptr; // download progress over the map
lv_obj_t* _dl_overlay = nullptr; // download popup
lv_obj_t* _dl_info = nullptr;
lv_obj_t* _dl_bar = nullptr; // progress while downloading
lv_obj_t* _dl_err = nullptr; // failures, one line
lv_obj_t* _dl_sub = nullptr;
lv_obj_t* _dl_start_lbl = nullptr;
lv_obj_t* _dl_job_row = nullptr; // "Unfinished ... Resume" in the popup
lv_obj_t* _dl_job_lbl = nullptr;
uint8_t _dl_last_state = 0;
// Navigation map (SCR_MAP with _map_nav)
bool _map_nav = false;
bool _map_nodes = false; // opened from Nodes: their markers too, Back returns there
uint32_t _next_nav_bar_ms = 0;
lv_obj_t* _nav_bar = nullptr; // target bar along the bottom
lv_obj_t* _nav_title = nullptr;
lv_obj_t* _nav_info = nullptr;
lv_obj_t* _nav_clear = nullptr;
lv_obj_t* _nav_overlay = nullptr; // target list / waypoint menu / rename
lv_obj_t* _nav_ta = nullptr;
lv_obj_t* _nav_kb = nullptr;
lv_obj_t* _nav_del_lbl = nullptr;
int _nav_wp = -1; // waypoint the menu / rename is about
lv_obj_t* _nav_rec = nullptr; // "REC 1.2 km LIVE 58m" pill
lv_obj_t* _nav_avg_pill = nullptr; // GPS averaging progress (tap: cancel)
lv_obj_t* _nav_trail_lbl = nullptr; // tools panel
lv_obj_t* _nav_trail_btn = nullptr;
lv_obj_t* _nav_reset_lbl = nullptr;
lv_obj_t* _nav_share_lbl = nullptr;
lv_obj_t* _nav_share_btn = nullptr;
lv_obj_t* _nav_tb_btn = nullptr;
int32_t _nav_spot_lat = 0, _nav_spot_lon = 0; // the spot a long-press picked
char _share_text[96] = ""; // waiting for a conversation to be picked (shareToMessage)
// WiFi settings (SCR_WIFI)
bool _wifi_scanning = false;
lv_obj_t* _wifi_ssid = nullptr;
lv_obj_t* _wifi_pass = nullptr;
lv_obj_t* _wifi_kb = nullptr;
lv_obj_t* _wifi_list = nullptr;
lv_obj_t* _wifi_status = nullptr;
};