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-06-29 18:20:47 +02:00
|
|
|
|
// ── Screen fragments — included into THIS translation unit only ───────────────
|
|
|
|
|
|
// These headers are not standalone: they are compiled solely as part of
|
|
|
|
|
|
// UITask.cpp, in the order below. Two consequences a new screen must respect:
|
|
|
|
|
|
// • Order matters. A `static inline` helper (drawList, msgReplyBody, geo::…)
|
|
|
|
|
|
// or a shared scratch buffer (FullscreenMsgView's s_wrap_*) is only visible
|
|
|
|
|
|
// to fragments included *after* the one that defines it. Add new screens
|
|
|
|
|
|
// after their dependencies.
|
|
|
|
|
|
// • Single-TU only. Some fragments define external-linkage symbols at file
|
|
|
|
|
|
// scope (e.g. NearbyScreen::FILTER_LABELS), so including any of them from a
|
|
|
|
|
|
// second .cpp is a duplicate-symbol link error. Keep them UITask-internal;
|
|
|
|
|
|
// anything genuinely shareable belongs in a real header (icons.h, GeoUtils.h).
|
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"
|
feat(ui): on-device channel management, remote admin tool, per-language keyboards
Messages:
- Add/edit/delete channels on-device (new ChannelsView, owned by the renamed
MessagesScreen — was QuickMsgScreen, whose name no longer matched its scope).
Channel secret entry supports a typed passphrase (SHA-256'd, same primitive
the library already uses for the routing hash) or a raw 32-hex-char key.
- MyMesh::setChannelLocal() factors out the setChannel/saveChannels/
onChannelRemoved sequence previously duplicated across the two
CMD_SET_CHANNEL branches, shared now by the BLE and on-device paths.
Tools > Admin (new):
- Log into a repeater/room server's admin account and send CLI commands,
the on-device equivalent of the app's repeater-admin feature.
- Commands are organised into category tabs (System/Radio/Routing/Actions)
with common get/set fields (name, radio profile, tx power, repeat, advert
intervals, ...) plus a free-text "Custom command..." fallback for anything
else. A field row fetches the current value, opens it pre-filled for
editing, and sends the change — falling back to a blank editor if the
fetch fails or times out.
- The admin password persists and self-heals exactly like room logins in
Messages: saved on a confirmed admin-level login, forgotten on a failed
one, left alone if merely under-privileged.
- New MyMesh::sendAdminCommand()/AbstractUITask::onAdminReply() plumbing so
a reply reaches the UI without touching the existing BLE/app CLI-terminal
path (queueMessage's should_display gate is untouched).
Shared TabBar.h extracted from NearbyScreen/BotScreen's independently
duplicated tab-carousel rendering (now a third consumer via Admin) — also
fixes neighbouring tabs vanishing outright when they didn't fully fit;
they now truncate with an ellipsis instead.
Keyboard: the combined "Ext.Latin" alphabet split into 8 separate,
linguistically complete per-language keyboards (Polish, Czech, Slovak,
German, French, Spanish, Portuguese, Nordic), and fixed an OLED-only bug
where tall accented glyphs overlapped the keyboard's separator line
(SH1106's Lemon-font ascent constant was 2-3px short for them).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-12 19:40:53 +02:00
|
|
|
|
#include "MessageHistory.h" // RAM history rings (DM + channel) used by MessagesScreen
|
|
|
|
|
|
#include "MessagesScreen.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(ui): on-device channel management, remote admin tool, per-language keyboards
Messages:
- Add/edit/delete channels on-device (new ChannelsView, owned by the renamed
MessagesScreen — was QuickMsgScreen, whose name no longer matched its scope).
Channel secret entry supports a typed passphrase (SHA-256'd, same primitive
the library already uses for the routing hash) or a raw 32-hex-char key.
- MyMesh::setChannelLocal() factors out the setChannel/saveChannels/
onChannelRemoved sequence previously duplicated across the two
CMD_SET_CHANNEL branches, shared now by the BLE and on-device paths.
Tools > Admin (new):
- Log into a repeater/room server's admin account and send CLI commands,
the on-device equivalent of the app's repeater-admin feature.
- Commands are organised into category tabs (System/Radio/Routing/Actions)
with common get/set fields (name, radio profile, tx power, repeat, advert
intervals, ...) plus a free-text "Custom command..." fallback for anything
else. A field row fetches the current value, opens it pre-filled for
editing, and sends the change — falling back to a blank editor if the
fetch fails or times out.
- The admin password persists and self-heals exactly like room logins in
Messages: saved on a confirmed admin-level login, forgotten on a failed
one, left alone if merely under-privileged.
- New MyMesh::sendAdminCommand()/AbstractUITask::onAdminReply() plumbing so
a reply reaches the UI without touching the existing BLE/app CLI-terminal
path (queueMessage's should_display gate is untouched).
Shared TabBar.h extracted from NearbyScreen/BotScreen's independently
duplicated tab-carousel rendering (now a third consumer via Admin) — also
fixes neighbouring tabs vanishing outright when they didn't fully fit;
they now truncate with an ellipsis instead.
Keyboard: the combined "Ext.Latin" alphabet split into 8 separate,
linguistically complete per-language keyboards (Polish, Czech, Slovak,
German, French, Spanish, Portuguese, Nordic), and fixed an OLED-only bug
where tall accented glyphs overlapped the keyboard's separator line
(SH1106's Lemon-font ascent constant was 2-3px short for them).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-12 19:40:53 +02:00
|
|
|
|
#include "AdminScreen.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-07-21 20:30:03 +02:00
|
|
|
|
#if defined(PIN_GPIO1)
|
|
|
|
|
|
#include "GpioScreen.h"
|
|
|
|
|
|
#endif
|
2026-05-12 14:45:02 +02:00
|
|
|
|
#include "ToolsScreen.h"
|
feat(companion): clock tools — alarm, countdown timer, stopwatch
Add a Clock Tools screen (Enter on the home Clock page) with three time
utilities, plus the engine that drives them from UITask::loop() so they
work regardless of the current screen / display state:
- Alarm: a single one-shot wake alarm. Persisted in NodePrefs as a local
time-of-day; UITask schedules it as an ABSOLUTE fire instant recomputed
from that time, so it survives RTC re-syncs (mesh/app/GPS/CLI all jump
the clock) — small corrections still fire on time, a jump over the
target still fires (late, up to 6 h). Disarms after firing. A bell
icon signals an armed alarm on the clock face and the top status bar.
- Timer: a millis-based countdown (sync-immune), big HH:MM:SS readout,
rings even when off-screen.
- Stopwatch: millis-based, keeps running in the background.
Numeric fields use the shared framework DigitEditor (digit-by-digit,
min/max enforced). The alarm/timer/ring engine lives in UITask alongside
the locator/live-share engines (no screen-cast for time-critical logic).
E-ink: live readouts refresh coarsely (and on any key); timing is exact
regardless, and the countdown's buzzer fires on time. Rings override mute
and are dismissed by any key (auto-stop after 1 min). Cannot wake from a
full Shutdown (CPU/RAM powered down).
NodePrefs: +alarm_on/alarm_hour/alarm_min (fit existing tail padding,
sizeof unchanged 2488), SCHEMA_SENTINEL 0xC0DE0016 -> 0xC0DE0017,
DataStore read/clamp/write in lockstep.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 01:54:01 +02:00
|
|
|
|
#include "ClockToolsScreen.h" // Alarm / Timer / Stopwatch (Clock page › 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
|
|
|
|
|
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;
|
|
|
|
|
|
|
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;
|
2026-07-02 10:34:38 +02:00
|
|
|
|
if (page == MAP) return NodePrefs::HPB_MAP;
|
2026-05-24 22:26:56 +02:00
|
|
|
|
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-07-02 10:34:38 +02:00
|
|
|
|
case NodePrefs::HPB_MAP: return MAP;
|
2026-05-22 23:33:06 +02:00
|
|
|
|
default: return -1;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-12 09:51:30 +02:00
|
|
|
|
bool isPageVisible(int page) const {
|
2026-07-07 01:53:02 +02:00
|
|
|
|
if (page == RECENT) return false; // Recent adverts folded into Nearby Nodes; page retired
|
2026-05-12 09:51:30 +02:00
|
|
|
|
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-07-17 21:02:33 +02:00
|
|
|
|
const int ind_h = display.isSingleFont() ? lh - 2 : lh;
|
2026-05-23 14:45:24 +02:00
|
|
|
|
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-07-15 15:36:39 +02:00
|
|
|
|
} else { // icon — scales with lh, same box height as the status icons beside it (ind_h)
|
|
|
|
|
|
const int iconH = ind_h;
|
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);
|
2026-07-15 15:36:39 +02:00
|
|
|
|
// Nub height/2, vertically centred by remaining-space/2 rather than a flat
|
|
|
|
|
|
// iconH/4 margin — the flat form only centres when iconH is a multiple of
|
|
|
|
|
|
// 4 (true for the old built-in font's lh=8, false for misc-fixed's 7/9),
|
|
|
|
|
|
// so it visibly drifted off-centre once the box height changed.
|
|
|
|
|
|
const int nub_h = iconH / 2;
|
|
|
|
|
|
display.fillRect(battLeftX + iconW, (iconH - nub_h) / 2, 2, nub_h);
|
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
|
|
|
|
|
2026-07-07 10:30:48 +02:00
|
|
|
|
// Secondary status icons, laid out right→left in PRIORITY order so a crowded
|
|
|
|
|
|
// bar sheds its least-important cues instead of crushing the node name. Once
|
|
|
|
|
|
// an icon won't fit above the reserved name area, every lower-priority icon
|
|
|
|
|
|
// after it is dropped too (the list is ordered high→low). A blinking icon
|
|
|
|
|
|
// still reserves its slot while off, so the name width doesn't flicker.
|
|
|
|
|
|
//
|
|
|
|
|
|
// Priority: BT > GPS fix > alarm > mute > auto-advert > trail > live-share >
|
|
|
|
|
|
// repeater. Battery (drawn above) is always rightmost. The background modes
|
|
|
|
|
|
// (advert / trail / live-share / repeater) stay outside any BT gate — they
|
|
|
|
|
|
// keep running with Bluetooth off, so their cue must not vanish with it.
|
|
|
|
|
|
LocationProvider* loc = _sensors ? _sensors->getLocationProvider() : nullptr;
|
|
|
|
|
|
bool gps_on = loc && _node_prefs && _node_prefs->gps_enabled;
|
|
|
|
|
|
bool mute_on = false;
|
2026-02-11 09:51:28 +01:00
|
|
|
|
#ifdef PIN_BUZZER
|
2026-07-07 10:30:48 +02:00
|
|
|
|
mute_on = _task->isBuzzerQuiet();
|
2026-02-11 09:51:28 +01:00
|
|
|
|
#endif
|
2026-07-07 10:30:48 +02:00
|
|
|
|
struct Sicon { bool active; const MiniIcon* icon; bool boxed; bool blink; };
|
|
|
|
|
|
const Sicon icons[] = {
|
|
|
|
|
|
{ _task->isSerialEnabled(), &ICON_BLUETOOTH, _task->isSerialEnabled() && _task->isBLEConnected(), false },
|
|
|
|
|
|
{ gps_on, &ICON_GPS, gps_on && loc->isValid(), false },
|
|
|
|
|
|
{ _node_prefs && _node_prefs->alarm_on, &ICON_ALARM, true, false },
|
|
|
|
|
|
{ mute_on, &ICON_MUTE, true, false },
|
|
|
|
|
|
{ _node_prefs && _node_prefs->advert_auto_interval_sec > 0, &ICON_ADVERT, true, true },
|
|
|
|
|
|
{ _task->trail().isActive(), &ICON_TRAIL, true, true },
|
|
|
|
|
|
{ _node_prefs && _node_prefs->loc_share_enabled, &ICON_MAP_CONTACT, true, true },
|
|
|
|
|
|
{ _node_prefs && _node_prefs->client_repeat, &ICON_REPEATER, true, true },
|
|
|
|
|
|
};
|
2026-05-11 16:52:24 +02:00
|
|
|
|
|
2026-07-07 10:30:48 +02:00
|
|
|
|
int x = battLeftX;
|
|
|
|
|
|
const int name_min = display.getCharWidth() * 5; // always keep ~5 chars for the name
|
|
|
|
|
|
for (const Sicon& s : icons) {
|
|
|
|
|
|
if (!s.active) continue;
|
|
|
|
|
|
int ix = x - ind - ind_gap;
|
|
|
|
|
|
if (ix < name_min) break; // out of room — drop this + all lower priority
|
|
|
|
|
|
if (!s.blink || blinkOn()) {
|
|
|
|
|
|
if (s.boxed) drawBoxedIcon(display, ix, ind, ind_h, *s.icon);
|
|
|
|
|
|
else drawSlotIcon(display, ix, ind, ind_h, *s.icon);
|
|
|
|
|
|
}
|
|
|
|
|
|
x = ix;
|
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
|
|
|
|
}
|
2026-07-07 10:30:48 +02:00
|
|
|
|
return x;
|
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
|
|
|
|
|
feat(companion): clock tools — alarm, countdown timer, stopwatch
Add a Clock Tools screen (Enter on the home Clock page) with three time
utilities, plus the engine that drives them from UITask::loop() so they
work regardless of the current screen / display state:
- Alarm: a single one-shot wake alarm. Persisted in NodePrefs as a local
time-of-day; UITask schedules it as an ABSOLUTE fire instant recomputed
from that time, so it survives RTC re-syncs (mesh/app/GPS/CLI all jump
the clock) — small corrections still fire on time, a jump over the
target still fires (late, up to 6 h). Disarms after firing. A bell
icon signals an armed alarm on the clock face and the top status bar.
- Timer: a millis-based countdown (sync-immune), big HH:MM:SS readout,
rings even when off-screen.
- Stopwatch: millis-based, keeps running in the background.
Numeric fields use the shared framework DigitEditor (digit-by-digit,
min/max enforced). The alarm/timer/ring engine lives in UITask alongside
the locator/live-share engines (no screen-cast for time-critical logic).
E-ink: live readouts refresh coarsely (and on any key); timing is exact
regardless, and the countdown's buzzer fires on time. Rings override mute
and are dismissed by any key (auto-stop after 1 min). Cannot wake from a
full Shutdown (CPU/RAM powered down).
NodePrefs: +alarm_on/alarm_hour/alarm_min (fit existing tail padding,
sizeof unchanged 2488), SCHEMA_SENTINEL 0xC0DE0016 -> 0xC0DE0017,
DataStore read/clamp/write in lockstep.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 01:54:01 +02:00
|
|
|
|
// Alarm armed: a small bell in the top-left corner. The status bar (and
|
|
|
|
|
|
// its bell) is hidden on this page, so signal the armed alarm here. Just
|
|
|
|
|
|
// the glyph — no time text — so it stays clear of the centred clock
|
|
|
|
|
|
// digits (which can reach the corner when seconds are shown), matching
|
|
|
|
|
|
// the icon-only status-bar indicator. The exact time is in Clock Tools.
|
|
|
|
|
|
if (_node_prefs && _node_prefs->alarm_on) {
|
|
|
|
|
|
display.setColor(DisplayDriver::LIGHT);
|
|
|
|
|
|
miniIconDrawTop(display, 0, 0, ICON_ALARM);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
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::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;
|
|
|
|
|
|
|
2026-07-05 22:40:14 +02:00
|
|
|
|
bool fav_changed = false; // a stale (gone) slot was pruned this pass → persist once after the loop
|
2026-05-24 23:00:27 +02:00
|
|
|
|
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; }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-06 20:18:36 +02:00
|
|
|
|
ContactInfo ci;
|
|
|
|
|
|
bool has_contact = false;
|
2026-05-24 23:00:27 +02:00
|
|
|
|
if (filled) {
|
|
|
|
|
|
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) {
|
2026-07-06 20:18:36 +02:00
|
|
|
|
ci = c; has_contact = true; break;
|
2026-05-24 23:00:27 +02:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-07-06 20:18:36 +02:00
|
|
|
|
if (!has_contact && _node_prefs) {
|
2026-07-05 22:40:14 +02:00
|
|
|
|
// Pinned contact is gone — prefs outlived the contact list (e.g. a
|
|
|
|
|
|
// wiped /contacts3; onContactRemoved only catches a live delete).
|
2026-07-06 20:18:36 +02:00
|
|
|
|
// Clear the stale slot so it renders as an empty "+" tile (below)
|
|
|
|
|
|
// instead of "(gone)". Persisted once after the loop.
|
|
|
|
|
|
memset(_node_prefs->favourite_contacts[i], 0, NodePrefs::FAVOURITE_PREFIX_LEN);
|
2026-07-05 22:40:14 +02:00
|
|
|
|
fav_changed = true;
|
|
|
|
|
|
}
|
2026-07-06 20:18:36 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (has_contact) {
|
2026-05-24 23:00:27 +02:00
|
|
|
|
char name[24];
|
2026-07-05 22:40:14 +02:00
|
|
|
|
display.translateUTF8ToBlocks(name, ci.name, sizeof(name));
|
2026-05-24 23:00:27 +02:00
|
|
|
|
|
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.
|
2026-07-05 22:51:56 +02:00
|
|
|
|
uint8_t unread = _task->getDMUnread(ci.id.pub_key);
|
feat(ui): pill unread badges, unified DM/CH headers, trimmed home carousel
UI-polish trio from CODE_REVIEW (biggest "feels finished" gain per line):
- Pill unread badges: new DisplayDriver::drawUnreadBadge()/unreadBadgeWidth()
draw a filled capsule with the count knocked out (corners knocked back for a
rounded look; inverts on a selected row). Replaces the bare right-aligned
digits in MODE_SELECT, the contact/channel pickers and favourites tiles.
No <stdio.h> in the header — fmtBadgeCount formats manually, clamps to 99+.
- Unified DM/CH history headers: DM_HIST and CHANNEL_HIST drew their titles by
hand (drawTextCentered + fillRect at lh+1), a different height/separator than
every other screen. Both now route through drawCenteredHeader().
- Trimmed default home carousel: new NodePrefs::HP_DEFAULT (Clock, Tools,
Shutdown, Favourites, Map; Messages + Settings always visible = 7 pages).
applyDefaults() seeds it instead of HP_ALL. Recent/Radio/BT/Advert/GPS/Sensors
are opt-in via Settings > Home Pages. Existing users keep their saved mask —
factory default only; no migration, no schema bump.
Both solo envs build green (OLED RAM 69.9%/Flash 62.8%; e-ink SUCCESS).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 01:12:51 +02:00
|
|
|
|
int bw = unread > 0 ? display.unreadBadgeWidth(unread) + 3 : 0; // badge + 3 px gap
|
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);
|
feat(ui): pill unread badges, unified DM/CH headers, trimmed home carousel
UI-polish trio from CODE_REVIEW (biggest "feels finished" gain per line):
- Pill unread badges: new DisplayDriver::drawUnreadBadge()/unreadBadgeWidth()
draw a filled capsule with the count knocked out (corners knocked back for a
rounded look; inverts on a selected row). Replaces the bare right-aligned
digits in MODE_SELECT, the contact/channel pickers and favourites tiles.
No <stdio.h> in the header — fmtBadgeCount formats manually, clamps to 99+.
- Unified DM/CH history headers: DM_HIST and CHANNEL_HIST drew their titles by
hand (drawTextCentered + fillRect at lh+1), a different height/separator than
every other screen. Both now route through drawCenteredHeader().
- Trimmed default home carousel: new NodePrefs::HP_DEFAULT (Clock, Tools,
Shutdown, Favourites, Map; Messages + Settings always visible = 7 pages).
applyDefaults() seeds it instead of HP_ALL. Recent/Radio/BT/Advert/GPS/Sensors
are opt-in via Settings > Home Pages. Existing users keep their saved mask —
factory default only; no migration, no schema bump.
Both solo envs build green (OLED RAM 69.9%/Flash 62.8%; e-ink SUCCESS).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 01:12:51 +02:00
|
|
|
|
if (unread > 0)
|
|
|
|
|
|
display.drawUnreadBadge(cx + cell_w - 2, name_y, unread, sel);
|
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-07-05 22:40:14 +02:00
|
|
|
|
// Persist any pruned slots once, outside the loop — a render pass can clear
|
|
|
|
|
|
// several gone tiles but only one flash write is needed. Self-healing: once
|
|
|
|
|
|
// cleared, the slot is empty next frame so this can't re-fire per frame.
|
|
|
|
|
|
if (fav_changed) the_mesh.savePrefs();
|
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
|
|
|
|
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) {
|
feat(ui): on-device channel management, remote admin tool, per-language keyboards
Messages:
- Add/edit/delete channels on-device (new ChannelsView, owned by the renamed
MessagesScreen — was QuickMsgScreen, whose name no longer matched its scope).
Channel secret entry supports a typed passphrase (SHA-256'd, same primitive
the library already uses for the routing hash) or a raw 32-hex-char key.
- MyMesh::setChannelLocal() factors out the setChannel/saveChannels/
onChannelRemoved sequence previously duplicated across the two
CMD_SET_CHANNEL branches, shared now by the BLE and on-device paths.
Tools > Admin (new):
- Log into a repeater/room server's admin account and send CLI commands,
the on-device equivalent of the app's repeater-admin feature.
- Commands are organised into category tabs (System/Radio/Routing/Actions)
with common get/set fields (name, radio profile, tx power, repeat, advert
intervals, ...) plus a free-text "Custom command..." fallback for anything
else. A field row fetches the current value, opens it pre-filled for
editing, and sends the change — falling back to a blank editor if the
fetch fails or times out.
- The admin password persists and self-heals exactly like room logins in
Messages: saved on a confirmed admin-level login, forgotten on a failed
one, left alone if merely under-privileged.
- New MyMesh::sendAdminCommand()/AbstractUITask::onAdminReply() plumbing so
a reply reaches the UI without touching the existing BLE/app CLI-terminal
path (queueMessage's should_display gate is untouched).
Shared TabBar.h extracted from NearbyScreen/BotScreen's independently
duplicated tab-carousel rendering (now a third consumer via Admin) — also
fixes neighbouring tabs vanishing outright when they didn't fully fit;
they now truncate with an ellipsis instead.
Keyboard: the combined "Ext.Latin" alphabet split into 8 separate,
linguistically complete per-language keyboards (Polish, Czech, Slovak,
German, French, Spanish, Portuguese, Nordic), and fixed an OLED-only bug
where tall accented glyphs overlapped the keyboard's separator line
(SH1106's Lemon-font ascent constant was 2-3px short for them).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-12 19:40:53 +02:00
|
|
|
|
_task->gotoMessagesScreen();
|
2026-05-10 18:45:35 +02:00
|
|
|
|
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(companion): clock tools — alarm, countdown timer, stopwatch
Add a Clock Tools screen (Enter on the home Clock page) with three time
utilities, plus the engine that drives them from UITask::loop() so they
work regardless of the current screen / display state:
- Alarm: a single one-shot wake alarm. Persisted in NodePrefs as a local
time-of-day; UITask schedules it as an ABSOLUTE fire instant recomputed
from that time, so it survives RTC re-syncs (mesh/app/GPS/CLI all jump
the clock) — small corrections still fire on time, a jump over the
target still fires (late, up to 6 h). Disarms after firing. A bell
icon signals an armed alarm on the clock face and the top status bar.
- Timer: a millis-based countdown (sync-immune), big HH:MM:SS readout,
rings even when off-screen.
- Stopwatch: millis-based, keeps running in the background.
Numeric fields use the shared framework DigitEditor (digit-by-digit,
min/max enforced). The alarm/timer/ring engine lives in UITask alongside
the locator/live-share engines (no screen-cast for time-critical logic).
E-ink: live readouts refresh coarsely (and on any key); timing is exact
regardless, and the countdown's buzzer fires on time. Rings override mute
and are dismissed by any key (auto-stop after 1 min). Cannot wake from a
full Shutdown (CPU/RAM powered down).
NodePrefs: +alarm_on/alarm_hour/alarm_min (fit existing tail padding,
sizeof unchanged 2488), SCHEMA_SENTINEL 0xC0DE0016 -> 0xC0DE0017,
DataStore read/clamp/write in lockstep.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 01:54:01 +02:00
|
|
|
|
if (c == KEY_ENTER && _page == HomePage::CLOCK) {
|
|
|
|
|
|
_task->gotoClockTools(); // Alarm / Timer / Stopwatch
|
|
|
|
|
|
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;
|
2026-07-02 12:21:52 +02:00
|
|
|
|
_kb.prefs = node_prefs;
|
2026-05-10 15:18:35 +02:00
|
|
|
|
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
|
2026-06-30 22:10:18 +02:00
|
|
|
|
#if UI_HAS_JOYSTICK
|
|
|
|
|
|
// The directional joystick + Back share the same MomentaryButton machinery as
|
|
|
|
|
|
// user_btn but were never begin()'d — they only worked because the pins
|
|
|
|
|
|
// default to INPUT and the board has external pulls. That left them on the
|
|
|
|
|
|
// polling path: with BUTTON_USE_INTERRUPTS (e-ink) they'd silently never
|
|
|
|
|
|
// attach a GPIOTE channel, so edges landing during a blocking panel refresh
|
|
|
|
|
|
// were lost. begin() sets pinMode and claims an IRQ slot for each.
|
|
|
|
|
|
joystick_left.begin();
|
|
|
|
|
|
joystick_right.begin();
|
|
|
|
|
|
back_btn.begin();
|
|
|
|
|
|
#if UI_HAS_JOYSTICK_UPDOWN
|
|
|
|
|
|
joystick_up.begin();
|
|
|
|
|
|
joystick_down.begin();
|
|
|
|
|
|
#endif
|
|
|
|
|
|
#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);
|
feat(ui): on-device channel management, remote admin tool, per-language keyboards
Messages:
- Add/edit/delete channels on-device (new ChannelsView, owned by the renamed
MessagesScreen — was QuickMsgScreen, whose name no longer matched its scope).
Channel secret entry supports a typed passphrase (SHA-256'd, same primitive
the library already uses for the routing hash) or a raw 32-hex-char key.
- MyMesh::setChannelLocal() factors out the setChannel/saveChannels/
onChannelRemoved sequence previously duplicated across the two
CMD_SET_CHANNEL branches, shared now by the BLE and on-device paths.
Tools > Admin (new):
- Log into a repeater/room server's admin account and send CLI commands,
the on-device equivalent of the app's repeater-admin feature.
- Commands are organised into category tabs (System/Radio/Routing/Actions)
with common get/set fields (name, radio profile, tx power, repeat, advert
intervals, ...) plus a free-text "Custom command..." fallback for anything
else. A field row fetches the current value, opens it pre-filled for
editing, and sends the change — falling back to a blank editor if the
fetch fails or times out.
- The admin password persists and self-heals exactly like room logins in
Messages: saved on a confirmed admin-level login, forgotten on a failed
one, left alone if merely under-privileged.
- New MyMesh::sendAdminCommand()/AbstractUITask::onAdminReply() plumbing so
a reply reaches the UI without touching the existing BLE/app CLI-terminal
path (queueMessage's should_display gate is untouched).
Shared TabBar.h extracted from NearbyScreen/BotScreen's independently
duplicated tab-carousel rendering (now a third consumer via Admin) — also
fixes neighbouring tabs vanishing outright when they didn't fully fit;
they now truncate with an ellipsis instead.
Keyboard: the combined "Ext.Latin" alphabet split into 8 separate,
linguistically complete per-language keyboards (Polish, Czech, Slovak,
German, French, Spanish, Portuguese, Nordic), and fixed an OLED-only bug
where tall accented glyphs overlapped the keyboard's separator line
(SH1106's Lemon-font ascent constant was 2-3px short for them).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-12 19:40:53 +02:00
|
|
|
|
messages_screen = new MessagesScreen(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(ui): on-device channel management, remote admin tool, per-language keyboards
Messages:
- Add/edit/delete channels on-device (new ChannelsView, owned by the renamed
MessagesScreen — was QuickMsgScreen, whose name no longer matched its scope).
Channel secret entry supports a typed passphrase (SHA-256'd, same primitive
the library already uses for the routing hash) or a raw 32-hex-char key.
- MyMesh::setChannelLocal() factors out the setChannel/saveChannels/
onChannelRemoved sequence previously duplicated across the two
CMD_SET_CHANNEL branches, shared now by the BLE and on-device paths.
Tools > Admin (new):
- Log into a repeater/room server's admin account and send CLI commands,
the on-device equivalent of the app's repeater-admin feature.
- Commands are organised into category tabs (System/Radio/Routing/Actions)
with common get/set fields (name, radio profile, tx power, repeat, advert
intervals, ...) plus a free-text "Custom command..." fallback for anything
else. A field row fetches the current value, opens it pre-filled for
editing, and sends the change — falling back to a blank editor if the
fetch fails or times out.
- The admin password persists and self-heals exactly like room logins in
Messages: saved on a confirmed admin-level login, forgotten on a failed
one, left alone if merely under-privileged.
- New MyMesh::sendAdminCommand()/AbstractUITask::onAdminReply() plumbing so
a reply reaches the UI without touching the existing BLE/app CLI-terminal
path (queueMessage's should_display gate is untouched).
Shared TabBar.h extracted from NearbyScreen/BotScreen's independently
duplicated tab-carousel rendering (now a third consumer via Admin) — also
fixes neighbouring tabs vanishing outright when they didn't fully fit;
they now truncate with an ellipsis instead.
Keyboard: the combined "Ext.Latin" alphabet split into 8 separate,
linguistically complete per-language keyboards (Polish, Czech, Slovak,
German, French, Spanish, Portuguese, Nordic), and fixed an OLED-only bug
where tall accented glyphs overlapped the keyboard's separator line
(SH1106's Lemon-font ascent constant was 2-3px short for them).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-12 19:40:53 +02:00
|
|
|
|
admin_screen = new AdminScreen(this);
|
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);
|
feat(companion): clock tools — alarm, countdown timer, stopwatch
Add a Clock Tools screen (Enter on the home Clock page) with three time
utilities, plus the engine that drives them from UITask::loop() so they
work regardless of the current screen / display state:
- Alarm: a single one-shot wake alarm. Persisted in NodePrefs as a local
time-of-day; UITask schedules it as an ABSOLUTE fire instant recomputed
from that time, so it survives RTC re-syncs (mesh/app/GPS/CLI all jump
the clock) — small corrections still fire on time, a jump over the
target still fires (late, up to 6 h). Disarms after firing. A bell
icon signals an armed alarm on the clock face and the top status bar.
- Timer: a millis-based countdown (sync-immune), big HH:MM:SS readout,
rings even when off-screen.
- Stopwatch: millis-based, keeps running in the background.
Numeric fields use the shared framework DigitEditor (digit-by-digit,
min/max enforced). The alarm/timer/ring engine lives in UITask alongside
the locator/live-share engines (no screen-cast for time-critical logic).
E-ink: live readouts refresh coarsely (and on any key); timing is exact
regardless, and the countdown's buzzer fires on time. Rings override mute
and are dismissed by any key (auto-stop after 1 min). Cannot wake from a
full Shutdown (CPU/RAM powered down).
NodePrefs: +alarm_on/alarm_hour/alarm_min (fit existing tail padding,
sizeof unchanged 2488), SCHEMA_SENTINEL 0xC0DE0016 -> 0xC0DE0017,
DataStore read/clamp/write in lockstep.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 01:54:01 +02:00
|
|
|
|
clock_tools = new ClockToolsScreen(this, node_prefs);
|
2026-07-21 20:30:03 +02:00
|
|
|
|
#if defined(PIN_GPIO1)
|
|
|
|
|
|
gpio_screen = new GpioScreen(this, node_prefs);
|
|
|
|
|
|
#endif
|
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-07-21 20:30:03 +02:00
|
|
|
|
applyAllGpioModes(); // restore persisted pin modes to hardware before any UI/bot use
|
2026-05-22 23:50:21 +02:00
|
|
|
|
setCurrScreen(splash);
|
2025-08-08 20:01:31 +10:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-29 18:37:04 +02:00
|
|
|
|
// onShow() is invoked by setCurrScreen(), so most navigators are just that.
|
|
|
|
|
|
void UITask::gotoSettingsScreen() { setCurrScreen(settings); }
|
|
|
|
|
|
void UITask::gotoToolsScreen() { setCurrScreen(tools_screen); }
|
|
|
|
|
|
void UITask::gotoBotScreen() { setCurrScreen(bot_screen); }
|
|
|
|
|
|
void UITask::gotoNearbyScreen() { setCurrScreen(nearby_screen); }
|
2026-07-12 20:32:04 +02:00
|
|
|
|
|
|
|
|
|
|
void UITask::pickAdminTarget() {
|
|
|
|
|
|
setCurrScreen(nearby_screen); // runs NearbyScreen::onShow()'s reset first
|
|
|
|
|
|
((NearbyScreen*)nearby_screen)->startPickAdminTarget();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
feat(admin): typed Radio/Routing field editors; pre-release audit fixes
Tools > Admin: split the Radio tab's single "f,bw,sf,cr" comma-string
field (free-text keyboard) into 4 independent, type-appropriate rows --
Frequency (same digit-cursor DigitEditor Settings/Repeater use locally),
Bandwidth/Spreading factor/Coding rate (discrete-set LEFT/RIGHT stepping),
plus TX power and the 3 Routing numeric fields as number steppers and
Repeat as an ON/OFF toggle. Edits happen locally (zero mesh traffic per
keystroke); only Enter sends one combined `set`, Cancel sends nothing.
Full pre-release audit of all 16 commits since v1.22 (5 parallel focus
areas: display/font, keyboard/messages, Nodes/Admin, Bot/channels,
UITask+NodePrefs schema) turned up and fixed:
- Admin: fetched FK_NUMBER values weren't clamped to the field's range,
so a value already out-of-range could get stuck unreachable; fixed
commit-time float format noise (%.6f -> %.3f); Cancel/failed-login
always returned to the Nodes picker even when Admin was opened
directly from a node's Hold-Enter action -- now returns to wherever
it was actually opened from (AdminScreen::_from_picker).
- Keyboard: one-shot Shift was consumed after the *first* T9 multi-tap,
so cycling to the 2nd/3rd candidate always came out lowercase --
fixed by caching the cycle's caps state (t9_caps).
- Messages: history is numbered newest-first, so a message arriving
while scrolled up to an older one silently relabeled the view onto a
different message -- selection now shifts with the insert.
- Channels: onChannelRemoved() didn't clear ch_notif_override/
ch_notif_muted/ch_fav_bitmask despite its own contract comment
requiring it (now far more reachable via the on-device Delete);
an all-zero hex secret silently self-deleted the channel it was
just saved into (collides with the empty-slot sentinel) -- rejected.
- Room login: isRoomLoggedIn() indexed the login-tracking ring
directly instead of via its head offset, silently wrong once the
ring wraps (8+ rooms/session) -- the new on-device Logout depends
on this being right.
- Font/display: removed the now fully-inert Settings > Display > Font
toggle and the dead LemonFont.h (retired by the earlier misc-fixed
font unification, zero remaining includes); fixed a copy-pasted
"5x7" comment (font is 6x9), two meaningless dead ternaries, and an
OLED/e-ink inconsistency in the undefined-glyph fallback box offset.
- AdminField's `kind`/bounds fields no longer rely on default member
initializers inside aggregate-init: this toolchain's actual nRF52
build (unlike env:native) has no explicit -std= override, so it
predates C++14's aggregate-with-default-member-initializer rule.
Given an explicit constructor instead -- portable regardless of
standard, all existing field-table literals unchanged.
Docs updated to match: tools_screen.md (Diagnostics as a 3-tab
carousel, was documented as one flat screen), message_screen.md
(chat bubbles, newest-at-bottom, cursor mode, secret validation),
settings_screen.md (dropped the dead Font row), solo_ui_framework.md
(header menu_hint signatures, icon priority-drop, KeyboardWidget's
T9/alphabets/cursor-mode). release-notes.md gains the v1.23 section
covering all of the above plus the other 15 commits since v1.22.
Build-verified on WioTrackerL1_companion_solo_dual.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-17 19:38:51 +02:00
|
|
|
|
void UITask::openAdminFor(const ContactInfo& ci, bool from_picker) {
|
2026-07-12 20:32:04 +02:00
|
|
|
|
setCurrScreen(admin_screen); // runs AdminScreen::onShow()'s reset first
|
feat(admin): typed Radio/Routing field editors; pre-release audit fixes
Tools > Admin: split the Radio tab's single "f,bw,sf,cr" comma-string
field (free-text keyboard) into 4 independent, type-appropriate rows --
Frequency (same digit-cursor DigitEditor Settings/Repeater use locally),
Bandwidth/Spreading factor/Coding rate (discrete-set LEFT/RIGHT stepping),
plus TX power and the 3 Routing numeric fields as number steppers and
Repeat as an ON/OFF toggle. Edits happen locally (zero mesh traffic per
keystroke); only Enter sends one combined `set`, Cancel sends nothing.
Full pre-release audit of all 16 commits since v1.22 (5 parallel focus
areas: display/font, keyboard/messages, Nodes/Admin, Bot/channels,
UITask+NodePrefs schema) turned up and fixed:
- Admin: fetched FK_NUMBER values weren't clamped to the field's range,
so a value already out-of-range could get stuck unreachable; fixed
commit-time float format noise (%.6f -> %.3f); Cancel/failed-login
always returned to the Nodes picker even when Admin was opened
directly from a node's Hold-Enter action -- now returns to wherever
it was actually opened from (AdminScreen::_from_picker).
- Keyboard: one-shot Shift was consumed after the *first* T9 multi-tap,
so cycling to the 2nd/3rd candidate always came out lowercase --
fixed by caching the cycle's caps state (t9_caps).
- Messages: history is numbered newest-first, so a message arriving
while scrolled up to an older one silently relabeled the view onto a
different message -- selection now shifts with the insert.
- Channels: onChannelRemoved() didn't clear ch_notif_override/
ch_notif_muted/ch_fav_bitmask despite its own contract comment
requiring it (now far more reachable via the on-device Delete);
an all-zero hex secret silently self-deleted the channel it was
just saved into (collides with the empty-slot sentinel) -- rejected.
- Room login: isRoomLoggedIn() indexed the login-tracking ring
directly instead of via its head offset, silently wrong once the
ring wraps (8+ rooms/session) -- the new on-device Logout depends
on this being right.
- Font/display: removed the now fully-inert Settings > Display > Font
toggle and the dead LemonFont.h (retired by the earlier misc-fixed
font unification, zero remaining includes); fixed a copy-pasted
"5x7" comment (font is 6x9), two meaningless dead ternaries, and an
OLED/e-ink inconsistency in the undefined-glyph fallback box offset.
- AdminField's `kind`/bounds fields no longer rely on default member
initializers inside aggregate-init: this toolchain's actual nRF52
build (unlike env:native) has no explicit -std= override, so it
predates C++14's aggregate-with-default-member-initializer rule.
Given an explicit constructor instead -- portable regardless of
standard, all existing field-table literals unchanged.
Docs updated to match: tools_screen.md (Diagnostics as a 3-tab
carousel, was documented as one flat screen), message_screen.md
(chat bubbles, newest-at-bottom, cursor mode, secret validation),
settings_screen.md (dropped the dead Font row), solo_ui_framework.md
(header menu_hint signatures, icon priority-drop, KeyboardWidget's
T9/alphabets/cursor-mode). release-notes.md gains the v1.23 section
covering all of the above plus the other 15 commits since v1.22.
Build-verified on WioTrackerL1_companion_solo_dual.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-17 19:38:51 +02:00
|
|
|
|
((AdminScreen*)admin_screen)->startFor(ci, from_picker);
|
2026-07-12 20:32:04 +02:00
|
|
|
|
}
|
2026-06-29 18:37:04 +02:00
|
|
|
|
void UITask::gotoDashboardConfig() { setCurrScreen(dashboard_config); }
|
|
|
|
|
|
void UITask::gotoTrailScreen() { setCurrScreen(trail_screen); }
|
|
|
|
|
|
void UITask::gotoCompassScreen() { setCurrScreen(compass_screen); }
|
|
|
|
|
|
void UITask::gotoDiagnosticsScreen() { setCurrScreen(diag_screen); }
|
|
|
|
|
|
void UITask::gotoRepeaterScreen() { setCurrScreen(repeater_screen); }
|
feat(companion): clock tools — alarm, countdown timer, stopwatch
Add a Clock Tools screen (Enter on the home Clock page) with three time
utilities, plus the engine that drives them from UITask::loop() so they
work regardless of the current screen / display state:
- Alarm: a single one-shot wake alarm. Persisted in NodePrefs as a local
time-of-day; UITask schedules it as an ABSOLUTE fire instant recomputed
from that time, so it survives RTC re-syncs (mesh/app/GPS/CLI all jump
the clock) — small corrections still fire on time, a jump over the
target still fires (late, up to 6 h). Disarms after firing. A bell
icon signals an armed alarm on the clock face and the top status bar.
- Timer: a millis-based countdown (sync-immune), big HH:MM:SS readout,
rings even when off-screen.
- Stopwatch: millis-based, keeps running in the background.
Numeric fields use the shared framework DigitEditor (digit-by-digit,
min/max enforced). The alarm/timer/ring engine lives in UITask alongside
the locator/live-share engines (no screen-cast for time-critical logic).
E-ink: live readouts refresh coarsely (and on any key); timing is exact
regardless, and the countdown's buzzer fires on time. Rings override mute
and are dismissed by any key (auto-stop after 1 min). Cannot wake from a
full Shutdown (CPU/RAM powered down).
NodePrefs: +alarm_on/alarm_hour/alarm_min (fit existing tail padding,
sizeof unchanged 2488), SCHEMA_SENTINEL 0xC0DE0016 -> 0xC0DE0017,
DataStore read/clamp/write in lockstep.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 01:54:01 +02:00
|
|
|
|
void UITask::gotoClockTools() { setCurrScreen(clock_tools); }
|
2026-07-21 20:30:03 +02:00
|
|
|
|
void UITask::gotoGpioScreen() {
|
|
|
|
|
|
#if defined(PIN_GPIO1)
|
|
|
|
|
|
setCurrScreen(gpio_screen);
|
|
|
|
|
|
#endif
|
|
|
|
|
|
}
|
2026-06-29 18:37:04 +02:00
|
|
|
|
void UITask::gotoLiveShareScreen() { setCurrScreen(live_share_screen); }
|
|
|
|
|
|
|
feat(companion): clock tools — alarm, countdown timer, stopwatch
Add a Clock Tools screen (Enter on the home Clock page) with three time
utilities, plus the engine that drives them from UITask::loop() so they
work regardless of the current screen / display state:
- Alarm: a single one-shot wake alarm. Persisted in NodePrefs as a local
time-of-day; UITask schedules it as an ABSOLUTE fire instant recomputed
from that time, so it survives RTC re-syncs (mesh/app/GPS/CLI all jump
the clock) — small corrections still fire on time, a jump over the
target still fires (late, up to 6 h). Disarms after firing. A bell
icon signals an armed alarm on the clock face and the top status bar.
- Timer: a millis-based countdown (sync-immune), big HH:MM:SS readout,
rings even when off-screen.
- Stopwatch: millis-based, keeps running in the background.
Numeric fields use the shared framework DigitEditor (digit-by-digit,
min/max enforced). The alarm/timer/ring engine lives in UITask alongside
the locator/live-share engines (no screen-cast for time-critical logic).
E-ink: live readouts refresh coarsely (and on any key); timing is exact
regardless, and the countdown's buzzer fires on time. Rings override mute
and are dismissed by any key (auto-stop after 1 min). Cannot wake from a
full Shutdown (CPU/RAM powered down).
NodePrefs: +alarm_on/alarm_hour/alarm_min (fit existing tail padding,
sizeof unchanged 2488), SCHEMA_SENTINEL 0xC0DE0016 -> 0xC0DE0017,
DataStore read/clamp/write in lockstep.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 01:54:01 +02:00
|
|
|
|
// ── Clock tools engine (alarm / countdown / ring) ───────────────────────────
|
|
|
|
|
|
// Lives here, not in ClockToolsScreen, so it fires regardless of the current
|
|
|
|
|
|
// screen. The melody overrides mute (playMelody → buzzer.playForced); the ring
|
|
|
|
|
|
// auto-stops after CLOCK_RING_MS if no key dismisses it (see UITask::loop).
|
|
|
|
|
|
static const char* CLOCK_ALARM_MELODY = "alarm:d=8,o=6,b=125:c,c,c,c,p,c,c,c,c,p";
|
|
|
|
|
|
static const uint32_t CLOCK_RING_MS = 60000;
|
|
|
|
|
|
static const uint32_t CLOCK_ALARM_CATCHUP_SECS = 6 * 3600; // fire late up to 6 h, else reschedule
|
|
|
|
|
|
|
2026-07-05 16:08:44 +02:00
|
|
|
|
void UITask::wakeForAlarm() {
|
|
|
|
|
|
if (_display != NULL) _display->turnOn();
|
|
|
|
|
|
// Locked: the lock-screen blanking check (loop()) turns the display straight
|
|
|
|
|
|
// back off once _lock_wake_until is in the past — which it always is by the
|
|
|
|
|
|
// time an alarm fires. Hold the wake window open for the whole ring so the
|
|
|
|
|
|
// lock screen (and its alert overlay) stays visible while ringing.
|
|
|
|
|
|
if (_locked) _lock_wake_until = millis() + CLOCK_RING_MS;
|
|
|
|
|
|
_next_refresh = 0; // draw the alert overlay immediately
|
|
|
|
|
|
}
|
|
|
|
|
|
|
feat(companion): clock tools — alarm, countdown timer, stopwatch
Add a Clock Tools screen (Enter on the home Clock page) with three time
utilities, plus the engine that drives them from UITask::loop() so they
work regardless of the current screen / display state:
- Alarm: a single one-shot wake alarm. Persisted in NodePrefs as a local
time-of-day; UITask schedules it as an ABSOLUTE fire instant recomputed
from that time, so it survives RTC re-syncs (mesh/app/GPS/CLI all jump
the clock) — small corrections still fire on time, a jump over the
target still fires (late, up to 6 h). Disarms after firing. A bell
icon signals an armed alarm on the clock face and the top status bar.
- Timer: a millis-based countdown (sync-immune), big HH:MM:SS readout,
rings even when off-screen.
- Stopwatch: millis-based, keeps running in the background.
Numeric fields use the shared framework DigitEditor (digit-by-digit,
min/max enforced). The alarm/timer/ring engine lives in UITask alongside
the locator/live-share engines (no screen-cast for time-critical logic).
E-ink: live readouts refresh coarsely (and on any key); timing is exact
regardless, and the countdown's buzzer fires on time. Rings override mute
and are dismissed by any key (auto-stop after 1 min). Cannot wake from a
full Shutdown (CPU/RAM powered down).
NodePrefs: +alarm_on/alarm_hour/alarm_min (fit existing tail padding,
sizeof unchanged 2488), SCHEMA_SENTINEL 0xC0DE0016 -> 0xC0DE0017,
DataStore read/clamp/write in lockstep.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 01:54:01 +02:00
|
|
|
|
// Next absolute wall instant matching alarm_hour:alarm_min in local time,
|
|
|
|
|
|
// strictly after now_wall (an alarm set to the current minute waits a day).
|
feat(clock,keyboard): alarm repeat + non-Latin keyboard alphabets
Alarm repeat (Clock Tools):
- NodePrefs::alarm_repeat_mask (weekday bitmask, struct tm::tm_wday
convention) — new Repeat row cycles Off/Daily/Weekdays/Weekends.
computeAlarmNextFire() scans the next 7 days for a matching weekday when
set; evaluateAlarm() only clears alarm_on (one-shot) when the mask is
empty, otherwise re-arms. mask==0 is byte-for-byte the original one-shot
behaviour, so existing users see no change.
On-screen keyboard alphabets (Settings > Keyboard > Alphabet):
- KeyboardWidget reworked from single-byte ASCII cells to UTF-8 codepoints
(insertion, backspace and T9 in-place cycling all codepoint-aware now, so a
multi-byte character is never split) — see kbApplyCapsUtf8/kbUtf8Len/
kbUtf8CharAt/kbUtf8LastCharBytes.
- Cyrillic, Greek and an Extended Latin set (Polish/Czech/Slovak/German/
French/Spanish/Nordic diacritics) join the keyboard's existing #@/abc page
cycle (Latin -> alt alphabet -> Symbols -> Latin) — no new key needed.
NodePrefs::keyboard_alt_alphabet picks which one, if any, is active.
- Lemon font is forced on transiently inside KeyboardWidget::render() (saved
and restored every call) whenever the alt-alphabet page is showing or
already-typed text has non-ASCII bytes, so composing is visible even if
Font is set to Default.
- Each script's caps-shift rule is distinct and documented in
kbApplyCapsUtf8: flat -0x20 for ASCII/Cyrillic/Greek (with the ё/ς
exceptions), flat -0x20 for Latin-1 (à-þ), and an adjacent-pair -1 for
Latin Extended-A (verified against every character actually used, not a
blanket rule for that whole Unicode block).
NodePrefs schema: two tail-appended fields this session (alarm_repeat_mask,
keyboard_alt_alphabet), SCHEMA_SENTINEL 0xC0DE001B -> 0xC0DE001D,
sizeof(NodePrefs) 2496 -> 2504 (verified via a standalone host compile).
Docs: Clock Tools' Repeat row, Settings > Keyboard > Alphabet, and the
Rooms keyboard note (no longer ASCII-only once an alphabet is enabled).
Not build-verified — no PlatformIO toolchain available this session. Caps
mappings cross-checked against Python's Unicode case tables; UTF-8 literal
bytes verified at the byte level.
2026-07-10 16:01:42 +02:00
|
|
|
|
// With alarm_repeat_mask == 0 that's just tomorrow's occurrence (one-shot).
|
|
|
|
|
|
// With a repeat mask set, scan today..+6 days for the next weekday whose bit
|
|
|
|
|
|
// is set (struct tm's tm_wday convention, same as the mask) — today counts
|
|
|
|
|
|
// only if its time hasn't already passed.
|
feat(companion): clock tools — alarm, countdown timer, stopwatch
Add a Clock Tools screen (Enter on the home Clock page) with three time
utilities, plus the engine that drives them from UITask::loop() so they
work regardless of the current screen / display state:
- Alarm: a single one-shot wake alarm. Persisted in NodePrefs as a local
time-of-day; UITask schedules it as an ABSOLUTE fire instant recomputed
from that time, so it survives RTC re-syncs (mesh/app/GPS/CLI all jump
the clock) — small corrections still fire on time, a jump over the
target still fires (late, up to 6 h). Disarms after firing. A bell
icon signals an armed alarm on the clock face and the top status bar.
- Timer: a millis-based countdown (sync-immune), big HH:MM:SS readout,
rings even when off-screen.
- Stopwatch: millis-based, keeps running in the background.
Numeric fields use the shared framework DigitEditor (digit-by-digit,
min/max enforced). The alarm/timer/ring engine lives in UITask alongside
the locator/live-share engines (no screen-cast for time-critical logic).
E-ink: live readouts refresh coarsely (and on any key); timing is exact
regardless, and the countdown's buzzer fires on time. Rings override mute
and are dismissed by any key (auto-stop after 1 min). Cannot wake from a
full Shutdown (CPU/RAM powered down).
NodePrefs: +alarm_on/alarm_hour/alarm_min (fit existing tail padding,
sizeof unchanged 2488), SCHEMA_SENTINEL 0xC0DE0016 -> 0xC0DE0017,
DataStore read/clamp/write in lockstep.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 01:54:01 +02:00
|
|
|
|
uint32_t UITask::computeAlarmNextFire(uint32_t now_wall) const {
|
|
|
|
|
|
int tz = _node_prefs ? _node_prefs->tz_offset_hours : 0;
|
|
|
|
|
|
int64_t now_local = (int64_t)now_wall + (int64_t)tz * 3600;
|
|
|
|
|
|
time_t t = (time_t)now_local;
|
|
|
|
|
|
struct tm* ti = gmtime(&t);
|
|
|
|
|
|
int64_t sod = ti->tm_hour * 3600 + ti->tm_min * 60 + ti->tm_sec; // secs since local midnight
|
feat(clock,keyboard): alarm repeat + non-Latin keyboard alphabets
Alarm repeat (Clock Tools):
- NodePrefs::alarm_repeat_mask (weekday bitmask, struct tm::tm_wday
convention) — new Repeat row cycles Off/Daily/Weekdays/Weekends.
computeAlarmNextFire() scans the next 7 days for a matching weekday when
set; evaluateAlarm() only clears alarm_on (one-shot) when the mask is
empty, otherwise re-arms. mask==0 is byte-for-byte the original one-shot
behaviour, so existing users see no change.
On-screen keyboard alphabets (Settings > Keyboard > Alphabet):
- KeyboardWidget reworked from single-byte ASCII cells to UTF-8 codepoints
(insertion, backspace and T9 in-place cycling all codepoint-aware now, so a
multi-byte character is never split) — see kbApplyCapsUtf8/kbUtf8Len/
kbUtf8CharAt/kbUtf8LastCharBytes.
- Cyrillic, Greek and an Extended Latin set (Polish/Czech/Slovak/German/
French/Spanish/Nordic diacritics) join the keyboard's existing #@/abc page
cycle (Latin -> alt alphabet -> Symbols -> Latin) — no new key needed.
NodePrefs::keyboard_alt_alphabet picks which one, if any, is active.
- Lemon font is forced on transiently inside KeyboardWidget::render() (saved
and restored every call) whenever the alt-alphabet page is showing or
already-typed text has non-ASCII bytes, so composing is visible even if
Font is set to Default.
- Each script's caps-shift rule is distinct and documented in
kbApplyCapsUtf8: flat -0x20 for ASCII/Cyrillic/Greek (with the ё/ς
exceptions), flat -0x20 for Latin-1 (à-þ), and an adjacent-pair -1 for
Latin Extended-A (verified against every character actually used, not a
blanket rule for that whole Unicode block).
NodePrefs schema: two tail-appended fields this session (alarm_repeat_mask,
keyboard_alt_alphabet), SCHEMA_SENTINEL 0xC0DE001B -> 0xC0DE001D,
sizeof(NodePrefs) 2496 -> 2504 (verified via a standalone host compile).
Docs: Clock Tools' Repeat row, Settings > Keyboard > Alphabet, and the
Rooms keyboard note (no longer ASCII-only once an alphabet is enabled).
Not build-verified — no PlatformIO toolchain available this session. Caps
mappings cross-checked against Python's Unicode case tables; UTF-8 literal
bytes verified at the byte level.
2026-07-10 16:01:42 +02:00
|
|
|
|
int64_t midnight = now_local - sod;
|
|
|
|
|
|
int64_t time_of_day = (int64_t)_node_prefs->alarm_hour * 3600 + (int64_t)_node_prefs->alarm_min * 60;
|
|
|
|
|
|
uint8_t mask = _node_prefs->alarm_repeat_mask;
|
|
|
|
|
|
if (mask != 0) {
|
|
|
|
|
|
for (int d = 0; d < 7; d++) {
|
|
|
|
|
|
if (mask & (1 << ((ti->tm_wday + d) % 7))) {
|
|
|
|
|
|
int64_t target = midnight + (int64_t)d * 86400 + time_of_day;
|
|
|
|
|
|
if (target > now_local) return (uint32_t)(target - (int64_t)tz * 3600);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
// Mask had no bit set (shouldn't happen — the UI only offers non-empty
|
|
|
|
|
|
// presets) — fall through to the one-shot calculation so it still fires.
|
|
|
|
|
|
}
|
|
|
|
|
|
int64_t target = midnight + time_of_day;
|
feat(companion): clock tools — alarm, countdown timer, stopwatch
Add a Clock Tools screen (Enter on the home Clock page) with three time
utilities, plus the engine that drives them from UITask::loop() so they
work regardless of the current screen / display state:
- Alarm: a single one-shot wake alarm. Persisted in NodePrefs as a local
time-of-day; UITask schedules it as an ABSOLUTE fire instant recomputed
from that time, so it survives RTC re-syncs (mesh/app/GPS/CLI all jump
the clock) — small corrections still fire on time, a jump over the
target still fires (late, up to 6 h). Disarms after firing. A bell
icon signals an armed alarm on the clock face and the top status bar.
- Timer: a millis-based countdown (sync-immune), big HH:MM:SS readout,
rings even when off-screen.
- Stopwatch: millis-based, keeps running in the background.
Numeric fields use the shared framework DigitEditor (digit-by-digit,
min/max enforced). The alarm/timer/ring engine lives in UITask alongside
the locator/live-share engines (no screen-cast for time-critical logic).
E-ink: live readouts refresh coarsely (and on any key); timing is exact
regardless, and the countdown's buzzer fires on time. Rings override mute
and are dismissed by any key (auto-stop after 1 min). Cannot wake from a
full Shutdown (CPU/RAM powered down).
NodePrefs: +alarm_on/alarm_hour/alarm_min (fit existing tail padding,
sizeof unchanged 2488), SCHEMA_SENTINEL 0xC0DE0016 -> 0xC0DE0017,
DataStore read/clamp/write in lockstep.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 01:54:01 +02:00
|
|
|
|
if (target <= now_local) target += 86400;
|
|
|
|
|
|
return (uint32_t)(target - (int64_t)tz * 3600);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void UITask::fireClockAlert(const char* label) {
|
|
|
|
|
|
snprintf(_ring_label, sizeof(_ring_label), "%s", label);
|
|
|
|
|
|
_ringing = true;
|
|
|
|
|
|
_ring_until_ms = millis() + CLOCK_RING_MS;
|
|
|
|
|
|
wakeForAlarm();
|
|
|
|
|
|
showAlert(label, CLOCK_RING_MS);
|
|
|
|
|
|
playMelody(CLOCK_ALARM_MELODY);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void UITask::evaluateAlarm() {
|
|
|
|
|
|
if (!_node_prefs || !_node_prefs->alarm_on) return;
|
|
|
|
|
|
uint32_t now_ms = millis();
|
|
|
|
|
|
if (now_ms - _alarm_check_ms < 500) return; // ~2 Hz is plenty for a minute alarm
|
|
|
|
|
|
_alarm_check_ms = now_ms;
|
|
|
|
|
|
uint32_t now_wall = rtc_clock.getCurrentTime();
|
|
|
|
|
|
if (now_wall < 1000000000UL) return; // need a real time sync first
|
|
|
|
|
|
if (_alarm_next_fire == 0) _alarm_next_fire = computeAlarmNextFire(now_wall);
|
|
|
|
|
|
if (now_wall < _alarm_next_fire) return;
|
|
|
|
|
|
if (now_wall - _alarm_next_fire < CLOCK_ALARM_CATCHUP_SECS) {
|
|
|
|
|
|
char lbl[20];
|
|
|
|
|
|
snprintf(lbl, sizeof(lbl), "Alarm %02d:%02d", _node_prefs->alarm_hour, _node_prefs->alarm_min);
|
feat(clock,keyboard): alarm repeat + non-Latin keyboard alphabets
Alarm repeat (Clock Tools):
- NodePrefs::alarm_repeat_mask (weekday bitmask, struct tm::tm_wday
convention) — new Repeat row cycles Off/Daily/Weekdays/Weekends.
computeAlarmNextFire() scans the next 7 days for a matching weekday when
set; evaluateAlarm() only clears alarm_on (one-shot) when the mask is
empty, otherwise re-arms. mask==0 is byte-for-byte the original one-shot
behaviour, so existing users see no change.
On-screen keyboard alphabets (Settings > Keyboard > Alphabet):
- KeyboardWidget reworked from single-byte ASCII cells to UTF-8 codepoints
(insertion, backspace and T9 in-place cycling all codepoint-aware now, so a
multi-byte character is never split) — see kbApplyCapsUtf8/kbUtf8Len/
kbUtf8CharAt/kbUtf8LastCharBytes.
- Cyrillic, Greek and an Extended Latin set (Polish/Czech/Slovak/German/
French/Spanish/Nordic diacritics) join the keyboard's existing #@/abc page
cycle (Latin -> alt alphabet -> Symbols -> Latin) — no new key needed.
NodePrefs::keyboard_alt_alphabet picks which one, if any, is active.
- Lemon font is forced on transiently inside KeyboardWidget::render() (saved
and restored every call) whenever the alt-alphabet page is showing or
already-typed text has non-ASCII bytes, so composing is visible even if
Font is set to Default.
- Each script's caps-shift rule is distinct and documented in
kbApplyCapsUtf8: flat -0x20 for ASCII/Cyrillic/Greek (with the ё/ς
exceptions), flat -0x20 for Latin-1 (à-þ), and an adjacent-pair -1 for
Latin Extended-A (verified against every character actually used, not a
blanket rule for that whole Unicode block).
NodePrefs schema: two tail-appended fields this session (alarm_repeat_mask,
keyboard_alt_alphabet), SCHEMA_SENTINEL 0xC0DE001B -> 0xC0DE001D,
sizeof(NodePrefs) 2496 -> 2504 (verified via a standalone host compile).
Docs: Clock Tools' Repeat row, Settings > Keyboard > Alphabet, and the
Rooms keyboard note (no longer ASCII-only once an alphabet is enabled).
Not build-verified — no PlatformIO toolchain available this session. Caps
mappings cross-checked against Python's Unicode case tables; UTF-8 literal
bytes verified at the byte level.
2026-07-10 16:01:42 +02:00
|
|
|
|
if (_node_prefs->alarm_repeat_mask == 0) {
|
|
|
|
|
|
_node_prefs->alarm_on = 0; // one-shot
|
|
|
|
|
|
bool dirty = true; savePrefsIfDirty(dirty);
|
|
|
|
|
|
}
|
|
|
|
|
|
// Repeating: alarm_on stays set: computeAlarmNextFire() re-arms it for the
|
|
|
|
|
|
// next matching weekday below.
|
feat(companion): clock tools — alarm, countdown timer, stopwatch
Add a Clock Tools screen (Enter on the home Clock page) with three time
utilities, plus the engine that drives them from UITask::loop() so they
work regardless of the current screen / display state:
- Alarm: a single one-shot wake alarm. Persisted in NodePrefs as a local
time-of-day; UITask schedules it as an ABSOLUTE fire instant recomputed
from that time, so it survives RTC re-syncs (mesh/app/GPS/CLI all jump
the clock) — small corrections still fire on time, a jump over the
target still fires (late, up to 6 h). Disarms after firing. A bell
icon signals an armed alarm on the clock face and the top status bar.
- Timer: a millis-based countdown (sync-immune), big HH:MM:SS readout,
rings even when off-screen.
- Stopwatch: millis-based, keeps running in the background.
Numeric fields use the shared framework DigitEditor (digit-by-digit,
min/max enforced). The alarm/timer/ring engine lives in UITask alongside
the locator/live-share engines (no screen-cast for time-critical logic).
E-ink: live readouts refresh coarsely (and on any key); timing is exact
regardless, and the countdown's buzzer fires on time. Rings override mute
and are dismissed by any key (auto-stop after 1 min). Cannot wake from a
full Shutdown (CPU/RAM powered down).
NodePrefs: +alarm_on/alarm_hour/alarm_min (fit existing tail padding,
sizeof unchanged 2488), SCHEMA_SENTINEL 0xC0DE0016 -> 0xC0DE0017,
DataStore read/clamp/write in lockstep.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 01:54:01 +02:00
|
|
|
|
_alarm_next_fire = 0;
|
|
|
|
|
|
fireClockAlert(lbl);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// Clock jumped implausibly far past the target — reschedule rather than
|
|
|
|
|
|
// ringing absurdly late.
|
|
|
|
|
|
_alarm_next_fire = computeAlarmNextFire(now_wall);
|
|
|
|
|
|
}
|
2026-05-10 22:12:29 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
feat(companion): clock tools — alarm, countdown timer, stopwatch
Add a Clock Tools screen (Enter on the home Clock page) with three time
utilities, plus the engine that drives them from UITask::loop() so they
work regardless of the current screen / display state:
- Alarm: a single one-shot wake alarm. Persisted in NodePrefs as a local
time-of-day; UITask schedules it as an ABSOLUTE fire instant recomputed
from that time, so it survives RTC re-syncs (mesh/app/GPS/CLI all jump
the clock) — small corrections still fire on time, a jump over the
target still fires (late, up to 6 h). Disarms after firing. A bell
icon signals an armed alarm on the clock face and the top status bar.
- Timer: a millis-based countdown (sync-immune), big HH:MM:SS readout,
rings even when off-screen.
- Stopwatch: millis-based, keeps running in the background.
Numeric fields use the shared framework DigitEditor (digit-by-digit,
min/max enforced). The alarm/timer/ring engine lives in UITask alongside
the locator/live-share engines (no screen-cast for time-critical logic).
E-ink: live readouts refresh coarsely (and on any key); timing is exact
regardless, and the countdown's buzzer fires on time. Rings override mute
and are dismissed by any key (auto-stop after 1 min). Cannot wake from a
full Shutdown (CPU/RAM powered down).
NodePrefs: +alarm_on/alarm_hour/alarm_min (fit existing tail padding,
sizeof unchanged 2488), SCHEMA_SENTINEL 0xC0DE0016 -> 0xC0DE0017,
DataStore read/clamp/write in lockstep.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 01:54:01 +02:00
|
|
|
|
void UITask::tickClockTools() {
|
|
|
|
|
|
uint32_t now_ms = millis();
|
|
|
|
|
|
// Ring maintenance: repeat the melody until dismissed or the window elapses.
|
2026-07-05 16:08:44 +02:00
|
|
|
|
// Signed-difference compares (like the trail/loc-share timers) so deadlines
|
|
|
|
|
|
// landing past the millis() rollover don't read as already elapsed.
|
feat(companion): clock tools — alarm, countdown timer, stopwatch
Add a Clock Tools screen (Enter on the home Clock page) with three time
utilities, plus the engine that drives them from UITask::loop() so they
work regardless of the current screen / display state:
- Alarm: a single one-shot wake alarm. Persisted in NodePrefs as a local
time-of-day; UITask schedules it as an ABSOLUTE fire instant recomputed
from that time, so it survives RTC re-syncs (mesh/app/GPS/CLI all jump
the clock) — small corrections still fire on time, a jump over the
target still fires (late, up to 6 h). Disarms after firing. A bell
icon signals an armed alarm on the clock face and the top status bar.
- Timer: a millis-based countdown (sync-immune), big HH:MM:SS readout,
rings even when off-screen.
- Stopwatch: millis-based, keeps running in the background.
Numeric fields use the shared framework DigitEditor (digit-by-digit,
min/max enforced). The alarm/timer/ring engine lives in UITask alongside
the locator/live-share engines (no screen-cast for time-critical logic).
E-ink: live readouts refresh coarsely (and on any key); timing is exact
regardless, and the countdown's buzzer fires on time. Rings override mute
and are dismissed by any key (auto-stop after 1 min). Cannot wake from a
full Shutdown (CPU/RAM powered down).
NodePrefs: +alarm_on/alarm_hour/alarm_min (fit existing tail padding,
sizeof unchanged 2488), SCHEMA_SENTINEL 0xC0DE0016 -> 0xC0DE0017,
DataStore read/clamp/write in lockstep.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 01:54:01 +02:00
|
|
|
|
if (_ringing) {
|
2026-07-05 16:08:44 +02:00
|
|
|
|
if ((int32_t)(now_ms - _ring_until_ms) >= 0) { stopMelody(); _ringing = false; clearAlert(); }
|
feat(companion): clock tools — alarm, countdown timer, stopwatch
Add a Clock Tools screen (Enter on the home Clock page) with three time
utilities, plus the engine that drives them from UITask::loop() so they
work regardless of the current screen / display state:
- Alarm: a single one-shot wake alarm. Persisted in NodePrefs as a local
time-of-day; UITask schedules it as an ABSOLUTE fire instant recomputed
from that time, so it survives RTC re-syncs (mesh/app/GPS/CLI all jump
the clock) — small corrections still fire on time, a jump over the
target still fires (late, up to 6 h). Disarms after firing. A bell
icon signals an armed alarm on the clock face and the top status bar.
- Timer: a millis-based countdown (sync-immune), big HH:MM:SS readout,
rings even when off-screen.
- Stopwatch: millis-based, keeps running in the background.
Numeric fields use the shared framework DigitEditor (digit-by-digit,
min/max enforced). The alarm/timer/ring engine lives in UITask alongside
the locator/live-share engines (no screen-cast for time-critical logic).
E-ink: live readouts refresh coarsely (and on any key); timing is exact
regardless, and the countdown's buzzer fires on time. Rings override mute
and are dismissed by any key (auto-stop after 1 min). Cannot wake from a
full Shutdown (CPU/RAM powered down).
NodePrefs: +alarm_on/alarm_hour/alarm_min (fit existing tail padding,
sizeof unchanged 2488), SCHEMA_SENTINEL 0xC0DE0016 -> 0xC0DE0017,
DataStore read/clamp/write in lockstep.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 01:54:01 +02:00
|
|
|
|
else if (!isMelodyPlaying()) playMelody(CLOCK_ALARM_MELODY);
|
|
|
|
|
|
}
|
|
|
|
|
|
// Countdown timer (millis — sync-immune).
|
2026-07-05 16:08:44 +02:00
|
|
|
|
if (_timer_running && (int32_t)(now_ms - _timer_deadline_ms) >= 0) {
|
feat(companion): clock tools — alarm, countdown timer, stopwatch
Add a Clock Tools screen (Enter on the home Clock page) with three time
utilities, plus the engine that drives them from UITask::loop() so they
work regardless of the current screen / display state:
- Alarm: a single one-shot wake alarm. Persisted in NodePrefs as a local
time-of-day; UITask schedules it as an ABSOLUTE fire instant recomputed
from that time, so it survives RTC re-syncs (mesh/app/GPS/CLI all jump
the clock) — small corrections still fire on time, a jump over the
target still fires (late, up to 6 h). Disarms after firing. A bell
icon signals an armed alarm on the clock face and the top status bar.
- Timer: a millis-based countdown (sync-immune), big HH:MM:SS readout,
rings even when off-screen.
- Stopwatch: millis-based, keeps running in the background.
Numeric fields use the shared framework DigitEditor (digit-by-digit,
min/max enforced). The alarm/timer/ring engine lives in UITask alongside
the locator/live-share engines (no screen-cast for time-critical logic).
E-ink: live readouts refresh coarsely (and on any key); timing is exact
regardless, and the countdown's buzzer fires on time. Rings override mute
and are dismissed by any key (auto-stop after 1 min). Cannot wake from a
full Shutdown (CPU/RAM powered down).
NodePrefs: +alarm_on/alarm_hour/alarm_min (fit existing tail padding,
sizeof unchanged 2488), SCHEMA_SENTINEL 0xC0DE0016 -> 0xC0DE0017,
DataStore read/clamp/write in lockstep.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 01:54:01 +02:00
|
|
|
|
_timer_running = false;
|
|
|
|
|
|
fireClockAlert("Timer done");
|
|
|
|
|
|
}
|
|
|
|
|
|
// Alarm (wall clock — absolute schedule for sync robustness).
|
|
|
|
|
|
evaluateAlarm();
|
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-06-29 18:37:04 +02:00
|
|
|
|
// Ringtone takes a slot argument that onShow() can't carry — pass it after the
|
|
|
|
|
|
// reset (setCurrScreen's onShow runs first, then this layers the slot on top).
|
2026-05-15 11:43:38 +02:00
|
|
|
|
void UITask::gotoRingtoneEditor(int 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-06-29 18:37:04 +02:00
|
|
|
|
((RingtoneEditorScreen*)ringtone_edit)->selectSlot(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
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-29 18:37:04 +02:00
|
|
|
|
// Map is a sub-view variant of the Trail screen: reset via onShow(), then
|
|
|
|
|
|
// switch into the map view.
|
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() {
|
|
|
|
|
|
setCurrScreen(trail_screen);
|
2026-06-29 18:37:04 +02:00
|
|
|
|
((TrailScreen*)trail_screen)->showMapView();
|
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
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-29 18:37:04 +02:00
|
|
|
|
void UITask::gotoLocatorScreen() { setCurrScreen(locator_screen); }
|
|
|
|
|
|
void UITask::gotoAutoAdvertScreen() { setCurrScreen(auto_advert_screen); }
|
2026-05-14 16:08:33 +02:00
|
|
|
|
|
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
|
|
|
|
|
|
}
|
|
|
|
|
|
|
feat(ui): on-device channel management, remote admin tool, per-language keyboards
Messages:
- Add/edit/delete channels on-device (new ChannelsView, owned by the renamed
MessagesScreen — was QuickMsgScreen, whose name no longer matched its scope).
Channel secret entry supports a typed passphrase (SHA-256'd, same primitive
the library already uses for the routing hash) or a raw 32-hex-char key.
- MyMesh::setChannelLocal() factors out the setChannel/saveChannels/
onChannelRemoved sequence previously duplicated across the two
CMD_SET_CHANNEL branches, shared now by the BLE and on-device paths.
Tools > Admin (new):
- Log into a repeater/room server's admin account and send CLI commands,
the on-device equivalent of the app's repeater-admin feature.
- Commands are organised into category tabs (System/Radio/Routing/Actions)
with common get/set fields (name, radio profile, tx power, repeat, advert
intervals, ...) plus a free-text "Custom command..." fallback for anything
else. A field row fetches the current value, opens it pre-filled for
editing, and sends the change — falling back to a blank editor if the
fetch fails or times out.
- The admin password persists and self-heals exactly like room logins in
Messages: saved on a confirmed admin-level login, forgotten on a failed
one, left alone if merely under-privileged.
- New MyMesh::sendAdminCommand()/AbstractUITask::onAdminReply() plumbing so
a reply reaches the UI without touching the existing BLE/app CLI-terminal
path (queueMessage's should_display gate is untouched).
Shared TabBar.h extracted from NearbyScreen/BotScreen's independently
duplicated tab-carousel rendering (now a third consumer via Admin) — also
fixes neighbouring tabs vanishing outright when they didn't fully fit;
they now truncate with an ellipsis instead.
Keyboard: the combined "Ext.Latin" alphabet split into 8 separate,
linguistically complete per-language keyboards (Polish, Czech, Slovak,
German, French, Spanish, Portuguese, Nordic), and fixed an OLED-only bug
where tall accented glyphs overlapped the keyboard's separator line
(SH1106's Lemon-font ascent constant was 2-3px short for them).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-12 19:40:53 +02:00
|
|
|
|
void UITask::gotoMessagesScreen() {
|
|
|
|
|
|
((MessagesScreen*)messages_screen)->reset();
|
|
|
|
|
|
setCurrScreen(messages_screen);
|
2026-05-10 18:45:35 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-25 07:52:56 +02:00
|
|
|
|
void UITask::openContactDM(const ContactInfo& ci) {
|
feat(ui): on-device channel management, remote admin tool, per-language keyboards
Messages:
- Add/edit/delete channels on-device (new ChannelsView, owned by the renamed
MessagesScreen — was QuickMsgScreen, whose name no longer matched its scope).
Channel secret entry supports a typed passphrase (SHA-256'd, same primitive
the library already uses for the routing hash) or a raw 32-hex-char key.
- MyMesh::setChannelLocal() factors out the setChannel/saveChannels/
onChannelRemoved sequence previously duplicated across the two
CMD_SET_CHANNEL branches, shared now by the BLE and on-device paths.
Tools > Admin (new):
- Log into a repeater/room server's admin account and send CLI commands,
the on-device equivalent of the app's repeater-admin feature.
- Commands are organised into category tabs (System/Radio/Routing/Actions)
with common get/set fields (name, radio profile, tx power, repeat, advert
intervals, ...) plus a free-text "Custom command..." fallback for anything
else. A field row fetches the current value, opens it pre-filled for
editing, and sends the change — falling back to a blank editor if the
fetch fails or times out.
- The admin password persists and self-heals exactly like room logins in
Messages: saved on a confirmed admin-level login, forgotten on a failed
one, left alone if merely under-privileged.
- New MyMesh::sendAdminCommand()/AbstractUITask::onAdminReply() plumbing so
a reply reaches the UI without touching the existing BLE/app CLI-terminal
path (queueMessage's should_display gate is untouched).
Shared TabBar.h extracted from NearbyScreen/BotScreen's independently
duplicated tab-carousel rendering (now a third consumer via Admin) — also
fixes neighbouring tabs vanishing outright when they didn't fully fit;
they now truncate with an ellipsis instead.
Keyboard: the combined "Ext.Latin" alphabet split into 8 separate,
linguistically complete per-language keyboards (Polish, Czech, Slovak,
German, French, Spanish, Portuguese, Nordic), and fixed an OLED-only bug
where tall accented glyphs overlapped the keyboard's separator line
(SH1106's Lemon-font ascent constant was 2-3px short for them).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-12 19:40:53 +02:00
|
|
|
|
((MessagesScreen*)messages_screen)->reset();
|
|
|
|
|
|
((MessagesScreen*)messages_screen)->enterDM(ci);
|
|
|
|
|
|
setCurrScreen(messages_screen);
|
2026-05-25 07:52:56 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-04 11:38:12 +02:00
|
|
|
|
void UITask::shareToMessage(const char* text) {
|
feat(ui): on-device channel management, remote admin tool, per-language keyboards
Messages:
- Add/edit/delete channels on-device (new ChannelsView, owned by the renamed
MessagesScreen — was QuickMsgScreen, whose name no longer matched its scope).
Channel secret entry supports a typed passphrase (SHA-256'd, same primitive
the library already uses for the routing hash) or a raw 32-hex-char key.
- MyMesh::setChannelLocal() factors out the setChannel/saveChannels/
onChannelRemoved sequence previously duplicated across the two
CMD_SET_CHANNEL branches, shared now by the BLE and on-device paths.
Tools > Admin (new):
- Log into a repeater/room server's admin account and send CLI commands,
the on-device equivalent of the app's repeater-admin feature.
- Commands are organised into category tabs (System/Radio/Routing/Actions)
with common get/set fields (name, radio profile, tx power, repeat, advert
intervals, ...) plus a free-text "Custom command..." fallback for anything
else. A field row fetches the current value, opens it pre-filled for
editing, and sends the change — falling back to a blank editor if the
fetch fails or times out.
- The admin password persists and self-heals exactly like room logins in
Messages: saved on a confirmed admin-level login, forgotten on a failed
one, left alone if merely under-privileged.
- New MyMesh::sendAdminCommand()/AbstractUITask::onAdminReply() plumbing so
a reply reaches the UI without touching the existing BLE/app CLI-terminal
path (queueMessage's should_display gate is untouched).
Shared TabBar.h extracted from NearbyScreen/BotScreen's independently
duplicated tab-carousel rendering (now a third consumer via Admin) — also
fixes neighbouring tabs vanishing outright when they didn't fully fit;
they now truncate with an ellipsis instead.
Keyboard: the combined "Ext.Latin" alphabet split into 8 separate,
linguistically complete per-language keyboards (Polish, Czech, Slovak,
German, French, Spanish, Portuguese, Nordic), and fixed an OLED-only bug
where tall accented glyphs overlapped the keyboard's separator line
(SH1106's Lemon-font ascent constant was 2-3px short for them).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-12 19:40:53 +02:00
|
|
|
|
((MessagesScreen*)messages_screen)->startShare(text);
|
|
|
|
|
|
setCurrScreen(messages_screen);
|
2026-06-04 11:38: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::pickLocShareTarget() {
|
feat(ui): on-device channel management, remote admin tool, per-language keyboards
Messages:
- Add/edit/delete channels on-device (new ChannelsView, owned by the renamed
MessagesScreen — was QuickMsgScreen, whose name no longer matched its scope).
Channel secret entry supports a typed passphrase (SHA-256'd, same primitive
the library already uses for the routing hash) or a raw 32-hex-char key.
- MyMesh::setChannelLocal() factors out the setChannel/saveChannels/
onChannelRemoved sequence previously duplicated across the two
CMD_SET_CHANNEL branches, shared now by the BLE and on-device paths.
Tools > Admin (new):
- Log into a repeater/room server's admin account and send CLI commands,
the on-device equivalent of the app's repeater-admin feature.
- Commands are organised into category tabs (System/Radio/Routing/Actions)
with common get/set fields (name, radio profile, tx power, repeat, advert
intervals, ...) plus a free-text "Custom command..." fallback for anything
else. A field row fetches the current value, opens it pre-filled for
editing, and sends the change — falling back to a blank editor if the
fetch fails or times out.
- The admin password persists and self-heals exactly like room logins in
Messages: saved on a confirmed admin-level login, forgotten on a failed
one, left alone if merely under-privileged.
- New MyMesh::sendAdminCommand()/AbstractUITask::onAdminReply() plumbing so
a reply reaches the UI without touching the existing BLE/app CLI-terminal
path (queueMessage's should_display gate is untouched).
Shared TabBar.h extracted from NearbyScreen/BotScreen's independently
duplicated tab-carousel rendering (now a third consumer via Admin) — also
fixes neighbouring tabs vanishing outright when they didn't fully fit;
they now truncate with an ellipsis instead.
Keyboard: the combined "Ext.Latin" alphabet split into 8 separate,
linguistically complete per-language keyboards (Polish, Czech, Slovak,
German, French, Spanish, Portuguese, Nordic), and fixed an OLED-only bug
where tall accented glyphs overlapped the keyboard's separator line
(SH1106's Lemon-font ascent constant was 2-3px short for them).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-12 19:40:53 +02:00
|
|
|
|
((MessagesScreen*)messages_screen)->startPickTarget();
|
|
|
|
|
|
setCurrScreen(messages_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
|
|
|
|
}
|
|
|
|
|
|
|
feat(bot): room-server target, tab-carousel UI, per-target independence
Auto-Reply Bot gains a third target (room servers, alongside DM and
channel), new {name}/{hops} reply placeholders, and a DM all/favourites
allow-list. BotScreen is redesigned as a circular tab carousel (Direct /
Channel / Room / Other, same interaction as Nearby Nodes' filter tabs)
instead of one long scrolling list, which also exposed and fixed a
leftover coupling where Channel/Room trigger-replies and their !command
handling secretly depended on the DM tab's Enable/Commands toggles —
each target's Enable and Commands are now fully independent.
- NodePrefs: bot_room_enabled/prefix/trigger/reply, bot_dm_scope,
bot_commands_ch/bot_commands_room (bot_commands_enabled repurposed as
DM-only). SCHEMA_SENTINEL 0xC0DE001D -> 0xC0DE001F; sizeof unchanged
at 2712 (new bytes absorbed existing padding, verified via a
standalone host compile + offsetof check).
- DataStore: persists all new fields; seeds bot_commands_ch/room from
the old shared bot_commands_enabled on upgrade so existing
channel/room command behaviour isn't silently lost.
- MyMeshBot: tryBotReplyRoom/tryBotRoomCommand mirror the channel bot's
shape but post via sendMessage (room relays to members itself);
requires an existing login session with that room, same as a manual
post would. botDmSenderAllowed() gates DM trigger-reply/commands on
the favourites bit when bot_dm_scope=Fav.
- MsgExpand: {name}/{hops} as optional trailing params (default
nullptr/-1, no existing caller affected) — deliberately not exposed
on the general compose keyboard, only on bot Reply fields.
- QuickMsgScreen/UITask: room-target picker (mirrors the channel
picker), routing through the existing room-login prompt when there's
no saved password yet.
- BotScreen: tab carousel (LEFT/RIGHT switches tabs, UP/DOWN moves
rows, Enter is now the only way to change a value); Enable split out
of Channel/Room's combo row; Quiet Hours gained a stepper sub-mode;
Commands moved from a shared toggle into each tab.
- docs/tools_screen.md updated for the new tab layout and behaviour.
Not build-verified — no PlatformIO toolchain in this environment.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-10 20:16:59 +02:00
|
|
|
|
void UITask::pickBotChannelTarget() {
|
feat(ui): on-device channel management, remote admin tool, per-language keyboards
Messages:
- Add/edit/delete channels on-device (new ChannelsView, owned by the renamed
MessagesScreen — was QuickMsgScreen, whose name no longer matched its scope).
Channel secret entry supports a typed passphrase (SHA-256'd, same primitive
the library already uses for the routing hash) or a raw 32-hex-char key.
- MyMesh::setChannelLocal() factors out the setChannel/saveChannels/
onChannelRemoved sequence previously duplicated across the two
CMD_SET_CHANNEL branches, shared now by the BLE and on-device paths.
Tools > Admin (new):
- Log into a repeater/room server's admin account and send CLI commands,
the on-device equivalent of the app's repeater-admin feature.
- Commands are organised into category tabs (System/Radio/Routing/Actions)
with common get/set fields (name, radio profile, tx power, repeat, advert
intervals, ...) plus a free-text "Custom command..." fallback for anything
else. A field row fetches the current value, opens it pre-filled for
editing, and sends the change — falling back to a blank editor if the
fetch fails or times out.
- The admin password persists and self-heals exactly like room logins in
Messages: saved on a confirmed admin-level login, forgotten on a failed
one, left alone if merely under-privileged.
- New MyMesh::sendAdminCommand()/AbstractUITask::onAdminReply() plumbing so
a reply reaches the UI without touching the existing BLE/app CLI-terminal
path (queueMessage's should_display gate is untouched).
Shared TabBar.h extracted from NearbyScreen/BotScreen's independently
duplicated tab-carousel rendering (now a third consumer via Admin) — also
fixes neighbouring tabs vanishing outright when they didn't fully fit;
they now truncate with an ellipsis instead.
Keyboard: the combined "Ext.Latin" alphabet split into 8 separate,
linguistically complete per-language keyboards (Polish, Czech, Slovak,
German, French, Spanish, Portuguese, Nordic), and fixed an OLED-only bug
where tall accented glyphs overlapped the keyboard's separator line
(SH1106's Lemon-font ascent constant was 2-3px short for them).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-12 19:40:53 +02:00
|
|
|
|
((MessagesScreen*)messages_screen)->startPickBotChannel();
|
|
|
|
|
|
setCurrScreen(messages_screen);
|
feat(bot): room-server target, tab-carousel UI, per-target independence
Auto-Reply Bot gains a third target (room servers, alongside DM and
channel), new {name}/{hops} reply placeholders, and a DM all/favourites
allow-list. BotScreen is redesigned as a circular tab carousel (Direct /
Channel / Room / Other, same interaction as Nearby Nodes' filter tabs)
instead of one long scrolling list, which also exposed and fixed a
leftover coupling where Channel/Room trigger-replies and their !command
handling secretly depended on the DM tab's Enable/Commands toggles —
each target's Enable and Commands are now fully independent.
- NodePrefs: bot_room_enabled/prefix/trigger/reply, bot_dm_scope,
bot_commands_ch/bot_commands_room (bot_commands_enabled repurposed as
DM-only). SCHEMA_SENTINEL 0xC0DE001D -> 0xC0DE001F; sizeof unchanged
at 2712 (new bytes absorbed existing padding, verified via a
standalone host compile + offsetof check).
- DataStore: persists all new fields; seeds bot_commands_ch/room from
the old shared bot_commands_enabled on upgrade so existing
channel/room command behaviour isn't silently lost.
- MyMeshBot: tryBotReplyRoom/tryBotRoomCommand mirror the channel bot's
shape but post via sendMessage (room relays to members itself);
requires an existing login session with that room, same as a manual
post would. botDmSenderAllowed() gates DM trigger-reply/commands on
the favourites bit when bot_dm_scope=Fav.
- MsgExpand: {name}/{hops} as optional trailing params (default
nullptr/-1, no existing caller affected) — deliberately not exposed
on the general compose keyboard, only on bot Reply fields.
- QuickMsgScreen/UITask: room-target picker (mirrors the channel
picker), routing through the existing room-login prompt when there's
no saved password yet.
- BotScreen: tab carousel (LEFT/RIGHT switches tabs, UP/DOWN moves
rows, Enter is now the only way to change a value); Enable split out
of Channel/Room's combo row; Quiet Hours gained a stepper sub-mode;
Commands moved from a shared toggle into each tab.
- docs/tools_screen.md updated for the new tab layout and behaviour.
Not build-verified — no PlatformIO toolchain in this environment.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-10 20:16:59 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void UITask::pickBotRoomTarget() {
|
feat(ui): on-device channel management, remote admin tool, per-language keyboards
Messages:
- Add/edit/delete channels on-device (new ChannelsView, owned by the renamed
MessagesScreen — was QuickMsgScreen, whose name no longer matched its scope).
Channel secret entry supports a typed passphrase (SHA-256'd, same primitive
the library already uses for the routing hash) or a raw 32-hex-char key.
- MyMesh::setChannelLocal() factors out the setChannel/saveChannels/
onChannelRemoved sequence previously duplicated across the two
CMD_SET_CHANNEL branches, shared now by the BLE and on-device paths.
Tools > Admin (new):
- Log into a repeater/room server's admin account and send CLI commands,
the on-device equivalent of the app's repeater-admin feature.
- Commands are organised into category tabs (System/Radio/Routing/Actions)
with common get/set fields (name, radio profile, tx power, repeat, advert
intervals, ...) plus a free-text "Custom command..." fallback for anything
else. A field row fetches the current value, opens it pre-filled for
editing, and sends the change — falling back to a blank editor if the
fetch fails or times out.
- The admin password persists and self-heals exactly like room logins in
Messages: saved on a confirmed admin-level login, forgotten on a failed
one, left alone if merely under-privileged.
- New MyMesh::sendAdminCommand()/AbstractUITask::onAdminReply() plumbing so
a reply reaches the UI without touching the existing BLE/app CLI-terminal
path (queueMessage's should_display gate is untouched).
Shared TabBar.h extracted from NearbyScreen/BotScreen's independently
duplicated tab-carousel rendering (now a third consumer via Admin) — also
fixes neighbouring tabs vanishing outright when they didn't fully fit;
they now truncate with an ellipsis instead.
Keyboard: the combined "Ext.Latin" alphabet split into 8 separate,
linguistically complete per-language keyboards (Polish, Czech, Slovak,
German, French, Spanish, Portuguese, Nordic), and fixed an OLED-only bug
where tall accented glyphs overlapped the keyboard's separator line
(SH1106's Lemon-font ascent constant was 2-3px short for them).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-12 19:40:53 +02:00
|
|
|
|
((MessagesScreen*)messages_screen)->startPickBotRoom();
|
|
|
|
|
|
setCurrScreen(messages_screen);
|
feat(bot): room-server target, tab-carousel UI, per-target independence
Auto-Reply Bot gains a third target (room servers, alongside DM and
channel), new {name}/{hops} reply placeholders, and a DM all/favourites
allow-list. BotScreen is redesigned as a circular tab carousel (Direct /
Channel / Room / Other, same interaction as Nearby Nodes' filter tabs)
instead of one long scrolling list, which also exposed and fixed a
leftover coupling where Channel/Room trigger-replies and their !command
handling secretly depended on the DM tab's Enable/Commands toggles —
each target's Enable and Commands are now fully independent.
- NodePrefs: bot_room_enabled/prefix/trigger/reply, bot_dm_scope,
bot_commands_ch/bot_commands_room (bot_commands_enabled repurposed as
DM-only). SCHEMA_SENTINEL 0xC0DE001D -> 0xC0DE001F; sizeof unchanged
at 2712 (new bytes absorbed existing padding, verified via a
standalone host compile + offsetof check).
- DataStore: persists all new fields; seeds bot_commands_ch/room from
the old shared bot_commands_enabled on upgrade so existing
channel/room command behaviour isn't silently lost.
- MyMeshBot: tryBotReplyRoom/tryBotRoomCommand mirror the channel bot's
shape but post via sendMessage (room relays to members itself);
requires an existing login session with that room, same as a manual
post would. botDmSenderAllowed() gates DM trigger-reply/commands on
the favourites bit when bot_dm_scope=Fav.
- MsgExpand: {name}/{hops} as optional trailing params (default
nullptr/-1, no existing caller affected) — deliberately not exposed
on the general compose keyboard, only on bot Reply fields.
- QuickMsgScreen/UITask: room-target picker (mirrors the channel
picker), routing through the existing room-login prompt when there's
no saved password yet.
- BotScreen: tab carousel (LEFT/RIGHT switches tabs, UP/DOWN moves
rows, Enter is now the only way to change a value); Enable split out
of Channel/Room's combo row; Quiet Hours gained a stepper sub-mode;
Commands moved from a shared toggle into each tab.
- docs/tools_screen.md updated for the new tab layout and behaviour.
Not build-verified — no PlatformIO toolchain in this environment.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-10 20:16:59 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-25 08:08:12 +02:00
|
|
|
|
int UITask::getRecentDMContacts(uint8_t out[][NodePrefs::FAVOURITE_PREFIX_LEN], int max) const {
|
feat(ui): on-device channel management, remote admin tool, per-language keyboards
Messages:
- Add/edit/delete channels on-device (new ChannelsView, owned by the renamed
MessagesScreen — was QuickMsgScreen, whose name no longer matched its scope).
Channel secret entry supports a typed passphrase (SHA-256'd, same primitive
the library already uses for the routing hash) or a raw 32-hex-char key.
- MyMesh::setChannelLocal() factors out the setChannel/saveChannels/
onChannelRemoved sequence previously duplicated across the two
CMD_SET_CHANNEL branches, shared now by the BLE and on-device paths.
Tools > Admin (new):
- Log into a repeater/room server's admin account and send CLI commands,
the on-device equivalent of the app's repeater-admin feature.
- Commands are organised into category tabs (System/Radio/Routing/Actions)
with common get/set fields (name, radio profile, tx power, repeat, advert
intervals, ...) plus a free-text "Custom command..." fallback for anything
else. A field row fetches the current value, opens it pre-filled for
editing, and sends the change — falling back to a blank editor if the
fetch fails or times out.
- The admin password persists and self-heals exactly like room logins in
Messages: saved on a confirmed admin-level login, forgotten on a failed
one, left alone if merely under-privileged.
- New MyMesh::sendAdminCommand()/AbstractUITask::onAdminReply() plumbing so
a reply reaches the UI without touching the existing BLE/app CLI-terminal
path (queueMessage's should_display gate is untouched).
Shared TabBar.h extracted from NearbyScreen/BotScreen's independently
duplicated tab-carousel rendering (now a third consumer via Admin) — also
fixes neighbouring tabs vanishing outright when they didn't fully fit;
they now truncate with an ellipsis instead.
Keyboard: the combined "Ext.Latin" alphabet split into 8 separate,
linguistically complete per-language keyboards (Polish, Czech, Slovak,
German, French, Spanish, Portuguese, Nordic), and fixed an OLED-only bug
where tall accented glyphs overlapped the keyboard's separator line
(SH1106's Lemon-font ascent constant was 2-3px short for them).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-12 19:40:53 +02:00
|
|
|
|
return ((MessagesScreen*)messages_screen)->getRecentDMContacts(out, max);
|
2026-05-25 08:08:12 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
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;
|
feat(ui): on-device channel management, remote admin tool, per-language keyboards
Messages:
- Add/edit/delete channels on-device (new ChannelsView, owned by the renamed
MessagesScreen — was QuickMsgScreen, whose name no longer matched its scope).
Channel secret entry supports a typed passphrase (SHA-256'd, same primitive
the library already uses for the routing hash) or a raw 32-hex-char key.
- MyMesh::setChannelLocal() factors out the setChannel/saveChannels/
onChannelRemoved sequence previously duplicated across the two
CMD_SET_CHANNEL branches, shared now by the BLE and on-device paths.
Tools > Admin (new):
- Log into a repeater/room server's admin account and send CLI commands,
the on-device equivalent of the app's repeater-admin feature.
- Commands are organised into category tabs (System/Radio/Routing/Actions)
with common get/set fields (name, radio profile, tx power, repeat, advert
intervals, ...) plus a free-text "Custom command..." fallback for anything
else. A field row fetches the current value, opens it pre-filled for
editing, and sends the change — falling back to a blank editor if the
fetch fails or times out.
- The admin password persists and self-heals exactly like room logins in
Messages: saved on a confirmed admin-level login, forgotten on a failed
one, left alone if merely under-privileged.
- New MyMesh::sendAdminCommand()/AbstractUITask::onAdminReply() plumbing so
a reply reaches the UI without touching the existing BLE/app CLI-terminal
path (queueMessage's should_display gate is untouched).
Shared TabBar.h extracted from NearbyScreen/BotScreen's independently
duplicated tab-carousel rendering (now a third consumer via Admin) — also
fixes neighbouring tabs vanishing outright when they didn't fully fit;
they now truncate with an ellipsis instead.
Keyboard: the combined "Ext.Latin" alphabet split into 8 separate,
linguistically complete per-language keyboards (Polish, Czech, Slovak,
German, French, Spanish, Portuguese, Nordic), and fixed an OLED-only bug
where tall accented glyphs overlapped the keyboard's separator line
(SH1106's Lemon-font ascent constant was 2-3px short for them).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-12 19:40:53 +02:00
|
|
|
|
((MessagesScreen*)messages_screen)->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 {
|
feat(ui): on-device channel management, remote admin tool, per-language keyboards
Messages:
- Add/edit/delete channels on-device (new ChannelsView, owned by the renamed
MessagesScreen — was QuickMsgScreen, whose name no longer matched its scope).
Channel secret entry supports a typed passphrase (SHA-256'd, same primitive
the library already uses for the routing hash) or a raw 32-hex-char key.
- MyMesh::setChannelLocal() factors out the setChannel/saveChannels/
onChannelRemoved sequence previously duplicated across the two
CMD_SET_CHANNEL branches, shared now by the BLE and on-device paths.
Tools > Admin (new):
- Log into a repeater/room server's admin account and send CLI commands,
the on-device equivalent of the app's repeater-admin feature.
- Commands are organised into category tabs (System/Radio/Routing/Actions)
with common get/set fields (name, radio profile, tx power, repeat, advert
intervals, ...) plus a free-text "Custom command..." fallback for anything
else. A field row fetches the current value, opens it pre-filled for
editing, and sends the change — falling back to a blank editor if the
fetch fails or times out.
- The admin password persists and self-heals exactly like room logins in
Messages: saved on a confirmed admin-level login, forgotten on a failed
one, left alone if merely under-privileged.
- New MyMesh::sendAdminCommand()/AbstractUITask::onAdminReply() plumbing so
a reply reaches the UI without touching the existing BLE/app CLI-terminal
path (queueMessage's should_display gate is untouched).
Shared TabBar.h extracted from NearbyScreen/BotScreen's independently
duplicated tab-carousel rendering (now a third consumer via Admin) — also
fixes neighbouring tabs vanishing outright when they didn't fully fit;
they now truncate with an ellipsis instead.
Keyboard: the combined "Ext.Latin" alphabet split into 8 separate,
linguistically complete per-language keyboards (Polish, Czech, Slovak,
German, French, Spanish, Portuguese, Nordic), and fixed an OLED-only bug
where tall accented glyphs overlapped the keyboard's separator line
(SH1106's Lemon-font ascent constant was 2-3px short for them).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-12 19:40:53 +02:00
|
|
|
|
return ((MessagesScreen*)messages_screen)->getTotalChannelUnread();
|
2026-05-11 19:11:19 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-14 23:33:16 +02:00
|
|
|
|
void UITask::onMsgAck(uint32_t ack_crc) {
|
feat(ui): on-device channel management, remote admin tool, per-language keyboards
Messages:
- Add/edit/delete channels on-device (new ChannelsView, owned by the renamed
MessagesScreen — was QuickMsgScreen, whose name no longer matched its scope).
Channel secret entry supports a typed passphrase (SHA-256'd, same primitive
the library already uses for the routing hash) or a raw 32-hex-char key.
- MyMesh::setChannelLocal() factors out the setChannel/saveChannels/
onChannelRemoved sequence previously duplicated across the two
CMD_SET_CHANNEL branches, shared now by the BLE and on-device paths.
Tools > Admin (new):
- Log into a repeater/room server's admin account and send CLI commands,
the on-device equivalent of the app's repeater-admin feature.
- Commands are organised into category tabs (System/Radio/Routing/Actions)
with common get/set fields (name, radio profile, tx power, repeat, advert
intervals, ...) plus a free-text "Custom command..." fallback for anything
else. A field row fetches the current value, opens it pre-filled for
editing, and sends the change — falling back to a blank editor if the
fetch fails or times out.
- The admin password persists and self-heals exactly like room logins in
Messages: saved on a confirmed admin-level login, forgotten on a failed
one, left alone if merely under-privileged.
- New MyMesh::sendAdminCommand()/AbstractUITask::onAdminReply() plumbing so
a reply reaches the UI without touching the existing BLE/app CLI-terminal
path (queueMessage's should_display gate is untouched).
Shared TabBar.h extracted from NearbyScreen/BotScreen's independently
duplicated tab-carousel rendering (now a third consumer via Admin) — also
fixes neighbouring tabs vanishing outright when they didn't fully fit;
they now truncate with an ellipsis instead.
Keyboard: the combined "Ext.Latin" alphabet split into 8 separate,
linguistically complete per-language keyboards (Polish, Czech, Slovak,
German, French, Spanish, Portuguese, Nordic), and fixed an OLED-only bug
where tall accented glyphs overlapped the keyboard's separator line
(SH1106's Lemon-font ascent constant was 2-3px short for them).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-12 19:40:53 +02:00
|
|
|
|
((MessagesScreen*)messages_screen)->markDmDelivered(ack_crc);
|
2026-06-14 23:33:16 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void UITask::onChannelRelayed(uint32_t seq) {
|
feat(ui): on-device channel management, remote admin tool, per-language keyboards
Messages:
- Add/edit/delete channels on-device (new ChannelsView, owned by the renamed
MessagesScreen — was QuickMsgScreen, whose name no longer matched its scope).
Channel secret entry supports a typed passphrase (SHA-256'd, same primitive
the library already uses for the routing hash) or a raw 32-hex-char key.
- MyMesh::setChannelLocal() factors out the setChannel/saveChannels/
onChannelRemoved sequence previously duplicated across the two
CMD_SET_CHANNEL branches, shared now by the BLE and on-device paths.
Tools > Admin (new):
- Log into a repeater/room server's admin account and send CLI commands,
the on-device equivalent of the app's repeater-admin feature.
- Commands are organised into category tabs (System/Radio/Routing/Actions)
with common get/set fields (name, radio profile, tx power, repeat, advert
intervals, ...) plus a free-text "Custom command..." fallback for anything
else. A field row fetches the current value, opens it pre-filled for
editing, and sends the change — falling back to a blank editor if the
fetch fails or times out.
- The admin password persists and self-heals exactly like room logins in
Messages: saved on a confirmed admin-level login, forgotten on a failed
one, left alone if merely under-privileged.
- New MyMesh::sendAdminCommand()/AbstractUITask::onAdminReply() plumbing so
a reply reaches the UI without touching the existing BLE/app CLI-terminal
path (queueMessage's should_display gate is untouched).
Shared TabBar.h extracted from NearbyScreen/BotScreen's independently
duplicated tab-carousel rendering (now a third consumer via Admin) — also
fixes neighbouring tabs vanishing outright when they didn't fully fit;
they now truncate with an ellipsis instead.
Keyboard: the combined "Ext.Latin" alphabet split into 8 separate,
linguistically complete per-language keyboards (Polish, Czech, Slovak,
German, French, Spanish, Portuguese, Nordic), and fixed an OLED-only bug
where tall accented glyphs overlapped the keyboard's separator line
(SH1106's Lemon-font ascent constant was 2-3px short for them).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-12 19:40:53 +02:00
|
|
|
|
((MessagesScreen*)messages_screen)->markChannelRelayed(seq);
|
2026-06-14 23:33:16 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-26 10:55:24 +02:00
|
|
|
|
void UITask::onRoomLoginResult(const uint8_t* pub_key, bool success, uint8_t permissions) {
|
feat(ui): on-device channel management, remote admin tool, per-language keyboards
Messages:
- Add/edit/delete channels on-device (new ChannelsView, owned by the renamed
MessagesScreen — was QuickMsgScreen, whose name no longer matched its scope).
Channel secret entry supports a typed passphrase (SHA-256'd, same primitive
the library already uses for the routing hash) or a raw 32-hex-char key.
- MyMesh::setChannelLocal() factors out the setChannel/saveChannels/
onChannelRemoved sequence previously duplicated across the two
CMD_SET_CHANNEL branches, shared now by the BLE and on-device paths.
Tools > Admin (new):
- Log into a repeater/room server's admin account and send CLI commands,
the on-device equivalent of the app's repeater-admin feature.
- Commands are organised into category tabs (System/Radio/Routing/Actions)
with common get/set fields (name, radio profile, tx power, repeat, advert
intervals, ...) plus a free-text "Custom command..." fallback for anything
else. A field row fetches the current value, opens it pre-filled for
editing, and sends the change — falling back to a blank editor if the
fetch fails or times out.
- The admin password persists and self-heals exactly like room logins in
Messages: saved on a confirmed admin-level login, forgotten on a failed
one, left alone if merely under-privileged.
- New MyMesh::sendAdminCommand()/AbstractUITask::onAdminReply() plumbing so
a reply reaches the UI without touching the existing BLE/app CLI-terminal
path (queueMessage's should_display gate is untouched).
Shared TabBar.h extracted from NearbyScreen/BotScreen's independently
duplicated tab-carousel rendering (now a third consumer via Admin) — also
fixes neighbouring tabs vanishing outright when they didn't fully fit;
they now truncate with an ellipsis instead.
Keyboard: the combined "Ext.Latin" alphabet split into 8 separate,
linguistically complete per-language keyboards (Polish, Czech, Slovak,
German, French, Spanish, Portuguese, Nordic), and fixed an OLED-only bug
where tall accented glyphs overlapped the keyboard's separator line
(SH1106's Lemon-font ascent constant was 2-3px short for them).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-12 19:40:53 +02:00
|
|
|
|
// Only one on-device login can be in flight at a time (MyMesh::ui_pending_login
|
|
|
|
|
|
// is a single slot) -- route the result to whichever of the two screens that
|
|
|
|
|
|
// can trigger a login is currently active, rather than always MessagesScreen.
|
|
|
|
|
|
if (curr == admin_screen) ((AdminScreen*)admin_screen)->onRoomLoginResult(pub_key, success, permissions);
|
|
|
|
|
|
else ((MessagesScreen*)messages_screen)->onRoomLoginResult(pub_key, success, permissions);
|
2026-06-26 10:55:24 +02:00
|
|
|
|
// 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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
feat(ui): on-device channel management, remote admin tool, per-language keyboards
Messages:
- Add/edit/delete channels on-device (new ChannelsView, owned by the renamed
MessagesScreen — was QuickMsgScreen, whose name no longer matched its scope).
Channel secret entry supports a typed passphrase (SHA-256'd, same primitive
the library already uses for the routing hash) or a raw 32-hex-char key.
- MyMesh::setChannelLocal() factors out the setChannel/saveChannels/
onChannelRemoved sequence previously duplicated across the two
CMD_SET_CHANNEL branches, shared now by the BLE and on-device paths.
Tools > Admin (new):
- Log into a repeater/room server's admin account and send CLI commands,
the on-device equivalent of the app's repeater-admin feature.
- Commands are organised into category tabs (System/Radio/Routing/Actions)
with common get/set fields (name, radio profile, tx power, repeat, advert
intervals, ...) plus a free-text "Custom command..." fallback for anything
else. A field row fetches the current value, opens it pre-filled for
editing, and sends the change — falling back to a blank editor if the
fetch fails or times out.
- The admin password persists and self-heals exactly like room logins in
Messages: saved on a confirmed admin-level login, forgotten on a failed
one, left alone if merely under-privileged.
- New MyMesh::sendAdminCommand()/AbstractUITask::onAdminReply() plumbing so
a reply reaches the UI without touching the existing BLE/app CLI-terminal
path (queueMessage's should_display gate is untouched).
Shared TabBar.h extracted from NearbyScreen/BotScreen's independently
duplicated tab-carousel rendering (now a third consumer via Admin) — also
fixes neighbouring tabs vanishing outright when they didn't fully fit;
they now truncate with an ellipsis instead.
Keyboard: the combined "Ext.Latin" alphabet split into 8 separate,
linguistically complete per-language keyboards (Polish, Czech, Slovak,
German, French, Spanish, Portuguese, Nordic), and fixed an OLED-only bug
where tall accented glyphs overlapped the keyboard's separator line
(SH1106's Lemon-font ascent constant was 2-3px short for them).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-12 19:40:53 +02:00
|
|
|
|
void UITask::onAdminReply(const uint8_t* pub_key, const char* text) {
|
|
|
|
|
|
((AdminScreen*)admin_screen)->onAdminReply(pub_key, text);
|
|
|
|
|
|
_next_refresh = 0; // same reasoning as onRoomLoginResult above
|
|
|
|
|
|
}
|
|
|
|
|
|
|
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) {
|
feat(ui): on-device channel management, remote admin tool, per-language keyboards
Messages:
- Add/edit/delete channels on-device (new ChannelsView, owned by the renamed
MessagesScreen — was QuickMsgScreen, whose name no longer matched its scope).
Channel secret entry supports a typed passphrase (SHA-256'd, same primitive
the library already uses for the routing hash) or a raw 32-hex-char key.
- MyMesh::setChannelLocal() factors out the setChannel/saveChannels/
onChannelRemoved sequence previously duplicated across the two
CMD_SET_CHANNEL branches, shared now by the BLE and on-device paths.
Tools > Admin (new):
- Log into a repeater/room server's admin account and send CLI commands,
the on-device equivalent of the app's repeater-admin feature.
- Commands are organised into category tabs (System/Radio/Routing/Actions)
with common get/set fields (name, radio profile, tx power, repeat, advert
intervals, ...) plus a free-text "Custom command..." fallback for anything
else. A field row fetches the current value, opens it pre-filled for
editing, and sends the change — falling back to a blank editor if the
fetch fails or times out.
- The admin password persists and self-heals exactly like room logins in
Messages: saved on a confirmed admin-level login, forgotten on a failed
one, left alone if merely under-privileged.
- New MyMesh::sendAdminCommand()/AbstractUITask::onAdminReply() plumbing so
a reply reaches the UI without touching the existing BLE/app CLI-terminal
path (queueMessage's should_display gate is untouched).
Shared TabBar.h extracted from NearbyScreen/BotScreen's independently
duplicated tab-carousel rendering (now a third consumer via Admin) — also
fixes neighbouring tabs vanishing outright when they didn't fully fit;
they now truncate with an ellipsis instead.
Keyboard: the combined "Ext.Latin" alphabet split into 8 separate,
linguistically complete per-language keyboards (Polish, Czech, Slovak,
German, French, Spanish, Portuguese, Nordic), and fixed an OLED-only bug
where tall accented glyphs overlapped the keyboard's separator line
(SH1106's Lemon-font ascent constant was 2-3px short for them).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-12 19:40:53 +02:00
|
|
|
|
((MessagesScreen*)messages_screen)->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));
|
feat(ui): on-device channel management, remote admin tool, per-language keyboards
Messages:
- Add/edit/delete channels on-device (new ChannelsView, owned by the renamed
MessagesScreen — was QuickMsgScreen, whose name no longer matched its scope).
Channel secret entry supports a typed passphrase (SHA-256'd, same primitive
the library already uses for the routing hash) or a raw 32-hex-char key.
- MyMesh::setChannelLocal() factors out the setChannel/saveChannels/
onChannelRemoved sequence previously duplicated across the two
CMD_SET_CHANNEL branches, shared now by the BLE and on-device paths.
Tools > Admin (new):
- Log into a repeater/room server's admin account and send CLI commands,
the on-device equivalent of the app's repeater-admin feature.
- Commands are organised into category tabs (System/Radio/Routing/Actions)
with common get/set fields (name, radio profile, tx power, repeat, advert
intervals, ...) plus a free-text "Custom command..." fallback for anything
else. A field row fetches the current value, opens it pre-filled for
editing, and sends the change — falling back to a blank editor if the
fetch fails or times out.
- The admin password persists and self-heals exactly like room logins in
Messages: saved on a confirmed admin-level login, forgotten on a failed
one, left alone if merely under-privileged.
- New MyMesh::sendAdminCommand()/AbstractUITask::onAdminReply() plumbing so
a reply reaches the UI without touching the existing BLE/app CLI-terminal
path (queueMessage's should_display gate is untouched).
Shared TabBar.h extracted from NearbyScreen/BotScreen's independently
duplicated tab-carousel rendering (now a third consumer via Admin) — also
fixes neighbouring tabs vanishing outright when they didn't fully fit;
they now truncate with an ellipsis instead.
Keyboard: the combined "Ext.Latin" alphabet split into 8 separate,
linguistically complete per-language keyboards (Polish, Czech, Slovak,
German, French, Spanish, Portuguese, Nordic), and fixed an OLED-only bug
where tall accented glyphs overlapped the keyboard's separator line
(SH1106's Lemon-font ascent constant was 2-3px short for them).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-12 19:40:53 +02:00
|
|
|
|
((MessagesScreen*)messages_screen)->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
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-05 16:08:44 +02:00
|
|
|
|
// Centred alert box. Long text used to be drawn as one drawTextCentered line
|
|
|
|
|
|
// that overflowed the border on both sides (e.g. "GPS on, tracking started"
|
|
|
|
|
|
// is already wider than a 128 px OLED); wrap it to up to three lines inside
|
|
|
|
|
|
// the box instead. Uses the shared wrap scratch (s_wrap_*) — single-threaded
|
|
|
|
|
|
// render path, same contract as the message views.
|
|
|
|
|
|
void UITask::renderAlertOverlay() {
|
|
|
|
|
|
_display->setTextSize(1);
|
|
|
|
|
|
const int lh = _display->getLineHeight();
|
|
|
|
|
|
const int pad = 3;
|
|
|
|
|
|
const int box_w = _display->width() - 8;
|
|
|
|
|
|
const int box_x = 4;
|
|
|
|
|
|
_display->translateUTF8ToBlocks(s_wrap_trans, _alert, sizeof(s_wrap_trans));
|
|
|
|
|
|
int nl = FullscreenMsgView::wrapLines(*_display, s_wrap_trans, box_w - pad * 2, s_wrap_lines, 3);
|
|
|
|
|
|
if (nl < 1) nl = 1;
|
|
|
|
|
|
int box_h = nl * lh + pad * 2;
|
|
|
|
|
|
int box_y = (_display->height() - box_h) / 2;
|
|
|
|
|
|
_display->setColor(DisplayDriver::DARK);
|
|
|
|
|
|
_display->fillRect(box_x, box_y, box_w, box_h);
|
|
|
|
|
|
_display->setColor(DisplayDriver::LIGHT);
|
|
|
|
|
|
_display->drawRect(box_x, box_y, box_w, box_h);
|
|
|
|
|
|
for (int i = 0; i < nl; i++)
|
|
|
|
|
|
_display->drawTextCentered(_display->width() / 2, box_y + pad + i * lh, s_wrap_lines[i]);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-08-08 20:01:31 +10:00
|
|
|
|
void UITask::setCurrScreen(UIScreen* c) {
|
2026-06-29 18:42:59 +02:00
|
|
|
|
// Fail safe on a null target: a screen pointer left uninitialised (member
|
|
|
|
|
|
// declared + navigator wired, but the `new XScreen()` line forgotten in
|
|
|
|
|
|
// begin()) stays nullptr thanks to the in-class initialisers. Bail here so
|
|
|
|
|
|
// that mistake is an inert no-op instead of a null deref in render()/poll().
|
|
|
|
|
|
if (!c) return;
|
2025-08-08 20:01:31 +10:00
|
|
|
|
curr = c;
|
2026-06-29 18:42:59 +02:00
|
|
|
|
c->onShow(); // central per-visit reset hook (see UIScreen::onShow)
|
2025-09-03 17:22:11 +02:00
|
|
|
|
_next_refresh = 100;
|
2025-08-08 20:01:31 +10:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-29 18:47:51 +02:00
|
|
|
|
bool UITask::savePrefsIfDirty(bool& dirty) {
|
|
|
|
|
|
if (!dirty) return false;
|
|
|
|
|
|
the_mesh.savePrefs();
|
|
|
|
|
|
dirty = false;
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
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
|
|
|
|
|
2026-07-05 22:21:42 +02:00
|
|
|
|
// Auto-save the live GPS trail before power-off when the user enabled it
|
|
|
|
|
|
// (Tools › Trail › Settings › Auto-save). This covers the low-battery
|
|
|
|
|
|
// auto-shutdown, which otherwise loses the whole route. Overwrites /trail
|
|
|
|
|
|
// (same file as the manual Trail › Save); guarded on count()>0 so an empty
|
|
|
|
|
|
// trail can't wipe a previously saved one.
|
|
|
|
|
|
if (_node_prefs && _node_prefs->trail_autosave_lowbatt && _trail.count() > 0) {
|
|
|
|
|
|
DataStore* ds = the_mesh.getDataStore();
|
|
|
|
|
|
if (ds) {
|
|
|
|
|
|
File f = ds->openWrite("/trail");
|
|
|
|
|
|
if (f) { _trail.writeTo(f); f.close(); }
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
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, "--");
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-30 22:10:18 +02:00
|
|
|
|
void UITask::enqueueKey(char c) {
|
|
|
|
|
|
if (c == 0) return;
|
|
|
|
|
|
uint8_t next = (_kq_head + 1) % KEY_QUEUE_SIZE;
|
|
|
|
|
|
if (next == _kq_tail) return; // full: drop newest rather than clobber unprocessed keys
|
|
|
|
|
|
_key_queue[_kq_head] = c;
|
|
|
|
|
|
_kq_head = next;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
bool UITask::dequeueKey(char& c) {
|
|
|
|
|
|
if (_kq_tail == _kq_head) return false;
|
|
|
|
|
|
c = _key_queue[_kq_tail];
|
|
|
|
|
|
_kq_tail = (_kq_tail + 1) % KEY_QUEUE_SIZE;
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-03-10 17:11:55 +01:00
|
|
|
|
void UITask::loop() {
|
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.
|
feat(ui): on-device channel management, remote admin tool, per-language keyboards
Messages:
- Add/edit/delete channels on-device (new ChannelsView, owned by the renamed
MessagesScreen — was QuickMsgScreen, whose name no longer matched its scope).
Channel secret entry supports a typed passphrase (SHA-256'd, same primitive
the library already uses for the routing hash) or a raw 32-hex-char key.
- MyMesh::setChannelLocal() factors out the setChannel/saveChannels/
onChannelRemoved sequence previously duplicated across the two
CMD_SET_CHANNEL branches, shared now by the BLE and on-device paths.
Tools > Admin (new):
- Log into a repeater/room server's admin account and send CLI commands,
the on-device equivalent of the app's repeater-admin feature.
- Commands are organised into category tabs (System/Radio/Routing/Actions)
with common get/set fields (name, radio profile, tx power, repeat, advert
intervals, ...) plus a free-text "Custom command..." fallback for anything
else. A field row fetches the current value, opens it pre-filled for
editing, and sends the change — falling back to a blank editor if the
fetch fails or times out.
- The admin password persists and self-heals exactly like room logins in
Messages: saved on a confirmed admin-level login, forgotten on a failed
one, left alone if merely under-privileged.
- New MyMesh::sendAdminCommand()/AbstractUITask::onAdminReply() plumbing so
a reply reaches the UI without touching the existing BLE/app CLI-terminal
path (queueMessage's should_display gate is untouched).
Shared TabBar.h extracted from NearbyScreen/BotScreen's independently
duplicated tab-carousel rendering (now a third consumer via Admin) — also
fixes neighbouring tabs vanishing outright when they didn't fully fit;
they now truncate with an ellipsis instead.
Keyboard: the combined "Ext.Latin" alphabet split into 8 separate,
linguistically complete per-language keyboards (Polish, Czech, Slovak,
German, French, Spanish, Portuguese, Nordic), and fixed an OLED-only bug
where tall accented glyphs overlapped the keyboard's separator line
(SH1106's Lemon-font ascent constant was 2-3px short for them).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-12 19:40:53 +02:00
|
|
|
|
((MessagesScreen*)messages_screen)->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 {
|
2026-06-30 22:10:18 +02:00
|
|
|
|
enqueueKey(checkDisplayOn(KEY_ENTER));
|
2026-05-17 10:09:54 +02:00
|
|
|
|
}
|
2025-08-08 20:01:31 +10:00
|
|
|
|
} else if (ev == BUTTON_EVENT_LONG_PRESS) {
|
2026-06-30 22:10:18 +02:00
|
|
|
|
enqueueKey(handleLongPress(KEY_ENTER)); // REVISIT: could be mapped to different key code
|
2025-08-08 20:01:31 +10:00
|
|
|
|
}
|
2026-06-30 22:10:18 +02:00
|
|
|
|
// Drain each direction fully: a burst of taps captured during a blocking
|
|
|
|
|
|
// refresh replays as several CLICKs, queued here and applied before one
|
|
|
|
|
|
// redraw (see enqueueKey / the dispatch at the end of loop()).
|
2026-05-10 15:18:35 +02:00
|
|
|
|
#if UI_HAS_JOYSTICK_UPDOWN
|
2026-06-30 22:10:18 +02:00
|
|
|
|
while (joystick_up.check() == BUTTON_EVENT_CLICK)
|
|
|
|
|
|
enqueueKey(checkDisplayOn(rotateJoystickKey(KEY_UP, joy_rot)));
|
|
|
|
|
|
while (joystick_down.check() == BUTTON_EVENT_CLICK)
|
|
|
|
|
|
enqueueKey(checkDisplayOn(rotateJoystickKey(KEY_DOWN, joy_rot)));
|
2026-05-10 15:18:35 +02:00
|
|
|
|
#endif
|
2026-06-30 22:10:18 +02:00
|
|
|
|
while ((ev = joystick_left.check()) != BUTTON_EVENT_NONE) {
|
|
|
|
|
|
if (ev == BUTTON_EVENT_CLICK) enqueueKey(checkDisplayOn(rotateJoystickKey(KEY_LEFT, joy_rot)));
|
|
|
|
|
|
else { if (ev == BUTTON_EVENT_LONG_PRESS) enqueueKey(handleLongPress(rotateJoystickKey(KEY_LEFT, joy_rot))); break; }
|
2025-08-17 19:01:28 +10:00
|
|
|
|
}
|
2026-06-30 22:10:18 +02:00
|
|
|
|
while ((ev = joystick_right.check()) != BUTTON_EVENT_NONE) {
|
|
|
|
|
|
if (ev == BUTTON_EVENT_CLICK) enqueueKey(checkDisplayOn(rotateJoystickKey(KEY_RIGHT, joy_rot)));
|
|
|
|
|
|
else { if (ev == BUTTON_EVENT_LONG_PRESS) enqueueKey(handleLongPress(rotateJoystickKey(KEY_RIGHT, joy_rot))); break; }
|
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 {
|
2026-06-30 22:10:18 +02:00
|
|
|
|
enqueueKey(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-06-30 22:10:18 +02:00
|
|
|
|
if (!_locked) enqueueKey(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) {
|
2026-06-30 22:10:18 +02:00
|
|
|
|
enqueueKey(checkDisplayOn(KEY_NEXT));
|
2025-10-16 17:33:22 +11:00
|
|
|
|
} else if (ev == BUTTON_EVENT_LONG_PRESS) {
|
2026-06-30 22:10:18 +02:00
|
|
|
|
enqueueKey(handleLongPress(KEY_ENTER));
|
2025-10-16 17:33:22 +11:00
|
|
|
|
} else if (ev == BUTTON_EVENT_DOUBLE_CLICK) {
|
2026-06-30 22:10:18 +02:00
|
|
|
|
enqueueKey(handleDoubleClick(KEY_PREV));
|
2025-10-16 17:33:22 +11:00
|
|
|
|
} else if (ev == BUTTON_EVENT_TRIPLE_CLICK) {
|
2026-06-30 22:10:18 +02:00
|
|
|
|
enqueueKey(handleTripleClick(KEY_SELECT));
|
2025-10-16 17:33:22 +11:00
|
|
|
|
}
|
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) {
|
2026-06-30 22:10:18 +02:00
|
|
|
|
enqueueKey(checkDisplayOn(KEY_NEXT));
|
2025-10-31 13:04:59 +00:00
|
|
|
|
} else if (ev == BUTTON_EVENT_LONG_PRESS) {
|
2026-06-30 22:10:18 +02:00
|
|
|
|
enqueueKey(handleLongPress(KEY_ENTER));
|
2025-10-31 13:04:59 +00:00
|
|
|
|
} else if (ev == BUTTON_EVENT_DOUBLE_CLICK) {
|
2026-06-30 22:10:18 +02:00
|
|
|
|
enqueueKey(handleDoubleClick(KEY_PREV));
|
2025-10-31 13:04:59 +00:00
|
|
|
|
} else if (ev == BUTTON_EVENT_TRIPLE_CLICK) {
|
2026-06-30 22:10:18 +02:00
|
|
|
|
enqueueKey(handleTripleClick(KEY_SELECT));
|
2025-10-31 13:04:59 +00:00
|
|
|
|
}
|
|
|
|
|
|
_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
|
|
|
|
|
feat(companion): clock tools — alarm, countdown timer, stopwatch
Add a Clock Tools screen (Enter on the home Clock page) with three time
utilities, plus the engine that drives them from UITask::loop() so they
work regardless of the current screen / display state:
- Alarm: a single one-shot wake alarm. Persisted in NodePrefs as a local
time-of-day; UITask schedules it as an ABSOLUTE fire instant recomputed
from that time, so it survives RTC re-syncs (mesh/app/GPS/CLI all jump
the clock) — small corrections still fire on time, a jump over the
target still fires (late, up to 6 h). Disarms after firing. A bell
icon signals an armed alarm on the clock face and the top status bar.
- Timer: a millis-based countdown (sync-immune), big HH:MM:SS readout,
rings even when off-screen.
- Stopwatch: millis-based, keeps running in the background.
Numeric fields use the shared framework DigitEditor (digit-by-digit,
min/max enforced). The alarm/timer/ring engine lives in UITask alongside
the locator/live-share engines (no screen-cast for time-critical logic).
E-ink: live readouts refresh coarsely (and on any key); timing is exact
regardless, and the countdown's buzzer fires on time. Rings override mute
and are dismissed by any key (auto-stop after 1 min). Cannot wake from a
full Shutdown (CPU/RAM powered down).
NodePrefs: +alarm_on/alarm_hour/alarm_min (fit existing tail padding,
sizeof unchanged 2488), SCHEMA_SENTINEL 0xC0DE0016 -> 0xC0DE0017,
DataStore read/clamp/write in lockstep.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 01:54:01 +02:00
|
|
|
|
// A ringing alarm/timer is dismissed by ANY key, even when locked or on another
|
2026-06-30 22:13:18 +02:00
|
|
|
|
// screen — and the queued keys are swallowed so they don't also act on the view.
|
|
|
|
|
|
if (_kq_head != _kq_tail && isRinging()) {
|
feat(companion): clock tools — alarm, countdown timer, stopwatch
Add a Clock Tools screen (Enter on the home Clock page) with three time
utilities, plus the engine that drives them from UITask::loop() so they
work regardless of the current screen / display state:
- Alarm: a single one-shot wake alarm. Persisted in NodePrefs as a local
time-of-day; UITask schedules it as an ABSOLUTE fire instant recomputed
from that time, so it survives RTC re-syncs (mesh/app/GPS/CLI all jump
the clock) — small corrections still fire on time, a jump over the
target still fires (late, up to 6 h). Disarms after firing. A bell
icon signals an armed alarm on the clock face and the top status bar.
- Timer: a millis-based countdown (sync-immune), big HH:MM:SS readout,
rings even when off-screen.
- Stopwatch: millis-based, keeps running in the background.
Numeric fields use the shared framework DigitEditor (digit-by-digit,
min/max enforced). The alarm/timer/ring engine lives in UITask alongside
the locator/live-share engines (no screen-cast for time-critical logic).
E-ink: live readouts refresh coarsely (and on any key); timing is exact
regardless, and the countdown's buzzer fires on time. Rings override mute
and are dismissed by any key (auto-stop after 1 min). Cannot wake from a
full Shutdown (CPU/RAM powered down).
NodePrefs: +alarm_on/alarm_hour/alarm_min (fit existing tail padding,
sizeof unchanged 2488), SCHEMA_SENTINEL 0xC0DE0016 -> 0xC0DE0017,
DataStore read/clamp/write in lockstep.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 01:54:01 +02:00
|
|
|
|
dismissRing();
|
2026-06-30 22:13:18 +02:00
|
|
|
|
_kq_head = _kq_tail = 0;
|
feat(companion): clock tools — alarm, countdown timer, stopwatch
Add a Clock Tools screen (Enter on the home Clock page) with three time
utilities, plus the engine that drives them from UITask::loop() so they
work regardless of the current screen / display state:
- Alarm: a single one-shot wake alarm. Persisted in NodePrefs as a local
time-of-day; UITask schedules it as an ABSOLUTE fire instant recomputed
from that time, so it survives RTC re-syncs (mesh/app/GPS/CLI all jump
the clock) — small corrections still fire on time, a jump over the
target still fires (late, up to 6 h). Disarms after firing. A bell
icon signals an armed alarm on the clock face and the top status bar.
- Timer: a millis-based countdown (sync-immune), big HH:MM:SS readout,
rings even when off-screen.
- Stopwatch: millis-based, keeps running in the background.
Numeric fields use the shared framework DigitEditor (digit-by-digit,
min/max enforced). The alarm/timer/ring engine lives in UITask alongside
the locator/live-share engines (no screen-cast for time-critical logic).
E-ink: live readouts refresh coarsely (and on any key); timing is exact
regardless, and the countdown's buzzer fires on time. Rings override mute
and are dismissed by any key (auto-stop after 1 min). Cannot wake from a
full Shutdown (CPU/RAM powered down).
NodePrefs: +alarm_on/alarm_hour/alarm_min (fit existing tail padding,
sizeof unchanged 2488), SCHEMA_SENTINEL 0xC0DE0016 -> 0xC0DE0017,
DataStore read/clamp/write in lockstep.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 01:54:01 +02:00
|
|
|
|
_next_refresh = 0;
|
2026-07-05 16:08:44 +02:00
|
|
|
|
// Locked: wakeForAlarm() held the wake window open for the whole ring;
|
|
|
|
|
|
// once dismissed, fall back to the usual brief lock-screen glance.
|
|
|
|
|
|
if (_locked) _lock_wake_until = millis() + 5000;
|
feat(companion): clock tools — alarm, countdown timer, stopwatch
Add a Clock Tools screen (Enter on the home Clock page) with three time
utilities, plus the engine that drives them from UITask::loop() so they
work regardless of the current screen / display state:
- Alarm: a single one-shot wake alarm. Persisted in NodePrefs as a local
time-of-day; UITask schedules it as an ABSOLUTE fire instant recomputed
from that time, so it survives RTC re-syncs (mesh/app/GPS/CLI all jump
the clock) — small corrections still fire on time, a jump over the
target still fires (late, up to 6 h). Disarms after firing. A bell
icon signals an armed alarm on the clock face and the top status bar.
- Timer: a millis-based countdown (sync-immune), big HH:MM:SS readout,
rings even when off-screen.
- Stopwatch: millis-based, keeps running in the background.
Numeric fields use the shared framework DigitEditor (digit-by-digit,
min/max enforced). The alarm/timer/ring engine lives in UITask alongside
the locator/live-share engines (no screen-cast for time-critical logic).
E-ink: live readouts refresh coarsely (and on any key); timing is exact
regardless, and the countdown's buzzer fires on time. Rings override mute
and are dismissed by any key (auto-stop after 1 min). Cannot wake from a
full Shutdown (CPU/RAM powered down).
NodePrefs: +alarm_on/alarm_hour/alarm_min (fit existing tail padding,
sizeof unchanged 2488), SCHEMA_SENTINEL 0xC0DE0016 -> 0xC0DE0017,
DataStore read/clamp/write in lockstep.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 01:54:01 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-30 22:10:18 +02:00
|
|
|
|
if (_kq_head != _kq_tail) {
|
2026-05-17 10:09:54 +02:00
|
|
|
|
if (!_locked && curr) {
|
2026-06-30 22:10:18 +02:00
|
|
|
|
// Apply the whole queued burst, then redraw once — N taps captured during
|
|
|
|
|
|
// a blocking refresh become N navigation steps at the cost of one refresh.
|
|
|
|
|
|
char k;
|
|
|
|
|
|
while (dequeueKey(k)) curr->handleInput(k);
|
2026-05-17 09:55:51 +02:00
|
|
|
|
{ uint32_t aoff = autoOffMillis(); if (aoff > 0) _auto_off = millis() + aoff; } // extend auto-off timer
|
2026-06-28 11:37:48 +02:00
|
|
|
|
// Note timing no longer depends on render cadence (TIMER1 IRQ advances
|
|
|
|
|
|
// notes directly — see buzzer.cpp), so a redraw right after a keypress
|
|
|
|
|
|
// can't clip a note; no need to hold it back while buzzer.isPlaying().
|
|
|
|
|
|
_next_refresh = 100; // trigger refresh immediately
|
2026-06-30 22:10:18 +02:00
|
|
|
|
} else {
|
|
|
|
|
|
_kq_head = _kq_tail = 0; // locked or no screen: eat all queued keys
|
|
|
|
|
|
// Locked: wake window is set only when display first turns on
|
|
|
|
|
|
if (_locked) _next_refresh = 0;
|
2026-05-17 09:55:51 +02:00
|
|
|
|
}
|
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();
|
|
|
|
|
|
|
feat(companion): clock tools — alarm, countdown timer, stopwatch
Add a Clock Tools screen (Enter on the home Clock page) with three time
utilities, plus the engine that drives them from UITask::loop() so they
work regardless of the current screen / display state:
- Alarm: a single one-shot wake alarm. Persisted in NodePrefs as a local
time-of-day; UITask schedules it as an ABSOLUTE fire instant recomputed
from that time, so it survives RTC re-syncs (mesh/app/GPS/CLI all jump
the clock) — small corrections still fire on time, a jump over the
target still fires (late, up to 6 h). Disarms after firing. A bell
icon signals an armed alarm on the clock face and the top status bar.
- Timer: a millis-based countdown (sync-immune), big HH:MM:SS readout,
rings even when off-screen.
- Stopwatch: millis-based, keeps running in the background.
Numeric fields use the shared framework DigitEditor (digit-by-digit,
min/max enforced). The alarm/timer/ring engine lives in UITask alongside
the locator/live-share engines (no screen-cast for time-critical logic).
E-ink: live readouts refresh coarsely (and on any key); timing is exact
regardless, and the countdown's buzzer fires on time. Rings override mute
and are dismissed by any key (auto-stop after 1 min). Cannot wake from a
full Shutdown (CPU/RAM powered down).
NodePrefs: +alarm_on/alarm_hour/alarm_min (fit existing tail padding,
sizeof unchanged 2488), SCHEMA_SENTINEL 0xC0DE0016 -> 0xC0DE0017,
DataStore read/clamp/write in lockstep.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 01:54:01 +02:00
|
|
|
|
// Alarm + countdown run regardless of the current screen / display state, so
|
|
|
|
|
|
// they're driven here (not via the current screen's poll()).
|
|
|
|
|
|
tickClockTools();
|
|
|
|
|
|
|
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);
|
2026-07-05 16:08:44 +02:00
|
|
|
|
// Alert overlay on top — without this a ringing alarm on a locked device
|
|
|
|
|
|
// played its melody against a screen that never said what was ringing.
|
|
|
|
|
|
if (millis() < _alert_expiry) renderAlertOverlay();
|
2026-05-17 09:55:51 +02:00
|
|
|
|
_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();
|
2026-07-17 22:04:29 +02:00
|
|
|
|
_kb.beginFrame();
|
2025-08-08 20:01:31 +10:00
|
|
|
|
int delay_millis = curr->render(*_display);
|
2026-07-17 22:04:29 +02:00
|
|
|
|
// Skip the alert overlay (new-message toast) while the keyboard is the
|
|
|
|
|
|
// thing actually on screen this frame -- it's shared across Messages/
|
|
|
|
|
|
// Bot/Settings/Admin/etc., so this covers every screen that uses it for
|
|
|
|
|
|
// full-screen text entry, not just message compose. Otherwise a message
|
|
|
|
|
|
// arriving mid-typing blanks out the letter grid for 3s with no way to
|
|
|
|
|
|
// see what's being typed.
|
|
|
|
|
|
if (millis() < _alert_expiry && !_kb.isVisible()) { // alert overlay on top of any (non-keyboard) screen
|
2026-07-05 16:08:44 +02:00
|
|
|
|
renderAlertOverlay();
|
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
|
feat(companion): clock tools — alarm, countdown timer, stopwatch
Add a Clock Tools screen (Enter on the home Clock page) with three time
utilities, plus the engine that drives them from UITask::loop() so they
work regardless of the current screen / display state:
- Alarm: a single one-shot wake alarm. Persisted in NodePrefs as a local
time-of-day; UITask schedules it as an ABSOLUTE fire instant recomputed
from that time, so it survives RTC re-syncs (mesh/app/GPS/CLI all jump
the clock) — small corrections still fire on time, a jump over the
target still fires (late, up to 6 h). Disarms after firing. A bell
icon signals an armed alarm on the clock face and the top status bar.
- Timer: a millis-based countdown (sync-immune), big HH:MM:SS readout,
rings even when off-screen.
- Stopwatch: millis-based, keeps running in the background.
Numeric fields use the shared framework DigitEditor (digit-by-digit,
min/max enforced). The alarm/timer/ring engine lives in UITask alongside
the locator/live-share engines (no screen-cast for time-critical logic).
E-ink: live readouts refresh coarsely (and on any key); timing is exact
regardless, and the countdown's buzzer fires on time. Rings override mute
and are dismissed by any key (auto-stop after 1 min). Cannot wake from a
full Shutdown (CPU/RAM powered down).
NodePrefs: +alarm_on/alarm_hour/alarm_min (fit existing tail padding,
sizeof unchanged 2488), SCHEMA_SENTINEL 0xC0DE0016 -> 0xC0DE0017,
DataStore read/clamp/write in lockstep.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 01:54:01 +02:00
|
|
|
|
if (!_locked && autoOffMillis() > 0 && millis() > _auto_off && !isRinging()) {
|
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();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-29 18:17:38 +02:00
|
|
|
|
// CONTRACT: every NodePrefs field that keys on a contact pubkey/prefix is
|
|
|
|
|
|
// cleared here, so a removed contact can't leave a dangling reference. If you
|
|
|
|
|
|
// add such a field, add its cleanup below (and mark the field in NodePrefs.h).
|
|
|
|
|
|
// Currently covered: favourite_contacts, locator_key, loc_share_dm_prefix,
|
|
|
|
|
|
// dm_notif[], dm_melody[]. Called for both explicit removal and silent
|
|
|
|
|
|
// auto-eviction (see MyMesh CMD_REMOVE_CONTACT / onContactOverwrite).
|
2026-06-26 17:48:05 +02:00
|
|
|
|
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;
|
|
|
|
|
|
}
|
feat(bot): room-server target, tab-carousel UI, per-target independence
Auto-Reply Bot gains a third target (room servers, alongside DM and
channel), new {name}/{hops} reply placeholders, and a DM all/favourites
allow-list. BotScreen is redesigned as a circular tab carousel (Direct /
Channel / Room / Other, same interaction as Nearby Nodes' filter tabs)
instead of one long scrolling list, which also exposed and fixed a
leftover coupling where Channel/Room trigger-replies and their !command
handling secretly depended on the DM tab's Enable/Commands toggles —
each target's Enable and Commands are now fully independent.
- NodePrefs: bot_room_enabled/prefix/trigger/reply, bot_dm_scope,
bot_commands_ch/bot_commands_room (bot_commands_enabled repurposed as
DM-only). SCHEMA_SENTINEL 0xC0DE001D -> 0xC0DE001F; sizeof unchanged
at 2712 (new bytes absorbed existing padding, verified via a
standalone host compile + offsetof check).
- DataStore: persists all new fields; seeds bot_commands_ch/room from
the old shared bot_commands_enabled on upgrade so existing
channel/room command behaviour isn't silently lost.
- MyMeshBot: tryBotReplyRoom/tryBotRoomCommand mirror the channel bot's
shape but post via sendMessage (room relays to members itself);
requires an existing login session with that room, same as a manual
post would. botDmSenderAllowed() gates DM trigger-reply/commands on
the favourites bit when bot_dm_scope=Fav.
- MsgExpand: {name}/{hops} as optional trailing params (default
nullptr/-1, no existing caller affected) — deliberately not exposed
on the general compose keyboard, only on bot Reply fields.
- QuickMsgScreen/UITask: room-target picker (mirrors the channel
picker), routing through the existing room-login prompt when there's
no saved password yet.
- BotScreen: tab carousel (LEFT/RIGHT switches tabs, UP/DOWN moves
rows, Enter is now the only way to change a value); Enable split out
of Channel/Room's combo row; Quiet Hours gained a stepper sub-mode;
Commands moved from a shared toggle into each tab.
- docs/tools_screen.md updated for the new tab layout and behaviour.
Not build-verified — no PlatformIO toolchain in this environment.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-10 20:16:59 +02:00
|
|
|
|
// Same fail-closed rule for the room bot's target: the room contact is
|
|
|
|
|
|
// gone, disable it rather than risk a re-added contact silently inheriting
|
|
|
|
|
|
// the old bot config.
|
|
|
|
|
|
if (_node_prefs->bot_room_enabled
|
|
|
|
|
|
&& memcmp(_node_prefs->bot_room_prefix, pub_key, NodePrefs::FAVOURITE_PREFIX_LEN) == 0) {
|
|
|
|
|
|
_node_prefs->bot_room_enabled = 0;
|
|
|
|
|
|
changed = true;
|
|
|
|
|
|
}
|
2026-06-26 17:48:05 +02:00
|
|
|
|
// 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-29 18:17:38 +02:00
|
|
|
|
// CONTRACT: every NodePrefs field that keys on a channel index is cleared here,
|
|
|
|
|
|
// so a channel re-added at a freed slot can't inherit the old one's settings.
|
|
|
|
|
|
// If you add such a field, add its cleanup below (and mark it in NodePrefs.h).
|
feat(admin): typed Radio/Routing field editors; pre-release audit fixes
Tools > Admin: split the Radio tab's single "f,bw,sf,cr" comma-string
field (free-text keyboard) into 4 independent, type-appropriate rows --
Frequency (same digit-cursor DigitEditor Settings/Repeater use locally),
Bandwidth/Spreading factor/Coding rate (discrete-set LEFT/RIGHT stepping),
plus TX power and the 3 Routing numeric fields as number steppers and
Repeat as an ON/OFF toggle. Edits happen locally (zero mesh traffic per
keystroke); only Enter sends one combined `set`, Cancel sends nothing.
Full pre-release audit of all 16 commits since v1.22 (5 parallel focus
areas: display/font, keyboard/messages, Nodes/Admin, Bot/channels,
UITask+NodePrefs schema) turned up and fixed:
- Admin: fetched FK_NUMBER values weren't clamped to the field's range,
so a value already out-of-range could get stuck unreachable; fixed
commit-time float format noise (%.6f -> %.3f); Cancel/failed-login
always returned to the Nodes picker even when Admin was opened
directly from a node's Hold-Enter action -- now returns to wherever
it was actually opened from (AdminScreen::_from_picker).
- Keyboard: one-shot Shift was consumed after the *first* T9 multi-tap,
so cycling to the 2nd/3rd candidate always came out lowercase --
fixed by caching the cycle's caps state (t9_caps).
- Messages: history is numbered newest-first, so a message arriving
while scrolled up to an older one silently relabeled the view onto a
different message -- selection now shifts with the insert.
- Channels: onChannelRemoved() didn't clear ch_notif_override/
ch_notif_muted/ch_fav_bitmask despite its own contract comment
requiring it (now far more reachable via the on-device Delete);
an all-zero hex secret silently self-deleted the channel it was
just saved into (collides with the empty-slot sentinel) -- rejected.
- Room login: isRoomLoggedIn() indexed the login-tracking ring
directly instead of via its head offset, silently wrong once the
ring wraps (8+ rooms/session) -- the new on-device Logout depends
on this being right.
- Font/display: removed the now fully-inert Settings > Display > Font
toggle and the dead LemonFont.h (retired by the earlier misc-fixed
font unification, zero remaining includes); fixed a copy-pasted
"5x7" comment (font is 6x9), two meaningless dead ternaries, and an
OLED/e-ink inconsistency in the undefined-glyph fallback box offset.
- AdminField's `kind`/bounds fields no longer rely on default member
initializers inside aggregate-init: this toolchain's actual nRF52
build (unlike env:native) has no explicit -std= override, so it
predates C++14's aggregate-with-default-member-initializer rule.
Given an explicit constructor instead -- portable regardless of
standard, all existing field-table literals unchanged.
Docs updated to match: tools_screen.md (Diagnostics as a 3-tab
carousel, was documented as one flat screen), message_screen.md
(chat bubbles, newest-at-bottom, cursor mode, secret validation),
settings_screen.md (dropped the dead Font row), solo_ui_framework.md
(header menu_hint signatures, icon priority-drop, KeyboardWidget's
T9/alphabets/cursor-mode). release-notes.md gains the v1.23 section
covering all of the above plus the other 15 commits since v1.22.
Build-verified on WioTrackerL1_companion_solo_dual.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-17 19:38:51 +02:00
|
|
|
|
// Currently covered: bot_channel_idx, loc_share_channel_idx, ch_notif_melody_*,
|
|
|
|
|
|
// ch_notif_override/ch_notif_muted, ch_fav_bitmask.
|
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;
|
|
|
|
|
|
}
|
feat(admin): typed Radio/Routing field editors; pre-release audit fixes
Tools > Admin: split the Radio tab's single "f,bw,sf,cr" comma-string
field (free-text keyboard) into 4 independent, type-appropriate rows --
Frequency (same digit-cursor DigitEditor Settings/Repeater use locally),
Bandwidth/Spreading factor/Coding rate (discrete-set LEFT/RIGHT stepping),
plus TX power and the 3 Routing numeric fields as number steppers and
Repeat as an ON/OFF toggle. Edits happen locally (zero mesh traffic per
keystroke); only Enter sends one combined `set`, Cancel sends nothing.
Full pre-release audit of all 16 commits since v1.22 (5 parallel focus
areas: display/font, keyboard/messages, Nodes/Admin, Bot/channels,
UITask+NodePrefs schema) turned up and fixed:
- Admin: fetched FK_NUMBER values weren't clamped to the field's range,
so a value already out-of-range could get stuck unreachable; fixed
commit-time float format noise (%.6f -> %.3f); Cancel/failed-login
always returned to the Nodes picker even when Admin was opened
directly from a node's Hold-Enter action -- now returns to wherever
it was actually opened from (AdminScreen::_from_picker).
- Keyboard: one-shot Shift was consumed after the *first* T9 multi-tap,
so cycling to the 2nd/3rd candidate always came out lowercase --
fixed by caching the cycle's caps state (t9_caps).
- Messages: history is numbered newest-first, so a message arriving
while scrolled up to an older one silently relabeled the view onto a
different message -- selection now shifts with the insert.
- Channels: onChannelRemoved() didn't clear ch_notif_override/
ch_notif_muted/ch_fav_bitmask despite its own contract comment
requiring it (now far more reachable via the on-device Delete);
an all-zero hex secret silently self-deleted the channel it was
just saved into (collides with the empty-slot sentinel) -- rejected.
- Room login: isRoomLoggedIn() indexed the login-tracking ring
directly instead of via its head offset, silently wrong once the
ring wraps (8+ rooms/session) -- the new on-device Logout depends
on this being right.
- Font/display: removed the now fully-inert Settings > Display > Font
toggle and the dead LemonFont.h (retired by the earlier misc-fixed
font unification, zero remaining includes); fixed a copy-pasted
"5x7" comment (font is 6x9), two meaningless dead ternaries, and an
OLED/e-ink inconsistency in the undefined-glyph fallback box offset.
- AdminField's `kind`/bounds fields no longer rely on default member
initializers inside aggregate-init: this toolchain's actual nRF52
build (unlike env:native) has no explicit -std= override, so it
predates C++14's aggregate-with-default-member-initializer rule.
Given an explicit constructor instead -- portable regardless of
standard, all existing field-table literals unchanged.
Docs updated to match: tools_screen.md (Diagnostics as a 3-tab
carousel, was documented as one flat screen), message_screen.md
(chat bubbles, newest-at-bottom, cursor mode, secret validation),
settings_screen.md (dropped the dead Font row), solo_ui_framework.md
(header menu_hint signatures, icon priority-drop, KeyboardWidget's
T9/alphabets/cursor-mode). release-notes.md gains the v1.23 section
covering all of the above plus the other 15 commits since v1.22.
Build-verified on WioTrackerL1_companion_solo_dual.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-17 19:38:51 +02:00
|
|
|
|
if (_node_prefs->ch_notif_override & mask) {
|
|
|
|
|
|
_node_prefs->ch_notif_override &= ~mask;
|
|
|
|
|
|
_node_prefs->ch_notif_muted &= ~mask;
|
|
|
|
|
|
changed = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (_node_prefs->ch_fav_bitmask & mask) {
|
|
|
|
|
|
_node_prefs->ch_fav_bitmask &= ~mask;
|
|
|
|
|
|
changed = true;
|
|
|
|
|
|
}
|
2026-06-27 00:39:10 +02:00
|
|
|
|
|
|
|
|
|
|
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() {
|
2026-07-21 20:30:03 +02:00
|
|
|
|
if (_node_prefs) applyGpsState(_node_prefs->gps_enabled == 0);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Sets GPS to an absolute state (vs. toggleGPS()'s flip) -- shared by the
|
|
|
|
|
|
// Home-page manual toggle and the bot's !gps on/off command, which needs to
|
|
|
|
|
|
// set a specific state rather than flip whatever it currently is.
|
|
|
|
|
|
void UITask::applyGpsState(bool on) {
|
|
|
|
|
|
if (_sensors == NULL) return;
|
|
|
|
|
|
int num = _sensors->getNumSettings();
|
|
|
|
|
|
for (int i = 0; i < num; i++) {
|
|
|
|
|
|
if (strcmp(_sensors->getSettingName(i), "gps") == 0) {
|
|
|
|
|
|
_sensors->setSettingValue("gps", on ? "1" : "0");
|
|
|
|
|
|
_node_prefs->gps_enabled = on ? 1 : 0;
|
|
|
|
|
|
notify(UIEventType::ack);
|
|
|
|
|
|
the_mesh.savePrefs();
|
|
|
|
|
|
showAlert(_node_prefs->gps_enabled ? "GPS: Enabled" : "GPS: Disabled", 800);
|
|
|
|
|
|
_next_refresh = 0;
|
|
|
|
|
|
break;
|
2025-09-03 08:25:59 +10:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-21 20:30:03 +02:00
|
|
|
|
void UITask::botSetGPS(bool on) {
|
|
|
|
|
|
applyGpsState(on);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Bot !buzz [seconds] -- a find-me signal, so it deliberately uses
|
|
|
|
|
|
// playForced() (bypasses the buzzer_quiet mute) rather than play(): a
|
|
|
|
|
|
// find-me beep that respects mute defeats its own purpose. Builds a simple
|
|
|
|
|
|
// repeating beep/rest RTTTL string sized to the requested duration into the
|
|
|
|
|
|
// persistent _bot_buzz_buf -- the nRF52 RTTTL player keeps a raw pointer into
|
|
|
|
|
|
// whatever buffer it's given and reads from it across loop() calls for the
|
|
|
|
|
|
// whole playback (same constraint as _notif_mel_buf), so this can't be a
|
|
|
|
|
|
// local/stack buffer.
|
|
|
|
|
|
void UITask::botBuzz(int seconds) {
|
|
|
|
|
|
#if defined(PIN_BUZZER)
|
|
|
|
|
|
if (seconds < 1) seconds = 5;
|
|
|
|
|
|
if (seconds > 30) seconds = 30;
|
|
|
|
|
|
int pairs = seconds * 2; // b=120: an "8c,8p," pair is 250+250 = 500ms
|
|
|
|
|
|
int n = snprintf(_bot_buzz_buf, sizeof(_bot_buzz_buf), "Buzz:b=120:");
|
|
|
|
|
|
for (int i = 0; i < pairs && n < (int)sizeof(_bot_buzz_buf) - 7; i++)
|
|
|
|
|
|
n += snprintf(_bot_buzz_buf + n, sizeof(_bot_buzz_buf) - n, "8c,8p,");
|
|
|
|
|
|
buzzer.playForced(_bot_buzz_buf);
|
|
|
|
|
|
#endif
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#if defined(PIN_GPIO1)
|
|
|
|
|
|
static uint32_t gpioPin(int idx) { // idx 1..4
|
|
|
|
|
|
static const uint32_t pins[4] = { PIN_GPIO1, PIN_GPIO2, PIN_GPIO3, PIN_GPIO4 };
|
|
|
|
|
|
return (idx >= 1 && idx <= 4) ? pins[idx - 1] : 0xFFFFFFFF;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
static uint8_t* gpioModeField(NodePrefs* p, int idx) { // idx 1..4
|
|
|
|
|
|
switch (idx) {
|
|
|
|
|
|
case 1: return &p->gpio1_mode;
|
|
|
|
|
|
case 2: return &p->gpio2_mode;
|
|
|
|
|
|
case 3: return &p->gpio3_mode;
|
|
|
|
|
|
case 4: return &p->gpio4_mode;
|
|
|
|
|
|
default: return NULL;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Push a saved mode value to the actual pin hardware -- shared by
|
|
|
|
|
|
// setGpioMode() (live edits from the UI) and applyAllGpioModes() (boot
|
|
|
|
|
|
// restore), which differ only in whether the mode gets persisted. Mode 4
|
|
|
|
|
|
// (Analog) uses the same "leave it alone" config as Off: the SAADC reads the
|
|
|
|
|
|
// pin directly regardless of the GPIO block's state, and cfg_default (no
|
|
|
|
|
|
// pull, disconnected buffer) is exactly what Nordic recommends for an ADC
|
|
|
|
|
|
// input to avoid extra leakage current -- there's nothing separate to set up
|
|
|
|
|
|
// here, unlike Input/Output.
|
|
|
|
|
|
static void applyGpioModeToPin(uint32_t pin, uint8_t mode) {
|
|
|
|
|
|
switch (mode) {
|
|
|
|
|
|
case 1: nrf_gpio_cfg_input(pin, NRF_GPIO_PIN_PULLUP); break; // Input
|
|
|
|
|
|
case 2: nrf_gpio_cfg_output(pin); nrf_gpio_pin_clear(pin); break; // Output, off
|
|
|
|
|
|
case 3: nrf_gpio_cfg_output(pin); nrf_gpio_pin_set(pin); break; // Output, on
|
|
|
|
|
|
default: nrf_gpio_cfg_default(pin); break; // Off / Analog
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// GPIO1 (P0.02) = AIN0, GPIO2 (P0.29) = AIN5 -- the only two user pins wired
|
|
|
|
|
|
// to the nRF52840's SAADC (confirmed against wiring_analog_nRF52.c's own
|
|
|
|
|
|
// pin->channel switch). GPIO3/GPIO4 (P0.09/P0.10) have no ADC channel.
|
|
|
|
|
|
static uint32_t gpioAnalogPsel(int idx) { // idx 1..4; 0 (NC) if unsupported
|
|
|
|
|
|
if (idx == 1) return SAADC_CH_PSELP_PSELP_AnalogInput0;
|
|
|
|
|
|
if (idx == 2) return SAADC_CH_PSELP_PSELP_AnalogInput5;
|
|
|
|
|
|
return SAADC_CH_PSELP_PSELP_NC;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// One-shot SAADC read, bypassing Arduino's analogRead() -- that function
|
|
|
|
|
|
// treats its argument as an ARDUINO PIN INDEX (looked up through
|
|
|
|
|
|
// g_ADigitalPinMap[]), not a raw channel, and no Arduino index maps to our
|
|
|
|
|
|
// raw GPIO1/GPIO2 pins (same reason digitalWrite()/pinMode() can't be used
|
|
|
|
|
|
// for these pins either -- see the file-level notes on PIN_GPIO1..4).
|
|
|
|
|
|
// Mirrors wiring_analog_nRF52.c's analogRead_internal() exactly (10-bit,
|
|
|
|
|
|
// 0.6V internal reference, 1/6 gain -> 0-3.6V range) so the numbers read the
|
|
|
|
|
|
// same as a normal analogRead() would, just addressing the SAADC channel
|
|
|
|
|
|
// directly instead of going through the pin-index dispatch.
|
|
|
|
|
|
static uint16_t readAnalogMv(uint32_t psel) {
|
|
|
|
|
|
NRF_SAADC->RESOLUTION = SAADC_RESOLUTION_VAL_10bit;
|
|
|
|
|
|
NRF_SAADC->ENABLE = (SAADC_ENABLE_ENABLE_Enabled << SAADC_ENABLE_ENABLE_Pos);
|
|
|
|
|
|
for (int i = 0; i < 8; i++) {
|
|
|
|
|
|
NRF_SAADC->CH[i].PSELN = SAADC_CH_PSELP_PSELP_NC;
|
|
|
|
|
|
NRF_SAADC->CH[i].PSELP = SAADC_CH_PSELP_PSELP_NC;
|
|
|
|
|
|
}
|
|
|
|
|
|
NRF_SAADC->CH[0].CONFIG =
|
|
|
|
|
|
((SAADC_CH_CONFIG_RESP_Bypass << SAADC_CH_CONFIG_RESP_Pos) & SAADC_CH_CONFIG_RESP_Msk)
|
|
|
|
|
|
| ((SAADC_CH_CONFIG_RESP_Bypass << SAADC_CH_CONFIG_RESN_Pos) & SAADC_CH_CONFIG_RESN_Msk)
|
|
|
|
|
|
| ((SAADC_CH_CONFIG_GAIN_Gain1_6 << SAADC_CH_CONFIG_GAIN_Pos) & SAADC_CH_CONFIG_GAIN_Msk)
|
|
|
|
|
|
| ((SAADC_CH_CONFIG_REFSEL_Internal << SAADC_CH_CONFIG_REFSEL_Pos) & SAADC_CH_CONFIG_REFSEL_Msk)
|
|
|
|
|
|
| ((SAADC_CH_CONFIG_TACQ_3us << SAADC_CH_CONFIG_TACQ_Pos) & SAADC_CH_CONFIG_TACQ_Msk)
|
|
|
|
|
|
| ((SAADC_CH_CONFIG_MODE_SE << SAADC_CH_CONFIG_MODE_Pos) & SAADC_CH_CONFIG_MODE_Msk);
|
|
|
|
|
|
NRF_SAADC->CH[0].PSELN = psel;
|
|
|
|
|
|
NRF_SAADC->CH[0].PSELP = psel;
|
|
|
|
|
|
|
|
|
|
|
|
volatile int16_t value = 0;
|
|
|
|
|
|
NRF_SAADC->RESULT.PTR = (uint32_t)&value;
|
|
|
|
|
|
NRF_SAADC->RESULT.MAXCNT = 1;
|
|
|
|
|
|
|
|
|
|
|
|
NRF_SAADC->TASKS_START = 1;
|
|
|
|
|
|
while (!NRF_SAADC->EVENTS_STARTED);
|
|
|
|
|
|
NRF_SAADC->EVENTS_STARTED = 0;
|
|
|
|
|
|
|
|
|
|
|
|
NRF_SAADC->TASKS_SAMPLE = 1;
|
|
|
|
|
|
while (!NRF_SAADC->EVENTS_END);
|
|
|
|
|
|
NRF_SAADC->EVENTS_END = 0;
|
|
|
|
|
|
|
|
|
|
|
|
NRF_SAADC->TASKS_STOP = 1;
|
|
|
|
|
|
while (!NRF_SAADC->EVENTS_STOPPED);
|
|
|
|
|
|
NRF_SAADC->EVENTS_STOPPED = 0;
|
|
|
|
|
|
|
|
|
|
|
|
NRF_SAADC->ENABLE = (SAADC_ENABLE_ENABLE_Disabled << SAADC_ENABLE_ENABLE_Pos);
|
|
|
|
|
|
|
|
|
|
|
|
if (value < 0) value = 0;
|
|
|
|
|
|
// 10-bit, 1/6 gain, 0.6V internal ref -> full-scale = 0.6V / (1/6) = 3.6V
|
|
|
|
|
|
return (uint16_t)(((uint32_t)value * 3600) / 1024);
|
|
|
|
|
|
}
|
|
|
|
|
|
#endif
|
|
|
|
|
|
|
|
|
|
|
|
// Cycle a user GPIO pin's mode (Off/In/Out-low/Out-high), apply it to the
|
|
|
|
|
|
// actual pin, and persist. Called by GpioScreen on Enter.
|
|
|
|
|
|
void UITask::setGpioMode(int idx, uint8_t mode) {
|
|
|
|
|
|
#if defined(PIN_GPIO1)
|
|
|
|
|
|
if (!_node_prefs) return;
|
|
|
|
|
|
uint8_t* f = gpioModeField(_node_prefs, idx);
|
|
|
|
|
|
uint32_t pin = gpioPin(idx);
|
|
|
|
|
|
if (!f || pin == 0xFFFFFFFF) return;
|
|
|
|
|
|
if (mode == 4 && !gpioSupportsAnalog(idx)) mode = 0; // no ADC channel on this pin -- fall back to Off
|
|
|
|
|
|
*f = mode;
|
|
|
|
|
|
applyGpioModeToPin(pin, mode);
|
|
|
|
|
|
the_mesh.savePrefs();
|
|
|
|
|
|
#else
|
|
|
|
|
|
(void)idx; (void)mode;
|
|
|
|
|
|
#endif
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Boot-time restore: push each pin's saved mode to hardware before any UI/bot
|
|
|
|
|
|
// interaction (mirrors MyMesh::applyGpsPrefs()'s role for the GPS toggle --
|
|
|
|
|
|
// there's no generic "restore all settings" hook in this codebase, each
|
|
|
|
|
|
// persisted hardware toggle gets its own bespoke boot call). Deliberately
|
|
|
|
|
|
// doesn't call savePrefs() -- nothing changed, just re-applying what's
|
|
|
|
|
|
// already on disk.
|
|
|
|
|
|
void UITask::applyAllGpioModes() {
|
|
|
|
|
|
#if defined(PIN_GPIO1)
|
|
|
|
|
|
if (!_node_prefs) return;
|
|
|
|
|
|
for (int i = 1; i <= 4; i++) {
|
|
|
|
|
|
uint8_t* f = gpioModeField(_node_prefs, i);
|
|
|
|
|
|
if (f) applyGpioModeToPin(gpioPin(i), *f);
|
|
|
|
|
|
}
|
|
|
|
|
|
#endif
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
bool UITask::botSetGPIO(int idx, bool on) {
|
|
|
|
|
|
#if defined(PIN_GPIO1)
|
|
|
|
|
|
if (!_node_prefs) return false;
|
|
|
|
|
|
uint8_t* f = gpioModeField(_node_prefs, idx);
|
|
|
|
|
|
if (!f || (*f != 2 && *f != 3)) return false; // not configured as Output
|
|
|
|
|
|
setGpioMode(idx, on ? 3 : 2);
|
|
|
|
|
|
return true;
|
|
|
|
|
|
#else
|
|
|
|
|
|
(void)idx; (void)on;
|
|
|
|
|
|
return false;
|
|
|
|
|
|
#endif
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
bool UITask::botGetGPIO(int idx, bool& is_output, bool& value) {
|
|
|
|
|
|
#if defined(PIN_GPIO1)
|
|
|
|
|
|
if (!_node_prefs) return false;
|
|
|
|
|
|
uint8_t* f = gpioModeField(_node_prefs, idx);
|
|
|
|
|
|
uint32_t pin = gpioPin(idx);
|
|
|
|
|
|
if (!f || *f == 0 || *f == 4 || pin == 0xFFFFFFFF) return false; // Off / Analog / unsupported
|
|
|
|
|
|
is_output = (*f == 2 || *f == 3);
|
|
|
|
|
|
value = is_output ? (nrf_gpio_pin_out_read(pin) != 0) : (nrf_gpio_pin_read(pin) != 0);
|
|
|
|
|
|
return true;
|
|
|
|
|
|
#else
|
|
|
|
|
|
(void)idx; (void)is_output; (void)value;
|
|
|
|
|
|
return false;
|
|
|
|
|
|
#endif
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
bool UITask::gpioSupportsAnalog(int idx) const {
|
|
|
|
|
|
#if defined(PIN_GPIO1)
|
|
|
|
|
|
return idx == 1 || idx == 2;
|
|
|
|
|
|
#else
|
|
|
|
|
|
(void)idx;
|
|
|
|
|
|
return false;
|
|
|
|
|
|
#endif
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
bool UITask::botGetGPIOAnalog(int idx, int& millivolts) {
|
|
|
|
|
|
#if defined(PIN_GPIO1)
|
|
|
|
|
|
if (!_node_prefs || !gpioSupportsAnalog(idx)) return false;
|
|
|
|
|
|
uint8_t* f = gpioModeField(_node_prefs, idx);
|
|
|
|
|
|
if (!f || *f != 4) return false; // not in Analog mode
|
|
|
|
|
|
millivolts = readAnalogMv(gpioAnalogPsel(idx));
|
|
|
|
|
|
return true;
|
|
|
|
|
|
#else
|
|
|
|
|
|
(void)idx; (void)millivolts;
|
|
|
|
|
|
return false;
|
|
|
|
|
|
#endif
|
|
|
|
|
|
}
|
|
|
|
|
|
|
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) {
|
2026-07-17 21:02:33 +02:00
|
|
|
|
_display->setSingleFont(_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
|
|
|
|
|
|
}
|