diff --git a/docs/development/l2-hardware-checklist.md b/docs/development/l2-hardware-checklist.md new file mode 100644 index 00000000..0e5fb68a --- /dev/null +++ b/docs/development/l2-hardware-checklist.md @@ -0,0 +1,113 @@ +# Wio Tracker L2 (ui-lvgl) — hardware checklist + +Things verified only in the browser simulator so far. Tick on the device; +note anything odd next to the item. Branch `wio-tracker-l2`. + +## Feedback round 1 (2026-09-25) — fixed, check on the device +Confirmed on the device: live share sends and receives positions. +- [ ] Map: ☰ tools moved to the left column (back, pin, ☰); nothing overlaps the crosshair +- [ ] Target ring sits exactly on the marker (all markers were drawn ~3 px low) +- [ ] A person sharing on a channel, picked as target, is followed as they move (was a fixed point where they were); bar shows the person icon +- [ ] Settings: "Trail, live share, alerts" subtitle no longer covers the title (all list rows) +- [ ] Map credit is a small "©" (tap: full line in a toast); full text also in Settings > About; long toasts wrap +- [ ] Home: Messages, Nearby, Map, Settings — "Map" is the navigation map; the nodes map opens from Nearby's header (map icon), back returns to Nearby +- [ ] Home tiles are wider; more apps will go on further swipeable pages (dots appear once there are two) +- [ ] Overzoom limited to 2 levels (x4); past that "No map detail here at this zoom" — download more +- [ ] Panning: no tile decoding while the finger moves (smooth drag, tiles fill in once you stop); tiles around the view are decoded ahead while idle; newly revealed area shows the coarser tile until the sharp one is ready +- [ ] Note how panning feels now: ______ (next step if still slow: decode on the second core / JPEG tiles) + +## Feedback round 2 (2026-09-25) — fixed, check on the device +Confirmed: settings rows OK, map pans much faster, ring centred. +- [ ] Target ring is wider (30 px) so the green / flag marker shows inside it; a target with no marker on it (map point, message position, stale live share) gets an orange centre dot +- [ ] "↓ Resume?" pill sits top right beside +, clear of the zoom pill; tapping it opens the download popup +- [ ] Pin button → Waypoints list (distance, tap → Go / Name / Share / Delete) with "Here (GPS)" and "Coordinates" +- [ ] Coordinates: digits keyboard, "50.06142, 19.93721 Name" or with a space instead of the comma; bad input keeps the field with a hint; the map centres on the new waypoint +- [ ] Holding the map no longer adds a waypoint straight away: "This spot" popup with Add waypoint / Go here + +## Feedback round 3 (2026-09-25) — fixed, check on the device +Confirmed: green dot inside the ring, waypoints much better, download works, settings OK. +- [ ] Ring exactly on the marker at every zoom (it was computed in float: pixels off at z16+). The trail line had the same error and is fixed too +- [ ] Home: swipe left / right anywhere (clock or tiles) turns the page; a swipe starting on a tile doesn't open it; tapping a dot also switches +- [ ] Settings > Display & power: Brightness is a slider (5-100 %), changes live while dragging, kept after a reboot and after the screen sleeps / wakes +- [ ] After the update: prefs load normally (new field, schema sentinel 0xC0DE002F) — brightness starts from the old level, nothing else reset + +## New: Clock, settings pages, radio (2026-09-25) +- [ ] Home has two pages now: swipe left → Clock (dots under the tiles follow; Home remembers the page) +- [ ] Clock > Alarm: hour / minute rollers (setting a time arms it), On switch, Repeat (Once / Daily / Weekdays / Weekends); rings at the local time +- [ ] Clock > Timer: H / M / S rollers, Start → big countdown, Stop; when it ends a full-screen "Timer done" card with Dismiss appears on any screen (also wakes the display; the user button dismisses too). Silent: no speaker driver yet +- [ ] Clock > Stopwatch: Start / Stop / Reset with tenths +- [ ] Settings > Display & power: Screen off after; Wake on message +- [ ] Settings > Display & power: Battery shutdown (not while on USB), GPS power saving, Time zone (clock + alarm follow) +- [ ] Settings > Messages & contacts: Resend direct messages, Contact expiry + "Remove inactive contacts now" (first tap shows the count, second removes; favourites kept), Favourites first +- [ ] Settings > Radio: Preset list (built-ins + your saved ones), Frequency (tap, type), SF, Bandwidth, Coding rate, TX power, Auto power; changes apply at once — check messages still flow after switching back to your usual preset +- [ ] L1 (ui-new): Settings > Radio TX power / Auto power / presets still apply (now through the shared RadioControl) + +## Basics after the_mesh moved to PSRAM (`MESH_IN_PSRAM`) +- [ ] Boots normally, contacts and channels are all there +- [ ] Messages arrive and send (channel + DM), delivery ticks work +- [ ] Companion app over BLE connects and syncs +- [ ] A few hours of uptime without a reboot + +## WiFi map download +- [ ] Settings > WiFi: Scan lists networks, pick one, password, Save +- [ ] Map > download button: popup shows tile count / size, Download starts +- [ ] Pill on the map: "Connecting..." then "↓ n / total" +- [ ] Popup while downloading: IP, dBm, heap (internal free / largest block) — note the numbers: ______ +- [ ] Mesh messages still arrive during a download; BLE stays connected +- [ ] Stop → popup shows "Unfinished: z…-…, N tiles" with Resume / trash +- [ ] Power off mid-download → power on → map shows "↓ Resume?" (top right; tap it) → Resume continues (count jumps past the tiles already on the card) +- [ ] Trash discards the unfinished job +- [ ] Download finishes: toast "Map: Done, N new tiles", new tiles appear on the map + +## Overzoom (map past the downloaded detail) +- [ ] Zoom in beyond the highest downloaded level: map stays visible (magnified), zoom pill reads e.g. "z17 (map z15)" +- [ ] Panning over magnified tiles is smooth enough (note if it stutters) +- [ ] From a magnified view, download a higher zoom for that area — the sharp tiles replace the magnified ones + +## GPS +- [ ] Settings > Navigation shows a "GPS" switch; toggling it turns the module on/off (and survives a reboot) +- [ ] Status bar: GPS icon grey while searching, green with a fix, gone when off +- [ ] Map crosshair with GPS off: turns GPS on ("GPS on, waiting for a fix"), centres once a fix arrives +- [ ] Bot `!gps on` / `!gps off` works on L2 + +## Two maps +- [ ] Nodes map (Nearby > map icon): contacts with a position as markers, tap → node detail, back → map +- [ ] Map: hold the map → "This spot" → Add waypoint → "WPn" at the finger +- [ ] Map: pin button → Waypoints → Here (GPS) → waypoint at the GPS position +- [ ] Bar (bottom) → "Navigate to" list: waypoints with distance, Trail start (if a trail exists), people sharing live +- [ ] Tap a waypoint → target: ring, dashed line from you, bar with distance / bearing / your course / ETA; view frames you + target +- [ ] ✕ on the bar clears the target +- [ ] Waypoint menu (pencil): Go / Name (Polish letters, 11-byte limit) / Share / Delete (second tap confirms) +- [ ] Share → Messages → pick a conversation → compose holds `[WAY]lat,lon name` → send; a Solo L1 receiving it can save it +- [ ] Settings > Trail, live share, alerts > "Show others' positions" on → someone's `[LOC]` share appears on the Map and in the list; tap → navigate to them +- [ ] Node detail (a node with a position) has the compass button → Map with that node as target +- [ ] Settings > … > "Arrival alert" on + target set → toast "Arrived: …" when you get within the radius + +## Positions in messages +- [ ] A received message with `[WAY]lat,lon name`, `[LOC]lat,lon` or plain "lat, lon" shows Go / Save under the bubble +- [ ] Save → waypoint named from the [WAY] label (else the sender), Polish letters not cut in half +- [ ] Go → Map with that spot as the target + +## Trail & live share (Map > ☰ tools button, left column) +- [ ] Record → pill "REC 0 m" at the top; walk: blue trail line follows, distance grows +- [ ] Auto-pause (Settings > Trail, live share, alerts): standing still → "PAUSED", walking resumes +- [ ] Stop / Save / Load / Reset (second tap confirms) behave; a trail saved on L2 loads (same /trail file as L1) +- [ ] Export GPX → toast "Saved trails/trail-YYYYMMDD-HHMM.gpx"; the file opens in a GPX viewer (track + waypoints) +- [ ] Live share: pick "Send to" (channel or favourite), Share live → pill "LIVE 1h00"; another node sees the [LOC] updates while you move; stops by itself after the chosen time +- [ ] Send once: with sharing on → "Position sent"; with it off → Messages with `[LOC]lat,lon` waiting in the compose field +- [ ] Track back (with a recorded trail): bar "Back: n pt" with distance / bearing, ring on the breadcrumb; walking advances it; at the start toast "Back at the trail start"; ✕ stops it +- [ ] ☰ > Download this area opens the download popup +- [ ] Waypoint averaging 5 s/10 s/30 s: pin → "Averaging GPS… n s" pill (tap cancels) → waypoint saved at the mean + +## Settings > Trail, live share, alerts (schema-driven) +- [ ] Every row changes and survives a reboot; "Imperial units" relabels point spacing and distances +- [ ] Changing "Stop sharing after" during a session restarts its clock +- [ ] Radius / Alert on (arrive, leave, both) change when the arrival toast fires + +## Same data on L1 (ui-new) after the Core changes +- [ ] L1: waypoints saved before the update are still listed (Tools › Trail › Waypoints) +- [ ] L1: add / rename / delete waypoint, navigate, ETA line still shows +- [ ] L1: Home GPS toggle still works +- [ ] L1: Tools › Trail Save / Load / Reset still work (now via TrailEngine) +- [ ] L1: Mark with averaging (Trail › Settings › Mark avg) still counts down and marks +- [ ] L1: Track back still advances along the trail and ends at the start diff --git a/docs/development/ui-core.md b/docs/development/ui-core.md index 7d4a88b5..4a1de29c 100644 --- a/docs/development/ui-core.md +++ b/docs/development/ui-core.md @@ -29,18 +29,18 @@ interleaved. Classified: **Models — already UI-free, move as-is** - `MessageHistory.h` — channel (48) / DM (32) rings, unread counters + overflow flags. ✅ moved to `ui-core/`. -- `Trail.h` / `TrailStore`, waypoints (`WaypointsView` storage half), `ScopeList.h`. +- `Trail.h` / `TrailStore`, `ScopeList.h`. Waypoints ✅ `ui-core/WaypointModel.h` (store + `/waypoints` persistence + "delete clears the Locator target"; ui-new's `UITask::waypoints()` forwards to it). **Engines — logic living inside `UITask.cpp` / `UITask.h`** - Unread tracking — DM unread table ✅ (`ui-core/DmUnreadTable.h`), room unread (still in `UITask`). - Notifications ✅ split: the Core decides *what* happened (`MessageArrived` with kind / DM sender / channel slot, `AdvertHeard`); the frontend decides *how* to show it (`showAlert`, `SoundNotifier`, vibration, LED, wake-on-message) — those are platform services. - Live share ✅ `ui-core/LiveShareEngine.h` — session timer, movement/heartbeat gate, send + scope guard, peers' `LiveTrackStore` + expiry. - Locator ✅ `ui-core/LocatorEngine.h` — active target, geofence state machine, proximity beeper. -- Trail ✅ `ui-core/TrailEngine.h` — store, sampling, auto-pause, low-battery auto-save. +- Trail ✅ `ui-core/TrailEngine.h` — store, sampling, auto-pause, low-battery auto-save, recording actions (start/stop, reset, save/load `/trail`) used by both frontends. GPS averaging for marks ✅ `ui-core/GpsAverager.h`. Track back ✅ `ui-core/TrackBack.h`. - Course over ground + current position ✅ `ui-core/CourseEngine.h`. - Clock tools — alarm / countdown / ring ✅ `ui-core/ClockEngine.h`. - Ping ✅ `ui-core/PingEngine.h` (timeout still decided by `NearbyScreen`). -- Device controls — GPS on/off, GPIO (`setGpioMode`, bot GPIO), buzzer mode/volume, brightness, radio apply (`applyTxPower`, `applyApc`, `applyRadioParams`, …). +- Device controls — GPS on/off ✅ (`UiCore::setGpsEnabled`, used by ui-new's toggle and ui-lvgl's Settings / map crosshair), GPIO (`setGpioMode`, bot GPIO), buzzer mode/volume, brightness, radio apply (`applyTxPower`, `applyApc`, `applyRadioParams`, …). - Bot hooks — `botSetGPS`, `botBuzz`, `botSetGPIO`, … **Screen-embedded logic (harder to extract)** @@ -110,6 +110,8 @@ call the same ones, so e.g. "marking a channel read" has one implementation. ### 5. Declarative settings schema +`ui-core/SettingsSchema.h` holds three pages -- navigation (trail, live share, arrival alert, units), display & power (brightness, screen off, wake on message, battery shutdown, GPS power saving, time zone) and messages & contacts (DM resends, contact expiry, favourites first). An entry is a one-byte index by default, or a `values` table over a 1/2/4-byte field (seconds, millivolts, an hour offset, `{1, 0}` for a field stored inverted); side effects run through `UiCore` (`applyGpsInterval()`, `host()->applyDisplayPrefs()`). ui-lvgl renders each page; ui-new still uses its own screens. Radio settings are not schema rows (preset list, typed frequency, radio side effects): `ui-core/RadioControl.h` applies them (`applyParams`, `applyTxPower`, `applyApc`, presets = built-ins + user slots) for both ui-new's Settings > Radio and ui-lvgl's Radio screen. + Replaces the per-item code in `SettingsScreen`: ```cpp @@ -147,7 +149,7 @@ consumes keys only. - Theme tokens (colours, spacing, typography) in one place — the first chance to realise the "Amber Trace" direction in colour. - Fonts ✅ Noto Sans generated by `ui-lvgl/fonts/generate.sh` (lv_font_conv): European Latin (Latin-1, Extended-A/B), Greek, Cyrillic + supplement, typographic punctuation, currency, LVGL symbols; 12/14/16/20 px, clock 40 px digits only. On-screen keyboard (`ui-lvgl/Keyboard.h`), phone-style: one layout per script (Latin QWERTY / Cyrillic ЙЦУКЕН / Greek) chosen by the same `keyboard_main_alphabet` / `keyboard_alt_alphabet` prefs as `ui-new` (globe key switches), hold a key for its variants; compose limit counted in UTF-8 bytes. Long-press variants and UTF-8 case mapping are shared with `ui-new` in `ui-core/KeyboardData.h`. - Simulator ✅: `SIM_UI=lvgl variants/sim/build_wasm.sh` builds the same `ui-lvgl` code for the browser (`SimLcdDisplay` in `variants/sim/SimDisplayDriver.h` blits LVGL's RGB565 flush to a 320×240 canvas; the `SIM_PLATFORM` branch of `ui-lvgl/LvglPort.h` feeds the mouse in as touch). LVGL comes from the L2 env's PlatformIO libdeps; its objects are cached in `web/build/obj_lvgl/`. `web/lvgl.html` runs it next to a `ui-new` peer on a direct link (buttons: advert, peer DM, peer → Public, the board button) and exposes `window.sim` (`tap`, `hold`, `touch`, `peerDM`, `peerChannel`) for headless checks. -- Map (in progress): offline Web-Mercator raster tiles on the microSD (`/maps/{z}/{x}/{y}.png`, the Meshtastic MUI layout; optional per-column `.pak` from PR #3381's packer), SDMMC 1-bit mount in `LvglPort.h`. `ui-lvgl/map/TileProvider.h` is the swap point: the screen and `map/TileCache.h` (12 decoded RGB565 tiles in PSRAM, LRU) only call `renderTile(z, x, y, rgb565)`, so a vector renderer can replace the PNG provider later. `MapScreen.h`: pan by drag, +/-, re-centre/follow GPS, Nearby nodes with a position as markers (tap = node detail), one tile decoded per loop pass. `maps/attribution.txt` is shown on the map. Planned split (user, 2026-09-25): a Nodes map (every contact with a position) and a separate Navigation map (waypoints + live shares only, for walking to a target). Tiles come from `tools/maps/fetch_tiles.py` (no default server: tile.openstreetmap.org blocks offline use) or on the device: the map's download button fetches the visible area (a few overview levels up to a chosen zoom) over WiFi (`map/TileDownloader.h`; skips tiles on the card, rejects non-PNG and placeholder answers; server from `maps/source.txt`, default OpenTopoMap). The HTTP GET runs on a worker task (`LvglPort.h`, keep-alive TLS); WiFi credentials (Settings > WiFi, with scan) live in NVS, and WiFi is on only during a download. Internal RAM is the constraint (Arduino-ESP32 2.0.17 / IDF 4.4: task stacks and, by default, mbedTLS buffers must be internal): with WiFi + Bluedroid up only ~13 KB was left, so the L2 LVGL env sets `MESH_IN_PSRAM` (`the_mesh`, ~120 KB, placement-new into PSRAM in `main.cpp`) and the fetch task points mbedTLS at PSRAM. The download popup shows IP, RSSI, free/largest internal heap and the last TLS/HTTP error. Arduino-ESP32 3.x (IDF 5: PSRAM stacks, NimBLE) is to be evaluated in the polishing phase. In the simulator the same code downloads through emscripten fetch. +- Map (in progress): offline Web-Mercator raster tiles on the microSD (`/maps/{z}/{x}/{y}.png`, the Meshtastic MUI layout; optional per-column `.pak` from PR #3381's packer), SDMMC 1-bit mount in `LvglPort.h`. `ui-lvgl/map/TileProvider.h` is the swap point: the screen and `map/TileCache.h` (24 decoded RGB565 tiles in PSRAM, LRU) only call `renderTile(z, x, y, rgb565)`, so a vector renderer can replace the PNG provider later. `MapScreen.h`: pan by drag, +/-, re-centre/follow GPS, Nearby nodes with a position as markers (tap = node detail), one tile decoded per loop pass, never during a drag (the view fills in once the finger rests; an idle map decodes the ring around the view ahead, and a not-yet-decoded tile shows a cached coarser one meanwhile). `maps/attribution.txt` is behind a "©" on the map and in Settings > About. Two maps over the same tile view: Nearby's header opens the Nodes map (every node Nearby knows a position for), Home > Map the Navigation map (`NavMap.h`): saved waypoints, live shares, the recorded trail and the Locator target with a dashed line to it and a bar with distance / bearing / own course / ETA (`ui-core/EtaTracker.h`, shared with ui-new's NavView). Hold the map to drop a waypoint, the pin marks the GPS position; the bar opens the target list and the waypoint menu (go, rename, share as `[WAY]` into a conversation, delete). Node detail has Navigate (target followed by key); a person sharing on a channel is followed by sender name (Locator target kind 2). Settings > Navigation carries the `track_shared_loc` / `locator_enabled` switches ui-new has on its Live share / Locator screens. Tiles come from `tools/maps/fetch_tiles.py` (no default server: tile.openstreetmap.org blocks offline use) or on the device: the map's download button fetches the visible area (a few overview levels up to a chosen zoom) over WiFi (`map/TileDownloader.h`; skips tiles on the card, rejects non-PNG and placeholder answers; server from `maps/source.txt`, default OpenTopoMap). The HTTP GET runs on a worker task (`LvglPort.h`, keep-alive TLS); WiFi credentials (Settings > WiFi, with scan) live in NVS, and WiFi is on only during a download. The job (area + zoom range) stays in `maps/.job` until it completes, so a download cut short by power-off, lost WiFi or Stop is offered for Resume (tiles already on the card are skipped). Past the downloaded detail the map magnifies the nearest coarser tile it has (up to 2 levels -- more is just big pixels; zoom pill shows e.g. `z17 (map z15)`); known-missing tiles are remembered without a pixel buffer. Internal RAM is the constraint (Arduino-ESP32 2.0.17 / IDF 4.4: task stacks and, by default, mbedTLS buffers must be internal): with WiFi + Bluedroid up only ~13 KB was left, so the L2 LVGL env sets `MESH_IN_PSRAM` (`the_mesh`, ~120 KB, placement-new into PSRAM in `main.cpp`) and the fetch task points mbedTLS at PSRAM. The download popup shows IP, RSSI, free/largest internal heap and the last TLS/HTTP error. Arduino-ESP32 3.x (IDF 5: PSRAM stacks, NimBLE) is to be evaluated in the polishing phase. In the simulator the same code downloads through emscripten fetch. - PR #3381's LVGL UI is a reference for platform glue (flush, GT911, PSRAM pool) only; its screens are not adopted. ## Migration plan diff --git a/examples/companion_radio/ui-lvgl/ClockScreen.h b/examples/companion_radio/ui-lvgl/ClockScreen.h new file mode 100644 index 00000000..74cdf94f --- /dev/null +++ b/examples/companion_radio/ui-lvgl/ClockScreen.h @@ -0,0 +1,308 @@ +#pragma once +// Clock tools (Home > Clock): alarm, countdown timer and stopwatch -- ui-new's +// Tools > Clock on a touch screen. The alarm (NodePrefs alarm_*) and the +// countdown run in the Core's ClockEngine, so they fire on any screen and while +// the display sleeps; the stopwatch is view state here, as in ui-new. Whatever +// fires brings up a full-screen card with Dismiss (silent: the L2 speaker has +// no driver yet). +// +// Single-TU fragment: included by ui-lvgl/UITask.cpp after NavMap.h. + +namespace clockview { + +enum : uint8_t { TAB_ALARM, TAB_TIMER, TAB_STOPWATCH, TAB_COUNT }; +enum : uint8_t { ACT_START_STOP, ACT_RESET }; + +static uint8_t s_tab = TAB_ALARM; +static uint8_t s_timer_h = 0, s_timer_m = 5, s_timer_s = 0; // countdown to start +static bool s_sw_running = false; +static uint32_t s_sw_start = 0, s_sw_accum = 0; +static bool s_shown_running = false; // timer state the screen was built for +static lv_obj_t* s_big = nullptr; // timer remaining / stopwatch reading +static lv_obj_t* s_go_lbl = nullptr; // Start / Stop +static lv_obj_t* s_ring = nullptr; // "Timer done" card on the top layer +static lv_obj_t* s_ring_lbl = nullptr; +static lv_obj_t* s_alarm_sw = nullptr; // follows a time change (setting it arms the alarm) + +static char s_opts24[24 * 3], s_opts60[60 * 3]; // "00\n01\n..." for the rollers +static void buildOptions() { + if (s_opts24[0]) return; + int o = 0; + for (int i = 0; i < 24; i++) o += snprintf(s_opts24 + o, sizeof(s_opts24) - o, i ? "\n%02d" : "%02d", i); + o = 0; + for (int i = 0; i < 60; i++) o += snprintf(s_opts60 + o, sizeof(s_opts60) - o, i ? "\n%02d" : "%02d", i); +} + +static uint32_t swElapsed() { return s_sw_accum + (s_sw_running ? millis() - s_sw_start : 0); } + +// Countdown: rounds up, so "00:01" shows until it actually fires. +static void fmtRemaining(char* b, int n, uint32_t ms) { + uint32_t t = (ms + 999) / 1000; + if (t >= 3600) snprintf(b, n, "%lu:%02lu:%02lu", (unsigned long)(t / 3600), (unsigned long)(t / 60 % 60), (unsigned long)(t % 60)); + else snprintf(b, n, "%02lu:%02lu", (unsigned long)(t / 60), (unsigned long)(t % 60)); +} +static void fmtStopwatch(char* b, int n, uint32_t ms) { + uint32_t t = ms / 1000; + if (t >= 3600) snprintf(b, n, "%lu:%02lu:%02lu", (unsigned long)(t / 3600), (unsigned long)(t / 60 % 60), (unsigned long)(t % 60)); + else snprintf(b, n, "%02lu:%02lu.%lu", (unsigned long)(t / 60), (unsigned long)(t % 60), (unsigned long)(ms / 100 % 10)); +} + +static lv_obj_t* roller(lv_obj_t* parent, const char* opts, int sel, lv_event_cb_t cb, uintptr_t which) { + lv_obj_t* r = lv_roller_create(parent); + lv_roller_set_options(r, opts, LV_ROLLER_MODE_INFINITE); + lv_roller_set_selected(r, sel, LV_ANIM_OFF); + lv_obj_set_size(r, 60, 84); // three rows; leaves room for the button below + lv_obj_set_style_text_font(r, THEME_FONT_TITLE, 0); + lv_obj_set_style_text_font(r, THEME_FONT_LARGE, LV_PART_SELECTED); + lv_obj_set_style_text_line_space(r, 4, 0); + lv_obj_set_style_text_color(r, lv_color_hex(theme::TEXT_MUTED), 0); + lv_obj_set_style_bg_color(r, lv_color_hex(theme::SURFACE), 0); + lv_obj_set_style_border_width(r, 0, 0); + lv_obj_set_style_bg_color(r, lv_color_hex(theme::SURFACE_2), LV_PART_SELECTED); + lv_obj_set_style_text_color(r, lv_color_hex(theme::ACCENT), LV_PART_SELECTED); + lv_obj_add_event_cb(r, cb, LV_EVENT_VALUE_CHANGED, (void*)which); + return r; +} + +static lv_obj_t* colon(lv_obj_t* parent) { return label(parent, ":", THEME_FONT_LARGE, theme::TEXT_MUTED); } + +static lv_obj_t* row(lv_obj_t* parent) { + lv_obj_t* r = lv_obj_create(parent); + styleSurface(r, theme::BG); + lv_obj_remove_flag(r, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_size(r, LV_PCT(100), LV_SIZE_CONTENT); + lv_obj_set_flex_flow(r, LV_FLEX_FLOW_ROW); + lv_obj_set_flex_align(r, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); + lv_obj_set_style_pad_column(r, theme::GAP, 0); + return r; +} + +} // namespace clockview + +static void onOpenClock(lv_event_t* e) { (void)e; s_ui->showClock(); } +static void onClockTab(lv_event_t* e) { s_ui->clockTab((int)(uintptr_t)lv_event_get_user_data(e)); } +static void onClockAct(lv_event_t* e) { s_ui->clockAction((uint8_t)(uintptr_t)lv_event_get_user_data(e)); } +static void onRingDismiss(lv_event_t* e) { (void)e; s_ui->dismissRing(); } + +// Alarm: hour / minute rollers, on switch, repeat. Saved on every change. +static void onAlarmRoller(lv_event_t* e) { + s_ui->setAlarm((int)(uintptr_t)lv_event_get_user_data(e), (int)lv_roller_get_selected((lv_obj_t*)lv_event_get_target(e))); +} +static void onAlarmSwitch(lv_event_t* e) { + s_ui->setAlarm(2, lv_obj_has_state((lv_obj_t*)lv_event_get_target(e), LV_STATE_CHECKED) ? 1 : 0); +} +static void onAlarmRepeat(lv_event_t* e) { + s_ui->setAlarm(3, (int)lv_dropdown_get_selected((lv_obj_t*)lv_event_get_target(e))); +} +static void onTimerRoller(lv_event_t* e) { + int v = (int)lv_roller_get_selected((lv_obj_t*)lv_event_get_target(e)); + switch ((uintptr_t)lv_event_get_user_data(e)) { + case 0: clockview::s_timer_h = (uint8_t)v; break; + case 1: clockview::s_timer_m = (uint8_t)v; break; + default: clockview::s_timer_s = (uint8_t)v; break; + } +} + +void UITask::showClock() { + _screen = SCR_CLOCK; + buildClock(); +} + +void UITask::clockTab(int tab) { + if (tab < 0 || tab >= clockview::TAB_COUNT) return; + clockview::s_tab = (uint8_t)tab; + buildClock(); +} + +void UITask::buildClock() { + using namespace clockview; + buildOptions(); + lv_obj_t* body = newScreen("Clock", true); + lv_obj_set_style_pad_row(body, 6, 0); + lv_obj_remove_flag(body, LV_OBJ_FLAG_SCROLLABLE); + s_big = s_go_lbl = s_alarm_sw = nullptr; + + // Tabs + lv_obj_t* tabs = row(body); + static const char* const NAMES[TAB_COUNT] = { LV_SYMBOL_BELL " Alarm", UI_SYMBOL_CLOCK " Timer", + UI_SYMBOL_STOPWATCH " Stopwatch" }; + for (int t = 0; t < TAB_COUNT; t++) { + lv_obj_t* b = lv_button_create(tabs); + lv_obj_set_height(b, 32); + lv_obj_set_flex_grow(b, 1); + lv_obj_set_style_shadow_width(b, 0, 0); + lv_obj_set_style_radius(b, theme::RADIUS, 0); + lv_obj_set_style_bg_color(b, lv_color_hex(t == s_tab ? theme::ACCENT_DIM : theme::SURFACE), 0); + lv_obj_add_event_cb(b, onClockTab, LV_EVENT_CLICKED, (void*)(uintptr_t)t); + lv_obj_center(label(b, NAMES[t], THEME_FONT_SMALL, theme::TEXT)); + } + + if (s_tab == TAB_ALARM) { + lv_obj_t* r = row(body); + roller(r, s_opts24, _prefs->alarm_hour % 24, onAlarmRoller, 0); + colon(r); + roller(r, s_opts60, _prefs->alarm_min % 60, onAlarmRoller, 1); + lv_obj_t* col = lv_obj_create(r); + styleSurface(col, theme::BG); + lv_obj_remove_flag(col, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_size(col, 120, LV_SIZE_CONTENT); + lv_obj_set_flex_flow(col, LV_FLEX_FLOW_COLUMN); + lv_obj_set_flex_align(col, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_START); + lv_obj_set_style_pad_row(col, 6, 0); + lv_obj_set_style_pad_left(col, 8, 0); + label(col, "Alarm on", THEME_FONT_SMALL, theme::TEXT_MUTED); + lv_obj_t* sw = s_alarm_sw = lv_switch_create(col); + lv_obj_set_size(sw, 50, 26); + if (_prefs->alarm_on) lv_obj_add_state(sw, LV_STATE_CHECKED); + lv_obj_add_event_cb(sw, onAlarmSwitch, LV_EVENT_VALUE_CHANGED, NULL); + label(col, "Repeat", THEME_FONT_SMALL, theme::TEXT_MUTED); + lv_obj_t* dd = lv_dropdown_create(col); + char opts[48]; + int o = 0; + for (uint8_t i = 0; i < NodePrefs::ALARM_REPEAT_COUNT; i++) + o += snprintf(opts + o, sizeof(opts) - o, "%s%s", i ? "\n" : "", i ? NodePrefs::alarmRepeatLabel(i) : "Once"); + lv_dropdown_set_options(dd, opts); + lv_dropdown_set_selected(dd, NodePrefs::alarmRepeatIdxForMask(_prefs->alarm_repeat_mask)); + lv_obj_set_width(dd, 112); + lv_obj_add_event_cb(dd, onAlarmRepeat, LV_EVENT_VALUE_CHANGED, NULL); + bool synced = rtc_clock.getCurrentTime() > 1000000000UL; + label(body, synced ? "Rings at this local time (Settings time zone)." : "The clock isn't set yet: the alarm waits for a time sync.", + THEME_FONT_SMALL, theme::TEXT_MUTED); + return; + } + + if (s_tab == TAB_TIMER) { + s_shown_running = _core->clock.isTimerRunning(); + if (s_shown_running) { + s_big = label(body, "", THEME_FONT_CLOCK, theme::TEXT); + lv_obj_set_width(s_big, LV_PCT(100)); + lv_obj_set_style_text_align(s_big, LV_TEXT_ALIGN_CENTER, 0); + lv_obj_set_style_pad_ver(s_big, 18, 0); + } else { + lv_obj_t* r = row(body); + roller(r, s_opts24, s_timer_h, onTimerRoller, 0); + colon(r); + roller(r, s_opts60, s_timer_m, onTimerRoller, 1); + colon(r); + roller(r, s_opts60, s_timer_s, onTimerRoller, 2); + } + } else { // stopwatch + s_big = label(body, "", THEME_FONT_CLOCK, theme::TEXT); + lv_obj_set_width(s_big, LV_PCT(100)); + lv_obj_set_style_text_align(s_big, LV_TEXT_ALIGN_CENTER, 0); + lv_obj_set_style_pad_ver(s_big, 18, 0); + } + + lv_obj_t* acts = row(body); + lv_obj_t* go = lv_button_create(acts); + lv_obj_set_size(go, 150, 40); + lv_obj_set_style_shadow_width(go, 0, 0); + lv_obj_set_style_radius(go, theme::RADIUS, 0); + lv_obj_set_style_bg_color(go, lv_color_hex(theme::ACCENT_DIM), 0); + lv_obj_add_event_cb(go, onClockAct, LV_EVENT_CLICKED, (void*)(uintptr_t)ACT_START_STOP); + s_go_lbl = label(go, "", THEME_FONT_BODY, theme::TEXT); + lv_obj_center(s_go_lbl); + if (s_tab == TAB_STOPWATCH) { + lv_obj_t* rs = lv_button_create(acts); + lv_obj_set_size(rs, 100, 40); + lv_obj_set_style_shadow_width(rs, 0, 0); + lv_obj_set_style_radius(rs, theme::RADIUS, 0); + lv_obj_set_style_bg_color(rs, lv_color_hex(theme::SURFACE), 0); + lv_obj_add_event_cb(rs, onClockAct, LV_EVENT_CLICKED, (void*)(uintptr_t)ACT_RESET); + lv_obj_center(label(rs, LV_SYMBOL_REFRESH " Reset", THEME_FONT_BODY, theme::TEXT)); + } + refreshClock(); +} + +// From loop() while SCR_CLOCK is shown: the running readouts. +void UITask::refreshClock() { + using namespace clockview; + if (s_tab == TAB_TIMER) { + bool running = _core->clock.isTimerRunning(); + if (running != s_shown_running) { buildClock(); return; } // started elsewhere / just fired + if (s_big) { + char b[16]; + fmtRemaining(b, sizeof(b), _core->clock.timerRemainingMs()); + lv_label_set_text(s_big, b); + } + if (s_go_lbl) lv_label_set_text(s_go_lbl, running ? LV_SYMBOL_STOP " Stop" : LV_SYMBOL_PLAY " Start"); + } else if (s_tab == TAB_STOPWATCH) { + if (s_big) { + char b[16]; + fmtStopwatch(b, sizeof(b), swElapsed()); + lv_label_set_text(s_big, b); + } + if (s_go_lbl) lv_label_set_text(s_go_lbl, s_sw_running ? LV_SYMBOL_PAUSE " Stop" : LV_SYMBOL_PLAY " Start"); + } +} + +void UITask::clockAction(uint8_t act) { + using namespace clockview; + if (s_tab == TAB_TIMER) { + if (_core->clock.isTimerRunning()) _core->clock.stopTimer(); + else { + uint32_t ms = ((uint32_t)s_timer_h * 3600 + s_timer_m * 60 + s_timer_s) * 1000UL; + if (ms == 0) { showToast("Set a time first"); return; } + _core->clock.startTimer(ms); + } + buildClock(); + } else if (s_tab == TAB_STOPWATCH) { + if (act == ACT_RESET) { s_sw_accum = 0; s_sw_start = millis(); } + else if (s_sw_running) { s_sw_accum += millis() - s_sw_start; s_sw_running = false; } + else { s_sw_start = millis(); s_sw_running = true; } + refreshClock(); + } +} + +// which: 0 hour, 1 minute, 2 on/off, 3 repeat preset. +void UITask::setAlarm(int which, int v) { + if (!_prefs) return; + switch (which) { + case 0: _prefs->alarm_hour = (uint8_t)v; _prefs->alarm_on = 1; break; // setting a time arms it + case 1: _prefs->alarm_min = (uint8_t)v; _prefs->alarm_on = 1; break; + case 2: _prefs->alarm_on = (uint8_t)v; break; + case 3: _prefs->alarm_repeat_mask = NodePrefs::alarmRepeatMaskForIdx((uint8_t)v); break; + } + _core->clock.onAlarmChanged(); + the_mesh.savePrefs(); + if (which < 2 && clockview::s_alarm_sw) lv_obj_add_state(clockview::s_alarm_sw, LV_STATE_CHECKED); +} + +// ── Ring card (any screen) ──────────────────────────────────────────────────── + +void UITask::showRing(const char* text) { + using namespace clockview; + wake(); + if (!s_ring) { + s_ring = lv_obj_create(lv_layer_top()); + lv_obj_remove_style_all(s_ring); + lv_obj_set_size(s_ring, LV_PCT(100), LV_PCT(100)); + lv_obj_set_style_bg_color(s_ring, lv_color_hex(theme::BG), 0); + lv_obj_set_style_bg_opa(s_ring, LV_OPA_COVER, 0); + lv_obj_add_flag(s_ring, LV_OBJ_FLAG_CLICKABLE); + lv_obj_set_flex_flow(s_ring, LV_FLEX_FLOW_COLUMN); + lv_obj_set_flex_align(s_ring, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); + lv_obj_set_style_pad_row(s_ring, 14, 0); + label(s_ring, LV_SYMBOL_BELL, THEME_FONT_LARGE, theme::ACCENT); + s_ring_lbl = label(s_ring, "", THEME_FONT_LARGE, theme::TEXT); + lv_obj_t* b = lv_button_create(s_ring); + lv_obj_set_size(b, 200, 56); + lv_obj_set_style_shadow_width(b, 0, 0); + lv_obj_set_style_radius(b, theme::RADIUS, 0); + lv_obj_set_style_bg_color(b, lv_color_hex(theme::ACCENT_DIM), 0); + lv_obj_add_event_cb(b, onRingDismiss, LV_EVENT_CLICKED, NULL); + lv_obj_center(label(b, "Dismiss", THEME_FONT_LARGE, theme::TEXT)); + } + lv_label_set_text(s_ring_lbl, text); + lv_obj_remove_flag(s_ring, LV_OBJ_FLAG_HIDDEN); +} + +void UITask::dismissRing() { + _core->clock.dismissRing(); + hideRing(); +} + +void UITask::hideRing() { + if (clockview::s_ring) lv_obj_add_flag(clockview::s_ring, LV_OBJ_FLAG_HIDDEN); + if (_screen == SCR_CLOCK) buildClock(); // e.g. the timer's Start again +} diff --git a/examples/companion_radio/ui-lvgl/LvglPort.h b/examples/companion_radio/ui-lvgl/LvglPort.h index 3c8db97b..86e57d8e 100644 --- a/examples/companion_radio/ui-lvgl/LvglPort.h +++ b/examples/companion_radio/ui-lvgl/LvglPort.h @@ -87,6 +87,12 @@ static bool touched() { static void swallowTouch() { s_swallow = true; } +// Backlight 1-100 % (the brightness slider), on the LP5814 PWM. +static void setBacklightPct(uint8_t pct) { + if (pct > 100) pct = 100; + s_gfx->setBrightness((uint8_t)(8 + (uint16_t)pct * 247 / 100)); // never fully dark +} + // microSD over SDMMC, 1-bit (CLK 2, CMD 3, D0 1); its power rail (expander // P14) is switched on in WioTrackerL2Board::begin(). Retried on every call // until it works, so a card inserted later is picked up the next time. @@ -328,6 +334,7 @@ static bool begin() { static bool touched() { return SimLcdDisplay::touchState().down; } static void swallowTouch() { s_swallow = true; } +static void setBacklightPct(uint8_t pct) { (void)pct; } // the browser canvas has no backlight // The host page preloads map tiles into the in-memory FS under /sdcard/maps. static bool mountStorage() { return true; } diff --git a/examples/companion_radio/ui-lvgl/MapScreen.h b/examples/companion_radio/ui-lvgl/MapScreen.h index 5e917b46..30a8be7c 100644 --- a/examples/companion_radio/ui-lvgl/MapScreen.h +++ b/examples/companion_radio/ui-lvgl/MapScreen.h @@ -1,12 +1,16 @@ #pragma once // Map screen: offline Web-Mercator tiles from the card (map/TileProvider.h, -// cached by map/TileCache.h) under own position and the nodes Nearby knows a -// position for. Drag to pan, +/- to zoom, the crosshair re-centres and -// follows the GPS again; tap a marker for its node detail. The download -// button fetches the visible area over WiFi (map/TileDownloader.h). +// cached by map/TileCache.h) under own position. Two modes over the same tile +// view: the Navigation map (Home > Map, NavMap.h) shows waypoints, live +// shares, the trail and the active target; the Nodes map (Nearby > map +// button) marks every node Nearby knows a position for, tap one for its +// detail. Drag to pan, +/- to zoom, the crosshair re-centres and follows the +// GPS again. The download button fetches the visible area over WiFi +// (map/TileDownloader.h). // -// Tiles decode one per loop pass (a PNG takes tens of ms), so panning stays -// responsive and the radio keeps being serviced while a view fills in. +// Tiles decode one per loop pass (a PNG takes tens of ms), never while a drag +// is moving the map, so panning stays smooth and the radio keeps being +// serviced; an idle map decodes the tiles around the view ahead of time. // // Single-TU fragment: included by ui-lvgl/UITask.cpp after the Nearby section. @@ -36,15 +40,89 @@ static double latToTileY(double lat, int z) { return (1.0 - asinh(tan(r)) / M_PI) / 2.0 * (double)(1 << z); } -// Markers: one per positioned Nearby row, remembered so layout can reposition them. -struct Mark { int32_t lat_e6, lon_e6; int row; lv_obj_t* obj; }; -static Mark s_marks[NearbyModel::MAX_NEARBY]; +// Markers, remembered so layout can reposition them. `idx` is the Nearby row +// (MK_NODE), the waypoint index (MK_WAYPOINT) or the live-share slot (MK_LIVE). +enum : uint8_t { MK_NODE, MK_WAYPOINT, MK_LIVE }; +struct Mark { int32_t lat_e6, lon_e6; int idx; uint8_t kind; lv_obj_t* obj; lv_obj_t* dot; }; +static const int MAX_MARKS = 48; // >= MAX_NEARBY, and >= waypoints + live shares +static Mark s_marks[MAX_MARKS]; static int s_mark_count = 0; static int s_drag = 0; // px moved in the current press: a drag isn't a tap static bool s_available = false; // provider has data; checked when the map opens, not per frame +static double s_left = 0, s_top = 0; // world px (at the current zoom) of the view's top-left +static lv_obj_t* s_cells[GRID_COLS * GRID_ROWS]; // clip each grid tile (a magnified parent overflows) + +// Overzoom: a tile the card doesn't have at this zoom is drawn as the matching +// part of the nearest coarser tile it does have, magnified -- so the map stays +// usable a little past the downloaded detail. Only two levels (x4): beyond +// that a raster tile is just big pixels, and the map says to download more. +// Returns that tile (k = levels up) or nullptr; want_z/x/y is the nearest +// coarser tile not looked up yet (decode it next), want_z = -1 if none. +static const int OVERZOOM = 2; +static TileCache::Slot* ancestorFor(int z, int x, int y, int& k, int& want_z, int& want_x, int& want_y) { + want_z = -1; + for (int d = 1; d <= OVERZOOM && z - d >= 0; d++) { + int ax = x >> d, ay = y >> d; + TileCache::Slot* s = s_cache.find(z - d, ax, ay); + if (!s) { want_z = z - d; want_x = ax; want_y = ay; return nullptr; } + if (s->present) { k = d; return s; } + } + return nullptr; +} + +// Stand-in while z/x/y hasn't been decoded yet: a coarser tile already in the +// cache (typically the view you just zoomed in from), so a newly revealed part +// of the map shows something at once instead of a blank. Never decodes. +static TileCache::Slot* cachedAncestor(int z, int x, int y, int& k) { + for (int d = 1; d <= 3 && z - d >= 0; d++) { + TileCache::Slot* s = s_cache.find(z - d, x >> d, y >> d); + if (s && s->present) { k = d; return s; } + } + return nullptr; +} + +// Decoding a tile holds the loop for tens of ms, which makes a drag stutter. +// So nothing is decoded while the finger is moving the map; the view fills in +// once it rests, and when idle the ring of tiles around the view is decoded +// ahead of the next pan. +static uint32_t s_last_pan_ms = 0; +static const uint32_t PAN_SETTLE_MS = 150, PREFETCH_IDLE_MS = 400; + +// Idle read-ahead: decode the nearest not-yet-looked-up tile in the ring just +// outside the view (one per call). False when the ring is complete. +static bool prefetchOne(double left, double top, int w, int h, int z) { + int tx0 = (int)floor(left / TILE_PX), ty0 = (int)floor(top / TILE_PX); + int n = 1 << z; + int bx = 0, by = 0; + double best = 1e18; + for (int j = -1; j <= GRID_ROWS; j++) { + for (int i = -1; i <= GRID_COLS; i++) { + int tx = tx0 + i, ty = ty0 + j; + double px = tx * (double)TILE_PX - left, py = ty * (double)TILE_PX - top; + if (px < w && py < h && px + TILE_PX > 0 && py + TILE_PX > 0) continue; // on screen: not ours + if (ty < 0 || ty >= n) continue; + int wx = ((tx % n) + n) % n; + if (s_cache.find(z, wx, ty)) continue; // also keeps a decoded neighbour from being evicted + double d = fabs(px + TILE_PX / 2 - w / 2.0) + fabs(py + TILE_PX / 2 - h / 2.0); + if (d < best) { best = d; bx = wx; by = ty; } + } + } + if (best >= 1e18) return false; + s_cache.load(*s_provider, z, bx, by); + return true; +} + +static const int MARK_D = 12; // marker dot diameter } // namespace mapview +namespace navmap { // shared with NavMap.h (included after this file) +static const int BAR_H = 44; // target bar along the bottom of the Navigation map +// Navigation targets as one int (event user data): type << 8 | index. +enum : uint8_t { T_CLEAR, T_WAYPOINT, T_TRAILSTART, T_LIVE }; +static int code(uint8_t type, int idx) { return (type << 8) | (idx & 0xFF); } +} // namespace navmap + static void onMapPress(lv_event_t* e) { lv_event_code_t code = lv_event_get_code(e); if (code == LV_EVENT_PRESSED) { mapview::s_drag = 0; return; } @@ -52,8 +130,17 @@ static void onMapPress(lv_event_t* e) { lv_indev_get_vect(lv_indev_active(), &v); if (v.x == 0 && v.y == 0) return; mapview::s_drag += abs(v.x) + abs(v.y); + mapview::s_last_pan_ms = millis(); s_ui->mapPan(v.x, v.y); } +static void onMapLongPress(lv_event_t* e) { + if (mapview::s_drag > 8) return; // held after a pan: not a long-press on a spot + lv_point_t p; + lv_indev_get_point(lv_indev_active(), &p); + lv_area_t a; + lv_obj_get_coords((lv_obj_t*)lv_event_get_target(e), &a); + s_ui->mapLongPress(p.x - a.x1, p.y - a.y1); +} static void onMapZoomIn(lv_event_t* e) { (void)e; s_ui->mapZoom(+1); } static void onMapZoomOut(lv_event_t* e) { (void)e; s_ui->mapZoom(-1); } static void onMapCenter(lv_event_t* e) { (void)e; s_ui->mapCenterOnMe(); } @@ -63,6 +150,10 @@ static void onDlStart(lv_event_t* e) { (void)e; s_ui->mapDownloadStart(); } static void onDlZoomMinus(lv_event_t* e) { (void)e; s_ui->mapDownloadZmax(-1); } static void onDlZoomPlus(lv_event_t* e) { (void)e; s_ui->mapDownloadZmax(+1); } static void onDlWifi(lv_event_t* e) { (void)e; s_ui->showWifi(true); } +static void onDlResume(lv_event_t* e) { (void)e; s_ui->mapDownloadResume(); } +static void onDlDiscard(lv_event_t* e) { (void)e; s_ui->mapDownloadDiscard(); } +static void onNavTools(lv_event_t* e); // NavMap.h +static void onMapCredits(lv_event_t* e) { (void)e; s_ui->showToast(mapview::s_provider->attribution(), 4000); } static void onMapMarker(lv_event_t* e) { if (mapview::s_drag > 8) return; // the press was a pan that ended on a marker s_ui->mapOpenMarker((int)(uintptr_t)lv_event_get_user_data(e)); @@ -93,6 +184,17 @@ static lv_obj_t* mapPill(lv_obj_t* parent, const char* text) { return l; } +// Centring on a point puts it mid-way down the visible map: above the nav bar +// on the Navigation map (the bias is in tiles, added to the view centre). +double UITask::mapCenterBias() const { + return _map_nav ? navmap::BAR_H / 2.0 / mapview::TILE_PX : 0.0; +} + +void UITask::openMap(bool nav) { + _map_nav = nav; + showMap(); +} + void UITask::showMap() { _screen = SCR_MAP; mapview::s_available = lvport::mountStorage() && mapview::s_provider->available(); @@ -109,6 +211,7 @@ void UITask::showMap() { _map_cy = mapview::latToTileY(la, z); } buildMap(); + if (_map_nav && _prefs && _prefs->locator_has_target) navFrameTarget(); // show where we're going } void UITask::buildMap() { @@ -120,11 +223,19 @@ void UITask::buildMap() { lv_obj_add_flag(body, LV_OBJ_FLAG_CLICKABLE); lv_obj_add_event_cb(body, onMapPress, LV_EVENT_PRESSED, NULL); lv_obj_add_event_cb(body, onMapPress, LV_EVENT_PRESSING, NULL); + lv_obj_add_event_cb(body, onMapLongPress, LV_EVENT_LONG_PRESSED, NULL); _map_area = body; for (int i = 0; i < mapview::GRID_COLS * mapview::GRID_ROWS; i++) { - _map_tiles[i] = lv_image_create(body); - lv_obj_add_flag(_map_tiles[i], LV_OBJ_FLAG_HIDDEN); + lv_obj_t* cell = lv_obj_create(body); + lv_obj_remove_style_all(cell); + lv_obj_set_size(cell, mapview::TILE_PX, mapview::TILE_PX); + lv_obj_remove_flag(cell, LV_OBJ_FLAG_CLICKABLE); + lv_obj_remove_flag(cell, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_add_flag(cell, LV_OBJ_FLAG_HIDDEN); + mapview::s_cells[i] = cell; + _map_tiles[i] = lv_image_create(cell); + lv_image_set_pivot(_map_tiles[i], 0, 0); // magnify from the top-left (overzoom) } // Marker layer: same size as the map, lets presses through to it. @@ -135,6 +246,8 @@ void UITask::buildMap() { lv_obj_remove_flag(_map_marks, LV_OBJ_FLAG_SCROLLABLE); lv_obj_add_flag(_map_marks, LV_OBJ_FLAG_EVENT_BUBBLE); + if (_map_nav) buildNavLayers(); // trail + line to the target, under the markers + _map_me = lv_obj_create(_map_marks); lv_obj_remove_style_all(_map_me); lv_obj_set_size(_map_me, 16, 16); @@ -151,25 +264,38 @@ void UITask::buildMap() { lv_obj_align(back, LV_ALIGN_TOP_LEFT, 6, 6); lv_obj_align(mapButton(body, LV_SYMBOL_PLUS, onMapZoomIn), LV_ALIGN_TOP_RIGHT, -6, 6); lv_obj_align(mapButton(body, LV_SYMBOL_MINUS, onMapZoomOut), LV_ALIGN_TOP_RIGHT, -6, 52); - lv_obj_align(mapButton(body, LV_SYMBOL_DOWNLOAD, onMapDownload), LV_ALIGN_TOP_RIGHT, -6, 98); - lv_obj_align(mapButton(body, LV_SYMBOL_GPS, onMapCenter), LV_ALIGN_BOTTOM_RIGHT, -6, -6); - _map_dl_pill = mapPill(body, ""); + // Nav map: trail / live share / download live in its tools panel, in the + // left column (back, pin, tools) -- the right one has no room above the bar. + if (_map_nav) lv_obj_align(mapButton(body, LV_SYMBOL_BARS, onNavTools), LV_ALIGN_TOP_LEFT, 6, 98); + else lv_obj_align(mapButton(body, LV_SYMBOL_DOWNLOAD, onMapDownload), LV_ALIGN_TOP_RIGHT, -6, 98); + int bottom = _map_nav ? navmap::BAR_H : 0; // the nav bar takes the bottom edge + lv_obj_align(mapButton(body, LV_SYMBOL_GPS, onMapCenter), LV_ALIGN_BOTTOM_RIGHT, -6, -6 - bottom); + _map_dl_pill = mapPill(body, ""); // tap: the download popup lv_obj_set_style_text_color(_map_dl_pill, lv_color_hex(theme::ACCENT), 0); - lv_obj_align(_map_dl_pill, LV_ALIGN_TOP_MID, 0, 16); + lv_obj_set_style_pad_ver(_map_dl_pill, 3, 0); + lv_obj_align(_map_dl_pill, LV_ALIGN_TOP_RIGHT, -52, 14); // beside +, clear of the zoom pill on the left + lv_obj_add_flag(_map_dl_pill, LV_OBJ_FLAG_CLICKABLE); + lv_obj_set_ext_click_area(_map_dl_pill, 6); + lv_obj_add_event_cb(_map_dl_pill, onMapDownload, LV_EVENT_CLICKED, NULL); lv_obj_add_flag(_map_dl_pill, LV_OBJ_FLAG_HIDDEN); _map_zoom_lbl = mapPill(body, ""); lv_obj_align(_map_zoom_lbl, LV_ALIGN_TOP_LEFT, 52, 16); - lv_obj_t* attr = mapPill(body, mapview::s_provider->attribution()); // required by the data licences - lv_label_set_long_mode(attr, LV_LABEL_LONG_WRAP); - lv_obj_set_style_max_width(attr, 250, 0); - lv_obj_set_width(attr, LV_SIZE_CONTENT); - lv_obj_align(attr, LV_ALIGN_BOTTOM_LEFT, 6, -6); + // Map data credit (the licences require it on the map): a small "©" that + // shows the full line when tapped; it is also in Settings > About. + lv_obj_t* attr = mapPill(body, "\xC2\xA9"); + lv_obj_set_style_pad_hor(attr, 7, 0); + lv_obj_set_style_pad_ver(attr, 3, 0); + lv_obj_add_flag(attr, LV_OBJ_FLAG_CLICKABLE); + lv_obj_set_ext_click_area(attr, 8); + lv_obj_add_event_cb(attr, onMapCredits, LV_EVENT_CLICKED, NULL); + lv_obj_align(attr, LV_ALIGN_BOTTOM_LEFT, 6, -6 - bottom); _map_hint = mapPill(body, ""); lv_label_set_long_mode(_map_hint, LV_LABEL_LONG_WRAP); lv_obj_set_width(_map_hint, 200); lv_obj_set_style_text_align(_map_hint, LV_TEXT_ALIGN_CENTER, 0); lv_obj_center(_map_hint); lv_obj_add_flag(_map_hint, LV_OBJ_FLAG_HIDDEN); + if (_map_nav) buildNavControls(body); lv_obj_update_layout(body); mapview::s_mark_count = 0; // the previous markers went with the previous screen @@ -184,32 +310,49 @@ void UITask::layoutMap() { int w = lv_obj_get_width(_map_area), h = lv_obj_get_height(_map_area); double left = _map_cx * mapview::TILE_PX - w / 2.0; // world px of the view's top-left double top = _map_cy * mapview::TILE_PX - h / 2.0; + mapview::s_left = left; mapview::s_top = top; int tx0 = (int)floor(left / mapview::TILE_PX), ty0 = (int)floor(top / mapview::TILE_PX); int n = 1 << _map_z; bool have_provider = mapview::s_available; - int shown = 0, missing = 0; + int shown = 0, missing = 0, over = 0; _map_pending = false; for (int j = 0; j < mapview::GRID_ROWS; j++) { for (int i = 0; i < mapview::GRID_COLS; i++) { + lv_obj_t* cell = mapview::s_cells[j * mapview::GRID_COLS + i]; lv_obj_t* img = _map_tiles[j * mapview::GRID_COLS + i]; - int tx = tx0 + i, ty = ty0 + j; + int tx = tx0 + i, ty = ty0 + j, wx = ((tx % n) + n) % n; int px = (int)lround(tx * (double)mapview::TILE_PX - left); int py = (int)lround(ty * (double)mapview::TILE_PX - top); bool on_screen = px < w && py < h && px + mapview::TILE_PX > 0 && py + mapview::TILE_PX > 0; mapview::TileCache::Slot* s = nullptr; if (have_provider && on_screen && ty >= 0 && ty < n) { - s = mapview::s_cache.find(_map_z, ((tx % n) + n) % n, ty); + s = mapview::s_cache.find(_map_z, wx, ty); if (!s) _map_pending = true; } - if (s && s->present) { - if (lv_image_get_src(img) != &s->dsc) lv_image_set_src(img, &s->dsc); - lv_obj_set_pos(img, px, py); - lv_obj_remove_flag(img, LV_OBJ_FLAG_HIDDEN); + mapview::TileCache::Slot* show = (s && s->present) ? s : nullptr; + int k = 0; + bool stand_in = false; + if (s && !s->present) { // none at this zoom: magnify a coarser one + missing++; + int wz, ax, ay; + show = mapview::ancestorFor(_map_z, wx, ty, k, wz, ax, ay); + if (!show && wz >= 0) _map_pending = true; + } else if (!s && have_provider && on_screen && ty >= 0 && ty < n) { // not decoded yet + show = mapview::cachedAncestor(_map_z, wx, ty, k); + stand_in = show != nullptr; + } + if (show) { + if (lv_image_get_src(img) != &show->dsc) lv_image_set_src(img, &show->dsc); + lv_image_set_scale(img, LV_SCALE_NONE << k); + int m = (1 << k) - 1; // which part of the magnified tile this cell is + lv_obj_set_pos(img, -(wx & m) * mapview::TILE_PX, -(ty & m) * mapview::TILE_PX); + lv_obj_set_pos(cell, px, py); + lv_obj_remove_flag(cell, LV_OBJ_FLAG_HIDDEN); shown++; + if (k > over && !stand_in) over = k; } else { - lv_obj_add_flag(img, LV_OBJ_FLAG_HIDDEN); - if (s) missing++; + lv_obj_add_flag(cell, LV_OBJ_FLAG_HIDDEN); } } } @@ -224,16 +367,21 @@ void UITask::layoutMap() { } else { lv_obj_add_flag(_map_me, LV_OBJ_FLAG_HIDDEN); } + const int half = mapview::MARK_D / 2; for (int k = 0; k < mapview::s_mark_count; k++) { const mapview::Mark& m = mapview::s_marks[k]; + int dy = lv_obj_get_y(m.dot); // the dot within the marker (vertically centred on the name) double x = mapview::lonToTileX(m.lon_e6 / 1e6, _map_z) * mapview::TILE_PX - left; double y = mapview::latToTileY(m.lat_e6 / 1e6, _map_z) * mapview::TILE_PX - top; - lv_obj_set_pos(m.obj, (int)x - 6, (int)y - 6); // the dot's centre on the spot; name to its right + lv_obj_set_pos(m.obj, (int)lround(x) - half, (int)lround(y) - half - dy); // the dot's centre on the spot } + if (_map_nav) layoutNav(); - lv_label_set_text_fmt(_map_zoom_lbl, "z%d", _map_z); + if (over) lv_label_set_text_fmt(_map_zoom_lbl, "z%d (map z%d)", _map_z, _map_z - over); // magnified + else lv_label_set_text_fmt(_map_zoom_lbl, "z%d", _map_z); const char* hint = !have_provider ? "No map on the SD card.\nPut tiles in /maps (tools/maps)." - : (!_map_pending && shown == 0 && missing > 0) ? "No tiles here at this zoom." : nullptr; + : (!_map_pending && shown == 0 && missing > 0) + ? "No map detail here at this zoom.\nZoom out, or download this area." : nullptr; if (hint) { lv_label_set_text(_map_hint, hint); lv_obj_remove_flag(_map_hint, LV_OBJ_FLAG_HIDDEN); } else lv_obj_add_flag(_map_hint, LV_OBJ_FLAG_HIDDEN); } @@ -246,15 +394,27 @@ void UITask::mapLoop() { int32_t lat, lon; if (_core->course.currentLocation(lat, lon)) { _map_cx = mapview::lonToTileX(lon / 1e6, _map_z); - _map_cy = mapview::latToTileY(lat / 1e6, _map_z); + _map_cy = mapview::latToTileY(lat / 1e6, _map_z) + mapCenterBias(); } } rebuildMapMarkers(); layoutMap(); } - if (!_map_pending || !_map_area) return; + if (_map_nav) navPollAveraging(); + if (_map_nav && (int32_t)(millis() - _next_nav_bar_ms) >= 0) { + _next_nav_bar_ms = millis() + 1000; + refreshNavBar(); + } + if (!_map_area) return; + uint32_t since_pan = millis() - mapview::s_last_pan_ms; + if (since_pan < mapview::PAN_SETTLE_MS) return; // mid-drag: keep it smooth, decode after int w = lv_obj_get_width(_map_area), h = lv_obj_get_height(_map_area); double left = _map_cx * mapview::TILE_PX - w / 2.0, top = _map_cy * mapview::TILE_PX - h / 2.0; + if (!_map_pending) { // view complete: when idle, decode the next tile a pan would reveal + if (since_pan >= mapview::PREFETCH_IDLE_MS && mapview::s_available) + mapview::prefetchOne(left, top, w, h, _map_z); + return; + } int tx0 = (int)floor(left / mapview::TILE_PX), ty0 = (int)floor(top / mapview::TILE_PX); int n = 1 << _map_z; int best_x = 0, best_y = 0; @@ -271,7 +431,28 @@ void UITask::mapLoop() { if (d < best_d) { best_d = d; best_x = wx; best_y = ty; } } } - if (best_d < 1e18) mapview::s_cache.load(*mapview::s_provider, _map_z, best_x, best_y); + if (best_d < 1e18) { + mapview::s_cache.load(*mapview::s_provider, _map_z, best_x, best_y); + } else { // every tile at this zoom looked up: decode a coarser one for a missing tile + best_d = 1e18; + int bz = -1; + for (int j = 0; j < mapview::GRID_ROWS; j++) { + for (int i = 0; i < mapview::GRID_COLS; i++) { + int tx = tx0 + i, ty = ty0 + j; + double px = tx * (double)mapview::TILE_PX - left, py = ty * (double)mapview::TILE_PX - top; + if (px >= w || py >= h || px + mapview::TILE_PX <= 0 || py + mapview::TILE_PX <= 0) continue; + if (ty < 0 || ty >= n) continue; + int wx = ((tx % n) + n) % n; + mapview::TileCache::Slot* s = mapview::s_cache.find(_map_z, wx, ty); + if (!s || s->present) continue; + int k, wz, ax, ay; + if (mapview::ancestorFor(_map_z, wx, ty, k, wz, ax, ay) || wz < 0) continue; + double d = fabs(px + 128 - w / 2.0) + fabs(py + 128 - h / 2.0); + if (d < best_d) { best_d = d; bz = wz; best_x = ax; best_y = ay; } + } + } + if (bz >= 0) mapview::s_cache.load(*mapview::s_provider, bz, best_x, best_y); + } layoutMap(); } @@ -279,38 +460,50 @@ void UITask::rebuildMapMarkers() { if (!_map_marks) return; for (int k = 0; k < mapview::s_mark_count; k++) lv_obj_delete(mapview::s_marks[k].obj); mapview::s_mark_count = 0; + if (_map_nav) rebuildNavMarkers(); + else rebuildNodeMarkers(); + lv_obj_update_layout(_map_marks); // so layoutMap() knows where each dot sits in its marker +} + +void UITask::rebuildNodeMarkers() { _nearby->refreshStored(); - for (int i = 0; i < _nearby->count() && mapview::s_mark_count < NearbyModel::MAX_NEARBY; i++) { + for (int i = 0; i < _nearby->count() && mapview::s_mark_count < mapview::MAX_MARKS; i++) { const NearbyModel::Entry& e = _nearby->at(i); if (e.lat_e6 == 0 && e.lon_e6 == 0) continue; uint32_t col = e.is_live ? theme::OK : e.type == ADV_TYPE_CHAT ? theme::TEXT : theme::TEXT_MUTED; - - // One clickable object per marker: a dot plus the name beside it. - lv_obj_t* m = lv_obj_create(_map_marks); - lv_obj_remove_style_all(m); - lv_obj_set_size(m, LV_SIZE_CONTENT, LV_SIZE_CONTENT); - lv_obj_set_flex_flow(m, LV_FLEX_FLOW_ROW); - lv_obj_set_flex_align(m, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); - lv_obj_set_style_pad_column(m, 3, 0); - lv_obj_add_flag(m, LV_OBJ_FLAG_EVENT_BUBBLE); // a drag starting here still pans - lv_obj_add_event_cb(m, onMapMarker, LV_EVENT_CLICKED, (void*)(uintptr_t)mapview::s_mark_count); - lv_obj_t* dot = lv_obj_create(m); - lv_obj_remove_style_all(dot); - lv_obj_set_size(dot, 12, 12); - lv_obj_set_style_radius(dot, LV_RADIUS_CIRCLE, 0); - lv_obj_set_style_bg_color(dot, lv_color_hex(col), 0); - lv_obj_set_style_bg_opa(dot, LV_OPA_COVER, 0); - lv_obj_set_style_border_color(dot, lv_color_hex(theme::BG), 0); - lv_obj_set_style_border_width(dot, 2, 0); - lv_obj_remove_flag(dot, LV_OBJ_FLAG_CLICKABLE); - lv_obj_t* name = mapPill(m, e.name[0] ? e.name : "?"); - lv_obj_remove_flag(name, LV_OBJ_FLAG_CLICKABLE); - - mapview::Mark& mk = mapview::s_marks[mapview::s_mark_count++]; - mk.lat_e6 = e.lat_e6; mk.lon_e6 = e.lon_e6; mk.row = i; mk.obj = m; + addMapMark(mapview::MK_NODE, i, e.lat_e6, e.lon_e6, col, e.name[0] ? e.name : "?"); } } +// One clickable object per marker: a dot plus the name beside it. The object +// is placed so the dot's centre is on the spot, from where layout actually put +// the dot inside it (Mark::dot) -- the name may be taller than the dot. +void UITask::addMapMark(uint8_t kind, int idx, int32_t lat_e6, int32_t lon_e6, uint32_t col, const char* text) { + if (!_map_marks || mapview::s_mark_count >= mapview::MAX_MARKS) return; + lv_obj_t* m = lv_obj_create(_map_marks); + lv_obj_remove_style_all(m); + lv_obj_set_size(m, LV_SIZE_CONTENT, LV_SIZE_CONTENT); + lv_obj_set_flex_flow(m, LV_FLEX_FLOW_ROW); + lv_obj_set_flex_align(m, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); + lv_obj_set_style_pad_column(m, 3, 0); + lv_obj_add_flag(m, LV_OBJ_FLAG_EVENT_BUBBLE); // a drag starting here still pans + lv_obj_add_event_cb(m, onMapMarker, LV_EVENT_CLICKED, (void*)(uintptr_t)mapview::s_mark_count); + lv_obj_t* dot = lv_obj_create(m); + lv_obj_remove_style_all(dot); + lv_obj_set_size(dot, mapview::MARK_D, mapview::MARK_D); + lv_obj_set_style_radius(dot, LV_RADIUS_CIRCLE, 0); + lv_obj_set_style_bg_color(dot, lv_color_hex(col), 0); + lv_obj_set_style_bg_opa(dot, LV_OPA_COVER, 0); + lv_obj_set_style_border_color(dot, lv_color_hex(theme::BG), 0); + lv_obj_set_style_border_width(dot, 2, 0); + lv_obj_remove_flag(dot, LV_OBJ_FLAG_CLICKABLE); + lv_obj_t* name = mapPill(m, text); + lv_obj_remove_flag(name, LV_OBJ_FLAG_CLICKABLE); + + mapview::Mark& mk = mapview::s_marks[mapview::s_mark_count++]; + mk.lat_e6 = lat_e6; mk.lon_e6 = lon_e6; mk.idx = idx; mk.kind = kind; mk.obj = m; mk.dot = dot; +} + void UITask::mapPan(int dx, int dy) { _map_cx -= dx / (double)mapview::TILE_PX; _map_cy -= dy / (double)mapview::TILE_PX; @@ -332,19 +525,36 @@ void UITask::mapZoom(int delta) { void UITask::mapCenterOnMe() { int32_t lat, lon; - if (!_core->course.currentLocation(lat, lon)) { showToast("No GPS fix"); return; } + if (!ensureGps() || !_core->course.currentLocation(lat, lon)) { _map_follow = true; return; } // centres once a fix comes _map_follow = true; _map_cx = mapview::lonToTileX(lon / 1e6, _map_z); - _map_cy = mapview::latToTileY(lat / 1e6, _map_z); + _map_cy = mapview::latToTileY(lat / 1e6, _map_z) + mapCenterBias(); layoutMap(); } void UITask::mapOpenMarker(int idx) { if (idx < 0 || idx >= mapview::s_mark_count) return; - openNode(mapview::s_marks[idx].row); + const mapview::Mark& m = mapview::s_marks[idx]; + if (m.kind == mapview::MK_WAYPOINT) { navWaypointMenu(m.idx); return; } + if (m.kind == mapview::MK_LIVE) { navPick(navmap::code(navmap::T_LIVE, m.idx)); return; } + openNode(m.idx); _node_from_map = true; } +// Screen point (relative to the map area) -> lat/lon at the current view. +void UITask::mapPointToLatLon(int x, int y, int32_t& lat_e6, int32_t& lon_e6) const { + double n = (double)(1 << _map_z); + double tx = (mapview::s_left + x) / mapview::TILE_PX, ty = (mapview::s_top + y) / mapview::TILE_PX; + double lon = tx / n * 360.0 - 180.0; + double lat = atan(sinh(M_PI * (1.0 - 2.0 * ty / n))) * 180.0 / M_PI; + lat_e6 = (int32_t)lround(lat * 1e6); + lon_e6 = (int32_t)lround(lon * 1e6); +} + +void UITask::mapLongPress(int x, int y) { + if (_map_nav) navDropAt(x, y); +} + // ── Area download ───────────────────────────────────────────────────────────── @@ -405,6 +615,21 @@ void UITask::mapDownloadPopup() { lv_obj_align(_dl_zoom_lbl, LV_ALIGN_RIGHT_MID, -56, 0); headerButton(zr, LV_SYMBOL_MINUS, onDlZoomMinus, 96, NULL); + // Unfinished job (power-off, lost WiFi, Stop): resume or drop it. + _dl_job_row = lv_obj_create(panel); + styleSurface(_dl_job_row, theme::SURFACE); + lv_obj_remove_flag(_dl_job_row, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_size(_dl_job_row, LV_PCT(100), 40); + lv_obj_set_style_radius(_dl_job_row, theme::RADIUS, 0); + _dl_job_lbl = label(_dl_job_row, "", THEME_FONT_SMALL, theme::TEXT); + lv_obj_set_width(_dl_job_lbl, 170); + lv_label_set_long_mode(_dl_job_lbl, LV_LABEL_LONG_WRAP); + lv_obj_align(_dl_job_lbl, LV_ALIGN_LEFT_MID, theme::PAD, 0); + lv_obj_t* res = headerButton(_dl_job_row, LV_SYMBOL_PLAY " Resume", onDlResume, 48, NULL); + lv_obj_set_style_bg_color(res, lv_color_hex(theme::ACCENT_DIM), 0); + headerButton(_dl_job_row, LV_SYMBOL_TRASH, onDlDiscard, 4, NULL); + lv_obj_add_flag(_dl_job_row, LV_OBJ_FLAG_HIDDEN); + _dl_info = label(panel, "", THEME_FONT_SMALL, theme::TEXT); lv_label_set_long_mode(_dl_info, LV_LABEL_LONG_WRAP); lv_obj_set_width(_dl_info, LV_PCT(100)); @@ -439,7 +664,7 @@ void UITask::mapDownloadPopup() { void UITask::mapDownloadClose() { if (_dl_overlay) lv_obj_delete_async(_dl_overlay); // may be closing from its own button - _dl_overlay = _dl_info = _dl_zoom_lbl = _dl_start_lbl = nullptr; + _dl_overlay = _dl_info = _dl_zoom_lbl = _dl_start_lbl = _dl_job_row = _dl_job_lbl = nullptr; } void UITask::mapDownloadZmax(int delta) { @@ -468,23 +693,32 @@ void UITask::refreshDownloadPopup() { (unsigned long)dl.processed(), (unsigned long)dl.total(), (unsigned long)dl.downloaded(), dl.sourceHost(), net); lv_label_set_text(_dl_start_lbl, LV_SYMBOL_STOP " Stop"); + if (_dl_job_row) lv_obj_add_flag(_dl_job_row, LV_OBJ_FLAG_HIDDEN); return; } + mapview::TileArea job; + if (_dl_job_row && dl.savedJob(job)) { + lv_label_set_text_fmt(_dl_job_lbl, "Unfinished: z%d-%d,\n%lu tiles", job.zmin, job.zmax, + (unsigned long)mapview::countTiles(job)); + lv_obj_remove_flag(_dl_job_row, LV_OBJ_FLAG_HIDDEN); + } else if (_dl_job_row) { + lv_obj_add_flag(_dl_job_row, LV_OBJ_FLAG_HIDDEN); + } int w = _map_area ? lv_obj_get_width(_map_area) : 320, h = _map_area ? lv_obj_get_height(_map_area) : 218; mapview::TileArea a = visibleArea(_map_cx, _map_cy, _map_z, w, h, dlZmin(_map_z), _dl_zmax); uint32_t n = mapview::countTiles(a); char ssid[33], pass[65]; bool have_wifi = lvport::loadWifi(ssid, sizeof(ssid), pass, sizeof(pass)); char last[64] = ""; - if (dl.state() == mapview::TileDownloader::DONE || dl.state() == mapview::TileDownloader::FAILED || - dl.state() == mapview::TileDownloader::CANCELLED) + bool job_shown = _dl_job_row && !lv_obj_has_flag(_dl_job_row, LV_OBJ_FLAG_HIDDEN); // it says enough + if (!job_shown && (dl.state() == mapview::TileDownloader::DONE || dl.state() == mapview::TileDownloader::FAILED || + dl.state() == mapview::TileDownloader::CANCELLED)) snprintf(last, sizeof(last), "\nLast: %s (%lu new)", dl.message(), (unsigned long)dl.downloaded()); char size[16]; uint64_t bytes = (uint64_t)n * mapview::AVG_TILE_BYTES; if (bytes < 1024 * 1024) snprintf(size, sizeof(size), "%lu KB", (unsigned long)(bytes / 1024)); else snprintf(size, sizeof(size), "%lu MB", (unsigned long)((bytes + 512 * 1024) / (1024 * 1024))); - lv_label_set_text_fmt(_dl_info, "z%d-%d: %lu tiles, about %s (tiles on the card are skipped)\n" - "From %s\nWiFi: %s%s", + lv_label_set_text_fmt(_dl_info, "z%d-%d: %lu tiles, about %s - from %s\nWiFi: %s%s", a.zmin, a.zmax, (unsigned long)n, size, dl.sourceHost(), have_wifi ? ssid : "not set - tap WiFi", last); lv_label_set_text(_dl_start_lbl, n > mapview::TileDownloader::MAX_TILES ? "Too large - zoom in" @@ -502,11 +736,28 @@ void UITask::mapDownloadStart() { refreshDownloadPopup(); } +void UITask::mapDownloadResume() { + mapview::TileArea a; + if (mapview::s_dl.active() || !mapview::s_dl.savedJob(a)) return; + char ssid[33], pass[65]; + if (!lvport::loadWifi(ssid, sizeof(ssid), pass, sizeof(pass))) { showWifi(true); return; } + if (!lvport::mountStorage()) { showToast("No SD card"); return; } + if (!mapview::s_dl.start(a, ssid, pass)) { showToast(mapview::s_dl.message()); return; } + refreshDownloadPopup(); +} + +void UITask::mapDownloadDiscard() { + mapview::s_dl.discardJob(); + refreshDownloadPopup(); +} + void UITask::mapDownloadStop() { mapview::s_dl.cancel(); refreshDownloadPopup(); } +static mapview::TileArea s_job_tmp; + // Every UI loop pass, whatever the screen: a download keeps going in the background. void UITask::mapDownloadTick() { mapview::TileDownloader& dl = mapview::s_dl; @@ -539,6 +790,9 @@ void UITask::mapDownloadTick() { lv_label_set_text_fmt(_map_dl_pill, LV_SYMBOL_DOWNLOAD " %lu / %lu", (unsigned long)dl.processed(), (unsigned long)dl.total()); lv_obj_remove_flag(_map_dl_pill, LV_OBJ_FLAG_HIDDEN); + } else if (mapview::s_available && dl.savedJob(s_job_tmp)) { + lv_label_set_text(_map_dl_pill, LV_SYMBOL_DOWNLOAD " Resume?"); + lv_obj_remove_flag(_map_dl_pill, LV_OBJ_FLAG_HIDDEN); } else { lv_obj_add_flag(_map_dl_pill, LV_OBJ_FLAG_HIDDEN); } diff --git a/examples/companion_radio/ui-lvgl/NavMap.h b/examples/companion_radio/ui-lvgl/NavMap.h new file mode 100644 index 00000000..8430e025 --- /dev/null +++ b/examples/companion_radio/ui-lvgl/NavMap.h @@ -0,0 +1,1176 @@ +#pragma once +// Navigation map (Home > Map): one of the map screen's two modes. It shows only +// what you navigate by: saved waypoints, people sharing their position live, +// the recorded trail, and the Locator's active target with a line to it and a +// bar with distance, bearing, course and ETA. The Nodes map (Nearby > map +// button) is the one with every contact. +// +// Everything shown lives in the UI Core (waypoints, locator target, live +// shares, trail), so ui-new's Trail / Waypoints / Locator screens and this map +// work on the same data. Here: the pin button opens the waypoint list (add at +// the GPS position or by coordinates; tap one for its menu: navigate, rename, +// share, delete), a long-press on the map offers that spot as a waypoint or a +// target, the bar opens the target list. +// +// Single-TU fragment: included by ui-lvgl/UITask.cpp after MapScreen.h. + +namespace navmap { + +enum : uint8_t { WP_NAV, WP_RENAME, WP_SHARE, WP_DELETE }; + +// Trail: normalised Web-Mercator coords (0..1) cached when the trail is +// re-read, so a pan only scales and offsets them. One lv_line per recorded +// segment (a pause starts a new one); extra segments join the last. +static const int TRAIL_SEGS = 8; +// Stored as float offsets from the first point (s_ox/s_oy): a float holds a +// small offset exactly enough, but not a whole 0..1 coordinate at street zoom +// (2^18 * 256 px across -- a float is off by pixels there). +static float s_nx[TrailStore::CAPACITY], s_ny[TrailStore::CAPACITY]; +static double s_ox = 0, s_oy = 0; +static lv_point_precise_t s_pts[TrailStore::CAPACITY]; +static int s_seg_first[TRAIL_SEGS], s_seg_len[TRAIL_SEGS], s_segs = 0; +static lv_obj_t* s_trail[TRAIL_SEGS]; +static lv_obj_t* s_target_line = nullptr; +static lv_point_precise_t s_target_pts[2]; +static lv_obj_t* s_target_ring = nullptr; +static lv_obj_t* s_target_dot = nullptr; +static const int RING_D = 30; // clear of a 12 px marker dot inside it +static navview::EtaTracker s_eta; +static GpsAverager s_avg; // "mark here" with Settings > Waypoint averaging +static TrackBack s_tb; // walking the trail back: its breadcrumb is the target +static const uint32_t TRAIL_COLOR = 0x4FA3FF; + +static void fmtDuration(char* b, int n, uint32_t secs) { + if (secs >= 3600) snprintf(b, n, "%luh%02lu", (unsigned long)(secs / 3600), (unsigned long)(secs % 3600 / 60)); + else snprintf(b, n, "%lum", (unsigned long)((secs + 59) / 60)); +} + +// Web-Mercator 0..1. double: at z18 one pixel is 1/67M of the world. +static double normX(int32_t lon_e6) { return (lon_e6 / 1e6 + 180.0) / 360.0; } +static double normY(int32_t lat_e6) { + double r = lat_e6 / 1e6 * M_PI / 180.0; + return (1.0 - asinh(tan(r)) / M_PI) / 2.0; +} + +// Waypoint labels are 11 bytes of UTF-8 (Waypoint.h): reject input past that, +// so a two-byte letter is never cut in half. +static void onLabelInsert(lv_event_t* e) { + lv_obj_t* ta = (lv_obj_t*)lv_event_get_target(e); + const char* ins = (const char*)lv_event_get_param(e); + if (ins && strlen(lv_textarea_get_text(ta)) + strlen(ins) > WAYPOINT_LABEL_LEN - 1) + lv_textarea_set_insert_replace(ta, ""); +} + +} // namespace navmap + +static void onNavList(lv_event_t* e) { (void)e; s_ui->navTargetsPopup(); } +static void onNavMark(lv_event_t* e) { (void)e; s_ui->navWaypointsPopup(); } +static void onNavAvgCancel(lv_event_t* e) { (void)e; s_ui->navAveragingCancel(); } +static void onNavClear(lv_event_t* e) { (void)e; s_ui->navPick(navmap::code(navmap::T_CLEAR, 0)); } +static void onNavPick(lv_event_t* e) { s_ui->navPick((int)(uintptr_t)lv_event_get_user_data(e)); } +static void onNavWpMenu(lv_event_t* e) { s_ui->navWaypointMenu((int)(uintptr_t)lv_event_get_user_data(e)); } +static void onNavWpAction(lv_event_t* e){ s_ui->navWaypointAction((uint8_t)(uintptr_t)lv_event_get_user_data(e)); } +static void onNavPopupClose(lv_event_t* e) { (void)e; s_ui->navClosePopup(); } +static void onNavRenameKb(lv_event_t* e) { s_ui->navRenameDone(lv_event_get_code(e) == LV_EVENT_READY); } + +// ── Layers and controls (built by buildMap in nav mode) ─────────────────────── + +void UITask::buildNavLayers() { + for (int i = 0; i < navmap::TRAIL_SEGS; i++) { + lv_obj_t* l = lv_line_create(_map_marks); + lv_obj_set_size(l, LV_PCT(100), LV_PCT(100)); // clip to the view, whatever the points + lv_obj_set_style_line_width(l, 3, 0); + lv_obj_set_style_line_color(l, lv_color_hex(navmap::TRAIL_COLOR), 0); + lv_obj_set_style_line_rounded(l, true, 0); + lv_obj_remove_flag(l, LV_OBJ_FLAG_CLICKABLE); + lv_obj_add_flag(l, LV_OBJ_FLAG_HIDDEN); + navmap::s_trail[i] = l; + } + navmap::s_target_line = lv_line_create(_map_marks); + lv_obj_set_size(navmap::s_target_line, LV_PCT(100), LV_PCT(100)); + lv_obj_set_style_line_width(navmap::s_target_line, 2, 0); + lv_obj_set_style_line_color(navmap::s_target_line, lv_color_hex(theme::ACCENT), 0); + lv_obj_set_style_line_dash_width(navmap::s_target_line, 8, 0); + lv_obj_set_style_line_dash_gap(navmap::s_target_line, 6, 0); + lv_obj_remove_flag(navmap::s_target_line, LV_OBJ_FLAG_CLICKABLE); + lv_obj_add_flag(navmap::s_target_line, LV_OBJ_FLAG_HIDDEN); + + navmap::s_target_ring = lv_obj_create(_map_marks); + lv_obj_remove_style_all(navmap::s_target_ring); + lv_obj_set_size(navmap::s_target_ring, navmap::RING_D, navmap::RING_D); + lv_obj_set_style_radius(navmap::s_target_ring, LV_RADIUS_CIRCLE, 0); + lv_obj_set_style_border_color(navmap::s_target_ring, lv_color_hex(theme::ACCENT), 0); + lv_obj_set_style_border_width(navmap::s_target_ring, 3, 0); + lv_obj_remove_flag(navmap::s_target_ring, LV_OBJ_FLAG_CLICKABLE); + lv_obj_add_flag(navmap::s_target_ring, LV_OBJ_FLAG_HIDDEN); + // Centre dot, for a target no marker stands on (a map point, a message + // position, a person whose live share went stale: last known place). + navmap::s_target_dot = lv_obj_create(navmap::s_target_ring); + lv_obj_remove_style_all(navmap::s_target_dot); + lv_obj_set_size(navmap::s_target_dot, 8, 8); + lv_obj_set_style_radius(navmap::s_target_dot, LV_RADIUS_CIRCLE, 0); + lv_obj_set_style_bg_color(navmap::s_target_dot, lv_color_hex(theme::ACCENT), 0); + lv_obj_set_style_bg_opa(navmap::s_target_dot, LV_OPA_COVER, 0); + lv_obj_remove_flag(navmap::s_target_dot, LV_OBJ_FLAG_CLICKABLE); + lv_obj_center(navmap::s_target_dot); + navmap::s_segs = 0; +} + +void UITask::buildNavControls(lv_obj_t* body) { + // The target list opens from the bar; the left column: back, mark the spot, tools. + lv_obj_align(mapButton(body, UI_SYMBOL_PIN, onNavMark), LV_ALIGN_TOP_LEFT, 6, 52); + + _nav_bar = lv_button_create(body); // tap: the target list + lv_obj_set_size(_nav_bar, LV_PCT(100), navmap::BAR_H); + lv_obj_align(_nav_bar, LV_ALIGN_BOTTOM_MID, 0, 0); + lv_obj_set_style_radius(_nav_bar, 0, 0); + lv_obj_set_style_shadow_width(_nav_bar, 0, 0); + lv_obj_set_style_bg_color(_nav_bar, lv_color_hex(theme::BG), 0); + lv_obj_set_style_bg_opa(_nav_bar, LV_OPA_90, 0); + lv_obj_set_style_bg_color(_nav_bar, lv_color_hex(theme::SURFACE_2), LV_STATE_PRESSED); + lv_obj_set_style_border_side(_nav_bar, LV_BORDER_SIDE_TOP, 0); + lv_obj_set_style_border_color(_nav_bar, lv_color_hex(theme::SURFACE_2), 0); + lv_obj_set_style_border_width(_nav_bar, 1, 0); + lv_obj_set_style_pad_hor(_nav_bar, theme::PAD, 0); + lv_obj_set_style_pad_ver(_nav_bar, 3, 0); + lv_obj_add_event_cb(_nav_bar, onNavList, LV_EVENT_CLICKED, NULL); + _nav_title = label(_nav_bar, "", THEME_FONT_BODY, theme::ACCENT); + lv_label_set_long_mode(_nav_title, LV_LABEL_LONG_DOT); + lv_obj_set_size(_nav_title, 260, 19); // fixed height: LONG_DOT cuts instead of wrapping + lv_obj_align(_nav_title, LV_ALIGN_TOP_LEFT, 0, 0); + _nav_info = label(_nav_bar, "", THEME_FONT_SMALL, theme::TEXT); + lv_label_set_long_mode(_nav_info, LV_LABEL_LONG_DOT); + lv_obj_set_size(_nav_info, 270, 16); + lv_obj_align(_nav_info, LV_ALIGN_BOTTOM_LEFT, 0, 0); + _nav_clear = lv_button_create(_nav_bar); + lv_obj_set_size(_nav_clear, 34, 34); + lv_obj_align(_nav_clear, LV_ALIGN_RIGHT_MID, 4, 0); + lv_obj_set_style_shadow_width(_nav_clear, 0, 0); + lv_obj_set_style_radius(_nav_clear, theme::RADIUS, 0); + lv_obj_set_style_bg_color(_nav_clear, lv_color_hex(theme::SURFACE), 0); + lv_obj_add_event_cb(_nav_clear, onNavClear, LV_EVENT_CLICKED, NULL); + lv_obj_center(label(_nav_clear, LV_SYMBOL_CLOSE, THEME_FONT_BODY, theme::TEXT)); + _nav_avg_pill = mapPill(body, ""); // tap: cancel averaging + lv_obj_set_style_text_color(_nav_avg_pill, lv_color_hex(theme::ACCENT), 0); + lv_obj_set_style_pad_all(_nav_avg_pill, 8, 0); + lv_obj_align(_nav_avg_pill, LV_ALIGN_CENTER, 0, -30); + lv_obj_add_flag(_nav_avg_pill, LV_OBJ_FLAG_CLICKABLE); + lv_obj_add_event_cb(_nav_avg_pill, onNavAvgCancel, LV_EVENT_CLICKED, NULL); + lv_obj_add_flag(_nav_avg_pill, LV_OBJ_FLAG_HIDDEN); + _nav_rec = mapPill(body, ""); // trail recording / live share running + lv_obj_set_style_text_color(_nav_rec, lv_color_hex(theme::FAIL), 0); + lv_obj_align(_nav_rec, LV_ALIGN_TOP_MID, 0, 40); + lv_obj_add_flag(_nav_rec, LV_OBJ_FLAG_HIDDEN); + _next_nav_bar_ms = 0; + navmap::s_eta.reset(); + refreshNavBar(); +} + +// ── Markers, trail, target (rebuilt every few seconds, laid out on every pan) ─ + +void UITask::rebuildNavMarkers() { + const WaypointModel& wp = _core->waypoints; + for (int i = 0; i < wp.count(); i++) { + const Waypoint& w = wp.at(i); + char t[WAYPOINT_LABEL_LEN + 8]; + snprintf(t, sizeof(t), UI_SYMBOL_FLAG " %s", w.label[0] ? w.label : "?"); + addMapMark(mapview::MK_WAYPOINT, i, w.lat_1e6, w.lon_1e6, theme::ACCENT, t); + } + const LiveTrackStore& lt = _core->live_share.track(); + uint32_t now = rtc_clock.getCurrentTime(); + for (int i = 0; i < LiveTrackStore::CAPACITY; i++) { + if (!lt.isActive(i, now)) continue; + const LiveTrackStore::Entry& e = lt.slotAt(i); + char age[8], t[LiveTrackStore::NAME_LEN + 12]; + geo::fmtAgeShort(age, sizeof(age), now, e.ts); + snprintf(t, sizeof(t), "%s%s%s", e.name, age[0] ? " " : "", age); + addMapMark(mapview::MK_LIVE, i, e.lat_1e6, e.lon_1e6, theme::OK, t); + } + + // Trail -> normalised points in segments. + TrailStore& ts = _core->trail.store(); + navmap::s_segs = 0; + int n = ts.count(); + for (int i = 0; i < n; i++) { + const TrailPoint& p = ts.at(i); + if (i == 0) { navmap::s_ox = navmap::normX(p.lon_1e6); navmap::s_oy = navmap::normY(p.lat_1e6); } + navmap::s_nx[i] = (float)(navmap::normX(p.lon_1e6) - navmap::s_ox); + navmap::s_ny[i] = (float)(navmap::normY(p.lat_1e6) - navmap::s_oy); + bool new_seg = i == 0 || ((p.flags & TRAIL_FLAG_SEG_START) && navmap::s_segs < navmap::TRAIL_SEGS); + if (new_seg) { + navmap::s_seg_first[navmap::s_segs] = i; + navmap::s_seg_len[navmap::s_segs] = 0; + navmap::s_segs++; + } + navmap::s_seg_len[navmap::s_segs - 1]++; + } +} + +void UITask::layoutNav() { + if (!navmap::s_target_line) return; + double scale = (double)(1 << _map_z) * mapview::TILE_PX; + for (int s = 0; s < navmap::TRAIL_SEGS; s++) { + lv_obj_t* l = navmap::s_trail[s]; + if (s >= navmap::s_segs || navmap::s_seg_len[s] < 2) { lv_obj_add_flag(l, LV_OBJ_FLAG_HIDDEN); continue; } + int f = navmap::s_seg_first[s], len = navmap::s_seg_len[s]; + for (int i = f; i < f + len; i++) { + navmap::s_pts[i].x = (lv_value_precise_t)lround((navmap::s_ox + navmap::s_nx[i]) * scale - mapview::s_left); + navmap::s_pts[i].y = (lv_value_precise_t)lround((navmap::s_oy + navmap::s_ny[i]) * scale - mapview::s_top); + } + lv_line_set_points(l, &navmap::s_pts[f], len); + lv_obj_remove_flag(l, LV_OBJ_FLAG_HIDDEN); + } + + int32_t tlat, tlon, mlat, mlon; + char name[8]; + bool person, set; + bool target = navCurrentTarget(tlat, tlon, name, sizeof(name), person, set); + if (target) { + int tx = (int)lround(navmap::normX(tlon) * scale - mapview::s_left); + int ty = (int)lround(navmap::normY(tlat) * scale - mapview::s_top); + lv_obj_set_pos(navmap::s_target_ring, tx - navmap::RING_D / 2, ty - navmap::RING_D / 2); + lv_obj_remove_flag(navmap::s_target_ring, LV_OBJ_FLAG_HIDDEN); + bool marked = false; // a waypoint / live marker already sits on the target + const int half = mapview::MARK_D / 2; + for (int k = 0; k < mapview::s_mark_count && !marked; k++) { + lv_obj_t* m = mapview::s_marks[k].obj; + marked = abs(lv_obj_get_x(m) + half - tx) <= 3 && abs(lv_obj_get_y(m) + lv_obj_get_y(mapview::s_marks[k].dot) + half - ty) <= 3; + } + if (marked) lv_obj_add_flag(navmap::s_target_dot, LV_OBJ_FLAG_HIDDEN); + else lv_obj_remove_flag(navmap::s_target_dot, LV_OBJ_FLAG_HIDDEN); + if (_core->course.currentLocation(mlat, mlon)) { + navmap::s_target_pts[0].x = (lv_value_precise_t)lround(navmap::normX(mlon) * scale - mapview::s_left); + navmap::s_target_pts[0].y = (lv_value_precise_t)lround(navmap::normY(mlat) * scale - mapview::s_top); + navmap::s_target_pts[1].x = tx; + navmap::s_target_pts[1].y = ty; + lv_line_set_points(navmap::s_target_line, navmap::s_target_pts, 2); + lv_obj_remove_flag(navmap::s_target_line, LV_OBJ_FLAG_HIDDEN); + } else { + lv_obj_add_flag(navmap::s_target_line, LV_OBJ_FLAG_HIDDEN); + } + } else { + lv_obj_add_flag(navmap::s_target_ring, LV_OBJ_FLAG_HIDDEN); + lv_obj_add_flag(navmap::s_target_line, LV_OBJ_FLAG_HIDDEN); + } +} + +// Bottom bar: the target, then distance / bearing / own course / ETA. +void UITask::refreshNavBar() { + if (!_nav_bar) return; + refreshNavTools(); + { + char t[48] = ""; + int o = 0; + TrailStore& ts = _core->trail.store(); + if (ts.isActive()) { + char d[12]; + geo::fmtDist(d, sizeof(d), ts.totalDistanceMeters() / 1000.0f, _prefs && _prefs->units_imperial); + o += snprintf(t + o, sizeof(t) - o, "%s %s", ts.isPaused() ? "PAUSED" : "REC", d); + } + if (_prefs && _prefs->loc_share_enabled) { + char left[12]; + navmap::fmtDuration(left, sizeof(left), _core->live_share.remainingSecs()); + o += snprintf(t + o, sizeof(t) - o, "%sLIVE %s", o ? " " : "", left); + } + lv_label_set_text(_nav_rec, t); + if (o) lv_obj_remove_flag(_nav_rec, LV_OBJ_FLAG_HIDDEN); else lv_obj_add_flag(_nav_rec, LV_OBJ_FLAG_HIDDEN); + } + int32_t tlat, tlon; + char name[40]; + bool person = false, set = false; + bool pos = navCurrentTarget(tlat, tlon, name, sizeof(name), person, set); + if (!set) { + lv_label_set_text(_nav_title, "No target"); + lv_obj_set_style_text_color(_nav_title, lv_color_hex(theme::TEXT_MUTED), 0); + lv_label_set_text(_nav_info, "Tap here to pick a target, or hold the map on a spot"); + lv_obj_add_flag(_nav_clear, LV_OBJ_FLAG_HIDDEN); + return; + } + lv_obj_set_style_text_color(_nav_title, lv_color_hex(theme::ACCENT), 0); + lv_obj_remove_flag(_nav_clear, LV_OBJ_FLAG_HIDDEN); + lv_label_set_text_fmt(_nav_title, "%s %s", navmap::s_tb.active() ? LV_SYMBOL_LOOP : person ? UI_SYMBOL_USERS : UI_SYMBOL_FLAG, + name); + if (!pos) { + lv_label_set_text(_nav_info, "Position unknown (not shared recently)"); + return; + } + int32_t lat, lon; + if (!_core->course.currentLocation(lat, lon)) { + lv_label_set_text(_nav_info, _core->gpsEnabled() || !_core->gpsAvailable() + ? "Waiting for a GPS fix..." : "GPS is off - tap the crosshair to turn it on"); + return; + } + bool imperial = _prefs->units_imperial; + float km = geo::haversineKm(lat, lon, tlat, tlon); + navmap::s_eta.update(km, millis()); + char dist[12], eta[12], hdg[16] = ""; + geo::fmtDist(dist, sizeof(dist), km, imperial); + int to = geo::bearingDeg(lat, lon, tlat, tlon); + int cog; + if (_core->course.currentCourse(cog)) snprintf(hdg, sizeof(hdg), " - you %d\xC2\xB0", cog); + if (navmap::s_eta.eta(km, eta, sizeof(eta))) + lv_label_set_text_fmt(_nav_info, "%s %d\xC2\xB0 %s%s - ETA %s", dist, to, geo::bearingCardinal(to), hdg, eta); + else + lv_label_set_text_fmt(_nav_info, "%s %d\xC2\xB0 %s%s", dist, to, geo::bearingCardinal(to), hdg); +} + +// ── Target selection ────────────────────────────────────────────────────────── + +// What the map navigates to: the track-back breadcrumb while walking the trail +// back, else the Locator target. `set`: there is a target at all; returns +// whether its position is known. +bool UITask::navCurrentTarget(int32_t& lat, int32_t& lon, char* name, int n, bool& person, bool& set) { + person = false; + TrailStore& ts = _core->trail.store(); + if (navmap::s_tb.active() && navmap::s_tb.index() < ts.count()) { + set = true; + navmap::s_tb.label(name, n); + lat = ts.at(navmap::s_tb.index()).lat_1e6; + lon = ts.at(navmap::s_tb.index()).lon_1e6; + return true; + } + set = _prefs && _prefs->locator_has_target; + if (!set) return false; + person = _prefs->locator_target_kind != 0; + snprintf(name, n, "%s", _prefs->locator_label[0] ? _prefs->locator_label : "Target"); + return _core->locator.activeTargetPos(lat, lon); +} + +// From loop(), once a second on any screen: advance along the trail. +void UITask::navPollTrackBack() { + if (!navmap::s_tb.active()) return; + int32_t lat = 0, lon = 0; + bool fix = _core->course.currentLocation(lat, lon); + TrackBack::Step st = navmap::s_tb.poll(_core->trail.store(), fix, lat, lon); + if (st == TrackBack::ADVANCED) navmap::s_eta.reset(); + else if (st == TrackBack::ARRIVED) showToast("Back at the trail start", 4000); + if (st != TrackBack::NONE && _screen == SCR_MAP && _map_nav) { layoutMap(); refreshNavBar(); } +} + +// Frame own position and the target together (or just centre the target). +void UITask::navFrameTarget() { + int32_t tlat, tlon, lat, lon; + char name[8]; + bool person, set; + if (!navCurrentTarget(tlat, tlon, name, sizeof(name), person, set)) return; + _map_follow = false; + int w = _map_area ? lv_obj_get_width(_map_area) : 320; + int h = (_map_area ? lv_obj_get_height(_map_area) : 218) - navmap::BAR_H; + double tx = navmap::normX(tlon), ty = navmap::normY(tlat); + double cx = tx, cy = ty; + int z = _map_z < 14 ? 14 : _map_z; + if (_core->course.currentLocation(lat, lon)) { + double mx = navmap::normX(lon), my = navmap::normY(lat); + cx = (tx + mx) / 2; cy = (ty + my) / 2; + double dx = fabs(tx - mx) * mapview::TILE_PX, dy = fabs(ty - my) * mapview::TILE_PX; + z = mapview::MAX_Z - 1; + while (z > mapview::MIN_Z && (dx * (1 << z) > w - 150 || dy * (1 << z) > h - 110)) z--; // clear of the buttons + } + _map_z = z; + double n = (double)(1 << z); + _map_cx = cx * n; + _map_cy = cy * n + navmap::BAR_H / 2.0 / mapview::TILE_PX; // keep it clear of the bar + layoutMap(); +} + +void UITask::navSetTarget(uint8_t kind, const uint8_t* key, int32_t lat, int32_t lon, const char* name) { + navmap::s_tb.stop(); // a chosen target replaces walking the trail back + _core->locator.setTarget(kind, key, lat, lon, name); + the_mesh.savePrefs(); + navmap::s_eta.reset(); + char t[40]; + snprintf(t, sizeof(t), "Navigating to %s", name); + showToast(t); +} + +void UITask::navPick(int code) { + uint8_t type = (uint8_t)(code >> 8); + int idx = code & 0xFF; + switch (type) { + case navmap::T_CLEAR: + if (navmap::s_tb.active()) { navmap::s_tb.stop(); showToast("Track back stopped"); break; } + _core->locator.clearTarget(); + the_mesh.savePrefs(); + showToast("Target cleared"); + break; + case navmap::T_WAYPOINT: { + if (idx >= _core->waypoints.count()) return; + const Waypoint& w = _core->waypoints.at(idx); + navSetTarget(0, nullptr, w.lat_1e6, w.lon_1e6, w.label[0] ? w.label : "Waypoint"); + break; + } + case navmap::T_TRAILSTART: { + TrailStore& ts = _core->trail.store(); + if (ts.empty()) return; + navSetTarget(0, nullptr, ts.first().lat_1e6, ts.first().lon_1e6, "Trail start"); + break; + } + case navmap::T_LIVE: { + const LiveTrackStore& lt = _core->live_share.track(); + if (idx >= LiveTrackStore::CAPACITY || !lt.isActive(idx, rtc_clock.getCurrentTime())) return; + const LiveTrackStore::Entry& e = lt.slotAt(idx); + // Followed as the person moves: a verified (DM) share by key, a channel + // share by sender name. + navSetTarget(e.verified ? 1 : 2, e.verified ? e.key : nullptr, e.lat_1e6, e.lon_1e6, e.name); + break; + } + } + navClosePopup(); + if (_screen != SCR_MAP || !_map_nav) { openMap(true); } + navFrameTarget(); + refreshNavBar(); +} + +// Node detail > Navigate: a contact as the target (followed by key). +void UITask::navToNode(const uint8_t* key, int32_t lat, int32_t lon, const char* name) { + navSetTarget(1, key, lat, lon, name); + openMap(true); + navFrameTarget(); + refreshNavBar(); +} + +// ── Popups: target list, waypoint menu, rename ──────────────────────────────── + +lv_obj_t* UITask::navPopupPanel(const char* title, bool full) { + navClosePopup(); + _nav_overlay = lv_obj_create(lv_screen_active()); + lv_obj_remove_style_all(_nav_overlay); + lv_obj_set_size(_nav_overlay, LV_PCT(100), LV_PCT(100)); + lv_obj_set_style_bg_color(_nav_overlay, lv_color_hex(0x000000), 0); + lv_obj_set_style_bg_opa(_nav_overlay, LV_OPA_60, 0); + lv_obj_add_flag(_nav_overlay, LV_OBJ_FLAG_CLICKABLE); + lv_obj_remove_flag(_nav_overlay, LV_OBJ_FLAG_SCROLLABLE); + + lv_obj_t* panel = lv_obj_create(_nav_overlay); + int w = lv_display_get_horizontal_resolution(NULL) - 16; + if (full) { + lv_obj_set_size(panel, w, lv_display_get_vertical_resolution(NULL) - theme::STATUS_H - 12); + lv_obj_set_pos(panel, 8, theme::STATUS_H + 6); + } else { + lv_obj_set_size(panel, w, LV_SIZE_CONTENT); + lv_obj_align(panel, LV_ALIGN_CENTER, 0, theme::STATUS_H / 2); + } + lv_obj_set_style_bg_color(panel, lv_color_hex(theme::BG), 0); + lv_obj_set_style_border_color(panel, lv_color_hex(theme::ACCENT), 0); + lv_obj_set_style_border_width(panel, 1, 0); + lv_obj_set_style_radius(panel, theme::RADIUS, 0); + lv_obj_set_style_pad_all(panel, theme::PAD, 0); + lv_obj_set_style_pad_row(panel, 6, 0); + lv_obj_set_flex_flow(panel, LV_FLEX_FLOW_COLUMN); + lv_obj_remove_flag(panel, LV_OBJ_FLAG_SCROLLABLE); + + lv_obj_t* hdr = lv_obj_create(panel); + styleSurface(hdr, theme::BG); + lv_obj_remove_flag(hdr, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_size(hdr, LV_PCT(100), 28); + lv_obj_t* t = label(hdr, title, THEME_FONT_TITLE, theme::TEXT); + lv_label_set_long_mode(t, LV_LABEL_LONG_DOT); + lv_obj_set_width(t, 230); + lv_obj_align(t, LV_ALIGN_LEFT_MID, 0, 0); + headerButton(hdr, LV_SYMBOL_CLOSE, onNavPopupClose, 0, NULL); + return panel; +} + +void UITask::navClosePopup() { + if (_nav_overlay) lv_obj_delete_async(_nav_overlay); // may be closing from its own button + _nav_overlay = _nav_ta = _nav_kb = _nav_del_lbl = nullptr; + _nav_trail_lbl = _nav_trail_btn = _nav_reset_lbl = _nav_share_lbl = _nav_share_btn = _nav_tb_btn = nullptr; + _nav_del_armed_ms = 0; +} + +static void navRowRight(lv_obj_t* row, const char* text, uint32_t col, int right) { + lv_obj_align(label(row, text, THEME_FONT_SMALL, col), LV_ALIGN_RIGHT_MID, -right, 0); +} + +void UITask::navTargetsPopup() { + lv_obj_t* panel = navPopupPanel("Navigate to", true); + lv_obj_t* list = lv_obj_create(panel); + styleSurface(list, theme::BG); + lv_obj_set_width(list, LV_PCT(100)); + lv_obj_set_flex_grow(list, 1); + lv_obj_set_flex_flow(list, LV_FLEX_FLOW_COLUMN); + lv_obj_set_style_pad_row(list, theme::GAP, 0); + lv_obj_set_scrollbar_mode(list, LV_SCROLLBAR_MODE_ACTIVE); + + int32_t lat = 0, lon = 0; + bool gps = _core->course.currentLocation(lat, lon); + bool imperial = _prefs && _prefs->units_imperial; + auto dist = [&](int32_t la, int32_t lo, char* out, size_t n) { + if (gps) geo::fmtDist(out, n, geo::haversineKm(lat, lon, la, lo), imperial); + else out[0] = '\0'; + }; + + if (_prefs && _prefs->locator_has_target) { + char sub[40]; + snprintf(sub, sizeof(sub), "Now: %s", _prefs->locator_label[0] ? _prefs->locator_label : "target"); + listRow(list, LV_SYMBOL_CLOSE " Clear target", sub, onNavPick, (void*)(uintptr_t)navmap::code(navmap::T_CLEAR, 0)); + } + + const WaypointModel& wp = _core->waypoints; + char sec[32]; + snprintf(sec, sizeof(sec), "WAYPOINTS %d/%d", wp.count(), WaypointStore::CAPACITY); + sectionTitle(list, sec); + for (int i = 0; i < wp.count(); i++) { + const Waypoint& w = wp.at(i); + char title[WAYPOINT_LABEL_LEN + 8], sub[40], d[12]; + snprintf(title, sizeof(title), UI_SYMBOL_FLAG " %s", w.label[0] ? w.label : "(unnamed)"); + snprintf(sub, sizeof(sub), "%.5f, %.5f", w.lat_1e6 / 1e6, w.lon_1e6 / 1e6); + lv_obj_t* row = listRow(list, title, sub, onNavPick, (void*)(uintptr_t)navmap::code(navmap::T_WAYPOINT, i)); + lv_obj_t* more = lv_button_create(row); // the waypoint menu + lv_obj_set_size(more, 40, 34); + lv_obj_align(more, LV_ALIGN_RIGHT_MID, -4, 0); + lv_obj_set_style_shadow_width(more, 0, 0); + lv_obj_set_style_radius(more, theme::RADIUS, 0); + lv_obj_set_style_bg_color(more, lv_color_hex(theme::SURFACE_2), 0); + lv_obj_add_event_cb(more, onNavWpMenu, LV_EVENT_CLICKED, (void*)(uintptr_t)i); + lv_obj_center(label(more, LV_SYMBOL_EDIT, THEME_FONT_BODY, theme::TEXT)); + dist(w.lat_1e6, w.lon_1e6, d, sizeof(d)); + if (d[0]) navRowRight(row, d, theme::TEXT_MUTED, 52); + } + if (wp.count() == 0) { + lv_obj_t* l = label(list, "None yet. Hold the map to drop one, or tap the pin to mark where you are.", + THEME_FONT_SMALL, theme::TEXT_MUTED); + lv_label_set_long_mode(l, LV_LABEL_LONG_WRAP); + lv_obj_set_width(l, LV_PCT(100)); + } + TrailStore& ts = _core->trail.store(); + if (!ts.empty()) { + char d[12], sub[24]; + dist(ts.first().lat_1e6, ts.first().lon_1e6, d, sizeof(d)); + snprintf(sub, sizeof(sub), "%d trail points", ts.count()); + lv_obj_t* row = listRow(list, LV_SYMBOL_HOME " Trail start", sub, onNavPick, + (void*)(uintptr_t)navmap::code(navmap::T_TRAILSTART, 0)); + if (d[0]) navRowRight(row, d, theme::TEXT_MUTED, theme::PAD); + } + + const LiveTrackStore& lt = _core->live_share.track(); + uint32_t now = rtc_clock.getCurrentTime(); + sectionTitle(list, "SHARING LIVE"); + int live = 0; + for (int i = 0; i < LiveTrackStore::CAPACITY; i++) { + if (!lt.isActive(i, now)) continue; + const LiveTrackStore::Entry& e = lt.slotAt(i); + char age[8], sub[40], d[12]; + geo::fmtAgeShort(age, sizeof(age), now, e.ts); + snprintf(sub, sizeof(sub), "%s - %s ago", e.verified ? "direct" : "channel", age[0] ? age : "0s"); + lv_obj_t* row = listRow(list, e.name, sub, onNavPick, (void*)(uintptr_t)navmap::code(navmap::T_LIVE, i)); + dist(e.lat_1e6, e.lon_1e6, d, sizeof(d)); + if (d[0]) navRowRight(row, d, theme::OK, theme::PAD); + live++; + } + if (live == 0) label(list, "Nobody is sharing their position.", THEME_FONT_SMALL, theme::TEXT_MUTED); +} + +void UITask::navWaypointMenu(int idx) { + if (idx < 0 || idx >= _core->waypoints.count()) return; + _nav_wp = idx; + const Waypoint& w = _core->waypoints.at(idx); + lv_obj_t* panel = navPopupPanel(w.label[0] ? w.label : "(unnamed)", false); + char info[80]; + int o = snprintf(info, sizeof(info), "%.5f, %.5f", w.lat_1e6 / 1e6, w.lon_1e6 / 1e6); + int32_t lat, lon; + if (_core->course.currentLocation(lat, lon)) { + char d[12]; + geo::fmtDist(d, sizeof(d), geo::haversineKm(lat, lon, w.lat_1e6, w.lon_1e6), _prefs && _prefs->units_imperial); + int az = geo::bearingDeg(lat, lon, w.lat_1e6, w.lon_1e6); + snprintf(info + o, sizeof(info) - o, "\n%s %d\xC2\xB0 %s", d, az, geo::bearingCardinal(az)); + } + label(panel, info, THEME_FONT_BODY, theme::TEXT); + + lv_obj_t* acts = lv_obj_create(panel); + styleSurface(acts, theme::BG); + lv_obj_remove_flag(acts, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_size(acts, LV_PCT(100), LV_SIZE_CONTENT); + lv_obj_set_flex_flow(acts, LV_FLEX_FLOW_ROW); + lv_obj_set_style_pad_column(acts, theme::GAP, 0); + struct { const char* text; uint8_t act; bool accent; } btns[] = { + { UI_SYMBOL_COMPASS " Go", navmap::WP_NAV, true }, + { LV_SYMBOL_EDIT " Name", navmap::WP_RENAME, false }, + { LV_SYMBOL_UPLOAD " Share", navmap::WP_SHARE, false }, + { LV_SYMBOL_TRASH, navmap::WP_DELETE, false }, + }; + for (auto& b : btns) { + lv_obj_t* bt = lv_button_create(acts); + lv_obj_set_height(bt, 40); + lv_obj_set_flex_grow(bt, 1); + lv_obj_set_style_pad_hor(bt, 4, 0); + lv_obj_set_style_radius(bt, theme::RADIUS, 0); + lv_obj_set_style_shadow_width(bt, 0, 0); + lv_obj_set_style_bg_color(bt, lv_color_hex(b.accent ? theme::ACCENT_DIM : theme::SURFACE), 0); + lv_obj_add_event_cb(bt, onNavWpAction, LV_EVENT_CLICKED, (void*)(uintptr_t)b.act); + lv_obj_t* l = label(bt, b.text, THEME_FONT_SMALL, theme::TEXT); + lv_obj_center(l); + if (b.act == navmap::WP_DELETE) _nav_del_lbl = l; + } +} + +void UITask::navWaypointAction(uint8_t act) { + int i = _nav_wp; + if (i < 0 || i >= _core->waypoints.count()) { navClosePopup(); return; } + switch (act) { + case navmap::WP_NAV: + navPick(navmap::code(navmap::T_WAYPOINT, i)); + break; + case navmap::WP_RENAME: + navRenamePopup(i); + break; + case navmap::WP_SHARE: { + char text[80]; + _core->waypoints.shareText(i, text, sizeof(text)); + navClosePopup(); + shareToMessage(text); + break; + } + case navmap::WP_DELETE: + if (!_nav_del_armed_ms || millis() - _nav_del_armed_ms > 3000) { // second tap within 3 s confirms + _nav_del_armed_ms = millis() | 1; + if (_nav_del_lbl) lv_label_set_text(_nav_del_lbl, "Delete?"); + break; + } + _core->waypoints.remove(i); + navClosePopup(); + showToast("Waypoint deleted"); + rebuildMapMarkers(); + layoutMap(); + refreshNavBar(); + break; + } +} + +void UITask::navRenamePopup(int idx) { + _nav_wp = idx; + lv_obj_t* panel = navPopupPanel("Waypoint name", false); + lv_obj_align(panel, LV_ALIGN_TOP_MID, 0, theme::STATUS_H + 4); // above the keyboard + _nav_ta = lv_textarea_create(panel); + lv_textarea_set_one_line(_nav_ta, true); + lv_textarea_set_text(_nav_ta, _core->waypoints.at(idx).label); + lv_obj_set_size(_nav_ta, LV_PCT(100), 34); + lv_obj_set_style_border_width(_nav_ta, 1, 0); + lv_obj_set_style_pad_ver(_nav_ta, 6, 0); + lv_obj_set_style_pad_hor(_nav_ta, 10, 0); + lv_obj_set_scrollbar_mode(_nav_ta, LV_SCROLLBAR_MODE_OFF); + lv_obj_set_style_border_color(_nav_ta, lv_color_hex(theme::ACCENT), LV_PART_CURSOR | LV_STATE_FOCUSED); + lv_obj_set_style_border_width(_nav_ta, 2, LV_PART_CURSOR | LV_STATE_FOCUSED); + lv_obj_add_state(_nav_ta, LV_STATE_FOCUSED); // draws the cursor + lv_obj_add_event_cb(_nav_ta, navmap::onLabelInsert, LV_EVENT_INSERT, NULL); + + _nav_kb = kb::create(_nav_overlay, _prefs); + lv_obj_set_size(_nav_kb, LV_PCT(100), 124); + lv_obj_align(_nav_kb, LV_ALIGN_BOTTOM_MID, 0, 0); + lv_keyboard_set_textarea(_nav_kb, _nav_ta); + lv_obj_add_event_cb(_nav_kb, onNavRenameKb, LV_EVENT_READY, NULL); + lv_obj_add_event_cb(_nav_kb, onNavRenameKb, LV_EVENT_CANCEL, NULL); +} + +void UITask::navRenameDone(bool ok) { + if (ok && _nav_ta && _nav_wp >= 0 && _nav_wp < _core->waypoints.count()) { + const Waypoint& w = _core->waypoints.at(_nav_wp); + bool was_target = _prefs && _prefs->locator_has_target && _prefs->locator_target_kind == 0 && + _prefs->locator_lat_1e6 == w.lat_1e6 && _prefs->locator_lon_1e6 == w.lon_1e6; + _core->waypoints.rename(_nav_wp, lv_textarea_get_text(_nav_ta)); + if (was_target) { // the bar shows the target's saved label + snprintf(_prefs->locator_label, sizeof(_prefs->locator_label), "%s", _core->waypoints.at(_nav_wp).label); + the_mesh.savePrefs(); + } + rebuildMapMarkers(); + layoutMap(); + refreshNavBar(); + } + navClosePopup(); +} + +// ── Adding waypoints ────────────────────────────────────────────────────────── + +void UITask::navAddWaypoint(int32_t lat, int32_t lon) { + if (_core->waypoints.full()) { showToast("Waypoints full (16)"); return; } + if (!_core->waypoints.add(lat, lon, rtc_clock.getCurrentTime(), "")) return; + int i = _core->waypoints.count() - 1; + char t[48]; + snprintf(t, sizeof(t), "Saved %s", _core->waypoints.at(i).label); + showToast(t); + rebuildMapMarkers(); + layoutMap(); +} + +void UITask::navMarkHere() { + int32_t lat, lon; + if (!ensureGps() || !_core->course.currentLocation(lat, lon)) return; + uint16_t secs = NodePrefs::gpsAvgSecs(_prefs ? _prefs->gps_avg_idx : 0); + if (secs == 0) { navAddWaypoint(lat, lon); return; } + if (_core->waypoints.full()) { showToast("Waypoints full (16)"); return; } + navmap::s_avg.start(secs, lat, lon); + navPollAveraging(); +} + +void UITask::navAveragingCancel() { + navmap::s_avg.cancel(); + if (_nav_avg_pill) lv_obj_add_flag(_nav_avg_pill, LV_OBJ_FLAG_HIDDEN); + showToast("Mark cancelled"); +} + +// From mapLoop(): sample, update the pill, mark the mean when the window closes. +void UITask::navPollAveraging() { + if (!navmap::s_avg.active()) return; + int32_t lat = 0, lon = 0, mlat, mlon; + bool fix = _core->course.currentLocation(lat, lon); + GpsAverager::Step st = navmap::s_avg.poll(fix, lat, lon, mlat, mlon); + if (_nav_avg_pill) { + if (st == GpsAverager::RUNNING) { + lv_label_set_text_fmt(_nav_avg_pill, UI_SYMBOL_PIN " Averaging GPS... %d s (%lu fixes) " LV_SYMBOL_CLOSE, + navmap::s_avg.remainingSecs(), (unsigned long)navmap::s_avg.samples()); + lv_obj_remove_flag(_nav_avg_pill, LV_OBJ_FLAG_HIDDEN); + } else { + lv_obj_add_flag(_nav_avg_pill, LV_OBJ_FLAG_HIDDEN); + } + } + if (st == GpsAverager::DONE) navAddWaypoint(mlat, mlon); + else if (st == GpsAverager::NO_FIX) showToast("No GPS fix"); +} + +// Long-press on the map: what to do with that spot (nothing is added until asked). +void UITask::navDropAt(int x, int y) { + int32_t lat, lon; + mapPointToLatLon(x, y, lat, lon); + navSpotPopup(lat, lon); +} + +// ── Share into a conversation ───────────────────────────────────────────────── + +// Pick a conversation; the text is waiting in its compose field. +void UITask::shareToMessage(const char* text) { + snprintf(_share_text, sizeof(_share_text), "%s", text); + showChats(); + showToast("Pick a conversation to share it in"); +} + +// ── Tools panel: trail recording, live share, map download ──────────────────── + +namespace navmap { +enum : uint8_t { TL_TRAIL_TOGGLE, TL_TRAIL_SAVE, TL_TRAIL_LOAD, TL_TRAIL_RESET, TL_TRAIL_GPX, TL_TRACKBACK, + TL_SHARE_TOGGLE, TL_SHARE_ONCE, TL_DOWNLOAD, + TL_WP_HERE, TL_WP_COORDS, TL_SPOT_ADD, TL_SPOT_GO }; + +// Live-share targets offered in the dropdown: channels, then favourite contacts. +static const int SHARE_TARGETS = MAX_GROUP_CHANNELS + 16; +static uint8_t s_share_kind[SHARE_TARGETS]; // 0 = channel, 1 = contact +static uint8_t s_share_ch[SHARE_TARGETS]; +static uint8_t s_share_key[SHARE_TARGETS][NodePrefs::FAVOURITE_PREFIX_LEN]; +static int s_share_n = 0; + +// Print sink over a FILE* for TrailStore's GPX writer (same duck type as +// ui-new's BoundedSerialPrint). +struct FilePrint { + FILE* f; + bool ok; + explicit FilePrint(FILE* file) : f(file), ok(true) {} + size_t write(const uint8_t* b, size_t n) { + if (!ok) return 0; + size_t w = fwrite(b, 1, n, f); + if (w != n) ok = false; + return w; + } + size_t print(const char* s) { return write((const uint8_t*)s, strlen(s)); } + size_t print(const __FlashStringHelper* s) { return print(reinterpret_cast(s)); } +}; + +} // namespace navmap + +static void onNavTools(lv_event_t* e) { (void)e; s_ui->navToolsPopup(); } +static void onNavTool(lv_event_t* e) { s_ui->navToolAction((uint8_t)(uintptr_t)lv_event_get_user_data(e)); } +static void onNavShareTarget(lv_event_t* e) { + s_ui->navSetShareTarget((int)lv_dropdown_get_selected((lv_obj_t*)lv_event_get_target(e))); +} + +static lv_obj_t* toolRow(lv_obj_t* parent) { + lv_obj_t* r = lv_obj_create(parent); + styleSurface(r, theme::BG); + lv_obj_remove_flag(r, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_size(r, LV_PCT(100), LV_SIZE_CONTENT); + lv_obj_set_flex_flow(r, LV_FLEX_FLOW_ROW); + lv_obj_set_style_pad_column(r, theme::GAP, 0); + return r; +} + +static lv_obj_t* toolButton(lv_obj_t* row, const char* text, uint8_t act, bool accent) { + lv_obj_t* b = lv_button_create(row); + lv_obj_set_height(b, 36); + lv_obj_set_flex_grow(b, 1); + lv_obj_set_style_pad_hor(b, 4, 0); + lv_obj_set_style_radius(b, theme::RADIUS, 0); + lv_obj_set_style_shadow_width(b, 0, 0); + lv_obj_set_style_bg_color(b, lv_color_hex(accent ? theme::ACCENT_DIM : theme::SURFACE), 0); + lv_obj_set_style_bg_color(b, lv_color_hex(theme::SURFACE_2), LV_STATE_PRESSED); + lv_obj_add_event_cb(b, onNavTool, LV_EVENT_CLICKED, (void*)(uintptr_t)act); + lv_obj_t* l = label(b, text, THEME_FONT_SMALL, theme::TEXT); + lv_obj_center(l); + return l; +} + +void UITask::navToolsPopup() { + lv_obj_t* panel = navPopupPanel("Trail & sharing", true); + lv_obj_t* list = lv_obj_create(panel); + styleSurface(list, theme::BG); + lv_obj_set_width(list, LV_PCT(100)); + lv_obj_set_flex_grow(list, 1); + lv_obj_set_flex_flow(list, LV_FLEX_FLOW_COLUMN); + lv_obj_set_style_pad_row(list, theme::GAP, 0); + lv_obj_set_scrollbar_mode(list, LV_SCROLLBAR_MODE_ACTIVE); + + sectionTitle(list, "TRAIL"); + _nav_trail_lbl = label(list, "", THEME_FONT_SMALL, theme::TEXT); + lv_obj_t* r = toolRow(list); + _nav_trail_btn = toolButton(r, "", navmap::TL_TRAIL_TOGGLE, true); + toolButton(r, LV_SYMBOL_SAVE " Save", navmap::TL_TRAIL_SAVE, false); + toolButton(r, LV_SYMBOL_DIRECTORY " Load", navmap::TL_TRAIL_LOAD, false); + r = toolRow(list); + _nav_tb_btn = toolButton(r, "", navmap::TL_TRACKBACK, false); + toolButton(r, LV_SYMBOL_SD_CARD " GPX", navmap::TL_TRAIL_GPX, false); + _nav_reset_lbl = toolButton(r, LV_SYMBOL_TRASH " Reset", navmap::TL_TRAIL_RESET, false); + + sectionTitle(list, "LIVE SHARE"); + _nav_share_lbl = label(list, "", THEME_FONT_SMALL, theme::TEXT); + lv_label_set_long_mode(_nav_share_lbl, LV_LABEL_LONG_WRAP); + lv_obj_set_width(_nav_share_lbl, LV_PCT(100)); + // Target: channels, then favourite contacts. + char opts[MAX_GROUP_CHANNELS * 24 + 16 * 36]; + int o = 0, sel = 0; + navmap::s_share_n = 0; + for (int i = 0; i < MAX_GROUP_CHANNELS && navmap::s_share_n < navmap::SHARE_TARGETS; i++) { + ChannelDetails ch; + if (!the_mesh.getChannel(i, ch) || !ch.name[0]) continue; + if (_prefs->loc_share_target_type == 0 && _prefs->loc_share_channel_idx == i) sel = navmap::s_share_n; + navmap::s_share_kind[navmap::s_share_n] = 0; + navmap::s_share_ch[navmap::s_share_n++] = (uint8_t)i; + o += snprintf(opts + o, sizeof(opts) - o, "%s# %s", o ? "\n" : "", ch.name); + } + int favs = 0; + for (int i = 0; i < the_mesh.getNumContacts() && favs < 16 && navmap::s_share_n < navmap::SHARE_TARGETS; i++) { + ContactInfo c; + if (!the_mesh.getContactByIdx(MAX_ANON_CONTACTS + i, c)) continue; + if (c.type != ADV_TYPE_CHAT || !(c.flags & 0x01)) continue; + if (_prefs->loc_share_target_type == 1 && + memcmp(_prefs->loc_share_dm_prefix, c.id.pub_key, NodePrefs::FAVOURITE_PREFIX_LEN) == 0) sel = navmap::s_share_n; + navmap::s_share_kind[navmap::s_share_n] = 1; + memcpy(navmap::s_share_key[navmap::s_share_n++], c.id.pub_key, NodePrefs::FAVOURITE_PREFIX_LEN); + o += snprintf(opts + o, sizeof(opts) - o, "%s%s", o ? "\n" : "", c.name); + favs++; + } + lv_obj_t* tr = lv_obj_create(list); + styleSurface(tr, theme::SURFACE); + lv_obj_remove_flag(tr, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_size(tr, LV_PCT(100), 40); + lv_obj_set_style_radius(tr, theme::RADIUS, 0); + lv_obj_align(label(tr, "Send to", THEME_FONT_BODY, theme::TEXT), LV_ALIGN_LEFT_MID, theme::PAD, 0); + lv_obj_t* dd = lv_dropdown_create(tr); + lv_dropdown_set_options(dd, o ? opts : "(no channels)"); + lv_dropdown_set_selected(dd, sel); + lv_obj_set_width(dd, 170); + lv_obj_align(dd, LV_ALIGN_RIGHT_MID, -4, 0); + lv_obj_add_event_cb(dd, onNavShareTarget, LV_EVENT_VALUE_CHANGED, NULL); + r = toolRow(list); + _nav_share_btn = toolButton(r, "", navmap::TL_SHARE_TOGGLE, true); + toolButton(r, LV_SYMBOL_UPLOAD " Send once", navmap::TL_SHARE_ONCE, false); + lv_obj_t* hint = label(list, "Timing and duration: Settings > Trail, live share, alerts.", THEME_FONT_SMALL, + theme::TEXT_MUTED); + lv_label_set_long_mode(hint, LV_LABEL_LONG_WRAP); + lv_obj_set_width(hint, LV_PCT(100)); + + sectionTitle(list, "MAP"); + r = toolRow(list); + toolButton(r, LV_SYMBOL_DOWNLOAD " Download this area", navmap::TL_DOWNLOAD, false); + + _nav_reset_armed_ms = 0; + refreshNavTools(); +} + +void UITask::refreshNavTools() { + if (!_nav_trail_lbl) return; + TrailStore& ts = _core->trail.store(); + bool imperial = _prefs && _prefs->units_imperial; + char dist[12], dur[12]; + geo::fmtDist(dist, sizeof(dist), ts.totalDistanceMeters() / 1000.0f, imperial); + navmap::fmtDuration(dur, sizeof(dur), ts.elapsedSeconds()); + const char* state = !ts.isActive() ? (ts.empty() ? "Not recording" : "Stopped") + : ts.isPaused() ? "Paused (standing still)" : "Recording"; + if (ts.empty()) lv_label_set_text(_nav_trail_lbl, state); + else lv_label_set_text_fmt(_nav_trail_lbl, "%s - %s, %s, %d points", state, dist, dur, ts.count()); + lv_label_set_text(_nav_trail_btn, ts.isActive() ? LV_SYMBOL_STOP " Stop" : LV_SYMBOL_PLAY " Record"); + lv_label_set_text(_nav_tb_btn, navmap::s_tb.active() ? LV_SYMBOL_STOP " Stop back" : LV_SYMBOL_LOOP " Track back"); + if (_nav_reset_armed_ms && millis() - _nav_reset_armed_ms > 3000) { + _nav_reset_armed_ms = 0; + lv_label_set_text(_nav_reset_lbl, LV_SYMBOL_TRASH " Reset"); + } + + if (_prefs->loc_share_enabled) { + char left[12]; + navmap::fmtDuration(left, sizeof(left), _core->live_share.remainingSecs()); + lv_label_set_text_fmt(_nav_share_lbl, "Sharing your position - %s left", left); + } else { + lv_label_set_text(_nav_share_lbl, "Off. Sends your position while you move, then stops by itself."); + } + lv_label_set_text(_nav_share_btn, _prefs->loc_share_enabled ? LV_SYMBOL_STOP " Stop sharing" : LV_SYMBOL_PLAY " Share live"); +} + +void UITask::navSetShareTarget(int sel) { + if (sel < 0 || sel >= navmap::s_share_n) return; + _prefs->loc_share_target_type = navmap::s_share_kind[sel]; + if (navmap::s_share_kind[sel] == 0) _prefs->loc_share_channel_idx = navmap::s_share_ch[sel]; + else memcpy(_prefs->loc_share_dm_prefix, navmap::s_share_key[sel], NodePrefs::FAVOURITE_PREFIX_LEN); + the_mesh.savePrefs(); +} + +// GPX of the live trail (with the waypoints) to /sdcard/trails/. +bool UITask::exportTrailGpx(char* name_out, size_t n) { + if (!lvport::mountStorage()) return false; + char path[64]; + uint32_t now = rtc_clock.getCurrentTime(); + if (now > 1000000000UL) { + time_t t = (time_t)((int64_t)now + (int64_t)_prefs->tz_offset_hours * 3600); + struct tm* ti = gmtime(&t); + snprintf(path, sizeof(path), "/sdcard/trails/trail-%04d%02d%02d-%02d%02d.gpx", ti->tm_year + 1900, + ti->tm_mon + 1, ti->tm_mday, ti->tm_hour, ti->tm_min); + } else { + snprintf(path, sizeof(path), "/sdcard/trails/trail-%lu.gpx", (unsigned long)(millis() / 1000)); + } + mapview::makeParents(path); + FILE* f = fopen(path, "w"); + if (!f) return false; + navmap::FilePrint out(f); + _core->trail.store().exportGpx(out, _core->waypoints.store()); + bool ok = out.ok; + ok = (fclose(f) == 0) && ok; + snprintf(name_out, n, "%s", strrchr(path, '/') + 1); + return ok; +} + +void UITask::navToolAction(uint8_t act) { + TrailEngine& tr = _core->trail; + switch (act) { + case navmap::TL_TRAIL_TOGGLE: + if (!tr.isActive()) { + tr.setActive(true); + int32_t lat, lon; + if (_core->course.currentLocation(lat, lon)) showToast("Recording the trail"); + else ensureGps(); // turns GPS on, or says it's waiting + } else { + tr.setActive(false); + showToast("Trail stopped"); + } + break; + case navmap::TL_TRAIL_SAVE: { + TrailEngine::FileResult r = tr.save(); + showToast(r == TrailEngine::FILE_OK ? "Trail saved" : "Save failed"); + break; + } + case navmap::TL_TRAIL_LOAD: { + TrailEngine::FileResult r = tr.load(); + showToast(r == TrailEngine::FILE_OK ? "Trail loaded" : r == TrailEngine::FILE_MISSING ? "No saved trail" : "Load failed"); + rebuildMapMarkers(); + layoutMap(); + break; + } + case navmap::TL_TRAIL_RESET: + if (!_nav_reset_armed_ms || millis() - _nav_reset_armed_ms > 3000) { // second tap within 3 s confirms + _nav_reset_armed_ms = millis() | 1; + lv_label_set_text(_nav_reset_lbl, "Reset?"); + return; + } + tr.reset(); + _nav_reset_armed_ms = 0; + showToast("Trail cleared"); + rebuildMapMarkers(); + layoutMap(); + break; + case navmap::TL_TRACKBACK: { + if (navmap::s_tb.active()) { navmap::s_tb.stop(); showToast("Track back stopped"); break; } + int32_t lat = 0, lon = 0; + bool fix = _core->course.currentLocation(lat, lon); + if (!navmap::s_tb.start(tr.store(), fix, lat, lon)) { showToast("No trail to walk back"); break; } + navmap::s_eta.reset(); + if (!fix) ensureGps(); + navClosePopup(); + navFrameTarget(); + refreshNavBar(); + return; + } + case navmap::TL_TRAIL_GPX: { + if (tr.store().empty()) { showToast("No trail to export"); break; } + char name[40], t[64]; + if (exportTrailGpx(name, sizeof(name))) { snprintf(t, sizeof(t), "Saved trails/%s", name); showToast(t, 3500); } + else showToast("Can't write to the SD card"); + break; + } + case navmap::TL_SHARE_TOGGLE: + if (_prefs->loc_share_enabled) { + _prefs->loc_share_enabled = 0; + showToast("Live share stopped"); + } else { + _prefs->loc_share_enabled = 1; + _core->live_share.restartSession(); + int32_t lat, lon; + if (_core->course.currentLocation(lat, lon)) showToast("Sharing your position"); + else ensureGps(); + } + the_mesh.savePrefs(); + break; + case navmap::TL_SHARE_ONCE: { + int32_t lat, lon; + if (!ensureGps() || !_core->course.currentLocation(lat, lon)) break; + // With a session on, straight to its target; otherwise pick a conversation. + if (_prefs->loc_share_enabled && _core->live_share.send(lat, lon)) { showToast("Position sent"); break; } + char text[40]; + snprintf(text, sizeof(text), LOCATION_MSG_TAG "%.5f,%.5f", lat / 1e6, lon / 1e6); + navClosePopup(); + shareToMessage(text); + return; + } + case navmap::TL_DOWNLOAD: + navClosePopup(); + mapDownloadPopup(); + return; + case navmap::TL_WP_HERE: + navClosePopup(); + navMarkHere(); + return; + case navmap::TL_WP_COORDS: + navCoordsPopup(); + return; + case navmap::TL_SPOT_ADD: + navClosePopup(); + navAddWaypoint(_nav_spot_lat, _nav_spot_lon); + return; + case navmap::TL_SPOT_GO: + navClosePopup(); + navSetTarget(0, nullptr, _nav_spot_lat, _nav_spot_lon, "Map point"); + navFrameTarget(); + refreshNavBar(); + return; + } + refreshNavTools(); + refreshNavBar(); +} + +// ── Waypoint list, add by coordinates, long-press spot ──────────────────────── + +static void onNavCoordsKb(lv_event_t* e) { s_ui->navCoordsDone(lv_event_get_code(e) == LV_EVENT_READY); } + +// The pin button: every waypoint (tap for its menu), plus the two ways to add +// one that aren't a long-press -- here (GPS, averaged per Settings) or typed. +void UITask::navWaypointsPopup() { + const WaypointModel& wp = _core->waypoints; + char title[32]; + snprintf(title, sizeof(title), "Waypoints %d/%d", wp.count(), WaypointStore::CAPACITY); + lv_obj_t* panel = navPopupPanel(title, true); + lv_obj_t* r = toolRow(panel); + toolButton(r, UI_SYMBOL_PIN " Here (GPS)", navmap::TL_WP_HERE, true); + toolButton(r, LV_SYMBOL_KEYBOARD " Coordinates", navmap::TL_WP_COORDS, false); + + lv_obj_t* list = lv_obj_create(panel); + styleSurface(list, theme::BG); + lv_obj_set_width(list, LV_PCT(100)); + lv_obj_set_flex_grow(list, 1); + lv_obj_set_flex_flow(list, LV_FLEX_FLOW_COLUMN); + lv_obj_set_style_pad_row(list, theme::GAP, 0); + lv_obj_set_scrollbar_mode(list, LV_SCROLLBAR_MODE_ACTIVE); + int32_t lat = 0, lon = 0; + bool gps = _core->course.currentLocation(lat, lon); + for (int i = 0; i < wp.count(); i++) { + const Waypoint& w = wp.at(i); + char t[WAYPOINT_LABEL_LEN + 8], sub[40]; + snprintf(t, sizeof(t), UI_SYMBOL_FLAG " %s", w.label[0] ? w.label : "(unnamed)"); + snprintf(sub, sizeof(sub), "%.5f, %.5f", w.lat_1e6 / 1e6, w.lon_1e6 / 1e6); + lv_obj_t* row = listRow(list, t, sub, onNavWpMenu, (void*)(uintptr_t)i); + if (gps) { + char d[12]; + geo::fmtDist(d, sizeof(d), geo::haversineKm(lat, lon, w.lat_1e6, w.lon_1e6), _prefs && _prefs->units_imperial); + navRowRight(row, d, theme::TEXT_MUTED, theme::PAD); + } + } + if (wp.count() == 0) { + lv_obj_t* l = label(list, "None yet. Add one here, type its coordinates, or hold the map on a spot.", + THEME_FONT_SMALL, theme::TEXT_MUTED); + lv_label_set_long_mode(l, LV_LABEL_LONG_WRAP); + lv_obj_set_width(l, LV_PCT(100)); + } +} + +// "50.06142, 19.93721 Name" (decimal degrees; comma or space between them; the +// name is optional) -> a new waypoint. +void UITask::navCoordsPopup() { + if (_core->waypoints.full()) { showToast("Waypoints full (16)"); return; } + lv_obj_t* panel = navPopupPanel("Add by coordinates", false); + lv_obj_align(panel, LV_ALIGN_TOP_MID, 0, theme::STATUS_H + 4); // above the keyboard + _nav_ta = lv_textarea_create(panel); + lv_textarea_set_one_line(_nav_ta, true); + lv_textarea_set_placeholder_text(_nav_ta, "50.06142, 19.93721 Name"); + lv_obj_set_size(_nav_ta, LV_PCT(100), 34); + lv_obj_set_style_border_width(_nav_ta, 1, 0); + lv_obj_set_style_pad_ver(_nav_ta, 6, 0); + lv_obj_set_style_pad_hor(_nav_ta, 10, 0); + lv_obj_set_scrollbar_mode(_nav_ta, LV_SCROLLBAR_MODE_OFF); + lv_obj_set_style_border_color(_nav_ta, lv_color_hex(theme::ACCENT), LV_PART_CURSOR | LV_STATE_FOCUSED); + lv_obj_set_style_border_width(_nav_ta, 2, LV_PART_CURSOR | LV_STATE_FOCUSED); + lv_obj_add_state(_nav_ta, LV_STATE_FOCUSED); // draws the cursor + + _nav_kb = kb::create(_nav_overlay, _prefs); + lv_obj_set_size(_nav_kb, LV_PCT(100), 124); + lv_obj_align(_nav_kb, LV_ALIGN_BOTTOM_MID, 0, 0); + lv_keyboard_set_textarea(_nav_kb, _nav_ta); + kb::apply(_nav_kb, kb::L_SYM); // digits first; "abc" gives letters for the name + lv_obj_add_event_cb(_nav_kb, onNavCoordsKb, LV_EVENT_READY, NULL); + lv_obj_add_event_cb(_nav_kb, onNavCoordsKb, LV_EVENT_CANCEL, NULL); +} + +void UITask::navCoordsDone(bool ok) { + if (!ok || !_nav_ta) { navWaypointsPopup(); return; } + // parseLatLon wants "lat,lon"; accept a space too, and read what follows as + // the name the way a [WAY] share carries it. + char buf[80] = WAYPOINT_MSG_TAG; + size_t o = strlen(buf); + const char* in = lv_textarea_get_text(_nav_ta); + while (*in == ' ') in++; + bool sep = strchr(in, ',') != nullptr; + for (; *in && o < sizeof(buf) - 1; in++) { + if (!sep && *in == ' ') { buf[o++] = ','; sep = true; while (in[1] == ' ') in++; continue; } + buf[o++] = *in; + } + buf[o] = '\0'; + int32_t lat, lon; + char name[WAYPOINT_LABEL_LEN * 2]; + if (!geo::parseLatLon(buf, lat, lon, name, sizeof(name))) { + showToast("Use decimal degrees, e.g. 50.06142, 19.93721"); + return; // keep the field for a fix + } + navClosePopup(); + if (!_core->waypoints.add(lat, lon, rtc_clock.getCurrentTime(), name)) { showToast("Waypoints full (16)"); return; } + const Waypoint& w = _core->waypoints.at(_core->waypoints.count() - 1); + char t[40]; + snprintf(t, sizeof(t), "Saved %s", w.label); + showToast(t); + // Show where it is. + double n = (double)(1 << _map_z); + _map_follow = false; + _map_cx = navmap::normX(lon) * n; + _map_cy = navmap::normY(lat) * n + mapCenterBias(); + rebuildMapMarkers(); + layoutMap(); +} + +void UITask::navSpotPopup(int32_t lat, int32_t lon) { + _nav_spot_lat = lat; + _nav_spot_lon = lon; + lv_obj_t* panel = navPopupPanel("This spot", false); + char info[64]; + int o = snprintf(info, sizeof(info), "%.5f, %.5f", lat / 1e6, lon / 1e6); + int32_t mlat, mlon; + if (_core->course.currentLocation(mlat, mlon)) { + char d[12]; + geo::fmtDist(d, sizeof(d), geo::haversineKm(mlat, mlon, lat, lon), _prefs && _prefs->units_imperial); + int az = geo::bearingDeg(mlat, mlon, lat, lon); + snprintf(info + o, sizeof(info) - o, "\n%s %d\xC2\xB0 %s", d, az, geo::bearingCardinal(az)); + } + label(panel, info, THEME_FONT_BODY, theme::TEXT); + lv_obj_t* r = toolRow(panel); + toolButton(r, UI_SYMBOL_FLAG " Add waypoint", navmap::TL_SPOT_ADD, true); + toolButton(r, UI_SYMBOL_COMPASS " Go here", navmap::TL_SPOT_GO, false); +} diff --git a/examples/companion_radio/ui-lvgl/RadioScreen.h b/examples/companion_radio/ui-lvgl/RadioScreen.h new file mode 100644 index 00000000..4c18d137 --- /dev/null +++ b/examples/companion_radio/ui-lvgl/RadioScreen.h @@ -0,0 +1,220 @@ +#pragma once +// Settings > Radio: preset, frequency, SF / bandwidth / coding rate, TX power +// and Adaptive Power Control -- ui-new's Settings > Radio. Every change is +// applied to the radio at once and saved (ui-core/RadioControl.h does the +// applying for both frontends). +// +// Single-TU fragment: included by ui-lvgl/UITask.cpp after ClockScreen.h. + +namespace radioview { + +enum : uint8_t { R_PRESET, R_SF, R_BW, R_CR, R_TX, R_APC }; +static const int TX_MIN = 2; +#ifdef LORA_TX_POWER +static const int TX_MAX = LORA_TX_POWER; +#else +static const int TX_MAX = 22; +#endif + +static lv_obj_t* s_overlay = nullptr; // frequency entry +static lv_obj_t* s_ta = nullptr; +static char s_opts[1024]; + +// Label + optional hint on the left of a settings row; the control goes on the right. +static lv_obj_t* settingRow(lv_obj_t* parent, const char* text, const char* hint) { + lv_obj_t* row = lv_obj_create(parent); + styleSurface(row, theme::SURFACE); + lv_obj_remove_flag(row, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_size(row, LV_PCT(100), theme::ROW_H); + lv_obj_set_style_radius(row, theme::RADIUS, 0); + lv_obj_align(label(row, text, THEME_FONT_BODY, theme::TEXT), LV_ALIGN_TOP_LEFT, theme::PAD, hint ? 5 : 13); + if (hint) { + lv_obj_t* h = label(row, hint, THEME_FONT_SMALL, theme::TEXT_MUTED); + lv_label_set_long_mode(h, LV_LABEL_LONG_DOT); + lv_obj_set_size(h, 150, 15); + lv_obj_align(h, LV_ALIGN_BOTTOM_LEFT, theme::PAD, -5); + } + return row; +} + +static lv_obj_t* rowDropdown(lv_obj_t* row, const char* opts, int sel, int width, lv_event_cb_t cb, uintptr_t which) { + lv_obj_t* dd = lv_dropdown_create(row); + lv_dropdown_set_options(dd, opts); + if (sel >= 0) lv_dropdown_set_selected(dd, sel); + lv_obj_set_width(dd, width); + lv_obj_align(dd, LV_ALIGN_RIGHT_MID, -4, 0); + lv_obj_add_event_cb(dd, cb, LV_EVENT_VALUE_CHANGED, (void*)which); + return dd; +} + +} // namespace radioview + +static void onRadioDropdown(lv_event_t* e) { + s_ui->radioSet((int)(uintptr_t)lv_event_get_user_data(e), (int)lv_dropdown_get_selected((lv_obj_t*)lv_event_get_target(e))); +} +static void onRadioSwitch(lv_event_t* e) { + s_ui->radioSet((int)(uintptr_t)lv_event_get_user_data(e), + lv_obj_has_state((lv_obj_t*)lv_event_get_target(e), LV_STATE_CHECKED) ? 1 : 0); +} +static void onRadioFreq(lv_event_t* e) { (void)e; s_ui->radioFreqPopup(); } +static void onRadioFreqKb(lv_event_t* e) { s_ui->radioFreqDone(lv_event_get_code(e) == LV_EVENT_READY); } +static void onOpenRadio(lv_event_t* e) { (void)e; s_ui->showRadio(); } + +void UITask::showRadio() { + _screen = SCR_RADIO; + buildRadio(); +} + +void UITask::buildRadio() { + using namespace radioview; + lv_obj_t* body = newScreen("Radio", true); + s_overlay = s_ta = nullptr; + NodePrefs* p = _prefs; + + // Preset: "Custom" first (current params match none), then the list. + int o = snprintf(s_opts, sizeof(s_opts), "Custom"); + const char* name; float f, b; uint8_t sf, cr; + for (int i = 0; radioctl::presetAt(p, i, name, f, b, sf, cr) && o < (int)sizeof(s_opts) - 24; i++) + o += snprintf(s_opts + o, sizeof(s_opts) - o, "\n%s", name); + lv_obj_t* row = settingRow(body, "Preset", nullptr); + lv_obj_t* dd = rowDropdown(row, s_opts, radioctl::currentPreset(p) + 1, 200, onRadioDropdown, R_PRESET); + lv_dropdown_set_dir(dd, LV_DIR_BOTTOM); + + // Frequency: tap to type it. + row = settingRow(body, "Frequency", "MHz"); + lv_obj_t* fb = lv_button_create(row); + lv_obj_set_size(fb, 120, 34); + lv_obj_align(fb, LV_ALIGN_RIGHT_MID, -4, 0); + lv_obj_set_style_shadow_width(fb, 0, 0); + lv_obj_set_style_radius(fb, theme::RADIUS, 0); + lv_obj_set_style_bg_color(fb, lv_color_hex(theme::SURFACE_2), 0); + lv_obj_add_event_cb(fb, onRadioFreq, LV_EVENT_CLICKED, NULL); + char fs[16]; + snprintf(fs, sizeof(fs), "%.3f", p->freq); + lv_obj_center(label(fb, fs, THEME_FONT_BODY, theme::TEXT)); + + o = 0; + for (int v = 5; v <= 12; v++) o += snprintf(s_opts + o, sizeof(s_opts) - o, v > 5 ? "\n%d" : "%d", v); + rowDropdown(settingRow(body, "Spreading factor", "Higher = longer range"), s_opts, + p->sf >= 5 && p->sf <= 12 ? p->sf - 5 : 0, 90, onRadioDropdown, R_SF); + o = 0; + for (int i = 0; i < LORA_BW_OPT_COUNT; i++) + o += snprintf(s_opts + o, sizeof(s_opts) - o, "%s%g kHz", i ? "\n" : "", (double)LORA_BW_OPTS[i]); + rowDropdown(settingRow(body, "Bandwidth", nullptr), s_opts, nearestBwIndex(p->bw), 130, onRadioDropdown, R_BW); + o = 0; + for (int v = 5; v <= 8; v++) o += snprintf(s_opts + o, sizeof(s_opts) - o, v > 5 ? "\n4/%d" : "4/%d", v); + rowDropdown(settingRow(body, "Coding rate", nullptr), s_opts, p->cr >= 5 && p->cr <= 8 ? p->cr - 5 : 0, 90, + onRadioDropdown, R_CR); + + sectionTitle(body, "TRANSMIT"); + o = 0; + for (int v = TX_MIN; v <= TX_MAX; v++) o += snprintf(s_opts + o, sizeof(s_opts) - o, v > TX_MIN ? "\n%d dBm" : "%d dBm", v); + int tx = p->tx_power_dbm < TX_MIN ? TX_MIN : p->tx_power_dbm > TX_MAX ? TX_MAX : p->tx_power_dbm; + rowDropdown(settingRow(body, "TX power", p->tx_apc ? "Ceiling for auto power" : nullptr), s_opts, tx - TX_MIN, 110, + onRadioDropdown, R_TX); + row = settingRow(body, "Auto power", p->client_repeat ? "Off while repeating" : "Lowers power on good links"); + lv_obj_t* sw = lv_switch_create(row); + lv_obj_set_size(sw, 46, 24); + lv_obj_align(sw, LV_ALIGN_RIGHT_MID, -theme::PAD, 0); + if (p->tx_apc) lv_obj_add_state(sw, LV_STATE_CHECKED); + if (p->client_repeat) lv_obj_add_state(sw, LV_STATE_DISABLED); + lv_obj_add_event_cb(sw, onRadioSwitch, LV_EVENT_VALUE_CHANGED, (void*)(uintptr_t)R_APC); + + lv_obj_t* note = label(body, "Everyone you talk to must use the same frequency, SF, bandwidth and coding rate.", + THEME_FONT_SMALL, theme::TEXT_MUTED); + lv_label_set_long_mode(note, LV_LABEL_LONG_WRAP); + lv_obj_set_width(note, LV_PCT(100)); +} + +void UITask::radioSet(int which, int v) { + using namespace radioview; + NodePrefs* p = _prefs; + if (!p) return; + switch (which) { + case R_PRESET: + if (v == 0) return; // "Custom": nothing to take + radioctl::choosePreset(p, v - 1); + break; + case R_SF: p->sf = (uint8_t)(5 + v); radioctl::applyParams(); break; + case R_BW: if (v >= 0 && v < LORA_BW_OPT_COUNT) { p->bw = LORA_BW_OPTS[v]; radioctl::applyParams(); } break; + case R_CR: p->cr = (uint8_t)(5 + v); radioctl::applyParams(); break; + case R_TX: p->tx_power_dbm = (int8_t)(TX_MIN + v); radioctl::applyTxPower(p); break; + case R_APC: p->tx_apc = (uint8_t)v; radioctl::applyApc(); break; + } + the_mesh.savePrefs(); + lv_obj_t* body = _body; + int32_t y = body ? lv_obj_get_scroll_y(body) : 0; + buildRadio(); // preset name / hints follow + if (_body) { lv_obj_update_layout(_body); lv_obj_scroll_to_y(_body, y, LV_ANIM_OFF); } +} + +void UITask::radioFreqPopup() { + using namespace radioview; + if (s_overlay) return; + s_overlay = lv_obj_create(lv_screen_active()); + lv_obj_remove_style_all(s_overlay); + lv_obj_set_size(s_overlay, LV_PCT(100), LV_PCT(100)); + lv_obj_set_style_bg_color(s_overlay, lv_color_hex(0x000000), 0); + lv_obj_set_style_bg_opa(s_overlay, LV_OPA_60, 0); + lv_obj_add_flag(s_overlay, LV_OBJ_FLAG_CLICKABLE); + lv_obj_t* panel = lv_obj_create(s_overlay); + lv_obj_set_size(panel, lv_display_get_horizontal_resolution(NULL) - 16, LV_SIZE_CONTENT); + lv_obj_align(panel, LV_ALIGN_TOP_MID, 0, theme::STATUS_H + 4); + lv_obj_set_style_bg_color(panel, lv_color_hex(theme::BG), 0); + lv_obj_set_style_border_color(panel, lv_color_hex(theme::ACCENT), 0); + lv_obj_set_style_border_width(panel, 1, 0); + lv_obj_set_style_radius(panel, theme::RADIUS, 0); + lv_obj_set_style_pad_all(panel, theme::PAD, 0); + lv_obj_set_style_pad_row(panel, 6, 0); + lv_obj_set_flex_flow(panel, LV_FLEX_FLOW_COLUMN); + lv_obj_remove_flag(panel, LV_OBJ_FLAG_SCROLLABLE); + float lo, hi; + radio_driver.getFreqBounds(lo, hi); + char t[48]; + snprintf(t, sizeof(t), "Frequency, MHz (%.0f - %.0f)", lo, hi); + label(panel, t, THEME_FONT_BODY, theme::TEXT); + s_ta = lv_textarea_create(panel); + lv_textarea_set_one_line(s_ta, true); + lv_textarea_set_accepted_chars(s_ta, "0123456789."); + lv_textarea_set_max_length(s_ta, 10); + char fs[16]; + snprintf(fs, sizeof(fs), "%.3f", _prefs->freq); + lv_textarea_set_text(s_ta, fs); + lv_obj_set_size(s_ta, LV_PCT(100), 34); + lv_obj_set_style_border_width(s_ta, 1, 0); + lv_obj_set_style_pad_ver(s_ta, 6, 0); + lv_obj_set_style_pad_hor(s_ta, 10, 0); + lv_obj_set_scrollbar_mode(s_ta, LV_SCROLLBAR_MODE_OFF); + lv_obj_set_style_border_color(s_ta, lv_color_hex(theme::ACCENT), LV_PART_CURSOR | LV_STATE_FOCUSED); + lv_obj_set_style_border_width(s_ta, 2, LV_PART_CURSOR | LV_STATE_FOCUSED); + lv_obj_add_state(s_ta, LV_STATE_FOCUSED); + lv_obj_t* kbd = kb::create(s_overlay, _prefs); + lv_obj_set_size(kbd, LV_PCT(100), 124); + lv_obj_align(kbd, LV_ALIGN_BOTTOM_MID, 0, 0); + lv_keyboard_set_textarea(kbd, s_ta); + kb::apply(kbd, kb::L_SYM); + lv_obj_add_event_cb(kbd, onRadioFreqKb, LV_EVENT_READY, NULL); + lv_obj_add_event_cb(kbd, onRadioFreqKb, LV_EVENT_CANCEL, NULL); +} + +void UITask::radioFreqDone(bool ok) { + using namespace radioview; + if (ok && s_ta) { + float lo, hi; + radio_driver.getFreqBounds(lo, hi); + float f = strtof(lv_textarea_get_text(s_ta), nullptr); + if (f < lo || f > hi) { showToast("Out of the radio's range"); return; } + _prefs->freq = f; + radioctl::applyParams(); + the_mesh.savePrefs(); + } + radioCloseFreq(); + if (ok) buildRadio(); +} + +void UITask::radioCloseFreq() { + if (radioview::s_overlay) lv_obj_delete_async(radioview::s_overlay); + radioview::s_overlay = radioview::s_ta = nullptr; +} + +bool UITask::radioPopupOpen() const { return radioview::s_overlay != nullptr; } diff --git a/examples/companion_radio/ui-lvgl/Theme.h b/examples/companion_radio/ui-lvgl/Theme.h index b611339d..a3d07de9 100644 --- a/examples/companion_radio/ui-lvgl/Theme.h +++ b/examples/companion_radio/ui-lvgl/Theme.h @@ -22,7 +22,7 @@ namespace theme { static const int ROW_H = 44; // list row (touch target) // Type -- Noto Sans, generated by fonts/generate.sh (declared in lv_conf.h). - // ui_font_40 holds digits / ':' / '-' only (clock). + // ui_font_40 holds digits / ':' / '-' / '.' only (clock, timer, stopwatch). #define THEME_FONT_SMALL (&ui_font_12) #define THEME_FONT_BODY (&ui_font_14) #define THEME_FONT_TITLE (&ui_font_16) @@ -33,4 +33,10 @@ namespace theme { #define UI_SYMBOL_STAR "\xEF\x80\x85" // U+F005, favourite #define UI_SYMBOL_MAP "\xEF\x89\xB9" // U+F279 #define UI_SYMBOL_USERS "\xEF\x83\x80" // U+F0C0, Nearby + #define UI_SYMBOL_COMPASS "\xEF\x85\x8E" // U+F14E, Navigate + #define UI_SYMBOL_FLAG "\xEF\x80\xA4" // U+F024, waypoint + #define UI_SYMBOL_PIN "\xEF\x8F\x85" // U+F3C5, mark here + #define UI_SYMBOL_CLOCK "\xEF\x80\x97" // U+F017, Clock tools + #define UI_SYMBOL_STOPWATCH "\xEF\x8B\xB2" // U+F2F2 + #define UI_SYMBOL_RADIO "\xEF\x94\x99" // U+F519, broadcast tower } diff --git a/examples/companion_radio/ui-lvgl/UITask.cpp b/examples/companion_radio/ui-lvgl/UITask.cpp index 8e10f01b..6b472008 100644 --- a/examples/companion_radio/ui-lvgl/UITask.cpp +++ b/examples/companion_radio/ui-lvgl/UITask.cpp @@ -8,6 +8,11 @@ #include "../ui-core/UiCore.h" // shared UI Core (header-only, this TU) #include "../ui-core/NearbyModel.h" +#include "../ui-core/EtaTracker.h" +#include "../ui-core/SettingsSchema.h" +#include "../ui-core/GpsAverager.h" +#include "../ui-core/TrackBack.h" +#include "../ui-core/RadioControl.h" #include "Theme.h" #include "LvglPort.h" #include "../ui-core/KeyboardData.h" @@ -109,6 +114,7 @@ void UITask::begin(DisplayDriver* display_drv, SensorManager* sensors, NodePrefs lv_display_set_theme(lv_display_get_default(), th); buildStatusBar(); + applyDisplayPrefs(); // a slider percentage overrides the level main.cpp set showHome(); } @@ -126,7 +132,8 @@ void UITask::loop() { s_sim_btn_click = false; #endif if (btn_click) { - if (_asleep) wake(); + if (_core->clock.isRinging()) dismissRing(); + else if (_asleep) wake(); else if (_screen == SCR_HOME) sleep(); else back(); } @@ -135,12 +142,17 @@ void UITask::loop() { if (lvport::touched()) { lvport::swallowTouch(); wake(); } } else { uint32_t aoff = autoOffMillis(); - if (aoff > 0 && lv_display_get_inactive_time(NULL) > aoff) sleep(); + if (aoff > 0 && lv_display_get_inactive_time(NULL) > aoff && !_core->clock.isRinging()) sleep(); } _core->loop(); drainCoreEvents(); + checkLowBattery(); mapDownloadTick(); // a map download keeps running on any screen, and asleep + if ((int32_t)(millis() - _next_trackback_ms) >= 0) { // walking the trail back, on any screen + _next_trackback_ms = millis() + 1000; + navPollTrackBack(); + } if (!_asleep) { if ((int32_t)(millis() - _next_status_ms) >= 0) { @@ -148,6 +160,7 @@ void UITask::loop() { refreshStatusBar(); if (_screen == SCR_HOME) refreshHome(); } + if (_screen == SCR_HOME) homeSwipePoll(); if ((_screen == SCR_NEARBY || _screen == SCR_NODE) && (int32_t)(millis() - _next_nearby_ms) >= 0) { // Scan results trickle in over a few seconds: poll fast while scanning. _next_nearby_ms = millis() + (_scanning ? 250 : 2000); @@ -161,6 +174,10 @@ void UITask::loop() { } if (_screen == SCR_MAP) mapLoop(); if (_screen == SCR_WIFI) pollWifiScan(); + if (_screen == SCR_CLOCK && (int32_t)(millis() - _next_clock_ms) >= 0) { + _next_clock_ms = millis() + 100; // stopwatch tenths + refreshClock(); + } if (_screen == SCR_THREAD && (int32_t)(millis() - _next_thread_check_ms) >= 0) { _next_thread_check_ms = millis() + 500; uint32_t sig = threadSignature(); @@ -188,6 +205,22 @@ void UITask::shutdown(bool restart) { } } +// Settings > Display & power > Battery shutdown (NodePrefs::low_batt_mv), as +// ui-new does it: a smoothed reading every 8 s, never while on USB power. +void UITask::checkLowBattery() { + if ((int32_t)(millis() - _next_batt_ms) < 0) return; + _next_batt_ms = millis() + 8000; + uint16_t raw = getBattMilliVolts(); + if (raw > 0) _batt_mv = _batt_mv == 0 ? raw : (uint16_t)((_batt_mv * 4u + raw) / 5u); // EMA, alpha 0.2 + uint16_t low = _prefs ? _prefs->low_batt_mv : 0; + if (low == 0 || _batt_mv == 0 || _batt_mv >= low || _board->isExternalPowered()) return; + wake(); + showToast("Low battery - shutting down", 3000); + lv_timer_handler(); + delay(2000); + shutdown(); +} + uint32_t UITask::autoOffMillis() const { if (!_prefs || _prefs->auto_off_secs == 0) return 0; return (uint32_t)_prefs->auto_off_secs * 1000UL; @@ -220,10 +253,10 @@ void UITask::drainCoreEvents() { onMessageArrived(ev); break; case UiEventType::ClockAlert: - wake(); - showToast(ev.text, ClockEngine::RING_MS); + showRing(ev.text); break; case UiEventType::ClockRingEnded: + hideRing(); break; case UiEventType::LiveShareEnded: showToast("Live share ended"); @@ -272,6 +305,7 @@ void UITask::buildStatusBar() { lv_obj_align(_status_time, LV_ALIGN_LEFT_MID, 0, 0); _status_icons = label(bar, "", THEME_FONT_SMALL, theme::TEXT_MUTED); lv_obj_align(_status_icons, LV_ALIGN_RIGHT_MID, 0, 0); + _status_gps = label(bar, LV_SYMBOL_GPS, THEME_FONT_SMALL, theme::TEXT_MUTED); _toast = lv_obj_create(lv_layer_top()); lv_obj_remove_flag(_toast, LV_OBJ_FLAG_SCROLLABLE); @@ -284,7 +318,10 @@ void UITask::buildStatusBar() { lv_obj_set_style_pad_hor(_toast, 12, 0); lv_obj_set_style_pad_ver(_toast, 6, 0); lv_obj_align(_toast, LV_ALIGN_BOTTOM_MID, 0, -12); - label(_toast, "", THEME_FONT_BODY, theme::TEXT); + lv_obj_t* tl = label(_toast, "", THEME_FONT_BODY, theme::TEXT); + lv_label_set_long_mode(tl, LV_LABEL_LONG_WRAP); // long texts wrap instead of running off the screen + lv_obj_set_style_max_width(tl, lv_display_get_horizontal_resolution(NULL) - 40, 0); + lv_obj_set_style_text_align(tl, LV_TEXT_ALIGN_CENTER, 0); lv_obj_add_flag(_toast, LV_OBJ_FLAG_HIDDEN); refreshStatusBar(); @@ -305,15 +342,33 @@ void UITask::refreshStatusBar() { int pct = mv <= 3300 ? 0 : mv >= 4200 ? 100 : (int)(mv - 3300) * 100 / 900; const char* batt = pct > 80 ? LV_SYMBOL_BATTERY_FULL : pct > 55 ? LV_SYMBOL_BATTERY_3 : pct > 30 ? LV_SYMBOL_BATTERY_2 : pct > 10 ? LV_SYMBOL_BATTERY_1 : LV_SYMBOL_BATTERY_EMPTY; - bool gps = false; - if (_sensors) { - LocationProvider* loc = _sensors->getLocationProvider(); - gps = loc && loc->isValid(); + lv_label_set_text_fmt(_status_icons, "%s%s %d%%", hasConnection() ? LV_SYMBOL_BLUETOOTH " " : "", batt, pct); + // GPS: green with a fix, muted while on and searching, absent when off. + int32_t lat, lon; + bool fix = _core->course.currentLocation(lat, lon); + if (_core->gpsEnabled() || fix) { + lv_obj_set_style_text_color(_status_gps, lv_color_hex(fix ? theme::OK : theme::TEXT_MUTED), 0); + lv_obj_remove_flag(_status_gps, LV_OBJ_FLAG_HIDDEN); + lv_obj_align_to(_status_gps, _status_icons, LV_ALIGN_OUT_LEFT_MID, -8, 0); + } else { + lv_obj_add_flag(_status_gps, LV_OBJ_FLAG_HIDDEN); } - lv_label_set_text_fmt(_status_icons, "%s%s%s %d%%", - hasConnection() ? LV_SYMBOL_BLUETOOTH " " : "", - gps ? LV_SYMBOL_GPS " " : "", - batt, pct); +} + +void UITask::setGps(bool on) { + if (!_core->setGpsEnabled(on)) { showToast("No GPS on this device"); return; } + showToast(on ? "GPS on, waiting for a fix" : "GPS off"); + refreshStatusBar(); +} + +void UITask::botSetGPS(bool on) { if (_core->setGpsEnabled(on)) refreshStatusBar(); } + +bool UITask::ensureGps() { + int32_t lat, lon; + if (_core->course.currentLocation(lat, lon)) return true; + if (_core->gpsAvailable() && !_core->gpsEnabled()) setGps(true); + else showToast("Waiting for a GPS fix"); + return false; } // One persistent timer, paused between toasts: hides the toast when it fires. @@ -347,7 +402,10 @@ lv_obj_t* UITask::newScreen(const char* title, bool with_back) { _node_info = _node_ping = _node_delete_lbl = nullptr; _scan_overlay = _scan_list = _scan_status = nullptr; // the popup went with the old screen _map_area = _map_marks = _map_me = _map_zoom_lbl = _map_hint = _map_dl_pill = nullptr; - _dl_overlay = _dl_info = _dl_zoom_lbl = _dl_start_lbl = nullptr; + _dl_overlay = _dl_info = _dl_zoom_lbl = _dl_start_lbl = _dl_job_row = _dl_job_lbl = nullptr; + _nav_bar = _nav_title = _nav_info = _nav_clear = nullptr; + _nav_overlay = _nav_ta = _nav_kb = _nav_del_lbl = _nav_rec = _nav_avg_pill = nullptr; + _nav_trail_lbl = _nav_trail_btn = _nav_reset_lbl = _nav_share_lbl = _nav_share_btn = _nav_tb_btn = nullptr; _wifi_ssid = _wifi_pass = _wifi_kb = _wifi_list = _wifi_status = nullptr; _wifi_scanning = false; for (lv_obj_t*& t : _map_tiles) t = nullptr; @@ -398,19 +456,24 @@ void UITask::back() { case SCR_THREAD: showChats(); break; case SCR_CONTACTS: showChats(); break; case SCR_SETTINGS: showHome(); break; + case SCR_SETTINGS_NAV: showSettings(); break; + case SCR_CLOCK: showHome(); break; + case SCR_RADIO: if (radioPopupOpen()) radioCloseFreq(); else showSettings(); break; case SCR_CHATS: showHome(); break; case SCR_NEARBY: if (_scan_overlay) closeScanPopup(); else showHome(); break; case SCR_NODE: // back to where the node was picked: map, the list, or the scan popup over it - if (_node_from_map) { showMap(); break; } + if (_node_from_map) { openMap(false); break; } _screen = SCR_NEARBY; buildNearby(); if (_node_from_scan) showScanPopup(); break; case SCR_MAP: - if (_dl_overlay) mapDownloadClose(); + if (_nav_overlay) navClosePopup(); + else if (_dl_overlay) mapDownloadClose(); + else if (!_map_nav) showNearby(); // the Nodes map opens from Nearby else showHome(); break; case SCR_WIFI: if (_wifi_from_map) showMap(); else showSettings(); break; @@ -424,9 +487,40 @@ static void onOpenChats(lv_event_t* e) { (void)e; s_ui->showChats(); } static void onOpenSettings(lv_event_t* e) { (void)e; s_ui->showSettings(); } static void onOpenNearby(lv_event_t* e) { (void)e; s_ui->showNearby(); } -static void onOpenMap(lv_event_t* e) { (void)e; s_ui->showMap(); } +static void onOpenNodesMap(lv_event_t* e) { (void)e; s_ui->openMap(false); } +static void onOpenNav(lv_event_t* e) { (void)e; s_ui->openMap(true); } +static void onOpenClock(lv_event_t* e); // ClockScreen.h -// Home tile: icon over label, one of a row of three; optional amber value +// Home apps, in order; PER_PAGE to a page, further pages are swiped to (with +// dots underneath), so new apps don't squeeze the row. +namespace home { +struct App { const char* icon; const char* text; lv_event_cb_t cb; bool unread; }; +static const App APPS[] = { + { LV_SYMBOL_ENVELOPE, "Messages", onOpenChats, true }, + { UI_SYMBOL_USERS, "Nearby", onOpenNearby, false }, + { UI_SYMBOL_MAP, "Map", onOpenNav, false }, + { LV_SYMBOL_SETTINGS, "Settings", onOpenSettings, false }, + // Page 2: tools + { UI_SYMBOL_CLOCK, "Clock", onOpenClock, false }, +}; +static const int COUNT = sizeof(APPS) / sizeof(APPS[0]); +static const int PER_PAGE = 4; +static const int PAGES = (COUNT + PER_PAGE - 1) / PER_PAGE; +static lv_obj_t* s_row = nullptr; // the current page's tiles +static lv_obj_t* s_dots = nullptr; +static int s_page = 0; // kept across rebuilds (the home screen is rebuilt on return) + +// A horizontal swipe anywhere on Home turns the page (the tiles row alone is +// too small a target), tracked from the touch itself: LVGL's gesture detector +// drops a slow swipe (its sum resets on every read without movement). Tapping +// a dot also goes to that page. +static bool s_touching = false, s_swiped = false; +static lv_point_t s_start; +static const int SWIPE_PX = 40; +static void onDot(lv_event_t* e) { s_ui->setHomePage((int)(uintptr_t)lv_event_get_user_data(e)); } +} // namespace home + +// Home tile: icon over label, one of a page's row; optional amber value // (unread count) in its top-right corner. static lv_obj_t* homeTile(lv_obj_t* parent, const char* icon, const char* text, lv_event_cb_t cb, lv_obj_t** value_out) { @@ -450,6 +544,7 @@ static lv_obj_t* homeTile(lv_obj_t* parent, const char* icon, const char* text, void UITask::showHome() { _screen = SCR_HOME; + _share_text[0] = '\0'; // a share not sent to anyone buildHome(); refreshHome(); } @@ -463,16 +558,72 @@ void UITask::buildHome() { lv_obj_t* name = label(body, the_mesh.getNodeName(), THEME_FONT_BODY, theme::ACCENT); lv_obj_set_style_pad_bottom(name, 4, 0); - lv_obj_t* tiles = lv_obj_create(body); - styleSurface(tiles, theme::BG); - lv_obj_remove_flag(tiles, LV_OBJ_FLAG_SCROLLABLE); - lv_obj_set_size(tiles, LV_PCT(100), LV_SIZE_CONTENT); - lv_obj_set_flex_flow(tiles, LV_FLEX_FLOW_ROW); - lv_obj_set_style_pad_column(tiles, theme::GAP, 0); - homeTile(tiles, LV_SYMBOL_ENVELOPE, "Messages", onOpenChats, &_home_unread); - homeTile(tiles, UI_SYMBOL_USERS, "Nearby", onOpenNearby, NULL); - homeTile(tiles, UI_SYMBOL_MAP, "Map", onOpenMap, NULL); - homeTile(tiles, LV_SYMBOL_SETTINGS, "Settings", onOpenSettings, NULL); + lv_obj_remove_flag(body, LV_OBJ_FLAG_SCROLLABLE); // a drag is a page swipe, not a scroll + home::s_row = lv_obj_create(body); + styleSurface(home::s_row, theme::BG); + lv_obj_remove_flag(home::s_row, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_size(home::s_row, LV_PCT(100), LV_SIZE_CONTENT); + lv_obj_set_flex_flow(home::s_row, LV_FLEX_FLOW_ROW); + lv_obj_set_style_pad_column(home::s_row, theme::GAP, 0); + home::s_dots = nullptr; + if (home::PAGES > 1) { + home::s_dots = lv_obj_create(body); + lv_obj_remove_style_all(home::s_dots); + lv_obj_set_size(home::s_dots, LV_SIZE_CONTENT, 22); + lv_obj_set_flex_flow(home::s_dots, LV_FLEX_FLOW_ROW); + lv_obj_set_flex_align(home::s_dots, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); + lv_obj_set_style_pad_column(home::s_dots, 12, 0); + for (int p = 0; p < home::PAGES; p++) { + lv_obj_t* d = lv_obj_create(home::s_dots); + lv_obj_remove_style_all(d); + lv_obj_set_size(d, 8, 8); + lv_obj_set_style_radius(d, LV_RADIUS_CIRCLE, 0); + lv_obj_set_style_bg_opa(d, LV_OPA_COVER, 0); + lv_obj_set_ext_click_area(d, 8); + lv_obj_add_flag(d, LV_OBJ_FLAG_CLICKABLE); + lv_obj_add_event_cb(d, home::onDot, LV_EVENT_CLICKED, (void*)(uintptr_t)p); + } + } + setHomePage(home::s_page); +} + +// From loop() on Home: turn the page once a press has moved SWIPE_PX sideways. +void UITask::homeSwipePoll() { + lv_indev_t* in = lv_indev_get_next(NULL); + if (!in || home::PAGES < 2) return; + bool down = lv_indev_get_state(in) == LV_INDEV_STATE_PRESSED; + lv_point_t p; + lv_indev_get_point(in, &p); + if (!down) { home::s_touching = false; return; } + if (!home::s_touching) { home::s_touching = true; home::s_swiped = false; home::s_start = p; return; } + if (home::s_swiped) return; + int dx = p.x - home::s_start.x, dy = p.y - home::s_start.y; + if (abs(dx) < home::SWIPE_PX || abs(dx) < 2 * abs(dy)) return; + home::s_swiped = true; + lv_indev_wait_release(in); // the swipe isn't also a tap on the tile it started on + setHomePage(home::s_page + (dx < 0 ? 1 : -1)); +} + +void UITask::setHomePage(int page) { + if (_screen != SCR_HOME || !home::s_row) return; // s_row went with an older screen + if (page < 0 || page >= home::PAGES) return; + home::s_page = page; + lv_obj_clean(home::s_row); + _home_unread = nullptr; + for (int i = page * home::PER_PAGE; i < (page + 1) * home::PER_PAGE; i++) { + if (i < home::COUNT) { + const home::App& a = home::APPS[i]; + homeTile(home::s_row, a.icon, a.text, a.cb, a.unread ? &_home_unread : NULL); + } else { // keep a short last page's tiles the same width + lv_obj_t* gap = lv_obj_create(home::s_row); + lv_obj_remove_style_all(gap); + lv_obj_set_height(gap, 1); + lv_obj_set_flex_grow(gap, 1); + } + } + for (int i = 0; home::s_dots && i < (int)lv_obj_get_child_count(home::s_dots); i++) + lv_obj_set_style_bg_color(lv_obj_get_child(home::s_dots, i), lv_color_hex(i == page ? theme::ACCENT : theme::SURFACE_2), 0); + refreshHome(); } void UITask::refreshHome() { @@ -491,6 +642,7 @@ void UITask::refreshHome() { lv_label_set_text(_home_clock, "--:--"); lv_label_set_text(_home_date, "time not synced"); } + if (!_home_unread) return; int unread = _core->dmUnreadTotal() + _core->history.getTotalChannelUnread() + _core->roomUnread(); if (unread > 0) lv_label_set_text_fmt(_home_unread, "%d", unread); else lv_label_set_text(_home_unread, ""); @@ -532,7 +684,7 @@ static lv_obj_t* listRow(lv_obj_t* parent, const char* title, const char* sub, if (sub) { lv_obj_t* s = label(row, sub, THEME_FONT_SMALL, theme::TEXT_MUTED); lv_label_set_long_mode(s, LV_LABEL_LONG_DOT); - lv_obj_set_width(s, 230); + lv_obj_set_size(s, 230, 15); // fixed height: LONG_DOT cuts instead of wrapping over the title lv_obj_align(s, LV_ALIGN_BOTTOM_LEFT, theme::PAD, -5); } return row; @@ -631,7 +783,7 @@ void UITask::buildContacts() { // model, since it is a different set: who is in range, not who is known. static NearbyModel::Entry s_node; // the node open in SCR_NODE (a copy: the list re-sorts) -enum : uint8_t { NODE_MSG, NODE_PING, NODE_FAV, NODE_ADD, NODE_DELETE }; +enum : uint8_t { NODE_MSG, NODE_PING, NODE_FAV, NODE_ADD, NODE_DELETE, NODE_NAV }; static void onNearbyChip(lv_event_t* e) { s_ui->setNearbyFilter((uint8_t)(uintptr_t)lv_event_get_user_data(e)); } static void onNearbySort(lv_event_t* e) { (void)e; s_ui->toggleNearbySort(); } @@ -668,7 +820,8 @@ void UITask::buildNearby() { lv_obj_set_style_pad_row(body, 4, 0); if (_header) { headerButton(_header, LV_SYMBOL_REFRESH " Scan", onNearbyScan, 4, NULL); - headerButton(_header, "", onNearbySort, 84, &_nearby_sort_lbl); + lv_obj_set_width(headerButton(_header, "", onNearbySort, 84, &_nearby_sort_lbl), 62); // fits "Recent" / "Dist" + headerButton(_header, UI_SYMBOL_MAP, onOpenNodesMap, 152, NULL); // the Nodes map } // Type filter chips @@ -939,6 +1092,7 @@ void UITask::buildNode() { bool contact = e.contact_idx >= 0; if (contact && e.type == ADV_TYPE_CHAT) actionButton(acts, LV_SYMBOL_ENVELOPE " Message", NODE_MSG, true); if (e.has_key) actionButton(acts, LV_SYMBOL_LOOP " Ping", NODE_PING, false); + if (e.lat_e6 != 0 || e.lon_e6 != 0) actionButton(acts, UI_SYMBOL_COMPASS, NODE_NAV, false); if (contact) actionButton(acts, e.fav ? UI_SYMBOL_STAR " Unfav" : UI_SYMBOL_STAR " Fav", NODE_FAV, false); if (!contact && e.has_key && !e.is_known) actionButton(acts, LV_SYMBOL_PLUS " Add", NODE_ADD, true); if (contact) _node_delete_lbl = actionButton(acts, LV_SYMBOL_TRASH, NODE_DELETE, false); @@ -1046,6 +1200,17 @@ void UITask::nodeAction(uint8_t action) { showToast("Contacts full"); } break; + case NODE_NAV: + // Followed by key when there is one (the Locator resolves live share, + // then advert position); otherwise a fixed point where it was seen. + if (e.has_prefix) navToNode(e.pub_key, e.lat_e6, e.lon_e6, e.name[0] ? e.name : "Node"); + else { + _core->locator.setTarget(0, nullptr, e.lat_e6, e.lon_e6, e.name[0] ? e.name : "Node"); + the_mesh.savePrefs(); + openMap(true); + navFrameTarget(); + } + break; case NODE_DELETE: if (!_delete_armed_ms) { // destructive: second tap within 3 s confirms _delete_armed_ms = millis(); @@ -1063,6 +1228,9 @@ void UITask::nodeAction(uint8_t action) { } #include "MapScreen.h" +#include "NavMap.h" +#include "ClockScreen.h" +#include "RadioScreen.h" #include "WifiScreen.h" // ── Conversation ────────────────────────────────────────────────────────────── @@ -1194,6 +1362,12 @@ void UITask::buildThread() { lv_obj_add_event_cb(_compose_ta, onComposeClicked, LV_EVENT_CLICKED, _keyboard); } refreshThread(); + if (_compose_ta && _share_text[0]) { // shareToMessage(): the text waits in the field + lv_textarea_set_text(_compose_ta, _share_text); + _share_text[0] = '\0'; + lv_obj_update_layout(lv_screen_active()); + setKeyboardVisible(true); + } } // Changes whenever the open conversation's content or delivery markers change. @@ -1218,9 +1392,34 @@ uint32_t UITask::threadSignature() const { return sig; } +// Positions found in the shown messages ([WAY] / [LOC] / plain "lat,lon"), +// for the Go / Save buttons under such a bubble. +struct MsgLoc { int32_t lat, lon; char label[WAYPOINT_LABEL_LEN * 2]; }; +static const int THREAD_MAX_SHOWN = 30; +static MsgLoc s_msg_locs[THREAD_MAX_SHOWN]; +static int s_msg_loc_n = 0; + +static void onMsgLoc(lv_event_t* e) { + uintptr_t v = (uintptr_t)lv_event_get_user_data(e); + s_ui->messageLocationAction((int)(v >> 1), (v & 1) != 0); +} + +static void msgLocButton(lv_obj_t* parent, const char* text, int idx, bool save, bool accent) { + lv_obj_t* b = lv_button_create(parent); + lv_obj_set_size(b, LV_SIZE_CONTENT, 28); + lv_obj_set_style_pad_hor(b, 10, 0); + lv_obj_set_style_pad_ver(b, 0, 0); + lv_obj_set_style_radius(b, 14, 0); + lv_obj_set_style_shadow_width(b, 0, 0); + lv_obj_set_style_bg_color(b, lv_color_hex(accent ? theme::ACCENT : theme::SURFACE_2), 0); + lv_obj_add_event_cb(b, onMsgLoc, LV_EVENT_CLICKED, (void*)(uintptr_t)((idx << 1) | (save ? 1 : 0))); + lv_obj_center(label(b, text, THEME_FONT_SMALL, accent ? theme::BG : theme::TEXT)); +} + // One message bubble. Own messages right-aligned in amber, others left. +// loc_idx >= 0: the text carries a position (s_msg_locs[loc_idx]). static void bubble(lv_obj_t* list, const char* from, const char* text, bool own, - uint32_t ts, const char* status, uint32_t status_col) { + uint32_t ts, const char* status, uint32_t status_col, int loc_idx = -1) { // Full-width row that pushes the bubble to its side. lv_obj_t* row = lv_obj_create(list); styleSurface(row, theme::BG); @@ -1267,6 +1466,20 @@ static void bubble(lv_obj_t* list, const char* from, const char* text, bool own, lv_obj_set_style_max_width(t, 238, 0); lv_obj_set_width(t, LV_SIZE_CONTENT); + if (loc_idx >= 0) { // position: navigate there / keep it as a waypoint + lv_obj_t* acts = lv_obj_create(b); + styleSurface(acts, theme::BG); + lv_obj_set_style_bg_opa(acts, LV_OPA_TRANSP, 0); + lv_obj_remove_flag(acts, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_remove_flag(acts, LV_OBJ_FLAG_CLICKABLE); + lv_obj_set_size(acts, LV_SIZE_CONTENT, LV_SIZE_CONTENT); + lv_obj_set_flex_flow(acts, LV_FLEX_FLOW_ROW); + lv_obj_set_style_pad_column(acts, 6, 0); + lv_obj_set_style_pad_top(acts, 2, 0); + msgLocButton(acts, UI_SYMBOL_COMPASS " Go", loc_idx, false, true); + msgLocButton(acts, UI_SYMBOL_FLAG " Save", loc_idx, true, false); + } + if (!meta_in_header) { lv_obj_t* m = label(b, meta, THEME_FONT_SMALL, theme::TEXT_MUTED); if (status) { @@ -1276,13 +1489,43 @@ static void bubble(lv_obj_t* list, const char* from, const char* text, bool own, } } +// Remember the position in `text` (if any) for its bubble's buttons; label +// from a [WAY] tag, else the sender. Returns the slot or -1. +static int noteMsgLocation(const char* text, const char* sender) { + if (s_msg_loc_n >= THREAD_MAX_SHOWN) return -1; + MsgLoc& m = s_msg_locs[s_msg_loc_n]; + if (!geo::parseLatLon(text, m.lat, m.lon, m.label, sizeof(m.label))) return -1; + if (!m.label[0]) snprintf(m.label, sizeof(m.label), "%s", sender && sender[0] ? sender : "Msg loc"); + return s_msg_loc_n++; +} + +void UITask::messageLocationAction(int idx, bool save) { + if (idx < 0 || idx >= s_msg_loc_n) return; + const MsgLoc& m = s_msg_locs[idx]; + if (save) { + if (_core->waypoints.full()) { showToast("Waypoints full (16)"); return; } + if (_core->waypoints.add(m.lat, m.lon, rtc_clock.getCurrentTime(), m.label)) { + char t[48]; + snprintf(t, sizeof(t), "Saved %s", _core->waypoints.at(_core->waypoints.count() - 1).label); + showToast(t); + } + return; + } + // A place snapshotted from the text (kind 0): nothing to keep re-resolving. + navSetTarget(0, nullptr, m.lat, m.lon, m.label); + openMap(true); + navFrameTarget(); + refreshNavBar(); +} + void UITask::refreshThread() { if (!_thread_list) return; _thread_dirty = false; _thread_sig = threadSignature(); lv_obj_clean(_thread_list); const MessageHistory& h = _core->history; - const int MAX_SHOWN = 30; + const int MAX_SHOWN = THREAD_MAX_SHOWN; + s_msg_loc_n = 0; if (_thread_is_channel) { int n = h.histCountForChannel(_thread_channel); @@ -1302,7 +1545,8 @@ void UITask::refreshThread() { const char* st = NULL; uint32_t col = theme::TEXT_MUTED; if (own && e.relay_status == ACK_OK) { st = LV_SYMBOL_OK " relayed"; col = theme::OK; } else if (own && e.relay_status == ACK_PENDING) st = "sent"; - bubble(_thread_list, own ? NULL : from, body, own, e.timestamp, st, col); + bubble(_thread_list, own ? NULL : from, body, own, e.timestamp, st, col, + own ? -1 : noteMsgLocation(body, from)); } } else { int n = h.dmHistCountForContact(_thread_key); @@ -1318,7 +1562,10 @@ void UITask::refreshThread() { default: st = "sent"; break; } } - bubble(_thread_list, NULL, e.text, e.outgoing, e.timestamp, st, col); + char who[33] = ""; + if (!e.outgoing) contactName(_thread_key, who, sizeof(who)); + bubble(_thread_list, NULL, e.text, e.outgoing, e.timestamp, st, col, + e.outgoing ? -1 : noteMsgLocation(e.text, who)); } } if (lv_obj_get_child_count(_thread_list) == 0) @@ -1375,12 +1622,197 @@ static lv_obj_t* dropdownRow(lv_obj_t* parent, const char* text, const char* opt return dd; } +// A preference toggle (0/1 byte in NodePrefs), saved on change. +static void onPrefSwitch(lv_event_t* e) { + uint8_t* pref = (uint8_t*)lv_event_get_user_data(e); + *pref = lv_obj_has_state((lv_obj_t*)lv_event_get_target(e), LV_STATE_CHECKED) ? 1 : 0; + the_mesh.savePrefs(); +} + +static void onGpsSwitch(lv_event_t* e) { + s_ui->setGps(lv_obj_has_state((lv_obj_t*)lv_event_get_target(e), LV_STATE_CHECKED)); +} + +// pref == nullptr: the caller wires the switch itself. +static lv_obj_t* switchRow(lv_obj_t* parent, const char* text, const char* sub, uint8_t* pref) { + lv_obj_t* row = lv_obj_create(parent); + styleSurface(row, theme::SURFACE); + lv_obj_remove_flag(row, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_size(row, LV_PCT(100), theme::ROW_H); + lv_obj_set_style_radius(row, theme::RADIUS, 0); + lv_obj_t* l = label(row, text, THEME_FONT_BODY, theme::TEXT); + lv_obj_align(l, LV_ALIGN_TOP_LEFT, theme::PAD, sub ? 5 : 13); + if (sub) { + lv_obj_t* h = label(row, sub, THEME_FONT_SMALL, theme::TEXT_MUTED); + lv_label_set_long_mode(h, LV_LABEL_LONG_DOT); + lv_obj_set_size(h, 236, 15); // clear of the switch + lv_obj_align(h, LV_ALIGN_BOTTOM_LEFT, theme::PAD, -5); + } + lv_obj_t* sw = lv_switch_create(row); + lv_obj_set_size(sw, 46, 24); + lv_obj_align(sw, LV_ALIGN_RIGHT_MID, -theme::PAD, 0); + if (pref) { + if (*pref) lv_obj_add_state(sw, LV_STATE_CHECKED); + lv_obj_add_event_cb(sw, onPrefSwitch, LV_EVENT_VALUE_CHANGED, pref); + } + return sw; +} + +// ── Schema-driven settings (ui-core/SettingsSchema.h) ───────────────────────── + +static void onSchemaSwitch(lv_event_t* e) { + s_ui->setSchemaValue((int)(uintptr_t)lv_event_get_user_data(e), + lv_obj_has_state((lv_obj_t*)lv_event_get_target(e), LV_STATE_CHECKED) ? 1 : 0); +} +static void onSchemaDropdown(lv_event_t* e) { + s_ui->setSchemaValue((int)(uintptr_t)lv_event_get_user_data(e), + (int)lv_dropdown_get_selected((lv_obj_t*)lv_event_get_target(e))); +} +static void onOpenSchemaPage(lv_event_t* e) { s_ui->showSchemaSettings((int)(uintptr_t)lv_event_get_user_data(e)); } +static void onPruneContacts(lv_event_t* e) { (void)e; s_ui->pruneContacts(); } + +void UITask::showSchemaSettings(int page) { + _screen = SCR_SETTINGS_NAV; + _settings_page = (uint8_t)(page < settings::PG_COUNT ? page : 0); + buildSchemaSettings(); +} + +// Contacts > prune now: first tap shows how many, the second (within 3 s) removes them. +void UITask::pruneContacts() { + int n = the_mesh.countStaleContacts(); + if (n == 0) { + showToast(_prefs && _prefs->contact_expiry_idx == 0 ? "Contact expiry is off" : "No inactive contacts"); + return; + } + if (!_prune_armed_ms || millis() - _prune_armed_ms > 3000) { + _prune_armed_ms = millis() | 1; + if (_prune_lbl) lv_label_set_text_fmt(_prune_lbl, LV_SYMBOL_TRASH " Remove %d contact%s?", n, n == 1 ? "" : "s"); + return; + } + _prune_armed_ms = 0; + int removed = the_mesh.pruneStaleContacts(); + char t[40]; + snprintf(t, sizeof(t), "Removed %d contact%s", removed, removed == 1 ? "" : "s"); + showToast(t); + if (_prune_lbl) lv_label_set_text(_prune_lbl, LV_SYMBOL_TRASH " Remove inactive contacts now"); +} + +void UITask::applyDisplayPrefs() { + if (!_prefs) return; + if (_prefs->display_brightness_pct) lvport::setBacklightPct(_prefs->display_brightness_pct); + else if (_display) _display->setBrightness(_prefs->display_brightness); +} + +// Brightness slider: live while dragging, saved on release. +static void onBrightnessSlider(lv_event_t* e) { + lv_obj_t* sl = (lv_obj_t*)lv_event_get_target(e); + s_ui->setBrightnessPct((uint8_t)lv_slider_get_value(sl), lv_event_get_code(e) == LV_EVENT_RELEASED); +} + +void UITask::setBrightnessPct(uint8_t pct, bool save) { + if (!_prefs) return; + _prefs->display_brightness_pct = pct; + _prefs->display_brightness = (uint8_t)((pct + 12) / 25 > 4 ? 4 : (pct + 12) / 25); // nearest level, for anything reading it + applyDisplayPrefs(); + if (save) the_mesh.savePrefs(); +} + +void UITask::buildSchemaSettings() { + lv_obj_t* body = newScreen(settings::pageTitle(_settings_page), true); + _prune_lbl = nullptr; + _prune_armed_ms = 0; + uint8_t sec = 0xFF; + for (int i = 0; i < settings::COUNT; i++) { + const settings::Setting& st = settings::ALL[i]; + if (settings::sectionPage(st.section) != _settings_page) continue; + if (st.section != sec) { sec = st.section; sectionTitle(body, settings::sectionTitle(sec)); } + uint8_t v = settings::get(*_prefs, st); + if (st.offset == offsetof(NodePrefs, display_brightness)) { // a slider here instead of five steps + lv_obj_t* row = lv_obj_create(body); + styleSurface(row, theme::SURFACE); + lv_obj_remove_flag(row, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_size(row, LV_PCT(100), theme::ROW_H); + lv_obj_set_style_radius(row, theme::RADIUS, 0); + lv_obj_align(label(row, st.label, THEME_FONT_BODY, theme::TEXT), LV_ALIGN_LEFT_MID, theme::PAD, 0); + lv_obj_t* sl = lv_slider_create(row); + lv_slider_set_range(sl, 5, 100); + uint8_t pct = _prefs->display_brightness_pct ? _prefs->display_brightness_pct + : (uint8_t)(_prefs->display_brightness * 25 > 5 ? _prefs->display_brightness * 25 : 5); + lv_slider_set_value(sl, pct, LV_ANIM_OFF); + lv_obj_set_size(sl, 170, 10); + lv_obj_align(sl, LV_ALIGN_RIGHT_MID, -18, 0); + lv_obj_set_ext_click_area(sl, 14); + lv_obj_add_event_cb(sl, onBrightnessSlider, LV_EVENT_VALUE_CHANGED, NULL); + lv_obj_add_event_cb(sl, onBrightnessSlider, LV_EVENT_RELEASED, NULL); + continue; + } + if (!st.option) { + lv_obj_t* sw = switchRow(body, st.label, st.hint, nullptr); + if (v) lv_obj_add_state(sw, LV_STATE_CHECKED); + lv_obj_add_event_cb(sw, onSchemaSwitch, LV_EVENT_VALUE_CHANGED, (void*)(uintptr_t)i); + continue; + } + char opts[160]; + int o = 0; + for (uint8_t k = 0; k < st.count && o < (int)sizeof(opts) - 16; k++) { + if (k) opts[o++] = '\n'; + st.option(k, opts + o, sizeof(opts) - o, *_prefs); + o += strlen(opts + o); + } + opts[o] = '\0'; + lv_obj_t* row = lv_obj_create(body); + styleSurface(row, theme::SURFACE); + lv_obj_remove_flag(row, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_size(row, LV_PCT(100), theme::ROW_H); + lv_obj_set_style_radius(row, theme::RADIUS, 0); + lv_obj_t* l = label(row, st.label, THEME_FONT_BODY, theme::TEXT); + lv_obj_align(l, LV_ALIGN_TOP_LEFT, theme::PAD, st.hint ? 5 : 13); + if (st.hint) { + lv_obj_t* h = label(row, st.hint, THEME_FONT_SMALL, theme::TEXT_MUTED); + lv_label_set_long_mode(h, LV_LABEL_LONG_DOT); + lv_obj_set_size(h, 176, 15); // fixed height: cut, don't wrap onto the label + lv_obj_align(h, LV_ALIGN_BOTTOM_LEFT, theme::PAD, -5); + } + lv_obj_t* dd = lv_dropdown_create(row); + lv_dropdown_set_options(dd, opts); + lv_dropdown_set_selected(dd, v); + lv_obj_set_width(dd, 112); + lv_obj_align(dd, LV_ALIGN_RIGHT_MID, -4, 0); + lv_obj_add_event_cb(dd, onSchemaDropdown, LV_EVENT_VALUE_CHANGED, (void*)(uintptr_t)i); + } + if (_settings_page == settings::PG_MESSAGES) { // the action that goes with "Contact expiry" + lv_obj_t* b = lv_button_create(body); + lv_obj_set_size(b, LV_PCT(100), 38); + lv_obj_set_style_shadow_width(b, 0, 0); + lv_obj_set_style_radius(b, theme::RADIUS, 0); + lv_obj_set_style_bg_color(b, lv_color_hex(theme::SURFACE), 0); + lv_obj_add_event_cb(b, onPruneContacts, LV_EVENT_CLICKED, NULL); + _prune_lbl = label(b, LV_SYMBOL_TRASH " Remove inactive contacts now", THEME_FONT_BODY, theme::TEXT); + lv_obj_center(_prune_lbl); + } +} + +void UITask::setSchemaValue(int idx, int v) { + if (!_prefs || idx < 0 || idx >= settings::COUNT) return; + const settings::Setting& st = settings::ALL[idx]; + settings::set(*_prefs, st, (uint8_t)v); + if (st.changed) st.changed(*_core); + the_mesh.savePrefs(); + if (st.offset == offsetof(NodePrefs, units_imperial)) { // other labels depend on it + lv_obj_t* body = _body; + int32_t y = body ? lv_obj_get_scroll_y(body) : 0; + buildSchemaSettings(); + if (_body) { lv_obj_update_layout(_body); lv_obj_scroll_to_y(_body, y, LV_ANIM_OFF); } + } +} + void UITask::showSettings() { _screen = SCR_SETTINGS; buildSettings(); } static void onOpenWifi(lv_event_t* e) { (void)e; s_ui->showWifi(false); } +static void onOpenRadio(lv_event_t* e); // RadioScreen.h void UITask::buildSettings() { lv_obj_t* body = newScreen("Settings", true); @@ -1388,6 +1820,29 @@ void UITask::buildSettings() { char ssid[33], pass[65]; bool have = lvport::loadWifi(ssid, sizeof(ssid), pass, sizeof(pass)); listRow(body, LV_SYMBOL_WIFI " WiFi", have ? ssid : "Not set (for map downloads)", onOpenWifi, NULL); + if (_prefs) { + char sub[48]; + int pi = radioctl::currentPreset(_prefs); + const char* pn = "Custom"; float f, b; uint8_t sf, cr; + if (pi >= 0) radioctl::presetAt(_prefs, pi, pn, f, b, sf, cr); + snprintf(sub, sizeof(sub), "%s - %.3f MHz, %d dBm", pn, _prefs->freq, _prefs->tx_power_dbm); + listRow(body, UI_SYMBOL_RADIO " Radio", sub, onOpenRadio, NULL); + } + if (_prefs) { // the same NodePrefs switches as ui-new's Home GPS toggle / Live share / Locator screens + sectionTitle(body, "NAVIGATION"); + if (_core->gpsAvailable()) { + lv_obj_t* sw = switchRow(body, "GPS", "Position for maps, sharing and adverts", nullptr); + if (_core->gpsEnabled()) lv_obj_add_state(sw, LV_STATE_CHECKED); + lv_obj_add_event_cb(sw, onGpsSwitch, LV_EVENT_VALUE_CHANGED, NULL); + } + listRow(body, UI_SYMBOL_COMPASS " Trail, live share, alerts", "Point spacing, auto-pause, sharing, radius", + onOpenSchemaPage, (void*)(uintptr_t)settings::PG_NAV); + sectionTitle(body, "DEVICE"); + listRow(body, LV_SYMBOL_EYE_OPEN " Display & power", "Brightness, screen off, battery, GPS, time zone", + onOpenSchemaPage, (void*)(uintptr_t)settings::PG_DEVICE); + listRow(body, LV_SYMBOL_ENVELOPE " Messages & contacts", "Resending, contact expiry, sorting", + onOpenSchemaPage, (void*)(uintptr_t)settings::PG_MESSAGES); + } sectionTitle(body, "KEYBOARD"); uint8_t main_a = _prefs ? _prefs->keyboard_main_alphabet : 0; uint8_t alt_a = _prefs ? _prefs->keyboard_alt_alphabet : 0; @@ -1399,6 +1854,16 @@ void UITask::buildSettings() { s_kb_alt_dd = dropdownRow(body, "Additional", "None\nLatin\nCyrillic\nGreek", alt_a == main_a ? 0 : alt_a + 1); label(body, "Hold a letter for accents and other variants.", THEME_FONT_SMALL, theme::TEXT_MUTED); + + sectionTitle(body, "ABOUT"); + char about[200], built[24] = ""; + if (!strstr(FIRMWARE_VERSION, FIRMWARE_BUILD_DATE)) snprintf(built, sizeof(built), " (%s)", FIRMWARE_BUILD_DATE); + snprintf(about, sizeof(about), "%s\nFirmware %s%s\n\nMap data: %s", the_mesh.getNodeName(), FIRMWARE_VERSION, built, + (lvport::mountStorage() && mapview::s_provider->available()) ? mapview::s_provider->attribution() + : "\xC2\xA9 OpenStreetMap contributors"); + lv_obj_t* a = label(body, about, THEME_FONT_SMALL, theme::TEXT); + lv_label_set_long_mode(a, LV_LABEL_LONG_WRAP); + lv_obj_set_width(a, LV_PCT(100)); } void UITask::setKeyboardAlphabets(int main_idx, int alt_sel) { diff --git a/examples/companion_radio/ui-lvgl/UITask.h b/examples/companion_radio/ui-lvgl/UITask.h index 3ecf35fa..58fd2443 100644 --- a/examples/companion_radio/ui-lvgl/UITask.h +++ b/examples/companion_radio/ui-lvgl/UITask.h @@ -32,14 +32,23 @@ public: 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; // ── Navigation (called from LVGL event callbacks) ──────────────────────── void showHome(); + void setHomePage(int page); void showChats(); void showContacts(); void showSettings(); + void showSchemaSettings(int page); + void pruneContacts(); + void setBrightnessPct(uint8_t pct, bool save); + void applyDisplayPrefs() override; + void setSchemaValue(int idx, int v); void showNearby(); - void showMap(); + 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); @@ -48,7 +57,27 @@ public: void mapDownloadClose(); void mapDownloadStart(); void mapDownloadStop(); + void mapDownloadResume(); + void mapDownloadDiscard(); void mapDownloadZmax(int delta); + // 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(bool from_map); void wifiScan(); void wifiPick(int idx); @@ -69,22 +98,37 @@ public: 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(); + // Settings > Radio (RadioScreen.h) + void showRadio(); + void radioSet(int which, int v); + void radioFreqPopup(); + void radioFreqDone(bool ok); + 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)ci; (void)from_picker; } private: - enum Screen : uint8_t { SCR_HOME, SCR_CHATS, SCR_CONTACTS, SCR_THREAD, SCR_SETTINGS, SCR_NEARBY, SCR_NODE, SCR_MAP, SCR_WIFI }; + 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 }; void buildStatusBar(); void refreshStatusBar(); lv_obj_t* newScreen(const char* title, bool with_back); void buildHome(); void refreshHome(); + void homeSwipePoll(); void buildChats(); void buildContacts(); void buildSettings(); + void buildSchemaSettings(); void buildNearby(); void refreshNearbyList(); uint32_t nearbySignature() const; @@ -94,6 +138,27 @@ private: 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 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 refreshDownloadPopup(); void buildWifi(); @@ -103,11 +168,19 @@ private: void buildThread(); void refreshThread(); uint32_t threadSignature() const; + void buildClock(); + void buildRadio(); + void radioCloseFreq(); + bool radioPopupOpen() const; + void refreshClock(); + void showRing(const char* text); + void hideRing(); void drainCoreEvents(); void onMessageArrived(const UiEvent& ev); void wake(); void sleep(); uint32_t autoOffMillis() const; + void checkLowBattery(); DisplayDriver* _display = nullptr; SensorManager* _sensors = nullptr; @@ -118,6 +191,13 @@ private: 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 + uint32_t _prune_armed_ms = 0; + uint16_t _batt_mv = 0; // smoothed, for the low-battery shutdown + uint32_t _next_batt_ms = 0; + lv_obj_t* _prune_lbl = nullptr; // Open conversation (SCR_THREAD) bool _thread_is_channel = false; @@ -129,6 +209,7 @@ private: // Widgets lv_obj_t* _status_time = nullptr; lv_obj_t* _status_icons = nullptr; + lv_obj_t* _status_gps = nullptr; lv_obj_t* _toast = nullptr; lv_timer_t* _toast_timer = nullptr; lv_obj_t* _home_clock = nullptr; @@ -181,9 +262,36 @@ private: lv_obj_t* _dl_info = nullptr; 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_from_map = false; bool _wifi_scanning = false; diff --git a/examples/companion_radio/ui-lvgl/fonts/generate.sh b/examples/companion_radio/ui-lvgl/fonts/generate.sh index f83de128..3e09489e 100755 --- a/examples/companion_radio/ui-lvgl/fonts/generate.sh +++ b/examples/companion_radio/ui-lvgl/fonts/generate.sh @@ -27,8 +27,10 @@ TEXT="0x20-0x7E,0xA0-0x24F,0x370-0x3FF,0x400-0x52F,0x1E80-0x1E85,0x1E9E,0x1EF2-0 TEXT="$TEXT,0x2010-0x2027,0x2030,0x2039-0x203A,0x20A4-0x20BF,0x2116,0x2122" # LVGL built-in symbols (same list as lvgl/scripts/built_in_font/built_in_font_gen.py) # plus 61612 = globe (keyboard script key), 61445 = star (favourite), -# 62073 = map, 61632 = users (home tiles) -SYMS="61612,61445,62073,61632,61441,61448,61451,61452,61453,61457,61459,61461,61465,61468,61473,61478,61479,61480,61502,61507,61512,61515,61516,61517,61521,61522,61523,61524,61543,61544,61550,61552,61553,61556,61559,61560,61561,61563,61587,61589,61636,61637,61639,61641,61664,61671,61674,61683,61724,61732,61787,61931,62016,62017,62018,62019,62020,62087,62099,62212,62189,62810,63426,63650" +# 62073 = map, 61632 = users (home tiles), 61774 = compass (Navigate), +# 61476 = flag (waypoint), 62405 = map marker (mark here), 61463 = clock, +# 62194 = stopwatch, 62745 = broadcast tower (radio) +SYMS="62745,61463,62194,61612,61445,62073,61632,61774,61476,62405,61441,61448,61451,61452,61453,61457,61459,61461,61465,61468,61473,61478,61479,61480,61502,61507,61512,61515,61516,61517,61521,61522,61523,61524,61543,61544,61550,61552,61553,61556,61559,61560,61561,61563,61587,61589,61636,61637,61639,61641,61664,61671,61674,61683,61724,61732,61787,61931,62016,62017,62018,62019,62020,62087,62099,62212,62189,62810,63426,63650" CONV="npx -y lv_font_conv@1.5.3" gen() { # size weight ranges name @@ -38,7 +40,7 @@ gen() { # size weight ranges name --format lvgl --lv-include lvgl.h --lv-font-name "$4" -o "$HERE/$4.c" } for s in 12 14 16 20; do gen "$s" Medium "$TEXT" "ui_font_$s"; done -# Clock: digits, colon, dash, space only. -$CONV --bpp 4 --size 40 --no-compress --font "$CACHE/NotoSans-SemiBold.ttf" -r "0x20,0x2D,0x30-0x3A" \ +# Clock: digits, colon, dash, dot, space only. +$CONV --bpp 4 --size 40 --no-compress --font "$CACHE/NotoSans-SemiBold.ttf" -r "0x20,0x2D-0x2E,0x30-0x3A" \ --format lvgl --lv-include lvgl.h --lv-font-name ui_font_40 -o "$HERE/ui_font_40.c" echo "done: $(ls "$HERE"/ui_font_*.c | wc -l) fonts" diff --git a/examples/companion_radio/ui-lvgl/fonts/ui_font_12.c b/examples/companion_radio/ui-lvgl/fonts/ui_font_12.c index d685946a..c5055826 100644 --- a/examples/companion_radio/ui-lvgl/fonts/ui_font_12.c +++ b/examples/companion_radio/ui-lvgl/fonts/ui_font_12.c @@ -1,7 +1,7 @@ /******************************************************************************* * Size: 12 px * Bpp: 4 - * Opts: --bpp 4 --size 12 --force-fast-kern-format --font /var/folders/1d/dgkmygls6x5gsb16pbn2mwlr0000gn/T//meshcore-ui-fonts/NotoSans-Medium.ttf -r 0x20-0x7E,0xA0-0x24F,0x370-0x3FF,0x400-0x52F,0x1E80-0x1E85,0x1E9E,0x1EF2-0x1EF3,0x2010-0x2027,0x2030,0x2039-0x203A,0x20A4-0x20BF,0x2116,0x2122 --font /var/folders/1d/dgkmygls6x5gsb16pbn2mwlr0000gn/T//meshcore-ui-fonts/fa.woff -r 61612,61445,62073,61632,61441,61448,61451,61452,61453,61457,61459,61461,61465,61468,61473,61478,61479,61480,61502,61507,61512,61515,61516,61517,61521,61522,61523,61524,61543,61544,61550,61552,61553,61556,61559,61560,61561,61563,61587,61589,61636,61637,61639,61641,61664,61671,61674,61683,61724,61732,61787,61931,62016,62017,62018,62019,62020,62087,62099,62212,62189,62810,63426,63650 --format lvgl --lv-include lvgl.h --lv-font-name ui_font_12 -o /Users/jakub/Documents/MeshCore/MeshCore/examples/companion_radio/ui-lvgl/fonts/ui_font_12.c + * Opts: --bpp 4 --size 12 --force-fast-kern-format --font /var/folders/1d/dgkmygls6x5gsb16pbn2mwlr0000gn/T//meshcore-ui-fonts/NotoSans-Medium.ttf -r 0x20-0x7E,0xA0-0x24F,0x370-0x3FF,0x400-0x52F,0x1E80-0x1E85,0x1E9E,0x1EF2-0x1EF3,0x2010-0x2027,0x2030,0x2039-0x203A,0x20A4-0x20BF,0x2116,0x2122 --font /var/folders/1d/dgkmygls6x5gsb16pbn2mwlr0000gn/T//meshcore-ui-fonts/fa.woff -r 62745,61463,62194,61612,61445,62073,61632,61774,61476,62405,61441,61448,61451,61452,61453,61457,61459,61461,61465,61468,61473,61478,61479,61480,61502,61507,61512,61515,61516,61517,61521,61522,61523,61524,61543,61544,61550,61552,61553,61556,61559,61560,61561,61563,61587,61589,61636,61637,61639,61641,61664,61671,61674,61683,61724,61732,61787,61931,62016,62017,62018,62019,62020,62087,62099,62212,62189,62810,63426,63650 --format lvgl --lv-include lvgl.h --lv-font-name ui_font_12 -o /Users/jakub/Documents/MeshCore/MeshCore/examples/companion_radio/ui-lvgl/fonts/ui_font_12.c ******************************************************************************/ #ifdef LV_LVGL_H_INCLUDE_SIMPLE @@ -6457,6 +6457,16 @@ static LV_ATTRIBUTE_LARGE_CONST const uint8_t glyph_bitmap[] = { 0x1, 0xcb, 0xfa, 0x1, 0xff, 0xca, 0x60, 0x1, 0x1, 0x80, 0x4e, 0x0, + /* U+F017 "" */ + 0x0, 0xc2, 0x68, 0x20, 0x1e, 0x2b, 0xec, 0xbe, + 0xb2, 0x0, 0x93, 0x50, 0x3, 0x26, 0xa0, 0xd, + 0x0, 0x5d, 0xc0, 0xa, 0x86, 0x44, 0x3, 0xf0, + 0xca, 0x0, 0x7f, 0x90, 0x40, 0x38, 0x48, 0x2, + 0x11, 0x0, 0x67, 0xd, 0x40, 0x0, 0xa0, 0x6, + 0x9b, 0x26, 0x0, 0x24, 0x80, 0x72, 0x69, 0x0, + 0x24, 0x6c, 0x3, 0xf5, 0x88, 0x26, 0x18, 0x6, + 0x3c, 0x40, 0x8, 0xf3, 0x6e, 0xdb, 0x86, 0x0, + /* U+F019 "" */ 0x0, 0xc4, 0xee, 0x20, 0xf, 0xcb, 0x10, 0x50, 0xf, 0xde, 0x0, 0xf0, 0xf, 0xfe, 0x96, 0xfb, @@ -6486,6 +6496,14 @@ static LV_ATTRIBUTE_LARGE_CONST const uint8_t glyph_bitmap[] = { 0x9b, 0x79, 0x83, 0x78, 0x47, 0x76, 0x91, 0x0, 0x0, + /* U+F024 "" */ + 0x3, 0x0, 0xff, 0x9f, 0x34, 0x3, 0xff, 0x80, + 0x55, 0x94, 0xc2, 0x4, 0xf6, 0xc, 0x0, 0x53, + 0x59, 0xef, 0xd8, 0x42, 0x0, 0xff, 0xfb, 0x8b, + 0x44, 0x10, 0x2, 0x18, 0x20, 0x4, 0x4b, 0xba, + 0xff, 0x75, 0xce, 0x1, 0xfc, 0x44, 0x0, 0x85, + 0x54, 0x1, 0xfe, + /* U+F026 "" */ 0x0, 0xc5, 0x40, 0x11, 0x62, 0xbc, 0x43, 0x4, 0x21, 0xdc, 0x20, 0x1f, 0xfc, 0x22, 0x0, 0xed, @@ -6793,6 +6811,17 @@ static LV_ATTRIBUTE_LARGE_CONST const uint8_t glyph_bitmap[] = { 0xe0, 0x30, 0x7, 0xff, 0x6, 0x40, 0x3f, 0xce, 0xc, 0x20, 0x1f, 0xe9, 0x98, 0x3, 0x80, + /* U+F14E "" */ + 0x0, 0xc8, 0xca, 0x40, 0x1e, 0x5e, 0xb9, 0xad, + 0xb2, 0x0, 0x9e, 0x84, 0x3, 0x26, 0x10, 0x1c, + 0x0, 0x71, 0x28, 0xf8, 0x48, 0x6, 0x3b, 0xdc, + 0x2, 0x44, 0x0, 0x51, 0xea, 0x16, 0x0, 0xc1, + 0x0, 0xb, 0xf3, 0x21, 0x0, 0x4, 0x40, 0x9, + 0xd, 0x78, 0x0, 0xb9, 0xc0, 0xc, 0x2f, 0xc6, + 0x1, 0x25, 0x80, 0x23, 0xa0, 0x40, 0x24, 0x32, + 0xb0, 0x60, 0xe, 0x3a, 0x0, 0x26, 0xa0, 0x4, + 0x2f, 0x82, 0x1, 0x15, 0xfe, 0xeb, 0xa0, 0x2, + /* U+F15B "" */ 0x9b, 0xbc, 0x4e, 0x0, 0x64, 0x4c, 0xd1, 0x20, 0x1f, 0x9a, 0x40, 0x38, 0x6e, 0xda, 0x1, 0xd5, @@ -6889,6 +6918,16 @@ static LV_ATTRIBUTE_LARGE_CONST const uint8_t glyph_bitmap[] = { 0x1, 0x38, 0x2a, 0x86, 0x42, 0x81, 0xc0, 0xe5, 0xdf, 0xd1, 0x20, + /* U+F2F2 "" */ + 0x0, 0x92, 0x20, 0xa0, 0x1f, 0x73, 0xbb, 0x80, + 0x3e, 0xa3, 0x2b, 0x0, 0xf4, 0x79, 0x97, 0xc4, + 0x1c, 0x1, 0x4e, 0x6, 0x80, 0xee, 0xe0, 0x45, + 0x0, 0x44, 0x0, 0x2c, 0xe, 0x0, 0xfd, 0xc0, + 0xe0, 0x1f, 0x94, 0x4, 0x2, 0x17, 0x0, 0x8c, + 0x10, 0x2, 0xab, 0x0, 0x90, 0x24, 0x40, 0x3c, + 0x30, 0x3, 0xa2, 0x1, 0x87, 0x44, 0x0, 0x7d, + 0xa, 0xa8, 0xf3, 0x0, 0x0, + /* U+F304 "" */ 0x0, 0xfe, 0x17, 0x10, 0xf, 0xe1, 0xf8, 0xc1, 0x0, 0xf8, 0x50, 0x40, 0xb4, 0x3, 0xc3, 0xfe, @@ -6900,6 +6939,28 @@ static LV_ATTRIBUTE_LARGE_CONST const uint8_t glyph_bitmap[] = { 0x3, 0xc4, 0x0, 0x1c, 0x20, 0xf, 0xa6, 0x57, 0xe4, 0x1, 0xf0, + /* U+F3C5 "" */ + 0x0, 0xa, 0xba, 0x88, 0x4, 0xbd, 0x51, 0x5c, + 0xa0, 0x74, 0x1, 0xd4, 0x70, 0x0, 0x8d, 0x80, + 0x4, 0x20, 0x23, 0x93, 0xa0, 0x20, 0x7, 0xf2, + 0x2, 0x39, 0x3a, 0x2, 0x40, 0x2, 0x36, 0x0, + 0x10, 0x6c, 0x1, 0xcc, 0x61, 0x24, 0x1, 0x14, + 0x80, 0x5c, 0x1, 0x70, 0x6, 0x37, 0x7, 0x30, + 0xe, 0x8a, 0x80, 0x8, + + /* U+F519 "" */ + 0x14, 0x0, 0xff, 0x90, 0x62, 0xc0, 0x3f, 0xeb, + 0x84, 0x50, 0x97, 0x5, 0xb5, 0x7, 0x90, 0x54, + 0x22, 0x3, 0x78, 0x5a, 0x58, 0x7b, 0x1, 0x10, + 0x44, 0x2, 0x40, 0x60, 0x60, 0x42, 0x2, 0x25, + 0x40, 0xcb, 0x22, 0x9, 0x12, 0xf0, 0x11, 0x7d, + 0x80, 0x86, 0xc3, 0x2, 0xc4, 0x85, 0xbd, 0x2c, + 0x2, 0x66, 0x13, 0x30, 0x40, 0x6d, 0x0, 0x33, + 0x3, 0x7b, 0x3, 0x0, 0x7e, 0xb2, 0x77, 0x8a, + 0x80, 0x3e, 0x42, 0x88, 0xe2, 0x40, 0xf, 0x70, + 0xa8, 0x4, 0xa3, 0xe0, 0x1e, 0x9a, 0x0, 0xea, + 0x80, 0x8, + /* U+F55A "" */ 0x0, 0x97, 0xbf, 0xff, 0xa0, 0x0, 0xb4, 0x20, 0x1f, 0xce, 0xb, 0x40, 0x13, 0x71, 0x13, 0x98, @@ -7958,60 +8019,66 @@ static const lv_font_fmt_txt_glyph_dsc_t glyph_dsc[] = { {.bitmap_index = 31387, .adv_w = 192, .box_w = 12, .box_h = 13, .ofs_x = 0, .ofs_y = -2}, {.bitmap_index = 31462, .adv_w = 216, .box_w = 14, .box_h = 11, .ofs_x = 0, .ofs_y = -1}, {.bitmap_index = 31522, .adv_w = 192, .box_w = 12, .box_h = 13, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 31576, .adv_w = 216, .box_w = 14, .box_h = 9, .ofs_x = 0, .ofs_y = 0}, - {.bitmap_index = 31624, .adv_w = 192, .box_w = 12, .box_h = 13, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 31697, .adv_w = 96, .box_w = 6, .box_h = 10, .ofs_x = 0, .ofs_y = -1}, - {.bitmap_index = 31720, .adv_w = 144, .box_w = 9, .box_h = 10, .ofs_x = 0, .ofs_y = -1}, - {.bitmap_index = 31755, .adv_w = 216, .box_w = 14, .box_h = 13, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 31829, .adv_w = 192, .box_w = 12, .box_h = 9, .ofs_x = 0, .ofs_y = 0}, - {.bitmap_index = 31871, .adv_w = 132, .box_w = 9, .box_h = 13, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 31921, .adv_w = 168, .box_w = 8, .box_h = 12, .ofs_x = 1, .ofs_y = -2}, - {.bitmap_index = 31958, .adv_w = 168, .box_w = 11, .box_h = 13, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 32004, .adv_w = 168, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -1}, - {.bitmap_index = 32031, .adv_w = 168, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -1}, - {.bitmap_index = 32048, .adv_w = 168, .box_w = 8, .box_h = 12, .ofs_x = 1, .ofs_y = -2}, - {.bitmap_index = 32086, .adv_w = 168, .box_w = 12, .box_h = 11, .ofs_x = -1, .ofs_y = -1}, - {.bitmap_index = 32133, .adv_w = 120, .box_w = 7, .box_h = 11, .ofs_x = 0, .ofs_y = -1}, - {.bitmap_index = 32169, .adv_w = 120, .box_w = 7, .box_h = 11, .ofs_x = 0, .ofs_y = -1}, - {.bitmap_index = 32206, .adv_w = 168, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -1}, - {.bitmap_index = 32245, .adv_w = 168, .box_w = 11, .box_h = 3, .ofs_x = 0, .ofs_y = 3}, - {.bitmap_index = 32254, .adv_w = 216, .box_w = 14, .box_h = 9, .ofs_x = 0, .ofs_y = 0}, - {.bitmap_index = 32316, .adv_w = 240, .box_w = 16, .box_h = 13, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 32407, .adv_w = 216, .box_w = 15, .box_h = 13, .ofs_x = -1, .ofs_y = -2}, - {.bitmap_index = 32482, .adv_w = 192, .box_w = 12, .box_h = 11, .ofs_x = 0, .ofs_y = -1}, - {.bitmap_index = 32541, .adv_w = 168, .box_w = 11, .box_h = 7, .ofs_x = 0, .ofs_y = 1}, - {.bitmap_index = 32575, .adv_w = 168, .box_w = 11, .box_h = 7, .ofs_x = 0, .ofs_y = 1}, - {.bitmap_index = 32609, .adv_w = 240, .box_w = 16, .box_h = 10, .ofs_x = -1, .ofs_y = 0}, - {.bitmap_index = 32669, .adv_w = 192, .box_w = 12, .box_h = 9, .ofs_x = 0, .ofs_y = 0}, - {.bitmap_index = 32689, .adv_w = 192, .box_w = 12, .box_h = 13, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 32746, .adv_w = 192, .box_w = 13, .box_h = 13, .ofs_x = -1, .ofs_y = -2}, - {.bitmap_index = 32810, .adv_w = 186, .box_w = 12, .box_h = 13, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 32880, .adv_w = 240, .box_w = 15, .box_h = 11, .ofs_x = 0, .ofs_y = -1}, - {.bitmap_index = 32954, .adv_w = 168, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -1}, - {.bitmap_index = 33015, .adv_w = 168, .box_w = 11, .box_h = 13, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 33055, .adv_w = 168, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -1}, - {.bitmap_index = 33099, .adv_w = 168, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -1}, - {.bitmap_index = 33131, .adv_w = 192, .box_w = 12, .box_h = 9, .ofs_x = 0, .ofs_y = 0}, - {.bitmap_index = 33170, .adv_w = 120, .box_w = 9, .box_h = 13, .ofs_x = -1, .ofs_y = -2}, - {.bitmap_index = 33216, .adv_w = 168, .box_w = 11, .box_h = 13, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 33261, .adv_w = 168, .box_w = 11, .box_h = 13, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 33314, .adv_w = 216, .box_w = 14, .box_h = 9, .ofs_x = 0, .ofs_y = 0}, - {.bitmap_index = 33370, .adv_w = 192, .box_w = 14, .box_h = 13, .ofs_x = -1, .ofs_y = -2}, - {.bitmap_index = 33433, .adv_w = 144, .box_w = 9, .box_h = 13, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 33456, .adv_w = 240, .box_w = 15, .box_h = 12, .ofs_x = 0, .ofs_y = -1}, - {.bitmap_index = 33531, .adv_w = 240, .box_w = 15, .box_h = 9, .ofs_x = 0, .ofs_y = 0}, - {.bitmap_index = 33574, .adv_w = 240, .box_w = 15, .box_h = 9, .ofs_x = 0, .ofs_y = 0}, - {.bitmap_index = 33623, .adv_w = 240, .box_w = 15, .box_h = 9, .ofs_x = 0, .ofs_y = 0}, - {.bitmap_index = 33672, .adv_w = 240, .box_w = 15, .box_h = 9, .ofs_x = 0, .ofs_y = 0}, - {.bitmap_index = 33721, .adv_w = 240, .box_w = 15, .box_h = 9, .ofs_x = 0, .ofs_y = 0}, - {.bitmap_index = 33763, .adv_w = 216, .box_w = 14, .box_h = 11, .ofs_x = 0, .ofs_y = -1}, - {.bitmap_index = 33800, .adv_w = 240, .box_w = 16, .box_h = 11, .ofs_x = 0, .ofs_y = -1}, - {.bitmap_index = 33865, .adv_w = 168, .box_w = 10, .box_h = 13, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 33929, .adv_w = 168, .box_w = 11, .box_h = 13, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 33972, .adv_w = 192, .box_w = 13, .box_h = 13, .ofs_x = -1, .ofs_y = -2}, - {.bitmap_index = 34039, .adv_w = 240, .box_w = 15, .box_h = 9, .ofs_x = 0, .ofs_y = 0}, - {.bitmap_index = 34094, .adv_w = 144, .box_w = 9, .box_h = 13, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 34123, .adv_w = 193, .box_w = 13, .box_h = 9, .ofs_x = 0, .ofs_y = 0} + {.bitmap_index = 31586, .adv_w = 192, .box_w = 12, .box_h = 13, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 31640, .adv_w = 216, .box_w = 14, .box_h = 9, .ofs_x = 0, .ofs_y = 0}, + {.bitmap_index = 31688, .adv_w = 192, .box_w = 12, .box_h = 13, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 31761, .adv_w = 192, .box_w = 13, .box_h = 13, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 31804, .adv_w = 96, .box_w = 6, .box_h = 10, .ofs_x = 0, .ofs_y = -1}, + {.bitmap_index = 31827, .adv_w = 144, .box_w = 9, .box_h = 10, .ofs_x = 0, .ofs_y = -1}, + {.bitmap_index = 31862, .adv_w = 216, .box_w = 14, .box_h = 13, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 31936, .adv_w = 192, .box_w = 12, .box_h = 9, .ofs_x = 0, .ofs_y = 0}, + {.bitmap_index = 31978, .adv_w = 132, .box_w = 9, .box_h = 13, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 32028, .adv_w = 168, .box_w = 8, .box_h = 12, .ofs_x = 1, .ofs_y = -2}, + {.bitmap_index = 32065, .adv_w = 168, .box_w = 11, .box_h = 13, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 32111, .adv_w = 168, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -1}, + {.bitmap_index = 32138, .adv_w = 168, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -1}, + {.bitmap_index = 32155, .adv_w = 168, .box_w = 8, .box_h = 12, .ofs_x = 1, .ofs_y = -2}, + {.bitmap_index = 32193, .adv_w = 168, .box_w = 12, .box_h = 11, .ofs_x = -1, .ofs_y = -1}, + {.bitmap_index = 32240, .adv_w = 120, .box_w = 7, .box_h = 11, .ofs_x = 0, .ofs_y = -1}, + {.bitmap_index = 32276, .adv_w = 120, .box_w = 7, .box_h = 11, .ofs_x = 0, .ofs_y = -1}, + {.bitmap_index = 32313, .adv_w = 168, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -1}, + {.bitmap_index = 32352, .adv_w = 168, .box_w = 11, .box_h = 3, .ofs_x = 0, .ofs_y = 3}, + {.bitmap_index = 32361, .adv_w = 216, .box_w = 14, .box_h = 9, .ofs_x = 0, .ofs_y = 0}, + {.bitmap_index = 32423, .adv_w = 240, .box_w = 16, .box_h = 13, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 32514, .adv_w = 216, .box_w = 15, .box_h = 13, .ofs_x = -1, .ofs_y = -2}, + {.bitmap_index = 32589, .adv_w = 192, .box_w = 12, .box_h = 11, .ofs_x = 0, .ofs_y = -1}, + {.bitmap_index = 32648, .adv_w = 168, .box_w = 11, .box_h = 7, .ofs_x = 0, .ofs_y = 1}, + {.bitmap_index = 32682, .adv_w = 168, .box_w = 11, .box_h = 7, .ofs_x = 0, .ofs_y = 1}, + {.bitmap_index = 32716, .adv_w = 240, .box_w = 16, .box_h = 10, .ofs_x = -1, .ofs_y = 0}, + {.bitmap_index = 32776, .adv_w = 192, .box_w = 12, .box_h = 9, .ofs_x = 0, .ofs_y = 0}, + {.bitmap_index = 32796, .adv_w = 192, .box_w = 12, .box_h = 13, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 32853, .adv_w = 192, .box_w = 13, .box_h = 13, .ofs_x = -1, .ofs_y = -2}, + {.bitmap_index = 32917, .adv_w = 186, .box_w = 12, .box_h = 13, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 32987, .adv_w = 240, .box_w = 15, .box_h = 11, .ofs_x = 0, .ofs_y = -1}, + {.bitmap_index = 33061, .adv_w = 168, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -1}, + {.bitmap_index = 33122, .adv_w = 168, .box_w = 11, .box_h = 13, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 33162, .adv_w = 168, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -1}, + {.bitmap_index = 33206, .adv_w = 168, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -1}, + {.bitmap_index = 33238, .adv_w = 192, .box_w = 12, .box_h = 9, .ofs_x = 0, .ofs_y = 0}, + {.bitmap_index = 33277, .adv_w = 120, .box_w = 9, .box_h = 13, .ofs_x = -1, .ofs_y = -2}, + {.bitmap_index = 33323, .adv_w = 168, .box_w = 11, .box_h = 13, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 33368, .adv_w = 168, .box_w = 11, .box_h = 13, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 33421, .adv_w = 216, .box_w = 14, .box_h = 9, .ofs_x = 0, .ofs_y = 0}, + {.bitmap_index = 33477, .adv_w = 192, .box_w = 14, .box_h = 13, .ofs_x = -1, .ofs_y = -2}, + {.bitmap_index = 33540, .adv_w = 186, .box_w = 12, .box_h = 13, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 33612, .adv_w = 144, .box_w = 9, .box_h = 13, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 33635, .adv_w = 240, .box_w = 15, .box_h = 12, .ofs_x = 0, .ofs_y = -1}, + {.bitmap_index = 33710, .adv_w = 240, .box_w = 15, .box_h = 9, .ofs_x = 0, .ofs_y = 0}, + {.bitmap_index = 33753, .adv_w = 240, .box_w = 15, .box_h = 9, .ofs_x = 0, .ofs_y = 0}, + {.bitmap_index = 33802, .adv_w = 240, .box_w = 15, .box_h = 9, .ofs_x = 0, .ofs_y = 0}, + {.bitmap_index = 33851, .adv_w = 240, .box_w = 15, .box_h = 9, .ofs_x = 0, .ofs_y = 0}, + {.bitmap_index = 33900, .adv_w = 240, .box_w = 15, .box_h = 9, .ofs_x = 0, .ofs_y = 0}, + {.bitmap_index = 33942, .adv_w = 216, .box_w = 14, .box_h = 11, .ofs_x = 0, .ofs_y = -1}, + {.bitmap_index = 33979, .adv_w = 240, .box_w = 16, .box_h = 11, .ofs_x = 0, .ofs_y = -1}, + {.bitmap_index = 34044, .adv_w = 168, .box_w = 10, .box_h = 13, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 34108, .adv_w = 168, .box_w = 11, .box_h = 13, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 34151, .adv_w = 168, .box_w = 11, .box_h = 13, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 34212, .adv_w = 192, .box_w = 13, .box_h = 13, .ofs_x = -1, .ofs_y = -2}, + {.bitmap_index = 34279, .adv_w = 144, .box_w = 9, .box_h = 13, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 34331, .adv_w = 240, .box_w = 15, .box_h = 13, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 34413, .adv_w = 240, .box_w = 15, .box_h = 9, .ofs_x = 0, .ofs_y = 0}, + {.bitmap_index = 34468, .adv_w = 144, .box_w = 9, .box_h = 13, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 34497, .adv_w = 193, .box_w = 13, .box_h = 9, .ofs_x = 0, .ofs_y = 0} }; /*--------------------- @@ -8036,14 +8103,14 @@ static const uint16_t unicode_list_10[] = { static const uint16_t unicode_list_12[] = { 0x0, 0xc, 0xceeb, 0xceef, 0xcef2, 0xcef5, 0xcef6, 0xcef7, - 0xcefb, 0xcefd, 0xceff, 0xcf03, 0xcf06, 0xcf0b, 0xcf10, 0xcf11, - 0xcf12, 0xcf28, 0xcf2d, 0xcf32, 0xcf35, 0xcf36, 0xcf37, 0xcf3b, - 0xcf3c, 0xcf3d, 0xcf3e, 0xcf51, 0xcf52, 0xcf58, 0xcf5a, 0xcf5b, - 0xcf5e, 0xcf61, 0xcf62, 0xcf63, 0xcf65, 0xcf7d, 0xcf7f, 0xcf96, - 0xcfaa, 0xcfae, 0xcfaf, 0xcfb1, 0xcfb3, 0xcfca, 0xcfd1, 0xcfd4, - 0xcfdd, 0xd006, 0xd00e, 0xd045, 0xd0d5, 0xd12a, 0xd12b, 0xd12c, - 0xd12d, 0xd12e, 0xd163, 0xd171, 0xd17d, 0xd1d7, 0xd1ee, 0xd444, - 0xd6ac, 0xd78c + 0xcefb, 0xcefd, 0xceff, 0xcf01, 0xcf03, 0xcf06, 0xcf0b, 0xcf0e, + 0xcf10, 0xcf11, 0xcf12, 0xcf28, 0xcf2d, 0xcf32, 0xcf35, 0xcf36, + 0xcf37, 0xcf3b, 0xcf3c, 0xcf3d, 0xcf3e, 0xcf51, 0xcf52, 0xcf58, + 0xcf5a, 0xcf5b, 0xcf5e, 0xcf61, 0xcf62, 0xcf63, 0xcf65, 0xcf7d, + 0xcf7f, 0xcf96, 0xcfaa, 0xcfae, 0xcfaf, 0xcfb1, 0xcfb3, 0xcfca, + 0xcfd1, 0xcfd4, 0xcfdd, 0xd006, 0xd00e, 0xd038, 0xd045, 0xd0d5, + 0xd12a, 0xd12b, 0xd12c, 0xd12d, 0xd12e, 0xd163, 0xd171, 0xd17d, + 0xd1d7, 0xd1dc, 0xd1ee, 0xd2af, 0xd403, 0xd444, 0xd6ac, 0xd78c }; /*Collect the unicode lists and glyph_id offsets*/ @@ -8099,7 +8166,7 @@ static const lv_font_fmt_txt_cmap_t cmaps[] = }, { .range_start = 8470, .range_length = 55181, .glyph_id_start = 1017, - .unicode_list = unicode_list_12, .glyph_id_ofs_list = NULL, .list_length = 66, .type = LV_FONT_FMT_TXT_CMAP_SPARSE_TINY + .unicode_list = unicode_list_12, .glyph_id_ofs_list = NULL, .list_length = 72, .type = LV_FONT_FMT_TXT_CMAP_SPARSE_TINY } }; @@ -8246,7 +8313,8 @@ static const uint8_t kern_left_class_mapping[] = 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0 + 0, 0, 0, 0, 0, 0, 0, 0, + 0 }; /*Map glyph_ids to kern right classes*/ @@ -8387,7 +8455,8 @@ static const uint8_t kern_right_class_mapping[] = 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0 + 0, 0, 0, 0, 0, 0, 0, 0, + 0 }; /*Kern values between classes*/ diff --git a/examples/companion_radio/ui-lvgl/fonts/ui_font_14.c b/examples/companion_radio/ui-lvgl/fonts/ui_font_14.c index 49596201..da950b40 100644 --- a/examples/companion_radio/ui-lvgl/fonts/ui_font_14.c +++ b/examples/companion_radio/ui-lvgl/fonts/ui_font_14.c @@ -1,7 +1,7 @@ /******************************************************************************* * Size: 14 px * Bpp: 4 - * Opts: --bpp 4 --size 14 --force-fast-kern-format --font /var/folders/1d/dgkmygls6x5gsb16pbn2mwlr0000gn/T//meshcore-ui-fonts/NotoSans-Medium.ttf -r 0x20-0x7E,0xA0-0x24F,0x370-0x3FF,0x400-0x52F,0x1E80-0x1E85,0x1E9E,0x1EF2-0x1EF3,0x2010-0x2027,0x2030,0x2039-0x203A,0x20A4-0x20BF,0x2116,0x2122 --font /var/folders/1d/dgkmygls6x5gsb16pbn2mwlr0000gn/T//meshcore-ui-fonts/fa.woff -r 61612,61445,62073,61632,61441,61448,61451,61452,61453,61457,61459,61461,61465,61468,61473,61478,61479,61480,61502,61507,61512,61515,61516,61517,61521,61522,61523,61524,61543,61544,61550,61552,61553,61556,61559,61560,61561,61563,61587,61589,61636,61637,61639,61641,61664,61671,61674,61683,61724,61732,61787,61931,62016,62017,62018,62019,62020,62087,62099,62212,62189,62810,63426,63650 --format lvgl --lv-include lvgl.h --lv-font-name ui_font_14 -o /Users/jakub/Documents/MeshCore/MeshCore/examples/companion_radio/ui-lvgl/fonts/ui_font_14.c + * Opts: --bpp 4 --size 14 --force-fast-kern-format --font /var/folders/1d/dgkmygls6x5gsb16pbn2mwlr0000gn/T//meshcore-ui-fonts/NotoSans-Medium.ttf -r 0x20-0x7E,0xA0-0x24F,0x370-0x3FF,0x400-0x52F,0x1E80-0x1E85,0x1E9E,0x1EF2-0x1EF3,0x2010-0x2027,0x2030,0x2039-0x203A,0x20A4-0x20BF,0x2116,0x2122 --font /var/folders/1d/dgkmygls6x5gsb16pbn2mwlr0000gn/T//meshcore-ui-fonts/fa.woff -r 62745,61463,62194,61612,61445,62073,61632,61774,61476,62405,61441,61448,61451,61452,61453,61457,61459,61461,61465,61468,61473,61478,61479,61480,61502,61507,61512,61515,61516,61517,61521,61522,61523,61524,61543,61544,61550,61552,61553,61556,61559,61560,61561,61563,61587,61589,61636,61637,61639,61641,61664,61671,61674,61683,61724,61732,61787,61931,62016,62017,62018,62019,62020,62087,62099,62212,62189,62810,63426,63650 --format lvgl --lv-include lvgl.h --lv-font-name ui_font_14 -o /Users/jakub/Documents/MeshCore/MeshCore/examples/companion_radio/ui-lvgl/fonts/ui_font_14.c ******************************************************************************/ #ifdef LV_LVGL_H_INCLUDE_SIMPLE @@ -7348,6 +7348,18 @@ static LV_ATTRIBUTE_LARGE_CONST const uint8_t glyph_bitmap[] = { 0x63, 0x0, 0xfc, 0x8e, 0xe2, 0x0, 0xff, 0xe6, 0x10, 0x5, 0xc0, 0x6, 0x0, 0x9, 0x80, 0x0, + /* U+F017 "" */ + 0x0, 0xff, 0xe2, 0x8c, 0x6f, 0xfb, 0x60, 0x40, + 0x39, 0x39, 0xc8, 0x0, 0x4f, 0xc8, 0x1, 0x25, + 0x80, 0x4a, 0xa0, 0xa, 0xd0, 0x1, 0x40, 0x1a, + 0xa8, 0x1, 0xa8, 0x18, 0x40, 0x3f, 0xc2, 0xd8, + 0x1, 0xff, 0xc0, 0xc6, 0x0, 0xff, 0xe0, 0x33, + 0x80, 0x70, 0x87, 0x18, 0x4, 0xe6, 0x1, 0xde, + 0x63, 0x0, 0x11, 0xd8, 0x7, 0xe, 0x4d, 0x0, + 0x56, 0x6a, 0x1, 0xe6, 0x10, 0x2, 0x98, 0x51, + 0x80, 0x7e, 0x3a, 0x0, 0xb1, 0xc0, 0x3c, 0xf8, + 0x1, 0xd1, 0xd5, 0x10, 0xae, 0x80, 0x8, + /* U+F019 "" */ 0x0, 0xf1, 0x98, 0x3, 0xfe, 0xbc, 0xc5, 0x80, 0x7f, 0x9c, 0x0, 0xe0, 0x1f, 0xfe, 0x58, 0xff, @@ -7383,6 +7395,16 @@ static LV_ATTRIBUTE_LARGE_CONST const uint8_t glyph_bitmap[] = { 0x5, 0xea, 0x2f, 0x76, 0x71, 0x3f, 0x10, 0x1, 0x8d, 0x6b, 0xa2, 0x1a, 0xf0, 0x40, 0x0, + /* U+F024 "" */ + 0x1, 0x0, 0xff, 0xe0, 0xb7, 0x70, 0x40, 0x3f, + 0xec, 0x1, 0x75, 0x51, 0x0, 0x71, 0x98, 0x38, + 0x1, 0x35, 0x4d, 0xe9, 0x79, 0xdc, 0xc0, 0x28, + 0x7, 0x85, 0xa1, 0x88, 0x3, 0xff, 0xfe, 0x1, + 0xff, 0xc4, 0x20, 0x8, 0xab, 0x7f, 0xa9, 0x48, + 0x4e, 0x34, 0x2, 0x75, 0x20, 0x15, 0xad, 0xec, + 0x70, 0xf, 0xfe, 0x28, 0x9e, 0x80, 0x7f, 0xf0, + 0x0, + /* U+F026 "" */ 0x0, 0xe2, 0x70, 0xc, 0x59, 0x0, 0x11, 0x60, 0x86, 0xff, 0xb4, 0x40, 0x4, 0x1, 0xff, 0xcf, @@ -7750,6 +7772,20 @@ static LV_ATTRIBUTE_LARGE_CONST const uint8_t glyph_bitmap[] = { 0xa4, 0x3, 0xff, 0x80, 0x60, 0x4a, 0x1, 0xff, 0xc0, 0x94, 0xe0, 0xf, 0x0, + /* U+F14E "" */ + 0x0, 0xff, 0xe2, 0x95, 0x77, 0xfb, 0x1c, 0x3, + 0xcf, 0xaa, 0x20, 0x3, 0x8d, 0x20, 0x9, 0xe0, + 0x3, 0xe2, 0xc1, 0x2, 0x80, 0xf, 0x1d, 0x10, + 0xd0, 0x58, 0x7, 0x26, 0x61, 0x4c, 0x10, 0x90, + 0x3, 0x5d, 0x8c, 0x1c, 0x40, 0x8, 0x40, 0x12, + 0x23, 0xa0, 0x24, 0x2, 0xe1, 0x0, 0xa0, 0x26, + 0xd4, 0x40, 0x2f, 0x20, 0x1, 0x98, 0x1d, 0x6c, + 0x3, 0x15, 0x0, 0x28, 0x5f, 0xa4, 0x40, 0x21, + 0x56, 0x20, 0x8e, 0x81, 0x0, 0xe8, 0x0, 0x78, + 0x88, 0x3, 0xe6, 0x60, 0x0, 0xbd, 0x40, 0x38, + 0x66, 0x40, 0x18, 0x6b, 0x65, 0xe2, 0xf9, 0x80, + 0x20, + /* U+F15B "" */ 0x35, 0x5e, 0x2, 0x0, 0xb2, 0xab, 0x8b, 0x50, 0x3, 0xfd, 0x68, 0x1, 0xfe, 0xb3, 0x0, 0xf0, @@ -7858,6 +7894,17 @@ static LV_ATTRIBUTE_LARGE_CONST const uint8_t glyph_bitmap[] = { 0x10, 0xc, 0x60, 0x18, 0xc0, 0x18, 0x9, 0x1, 0x24, 0x6, 0x3, 0x8, 0x67, 0xf3, 0x28, 0x0, + /* U+F2F2 "" */ + 0x0, 0xff, 0xe2, 0x7f, 0xe0, 0xf, 0xca, 0x0, + 0x50, 0xf, 0xd2, 0x0, 0x90, 0xf, 0x1e, 0xe0, + 0x3, 0x35, 0x75, 0x1, 0x30, 0x80, 0x88, 0x5, + 0x44, 0x63, 0x40, 0x16, 0x60, 0x3, 0x3b, 0x8, + 0x7, 0xf3, 0x68, 0x7, 0xfb, 0x4, 0x3, 0xfc, + 0x24, 0x1, 0x99, 0x80, 0x18, 0xb4, 0x3, 0x44, + 0x0, 0x36, 0xa2, 0x80, 0x7e, 0x54, 0xa, 0x40, + 0xf, 0x25, 0x0, 0x57, 0x48, 0x22, 0x4a, 0xb0, + 0x0, + /* U+F304 "" */ 0x0, 0xff, 0x84, 0x80, 0x3f, 0xf8, 0x9, 0xda, 0xa0, 0x1f, 0xe2, 0xb0, 0x5, 0x28, 0x7, 0xe4, @@ -7870,6 +7917,35 @@ static LV_ATTRIBUTE_LARGE_CONST const uint8_t glyph_bitmap[] = { 0x32, 0x58, 0x7, 0xe2, 0x0, 0x92, 0xc0, 0x3f, 0x99, 0x5e, 0x6c, 0x3, 0xf8, + /* U+F3C5 "" */ + 0x0, 0xc4, 0x40, 0xf, 0x9f, 0xb7, 0x5f, 0x22, + 0x1, 0x4c, 0x8, 0x4, 0xd8, 0x20, 0xac, 0x1, + 0x8, 0x0, 0xac, 0x24, 0x0, 0x9f, 0xd0, 0x0, + 0x51, 0x30, 0x4, 0x80, 0x1c, 0x80, 0x4, 0x1, + 0xfe, 0x73, 0x0, 0x48, 0x1, 0xc8, 0x0, 0xb2, + 0x0, 0x4f, 0xe8, 0x0, 0x28, 0xaa, 0x0, 0x42, + 0x0, 0x1b, 0x0, 0x48, 0x7, 0xa0, 0x40, 0x5, + 0x20, 0x19, 0x18, 0x3, 0x32, 0x0, 0x6, 0x80, + 0x3d, 0x42, 0x16, 0x20, 0x1e, 0x1a, 0x85, 0x0, + 0xc0, + + /* U+F519 "" */ + 0x2, 0x0, 0xff, 0xe0, 0x88, 0x81, 0xb5, 0x80, + 0x3f, 0xf8, 0x19, 0xc1, 0xa0, 0xe2, 0xc2, 0x0, + 0x42, 0x0, 0x2a, 0x2, 0x82, 0x29, 0xb, 0xcb, + 0x85, 0xdb, 0x4c, 0x2e, 0x43, 0x0, 0xc4, 0x40, + 0x20, 0x40, 0x80, 0x4, 0x2, 0x20, 0x38, 0x78, + 0x90, 0x10, 0x10, 0x10, 0x0, 0x80, 0x88, 0xc, + 0x1e, 0x82, 0x4b, 0xcc, 0x64, 0x40, 0xe0, 0xfd, + 0xc, 0x4, 0x92, 0x70, 0x10, 0x90, 0x93, 0x42, + 0x10, 0x1, 0x21, 0x9b, 0x54, 0x0, 0x2a, 0x69, + 0x1, 0x60, 0x15, 0xd8, 0x3, 0xd2, 0x3, 0x7e, + 0xc, 0x1, 0xff, 0x30, 0x12, 0x20, 0x0, 0xc0, + 0x1f, 0xca, 0x2f, 0xff, 0x70, 0x58, 0x7, 0xf4, + 0x84, 0x80, 0x42, 0xc4, 0x80, 0x1f, 0x11, 0xa8, + 0x80, 0x6b, 0xe, 0x0, 0xf8, 0xa6, 0x0, 0x38, + 0xa6, 0x0, 0x30, + /* U+F55A "" */ 0x0, 0xc2, 0xf1, 0x1f, 0xce, 0x80, 0x18, 0xbe, 0x1d, 0xff, 0xa2, 0xd0, 0x0, 0x78, 0x20, 0x11, @@ -8934,60 +9010,66 @@ static const lv_font_fmt_txt_glyph_dsc_t glyph_dsc[] = { {.bitmap_index = 38484, .adv_w = 224, .box_w = 14, .box_h = 15, .ofs_x = 0, .ofs_y = -2}, {.bitmap_index = 38571, .adv_w = 252, .box_w = 16, .box_h = 13, .ofs_x = 0, .ofs_y = -1}, {.bitmap_index = 38651, .adv_w = 224, .box_w = 14, .box_h = 15, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 38716, .adv_w = 252, .box_w = 16, .box_h = 11, .ofs_x = 0, .ofs_y = 0}, - {.bitmap_index = 38779, .adv_w = 224, .box_w = 14, .box_h = 15, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 38874, .adv_w = 112, .box_w = 7, .box_h = 12, .ofs_x = 0, .ofs_y = -1}, - {.bitmap_index = 38902, .adv_w = 168, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -1}, - {.bitmap_index = 38948, .adv_w = 252, .box_w = 16, .box_h = 14, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 39038, .adv_w = 224, .box_w = 14, .box_h = 11, .ofs_x = 0, .ofs_y = 0}, - {.bitmap_index = 39094, .adv_w = 154, .box_w = 10, .box_h = 15, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 39154, .adv_w = 196, .box_w = 10, .box_h = 14, .ofs_x = 1, .ofs_y = -2}, - {.bitmap_index = 39201, .adv_w = 196, .box_w = 13, .box_h = 15, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 39259, .adv_w = 196, .box_w = 13, .box_h = 13, .ofs_x = 0, .ofs_y = -1}, - {.bitmap_index = 39296, .adv_w = 196, .box_w = 13, .box_h = 13, .ofs_x = 0, .ofs_y = -1}, - {.bitmap_index = 39321, .adv_w = 196, .box_w = 10, .box_h = 14, .ofs_x = 1, .ofs_y = -2}, - {.bitmap_index = 39370, .adv_w = 196, .box_w = 14, .box_h = 13, .ofs_x = -1, .ofs_y = -1}, - {.bitmap_index = 39429, .adv_w = 140, .box_w = 9, .box_h = 13, .ofs_x = 0, .ofs_y = -1}, - {.bitmap_index = 39477, .adv_w = 140, .box_w = 9, .box_h = 13, .ofs_x = 0, .ofs_y = -1}, - {.bitmap_index = 39526, .adv_w = 196, .box_w = 13, .box_h = 13, .ofs_x = 0, .ofs_y = -1}, - {.bitmap_index = 39573, .adv_w = 196, .box_w = 13, .box_h = 4, .ofs_x = 0, .ofs_y = 3}, - {.bitmap_index = 39587, .adv_w = 252, .box_w = 16, .box_h = 11, .ofs_x = 0, .ofs_y = 0}, - {.bitmap_index = 39670, .adv_w = 280, .box_w = 18, .box_h = 15, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 39787, .adv_w = 252, .box_w = 17, .box_h = 15, .ofs_x = -1, .ofs_y = -2}, - {.bitmap_index = 39875, .adv_w = 224, .box_w = 14, .box_h = 13, .ofs_x = 0, .ofs_y = -1}, - {.bitmap_index = 39957, .adv_w = 196, .box_w = 13, .box_h = 8, .ofs_x = 0, .ofs_y = 1}, - {.bitmap_index = 40000, .adv_w = 196, .box_w = 13, .box_h = 8, .ofs_x = 0, .ofs_y = 1}, - {.bitmap_index = 40045, .adv_w = 280, .box_w = 18, .box_h = 11, .ofs_x = 0, .ofs_y = 0}, - {.bitmap_index = 40116, .adv_w = 224, .box_w = 14, .box_h = 11, .ofs_x = 0, .ofs_y = 0}, - {.bitmap_index = 40145, .adv_w = 224, .box_w = 14, .box_h = 15, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 40217, .adv_w = 224, .box_w = 15, .box_h = 15, .ofs_x = -1, .ofs_y = -2}, - {.bitmap_index = 40296, .adv_w = 217, .box_w = 14, .box_h = 15, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 40389, .adv_w = 280, .box_w = 18, .box_h = 13, .ofs_x = 0, .ofs_y = -1}, - {.bitmap_index = 40487, .adv_w = 196, .box_w = 13, .box_h = 13, .ofs_x = 0, .ofs_y = -1}, - {.bitmap_index = 40568, .adv_w = 196, .box_w = 13, .box_h = 15, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 40616, .adv_w = 196, .box_w = 13, .box_h = 13, .ofs_x = 0, .ofs_y = -1}, - {.bitmap_index = 40672, .adv_w = 196, .box_w = 13, .box_h = 12, .ofs_x = 0, .ofs_y = -1}, - {.bitmap_index = 40712, .adv_w = 224, .box_w = 14, .box_h = 11, .ofs_x = 0, .ofs_y = 0}, - {.bitmap_index = 40765, .adv_w = 140, .box_w = 10, .box_h = 15, .ofs_x = -1, .ofs_y = -2}, - {.bitmap_index = 40821, .adv_w = 196, .box_w = 13, .box_h = 15, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 40884, .adv_w = 196, .box_w = 13, .box_h = 15, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 40950, .adv_w = 252, .box_w = 16, .box_h = 11, .ofs_x = 0, .ofs_y = 0}, - {.bitmap_index = 41023, .adv_w = 224, .box_w = 16, .box_h = 15, .ofs_x = -1, .ofs_y = -2}, - {.bitmap_index = 41100, .adv_w = 168, .box_w = 11, .box_h = 15, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 41132, .adv_w = 280, .box_w = 18, .box_h = 13, .ofs_x = 0, .ofs_y = -1}, - {.bitmap_index = 41229, .adv_w = 280, .box_w = 18, .box_h = 10, .ofs_x = 0, .ofs_y = 0}, - {.bitmap_index = 41281, .adv_w = 280, .box_w = 18, .box_h = 10, .ofs_x = 0, .ofs_y = 0}, - {.bitmap_index = 41336, .adv_w = 280, .box_w = 18, .box_h = 10, .ofs_x = 0, .ofs_y = 0}, - {.bitmap_index = 41391, .adv_w = 280, .box_w = 18, .box_h = 10, .ofs_x = 0, .ofs_y = 0}, - {.bitmap_index = 41446, .adv_w = 280, .box_w = 18, .box_h = 10, .ofs_x = 0, .ofs_y = 0}, - {.bitmap_index = 41495, .adv_w = 252, .box_w = 16, .box_h = 13, .ofs_x = 0, .ofs_y = -1}, - {.bitmap_index = 41540, .adv_w = 280, .box_w = 18, .box_h = 12, .ofs_x = 0, .ofs_y = -1}, - {.bitmap_index = 41620, .adv_w = 196, .box_w = 12, .box_h = 15, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 41704, .adv_w = 196, .box_w = 13, .box_h = 15, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 41760, .adv_w = 224, .box_w = 15, .box_h = 15, .ofs_x = -1, .ofs_y = -2}, - {.bitmap_index = 41837, .adv_w = 280, .box_w = 18, .box_h = 11, .ofs_x = 0, .ofs_y = 0}, - {.bitmap_index = 41915, .adv_w = 168, .box_w = 11, .box_h = 15, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 41951, .adv_w = 225, .box_w = 15, .box_h = 10, .ofs_x = 0, .ofs_y = 0} + {.bitmap_index = 38730, .adv_w = 224, .box_w = 14, .box_h = 15, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 38795, .adv_w = 252, .box_w = 16, .box_h = 11, .ofs_x = 0, .ofs_y = 0}, + {.bitmap_index = 38858, .adv_w = 224, .box_w = 14, .box_h = 15, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 38953, .adv_w = 224, .box_w = 15, .box_h = 15, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 39010, .adv_w = 112, .box_w = 7, .box_h = 12, .ofs_x = 0, .ofs_y = -1}, + {.bitmap_index = 39038, .adv_w = 168, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -1}, + {.bitmap_index = 39084, .adv_w = 252, .box_w = 16, .box_h = 14, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 39174, .adv_w = 224, .box_w = 14, .box_h = 11, .ofs_x = 0, .ofs_y = 0}, + {.bitmap_index = 39230, .adv_w = 154, .box_w = 10, .box_h = 15, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 39290, .adv_w = 196, .box_w = 10, .box_h = 14, .ofs_x = 1, .ofs_y = -2}, + {.bitmap_index = 39337, .adv_w = 196, .box_w = 13, .box_h = 15, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 39395, .adv_w = 196, .box_w = 13, .box_h = 13, .ofs_x = 0, .ofs_y = -1}, + {.bitmap_index = 39432, .adv_w = 196, .box_w = 13, .box_h = 13, .ofs_x = 0, .ofs_y = -1}, + {.bitmap_index = 39457, .adv_w = 196, .box_w = 10, .box_h = 14, .ofs_x = 1, .ofs_y = -2}, + {.bitmap_index = 39506, .adv_w = 196, .box_w = 14, .box_h = 13, .ofs_x = -1, .ofs_y = -1}, + {.bitmap_index = 39565, .adv_w = 140, .box_w = 9, .box_h = 13, .ofs_x = 0, .ofs_y = -1}, + {.bitmap_index = 39613, .adv_w = 140, .box_w = 9, .box_h = 13, .ofs_x = 0, .ofs_y = -1}, + {.bitmap_index = 39662, .adv_w = 196, .box_w = 13, .box_h = 13, .ofs_x = 0, .ofs_y = -1}, + {.bitmap_index = 39709, .adv_w = 196, .box_w = 13, .box_h = 4, .ofs_x = 0, .ofs_y = 3}, + {.bitmap_index = 39723, .adv_w = 252, .box_w = 16, .box_h = 11, .ofs_x = 0, .ofs_y = 0}, + {.bitmap_index = 39806, .adv_w = 280, .box_w = 18, .box_h = 15, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 39923, .adv_w = 252, .box_w = 17, .box_h = 15, .ofs_x = -1, .ofs_y = -2}, + {.bitmap_index = 40011, .adv_w = 224, .box_w = 14, .box_h = 13, .ofs_x = 0, .ofs_y = -1}, + {.bitmap_index = 40093, .adv_w = 196, .box_w = 13, .box_h = 8, .ofs_x = 0, .ofs_y = 1}, + {.bitmap_index = 40136, .adv_w = 196, .box_w = 13, .box_h = 8, .ofs_x = 0, .ofs_y = 1}, + {.bitmap_index = 40181, .adv_w = 280, .box_w = 18, .box_h = 11, .ofs_x = 0, .ofs_y = 0}, + {.bitmap_index = 40252, .adv_w = 224, .box_w = 14, .box_h = 11, .ofs_x = 0, .ofs_y = 0}, + {.bitmap_index = 40281, .adv_w = 224, .box_w = 14, .box_h = 15, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 40353, .adv_w = 224, .box_w = 15, .box_h = 15, .ofs_x = -1, .ofs_y = -2}, + {.bitmap_index = 40432, .adv_w = 217, .box_w = 14, .box_h = 15, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 40525, .adv_w = 280, .box_w = 18, .box_h = 13, .ofs_x = 0, .ofs_y = -1}, + {.bitmap_index = 40623, .adv_w = 196, .box_w = 13, .box_h = 13, .ofs_x = 0, .ofs_y = -1}, + {.bitmap_index = 40704, .adv_w = 196, .box_w = 13, .box_h = 15, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 40752, .adv_w = 196, .box_w = 13, .box_h = 13, .ofs_x = 0, .ofs_y = -1}, + {.bitmap_index = 40808, .adv_w = 196, .box_w = 13, .box_h = 12, .ofs_x = 0, .ofs_y = -1}, + {.bitmap_index = 40848, .adv_w = 224, .box_w = 14, .box_h = 11, .ofs_x = 0, .ofs_y = 0}, + {.bitmap_index = 40901, .adv_w = 140, .box_w = 10, .box_h = 15, .ofs_x = -1, .ofs_y = -2}, + {.bitmap_index = 40957, .adv_w = 196, .box_w = 13, .box_h = 15, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 41020, .adv_w = 196, .box_w = 13, .box_h = 15, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 41086, .adv_w = 252, .box_w = 16, .box_h = 11, .ofs_x = 0, .ofs_y = 0}, + {.bitmap_index = 41159, .adv_w = 224, .box_w = 16, .box_h = 15, .ofs_x = -1, .ofs_y = -2}, + {.bitmap_index = 41236, .adv_w = 217, .box_w = 14, .box_h = 15, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 41325, .adv_w = 168, .box_w = 11, .box_h = 15, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 41357, .adv_w = 280, .box_w = 18, .box_h = 13, .ofs_x = 0, .ofs_y = -1}, + {.bitmap_index = 41454, .adv_w = 280, .box_w = 18, .box_h = 10, .ofs_x = 0, .ofs_y = 0}, + {.bitmap_index = 41506, .adv_w = 280, .box_w = 18, .box_h = 10, .ofs_x = 0, .ofs_y = 0}, + {.bitmap_index = 41561, .adv_w = 280, .box_w = 18, .box_h = 10, .ofs_x = 0, .ofs_y = 0}, + {.bitmap_index = 41616, .adv_w = 280, .box_w = 18, .box_h = 10, .ofs_x = 0, .ofs_y = 0}, + {.bitmap_index = 41671, .adv_w = 280, .box_w = 18, .box_h = 10, .ofs_x = 0, .ofs_y = 0}, + {.bitmap_index = 41720, .adv_w = 252, .box_w = 16, .box_h = 13, .ofs_x = 0, .ofs_y = -1}, + {.bitmap_index = 41765, .adv_w = 280, .box_w = 18, .box_h = 12, .ofs_x = 0, .ofs_y = -1}, + {.bitmap_index = 41845, .adv_w = 196, .box_w = 12, .box_h = 15, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 41929, .adv_w = 196, .box_w = 13, .box_h = 15, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 41985, .adv_w = 196, .box_w = 12, .box_h = 15, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 42050, .adv_w = 224, .box_w = 15, .box_h = 15, .ofs_x = -1, .ofs_y = -2}, + {.bitmap_index = 42127, .adv_w = 168, .box_w = 11, .box_h = 15, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 42200, .adv_w = 280, .box_w = 18, .box_h = 15, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 42315, .adv_w = 280, .box_w = 18, .box_h = 11, .ofs_x = 0, .ofs_y = 0}, + {.bitmap_index = 42393, .adv_w = 168, .box_w = 11, .box_h = 15, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 42429, .adv_w = 225, .box_w = 15, .box_h = 10, .ofs_x = 0, .ofs_y = 0} }; /*--------------------- @@ -9012,14 +9094,14 @@ static const uint16_t unicode_list_10[] = { static const uint16_t unicode_list_12[] = { 0x0, 0xc, 0xceeb, 0xceef, 0xcef2, 0xcef5, 0xcef6, 0xcef7, - 0xcefb, 0xcefd, 0xceff, 0xcf03, 0xcf06, 0xcf0b, 0xcf10, 0xcf11, - 0xcf12, 0xcf28, 0xcf2d, 0xcf32, 0xcf35, 0xcf36, 0xcf37, 0xcf3b, - 0xcf3c, 0xcf3d, 0xcf3e, 0xcf51, 0xcf52, 0xcf58, 0xcf5a, 0xcf5b, - 0xcf5e, 0xcf61, 0xcf62, 0xcf63, 0xcf65, 0xcf7d, 0xcf7f, 0xcf96, - 0xcfaa, 0xcfae, 0xcfaf, 0xcfb1, 0xcfb3, 0xcfca, 0xcfd1, 0xcfd4, - 0xcfdd, 0xd006, 0xd00e, 0xd045, 0xd0d5, 0xd12a, 0xd12b, 0xd12c, - 0xd12d, 0xd12e, 0xd163, 0xd171, 0xd17d, 0xd1d7, 0xd1ee, 0xd444, - 0xd6ac, 0xd78c + 0xcefb, 0xcefd, 0xceff, 0xcf01, 0xcf03, 0xcf06, 0xcf0b, 0xcf0e, + 0xcf10, 0xcf11, 0xcf12, 0xcf28, 0xcf2d, 0xcf32, 0xcf35, 0xcf36, + 0xcf37, 0xcf3b, 0xcf3c, 0xcf3d, 0xcf3e, 0xcf51, 0xcf52, 0xcf58, + 0xcf5a, 0xcf5b, 0xcf5e, 0xcf61, 0xcf62, 0xcf63, 0xcf65, 0xcf7d, + 0xcf7f, 0xcf96, 0xcfaa, 0xcfae, 0xcfaf, 0xcfb1, 0xcfb3, 0xcfca, + 0xcfd1, 0xcfd4, 0xcfdd, 0xd006, 0xd00e, 0xd038, 0xd045, 0xd0d5, + 0xd12a, 0xd12b, 0xd12c, 0xd12d, 0xd12e, 0xd163, 0xd171, 0xd17d, + 0xd1d7, 0xd1dc, 0xd1ee, 0xd2af, 0xd403, 0xd444, 0xd6ac, 0xd78c }; /*Collect the unicode lists and glyph_id offsets*/ @@ -9075,7 +9157,7 @@ static const lv_font_fmt_txt_cmap_t cmaps[] = }, { .range_start = 8470, .range_length = 55181, .glyph_id_start = 1017, - .unicode_list = unicode_list_12, .glyph_id_ofs_list = NULL, .list_length = 66, .type = LV_FONT_FMT_TXT_CMAP_SPARSE_TINY + .unicode_list = unicode_list_12, .glyph_id_ofs_list = NULL, .list_length = 72, .type = LV_FONT_FMT_TXT_CMAP_SPARSE_TINY } }; @@ -9222,7 +9304,8 @@ static const uint8_t kern_left_class_mapping[] = 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0 + 0, 0, 0, 0, 0, 0, 0, 0, + 0 }; /*Map glyph_ids to kern right classes*/ @@ -9363,7 +9446,8 @@ static const uint8_t kern_right_class_mapping[] = 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0 + 0, 0, 0, 0, 0, 0, 0, 0, + 0 }; /*Kern values between classes*/ diff --git a/examples/companion_radio/ui-lvgl/fonts/ui_font_16.c b/examples/companion_radio/ui-lvgl/fonts/ui_font_16.c index 2b3dc611..fefabd90 100644 --- a/examples/companion_radio/ui-lvgl/fonts/ui_font_16.c +++ b/examples/companion_radio/ui-lvgl/fonts/ui_font_16.c @@ -1,7 +1,7 @@ /******************************************************************************* * Size: 16 px * Bpp: 4 - * Opts: --bpp 4 --size 16 --force-fast-kern-format --font /var/folders/1d/dgkmygls6x5gsb16pbn2mwlr0000gn/T//meshcore-ui-fonts/NotoSans-Medium.ttf -r 0x20-0x7E,0xA0-0x24F,0x370-0x3FF,0x400-0x52F,0x1E80-0x1E85,0x1E9E,0x1EF2-0x1EF3,0x2010-0x2027,0x2030,0x2039-0x203A,0x20A4-0x20BF,0x2116,0x2122 --font /var/folders/1d/dgkmygls6x5gsb16pbn2mwlr0000gn/T//meshcore-ui-fonts/fa.woff -r 61612,61445,62073,61632,61441,61448,61451,61452,61453,61457,61459,61461,61465,61468,61473,61478,61479,61480,61502,61507,61512,61515,61516,61517,61521,61522,61523,61524,61543,61544,61550,61552,61553,61556,61559,61560,61561,61563,61587,61589,61636,61637,61639,61641,61664,61671,61674,61683,61724,61732,61787,61931,62016,62017,62018,62019,62020,62087,62099,62212,62189,62810,63426,63650 --format lvgl --lv-include lvgl.h --lv-font-name ui_font_16 -o /Users/jakub/Documents/MeshCore/MeshCore/examples/companion_radio/ui-lvgl/fonts/ui_font_16.c + * Opts: --bpp 4 --size 16 --force-fast-kern-format --font /var/folders/1d/dgkmygls6x5gsb16pbn2mwlr0000gn/T//meshcore-ui-fonts/NotoSans-Medium.ttf -r 0x20-0x7E,0xA0-0x24F,0x370-0x3FF,0x400-0x52F,0x1E80-0x1E85,0x1E9E,0x1EF2-0x1EF3,0x2010-0x2027,0x2030,0x2039-0x203A,0x20A4-0x20BF,0x2116,0x2122 --font /var/folders/1d/dgkmygls6x5gsb16pbn2mwlr0000gn/T//meshcore-ui-fonts/fa.woff -r 62745,61463,62194,61612,61445,62073,61632,61774,61476,62405,61441,61448,61451,61452,61453,61457,61459,61461,61465,61468,61473,61478,61479,61480,61502,61507,61512,61515,61516,61517,61521,61522,61523,61524,61543,61544,61550,61552,61553,61556,61559,61560,61561,61563,61587,61589,61636,61637,61639,61641,61664,61671,61674,61683,61724,61732,61787,61931,62016,62017,62018,62019,62020,62087,62099,62212,62189,62810,63426,63650 --format lvgl --lv-include lvgl.h --lv-font-name ui_font_16 -o /Users/jakub/Documents/MeshCore/MeshCore/examples/companion_radio/ui-lvgl/fonts/ui_font_16.c ******************************************************************************/ #ifdef LV_LVGL_H_INCLUDE_SIMPLE @@ -8411,6 +8411,21 @@ static LV_ATTRIBUTE_LARGE_CONST const uint8_t glyph_bitmap[] = { 0x84, 0x3, 0xff, 0xa6, 0x20, 0x17, 0x0, 0x38, 0x2, 0x10, 0x8, + /* U+F017 "" */ + 0x0, 0xc3, 0x19, 0xdc, 0xc8, 0x10, 0xf, 0x9f, + 0x9c, 0xc4, 0x46, 0xfc, 0xe0, 0x1d, 0x30, 0x1, + 0xfa, 0x24, 0x2, 0x66, 0x0, 0x6f, 0xf0, 0x6, + 0x66, 0x0, 0x20, 0x3, 0x8, 0x0, 0x40, 0x34, + 0x2, 0x88, 0x7, 0xff, 0x0, 0x57, 0x0, 0x3f, + 0xf8, 0x58, 0x60, 0x1f, 0x8c, 0x3, 0x8c, 0x3, + 0xc2, 0x0, 0xc4, 0x0, 0xe2, 0x0, 0xf5, 0x88, + 0xac, 0x40, 0x22, 0xb0, 0xf, 0x27, 0x9b, 0x8, + 0x5, 0x66, 0x60, 0xf, 0xe, 0x48, 0x4, 0x66, + 0xe, 0x0, 0xff, 0xe0, 0x70, 0x0, 0xb0, 0x40, + 0x3f, 0xe, 0x10, 0x4, 0x78, 0x80, 0x1e, 0x4c, + 0x30, 0xe, 0x2b, 0xe9, 0x77, 0x4f, 0x59, 0x0, + 0x40, + /* U+F019 "" */ 0x0, 0xf6, 0xff, 0xb4, 0x3, 0xff, 0x80, 0x40, 0x2, 0x0, 0xff, 0xfe, 0x27, 0xfc, 0x1, 0xbf, @@ -8447,6 +8462,16 @@ static LV_ATTRIBUTE_LARGE_CONST const uint8_t glyph_bitmap[] = { 0x40, 0xa, 0xec, 0x40, 0x91, 0x2a, 0x3, 0x4e, 0x1, 0xc9, 0xb4, 0xa6, 0x49, 0x3c, 0xa0, 0x10, + /* U+F024 "" */ + 0x0, 0xff, 0xe2, 0x1f, 0x70, 0xc0, 0x3f, 0xf8, + 0x10, 0x22, 0x80, 0xf, 0xfe, 0x1, 0x80, 0x17, + 0x7f, 0xad, 0xc8, 0x0, 0x4f, 0xb9, 0x0, 0x1, + 0x20, 0x14, 0x8d, 0xff, 0x6c, 0x11, 0x80, 0x7f, + 0xff, 0xc0, 0x3f, 0xfb, 0xa4, 0x8c, 0x80, 0x1f, + 0x48, 0x5, 0xba, 0xb9, 0xbf, 0xb7, 0x56, 0x9e, + 0x60, 0x8, 0x80, 0x39, 0x22, 0xa5, 0x84, 0x3, + 0xff, 0x8c, 0x44, 0x0, 0xff, 0xe0, 0x80, + /* U+F026 "" */ 0x0, 0xff, 0xe1, 0x46, 0x80, 0x74, 0x39, 0x0, 0x68, 0x70, 0x6, 0xff, 0xce, 0x1, 0x10, 0x7, @@ -8851,6 +8876,21 @@ static LV_ATTRIBUTE_LARGE_CONST const uint8_t glyph_bitmap[] = { 0x38, 0x39, 0x0, 0x7f, 0xf0, 0xa3, 0xe0, 0x3, 0xe0, + /* U+F14E "" */ + 0x0, 0xc2, 0xd5, 0x75, 0x6, 0x1, 0xf9, 0xfa, + 0x55, 0x15, 0xf3, 0x4, 0x1, 0xd5, 0x0, 0x1f, + 0x1e, 0x98, 0x5, 0xa, 0x1, 0xfe, 0xd2, 0x2, + 0x70, 0xf, 0x93, 0x24, 0x6, 0x2, 0x80, 0x3c, + 0xdb, 0x67, 0xe0, 0x5, 0x15, 0x0, 0xc3, 0xb2, + 0x40, 0x2c, 0x1, 0x21, 0x0, 0x69, 0x3e, 0x80, + 0x90, 0xc, 0x40, 0x1c, 0xc0, 0x19, 0x80, 0x38, + 0x80, 0x27, 0x11, 0x74, 0x40, 0x40, 0x31, 0x28, + 0x5, 0x20, 0x6, 0xd7, 0x0, 0xe4, 0xa0, 0x1, + 0x3, 0xf4, 0x90, 0x7, 0x28, 0x93, 0x81, 0x74, + 0x8, 0x7, 0x86, 0x0, 0x10, 0xa0, 0x1f, 0xed, + 0x20, 0xa, 0xa0, 0x3, 0xe3, 0xd3, 0x0, 0xe7, + 0xe9, 0x54, 0x57, 0xcc, 0x10, 0x4, + /* U+F15B "" */ 0xdf, 0xff, 0x6, 0x88, 0x0, 0x80, 0x3e, 0x2c, 0x10, 0xf, 0xf1, 0x60, 0x80, 0x7f, 0x8b, 0x0, @@ -8962,6 +9002,19 @@ static LV_ATTRIBUTE_LARGE_CONST const uint8_t glyph_bitmap[] = { 0x0, 0x88, 0x4, 0x40, 0x1e, 0x66, 0x3, 0x30, 0x19, 0x80, 0x19, 0xc0, 0x3f, 0xce, 0x0, + /* U+F2F2 "" */ + 0x0, 0xc7, 0xff, 0x8c, 0x3, 0xff, 0x94, 0x7e, + 0x0, 0xf3, 0x0, 0xf8, 0x5f, 0x0, 0x18, 0xe0, + 0x28, 0x1, 0x1f, 0xc1, 0x80, 0xe, 0x3b, 0x2d, + 0x0, 0xb0, 0x40, 0xe, 0xe0, 0x0, 0x91, 0x14, + 0x24, 0x3, 0x44, 0x0, 0x33, 0x9, 0x20, 0x7, + 0xf9, 0x9, 0x0, 0x3f, 0xf8, 0x8, 0x20, 0x1f, + 0xfc, 0x1, 0x10, 0x7, 0xff, 0x0, 0x50, 0x3, + 0xbf, 0xc0, 0x1c, 0x84, 0x60, 0x1f, 0xe4, 0x20, + 0xe1, 0x0, 0xfc, 0x32, 0x0, 0x2d, 0x10, 0xf, + 0xe, 0x90, 0x4, 0x7d, 0x4, 0x0, 0x38, 0xe3, + 0x0, 0x0, + /* U+F304 "" */ 0x0, 0xff, 0xe0, 0x3f, 0xd0, 0x7, 0xff, 0x5, 0xe0, 0x16, 0xc0, 0x3f, 0xf8, 0x14, 0x1, 0x25, @@ -8976,6 +9029,38 @@ static LV_ATTRIBUTE_LARGE_CONST const uint8_t glyph_bitmap[] = { 0x1a, 0x1c, 0x3, 0xfc, 0x62, 0x49, 0x2e, 0x1, 0xff, 0x6f, 0x6d, 0xb0, 0x7, 0xfc, + /* U+F3C5 "" */ + 0x0, 0x9f, 0x3f, 0xd8, 0xe0, 0x18, 0x76, 0xc, + 0x0, 0x71, 0xa2, 0x0, 0xc2, 0x0, 0xf1, 0x60, + 0x39, 0x0, 0x4c, 0xc0, 0x8, 0x9e, 0xc0, 0x2d, + 0x99, 0x68, 0x5, 0x66, 0x0, 0x62, 0x0, 0x13, + 0x0, 0xc, 0x3, 0xff, 0x80, 0x60, 0x6, 0x20, + 0x1, 0x30, 0x0, 0xe8, 0x2, 0xd9, 0x96, 0x80, + 0x54, 0xc4, 0x1, 0x33, 0x0, 0x22, 0x60, 0xe0, + 0xf, 0xdc, 0x0, 0x37, 0x0, 0xf3, 0x98, 0x5, + 0x6, 0x1, 0x8e, 0x0, 0x3b, 0x80, 0x37, 0x0, + 0x78, 0xa0, 0x1, 0x4, 0x1, 0xf3, 0xa2, 0x1c, + 0x3, 0x0, + + /* U+F519 "" */ + 0x3f, 0xc0, 0xf, 0xfe, 0x16, 0x79, 0xd0, 0x58, + 0x7, 0xff, 0x9, 0x2, 0x90, 0x14, 0x2f, 0xc8, + 0x13, 0xb8, 0x80, 0x5f, 0x61, 0x40, 0x84, 0x4, + 0xa, 0x6, 0x14, 0x22, 0xa0, 0x30, 0x50, 0x20, + 0x20, 0xf, 0xfe, 0x28, 0x80, 0x8, 0xc, 0x14, + 0xc, 0xc, 0x0, 0x60, 0x60, 0xa0, 0x40, 0x48, + 0x8, 0x17, 0xe4, 0x64, 0x22, 0x23, 0x2f, 0xb0, + 0xb0, 0x4a, 0xb, 0x0, 0xd2, 0xc, 0xc0, 0x90, + 0xc, 0x81, 0x47, 0xf8, 0x1, 0xa, 0x85, 0x50, + 0x14, 0x40, 0x2c, 0xf3, 0x0, 0xf3, 0x2, 0x11, + 0x10, 0x18, 0x3, 0xff, 0x81, 0x40, 0x9f, 0xe4, + 0xa, 0x0, 0xff, 0x90, 0x80, 0x3c, 0x48, 0x1, + 0xfe, 0xf0, 0x8f, 0xfd, 0x1, 0xe0, 0x1f, 0xc4, + 0x80, 0xc0, 0x19, 0x81, 0x8, 0x3, 0xf5, 0x3, + 0x8, 0x6, 0x16, 0xa, 0x0, 0xfd, 0xc3, 0x40, + 0x1e, 0xa1, 0xe0, 0xf, 0xcd, 0xc6, 0x1, 0xe3, + 0xe6, 0x0, 0xc0, + /* U+F55A "" */ 0x0, 0xc3, 0x7f, 0xff, 0xfc, 0xe, 0x40, 0x8, 0x71, 0x0, 0x3f, 0xf8, 0x3, 0x40, 0x1, 0xc2, @@ -10043,60 +10128,66 @@ static const lv_font_fmt_txt_glyph_dsc_t glyph_dsc[] = { {.bitmap_index = 46964, .adv_w = 256, .box_w = 16, .box_h = 16, .ofs_x = 0, .ofs_y = -2}, {.bitmap_index = 47066, .adv_w = 288, .box_w = 18, .box_h = 14, .ofs_x = 0, .ofs_y = -1}, {.bitmap_index = 47157, .adv_w = 256, .box_w = 16, .box_h = 16, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 47225, .adv_w = 288, .box_w = 18, .box_h = 12, .ofs_x = 0, .ofs_y = 0}, - {.bitmap_index = 47288, .adv_w = 256, .box_w = 16, .box_h = 16, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 47392, .adv_w = 128, .box_w = 8, .box_h = 14, .ofs_x = 0, .ofs_y = -1}, - {.bitmap_index = 47423, .adv_w = 192, .box_w = 12, .box_h = 14, .ofs_x = 0, .ofs_y = -1}, - {.bitmap_index = 47474, .adv_w = 288, .box_w = 18, .box_h = 16, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 47581, .adv_w = 256, .box_w = 16, .box_h = 12, .ofs_x = 0, .ofs_y = 0}, - {.bitmap_index = 47648, .adv_w = 176, .box_w = 11, .box_h = 16, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 47715, .adv_w = 224, .box_w = 10, .box_h = 16, .ofs_x = 2, .ofs_y = -2}, - {.bitmap_index = 47767, .adv_w = 224, .box_w = 14, .box_h = 18, .ofs_x = 0, .ofs_y = -3}, - {.bitmap_index = 47839, .adv_w = 224, .box_w = 14, .box_h = 15, .ofs_x = 0, .ofs_y = -1}, - {.bitmap_index = 47868, .adv_w = 224, .box_w = 14, .box_h = 14, .ofs_x = 0, .ofs_y = -1}, - {.bitmap_index = 47888, .adv_w = 224, .box_w = 10, .box_h = 16, .ofs_x = 2, .ofs_y = -2}, - {.bitmap_index = 47940, .adv_w = 224, .box_w = 16, .box_h = 14, .ofs_x = -1, .ofs_y = -1}, - {.bitmap_index = 48011, .adv_w = 160, .box_w = 10, .box_h = 14, .ofs_x = 0, .ofs_y = -1}, - {.bitmap_index = 48068, .adv_w = 160, .box_w = 10, .box_h = 14, .ofs_x = 0, .ofs_y = -1}, - {.bitmap_index = 48125, .adv_w = 224, .box_w = 14, .box_h = 14, .ofs_x = 0, .ofs_y = -1}, - {.bitmap_index = 48170, .adv_w = 224, .box_w = 14, .box_h = 4, .ofs_x = 0, .ofs_y = 4}, - {.bitmap_index = 48187, .adv_w = 288, .box_w = 18, .box_h = 12, .ofs_x = 0, .ofs_y = 0}, - {.bitmap_index = 48286, .adv_w = 320, .box_w = 20, .box_h = 16, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 48420, .adv_w = 288, .box_w = 20, .box_h = 16, .ofs_x = -1, .ofs_y = -2}, - {.bitmap_index = 48532, .adv_w = 256, .box_w = 16, .box_h = 16, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 48632, .adv_w = 224, .box_w = 14, .box_h = 10, .ofs_x = 0, .ofs_y = 1}, - {.bitmap_index = 48687, .adv_w = 224, .box_w = 14, .box_h = 10, .ofs_x = 0, .ofs_y = 1}, - {.bitmap_index = 48744, .adv_w = 320, .box_w = 20, .box_h = 14, .ofs_x = 0, .ofs_y = -1}, - {.bitmap_index = 48832, .adv_w = 256, .box_w = 16, .box_h = 12, .ofs_x = 0, .ofs_y = 0}, - {.bitmap_index = 48861, .adv_w = 256, .box_w = 16, .box_h = 16, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 48929, .adv_w = 256, .box_w = 17, .box_h = 17, .ofs_x = -1, .ofs_y = -2}, - {.bitmap_index = 49023, .adv_w = 248, .box_w = 16, .box_h = 16, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 49131, .adv_w = 320, .box_w = 20, .box_h = 14, .ofs_x = 0, .ofs_y = -1}, - {.bitmap_index = 49241, .adv_w = 224, .box_w = 15, .box_h = 14, .ofs_x = 0, .ofs_y = -1}, - {.bitmap_index = 49332, .adv_w = 224, .box_w = 14, .box_h = 16, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 49379, .adv_w = 224, .box_w = 14, .box_h = 14, .ofs_x = 0, .ofs_y = -1}, - {.bitmap_index = 49431, .adv_w = 224, .box_w = 14, .box_h = 14, .ofs_x = 0, .ofs_y = -1}, - {.bitmap_index = 49486, .adv_w = 256, .box_w = 16, .box_h = 12, .ofs_x = 0, .ofs_y = 0}, - {.bitmap_index = 49545, .adv_w = 160, .box_w = 12, .box_h = 16, .ofs_x = -1, .ofs_y = -2}, - {.bitmap_index = 49613, .adv_w = 224, .box_w = 14, .box_h = 16, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 49674, .adv_w = 224, .box_w = 14, .box_h = 16, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 49744, .adv_w = 288, .box_w = 18, .box_h = 12, .ofs_x = 0, .ofs_y = 0}, - {.bitmap_index = 49810, .adv_w = 256, .box_w = 18, .box_h = 18, .ofs_x = -1, .ofs_y = -3}, - {.bitmap_index = 49899, .adv_w = 192, .box_w = 12, .box_h = 16, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 49932, .adv_w = 320, .box_w = 20, .box_h = 15, .ofs_x = 0, .ofs_y = -1}, - {.bitmap_index = 50045, .adv_w = 320, .box_w = 20, .box_h = 10, .ofs_x = 0, .ofs_y = 1}, - {.bitmap_index = 50090, .adv_w = 320, .box_w = 20, .box_h = 10, .ofs_x = 0, .ofs_y = 1}, - {.bitmap_index = 50134, .adv_w = 320, .box_w = 20, .box_h = 10, .ofs_x = 0, .ofs_y = 1}, - {.bitmap_index = 50178, .adv_w = 320, .box_w = 20, .box_h = 10, .ofs_x = 0, .ofs_y = 1}, - {.bitmap_index = 50224, .adv_w = 320, .box_w = 20, .box_h = 10, .ofs_x = 0, .ofs_y = 1}, - {.bitmap_index = 50264, .adv_w = 288, .box_w = 18, .box_h = 16, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 50323, .adv_w = 320, .box_w = 21, .box_h = 14, .ofs_x = 0, .ofs_y = -1}, - {.bitmap_index = 50424, .adv_w = 224, .box_w = 12, .box_h = 16, .ofs_x = 1, .ofs_y = -2}, - {.bitmap_index = 50520, .adv_w = 224, .box_w = 14, .box_h = 16, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 50575, .adv_w = 256, .box_w = 17, .box_h = 17, .ofs_x = -1, .ofs_y = -3}, - {.bitmap_index = 50669, .adv_w = 320, .box_w = 20, .box_h = 12, .ofs_x = 0, .ofs_y = 0}, - {.bitmap_index = 50758, .adv_w = 192, .box_w = 12, .box_h = 16, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 50797, .adv_w = 258, .box_w = 17, .box_h = 11, .ofs_x = 0, .ofs_y = 1} + {.bitmap_index = 47254, .adv_w = 256, .box_w = 16, .box_h = 16, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 47322, .adv_w = 288, .box_w = 18, .box_h = 12, .ofs_x = 0, .ofs_y = 0}, + {.bitmap_index = 47385, .adv_w = 256, .box_w = 16, .box_h = 16, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 47489, .adv_w = 256, .box_w = 16, .box_h = 17, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 47552, .adv_w = 128, .box_w = 8, .box_h = 14, .ofs_x = 0, .ofs_y = -1}, + {.bitmap_index = 47583, .adv_w = 192, .box_w = 12, .box_h = 14, .ofs_x = 0, .ofs_y = -1}, + {.bitmap_index = 47634, .adv_w = 288, .box_w = 18, .box_h = 16, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 47741, .adv_w = 256, .box_w = 16, .box_h = 12, .ofs_x = 0, .ofs_y = 0}, + {.bitmap_index = 47808, .adv_w = 176, .box_w = 11, .box_h = 16, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 47875, .adv_w = 224, .box_w = 10, .box_h = 16, .ofs_x = 2, .ofs_y = -2}, + {.bitmap_index = 47927, .adv_w = 224, .box_w = 14, .box_h = 18, .ofs_x = 0, .ofs_y = -3}, + {.bitmap_index = 47999, .adv_w = 224, .box_w = 14, .box_h = 15, .ofs_x = 0, .ofs_y = -1}, + {.bitmap_index = 48028, .adv_w = 224, .box_w = 14, .box_h = 14, .ofs_x = 0, .ofs_y = -1}, + {.bitmap_index = 48048, .adv_w = 224, .box_w = 10, .box_h = 16, .ofs_x = 2, .ofs_y = -2}, + {.bitmap_index = 48100, .adv_w = 224, .box_w = 16, .box_h = 14, .ofs_x = -1, .ofs_y = -1}, + {.bitmap_index = 48171, .adv_w = 160, .box_w = 10, .box_h = 14, .ofs_x = 0, .ofs_y = -1}, + {.bitmap_index = 48228, .adv_w = 160, .box_w = 10, .box_h = 14, .ofs_x = 0, .ofs_y = -1}, + {.bitmap_index = 48285, .adv_w = 224, .box_w = 14, .box_h = 14, .ofs_x = 0, .ofs_y = -1}, + {.bitmap_index = 48330, .adv_w = 224, .box_w = 14, .box_h = 4, .ofs_x = 0, .ofs_y = 4}, + {.bitmap_index = 48347, .adv_w = 288, .box_w = 18, .box_h = 12, .ofs_x = 0, .ofs_y = 0}, + {.bitmap_index = 48446, .adv_w = 320, .box_w = 20, .box_h = 16, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 48580, .adv_w = 288, .box_w = 20, .box_h = 16, .ofs_x = -1, .ofs_y = -2}, + {.bitmap_index = 48692, .adv_w = 256, .box_w = 16, .box_h = 16, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 48792, .adv_w = 224, .box_w = 14, .box_h = 10, .ofs_x = 0, .ofs_y = 1}, + {.bitmap_index = 48847, .adv_w = 224, .box_w = 14, .box_h = 10, .ofs_x = 0, .ofs_y = 1}, + {.bitmap_index = 48904, .adv_w = 320, .box_w = 20, .box_h = 14, .ofs_x = 0, .ofs_y = -1}, + {.bitmap_index = 48992, .adv_w = 256, .box_w = 16, .box_h = 12, .ofs_x = 0, .ofs_y = 0}, + {.bitmap_index = 49021, .adv_w = 256, .box_w = 16, .box_h = 16, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 49089, .adv_w = 256, .box_w = 17, .box_h = 17, .ofs_x = -1, .ofs_y = -2}, + {.bitmap_index = 49183, .adv_w = 248, .box_w = 16, .box_h = 16, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 49291, .adv_w = 320, .box_w = 20, .box_h = 14, .ofs_x = 0, .ofs_y = -1}, + {.bitmap_index = 49401, .adv_w = 224, .box_w = 15, .box_h = 14, .ofs_x = 0, .ofs_y = -1}, + {.bitmap_index = 49492, .adv_w = 224, .box_w = 14, .box_h = 16, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 49539, .adv_w = 224, .box_w = 14, .box_h = 14, .ofs_x = 0, .ofs_y = -1}, + {.bitmap_index = 49591, .adv_w = 224, .box_w = 14, .box_h = 14, .ofs_x = 0, .ofs_y = -1}, + {.bitmap_index = 49646, .adv_w = 256, .box_w = 16, .box_h = 12, .ofs_x = 0, .ofs_y = 0}, + {.bitmap_index = 49705, .adv_w = 160, .box_w = 12, .box_h = 16, .ofs_x = -1, .ofs_y = -2}, + {.bitmap_index = 49773, .adv_w = 224, .box_w = 14, .box_h = 16, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 49834, .adv_w = 224, .box_w = 14, .box_h = 16, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 49904, .adv_w = 288, .box_w = 18, .box_h = 12, .ofs_x = 0, .ofs_y = 0}, + {.bitmap_index = 49970, .adv_w = 256, .box_w = 18, .box_h = 18, .ofs_x = -1, .ofs_y = -3}, + {.bitmap_index = 50059, .adv_w = 248, .box_w = 16, .box_h = 16, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 50161, .adv_w = 192, .box_w = 12, .box_h = 16, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 50194, .adv_w = 320, .box_w = 20, .box_h = 15, .ofs_x = 0, .ofs_y = -1}, + {.bitmap_index = 50307, .adv_w = 320, .box_w = 20, .box_h = 10, .ofs_x = 0, .ofs_y = 1}, + {.bitmap_index = 50352, .adv_w = 320, .box_w = 20, .box_h = 10, .ofs_x = 0, .ofs_y = 1}, + {.bitmap_index = 50396, .adv_w = 320, .box_w = 20, .box_h = 10, .ofs_x = 0, .ofs_y = 1}, + {.bitmap_index = 50440, .adv_w = 320, .box_w = 20, .box_h = 10, .ofs_x = 0, .ofs_y = 1}, + {.bitmap_index = 50486, .adv_w = 320, .box_w = 20, .box_h = 10, .ofs_x = 0, .ofs_y = 1}, + {.bitmap_index = 50526, .adv_w = 288, .box_w = 18, .box_h = 16, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 50585, .adv_w = 320, .box_w = 21, .box_h = 14, .ofs_x = 0, .ofs_y = -1}, + {.bitmap_index = 50686, .adv_w = 224, .box_w = 12, .box_h = 16, .ofs_x = 1, .ofs_y = -2}, + {.bitmap_index = 50782, .adv_w = 224, .box_w = 14, .box_h = 16, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 50837, .adv_w = 224, .box_w = 14, .box_h = 16, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 50919, .adv_w = 256, .box_w = 17, .box_h = 17, .ofs_x = -1, .ofs_y = -3}, + {.bitmap_index = 51013, .adv_w = 192, .box_w = 12, .box_h = 16, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 51095, .adv_w = 320, .box_w = 20, .box_h = 17, .ofs_x = 0, .ofs_y = -3}, + {.bitmap_index = 51226, .adv_w = 320, .box_w = 20, .box_h = 12, .ofs_x = 0, .ofs_y = 0}, + {.bitmap_index = 51315, .adv_w = 192, .box_w = 12, .box_h = 16, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 51354, .adv_w = 258, .box_w = 17, .box_h = 11, .ofs_x = 0, .ofs_y = 1} }; /*--------------------- @@ -10121,14 +10212,14 @@ static const uint16_t unicode_list_10[] = { static const uint16_t unicode_list_12[] = { 0x0, 0xc, 0xceeb, 0xceef, 0xcef2, 0xcef5, 0xcef6, 0xcef7, - 0xcefb, 0xcefd, 0xceff, 0xcf03, 0xcf06, 0xcf0b, 0xcf10, 0xcf11, - 0xcf12, 0xcf28, 0xcf2d, 0xcf32, 0xcf35, 0xcf36, 0xcf37, 0xcf3b, - 0xcf3c, 0xcf3d, 0xcf3e, 0xcf51, 0xcf52, 0xcf58, 0xcf5a, 0xcf5b, - 0xcf5e, 0xcf61, 0xcf62, 0xcf63, 0xcf65, 0xcf7d, 0xcf7f, 0xcf96, - 0xcfaa, 0xcfae, 0xcfaf, 0xcfb1, 0xcfb3, 0xcfca, 0xcfd1, 0xcfd4, - 0xcfdd, 0xd006, 0xd00e, 0xd045, 0xd0d5, 0xd12a, 0xd12b, 0xd12c, - 0xd12d, 0xd12e, 0xd163, 0xd171, 0xd17d, 0xd1d7, 0xd1ee, 0xd444, - 0xd6ac, 0xd78c + 0xcefb, 0xcefd, 0xceff, 0xcf01, 0xcf03, 0xcf06, 0xcf0b, 0xcf0e, + 0xcf10, 0xcf11, 0xcf12, 0xcf28, 0xcf2d, 0xcf32, 0xcf35, 0xcf36, + 0xcf37, 0xcf3b, 0xcf3c, 0xcf3d, 0xcf3e, 0xcf51, 0xcf52, 0xcf58, + 0xcf5a, 0xcf5b, 0xcf5e, 0xcf61, 0xcf62, 0xcf63, 0xcf65, 0xcf7d, + 0xcf7f, 0xcf96, 0xcfaa, 0xcfae, 0xcfaf, 0xcfb1, 0xcfb3, 0xcfca, + 0xcfd1, 0xcfd4, 0xcfdd, 0xd006, 0xd00e, 0xd038, 0xd045, 0xd0d5, + 0xd12a, 0xd12b, 0xd12c, 0xd12d, 0xd12e, 0xd163, 0xd171, 0xd17d, + 0xd1d7, 0xd1dc, 0xd1ee, 0xd2af, 0xd403, 0xd444, 0xd6ac, 0xd78c }; /*Collect the unicode lists and glyph_id offsets*/ @@ -10184,7 +10275,7 @@ static const lv_font_fmt_txt_cmap_t cmaps[] = }, { .range_start = 8470, .range_length = 55181, .glyph_id_start = 1017, - .unicode_list = unicode_list_12, .glyph_id_ofs_list = NULL, .list_length = 66, .type = LV_FONT_FMT_TXT_CMAP_SPARSE_TINY + .unicode_list = unicode_list_12, .glyph_id_ofs_list = NULL, .list_length = 72, .type = LV_FONT_FMT_TXT_CMAP_SPARSE_TINY } }; @@ -10331,7 +10422,8 @@ static const uint8_t kern_left_class_mapping[] = 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0 + 0, 0, 0, 0, 0, 0, 0, 0, + 0 }; /*Map glyph_ids to kern right classes*/ @@ -10472,7 +10564,8 @@ static const uint8_t kern_right_class_mapping[] = 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0 + 0, 0, 0, 0, 0, 0, 0, 0, + 0 }; /*Kern values between classes*/ diff --git a/examples/companion_radio/ui-lvgl/fonts/ui_font_20.c b/examples/companion_radio/ui-lvgl/fonts/ui_font_20.c index 7ac04b59..d40c8091 100644 --- a/examples/companion_radio/ui-lvgl/fonts/ui_font_20.c +++ b/examples/companion_radio/ui-lvgl/fonts/ui_font_20.c @@ -1,7 +1,7 @@ /******************************************************************************* * Size: 20 px * Bpp: 4 - * Opts: --bpp 4 --size 20 --force-fast-kern-format --font /var/folders/1d/dgkmygls6x5gsb16pbn2mwlr0000gn/T//meshcore-ui-fonts/NotoSans-Medium.ttf -r 0x20-0x7E,0xA0-0x24F,0x370-0x3FF,0x400-0x52F,0x1E80-0x1E85,0x1E9E,0x1EF2-0x1EF3,0x2010-0x2027,0x2030,0x2039-0x203A,0x20A4-0x20BF,0x2116,0x2122 --font /var/folders/1d/dgkmygls6x5gsb16pbn2mwlr0000gn/T//meshcore-ui-fonts/fa.woff -r 61612,61445,62073,61632,61441,61448,61451,61452,61453,61457,61459,61461,61465,61468,61473,61478,61479,61480,61502,61507,61512,61515,61516,61517,61521,61522,61523,61524,61543,61544,61550,61552,61553,61556,61559,61560,61561,61563,61587,61589,61636,61637,61639,61641,61664,61671,61674,61683,61724,61732,61787,61931,62016,62017,62018,62019,62020,62087,62099,62212,62189,62810,63426,63650 --format lvgl --lv-include lvgl.h --lv-font-name ui_font_20 -o /Users/jakub/Documents/MeshCore/MeshCore/examples/companion_radio/ui-lvgl/fonts/ui_font_20.c + * Opts: --bpp 4 --size 20 --force-fast-kern-format --font /var/folders/1d/dgkmygls6x5gsb16pbn2mwlr0000gn/T//meshcore-ui-fonts/NotoSans-Medium.ttf -r 0x20-0x7E,0xA0-0x24F,0x370-0x3FF,0x400-0x52F,0x1E80-0x1E85,0x1E9E,0x1EF2-0x1EF3,0x2010-0x2027,0x2030,0x2039-0x203A,0x20A4-0x20BF,0x2116,0x2122 --font /var/folders/1d/dgkmygls6x5gsb16pbn2mwlr0000gn/T//meshcore-ui-fonts/fa.woff -r 62745,61463,62194,61612,61445,62073,61632,61774,61476,62405,61441,61448,61451,61452,61453,61457,61459,61461,61465,61468,61473,61478,61479,61480,61502,61507,61512,61515,61516,61517,61521,61522,61523,61524,61543,61544,61550,61552,61553,61556,61559,61560,61561,61563,61587,61589,61636,61637,61639,61641,61664,61671,61674,61683,61724,61732,61787,61931,62016,62017,62018,62019,62020,62087,62099,62212,62189,62810,63426,63650 --format lvgl --lv-include lvgl.h --lv-font-name ui_font_20 -o /Users/jakub/Documents/MeshCore/MeshCore/examples/companion_radio/ui-lvgl/fonts/ui_font_20.c ******************************************************************************/ #ifdef LV_LVGL_H_INCLUDE_SIMPLE @@ -10547,6 +10547,25 @@ static LV_ATTRIBUTE_LARGE_CONST const uint8_t glyph_bitmap[] = { 0x1c, 0x40, 0x11, 0x0, 0x66, 0x0, 0xf8, 0xbf, 0xf9, 0x0, 0x2c, 0xff, 0xd4, 0x1, 0x80, + /* U+F017 "" */ + 0x0, 0xff, 0xe6, 0x8b, 0xe7, 0x7f, 0xb6, 0x50, + 0x3, 0xfc, 0xfd, 0x6, 0x20, 0x2, 0x6b, 0xc2, + 0x0, 0xfa, 0xe0, 0x3, 0xf8, 0xf1, 0x0, 0x3a, + 0x90, 0x3, 0x12, 0x20, 0x3, 0xd, 0x90, 0x4, + 0xca, 0x1, 0xd7, 0x70, 0x80, 0x77, 0x80, 0x50, + 0x1, 0xff, 0xc3, 0x26, 0x6, 0x10, 0xf, 0xfe, + 0x25, 0x86, 0x0, 0x7f, 0xf1, 0x48, 0x58, 0x3, + 0xff, 0x8c, 0xa4, 0x1, 0xfe, 0x20, 0xf, 0x84, + 0xc0, 0x3f, 0xd8, 0xc0, 0x1e, 0x16, 0x0, 0xfa, + 0xcc, 0x1, 0x32, 0x0, 0xe6, 0xc0, 0xf, 0x8b, + 0x18, 0x1, 0x80, 0x18, 0x49, 0x40, 0x3f, 0xa6, + 0xa1, 0x40, 0x33, 0x81, 0x30, 0x7, 0xf2, 0xb8, + 0x7, 0x40, 0x2, 0x8, 0x3, 0xff, 0x85, 0x42, + 0x0, 0x1f, 0x10, 0xf, 0xfe, 0x3, 0xa8, 0x6, + 0x2f, 0x30, 0xf, 0xf5, 0x40, 0x7, 0x87, 0x28, + 0xc0, 0x38, 0x5b, 0x94, 0x3, 0xf9, 0x73, 0xb2, + 0xed, 0xbd, 0x22, 0x1, 0xc0, + /* U+F019 "" */ 0x0, 0xfc, 0xac, 0xc5, 0x0, 0xff, 0xe1, 0xbd, 0x4c, 0xa9, 0xc0, 0x3f, 0xf8, 0x5e, 0x1, 0xbc, @@ -10599,6 +10618,19 @@ static LV_ATTRIBUTE_LARGE_CONST const uint8_t glyph_bitmap[] = { 0x70, 0xc, 0xa9, 0x80, 0x12, 0xdf, 0xf7, 0x3f, 0x20, 0x40, 0x38, + /* U+F024 "" */ + 0x1, 0x30, 0xf, 0xfe, 0x2a, 0x76, 0x68, 0x80, + 0x7f, 0xf0, 0xe0, 0x0, 0x4c, 0x1, 0xff, 0xc3, + 0x70, 0xb, 0x66, 0xed, 0x2a, 0x20, 0x1e, 0x6b, + 0x49, 0x0, 0x95, 0x91, 0xd, 0x5d, 0x90, 0xf1, + 0x7f, 0x29, 0x4, 0x20, 0x1f, 0xc6, 0xf0, 0xe8, + 0x1, 0x8, 0x7, 0xff, 0xfc, 0x3, 0xff, 0xfe, + 0x1, 0xff, 0xd1, 0x40, 0xc, 0x4f, 0x57, 0x92, + 0x80, 0x1e, 0x18, 0x90, 0x9, 0xb6, 0x15, 0xd, + 0xaf, 0xee, 0x1e, 0x2f, 0x9c, 0x3, 0xff, 0x82, + 0x8f, 0xe, 0x80, 0x1f, 0xfe, 0x9, 0x4a, 0x0, + 0xff, 0xe2, 0x0, + /* U+F026 "" */ 0x0, 0xfe, 0x10, 0xf, 0x8b, 0xbc, 0x3, 0xc5, 0x82, 0x20, 0xe, 0x2c, 0x10, 0x2, 0x3b, 0xec, @@ -11154,6 +11186,27 @@ static LV_ATTRIBUTE_LARGE_CONST const uint8_t glyph_bitmap[] = { 0x20, 0x8, 0x0, 0xff, 0xe3, 0x64, 0x62, 0x0, 0x7e, + /* U+F14E "" */ + 0x0, 0xff, 0xe6, 0x94, 0x6f, 0xfd, 0xd4, 0x80, + 0x1f, 0xe9, 0xd7, 0x20, 0x8, 0x56, 0xf0, 0x80, + 0x3c, 0x38, 0xc0, 0x1f, 0xc7, 0xa8, 0x1, 0xda, + 0x60, 0x1f, 0xfc, 0xb, 0x30, 0xa, 0xc, 0x3, + 0xfc, 0xa6, 0x0, 0xd0, 0x0, 0xb8, 0x7, 0xf3, + 0xf5, 0x50, 0x0, 0x2e, 0xc, 0x1, 0xf0, 0xc7, + 0xc0, 0x85, 0x0, 0x54, 0x16, 0x1, 0xe5, 0xe7, + 0x0, 0x90, 0xc0, 0x22, 0x28, 0x3, 0xd6, 0x32, + 0x80, 0xf, 0x0, 0xe6, 0x10, 0xe, 0x71, 0x96, + 0xa0, 0x24, 0x0, 0xe1, 0x10, 0x7, 0x48, 0x51, + 0xc8, 0x40, 0x7, 0x84, 0x80, 0x32, 0x88, 0x16, + 0x3a, 0x28, 0x7, 0x99, 0x0, 0x34, 0x0, 0x49, + 0x76, 0x0, 0xf0, 0x95, 0x80, 0x46, 0x40, 0xbb, + 0x68, 0x1, 0xf3, 0x81, 0xa8, 0x0, 0xe7, 0xa8, + 0x80, 0x3f, 0xa0, 0x1, 0x62, 0x0, 0x61, 0x0, + 0xff, 0x50, 0x80, 0x7, 0x4, 0x3, 0xff, 0x81, + 0xa, 0x1, 0x8b, 0xcc, 0x3, 0xf8, 0x69, 0xc0, + 0x3c, 0x39, 0x46, 0x1, 0xc4, 0xfe, 0xa0, 0x1f, + 0xcb, 0x9f, 0x97, 0x9d, 0xb0, 0x20, 0x1c, + /* U+F15B "" */ 0x24, 0x4f, 0x8c, 0xc, 0x3, 0x6d, 0xdf, 0xde, 0x18, 0xc0, 0x1f, 0xfc, 0x29, 0x60, 0xf, 0xfe, @@ -11305,6 +11358,25 @@ static LV_ATTRIBUTE_LARGE_CONST const uint8_t glyph_bitmap[] = { 0x0, 0x70, 0x6, 0x0, 0x7f, 0xf0, 0x44, 0xc0, 0xf, 0x50, 0xef, 0xfe, 0x8c, 0x0, 0x0, + /* U+F2F2 "" */ + 0x0, 0xf2, 0xc4, 0x65, 0x0, 0xff, 0xe0, 0x43, + 0xbe, 0x90, 0xf, 0xfe, 0x0, 0x80, 0x67, 0x0, + 0xff, 0xe0, 0x65, 0x80, 0x32, 0xc0, 0x3f, 0xf8, + 0xb, 0xe0, 0x9, 0x50, 0xf, 0xf3, 0x75, 0x20, + 0x1, 0x6b, 0x95, 0x5e, 0xa0, 0x1a, 0x64, 0x20, + 0x1e, 0x1a, 0xa0, 0x58, 0x5, 0xc, 0x1, 0xc, + 0xc8, 0x40, 0x3b, 0x40, 0x6, 0xe0, 0x19, 0x59, + 0x84, 0x1, 0xc4, 0x0, 0x90, 0xf, 0xfe, 0x14, + 0x80, 0x10, 0x3, 0xff, 0x84, 0xa0, 0x22, 0x0, + 0xff, 0xe1, 0x10, 0x18, 0x7, 0xff, 0x14, 0x80, + 0x3c, 0xe0, 0x1, 0x0, 0xfc, 0x40, 0x1c, 0x7f, + 0xe2, 0x0, 0xe4, 0x0, 0x50, 0x7, 0xff, 0xb, + 0x40, 0xe, 0x20, 0x1f, 0xfc, 0x2, 0x60, 0xb, + 0x40, 0x3f, 0xe1, 0xf0, 0xc, 0x78, 0x20, 0x1f, + 0x8b, 0x8, 0x3, 0x8f, 0xdc, 0x40, 0x30, 0xc6, + 0x90, 0x7, 0xc3, 0x1d, 0xb7, 0x6d, 0xe7, 0x0, + 0xe0, + /* U+F304 "" */ 0x0, 0xff, 0xe1, 0x8b, 0x20, 0x7, 0xff, 0x14, 0x72, 0x6e, 0x0, 0x3f, 0xf8, 0x63, 0xe4, 0x0, @@ -11325,6 +11397,51 @@ static LV_ATTRIBUTE_LARGE_CONST const uint8_t glyph_bitmap[] = { 0x38, 0x40, 0x1f, 0xfc, 0x1a, 0x8a, 0xce, 0xe1, 0x0, 0x7f, 0xf0, 0x40, + /* U+F3C5 "" */ + 0x0, 0xe3, 0x67, 0x63, 0x0, 0xfc, 0xbb, 0x93, + 0x13, 0x9a, 0xa0, 0x1d, 0x34, 0x40, 0x1c, 0x55, + 0x20, 0x13, 0xb0, 0x7, 0xf3, 0x38, 0x14, 0x0, + 0x70, 0x80, 0x74, 0x15, 0x80, 0x65, 0xee, 0x94, + 0x3, 0x5a, 0x0, 0x47, 0x42, 0x3, 0x46, 0x1, + 0x21, 0x0, 0x56, 0x1, 0xd6, 0x1, 0x10, 0x7, + 0xff, 0xc, 0x80, 0x2b, 0x0, 0xeb, 0x0, 0x89, + 0x40, 0x23, 0xa1, 0x1, 0xa3, 0x0, 0x96, 0x40, + 0x32, 0xf7, 0x4a, 0x1, 0xa4, 0x5c, 0x3, 0x84, + 0x3, 0x9c, 0x42, 0x8, 0x3, 0xf8, 0xa0, 0x2, + 0xe0, 0xf, 0xee, 0x0, 0xc7, 0x0, 0x1f, 0x41, + 0x80, 0x73, 0x98, 0x7, 0x1b, 0x80, 0x7d, 0xc0, + 0x1d, 0xc0, 0x1f, 0x8a, 0x40, 0x29, 0x20, 0xf, + 0xe6, 0x40, 0x46, 0x0, 0xff, 0xaa, 0x28, 0x3, + 0xc0, + + /* U+F519 "" */ + 0x3, 0x41, 0x0, 0xff, 0xe2, 0x8a, 0x18, 0x16, + 0x5c, 0x80, 0x7f, 0xf1, 0x66, 0xf0, 0xa8, 0x1, + 0x40, 0x1f, 0xfc, 0x5a, 0x0, 0x52, 0x1, 0x90, + 0x3e, 0x40, 0x5, 0x17, 0x0, 0x14, 0x63, 0x81, + 0x18, 0x21, 0x2, 0x80, 0x24, 0xc8, 0x1, 0x4e, + 0x8f, 0x40, 0x2, 0x3a, 0x0, 0x28, 0x10, 0x80, + 0x80, 0x4, 0x8, 0x0, 0xa0, 0x12, 0x80, 0x8, + 0x8, 0x0, 0x20, 0x20, 0x1, 0x0, 0x8, 0x8, + 0x0, 0x80, 0x22, 0x0, 0x8, 0x8, 0x0, 0x40, + 0x4, 0x4, 0x0, 0x50, 0x50, 0x3, 0x0, 0x42, + 0x0, 0x50, 0x50, 0x1, 0x1, 0x20, 0x20, 0x2, + 0xb3, 0x40, 0x90, 0x2, 0x32, 0xd, 0xca, 0x0, + 0x20, 0x3e, 0x80, 0x18, 0x4, 0xc4, 0x2c, 0x1, + 0x60, 0xb, 0x1, 0x31, 0x6, 0x0, 0x7a, 0x2a, + 0x18, 0x7, 0x30, 0x22, 0x40, 0xc0, 0x1c, 0x68, + 0xaa, 0xa, 0xb5, 0x0, 0xcc, 0x0, 0xf0, 0xf0, + 0x3, 0x80, 0x65, 0xba, 0x0, 0xfe, 0xb0, 0x24, + 0x4, 0x20, 0xa0, 0xf, 0xfe, 0x1a, 0x10, 0x17, + 0xfc, 0x40, 0x48, 0x1, 0xff, 0xc2, 0xe0, 0xf, + 0xef, 0x0, 0xff, 0xe0, 0x8a, 0x82, 0x3b, 0xf2, + 0x2, 0x10, 0x7, 0xff, 0x2, 0x40, 0x13, 0x11, + 0xd2, 0x0, 0xa0, 0xf, 0xfe, 0x3, 0x2, 0x10, + 0x7, 0x12, 0x3, 0x0, 0x7f, 0xca, 0x21, 0xc0, + 0x1f, 0x70, 0xa, 0x80, 0x7f, 0xb0, 0x5, 0x40, + 0x3e, 0x51, 0xc, 0x0, 0xff, 0x4d, 0xe8, 0x7, + 0xf6, 0xd4, 0x80, 0x70, + /* U+F55A "" */ 0x0, 0xf3, 0x77, 0xff, 0xff, 0xe, 0xc8, 0x3, 0xa2, 0x44, 0x3, 0xff, 0x86, 0x92, 0x1, 0xa1, @@ -12399,60 +12516,66 @@ static const lv_font_fmt_txt_glyph_dsc_t glyph_dsc[] = { {.bitmap_index = 63746, .adv_w = 320, .box_w = 20, .box_h = 21, .ofs_x = 0, .ofs_y = -3}, {.bitmap_index = 63899, .adv_w = 360, .box_w = 23, .box_h = 19, .ofs_x = 0, .ofs_y = -2}, {.bitmap_index = 64042, .adv_w = 320, .box_w = 20, .box_h = 21, .ofs_x = 0, .ofs_y = -3}, - {.bitmap_index = 64146, .adv_w = 360, .box_w = 23, .box_h = 15, .ofs_x = 0, .ofs_y = 0}, - {.bitmap_index = 64243, .adv_w = 320, .box_w = 20, .box_h = 21, .ofs_x = 0, .ofs_y = -3}, - {.bitmap_index = 64398, .adv_w = 160, .box_w = 10, .box_h = 16, .ofs_x = 0, .ofs_y = 0}, - {.bitmap_index = 64436, .adv_w = 240, .box_w = 15, .box_h = 16, .ofs_x = 0, .ofs_y = -1}, - {.bitmap_index = 64510, .adv_w = 360, .box_w = 23, .box_h = 19, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 64664, .adv_w = 320, .box_w = 20, .box_h = 15, .ofs_x = 0, .ofs_y = 0}, - {.bitmap_index = 64759, .adv_w = 220, .box_w = 14, .box_h = 21, .ofs_x = 0, .ofs_y = -3}, - {.bitmap_index = 64862, .adv_w = 280, .box_w = 13, .box_h = 19, .ofs_x = 2, .ofs_y = -2}, - {.bitmap_index = 64930, .adv_w = 280, .box_w = 18, .box_h = 21, .ofs_x = 0, .ofs_y = -3}, - {.bitmap_index = 65032, .adv_w = 280, .box_w = 18, .box_h = 19, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 65089, .adv_w = 280, .box_w = 18, .box_h = 19, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 65130, .adv_w = 280, .box_w = 13, .box_h = 19, .ofs_x = 2, .ofs_y = -2}, - {.bitmap_index = 65204, .adv_w = 280, .box_w = 19, .box_h = 19, .ofs_x = -1, .ofs_y = -2}, - {.bitmap_index = 65303, .adv_w = 200, .box_w = 11, .box_h = 19, .ofs_x = 1, .ofs_y = -2}, - {.bitmap_index = 65388, .adv_w = 200, .box_w = 11, .box_h = 19, .ofs_x = 1, .ofs_y = -2}, - {.bitmap_index = 65469, .adv_w = 280, .box_w = 18, .box_h = 19, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 65541, .adv_w = 280, .box_w = 18, .box_h = 5, .ofs_x = 0, .ofs_y = 5}, - {.bitmap_index = 65563, .adv_w = 360, .box_w = 23, .box_h = 15, .ofs_x = 0, .ofs_y = 0}, - {.bitmap_index = 65713, .adv_w = 400, .box_w = 26, .box_h = 21, .ofs_x = -1, .ofs_y = -3}, - {.bitmap_index = 65916, .adv_w = 360, .box_w = 24, .box_h = 21, .ofs_x = -1, .ofs_y = -3}, - {.bitmap_index = 66069, .adv_w = 320, .box_w = 20, .box_h = 19, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 66211, .adv_w = 280, .box_w = 18, .box_h = 11, .ofs_x = 0, .ofs_y = 2}, - {.bitmap_index = 66287, .adv_w = 280, .box_w = 18, .box_h = 11, .ofs_x = 0, .ofs_y = 2}, - {.bitmap_index = 66365, .adv_w = 400, .box_w = 26, .box_h = 16, .ofs_x = 0, .ofs_y = -1}, - {.bitmap_index = 66483, .adv_w = 320, .box_w = 20, .box_h = 15, .ofs_x = 0, .ofs_y = 0}, - {.bitmap_index = 66527, .adv_w = 320, .box_w = 20, .box_h = 21, .ofs_x = 0, .ofs_y = -3}, - {.bitmap_index = 66637, .adv_w = 320, .box_w = 21, .box_h = 21, .ofs_x = -1, .ofs_y = -3}, - {.bitmap_index = 66775, .adv_w = 310, .box_w = 20, .box_h = 21, .ofs_x = 0, .ofs_y = -3}, - {.bitmap_index = 66934, .adv_w = 400, .box_w = 25, .box_h = 19, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 67103, .adv_w = 280, .box_w = 18, .box_h = 19, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 67251, .adv_w = 280, .box_w = 18, .box_h = 21, .ofs_x = 0, .ofs_y = -3}, - {.bitmap_index = 67324, .adv_w = 280, .box_w = 18, .box_h = 19, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 67417, .adv_w = 280, .box_w = 18, .box_h = 17, .ofs_x = 0, .ofs_y = -1}, - {.bitmap_index = 67477, .adv_w = 320, .box_w = 20, .box_h = 15, .ofs_x = 0, .ofs_y = 0}, - {.bitmap_index = 67567, .adv_w = 200, .box_w = 14, .box_h = 21, .ofs_x = -1, .ofs_y = -3}, - {.bitmap_index = 67660, .adv_w = 280, .box_w = 18, .box_h = 21, .ofs_x = 0, .ofs_y = -3}, - {.bitmap_index = 67748, .adv_w = 280, .box_w = 18, .box_h = 21, .ofs_x = 0, .ofs_y = -3}, - {.bitmap_index = 67859, .adv_w = 360, .box_w = 23, .box_h = 15, .ofs_x = 0, .ofs_y = 0}, - {.bitmap_index = 67985, .adv_w = 320, .box_w = 22, .box_h = 21, .ofs_x = -1, .ofs_y = -3}, - {.bitmap_index = 68106, .adv_w = 240, .box_w = 15, .box_h = 21, .ofs_x = 0, .ofs_y = -3}, - {.bitmap_index = 68154, .adv_w = 400, .box_w = 25, .box_h = 19, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 68318, .adv_w = 400, .box_w = 25, .box_h = 13, .ofs_x = 0, .ofs_y = 1}, - {.bitmap_index = 68385, .adv_w = 400, .box_w = 25, .box_h = 13, .ofs_x = 0, .ofs_y = 1}, - {.bitmap_index = 68453, .adv_w = 400, .box_w = 25, .box_h = 13, .ofs_x = 0, .ofs_y = 1}, - {.bitmap_index = 68520, .adv_w = 400, .box_w = 25, .box_h = 13, .ofs_x = 0, .ofs_y = 1}, - {.bitmap_index = 68587, .adv_w = 400, .box_w = 25, .box_h = 13, .ofs_x = 0, .ofs_y = 1}, - {.bitmap_index = 68647, .adv_w = 360, .box_w = 23, .box_h = 19, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 68734, .adv_w = 400, .box_w = 26, .box_h = 17, .ofs_x = 0, .ofs_y = -1}, - {.bitmap_index = 68868, .adv_w = 280, .box_w = 16, .box_h = 21, .ofs_x = 1, .ofs_y = -3}, - {.bitmap_index = 69015, .adv_w = 280, .box_w = 18, .box_h = 21, .ofs_x = 0, .ofs_y = -3}, - {.bitmap_index = 69102, .adv_w = 320, .box_w = 21, .box_h = 21, .ofs_x = -1, .ofs_y = -3}, - {.bitmap_index = 69242, .adv_w = 400, .box_w = 25, .box_h = 15, .ofs_x = 0, .ofs_y = 0}, - {.bitmap_index = 69362, .adv_w = 240, .box_w = 15, .box_h = 21, .ofs_x = 0, .ofs_y = -3}, - {.bitmap_index = 69421, .adv_w = 322, .box_w = 21, .box_h = 13, .ofs_x = 0, .ofs_y = 1} + {.bitmap_index = 64175, .adv_w = 320, .box_w = 20, .box_h = 21, .ofs_x = 0, .ofs_y = -3}, + {.bitmap_index = 64279, .adv_w = 360, .box_w = 23, .box_h = 15, .ofs_x = 0, .ofs_y = 0}, + {.bitmap_index = 64376, .adv_w = 320, .box_w = 20, .box_h = 21, .ofs_x = 0, .ofs_y = -3}, + {.bitmap_index = 64531, .adv_w = 320, .box_w = 20, .box_h = 21, .ofs_x = 0, .ofs_y = -3}, + {.bitmap_index = 64614, .adv_w = 160, .box_w = 10, .box_h = 16, .ofs_x = 0, .ofs_y = 0}, + {.bitmap_index = 64652, .adv_w = 240, .box_w = 15, .box_h = 16, .ofs_x = 0, .ofs_y = -1}, + {.bitmap_index = 64726, .adv_w = 360, .box_w = 23, .box_h = 19, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 64880, .adv_w = 320, .box_w = 20, .box_h = 15, .ofs_x = 0, .ofs_y = 0}, + {.bitmap_index = 64975, .adv_w = 220, .box_w = 14, .box_h = 21, .ofs_x = 0, .ofs_y = -3}, + {.bitmap_index = 65078, .adv_w = 280, .box_w = 13, .box_h = 19, .ofs_x = 2, .ofs_y = -2}, + {.bitmap_index = 65146, .adv_w = 280, .box_w = 18, .box_h = 21, .ofs_x = 0, .ofs_y = -3}, + {.bitmap_index = 65248, .adv_w = 280, .box_w = 18, .box_h = 19, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 65305, .adv_w = 280, .box_w = 18, .box_h = 19, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 65346, .adv_w = 280, .box_w = 13, .box_h = 19, .ofs_x = 2, .ofs_y = -2}, + {.bitmap_index = 65420, .adv_w = 280, .box_w = 19, .box_h = 19, .ofs_x = -1, .ofs_y = -2}, + {.bitmap_index = 65519, .adv_w = 200, .box_w = 11, .box_h = 19, .ofs_x = 1, .ofs_y = -2}, + {.bitmap_index = 65604, .adv_w = 200, .box_w = 11, .box_h = 19, .ofs_x = 1, .ofs_y = -2}, + {.bitmap_index = 65685, .adv_w = 280, .box_w = 18, .box_h = 19, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 65757, .adv_w = 280, .box_w = 18, .box_h = 5, .ofs_x = 0, .ofs_y = 5}, + {.bitmap_index = 65779, .adv_w = 360, .box_w = 23, .box_h = 15, .ofs_x = 0, .ofs_y = 0}, + {.bitmap_index = 65929, .adv_w = 400, .box_w = 26, .box_h = 21, .ofs_x = -1, .ofs_y = -3}, + {.bitmap_index = 66132, .adv_w = 360, .box_w = 24, .box_h = 21, .ofs_x = -1, .ofs_y = -3}, + {.bitmap_index = 66285, .adv_w = 320, .box_w = 20, .box_h = 19, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 66427, .adv_w = 280, .box_w = 18, .box_h = 11, .ofs_x = 0, .ofs_y = 2}, + {.bitmap_index = 66503, .adv_w = 280, .box_w = 18, .box_h = 11, .ofs_x = 0, .ofs_y = 2}, + {.bitmap_index = 66581, .adv_w = 400, .box_w = 26, .box_h = 16, .ofs_x = 0, .ofs_y = -1}, + {.bitmap_index = 66699, .adv_w = 320, .box_w = 20, .box_h = 15, .ofs_x = 0, .ofs_y = 0}, + {.bitmap_index = 66743, .adv_w = 320, .box_w = 20, .box_h = 21, .ofs_x = 0, .ofs_y = -3}, + {.bitmap_index = 66853, .adv_w = 320, .box_w = 21, .box_h = 21, .ofs_x = -1, .ofs_y = -3}, + {.bitmap_index = 66991, .adv_w = 310, .box_w = 20, .box_h = 21, .ofs_x = 0, .ofs_y = -3}, + {.bitmap_index = 67150, .adv_w = 400, .box_w = 25, .box_h = 19, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 67319, .adv_w = 280, .box_w = 18, .box_h = 19, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 67467, .adv_w = 280, .box_w = 18, .box_h = 21, .ofs_x = 0, .ofs_y = -3}, + {.bitmap_index = 67540, .adv_w = 280, .box_w = 18, .box_h = 19, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 67633, .adv_w = 280, .box_w = 18, .box_h = 17, .ofs_x = 0, .ofs_y = -1}, + {.bitmap_index = 67693, .adv_w = 320, .box_w = 20, .box_h = 15, .ofs_x = 0, .ofs_y = 0}, + {.bitmap_index = 67783, .adv_w = 200, .box_w = 14, .box_h = 21, .ofs_x = -1, .ofs_y = -3}, + {.bitmap_index = 67876, .adv_w = 280, .box_w = 18, .box_h = 21, .ofs_x = 0, .ofs_y = -3}, + {.bitmap_index = 67964, .adv_w = 280, .box_w = 18, .box_h = 21, .ofs_x = 0, .ofs_y = -3}, + {.bitmap_index = 68075, .adv_w = 360, .box_w = 23, .box_h = 15, .ofs_x = 0, .ofs_y = 0}, + {.bitmap_index = 68201, .adv_w = 320, .box_w = 22, .box_h = 21, .ofs_x = -1, .ofs_y = -3}, + {.bitmap_index = 68322, .adv_w = 310, .box_w = 20, .box_h = 21, .ofs_x = 0, .ofs_y = -3}, + {.bitmap_index = 68473, .adv_w = 240, .box_w = 15, .box_h = 21, .ofs_x = 0, .ofs_y = -3}, + {.bitmap_index = 68521, .adv_w = 400, .box_w = 25, .box_h = 19, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 68685, .adv_w = 400, .box_w = 25, .box_h = 13, .ofs_x = 0, .ofs_y = 1}, + {.bitmap_index = 68752, .adv_w = 400, .box_w = 25, .box_h = 13, .ofs_x = 0, .ofs_y = 1}, + {.bitmap_index = 68820, .adv_w = 400, .box_w = 25, .box_h = 13, .ofs_x = 0, .ofs_y = 1}, + {.bitmap_index = 68887, .adv_w = 400, .box_w = 25, .box_h = 13, .ofs_x = 0, .ofs_y = 1}, + {.bitmap_index = 68954, .adv_w = 400, .box_w = 25, .box_h = 13, .ofs_x = 0, .ofs_y = 1}, + {.bitmap_index = 69014, .adv_w = 360, .box_w = 23, .box_h = 19, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 69101, .adv_w = 400, .box_w = 26, .box_h = 17, .ofs_x = 0, .ofs_y = -1}, + {.bitmap_index = 69235, .adv_w = 280, .box_w = 16, .box_h = 21, .ofs_x = 1, .ofs_y = -3}, + {.bitmap_index = 69382, .adv_w = 280, .box_w = 18, .box_h = 21, .ofs_x = 0, .ofs_y = -3}, + {.bitmap_index = 69469, .adv_w = 280, .box_w = 18, .box_h = 21, .ofs_x = 0, .ofs_y = -3}, + {.bitmap_index = 69598, .adv_w = 320, .box_w = 21, .box_h = 21, .ofs_x = -1, .ofs_y = -3}, + {.bitmap_index = 69738, .adv_w = 240, .box_w = 15, .box_h = 21, .ofs_x = 0, .ofs_y = -3}, + {.bitmap_index = 69851, .adv_w = 400, .box_w = 25, .box_h = 21, .ofs_x = 0, .ofs_y = -3}, + {.bitmap_index = 70055, .adv_w = 400, .box_w = 25, .box_h = 15, .ofs_x = 0, .ofs_y = 0}, + {.bitmap_index = 70175, .adv_w = 240, .box_w = 15, .box_h = 21, .ofs_x = 0, .ofs_y = -3}, + {.bitmap_index = 70234, .adv_w = 322, .box_w = 21, .box_h = 13, .ofs_x = 0, .ofs_y = 1} }; /*--------------------- @@ -12477,14 +12600,14 @@ static const uint16_t unicode_list_10[] = { static const uint16_t unicode_list_12[] = { 0x0, 0xc, 0xceeb, 0xceef, 0xcef2, 0xcef5, 0xcef6, 0xcef7, - 0xcefb, 0xcefd, 0xceff, 0xcf03, 0xcf06, 0xcf0b, 0xcf10, 0xcf11, - 0xcf12, 0xcf28, 0xcf2d, 0xcf32, 0xcf35, 0xcf36, 0xcf37, 0xcf3b, - 0xcf3c, 0xcf3d, 0xcf3e, 0xcf51, 0xcf52, 0xcf58, 0xcf5a, 0xcf5b, - 0xcf5e, 0xcf61, 0xcf62, 0xcf63, 0xcf65, 0xcf7d, 0xcf7f, 0xcf96, - 0xcfaa, 0xcfae, 0xcfaf, 0xcfb1, 0xcfb3, 0xcfca, 0xcfd1, 0xcfd4, - 0xcfdd, 0xd006, 0xd00e, 0xd045, 0xd0d5, 0xd12a, 0xd12b, 0xd12c, - 0xd12d, 0xd12e, 0xd163, 0xd171, 0xd17d, 0xd1d7, 0xd1ee, 0xd444, - 0xd6ac, 0xd78c + 0xcefb, 0xcefd, 0xceff, 0xcf01, 0xcf03, 0xcf06, 0xcf0b, 0xcf0e, + 0xcf10, 0xcf11, 0xcf12, 0xcf28, 0xcf2d, 0xcf32, 0xcf35, 0xcf36, + 0xcf37, 0xcf3b, 0xcf3c, 0xcf3d, 0xcf3e, 0xcf51, 0xcf52, 0xcf58, + 0xcf5a, 0xcf5b, 0xcf5e, 0xcf61, 0xcf62, 0xcf63, 0xcf65, 0xcf7d, + 0xcf7f, 0xcf96, 0xcfaa, 0xcfae, 0xcfaf, 0xcfb1, 0xcfb3, 0xcfca, + 0xcfd1, 0xcfd4, 0xcfdd, 0xd006, 0xd00e, 0xd038, 0xd045, 0xd0d5, + 0xd12a, 0xd12b, 0xd12c, 0xd12d, 0xd12e, 0xd163, 0xd171, 0xd17d, + 0xd1d7, 0xd1dc, 0xd1ee, 0xd2af, 0xd403, 0xd444, 0xd6ac, 0xd78c }; /*Collect the unicode lists and glyph_id offsets*/ @@ -12540,7 +12663,7 @@ static const lv_font_fmt_txt_cmap_t cmaps[] = }, { .range_start = 8470, .range_length = 55181, .glyph_id_start = 1017, - .unicode_list = unicode_list_12, .glyph_id_ofs_list = NULL, .list_length = 66, .type = LV_FONT_FMT_TXT_CMAP_SPARSE_TINY + .unicode_list = unicode_list_12, .glyph_id_ofs_list = NULL, .list_length = 72, .type = LV_FONT_FMT_TXT_CMAP_SPARSE_TINY } }; @@ -12687,7 +12810,8 @@ static const uint8_t kern_left_class_mapping[] = 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0 + 0, 0, 0, 0, 0, 0, 0, 0, + 0 }; /*Map glyph_ids to kern right classes*/ @@ -12828,7 +12952,8 @@ static const uint8_t kern_right_class_mapping[] = 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0 + 0, 0, 0, 0, 0, 0, 0, 0, + 0 }; /*Kern values between classes*/ diff --git a/examples/companion_radio/ui-lvgl/fonts/ui_font_40.c b/examples/companion_radio/ui-lvgl/fonts/ui_font_40.c index 806c4562..e664ab07 100644 --- a/examples/companion_radio/ui-lvgl/fonts/ui_font_40.c +++ b/examples/companion_radio/ui-lvgl/fonts/ui_font_40.c @@ -1,7 +1,7 @@ /******************************************************************************* * Size: 40 px * Bpp: 4 - * Opts: --bpp 4 --size 40 --no-compress --font /var/folders/1d/dgkmygls6x5gsb16pbn2mwlr0000gn/T//meshcore-ui-fonts/NotoSans-SemiBold.ttf -r 0x20,0x2D,0x30-0x3A --format lvgl --lv-include lvgl.h --lv-font-name ui_font_40 -o /Users/jakub/Documents/MeshCore/MeshCore/examples/companion_radio/ui-lvgl/fonts/ui_font_40.c + * Opts: --bpp 4 --size 40 --no-compress --font /var/folders/1d/dgkmygls6x5gsb16pbn2mwlr0000gn/T//meshcore-ui-fonts/NotoSans-SemiBold.ttf -r 0x20,0x2D-0x2E,0x30-0x3A --format lvgl --lv-include lvgl.h --lv-font-name ui_font_40 -o /Users/jakub/Documents/MeshCore/MeshCore/examples/companion_radio/ui-lvgl/fonts/ui_font_40.c ******************************************************************************/ #ifdef LV_LVGL_H_INCLUDE_SIMPLE @@ -30,6 +30,12 @@ static LV_ATTRIBUTE_LARGE_CONST const uint8_t glyph_bitmap[] = { 0x8b, 0xff, 0xff, 0xff, 0xff, 0xf8, 0xbf, 0xff, 0xff, 0xff, 0xff, 0x80, + /* U+002E "." */ + 0x0, 0x2, 0x0, 0x0, 0x8f, 0xff, 0x80, 0x5f, + 0xff, 0xff, 0x58, 0xff, 0xff, 0xf8, 0x7f, 0xff, + 0xff, 0x83, 0xff, 0xff, 0xf3, 0x5, 0xdf, 0xd5, + 0x0, + /* U+0030 "0" */ 0x0, 0x0, 0x0, 0x4a, 0xdf, 0xfe, 0xc8, 0x20, 0x0, 0x0, 0x0, 0x0, 0x2, 0xdf, 0xff, 0xff, @@ -431,17 +437,18 @@ static const lv_font_fmt_txt_glyph_dsc_t glyph_dsc[] = { {.bitmap_index = 0, .adv_w = 0, .box_w = 0, .box_h = 0, .ofs_x = 0, .ofs_y = 0} /* id = 0 reserved */, {.bitmap_index = 0, .adv_w = 166, .box_w = 0, .box_h = 0, .ofs_x = 0, .ofs_y = 0}, {.bitmap_index = 0, .adv_w = 205, .box_w = 11, .box_h = 5, .ofs_x = 1, .ofs_y = 9}, - {.bitmap_index = 28, .adv_w = 366, .box_w = 21, .box_h = 28, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 322, .adv_w = 366, .box_w = 14, .box_h = 28, .ofs_x = 2, .ofs_y = 0}, - {.bitmap_index = 518, .adv_w = 366, .box_w = 21, .box_h = 28, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 812, .adv_w = 366, .box_w = 21, .box_h = 28, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 1106, .adv_w = 366, .box_w = 23, .box_h = 28, .ofs_x = 0, .ofs_y = 0}, - {.bitmap_index = 1428, .adv_w = 366, .box_w = 19, .box_h = 28, .ofs_x = 2, .ofs_y = 0}, - {.bitmap_index = 1694, .adv_w = 366, .box_w = 21, .box_h = 28, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 1988, .adv_w = 366, .box_w = 21, .box_h = 28, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 2282, .adv_w = 366, .box_w = 21, .box_h = 28, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 2576, .adv_w = 366, .box_w = 21, .box_h = 28, .ofs_x = 1, .ofs_y = 0}, - {.bitmap_index = 2870, .adv_w = 177, .box_w = 7, .box_h = 22, .ofs_x = 2, .ofs_y = 0} + {.bitmap_index = 28, .adv_w = 177, .box_w = 7, .box_h = 7, .ofs_x = 2, .ofs_y = 0}, + {.bitmap_index = 53, .adv_w = 366, .box_w = 21, .box_h = 28, .ofs_x = 1, .ofs_y = 0}, + {.bitmap_index = 347, .adv_w = 366, .box_w = 14, .box_h = 28, .ofs_x = 2, .ofs_y = 0}, + {.bitmap_index = 543, .adv_w = 366, .box_w = 21, .box_h = 28, .ofs_x = 1, .ofs_y = 0}, + {.bitmap_index = 837, .adv_w = 366, .box_w = 21, .box_h = 28, .ofs_x = 1, .ofs_y = 0}, + {.bitmap_index = 1131, .adv_w = 366, .box_w = 23, .box_h = 28, .ofs_x = 0, .ofs_y = 0}, + {.bitmap_index = 1453, .adv_w = 366, .box_w = 19, .box_h = 28, .ofs_x = 2, .ofs_y = 0}, + {.bitmap_index = 1719, .adv_w = 366, .box_w = 21, .box_h = 28, .ofs_x = 1, .ofs_y = 0}, + {.bitmap_index = 2013, .adv_w = 366, .box_w = 21, .box_h = 28, .ofs_x = 1, .ofs_y = 0}, + {.bitmap_index = 2307, .adv_w = 366, .box_w = 21, .box_h = 28, .ofs_x = 1, .ofs_y = 0}, + {.bitmap_index = 2601, .adv_w = 366, .box_w = 21, .box_h = 28, .ofs_x = 1, .ofs_y = 0}, + {.bitmap_index = 2895, .adv_w = 177, .box_w = 7, .box_h = 22, .ofs_x = 2, .ofs_y = 0} }; /*--------------------- @@ -449,18 +456,18 @@ static const lv_font_fmt_txt_glyph_dsc_t glyph_dsc[] = { *--------------------*/ static const uint16_t unicode_list_0[] = { - 0x0, 0xd + 0x0, 0xd, 0xe }; /*Collect the unicode lists and glyph_id offsets*/ static const lv_font_fmt_txt_cmap_t cmaps[] = { { - .range_start = 32, .range_length = 14, .glyph_id_start = 1, - .unicode_list = unicode_list_0, .glyph_id_ofs_list = NULL, .list_length = 2, .type = LV_FONT_FMT_TXT_CMAP_SPARSE_TINY + .range_start = 32, .range_length = 15, .glyph_id_start = 1, + .unicode_list = unicode_list_0, .glyph_id_ofs_list = NULL, .list_length = 3, .type = LV_FONT_FMT_TXT_CMAP_SPARSE_TINY }, { - .range_start = 48, .range_length = 11, .glyph_id_start = 3, + .range_start = 48, .range_length = 11, .glyph_id_start = 4, .unicode_list = NULL, .glyph_id_ofs_list = NULL, .list_length = 0, .type = LV_FONT_FMT_TXT_CMAP_FORMAT0_TINY } }; diff --git a/examples/companion_radio/ui-lvgl/map/TileCache.h b/examples/companion_radio/ui-lvgl/map/TileCache.h index d22a1ca4..55c5a67d 100644 --- a/examples/companion_radio/ui-lvgl/map/TileCache.h +++ b/examples/companion_radio/ui-lvgl/map/TileCache.h @@ -1,7 +1,9 @@ #pragma once // Decoded map tiles (RGB565, 128 KB each) kept for reuse while panning / zooming // back, least-recently-used evicted. Buffers live in PSRAM on the ESP32 and are -// allocated on first use, then kept for the session. +// allocated on first use, then kept for the session. Tiles found missing are +// remembered separately (no buffer), so looking up coarser zoom levels for the +// overzoom fallback doesn't push real tiles out. // // Single-TU fragment: included by ui-lvgl/UITask.cpp only. @@ -15,7 +17,7 @@ namespace mapview { class TileCache { public: - static const int SLOTS = 12; // 6 on screen at 320x218 + a ring of neighbours + static const int SLOTS = 24; // 6 on screen at 320x218 + the 14-tile read-ahead ring + a few coarser (3 MB) struct Slot { int16_t z = -1; @@ -26,10 +28,13 @@ public: lv_image_dsc_t dsc; }; - // The slot holding z/x/y if it was loaded (present or known missing), else nullptr. + // The slot holding z/x/y if it was loaded (present, or known missing: a + // shared slot with present == false), else nullptr. Slot* find(int z, int x, int y) { for (Slot& s : _slots) if (s.z == z && s.x == x && s.y == y) { s.used = ++_tick; return &s; } + for (const Missing& m : _missing) + if (m.z == z && m.x == x && m.y == y) return &_miss_slot; return nullptr; } @@ -49,6 +54,12 @@ public: s->z = z; s->x = x; s->y = y; s->used = ++_tick; s->present = src.renderTile(z, x, y, s->px); + if (!s->present) { // remember the miss without holding a buffer slot + Missing& m = _missing[_miss_next++ % MISSING]; + m.z = (int16_t)z; m.x = x; m.y = y; + s->z = -1; s->used = 0; + return &_miss_slot; + } memset(&s->dsc, 0, sizeof(s->dsc)); s->dsc.header.magic = LV_IMAGE_HEADER_MAGIC; s->dsc.header.cf = LV_COLOR_FORMAT_RGB565; @@ -61,13 +72,18 @@ public: } // Forget the "no tile here" answers (new tiles may have been written). - void forgetMissing() { for (Slot& s : _slots) if (s.z >= 0 && !s.present) { s.z = -1; s.used = 0; } } + void forgetMissing() { for (Missing& m : _missing) m.z = -1; } // Forget everything (e.g. the card was swapped); keeps the buffers. - void invalidate() { for (Slot& s : _slots) { s.z = -1; s.used = 0; } } + void invalidate() { for (Slot& s : _slots) { s.z = -1; s.used = 0; } forgetMissing(); } private: + struct Missing { int16_t z = -1; int32_t x = 0, y = 0; }; + static const int MISSING = 48; Slot _slots[SLOTS]; + Missing _missing[MISSING]; + int _miss_next = 0; + Slot _miss_slot; // present == false uint32_t _tick = 0; }; diff --git a/examples/companion_radio/ui-lvgl/map/TileDownloader.h b/examples/companion_radio/ui-lvgl/map/TileDownloader.h index 1f2f90f1..40bbc287 100644 --- a/examples/companion_radio/ui-lvgl/map/TileDownloader.h +++ b/examples/companion_radio/ui-lvgl/map/TileDownloader.h @@ -4,6 +4,10 @@ // alternative is tools/maps/fetch_tiles.py). Tiles already on the card are // skipped, so a download can be repeated to fill gaps or extend an area. // +// The job (area + zoom range) is kept in /.job until it completes, so a +// download cut short by a power-off, a lost network or Stop can be resumed: +// resuming re-runs the same job, and the tiles already written are skipped. +// // Driven by loop() from the UI loop: one small step per call, the HTTP GET // itself runs asynchronously in the board's fetcher (lvport::fetch*), so the // mesh keeps being serviced during a long download. @@ -78,6 +82,20 @@ public: const char* message() const { return _msg; } const char* sourceHost() { loadSource(); return _host; } + // Unfinished job from /.job (read once, then tracked in memory). + bool savedJob(TileArea& a) { + if (!_job_checked) { _job_checked = true; _has_job = readJob(_job); } + if (_has_job) a = _job; + return _has_job; + } + void discardJob() { + char path[64]; + jobPath(path, sizeof(path)); + remove(path); + _has_job = false; + _job_checked = true; + } + bool start(const TileArea& a, const char* ssid, const char* pass) { if (active()) return false; _area = a; @@ -88,6 +106,7 @@ public: if (_total == 0 || _total > MAX_TILES) { fail("Area too large: use the PC tool"); return false; } loadSource(); writeAttribution(); + writeJob(a); _z = a.zmin; tileRange(a, _z, _x0, _y0, _x1, _y1); _x = _x0; _y = _y0; @@ -174,6 +193,8 @@ private: char _attr[96] = ""; char _host[48] = ""; char _msg[64] = ""; + TileArea _job = {0, 0, 0, 0, 0, 0}; + bool _has_job = false, _job_checked = false; void fail(const char* m) { snprintf(_msg, sizeof(_msg), "%s", m); _state = FAILED; } @@ -181,6 +202,35 @@ private: snprintf(_msg, sizeof(_msg), "%s", m); _state = s; lvport::netEnd(); + if (s == DONE) discardJob(); // anything else stays resumable + } + + void jobPath(char* out, size_t n) const { snprintf(out, n, "%s/.job", _root); } + + void writeJob(const TileArea& a) { + char path[64]; + jobPath(path, sizeof(path)); + makeParents(path); + if (FILE* f = fopen(path, "w")) { + fprintf(f, "%.6f %.6f %.6f %.6f %d %d\n", a.lon0, a.lat0, a.lon1, a.lat1, a.zmin, a.zmax); + fclose(f); + } + _job = a; + _has_job = true; + _job_checked = true; + } + + bool readJob(TileArea& a) const { + char path[64]; + jobPath(path, sizeof(path)); + FILE* f = fopen(path, "r"); + if (!f) return false; + TileArea t; + int n = fscanf(f, "%lf %lf %lf %lf %d %d", &t.lon0, &t.lat0, &t.lon1, &t.lat1, &t.zmin, &t.zmax); + fclose(f); + if (n != 6 || t.zmin < 0 || t.zmax > 20 || t.zmin > t.zmax) return false; + a = t; + return true; } void onFailure(const char* m) { diff --git a/variants/wio-tracker-l2/WioTrackerL2Display.h b/variants/wio-tracker-l2/WioTrackerL2Display.h index 299df949..65fcc2b7 100644 --- a/variants/wio-tracker-l2/WioTrackerL2Display.h +++ b/variants/wio-tracker-l2/WioTrackerL2Display.h @@ -162,6 +162,12 @@ class WioTrackerL2Display : public LGFXDisplay { public: WioTrackerL2Display() : LGFXDisplay(320, 240, disp) {} + // NodePrefs::display_brightness level 0-4 -> backlight PWM + void setBrightness(uint8_t level) override { + static const uint8_t PWM[5] = { 20, 60, 120, 190, 255 }; + display->setBrightness(PWM[level > 4 ? 4 : level]); + } + // direct access to the LGFX device (LVGL flush/touch glue) lgfx::LGFX_Device* lgfxDevice() { return &disp; }