2025-03-04 23:09:43 +11:00
|
|
|
|
#include "UITask.h"
|
2026-06-08 21:51:03 +02:00
|
|
|
|
#include "SoundNotifier.h"
|
2025-03-04 23:09:43 +11:00
|
|
|
|
#include <helpers/TxtDataHelpers.h>
|
2025-08-16 20:04:54 +10:00
|
|
|
|
#include "../MyMesh.h"
|
2026-05-12 14:16:37 +02:00
|
|
|
|
#include "../MsgExpand.h"
|
2026-05-24 20:41:25 +02:00
|
|
|
|
#include "../Features.h"
|
2026-06-03 08:20:59 +02:00
|
|
|
|
#include "../GeoUtils.h"
|
2025-08-08 20:01:31 +10:00
|
|
|
|
#include "target.h"
|
2025-10-27 17:58:29 +01:00
|
|
|
|
#ifdef WIFI_SSID
|
|
|
|
|
|
#include <WiFi.h>
|
|
|
|
|
|
#endif
|
2025-03-04 23:09:43 +11:00
|
|
|
|
|
2025-09-03 18:17:37 +02:00
|
|
|
|
#ifndef AUTO_OFF_MILLIS
|
|
|
|
|
|
#define AUTO_OFF_MILLIS 15000 // 15 seconds
|
|
|
|
|
|
#endif
|
2025-06-07 15:57:22 -07:00
|
|
|
|
#define BOOT_SCREEN_MILLIS 3000 // 3 seconds
|
2025-03-04 23:09:43 +11:00
|
|
|
|
|
2025-04-24 21:54:37 -04:00
|
|
|
|
#ifdef PIN_STATUS_LED
|
|
|
|
|
|
#define LED_ON_MILLIS 20
|
|
|
|
|
|
#define LED_ON_MSG_MILLIS 200
|
|
|
|
|
|
#define LED_CYCLE_MILLIS 4000
|
|
|
|
|
|
#endif
|
|
|
|
|
|
|
2025-08-08 20:01:31 +10:00
|
|
|
|
#define LONG_PRESS_MILLIS 1200
|
|
|
|
|
|
|
2025-08-16 18:13:50 +02:00
|
|
|
|
#ifndef UI_RECENT_LIST_SIZE
|
|
|
|
|
|
#define UI_RECENT_LIST_SIZE 4
|
|
|
|
|
|
#endif
|
|
|
|
|
|
|
2025-10-16 17:33:22 +11:00
|
|
|
|
#if UI_HAS_JOYSTICK
|
|
|
|
|
|
#define PRESS_LABEL "press Enter"
|
|
|
|
|
|
#else
|
|
|
|
|
|
#define PRESS_LABEL "long press"
|
|
|
|
|
|
#endif
|
2025-08-08 20:01:31 +10:00
|
|
|
|
|
|
|
|
|
|
#include "icons.h"
|
feat(companion): live location sharing, Locator geofencing, trail auto-pause
Squash merge of feat/location-beacon-alerts-autopause (v1.21).
Features:
- Live Location Sharing — broadcast position as movement-gated [LOC]
messages to a channel or contact; live shares show as map pins with
distance/bearing in Nearby Nodes and a status-bar indicator. [LOC] is
parsed in DMs, channel messages and room messages; DM shares name the
sender.
- Locator (geofence) — arm a geofence around a saved waypoint or a person
(live [LOC] or last-known position), alert on arrive/leave or near/far,
with an optional homing beeper (gated to arrive/both modes). Arm from
Tools > Locator, Nearby Nodes, or Waypoints; target picker lists
favourites first, clearable via a "None" entry. Active target is drawn
as a flag on the map.
- One active target shared across Locator / Navigate / Map via a single
resolver (resolvePersonPos / activeTargetPos) that prefers a live
[LOC] share over the last-advertised GPS fix.
- Follow live contacts — Navigate to a live-sharing contact follows them
as they move and adds an ETA line; quick-share your own position from
the Map.
- Map & status-bar upgrades — home mini-map gets a north marker, scale
tick, and a connected trail line (was disconnected dots); status line
shows tracked-node count, an arrow + distance to the active Locator/Nav
target (falling back to the nearest live-tracked contact); GPS fix icon
in the top status bar, shown only on GPS boards with GPS enabled.
- Trail auto-pause — recording freezes on stops (banking elapsed time,
breaking the map line across the idle gap) and resumes on movement
without ending the session.
- Streaming trail simplification — GPS points are simplified in-stream
via a fixed-corridor (Reumann-Witkam) pass tuned for fidelity: straight
runs collapse to their endpoints, curves stay bounded to the Min-dist
tolerance, so the 512-point buffer covers a far longer route than a
flat point budget would suggest.
- Collapsible Tools (Location / Comms / System sections, fold-in-place
like Settings) and page-indicator icons on the home carousel.
- Waypoint coordinate editor — add a waypoint by scroll-editing lat/lon
digit by digit.
Fixes:
- Critical: low-heap hang and contact loss on RAM-tight builds. Halved
message-history scrollback rings (recovering ~14 KB free heap) and
made contacts/channels/prefs persistence atomic (temp file + rename),
so an interrupted save can no longer corrupt or wipe the store.
- Serial.write() bounded so a stalled USB host can't hang the device.
- Nearby Nodes: live [LOC] senders respect the type filter, sort by
shared position, and the list refreshes so live shares bubble up.
- Map: live contacts are labelled before waypoints.
- GPS status icon hidden when GPS is off in Settings.
- Splash screen no longer truncates a pre-release tag's own dash (e.g.
v1.21-rc1) when stripping the build's commit-hash suffix.
- Null-guarded the Locator target picker; clamped loc-share channel
index on load.
Under the hood:
- -Os size optimisation on the e-ink and GAT562 30S solo envs.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 19:04:21 +02:00
|
|
|
|
#include "GfxUtils.h" // gfx::drawLine — connects trail points on the Home map preview
|
2025-08-08 20:01:31 +10:00
|
|
|
|
|
2026-06-15 18:17:34 +02:00
|
|
|
|
// Blinking status indicators: on for the first half of a 4 s cycle, but e-ink
|
|
|
|
|
|
// can't repaint fast enough to blink, so it shows them steadily.
|
|
|
|
|
|
static inline bool blinkOn() {
|
|
|
|
|
|
return Features::BLINK_INDICATORS ? ((millis() % 4000) < 2000) : true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-08-08 20:01:31 +10:00
|
|
|
|
class SplashScreen : public UIScreen {
|
|
|
|
|
|
UITask* _task;
|
|
|
|
|
|
unsigned long dismiss_after;
|
|
|
|
|
|
char _version_info[12];
|
2026-05-29 12:46:29 +02:00
|
|
|
|
char _solo_ver[12];
|
2025-08-08 20:01:31 +10:00
|
|
|
|
|
|
|
|
|
|
public:
|
|
|
|
|
|
SplashScreen(UITask* task) : _task(task) {
|
2026-05-24 00:35:07 +02:00
|
|
|
|
// MeshCore upstream version shown large (e.g. "1.15")
|
|
|
|
|
|
strncpy(_version_info, MESHCORE_VERSION, sizeof(_version_info) - 1);
|
|
|
|
|
|
_version_info[sizeof(_version_info) - 1] = '\0';
|
|
|
|
|
|
|
feat(companion): live location sharing, Locator geofencing, trail auto-pause
Squash merge of feat/location-beacon-alerts-autopause (v1.21).
Features:
- Live Location Sharing — broadcast position as movement-gated [LOC]
messages to a channel or contact; live shares show as map pins with
distance/bearing in Nearby Nodes and a status-bar indicator. [LOC] is
parsed in DMs, channel messages and room messages; DM shares name the
sender.
- Locator (geofence) — arm a geofence around a saved waypoint or a person
(live [LOC] or last-known position), alert on arrive/leave or near/far,
with an optional homing beeper (gated to arrive/both modes). Arm from
Tools > Locator, Nearby Nodes, or Waypoints; target picker lists
favourites first, clearable via a "None" entry. Active target is drawn
as a flag on the map.
- One active target shared across Locator / Navigate / Map via a single
resolver (resolvePersonPos / activeTargetPos) that prefers a live
[LOC] share over the last-advertised GPS fix.
- Follow live contacts — Navigate to a live-sharing contact follows them
as they move and adds an ETA line; quick-share your own position from
the Map.
- Map & status-bar upgrades — home mini-map gets a north marker, scale
tick, and a connected trail line (was disconnected dots); status line
shows tracked-node count, an arrow + distance to the active Locator/Nav
target (falling back to the nearest live-tracked contact); GPS fix icon
in the top status bar, shown only on GPS boards with GPS enabled.
- Trail auto-pause — recording freezes on stops (banking elapsed time,
breaking the map line across the idle gap) and resumes on movement
without ending the session.
- Streaming trail simplification — GPS points are simplified in-stream
via a fixed-corridor (Reumann-Witkam) pass tuned for fidelity: straight
runs collapse to their endpoints, curves stay bounded to the Min-dist
tolerance, so the 512-point buffer covers a far longer route than a
flat point budget would suggest.
- Collapsible Tools (Location / Comms / System sections, fold-in-place
like Settings) and page-indicator icons on the home carousel.
- Waypoint coordinate editor — add a waypoint by scroll-editing lat/lon
digit by digit.
Fixes:
- Critical: low-heap hang and contact loss on RAM-tight builds. Halved
message-history scrollback rings (recovering ~14 KB free heap) and
made contacts/channels/prefs persistence atomic (temp file + rename),
so an interrupted save can no longer corrupt or wipe the store.
- Serial.write() bounded so a stalled USB host can't hang the device.
- Nearby Nodes: live [LOC] senders respect the type filter, sort by
shared position, and the list refreshes so live shares bubble up.
- Map: live contacts are labelled before waypoints.
- GPS status icon hidden when GPS is off in Settings.
- Splash screen no longer truncates a pre-release tag's own dash (e.g.
v1.21-rc1) when stripping the build's commit-hash suffix.
- Null-guarded the Locator target picker; clamped loc-share channel
index on load.
Under the hood:
- -Os size optimisation on the e-ink and GAT562 30S solo envs.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 19:04:21 +02:00
|
|
|
|
// Solo firmware version: strip the commit-hash suffix build.sh always
|
|
|
|
|
|
// appends as the LAST dash-segment (v1.15-solo.1-abcdef -> v1.15-solo.1).
|
|
|
|
|
|
// Must be the last dash, not the first: a tag like v1.21-rc1 has a dash
|
|
|
|
|
|
// of its own before the commit hash gets appended.
|
2025-08-08 20:01:31 +10:00
|
|
|
|
const char *ver = FIRMWARE_VERSION;
|
feat(companion): live location sharing, Locator geofencing, trail auto-pause
Squash merge of feat/location-beacon-alerts-autopause (v1.21).
Features:
- Live Location Sharing — broadcast position as movement-gated [LOC]
messages to a channel or contact; live shares show as map pins with
distance/bearing in Nearby Nodes and a status-bar indicator. [LOC] is
parsed in DMs, channel messages and room messages; DM shares name the
sender.
- Locator (geofence) — arm a geofence around a saved waypoint or a person
(live [LOC] or last-known position), alert on arrive/leave or near/far,
with an optional homing beeper (gated to arrive/both modes). Arm from
Tools > Locator, Nearby Nodes, or Waypoints; target picker lists
favourites first, clearable via a "None" entry. Active target is drawn
as a flag on the map.
- One active target shared across Locator / Navigate / Map via a single
resolver (resolvePersonPos / activeTargetPos) that prefers a live
[LOC] share over the last-advertised GPS fix.
- Follow live contacts — Navigate to a live-sharing contact follows them
as they move and adds an ETA line; quick-share your own position from
the Map.
- Map & status-bar upgrades — home mini-map gets a north marker, scale
tick, and a connected trail line (was disconnected dots); status line
shows tracked-node count, an arrow + distance to the active Locator/Nav
target (falling back to the nearest live-tracked contact); GPS fix icon
in the top status bar, shown only on GPS boards with GPS enabled.
- Trail auto-pause — recording freezes on stops (banking elapsed time,
breaking the map line across the idle gap) and resumes on movement
without ending the session.
- Streaming trail simplification — GPS points are simplified in-stream
via a fixed-corridor (Reumann-Witkam) pass tuned for fidelity: straight
runs collapse to their endpoints, curves stay bounded to the Min-dist
tolerance, so the 512-point buffer covers a far longer route than a
flat point budget would suggest.
- Collapsible Tools (Location / Comms / System sections, fold-in-place
like Settings) and page-indicator icons on the home carousel.
- Waypoint coordinate editor — add a waypoint by scroll-editing lat/lon
digit by digit.
Fixes:
- Critical: low-heap hang and contact loss on RAM-tight builds. Halved
message-history scrollback rings (recovering ~14 KB free heap) and
made contacts/channels/prefs persistence atomic (temp file + rename),
so an interrupted save can no longer corrupt or wipe the store.
- Serial.write() bounded so a stalled USB host can't hang the device.
- Nearby Nodes: live [LOC] senders respect the type filter, sort by
shared position, and the list refreshes so live shares bubble up.
- Map: live contacts are labelled before waypoints.
- GPS status icon hidden when GPS is off in Settings.
- Splash screen no longer truncates a pre-release tag's own dash (e.g.
v1.21-rc1) when stripping the build's commit-hash suffix.
- Null-guarded the Locator target picker; clamped loc-share channel
index on load.
Under the hood:
- -Os size optimisation on the e-ink and GAT562 30S solo envs.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 19:04:21 +02:00
|
|
|
|
const char *dash = strrchr(ver, '-');
|
2026-05-24 00:35:07 +02:00
|
|
|
|
int plen = dash ? (int)(dash - ver) : (int)strlen(ver);
|
2026-05-29 12:46:29 +02:00
|
|
|
|
if (plen >= (int)sizeof(_solo_ver)) plen = sizeof(_solo_ver) - 1;
|
|
|
|
|
|
memcpy(_solo_ver, ver, plen);
|
|
|
|
|
|
_solo_ver[plen] = '\0';
|
2026-05-16 00:33:13 +02:00
|
|
|
|
|
2025-08-08 20:01:31 +10:00
|
|
|
|
dismiss_after = millis() + BOOT_SCREEN_MILLIS;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
int render(DisplayDriver& display) override {
|
refactor: replace all hardcoded pixel positions with layout helpers
Add lineStep(), headerH(), listStart(), listVisible(), valCol() to
DisplayDriver so layout derives from getLineHeight() instead of fixed
OLED constants. Refactor all screens (SettingsScreen, NearbyScreen,
FullscreenMsgView, QuickMsgScreen, BotScreen, DashboardConfigScreen,
AutoAdvertScreen, RingtoneEditorScreen) and all HomeScreen pages plus
the lock screen and splash screen in UITask.cpp.
On OLED (lh=8) positions are unchanged; on landscape e-ink (lh=16) all
text and widgets now scale correctly to the 250×122 display.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 18:05:09 +02:00
|
|
|
|
display.setTextSize(1);
|
|
|
|
|
|
const int lh = display.getLineHeight();
|
|
|
|
|
|
const int step = display.lineStep();
|
|
|
|
|
|
|
2025-08-08 20:01:31 +10:00
|
|
|
|
// meshcore logo
|
WioTrackerL1: redesign companion UI (channels, keyboard, msg preview)
- QuickMsg → Message screen: channel history, on-screen keyboard, custom message first
- Channel history: RAM ring buffer (32 entries, 80 chars), fullscreen view with word-wrap scroll
- After channel send: stay in history, own message shown as "Me: ..."
- MsgPreview: one message per screen with up/down nav, ENTER opens fullscreen scroll
- Monochromatic display: all color usage corrected to DARK/LIGHT only
- Top bar: battery icon, BT muted indicator, splash "Plus for Wio"
- KB_MAX_LEN: 64 → 100 chars; channel text buffer: 52 → 80 chars
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 15:56:30 +02:00
|
|
|
|
display.setColor(DisplayDriver::LIGHT);
|
2026-05-22 23:53:16 +02:00
|
|
|
|
int logoWidth = 128;
|
refactor: replace all hardcoded pixel positions with layout helpers
Add lineStep(), headerH(), listStart(), listVisible(), valCol() to
DisplayDriver so layout derives from getLineHeight() instead of fixed
OLED constants. Refactor all screens (SettingsScreen, NearbyScreen,
FullscreenMsgView, QuickMsgScreen, BotScreen, DashboardConfigScreen,
AutoAdvertScreen, RingtoneEditorScreen) and all HomeScreen pages plus
the lock screen and splash screen in UITask.cpp.
On OLED (lh=8) positions are unchanged; on landscape e-ink (lh=16) all
text and widgets now scale correctly to the 250×122 display.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 18:05:09 +02:00
|
|
|
|
int logo_y = 3;
|
|
|
|
|
|
display.drawXbm((display.width() - logoWidth) / 2, logo_y, meshcore_logo, logoWidth, 13);
|
2025-08-08 20:01:31 +10:00
|
|
|
|
|
refactor: replace all hardcoded pixel positions with layout helpers
Add lineStep(), headerH(), listStart(), listVisible(), valCol() to
DisplayDriver so layout derives from getLineHeight() instead of fixed
OLED constants. Refactor all screens (SettingsScreen, NearbyScreen,
FullscreenMsgView, QuickMsgScreen, BotScreen, DashboardConfigScreen,
AutoAdvertScreen, RingtoneEditorScreen) and all HomeScreen pages plus
the lock screen and splash screen in UITask.cpp.
On OLED (lh=8) positions are unchanged; on landscape e-ink (lh=16) all
text and widgets now scale correctly to the 250×122 display.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 18:05:09 +02:00
|
|
|
|
// version info at sz2
|
|
|
|
|
|
int ver_y = logo_y + 13 + 2;
|
2025-08-08 20:01:31 +10:00
|
|
|
|
display.setTextSize(2);
|
2026-05-22 18:34:20 +02:00
|
|
|
|
int lh2 = display.getLineHeight();
|
refactor: replace all hardcoded pixel positions with layout helpers
Add lineStep(), headerH(), listStart(), listVisible(), valCol() to
DisplayDriver so layout derives from getLineHeight() instead of fixed
OLED constants. Refactor all screens (SettingsScreen, NearbyScreen,
FullscreenMsgView, QuickMsgScreen, BotScreen, DashboardConfigScreen,
AutoAdvertScreen, RingtoneEditorScreen) and all HomeScreen pages plus
the lock screen and splash screen in UITask.cpp.
On OLED (lh=8) positions are unchanged; on landscape e-ink (lh=16) all
text and widgets now scale correctly to the 250×122 display.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 18:05:09 +02:00
|
|
|
|
display.drawTextCentered(display.width()/2, ver_y, _version_info);
|
2025-08-08 20:01:31 +10:00
|
|
|
|
|
2026-05-22 18:34:20 +02:00
|
|
|
|
// build date at sz1, below sz2 version
|
|
|
|
|
|
int date_y = ver_y + lh2 + 2;
|
2025-08-08 20:01:31 +10:00
|
|
|
|
display.setTextSize(1);
|
refactor: replace all hardcoded pixel positions with layout helpers
Add lineStep(), headerH(), listStart(), listVisible(), valCol() to
DisplayDriver so layout derives from getLineHeight() instead of fixed
OLED constants. Refactor all screens (SettingsScreen, NearbyScreen,
FullscreenMsgView, QuickMsgScreen, BotScreen, DashboardConfigScreen,
AutoAdvertScreen, RingtoneEditorScreen) and all HomeScreen pages plus
the lock screen and splash screen in UITask.cpp.
On OLED (lh=8) positions are unchanged; on landscape e-ink (lh=16) all
text and widgets now scale correctly to the 250×122 display.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 18:05:09 +02:00
|
|
|
|
display.drawTextCentered(display.width()/2, date_y, FIRMWARE_BUILD_DATE);
|
2025-08-08 20:01:31 +10:00
|
|
|
|
|
2026-05-29 12:25:34 +02:00
|
|
|
|
#ifdef FIRMWARE_SOLO_BUILD
|
2026-05-29 12:46:29 +02:00
|
|
|
|
int solo_y = date_y + step;
|
|
|
|
|
|
display.fillRect(0, solo_y - 1, display.width(), lh + 2);
|
WioTrackerL1: redesign companion UI (channels, keyboard, msg preview)
- QuickMsg → Message screen: channel history, on-screen keyboard, custom message first
- Channel history: RAM ring buffer (32 entries, 80 chars), fullscreen view with word-wrap scroll
- After channel send: stay in history, own message shown as "Me: ..."
- MsgPreview: one message per screen with up/down nav, ENTER opens fullscreen scroll
- Monochromatic display: all color usage corrected to DARK/LIGHT only
- Top bar: battery icon, BT muted indicator, splash "Plus for Wio"
- KB_MAX_LEN: 64 → 100 chars; channel text buffer: 52 → 80 chars
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 15:56:30 +02:00
|
|
|
|
display.setColor(DisplayDriver::DARK);
|
2026-05-29 12:46:29 +02:00
|
|
|
|
char solo_label[24];
|
|
|
|
|
|
if (_solo_ver[0])
|
2026-06-12 09:46:45 +02:00
|
|
|
|
snprintf(solo_label, sizeof(solo_label), "Solo %s", _solo_ver);
|
2026-05-16 00:33:13 +02:00
|
|
|
|
else
|
2026-06-12 09:46:45 +02:00
|
|
|
|
snprintf(solo_label, sizeof(solo_label), "Solo");
|
2026-05-29 12:46:29 +02:00
|
|
|
|
display.drawTextCentered(display.width()/2, solo_y, solo_label);
|
WioTrackerL1: redesign companion UI (channels, keyboard, msg preview)
- QuickMsg → Message screen: channel history, on-screen keyboard, custom message first
- Channel history: RAM ring buffer (32 entries, 80 chars), fullscreen view with word-wrap scroll
- After channel send: stay in history, own message shown as "Me: ..."
- MsgPreview: one message per screen with up/down nav, ENTER opens fullscreen scroll
- Monochromatic display: all color usage corrected to DARK/LIGHT only
- Top bar: battery icon, BT muted indicator, splash "Plus for Wio"
- KB_MAX_LEN: 64 → 100 chars; channel text buffer: 52 → 80 chars
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 15:56:30 +02:00
|
|
|
|
display.setColor(DisplayDriver::LIGHT);
|
2026-05-10 22:58:46 +02:00
|
|
|
|
#endif
|
|
|
|
|
|
|
2025-08-08 20:01:31 +10:00
|
|
|
|
return 1000;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void poll() override {
|
|
|
|
|
|
if (millis() >= dismiss_after) {
|
|
|
|
|
|
_task->gotoHomeScreen();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-05-11 16:52:24 +02:00
|
|
|
|
static const int QUICK_MSGS_MAX = 10;
|
|
|
|
|
|
|
2026-05-12 09:51:30 +02:00
|
|
|
|
|
2026-05-13 23:54:26 +02:00
|
|
|
|
#include "FullscreenMsgView.h"
|
2026-05-14 00:12:07 +02:00
|
|
|
|
#include "SensorPlaceholders.h"
|
2026-05-17 00:12:40 +02:00
|
|
|
|
#include "SettingsScreen.h"
|
|
|
|
|
|
#include "QuickMsgScreen.h"
|
2026-05-10 18:45:35 +02:00
|
|
|
|
|
2026-05-12 14:45:02 +02:00
|
|
|
|
// ── Custom screens (separate files to ease upstream merges) ───────────────────
|
|
|
|
|
|
#include "RingtoneEditorScreen.h"
|
|
|
|
|
|
#include "BotScreen.h"
|
feat: NearbyScreen, configurable clock dashboard, memory optimizations
- Add NearbyScreen: GPS contact list with haversine distance sort,
type filter (ALL/Chat/Rpt/Room/Snsr), detail view with ping (RTT via
ACK poll), node scan via advert(), context menu
- Add DashboardConfigScreen: configure up to 3 data fields on the clock
page (Battery, Temp, Humidity, Pressure, GPS, Altitude, Lux, CO2,
Contacts); entered via long-press ENTER on clock page
- Rework HP_CLOCK layout: full-screen clock (size 2 at y=0), date at
y=19, separator at y=28, three data field rows at y=31/41/51;
hide node name/battery indicator on clock page
- Persist dashboard_fields[3] in NodePrefs and DataStore
- Add isAckPending() to MyMesh for NearbyScreen ping detection
- Wire NearbyScreen into ToolsScreen and UITask
- Reduce NearbyScreen entry buffer from MAX_CONTACTS(100) to 32,
saving ~3.3 KB RAM
- Switch haversine/bearing math from double to float (sinf/cosf/atan2f),
eliminating double libm and saving ~5-10 KB flash
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 11:28:36 +02:00
|
|
|
|
#include "NearbyScreen.h"
|
|
|
|
|
|
#include "DashboardConfigScreen.h"
|
2026-05-14 16:08:33 +02:00
|
|
|
|
#include "AutoAdvertScreen.h"
|
feat(companion): live location sharing, Locator geofencing, trail auto-pause
Squash merge of feat/location-beacon-alerts-autopause (v1.21).
Features:
- Live Location Sharing — broadcast position as movement-gated [LOC]
messages to a channel or contact; live shares show as map pins with
distance/bearing in Nearby Nodes and a status-bar indicator. [LOC] is
parsed in DMs, channel messages and room messages; DM shares name the
sender.
- Locator (geofence) — arm a geofence around a saved waypoint or a person
(live [LOC] or last-known position), alert on arrive/leave or near/far,
with an optional homing beeper (gated to arrive/both modes). Arm from
Tools > Locator, Nearby Nodes, or Waypoints; target picker lists
favourites first, clearable via a "None" entry. Active target is drawn
as a flag on the map.
- One active target shared across Locator / Navigate / Map via a single
resolver (resolvePersonPos / activeTargetPos) that prefers a live
[LOC] share over the last-advertised GPS fix.
- Follow live contacts — Navigate to a live-sharing contact follows them
as they move and adds an ETA line; quick-share your own position from
the Map.
- Map & status-bar upgrades — home mini-map gets a north marker, scale
tick, and a connected trail line (was disconnected dots); status line
shows tracked-node count, an arrow + distance to the active Locator/Nav
target (falling back to the nearest live-tracked contact); GPS fix icon
in the top status bar, shown only on GPS boards with GPS enabled.
- Trail auto-pause — recording freezes on stops (banking elapsed time,
breaking the map line across the idle gap) and resumes on movement
without ending the session.
- Streaming trail simplification — GPS points are simplified in-stream
via a fixed-corridor (Reumann-Witkam) pass tuned for fidelity: straight
runs collapse to their endpoints, curves stay bounded to the Min-dist
tolerance, so the 512-point buffer covers a far longer route than a
flat point budget would suggest.
- Collapsible Tools (Location / Comms / System sections, fold-in-place
like Settings) and page-indicator icons on the home carousel.
- Waypoint coordinate editor — add a waypoint by scroll-editing lat/lon
digit by digit.
Fixes:
- Critical: low-heap hang and contact loss on RAM-tight builds. Halved
message-history scrollback rings (recovering ~14 KB free heap) and
made contacts/channels/prefs persistence atomic (temp file + rename),
so an interrupted save can no longer corrupt or wipe the store.
- Serial.write() bounded so a stalled USB host can't hang the device.
- Nearby Nodes: live [LOC] senders respect the type filter, sort by
shared position, and the list refreshes so live shares bubble up.
- Map: live contacts are labelled before waypoints.
- GPS status icon hidden when GPS is off in Settings.
- Splash screen no longer truncates a pre-release tag's own dash (e.g.
v1.21-rc1) when stripping the build's commit-hash suffix.
- Null-guarded the Locator target picker; clamped loc-share channel
index on load.
Under the hood:
- -Os size optimisation on the e-ink and GAT562 30S solo envs.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 19:04:21 +02:00
|
|
|
|
#include "LiveShareScreen.h"
|
|
|
|
|
|
#include "LocatorScreen.h"
|
2026-05-25 09:10:40 +02:00
|
|
|
|
#include "TrailScreen.h"
|
2026-06-03 14:07:53 +02:00
|
|
|
|
#include "CompassScreen.h"
|
2026-06-18 22:34:52 +02:00
|
|
|
|
#include "DiagnosticsScreen.h"
|
feat(repeater): politeness controls, dedicated radio profile, and diagnostics
Settings > Radio gains a repeater toggle, 16 community-suggested radio
presets plus manual Freq/SF/BW/CR tuning (digit-by-digit Freq editor),
4 persisted user preset slots, and the packet pool bumped 16->32 so
queued retransmits stop starving incoming-packet allocation while
relaying.
Four opt-in politeness knobs (skip-advert, max-hops, yield, min-SNR),
all flood-only and off by default, plus overhear suppression that
cancels a queued retransmit if a peer relays the same packet first.
Adaptive Power Control is suppressed while relaying (pins TX power to
the ceiling) and duty-cycle RX is forced off, since a repeater needs
to hear and relay at consistent power.
A dedicated Tools > Repeater screen consolidates the toggle, the
politeness knobs, and live forwarding stats, plus an optional "Custom"
radio profile — a dedicated frequency/SF/BW/CR for relaying, separate
from the companion's own network, band-matched to the companion
frequency by default and used everywhere a radio change can happen
(boot, on-device toggle, app-driven CMD_SET_RADIO_PARAMS, Settings
radio edits) so the device never silently falls back to the wrong
params mid-relay.
Diagnostics gains the actually-forwarded packet count, real heap-free
via mallinfo(), a reset-counters popup, and hardened loop-detect
bounds. Also: a frequency-floor fix and a float-equality preset-match
fix in the repeater profile logic, deduped BW-table/valCol/profile-
seeding helpers shared between Settings and the Repeater screen, and
a build.sh fix tolerating control characters in `pio project config`'s
JSON dump.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-20 09:07:00 +02:00
|
|
|
|
#include "RepeaterScreen.h"
|
2026-05-12 14:45:02 +02:00
|
|
|
|
#include "ToolsScreen.h"
|
Add ringtone editor, buzzer volume, unread fixes and buzzer notification fixes
- RingtoneEditorScreen: 32-note step sequencer accessible via new Tools page on home screen. U/D=pitch, ENTER=octave cycle, L/R=cursor, MENU=options (play, duration, BPM, insert/delete, save). Notes stored packed in NodePrefs and persisted via DataStore.
- ToolsScreen: new "Tools" home page entry launching the editor
- Buzzer volume: 0-4 bar control in Settings > Sound, persisted to prefs
- Buzzer notifications: fixed notify() firing inside wrong serial-guard branch and before addChannelMsg set the channel index
- Unread counter: fixed newest-first index direction; watermark formula now correct
- OLED brightness: wider range via pre-charge register 0xD9 combined with contrast
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 09:40:37 +02:00
|
|
|
|
|
2026-05-28 22:57:50 +02:00
|
|
|
|
#ifndef BATT_MIN_MILLIVOLTS
|
|
|
|
|
|
#define BATT_MIN_MILLIVOLTS 3200
|
|
|
|
|
|
#endif
|
|
|
|
|
|
|
|
|
|
|
|
// LiPo discharge curve: voltage (mV) → raw capacity (%). Shared by the top-bar
|
|
|
|
|
|
// battery indicator and the dashboard Batt% field so both report the same
|
|
|
|
|
|
// number for the same voltage. low_mv (typically NodePrefs.low_batt_mv, the
|
|
|
|
|
|
// user-configurable auto-shutdown threshold in Settings) is rescaled to 0%
|
|
|
|
|
|
// so the bar empties at the cutoff the user actually cares about.
|
|
|
|
|
|
static int battMvToPercent(int mv, int low_mv) {
|
|
|
|
|
|
static const struct { uint16_t mv; uint8_t pct; } CURVE[] = {
|
|
|
|
|
|
{3200, 0}, {3300, 3}, {3400, 8}, {3500, 15},
|
|
|
|
|
|
{3600, 25}, {3650, 33}, {3700, 45}, {3750, 58},
|
2026-06-09 17:00:00 +02:00
|
|
|
|
{3800, 68}, {3900, 77}, {4000, 86}, {4100, 93}, {4170, 100}
|
2026-05-28 22:57:50 +02:00
|
|
|
|
};
|
|
|
|
|
|
static const int CURVE_LEN = sizeof(CURVE) / sizeof(CURVE[0]);
|
|
|
|
|
|
auto curveAt = [&](int v) -> int {
|
|
|
|
|
|
if (v <= (int)CURVE[0].mv) return CURVE[0].pct;
|
|
|
|
|
|
if (v >= (int)CURVE[CURVE_LEN-1].mv) return CURVE[CURVE_LEN-1].pct;
|
|
|
|
|
|
for (int i = 1; i < CURVE_LEN; i++) {
|
|
|
|
|
|
if (v <= (int)CURVE[i].mv) {
|
|
|
|
|
|
int span_mv = CURVE[i].mv - CURVE[i-1].mv;
|
|
|
|
|
|
int span_pct = CURVE[i].pct - CURVE[i-1].pct;
|
|
|
|
|
|
return CURVE[i-1].pct + (v - (int)CURVE[i-1].mv) * span_pct / span_mv;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return 100;
|
|
|
|
|
|
};
|
|
|
|
|
|
if (low_mv <= 0) low_mv = BATT_MIN_MILLIVOLTS;
|
|
|
|
|
|
int raw_pct = curveAt(mv);
|
|
|
|
|
|
int low_pct = curveAt(low_mv);
|
|
|
|
|
|
int pct = (low_pct >= 100) ? 0 : (raw_pct - low_pct) * 100 / (100 - low_pct);
|
|
|
|
|
|
if (pct < 0) pct = 0;
|
|
|
|
|
|
if (pct > 100) pct = 100;
|
|
|
|
|
|
return pct;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-02 23:32:27 +02:00
|
|
|
|
// Render the time starting at top_y; returns the y just below the time block
|
|
|
|
|
|
// so the caller can flow the date / dashboard rows beneath it.
|
|
|
|
|
|
//
|
|
|
|
|
|
// On a tall portrait panel (e-ink in portrait — height > width) HH and MM are
|
|
|
|
|
|
// stacked on two lines in the huge built-in font (size 4, ~56 px tall) so the
|
|
|
|
|
|
// digits fill the narrow width. On wide panels (OLED, landscape e-ink) the
|
|
|
|
|
|
// classic single-line "HH:MM" at size 2 is kept.
|
|
|
|
|
|
static int drawClockTime(DisplayDriver& d, int top_y, const struct tm* ti,
|
|
|
|
|
|
bool h12, bool show_sec) {
|
|
|
|
|
|
const bool tall = d.height() > d.width(); // true only on portrait e-ink
|
|
|
|
|
|
|
|
|
|
|
|
if (tall) {
|
|
|
|
|
|
int hh = ti->tm_hour;
|
|
|
|
|
|
const char* ap = nullptr;
|
|
|
|
|
|
if (h12) { ap = (hh < 12) ? "AM" : "PM"; hh %= 12; if (hh == 0) hh = 12; }
|
|
|
|
|
|
const int cx = d.width() / 2;
|
|
|
|
|
|
char hbuf[4], mbuf[4];
|
|
|
|
|
|
snprintf(hbuf, sizeof(hbuf), "%02d", hh);
|
|
|
|
|
|
snprintf(mbuf, sizeof(mbuf), "%02d", ti->tm_min);
|
|
|
|
|
|
|
|
|
|
|
|
int y = top_y;
|
|
|
|
|
|
d.setTextSize(4);
|
|
|
|
|
|
const int lhb = d.getLineHeight();
|
|
|
|
|
|
// The built-in GFX font advances 6 px per char but the glyph is only 5 px
|
|
|
|
|
|
// wide, so getTextWidth() over-reports by one trailing blank column and
|
|
|
|
|
|
// drawTextCentered() would bias the digits ~half a column to the left.
|
|
|
|
|
|
// Centre on the visible width (minus that trailing column) instead.
|
|
|
|
|
|
const int trail = d.getCharWidth() / 6; // one built-in column at this size
|
|
|
|
|
|
auto drawBig = [&](const char* s, int yy) {
|
|
|
|
|
|
int w = (int)d.getTextWidth(s) - trail;
|
|
|
|
|
|
d.setCursor(cx - w / 2, yy);
|
|
|
|
|
|
d.print(s);
|
|
|
|
|
|
};
|
|
|
|
|
|
drawBig(hbuf, y); y += lhb + 2;
|
|
|
|
|
|
drawBig(mbuf, y); y += lhb + 2;
|
|
|
|
|
|
if (ap) { d.setTextSize(2); d.drawTextCentered(cx, y, ap); y += d.getLineHeight() + 1; }
|
|
|
|
|
|
d.setTextSize(1);
|
|
|
|
|
|
return y;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Wide layout: single inline line at size 2.
|
|
|
|
|
|
char buf[16];
|
|
|
|
|
|
d.setTextSize(2);
|
|
|
|
|
|
const int lh2 = d.getLineHeight();
|
|
|
|
|
|
if (h12) {
|
|
|
|
|
|
int hh = ti->tm_hour % 12; if (hh == 0) hh = 12;
|
|
|
|
|
|
const char* ap = (ti->tm_hour < 12) ? "AM" : "PM";
|
|
|
|
|
|
if (show_sec) snprintf(buf, sizeof(buf), "%d:%02d:%02d%s", hh, ti->tm_min, ti->tm_sec, ap);
|
|
|
|
|
|
else snprintf(buf, sizeof(buf), "%d:%02d %s", hh, ti->tm_min, ap);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
if (show_sec) snprintf(buf, sizeof(buf), "%02d:%02d:%02d", ti->tm_hour, ti->tm_min, ti->tm_sec);
|
|
|
|
|
|
else snprintf(buf, sizeof(buf), "%02d:%02d", ti->tm_hour, ti->tm_min);
|
|
|
|
|
|
}
|
|
|
|
|
|
d.drawTextCentered(d.width() / 2, top_y, buf);
|
|
|
|
|
|
d.setTextSize(1);
|
|
|
|
|
|
return top_y + lh2 + 2;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
Add ringtone editor, buzzer volume, unread fixes and buzzer notification fixes
- RingtoneEditorScreen: 32-note step sequencer accessible via new Tools page on home screen. U/D=pitch, ENTER=octave cycle, L/R=cursor, MENU=options (play, duration, BPM, insert/delete, save). Notes stored packed in NodePrefs and persisted via DataStore.
- ToolsScreen: new "Tools" home page entry launching the editor
- Buzzer volume: 0-4 bar control in Settings > Sound, persisted to prefs
- Buzzer notifications: fixed notify() firing inside wrong serial-guard branch and before addChannelMsg set the channel index
- Unread counter: fixed newest-first index direction; watermark formula now correct
- OLED brightness: wider range via pre-charge register 0xD9 combined with contrast
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 09:40:37 +02:00
|
|
|
|
// ── HomeScreen ────────────────────────────────────────────────────────────────
|
2025-08-08 20:01:31 +10:00
|
|
|
|
class HomeScreen : public UIScreen {
|
|
|
|
|
|
enum HomePage {
|
2026-05-10 15:18:35 +02:00
|
|
|
|
CLOCK,
|
2026-05-24 23:00:27 +02:00
|
|
|
|
FAVOURITES,
|
2025-08-08 20:01:31 +10:00
|
|
|
|
RECENT,
|
|
|
|
|
|
RADIO,
|
|
|
|
|
|
BLUETOOTH,
|
|
|
|
|
|
ADVERT,
|
2025-09-28 09:43:28 +02:00
|
|
|
|
#if ENV_INCLUDE_GPS == 1
|
2025-09-23 10:39:43 +02:00
|
|
|
|
GPS,
|
|
|
|
|
|
#endif
|
2025-09-09 16:32:41 +02:00
|
|
|
|
#if UI_SENSORS_PAGE == 1
|
2025-09-05 15:20:52 +02:00
|
|
|
|
SENSORS,
|
2025-09-09 16:32:41 +02:00
|
|
|
|
#endif
|
2026-05-10 15:18:35 +02:00
|
|
|
|
SETTINGS,
|
feat(companion): live location sharing, Locator geofencing, trail auto-pause
Squash merge of feat/location-beacon-alerts-autopause (v1.21).
Features:
- Live Location Sharing — broadcast position as movement-gated [LOC]
messages to a channel or contact; live shares show as map pins with
distance/bearing in Nearby Nodes and a status-bar indicator. [LOC] is
parsed in DMs, channel messages and room messages; DM shares name the
sender.
- Locator (geofence) — arm a geofence around a saved waypoint or a person
(live [LOC] or last-known position), alert on arrive/leave or near/far,
with an optional homing beeper (gated to arrive/both modes). Arm from
Tools > Locator, Nearby Nodes, or Waypoints; target picker lists
favourites first, clearable via a "None" entry. Active target is drawn
as a flag on the map.
- One active target shared across Locator / Navigate / Map via a single
resolver (resolvePersonPos / activeTargetPos) that prefers a live
[LOC] share over the last-advertised GPS fix.
- Follow live contacts — Navigate to a live-sharing contact follows them
as they move and adds an ETA line; quick-share your own position from
the Map.
- Map & status-bar upgrades — home mini-map gets a north marker, scale
tick, and a connected trail line (was disconnected dots); status line
shows tracked-node count, an arrow + distance to the active Locator/Nav
target (falling back to the nearest live-tracked contact); GPS fix icon
in the top status bar, shown only on GPS boards with GPS enabled.
- Trail auto-pause — recording freezes on stops (banking elapsed time,
breaking the map line across the idle gap) and resumes on movement
without ending the session.
- Streaming trail simplification — GPS points are simplified in-stream
via a fixed-corridor (Reumann-Witkam) pass tuned for fidelity: straight
runs collapse to their endpoints, curves stay bounded to the Min-dist
tolerance, so the 512-point buffer covers a far longer route than a
flat point budget would suggest.
- Collapsible Tools (Location / Comms / System sections, fold-in-place
like Settings) and page-indicator icons on the home carousel.
- Waypoint coordinate editor — add a waypoint by scroll-editing lat/lon
digit by digit.
Fixes:
- Critical: low-heap hang and contact loss on RAM-tight builds. Halved
message-history scrollback rings (recovering ~14 KB free heap) and
made contacts/channels/prefs persistence atomic (temp file + rename),
so an interrupted save can no longer corrupt or wipe the store.
- Serial.write() bounded so a stalled USB host can't hang the device.
- Nearby Nodes: live [LOC] senders respect the type filter, sort by
shared position, and the list refreshes so live shares bubble up.
- Map: live contacts are labelled before waypoints.
- GPS status icon hidden when GPS is off in Settings.
- Splash screen no longer truncates a pre-release tag's own dash (e.g.
v1.21-rc1) when stripping the build's commit-hash suffix.
- Null-guarded the Locator target picker; clamped loc-share channel
index on load.
Under the hood:
- -Os size optimisation on the e-ink and GAT562 30S solo envs.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 19:04:21 +02:00
|
|
|
|
MAP,
|
Add ringtone editor, buzzer volume, unread fixes and buzzer notification fixes
- RingtoneEditorScreen: 32-note step sequencer accessible via new Tools page on home screen. U/D=pitch, ENTER=octave cycle, L/R=cursor, MENU=options (play, duration, BPM, insert/delete, save). Notes stored packed in NodePrefs and persisted via DataStore.
- ToolsScreen: new "Tools" home page entry launching the editor
- Buzzer volume: 0-4 bar control in Settings > Sound, persisted to prefs
- Buzzer notifications: fixed notify() firing inside wrong serial-guard branch and before addChannelMsg set the channel index
- Unread counter: fixed newest-first index direction; watermark formula now correct
- OLED brightness: wider range via pre-charge register 0xD9 combined with contrast
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 09:40:37 +02:00
|
|
|
|
TOOLS,
|
2026-05-10 18:45:35 +02:00
|
|
|
|
QUICK_MSG,
|
2025-08-08 20:01:31 +10:00
|
|
|
|
SHUTDOWN,
|
|
|
|
|
|
Count // keep as last
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-05-24 23:00:27 +02:00
|
|
|
|
// Selected slot on the Favourites page (0..FAVOURITES_COUNT - 1).
|
|
|
|
|
|
uint8_t _fav_sel = 0;
|
|
|
|
|
|
|
2026-05-25 08:08:12 +02:00
|
|
|
|
// Build the in-place pin picker list for an empty slot. Favourited chat
|
|
|
|
|
|
// contacts first (`c.flags & 0x01`), then recent DM contacts deduped
|
|
|
|
|
|
// against the favourites list. Up to PIN_PICKER_MAX.
|
|
|
|
|
|
void buildPinPicker(int slot) {
|
|
|
|
|
|
_pin_target_slot = slot;
|
|
|
|
|
|
_pin_count = 0;
|
|
|
|
|
|
// 1) Upstream-favourited chat contacts.
|
|
|
|
|
|
for (int idx = 0; _pin_count < PIN_PICKER_MAX; idx++) {
|
|
|
|
|
|
ContactInfo c;
|
|
|
|
|
|
if (!the_mesh.getContactByIdx(idx, c)) break;
|
|
|
|
|
|
if (c.type != ADV_TYPE_CHAT) continue;
|
|
|
|
|
|
if (!(c.flags & 0x01)) continue;
|
|
|
|
|
|
memcpy(_pin_keys[_pin_count], c.id.pub_key, NodePrefs::FAVOURITE_PREFIX_LEN);
|
|
|
|
|
|
DisplayDriver::translateUTF8Static(_pin_labels[_pin_count], c.name, sizeof(_pin_labels[_pin_count]));
|
|
|
|
|
|
_pin_count++;
|
|
|
|
|
|
}
|
|
|
|
|
|
// 2) Recent DM contacts (deduped).
|
|
|
|
|
|
uint8_t recent[PIN_PICKER_MAX][NodePrefs::FAVOURITE_PREFIX_LEN];
|
|
|
|
|
|
int rn = _task->getRecentDMContacts(recent, PIN_PICKER_MAX);
|
|
|
|
|
|
for (int i = 0; i < rn && _pin_count < PIN_PICKER_MAX; i++) {
|
|
|
|
|
|
bool dup = false;
|
|
|
|
|
|
for (int j = 0; j < _pin_count; j++)
|
|
|
|
|
|
if (memcmp(_pin_keys[j], recent[i], NodePrefs::FAVOURITE_PREFIX_LEN) == 0) { dup = true; break; }
|
|
|
|
|
|
if (dup) continue;
|
|
|
|
|
|
for (int idx = 0; ; idx++) {
|
|
|
|
|
|
ContactInfo c;
|
|
|
|
|
|
if (!the_mesh.getContactByIdx(idx, c)) break;
|
|
|
|
|
|
if (memcmp(c.id.pub_key, recent[i], NodePrefs::FAVOURITE_PREFIX_LEN) == 0) {
|
|
|
|
|
|
memcpy(_pin_keys[_pin_count], recent[i], NodePrefs::FAVOURITE_PREFIX_LEN);
|
|
|
|
|
|
DisplayDriver::translateUTF8Static(_pin_labels[_pin_count], c.name, sizeof(_pin_labels[_pin_count]));
|
|
|
|
|
|
_pin_count++;
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-05-29 10:59:34 +02:00
|
|
|
|
// 3) Fallback: all remaining chat contacts not already in the list.
|
2026-05-25 08:08:12 +02:00
|
|
|
|
if (_pin_count == 0) {
|
2026-05-29 10:59:34 +02:00
|
|
|
|
for (int idx = 0; _pin_count < PIN_PICKER_MAX; idx++) {
|
|
|
|
|
|
ContactInfo c;
|
|
|
|
|
|
if (!the_mesh.getContactByIdx(idx, c)) break;
|
|
|
|
|
|
if (c.type != ADV_TYPE_CHAT) continue;
|
|
|
|
|
|
bool dup = false;
|
|
|
|
|
|
for (int j = 0; j < _pin_count; j++)
|
|
|
|
|
|
if (memcmp(_pin_keys[j], c.id.pub_key, NodePrefs::FAVOURITE_PREFIX_LEN) == 0) { dup = true; break; }
|
|
|
|
|
|
if (dup) continue;
|
|
|
|
|
|
memcpy(_pin_keys[_pin_count], c.id.pub_key, NodePrefs::FAVOURITE_PREFIX_LEN);
|
|
|
|
|
|
DisplayDriver::translateUTF8Static(_pin_labels[_pin_count], c.name, sizeof(_pin_labels[_pin_count]));
|
|
|
|
|
|
_pin_count++;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if (_pin_count == 0) {
|
|
|
|
|
|
_task->showAlert("No contacts", 1000);
|
2026-05-25 08:08:12 +02:00
|
|
|
|
_pin_target_slot = -1;
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
_pin_menu.begin("Pick contact", 3);
|
|
|
|
|
|
for (int i = 0; i < _pin_count; i++) _pin_menu.addItem(_pin_labels[i]);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// In-place pin picker (opens when Enter hits an empty Favourites tile).
|
|
|
|
|
|
static const int PIN_PICKER_MAX = 12;
|
|
|
|
|
|
PopupMenu _pin_menu;
|
|
|
|
|
|
uint8_t _pin_keys[PIN_PICKER_MAX][NodePrefs::FAVOURITE_PREFIX_LEN];
|
|
|
|
|
|
char _pin_labels[PIN_PICKER_MAX][22];
|
|
|
|
|
|
int _pin_count = 0;
|
|
|
|
|
|
int _pin_target_slot = -1;
|
|
|
|
|
|
|
2025-08-08 20:01:31 +10:00
|
|
|
|
UITask* _task;
|
|
|
|
|
|
mesh::RTCClock* _rtc;
|
|
|
|
|
|
SensorManager* _sensors;
|
|
|
|
|
|
NodePrefs* _node_prefs;
|
|
|
|
|
|
uint8_t _page;
|
|
|
|
|
|
bool _shutdown_init;
|
2025-08-16 18:13:50 +02:00
|
|
|
|
AdvertPath recent[UI_RECENT_LIST_SIZE];
|
2025-08-08 20:01:31 +10:00
|
|
|
|
|
2026-05-12 09:51:30 +02:00
|
|
|
|
int pageBit(int page) const {
|
2026-05-24 23:00:27 +02:00
|
|
|
|
if (page == CLOCK) return NodePrefs::HPB_CLOCK;
|
|
|
|
|
|
if (page == FAVOURITES) return NodePrefs::HPB_FAVOURITES;
|
2026-05-24 22:26:56 +02:00
|
|
|
|
if (page == RECENT) return NodePrefs::HPB_RECENT;
|
|
|
|
|
|
if (page == RADIO) return NodePrefs::HPB_RADIO;
|
|
|
|
|
|
if (page == BLUETOOTH) return NodePrefs::HPB_BLUETOOTH;
|
|
|
|
|
|
if (page == ADVERT) return NodePrefs::HPB_ADVERT;
|
2026-05-12 09:51:30 +02:00
|
|
|
|
#if ENV_INCLUDE_GPS == 1
|
2026-05-24 22:26:56 +02:00
|
|
|
|
if (page == GPS) return NodePrefs::HPB_GPS;
|
2026-05-12 09:51:30 +02:00
|
|
|
|
#endif
|
|
|
|
|
|
#if UI_SENSORS_PAGE == 1
|
2026-05-24 22:26:56 +02:00
|
|
|
|
if (page == SENSORS) return NodePrefs::HPB_SENSORS;
|
2026-05-12 09:51:30 +02:00
|
|
|
|
#endif
|
2026-05-24 22:26:56 +02:00
|
|
|
|
if (page == TOOLS) return NodePrefs::HPB_TOOLS;
|
|
|
|
|
|
if (page == SHUTDOWN) return NodePrefs::HPB_SHUTDOWN;
|
|
|
|
|
|
return -1; // SETTINGS, QUICK_MSG always visible (no mask bit)
|
2026-05-12 09:51:30 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-24 22:26:56 +02:00
|
|
|
|
// Maps page_order bit-index back to the HomePage enum value for this build.
|
2026-05-22 23:33:06 +02:00
|
|
|
|
// Returns -1 if the page is not compiled in.
|
|
|
|
|
|
int bitToPage(int bit) const {
|
|
|
|
|
|
switch (bit) {
|
2026-05-24 23:00:27 +02:00
|
|
|
|
case NodePrefs::HPB_CLOCK: return CLOCK;
|
|
|
|
|
|
case NodePrefs::HPB_FAVOURITES: return FAVOURITES;
|
2026-05-24 22:26:56 +02:00
|
|
|
|
case NodePrefs::HPB_RECENT: return RECENT;
|
|
|
|
|
|
case NodePrefs::HPB_RADIO: return RADIO;
|
|
|
|
|
|
case NodePrefs::HPB_BLUETOOTH: return BLUETOOTH;
|
|
|
|
|
|
case NodePrefs::HPB_ADVERT: return ADVERT;
|
2026-05-22 23:33:06 +02:00
|
|
|
|
#if ENV_INCLUDE_GPS == 1
|
2026-05-24 22:26:56 +02:00
|
|
|
|
case NodePrefs::HPB_GPS: return GPS;
|
2026-05-22 23:33:06 +02:00
|
|
|
|
#endif
|
|
|
|
|
|
#if UI_SENSORS_PAGE == 1
|
2026-05-24 22:26:56 +02:00
|
|
|
|
case NodePrefs::HPB_SENSORS: return SENSORS;
|
2026-05-22 23:33:06 +02:00
|
|
|
|
#endif
|
2026-05-24 22:26:56 +02:00
|
|
|
|
case NodePrefs::HPB_TOOLS: return TOOLS;
|
|
|
|
|
|
case NodePrefs::HPB_SHUTDOWN: return SHUTDOWN;
|
|
|
|
|
|
case NodePrefs::HPB_SETTINGS: return SETTINGS;
|
|
|
|
|
|
case NodePrefs::HPB_QUICK_MSG: return QUICK_MSG;
|
2026-05-22 23:33:06 +02:00
|
|
|
|
default: return -1;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-12 09:51:30 +02:00
|
|
|
|
bool isPageVisible(int page) const {
|
|
|
|
|
|
int bit = pageBit(page);
|
|
|
|
|
|
if (bit < 0) return true;
|
refactor(ui): apply code review fixes — bugs, risk, optimisations, cleanup
Fixes 7 bugs (timing UB on unsigned long, abs() no-op, XBM CRC PROGMEM
scan, CayenneLPP heap thrash ×2, UTF-8 reply prefix, lock-seq reset on
wake), 3 risk items (notif melody stop guard, bot trigger pre-lowercase,
lock-seq cleared on display wake), plus optimisations (fmtMsgAge/
getTextWidth caching, RTTTL builder consolidated to NodePrefs, HP_*
constants and homePageLabel table moved to NodePrefs, DataStore flat
lambda schema, translateUTF8Static factored out) and minor cleanup.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-23 20:03:45 +02:00
|
|
|
|
uint16_t mask = (_node_prefs && _node_prefs->home_pages_mask) ? _node_prefs->home_pages_mask : NodePrefs::HP_ALL;
|
2026-05-12 09:51:30 +02:00
|
|
|
|
return (mask >> bit) & 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-22 23:33:06 +02:00
|
|
|
|
// Build ordered list of all visible pages, respecting page_order when set.
|
|
|
|
|
|
// Returns count; out[] receives HomePage enum values.
|
|
|
|
|
|
int buildVisibleOrder(int* out) const {
|
|
|
|
|
|
int n = 0;
|
2026-05-24 19:53:05 +02:00
|
|
|
|
bool custom = _node_prefs && _node_prefs->page_order_set == NodePrefs::PAGE_ORDER_MAGIC;
|
2026-05-22 23:33:06 +02:00
|
|
|
|
if (custom) {
|
2026-05-24 23:00:27 +02:00
|
|
|
|
for (int i = 0; i < NodePrefs::PAGE_ORDER_LEN; i++) {
|
2026-05-22 23:33:06 +02:00
|
|
|
|
uint8_t v = _node_prefs->page_order[i];
|
2026-05-24 22:26:56 +02:00
|
|
|
|
if (v < 1 || v > NodePrefs::HPB_COUNT) break;
|
2026-05-22 23:33:06 +02:00
|
|
|
|
int pg = bitToPage(v - 1);
|
|
|
|
|
|
if (pg >= 0 && pg < (int)Count && isPageVisible(pg)) out[n++] = pg;
|
|
|
|
|
|
}
|
2026-05-23 18:11:24 +02:00
|
|
|
|
// Append any visible page missing from page_order (handles corrupted/migrated prefs)
|
|
|
|
|
|
for (int pg = 0; pg < (int)Count; pg++) {
|
|
|
|
|
|
if (!isPageVisible(pg)) continue;
|
|
|
|
|
|
bool found = false;
|
|
|
|
|
|
for (int i = 0; i < n; i++) if (out[i] == pg) { found = true; break; }
|
|
|
|
|
|
if (!found) out[n++] = pg;
|
|
|
|
|
|
}
|
2026-05-22 23:33:06 +02:00
|
|
|
|
} else {
|
|
|
|
|
|
for (int pg = 0; pg < (int)Count; pg++)
|
|
|
|
|
|
if (isPageVisible(pg)) out[n++] = pg;
|
2026-05-12 09:51:30 +02:00
|
|
|
|
}
|
2026-05-22 23:33:06 +02:00
|
|
|
|
return n;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
int navPage(int from, int dir) const {
|
2026-05-25 12:59:32 +02:00
|
|
|
|
int order[(int)Count]; int n = buildVisibleOrder(order);
|
2026-05-22 23:33:06 +02:00
|
|
|
|
if (n == 0) return from;
|
|
|
|
|
|
int cur = 0;
|
|
|
|
|
|
for (int i = 0; i < n; i++) if (order[i] == from) { cur = i; break; }
|
|
|
|
|
|
return order[((cur + dir) % n + n) % n];
|
2026-05-12 09:51:30 +02:00
|
|
|
|
}
|
2025-09-16 17:17:15 -07:00
|
|
|
|
|
2026-05-14 18:13:18 +02:00
|
|
|
|
int renderBatteryIndicator(DisplayDriver& display, uint16_t batteryMilliVolts) {
|
2026-05-28 22:57:50 +02:00
|
|
|
|
int low_mv = _node_prefs ? (int)_node_prefs->low_batt_mv : 0;
|
|
|
|
|
|
int pct = battMvToPercent((int)batteryMilliVolts, low_mv);
|
2025-08-08 20:01:31 +10:00
|
|
|
|
|
2026-05-10 15:18:35 +02:00
|
|
|
|
uint8_t mode = (_node_prefs && _node_prefs->batt_display_mode < 3)
|
|
|
|
|
|
? _node_prefs->batt_display_mode : 0;
|
2025-08-08 20:01:31 +10:00
|
|
|
|
|
2026-05-10 15:18:35 +02:00
|
|
|
|
display.setTextSize(1);
|
WioTrackerL1: redesign companion UI (channels, keyboard, msg preview)
- QuickMsg → Message screen: channel history, on-screen keyboard, custom message first
- Channel history: RAM ring buffer (32 entries, 80 chars), fullscreen view with word-wrap scroll
- After channel send: stay in history, own message shown as "Me: ..."
- MsgPreview: one message per screen with up/down nav, ENTER opens fullscreen scroll
- Monochromatic display: all color usage corrected to DARK/LIGHT only
- Top bar: battery icon, BT muted indicator, splash "Plus for Wio"
- KB_MAX_LEN: 64 → 100 chars; channel text buffer: 52 → 80 chars
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 15:56:30 +02:00
|
|
|
|
display.setColor(DisplayDriver::LIGHT);
|
2025-08-08 20:01:31 +10:00
|
|
|
|
|
2026-05-22 23:13:27 +02:00
|
|
|
|
const int lh = display.getLineHeight();
|
|
|
|
|
|
const int cw = display.getCharWidth();
|
|
|
|
|
|
const int ind = cw + 2; // single-char indicator width
|
2026-05-23 14:45:24 +02:00
|
|
|
|
const int ind_h = display.isLemonFont() ? lh - 2 : lh;
|
|
|
|
|
|
const int ind_gap = display.isLandscape() ? 3 : 1; // gap between indicator boxes
|
2026-05-22 18:34:20 +02:00
|
|
|
|
|
2026-05-10 15:18:35 +02:00
|
|
|
|
int battLeftX;
|
|
|
|
|
|
if (mode == 1) { // percent
|
|
|
|
|
|
char buf[6];
|
2026-05-24 20:35:49 +02:00
|
|
|
|
snprintf(buf, sizeof(buf),"%d%%", pct);
|
2026-05-10 15:18:35 +02:00
|
|
|
|
battLeftX = display.width() - display.getTextWidth(buf) - 1;
|
|
|
|
|
|
display.setCursor(battLeftX, 0);
|
|
|
|
|
|
display.print(buf);
|
|
|
|
|
|
} else if (mode == 2) { // voltage
|
|
|
|
|
|
char buf[8];
|
2026-05-24 20:35:49 +02:00
|
|
|
|
snprintf(buf, sizeof(buf),"%u.%02uV", batteryMilliVolts / 1000, (batteryMilliVolts % 1000) / 10);
|
2026-05-10 15:18:35 +02:00
|
|
|
|
battLeftX = display.width() - display.getTextWidth(buf) - 1;
|
|
|
|
|
|
display.setCursor(battLeftX, 0);
|
|
|
|
|
|
display.print(buf);
|
2026-05-22 18:34:20 +02:00
|
|
|
|
} else { // icon — scales with lh
|
|
|
|
|
|
const int iconH = lh;
|
2026-05-22 23:13:27 +02:00
|
|
|
|
const int iconW = lh * 2;
|
2026-05-23 14:45:24 +02:00
|
|
|
|
const int bm = display.isLandscape() ? 3 : 2; // inner margin: 3px on landscape e-ink, 2px on OLED/portrait
|
2026-05-22 18:34:20 +02:00
|
|
|
|
battLeftX = display.width() - iconW - 3;
|
|
|
|
|
|
display.drawRect(battLeftX, 0, iconW, iconH);
|
|
|
|
|
|
display.fillRect(battLeftX + iconW, iconH / 4, 2, iconH / 2);
|
2026-05-22 23:13:27 +02:00
|
|
|
|
int fillW = (pct * (iconW - 2 * bm)) / 100;
|
|
|
|
|
|
display.fillRect(battLeftX + bm, bm, fillW, iconH - 2 * bm);
|
2026-05-10 15:18:35 +02:00
|
|
|
|
}
|
2026-02-11 09:51:28 +01:00
|
|
|
|
|
|
|
|
|
|
#ifdef PIN_BUZZER
|
|
|
|
|
|
if (_task->isBuzzerQuiet()) {
|
2026-05-22 23:13:27 +02:00
|
|
|
|
int mx = battLeftX - ind - ind_gap;
|
2026-06-15 18:17:34 +02:00
|
|
|
|
drawBoxedIcon(display, mx, ind, ind_h, ICON_MUTE);
|
2026-05-22 18:34:20 +02:00
|
|
|
|
battLeftX = mx;
|
2026-02-11 09:51:28 +01:00
|
|
|
|
}
|
|
|
|
|
|
#endif
|
2026-05-11 16:52:24 +02:00
|
|
|
|
|
|
|
|
|
|
// BT connection indicator (left of muted/battery icons)
|
2026-05-14 18:13:18 +02:00
|
|
|
|
int leftmostX = battLeftX;
|
2026-05-11 16:52:24 +02:00
|
|
|
|
if (_task->isSerialEnabled()) {
|
2026-05-22 23:13:27 +02:00
|
|
|
|
int btX = battLeftX - ind - ind_gap;
|
2026-06-15 18:17:34 +02:00
|
|
|
|
if (_task->isBLEConnected()) // BT icon reflects BLE link, not USB
|
|
|
|
|
|
drawBoxedIcon(display, btX, ind, ind_h, ICON_BLUETOOTH);
|
|
|
|
|
|
else
|
|
|
|
|
|
drawSlotIcon(display, btX, ind, ind_h, ICON_BLUETOOTH); // plain glyph: available, not linked
|
2026-05-22 23:13:27 +02:00
|
|
|
|
leftmostX = btX - ind_gap;
|
2026-05-14 18:13:18 +02:00
|
|
|
|
|
2026-05-23 10:06:22 +02:00
|
|
|
|
// "A" indicator — left of BT; blinks on OLED, always shown on e-ink
|
2026-05-14 18:13:18 +02:00
|
|
|
|
if (_node_prefs && _node_prefs->advert_auto_interval_sec > 0) {
|
2026-05-22 18:34:20 +02:00
|
|
|
|
int aX = leftmostX - ind;
|
2026-06-15 18:17:34 +02:00
|
|
|
|
if (blinkOn()) drawBoxedIcon(display, aX, ind, ind_h, ICON_ADVERT);
|
2026-05-14 18:13:18 +02:00
|
|
|
|
leftmostX = aX - 1;
|
|
|
|
|
|
}
|
feat(ui): GPS breadcrumb phase 1 — storage, sampling, Summary view
Adds Tools › Breadcrumb. Enter on the screen toggles tracking; while
active, UITask::loop samples GPS at the cadence chosen in settings
(default 1 min) and drops the point into a 256-entry RAM ring guarded
by a min-distance gate (default 25 m). A "G" indicator joins "A" in
the status bar with the same blink convention.
New storage:
- examples/companion_radio/Breadcrumb.h — BreadcrumbStore class
- 256 × 12 B = 3 KB RAM (survives auto-off, lost on reboot — explicit
flash-slot save comes in phase 4)
- Haversine min-delta gate, total distance, elapsed seconds, current
km/h, bounding-box helper
New screen:
- examples/companion_radio/ui-new/BreadcrumbScreen.h — Summary view
with Status, Points, Dist (m or km), Time (h:mm), Speed; Enter
toggles tracking, Esc returns to Tools
Schema bump 0xC0DE0002 → 0xC0DE0003 for two new NodePrefs bytes:
breadcrumb_interval_idx (1min default) and breadcrumb_min_delta_idx
(25m default). Older saves leave both at zero, which matches the new
defaults. DataStore::loadPrefsInt/savePrefs read/write the pair.
Logging on/off is a runtime flag inside BreadcrumbStore — not a
preference — so reboot returns to the off state cleanly; the user
will be able to persist a snapshot to a slot in phase 4.
Phase 2 (map view), 3 (point list), 4 (slot save/load, GPX export over
USB), and 5 (settings UI) follow incrementally.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 08:22:12 +02:00
|
|
|
|
|
feat(companion): live location sharing, Locator geofencing, trail auto-pause
Squash merge of feat/location-beacon-alerts-autopause (v1.21).
Features:
- Live Location Sharing — broadcast position as movement-gated [LOC]
messages to a channel or contact; live shares show as map pins with
distance/bearing in Nearby Nodes and a status-bar indicator. [LOC] is
parsed in DMs, channel messages and room messages; DM shares name the
sender.
- Locator (geofence) — arm a geofence around a saved waypoint or a person
(live [LOC] or last-known position), alert on arrive/leave or near/far,
with an optional homing beeper (gated to arrive/both modes). Arm from
Tools > Locator, Nearby Nodes, or Waypoints; target picker lists
favourites first, clearable via a "None" entry. Active target is drawn
as a flag on the map.
- One active target shared across Locator / Navigate / Map via a single
resolver (resolvePersonPos / activeTargetPos) that prefers a live
[LOC] share over the last-advertised GPS fix.
- Follow live contacts — Navigate to a live-sharing contact follows them
as they move and adds an ETA line; quick-share your own position from
the Map.
- Map & status-bar upgrades — home mini-map gets a north marker, scale
tick, and a connected trail line (was disconnected dots); status line
shows tracked-node count, an arrow + distance to the active Locator/Nav
target (falling back to the nearest live-tracked contact); GPS fix icon
in the top status bar, shown only on GPS boards with GPS enabled.
- Trail auto-pause — recording freezes on stops (banking elapsed time,
breaking the map line across the idle gap) and resumes on movement
without ending the session.
- Streaming trail simplification — GPS points are simplified in-stream
via a fixed-corridor (Reumann-Witkam) pass tuned for fidelity: straight
runs collapse to their endpoints, curves stay bounded to the Min-dist
tolerance, so the 512-point buffer covers a far longer route than a
flat point budget would suggest.
- Collapsible Tools (Location / Comms / System sections, fold-in-place
like Settings) and page-indicator icons on the home carousel.
- Waypoint coordinate editor — add a waypoint by scroll-editing lat/lon
digit by digit.
Fixes:
- Critical: low-heap hang and contact loss on RAM-tight builds. Halved
message-history scrollback rings (recovering ~14 KB free heap) and
made contacts/channels/prefs persistence atomic (temp file + rename),
so an interrupted save can no longer corrupt or wipe the store.
- Serial.write() bounded so a stalled USB host can't hang the device.
- Nearby Nodes: live [LOC] senders respect the type filter, sort by
shared position, and the list refreshes so live shares bubble up.
- Map: live contacts are labelled before waypoints.
- GPS status icon hidden when GPS is off in Settings.
- Splash screen no longer truncates a pre-release tag's own dash (e.g.
v1.21-rc1) when stripping the build's commit-hash suffix.
- Null-guarded the Locator target picker; clamped loc-share channel
index on load.
Under the hood:
- -Os size optimisation on the e-ink and GAT562 30S solo envs.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 19:04:21 +02:00
|
|
|
|
// Live location sharing active. Same blink convention — another
|
|
|
|
|
|
// "leave it on and forget" broadcast, like auto-advert above. Reuses
|
|
|
|
|
|
// the diamond the map uses for a live-tracked contact, so the glyph
|
|
|
|
|
|
// already means "sharing position" elsewhere in the UI.
|
|
|
|
|
|
if (_node_prefs && _node_prefs->loc_share_enabled) {
|
|
|
|
|
|
int lsX = leftmostX - ind;
|
|
|
|
|
|
if (blinkOn()) drawBoxedIcon(display, lsX, ind, ind_h, ICON_MAP_CONTACT);
|
|
|
|
|
|
leftmostX = lsX - 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-15 18:17:34 +02:00
|
|
|
|
// GPS trail logging active. Same blink convention.
|
2026-05-25 09:10:40 +02:00
|
|
|
|
if (_task->trail().isActive()) {
|
feat(ui): GPS breadcrumb phase 1 — storage, sampling, Summary view
Adds Tools › Breadcrumb. Enter on the screen toggles tracking; while
active, UITask::loop samples GPS at the cadence chosen in settings
(default 1 min) and drops the point into a 256-entry RAM ring guarded
by a min-distance gate (default 25 m). A "G" indicator joins "A" in
the status bar with the same blink convention.
New storage:
- examples/companion_radio/Breadcrumb.h — BreadcrumbStore class
- 256 × 12 B = 3 KB RAM (survives auto-off, lost on reboot — explicit
flash-slot save comes in phase 4)
- Haversine min-delta gate, total distance, elapsed seconds, current
km/h, bounding-box helper
New screen:
- examples/companion_radio/ui-new/BreadcrumbScreen.h — Summary view
with Status, Points, Dist (m or km), Time (h:mm), Speed; Enter
toggles tracking, Esc returns to Tools
Schema bump 0xC0DE0002 → 0xC0DE0003 for two new NodePrefs bytes:
breadcrumb_interval_idx (1min default) and breadcrumb_min_delta_idx
(25m default). Older saves leave both at zero, which matches the new
defaults. DataStore::loadPrefsInt/savePrefs read/write the pair.
Logging on/off is a runtime flag inside BreadcrumbStore — not a
preference — so reboot returns to the off state cleanly; the user
will be able to persist a snapshot to a slot in phase 4.
Phase 2 (map view), 3 (point list), 4 (slot save/load, GPX export over
USB), and 5 (settings UI) follow incrementally.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 08:22:12 +02:00
|
|
|
|
int gX = leftmostX - ind;
|
2026-06-15 18:17:34 +02:00
|
|
|
|
if (blinkOn()) drawBoxedIcon(display, gX, ind, ind_h, ICON_TRAIL);
|
feat(ui): GPS breadcrumb phase 1 — storage, sampling, Summary view
Adds Tools › Breadcrumb. Enter on the screen toggles tracking; while
active, UITask::loop samples GPS at the cadence chosen in settings
(default 1 min) and drops the point into a 256-entry RAM ring guarded
by a min-distance gate (default 25 m). A "G" indicator joins "A" in
the status bar with the same blink convention.
New storage:
- examples/companion_radio/Breadcrumb.h — BreadcrumbStore class
- 256 × 12 B = 3 KB RAM (survives auto-off, lost on reboot — explicit
flash-slot save comes in phase 4)
- Haversine min-delta gate, total distance, elapsed seconds, current
km/h, bounding-box helper
New screen:
- examples/companion_radio/ui-new/BreadcrumbScreen.h — Summary view
with Status, Points, Dist (m or km), Time (h:mm), Speed; Enter
toggles tracking, Esc returns to Tools
Schema bump 0xC0DE0002 → 0xC0DE0003 for two new NodePrefs bytes:
breadcrumb_interval_idx (1min default) and breadcrumb_min_delta_idx
(25m default). Older saves leave both at zero, which matches the new
defaults. DataStore::loadPrefsInt/savePrefs read/write the pair.
Logging on/off is a runtime flag inside BreadcrumbStore — not a
preference — so reboot returns to the off state cleanly; the user
will be able to persist a snapshot to a slot in phase 4.
Phase 2 (map view), 3 (point list), 4 (slot save/load, GPX export over
USB), and 5 (settings UI) follow incrementally.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 08:22:12 +02:00
|
|
|
|
leftmostX = gX - 1;
|
|
|
|
|
|
}
|
feat(repeater): politeness controls, dedicated radio profile, and diagnostics
Settings > Radio gains a repeater toggle, 16 community-suggested radio
presets plus manual Freq/SF/BW/CR tuning (digit-by-digit Freq editor),
4 persisted user preset slots, and the packet pool bumped 16->32 so
queued retransmits stop starving incoming-packet allocation while
relaying.
Four opt-in politeness knobs (skip-advert, max-hops, yield, min-SNR),
all flood-only and off by default, plus overhear suppression that
cancels a queued retransmit if a peer relays the same packet first.
Adaptive Power Control is suppressed while relaying (pins TX power to
the ceiling) and duty-cycle RX is forced off, since a repeater needs
to hear and relay at consistent power.
A dedicated Tools > Repeater screen consolidates the toggle, the
politeness knobs, and live forwarding stats, plus an optional "Custom"
radio profile — a dedicated frequency/SF/BW/CR for relaying, separate
from the companion's own network, band-matched to the companion
frequency by default and used everywhere a radio change can happen
(boot, on-device toggle, app-driven CMD_SET_RADIO_PARAMS, Settings
radio edits) so the device never silently falls back to the wrong
params mid-relay.
Diagnostics gains the actually-forwarded packet count, real heap-free
via mallinfo(), a reset-counters popup, and hardened loop-detect
bounds. Also: a frequency-floor fix and a float-equality preset-match
fix in the repeater profile logic, deduped BW-table/valCol/profile-
seeding helpers shared between Settings and the Repeater screen, and
a build.sh fix tolerating control characters in `pio project config`'s
JSON dump.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-20 09:07:00 +02:00
|
|
|
|
|
|
|
|
|
|
// Repeater relaying. Same blink convention — a "leave it on and forget"
|
|
|
|
|
|
// mode, so an at-a-glance cue matters (especially on a Custom profile,
|
|
|
|
|
|
// where the device is off your own network while this is shown).
|
|
|
|
|
|
if (_node_prefs && _node_prefs->client_repeat) {
|
|
|
|
|
|
int rX = leftmostX - ind;
|
|
|
|
|
|
if (blinkOn()) drawBoxedIcon(display, rX, ind, ind_h, ICON_REPEATER);
|
|
|
|
|
|
leftmostX = rX - 1;
|
|
|
|
|
|
}
|
2026-05-11 16:52:24 +02:00
|
|
|
|
}
|
feat(companion): live location sharing, Locator geofencing, trail auto-pause
Squash merge of feat/location-beacon-alerts-autopause (v1.21).
Features:
- Live Location Sharing — broadcast position as movement-gated [LOC]
messages to a channel or contact; live shares show as map pins with
distance/bearing in Nearby Nodes and a status-bar indicator. [LOC] is
parsed in DMs, channel messages and room messages; DM shares name the
sender.
- Locator (geofence) — arm a geofence around a saved waypoint or a person
(live [LOC] or last-known position), alert on arrive/leave or near/far,
with an optional homing beeper (gated to arrive/both modes). Arm from
Tools > Locator, Nearby Nodes, or Waypoints; target picker lists
favourites first, clearable via a "None" entry. Active target is drawn
as a flag on the map.
- One active target shared across Locator / Navigate / Map via a single
resolver (resolvePersonPos / activeTargetPos) that prefers a live
[LOC] share over the last-advertised GPS fix.
- Follow live contacts — Navigate to a live-sharing contact follows them
as they move and adds an ETA line; quick-share your own position from
the Map.
- Map & status-bar upgrades — home mini-map gets a north marker, scale
tick, and a connected trail line (was disconnected dots); status line
shows tracked-node count, an arrow + distance to the active Locator/Nav
target (falling back to the nearest live-tracked contact); GPS fix icon
in the top status bar, shown only on GPS boards with GPS enabled.
- Trail auto-pause — recording freezes on stops (banking elapsed time,
breaking the map line across the idle gap) and resumes on movement
without ending the session.
- Streaming trail simplification — GPS points are simplified in-stream
via a fixed-corridor (Reumann-Witkam) pass tuned for fidelity: straight
runs collapse to their endpoints, curves stay bounded to the Min-dist
tolerance, so the 512-point buffer covers a far longer route than a
flat point budget would suggest.
- Collapsible Tools (Location / Comms / System sections, fold-in-place
like Settings) and page-indicator icons on the home carousel.
- Waypoint coordinate editor — add a waypoint by scroll-editing lat/lon
digit by digit.
Fixes:
- Critical: low-heap hang and contact loss on RAM-tight builds. Halved
message-history scrollback rings (recovering ~14 KB free heap) and
made contacts/channels/prefs persistence atomic (temp file + rename),
so an interrupted save can no longer corrupt or wipe the store.
- Serial.write() bounded so a stalled USB host can't hang the device.
- Nearby Nodes: live [LOC] senders respect the type filter, sort by
shared position, and the list refreshes so live shares bubble up.
- Map: live contacts are labelled before waypoints.
- GPS status icon hidden when GPS is off in Settings.
- Splash screen no longer truncates a pre-release tag's own dash (e.g.
v1.21-rc1) when stripping the build's commit-hash suffix.
- Null-guarded the Locator target picker; clamped loc-share channel
index on load.
Under the hood:
- -Os size optimisation on the e-ink and GAT562 30S solo envs.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 19:04:21 +02:00
|
|
|
|
|
|
|
|
|
|
// GPS fix status — boxed (lit) when the receiver has a valid fix, plain
|
|
|
|
|
|
// glyph while searching. Hidden entirely on boards with no GPS hardware
|
|
|
|
|
|
// and while the GPS setting itself is off, so it doesn't sit there as a
|
|
|
|
|
|
// permanently-empty slot or imply a search that isn't happening.
|
|
|
|
|
|
LocationProvider* loc = _sensors ? _sensors->getLocationProvider() : nullptr;
|
|
|
|
|
|
if (loc && _node_prefs && _node_prefs->gps_enabled) {
|
|
|
|
|
|
int gX = leftmostX - ind - ind_gap;
|
|
|
|
|
|
if (loc->isValid()) drawBoxedIcon(display, gX, ind, ind_h, ICON_GPS);
|
|
|
|
|
|
else drawSlotIcon(display, gX, ind, ind_h, ICON_GPS);
|
|
|
|
|
|
leftmostX = gX - 1;
|
|
|
|
|
|
}
|
2026-05-14 18:13:18 +02:00
|
|
|
|
return leftmostX;
|
2025-08-08 20:01:31 +10:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-09 16:32:41 +02:00
|
|
|
|
CayenneLPP sensors_lpp;
|
|
|
|
|
|
int sensors_nb = 0;
|
|
|
|
|
|
int sensors_scroll_offset = 0;
|
2025-09-05 15:20:52 +02:00
|
|
|
|
int next_sensors_refresh = 0;
|
2026-04-28 21:26:56 -07:00
|
|
|
|
|
2025-09-05 15:20:52 +02:00
|
|
|
|
void refresh_sensors() {
|
|
|
|
|
|
if (millis() > next_sensors_refresh) {
|
2025-09-09 16:32:41 +02:00
|
|
|
|
sensors_lpp.reset();
|
|
|
|
|
|
sensors_nb = 0;
|
|
|
|
|
|
sensors_lpp.addVoltage(TELEM_CHANNEL_SELF, (float)board.getBattMilliVolts() / 1000.0f);
|
|
|
|
|
|
sensors.querySensors(0xFF, sensors_lpp);
|
|
|
|
|
|
LPPReader reader (sensors_lpp.getBuffer(), sensors_lpp.getSize());
|
|
|
|
|
|
uint8_t channel, type;
|
|
|
|
|
|
while(reader.readHeader(channel, type)) {
|
|
|
|
|
|
reader.skipData(type);
|
|
|
|
|
|
sensors_nb ++;
|
|
|
|
|
|
}
|
2025-09-05 15:20:52 +02:00
|
|
|
|
#if AUTO_OFF_MILLIS > 0
|
|
|
|
|
|
next_sensors_refresh = millis() + 5000; // refresh sensor values every 5 sec
|
|
|
|
|
|
#else
|
|
|
|
|
|
next_sensors_refresh = millis() + 60000; // refresh sensor values every 1 min
|
|
|
|
|
|
#endif
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-08-08 20:01:31 +10:00
|
|
|
|
public:
|
|
|
|
|
|
HomeScreen(UITask* task, mesh::RTCClock* rtc, SensorManager* sensors, NodePrefs* node_prefs)
|
2026-04-28 21:26:56 -07:00
|
|
|
|
: _task(task), _rtc(rtc), _sensors(sensors), _node_prefs(node_prefs), _page(0),
|
2025-09-09 16:32:41 +02:00
|
|
|
|
_shutdown_init(false), sensors_lpp(200) { }
|
2025-08-08 20:01:31 +10:00
|
|
|
|
|
|
|
|
|
|
void poll() override {
|
|
|
|
|
|
if (_shutdown_init && !_task->isButtonPressed()) { // must wait for USR button to be released
|
|
|
|
|
|
_task->shutdown();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
feat(companion): live location sharing, Locator geofencing, trail auto-pause
Squash merge of feat/location-beacon-alerts-autopause (v1.21).
Features:
- Live Location Sharing — broadcast position as movement-gated [LOC]
messages to a channel or contact; live shares show as map pins with
distance/bearing in Nearby Nodes and a status-bar indicator. [LOC] is
parsed in DMs, channel messages and room messages; DM shares name the
sender.
- Locator (geofence) — arm a geofence around a saved waypoint or a person
(live [LOC] or last-known position), alert on arrive/leave or near/far,
with an optional homing beeper (gated to arrive/both modes). Arm from
Tools > Locator, Nearby Nodes, or Waypoints; target picker lists
favourites first, clearable via a "None" entry. Active target is drawn
as a flag on the map.
- One active target shared across Locator / Navigate / Map via a single
resolver (resolvePersonPos / activeTargetPos) that prefers a live
[LOC] share over the last-advertised GPS fix.
- Follow live contacts — Navigate to a live-sharing contact follows them
as they move and adds an ETA line; quick-share your own position from
the Map.
- Map & status-bar upgrades — home mini-map gets a north marker, scale
tick, and a connected trail line (was disconnected dots); status line
shows tracked-node count, an arrow + distance to the active Locator/Nav
target (falling back to the nearest live-tracked contact); GPS fix icon
in the top status bar, shown only on GPS boards with GPS enabled.
- Trail auto-pause — recording freezes on stops (banking elapsed time,
breaking the map line across the idle gap) and resumes on movement
without ending the session.
- Streaming trail simplification — GPS points are simplified in-stream
via a fixed-corridor (Reumann-Witkam) pass tuned for fidelity: straight
runs collapse to their endpoints, curves stay bounded to the Min-dist
tolerance, so the 512-point buffer covers a far longer route than a
flat point budget would suggest.
- Collapsible Tools (Location / Comms / System sections, fold-in-place
like Settings) and page-indicator icons on the home carousel.
- Waypoint coordinate editor — add a waypoint by scroll-editing lat/lon
digit by digit.
Fixes:
- Critical: low-heap hang and contact loss on RAM-tight builds. Halved
message-history scrollback rings (recovering ~14 KB free heap) and
made contacts/channels/prefs persistence atomic (temp file + rename),
so an interrupted save can no longer corrupt or wipe the store.
- Serial.write() bounded so a stalled USB host can't hang the device.
- Nearby Nodes: live [LOC] senders respect the type filter, sort by
shared position, and the list refreshes so live shares bubble up.
- Map: live contacts are labelled before waypoints.
- GPS status icon hidden when GPS is off in Settings.
- Splash screen no longer truncates a pre-release tag's own dash (e.g.
v1.21-rc1) when stripping the build's commit-hash suffix.
- Null-guarded the Locator target picker; clamped loc-share channel
index on load.
Under the hood:
- -Os size optimisation on the e-ink and GAT562 30S solo envs.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 19:04:21 +02:00
|
|
|
|
// Compact map preview for the Home "Map" page: own position, the GPS trail,
|
|
|
|
|
|
// and live-tracked contacts (◆) folded into one auto-scaled box. A simplified
|
|
|
|
|
|
// cousin of TrailScreen's map (no grid/labels, no break markers) so the home
|
|
|
|
|
|
// carousel stays light. Returns false (and draws nothing) when there's
|
|
|
|
|
|
// nothing to show.
|
|
|
|
|
|
bool drawMapPreview(DisplayDriver& display, int ax, int ay, int aw, int ah) {
|
|
|
|
|
|
if (aw < 8 || ah < 8) return false;
|
|
|
|
|
|
bool init = false;
|
|
|
|
|
|
int32_t mnla = 0, mxla = 0, mnlo = 0, mxlo = 0;
|
|
|
|
|
|
auto fold = [&](int32_t la, int32_t lo) {
|
|
|
|
|
|
if (!init) { mnla = mxla = la; mnlo = mxlo = lo; init = true; }
|
|
|
|
|
|
else { if (la < mnla) mnla = la; if (la > mxla) mxla = la;
|
|
|
|
|
|
if (lo < mnlo) mnlo = lo; if (lo > mxlo) mxlo = lo; }
|
|
|
|
|
|
};
|
|
|
|
|
|
TrailStore& tr = _task->trail();
|
|
|
|
|
|
if (!tr.empty()) { int32_t a, b, c, d; tr.boundingBox(a, b, c, d); fold(a, b); fold(c, d); }
|
|
|
|
|
|
LiveTrackStore& lt = _task->liveTrack();
|
|
|
|
|
|
uint32_t now = rtc_clock.getCurrentTime();
|
|
|
|
|
|
for (int i = 0; i < LiveTrackStore::CAPACITY; i++)
|
|
|
|
|
|
if (lt.isActive(i, now)) fold(lt.slotAt(i).lat_1e6, lt.slotAt(i).lon_1e6);
|
|
|
|
|
|
int32_t mla, mlo;
|
|
|
|
|
|
bool have_gps = _task->currentLocation(mla, mlo);
|
|
|
|
|
|
if (have_gps) fold(mla, mlo);
|
|
|
|
|
|
int32_t tla, tlo;
|
|
|
|
|
|
bool have_tgt = _task->activeTargetPos(tla, tlo); // active Locator/Nav target
|
|
|
|
|
|
if (have_tgt) fold(tla, tlo);
|
|
|
|
|
|
if (!init) return false;
|
|
|
|
|
|
|
|
|
|
|
|
// North marker — top-right, the same mini-icon as the full Trail map.
|
|
|
|
|
|
display.setColor(DisplayDriver::LIGHT);
|
|
|
|
|
|
{
|
|
|
|
|
|
const int ns = miniIconScale(display);
|
|
|
|
|
|
miniIconDrawTop(display, ax + aw - ICON_MAP_NORTH.w * ns - 1, ay + 1, ICON_MAP_NORTH);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
int cx = ax + aw / 2, cy = ay + ah / 2;
|
|
|
|
|
|
// Degenerate: one coincident point — just centre the markers.
|
|
|
|
|
|
if (mnla == mxla && mnlo == mxlo) {
|
|
|
|
|
|
for (int i = 0; i < LiveTrackStore::CAPACITY; i++)
|
|
|
|
|
|
if (lt.isActive(i, now)) { miniIconDrawCentered(display, cx, cy, ICON_MAP_CONTACT); break; }
|
|
|
|
|
|
if (have_gps || !tr.empty()) miniIconDrawCentered(display, cx, cy, ICON_MAP_CURRENT);
|
|
|
|
|
|
if (have_tgt) miniIconDrawCentered(display, cx, cy, ICON_MAP_TARGET); // highlight on top
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
float avg_lat_rad = ((mnla + mxla) / 2.0e6f) * (float)M_PI / 180.0f;
|
|
|
|
|
|
float lon_scale = cosf(avg_lat_rad); if (lon_scale < 0.05f) lon_scale = 0.05f;
|
|
|
|
|
|
float lat_span = (float)(mxla - mnla);
|
|
|
|
|
|
float lon_span = (float)(mxlo - mnlo) * lon_scale;
|
|
|
|
|
|
float slat = (float)ah / (lat_span > 0 ? lat_span : 1.0f);
|
|
|
|
|
|
float slon = (float)aw / (lon_span > 0 ? lon_span : 1.0f);
|
|
|
|
|
|
float scale = (slat < slon) ? slat : slon;
|
|
|
|
|
|
int off_x = ax + (aw - (int)(lon_span * scale)) / 2;
|
|
|
|
|
|
int off_y = ay + (ah - (int)(lat_span * scale)) / 2;
|
|
|
|
|
|
auto project = [&](int32_t la, int32_t lo, int& px, int& py) {
|
|
|
|
|
|
px = off_x + (int)((float)(lo - mnlo) * lon_scale * scale);
|
|
|
|
|
|
py = off_y + (int)((float)(mxla - la) * scale);
|
|
|
|
|
|
};
|
|
|
|
|
|
// Trail as a connected line, matching the full Trail map (shared helper —
|
|
|
|
|
|
// see gfx::drawTrail); no break marker here, just a silent gap.
|
|
|
|
|
|
gfx::drawTrail(display, tr, project, [](int, int, int, int) {});
|
|
|
|
|
|
for (int i = 0; i < LiveTrackStore::CAPACITY; i++) {
|
|
|
|
|
|
if (!lt.isActive(i, now)) continue;
|
|
|
|
|
|
int px, py; project(lt.slotAt(i).lat_1e6, lt.slotAt(i).lon_1e6, px, py);
|
|
|
|
|
|
miniIconDrawCentered(display, px, py, ICON_MAP_CONTACT);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (have_gps) { int px, py; project(mla, mlo, px, py); miniIconDrawCentered(display, px, py, ICON_MAP_CURRENT); }
|
|
|
|
|
|
// Active target flag drawn last so it stays legible even atop a contact/own dot.
|
|
|
|
|
|
if (have_tgt) { int px, py; project(tla, tlo, px, py); miniIconDrawCentered(display, px, py, ICON_MAP_TARGET); }
|
|
|
|
|
|
|
|
|
|
|
|
// Bottom-left scale reference, always shown — distance to the active
|
|
|
|
|
|
// target (or else the nearest live-tracked contact) now lives on the
|
|
|
|
|
|
// status line below instead (see statusDistanceKm() / render()), so this
|
|
|
|
|
|
// corner is free for it.
|
|
|
|
|
|
{
|
|
|
|
|
|
display.setColor(DisplayDriver::LIGHT);
|
|
|
|
|
|
int ty = ay + ah - display.getLineHeight();
|
|
|
|
|
|
static const float M_PER_1E6 = 0.11132f; // metres per 1e-6° lat
|
|
|
|
|
|
float ppm = scale / M_PER_1E6; // pixels per metre
|
|
|
|
|
|
if (ppm > 0.0f) {
|
|
|
|
|
|
bool imp = _task->useImperial();
|
|
|
|
|
|
static const float MET_M[] = { 5,10,25,50,100,250,500,1000,2000,5000,10000,25000,50000 };
|
|
|
|
|
|
static const char* MET_L[] = { "5m","10m","25m","50m","100m","250m","500m","1km","2km","5km","10km","25km","50km" };
|
|
|
|
|
|
static const float IMP_M[] = { 4.572f,15.24f,30.48f,76.2f,152.4f,402.34f,804.67f,1609.34f,4828.0f,16093.4f,80467.2f };
|
|
|
|
|
|
static const char* IMP_L[] = { "15ft","50ft","100ft","250ft","500ft","1/4mi","1/2mi","1mi","3mi","10mi","50mi" };
|
|
|
|
|
|
const float* M = imp ? IMP_M : MET_M;
|
|
|
|
|
|
const char* const* L = imp ? IMP_L : MET_L;
|
|
|
|
|
|
int N = imp ? (int)(sizeof(IMP_M) / sizeof(IMP_M[0])) : (int)(sizeof(MET_M) / sizeof(MET_M[0]));
|
|
|
|
|
|
float target = 8.0f / ppm; // short reference tick, not 1/3 of the width
|
|
|
|
|
|
int sel = 0;
|
|
|
|
|
|
for (int i = N - 1; i >= 0; i--) if (M[i] <= target) { sel = i; break; }
|
|
|
|
|
|
int barpx = (int)(M[sel] * ppm + 0.5f);
|
|
|
|
|
|
if (barpx < 5) barpx = 5;
|
|
|
|
|
|
if (barpx > aw / 4) barpx = aw / 4;
|
|
|
|
|
|
int bx = ax + 1, mid = ty + display.getLineHeight() / 2;
|
|
|
|
|
|
display.fillRect(bx, mid, barpx, 1); // single tick, on the text baseline
|
|
|
|
|
|
display.setCursor(bx + barpx + 2, ty);
|
|
|
|
|
|
display.print(L[sel]);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Distance shown on the MAP status line. The active Locator/Nav target
|
|
|
|
|
|
// takes priority — that's what the flag on the mini-map is pointing at,
|
|
|
|
|
|
// and it's the only way a waypoint target ever gets a distance readout
|
|
|
|
|
|
// here (a waypoint isn't a live-tracked contact). Falls back to the
|
|
|
|
|
|
// nearest live-tracked ([LOC]-sharing) contact when no target is set.
|
|
|
|
|
|
// -1 when we don't have a fix or nothing to measure against.
|
|
|
|
|
|
float statusDistanceKm() {
|
|
|
|
|
|
int32_t mla, mlo;
|
|
|
|
|
|
if (!_task->currentLocation(mla, mlo)) return -1.0f;
|
|
|
|
|
|
int32_t tla, tlo;
|
|
|
|
|
|
if (_task->activeTargetPos(tla, tlo)) return geo::haversineKm(mla, mlo, tla, tlo);
|
|
|
|
|
|
LiveTrackStore& lt = _task->liveTrack();
|
|
|
|
|
|
uint32_t now = rtc_clock.getCurrentTime();
|
|
|
|
|
|
float nearest_km = -1.0f;
|
|
|
|
|
|
for (int i = 0; i < LiveTrackStore::CAPACITY; i++) {
|
|
|
|
|
|
if (!lt.isActive(i, now)) continue;
|
|
|
|
|
|
float d = geo::haversineKm(mla, mlo, lt.slotAt(i).lat_1e6, lt.slotAt(i).lon_1e6);
|
|
|
|
|
|
if (nearest_km < 0.0f || d < nearest_km) nearest_km = d;
|
|
|
|
|
|
}
|
|
|
|
|
|
return nearest_km;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Small 5x5 glyph shown in the page-indicator row for each HomePage.
|
|
|
|
|
|
static const MiniIcon* pageIcon(int page) {
|
|
|
|
|
|
switch (page) {
|
|
|
|
|
|
case CLOCK: return &ICON_PG_CLOCK;
|
|
|
|
|
|
case FAVOURITES: return &ICON_PG_STAR;
|
|
|
|
|
|
case RECENT: return &ICON_PG_RECENT;
|
|
|
|
|
|
case RADIO: return &ICON_PG_RADIO;
|
|
|
|
|
|
case BLUETOOTH: return &ICON_PG_BT;
|
|
|
|
|
|
case ADVERT: return &ICON_PG_ADVERT;
|
|
|
|
|
|
#if ENV_INCLUDE_GPS == 1
|
|
|
|
|
|
case GPS: return &ICON_PG_GPS;
|
|
|
|
|
|
#endif
|
|
|
|
|
|
#if UI_SENSORS_PAGE == 1
|
|
|
|
|
|
case SENSORS: return &ICON_PG_SENSORS;
|
|
|
|
|
|
#endif
|
|
|
|
|
|
case SETTINGS: return &ICON_PG_SETTINGS;
|
|
|
|
|
|
case MAP: return &ICON_PG_MAP;
|
|
|
|
|
|
case TOOLS: return &ICON_PG_TOOLS;
|
|
|
|
|
|
case QUICK_MSG: return &ICON_PG_MSG;
|
|
|
|
|
|
case SHUTDOWN: return &ICON_PG_POWER;
|
|
|
|
|
|
}
|
|
|
|
|
|
return nullptr;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-08-08 20:01:31 +10:00
|
|
|
|
int render(DisplayDriver& display) override {
|
|
|
|
|
|
char tmp[80];
|
refactor: replace all hardcoded pixel positions with layout helpers
Add lineStep(), headerH(), listStart(), listVisible(), valCol() to
DisplayDriver so layout derives from getLineHeight() instead of fixed
OLED constants. Refactor all screens (SettingsScreen, NearbyScreen,
FullscreenMsgView, QuickMsgScreen, BotScreen, DashboardConfigScreen,
AutoAdvertScreen, RingtoneEditorScreen) and all HomeScreen pages plus
the lock screen and splash screen in UITask.cpp.
On OLED (lh=8) positions are unchanged; on landscape e-ink (lh=16) all
text and widgets now scale correctly to the 250×122 display.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 18:05:09 +02:00
|
|
|
|
display.setTextSize(1);
|
|
|
|
|
|
const int lh = display.getLineHeight(); // line height at sz1
|
|
|
|
|
|
const int step = display.lineStep(); // lh + 2
|
feat(companion): live location sharing, Locator geofencing, trail auto-pause
Squash merge of feat/location-beacon-alerts-autopause (v1.21).
Features:
- Live Location Sharing — broadcast position as movement-gated [LOC]
messages to a channel or contact; live shares show as map pins with
distance/bearing in Nearby Nodes and a status-bar indicator. [LOC] is
parsed in DMs, channel messages and room messages; DM shares name the
sender.
- Locator (geofence) — arm a geofence around a saved waypoint or a person
(live [LOC] or last-known position), alert on arrive/leave or near/far,
with an optional homing beeper (gated to arrive/both modes). Arm from
Tools > Locator, Nearby Nodes, or Waypoints; target picker lists
favourites first, clearable via a "None" entry. Active target is drawn
as a flag on the map.
- One active target shared across Locator / Navigate / Map via a single
resolver (resolvePersonPos / activeTargetPos) that prefers a live
[LOC] share over the last-advertised GPS fix.
- Follow live contacts — Navigate to a live-sharing contact follows them
as they move and adds an ETA line; quick-share your own position from
the Map.
- Map & status-bar upgrades — home mini-map gets a north marker, scale
tick, and a connected trail line (was disconnected dots); status line
shows tracked-node count, an arrow + distance to the active Locator/Nav
target (falling back to the nearest live-tracked contact); GPS fix icon
in the top status bar, shown only on GPS boards with GPS enabled.
- Trail auto-pause — recording freezes on stops (banking elapsed time,
breaking the map line across the idle gap) and resumes on movement
without ending the session.
- Streaming trail simplification — GPS points are simplified in-stream
via a fixed-corridor (Reumann-Witkam) pass tuned for fidelity: straight
runs collapse to their endpoints, curves stay bounded to the Min-dist
tolerance, so the 512-point buffer covers a far longer route than a
flat point budget would suggest.
- Collapsible Tools (Location / Comms / System sections, fold-in-place
like Settings) and page-indicator icons on the home carousel.
- Waypoint coordinate editor — add a waypoint by scroll-editing lat/lon
digit by digit.
Fixes:
- Critical: low-heap hang and contact loss on RAM-tight builds. Halved
message-history scrollback rings (recovering ~14 KB free heap) and
made contacts/channels/prefs persistence atomic (temp file + rename),
so an interrupted save can no longer corrupt or wipe the store.
- Serial.write() bounded so a stalled USB host can't hang the device.
- Nearby Nodes: live [LOC] senders respect the type filter, sort by
shared position, and the list refreshes so live shares bubble up.
- Map: live contacts are labelled before waypoints.
- GPS status icon hidden when GPS is off in Settings.
- Splash screen no longer truncates a pre-release tag's own dash (e.g.
v1.21-rc1) when stripping the build's commit-hash suffix.
- Null-guarded the Locator target picker; clamped loc-share channel
index on load.
Under the hood:
- -Os size optimisation on the e-ink and GAT562 30S solo envs.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 19:04:21 +02:00
|
|
|
|
// Page-indicator row: small (5px) page icons replace the old dots. Centre and
|
|
|
|
|
|
// gap scale with the font so the band clears the header above and content
|
|
|
|
|
|
// below (identical to the old lh+4 / +6 dots layout at 1x).
|
|
|
|
|
|
const int pg_half = (5 * miniIconScale(display) + 1) / 2;
|
|
|
|
|
|
const int dots_y = lh + pg_half + 1; // icon-row centre, below the header
|
|
|
|
|
|
const int content_y = dots_y + pg_half + 3; // first content row, below the icons
|
refactor: replace all hardcoded pixel positions with layout helpers
Add lineStep(), headerH(), listStart(), listVisible(), valCol() to
DisplayDriver so layout derives from getLineHeight() instead of fixed
OLED constants. Refactor all screens (SettingsScreen, NearbyScreen,
FullscreenMsgView, QuickMsgScreen, BotScreen, DashboardConfigScreen,
AutoAdvertScreen, RingtoneEditorScreen) and all HomeScreen pages plus
the lock screen and splash screen in UITask.cpp.
On OLED (lh=8) positions are unchanged; on landscape e-ink (lh=16) all
text and widgets now scale correctly to the 250×122 display.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 18:05:09 +02:00
|
|
|
|
|
feat: NearbyScreen, configurable clock dashboard, memory optimizations
- Add NearbyScreen: GPS contact list with haversine distance sort,
type filter (ALL/Chat/Rpt/Room/Snsr), detail view with ping (RTT via
ACK poll), node scan via advert(), context menu
- Add DashboardConfigScreen: configure up to 3 data fields on the clock
page (Battery, Temp, Humidity, Pressure, GPS, Altitude, Lux, CO2,
Contacts); entered via long-press ENTER on clock page
- Rework HP_CLOCK layout: full-screen clock (size 2 at y=0), date at
y=19, separator at y=28, three data field rows at y=31/41/51;
hide node name/battery indicator on clock page
- Persist dashboard_fields[3] in NodePrefs and DataStore
- Add isAckPending() to MyMesh for NearbyScreen ping detection
- Wire NearbyScreen into ToolsScreen and UITask
- Reduce NearbyScreen entry buffer from MAX_CONTACTS(100) to 32,
saving ~3.3 KB RAM
- Switch haversine/bearing math from double to float (sinf/cosf/atan2f),
eliminating double libm and saving ~5-10 KB flash
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 11:28:36 +02:00
|
|
|
|
// node name + battery — hidden on CLOCK page (full screen used for dashboard)
|
|
|
|
|
|
if (_page != CLOCK) {
|
|
|
|
|
|
display.setColor(DisplayDriver::LIGHT);
|
|
|
|
|
|
char filtered_name[sizeof(_node_prefs->node_name)];
|
|
|
|
|
|
display.translateUTF8ToBlocks(filtered_name, _node_prefs->node_name, sizeof(filtered_name));
|
2026-05-14 18:13:18 +02:00
|
|
|
|
int rightEdge = renderBatteryIndicator(display, _task->getBattMilliVolts());
|
|
|
|
|
|
display.setColor(DisplayDriver::LIGHT);
|
feat(repeater): politeness controls, dedicated radio profile, and diagnostics
Settings > Radio gains a repeater toggle, 16 community-suggested radio
presets plus manual Freq/SF/BW/CR tuning (digit-by-digit Freq editor),
4 persisted user preset slots, and the packet pool bumped 16->32 so
queued retransmits stop starving incoming-packet allocation while
relaying.
Four opt-in politeness knobs (skip-advert, max-hops, yield, min-SNR),
all flood-only and off by default, plus overhear suppression that
cancels a queued retransmit if a peer relays the same packet first.
Adaptive Power Control is suppressed while relaying (pins TX power to
the ceiling) and duty-cycle RX is forced off, since a repeater needs
to hear and relay at consistent power.
A dedicated Tools > Repeater screen consolidates the toggle, the
politeness knobs, and live forwarding stats, plus an optional "Custom"
radio profile — a dedicated frequency/SF/BW/CR for relaying, separate
from the companion's own network, band-matched to the companion
frequency by default and used everywhere a radio change can happen
(boot, on-device toggle, app-driven CMD_SET_RADIO_PARAMS, Settings
radio edits) so the device never silently falls back to the wrong
params mid-relay.
Diagnostics gains the actually-forwarded packet count, real heap-free
via mallinfo(), a reset-counters popup, and hardened loop-detect
bounds. Also: a frequency-floor fix and a float-equality preset-match
fix in the repeater profile logic, deduped BW-table/valCol/profile-
seeding helpers shared between Settings and the Repeater screen, and
a build.sh fix tolerating control characters in `pio project config`'s
JSON dump.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-20 09:07:00 +02:00
|
|
|
|
// Only show the live-power readout when APC is actually controlling power —
|
|
|
|
|
|
// not merely when the pref is set. While repeating APC is suppressed and
|
|
|
|
|
|
// power is pinned to the ceiling, so apcActive() is false and the name bar
|
|
|
|
|
|
// drops the readout (matching the "--" lock in Settings).
|
|
|
|
|
|
if (the_mesh.apcActive()) {
|
2026-06-10 23:30:43 +02:00
|
|
|
|
char pwr_buf[8];
|
|
|
|
|
|
snprintf(pwr_buf, sizeof(pwr_buf), "%ddB", (int)radio_driver.getTxPower());
|
|
|
|
|
|
int pwr_w = display.getTextWidth(pwr_buf);
|
|
|
|
|
|
display.drawTextEllipsized(0, 0, rightEdge - 2 - pwr_w - 2, filtered_name);
|
|
|
|
|
|
display.drawTextRightAlign(rightEdge - 2, 0, pwr_buf);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
display.drawTextEllipsized(0, 0, rightEdge - 2, filtered_name);
|
|
|
|
|
|
}
|
feat: NearbyScreen, configurable clock dashboard, memory optimizations
- Add NearbyScreen: GPS contact list with haversine distance sort,
type filter (ALL/Chat/Rpt/Room/Snsr), detail view with ping (RTT via
ACK poll), node scan via advert(), context menu
- Add DashboardConfigScreen: configure up to 3 data fields on the clock
page (Battery, Temp, Humidity, Pressure, GPS, Altitude, Lux, CO2,
Contacts); entered via long-press ENTER on clock page
- Rework HP_CLOCK layout: full-screen clock (size 2 at y=0), date at
y=19, separator at y=28, three data field rows at y=31/41/51;
hide node name/battery indicator on clock page
- Persist dashboard_fields[3] in NodePrefs and DataStore
- Add isAckPending() to MyMesh for NearbyScreen ping detection
- Wire NearbyScreen into ToolsScreen and UITask
- Reduce NearbyScreen entry buffer from MAX_CONTACTS(100) to 32,
saving ~3.3 KB RAM
- Switch haversine/bearing math from double to float (sinf/cosf/atan2f),
eliminating double libm and saving ~5-10 KB flash
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 11:28:36 +02:00
|
|
|
|
}
|
2025-08-08 20:01:31 +10:00
|
|
|
|
|
2026-05-12 09:51:30 +02:00
|
|
|
|
// ensure current page is visible (e.g. after settings change)
|
2026-05-12 09:57:21 +02:00
|
|
|
|
if (!isPageVisible(_page)) _page = navPage(_page, +1);
|
2026-05-12 09:51:30 +02:00
|
|
|
|
|
feat(companion): live location sharing, Locator geofencing, trail auto-pause
Squash merge of feat/location-beacon-alerts-autopause (v1.21).
Features:
- Live Location Sharing — broadcast position as movement-gated [LOC]
messages to a channel or contact; live shares show as map pins with
distance/bearing in Nearby Nodes and a status-bar indicator. [LOC] is
parsed in DMs, channel messages and room messages; DM shares name the
sender.
- Locator (geofence) — arm a geofence around a saved waypoint or a person
(live [LOC] or last-known position), alert on arrive/leave or near/far,
with an optional homing beeper (gated to arrive/both modes). Arm from
Tools > Locator, Nearby Nodes, or Waypoints; target picker lists
favourites first, clearable via a "None" entry. Active target is drawn
as a flag on the map.
- One active target shared across Locator / Navigate / Map via a single
resolver (resolvePersonPos / activeTargetPos) that prefers a live
[LOC] share over the last-advertised GPS fix.
- Follow live contacts — Navigate to a live-sharing contact follows them
as they move and adds an ETA line; quick-share your own position from
the Map.
- Map & status-bar upgrades — home mini-map gets a north marker, scale
tick, and a connected trail line (was disconnected dots); status line
shows tracked-node count, an arrow + distance to the active Locator/Nav
target (falling back to the nearest live-tracked contact); GPS fix icon
in the top status bar, shown only on GPS boards with GPS enabled.
- Trail auto-pause — recording freezes on stops (banking elapsed time,
breaking the map line across the idle gap) and resumes on movement
without ending the session.
- Streaming trail simplification — GPS points are simplified in-stream
via a fixed-corridor (Reumann-Witkam) pass tuned for fidelity: straight
runs collapse to their endpoints, curves stay bounded to the Min-dist
tolerance, so the 512-point buffer covers a far longer route than a
flat point budget would suggest.
- Collapsible Tools (Location / Comms / System sections, fold-in-place
like Settings) and page-indicator icons on the home carousel.
- Waypoint coordinate editor — add a waypoint by scroll-editing lat/lon
digit by digit.
Fixes:
- Critical: low-heap hang and contact loss on RAM-tight builds. Halved
message-history scrollback rings (recovering ~14 KB free heap) and
made contacts/channels/prefs persistence atomic (temp file + rename),
so an interrupted save can no longer corrupt or wipe the store.
- Serial.write() bounded so a stalled USB host can't hang the device.
- Nearby Nodes: live [LOC] senders respect the type filter, sort by
shared position, and the list refreshes so live shares bubble up.
- Map: live contacts are labelled before waypoints.
- GPS status icon hidden when GPS is off in Settings.
- Splash screen no longer truncates a pre-release tag's own dash (e.g.
v1.21-rc1) when stripping the build's commit-hash suffix.
- Null-guarded the Locator target picker; clamped loc-share channel
index on load.
Under the hood:
- -Os size optimisation on the e-ink and GAT562 30S solo envs.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 19:04:21 +02:00
|
|
|
|
// curr page indicator — a row of small page icons, one per visible page, with
|
|
|
|
|
|
// the current page underlined. Hidden on CLOCK (full screen used for dashboard).
|
2026-05-14 13:28:52 +02:00
|
|
|
|
if (_page != CLOCK) {
|
2026-05-25 12:59:32 +02:00
|
|
|
|
int order[(int)Count]; int n = buildVisibleOrder(order);
|
2026-05-22 23:33:06 +02:00
|
|
|
|
int curr_vis = 0;
|
|
|
|
|
|
for (int i = 0; i < n; i++) if (order[i] == _page) { curr_vis = i; break; }
|
feat(companion): live location sharing, Locator geofencing, trail auto-pause
Squash merge of feat/location-beacon-alerts-autopause (v1.21).
Features:
- Live Location Sharing — broadcast position as movement-gated [LOC]
messages to a channel or contact; live shares show as map pins with
distance/bearing in Nearby Nodes and a status-bar indicator. [LOC] is
parsed in DMs, channel messages and room messages; DM shares name the
sender.
- Locator (geofence) — arm a geofence around a saved waypoint or a person
(live [LOC] or last-known position), alert on arrive/leave or near/far,
with an optional homing beeper (gated to arrive/both modes). Arm from
Tools > Locator, Nearby Nodes, or Waypoints; target picker lists
favourites first, clearable via a "None" entry. Active target is drawn
as a flag on the map.
- One active target shared across Locator / Navigate / Map via a single
resolver (resolvePersonPos / activeTargetPos) that prefers a live
[LOC] share over the last-advertised GPS fix.
- Follow live contacts — Navigate to a live-sharing contact follows them
as they move and adds an ETA line; quick-share your own position from
the Map.
- Map & status-bar upgrades — home mini-map gets a north marker, scale
tick, and a connected trail line (was disconnected dots); status line
shows tracked-node count, an arrow + distance to the active Locator/Nav
target (falling back to the nearest live-tracked contact); GPS fix icon
in the top status bar, shown only on GPS boards with GPS enabled.
- Trail auto-pause — recording freezes on stops (banking elapsed time,
breaking the map line across the idle gap) and resumes on movement
without ending the session.
- Streaming trail simplification — GPS points are simplified in-stream
via a fixed-corridor (Reumann-Witkam) pass tuned for fidelity: straight
runs collapse to their endpoints, curves stay bounded to the Min-dist
tolerance, so the 512-point buffer covers a far longer route than a
flat point budget would suggest.
- Collapsible Tools (Location / Comms / System sections, fold-in-place
like Settings) and page-indicator icons on the home carousel.
- Waypoint coordinate editor — add a waypoint by scroll-editing lat/lon
digit by digit.
Fixes:
- Critical: low-heap hang and contact loss on RAM-tight builds. Halved
message-history scrollback rings (recovering ~14 KB free heap) and
made contacts/channels/prefs persistence atomic (temp file + rename),
so an interrupted save can no longer corrupt or wipe the store.
- Serial.write() bounded so a stalled USB host can't hang the device.
- Nearby Nodes: live [LOC] senders respect the type filter, sort by
shared position, and the list refreshes so live shares bubble up.
- Map: live contacts are labelled before waypoints.
- GPS status icon hidden when GPS is off in Settings.
- Splash screen no longer truncates a pre-release tag's own dash (e.g.
v1.21-rc1) when stripping the build's commit-hash suffix.
- Null-guarded the Locator target picker; clamped loc-share channel
index on load.
Under the hood:
- -Os size optimisation on the e-ink and GAT562 30S solo envs.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 19:04:21 +02:00
|
|
|
|
const int s = miniIconScale(display);
|
|
|
|
|
|
const int icon_w = 5 * s;
|
|
|
|
|
|
int pitch = icon_w + 5 * s; // comfortable spacing
|
|
|
|
|
|
if (n > 1) { // shrink to fit if many pages
|
|
|
|
|
|
int fit = (display.width() - icon_w) / (n - 1);
|
|
|
|
|
|
if (fit < pitch) pitch = fit;
|
|
|
|
|
|
}
|
|
|
|
|
|
int x = display.width() / 2 - pitch * (n - 1) / 2;
|
2026-05-22 23:33:06 +02:00
|
|
|
|
for (int i = 0; i < n; i++) {
|
feat(companion): live location sharing, Locator geofencing, trail auto-pause
Squash merge of feat/location-beacon-alerts-autopause (v1.21).
Features:
- Live Location Sharing — broadcast position as movement-gated [LOC]
messages to a channel or contact; live shares show as map pins with
distance/bearing in Nearby Nodes and a status-bar indicator. [LOC] is
parsed in DMs, channel messages and room messages; DM shares name the
sender.
- Locator (geofence) — arm a geofence around a saved waypoint or a person
(live [LOC] or last-known position), alert on arrive/leave or near/far,
with an optional homing beeper (gated to arrive/both modes). Arm from
Tools > Locator, Nearby Nodes, or Waypoints; target picker lists
favourites first, clearable via a "None" entry. Active target is drawn
as a flag on the map.
- One active target shared across Locator / Navigate / Map via a single
resolver (resolvePersonPos / activeTargetPos) that prefers a live
[LOC] share over the last-advertised GPS fix.
- Follow live contacts — Navigate to a live-sharing contact follows them
as they move and adds an ETA line; quick-share your own position from
the Map.
- Map & status-bar upgrades — home mini-map gets a north marker, scale
tick, and a connected trail line (was disconnected dots); status line
shows tracked-node count, an arrow + distance to the active Locator/Nav
target (falling back to the nearest live-tracked contact); GPS fix icon
in the top status bar, shown only on GPS boards with GPS enabled.
- Trail auto-pause — recording freezes on stops (banking elapsed time,
breaking the map line across the idle gap) and resumes on movement
without ending the session.
- Streaming trail simplification — GPS points are simplified in-stream
via a fixed-corridor (Reumann-Witkam) pass tuned for fidelity: straight
runs collapse to their endpoints, curves stay bounded to the Min-dist
tolerance, so the 512-point buffer covers a far longer route than a
flat point budget would suggest.
- Collapsible Tools (Location / Comms / System sections, fold-in-place
like Settings) and page-indicator icons on the home carousel.
- Waypoint coordinate editor — add a waypoint by scroll-editing lat/lon
digit by digit.
Fixes:
- Critical: low-heap hang and contact loss on RAM-tight builds. Halved
message-history scrollback rings (recovering ~14 KB free heap) and
made contacts/channels/prefs persistence atomic (temp file + rename),
so an interrupted save can no longer corrupt or wipe the store.
- Serial.write() bounded so a stalled USB host can't hang the device.
- Nearby Nodes: live [LOC] senders respect the type filter, sort by
shared position, and the list refreshes so live shares bubble up.
- Map: live contacts are labelled before waypoints.
- GPS status icon hidden when GPS is off in Settings.
- Splash screen no longer truncates a pre-release tag's own dash (e.g.
v1.21-rc1) when stripping the build's commit-hash suffix.
- Null-guarded the Locator target picker; clamped loc-share channel
index on load.
Under the hood:
- -Os size optimisation on the e-ink and GAT562 30S solo envs.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 19:04:21 +02:00
|
|
|
|
const MiniIcon* ic = pageIcon(order[i]);
|
|
|
|
|
|
if (ic) miniIconDrawCentered(display, x, dots_y, *ic);
|
|
|
|
|
|
if (i == curr_vis) // underline the current page
|
|
|
|
|
|
display.fillRect(x - icon_w / 2, dots_y + pg_half + 1, icon_w, s);
|
|
|
|
|
|
x += pitch;
|
2025-08-08 20:01:31 +10:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-10 15:18:35 +02:00
|
|
|
|
if (_page == HomePage::CLOCK) {
|
|
|
|
|
|
uint32_t unix_ts = _rtc->getCurrentTime();
|
|
|
|
|
|
if (unix_ts < 1000000000UL) {
|
|
|
|
|
|
display.setColor(DisplayDriver::LIGHT);
|
WioTrackerL1: redesign companion UI (channels, keyboard, msg preview)
- QuickMsg → Message screen: channel history, on-screen keyboard, custom message first
- Channel history: RAM ring buffer (32 entries, 80 chars), fullscreen view with word-wrap scroll
- After channel send: stay in history, own message shown as "Me: ..."
- MsgPreview: one message per screen with up/down nav, ENTER opens fullscreen scroll
- Monochromatic display: all color usage corrected to DARK/LIGHT only
- Top bar: battery icon, BT muted indicator, splash "Plus for Wio"
- KB_MAX_LEN: 64 → 100 chars; channel text buffer: 52 → 80 chars
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 15:56:30 +02:00
|
|
|
|
display.setTextSize(1);
|
refactor: replace all hardcoded pixel positions with layout helpers
Add lineStep(), headerH(), listStart(), listVisible(), valCol() to
DisplayDriver so layout derives from getLineHeight() instead of fixed
OLED constants. Refactor all screens (SettingsScreen, NearbyScreen,
FullscreenMsgView, QuickMsgScreen, BotScreen, DashboardConfigScreen,
AutoAdvertScreen, RingtoneEditorScreen) and all HomeScreen pages plus
the lock screen and splash screen in UITask.cpp.
On OLED (lh=8) positions are unchanged; on landscape e-ink (lh=16) all
text and widgets now scale correctly to the 250×122 display.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 18:05:09 +02:00
|
|
|
|
int mid_y = display.height() / 2 - step;
|
|
|
|
|
|
display.drawTextCentered(display.width() / 2, mid_y, "! No time sync");
|
|
|
|
|
|
display.drawTextCentered(display.width() / 2, mid_y + step, "Enable GPS or");
|
|
|
|
|
|
display.drawTextCentered(display.width() / 2, mid_y + step * 2, "connect app");
|
2026-05-10 15:18:35 +02:00
|
|
|
|
} else {
|
|
|
|
|
|
int8_t tz = _node_prefs ? _node_prefs->tz_offset_hours : 0;
|
|
|
|
|
|
unix_ts += (int32_t)tz * 3600;
|
|
|
|
|
|
time_t t = (time_t)unix_ts;
|
|
|
|
|
|
struct tm* ti = gmtime(&t);
|
|
|
|
|
|
|
|
|
|
|
|
char buf[24];
|
WioTrackerL1: redesign companion UI (channels, keyboard, msg preview)
- QuickMsg → Message screen: channel history, on-screen keyboard, custom message first
- Channel history: RAM ring buffer (32 entries, 80 chars), fullscreen view with word-wrap scroll
- After channel send: stay in history, own message shown as "Me: ..."
- MsgPreview: one message per screen with up/down nav, ENTER opens fullscreen scroll
- Monochromatic display: all color usage corrected to DARK/LIGHT only
- Top bar: battery icon, BT muted indicator, splash "Plus for Wio"
- KB_MAX_LEN: 64 → 100 chars; channel text buffer: 52 → 80 chars
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 15:56:30 +02:00
|
|
|
|
display.setColor(DisplayDriver::LIGHT);
|
2026-05-24 20:41:25 +02:00
|
|
|
|
bool show_sec = !Features::IS_EINK && (!_node_prefs || !_node_prefs->clock_hide_seconds);
|
2026-05-21 21:04:43 +02:00
|
|
|
|
bool h12 = _node_prefs && _node_prefs->clock_12h;
|
2026-06-02 23:32:27 +02:00
|
|
|
|
int date_y = drawClockTime(display, 0, ti, h12, show_sec);
|
2026-05-10 15:18:35 +02:00
|
|
|
|
|
|
|
|
|
|
display.setTextSize(1);
|
feat: NearbyScreen, configurable clock dashboard, memory optimizations
- Add NearbyScreen: GPS contact list with haversine distance sort,
type filter (ALL/Chat/Rpt/Room/Snsr), detail view with ping (RTT via
ACK poll), node scan via advert(), context menu
- Add DashboardConfigScreen: configure up to 3 data fields on the clock
page (Battery, Temp, Humidity, Pressure, GPS, Altitude, Lux, CO2,
Contacts); entered via long-press ENTER on clock page
- Rework HP_CLOCK layout: full-screen clock (size 2 at y=0), date at
y=19, separator at y=28, three data field rows at y=31/41/51;
hide node name/battery indicator on clock page
- Persist dashboard_fields[3] in NodePrefs and DataStore
- Add isAckPending() to MyMesh for NearbyScreen ping detection
- Wire NearbyScreen into ToolsScreen and UITask
- Reduce NearbyScreen entry buffer from MAX_CONTACTS(100) to 32,
saving ~3.3 KB RAM
- Switch haversine/bearing math from double to float (sinf/cosf/atan2f),
eliminating double libm and saving ~5-10 KB flash
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 11:28:36 +02:00
|
|
|
|
static const char* wd[] = {"Sun","Mon","Tue","Wed","Thu","Fri","Sat"};
|
|
|
|
|
|
static const char* mo[] = {"Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"};
|
2026-05-24 20:35:49 +02:00
|
|
|
|
snprintf(buf, sizeof(buf),"%s %d %s %d", wd[ti->tm_wday], ti->tm_mday, mo[ti->tm_mon], 1900 + ti->tm_year);
|
2026-05-22 18:34:20 +02:00
|
|
|
|
display.drawTextCentered(display.width() / 2, date_y, buf);
|
feat: NearbyScreen, configurable clock dashboard, memory optimizations
- Add NearbyScreen: GPS contact list with haversine distance sort,
type filter (ALL/Chat/Rpt/Room/Snsr), detail view with ping (RTT via
ACK poll), node scan via advert(), context menu
- Add DashboardConfigScreen: configure up to 3 data fields on the clock
page (Battery, Temp, Humidity, Pressure, GPS, Altitude, Lux, CO2,
Contacts); entered via long-press ENTER on clock page
- Rework HP_CLOCK layout: full-screen clock (size 2 at y=0), date at
y=19, separator at y=28, three data field rows at y=31/41/51;
hide node name/battery indicator on clock page
- Persist dashboard_fields[3] in NodePrefs and DataStore
- Add isAckPending() to MyMesh for NearbyScreen ping detection
- Wire NearbyScreen into ToolsScreen and UITask
- Reduce NearbyScreen entry buffer from MAX_CONTACTS(100) to 32,
saving ~3.3 KB RAM
- Switch haversine/bearing math from double to float (sinf/cosf/atan2f),
eliminating double libm and saving ~5-10 KB flash
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 11:28:36 +02:00
|
|
|
|
|
2026-05-22 18:34:20 +02:00
|
|
|
|
int sep_y = date_y + lh + 1;
|
2026-05-22 23:13:27 +02:00
|
|
|
|
int dash0 = sep_y + display.sepH() + 2;
|
|
|
|
|
|
display.fillRect(0, sep_y, display.width(), display.sepH());
|
feat: NearbyScreen, configurable clock dashboard, memory optimizations
- Add NearbyScreen: GPS contact list with haversine distance sort,
type filter (ALL/Chat/Rpt/Room/Snsr), detail view with ping (RTT via
ACK poll), node scan via advert(), context menu
- Add DashboardConfigScreen: configure up to 3 data fields on the clock
page (Battery, Temp, Humidity, Pressure, GPS, Altitude, Lux, CO2,
Contacts); entered via long-press ENTER on clock page
- Rework HP_CLOCK layout: full-screen clock (size 2 at y=0), date at
y=19, separator at y=28, three data field rows at y=31/41/51;
hide node name/battery indicator on clock page
- Persist dashboard_fields[3] in NodePrefs and DataStore
- Add isAckPending() to MyMesh for NearbyScreen ping detection
- Wire NearbyScreen into ToolsScreen and UITask
- Reduce NearbyScreen entry buffer from MAX_CONTACTS(100) to 32,
saving ~3.3 KB RAM
- Switch haversine/bearing math from double to float (sinf/cosf/atan2f),
eliminating double libm and saving ~5-10 KB flash
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 11:28:36 +02:00
|
|
|
|
|
|
|
|
|
|
// dashboard data fields
|
|
|
|
|
|
if (_node_prefs) {
|
|
|
|
|
|
refresh_sensors();
|
refactor: replace all hardcoded pixel positions with layout helpers
Add lineStep(), headerH(), listStart(), listVisible(), valCol() to
DisplayDriver so layout derives from getLineHeight() instead of fixed
OLED constants. Refactor all screens (SettingsScreen, NearbyScreen,
FullscreenMsgView, QuickMsgScreen, BotScreen, DashboardConfigScreen,
AutoAdvertScreen, RingtoneEditorScreen) and all HomeScreen pages plus
the lock screen and splash screen in UITask.cpp.
On OLED (lh=8) positions are unchanged; on landscape e-ink (lh=16) all
text and widgets now scale correctly to the 250×122 display.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 18:05:09 +02:00
|
|
|
|
const int FIELD_Y[3] = { dash0, dash0 + step, dash0 + step * 2 };
|
feat: NearbyScreen, configurable clock dashboard, memory optimizations
- Add NearbyScreen: GPS contact list with haversine distance sort,
type filter (ALL/Chat/Rpt/Room/Snsr), detail view with ping (RTT via
ACK poll), node scan via advert(), context menu
- Add DashboardConfigScreen: configure up to 3 data fields on the clock
page (Battery, Temp, Humidity, Pressure, GPS, Altitude, Lux, CO2,
Contacts); entered via long-press ENTER on clock page
- Rework HP_CLOCK layout: full-screen clock (size 2 at y=0), date at
y=19, separator at y=28, three data field rows at y=31/41/51;
hide node name/battery indicator on clock page
- Persist dashboard_fields[3] in NodePrefs and DataStore
- Add isAckPending() to MyMesh for NearbyScreen ping detection
- Wire NearbyScreen into ToolsScreen and UITask
- Reduce NearbyScreen entry buffer from MAX_CONTACTS(100) to 32,
saving ~3.3 KB RAM
- Switch haversine/bearing math from double to float (sinf/cosf/atan2f),
eliminating double libm and saving ~5-10 KB flash
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 11:28:36 +02:00
|
|
|
|
for (int fi = 0; fi < 3; fi++) {
|
|
|
|
|
|
uint8_t field = _node_prefs->dashboard_fields[fi];
|
|
|
|
|
|
if (field == DASH_NONE) continue;
|
|
|
|
|
|
|
|
|
|
|
|
char label[10], val[20];
|
|
|
|
|
|
label[0] = '\0';
|
|
|
|
|
|
val[0] = '\0';
|
|
|
|
|
|
|
2026-05-27 22:55:50 +02:00
|
|
|
|
if (field == DASH_BATT_V) {
|
feat: NearbyScreen, configurable clock dashboard, memory optimizations
- Add NearbyScreen: GPS contact list with haversine distance sort,
type filter (ALL/Chat/Rpt/Room/Snsr), detail view with ping (RTT via
ACK poll), node scan via advert(), context menu
- Add DashboardConfigScreen: configure up to 3 data fields on the clock
page (Battery, Temp, Humidity, Pressure, GPS, Altitude, Lux, CO2,
Contacts); entered via long-press ENTER on clock page
- Rework HP_CLOCK layout: full-screen clock (size 2 at y=0), date at
y=19, separator at y=28, three data field rows at y=31/41/51;
hide node name/battery indicator on clock page
- Persist dashboard_fields[3] in NodePrefs and DataStore
- Add isAckPending() to MyMesh for NearbyScreen ping detection
- Wire NearbyScreen into ToolsScreen and UITask
- Reduce NearbyScreen entry buffer from MAX_CONTACTS(100) to 32,
saving ~3.3 KB RAM
- Switch haversine/bearing math from double to float (sinf/cosf/atan2f),
eliminating double libm and saving ~5-10 KB flash
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 11:28:36 +02:00
|
|
|
|
strcpy(label, "Batt");
|
|
|
|
|
|
uint16_t mv = _task->getBattMilliVolts();
|
|
|
|
|
|
if (mv > 0) snprintf(val, sizeof(val), "%u.%02uV", mv/1000, (mv%1000)/10);
|
|
|
|
|
|
else strcpy(val, "--");
|
2026-05-27 22:55:50 +02:00
|
|
|
|
} else if (field == DASH_BATT_PCT) {
|
|
|
|
|
|
strcpy(label, "Batt");
|
|
|
|
|
|
uint16_t mv = _task->getBattMilliVolts();
|
2026-05-28 22:57:50 +02:00
|
|
|
|
if (mv > 0) snprintf(val, sizeof(val), "%d%%",
|
|
|
|
|
|
battMvToPercent(mv, _node_prefs->low_batt_mv));
|
|
|
|
|
|
else strcpy(val, "--");
|
feat: NearbyScreen, configurable clock dashboard, memory optimizations
- Add NearbyScreen: GPS contact list with haversine distance sort,
type filter (ALL/Chat/Rpt/Room/Snsr), detail view with ping (RTT via
ACK poll), node scan via advert(), context menu
- Add DashboardConfigScreen: configure up to 3 data fields on the clock
page (Battery, Temp, Humidity, Pressure, GPS, Altitude, Lux, CO2,
Contacts); entered via long-press ENTER on clock page
- Rework HP_CLOCK layout: full-screen clock (size 2 at y=0), date at
y=19, separator at y=28, three data field rows at y=31/41/51;
hide node name/battery indicator on clock page
- Persist dashboard_fields[3] in NodePrefs and DataStore
- Add isAckPending() to MyMesh for NearbyScreen ping detection
- Wire NearbyScreen into ToolsScreen and UITask
- Reduce NearbyScreen entry buffer from MAX_CONTACTS(100) to 32,
saving ~3.3 KB RAM
- Switch haversine/bearing math from double to float (sinf/cosf/atan2f),
eliminating double libm and saving ~5-10 KB flash
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 11:28:36 +02:00
|
|
|
|
} else if (field == DASH_GPS) {
|
|
|
|
|
|
strcpy(label, "GPS");
|
|
|
|
|
|
#if ENV_INCLUDE_GPS == 1
|
|
|
|
|
|
LocationProvider* loc = sensors.getLocationProvider();
|
|
|
|
|
|
if (loc && loc->isValid())
|
|
|
|
|
|
snprintf(val, sizeof(val), "%.3f %.3f",
|
|
|
|
|
|
loc->getLatitude()/1000000.0f, loc->getLongitude()/1000000.0f);
|
|
|
|
|
|
else
|
|
|
|
|
|
strcpy(val, "no fix");
|
|
|
|
|
|
#else
|
|
|
|
|
|
strcpy(val, "--");
|
|
|
|
|
|
#endif
|
|
|
|
|
|
} else if (field == DASH_NODES) {
|
|
|
|
|
|
strcpy(label, "Nodes");
|
|
|
|
|
|
snprintf(val, sizeof(val), "%d", the_mesh.getNumContacts());
|
2026-05-14 18:20:49 +02:00
|
|
|
|
} else if (field == DASH_MSGS) {
|
|
|
|
|
|
strcpy(label, "Msgs");
|
|
|
|
|
|
int unread = _task->getDMUnreadTotal() + _task->getChannelUnreadCount() + _task->getRoomUnreadCount();
|
|
|
|
|
|
snprintf(val, sizeof(val), "%d", unread);
|
feat: NearbyScreen, configurable clock dashboard, memory optimizations
- Add NearbyScreen: GPS contact list with haversine distance sort,
type filter (ALL/Chat/Rpt/Room/Snsr), detail view with ping (RTT via
ACK poll), node scan via advert(), context menu
- Add DashboardConfigScreen: configure up to 3 data fields on the clock
page (Battery, Temp, Humidity, Pressure, GPS, Altitude, Lux, CO2,
Contacts); entered via long-press ENTER on clock page
- Rework HP_CLOCK layout: full-screen clock (size 2 at y=0), date at
y=19, separator at y=28, three data field rows at y=31/41/51;
hide node name/battery indicator on clock page
- Persist dashboard_fields[3] in NodePrefs and DataStore
- Add isAckPending() to MyMesh for NearbyScreen ping detection
- Wire NearbyScreen into ToolsScreen and UITask
- Reduce NearbyScreen entry buffer from MAX_CONTACTS(100) to 32,
saving ~3.3 KB RAM
- Switch haversine/bearing math from double to float (sinf/cosf/atan2f),
eliminating double libm and saving ~5-10 KB flash
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 11:28:36 +02:00
|
|
|
|
} else {
|
|
|
|
|
|
uint8_t lpp_type = 0;
|
|
|
|
|
|
switch (field) {
|
|
|
|
|
|
case DASH_TEMP: strcpy(label, "Temp"); lpp_type = LPP_TEMPERATURE; break;
|
|
|
|
|
|
case DASH_HUM: strcpy(label, "Hum"); lpp_type = LPP_RELATIVE_HUMIDITY; break;
|
|
|
|
|
|
case DASH_PRES: strcpy(label, "Pres"); lpp_type = LPP_BAROMETRIC_PRESSURE; break;
|
|
|
|
|
|
case DASH_ALT: strcpy(label, "Alt"); lpp_type = LPP_ALTITUDE; break;
|
|
|
|
|
|
case DASH_LUX: strcpy(label, "Lux"); lpp_type = LPP_LUMINOSITY; break;
|
|
|
|
|
|
case DASH_CO2: strcpy(label, "CO2"); lpp_type = LPP_CONCENTRATION; break;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (lpp_type) {
|
|
|
|
|
|
LPPReader r(sensors_lpp.getBuffer(), sensors_lpp.getSize());
|
|
|
|
|
|
uint8_t ch, type;
|
|
|
|
|
|
while (r.readHeader(ch, type)) {
|
|
|
|
|
|
if (type == lpp_type) {
|
|
|
|
|
|
float v;
|
|
|
|
|
|
switch (lpp_type) {
|
|
|
|
|
|
case LPP_TEMPERATURE: r.readTemperature(v); snprintf(val, sizeof(val), "%.1f\xf8""C", v); break;
|
|
|
|
|
|
case LPP_RELATIVE_HUMIDITY: r.readRelativeHumidity(v); snprintf(val, sizeof(val), "%.0f%%", v); break;
|
|
|
|
|
|
case LPP_BAROMETRIC_PRESSURE: r.readPressure(v); snprintf(val, sizeof(val), "%.0fhPa", v); break;
|
|
|
|
|
|
case LPP_ALTITUDE: r.readAltitude(v); snprintf(val, sizeof(val), "%.0fm", v); break;
|
|
|
|
|
|
case LPP_LUMINOSITY: r.readLuminosity(v); snprintf(val, sizeof(val), "%.0flux", v); break;
|
|
|
|
|
|
case LPP_CONCENTRATION: r.readConcentration(v); snprintf(val, sizeof(val), "%.0fppm", v); break;
|
|
|
|
|
|
}
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
r.skipData(type);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!val[0]) strcpy(val, "--");
|
|
|
|
|
|
}
|
2026-05-10 15:18:35 +02:00
|
|
|
|
|
feat: NearbyScreen, configurable clock dashboard, memory optimizations
- Add NearbyScreen: GPS contact list with haversine distance sort,
type filter (ALL/Chat/Rpt/Room/Snsr), detail view with ping (RTT via
ACK poll), node scan via advert(), context menu
- Add DashboardConfigScreen: configure up to 3 data fields on the clock
page (Battery, Temp, Humidity, Pressure, GPS, Altitude, Lux, CO2,
Contacts); entered via long-press ENTER on clock page
- Rework HP_CLOCK layout: full-screen clock (size 2 at y=0), date at
y=19, separator at y=28, three data field rows at y=31/41/51;
hide node name/battery indicator on clock page
- Persist dashboard_fields[3] in NodePrefs and DataStore
- Add isAckPending() to MyMesh for NearbyScreen ping detection
- Wire NearbyScreen into ToolsScreen and UITask
- Reduce NearbyScreen entry buffer from MAX_CONTACTS(100) to 32,
saving ~3.3 KB RAM
- Switch haversine/bearing math from double to float (sinf/cosf/atan2f),
eliminating double libm and saving ~5-10 KB flash
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 11:28:36 +02:00
|
|
|
|
if (val[0] && label[0]) {
|
|
|
|
|
|
display.setColor(DisplayDriver::LIGHT);
|
|
|
|
|
|
display.setCursor(0, FIELD_Y[fi]);
|
|
|
|
|
|
display.print(label);
|
|
|
|
|
|
int vw = display.getTextWidth(val);
|
|
|
|
|
|
display.setCursor(display.width() - vw - 1, FIELD_Y[fi]);
|
|
|
|
|
|
display.print(val);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-05-10 15:18:35 +02:00
|
|
|
|
}
|
2025-08-08 20:01:31 +10:00
|
|
|
|
} else if (_page == HomePage::RECENT) {
|
2025-08-16 18:13:50 +02:00
|
|
|
|
the_mesh.getRecentlyHeard(recent, UI_RECENT_LIST_SIZE);
|
WioTrackerL1: redesign companion UI (channels, keyboard, msg preview)
- QuickMsg → Message screen: channel history, on-screen keyboard, custom message first
- Channel history: RAM ring buffer (32 entries, 80 chars), fullscreen view with word-wrap scroll
- After channel send: stay in history, own message shown as "Me: ..."
- MsgPreview: one message per screen with up/down nav, ENTER opens fullscreen scroll
- Monochromatic display: all color usage corrected to DARK/LIGHT only
- Top bar: battery icon, BT muted indicator, splash "Plus for Wio"
- KB_MAX_LEN: 64 → 100 chars; channel text buffer: 52 → 80 chars
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 15:56:30 +02:00
|
|
|
|
display.setColor(DisplayDriver::LIGHT);
|
refactor: replace all hardcoded pixel positions with layout helpers
Add lineStep(), headerH(), listStart(), listVisible(), valCol() to
DisplayDriver so layout derives from getLineHeight() instead of fixed
OLED constants. Refactor all screens (SettingsScreen, NearbyScreen,
FullscreenMsgView, QuickMsgScreen, BotScreen, DashboardConfigScreen,
AutoAdvertScreen, RingtoneEditorScreen) and all HomeScreen pages plus
the lock screen and splash screen in UITask.cpp.
On OLED (lh=8) positions are unchanged; on landscape e-ink (lh=16) all
text and widgets now scale correctly to the 250×122 display.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 18:05:09 +02:00
|
|
|
|
int y = content_y;
|
|
|
|
|
|
for (int i = 0; i < UI_RECENT_LIST_SIZE; i++, y += step) {
|
2025-08-08 20:01:31 +10:00
|
|
|
|
auto a = &recent[i];
|
|
|
|
|
|
if (a->name[0] == 0) continue; // empty slot
|
|
|
|
|
|
int secs = _rtc->getCurrentTime() - a->recv_timestamp;
|
|
|
|
|
|
if (secs < 60) {
|
2026-05-24 20:35:49 +02:00
|
|
|
|
snprintf(tmp, sizeof(tmp),"%ds", secs);
|
2025-08-08 20:01:31 +10:00
|
|
|
|
} else if (secs < 60*60) {
|
2026-05-24 20:35:49 +02:00
|
|
|
|
snprintf(tmp, sizeof(tmp),"%dm", secs / 60);
|
2025-08-08 20:01:31 +10:00
|
|
|
|
} else {
|
2026-05-24 20:35:49 +02:00
|
|
|
|
snprintf(tmp, sizeof(tmp),"%dh", secs / (60*60));
|
2025-08-08 20:01:31 +10:00
|
|
|
|
}
|
2026-04-28 21:26:56 -07:00
|
|
|
|
|
2025-09-16 17:17:15 -07:00
|
|
|
|
int timestamp_width = display.getTextWidth(tmp);
|
|
|
|
|
|
int max_name_width = display.width() - timestamp_width - 1;
|
2026-04-28 21:26:56 -07:00
|
|
|
|
|
2025-09-16 17:17:15 -07:00
|
|
|
|
char filtered_recent_name[sizeof(a->name)];
|
|
|
|
|
|
display.translateUTF8ToBlocks(filtered_recent_name, a->name, sizeof(filtered_recent_name));
|
|
|
|
|
|
display.drawTextEllipsized(0, y, max_name_width, filtered_recent_name);
|
|
|
|
|
|
display.setCursor(display.width() - timestamp_width - 1, y);
|
2025-08-08 20:01:31 +10:00
|
|
|
|
display.print(tmp);
|
|
|
|
|
|
}
|
|
|
|
|
|
} else if (_page == HomePage::RADIO) {
|
WioTrackerL1: redesign companion UI (channels, keyboard, msg preview)
- QuickMsg → Message screen: channel history, on-screen keyboard, custom message first
- Channel history: RAM ring buffer (32 entries, 80 chars), fullscreen view with word-wrap scroll
- After channel send: stay in history, own message shown as "Me: ..."
- MsgPreview: one message per screen with up/down nav, ENTER opens fullscreen scroll
- Monochromatic display: all color usage corrected to DARK/LIGHT only
- Top bar: battery icon, BT muted indicator, splash "Plus for Wio"
- KB_MAX_LEN: 64 → 100 chars; channel text buffer: 52 → 80 chars
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 15:56:30 +02:00
|
|
|
|
display.setColor(DisplayDriver::LIGHT);
|
2025-08-08 20:01:31 +10:00
|
|
|
|
// freq / sf
|
refactor: replace all hardcoded pixel positions with layout helpers
Add lineStep(), headerH(), listStart(), listVisible(), valCol() to
DisplayDriver so layout derives from getLineHeight() instead of fixed
OLED constants. Refactor all screens (SettingsScreen, NearbyScreen,
FullscreenMsgView, QuickMsgScreen, BotScreen, DashboardConfigScreen,
AutoAdvertScreen, RingtoneEditorScreen) and all HomeScreen pages plus
the lock screen and splash screen in UITask.cpp.
On OLED (lh=8) positions are unchanged; on landscape e-ink (lh=16) all
text and widgets now scale correctly to the 250×122 display.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 18:05:09 +02:00
|
|
|
|
display.setCursor(0, content_y);
|
2026-05-24 20:35:49 +02:00
|
|
|
|
snprintf(tmp, sizeof(tmp),"FQ: %06.3f SF: %d", _node_prefs->freq, _node_prefs->sf);
|
2025-08-08 20:01:31 +10:00
|
|
|
|
display.print(tmp);
|
|
|
|
|
|
|
refactor: replace all hardcoded pixel positions with layout helpers
Add lineStep(), headerH(), listStart(), listVisible(), valCol() to
DisplayDriver so layout derives from getLineHeight() instead of fixed
OLED constants. Refactor all screens (SettingsScreen, NearbyScreen,
FullscreenMsgView, QuickMsgScreen, BotScreen, DashboardConfigScreen,
AutoAdvertScreen, RingtoneEditorScreen) and all HomeScreen pages plus
the lock screen and splash screen in UITask.cpp.
On OLED (lh=8) positions are unchanged; on landscape e-ink (lh=16) all
text and widgets now scale correctly to the 250×122 display.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 18:05:09 +02:00
|
|
|
|
display.setCursor(0, content_y + step);
|
2026-05-24 20:35:49 +02:00
|
|
|
|
snprintf(tmp, sizeof(tmp),"BW: %03.2f CR: %d", _node_prefs->bw, _node_prefs->cr);
|
2025-08-08 20:01:31 +10:00
|
|
|
|
display.print(tmp);
|
|
|
|
|
|
|
refactor: replace all hardcoded pixel positions with layout helpers
Add lineStep(), headerH(), listStart(), listVisible(), valCol() to
DisplayDriver so layout derives from getLineHeight() instead of fixed
OLED constants. Refactor all screens (SettingsScreen, NearbyScreen,
FullscreenMsgView, QuickMsgScreen, BotScreen, DashboardConfigScreen,
AutoAdvertScreen, RingtoneEditorScreen) and all HomeScreen pages plus
the lock screen and splash screen in UITask.cpp.
On OLED (lh=8) positions are unchanged; on landscape e-ink (lh=16) all
text and widgets now scale correctly to the 250×122 display.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 18:05:09 +02:00
|
|
|
|
// tx power, noise floor
|
|
|
|
|
|
display.setCursor(0, content_y + step * 2);
|
2026-06-10 23:30:43 +02:00
|
|
|
|
snprintf(tmp, sizeof(tmp),"TX: %ddBm", radio_driver.getTxPower()); // live value (reflects APC)
|
2025-08-08 20:01:31 +10:00
|
|
|
|
display.print(tmp);
|
refactor: replace all hardcoded pixel positions with layout helpers
Add lineStep(), headerH(), listStart(), listVisible(), valCol() to
DisplayDriver so layout derives from getLineHeight() instead of fixed
OLED constants. Refactor all screens (SettingsScreen, NearbyScreen,
FullscreenMsgView, QuickMsgScreen, BotScreen, DashboardConfigScreen,
AutoAdvertScreen, RingtoneEditorScreen) and all HomeScreen pages plus
the lock screen and splash screen in UITask.cpp.
On OLED (lh=8) positions are unchanged; on landscape e-ink (lh=16) all
text and widgets now scale correctly to the 250×122 display.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 18:05:09 +02:00
|
|
|
|
display.setCursor(0, content_y + step * 3);
|
2026-06-10 23:30:43 +02:00
|
|
|
|
if (radio_driver.getPowerSaving()) { // duty-cycle RX doesn't sample the floor
|
|
|
|
|
|
snprintf(tmp, sizeof(tmp),"Noise floor: n/a");
|
|
|
|
|
|
} else {
|
|
|
|
|
|
snprintf(tmp, sizeof(tmp),"Noise floor: %d", radio_driver.getNoiseFloor());
|
|
|
|
|
|
}
|
2025-08-08 20:01:31 +10:00
|
|
|
|
display.print(tmp);
|
|
|
|
|
|
} else if (_page == HomePage::BLUETOOTH) {
|
WioTrackerL1: redesign companion UI (channels, keyboard, msg preview)
- QuickMsg → Message screen: channel history, on-screen keyboard, custom message first
- Channel history: RAM ring buffer (32 entries, 80 chars), fullscreen view with word-wrap scroll
- After channel send: stay in history, own message shown as "Me: ..."
- MsgPreview: one message per screen with up/down nav, ENTER opens fullscreen scroll
- Monochromatic display: all color usage corrected to DARK/LIGHT only
- Top bar: battery icon, BT muted indicator, splash "Plus for Wio"
- KB_MAX_LEN: 64 → 100 chars; channel text buffer: 52 → 80 chars
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 15:56:30 +02:00
|
|
|
|
display.setColor(DisplayDriver::LIGHT);
|
2025-08-08 20:01:31 +10:00
|
|
|
|
display.setTextSize(1);
|
2026-05-23 14:45:24 +02:00
|
|
|
|
display.drawXbm((display.width() - 32) / 2, content_y,
|
|
|
|
|
|
_task->isSerialEnabled() ? bluetooth_on : bluetooth_off, 32, 32);
|
|
|
|
|
|
const int text_y = content_y + 32 + 3;
|
2026-06-05 12:42:14 +02:00
|
|
|
|
// The pairing PIN is BLE-specific: show it while BLE is on but not yet
|
|
|
|
|
|
// bonded. (Gating on a plain isConnected() broke this on dual builds,
|
|
|
|
|
|
// where it's hardcoded true.)
|
2026-06-05 12:03:38 +02:00
|
|
|
|
const bool waiting_for_pair = _task->isSerialEnabled() && !_task->isBLEConnected() && the_mesh.getBLEPin() != 0;
|
2026-05-23 14:45:24 +02:00
|
|
|
|
if (waiting_for_pair && !display.isLandscape()) {
|
|
|
|
|
|
char pin_buf[16];
|
|
|
|
|
|
snprintf(pin_buf, sizeof(pin_buf), "PIN: %d", the_mesh.getBLEPin());
|
|
|
|
|
|
display.drawTextCentered(display.width() / 2, text_y, pin_buf);
|
|
|
|
|
|
} else if (waiting_for_pair) {
|
2026-05-11 16:52:24 +02:00
|
|
|
|
char pin_buf[16];
|
|
|
|
|
|
snprintf(pin_buf, sizeof(pin_buf), "PIN: %d", the_mesh.getBLEPin());
|
2026-05-23 14:45:24 +02:00
|
|
|
|
display.drawTextCentered(display.width() / 2, text_y, pin_buf);
|
|
|
|
|
|
display.drawTextCentered(display.width() / 2, text_y + step, "toggle: " PRESS_LABEL);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
display.drawTextCentered(display.width() / 2, text_y, "toggle: " PRESS_LABEL);
|
2026-05-11 16:52:24 +02:00
|
|
|
|
}
|
2025-08-08 20:01:31 +10:00
|
|
|
|
} else if (_page == HomePage::ADVERT) {
|
WioTrackerL1: redesign companion UI (channels, keyboard, msg preview)
- QuickMsg → Message screen: channel history, on-screen keyboard, custom message first
- Channel history: RAM ring buffer (32 entries, 80 chars), fullscreen view with word-wrap scroll
- After channel send: stay in history, own message shown as "Me: ..."
- MsgPreview: one message per screen with up/down nav, ENTER opens fullscreen scroll
- Monochromatic display: all color usage corrected to DARK/LIGHT only
- Top bar: battery icon, BT muted indicator, splash "Plus for Wio"
- KB_MAX_LEN: 64 → 100 chars; channel text buffer: 52 → 80 chars
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 15:56:30 +02:00
|
|
|
|
display.setColor(DisplayDriver::LIGHT);
|
refactor: replace all hardcoded pixel positions with layout helpers
Add lineStep(), headerH(), listStart(), listVisible(), valCol() to
DisplayDriver so layout derives from getLineHeight() instead of fixed
OLED constants. Refactor all screens (SettingsScreen, NearbyScreen,
FullscreenMsgView, QuickMsgScreen, BotScreen, DashboardConfigScreen,
AutoAdvertScreen, RingtoneEditorScreen) and all HomeScreen pages plus
the lock screen and splash screen in UITask.cpp.
On OLED (lh=8) positions are unchanged; on landscape e-ink (lh=16) all
text and widgets now scale correctly to the 250×122 display.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 18:05:09 +02:00
|
|
|
|
display.drawXbm((display.width() - 32) / 2, content_y, advert_icon, 32, 32);
|
|
|
|
|
|
display.drawTextCentered(display.width() / 2, content_y + 32 + 3, "advert: " PRESS_LABEL);
|
2025-09-28 09:43:28 +02:00
|
|
|
|
#if ENV_INCLUDE_GPS == 1
|
2025-09-23 10:39:43 +02:00
|
|
|
|
} else if (_page == HomePage::GPS) {
|
|
|
|
|
|
LocationProvider* nmea = sensors.getLocationProvider();
|
2025-11-28 11:11:13 +01:00
|
|
|
|
char buf[50];
|
refactor: replace all hardcoded pixel positions with layout helpers
Add lineStep(), headerH(), listStart(), listVisible(), valCol() to
DisplayDriver so layout derives from getLineHeight() instead of fixed
OLED constants. Refactor all screens (SettingsScreen, NearbyScreen,
FullscreenMsgView, QuickMsgScreen, BotScreen, DashboardConfigScreen,
AutoAdvertScreen, RingtoneEditorScreen) and all HomeScreen pages plus
the lock screen and splash screen in UITask.cpp.
On OLED (lh=8) positions are unchanged; on landscape e-ink (lh=16) all
text and widgets now scale correctly to the 250×122 display.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 18:05:09 +02:00
|
|
|
|
int y = content_y;
|
2025-11-28 11:11:13 +01:00
|
|
|
|
bool gps_state = _task->getGPSState();
|
|
|
|
|
|
#ifdef PIN_GPS_SWITCH
|
|
|
|
|
|
bool hw_gps_state = digitalRead(PIN_GPS_SWITCH);
|
|
|
|
|
|
if (gps_state != hw_gps_state) {
|
|
|
|
|
|
strcpy(buf, gps_state ? "gps off(hw)" : "gps off(sw)");
|
|
|
|
|
|
} else {
|
|
|
|
|
|
strcpy(buf, gps_state ? "gps on" : "gps off");
|
|
|
|
|
|
}
|
|
|
|
|
|
#else
|
|
|
|
|
|
strcpy(buf, gps_state ? "gps on" : "gps off");
|
|
|
|
|
|
#endif
|
|
|
|
|
|
display.drawTextLeftAlign(0, y, buf);
|
2025-09-23 10:39:43 +02:00
|
|
|
|
if (nmea == NULL) {
|
refactor: replace all hardcoded pixel positions with layout helpers
Add lineStep(), headerH(), listStart(), listVisible(), valCol() to
DisplayDriver so layout derives from getLineHeight() instead of fixed
OLED constants. Refactor all screens (SettingsScreen, NearbyScreen,
FullscreenMsgView, QuickMsgScreen, BotScreen, DashboardConfigScreen,
AutoAdvertScreen, RingtoneEditorScreen) and all HomeScreen pages plus
the lock screen and splash screen in UITask.cpp.
On OLED (lh=8) positions are unchanged; on landscape e-ink (lh=16) all
text and widgets now scale correctly to the 250×122 display.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 18:05:09 +02:00
|
|
|
|
y += step;
|
2025-09-30 09:21:12 +02:00
|
|
|
|
display.drawTextLeftAlign(0, y, "Can't access GPS");
|
2025-09-23 10:39:43 +02:00
|
|
|
|
} else {
|
|
|
|
|
|
strcpy(buf, nmea->isValid()?"fix":"no fix");
|
2025-09-30 09:21:12 +02:00
|
|
|
|
display.drawTextRightAlign(display.width()-1, y, buf);
|
refactor: replace all hardcoded pixel positions with layout helpers
Add lineStep(), headerH(), listStart(), listVisible(), valCol() to
DisplayDriver so layout derives from getLineHeight() instead of fixed
OLED constants. Refactor all screens (SettingsScreen, NearbyScreen,
FullscreenMsgView, QuickMsgScreen, BotScreen, DashboardConfigScreen,
AutoAdvertScreen, RingtoneEditorScreen) and all HomeScreen pages plus
the lock screen and splash screen in UITask.cpp.
On OLED (lh=8) positions are unchanged; on landscape e-ink (lh=16) all
text and widgets now scale correctly to the 250×122 display.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 18:05:09 +02:00
|
|
|
|
y += step;
|
2025-09-30 09:21:12 +02:00
|
|
|
|
display.drawTextLeftAlign(0, y, "sat");
|
2026-05-24 20:35:49 +02:00
|
|
|
|
snprintf(buf, sizeof(buf),"%d", nmea->satellitesCount());
|
2025-09-30 09:21:12 +02:00
|
|
|
|
display.drawTextRightAlign(display.width()-1, y, buf);
|
refactor: replace all hardcoded pixel positions with layout helpers
Add lineStep(), headerH(), listStart(), listVisible(), valCol() to
DisplayDriver so layout derives from getLineHeight() instead of fixed
OLED constants. Refactor all screens (SettingsScreen, NearbyScreen,
FullscreenMsgView, QuickMsgScreen, BotScreen, DashboardConfigScreen,
AutoAdvertScreen, RingtoneEditorScreen) and all HomeScreen pages plus
the lock screen and splash screen in UITask.cpp.
On OLED (lh=8) positions are unchanged; on landscape e-ink (lh=16) all
text and widgets now scale correctly to the 250×122 display.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 18:05:09 +02:00
|
|
|
|
y += step;
|
2025-09-30 09:21:12 +02:00
|
|
|
|
display.drawTextLeftAlign(0, y, "pos");
|
2026-05-24 20:35:49 +02:00
|
|
|
|
snprintf(buf, sizeof(buf),"%.4f %.4f",
|
2025-09-23 10:39:43 +02:00
|
|
|
|
nmea->getLatitude()/1000000., nmea->getLongitude()/1000000.);
|
2025-09-30 09:21:12 +02:00
|
|
|
|
display.drawTextRightAlign(display.width()-1, y, buf);
|
refactor: replace all hardcoded pixel positions with layout helpers
Add lineStep(), headerH(), listStart(), listVisible(), valCol() to
DisplayDriver so layout derives from getLineHeight() instead of fixed
OLED constants. Refactor all screens (SettingsScreen, NearbyScreen,
FullscreenMsgView, QuickMsgScreen, BotScreen, DashboardConfigScreen,
AutoAdvertScreen, RingtoneEditorScreen) and all HomeScreen pages plus
the lock screen and splash screen in UITask.cpp.
On OLED (lh=8) positions are unchanged; on landscape e-ink (lh=16) all
text and widgets now scale correctly to the 250×122 display.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 18:05:09 +02:00
|
|
|
|
y += step;
|
2025-09-30 09:21:12 +02:00
|
|
|
|
display.drawTextLeftAlign(0, y, "alt");
|
2026-05-24 20:35:49 +02:00
|
|
|
|
snprintf(buf, sizeof(buf),"%.2f", nmea->getAltitude()/1000.);
|
2025-09-30 09:21:12 +02:00
|
|
|
|
display.drawTextRightAlign(display.width()-1, y, buf);
|
refactor: replace all hardcoded pixel positions with layout helpers
Add lineStep(), headerH(), listStart(), listVisible(), valCol() to
DisplayDriver so layout derives from getLineHeight() instead of fixed
OLED constants. Refactor all screens (SettingsScreen, NearbyScreen,
FullscreenMsgView, QuickMsgScreen, BotScreen, DashboardConfigScreen,
AutoAdvertScreen, RingtoneEditorScreen) and all HomeScreen pages plus
the lock screen and splash screen in UITask.cpp.
On OLED (lh=8) positions are unchanged; on landscape e-ink (lh=16) all
text and widgets now scale correctly to the 250×122 display.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 18:05:09 +02:00
|
|
|
|
y += step;
|
2025-09-23 10:39:43 +02:00
|
|
|
|
}
|
|
|
|
|
|
#endif
|
2025-09-09 16:32:41 +02:00
|
|
|
|
#if UI_SENSORS_PAGE == 1
|
2025-09-05 15:20:52 +02:00
|
|
|
|
} else if (_page == HomePage::SENSORS) {
|
refactor: replace all hardcoded pixel positions with layout helpers
Add lineStep(), headerH(), listStart(), listVisible(), valCol() to
DisplayDriver so layout derives from getLineHeight() instead of fixed
OLED constants. Refactor all screens (SettingsScreen, NearbyScreen,
FullscreenMsgView, QuickMsgScreen, BotScreen, DashboardConfigScreen,
AutoAdvertScreen, RingtoneEditorScreen) and all HomeScreen pages plus
the lock screen and splash screen in UITask.cpp.
On OLED (lh=8) positions are unchanged; on landscape e-ink (lh=16) all
text and widgets now scale correctly to the 250×122 display.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 18:05:09 +02:00
|
|
|
|
int y = content_y;
|
2025-09-05 15:20:52 +02:00
|
|
|
|
refresh_sensors();
|
2025-09-09 16:32:41 +02:00
|
|
|
|
|
Merge upstream/main (v1.16.0) into wio-unified
Merge 254 upstream commits. Highlights relevant to the Wio fork: native
NRF52 companion power-saving (sleep when !hasPendingWork), preamble 16->32
for SF<9, generic sensor-registration model, CMD_SEND_RAW_PACKET, isEink()
display abstraction, KEEP_DISPLAY_ON_USB, contacts-sync/transient fixes.
Conflicts resolved (11 files):
- buzzer.h/.cpp: keep NRF52 include guard + our begin()->startup(); fold in
upstream doc comment.
- platformio (wio + eink): keep our slim env layout, add upstream kiss_modem
env (+ debug_tool=stlink on eink ble).
- GxEPDDisplay.h: take upstream isEink() override.
- EnvironmentSensorManager.h: take upstream's wider #if guard.
- DataStore.cpp: take upstream saveContacts(filter) signature, keep our
::openWrite (member openWrite would shadow the global 2-arg overload).
- main.cpp: keep our watchdog init + add board.onBootComplete(); adopt
upstream's `if(!hasPendingWork()) sleep` loop.
- MyMesh.h: version -> v1.16-solo.0, build date 6 Jun 2026.
- MyMesh.cpp: keep both CMD codes (SCREENSHOT 66 + RAW_PACKET 65), both field
inits, both handlers, our screenshot fns + upstream saveContacts/save_filter,
upstream hasPendingWork() + our MyMeshBot include.
- UITask.cpp: keep our solo splash/clock page/snprintf/buzzer init; merge
auto-off (+ opt-in KEEP_DISPLAY_ON_USB); merge low-batt shutdown (our EMA +
prefs threshold + upstream's !isExternalPowered() guard and eink-aware delay).
Post-merge drift fixes (upstream API/architecture changes, not conflicts):
- applyTxPower(): radio_set_tx_power() removed upstream -> radio_driver.setTxPower().
- Sensor refactor dropped the per-sensor *_initialized flags our
getAvailableLPPTypes() relied on; removed that override and the SENSORS page
now derives available LPP types from the populated sensors_lpp buffer.
README kept ours via .gitattributes merge=ours. Builds verified on all 5 Wio
envs (dual, eink dual, radio_ble, dual_dev, eink_dual_dev). Not yet
hardware-verified.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-06 17:21:41 +02:00
|
|
|
|
// Enumerate the distinct telemetry types directly from the freshly
|
|
|
|
|
|
// populated buffer. (Upstream replaced the per-sensor *_initialized flags
|
|
|
|
|
|
// with a generic registration model, so we derive availability from what
|
|
|
|
|
|
// querySensors() actually produced instead of asking the manager.)
|
2026-05-11 21:33:49 +02:00
|
|
|
|
uint8_t avail_types[16];
|
Merge upstream/main (v1.16.0) into wio-unified
Merge 254 upstream commits. Highlights relevant to the Wio fork: native
NRF52 companion power-saving (sleep when !hasPendingWork), preamble 16->32
for SF<9, generic sensor-registration model, CMD_SEND_RAW_PACKET, isEink()
display abstraction, KEEP_DISPLAY_ON_USB, contacts-sync/transient fixes.
Conflicts resolved (11 files):
- buzzer.h/.cpp: keep NRF52 include guard + our begin()->startup(); fold in
upstream doc comment.
- platformio (wio + eink): keep our slim env layout, add upstream kiss_modem
env (+ debug_tool=stlink on eink ble).
- GxEPDDisplay.h: take upstream isEink() override.
- EnvironmentSensorManager.h: take upstream's wider #if guard.
- DataStore.cpp: take upstream saveContacts(filter) signature, keep our
::openWrite (member openWrite would shadow the global 2-arg overload).
- main.cpp: keep our watchdog init + add board.onBootComplete(); adopt
upstream's `if(!hasPendingWork()) sleep` loop.
- MyMesh.h: version -> v1.16-solo.0, build date 6 Jun 2026.
- MyMesh.cpp: keep both CMD codes (SCREENSHOT 66 + RAW_PACKET 65), both field
inits, both handlers, our screenshot fns + upstream saveContacts/save_filter,
upstream hasPendingWork() + our MyMeshBot include.
- UITask.cpp: keep our solo splash/clock page/snprintf/buzzer init; merge
auto-off (+ opt-in KEEP_DISPLAY_ON_USB); merge low-batt shutdown (our EMA +
prefs threshold + upstream's !isExternalPowered() guard and eink-aware delay).
Post-merge drift fixes (upstream API/architecture changes, not conflicts):
- applyTxPower(): radio_set_tx_power() removed upstream -> radio_driver.setTxPower().
- Sensor refactor dropped the per-sensor *_initialized flags our
getAvailableLPPTypes() relied on; removed that override and the SENSORS page
now derives available LPP types from the populated sensors_lpp buffer.
README kept ours via .gitattributes merge=ours. Builds verified on all 5 Wio
envs (dual, eink dual, radio_ble, dual_dev, eink_dual_dev). Not yet
hardware-verified.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-06 17:21:41 +02:00
|
|
|
|
int avail_count = 0;
|
|
|
|
|
|
{
|
|
|
|
|
|
LPPReader er(sensors_lpp.getBuffer(), sensors_lpp.getSize());
|
|
|
|
|
|
uint8_t ech, etype;
|
|
|
|
|
|
while (er.readHeader(ech, etype) && avail_count < 16) {
|
|
|
|
|
|
er.skipData(etype);
|
|
|
|
|
|
bool dup = false;
|
|
|
|
|
|
for (int k = 0; k < avail_count; k++) if (avail_types[k] == etype) { dup = true; break; }
|
|
|
|
|
|
if (!dup) avail_types[avail_count++] = etype;
|
2025-09-09 16:32:41 +02:00
|
|
|
|
}
|
Merge upstream/main (v1.16.0) into wio-unified
Merge 254 upstream commits. Highlights relevant to the Wio fork: native
NRF52 companion power-saving (sleep when !hasPendingWork), preamble 16->32
for SF<9, generic sensor-registration model, CMD_SEND_RAW_PACKET, isEink()
display abstraction, KEEP_DISPLAY_ON_USB, contacts-sync/transient fixes.
Conflicts resolved (11 files):
- buzzer.h/.cpp: keep NRF52 include guard + our begin()->startup(); fold in
upstream doc comment.
- platformio (wio + eink): keep our slim env layout, add upstream kiss_modem
env (+ debug_tool=stlink on eink ble).
- GxEPDDisplay.h: take upstream isEink() override.
- EnvironmentSensorManager.h: take upstream's wider #if guard.
- DataStore.cpp: take upstream saveContacts(filter) signature, keep our
::openWrite (member openWrite would shadow the global 2-arg overload).
- main.cpp: keep our watchdog init + add board.onBootComplete(); adopt
upstream's `if(!hasPendingWork()) sleep` loop.
- MyMesh.h: version -> v1.16-solo.0, build date 6 Jun 2026.
- MyMesh.cpp: keep both CMD codes (SCREENSHOT 66 + RAW_PACKET 65), both field
inits, both handlers, our screenshot fns + upstream saveContacts/save_filter,
upstream hasPendingWork() + our MyMeshBot include.
- UITask.cpp: keep our solo splash/clock page/snprintf/buzzer init; merge
auto-off (+ opt-in KEEP_DISPLAY_ON_USB); merge low-batt shutdown (our EMA +
prefs threshold + upstream's !isExternalPowered() guard and eink-aware delay).
Post-merge drift fixes (upstream API/architecture changes, not conflicts):
- applyTxPower(): radio_set_tx_power() removed upstream -> radio_driver.setTxPower().
- Sensor refactor dropped the per-sensor *_initialized flags our
getAvailableLPPTypes() relied on; removed that override and the SENSORS page
now derives available LPP types from the populated sensors_lpp buffer.
README kept ours via .gitattributes merge=ours. Builds verified on all 5 Wio
envs (dual, eink dual, radio_ble, dual_dev, eink_dual_dev). Not yet
hardware-verified.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-06 17:21:41 +02:00
|
|
|
|
}
|
2026-05-11 21:33:49 +02:00
|
|
|
|
bool need_scroll = avail_count > UI_RECENT_LIST_SIZE;
|
|
|
|
|
|
int offset = need_scroll ? (sensors_scroll_offset % avail_count) : 0;
|
|
|
|
|
|
int show_n = need_scroll ? UI_RECENT_LIST_SIZE : avail_count;
|
|
|
|
|
|
|
|
|
|
|
|
for (int i = 0; i < show_n; i++) {
|
|
|
|
|
|
uint8_t target = avail_types[(offset + i) % avail_count];
|
|
|
|
|
|
|
|
|
|
|
|
// scan LPP buffer for this type
|
|
|
|
|
|
LPPReader r(sensors_lpp.getBuffer(), sensors_lpp.getSize());
|
|
|
|
|
|
uint8_t ch, type;
|
|
|
|
|
|
char buf[22] = "--";
|
|
|
|
|
|
while (r.readHeader(ch, type)) {
|
|
|
|
|
|
if (type == target) {
|
|
|
|
|
|
float v, v2, v3;
|
|
|
|
|
|
switch (type) {
|
|
|
|
|
|
case LPP_GPS:
|
|
|
|
|
|
r.readGPS(v, v2, v3);
|
|
|
|
|
|
if (v != 0 || v2 != 0) snprintf(buf, sizeof(buf), "%.4f %.4f", v, v2);
|
|
|
|
|
|
break;
|
|
|
|
|
|
case LPP_VOLTAGE: r.readVoltage(v); snprintf(buf, sizeof(buf), "%.2fV", v); break;
|
|
|
|
|
|
case LPP_CURRENT: r.readCurrent(v); snprintf(buf, sizeof(buf), "%.3fA", v); break;
|
|
|
|
|
|
case LPP_POWER: r.readPower(v); snprintf(buf, sizeof(buf), "%.1fW", v); break;
|
|
|
|
|
|
case LPP_TEMPERATURE:r.readTemperature(v); snprintf(buf, sizeof(buf), "%.1f\xf8""C", v); break;
|
|
|
|
|
|
case LPP_RELATIVE_HUMIDITY: r.readRelativeHumidity(v); snprintf(buf, sizeof(buf), "%.0f%%", v); break;
|
|
|
|
|
|
case LPP_BAROMETRIC_PRESSURE: r.readPressure(v); snprintf(buf, sizeof(buf), "%.1fhPa", v); break;
|
|
|
|
|
|
case LPP_ALTITUDE: r.readAltitude(v); snprintf(buf, sizeof(buf), "%.0fm", v); break;
|
|
|
|
|
|
case LPP_LUMINOSITY: r.readLuminosity(v); snprintf(buf, sizeof(buf), "%.0flux", v); break;
|
|
|
|
|
|
case LPP_PERCENTAGE: r.readPercentage(v); snprintf(buf, sizeof(buf), "%.0f%%", v); break;
|
|
|
|
|
|
case LPP_DISTANCE: r.readDistance(v); snprintf(buf, sizeof(buf), "%.2fm", v); break;
|
|
|
|
|
|
case LPP_CONCENTRATION: r.readConcentration(v); snprintf(buf, sizeof(buf), "%.0fppm", v); break;
|
|
|
|
|
|
default: r.skipData(type); continue;
|
|
|
|
|
|
}
|
2025-09-09 16:32:41 +02:00
|
|
|
|
break;
|
2026-05-11 21:33:49 +02:00
|
|
|
|
}
|
|
|
|
|
|
r.skipData(type);
|
2025-09-05 15:20:52 +02:00
|
|
|
|
}
|
2026-05-11 21:33:49 +02:00
|
|
|
|
|
|
|
|
|
|
static const struct { uint8_t type; const char* name; } TYPE_NAMES[] = {
|
|
|
|
|
|
{ LPP_VOLTAGE, "voltage" },
|
|
|
|
|
|
{ LPP_GPS, "gps" },
|
|
|
|
|
|
{ LPP_TEMPERATURE, "temp" },
|
|
|
|
|
|
{ LPP_RELATIVE_HUMIDITY, "humidity" },
|
|
|
|
|
|
{ LPP_BAROMETRIC_PRESSURE,"pressure" },
|
|
|
|
|
|
{ LPP_ALTITUDE, "altitude" },
|
|
|
|
|
|
{ LPP_CURRENT, "current" },
|
|
|
|
|
|
{ LPP_POWER, "power" },
|
|
|
|
|
|
{ LPP_LUMINOSITY, "light" },
|
|
|
|
|
|
{ LPP_PERCENTAGE, "moisture" },
|
|
|
|
|
|
{ LPP_DISTANCE, "distance" },
|
|
|
|
|
|
{ LPP_CONCENTRATION, "CO2" },
|
|
|
|
|
|
};
|
|
|
|
|
|
const char* name = "sensor";
|
|
|
|
|
|
for (auto& tn : TYPE_NAMES) { if (tn.type == target) { name = tn.name; break; } }
|
|
|
|
|
|
|
2025-09-05 15:20:52 +02:00
|
|
|
|
display.setCursor(0, y);
|
2025-09-09 16:32:41 +02:00
|
|
|
|
display.print(name);
|
2026-05-11 21:33:49 +02:00
|
|
|
|
display.setCursor(display.width() - display.getTextWidth(buf) - 1, y);
|
2025-09-05 15:20:52 +02:00
|
|
|
|
display.print(buf);
|
refactor: replace all hardcoded pixel positions with layout helpers
Add lineStep(), headerH(), listStart(), listVisible(), valCol() to
DisplayDriver so layout derives from getLineHeight() instead of fixed
OLED constants. Refactor all screens (SettingsScreen, NearbyScreen,
FullscreenMsgView, QuickMsgScreen, BotScreen, DashboardConfigScreen,
AutoAdvertScreen, RingtoneEditorScreen) and all HomeScreen pages plus
the lock screen and splash screen in UITask.cpp.
On OLED (lh=8) positions are unchanged; on landscape e-ink (lh=16) all
text and widgets now scale correctly to the 250×122 display.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 18:05:09 +02:00
|
|
|
|
y += step;
|
2025-09-05 15:20:52 +02:00
|
|
|
|
}
|
2026-05-11 21:33:49 +02:00
|
|
|
|
if (need_scroll) sensors_scroll_offset = (sensors_scroll_offset + 1) % avail_count;
|
2025-09-09 16:32:41 +02:00
|
|
|
|
else sensors_scroll_offset = 0;
|
|
|
|
|
|
#endif
|
2026-05-10 15:18:35 +02:00
|
|
|
|
} else if (_page == HomePage::SETTINGS) {
|
WioTrackerL1: redesign companion UI (channels, keyboard, msg preview)
- QuickMsg → Message screen: channel history, on-screen keyboard, custom message first
- Channel history: RAM ring buffer (32 entries, 80 chars), fullscreen view with word-wrap scroll
- After channel send: stay in history, own message shown as "Me: ..."
- MsgPreview: one message per screen with up/down nav, ENTER opens fullscreen scroll
- Monochromatic display: all color usage corrected to DARK/LIGHT only
- Top bar: battery icon, BT muted indicator, splash "Plus for Wio"
- KB_MAX_LEN: 64 → 100 chars; channel text buffer: 52 → 80 chars
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 15:56:30 +02:00
|
|
|
|
display.setColor(DisplayDriver::LIGHT);
|
2026-05-10 15:18:35 +02:00
|
|
|
|
display.setTextSize(1);
|
refactor: replace all hardcoded pixel positions with layout helpers
Add lineStep(), headerH(), listStart(), listVisible(), valCol() to
DisplayDriver so layout derives from getLineHeight() instead of fixed
OLED constants. Refactor all screens (SettingsScreen, NearbyScreen,
FullscreenMsgView, QuickMsgScreen, BotScreen, DashboardConfigScreen,
AutoAdvertScreen, RingtoneEditorScreen) and all HomeScreen pages plus
the lock screen and splash screen in UITask.cpp.
On OLED (lh=8) positions are unchanged; on landscape e-ink (lh=16) all
text and widgets now scale correctly to the 250×122 display.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 18:05:09 +02:00
|
|
|
|
display.drawTextCentered(display.width() / 2, content_y, "Settings");
|
|
|
|
|
|
display.drawTextCentered(display.width() / 2, content_y + step * 2, PRESS_LABEL " to open");
|
feat(companion): live location sharing, Locator geofencing, trail auto-pause
Squash merge of feat/location-beacon-alerts-autopause (v1.21).
Features:
- Live Location Sharing — broadcast position as movement-gated [LOC]
messages to a channel or contact; live shares show as map pins with
distance/bearing in Nearby Nodes and a status-bar indicator. [LOC] is
parsed in DMs, channel messages and room messages; DM shares name the
sender.
- Locator (geofence) — arm a geofence around a saved waypoint or a person
(live [LOC] or last-known position), alert on arrive/leave or near/far,
with an optional homing beeper (gated to arrive/both modes). Arm from
Tools > Locator, Nearby Nodes, or Waypoints; target picker lists
favourites first, clearable via a "None" entry. Active target is drawn
as a flag on the map.
- One active target shared across Locator / Navigate / Map via a single
resolver (resolvePersonPos / activeTargetPos) that prefers a live
[LOC] share over the last-advertised GPS fix.
- Follow live contacts — Navigate to a live-sharing contact follows them
as they move and adds an ETA line; quick-share your own position from
the Map.
- Map & status-bar upgrades — home mini-map gets a north marker, scale
tick, and a connected trail line (was disconnected dots); status line
shows tracked-node count, an arrow + distance to the active Locator/Nav
target (falling back to the nearest live-tracked contact); GPS fix icon
in the top status bar, shown only on GPS boards with GPS enabled.
- Trail auto-pause — recording freezes on stops (banking elapsed time,
breaking the map line across the idle gap) and resumes on movement
without ending the session.
- Streaming trail simplification — GPS points are simplified in-stream
via a fixed-corridor (Reumann-Witkam) pass tuned for fidelity: straight
runs collapse to their endpoints, curves stay bounded to the Min-dist
tolerance, so the 512-point buffer covers a far longer route than a
flat point budget would suggest.
- Collapsible Tools (Location / Comms / System sections, fold-in-place
like Settings) and page-indicator icons on the home carousel.
- Waypoint coordinate editor — add a waypoint by scroll-editing lat/lon
digit by digit.
Fixes:
- Critical: low-heap hang and contact loss on RAM-tight builds. Halved
message-history scrollback rings (recovering ~14 KB free heap) and
made contacts/channels/prefs persistence atomic (temp file + rename),
so an interrupted save can no longer corrupt or wipe the store.
- Serial.write() bounded so a stalled USB host can't hang the device.
- Nearby Nodes: live [LOC] senders respect the type filter, sort by
shared position, and the list refreshes so live shares bubble up.
- Map: live contacts are labelled before waypoints.
- GPS status icon hidden when GPS is off in Settings.
- Splash screen no longer truncates a pre-release tag's own dash (e.g.
v1.21-rc1) when stripping the build's commit-hash suffix.
- Null-guarded the Locator target picker; clamped loc-share channel
index on load.
Under the hood:
- -Os size optimisation on the e-ink and GAT562 30S solo envs.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 19:04:21 +02:00
|
|
|
|
} else if (_page == HomePage::MAP) {
|
|
|
|
|
|
display.setColor(DisplayDriver::LIGHT);
|
|
|
|
|
|
display.setTextSize(1);
|
|
|
|
|
|
// Mini-map preview filling the page, with one status line at the bottom.
|
|
|
|
|
|
int info_y = display.height() - step;
|
|
|
|
|
|
int area_h = info_y - content_y - 2;
|
|
|
|
|
|
bool drew = drawMapPreview(display, 2, content_y, display.width() - 4, area_h);
|
|
|
|
|
|
char left[20], right[16] = {0};
|
|
|
|
|
|
uint32_t now_m = rtc_clock.getCurrentTime();
|
|
|
|
|
|
LiveTrackStore& lt = _task->liveTrack();
|
|
|
|
|
|
int trk = lt.active(now_m);
|
|
|
|
|
|
// Fix state lives in the top-bar GPS icon. Track count plus an arrow +
|
|
|
|
|
|
// distance (to the active target, else the nearest live-tracked
|
|
|
|
|
|
// contact) share this one status line.
|
|
|
|
|
|
snprintf(left, sizeof(left), "Track:%d", trk);
|
|
|
|
|
|
float nearest_km = statusDistanceKm();
|
|
|
|
|
|
if (nearest_km >= 0.0f) geo::fmtDist(right, sizeof(right), nearest_km, _task->useImperial());
|
|
|
|
|
|
display.setColor(DisplayDriver::LIGHT);
|
|
|
|
|
|
if (!drew)
|
|
|
|
|
|
display.drawTextCentered(display.width() / 2, content_y + area_h / 2, "No GPS / no trail");
|
|
|
|
|
|
if (right[0]) {
|
|
|
|
|
|
// Manual layout (not drawTextCentered) so the arrow mini-icon sits
|
|
|
|
|
|
// inline between the two text runs.
|
|
|
|
|
|
const int s = miniIconScale(display);
|
|
|
|
|
|
const int gap = 3;
|
|
|
|
|
|
int lw = display.getTextWidth(left);
|
|
|
|
|
|
int iw = ICON_MAP_ARROW.w * s;
|
|
|
|
|
|
int rw = display.getTextWidth(right);
|
|
|
|
|
|
int x = display.width() / 2 - (lw + gap + iw + gap + rw) / 2;
|
|
|
|
|
|
display.setCursor(x, info_y);
|
|
|
|
|
|
display.print(left);
|
|
|
|
|
|
miniIconDrawTop(display, x + lw + gap, info_y + (lh - ICON_MAP_ARROW.h * s) / 2, ICON_MAP_ARROW);
|
|
|
|
|
|
display.setCursor(x + lw + gap + iw + gap, info_y);
|
|
|
|
|
|
display.print(right);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
display.drawTextCentered(display.width() / 2, info_y, left);
|
|
|
|
|
|
}
|
Add ringtone editor, buzzer volume, unread fixes and buzzer notification fixes
- RingtoneEditorScreen: 32-note step sequencer accessible via new Tools page on home screen. U/D=pitch, ENTER=octave cycle, L/R=cursor, MENU=options (play, duration, BPM, insert/delete, save). Notes stored packed in NodePrefs and persisted via DataStore.
- ToolsScreen: new "Tools" home page entry launching the editor
- Buzzer volume: 0-4 bar control in Settings > Sound, persisted to prefs
- Buzzer notifications: fixed notify() firing inside wrong serial-guard branch and before addChannelMsg set the channel index
- Unread counter: fixed newest-first index direction; watermark formula now correct
- OLED brightness: wider range via pre-charge register 0xD9 combined with contrast
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 09:40:37 +02:00
|
|
|
|
} else if (_page == HomePage::TOOLS) {
|
|
|
|
|
|
display.setColor(DisplayDriver::LIGHT);
|
|
|
|
|
|
display.setTextSize(1);
|
refactor: replace all hardcoded pixel positions with layout helpers
Add lineStep(), headerH(), listStart(), listVisible(), valCol() to
DisplayDriver so layout derives from getLineHeight() instead of fixed
OLED constants. Refactor all screens (SettingsScreen, NearbyScreen,
FullscreenMsgView, QuickMsgScreen, BotScreen, DashboardConfigScreen,
AutoAdvertScreen, RingtoneEditorScreen) and all HomeScreen pages plus
the lock screen and splash screen in UITask.cpp.
On OLED (lh=8) positions are unchanged; on landscape e-ink (lh=16) all
text and widgets now scale correctly to the 250×122 display.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 18:05:09 +02:00
|
|
|
|
display.drawTextCentered(display.width() / 2, content_y, "Tools");
|
|
|
|
|
|
display.drawTextCentered(display.width() / 2, content_y + step * 2, PRESS_LABEL " to open");
|
2026-05-10 18:45:35 +02:00
|
|
|
|
} else if (_page == HomePage::QUICK_MSG) {
|
WioTrackerL1: redesign companion UI (channels, keyboard, msg preview)
- QuickMsg → Message screen: channel history, on-screen keyboard, custom message first
- Channel history: RAM ring buffer (32 entries, 80 chars), fullscreen view with word-wrap scroll
- After channel send: stay in history, own message shown as "Me: ..."
- MsgPreview: one message per screen with up/down nav, ENTER opens fullscreen scroll
- Monochromatic display: all color usage corrected to DARK/LIGHT only
- Top bar: battery icon, BT muted indicator, splash "Plus for Wio"
- KB_MAX_LEN: 64 → 100 chars; channel text buffer: 52 → 80 chars
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 15:56:30 +02:00
|
|
|
|
display.setColor(DisplayDriver::LIGHT);
|
2026-05-10 18:45:35 +02:00
|
|
|
|
display.setTextSize(1);
|
refactor: replace all hardcoded pixel positions with layout helpers
Add lineStep(), headerH(), listStart(), listVisible(), valCol() to
DisplayDriver so layout derives from getLineHeight() instead of fixed
OLED constants. Refactor all screens (SettingsScreen, NearbyScreen,
FullscreenMsgView, QuickMsgScreen, BotScreen, DashboardConfigScreen,
AutoAdvertScreen, RingtoneEditorScreen) and all HomeScreen pages plus
the lock screen and splash screen in UITask.cpp.
On OLED (lh=8) positions are unchanged; on landscape e-ink (lh=16) all
text and widgets now scale correctly to the 250×122 display.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 18:05:09 +02:00
|
|
|
|
display.drawTextCentered(display.width() / 2, content_y, "Messages");
|
2026-05-13 10:25:55 +02:00
|
|
|
|
int total_unread = _task->getDMUnreadTotal() + _task->getChannelUnreadCount() + _task->getRoomUnreadCount();
|
2026-05-11 19:11:19 +02:00
|
|
|
|
if (total_unread > 0) {
|
|
|
|
|
|
char badge[20];
|
|
|
|
|
|
snprintf(badge, sizeof(badge), "%d unread", total_unread);
|
refactor: replace all hardcoded pixel positions with layout helpers
Add lineStep(), headerH(), listStart(), listVisible(), valCol() to
DisplayDriver so layout derives from getLineHeight() instead of fixed
OLED constants. Refactor all screens (SettingsScreen, NearbyScreen,
FullscreenMsgView, QuickMsgScreen, BotScreen, DashboardConfigScreen,
AutoAdvertScreen, RingtoneEditorScreen) and all HomeScreen pages plus
the lock screen and splash screen in UITask.cpp.
On OLED (lh=8) positions are unchanged; on landscape e-ink (lh=16) all
text and widgets now scale correctly to the 250×122 display.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 18:05:09 +02:00
|
|
|
|
display.drawTextCentered(display.width() / 2, content_y + step, badge);
|
2026-05-11 19:11:19 +02:00
|
|
|
|
}
|
refactor: replace all hardcoded pixel positions with layout helpers
Add lineStep(), headerH(), listStart(), listVisible(), valCol() to
DisplayDriver so layout derives from getLineHeight() instead of fixed
OLED constants. Refactor all screens (SettingsScreen, NearbyScreen,
FullscreenMsgView, QuickMsgScreen, BotScreen, DashboardConfigScreen,
AutoAdvertScreen, RingtoneEditorScreen) and all HomeScreen pages plus
the lock screen and splash screen in UITask.cpp.
On OLED (lh=8) positions are unchanged; on landscape e-ink (lh=16) all
text and widgets now scale correctly to the 250×122 display.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 18:05:09 +02:00
|
|
|
|
display.drawTextCentered(display.width() / 2, content_y + step * 2, PRESS_LABEL " to open");
|
2026-05-24 23:00:27 +02:00
|
|
|
|
} else if (_page == HomePage::FAVOURITES) {
|
|
|
|
|
|
// Grid of pinned contacts. Layout transposes to current orientation:
|
|
|
|
|
|
// landscape → 3×2, portrait → 2×3. Selected tile inverts via drawSelectionRow.
|
2026-05-24 23:17:48 +02:00
|
|
|
|
// No title — node name + battery (top bar) and the page-dots indicator above
|
|
|
|
|
|
// serve as the page identity.
|
2026-05-24 23:00:27 +02:00
|
|
|
|
display.setColor(DisplayDriver::LIGHT);
|
|
|
|
|
|
display.setTextSize(1);
|
|
|
|
|
|
|
|
|
|
|
|
const int cols = display.isLandscape() ? 3 : 2;
|
|
|
|
|
|
const int rows = NodePrefs::FAVOURITES_COUNT / cols;
|
2026-05-24 23:17:48 +02:00
|
|
|
|
const int margin = 2;
|
|
|
|
|
|
const int grid_y = content_y + margin;
|
|
|
|
|
|
const int grid_h = display.height() - grid_y - margin;
|
2026-05-24 23:00:27 +02:00
|
|
|
|
const int cell_w = display.width() / cols;
|
|
|
|
|
|
const int cell_h = grid_h / rows;
|
2026-05-24 23:17:48 +02:00
|
|
|
|
const int line_h = display.getLineHeight();
|
2026-05-24 23:00:27 +02:00
|
|
|
|
|
|
|
|
|
|
if (_fav_sel >= NodePrefs::FAVOURITES_COUNT) _fav_sel = 0;
|
|
|
|
|
|
|
|
|
|
|
|
for (uint8_t i = 0; i < NodePrefs::FAVOURITES_COUNT; i++) {
|
|
|
|
|
|
int row = i / cols;
|
|
|
|
|
|
int col = i % cols;
|
|
|
|
|
|
int cx = col * cell_w;
|
|
|
|
|
|
int cy = grid_y + row * cell_h;
|
|
|
|
|
|
bool sel = (i == _fav_sel);
|
|
|
|
|
|
display.drawSelectionRow(cx, cy, cell_w - 1, cell_h - 1, sel);
|
|
|
|
|
|
|
|
|
|
|
|
// Empty slot → all-zero prefix. Real keys collide with this with probability 2^-48.
|
|
|
|
|
|
const uint8_t* prefix = _node_prefs ? _node_prefs->favourite_contacts[i] : nullptr;
|
|
|
|
|
|
bool filled = false;
|
|
|
|
|
|
if (prefix) {
|
|
|
|
|
|
for (uint8_t b = 0; b < NodePrefs::FAVOURITE_PREFIX_LEN; b++)
|
|
|
|
|
|
if (prefix[b] != 0) { filled = true; break; }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (filled) {
|
|
|
|
|
|
ContactInfo ci;
|
|
|
|
|
|
bool found = false;
|
|
|
|
|
|
for (int idx = 0; ; idx++) {
|
|
|
|
|
|
ContactInfo c;
|
|
|
|
|
|
if (!the_mesh.getContactByIdx(idx, c)) break;
|
|
|
|
|
|
if (memcmp(c.id.pub_key, prefix, NodePrefs::FAVOURITE_PREFIX_LEN) == 0) {
|
|
|
|
|
|
ci = c; found = true; break;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
char name[24];
|
|
|
|
|
|
if (found) display.translateUTF8ToBlocks(name, ci.name, sizeof(name));
|
|
|
|
|
|
else strncpy(name, "(gone)", sizeof(name) - 1), name[sizeof(name) - 1] = '\0';
|
|
|
|
|
|
|
2026-05-25 13:30:12 +02:00
|
|
|
|
// Reserve space for the unread badge so the name's ellipsis lands
|
|
|
|
|
|
// before it instead of underneath. Badge and name share one baseline.
|
|
|
|
|
|
char badge[5] = "";
|
|
|
|
|
|
int bw = 0;
|
2026-05-24 23:00:27 +02:00
|
|
|
|
if (found) {
|
|
|
|
|
|
uint8_t unread = _task->getDMUnread(ci.id.pub_key);
|
|
|
|
|
|
if (unread > 0) {
|
|
|
|
|
|
snprintf(badge, sizeof(badge), "%d", unread);
|
2026-05-25 13:30:12 +02:00
|
|
|
|
bw = display.getTextWidth(badge) + 3; // badge + 3 px gap
|
2026-05-24 23:00:27 +02:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-05-25 13:30:12 +02:00
|
|
|
|
int name_y = cy + (cell_h - line_h) / 2;
|
|
|
|
|
|
int name_max_w = cell_w - 4 - bw;
|
|
|
|
|
|
if (name_max_w < 6) name_max_w = 6;
|
|
|
|
|
|
display.drawTextEllipsized(cx + 2, name_y, name_max_w, name);
|
|
|
|
|
|
if (badge[0]) {
|
|
|
|
|
|
display.setCursor(cx + cell_w - (bw - 3) - 2, name_y);
|
|
|
|
|
|
display.print(badge);
|
|
|
|
|
|
}
|
2026-05-24 23:00:27 +02:00
|
|
|
|
} else {
|
2026-05-24 23:17:48 +02:00
|
|
|
|
int plus_y = cy + (cell_h - line_h) / 2;
|
2026-05-24 23:00:27 +02:00
|
|
|
|
display.drawTextCentered(cx + cell_w / 2, plus_y, "+");
|
|
|
|
|
|
}
|
|
|
|
|
|
if (sel) display.setColor(DisplayDriver::LIGHT);
|
|
|
|
|
|
}
|
2026-05-25 08:08:12 +02:00
|
|
|
|
if (_pin_menu.active) _pin_menu.render(display);
|
2025-08-08 20:01:31 +10:00
|
|
|
|
} else if (_page == HomePage::SHUTDOWN) {
|
WioTrackerL1: redesign companion UI (channels, keyboard, msg preview)
- QuickMsg → Message screen: channel history, on-screen keyboard, custom message first
- Channel history: RAM ring buffer (32 entries, 80 chars), fullscreen view with word-wrap scroll
- After channel send: stay in history, own message shown as "Me: ..."
- MsgPreview: one message per screen with up/down nav, ENTER opens fullscreen scroll
- Monochromatic display: all color usage corrected to DARK/LIGHT only
- Top bar: battery icon, BT muted indicator, splash "Plus for Wio"
- KB_MAX_LEN: 64 → 100 chars; channel text buffer: 52 → 80 chars
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 15:56:30 +02:00
|
|
|
|
display.setColor(DisplayDriver::LIGHT);
|
2025-08-08 20:01:31 +10:00
|
|
|
|
display.setTextSize(1);
|
|
|
|
|
|
if (_shutdown_init) {
|
refactor: replace all hardcoded pixel positions with layout helpers
Add lineStep(), headerH(), listStart(), listVisible(), valCol() to
DisplayDriver so layout derives from getLineHeight() instead of fixed
OLED constants. Refactor all screens (SettingsScreen, NearbyScreen,
FullscreenMsgView, QuickMsgScreen, BotScreen, DashboardConfigScreen,
AutoAdvertScreen, RingtoneEditorScreen) and all HomeScreen pages plus
the lock screen and splash screen in UITask.cpp.
On OLED (lh=8) positions are unchanged; on landscape e-ink (lh=16) all
text and widgets now scale correctly to the 250×122 display.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 18:05:09 +02:00
|
|
|
|
display.drawTextCentered(display.width() / 2, content_y + step, "hibernating...");
|
2025-08-08 20:01:31 +10:00
|
|
|
|
} else {
|
refactor: replace all hardcoded pixel positions with layout helpers
Add lineStep(), headerH(), listStart(), listVisible(), valCol() to
DisplayDriver so layout derives from getLineHeight() instead of fixed
OLED constants. Refactor all screens (SettingsScreen, NearbyScreen,
FullscreenMsgView, QuickMsgScreen, BotScreen, DashboardConfigScreen,
AutoAdvertScreen, RingtoneEditorScreen) and all HomeScreen pages plus
the lock screen and splash screen in UITask.cpp.
On OLED (lh=8) positions are unchanged; on landscape e-ink (lh=16) all
text and widgets now scale correctly to the 250×122 display.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 18:05:09 +02:00
|
|
|
|
display.drawXbm((display.width() - 32) / 2, content_y, power_icon, 32, 32);
|
2026-05-23 14:45:24 +02:00
|
|
|
|
const int text_y = content_y + 32 + 3;
|
|
|
|
|
|
const int lh1 = display.getLineHeight();
|
|
|
|
|
|
if (text_y + lh1 <= display.height()) {
|
|
|
|
|
|
char hib_hint[32];
|
2026-05-24 00:56:43 +02:00
|
|
|
|
snprintf(hib_hint, sizeof(hib_hint), "hibernate:%s", PRESS_LABEL);
|
2026-05-23 14:45:24 +02:00
|
|
|
|
if (display.getTextWidth(hib_hint) < display.width()) {
|
|
|
|
|
|
display.drawTextCentered(display.width() / 2, text_y, hib_hint);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
display.drawTextCentered(display.width() / 2, text_y, "hibernate:");
|
|
|
|
|
|
if (text_y + step + lh1 <= display.height())
|
|
|
|
|
|
display.drawTextCentered(display.width() / 2, text_y + step, PRESS_LABEL);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-08-08 20:01:31 +10:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-05-14 17:25:27 +02:00
|
|
|
|
bool auto_adv = _node_prefs && _node_prefs->advert_auto_interval_sec > 0;
|
2026-06-16 08:54:11 +02:00
|
|
|
|
// Any blinking status-bar indicator needs a 1 s refresh to animate evenly —
|
|
|
|
|
|
// but the status bar (and its icons) is hidden on the CLOCK page, so don't
|
|
|
|
|
|
// pay the 1 s cadence there for icons that aren't drawn.
|
feat(companion): live location sharing, Locator geofencing, trail auto-pause
Squash merge of feat/location-beacon-alerts-autopause (v1.21).
Features:
- Live Location Sharing — broadcast position as movement-gated [LOC]
messages to a channel or contact; live shares show as map pins with
distance/bearing in Nearby Nodes and a status-bar indicator. [LOC] is
parsed in DMs, channel messages and room messages; DM shares name the
sender.
- Locator (geofence) — arm a geofence around a saved waypoint or a person
(live [LOC] or last-known position), alert on arrive/leave or near/far,
with an optional homing beeper (gated to arrive/both modes). Arm from
Tools > Locator, Nearby Nodes, or Waypoints; target picker lists
favourites first, clearable via a "None" entry. Active target is drawn
as a flag on the map.
- One active target shared across Locator / Navigate / Map via a single
resolver (resolvePersonPos / activeTargetPos) that prefers a live
[LOC] share over the last-advertised GPS fix.
- Follow live contacts — Navigate to a live-sharing contact follows them
as they move and adds an ETA line; quick-share your own position from
the Map.
- Map & status-bar upgrades — home mini-map gets a north marker, scale
tick, and a connected trail line (was disconnected dots); status line
shows tracked-node count, an arrow + distance to the active Locator/Nav
target (falling back to the nearest live-tracked contact); GPS fix icon
in the top status bar, shown only on GPS boards with GPS enabled.
- Trail auto-pause — recording freezes on stops (banking elapsed time,
breaking the map line across the idle gap) and resumes on movement
without ending the session.
- Streaming trail simplification — GPS points are simplified in-stream
via a fixed-corridor (Reumann-Witkam) pass tuned for fidelity: straight
runs collapse to their endpoints, curves stay bounded to the Min-dist
tolerance, so the 512-point buffer covers a far longer route than a
flat point budget would suggest.
- Collapsible Tools (Location / Comms / System sections, fold-in-place
like Settings) and page-indicator icons on the home carousel.
- Waypoint coordinate editor — add a waypoint by scroll-editing lat/lon
digit by digit.
Fixes:
- Critical: low-heap hang and contact loss on RAM-tight builds. Halved
message-history scrollback rings (recovering ~14 KB free heap) and
made contacts/channels/prefs persistence atomic (temp file + rename),
so an interrupted save can no longer corrupt or wipe the store.
- Serial.write() bounded so a stalled USB host can't hang the device.
- Nearby Nodes: live [LOC] senders respect the type filter, sort by
shared position, and the list refreshes so live shares bubble up.
- Map: live contacts are labelled before waypoints.
- GPS status icon hidden when GPS is off in Settings.
- Splash screen no longer truncates a pre-release tag's own dash (e.g.
v1.21-rc1) when stripping the build's commit-hash suffix.
- Null-guarded the Locator target picker; clamped loc-share channel
index on load.
Under the hood:
- -Os size optimisation on the e-ink and GAT562 30S solo envs.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 19:04:21 +02:00
|
|
|
|
bool repeating = _node_prefs && _node_prefs->client_repeat;
|
|
|
|
|
|
bool loc_sharing = _node_prefs && _node_prefs->loc_share_enabled;
|
|
|
|
|
|
bool need_blink = (_page != HomePage::CLOCK) &&
|
|
|
|
|
|
(auto_adv || _task->trail().isActive() || repeating || loc_sharing);
|
2026-05-24 20:41:25 +02:00
|
|
|
|
if (Features::IS_EINK) {
|
|
|
|
|
|
// slow display: poll every 30 s; inbound msgs force immediate refresh via notify()
|
|
|
|
|
|
return Features::HOME_REFRESH_MS;
|
|
|
|
|
|
}
|
2026-05-12 20:12:54 +02:00
|
|
|
|
if (_page == HomePage::CLOCK) {
|
|
|
|
|
|
bool show_sec = !_node_prefs || !_node_prefs->clock_hide_seconds;
|
2026-05-25 09:24:36 +02:00
|
|
|
|
return need_blink ? 1000 : (show_sec ? 1000 : 60000);
|
2026-05-12 20:12:54 +02:00
|
|
|
|
}
|
2026-05-25 09:24:36 +02:00
|
|
|
|
return need_blink ? 1000 : 5000;
|
2025-08-08 20:01:31 +10:00
|
|
|
|
}
|
2025-03-10 22:42:52 +01:00
|
|
|
|
|
2025-08-08 20:01:31 +10:00
|
|
|
|
bool handleInput(char c) override {
|
2026-05-24 23:00:27 +02:00
|
|
|
|
// Favourites grid claims joystick UP/DOWN and inner LEFT/RIGHT; LEFT at the
|
|
|
|
|
|
// left column and RIGHT at the right column fall through to page nav so the
|
|
|
|
|
|
// user can still leave the page sideways.
|
|
|
|
|
|
if (_page == HomePage::FAVOURITES) {
|
2026-05-25 08:08:12 +02:00
|
|
|
|
// Pin picker consumes all input while open.
|
|
|
|
|
|
if (_pin_menu.active) {
|
|
|
|
|
|
auto res = _pin_menu.handleInput(c);
|
|
|
|
|
|
if (res == PopupMenu::SELECTED && _pin_target_slot >= 0) {
|
|
|
|
|
|
int idx = _pin_menu.selectedIndex();
|
|
|
|
|
|
if (idx >= 0 && idx < _pin_count) {
|
|
|
|
|
|
// If this contact is already pinned elsewhere, vacate that slot first.
|
|
|
|
|
|
int existing = _task->findFavouriteSlot(_pin_keys[idx]);
|
|
|
|
|
|
if (existing >= 0 && existing != _pin_target_slot) _task->clearFavouriteSlot(existing);
|
|
|
|
|
|
_task->setFavouriteSlot(_pin_target_slot, _pin_keys[idx]);
|
|
|
|
|
|
the_mesh.savePrefs();
|
|
|
|
|
|
char alert[24];
|
|
|
|
|
|
snprintf(alert, sizeof(alert), "Pinned to slot %d", _pin_target_slot + 1);
|
|
|
|
|
|
_task->showAlert(alert, 800);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if (res != PopupMenu::NONE) _pin_target_slot = -1;
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
2026-05-24 23:00:27 +02:00
|
|
|
|
DisplayDriver* d = _task->getDisplay();
|
|
|
|
|
|
const int cols = (d && d->isLandscape()) ? 3 : 2;
|
|
|
|
|
|
const int rows = NodePrefs::FAVOURITES_COUNT / cols;
|
|
|
|
|
|
int col = _fav_sel % cols;
|
|
|
|
|
|
int row = _fav_sel / cols;
|
|
|
|
|
|
if ((c == KEY_LEFT || c == KEY_PREV) && col > 0) { _fav_sel--; return true; }
|
|
|
|
|
|
if ((c == KEY_RIGHT || c == KEY_NEXT) && col < cols - 1) { _fav_sel++; return true; }
|
|
|
|
|
|
if (c == KEY_UP && row > 0) { _fav_sel -= cols; return true; }
|
|
|
|
|
|
if (c == KEY_DOWN && row < rows - 1) { _fav_sel += cols; return true; }
|
|
|
|
|
|
if (c == KEY_ENTER) {
|
2026-05-25 07:52:56 +02:00
|
|
|
|
// Filled slot → open the DM directly. Empty slot waits for phase 3
|
|
|
|
|
|
// (mini-picker); for now show the pin hint.
|
|
|
|
|
|
NodePrefs* p = _task->getNodePrefs();
|
|
|
|
|
|
const uint8_t* pfx = (p && _fav_sel < NodePrefs::FAVOURITES_COUNT)
|
|
|
|
|
|
? p->favourite_contacts[_fav_sel] : nullptr;
|
|
|
|
|
|
bool filled = false;
|
|
|
|
|
|
if (pfx) for (uint8_t b = 0; b < NodePrefs::FAVOURITE_PREFIX_LEN; b++)
|
|
|
|
|
|
if (pfx[b]) { filled = true; break; }
|
|
|
|
|
|
if (filled) {
|
|
|
|
|
|
for (int idx = 0; ; idx++) {
|
|
|
|
|
|
ContactInfo c2;
|
|
|
|
|
|
if (!the_mesh.getContactByIdx(idx, c2)) break;
|
|
|
|
|
|
if (memcmp(c2.id.pub_key, pfx, NodePrefs::FAVOURITE_PREFIX_LEN) == 0) {
|
|
|
|
|
|
_task->openContactDM(c2);
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
_task->showAlert("Contact not found", 800);
|
|
|
|
|
|
} else {
|
2026-05-25 08:08:12 +02:00
|
|
|
|
// Empty slot → open in-place pin picker.
|
|
|
|
|
|
buildPinPicker(_fav_sel);
|
2026-05-25 07:52:56 +02:00
|
|
|
|
}
|
2026-05-24 23:00:27 +02:00
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
// Edge LEFT/RIGHT and unhandled keys fall through to page nav below.
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-03 16:28:58 +10:00
|
|
|
|
if (c == KEY_LEFT || c == KEY_PREV) {
|
2026-05-12 09:51:30 +02:00
|
|
|
|
_page = navPage(_page, -1);
|
2025-08-08 20:01:31 +10:00
|
|
|
|
return true;
|
|
|
|
|
|
}
|
2025-09-03 16:28:58 +10:00
|
|
|
|
if (c == KEY_NEXT || c == KEY_RIGHT) {
|
2026-05-12 09:51:30 +02:00
|
|
|
|
_page = navPage(_page, +1);
|
2025-08-08 20:01:31 +10:00
|
|
|
|
if (_page == HomePage::RECENT) {
|
|
|
|
|
|
_task->showAlert("Recent adverts", 800);
|
|
|
|
|
|
}
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (c == KEY_ENTER && _page == HomePage::BLUETOOTH) {
|
|
|
|
|
|
if (_task->isSerialEnabled()) { // toggle Bluetooth on/off
|
|
|
|
|
|
_task->disableSerial();
|
|
|
|
|
|
} else {
|
|
|
|
|
|
_task->enableSerial();
|
|
|
|
|
|
}
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (c == KEY_ENTER && _page == HomePage::ADVERT) {
|
2025-09-17 08:53:50 +08:00
|
|
|
|
_task->notify(UIEventType::ack);
|
2025-08-08 20:01:31 +10:00
|
|
|
|
if (the_mesh.advert()) {
|
|
|
|
|
|
_task->showAlert("Advert sent!", 1000);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
_task->showAlert("Advert failed..", 1000);
|
|
|
|
|
|
}
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
2025-09-28 09:43:28 +02:00
|
|
|
|
#if ENV_INCLUDE_GPS == 1
|
2025-09-23 10:39:43 +02:00
|
|
|
|
if (c == KEY_ENTER && _page == HomePage::GPS) {
|
|
|
|
|
|
_task->toggleGPS();
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
#endif
|
2025-09-09 16:32:41 +02:00
|
|
|
|
#if UI_SENSORS_PAGE == 1
|
2025-09-05 15:32:02 +02:00
|
|
|
|
if (c == KEY_ENTER && _page == HomePage::SENSORS) {
|
2026-05-25 19:26:02 +02:00
|
|
|
|
// _task->toggleGPS();
|
2025-09-05 15:35:04 +02:00
|
|
|
|
next_sensors_refresh=0;
|
2025-09-05 15:32:02 +02:00
|
|
|
|
return true;
|
|
|
|
|
|
}
|
2025-09-09 16:32:41 +02:00
|
|
|
|
#endif
|
2026-05-10 15:18:35 +02:00
|
|
|
|
if (c == KEY_ENTER && _page == HomePage::SETTINGS) {
|
|
|
|
|
|
_task->gotoSettingsScreen();
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
feat(companion): live location sharing, Locator geofencing, trail auto-pause
Squash merge of feat/location-beacon-alerts-autopause (v1.21).
Features:
- Live Location Sharing — broadcast position as movement-gated [LOC]
messages to a channel or contact; live shares show as map pins with
distance/bearing in Nearby Nodes and a status-bar indicator. [LOC] is
parsed in DMs, channel messages and room messages; DM shares name the
sender.
- Locator (geofence) — arm a geofence around a saved waypoint or a person
(live [LOC] or last-known position), alert on arrive/leave or near/far,
with an optional homing beeper (gated to arrive/both modes). Arm from
Tools > Locator, Nearby Nodes, or Waypoints; target picker lists
favourites first, clearable via a "None" entry. Active target is drawn
as a flag on the map.
- One active target shared across Locator / Navigate / Map via a single
resolver (resolvePersonPos / activeTargetPos) that prefers a live
[LOC] share over the last-advertised GPS fix.
- Follow live contacts — Navigate to a live-sharing contact follows them
as they move and adds an ETA line; quick-share your own position from
the Map.
- Map & status-bar upgrades — home mini-map gets a north marker, scale
tick, and a connected trail line (was disconnected dots); status line
shows tracked-node count, an arrow + distance to the active Locator/Nav
target (falling back to the nearest live-tracked contact); GPS fix icon
in the top status bar, shown only on GPS boards with GPS enabled.
- Trail auto-pause — recording freezes on stops (banking elapsed time,
breaking the map line across the idle gap) and resumes on movement
without ending the session.
- Streaming trail simplification — GPS points are simplified in-stream
via a fixed-corridor (Reumann-Witkam) pass tuned for fidelity: straight
runs collapse to their endpoints, curves stay bounded to the Min-dist
tolerance, so the 512-point buffer covers a far longer route than a
flat point budget would suggest.
- Collapsible Tools (Location / Comms / System sections, fold-in-place
like Settings) and page-indicator icons on the home carousel.
- Waypoint coordinate editor — add a waypoint by scroll-editing lat/lon
digit by digit.
Fixes:
- Critical: low-heap hang and contact loss on RAM-tight builds. Halved
message-history scrollback rings (recovering ~14 KB free heap) and
made contacts/channels/prefs persistence atomic (temp file + rename),
so an interrupted save can no longer corrupt or wipe the store.
- Serial.write() bounded so a stalled USB host can't hang the device.
- Nearby Nodes: live [LOC] senders respect the type filter, sort by
shared position, and the list refreshes so live shares bubble up.
- Map: live contacts are labelled before waypoints.
- GPS status icon hidden when GPS is off in Settings.
- Splash screen no longer truncates a pre-release tag's own dash (e.g.
v1.21-rc1) when stripping the build's commit-hash suffix.
- Null-guarded the Locator target picker; clamped loc-share channel
index on load.
Under the hood:
- -Os size optimisation on the e-ink and GAT562 30S solo envs.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 19:04:21 +02:00
|
|
|
|
if (c == KEY_ENTER && _page == HomePage::MAP) {
|
|
|
|
|
|
_task->gotoMapScreen();
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
Add ringtone editor, buzzer volume, unread fixes and buzzer notification fixes
- RingtoneEditorScreen: 32-note step sequencer accessible via new Tools page on home screen. U/D=pitch, ENTER=octave cycle, L/R=cursor, MENU=options (play, duration, BPM, insert/delete, save). Notes stored packed in NodePrefs and persisted via DataStore.
- ToolsScreen: new "Tools" home page entry launching the editor
- Buzzer volume: 0-4 bar control in Settings > Sound, persisted to prefs
- Buzzer notifications: fixed notify() firing inside wrong serial-guard branch and before addChannelMsg set the channel index
- Unread counter: fixed newest-first index direction; watermark formula now correct
- OLED brightness: wider range via pre-charge register 0xD9 combined with contrast
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 09:40:37 +02:00
|
|
|
|
if (c == KEY_ENTER && _page == HomePage::TOOLS) {
|
|
|
|
|
|
_task->gotoToolsScreen();
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
2026-05-10 18:45:35 +02:00
|
|
|
|
if (c == KEY_ENTER && _page == HomePage::QUICK_MSG) {
|
|
|
|
|
|
_task->gotoQuickMsgScreen();
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
2025-08-08 20:01:31 +10:00
|
|
|
|
if (c == KEY_ENTER && _page == HomePage::SHUTDOWN) {
|
|
|
|
|
|
_shutdown_init = true; // need to wait for button to be released
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
feat: NearbyScreen, configurable clock dashboard, memory optimizations
- Add NearbyScreen: GPS contact list with haversine distance sort,
type filter (ALL/Chat/Rpt/Room/Snsr), detail view with ping (RTT via
ACK poll), node scan via advert(), context menu
- Add DashboardConfigScreen: configure up to 3 data fields on the clock
page (Battery, Temp, Humidity, Pressure, GPS, Altitude, Lux, CO2,
Contacts); entered via long-press ENTER on clock page
- Rework HP_CLOCK layout: full-screen clock (size 2 at y=0), date at
y=19, separator at y=28, three data field rows at y=31/41/51;
hide node name/battery indicator on clock page
- Persist dashboard_fields[3] in NodePrefs and DataStore
- Add isAckPending() to MyMesh for NearbyScreen ping detection
- Wire NearbyScreen into ToolsScreen and UITask
- Reduce NearbyScreen entry buffer from MAX_CONTACTS(100) to 32,
saving ~3.3 KB RAM
- Switch haversine/bearing math from double to float (sinf/cosf/atan2f),
eliminating double libm and saving ~5-10 KB flash
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 11:28:36 +02:00
|
|
|
|
if (c == KEY_CONTEXT_MENU && _page == HomePage::CLOCK) {
|
|
|
|
|
|
_task->gotoDashboardConfig();
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
feat(companion): live location sharing, Locator geofencing, trail auto-pause
Squash merge of feat/location-beacon-alerts-autopause (v1.21).
Features:
- Live Location Sharing — broadcast position as movement-gated [LOC]
messages to a channel or contact; live shares show as map pins with
distance/bearing in Nearby Nodes and a status-bar indicator. [LOC] is
parsed in DMs, channel messages and room messages; DM shares name the
sender.
- Locator (geofence) — arm a geofence around a saved waypoint or a person
(live [LOC] or last-known position), alert on arrive/leave or near/far,
with an optional homing beeper (gated to arrive/both modes). Arm from
Tools > Locator, Nearby Nodes, or Waypoints; target picker lists
favourites first, clearable via a "None" entry. Active target is drawn
as a flag on the map.
- One active target shared across Locator / Navigate / Map via a single
resolver (resolvePersonPos / activeTargetPos) that prefers a live
[LOC] share over the last-advertised GPS fix.
- Follow live contacts — Navigate to a live-sharing contact follows them
as they move and adds an ETA line; quick-share your own position from
the Map.
- Map & status-bar upgrades — home mini-map gets a north marker, scale
tick, and a connected trail line (was disconnected dots); status line
shows tracked-node count, an arrow + distance to the active Locator/Nav
target (falling back to the nearest live-tracked contact); GPS fix icon
in the top status bar, shown only on GPS boards with GPS enabled.
- Trail auto-pause — recording freezes on stops (banking elapsed time,
breaking the map line across the idle gap) and resumes on movement
without ending the session.
- Streaming trail simplification — GPS points are simplified in-stream
via a fixed-corridor (Reumann-Witkam) pass tuned for fidelity: straight
runs collapse to their endpoints, curves stay bounded to the Min-dist
tolerance, so the 512-point buffer covers a far longer route than a
flat point budget would suggest.
- Collapsible Tools (Location / Comms / System sections, fold-in-place
like Settings) and page-indicator icons on the home carousel.
- Waypoint coordinate editor — add a waypoint by scroll-editing lat/lon
digit by digit.
Fixes:
- Critical: low-heap hang and contact loss on RAM-tight builds. Halved
message-history scrollback rings (recovering ~14 KB free heap) and
made contacts/channels/prefs persistence atomic (temp file + rename),
so an interrupted save can no longer corrupt or wipe the store.
- Serial.write() bounded so a stalled USB host can't hang the device.
- Nearby Nodes: live [LOC] senders respect the type filter, sort by
shared position, and the list refreshes so live shares bubble up.
- Map: live contacts are labelled before waypoints.
- GPS status icon hidden when GPS is off in Settings.
- Splash screen no longer truncates a pre-release tag's own dash (e.g.
v1.21-rc1) when stripping the build's commit-hash suffix.
- Null-guarded the Locator target picker; clamped loc-share channel
index on load.
Under the hood:
- -Os size optimisation on the e-ink and GAT562 30S solo envs.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 19:04:21 +02:00
|
|
|
|
if (c == KEY_CONTEXT_MENU && _page == HomePage::MAP) {
|
|
|
|
|
|
_task->quickShareMyLocation();
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
2025-08-08 20:01:31 +10:00
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-03-04 23:09:43 +11:00
|
|
|
|
|
2025-06-19 17:26:58 +02:00
|
|
|
|
void UITask::begin(DisplayDriver* display, SensorManager* sensors, NodePrefs* node_prefs) {
|
2025-03-16 13:42:36 +11:00
|
|
|
|
_display = display;
|
2025-06-19 17:26:58 +02:00
|
|
|
|
_sensors = sensors;
|
2026-05-10 15:18:35 +02:00
|
|
|
|
_node_prefs = node_prefs;
|
|
|
|
|
|
uint32_t aoff = autoOffMillis();
|
|
|
|
|
|
_auto_off = millis() + (aoff > 0 ? aoff : AUTO_OFF_MILLIS);
|
2025-08-08 20:01:31 +10:00
|
|
|
|
|
|
|
|
|
|
#if defined(PIN_USER_BTN)
|
|
|
|
|
|
user_btn.begin();
|
|
|
|
|
|
#endif
|
2025-09-01 17:11:55 +10:00
|
|
|
|
#if defined(PIN_USER_BTN_ANA)
|
|
|
|
|
|
analog_btn.begin();
|
|
|
|
|
|
#endif
|
2025-08-08 20:01:31 +10:00
|
|
|
|
|
2025-03-10 17:11:55 +01:00
|
|
|
|
if (_display != NULL) {
|
|
|
|
|
|
_display->turnOn();
|
|
|
|
|
|
}
|
2025-04-08 22:58:17 +12:00
|
|
|
|
|
2025-05-20 11:52:55 +10:00
|
|
|
|
#ifdef PIN_BUZZER
|
2025-11-20 18:55:39 -08:00
|
|
|
|
buzzer.quiet(_node_prefs->buzzer_quiet);
|
2026-05-12 09:17:37 +02:00
|
|
|
|
buzzer.setVolume(_node_prefs->buzzer_volume);
|
2026-05-10 15:18:35 +02:00
|
|
|
|
buzzer.begin();
|
2025-05-20 11:52:55 +10:00
|
|
|
|
#endif
|
2025-05-27 19:10:56 -07:00
|
|
|
|
|
2025-09-07 15:16:15 +08:00
|
|
|
|
#ifdef PIN_VIBRATION
|
|
|
|
|
|
vibration.begin();
|
|
|
|
|
|
#endif
|
|
|
|
|
|
|
2026-05-11 16:52:24 +02:00
|
|
|
|
// Set default quick message if slot 0 is empty (first boot)
|
|
|
|
|
|
if (_node_prefs && _node_prefs->custom_msgs[0][0] == '\0') {
|
|
|
|
|
|
strncpy(_node_prefs->custom_msgs[0], "OK", sizeof(_node_prefs->custom_msgs[0]) - 1);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-06-04 21:33:48 +10:00
|
|
|
|
ui_started_at = millis();
|
2025-08-08 20:01:31 +10:00
|
|
|
|
_alert_expiry = 0;
|
2026-05-10 15:18:35 +02:00
|
|
|
|
_batt_mv = AbstractUITask::getBattMilliVolts(); // seed EMA with first reading
|
2026-06-03 08:23:11 +02:00
|
|
|
|
|
|
|
|
|
|
// Load persisted waypoints (table survives reboots, unlike the RAM trail).
|
|
|
|
|
|
{
|
|
|
|
|
|
DataStore* ds = the_mesh.getDataStore();
|
|
|
|
|
|
if (ds) {
|
|
|
|
|
|
File f = ds->openRead("/waypoints");
|
|
|
|
|
|
if (f) { _waypoints.readFrom(f); f.close(); }
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-05-29 21:49:48 +02:00
|
|
|
|
|
|
|
|
|
|
// Initialize ping state
|
|
|
|
|
|
_ping_active = false;
|
|
|
|
|
|
_ping_tag = 0;
|
|
|
|
|
|
_ping_sent_ms = 0;
|
|
|
|
|
|
_ping_snr_out_x4 = 0;
|
|
|
|
|
|
_ping_snr_back_x4 = 0;
|
|
|
|
|
|
_ping_rtt_ms = 0;
|
2025-08-08 20:01:31 +10:00
|
|
|
|
|
|
|
|
|
|
splash = new SplashScreen(this);
|
|
|
|
|
|
home = new HomeScreen(this, &rtc_clock, sensors, node_prefs);
|
2026-06-08 20:42:18 +02:00
|
|
|
|
settings = new SettingsScreen(this, &_kb);
|
|
|
|
|
|
quick_msg = new QuickMsgScreen(this, &_kb);
|
Add ringtone editor, buzzer volume, unread fixes and buzzer notification fixes
- RingtoneEditorScreen: 32-note step sequencer accessible via new Tools page on home screen. U/D=pitch, ENTER=octave cycle, L/R=cursor, MENU=options (play, duration, BPM, insert/delete, save). Notes stored packed in NodePrefs and persisted via DataStore.
- ToolsScreen: new "Tools" home page entry launching the editor
- Buzzer volume: 0-4 bar control in Settings > Sound, persisted to prefs
- Buzzer notifications: fixed notify() firing inside wrong serial-guard branch and before addChannelMsg set the channel index
- Unread counter: fixed newest-first index direction; watermark formula now correct
- OLED brightness: wider range via pre-charge register 0xD9 combined with contrast
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 09:40:37 +02:00
|
|
|
|
tools_screen = new ToolsScreen(this);
|
|
|
|
|
|
ringtone_edit = new RingtoneEditorScreen(this, node_prefs);
|
2026-06-08 20:42:18 +02:00
|
|
|
|
bot_screen = new BotScreen(this, node_prefs, &_kb);
|
feat: NearbyScreen, configurable clock dashboard, memory optimizations
- Add NearbyScreen: GPS contact list with haversine distance sort,
type filter (ALL/Chat/Rpt/Room/Snsr), detail view with ping (RTT via
ACK poll), node scan via advert(), context menu
- Add DashboardConfigScreen: configure up to 3 data fields on the clock
page (Battery, Temp, Humidity, Pressure, GPS, Altitude, Lux, CO2,
Contacts); entered via long-press ENTER on clock page
- Rework HP_CLOCK layout: full-screen clock (size 2 at y=0), date at
y=19, separator at y=28, three data field rows at y=31/41/51;
hide node name/battery indicator on clock page
- Persist dashboard_fields[3] in NodePrefs and DataStore
- Add isAckPending() to MyMesh for NearbyScreen ping detection
- Wire NearbyScreen into ToolsScreen and UITask
- Reduce NearbyScreen entry buffer from MAX_CONTACTS(100) to 32,
saving ~3.3 KB RAM
- Switch haversine/bearing math from double to float (sinf/cosf/atan2f),
eliminating double libm and saving ~5-10 KB flash
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 11:28:36 +02:00
|
|
|
|
nearby_screen = new NearbyScreen(this);
|
|
|
|
|
|
dashboard_config = new DashboardConfigScreen(this, node_prefs);
|
2026-05-14 16:08:33 +02:00
|
|
|
|
auto_advert_screen = new AutoAdvertScreen(this, node_prefs);
|
feat(companion): live location sharing, Locator geofencing, trail auto-pause
Squash merge of feat/location-beacon-alerts-autopause (v1.21).
Features:
- Live Location Sharing — broadcast position as movement-gated [LOC]
messages to a channel or contact; live shares show as map pins with
distance/bearing in Nearby Nodes and a status-bar indicator. [LOC] is
parsed in DMs, channel messages and room messages; DM shares name the
sender.
- Locator (geofence) — arm a geofence around a saved waypoint or a person
(live [LOC] or last-known position), alert on arrive/leave or near/far,
with an optional homing beeper (gated to arrive/both modes). Arm from
Tools > Locator, Nearby Nodes, or Waypoints; target picker lists
favourites first, clearable via a "None" entry. Active target is drawn
as a flag on the map.
- One active target shared across Locator / Navigate / Map via a single
resolver (resolvePersonPos / activeTargetPos) that prefers a live
[LOC] share over the last-advertised GPS fix.
- Follow live contacts — Navigate to a live-sharing contact follows them
as they move and adds an ETA line; quick-share your own position from
the Map.
- Map & status-bar upgrades — home mini-map gets a north marker, scale
tick, and a connected trail line (was disconnected dots); status line
shows tracked-node count, an arrow + distance to the active Locator/Nav
target (falling back to the nearest live-tracked contact); GPS fix icon
in the top status bar, shown only on GPS boards with GPS enabled.
- Trail auto-pause — recording freezes on stops (banking elapsed time,
breaking the map line across the idle gap) and resumes on movement
without ending the session.
- Streaming trail simplification — GPS points are simplified in-stream
via a fixed-corridor (Reumann-Witkam) pass tuned for fidelity: straight
runs collapse to their endpoints, curves stay bounded to the Min-dist
tolerance, so the 512-point buffer covers a far longer route than a
flat point budget would suggest.
- Collapsible Tools (Location / Comms / System sections, fold-in-place
like Settings) and page-indicator icons on the home carousel.
- Waypoint coordinate editor — add a waypoint by scroll-editing lat/lon
digit by digit.
Fixes:
- Critical: low-heap hang and contact loss on RAM-tight builds. Halved
message-history scrollback rings (recovering ~14 KB free heap) and
made contacts/channels/prefs persistence atomic (temp file + rename),
so an interrupted save can no longer corrupt or wipe the store.
- Serial.write() bounded so a stalled USB host can't hang the device.
- Nearby Nodes: live [LOC] senders respect the type filter, sort by
shared position, and the list refreshes so live shares bubble up.
- Map: live contacts are labelled before waypoints.
- GPS status icon hidden when GPS is off in Settings.
- Splash screen no longer truncates a pre-release tag's own dash (e.g.
v1.21-rc1) when stripping the build's commit-hash suffix.
- Null-guarded the Locator target picker; clamped loc-share channel
index on load.
Under the hood:
- -Os size optimisation on the e-ink and GAT562 30S solo envs.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 19:04:21 +02:00
|
|
|
|
live_share_screen = new LiveShareScreen(this, node_prefs);
|
|
|
|
|
|
locator_screen = new LocatorScreen(this, node_prefs);
|
2026-05-25 09:10:40 +02:00
|
|
|
|
trail_screen = new TrailScreen(this, &_trail);
|
2026-06-03 14:07:53 +02:00
|
|
|
|
compass_screen = new CompassScreen(this);
|
2026-06-18 22:34:52 +02:00
|
|
|
|
diag_screen = new DiagnosticsScreen(this);
|
feat(repeater): politeness controls, dedicated radio profile, and diagnostics
Settings > Radio gains a repeater toggle, 16 community-suggested radio
presets plus manual Freq/SF/BW/CR tuning (digit-by-digit Freq editor),
4 persisted user preset slots, and the packet pool bumped 16->32 so
queued retransmits stop starving incoming-packet allocation while
relaying.
Four opt-in politeness knobs (skip-advert, max-hops, yield, min-SNR),
all flood-only and off by default, plus overhear suppression that
cancels a queued retransmit if a peer relays the same packet first.
Adaptive Power Control is suppressed while relaying (pins TX power to
the ceiling) and duty-cycle RX is forced off, since a repeater needs
to hear and relay at consistent power.
A dedicated Tools > Repeater screen consolidates the toggle, the
politeness knobs, and live forwarding stats, plus an optional "Custom"
radio profile — a dedicated frequency/SF/BW/CR for relaying, separate
from the companion's own network, band-matched to the companion
frequency by default and used everywhere a radio change can happen
(boot, on-device toggle, app-driven CMD_SET_RADIO_PARAMS, Settings
radio edits) so the device never silently falls back to the wrong
params mid-relay.
Diagnostics gains the actually-forwarded packet count, real heap-free
via mallinfo(), a reset-counters popup, and hardened loop-detect
bounds. Also: a frequency-floor fix and a float-equality preset-match
fix in the repeater profile logic, deduped BW-table/valCol/profile-
seeding helpers shared between Settings and the Repeater screen, and
a build.sh fix tolerating control characters in `pio project config`'s
JSON dump.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-20 09:07:00 +02:00
|
|
|
|
repeater_screen = new RepeaterScreen(this);
|
2026-05-10 15:18:35 +02:00
|
|
|
|
applyBrightness();
|
2026-05-20 09:29:38 +02:00
|
|
|
|
applyFont();
|
2026-05-20 21:53:40 +02:00
|
|
|
|
applyRotation();
|
2026-05-24 10:22:03 +02:00
|
|
|
|
applyFullRefreshInterval();
|
2026-05-22 23:50:21 +02:00
|
|
|
|
setCurrScreen(splash);
|
2025-08-08 20:01:31 +10:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-10 22:12:29 +02:00
|
|
|
|
void UITask::gotoSettingsScreen() {
|
|
|
|
|
|
((SettingsScreen*)settings)->markClean();
|
|
|
|
|
|
setCurrScreen(settings);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
Add ringtone editor, buzzer volume, unread fixes and buzzer notification fixes
- RingtoneEditorScreen: 32-note step sequencer accessible via new Tools page on home screen. U/D=pitch, ENTER=octave cycle, L/R=cursor, MENU=options (play, duration, BPM, insert/delete, save). Notes stored packed in NodePrefs and persisted via DataStore.
- ToolsScreen: new "Tools" home page entry launching the editor
- Buzzer volume: 0-4 bar control in Settings > Sound, persisted to prefs
- Buzzer notifications: fixed notify() firing inside wrong serial-guard branch and before addChannelMsg set the channel index
- Unread counter: fixed newest-first index direction; watermark formula now correct
- OLED brightness: wider range via pre-charge register 0xD9 combined with contrast
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 09:40:37 +02:00
|
|
|
|
void UITask::gotoToolsScreen() {
|
feat(companion): live location sharing, Locator geofencing, trail auto-pause
Squash merge of feat/location-beacon-alerts-autopause (v1.21).
Features:
- Live Location Sharing — broadcast position as movement-gated [LOC]
messages to a channel or contact; live shares show as map pins with
distance/bearing in Nearby Nodes and a status-bar indicator. [LOC] is
parsed in DMs, channel messages and room messages; DM shares name the
sender.
- Locator (geofence) — arm a geofence around a saved waypoint or a person
(live [LOC] or last-known position), alert on arrive/leave or near/far,
with an optional homing beeper (gated to arrive/both modes). Arm from
Tools > Locator, Nearby Nodes, or Waypoints; target picker lists
favourites first, clearable via a "None" entry. Active target is drawn
as a flag on the map.
- One active target shared across Locator / Navigate / Map via a single
resolver (resolvePersonPos / activeTargetPos) that prefers a live
[LOC] share over the last-advertised GPS fix.
- Follow live contacts — Navigate to a live-sharing contact follows them
as they move and adds an ETA line; quick-share your own position from
the Map.
- Map & status-bar upgrades — home mini-map gets a north marker, scale
tick, and a connected trail line (was disconnected dots); status line
shows tracked-node count, an arrow + distance to the active Locator/Nav
target (falling back to the nearest live-tracked contact); GPS fix icon
in the top status bar, shown only on GPS boards with GPS enabled.
- Trail auto-pause — recording freezes on stops (banking elapsed time,
breaking the map line across the idle gap) and resumes on movement
without ending the session.
- Streaming trail simplification — GPS points are simplified in-stream
via a fixed-corridor (Reumann-Witkam) pass tuned for fidelity: straight
runs collapse to their endpoints, curves stay bounded to the Min-dist
tolerance, so the 512-point buffer covers a far longer route than a
flat point budget would suggest.
- Collapsible Tools (Location / Comms / System sections, fold-in-place
like Settings) and page-indicator icons on the home carousel.
- Waypoint coordinate editor — add a waypoint by scroll-editing lat/lon
digit by digit.
Fixes:
- Critical: low-heap hang and contact loss on RAM-tight builds. Halved
message-history scrollback rings (recovering ~14 KB free heap) and
made contacts/channels/prefs persistence atomic (temp file + rename),
so an interrupted save can no longer corrupt or wipe the store.
- Serial.write() bounded so a stalled USB host can't hang the device.
- Nearby Nodes: live [LOC] senders respect the type filter, sort by
shared position, and the list refreshes so live shares bubble up.
- Map: live contacts are labelled before waypoints.
- GPS status icon hidden when GPS is off in Settings.
- Splash screen no longer truncates a pre-release tag's own dash (e.g.
v1.21-rc1) when stripping the build's commit-hash suffix.
- Null-guarded the Locator target picker; clamped loc-share channel
index on load.
Under the hood:
- -Os size optimisation on the e-ink and GAT562 30S solo envs.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 19:04:21 +02:00
|
|
|
|
((ToolsScreen*)tools_screen)->enter();
|
Add ringtone editor, buzzer volume, unread fixes and buzzer notification fixes
- RingtoneEditorScreen: 32-note step sequencer accessible via new Tools page on home screen. U/D=pitch, ENTER=octave cycle, L/R=cursor, MENU=options (play, duration, BPM, insert/delete, save). Notes stored packed in NodePrefs and persisted via DataStore.
- ToolsScreen: new "Tools" home page entry launching the editor
- Buzzer volume: 0-4 bar control in Settings > Sound, persisted to prefs
- Buzzer notifications: fixed notify() firing inside wrong serial-guard branch and before addChannelMsg set the channel index
- Unread counter: fixed newest-first index direction; watermark formula now correct
- OLED brightness: wider range via pre-charge register 0xD9 combined with contrast
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 09:40:37 +02:00
|
|
|
|
setCurrScreen(tools_screen);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-15 11:43:38 +02:00
|
|
|
|
void UITask::gotoRingtoneEditor(int slot) {
|
|
|
|
|
|
((RingtoneEditorScreen*)ringtone_edit)->enter(slot);
|
Add ringtone editor, buzzer volume, unread fixes and buzzer notification fixes
- RingtoneEditorScreen: 32-note step sequencer accessible via new Tools page on home screen. U/D=pitch, ENTER=octave cycle, L/R=cursor, MENU=options (play, duration, BPM, insert/delete, save). Notes stored packed in NodePrefs and persisted via DataStore.
- ToolsScreen: new "Tools" home page entry launching the editor
- Buzzer volume: 0-4 bar control in Settings > Sound, persisted to prefs
- Buzzer notifications: fixed notify() firing inside wrong serial-guard branch and before addChannelMsg set the channel index
- Unread counter: fixed newest-first index direction; watermark formula now correct
- OLED brightness: wider range via pre-charge register 0xD9 combined with contrast
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 09:40:37 +02:00
|
|
|
|
setCurrScreen(ringtone_edit);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-12 11:47:33 +02:00
|
|
|
|
void UITask::gotoBotScreen() {
|
|
|
|
|
|
((BotScreen*)bot_screen)->enter();
|
|
|
|
|
|
setCurrScreen(bot_screen);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
feat: NearbyScreen, configurable clock dashboard, memory optimizations
- Add NearbyScreen: GPS contact list with haversine distance sort,
type filter (ALL/Chat/Rpt/Room/Snsr), detail view with ping (RTT via
ACK poll), node scan via advert(), context menu
- Add DashboardConfigScreen: configure up to 3 data fields on the clock
page (Battery, Temp, Humidity, Pressure, GPS, Altitude, Lux, CO2,
Contacts); entered via long-press ENTER on clock page
- Rework HP_CLOCK layout: full-screen clock (size 2 at y=0), date at
y=19, separator at y=28, three data field rows at y=31/41/51;
hide node name/battery indicator on clock page
- Persist dashboard_fields[3] in NodePrefs and DataStore
- Add isAckPending() to MyMesh for NearbyScreen ping detection
- Wire NearbyScreen into ToolsScreen and UITask
- Reduce NearbyScreen entry buffer from MAX_CONTACTS(100) to 32,
saving ~3.3 KB RAM
- Switch haversine/bearing math from double to float (sinf/cosf/atan2f),
eliminating double libm and saving ~5-10 KB flash
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 11:28:36 +02:00
|
|
|
|
void UITask::gotoNearbyScreen() {
|
|
|
|
|
|
((NearbyScreen*)nearby_screen)->enter();
|
|
|
|
|
|
setCurrScreen(nearby_screen);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void UITask::gotoDashboardConfig() {
|
|
|
|
|
|
((DashboardConfigScreen*)dashboard_config)->enter();
|
|
|
|
|
|
setCurrScreen(dashboard_config);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-25 09:10:40 +02:00
|
|
|
|
void UITask::gotoTrailScreen() {
|
|
|
|
|
|
((TrailScreen*)trail_screen)->enter();
|
|
|
|
|
|
setCurrScreen(trail_screen);
|
feat(ui): GPS breadcrumb phase 1 — storage, sampling, Summary view
Adds Tools › Breadcrumb. Enter on the screen toggles tracking; while
active, UITask::loop samples GPS at the cadence chosen in settings
(default 1 min) and drops the point into a 256-entry RAM ring guarded
by a min-distance gate (default 25 m). A "G" indicator joins "A" in
the status bar with the same blink convention.
New storage:
- examples/companion_radio/Breadcrumb.h — BreadcrumbStore class
- 256 × 12 B = 3 KB RAM (survives auto-off, lost on reboot — explicit
flash-slot save comes in phase 4)
- Haversine min-delta gate, total distance, elapsed seconds, current
km/h, bounding-box helper
New screen:
- examples/companion_radio/ui-new/BreadcrumbScreen.h — Summary view
with Status, Points, Dist (m or km), Time (h:mm), Speed; Enter
toggles tracking, Esc returns to Tools
Schema bump 0xC0DE0002 → 0xC0DE0003 for two new NodePrefs bytes:
breadcrumb_interval_idx (1min default) and breadcrumb_min_delta_idx
(25m default). Older saves leave both at zero, which matches the new
defaults. DataStore::loadPrefsInt/savePrefs read/write the pair.
Logging on/off is a runtime flag inside BreadcrumbStore — not a
preference — so reboot returns to the off state cleanly; the user
will be able to persist a snapshot to a slot in phase 4.
Phase 2 (map view), 3 (point list), 4 (slot save/load, GPX export over
USB), and 5 (settings UI) follow incrementally.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 08:22:12 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
feat(companion): live location sharing, Locator geofencing, trail auto-pause
Squash merge of feat/location-beacon-alerts-autopause (v1.21).
Features:
- Live Location Sharing — broadcast position as movement-gated [LOC]
messages to a channel or contact; live shares show as map pins with
distance/bearing in Nearby Nodes and a status-bar indicator. [LOC] is
parsed in DMs, channel messages and room messages; DM shares name the
sender.
- Locator (geofence) — arm a geofence around a saved waypoint or a person
(live [LOC] or last-known position), alert on arrive/leave or near/far,
with an optional homing beeper (gated to arrive/both modes). Arm from
Tools > Locator, Nearby Nodes, or Waypoints; target picker lists
favourites first, clearable via a "None" entry. Active target is drawn
as a flag on the map.
- One active target shared across Locator / Navigate / Map via a single
resolver (resolvePersonPos / activeTargetPos) that prefers a live
[LOC] share over the last-advertised GPS fix.
- Follow live contacts — Navigate to a live-sharing contact follows them
as they move and adds an ETA line; quick-share your own position from
the Map.
- Map & status-bar upgrades — home mini-map gets a north marker, scale
tick, and a connected trail line (was disconnected dots); status line
shows tracked-node count, an arrow + distance to the active Locator/Nav
target (falling back to the nearest live-tracked contact); GPS fix icon
in the top status bar, shown only on GPS boards with GPS enabled.
- Trail auto-pause — recording freezes on stops (banking elapsed time,
breaking the map line across the idle gap) and resumes on movement
without ending the session.
- Streaming trail simplification — GPS points are simplified in-stream
via a fixed-corridor (Reumann-Witkam) pass tuned for fidelity: straight
runs collapse to their endpoints, curves stay bounded to the Min-dist
tolerance, so the 512-point buffer covers a far longer route than a
flat point budget would suggest.
- Collapsible Tools (Location / Comms / System sections, fold-in-place
like Settings) and page-indicator icons on the home carousel.
- Waypoint coordinate editor — add a waypoint by scroll-editing lat/lon
digit by digit.
Fixes:
- Critical: low-heap hang and contact loss on RAM-tight builds. Halved
message-history scrollback rings (recovering ~14 KB free heap) and
made contacts/channels/prefs persistence atomic (temp file + rename),
so an interrupted save can no longer corrupt or wipe the store.
- Serial.write() bounded so a stalled USB host can't hang the device.
- Nearby Nodes: live [LOC] senders respect the type filter, sort by
shared position, and the list refreshes so live shares bubble up.
- Map: live contacts are labelled before waypoints.
- GPS status icon hidden when GPS is off in Settings.
- Splash screen no longer truncates a pre-release tag's own dash (e.g.
v1.21-rc1) when stripping the build's commit-hash suffix.
- Null-guarded the Locator target picker; clamped loc-share channel
index on load.
Under the hood:
- -Os size optimisation on the e-ink and GAT562 30S solo envs.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 19:04:21 +02:00
|
|
|
|
void UITask::gotoMapScreen() {
|
|
|
|
|
|
((TrailScreen*)trail_screen)->enterMap();
|
|
|
|
|
|
setCurrScreen(trail_screen);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-03 14:07:53 +02:00
|
|
|
|
void UITask::gotoCompassScreen() {
|
|
|
|
|
|
((CompassScreen*)compass_screen)->enter();
|
|
|
|
|
|
setCurrScreen(compass_screen);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-18 22:34:52 +02:00
|
|
|
|
void UITask::gotoDiagnosticsScreen() {
|
|
|
|
|
|
setCurrScreen(diag_screen);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
feat(repeater): politeness controls, dedicated radio profile, and diagnostics
Settings > Radio gains a repeater toggle, 16 community-suggested radio
presets plus manual Freq/SF/BW/CR tuning (digit-by-digit Freq editor),
4 persisted user preset slots, and the packet pool bumped 16->32 so
queued retransmits stop starving incoming-packet allocation while
relaying.
Four opt-in politeness knobs (skip-advert, max-hops, yield, min-SNR),
all flood-only and off by default, plus overhear suppression that
cancels a queued retransmit if a peer relays the same packet first.
Adaptive Power Control is suppressed while relaying (pins TX power to
the ceiling) and duty-cycle RX is forced off, since a repeater needs
to hear and relay at consistent power.
A dedicated Tools > Repeater screen consolidates the toggle, the
politeness knobs, and live forwarding stats, plus an optional "Custom"
radio profile — a dedicated frequency/SF/BW/CR for relaying, separate
from the companion's own network, band-matched to the companion
frequency by default and used everywhere a radio change can happen
(boot, on-device toggle, app-driven CMD_SET_RADIO_PARAMS, Settings
radio edits) so the device never silently falls back to the wrong
params mid-relay.
Diagnostics gains the actually-forwarded packet count, real heap-free
via mallinfo(), a reset-counters popup, and hardened loop-detect
bounds. Also: a frequency-floor fix and a float-equality preset-match
fix in the repeater profile logic, deduped BW-table/valCol/profile-
seeding helpers shared between Settings and the Repeater screen, and
a build.sh fix tolerating control characters in `pio project config`'s
JSON dump.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-20 09:07:00 +02:00
|
|
|
|
void UITask::gotoRepeaterScreen() {
|
|
|
|
|
|
((RepeaterScreen*)repeater_screen)->enter();
|
|
|
|
|
|
setCurrScreen(repeater_screen);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
feat(companion): live location sharing, Locator geofencing, trail auto-pause
Squash merge of feat/location-beacon-alerts-autopause (v1.21).
Features:
- Live Location Sharing — broadcast position as movement-gated [LOC]
messages to a channel or contact; live shares show as map pins with
distance/bearing in Nearby Nodes and a status-bar indicator. [LOC] is
parsed in DMs, channel messages and room messages; DM shares name the
sender.
- Locator (geofence) — arm a geofence around a saved waypoint or a person
(live [LOC] or last-known position), alert on arrive/leave or near/far,
with an optional homing beeper (gated to arrive/both modes). Arm from
Tools > Locator, Nearby Nodes, or Waypoints; target picker lists
favourites first, clearable via a "None" entry. Active target is drawn
as a flag on the map.
- One active target shared across Locator / Navigate / Map via a single
resolver (resolvePersonPos / activeTargetPos) that prefers a live
[LOC] share over the last-advertised GPS fix.
- Follow live contacts — Navigate to a live-sharing contact follows them
as they move and adds an ETA line; quick-share your own position from
the Map.
- Map & status-bar upgrades — home mini-map gets a north marker, scale
tick, and a connected trail line (was disconnected dots); status line
shows tracked-node count, an arrow + distance to the active Locator/Nav
target (falling back to the nearest live-tracked contact); GPS fix icon
in the top status bar, shown only on GPS boards with GPS enabled.
- Trail auto-pause — recording freezes on stops (banking elapsed time,
breaking the map line across the idle gap) and resumes on movement
without ending the session.
- Streaming trail simplification — GPS points are simplified in-stream
via a fixed-corridor (Reumann-Witkam) pass tuned for fidelity: straight
runs collapse to their endpoints, curves stay bounded to the Min-dist
tolerance, so the 512-point buffer covers a far longer route than a
flat point budget would suggest.
- Collapsible Tools (Location / Comms / System sections, fold-in-place
like Settings) and page-indicator icons on the home carousel.
- Waypoint coordinate editor — add a waypoint by scroll-editing lat/lon
digit by digit.
Fixes:
- Critical: low-heap hang and contact loss on RAM-tight builds. Halved
message-history scrollback rings (recovering ~14 KB free heap) and
made contacts/channels/prefs persistence atomic (temp file + rename),
so an interrupted save can no longer corrupt or wipe the store.
- Serial.write() bounded so a stalled USB host can't hang the device.
- Nearby Nodes: live [LOC] senders respect the type filter, sort by
shared position, and the list refreshes so live shares bubble up.
- Map: live contacts are labelled before waypoints.
- GPS status icon hidden when GPS is off in Settings.
- Splash screen no longer truncates a pre-release tag's own dash (e.g.
v1.21-rc1) when stripping the build's commit-hash suffix.
- Null-guarded the Locator target picker; clamped loc-share channel
index on load.
Under the hood:
- -Os size optimisation on the e-ink and GAT562 30S solo envs.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 19:04:21 +02:00
|
|
|
|
void UITask::gotoLiveShareScreen() {
|
|
|
|
|
|
((LiveShareScreen*)live_share_screen)->enter();
|
|
|
|
|
|
setCurrScreen(live_share_screen);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void UITask::gotoLocatorScreen() {
|
|
|
|
|
|
((LocatorScreen*)locator_screen)->enter();
|
|
|
|
|
|
setCurrScreen(locator_screen);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-14 16:08:33 +02:00
|
|
|
|
void UITask::gotoAutoAdvertScreen() {
|
|
|
|
|
|
((AutoAdvertScreen*)auto_advert_screen)->enter();
|
|
|
|
|
|
setCurrScreen(auto_advert_screen);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-29 21:49:48 +02:00
|
|
|
|
// Public method to handle ping result callback
|
|
|
|
|
|
void UITask::handlePingResult(uint32_t tag, int16_t snr_out_x4, int16_t snr_back_x4, uint32_t rtt_ms) {
|
|
|
|
|
|
if (_ping_active && _ping_tag == tag) {
|
|
|
|
|
|
_ping_snr_out_x4 = snr_out_x4;
|
|
|
|
|
|
_ping_snr_back_x4 = snr_back_x4;
|
|
|
|
|
|
_ping_rtt_ms = rtt_ms;
|
|
|
|
|
|
// Release the in-flight slot immediately; the UI keeps the result values.
|
|
|
|
|
|
clearPing();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Static ping callback (for MyMesh)
|
|
|
|
|
|
static void onPingResult(uint32_t tag, int16_t snr_out_x4, int16_t snr_back_x4, uint32_t rtt_ms) {
|
|
|
|
|
|
AbstractUITask* ui = the_mesh.getUITask();
|
|
|
|
|
|
if (ui) {
|
|
|
|
|
|
UITask* task = static_cast<UITask*>(ui);
|
|
|
|
|
|
task->handlePingResult(tag, snr_out_x4, snr_back_x4, rtt_ms);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void UITask::clearPing() {
|
|
|
|
|
|
if (_ping_tag != 0) {
|
|
|
|
|
|
the_mesh.clearPingResult(_ping_tag);
|
|
|
|
|
|
}
|
|
|
|
|
|
_ping_active = false;
|
|
|
|
|
|
_ping_tag = 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
bool UITask::startPing(const uint8_t* pub_key) {
|
|
|
|
|
|
if (_ping_active || !pub_key) return false;
|
|
|
|
|
|
if (_node_prefs && _node_prefs->path_hash_mode > 1) {
|
|
|
|
|
|
showAlert("Ping not supported with 3-byte path hashes", 3000);
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
_ping_active = true;
|
|
|
|
|
|
_ping_tag = 0;
|
|
|
|
|
|
_ping_sent_ms = millis();
|
|
|
|
|
|
_ping_snr_out_x4 = 0;
|
|
|
|
|
|
_ping_snr_back_x4 = 0;
|
|
|
|
|
|
_ping_rtt_ms = 0;
|
|
|
|
|
|
|
|
|
|
|
|
// Always install the callback before sending so the response cannot race it.
|
|
|
|
|
|
the_mesh.setPingCallback(onPingResult, NULL);
|
|
|
|
|
|
_ping_tag = the_mesh.sendPing(pub_key, _node_prefs ? _node_prefs->path_hash_mode + 1 : 1);
|
|
|
|
|
|
if (_ping_tag == 0) {
|
|
|
|
|
|
clearPing();
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
Add ringtone editor, buzzer volume, unread fixes and buzzer notification fixes
- RingtoneEditorScreen: 32-note step sequencer accessible via new Tools page on home screen. U/D=pitch, ENTER=octave cycle, L/R=cursor, MENU=options (play, duration, BPM, insert/delete, save). Notes stored packed in NodePrefs and persisted via DataStore.
- ToolsScreen: new "Tools" home page entry launching the editor
- Buzzer volume: 0-4 bar control in Settings > Sound, persisted to prefs
- Buzzer notifications: fixed notify() firing inside wrong serial-guard branch and before addChannelMsg set the channel index
- Unread counter: fixed newest-first index direction; watermark formula now correct
- OLED brightness: wider range via pre-charge register 0xD9 combined with contrast
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 09:40:37 +02:00
|
|
|
|
void UITask::playMelody(const char* melody) {
|
|
|
|
|
|
#ifdef PIN_BUZZER
|
|
|
|
|
|
buzzer.playForced(melody);
|
|
|
|
|
|
#endif
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void UITask::stopMelody() {
|
|
|
|
|
|
#ifdef PIN_BUZZER
|
|
|
|
|
|
buzzer.stop();
|
|
|
|
|
|
#endif
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
bool UITask::isMelodyPlaying() {
|
|
|
|
|
|
#ifdef PIN_BUZZER
|
|
|
|
|
|
return buzzer.isPlaying();
|
|
|
|
|
|
#else
|
|
|
|
|
|
return false;
|
|
|
|
|
|
#endif
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-10 18:45:35 +02:00
|
|
|
|
void UITask::gotoQuickMsgScreen() {
|
|
|
|
|
|
((QuickMsgScreen*)quick_msg)->reset();
|
|
|
|
|
|
setCurrScreen(quick_msg);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-25 07:52:56 +02:00
|
|
|
|
void UITask::openContactDM(const ContactInfo& ci) {
|
|
|
|
|
|
((QuickMsgScreen*)quick_msg)->reset();
|
|
|
|
|
|
((QuickMsgScreen*)quick_msg)->enterDM(ci);
|
|
|
|
|
|
setCurrScreen(quick_msg);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-04 11:38:12 +02:00
|
|
|
|
void UITask::shareToMessage(const char* text) {
|
|
|
|
|
|
((QuickMsgScreen*)quick_msg)->startShare(text);
|
|
|
|
|
|
setCurrScreen(quick_msg);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
feat(companion): live location sharing, Locator geofencing, trail auto-pause
Squash merge of feat/location-beacon-alerts-autopause (v1.21).
Features:
- Live Location Sharing — broadcast position as movement-gated [LOC]
messages to a channel or contact; live shares show as map pins with
distance/bearing in Nearby Nodes and a status-bar indicator. [LOC] is
parsed in DMs, channel messages and room messages; DM shares name the
sender.
- Locator (geofence) — arm a geofence around a saved waypoint or a person
(live [LOC] or last-known position), alert on arrive/leave or near/far,
with an optional homing beeper (gated to arrive/both modes). Arm from
Tools > Locator, Nearby Nodes, or Waypoints; target picker lists
favourites first, clearable via a "None" entry. Active target is drawn
as a flag on the map.
- One active target shared across Locator / Navigate / Map via a single
resolver (resolvePersonPos / activeTargetPos) that prefers a live
[LOC] share over the last-advertised GPS fix.
- Follow live contacts — Navigate to a live-sharing contact follows them
as they move and adds an ETA line; quick-share your own position from
the Map.
- Map & status-bar upgrades — home mini-map gets a north marker, scale
tick, and a connected trail line (was disconnected dots); status line
shows tracked-node count, an arrow + distance to the active Locator/Nav
target (falling back to the nearest live-tracked contact); GPS fix icon
in the top status bar, shown only on GPS boards with GPS enabled.
- Trail auto-pause — recording freezes on stops (banking elapsed time,
breaking the map line across the idle gap) and resumes on movement
without ending the session.
- Streaming trail simplification — GPS points are simplified in-stream
via a fixed-corridor (Reumann-Witkam) pass tuned for fidelity: straight
runs collapse to their endpoints, curves stay bounded to the Min-dist
tolerance, so the 512-point buffer covers a far longer route than a
flat point budget would suggest.
- Collapsible Tools (Location / Comms / System sections, fold-in-place
like Settings) and page-indicator icons on the home carousel.
- Waypoint coordinate editor — add a waypoint by scroll-editing lat/lon
digit by digit.
Fixes:
- Critical: low-heap hang and contact loss on RAM-tight builds. Halved
message-history scrollback rings (recovering ~14 KB free heap) and
made contacts/channels/prefs persistence atomic (temp file + rename),
so an interrupted save can no longer corrupt or wipe the store.
- Serial.write() bounded so a stalled USB host can't hang the device.
- Nearby Nodes: live [LOC] senders respect the type filter, sort by
shared position, and the list refreshes so live shares bubble up.
- Map: live contacts are labelled before waypoints.
- GPS status icon hidden when GPS is off in Settings.
- Splash screen no longer truncates a pre-release tag's own dash (e.g.
v1.21-rc1) when stripping the build's commit-hash suffix.
- Null-guarded the Locator target picker; clamped loc-share channel
index on load.
Under the hood:
- -Os size optimisation on the e-ink and GAT562 30S solo envs.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 19:04:21 +02:00
|
|
|
|
void UITask::pickLocShareTarget() {
|
|
|
|
|
|
((QuickMsgScreen*)quick_msg)->startPickTarget();
|
|
|
|
|
|
setCurrScreen(quick_msg);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-25 08:08:12 +02:00
|
|
|
|
int UITask::getRecentDMContacts(uint8_t out[][NodePrefs::FAVOURITE_PREFIX_LEN], int max) const {
|
|
|
|
|
|
return ((QuickMsgScreen*)quick_msg)->getRecentDMContacts(out, max);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-28 11:00:45 +02:00
|
|
|
|
void UITask::addChannelMsg(uint8_t channel_idx, const char* text, uint32_t timestamp) {
|
2026-05-11 18:56:26 +02:00
|
|
|
|
_last_notif_ch_idx = (int)channel_idx;
|
2026-06-28 11:00:45 +02:00
|
|
|
|
((QuickMsgScreen*)quick_msg)->addChannelMsg(channel_idx, text, timestamp);
|
WioTrackerL1: redesign companion UI (channels, keyboard, msg preview)
- QuickMsg → Message screen: channel history, on-screen keyboard, custom message first
- Channel history: RAM ring buffer (32 entries, 80 chars), fullscreen view with word-wrap scroll
- After channel send: stay in history, own message shown as "Me: ..."
- MsgPreview: one message per screen with up/down nav, ENTER opens fullscreen scroll
- Monochromatic display: all color usage corrected to DARK/LIGHT only
- Top bar: battery icon, BT muted indicator, splash "Plus for Wio"
- KB_MAX_LEN: 64 → 100 chars; channel text buffer: 52 → 80 chars
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 15:56:30 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-11 19:11:19 +02:00
|
|
|
|
int UITask::getChannelUnreadCount() const {
|
|
|
|
|
|
return ((QuickMsgScreen*)quick_msg)->getTotalChannelUnread();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-14 23:33:16 +02:00
|
|
|
|
void UITask::onMsgAck(uint32_t ack_crc) {
|
|
|
|
|
|
((QuickMsgScreen*)quick_msg)->markDmDelivered(ack_crc);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void UITask::onChannelRelayed(uint32_t seq) {
|
|
|
|
|
|
((QuickMsgScreen*)quick_msg)->markChannelRelayed(seq);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-26 10:55:24 +02:00
|
|
|
|
void UITask::onRoomLoginResult(const uint8_t* pub_key, bool success, uint8_t permissions) {
|
|
|
|
|
|
((QuickMsgScreen*)quick_msg)->onRoomLoginResult(pub_key, success, permissions);
|
|
|
|
|
|
// Unlike the keypress-driven showAlert() calls elsewhere, this fires from a
|
|
|
|
|
|
// background mesh response with no keypress to schedule a redraw — without
|
|
|
|
|
|
// forcing one, the alert's short expiry can lapse before the next scheduled
|
|
|
|
|
|
// refresh ever draws it.
|
|
|
|
|
|
_next_refresh = 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-14 23:33:16 +02:00
|
|
|
|
void UITask::addDMMsg(const uint8_t* pub_key, bool outgoing, const char* text, uint32_t sender_timestamp) {
|
|
|
|
|
|
((QuickMsgScreen*)quick_msg)->addDMMsg(pub_key, outgoing, text, sender_timestamp);
|
2026-05-13 10:25:55 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
int UITask::getDMUnreadTotal() const {
|
|
|
|
|
|
int total = 0;
|
|
|
|
|
|
for (int i = 0; i < DM_UNREAD_TABLE_SIZE; i++)
|
|
|
|
|
|
total += _dm_unread_table[i].count;
|
|
|
|
|
|
return total;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-08-08 20:01:31 +10:00
|
|
|
|
void UITask::showAlert(const char* text, int duration_millis) {
|
2026-05-12 08:19:27 +02:00
|
|
|
|
snprintf(_alert, sizeof(_alert), "%s", text);
|
2025-08-08 20:01:31 +10:00
|
|
|
|
_alert_expiry = millis() + duration_millis;
|
2025-05-20 11:52:55 +10:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-17 08:53:50 +08:00
|
|
|
|
void UITask::notify(UIEventType t) {
|
2025-05-20 11:52:55 +10:00
|
|
|
|
#if defined(PIN_BUZZER)
|
2026-06-08 21:51:03 +02:00
|
|
|
|
{
|
|
|
|
|
|
SoundNotifier sn(buzzer, _node_prefs, _notif_mel_buf, sizeof(_notif_mel_buf));
|
|
|
|
|
|
switch(t){
|
|
|
|
|
|
case UIEventType::contactMessage:
|
|
|
|
|
|
sn.playDM(_last_notif_dm_valid, _last_notif_dm_prefix);
|
2026-05-13 10:48:42 +02:00
|
|
|
|
_last_notif_dm_valid = false;
|
2025-05-20 19:09:49 +12:00
|
|
|
|
break;
|
2026-06-08 21:51:03 +02:00
|
|
|
|
case UIEventType::channelMessage:
|
|
|
|
|
|
sn.playCH(_last_notif_ch_idx);
|
2026-05-15 11:43:38 +02:00
|
|
|
|
_last_notif_ch_idx = -1;
|
2025-05-23 17:58:13 +12:00
|
|
|
|
break;
|
feat(companion): live location sharing, Locator geofencing, trail auto-pause
Squash merge of feat/location-beacon-alerts-autopause (v1.21).
Features:
- Live Location Sharing — broadcast position as movement-gated [LOC]
messages to a channel or contact; live shares show as map pins with
distance/bearing in Nearby Nodes and a status-bar indicator. [LOC] is
parsed in DMs, channel messages and room messages; DM shares name the
sender.
- Locator (geofence) — arm a geofence around a saved waypoint or a person
(live [LOC] or last-known position), alert on arrive/leave or near/far,
with an optional homing beeper (gated to arrive/both modes). Arm from
Tools > Locator, Nearby Nodes, or Waypoints; target picker lists
favourites first, clearable via a "None" entry. Active target is drawn
as a flag on the map.
- One active target shared across Locator / Navigate / Map via a single
resolver (resolvePersonPos / activeTargetPos) that prefers a live
[LOC] share over the last-advertised GPS fix.
- Follow live contacts — Navigate to a live-sharing contact follows them
as they move and adds an ETA line; quick-share your own position from
the Map.
- Map & status-bar upgrades — home mini-map gets a north marker, scale
tick, and a connected trail line (was disconnected dots); status line
shows tracked-node count, an arrow + distance to the active Locator/Nav
target (falling back to the nearest live-tracked contact); GPS fix icon
in the top status bar, shown only on GPS boards with GPS enabled.
- Trail auto-pause — recording freezes on stops (banking elapsed time,
breaking the map line across the idle gap) and resumes on movement
without ending the session.
- Streaming trail simplification — GPS points are simplified in-stream
via a fixed-corridor (Reumann-Witkam) pass tuned for fidelity: straight
runs collapse to their endpoints, curves stay bounded to the Min-dist
tolerance, so the 512-point buffer covers a far longer route than a
flat point budget would suggest.
- Collapsible Tools (Location / Comms / System sections, fold-in-place
like Settings) and page-indicator icons on the home carousel.
- Waypoint coordinate editor — add a waypoint by scroll-editing lat/lon
digit by digit.
Fixes:
- Critical: low-heap hang and contact loss on RAM-tight builds. Halved
message-history scrollback rings (recovering ~14 KB free heap) and
made contacts/channels/prefs persistence atomic (temp file + rename),
so an interrupted save can no longer corrupt or wipe the store.
- Serial.write() bounded so a stalled USB host can't hang the device.
- Nearby Nodes: live [LOC] senders respect the type filter, sort by
shared position, and the list refreshes so live shares bubble up.
- Map: live contacts are labelled before waypoints.
- GPS status icon hidden when GPS is off in Settings.
- Splash screen no longer truncates a pre-release tag's own dash (e.g.
v1.21-rc1) when stripping the build's commit-hash suffix.
- Null-guarded the Locator target picker; clamped loc-share channel
index on load.
Under the hood:
- -Os size optimisation on the e-ink and GAT562 30S solo envs.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 19:04:21 +02:00
|
|
|
|
case UIEventType::roomMessage:
|
|
|
|
|
|
// Rooms have many authors and no per-room melody pref, so use the default DM
|
|
|
|
|
|
// notification (no per-sender melody/mute lookup — the author varies per post).
|
|
|
|
|
|
sn.playDM(false, nullptr);
|
|
|
|
|
|
break;
|
2026-06-07 10:53:17 +02:00
|
|
|
|
case UIEventType::advertReceivedFlood:
|
2026-06-08 21:51:03 +02:00
|
|
|
|
case UIEventType::advertReceivedZeroHop:
|
|
|
|
|
|
sn.playAD(t == UIEventType::advertReceivedFlood);
|
2026-06-04 08:46:15 +02:00
|
|
|
|
break;
|
2025-05-30 22:55:53 -07:00
|
|
|
|
case UIEventType::ack:
|
2025-05-30 22:58:30 -07:00
|
|
|
|
buzzer.play("ack:d=32,o=8,b=120:c");
|
2025-05-30 22:55:53 -07:00
|
|
|
|
break;
|
2025-05-20 19:33:21 +12:00
|
|
|
|
case UIEventType::none:
|
2025-05-20 19:09:49 +12:00
|
|
|
|
default:
|
|
|
|
|
|
break;
|
2026-06-08 21:51:03 +02:00
|
|
|
|
}
|
2025-05-20 19:09:49 +12:00
|
|
|
|
}
|
2025-05-20 11:52:55 +10:00
|
|
|
|
#endif
|
2025-03-10 17:11:55 +01:00
|
|
|
|
|
2025-09-07 15:16:15 +08:00
|
|
|
|
#ifdef PIN_VIBRATION
|
2025-09-17 08:53:50 +08:00
|
|
|
|
// Trigger vibration for all UI events except none
|
|
|
|
|
|
if (t != UIEventType::none) {
|
|
|
|
|
|
vibration.trigger();
|
|
|
|
|
|
}
|
2025-09-07 15:16:15 +08:00
|
|
|
|
#endif
|
2025-03-10 17:11:55 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-07 15:16:15 +08:00
|
|
|
|
|
2025-03-10 17:11:55 +01:00
|
|
|
|
void UITask::msgRead(int msgcount) {
|
|
|
|
|
|
_msgcount = msgcount;
|
|
|
|
|
|
if (msgcount == 0) {
|
2026-05-11 19:27:16 +02:00
|
|
|
|
_room_unread = 0;
|
2026-05-29 11:12:30 +02:00
|
|
|
|
memset(_dm_unread_table, 0, sizeof(_dm_unread_table));
|
2026-05-13 10:36:37 +02:00
|
|
|
|
((QuickMsgScreen*)quick_msg)->clearAllChannelUnread();
|
2025-03-10 17:11:55 +01:00
|
|
|
|
}
|
2025-03-04 23:09:43 +11:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-13 10:25:55 +02:00
|
|
|
|
void UITask::newMsg(uint8_t path_len, const char* from_name, const char* text, int msgcount, uint8_t contact_type, const uint8_t* pub_key) {
|
2025-03-10 17:11:55 +01:00
|
|
|
|
_msgcount = msgcount;
|
2026-05-11 19:34:07 +02:00
|
|
|
|
if (contact_type == ADV_TYPE_ROOM && _room_unread < _msgcount) _room_unread++;
|
2026-05-13 10:25:55 +02:00
|
|
|
|
if (contact_type == ADV_TYPE_CHAT && pub_key != nullptr) {
|
2026-05-13 10:48:42 +02:00
|
|
|
|
memcpy(_last_notif_dm_prefix, pub_key, 4);
|
|
|
|
|
|
_last_notif_dm_valid = true;
|
2026-05-13 10:25:55 +02:00
|
|
|
|
int slot = -1, empty_slot = -1;
|
|
|
|
|
|
for (int i = 0; i < DM_UNREAD_TABLE_SIZE; i++) {
|
|
|
|
|
|
if (_dm_unread_table[i].count > 0 && memcmp(_dm_unread_table[i].prefix, pub_key, 4) == 0) { slot = i; break; }
|
|
|
|
|
|
if (empty_slot < 0 && _dm_unread_table[i].count == 0) empty_slot = i;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (slot >= 0) {
|
|
|
|
|
|
if (_dm_unread_table[slot].count < 99) _dm_unread_table[slot].count++;
|
|
|
|
|
|
} else if (empty_slot >= 0) {
|
|
|
|
|
|
memcpy(_dm_unread_table[empty_slot].prefix, pub_key, 4);
|
|
|
|
|
|
_dm_unread_table[empty_slot].count = 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-03-10 17:11:55 +01:00
|
|
|
|
|
2026-05-11 17:19:03 +02:00
|
|
|
|
char alert_buf[80];
|
|
|
|
|
|
snprintf(alert_buf, sizeof(alert_buf), "Msg: %.20s", from_name);
|
|
|
|
|
|
showAlert(alert_buf, 3000);
|
2025-03-04 23:09:43 +11:00
|
|
|
|
|
2026-05-17 09:55:51 +02:00
|
|
|
|
if (_display != NULL && !_locked) {
|
2026-06-05 12:42:14 +02:00
|
|
|
|
if (!_display->isOn() && !isClientConnected()) { // wake for the msg unless an app (BLE/USB) is already showing it
|
2025-11-03 21:25:31 +01:00
|
|
|
|
_display->turnOn();
|
|
|
|
|
|
}
|
|
|
|
|
|
if (_display->isOn()) {
|
2026-05-11 17:19:03 +02:00
|
|
|
|
uint32_t aoff = autoOffMillis();
|
|
|
|
|
|
if (aoff > 0) _auto_off = millis() + aoff;
|
|
|
|
|
|
_next_refresh = 100;
|
2025-11-03 21:25:31 +01:00
|
|
|
|
}
|
2025-03-10 17:11:55 +01:00
|
|
|
|
}
|
2025-03-04 23:09:43 +11:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-03-10 17:11:55 +01:00
|
|
|
|
void UITask::userLedHandler() {
|
|
|
|
|
|
#ifdef PIN_STATUS_LED
|
refactor(ui): apply code review fixes — bugs, risk, optimisations, cleanup
Fixes 7 bugs (timing UB on unsigned long, abs() no-op, XBM CRC PROGMEM
scan, CayenneLPP heap thrash ×2, UTF-8 reply prefix, lock-seq reset on
wake), 3 risk items (notif melody stop guard, bot trigger pre-lowercase,
lock-seq cleared on display wake), plus optimisations (fmtMsgAge/
getTextWidth caching, RTTTL builder consolidated to NodePrefs, HP_*
constants and homePageLabel table moved to NodePrefs, DataStore flat
lambda schema, translateUTF8Static factored out) and minor cleanup.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-23 20:03:45 +02:00
|
|
|
|
unsigned long cur_time = millis();
|
2025-09-03 17:22:11 +02:00
|
|
|
|
if (cur_time > next_led_change) {
|
|
|
|
|
|
if (led_state == 0) {
|
|
|
|
|
|
led_state = 1;
|
2025-03-10 17:11:55 +01:00
|
|
|
|
if (_msgcount > 0) {
|
2025-09-03 17:22:11 +02:00
|
|
|
|
last_led_increment = LED_ON_MSG_MILLIS;
|
2025-03-10 17:11:55 +01:00
|
|
|
|
} else {
|
2025-09-03 17:22:11 +02:00
|
|
|
|
last_led_increment = LED_ON_MILLIS;
|
2025-03-10 17:11:55 +01:00
|
|
|
|
}
|
2025-09-03 17:22:11 +02:00
|
|
|
|
next_led_change = cur_time + last_led_increment;
|
2025-03-10 17:11:55 +01:00
|
|
|
|
} else {
|
2025-09-03 17:22:11 +02:00
|
|
|
|
led_state = 0;
|
|
|
|
|
|
next_led_change = cur_time + LED_CYCLE_MILLIS - last_led_increment;
|
2025-03-10 17:11:55 +01:00
|
|
|
|
}
|
2025-11-20 10:58:14 +01:00
|
|
|
|
digitalWrite(PIN_STATUS_LED, led_state == LED_STATE_ON);
|
2025-03-10 17:11:55 +01:00
|
|
|
|
}
|
|
|
|
|
|
#endif
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-08-08 20:01:31 +10:00
|
|
|
|
void UITask::setCurrScreen(UIScreen* c) {
|
|
|
|
|
|
curr = c;
|
2025-09-03 17:22:11 +02:00
|
|
|
|
_next_refresh = 100;
|
2025-08-08 20:01:31 +10:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-04 21:45:42 +02:00
|
|
|
|
/*
|
|
|
|
|
|
hardware-agnostic pre-shutdown activity should be done here
|
2025-05-27 11:07:51 +12:00
|
|
|
|
*/
|
|
|
|
|
|
void UITask::shutdown(bool restart){
|
2026-05-10 18:23:25 +02:00
|
|
|
|
the_mesh.saveRTCTime();
|
2025-05-27 11:07:51 +12:00
|
|
|
|
|
|
|
|
|
|
#ifdef PIN_BUZZER
|
|
|
|
|
|
/* note: we have a choice here -
|
|
|
|
|
|
we can do a blocking buzzer.loop() with non-deterministic consequences
|
|
|
|
|
|
or we can set a flag and delay the shutdown for a couple of seconds
|
|
|
|
|
|
while a non-blocking buzzer.loop() plays out in UITask::loop()
|
|
|
|
|
|
*/
|
|
|
|
|
|
buzzer.shutdown();
|
|
|
|
|
|
uint32_t buzzer_timer = millis(); // fail-safe shutdown
|
2026-05-15 15:34:10 +02:00
|
|
|
|
while (buzzer.isPlaying() && (millis() - buzzer_timer) < 2500)
|
2025-05-27 11:07:51 +12:00
|
|
|
|
buzzer.loop();
|
|
|
|
|
|
|
|
|
|
|
|
#endif // PIN_BUZZER
|
|
|
|
|
|
|
2025-08-08 20:01:31 +10:00
|
|
|
|
if (restart) {
|
2025-05-27 11:07:51 +12:00
|
|
|
|
_board->reboot();
|
2025-08-08 20:01:31 +10:00
|
|
|
|
} else {
|
|
|
|
|
|
_display->turnOff();
|
2025-11-22 02:06:44 +01:00
|
|
|
|
radio_driver.powerOff();
|
2026-06-14 13:21:59 +02:00
|
|
|
|
// Power GPS down through its provider before SYSTEMOFF — GPIO pins retain
|
|
|
|
|
|
// state in NRF52 SYSTEMOFF, so otherwise the module keeps draining the
|
|
|
|
|
|
// battery. The provider handles the enable + reset pins and the correct
|
|
|
|
|
|
// active level. gps_enabled is persisted; applyGpsPrefs() restores it on
|
|
|
|
|
|
// the next boot.
|
|
|
|
|
|
if (_sensors) {
|
|
|
|
|
|
LocationProvider* loc = _sensors->getLocationProvider();
|
|
|
|
|
|
if (loc) loc->stop();
|
|
|
|
|
|
}
|
2025-05-27 11:07:51 +12:00
|
|
|
|
_board->powerOff();
|
2025-08-08 20:01:31 +10:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
bool UITask::isButtonPressed() const {
|
|
|
|
|
|
#ifdef PIN_USER_BTN
|
|
|
|
|
|
return user_btn.isPressed();
|
|
|
|
|
|
#else
|
|
|
|
|
|
return false;
|
|
|
|
|
|
#endif
|
2025-05-27 11:07:51 +12:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-17 18:32:49 +02:00
|
|
|
|
static void formatDashVal(uint8_t field, char* val, int val_len, uint16_t batt_mv,
|
2026-05-28 22:57:50 +02:00
|
|
|
|
uint16_t low_batt_mv, CayenneLPP* lpp = nullptr) {
|
2026-05-17 10:04:28 +02:00
|
|
|
|
val[0] = '\0';
|
|
|
|
|
|
switch (field) {
|
|
|
|
|
|
case DASH_NONE: return;
|
2026-05-27 22:55:50 +02:00
|
|
|
|
case DASH_BATT_V:
|
2026-05-17 10:04:28 +02:00
|
|
|
|
if (batt_mv > 0) snprintf(val, val_len, "%u.%02uV", batt_mv/1000, (batt_mv%1000)/10);
|
|
|
|
|
|
else strcpy(val, "--");
|
|
|
|
|
|
return;
|
2026-05-27 22:55:50 +02:00
|
|
|
|
case DASH_BATT_PCT:
|
2026-05-28 22:57:50 +02:00
|
|
|
|
if (batt_mv > 0) snprintf(val, val_len, "%d%%", battMvToPercent(batt_mv, low_batt_mv));
|
|
|
|
|
|
else strcpy(val, "--");
|
2026-05-27 22:55:50 +02:00
|
|
|
|
return;
|
2026-05-17 10:04:28 +02:00
|
|
|
|
case DASH_NODES:
|
|
|
|
|
|
snprintf(val, val_len, "%d nodes", the_mesh.getNumContacts());
|
|
|
|
|
|
return;
|
|
|
|
|
|
#if ENV_INCLUDE_GPS == 1
|
|
|
|
|
|
case DASH_GPS: {
|
|
|
|
|
|
LocationProvider* loc = sensors.getLocationProvider();
|
|
|
|
|
|
if (loc && loc->isValid())
|
|
|
|
|
|
snprintf(val, val_len, "%.2f %.2f",
|
|
|
|
|
|
loc->getLatitude()/1000000.0f, loc->getLongitude()/1000000.0f);
|
|
|
|
|
|
else strcpy(val, "no fix");
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
#endif
|
|
|
|
|
|
default: break;
|
|
|
|
|
|
}
|
2026-05-17 18:32:49 +02:00
|
|
|
|
// LPP sensor fields
|
2026-05-17 10:04:28 +02:00
|
|
|
|
uint8_t lpp_type = 0;
|
|
|
|
|
|
switch (field) {
|
|
|
|
|
|
case DASH_TEMP: lpp_type = LPP_TEMPERATURE; break;
|
|
|
|
|
|
case DASH_HUM: lpp_type = LPP_RELATIVE_HUMIDITY; break;
|
|
|
|
|
|
case DASH_PRES: lpp_type = LPP_BAROMETRIC_PRESSURE; break;
|
|
|
|
|
|
case DASH_ALT: lpp_type = LPP_ALTITUDE; break;
|
|
|
|
|
|
case DASH_LUX: lpp_type = LPP_LUMINOSITY; break;
|
|
|
|
|
|
case DASH_CO2: lpp_type = LPP_CONCENTRATION; break;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (lpp_type) {
|
refactor(ui): apply code review fixes — bugs, risk, optimisations, cleanup
Fixes 7 bugs (timing UB on unsigned long, abs() no-op, XBM CRC PROGMEM
scan, CayenneLPP heap thrash ×2, UTF-8 reply prefix, lock-seq reset on
wake), 3 risk items (notif melody stop guard, bot trigger pre-lowercase,
lock-seq cleared on display wake), plus optimisations (fmtMsgAge/
getTextWidth caching, RTTTL builder consolidated to NodePrefs, HP_*
constants and homePageLabel table moved to NodePrefs, DataStore flat
lambda schema, translateUTF8Static factored out) and minor cleanup.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-23 20:03:45 +02:00
|
|
|
|
if (!lpp) { static CayenneLPP s_lpp(200); s_lpp.reset(); sensors.querySensors(0xFF, s_lpp); lpp = &s_lpp; }
|
2026-05-17 18:32:49 +02:00
|
|
|
|
LPPReader r(lpp->getBuffer(), lpp->getSize());
|
2026-05-17 10:04:28 +02:00
|
|
|
|
uint8_t ch, type;
|
|
|
|
|
|
while (r.readHeader(ch, type)) {
|
|
|
|
|
|
if (type == lpp_type) {
|
|
|
|
|
|
float v;
|
|
|
|
|
|
switch (lpp_type) {
|
|
|
|
|
|
case LPP_TEMPERATURE: r.readTemperature(v); snprintf(val, val_len, "%.1f\xf8""C", v); return;
|
|
|
|
|
|
case LPP_RELATIVE_HUMIDITY: r.readRelativeHumidity(v); snprintf(val, val_len, "%.0f%%", v); return;
|
|
|
|
|
|
case LPP_BAROMETRIC_PRESSURE: r.readPressure(v); snprintf(val, val_len, "%.0fhPa", v); return;
|
|
|
|
|
|
case LPP_ALTITUDE: r.readAltitude(v); snprintf(val, val_len, "%.0fm", v); return;
|
|
|
|
|
|
case LPP_LUMINOSITY: r.readLuminosity(v); snprintf(val, val_len, "%.0flux", v); return;
|
|
|
|
|
|
case LPP_CONCENTRATION: r.readConcentration(v); snprintf(val, val_len, "%.0fppm", v); return;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
r.skipData(type);
|
|
|
|
|
|
}
|
|
|
|
|
|
strcpy(val, "--");
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-03-10 17:11:55 +01:00
|
|
|
|
void UITask::loop() {
|
2025-08-08 20:01:31 +10:00
|
|
|
|
char c = 0;
|
2026-06-14 23:33:16 +02:00
|
|
|
|
// Background delivery: resend pending on-device DMs whose ACK timed out, and
|
|
|
|
|
|
// finalise the ✗ marker — runs regardless of which screen is active.
|
|
|
|
|
|
((QuickMsgScreen*)quick_msg)->tickDmResends();
|
2025-10-16 17:33:22 +11:00
|
|
|
|
#if UI_HAS_JOYSTICK
|
2026-05-23 15:20:40 +02:00
|
|
|
|
uint8_t joy_rot = _node_prefs ? _node_prefs->joystick_rotation : JOYSTICK_ROTATION;
|
2025-08-08 20:01:31 +10:00
|
|
|
|
int ev = user_btn.check();
|
|
|
|
|
|
if (ev == BUTTON_EVENT_CLICK) {
|
2026-05-17 10:09:54 +02:00
|
|
|
|
if (back_btn.isPressed()) {
|
|
|
|
|
|
// Enter clicked while Back is held — lock/unlock sequence
|
2026-05-17 11:17:59 +02:00
|
|
|
|
if (_display && !_display->isOn()) {
|
|
|
|
|
|
_display->turnOn(); // turn on display so hints are visible
|
|
|
|
|
|
}
|
|
|
|
|
|
_lock_wake_until = millis() + 5000; // keep display on during sequence
|
2026-05-17 10:09:54 +02:00
|
|
|
|
if (millis() - _lock_seq_ms > 3000) _lock_seq_count = 0; // timeout reset
|
|
|
|
|
|
_lock_seq_count++;
|
|
|
|
|
|
_lock_seq_ms = millis();
|
2026-05-17 11:17:59 +02:00
|
|
|
|
_next_refresh = 0; // update hint immediately on each press
|
2026-05-17 10:09:54 +02:00
|
|
|
|
if (_lock_seq_count >= 3) {
|
|
|
|
|
|
_lock_seq_count = 0;
|
2026-05-17 11:17:59 +02:00
|
|
|
|
_lock_seq_used = true; // suppress Back release click
|
2026-05-17 10:09:54 +02:00
|
|
|
|
_locked = !_locked;
|
|
|
|
|
|
if (_locked) {
|
|
|
|
|
|
_lock_wake_until = millis() + 2000;
|
|
|
|
|
|
} else {
|
2026-05-17 11:17:59 +02:00
|
|
|
|
if (_display && !_display->isOn()) _display->turnOn();
|
2026-05-17 10:09:54 +02:00
|
|
|
|
uint32_t aoff = autoOffMillis();
|
|
|
|
|
|
if (aoff > 0) _auto_off = millis() + aoff;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
// eat the Enter — don't pass to curr
|
|
|
|
|
|
} else {
|
|
|
|
|
|
c = checkDisplayOn(KEY_ENTER);
|
|
|
|
|
|
}
|
2025-08-08 20:01:31 +10:00
|
|
|
|
} else if (ev == BUTTON_EVENT_LONG_PRESS) {
|
2025-10-16 17:33:22 +11:00
|
|
|
|
c = handleLongPress(KEY_ENTER); // REVISIT: could be mapped to different key code
|
2025-08-08 20:01:31 +10:00
|
|
|
|
}
|
2026-05-10 15:18:35 +02:00
|
|
|
|
#if UI_HAS_JOYSTICK_UPDOWN
|
|
|
|
|
|
ev = joystick_up.check();
|
|
|
|
|
|
if (ev == BUTTON_EVENT_CLICK) {
|
2026-05-23 15:20:40 +02:00
|
|
|
|
c = checkDisplayOn(rotateJoystickKey(KEY_UP, joy_rot));
|
2026-05-10 15:18:35 +02:00
|
|
|
|
}
|
|
|
|
|
|
ev = joystick_down.check();
|
|
|
|
|
|
if (ev == BUTTON_EVENT_CLICK) {
|
2026-05-23 15:20:40 +02:00
|
|
|
|
c = checkDisplayOn(rotateJoystickKey(KEY_DOWN, joy_rot));
|
2026-05-10 15:18:35 +02:00
|
|
|
|
}
|
|
|
|
|
|
#endif
|
2025-08-17 19:01:28 +10:00
|
|
|
|
ev = joystick_left.check();
|
|
|
|
|
|
if (ev == BUTTON_EVENT_CLICK) {
|
2026-05-23 15:20:40 +02:00
|
|
|
|
c = checkDisplayOn(rotateJoystickKey(KEY_LEFT, joy_rot));
|
2025-08-17 19:01:28 +10:00
|
|
|
|
} else if (ev == BUTTON_EVENT_LONG_PRESS) {
|
2026-05-23 15:20:40 +02:00
|
|
|
|
c = handleLongPress(rotateJoystickKey(KEY_LEFT, joy_rot));
|
2025-08-17 19:01:28 +10:00
|
|
|
|
}
|
|
|
|
|
|
ev = joystick_right.check();
|
|
|
|
|
|
if (ev == BUTTON_EVENT_CLICK) {
|
2026-05-23 15:20:40 +02:00
|
|
|
|
c = checkDisplayOn(rotateJoystickKey(KEY_RIGHT, joy_rot));
|
2025-08-17 19:01:28 +10:00
|
|
|
|
} else if (ev == BUTTON_EVENT_LONG_PRESS) {
|
2026-05-23 15:20:40 +02:00
|
|
|
|
c = handleLongPress(rotateJoystickKey(KEY_RIGHT, joy_rot));
|
2025-08-17 19:01:28 +10:00
|
|
|
|
}
|
2026-05-17 12:17:02 +02:00
|
|
|
|
if (_lock_seq_used && millis() - _lock_seq_ms > 5000) {
|
|
|
|
|
|
_lock_seq_used = false; // safety reset if Back release event was missed
|
|
|
|
|
|
}
|
2025-10-18 13:37:18 +02:00
|
|
|
|
ev = back_btn.check();
|
2026-05-10 15:18:35 +02:00
|
|
|
|
if (ev == BUTTON_EVENT_CLICK) {
|
2026-05-17 11:17:59 +02:00
|
|
|
|
if (_lock_seq_count > 0 || _lock_seq_used) {
|
|
|
|
|
|
// Back released mid-sequence or after completing it — cancel/suppress
|
2026-05-17 09:55:51 +02:00
|
|
|
|
_lock_seq_count = 0;
|
2026-05-17 11:17:59 +02:00
|
|
|
|
_lock_seq_used = false;
|
2026-05-17 10:09:54 +02:00
|
|
|
|
} else {
|
|
|
|
|
|
c = checkDisplayOn(KEY_CANCEL);
|
2026-05-17 09:55:51 +02:00
|
|
|
|
}
|
2026-05-10 15:18:35 +02:00
|
|
|
|
} else if (ev == BUTTON_EVENT_TRIPLE_CLICK) {
|
2026-05-17 11:17:59 +02:00
|
|
|
|
if (!_locked) c = handleTripleClick(KEY_SELECT);
|
2025-10-18 13:37:18 +02:00
|
|
|
|
}
|
2025-10-16 17:33:22 +11:00
|
|
|
|
#elif defined(PIN_USER_BTN)
|
|
|
|
|
|
int ev = user_btn.check();
|
|
|
|
|
|
if (ev == BUTTON_EVENT_CLICK) {
|
|
|
|
|
|
c = checkDisplayOn(KEY_NEXT);
|
|
|
|
|
|
} else if (ev == BUTTON_EVENT_LONG_PRESS) {
|
|
|
|
|
|
c = handleLongPress(KEY_ENTER);
|
|
|
|
|
|
} else if (ev == BUTTON_EVENT_DOUBLE_CLICK) {
|
|
|
|
|
|
c = handleDoubleClick(KEY_PREV);
|
|
|
|
|
|
} else if (ev == BUTTON_EVENT_TRIPLE_CLICK) {
|
|
|
|
|
|
c = handleTripleClick(KEY_SELECT);
|
|
|
|
|
|
}
|
2025-08-17 19:01:28 +10:00
|
|
|
|
#endif
|
2025-09-01 17:11:55 +10:00
|
|
|
|
#if defined(PIN_USER_BTN_ANA)
|
refactor(ui): apply code review fixes — bugs, risk, optimisations, cleanup
Fixes 7 bugs (timing UB on unsigned long, abs() no-op, XBM CRC PROGMEM
scan, CayenneLPP heap thrash ×2, UTF-8 reply prefix, lock-seq reset on
wake), 3 risk items (notif melody stop guard, bot trigger pre-lowercase,
lock-seq cleared on display wake), plus optimisations (fmtMsgAge/
getTextWidth caching, RTTTL builder consolidated to NodePrefs, HP_*
constants and homePageLabel table moved to NodePrefs, DataStore flat
lambda schema, translateUTF8Static factored out) and minor cleanup.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-23 20:03:45 +02:00
|
|
|
|
if (millis() - _analogue_pin_read_millis > 10) {
|
2026-04-28 21:26:56 -07:00
|
|
|
|
int ev = analog_btn.check();
|
2025-10-31 13:04:59 +00:00
|
|
|
|
if (ev == BUTTON_EVENT_CLICK) {
|
|
|
|
|
|
c = checkDisplayOn(KEY_NEXT);
|
|
|
|
|
|
} else if (ev == BUTTON_EVENT_LONG_PRESS) {
|
|
|
|
|
|
c = handleLongPress(KEY_ENTER);
|
|
|
|
|
|
} else if (ev == BUTTON_EVENT_DOUBLE_CLICK) {
|
|
|
|
|
|
c = handleDoubleClick(KEY_PREV);
|
|
|
|
|
|
} else if (ev == BUTTON_EVENT_TRIPLE_CLICK) {
|
|
|
|
|
|
c = handleTripleClick(KEY_SELECT);
|
|
|
|
|
|
}
|
|
|
|
|
|
_analogue_pin_read_millis = millis();
|
2025-09-01 17:11:55 +10:00
|
|
|
|
}
|
|
|
|
|
|
#endif
|
2025-11-28 10:33:19 +01:00
|
|
|
|
#if defined(BACKLIGHT_BTN)
|
2025-09-03 17:22:11 +02:00
|
|
|
|
if (millis() > next_backlight_btn_check) {
|
2025-09-02 11:43:48 +02:00
|
|
|
|
bool touch_state = digitalRead(PIN_BUTTON2);
|
2025-11-28 10:33:19 +01:00
|
|
|
|
#if defined(DISP_BACKLIGHT)
|
2025-09-02 11:43:48 +02:00
|
|
|
|
digitalWrite(DISP_BACKLIGHT, !touch_state);
|
2025-11-28 10:33:19 +01:00
|
|
|
|
#elif defined(EXP_PIN_BACKLIGHT)
|
|
|
|
|
|
expander.digitalWrite(EXP_PIN_BACKLIGHT, !touch_state);
|
|
|
|
|
|
#endif
|
2025-09-03 17:22:11 +02:00
|
|
|
|
next_backlight_btn_check = millis() + 300;
|
2025-09-02 11:43:48 +02:00
|
|
|
|
}
|
|
|
|
|
|
#endif
|
2025-08-08 20:01:31 +10:00
|
|
|
|
|
2026-05-17 09:55:51 +02:00
|
|
|
|
if (c != 0) {
|
2026-05-17 10:09:54 +02:00
|
|
|
|
if (!_locked && curr) {
|
2026-05-17 09:55:51 +02:00
|
|
|
|
curr->handleInput(c);
|
|
|
|
|
|
{ uint32_t aoff = autoOffMillis(); if (aoff > 0) _auto_off = millis() + aoff; } // extend auto-off timer
|
2026-05-27 17:33:41 +02:00
|
|
|
|
#ifdef PIN_BUZZER
|
|
|
|
|
|
if (buzzer.isPlaying()) {
|
|
|
|
|
|
// Keep the next render at least 300 ms away so the blocking e-ink endFrame()
|
|
|
|
|
|
// doesn't extend the current note. 300 ms covers the slowest note at 120 BPM (1/4).
|
|
|
|
|
|
unsigned long deadline = millis() + 300;
|
|
|
|
|
|
if (_next_refresh < deadline) _next_refresh = deadline;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
_next_refresh = 100; // trigger refresh immediately
|
|
|
|
|
|
}
|
|
|
|
|
|
#else
|
2026-05-17 09:55:51 +02:00
|
|
|
|
_next_refresh = 100; // trigger refresh
|
2026-05-27 17:33:41 +02:00
|
|
|
|
#endif
|
2026-05-17 09:55:51 +02:00
|
|
|
|
} else if (_locked) {
|
2026-05-17 14:30:53 +02:00
|
|
|
|
// Locked: eat all keys — wake window is set only when display first turns on
|
2026-05-17 09:55:51 +02:00
|
|
|
|
_next_refresh = 0;
|
|
|
|
|
|
}
|
2025-08-08 20:01:31 +10:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-03-10 17:11:55 +01:00
|
|
|
|
userLedHandler();
|
2025-03-04 23:09:43 +11:00
|
|
|
|
|
2025-05-20 11:52:55 +10:00
|
|
|
|
#ifdef PIN_BUZZER
|
2026-05-12 20:12:54 +02:00
|
|
|
|
if (_node_prefs && _node_prefs->buzzer_auto) {
|
2026-06-05 12:42:14 +02:00
|
|
|
|
bool should_quiet = isClientConnected(); // BLE bonded or an open USB port
|
2026-05-12 20:12:54 +02:00
|
|
|
|
if (buzzer.isQuiet() != should_quiet) {
|
|
|
|
|
|
buzzer.quiet(should_quiet);
|
|
|
|
|
|
_next_refresh = 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-05-20 11:52:55 +10:00
|
|
|
|
if (buzzer.isPlaying()) buzzer.loop();
|
|
|
|
|
|
#endif
|
|
|
|
|
|
|
2025-08-08 20:01:31 +10:00
|
|
|
|
if (curr) curr->poll();
|
|
|
|
|
|
|
2025-03-10 17:11:55 +01:00
|
|
|
|
if (_display != NULL && _display->isOn()) {
|
2026-05-17 09:55:51 +02:00
|
|
|
|
if (_locked && millis() > _lock_wake_until) {
|
|
|
|
|
|
_display->turnOff();
|
|
|
|
|
|
} else if (_locked && millis() >= _next_refresh) {
|
|
|
|
|
|
_display->startFrame();
|
|
|
|
|
|
// Lock screen: clock + unlock hint popup
|
|
|
|
|
|
uint32_t unix_ts = rtc_clock.getCurrentTime();
|
|
|
|
|
|
_display->setColor(DisplayDriver::LIGHT);
|
refactor: replace all hardcoded pixel positions with layout helpers
Add lineStep(), headerH(), listStart(), listVisible(), valCol() to
DisplayDriver so layout derives from getLineHeight() instead of fixed
OLED constants. Refactor all screens (SettingsScreen, NearbyScreen,
FullscreenMsgView, QuickMsgScreen, BotScreen, DashboardConfigScreen,
AutoAdvertScreen, RingtoneEditorScreen) and all HomeScreen pages plus
the lock screen and splash screen in UITask.cpp.
On OLED (lh=8) positions are unchanged; on landscape e-ink (lh=16) all
text and widgets now scale correctly to the 250×122 display.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 18:05:09 +02:00
|
|
|
|
_display->setTextSize(1);
|
|
|
|
|
|
const int lk_lh = _display->getLineHeight();
|
|
|
|
|
|
const int lk_step = _display->lineStep();
|
2026-05-17 09:55:51 +02:00
|
|
|
|
if (unix_ts < 1000000000UL) {
|
refactor: replace all hardcoded pixel positions with layout helpers
Add lineStep(), headerH(), listStart(), listVisible(), valCol() to
DisplayDriver so layout derives from getLineHeight() instead of fixed
OLED constants. Refactor all screens (SettingsScreen, NearbyScreen,
FullscreenMsgView, QuickMsgScreen, BotScreen, DashboardConfigScreen,
AutoAdvertScreen, RingtoneEditorScreen) and all HomeScreen pages plus
the lock screen and splash screen in UITask.cpp.
On OLED (lh=8) positions are unchanged; on landscape e-ink (lh=16) all
text and widgets now scale correctly to the 250×122 display.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 18:05:09 +02:00
|
|
|
|
_display->drawTextCentered(_display->width() / 2, _display->height() / 2 - lk_step, "No time sync");
|
2026-05-17 09:55:51 +02:00
|
|
|
|
} else {
|
|
|
|
|
|
int8_t tz = _node_prefs ? _node_prefs->tz_offset_hours : 0;
|
|
|
|
|
|
unix_ts += (int32_t)tz * 3600;
|
|
|
|
|
|
time_t t = (time_t)unix_ts;
|
|
|
|
|
|
struct tm* ti = gmtime(&t);
|
|
|
|
|
|
char buf[12];
|
refactor: replace all hardcoded pixel positions with layout helpers
Add lineStep(), headerH(), listStart(), listVisible(), valCol() to
DisplayDriver so layout derives from getLineHeight() instead of fixed
OLED constants. Refactor all screens (SettingsScreen, NearbyScreen,
FullscreenMsgView, QuickMsgScreen, BotScreen, DashboardConfigScreen,
AutoAdvertScreen, RingtoneEditorScreen) and all HomeScreen pages plus
the lock screen and splash screen in UITask.cpp.
On OLED (lh=8) positions are unchanged; on landscape e-ink (lh=16) all
text and widgets now scale correctly to the 250×122 display.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 18:05:09 +02:00
|
|
|
|
const int clk_y = 2;
|
2026-06-02 23:32:27 +02:00
|
|
|
|
bool h12 = _node_prefs && _node_prefs->clock_12h;
|
|
|
|
|
|
int date_y = drawClockTime(*_display, clk_y, ti, h12, /*show_sec*/false);
|
2026-05-17 09:55:51 +02:00
|
|
|
|
_display->setTextSize(1);
|
|
|
|
|
|
static const char* wd[] = {"Sun","Mon","Tue","Wed","Thu","Fri","Sat"};
|
|
|
|
|
|
static const char* mo[] = {"Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"};
|
2026-05-24 20:35:49 +02:00
|
|
|
|
snprintf(buf, sizeof(buf),"%s %d %s", wd[ti->tm_wday], ti->tm_mday, mo[ti->tm_mon]);
|
refactor: replace all hardcoded pixel positions with layout helpers
Add lineStep(), headerH(), listStart(), listVisible(), valCol() to
DisplayDriver so layout derives from getLineHeight() instead of fixed
OLED constants. Refactor all screens (SettingsScreen, NearbyScreen,
FullscreenMsgView, QuickMsgScreen, BotScreen, DashboardConfigScreen,
AutoAdvertScreen, RingtoneEditorScreen) and all HomeScreen pages plus
the lock screen and splash screen in UITask.cpp.
On OLED (lh=8) positions are unchanged; on landscape e-ink (lh=16) all
text and widgets now scale correctly to the 250×122 display.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 18:05:09 +02:00
|
|
|
|
_display->drawTextCentered(_display->width() / 2, date_y, buf);
|
2026-05-17 10:04:28 +02:00
|
|
|
|
|
|
|
|
|
|
// Two sensor values side by side (dashboard_fields[0] and [1])
|
|
|
|
|
|
if (_node_prefs) {
|
|
|
|
|
|
char v0[20] = "", v1[20] = "";
|
2026-05-17 18:32:49 +02:00
|
|
|
|
CayenneLPP* lpp_ptr = nullptr;
|
|
|
|
|
|
uint8_t f0 = _node_prefs->dashboard_fields[0], f1 = _node_prefs->dashboard_fields[1];
|
|
|
|
|
|
auto isLPP = [](uint8_t f) {
|
|
|
|
|
|
return f==DASH_TEMP||f==DASH_HUM||f==DASH_PRES||f==DASH_ALT||f==DASH_LUX||f==DASH_CO2;
|
|
|
|
|
|
};
|
|
|
|
|
|
if (isLPP(f0) || isLPP(f1)) {
|
refactor(ui): apply code review fixes — bugs, risk, optimisations, cleanup
Fixes 7 bugs (timing UB on unsigned long, abs() no-op, XBM CRC PROGMEM
scan, CayenneLPP heap thrash ×2, UTF-8 reply prefix, lock-seq reset on
wake), 3 risk items (notif melody stop guard, bot trigger pre-lowercase,
lock-seq cleared on display wake), plus optimisations (fmtMsgAge/
getTextWidth caching, RTTTL builder consolidated to NodePrefs, HP_*
constants and homePageLabel table moved to NodePrefs, DataStore flat
lambda schema, translateUTF8Static factored out) and minor cleanup.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-23 20:03:45 +02:00
|
|
|
|
_dash_lpp.reset(); sensors.querySensors(0xFF, _dash_lpp); lpp_ptr = &_dash_lpp;
|
2026-05-17 18:32:49 +02:00
|
|
|
|
}
|
2026-05-28 22:57:50 +02:00
|
|
|
|
formatDashVal(f0, v0, sizeof(v0), _batt_mv, _node_prefs->low_batt_mv, lpp_ptr);
|
|
|
|
|
|
formatDashVal(f1, v1, sizeof(v1), _batt_mv, _node_prefs->low_batt_mv, lpp_ptr);
|
2026-05-17 10:04:28 +02:00
|
|
|
|
if (v0[0] || v1[0]) {
|
refactor: replace all hardcoded pixel positions with layout helpers
Add lineStep(), headerH(), listStart(), listVisible(), valCol() to
DisplayDriver so layout derives from getLineHeight() instead of fixed
OLED constants. Refactor all screens (SettingsScreen, NearbyScreen,
FullscreenMsgView, QuickMsgScreen, BotScreen, DashboardConfigScreen,
AutoAdvertScreen, RingtoneEditorScreen) and all HomeScreen pages plus
the lock screen and splash screen in UITask.cpp.
On OLED (lh=8) positions are unchanged; on landscape e-ink (lh=16) all
text and widgets now scale correctly to the 250×122 display.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 18:05:09 +02:00
|
|
|
|
int sv_y = date_y + lk_step;
|
2026-05-17 10:04:28 +02:00
|
|
|
|
_display->setColor(DisplayDriver::LIGHT);
|
|
|
|
|
|
if (v0[0] && v1[0]) {
|
refactor: replace all hardcoded pixel positions with layout helpers
Add lineStep(), headerH(), listStart(), listVisible(), valCol() to
DisplayDriver so layout derives from getLineHeight() instead of fixed
OLED constants. Refactor all screens (SettingsScreen, NearbyScreen,
FullscreenMsgView, QuickMsgScreen, BotScreen, DashboardConfigScreen,
AutoAdvertScreen, RingtoneEditorScreen) and all HomeScreen pages plus
the lock screen and splash screen in UITask.cpp.
On OLED (lh=8) positions are unchanged; on landscape e-ink (lh=16) all
text and widgets now scale correctly to the 250×122 display.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 18:05:09 +02:00
|
|
|
|
_display->setCursor(0, sv_y);
|
2026-05-17 10:04:28 +02:00
|
|
|
|
_display->print(v0);
|
|
|
|
|
|
int vw = _display->getTextWidth(v1);
|
refactor: replace all hardcoded pixel positions with layout helpers
Add lineStep(), headerH(), listStart(), listVisible(), valCol() to
DisplayDriver so layout derives from getLineHeight() instead of fixed
OLED constants. Refactor all screens (SettingsScreen, NearbyScreen,
FullscreenMsgView, QuickMsgScreen, BotScreen, DashboardConfigScreen,
AutoAdvertScreen, RingtoneEditorScreen) and all HomeScreen pages plus
the lock screen and splash screen in UITask.cpp.
On OLED (lh=8) positions are unchanged; on landscape e-ink (lh=16) all
text and widgets now scale correctly to the 250×122 display.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 18:05:09 +02:00
|
|
|
|
_display->setCursor(_display->width() - vw, sv_y);
|
2026-05-17 10:04:28 +02:00
|
|
|
|
_display->print(v1);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
const char* sv = v0[0] ? v0 : v1;
|
refactor: replace all hardcoded pixel positions with layout helpers
Add lineStep(), headerH(), listStart(), listVisible(), valCol() to
DisplayDriver so layout derives from getLineHeight() instead of fixed
OLED constants. Refactor all screens (SettingsScreen, NearbyScreen,
FullscreenMsgView, QuickMsgScreen, BotScreen, DashboardConfigScreen,
AutoAdvertScreen, RingtoneEditorScreen) and all HomeScreen pages plus
the lock screen and splash screen in UITask.cpp.
On OLED (lh=8) positions are unchanged; on landscape e-ink (lh=16) all
text and widgets now scale correctly to the 250×122 display.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 18:05:09 +02:00
|
|
|
|
_display->drawTextCentered(_display->width() / 2, sv_y, sv);
|
2026-05-17 10:04:28 +02:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-05-17 09:55:51 +02:00
|
|
|
|
}
|
|
|
|
|
|
// Hint popup at bottom (like alert style)
|
|
|
|
|
|
_display->setTextSize(1);
|
2026-05-17 10:09:54 +02:00
|
|
|
|
const char* hint = _lock_seq_count == 0 ? "Hold Back + 3xEnter" :
|
|
|
|
|
|
_lock_seq_count == 1 ? "Enter x2 more..." : "Enter x1 more...";
|
2026-05-17 09:55:51 +02:00
|
|
|
|
int p = 3;
|
refactor: replace all hardcoded pixel positions with layout helpers
Add lineStep(), headerH(), listStart(), listVisible(), valCol() to
DisplayDriver so layout derives from getLineHeight() instead of fixed
OLED constants. Refactor all screens (SettingsScreen, NearbyScreen,
FullscreenMsgView, QuickMsgScreen, BotScreen, DashboardConfigScreen,
AutoAdvertScreen, RingtoneEditorScreen) and all HomeScreen pages plus
the lock screen and splash screen in UITask.cpp.
On OLED (lh=8) positions are unchanged; on landscape e-ink (lh=16) all
text and widgets now scale correctly to the 250×122 display.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 18:05:09 +02:00
|
|
|
|
int hy = _display->height() - lk_lh - p * 2;
|
2026-05-17 09:55:51 +02:00
|
|
|
|
int hw = _display->getTextWidth(hint);
|
|
|
|
|
|
int hx = (_display->width() - hw) / 2;
|
|
|
|
|
|
_display->setColor(DisplayDriver::LIGHT);
|
refactor: replace all hardcoded pixel positions with layout helpers
Add lineStep(), headerH(), listStart(), listVisible(), valCol() to
DisplayDriver so layout derives from getLineHeight() instead of fixed
OLED constants. Refactor all screens (SettingsScreen, NearbyScreen,
FullscreenMsgView, QuickMsgScreen, BotScreen, DashboardConfigScreen,
AutoAdvertScreen, RingtoneEditorScreen) and all HomeScreen pages plus
the lock screen and splash screen in UITask.cpp.
On OLED (lh=8) positions are unchanged; on landscape e-ink (lh=16) all
text and widgets now scale correctly to the 250×122 display.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 18:05:09 +02:00
|
|
|
|
_display->fillRect(hx - p, hy - p, hw + p*2, lk_lh + p*2);
|
2026-05-17 09:55:51 +02:00
|
|
|
|
_display->setColor(DisplayDriver::DARK);
|
|
|
|
|
|
_display->setCursor(hx, hy);
|
|
|
|
|
|
_display->print(hint);
|
|
|
|
|
|
_display->endFrame();
|
2026-05-24 20:41:25 +02:00
|
|
|
|
_next_refresh = millis() + Features::LOCKSCREEN_REFRESH_MS;
|
2026-05-17 09:55:51 +02:00
|
|
|
|
} else if (!_locked && millis() >= _next_refresh && curr) {
|
2025-03-04 23:09:43 +11:00
|
|
|
|
_display->startFrame();
|
2025-08-08 20:01:31 +10:00
|
|
|
|
int delay_millis = curr->render(*_display);
|
2026-05-26 08:15:53 +02:00
|
|
|
|
if (millis() < _alert_expiry) { // alert overlay on top of any screen
|
2025-08-08 20:01:31 +10:00
|
|
|
|
_display->setTextSize(1);
|
2026-05-22 23:13:27 +02:00
|
|
|
|
int lh = _display->getLineHeight();
|
|
|
|
|
|
int pad = 3;
|
|
|
|
|
|
int box_h = lh + pad * 2;
|
|
|
|
|
|
int box_w = _display->width() - 8;
|
|
|
|
|
|
int box_x = 4;
|
|
|
|
|
|
int box_y = (_display->height() - box_h) / 2;
|
2025-08-08 20:01:31 +10:00
|
|
|
|
_display->setColor(DisplayDriver::DARK);
|
2026-05-22 23:13:27 +02:00
|
|
|
|
_display->fillRect(box_x, box_y, box_w, box_h);
|
|
|
|
|
|
_display->setColor(DisplayDriver::LIGHT);
|
|
|
|
|
|
_display->drawRect(box_x, box_y, box_w, box_h);
|
|
|
|
|
|
_display->drawTextCentered(_display->width() / 2, box_y + pad, _alert);
|
2026-06-15 21:23:00 +02:00
|
|
|
|
// Keep refreshing the underlying screen at its own cadence (capped at the
|
|
|
|
|
|
// alert's expiry) so layouts that settle over a frame — e.g. the message-
|
|
|
|
|
|
// history scrollbar reserve — don't stay stuck behind the alert. Unchanged
|
|
|
|
|
|
// frames are skipped by the display CRC, so e-ink isn't thrashed.
|
|
|
|
|
|
_next_refresh = millis() + delay_millis;
|
|
|
|
|
|
if (_next_refresh > _alert_expiry) _next_refresh = _alert_expiry;
|
2025-08-08 20:01:31 +10:00
|
|
|
|
} else {
|
|
|
|
|
|
_next_refresh = millis() + delay_millis;
|
|
|
|
|
|
}
|
2025-03-04 23:09:43 +11:00
|
|
|
|
_display->endFrame();
|
|
|
|
|
|
}
|
2025-09-03 18:17:37 +02:00
|
|
|
|
#if AUTO_OFF_MILLIS > 0
|
2026-05-28 20:02:50 +01:00
|
|
|
|
#ifdef KEEP_DISPLAY_ON_USB
|
|
|
|
|
|
// Opt-in: refresh the auto-off deadline while externally powered, so the
|
|
|
|
|
|
// timer counts from the moment external power is removed. Off by default
|
|
|
|
|
|
// because OLED panels burn in quickly; only enable for LCD targets or
|
|
|
|
|
|
// where the display is replaceable.
|
|
|
|
|
|
if (board.isExternalPowered()) {
|
|
|
|
|
|
_auto_off = millis() + AUTO_OFF_MILLIS;
|
|
|
|
|
|
}
|
|
|
|
|
|
#endif
|
2026-05-17 09:55:51 +02:00
|
|
|
|
if (!_locked && autoOffMillis() > 0 && millis() > _auto_off) {
|
2025-03-04 23:09:43 +11:00
|
|
|
|
_display->turnOff();
|
2026-05-10 18:01:59 +02:00
|
|
|
|
#ifdef PIN_LED
|
|
|
|
|
|
digitalWrite(PIN_LED, LOW); // turn off status LED with display to save power
|
|
|
|
|
|
#endif
|
2026-05-17 09:55:51 +02:00
|
|
|
|
if (_node_prefs && _node_prefs->auto_lock) {
|
|
|
|
|
|
_locked = true;
|
|
|
|
|
|
_lock_wake_until = 0;
|
|
|
|
|
|
}
|
2025-03-04 23:09:43 +11:00
|
|
|
|
}
|
2025-09-03 18:17:37 +02:00
|
|
|
|
#endif
|
2025-03-04 23:09:43 +11:00
|
|
|
|
}
|
2025-05-27 19:10:56 -07:00
|
|
|
|
|
2025-09-07 15:16:15 +08:00
|
|
|
|
#ifdef PIN_VIBRATION
|
|
|
|
|
|
vibration.loop();
|
|
|
|
|
|
#endif
|
|
|
|
|
|
|
2025-08-08 20:01:31 +10:00
|
|
|
|
if (millis() > next_batt_chck) {
|
2026-05-10 15:18:35 +02:00
|
|
|
|
uint16_t raw = AbstractUITask::getBattMilliVolts();
|
|
|
|
|
|
if (raw > 0) {
|
|
|
|
|
|
// EMA filter: alpha=0.2 (80% old, 20% new) — smooths ADC noise from uneven load
|
|
|
|
|
|
_batt_mv = (_batt_mv == 0) ? raw : (uint16_t)((_batt_mv * 4u + raw) / 5u);
|
|
|
|
|
|
}
|
|
|
|
|
|
uint16_t low_mv = _node_prefs ? _node_prefs->low_batt_mv : 0;
|
Merge upstream/main (v1.16.0) into wio-unified
Merge 254 upstream commits. Highlights relevant to the Wio fork: native
NRF52 companion power-saving (sleep when !hasPendingWork), preamble 16->32
for SF<9, generic sensor-registration model, CMD_SEND_RAW_PACKET, isEink()
display abstraction, KEEP_DISPLAY_ON_USB, contacts-sync/transient fixes.
Conflicts resolved (11 files):
- buzzer.h/.cpp: keep NRF52 include guard + our begin()->startup(); fold in
upstream doc comment.
- platformio (wio + eink): keep our slim env layout, add upstream kiss_modem
env (+ debug_tool=stlink on eink ble).
- GxEPDDisplay.h: take upstream isEink() override.
- EnvironmentSensorManager.h: take upstream's wider #if guard.
- DataStore.cpp: take upstream saveContacts(filter) signature, keep our
::openWrite (member openWrite would shadow the global 2-arg overload).
- main.cpp: keep our watchdog init + add board.onBootComplete(); adopt
upstream's `if(!hasPendingWork()) sleep` loop.
- MyMesh.h: version -> v1.16-solo.0, build date 6 Jun 2026.
- MyMesh.cpp: keep both CMD codes (SCREENSHOT 66 + RAW_PACKET 65), both field
inits, both handlers, our screenshot fns + upstream saveContacts/save_filter,
upstream hasPendingWork() + our MyMeshBot include.
- UITask.cpp: keep our solo splash/clock page/snprintf/buzzer init; merge
auto-off (+ opt-in KEEP_DISPLAY_ON_USB); merge low-batt shutdown (our EMA +
prefs threshold + upstream's !isExternalPowered() guard and eink-aware delay).
Post-merge drift fixes (upstream API/architecture changes, not conflicts):
- applyTxPower(): radio_set_tx_power() removed upstream -> radio_driver.setTxPower().
- Sensor refactor dropped the per-sensor *_initialized flags our
getAvailableLPPTypes() relied on; removed that override and the SENSORS page
now derives available LPP types from the populated sensors_lpp buffer.
README kept ours via .gitattributes merge=ours. Builds verified on all 5 Wio
envs (dual, eink dual, radio_ble, dual_dev, eink_dual_dev). Not yet
hardware-verified.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-06 17:21:41 +02:00
|
|
|
|
// Don't shut down while on external power (charging) — avoids a shutdown loop.
|
|
|
|
|
|
if (low_mv > 0 && _batt_mv > 0 && _batt_mv < low_mv && !board.isExternalPowered()) {
|
2025-08-30 23:09:01 +12:00
|
|
|
|
if (_display != NULL) {
|
|
|
|
|
|
_display->startFrame();
|
2026-05-16 17:47:22 +02:00
|
|
|
|
_display->setTextSize(1);
|
2026-05-10 15:18:35 +02:00
|
|
|
|
_display->setColor(DisplayDriver::LIGHT);
|
refactor: replace all hardcoded pixel positions with layout helpers
Add lineStep(), headerH(), listStart(), listVisible(), valCol() to
DisplayDriver so layout derives from getLineHeight() instead of fixed
OLED constants. Refactor all screens (SettingsScreen, NearbyScreen,
FullscreenMsgView, QuickMsgScreen, BotScreen, DashboardConfigScreen,
AutoAdvertScreen, RingtoneEditorScreen) and all HomeScreen pages plus
the lock screen and splash screen in UITask.cpp.
On OLED (lh=8) positions are unchanged; on landscape e-ink (lh=16) all
text and widgets now scale correctly to the 250×122 display.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 18:05:09 +02:00
|
|
|
|
int mid = _display->height() / 2;
|
|
|
|
|
|
int step = _display->lineStep();
|
|
|
|
|
|
_display->drawTextCentered(_display->width() / 2, mid - step, "Low Battery");
|
|
|
|
|
|
_display->drawTextCentered(_display->width() / 2, mid, "Shutting down");
|
2025-08-30 23:09:01 +12:00
|
|
|
|
_display->endFrame();
|
Merge upstream/main (v1.16.0) into wio-unified
Merge 254 upstream commits. Highlights relevant to the Wio fork: native
NRF52 companion power-saving (sleep when !hasPendingWork), preamble 16->32
for SF<9, generic sensor-registration model, CMD_SEND_RAW_PACKET, isEink()
display abstraction, KEEP_DISPLAY_ON_USB, contacts-sync/transient fixes.
Conflicts resolved (11 files):
- buzzer.h/.cpp: keep NRF52 include guard + our begin()->startup(); fold in
upstream doc comment.
- platformio (wio + eink): keep our slim env layout, add upstream kiss_modem
env (+ debug_tool=stlink on eink ble).
- GxEPDDisplay.h: take upstream isEink() override.
- EnvironmentSensorManager.h: take upstream's wider #if guard.
- DataStore.cpp: take upstream saveContacts(filter) signature, keep our
::openWrite (member openWrite would shadow the global 2-arg overload).
- main.cpp: keep our watchdog init + add board.onBootComplete(); adopt
upstream's `if(!hasPendingWork()) sleep` loop.
- MyMesh.h: version -> v1.16-solo.0, build date 6 Jun 2026.
- MyMesh.cpp: keep both CMD codes (SCREENSHOT 66 + RAW_PACKET 65), both field
inits, both handlers, our screenshot fns + upstream saveContacts/save_filter,
upstream hasPendingWork() + our MyMeshBot include.
- UITask.cpp: keep our solo splash/clock page/snprintf/buzzer init; merge
auto-off (+ opt-in KEEP_DISPLAY_ON_USB); merge low-batt shutdown (our EMA +
prefs threshold + upstream's !isExternalPowered() guard and eink-aware delay).
Post-merge drift fixes (upstream API/architecture changes, not conflicts):
- applyTxPower(): radio_set_tx_power() removed upstream -> radio_driver.setTxPower().
- Sensor refactor dropped the per-sensor *_initialized flags our
getAvailableLPPTypes() relied on; removed that override and the SENSORS page
now derives available LPP types from the populated sensors_lpp buffer.
README kept ours via .gitattributes merge=ours. Builds verified on all 5 Wio
envs (dual, eink dual, radio_ble, dual_dev, eink_dual_dev). Not yet
hardware-verified.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-06 17:21:41 +02:00
|
|
|
|
if (_display->isEink() == false) { delay(2000); }
|
2025-08-30 23:09:01 +12:00
|
|
|
|
}
|
2025-08-08 20:01:31 +10:00
|
|
|
|
shutdown();
|
2025-05-27 19:10:56 -07:00
|
|
|
|
}
|
2025-08-08 20:01:31 +10:00
|
|
|
|
next_batt_chck = millis() + 8000;
|
2025-05-27 19:10:56 -07:00
|
|
|
|
}
|
feat(ui): GPS breadcrumb phase 1 — storage, sampling, Summary view
Adds Tools › Breadcrumb. Enter on the screen toggles tracking; while
active, UITask::loop samples GPS at the cadence chosen in settings
(default 1 min) and drops the point into a 256-entry RAM ring guarded
by a min-distance gate (default 25 m). A "G" indicator joins "A" in
the status bar with the same blink convention.
New storage:
- examples/companion_radio/Breadcrumb.h — BreadcrumbStore class
- 256 × 12 B = 3 KB RAM (survives auto-off, lost on reboot — explicit
flash-slot save comes in phase 4)
- Haversine min-delta gate, total distance, elapsed seconds, current
km/h, bounding-box helper
New screen:
- examples/companion_radio/ui-new/BreadcrumbScreen.h — Summary view
with Status, Points, Dist (m or km), Time (h:mm), Speed; Enter
toggles tracking, Esc returns to Tools
Schema bump 0xC0DE0002 → 0xC0DE0003 for two new NodePrefs bytes:
breadcrumb_interval_idx (1min default) and breadcrumb_min_delta_idx
(25m default). Older saves leave both at zero, which matches the new
defaults. DataStore::loadPrefsInt/savePrefs read/write the pair.
Logging on/off is a runtime flag inside BreadcrumbStore — not a
preference — so reboot returns to the off state cleanly; the user
will be able to persist a snapshot to a slot in phase 4.
Phase 2 (map view), 3 (point list), 4 (slot save/load, GPX export over
USB), and 5 (settings UI) follow incrementally.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 08:22:12 +02:00
|
|
|
|
|
2026-05-25 09:10:40 +02:00
|
|
|
|
// GPS trail sampling — runs in the background while the trail is
|
feat(ui): GPS breadcrumb phase 1 — storage, sampling, Summary view
Adds Tools › Breadcrumb. Enter on the screen toggles tracking; while
active, UITask::loop samples GPS at the cadence chosen in settings
(default 1 min) and drops the point into a 256-entry RAM ring guarded
by a min-distance gate (default 25 m). A "G" indicator joins "A" in
the status bar with the same blink convention.
New storage:
- examples/companion_radio/Breadcrumb.h — BreadcrumbStore class
- 256 × 12 B = 3 KB RAM (survives auto-off, lost on reboot — explicit
flash-slot save comes in phase 4)
- Haversine min-delta gate, total distance, elapsed seconds, current
km/h, bounding-box helper
New screen:
- examples/companion_radio/ui-new/BreadcrumbScreen.h — Summary view
with Status, Points, Dist (m or km), Time (h:mm), Speed; Enter
toggles tracking, Esc returns to Tools
Schema bump 0xC0DE0002 → 0xC0DE0003 for two new NodePrefs bytes:
breadcrumb_interval_idx (1min default) and breadcrumb_min_delta_idx
(25m default). Older saves leave both at zero, which matches the new
defaults. DataStore::loadPrefsInt/savePrefs read/write the pair.
Logging on/off is a runtime flag inside BreadcrumbStore — not a
preference — so reboot returns to the off state cleanly; the user
will be able to persist a snapshot to a slot in phase 4.
Phase 2 (map view), 3 (point list), 4 (slot save/load, GPX export over
USB), and 5 (settings UI) follow incrementally.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 08:22:12 +02:00
|
|
|
|
// active, independent of which screen is shown. Skips silently if no GPS
|
|
|
|
|
|
// fix; min-delta gate inside addPoint() avoids near-stationary spam.
|
feat(companion): live location sharing, Locator geofencing, trail auto-pause
Squash merge of feat/location-beacon-alerts-autopause (v1.21).
Features:
- Live Location Sharing — broadcast position as movement-gated [LOC]
messages to a channel or contact; live shares show as map pins with
distance/bearing in Nearby Nodes and a status-bar indicator. [LOC] is
parsed in DMs, channel messages and room messages; DM shares name the
sender.
- Locator (geofence) — arm a geofence around a saved waypoint or a person
(live [LOC] or last-known position), alert on arrive/leave or near/far,
with an optional homing beeper (gated to arrive/both modes). Arm from
Tools > Locator, Nearby Nodes, or Waypoints; target picker lists
favourites first, clearable via a "None" entry. Active target is drawn
as a flag on the map.
- One active target shared across Locator / Navigate / Map via a single
resolver (resolvePersonPos / activeTargetPos) that prefers a live
[LOC] share over the last-advertised GPS fix.
- Follow live contacts — Navigate to a live-sharing contact follows them
as they move and adds an ETA line; quick-share your own position from
the Map.
- Map & status-bar upgrades — home mini-map gets a north marker, scale
tick, and a connected trail line (was disconnected dots); status line
shows tracked-node count, an arrow + distance to the active Locator/Nav
target (falling back to the nearest live-tracked contact); GPS fix icon
in the top status bar, shown only on GPS boards with GPS enabled.
- Trail auto-pause — recording freezes on stops (banking elapsed time,
breaking the map line across the idle gap) and resumes on movement
without ending the session.
- Streaming trail simplification — GPS points are simplified in-stream
via a fixed-corridor (Reumann-Witkam) pass tuned for fidelity: straight
runs collapse to their endpoints, curves stay bounded to the Min-dist
tolerance, so the 512-point buffer covers a far longer route than a
flat point budget would suggest.
- Collapsible Tools (Location / Comms / System sections, fold-in-place
like Settings) and page-indicator icons on the home carousel.
- Waypoint coordinate editor — add a waypoint by scroll-editing lat/lon
digit by digit.
Fixes:
- Critical: low-heap hang and contact loss on RAM-tight builds. Halved
message-history scrollback rings (recovering ~14 KB free heap) and
made contacts/channels/prefs persistence atomic (temp file + rename),
so an interrupted save can no longer corrupt or wipe the store.
- Serial.write() bounded so a stalled USB host can't hang the device.
- Nearby Nodes: live [LOC] senders respect the type filter, sort by
shared position, and the list refreshes so live shares bubble up.
- Map: live contacts are labelled before waypoints.
- GPS status icon hidden when GPS is off in Settings.
- Splash screen no longer truncates a pre-release tag's own dash (e.g.
v1.21-rc1) when stripping the build's commit-hash suffix.
- Null-guarded the Locator target picker; clamped loc-share channel
index on load.
Under the hood:
- -Os size optimisation on the e-ink and GAT562 30S solo envs.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 19:04:21 +02:00
|
|
|
|
if (!_trail.isActive()) _trail_pause_has_ref = false; // fresh ref on next start
|
2026-05-25 09:10:40 +02:00
|
|
|
|
if (_trail.isActive() && _node_prefs != NULL
|
|
|
|
|
|
&& (int32_t)(millis() - _next_trail_sample_ms) >= 0) {
|
2026-05-25 10:48:03 +02:00
|
|
|
|
_next_trail_sample_ms = millis() + (uint32_t)TrailStore::SAMPLING_SECS * 1000UL;
|
feat(ui): GPS breadcrumb phase 1 — storage, sampling, Summary view
Adds Tools › Breadcrumb. Enter on the screen toggles tracking; while
active, UITask::loop samples GPS at the cadence chosen in settings
(default 1 min) and drops the point into a 256-entry RAM ring guarded
by a min-distance gate (default 25 m). A "G" indicator joins "A" in
the status bar with the same blink convention.
New storage:
- examples/companion_radio/Breadcrumb.h — BreadcrumbStore class
- 256 × 12 B = 3 KB RAM (survives auto-off, lost on reboot — explicit
flash-slot save comes in phase 4)
- Haversine min-delta gate, total distance, elapsed seconds, current
km/h, bounding-box helper
New screen:
- examples/companion_radio/ui-new/BreadcrumbScreen.h — Summary view
with Status, Points, Dist (m or km), Time (h:mm), Speed; Enter
toggles tracking, Esc returns to Tools
Schema bump 0xC0DE0002 → 0xC0DE0003 for two new NodePrefs bytes:
breadcrumb_interval_idx (1min default) and breadcrumb_min_delta_idx
(25m default). Older saves leave both at zero, which matches the new
defaults. DataStore::loadPrefsInt/savePrefs read/write the pair.
Logging on/off is a runtime flag inside BreadcrumbStore — not a
preference — so reboot returns to the off state cleanly; the user
will be able to persist a snapshot to a slot in phase 4.
Phase 2 (map view), 3 (point list), 4 (slot save/load, GPX export over
USB), and 5 (settings UI) follow incrementally.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 08:22:12 +02:00
|
|
|
|
LocationProvider* loc = _sensors ? _sensors->getLocationProvider() : nullptr;
|
|
|
|
|
|
if (loc && loc->isValid()) {
|
feat(companion): live location sharing, Locator geofencing, trail auto-pause
Squash merge of feat/location-beacon-alerts-autopause (v1.21).
Features:
- Live Location Sharing — broadcast position as movement-gated [LOC]
messages to a channel or contact; live shares show as map pins with
distance/bearing in Nearby Nodes and a status-bar indicator. [LOC] is
parsed in DMs, channel messages and room messages; DM shares name the
sender.
- Locator (geofence) — arm a geofence around a saved waypoint or a person
(live [LOC] or last-known position), alert on arrive/leave or near/far,
with an optional homing beeper (gated to arrive/both modes). Arm from
Tools > Locator, Nearby Nodes, or Waypoints; target picker lists
favourites first, clearable via a "None" entry. Active target is drawn
as a flag on the map.
- One active target shared across Locator / Navigate / Map via a single
resolver (resolvePersonPos / activeTargetPos) that prefers a live
[LOC] share over the last-advertised GPS fix.
- Follow live contacts — Navigate to a live-sharing contact follows them
as they move and adds an ETA line; quick-share your own position from
the Map.
- Map & status-bar upgrades — home mini-map gets a north marker, scale
tick, and a connected trail line (was disconnected dots); status line
shows tracked-node count, an arrow + distance to the active Locator/Nav
target (falling back to the nearest live-tracked contact); GPS fix icon
in the top status bar, shown only on GPS boards with GPS enabled.
- Trail auto-pause — recording freezes on stops (banking elapsed time,
breaking the map line across the idle gap) and resumes on movement
without ending the session.
- Streaming trail simplification — GPS points are simplified in-stream
via a fixed-corridor (Reumann-Witkam) pass tuned for fidelity: straight
runs collapse to their endpoints, curves stay bounded to the Min-dist
tolerance, so the 512-point buffer covers a far longer route than a
flat point budget would suggest.
- Collapsible Tools (Location / Comms / System sections, fold-in-place
like Settings) and page-indicator icons on the home carousel.
- Waypoint coordinate editor — add a waypoint by scroll-editing lat/lon
digit by digit.
Fixes:
- Critical: low-heap hang and contact loss on RAM-tight builds. Halved
message-history scrollback rings (recovering ~14 KB free heap) and
made contacts/channels/prefs persistence atomic (temp file + rename),
so an interrupted save can no longer corrupt or wipe the store.
- Serial.write() bounded so a stalled USB host can't hang the device.
- Nearby Nodes: live [LOC] senders respect the type filter, sort by
shared position, and the list refreshes so live shares bubble up.
- Map: live contacts are labelled before waypoints.
- GPS status icon hidden when GPS is off in Settings.
- Splash screen no longer truncates a pre-release tag's own dash (e.g.
v1.21-rc1) when stripping the build's commit-hash suffix.
- Null-guarded the Locator target picker; clamped loc-share channel
index on load.
Under the hood:
- -Os size optimisation on the e-ink and GAT562 30S solo envs.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 19:04:21 +02:00
|
|
|
|
int32_t la = (int32_t)loc->getLatitude();
|
|
|
|
|
|
int32_t lo = (int32_t)loc->getLongitude();
|
2026-06-04 00:50:42 +02:00
|
|
|
|
uint16_t md = TrailStore::minDeltaMeters(_node_prefs->trail_min_delta_idx,
|
|
|
|
|
|
_node_prefs->units_imperial);
|
feat(companion): live location sharing, Locator geofencing, trail auto-pause
Squash merge of feat/location-beacon-alerts-autopause (v1.21).
Features:
- Live Location Sharing — broadcast position as movement-gated [LOC]
messages to a channel or contact; live shares show as map pins with
distance/bearing in Nearby Nodes and a status-bar indicator. [LOC] is
parsed in DMs, channel messages and room messages; DM shares name the
sender.
- Locator (geofence) — arm a geofence around a saved waypoint or a person
(live [LOC] or last-known position), alert on arrive/leave or near/far,
with an optional homing beeper (gated to arrive/both modes). Arm from
Tools > Locator, Nearby Nodes, or Waypoints; target picker lists
favourites first, clearable via a "None" entry. Active target is drawn
as a flag on the map.
- One active target shared across Locator / Navigate / Map via a single
resolver (resolvePersonPos / activeTargetPos) that prefers a live
[LOC] share over the last-advertised GPS fix.
- Follow live contacts — Navigate to a live-sharing contact follows them
as they move and adds an ETA line; quick-share your own position from
the Map.
- Map & status-bar upgrades — home mini-map gets a north marker, scale
tick, and a connected trail line (was disconnected dots); status line
shows tracked-node count, an arrow + distance to the active Locator/Nav
target (falling back to the nearest live-tracked contact); GPS fix icon
in the top status bar, shown only on GPS boards with GPS enabled.
- Trail auto-pause — recording freezes on stops (banking elapsed time,
breaking the map line across the idle gap) and resumes on movement
without ending the session.
- Streaming trail simplification — GPS points are simplified in-stream
via a fixed-corridor (Reumann-Witkam) pass tuned for fidelity: straight
runs collapse to their endpoints, curves stay bounded to the Min-dist
tolerance, so the 512-point buffer covers a far longer route than a
flat point budget would suggest.
- Collapsible Tools (Location / Comms / System sections, fold-in-place
like Settings) and page-indicator icons on the home carousel.
- Waypoint coordinate editor — add a waypoint by scroll-editing lat/lon
digit by digit.
Fixes:
- Critical: low-heap hang and contact loss on RAM-tight builds. Halved
message-history scrollback rings (recovering ~14 KB free heap) and
made contacts/channels/prefs persistence atomic (temp file + rename),
so an interrupted save can no longer corrupt or wipe the store.
- Serial.write() bounded so a stalled USB host can't hang the device.
- Nearby Nodes: live [LOC] senders respect the type filter, sort by
shared position, and the list refreshes so live shares bubble up.
- Map: live contacts are labelled before waypoints.
- GPS status icon hidden when GPS is off in Settings.
- Splash screen no longer truncates a pre-release tag's own dash (e.g.
v1.21-rc1) when stripping the build's commit-hash suffix.
- Null-guarded the Locator target picker; clamped loc-share channel
index on load.
Under the hood:
- -Os size optimisation on the e-ink and GAT562 30S solo envs.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 19:04:21 +02:00
|
|
|
|
// Auto-pause: freeze the trail once the device has stayed within
|
|
|
|
|
|
// TRAIL_AUTOPAUSE_MOVE_M of one spot for the configured delay; resume on
|
|
|
|
|
|
// the next real move. Its own coarse gate (not the trail min-delta) so
|
|
|
|
|
|
// GPS jitter while parked doesn't keep the idle timer alive.
|
|
|
|
|
|
uint16_t ap = NodePrefs::trailAutoPauseSecs(_node_prefs->trail_autopause_idx);
|
|
|
|
|
|
if (ap > 0) {
|
|
|
|
|
|
uint32_t now = millis();
|
|
|
|
|
|
float moved = _trail_pause_has_ref
|
|
|
|
|
|
? geo::haversineKm(_trail_pause_ref_lat, _trail_pause_ref_lon, la, lo) * 1000.0f
|
|
|
|
|
|
: 1e9f;
|
|
|
|
|
|
if (!_trail_pause_has_ref || moved >= (float)NodePrefs::TRAIL_AUTOPAUSE_MOVE_M) {
|
|
|
|
|
|
_trail_pause_ref_lat = la; _trail_pause_ref_lon = lo;
|
|
|
|
|
|
_trail_pause_has_ref = true;
|
|
|
|
|
|
_trail_last_move_ms = now;
|
|
|
|
|
|
if (_trail.isPaused()) _trail.setPaused(false);
|
|
|
|
|
|
} else if (!_trail.isPaused() && (now - _trail_last_move_ms) >= (uint32_t)ap * 1000UL) {
|
|
|
|
|
|
_trail.setPaused(true);
|
|
|
|
|
|
}
|
|
|
|
|
|
} else if (_trail.isPaused()) {
|
|
|
|
|
|
_trail.setPaused(false); // feature turned off → resume
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!_trail.isPaused())
|
|
|
|
|
|
_trail.addPoint(la, lo, (uint32_t)rtc_clock.getCurrentTime(), md);
|
feat(ui): GPS breadcrumb phase 1 — storage, sampling, Summary view
Adds Tools › Breadcrumb. Enter on the screen toggles tracking; while
active, UITask::loop samples GPS at the cadence chosen in settings
(default 1 min) and drops the point into a 256-entry RAM ring guarded
by a min-distance gate (default 25 m). A "G" indicator joins "A" in
the status bar with the same blink convention.
New storage:
- examples/companion_radio/Breadcrumb.h — BreadcrumbStore class
- 256 × 12 B = 3 KB RAM (survives auto-off, lost on reboot — explicit
flash-slot save comes in phase 4)
- Haversine min-delta gate, total distance, elapsed seconds, current
km/h, bounding-box helper
New screen:
- examples/companion_radio/ui-new/BreadcrumbScreen.h — Summary view
with Status, Points, Dist (m or km), Time (h:mm), Speed; Enter
toggles tracking, Esc returns to Tools
Schema bump 0xC0DE0002 → 0xC0DE0003 for two new NodePrefs bytes:
breadcrumb_interval_idx (1min default) and breadcrumb_min_delta_idx
(25m default). Older saves leave both at zero, which matches the new
defaults. DataStore::loadPrefsInt/savePrefs read/write the pair.
Logging on/off is a runtime flag inside BreadcrumbStore — not a
preference — so reboot returns to the off state cleanly; the user
will be able to persist a snapshot to a slot in phase 4.
Phase 2 (map view), 3 (point list), 4 (slot save/load, GPX export over
USB), and 5 (settings UI) follow incrementally.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 08:22:12 +02:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-06-03 08:20:59 +02:00
|
|
|
|
|
feat(companion): live location sharing, Locator geofencing, trail auto-pause
Squash merge of feat/location-beacon-alerts-autopause (v1.21).
Features:
- Live Location Sharing — broadcast position as movement-gated [LOC]
messages to a channel or contact; live shares show as map pins with
distance/bearing in Nearby Nodes and a status-bar indicator. [LOC] is
parsed in DMs, channel messages and room messages; DM shares name the
sender.
- Locator (geofence) — arm a geofence around a saved waypoint or a person
(live [LOC] or last-known position), alert on arrive/leave or near/far,
with an optional homing beeper (gated to arrive/both modes). Arm from
Tools > Locator, Nearby Nodes, or Waypoints; target picker lists
favourites first, clearable via a "None" entry. Active target is drawn
as a flag on the map.
- One active target shared across Locator / Navigate / Map via a single
resolver (resolvePersonPos / activeTargetPos) that prefers a live
[LOC] share over the last-advertised GPS fix.
- Follow live contacts — Navigate to a live-sharing contact follows them
as they move and adds an ETA line; quick-share your own position from
the Map.
- Map & status-bar upgrades — home mini-map gets a north marker, scale
tick, and a connected trail line (was disconnected dots); status line
shows tracked-node count, an arrow + distance to the active Locator/Nav
target (falling back to the nearest live-tracked contact); GPS fix icon
in the top status bar, shown only on GPS boards with GPS enabled.
- Trail auto-pause — recording freezes on stops (banking elapsed time,
breaking the map line across the idle gap) and resumes on movement
without ending the session.
- Streaming trail simplification — GPS points are simplified in-stream
via a fixed-corridor (Reumann-Witkam) pass tuned for fidelity: straight
runs collapse to their endpoints, curves stay bounded to the Min-dist
tolerance, so the 512-point buffer covers a far longer route than a
flat point budget would suggest.
- Collapsible Tools (Location / Comms / System sections, fold-in-place
like Settings) and page-indicator icons on the home carousel.
- Waypoint coordinate editor — add a waypoint by scroll-editing lat/lon
digit by digit.
Fixes:
- Critical: low-heap hang and contact loss on RAM-tight builds. Halved
message-history scrollback rings (recovering ~14 KB free heap) and
made contacts/channels/prefs persistence atomic (temp file + rename),
so an interrupted save can no longer corrupt or wipe the store.
- Serial.write() bounded so a stalled USB host can't hang the device.
- Nearby Nodes: live [LOC] senders respect the type filter, sort by
shared position, and the list refreshes so live shares bubble up.
- Map: live contacts are labelled before waypoints.
- GPS status icon hidden when GPS is off in Settings.
- Splash screen no longer truncates a pre-release tag's own dash (e.g.
v1.21-rc1) when stripping the build's commit-hash suffix.
- Null-guarded the Locator target picker; clamped loc-share channel
index on load.
Under the hood:
- -Os size optimisation on the e-ink and GAT562 30S solo envs.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 19:04:21 +02:00
|
|
|
|
// Live-track housekeeping — drop shared positions that have gone stale, so
|
|
|
|
|
|
// the Nearby "Live" view / map don't show ghosts. Cheap; once a minute.
|
|
|
|
|
|
if ((int32_t)(millis() - _next_livetrack_expire_ms) >= 0) {
|
|
|
|
|
|
_next_livetrack_expire_ms = millis() + 60000UL;
|
|
|
|
|
|
_livetrack.expire((uint32_t)rtc_clock.getCurrentTime());
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Live location sharing — periodically broadcast my [LOC] to the configured
|
|
|
|
|
|
// target while moving (Map › Live share). Movement-gated so a stationary
|
|
|
|
|
|
// device stays quiet unless a heartbeat is configured.
|
|
|
|
|
|
if (_node_prefs && _node_prefs->loc_share_enabled
|
|
|
|
|
|
&& (int32_t)(millis() - _next_loc_share_check_ms) >= 0) {
|
|
|
|
|
|
_next_loc_share_check_ms = millis() + 2000UL;
|
|
|
|
|
|
if (!_loc_share_was_enabled) _loc_share_has_last = false; // re-announce on enable
|
|
|
|
|
|
_loc_share_was_enabled = true;
|
|
|
|
|
|
int32_t lat, lon;
|
|
|
|
|
|
if (currentLocation(lat, lon)) {
|
|
|
|
|
|
uint16_t move_m = NodePrefs::locShareMoveMeters(_node_prefs->loc_share_move_idx);
|
|
|
|
|
|
uint16_t gap_s = NodePrefs::locShareIntervalSecs(_node_prefs->loc_share_interval_idx);
|
|
|
|
|
|
uint16_t hb_s = NodePrefs::locShareHeartbeatSecs(_node_prefs->loc_share_heartbeat_idx);
|
|
|
|
|
|
uint32_t now = millis();
|
|
|
|
|
|
bool first = !_loc_share_has_last;
|
|
|
|
|
|
float moved = first ? 1e9f
|
|
|
|
|
|
: geo::haversineKm(_loc_share_last_lat, _loc_share_last_lon, lat, lon) * 1000.0f;
|
|
|
|
|
|
bool gap_ok = first || (now - _loc_share_last_ms) >= (uint32_t)gap_s * 1000UL;
|
|
|
|
|
|
bool hb_due = (hb_s > 0) && !first && (now - _loc_share_last_ms) >= (uint32_t)hb_s * 1000UL;
|
|
|
|
|
|
if ((moved >= (float)move_m && gap_ok) || first || hb_due) {
|
|
|
|
|
|
if (sendLocationShare(lat, lon)) {
|
|
|
|
|
|
_loc_share_last_lat = lat;
|
|
|
|
|
|
_loc_share_last_lon = lon;
|
|
|
|
|
|
_loc_share_last_ms = now;
|
|
|
|
|
|
_loc_share_has_last = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
} else if (_node_prefs && !_node_prefs->loc_share_enabled) {
|
|
|
|
|
|
_loc_share_was_enabled = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-03 08:20:59 +02:00
|
|
|
|
// Course-over-ground sampling — every ~1 s regardless of trail state, so the
|
|
|
|
|
|
// heading is available to navigation even when not recording a trail.
|
|
|
|
|
|
if ((int32_t)(millis() - _next_cog_sample_ms) >= 0) {
|
|
|
|
|
|
_next_cog_sample_ms = millis() + 1000UL;
|
|
|
|
|
|
LocationProvider* loc = _sensors ? _sensors->getLocationProvider() : nullptr;
|
|
|
|
|
|
if (loc && loc->isValid()) {
|
|
|
|
|
|
pushCogFix((int32_t)loc->getLatitude(), (int32_t)loc->getLongitude());
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
feat(companion): live location sharing, Locator geofencing, trail auto-pause
Squash merge of feat/location-beacon-alerts-autopause (v1.21).
Features:
- Live Location Sharing — broadcast position as movement-gated [LOC]
messages to a channel or contact; live shares show as map pins with
distance/bearing in Nearby Nodes and a status-bar indicator. [LOC] is
parsed in DMs, channel messages and room messages; DM shares name the
sender.
- Locator (geofence) — arm a geofence around a saved waypoint or a person
(live [LOC] or last-known position), alert on arrive/leave or near/far,
with an optional homing beeper (gated to arrive/both modes). Arm from
Tools > Locator, Nearby Nodes, or Waypoints; target picker lists
favourites first, clearable via a "None" entry. Active target is drawn
as a flag on the map.
- One active target shared across Locator / Navigate / Map via a single
resolver (resolvePersonPos / activeTargetPos) that prefers a live
[LOC] share over the last-advertised GPS fix.
- Follow live contacts — Navigate to a live-sharing contact follows them
as they move and adds an ETA line; quick-share your own position from
the Map.
- Map & status-bar upgrades — home mini-map gets a north marker, scale
tick, and a connected trail line (was disconnected dots); status line
shows tracked-node count, an arrow + distance to the active Locator/Nav
target (falling back to the nearest live-tracked contact); GPS fix icon
in the top status bar, shown only on GPS boards with GPS enabled.
- Trail auto-pause — recording freezes on stops (banking elapsed time,
breaking the map line across the idle gap) and resumes on movement
without ending the session.
- Streaming trail simplification — GPS points are simplified in-stream
via a fixed-corridor (Reumann-Witkam) pass tuned for fidelity: straight
runs collapse to their endpoints, curves stay bounded to the Min-dist
tolerance, so the 512-point buffer covers a far longer route than a
flat point budget would suggest.
- Collapsible Tools (Location / Comms / System sections, fold-in-place
like Settings) and page-indicator icons on the home carousel.
- Waypoint coordinate editor — add a waypoint by scroll-editing lat/lon
digit by digit.
Fixes:
- Critical: low-heap hang and contact loss on RAM-tight builds. Halved
message-history scrollback rings (recovering ~14 KB free heap) and
made contacts/channels/prefs persistence atomic (temp file + rename),
so an interrupted save can no longer corrupt or wipe the store.
- Serial.write() bounded so a stalled USB host can't hang the device.
- Nearby Nodes: live [LOC] senders respect the type filter, sort by
shared position, and the list refreshes so live shares bubble up.
- Map: live contacts are labelled before waypoints.
- GPS status icon hidden when GPS is off in Settings.
- Splash screen no longer truncates a pre-release tag's own dash (e.g.
v1.21-rc1) when stripping the build's commit-hash suffix.
- Null-guarded the Locator target picker; clamped loc-share channel
index on load.
Under the hood:
- -Os size optimisation on the e-ink and GAT562 30S solo envs.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 19:04:21 +02:00
|
|
|
|
|
|
|
|
|
|
// Locator — beep + alert when the device crosses into / out of the armed
|
|
|
|
|
|
// geofence. Cheap; a few seconds of latency at the boundary is fine.
|
|
|
|
|
|
if ((int32_t)(millis() - _next_locator_ms) >= 0) {
|
|
|
|
|
|
_next_locator_ms = millis() + 3000UL;
|
|
|
|
|
|
evaluateLocator();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Locator proximity beeper — ticks faster the closer to the target. Runs on
|
|
|
|
|
|
// its own short cadence (the crossing check above is too coarse for this).
|
|
|
|
|
|
locatorProximityBeeper();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Evaluate the single geofence against the current GPS fix. Crossing the radius
|
|
|
|
|
|
// fires fireLocator() according to the configured mode; a hysteresis band on
|
|
|
|
|
|
// the "leave" edge stops it chattering at the boundary, and the first reading
|
|
|
|
|
|
// after arming only seeds the inside/outside state (no spurious alert).
|
|
|
|
|
|
// Distance (m) from the current GPS fix to the locator target, plus the
|
|
|
|
|
|
// configured radius (m). Returns false when no target is set or there's no fix
|
|
|
|
|
|
// — the single place the target-distance maths lives, shared by the crossing
|
|
|
|
|
|
// evaluator and the proximity beeper.
|
|
|
|
|
|
// One precedence for a person's position — an active [LOC] live share wins,
|
|
|
|
|
|
// else the last-advertised GPS fix. Not everyone keeps live-sharing on, so the
|
|
|
|
|
|
// fallback lets a rarely-updating but stationary node (a repeater, or someone
|
|
|
|
|
|
// who shared a fix once) still work as a target.
|
|
|
|
|
|
bool UITask::resolvePersonPos(const uint8_t* key, int32_t& lat, int32_t& lon,
|
|
|
|
|
|
bool* live, uint32_t* ts) const {
|
|
|
|
|
|
if (live) *live = false;
|
|
|
|
|
|
if (ts) *ts = 0;
|
|
|
|
|
|
if (!key) return false;
|
|
|
|
|
|
const LiveTrackStore::Entry* e =
|
|
|
|
|
|
_livetrack.activeByKey(key, (uint32_t)rtc_clock.getCurrentTime());
|
|
|
|
|
|
if (e) {
|
|
|
|
|
|
lat = e->lat_1e6; lon = e->lon_1e6;
|
|
|
|
|
|
if (live) *live = true;
|
|
|
|
|
|
if (ts) *ts = e->ts;
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
ContactInfo* c = the_mesh.lookupContactByPubKey(key, NodePrefs::FAVOURITE_PREFIX_LEN);
|
|
|
|
|
|
if (c && (c->gps_lat != 0 || c->gps_lon != 0)) {
|
|
|
|
|
|
lat = c->gps_lat; lon = c->gps_lon;
|
|
|
|
|
|
if (ts) *ts = c->lastmod;
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
bool UITask::activeTargetPos(int32_t& lat, int32_t& lon) const {
|
|
|
|
|
|
if (!_node_prefs || !_node_prefs->locator_has_target) return false;
|
|
|
|
|
|
if (_node_prefs->locator_target_kind == 1)
|
|
|
|
|
|
return resolvePersonPos(_node_prefs->locator_key, lat, lon);
|
|
|
|
|
|
lat = _node_prefs->locator_lat_1e6;
|
|
|
|
|
|
lon = _node_prefs->locator_lon_1e6;
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
bool UITask::locatorDistance(float& dist_m, float& radius_m) const {
|
|
|
|
|
|
int32_t tlat, tlon;
|
|
|
|
|
|
if (!activeTargetPos(tlat, tlon)) return false;
|
|
|
|
|
|
int32_t lat, lon;
|
|
|
|
|
|
if (!currentLocation(lat, lon)) return false;
|
|
|
|
|
|
dist_m = geo::haversineKm(lat, lon, tlat, tlon) * 1000.0f;
|
|
|
|
|
|
radius_m = (float)NodePrefs::locatorRadiusMeters(_node_prefs->locator_radius_idx);
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void UITask::evaluateLocator() {
|
|
|
|
|
|
if (!_node_prefs || !_node_prefs->locator_enabled || !_node_prefs->locator_has_target) {
|
|
|
|
|
|
_locator_known = false;
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
float dist, r;
|
|
|
|
|
|
if (!locatorDistance(dist, r)) return; // armed but no fix yet — keep state
|
|
|
|
|
|
bool inside;
|
|
|
|
|
|
if (!_locator_known) inside = dist <= r; // seed state
|
|
|
|
|
|
else if (_locator_inside) inside = dist <= r * 1.25f; // leave past band
|
|
|
|
|
|
else inside = dist <= r; // arrive at edge
|
|
|
|
|
|
|
|
|
|
|
|
if (_locator_known && inside != _locator_inside) {
|
|
|
|
|
|
uint8_t mode = _node_prefs->locator_mode; // 0=arrive,1=leave,2=both
|
|
|
|
|
|
bool fire = inside ? (mode == 0 || mode == 2) : (mode == 1 || mode == 2);
|
|
|
|
|
|
if (fire) fireLocator(inside);
|
|
|
|
|
|
}
|
|
|
|
|
|
_locator_inside = inside;
|
|
|
|
|
|
_locator_known = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void UITask::fireLocator(bool arrived) {
|
|
|
|
|
|
const char* lbl = _node_prefs->locator_label[0] ? _node_prefs->locator_label : "target";
|
|
|
|
|
|
bool person = _node_prefs->locator_target_kind == 1;
|
|
|
|
|
|
char msg[40];
|
|
|
|
|
|
// "Near/Away" reads naturally for a moving person; "Arrived/Left" for a place.
|
|
|
|
|
|
snprintf(msg, sizeof(msg),
|
|
|
|
|
|
arrived ? (person ? "Near: %s" : "Arrived: %s")
|
|
|
|
|
|
: (person ? "Away: %s" : "Left: %s"), lbl);
|
|
|
|
|
|
showAlert(msg, 3000);
|
|
|
|
|
|
if (!isBuzzerQuiet())
|
|
|
|
|
|
playMelody(arrived ? "locarr:d=8,o=6,b=140:c,e,g" : "loclv:d=8,o=6,b=140:g,e,c");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void UITask::setTarget(uint8_t kind, const uint8_t* key, int32_t lat, int32_t lon, const char* name) {
|
|
|
|
|
|
if (!_node_prefs) return;
|
|
|
|
|
|
_node_prefs->locator_target_kind = kind;
|
|
|
|
|
|
if (kind == 1 && key) memcpy(_node_prefs->locator_key, key, NodePrefs::FAVOURITE_PREFIX_LEN);
|
|
|
|
|
|
_node_prefs->locator_lat_1e6 = lat;
|
|
|
|
|
|
_node_prefs->locator_lon_1e6 = lon;
|
|
|
|
|
|
snprintf(_node_prefs->locator_label, sizeof(_node_prefs->locator_label), "%s", name);
|
|
|
|
|
|
_node_prefs->locator_has_target = 1;
|
|
|
|
|
|
resetLocator(); // re-seed the crossing engine so the change can't fire on a stale state
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void UITask::setTargetNow(uint8_t kind, const uint8_t* key, int32_t lat, int32_t lon, const char* name) {
|
|
|
|
|
|
if (!_node_prefs) return;
|
|
|
|
|
|
setTarget(kind, key, lat, lon, name);
|
|
|
|
|
|
the_mesh.savePrefs();
|
|
|
|
|
|
showAlert("Target set", 1200);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void UITask::clearTarget() {
|
|
|
|
|
|
if (!_node_prefs) return;
|
|
|
|
|
|
_node_prefs->locator_has_target = 0;
|
|
|
|
|
|
resetLocator();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-26 17:48:05 +02:00
|
|
|
|
void UITask::clearTargetIfWaypoint(int32_t lat_1e6, int32_t lon_1e6) {
|
|
|
|
|
|
if (!_node_prefs || !_node_prefs->locator_has_target || _node_prefs->locator_target_kind != 0) return;
|
|
|
|
|
|
if (_node_prefs->locator_lat_1e6 != lat_1e6 || _node_prefs->locator_lon_1e6 != lon_1e6) return;
|
|
|
|
|
|
clearTarget();
|
|
|
|
|
|
the_mesh.savePrefs();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void UITask::onContactRemoved(const uint8_t* pub_key) {
|
|
|
|
|
|
if (!_node_prefs || !pub_key) return;
|
|
|
|
|
|
bool changed = false;
|
|
|
|
|
|
|
|
|
|
|
|
int slot = findFavouriteSlot(pub_key);
|
|
|
|
|
|
if (slot >= 0) { clearFavouriteSlot(slot); changed = true; }
|
|
|
|
|
|
|
|
|
|
|
|
if (_node_prefs->locator_has_target && _node_prefs->locator_target_kind == 1
|
|
|
|
|
|
&& memcmp(_node_prefs->locator_key, pub_key, NodePrefs::FAVOURITE_PREFIX_LEN) == 0) {
|
|
|
|
|
|
clearTarget();
|
|
|
|
|
|
changed = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
// Fail closed rather than guess a new recipient: a contact target that's
|
|
|
|
|
|
// gone just turns auto-share off, it doesn't fall back to some other target.
|
|
|
|
|
|
if (_node_prefs->loc_share_target_type == 1
|
|
|
|
|
|
&& memcmp(_node_prefs->loc_share_dm_prefix, pub_key, NodePrefs::FAVOURITE_PREFIX_LEN) == 0) {
|
|
|
|
|
|
_node_prefs->loc_share_enabled = 0;
|
|
|
|
|
|
changed = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
// Per-contact mute/melody overrides — only 16 slots shared across every
|
|
|
|
|
|
// contact, so an orphaned entry isn't just stale, it can eventually starve
|
|
|
|
|
|
// new overrides for contacts that still exist. Keyed by a 4-byte prefix
|
|
|
|
|
|
// (narrower than the 6-byte one above), so compare only that many bytes.
|
|
|
|
|
|
for (int i = 0; i < NodePrefs::DM_NOTIF_TABLE_MAX; i++) {
|
|
|
|
|
|
if (_node_prefs->dm_notif[i].state && memcmp(_node_prefs->dm_notif[i].prefix, pub_key, 4) == 0) {
|
|
|
|
|
|
memset(&_node_prefs->dm_notif[i], 0, sizeof(_node_prefs->dm_notif[i]));
|
|
|
|
|
|
changed = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
for (int i = 0; i < NodePrefs::DM_MELODY_TABLE_MAX; i++) {
|
|
|
|
|
|
if (_node_prefs->dm_melody[i].slot && memcmp(_node_prefs->dm_melody[i].prefix, pub_key, 4) == 0) {
|
|
|
|
|
|
memset(&_node_prefs->dm_melody[i], 0, sizeof(_node_prefs->dm_melody[i]));
|
|
|
|
|
|
changed = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (changed) the_mesh.savePrefs();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-27 00:39:10 +02:00
|
|
|
|
void UITask::onChannelRemoved(uint8_t channel_idx) {
|
|
|
|
|
|
if (!_node_prefs) return;
|
|
|
|
|
|
bool changed = false;
|
|
|
|
|
|
|
|
|
|
|
|
if (_node_prefs->bot_channel_enabled && _node_prefs->bot_channel_idx == channel_idx) {
|
|
|
|
|
|
_node_prefs->bot_channel_enabled = 0;
|
|
|
|
|
|
changed = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
// Fail closed, same policy as onContactRemoved()'s Live Share case.
|
|
|
|
|
|
if (_node_prefs->loc_share_target_type == 0 && _node_prefs->loc_share_channel_idx == channel_idx) {
|
|
|
|
|
|
_node_prefs->loc_share_enabled = 0;
|
|
|
|
|
|
changed = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
uint64_t mask = 1ULL << channel_idx;
|
|
|
|
|
|
if (_node_prefs->ch_notif_melody_set & mask) {
|
|
|
|
|
|
_node_prefs->ch_notif_melody_set &= ~mask;
|
|
|
|
|
|
_node_prefs->ch_notif_melody_2 &= ~mask;
|
|
|
|
|
|
changed = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (changed) the_mesh.savePrefs();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
feat(companion): live location sharing, Locator geofencing, trail auto-pause
Squash merge of feat/location-beacon-alerts-autopause (v1.21).
Features:
- Live Location Sharing — broadcast position as movement-gated [LOC]
messages to a channel or contact; live shares show as map pins with
distance/bearing in Nearby Nodes and a status-bar indicator. [LOC] is
parsed in DMs, channel messages and room messages; DM shares name the
sender.
- Locator (geofence) — arm a geofence around a saved waypoint or a person
(live [LOC] or last-known position), alert on arrive/leave or near/far,
with an optional homing beeper (gated to arrive/both modes). Arm from
Tools > Locator, Nearby Nodes, or Waypoints; target picker lists
favourites first, clearable via a "None" entry. Active target is drawn
as a flag on the map.
- One active target shared across Locator / Navigate / Map via a single
resolver (resolvePersonPos / activeTargetPos) that prefers a live
[LOC] share over the last-advertised GPS fix.
- Follow live contacts — Navigate to a live-sharing contact follows them
as they move and adds an ETA line; quick-share your own position from
the Map.
- Map & status-bar upgrades — home mini-map gets a north marker, scale
tick, and a connected trail line (was disconnected dots); status line
shows tracked-node count, an arrow + distance to the active Locator/Nav
target (falling back to the nearest live-tracked contact); GPS fix icon
in the top status bar, shown only on GPS boards with GPS enabled.
- Trail auto-pause — recording freezes on stops (banking elapsed time,
breaking the map line across the idle gap) and resumes on movement
without ending the session.
- Streaming trail simplification — GPS points are simplified in-stream
via a fixed-corridor (Reumann-Witkam) pass tuned for fidelity: straight
runs collapse to their endpoints, curves stay bounded to the Min-dist
tolerance, so the 512-point buffer covers a far longer route than a
flat point budget would suggest.
- Collapsible Tools (Location / Comms / System sections, fold-in-place
like Settings) and page-indicator icons on the home carousel.
- Waypoint coordinate editor — add a waypoint by scroll-editing lat/lon
digit by digit.
Fixes:
- Critical: low-heap hang and contact loss on RAM-tight builds. Halved
message-history scrollback rings (recovering ~14 KB free heap) and
made contacts/channels/prefs persistence atomic (temp file + rename),
so an interrupted save can no longer corrupt or wipe the store.
- Serial.write() bounded so a stalled USB host can't hang the device.
- Nearby Nodes: live [LOC] senders respect the type filter, sort by
shared position, and the list refreshes so live shares bubble up.
- Map: live contacts are labelled before waypoints.
- GPS status icon hidden when GPS is off in Settings.
- Splash screen no longer truncates a pre-release tag's own dash (e.g.
v1.21-rc1) when stripping the build's commit-hash suffix.
- Null-guarded the Locator target picker; clamped loc-share channel
index on load.
Under the hood:
- -Os size optimisation on the e-ink and GAT562 30S solo envs.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 19:04:21 +02:00
|
|
|
|
// Homing beeper: while armed with a target and inside the radius, emit a short
|
|
|
|
|
|
// tick whose interval shrinks linearly with distance — slow at the edge, rapid
|
|
|
|
|
|
// near the centre. Polls distance a few times a second; silent outside the
|
|
|
|
|
|
// radius. The beeper has its own toggle (locator_beeper), so turning it on is
|
|
|
|
|
|
// an explicit "I want to hear this" — it deliberately overrides the global
|
|
|
|
|
|
// buzzer mute (playMelody → buzzer.playForced ignores the quiet flag).
|
|
|
|
|
|
void UITask::locatorProximityBeeper() {
|
|
|
|
|
|
static const uint32_t BEEP_MIN_MS = 150; // fastest cadence (at the target)
|
|
|
|
|
|
static const uint32_t BEEP_MAX_MS = 2000; // slowest cadence (at the edge)
|
|
|
|
|
|
if (!_node_prefs || !_node_prefs->locator_enabled || !_node_prefs->locator_beeper
|
|
|
|
|
|
|| !_node_prefs->locator_has_target || _node_prefs->locator_mode == 1) { // leave-only mode: no homing
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
if ((int32_t)(millis() - _locator_beep_check_ms) < 0) return;
|
|
|
|
|
|
_locator_beep_check_ms = millis() + 250UL;
|
|
|
|
|
|
|
|
|
|
|
|
float dist, r;
|
|
|
|
|
|
if (!locatorDistance(dist, r)) return;
|
|
|
|
|
|
if (dist > r) { // outside the zone: stay quiet, beep on re-entry
|
|
|
|
|
|
_locator_beep_next_ms = millis();
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
if ((int32_t)(millis() - _locator_beep_next_ms) < 0) return;
|
|
|
|
|
|
float frac = (r > 0) ? dist / r : 0; // 0 at centre, 1 at edge
|
|
|
|
|
|
if (frac < 0) frac = 0; else if (frac > 1) frac = 1;
|
|
|
|
|
|
uint32_t interval = BEEP_MIN_MS + (uint32_t)(frac * (BEEP_MAX_MS - BEEP_MIN_MS));
|
|
|
|
|
|
playMelody("locp:d=32,o=7,b=200:c");
|
|
|
|
|
|
_locator_beep_next_ms = millis() + interval;
|
2026-06-03 08:20:59 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Insert a GPS fix into the course-over-ground ring, rejecting gross outliers
|
|
|
|
|
|
// (a jump implying an impossible speed) so one bad fix can't swing the heading.
|
|
|
|
|
|
void UITask::pushCogFix(int32_t lat, int32_t lon) {
|
2026-06-04 00:12:15 +02:00
|
|
|
|
static const uint32_t COG_MAX_GAP_MS = 15000; // GPS gap longer than this → window is stale
|
2026-06-03 08:20:59 +02:00
|
|
|
|
uint32_t now = millis();
|
|
|
|
|
|
if (_cog_count > 0) {
|
|
|
|
|
|
const CogFix& prev = _cog[(_cog_head + _cog_count - 1) % COG_RING];
|
|
|
|
|
|
uint32_t dt = now - prev.ms;
|
2026-06-04 00:12:15 +02:00
|
|
|
|
if (dt > COG_MAX_GAP_MS) {
|
|
|
|
|
|
// GPS was lost for a while: the old fixes are far in the past, so a
|
|
|
|
|
|
// window spanning them would imply a bogus "teleport" heading. Restart
|
|
|
|
|
|
// the ring from this fix (the last-good _cog_deg is kept for display).
|
|
|
|
|
|
_cog_head = 0; _cog_count = 0;
|
|
|
|
|
|
} else if (dt > 0) {
|
2026-06-03 08:20:59 +02:00
|
|
|
|
float dist_m = geo::haversineKm(prev.lat, prev.lon, lat, lon) * 1000.0f;
|
|
|
|
|
|
float speed = dist_m / (dt / 1000.0f); // m/s
|
|
|
|
|
|
if (speed > 50.0f) return; // > 180 km/h between fixes → reject
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
int pos;
|
|
|
|
|
|
if (_cog_count < COG_RING) { pos = (_cog_head + _cog_count) % COG_RING; _cog_count++; }
|
|
|
|
|
|
else { pos = _cog_head; _cog_head = (_cog_head + 1) % COG_RING; }
|
|
|
|
|
|
_cog[pos].lat = lat; _cog[pos].lon = lon; _cog[pos].ms = now;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
bool UITask::currentCourse(int& deg_out) const {
|
|
|
|
|
|
static const float COG_MIN_MOVE_M = 6.0f; // window must span ≥ this to be a real heading
|
|
|
|
|
|
if (_cog_count < 2) {
|
|
|
|
|
|
if (_cog_deg >= 0) { deg_out = _cog_deg; return true; } // hold last good
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
const CogFix& oldest = _cog[_cog_head];
|
|
|
|
|
|
const CogFix& newest = _cog[(_cog_head + _cog_count - 1) % COG_RING];
|
|
|
|
|
|
float span_m = geo::haversineKm(oldest.lat, oldest.lon, newest.lat, newest.lon) * 1000.0f;
|
|
|
|
|
|
if (span_m < COG_MIN_MOVE_M) {
|
|
|
|
|
|
if (_cog_deg >= 0) { deg_out = _cog_deg; return true; } // standing still → hold last
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
// Cache as last-good (mutable-free: recompute is cheap, but keep _cog_deg fresh).
|
|
|
|
|
|
const_cast<UITask*>(this)->_cog_deg =
|
|
|
|
|
|
geo::bearingDeg(oldest.lat, oldest.lon, newest.lat, newest.lon);
|
|
|
|
|
|
deg_out = _cog_deg;
|
|
|
|
|
|
return true;
|
2025-05-27 19:10:56 -07:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-04 00:12:15 +02:00
|
|
|
|
bool UITask::currentLocation(int32_t& lat, int32_t& lon) const {
|
|
|
|
|
|
LocationProvider* loc = _sensors ? _sensors->getLocationProvider() : nullptr;
|
|
|
|
|
|
if (loc && loc->isValid()) {
|
|
|
|
|
|
lat = (int32_t)loc->getLatitude();
|
|
|
|
|
|
lon = (int32_t)loc->getLongitude();
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
feat(companion): live location sharing, Locator geofencing, trail auto-pause
Squash merge of feat/location-beacon-alerts-autopause (v1.21).
Features:
- Live Location Sharing — broadcast position as movement-gated [LOC]
messages to a channel or contact; live shares show as map pins with
distance/bearing in Nearby Nodes and a status-bar indicator. [LOC] is
parsed in DMs, channel messages and room messages; DM shares name the
sender.
- Locator (geofence) — arm a geofence around a saved waypoint or a person
(live [LOC] or last-known position), alert on arrive/leave or near/far,
with an optional homing beeper (gated to arrive/both modes). Arm from
Tools > Locator, Nearby Nodes, or Waypoints; target picker lists
favourites first, clearable via a "None" entry. Active target is drawn
as a flag on the map.
- One active target shared across Locator / Navigate / Map via a single
resolver (resolvePersonPos / activeTargetPos) that prefers a live
[LOC] share over the last-advertised GPS fix.
- Follow live contacts — Navigate to a live-sharing contact follows them
as they move and adds an ETA line; quick-share your own position from
the Map.
- Map & status-bar upgrades — home mini-map gets a north marker, scale
tick, and a connected trail line (was disconnected dots); status line
shows tracked-node count, an arrow + distance to the active Locator/Nav
target (falling back to the nearest live-tracked contact); GPS fix icon
in the top status bar, shown only on GPS boards with GPS enabled.
- Trail auto-pause — recording freezes on stops (banking elapsed time,
breaking the map line across the idle gap) and resumes on movement
without ending the session.
- Streaming trail simplification — GPS points are simplified in-stream
via a fixed-corridor (Reumann-Witkam) pass tuned for fidelity: straight
runs collapse to their endpoints, curves stay bounded to the Min-dist
tolerance, so the 512-point buffer covers a far longer route than a
flat point budget would suggest.
- Collapsible Tools (Location / Comms / System sections, fold-in-place
like Settings) and page-indicator icons on the home carousel.
- Waypoint coordinate editor — add a waypoint by scroll-editing lat/lon
digit by digit.
Fixes:
- Critical: low-heap hang and contact loss on RAM-tight builds. Halved
message-history scrollback rings (recovering ~14 KB free heap) and
made contacts/channels/prefs persistence atomic (temp file + rename),
so an interrupted save can no longer corrupt or wipe the store.
- Serial.write() bounded so a stalled USB host can't hang the device.
- Nearby Nodes: live [LOC] senders respect the type filter, sort by
shared position, and the list refreshes so live shares bubble up.
- Map: live contacts are labelled before waypoints.
- GPS status icon hidden when GPS is off in Settings.
- Splash screen no longer truncates a pre-release tag's own dash (e.g.
v1.21-rc1) when stripping the build's commit-hash suffix.
- Null-guarded the Locator target picker; clamped loc-share channel
index on load.
Under the hood:
- -Os size optimisation on the e-ink and GAT562 30S solo envs.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 19:04:21 +02:00
|
|
|
|
// A peer broadcast its position via a [LOC] message (parsed in MyMesh). Record
|
|
|
|
|
|
// it in the live-track table for the Nearby "Live" view / map. Gated on the
|
|
|
|
|
|
// user preference so it stays opt-in.
|
|
|
|
|
|
void UITask::onSharedLocation(const uint8_t* pub_key, const char* name,
|
|
|
|
|
|
int32_t lat_1e6, int32_t lon_1e6,
|
|
|
|
|
|
uint32_t ts, bool verified) {
|
|
|
|
|
|
if (!_node_prefs || !_node_prefs->track_shared_loc) return;
|
|
|
|
|
|
_livetrack.update(pub_key, name, lat_1e6, lon_1e6, ts, verified);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
bool UITask::sendLocationShare(int32_t lat, int32_t lon) {
|
|
|
|
|
|
if (!_node_prefs) return false;
|
|
|
|
|
|
char text[80];
|
|
|
|
|
|
if (_node_prefs->loc_share_target_type == 0) {
|
|
|
|
|
|
// Channel: sendGroupMessage prepends "<name>: ", so the payload already
|
|
|
|
|
|
// names the sender — keep the [LOC] text bare.
|
|
|
|
|
|
snprintf(text, sizeof(text), LOCATION_MSG_TAG "%.5f,%.5f", lat / 1e6, lon / 1e6);
|
|
|
|
|
|
ChannelDetails ch;
|
|
|
|
|
|
if (!the_mesh.getChannel(_node_prefs->loc_share_channel_idx, ch)) return false;
|
|
|
|
|
|
return the_mesh.sendGroupMessage(rtc_clock.getCurrentTime(), ch.channel,
|
|
|
|
|
|
the_mesh.getNodeName(), text, strlen(text));
|
|
|
|
|
|
}
|
|
|
|
|
|
// DM carries no per-message sender prefix, so embed the name in the text — the
|
|
|
|
|
|
// share is then self-describing in any chat client (a trailing token after the
|
|
|
|
|
|
// coordinate, which parseLocShare ignores on the receiving side).
|
|
|
|
|
|
ContactInfo* c = the_mesh.lookupContactByPubKey(_node_prefs->loc_share_dm_prefix,
|
|
|
|
|
|
NodePrefs::FAVOURITE_PREFIX_LEN);
|
|
|
|
|
|
if (!c) return false;
|
|
|
|
|
|
snprintf(text, sizeof(text), LOCATION_MSG_TAG "%.5f,%.5f %s",
|
|
|
|
|
|
lat / 1e6, lon / 1e6, the_mesh.getNodeName());
|
|
|
|
|
|
uint32_t expected_ack = 0, est_timeout = 0;
|
|
|
|
|
|
return the_mesh.sendMessage(*c, rtc_clock.getCurrentTime(), 0, text, expected_ack, est_timeout) > 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// One-shot "share my position" from the home Map page (Hold Enter). When live
|
|
|
|
|
|
// sharing is already on, push an immediate [LOC] to the same target; otherwise
|
|
|
|
|
|
// hand a [LOC] message to the recipient picker so the user chooses where it
|
|
|
|
|
|
// goes (no accidental broadcast to a default channel).
|
|
|
|
|
|
void UITask::quickShareMyLocation() {
|
|
|
|
|
|
int32_t lat, lon;
|
|
|
|
|
|
if (!currentLocation(lat, lon)) { showAlert("No GPS fix", 1000); return; }
|
|
|
|
|
|
if (_node_prefs && _node_prefs->loc_share_enabled && sendLocationShare(lat, lon)) {
|
|
|
|
|
|
showAlert("Position shared", 900);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
char text[40];
|
|
|
|
|
|
snprintf(text, sizeof(text), LOCATION_MSG_TAG "%.5f,%.5f", lat / 1e6, lon / 1e6);
|
|
|
|
|
|
shareToMessage(text);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-03 08:23:11 +02:00
|
|
|
|
void UITask::saveWaypoints() {
|
|
|
|
|
|
DataStore* ds = the_mesh.getDataStore();
|
|
|
|
|
|
if (!ds) return;
|
|
|
|
|
|
File f = ds->openWrite("/waypoints");
|
|
|
|
|
|
if (!f) return;
|
|
|
|
|
|
_waypoints.writeTo(f);
|
|
|
|
|
|
f.close();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-09 15:09:57 +02:00
|
|
|
|
bool UITask::addWaypoint(int32_t lat, int32_t lon, uint32_t ts, const char* label) {
|
|
|
|
|
|
if (_waypoints.full()) { showAlert("Waypoints full", 1000); return false; }
|
|
|
|
|
|
if (_waypoints.add(lat, lon, ts, label)) {
|
|
|
|
|
|
saveWaypoints();
|
|
|
|
|
|
showAlert("Waypoint saved", 800);
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
showAlert("Waypoints full", 1000);
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
bool UITask::addWaypoint(int32_t lat, int32_t lon, const char* label) {
|
|
|
|
|
|
return addWaypoint(lat, lon, (uint32_t)rtc_clock.getCurrentTime(), label);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-08-08 20:01:31 +10:00
|
|
|
|
char UITask::checkDisplayOn(char c) {
|
2025-05-27 19:10:56 -07:00
|
|
|
|
if (_display != NULL) {
|
2025-08-08 20:01:31 +10:00
|
|
|
|
if (!_display->isOn()) {
|
2026-05-10 18:01:59 +02:00
|
|
|
|
_display->turnOn();
|
|
|
|
|
|
#ifdef PIN_LED
|
|
|
|
|
|
digitalWrite(PIN_LED, LOW); // ensure LED is off when waking display (userLedHandler takes over)
|
|
|
|
|
|
#endif
|
2026-05-17 09:55:51 +02:00
|
|
|
|
if (_locked) {
|
|
|
|
|
|
_lock_wake_until = millis() + 5000;
|
|
|
|
|
|
_next_refresh = 0;
|
|
|
|
|
|
return 0; // eat the waking key press
|
|
|
|
|
|
}
|
refactor(ui): apply code review fixes — bugs, risk, optimisations, cleanup
Fixes 7 bugs (timing UB on unsigned long, abs() no-op, XBM CRC PROGMEM
scan, CayenneLPP heap thrash ×2, UTF-8 reply prefix, lock-seq reset on
wake), 3 risk items (notif melody stop guard, bot trigger pre-lowercase,
lock-seq cleared on display wake), plus optimisations (fmtMsgAge/
getTextWidth caching, RTTTL builder consolidated to NodePrefs, HP_*
constants and homePageLabel table moved to NodePrefs, DataStore flat
lambda schema, translateUTF8Static factored out) and minor cleanup.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-23 20:03:45 +02:00
|
|
|
|
_lock_seq_count = 0;
|
|
|
|
|
|
_lock_seq_used = false;
|
2025-08-08 20:01:31 +10:00
|
|
|
|
c = 0;
|
2025-05-27 19:10:56 -07:00
|
|
|
|
}
|
2026-05-17 09:55:51 +02:00
|
|
|
|
if (!_locked) {
|
|
|
|
|
|
uint32_t aoff = autoOffMillis();
|
|
|
|
|
|
if (aoff > 0) _auto_off = millis() + aoff; // extend auto-off timer
|
|
|
|
|
|
}
|
2025-08-08 20:01:31 +10:00
|
|
|
|
_next_refresh = 0; // trigger refresh
|
2025-05-27 19:10:56 -07:00
|
|
|
|
}
|
2025-08-08 20:01:31 +10:00
|
|
|
|
return c;
|
2025-05-27 19:10:56 -07:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-08-08 20:01:31 +10:00
|
|
|
|
char UITask::handleLongPress(char c) {
|
|
|
|
|
|
if (millis() - ui_started_at < 8000) { // long press in first 8 seconds since startup -> CLI/rescue
|
|
|
|
|
|
the_mesh.enterCLIRescue();
|
2026-05-11 18:56:26 +02:00
|
|
|
|
return 0;
|
2025-05-29 13:06:24 -07:00
|
|
|
|
}
|
2026-05-11 18:56:26 +02:00
|
|
|
|
if (c == KEY_ENTER) return KEY_CONTEXT_MENU;
|
2025-08-08 20:01:31 +10:00
|
|
|
|
return c;
|
2025-05-27 19:10:56 -07:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-03 08:25:59 +10:00
|
|
|
|
char UITask::handleDoubleClick(char c) {
|
2026-06-02 22:12:18 +02:00
|
|
|
|
MESH_DEBUG_PRINTLN("UITask: double-click triggered");
|
2025-09-03 08:31:38 +10:00
|
|
|
|
checkDisplayOn(c);
|
2025-09-03 08:25:59 +10:00
|
|
|
|
return c;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
char UITask::handleTripleClick(char c) {
|
2025-09-03 08:31:38 +10:00
|
|
|
|
checkDisplayOn(c);
|
2025-09-03 08:25:59 +10:00
|
|
|
|
toggleBuzzer();
|
refactor(ui): apply code review fixes — bugs, risk, optimisations, cleanup
Fixes 7 bugs (timing UB on unsigned long, abs() no-op, XBM CRC PROGMEM
scan, CayenneLPP heap thrash ×2, UTF-8 reply prefix, lock-seq reset on
wake), 3 risk items (notif melody stop guard, bot trigger pre-lowercase,
lock-seq cleared on display wake), plus optimisations (fmtMsgAge/
getTextWidth caching, RTTTL builder consolidated to NodePrefs, HP_*
constants and homePageLabel table moved to NodePrefs, DataStore flat
lambda schema, translateUTF8Static factored out) and minor cleanup.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-23 20:03:45 +02:00
|
|
|
|
return 0;
|
2025-09-03 08:25:59 +10:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-23 10:39:43 +02:00
|
|
|
|
bool UITask::getGPSState() {
|
|
|
|
|
|
if (_sensors != NULL) {
|
|
|
|
|
|
int num = _sensors->getNumSettings();
|
|
|
|
|
|
for (int i = 0; i < num; i++) {
|
|
|
|
|
|
if (strcmp(_sensors->getSettingName(i), "gps") == 0) {
|
|
|
|
|
|
return !strcmp(_sensors->getSettingValue(i), "1");
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-04-28 21:26:56 -07:00
|
|
|
|
}
|
2025-09-23 10:39:43 +02:00
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-26 14:11:48 +02:00
|
|
|
|
bool UITask::hasGPS() {
|
|
|
|
|
|
if (_sensors != NULL) {
|
|
|
|
|
|
int num = _sensors->getNumSettings();
|
|
|
|
|
|
for (int i = 0; i < num; i++) {
|
|
|
|
|
|
if (strcmp(_sensors->getSettingName(i), "gps") == 0) return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-03 08:25:59 +10:00
|
|
|
|
void UITask::toggleGPS() {
|
|
|
|
|
|
if (_sensors != NULL) {
|
|
|
|
|
|
// toggle GPS on/off
|
|
|
|
|
|
int num = _sensors->getNumSettings();
|
|
|
|
|
|
for (int i = 0; i < num; i++) {
|
|
|
|
|
|
if (strcmp(_sensors->getSettingName(i), "gps") == 0) {
|
|
|
|
|
|
if (strcmp(_sensors->getSettingValue(i), "1") == 0) {
|
|
|
|
|
|
_sensors->setSettingValue("gps", "0");
|
2025-11-29 16:37:10 +08:00
|
|
|
|
_node_prefs->gps_enabled = 0;
|
2025-09-17 08:53:50 +08:00
|
|
|
|
notify(UIEventType::ack);
|
2025-09-03 08:25:59 +10:00
|
|
|
|
} else {
|
|
|
|
|
|
_sensors->setSettingValue("gps", "1");
|
2025-11-29 16:37:10 +08:00
|
|
|
|
_node_prefs->gps_enabled = 1;
|
2025-09-17 08:53:50 +08:00
|
|
|
|
notify(UIEventType::ack);
|
2025-09-03 08:25:59 +10:00
|
|
|
|
}
|
2025-11-29 16:37:10 +08:00
|
|
|
|
the_mesh.savePrefs();
|
2025-12-11 09:26:09 +01:00
|
|
|
|
showAlert(_node_prefs->gps_enabled ? "GPS: Enabled" : "GPS: Disabled", 800);
|
2025-09-03 08:25:59 +10:00
|
|
|
|
_next_refresh = 0;
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-10 15:18:35 +02:00
|
|
|
|
void UITask::applyTxPower() {
|
|
|
|
|
|
if (_node_prefs == NULL) return;
|
2026-06-10 23:30:43 +02:00
|
|
|
|
// With APC on, tx_power_dbm is the ceiling — re-baseline the controller to it
|
|
|
|
|
|
// (which also sets the radio) so the live power tracks the new ceiling at once.
|
|
|
|
|
|
if (_node_prefs->tx_apc) { the_mesh.applyApc(); return; }
|
Merge upstream/main (v1.16.0) into wio-unified
Merge 254 upstream commits. Highlights relevant to the Wio fork: native
NRF52 companion power-saving (sleep when !hasPendingWork), preamble 16->32
for SF<9, generic sensor-registration model, CMD_SEND_RAW_PACKET, isEink()
display abstraction, KEEP_DISPLAY_ON_USB, contacts-sync/transient fixes.
Conflicts resolved (11 files):
- buzzer.h/.cpp: keep NRF52 include guard + our begin()->startup(); fold in
upstream doc comment.
- platformio (wio + eink): keep our slim env layout, add upstream kiss_modem
env (+ debug_tool=stlink on eink ble).
- GxEPDDisplay.h: take upstream isEink() override.
- EnvironmentSensorManager.h: take upstream's wider #if guard.
- DataStore.cpp: take upstream saveContacts(filter) signature, keep our
::openWrite (member openWrite would shadow the global 2-arg overload).
- main.cpp: keep our watchdog init + add board.onBootComplete(); adopt
upstream's `if(!hasPendingWork()) sleep` loop.
- MyMesh.h: version -> v1.16-solo.0, build date 6 Jun 2026.
- MyMesh.cpp: keep both CMD codes (SCREENSHOT 66 + RAW_PACKET 65), both field
inits, both handlers, our screenshot fns + upstream saveContacts/save_filter,
upstream hasPendingWork() + our MyMeshBot include.
- UITask.cpp: keep our solo splash/clock page/snprintf/buzzer init; merge
auto-off (+ opt-in KEEP_DISPLAY_ON_USB); merge low-batt shutdown (our EMA +
prefs threshold + upstream's !isExternalPowered() guard and eink-aware delay).
Post-merge drift fixes (upstream API/architecture changes, not conflicts):
- applyTxPower(): radio_set_tx_power() removed upstream -> radio_driver.setTxPower().
- Sensor refactor dropped the per-sensor *_initialized flags our
getAvailableLPPTypes() relied on; removed that override and the SENSORS page
now derives available LPP types from the populated sensors_lpp buffer.
README kept ours via .gitattributes merge=ours. Builds verified on all 5 Wio
envs (dual, eink dual, radio_ble, dual_dev, eink_dual_dev). Not yet
hardware-verified.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-06 17:21:41 +02:00
|
|
|
|
radio_driver.setTxPower(_node_prefs->tx_power_dbm);
|
2026-05-10 15:18:35 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-10 23:30:43 +02:00
|
|
|
|
void UITask::applyPowerSave() {
|
|
|
|
|
|
if (_node_prefs == NULL) return;
|
feat(repeater): politeness controls, dedicated radio profile, and diagnostics
Settings > Radio gains a repeater toggle, 16 community-suggested radio
presets plus manual Freq/SF/BW/CR tuning (digit-by-digit Freq editor),
4 persisted user preset slots, and the packet pool bumped 16->32 so
queued retransmits stop starving incoming-packet allocation while
relaying.
Four opt-in politeness knobs (skip-advert, max-hops, yield, min-SNR),
all flood-only and off by default, plus overhear suppression that
cancels a queued retransmit if a peer relays the same packet first.
Adaptive Power Control is suppressed while relaying (pins TX power to
the ceiling) and duty-cycle RX is forced off, since a repeater needs
to hear and relay at consistent power.
A dedicated Tools > Repeater screen consolidates the toggle, the
politeness knobs, and live forwarding stats, plus an optional "Custom"
radio profile — a dedicated frequency/SF/BW/CR for relaying, separate
from the companion's own network, band-matched to the companion
frequency by default and used everywhere a radio change can happen
(boot, on-device toggle, app-driven CMD_SET_RADIO_PARAMS, Settings
radio edits) so the device never silently falls back to the wrong
params mid-relay.
Diagnostics gains the actually-forwarded packet count, real heap-free
via mallinfo(), a reset-counters popup, and hardened loop-detect
bounds. Also: a frequency-floor fix and a float-equality preset-match
fix in the repeater profile logic, deduped BW-table/valCol/profile-
seeding helpers shared between Settings and the Repeater screen, and
a build.sh fix tolerating control characters in `pio project config`'s
JSON dump.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-20 09:07:00 +02:00
|
|
|
|
// A repeater must hear every packet to relay it, so duty-cycle RX (which sleeps
|
|
|
|
|
|
// between preamble checks) is forced off while repeating — the user's pref is
|
|
|
|
|
|
// kept and restored when the repeater is switched off.
|
|
|
|
|
|
radio_driver.setPowerSaving(_node_prefs->rx_powersave && !_node_prefs->client_repeat);
|
2026-06-10 23:30:43 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void UITask::applyApc() {
|
|
|
|
|
|
the_mesh.applyApc(); // (re)initialise Adaptive Power Control from prefs
|
|
|
|
|
|
}
|
|
|
|
|
|
|
feat(repeater): politeness controls, dedicated radio profile, and diagnostics
Settings > Radio gains a repeater toggle, 16 community-suggested radio
presets plus manual Freq/SF/BW/CR tuning (digit-by-digit Freq editor),
4 persisted user preset slots, and the packet pool bumped 16->32 so
queued retransmits stop starving incoming-packet allocation while
relaying.
Four opt-in politeness knobs (skip-advert, max-hops, yield, min-SNR),
all flood-only and off by default, plus overhear suppression that
cancels a queued retransmit if a peer relays the same packet first.
Adaptive Power Control is suppressed while relaying (pins TX power to
the ceiling) and duty-cycle RX is forced off, since a repeater needs
to hear and relay at consistent power.
A dedicated Tools > Repeater screen consolidates the toggle, the
politeness knobs, and live forwarding stats, plus an optional "Custom"
radio profile — a dedicated frequency/SF/BW/CR for relaying, separate
from the companion's own network, band-matched to the companion
frequency by default and used everywhere a radio change can happen
(boot, on-device toggle, app-driven CMD_SET_RADIO_PARAMS, Settings
radio edits) so the device never silently falls back to the wrong
params mid-relay.
Diagnostics gains the actually-forwarded packet count, real heap-free
via mallinfo(), a reset-counters popup, and hardened loop-detect
bounds. Also: a frequency-floor fix and a float-equality preset-match
fix in the repeater profile logic, deduped BW-table/valCol/profile-
seeding helpers shared between Settings and the Repeater screen, and
a build.sh fix tolerating control characters in `pio project config`'s
JSON dump.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-20 09:07:00 +02:00
|
|
|
|
void UITask::applyRadioParams() {
|
|
|
|
|
|
if (_node_prefs == NULL) return;
|
|
|
|
|
|
the_mesh.applyRepeaterRadio(); // companion params, or the repeater profile if relaying with one set
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-10 15:18:35 +02:00
|
|
|
|
void UITask::applyBrightness() {
|
|
|
|
|
|
if (_display != NULL && _node_prefs != NULL) {
|
|
|
|
|
|
_display->setBrightness(_node_prefs->display_brightness);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-20 09:29:38 +02:00
|
|
|
|
void UITask::applyFont() {
|
|
|
|
|
|
if (_display != NULL && _node_prefs != NULL) {
|
|
|
|
|
|
_display->setLemonFont(_node_prefs->use_lemon_font != 0);
|
2026-05-20 09:36:27 +02:00
|
|
|
|
_next_refresh = 0;
|
2026-05-20 09:29:38 +02:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-20 21:53:40 +02:00
|
|
|
|
void UITask::applyRotation() {
|
|
|
|
|
|
if (_display != NULL && _node_prefs != NULL) {
|
|
|
|
|
|
_display->setDisplayRotation(_node_prefs->display_rotation);
|
|
|
|
|
|
_next_refresh = 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-24 10:22:03 +02:00
|
|
|
|
void UITask::applyFullRefreshInterval() {
|
|
|
|
|
|
if (_display != NULL && _node_prefs != NULL) {
|
|
|
|
|
|
static const uint8_t OPTS[] = { 0, 5, 10, 20, 30 };
|
|
|
|
|
|
static const int OPTS_COUNT = 5;
|
|
|
|
|
|
uint8_t idx = _node_prefs->eink_full_refresh_every;
|
|
|
|
|
|
if (idx >= OPTS_COUNT) idx = 0;
|
|
|
|
|
|
_display->setFullRefreshInterval(OPTS[idx]);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-10 15:18:35 +02:00
|
|
|
|
void UITask::setBrightnessLevel(uint8_t level) {
|
|
|
|
|
|
if (_node_prefs == NULL) return;
|
|
|
|
|
|
if (level > 4) level = 4;
|
|
|
|
|
|
_node_prefs->display_brightness = level;
|
|
|
|
|
|
applyBrightness();
|
|
|
|
|
|
_next_refresh = 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-12 09:17:37 +02:00
|
|
|
|
void UITask::setBuzzerVolumeLevel(uint8_t level) {
|
|
|
|
|
|
#ifdef PIN_BUZZER
|
|
|
|
|
|
if (_node_prefs == NULL) return;
|
|
|
|
|
|
if (level > 4) level = 4;
|
|
|
|
|
|
_node_prefs->buzzer_volume = level;
|
|
|
|
|
|
buzzer.setVolume(level);
|
2026-05-20 09:06:00 +02:00
|
|
|
|
if (level > 0) buzzer.playForced("Vol:d=16,o=6,b=120:c");
|
2026-05-12 09:17:37 +02:00
|
|
|
|
_next_refresh = 0;
|
|
|
|
|
|
#endif
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-03 08:25:59 +10:00
|
|
|
|
void UITask::toggleBuzzer() {
|
2025-05-27 19:10:56 -07:00
|
|
|
|
#ifdef PIN_BUZZER
|
2026-05-12 20:12:54 +02:00
|
|
|
|
if (_node_prefs) _node_prefs->buzzer_auto = 0; // exit auto mode
|
2025-05-30 22:55:53 -07:00
|
|
|
|
if (buzzer.isQuiet()) {
|
|
|
|
|
|
buzzer.quiet(false);
|
2025-09-17 08:53:50 +08:00
|
|
|
|
notify(UIEventType::ack);
|
2025-05-30 22:55:53 -07:00
|
|
|
|
} else {
|
|
|
|
|
|
buzzer.quiet(true);
|
|
|
|
|
|
}
|
2026-05-12 20:12:54 +02:00
|
|
|
|
if (_node_prefs) _node_prefs->buzzer_quiet = buzzer.isQuiet();
|
2025-11-20 18:55:39 -08:00
|
|
|
|
the_mesh.savePrefs();
|
2025-12-11 09:26:09 +01:00
|
|
|
|
showAlert(buzzer.isQuiet() ? "Buzzer: OFF" : "Buzzer: ON", 800);
|
2026-05-12 20:12:54 +02:00
|
|
|
|
_next_refresh = 0;
|
2025-05-27 19:10:56 -07:00
|
|
|
|
#endif
|
|
|
|
|
|
}
|
2026-05-12 20:12:54 +02:00
|
|
|
|
|
|
|
|
|
|
int UITask::getBuzzerMode() {
|
|
|
|
|
|
#ifdef PIN_BUZZER
|
|
|
|
|
|
if (_node_prefs && _node_prefs->buzzer_auto) return 2;
|
|
|
|
|
|
return buzzer.isQuiet() ? 1 : 0;
|
|
|
|
|
|
#else
|
|
|
|
|
|
return 1;
|
|
|
|
|
|
#endif
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void UITask::cycleBuzzerMode() {
|
|
|
|
|
|
#ifdef PIN_BUZZER
|
|
|
|
|
|
if (!_node_prefs) return;
|
|
|
|
|
|
int mode = getBuzzerMode();
|
|
|
|
|
|
mode = (mode + 1) % 3; // ON(0) → OFF(1) → Auto(2) → ON
|
|
|
|
|
|
_node_prefs->buzzer_auto = (mode == 2) ? 1 : 0;
|
|
|
|
|
|
if (mode == 0) { buzzer.quiet(false); _node_prefs->buzzer_quiet = 0; notify(UIEventType::ack); }
|
|
|
|
|
|
if (mode == 1) { buzzer.quiet(true); _node_prefs->buzzer_quiet = 1; }
|
2026-06-05 12:42:14 +02:00
|
|
|
|
if (mode == 2) { buzzer.quiet(isClientConnected()); }
|
2026-05-12 20:12:54 +02:00
|
|
|
|
static const char* labels[] = { "Buzzer: ON", "Buzzer: OFF", "Buzzer: Auto" };
|
|
|
|
|
|
showAlert(labels[mode], 800);
|
|
|
|
|
|
_next_refresh = 0;
|
|
|
|
|
|
#endif
|
|
|
|
|
|
}
|