Files
MeshCore-Solo/examples/companion_radio/ui-lvgl/UITask.h
T
JakubandClaude Opus 5.5 8b231935ba feat(ui-lvgl): foldable Messages sections with New in the header, storage bar by category
- Messages: New message moves to the header; Channels / Direct / Rooms fold
  on a tap of their title (kept in NVS), a folded one shows its unread count
- Storage: the card's bar is split into one coloured segment per category
  (plus the uncounted rest), each category row keyed with its colour

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 09:08:25 +02:00

540 lines
20 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/development/ui-core.md.
#include <MeshCore.h>
#include <Arduino.h>
#include <helpers/ui/DisplayDriver.h>
#include <helpers/SensorManager.h>
#include <helpers/BaseSerialInterface.h>
#include <lvgl.h>
#ifdef PIN_BUZZER
#include <helpers/ui/buzzer.h>
#endif
#include "../AbstractUITask.h"
#include "../NodePrefs.h"
#include "../ui-core/UiCoreHost.h"
class UiCore;
class NearbyModel;
struct UiEvent;
class UITask : public UITaskBase, public UiCoreHost {
public:
UITask(mesh::MainBoard* board, BaseSerialInterface* 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;
// 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;
// ── Navigation (called from LVGL event callbacks) ────────────────────────
void showHome();
void setHomePage(int page);
void goHome(); // the side button: Home's clock page
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();
void setBrightnessPct(uint8_t pct, bool save);
void applyDisplayPrefs() override;
void setSchemaValue(int idx, int v);
void showNearby();
void showMap(); // in its current mode
void openMap(bool nav); // Nodes map (false) or Navigation map (true)
void mapLongPress(int x, int y);
void mapZoom(int delta);
void mapCenterOnMe();
void mapPan(int dx, int dy);
void mapOpenMarker(int idx);
void mapDownloadPopup();
void setTapWake(bool on);
void mapDownloadClose();
void mapDownloadStart();
void mapDownloadStop();
void mapDownloadResume();
void mapDownloadDiscard();
void mapDownloadZmax(int delta);
void setLiveTiles(bool on); // Map tools > Live tiles
// 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();
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 wifiSave();
void wifiEdit(lv_obj_t* ta);
void wifiKeyboardHide();
void setNearbyFilter(uint8_t f);
void toggleNearbySort();
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 back();
void sendFromCompose();
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);
// Settings > 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 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();
bool locked() const;
void pinKey(const char* key); // the lock screen's keypad
void pinSetupPopup(); // Settings > Display & power > Screen PIN
void pinSetupKey(const char* key);
void pinRemove();
void pinRowRefresh();
void accentRow(lv_obj_t* body); // Settings > Display & power > Accent colour
void setAccent(int idx);
void showDiag();
void diagTab(int tab);
void diagResetPopup();
void diagReset();
void showCompass();
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); }
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 };
void buildStatusBar();
void refreshStatusBar();
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);
void homeMapFit();
void homeMapLayout();
void homeMapLoop();
void refreshHome();
void homeSwipePoll();
void buildChats();
void buildContacts();
void buildSettings();
void buildSchemaSettings();
void schemaRows(lv_obj_t* body, uint8_t page);
void schemaRow(lv_obj_t* card, int idx);
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 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;
void buildNavLayers();
void buildNavControls(lv_obj_t* body);
void rebuildNavMarkers();
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);
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 mapLiveBegin();
void refreshDownloadPopup();
void buildWifi();
void pollWifiScan();
void buildNode();
void refreshNode();
void buildThread();
void refreshThread();
uint32_t threadSignature() const;
void buildClock();
void buildRadio();
void rebuildRadio();
void buildRadioExtras(lv_obj_t* body);
void buildScopes();
void buildRepeater();
void rebuildRepeater();
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 refreshDiag();
void buildCompass();
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();
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);
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_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)
uint32_t _prune_armed_ms = 0;
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); stored in NVS (lvport::loadPin)
char _pin[9] = ""; // "" = no PIN
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
uint8_t _pin_fails = 0;
uint32_t _pin_block_until = 0; // millis() until which entry is refused after 5 misses
// 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 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* _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 _delete_armed_ms = 0;
lv_obj_t* _nearby_list = nullptr;
lv_obj_t* _nearby_status = nullptr;
lv_obj_t* _nearby_sort_lbl = nullptr;
lv_obj_t* _nearby_chips = nullptr;
lv_obj_t* _node_info = nullptr;
lv_obj_t* _node_ping = nullptr;
lv_obj_t* _node_delete_lbl = nullptr;
bool _node_from_map = false;
// 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_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_zoom_lbl = 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;
int _dl_zmax = 0;
uint8_t _dl_last_state = 0;
// Navigation map (SCR_MAP with _map_nav)
bool _map_nav = false;
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;
uint32_t _nav_del_armed_ms = 0;
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;
uint32_t _nav_reset_armed_ms = 0;
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;
};