2025-06-06 15:30:35 +10:00
|
|
|
#include <Arduino.h>
|
|
|
|
|
#include "DataStore.h"
|
2026-06-17 21:35:30 +02:00
|
|
|
#include "Features.h" // FEAT_JOYSTICK_ROTATION_SETTING (else `#if !FEAT_…` is always true)
|
2026-06-21 23:09:27 +02:00
|
|
|
#include <target.h> // radio_driver — repeater-profile freq bounds (getFreqBounds)
|
2025-06-06 15:30:35 +10:00
|
|
|
|
2025-08-02 08:31:23 +10:00
|
|
|
#if defined(EXTRAFS) || defined(QSPIFLASH)
|
2025-07-29 20:06:35 +10:00
|
|
|
#define MAX_BLOBRECS 100
|
|
|
|
|
#else
|
|
|
|
|
#define MAX_BLOBRECS 20
|
|
|
|
|
#endif
|
|
|
|
|
|
2025-09-06 14:07:18 +10:00
|
|
|
DataStore::DataStore(FILESYSTEM& fs, mesh::RTCClock& clock) : _fs(&fs), _fsExtra(nullptr), _clock(&clock),
|
2025-06-06 15:30:35 +10:00
|
|
|
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
|
|
|
|
|
identity_store(fs, "")
|
|
|
|
|
#elif defined(RP2040_PLATFORM)
|
|
|
|
|
identity_store(fs, "/identity")
|
|
|
|
|
#else
|
|
|
|
|
identity_store(fs, "/identity")
|
|
|
|
|
#endif
|
|
|
|
|
{
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-22 13:09:54 +10:00
|
|
|
#if defined(EXTRAFS) || defined(QSPIFLASH)
|
|
|
|
|
DataStore::DataStore(FILESYSTEM& fs, FILESYSTEM& fsExtra, mesh::RTCClock& clock) : _fs(&fs), _fsExtra(&fsExtra), _clock(&clock),
|
2025-06-06 19:50:51 +10:00
|
|
|
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
|
2025-08-22 13:09:54 +10:00
|
|
|
identity_store(fs, "")
|
2025-06-06 19:50:51 +10:00
|
|
|
#elif defined(RP2040_PLATFORM)
|
2025-08-22 13:09:54 +10:00
|
|
|
identity_store(fs, "/identity")
|
2025-06-06 19:50:51 +10:00
|
|
|
#else
|
2025-08-22 13:09:54 +10:00
|
|
|
identity_store(fs, "/identity")
|
|
|
|
|
#endif
|
|
|
|
|
{
|
|
|
|
|
}
|
|
|
|
|
#endif
|
|
|
|
|
|
|
|
|
|
static File openWrite(FILESYSTEM* fs, const char* filename) {
|
|
|
|
|
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
|
|
|
|
|
fs->remove(filename);
|
|
|
|
|
return fs->open(filename, FILE_O_WRITE);
|
|
|
|
|
#elif defined(RP2040_PLATFORM)
|
|
|
|
|
return fs->open(filename, "w");
|
|
|
|
|
#else
|
|
|
|
|
return fs->open(filename, "w", true);
|
2025-06-06 19:50:51 +10:00
|
|
|
#endif
|
|
|
|
|
}
|
|
|
|
|
|
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
|
|
|
// Atomically swap a fully-written temp file over its final path. LittleFS
|
|
|
|
|
// (nRF52/STM32) rename replaces an existing destination atomically, so a crash
|
|
|
|
|
// leaves either the old file or the new one intact — never a truncated mix.
|
|
|
|
|
// Other Arduino filesystems can't rename onto an existing file, so the
|
|
|
|
|
// destination is dropped first (a metadata-only window, vs. the record-by-record
|
|
|
|
|
// write window of a direct overwrite). Returns false if the swap fails.
|
|
|
|
|
static bool commitTempFile(FILESYSTEM* fs, const char* tmp, const char* final_path) {
|
|
|
|
|
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
|
|
|
|
|
return fs->rename(tmp, final_path);
|
|
|
|
|
#else
|
|
|
|
|
fs->remove(final_path);
|
|
|
|
|
return fs->rename(tmp, final_path);
|
|
|
|
|
#endif
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-20 17:36:52 +10:00
|
|
|
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
|
|
|
|
|
static uint32_t _ContactsChannelsTotalBlocks = 0;
|
|
|
|
|
#endif
|
|
|
|
|
|
2025-06-06 15:30:35 +10:00
|
|
|
void DataStore::begin() {
|
|
|
|
|
#if defined(RP2040_PLATFORM)
|
|
|
|
|
identity_store.begin();
|
|
|
|
|
#endif
|
|
|
|
|
|
2025-06-06 19:50:51 +10:00
|
|
|
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
|
2025-09-20 17:36:52 +10:00
|
|
|
_ContactsChannelsTotalBlocks = _getContactsChannelsFS()->_getFS()->cfg->block_count;
|
2025-06-06 19:50:51 +10:00
|
|
|
checkAdvBlobFile();
|
2025-08-22 13:09:54 +10:00
|
|
|
#if defined(EXTRAFS) || defined(QSPIFLASH)
|
|
|
|
|
migrateToSecondaryFS();
|
|
|
|
|
#endif
|
2025-06-06 19:50:51 +10:00
|
|
|
#else
|
2025-06-06 15:30:35 +10:00
|
|
|
// init 'blob store' support
|
|
|
|
|
_fs->mkdir("/bl");
|
2025-06-06 19:50:51 +10:00
|
|
|
#endif
|
2025-06-06 15:30:35 +10:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#if defined(ESP32)
|
|
|
|
|
#include <SPIFFS.h>
|
2025-12-31 13:40:05 +01:00
|
|
|
#include <nvs_flash.h>
|
2025-06-06 15:30:35 +10:00
|
|
|
#elif defined(RP2040_PLATFORM)
|
|
|
|
|
#include <LittleFS.h>
|
2025-08-25 12:41:59 +10:00
|
|
|
#elif defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
|
2025-08-02 08:31:23 +10:00
|
|
|
#if defined(QSPIFLASH)
|
|
|
|
|
#include <CustomLFS_QSPIFlash.h>
|
2025-09-06 14:07:18 +10:00
|
|
|
#elif defined(EXTRAFS)
|
2025-08-25 12:41:59 +10:00
|
|
|
#include <CustomLFS.h>
|
|
|
|
|
#else
|
|
|
|
|
#include <InternalFileSystem.h>
|
2025-08-02 08:31:23 +10:00
|
|
|
#endif
|
2025-08-25 12:41:59 +10:00
|
|
|
#endif
|
2025-06-06 15:30:35 +10:00
|
|
|
|
2025-06-22 21:35:21 +12:00
|
|
|
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
|
|
|
|
|
int _countLfsBlock(void *p, lfs_block_t block){
|
2025-09-20 17:36:52 +10:00
|
|
|
if (block > _ContactsChannelsTotalBlocks) {
|
|
|
|
|
MESH_DEBUG_PRINTLN("ERROR: Block %d exceeds filesystem bounds - CORRUPTION DETECTED!", block);
|
|
|
|
|
return LFS_ERR_CORRUPT; // return error to abort lfs_traverse() gracefully
|
|
|
|
|
}
|
2025-06-22 21:38:35 +12:00
|
|
|
lfs_size_t *size = (lfs_size_t*) p;
|
|
|
|
|
*size += 1;
|
2025-09-20 17:36:52 +10:00
|
|
|
return 0;
|
2025-06-22 21:35:21 +12:00
|
|
|
}
|
|
|
|
|
|
2025-07-29 20:06:35 +10:00
|
|
|
lfs_ssize_t _getLfsUsedBlockCount(FILESYSTEM* fs) {
|
2025-06-22 21:35:21 +12:00
|
|
|
lfs_size_t size = 0;
|
2025-09-20 17:36:52 +10:00
|
|
|
int err = lfs_traverse(fs->_getFS(), _countLfsBlock, &size);
|
|
|
|
|
if (err) {
|
|
|
|
|
MESH_DEBUG_PRINTLN("ERROR: lfs_traverse() error: %d", err);
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
2025-06-22 21:35:21 +12:00
|
|
|
return size;
|
|
|
|
|
}
|
|
|
|
|
#endif
|
|
|
|
|
|
2025-06-20 16:47:59 +10:00
|
|
|
uint32_t DataStore::getStorageUsedKb() const {
|
|
|
|
|
#if defined(ESP32)
|
|
|
|
|
return SPIFFS.usedBytes() / 1024;
|
|
|
|
|
#elif defined(RP2040_PLATFORM)
|
|
|
|
|
FSInfo info;
|
|
|
|
|
info.usedBytes = 0;
|
|
|
|
|
_fs->info(info);
|
|
|
|
|
return info.usedBytes / 1024;
|
2025-06-22 21:35:21 +12:00
|
|
|
#elif defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
|
2025-09-06 14:07:18 +10:00
|
|
|
const lfs_config* config = _getContactsChannelsFS()->_getFS()->cfg;
|
|
|
|
|
int usedBlockCount = _getLfsUsedBlockCount(_getContactsChannelsFS());
|
2025-06-22 21:38:35 +12:00
|
|
|
int usedBytes = config->block_size * usedBlockCount;
|
2025-06-22 21:35:21 +12:00
|
|
|
return usedBytes / 1024;
|
2025-06-20 16:47:59 +10:00
|
|
|
#else
|
2025-06-22 21:35:21 +12:00
|
|
|
return 0;
|
2025-06-20 16:47:59 +10:00
|
|
|
#endif
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
uint32_t DataStore::getStorageTotalKb() const {
|
|
|
|
|
#if defined(ESP32)
|
|
|
|
|
return SPIFFS.totalBytes() / 1024;
|
|
|
|
|
#elif defined(RP2040_PLATFORM)
|
|
|
|
|
FSInfo info;
|
|
|
|
|
info.totalBytes = 0;
|
|
|
|
|
_fs->info(info);
|
|
|
|
|
return info.totalBytes / 1024;
|
2025-06-22 21:35:21 +12:00
|
|
|
#elif defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
|
2025-09-06 14:07:18 +10:00
|
|
|
const lfs_config* config = _getContactsChannelsFS()->_getFS()->cfg;
|
2025-06-22 21:38:35 +12:00
|
|
|
int totalBytes = config->block_size * config->block_count;
|
2025-06-22 21:35:21 +12:00
|
|
|
return totalBytes / 1024;
|
2025-06-20 16:47:59 +10:00
|
|
|
#else
|
2025-06-22 21:35:21 +12:00
|
|
|
return 0;
|
2025-06-20 16:47:59 +10:00
|
|
|
#endif
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-07 15:23:55 +12:00
|
|
|
File DataStore::openRead(const char* filename) {
|
|
|
|
|
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
|
|
|
|
|
return _fs->open(filename, FILE_O_READ);
|
|
|
|
|
#elif defined(RP2040_PLATFORM)
|
|
|
|
|
return _fs->open(filename, "r");
|
|
|
|
|
#else
|
2025-06-07 16:17:45 +12:00
|
|
|
return _fs->open(filename, "r", false);
|
2025-06-07 15:23:55 +12:00
|
|
|
#endif
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-22 13:09:54 +10:00
|
|
|
File DataStore::openRead(FILESYSTEM* fs, const char* filename) {
|
|
|
|
|
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
|
|
|
|
|
return fs->open(filename, FILE_O_READ);
|
|
|
|
|
#elif defined(RP2040_PLATFORM)
|
|
|
|
|
return fs->open(filename, "r");
|
|
|
|
|
#else
|
|
|
|
|
return fs->open(filename, "r", false);
|
|
|
|
|
#endif
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-26 07:49:17 +02:00
|
|
|
File DataStore::openWrite(const char* filename) {
|
|
|
|
|
return ::openWrite(_fs, filename);
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-26 10:55:24 +02:00
|
|
|
bool DataStore::commitFile(const char* tmp_path, const char* final_path) {
|
|
|
|
|
return commitTempFile(_fs, tmp_path, final_path);
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-07 15:44:36 +12:00
|
|
|
bool DataStore::removeFile(const char* filename) {
|
|
|
|
|
return _fs->remove(filename);
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-22 13:09:54 +10:00
|
|
|
bool DataStore::removeFile(FILESYSTEM* fs, const char* filename) {
|
|
|
|
|
return fs->remove(filename);
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-06 15:30:35 +10:00
|
|
|
bool DataStore::formatFileSystem() {
|
|
|
|
|
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
|
2025-09-06 14:07:18 +10:00
|
|
|
if (_fsExtra == nullptr) {
|
|
|
|
|
return _fs->format();
|
|
|
|
|
} else {
|
|
|
|
|
return _fs->format() && _fsExtra->format();
|
|
|
|
|
}
|
2025-06-06 15:30:35 +10:00
|
|
|
#elif defined(RP2040_PLATFORM)
|
|
|
|
|
return LittleFS.format();
|
|
|
|
|
#elif defined(ESP32)
|
2025-12-31 13:40:05 +01:00
|
|
|
bool fs_success = ((fs::SPIFFSFS *)_fs)->format();
|
|
|
|
|
esp_err_t nvs_err = nvs_flash_erase(); // no need to reinit, will be done by reboot
|
|
|
|
|
return fs_success && (nvs_err == ESP_OK);
|
2025-06-06 15:30:35 +10:00
|
|
|
#else
|
|
|
|
|
#error "need to implement format()"
|
|
|
|
|
#endif
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool DataStore::loadMainIdentity(mesh::LocalIdentity &identity) {
|
|
|
|
|
return identity_store.load("_main", identity);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool DataStore::saveMainIdentity(const mesh::LocalIdentity &identity) {
|
|
|
|
|
return identity_store.save("_main", identity);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void DataStore::loadPrefs(NodePrefs& prefs, double& node_lat, double& node_lon) {
|
|
|
|
|
if (_fs->exists("/new_prefs")) {
|
|
|
|
|
loadPrefsInt("/new_prefs", prefs, node_lat, node_lon); // new filename
|
|
|
|
|
} else if (_fs->exists("/node_prefs")) {
|
|
|
|
|
loadPrefsInt("/node_prefs", prefs, node_lat, node_lon);
|
|
|
|
|
savePrefs(prefs, node_lat, node_lon); // save to new filename
|
|
|
|
|
_fs->remove("/node_prefs"); // remove old
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void DataStore::loadPrefsInt(const char *filename, NodePrefs& _prefs, double& node_lat, double& node_lon) {
|
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
|
|
|
// Set hardware defaults before reading — if the file is older and lacks these fields,
|
|
|
|
|
// the compile-time values apply rather than the zero from memset in MyMesh::begin().
|
2026-05-20 21:53:40 +02:00
|
|
|
#ifdef DISPLAY_ROTATION
|
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
|
|
|
_prefs.display_rotation = DISPLAY_ROTATION;
|
2026-05-23 15:20:40 +02:00
|
|
|
#endif
|
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
|
|
|
// 0 is a valid SNR threshold, so the "off" state needs its own sentinel set
|
|
|
|
|
// before reading — an older file lacking this field must read as disabled,
|
|
|
|
|
// not as "filter everything below 0 dB".
|
|
|
|
|
_prefs.repeat_min_snr = NodePrefs::REPEAT_SNR_DISABLED;
|
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
|
|
|
File file = openRead(_fs, filename);
|
|
|
|
|
if (!file) return;
|
|
|
|
|
|
|
|
|
|
uint8_t pad[8];
|
|
|
|
|
|
|
|
|
|
// Core fields — present in every saved file (written unconditionally since the beginning).
|
|
|
|
|
file.read((uint8_t *)&_prefs.airtime_factor, sizeof(_prefs.airtime_factor));
|
|
|
|
|
file.read((uint8_t *)_prefs.node_name, sizeof(_prefs.node_name));
|
|
|
|
|
file.read(pad, 4);
|
|
|
|
|
file.read((uint8_t *)&node_lat, sizeof(node_lat));
|
|
|
|
|
file.read((uint8_t *)&node_lon, sizeof(node_lon));
|
|
|
|
|
file.read((uint8_t *)&_prefs.freq, sizeof(_prefs.freq));
|
|
|
|
|
file.read((uint8_t *)&_prefs.sf, sizeof(_prefs.sf));
|
|
|
|
|
file.read((uint8_t *)&_prefs.cr, sizeof(_prefs.cr));
|
|
|
|
|
file.read((uint8_t *)&_prefs.client_repeat, sizeof(_prefs.client_repeat));
|
|
|
|
|
file.read((uint8_t *)&_prefs.manual_add_contacts, sizeof(_prefs.manual_add_contacts));
|
|
|
|
|
file.read((uint8_t *)&_prefs.bw, sizeof(_prefs.bw));
|
|
|
|
|
file.read((uint8_t *)&_prefs.tx_power_dbm, sizeof(_prefs.tx_power_dbm));
|
|
|
|
|
file.read((uint8_t *)&_prefs.telemetry_mode_base, sizeof(_prefs.telemetry_mode_base));
|
|
|
|
|
file.read((uint8_t *)&_prefs.telemetry_mode_loc, sizeof(_prefs.telemetry_mode_loc));
|
|
|
|
|
file.read((uint8_t *)&_prefs.telemetry_mode_env, sizeof(_prefs.telemetry_mode_env));
|
|
|
|
|
file.read((uint8_t *)&_prefs.rx_delay_base, sizeof(_prefs.rx_delay_base));
|
|
|
|
|
file.read((uint8_t *)&_prefs.advert_loc_policy, sizeof(_prefs.advert_loc_policy));
|
|
|
|
|
file.read((uint8_t *)&_prefs.multi_acks, sizeof(_prefs.multi_acks));
|
|
|
|
|
file.read((uint8_t *)&_prefs.path_hash_mode, sizeof(_prefs.path_hash_mode));
|
|
|
|
|
file.read(pad, 1);
|
|
|
|
|
file.read((uint8_t *)&_prefs.ble_pin, sizeof(_prefs.ble_pin));
|
|
|
|
|
file.read((uint8_t *)&_prefs.buzzer_quiet, sizeof(_prefs.buzzer_quiet));
|
|
|
|
|
file.read((uint8_t *)&_prefs.gps_enabled, sizeof(_prefs.gps_enabled));
|
|
|
|
|
file.read((uint8_t *)&_prefs.gps_interval, sizeof(_prefs.gps_interval));
|
|
|
|
|
file.read((uint8_t *)&_prefs.autoadd_config, sizeof(_prefs.autoadd_config));
|
|
|
|
|
file.read((uint8_t *)&_prefs.autoadd_max_hops, sizeof(_prefs.autoadd_max_hops));
|
|
|
|
|
file.read((uint8_t *)&_prefs.rx_boosted_gain, sizeof(_prefs.rx_boosted_gain));
|
|
|
|
|
file.read((uint8_t *)_prefs.default_scope_name, sizeof(_prefs.default_scope_name));
|
|
|
|
|
file.read((uint8_t *)_prefs.default_scope_key, sizeof(_prefs.default_scope_key));
|
|
|
|
|
file.read((uint8_t *)&_prefs.display_brightness, sizeof(_prefs.display_brightness));
|
|
|
|
|
file.read((uint8_t *)&_prefs.auto_off_secs, sizeof(_prefs.auto_off_secs));
|
|
|
|
|
file.read((uint8_t *)&_prefs.tz_offset_hours, sizeof(_prefs.tz_offset_hours));
|
|
|
|
|
file.read((uint8_t *)&_prefs.low_batt_mv, sizeof(_prefs.low_batt_mv));
|
|
|
|
|
file.read((uint8_t *)&_prefs.batt_display_mode, sizeof(_prefs.batt_display_mode));
|
|
|
|
|
|
|
|
|
|
// Extension fields — append-only, newest at the bottom.
|
|
|
|
|
// Each read is gated on file.available(); fields absent in older files stay at their
|
|
|
|
|
// zero-initialised or hardware-default values set above.
|
|
|
|
|
auto rd = [&](void* p, size_t n) {
|
|
|
|
|
if (file.available() >= (int)n) file.read((uint8_t*)p, n);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
rd(_prefs.custom_msgs, sizeof(_prefs.custom_msgs));
|
|
|
|
|
rd(&_prefs.ch_notif_override, sizeof(_prefs.ch_notif_override));
|
|
|
|
|
rd(&_prefs.ch_notif_muted, sizeof(_prefs.ch_notif_muted));
|
|
|
|
|
rd(&_prefs.dm_show_all, sizeof(_prefs.dm_show_all));
|
|
|
|
|
rd(&_prefs.room_fav_only, sizeof(_prefs.room_fav_only));
|
|
|
|
|
rd(&_prefs.buzzer_volume, sizeof(_prefs.buzzer_volume));
|
|
|
|
|
rd(&_prefs.ringtone_bpm_idx, sizeof(_prefs.ringtone_bpm_idx));
|
|
|
|
|
rd(&_prefs.ringtone_len, sizeof(_prefs.ringtone_len));
|
|
|
|
|
if (_prefs.ringtone_len > 32) _prefs.ringtone_len = 0;
|
|
|
|
|
rd(_prefs.ringtone_notes, sizeof(_prefs.ringtone_notes));
|
|
|
|
|
rd(&_prefs.home_pages_mask, sizeof(_prefs.home_pages_mask));
|
|
|
|
|
rd(&_prefs.bot_enabled, sizeof(_prefs.bot_enabled));
|
|
|
|
|
rd(&_prefs.bot_channel_enabled, sizeof(_prefs.bot_channel_enabled));
|
|
|
|
|
rd(&_prefs.bot_channel_idx, sizeof(_prefs.bot_channel_idx));
|
|
|
|
|
rd(_prefs.bot_trigger, sizeof(_prefs.bot_trigger));
|
|
|
|
|
rd(_prefs.bot_reply_dm, sizeof(_prefs.bot_reply_dm));
|
|
|
|
|
rd(_prefs.bot_reply_ch, sizeof(_prefs.bot_reply_ch));
|
|
|
|
|
rd(&_prefs.clock_hide_seconds, sizeof(_prefs.clock_hide_seconds));
|
|
|
|
|
rd(&_prefs.buzzer_auto, sizeof(_prefs.buzzer_auto));
|
|
|
|
|
rd(_prefs.dm_notif, sizeof(_prefs.dm_notif));
|
|
|
|
|
rd(_prefs.dashboard_fields, sizeof(_prefs.dashboard_fields));
|
|
|
|
|
rd(&_prefs.advert_auto_interval_sec, sizeof(_prefs.advert_auto_interval_sec));
|
|
|
|
|
rd(&_prefs.ringtone2_bpm_idx, sizeof(_prefs.ringtone2_bpm_idx));
|
|
|
|
|
rd(&_prefs.ringtone2_len, sizeof(_prefs.ringtone2_len));
|
|
|
|
|
if (_prefs.ringtone2_len > 32) _prefs.ringtone2_len = 0;
|
|
|
|
|
rd(_prefs.ringtone2_notes, sizeof(_prefs.ringtone2_notes));
|
|
|
|
|
rd(&_prefs.notif_melody_dm, sizeof(_prefs.notif_melody_dm));
|
2026-06-07 16:13:16 +02:00
|
|
|
if (_prefs.notif_melody_dm > 3) _prefs.notif_melody_dm = 0;
|
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
|
|
|
rd(&_prefs.notif_melody_ch, sizeof(_prefs.notif_melody_ch));
|
2026-06-07 16:13:16 +02:00
|
|
|
if (_prefs.notif_melody_ch > 3) _prefs.notif_melody_ch = 0;
|
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
|
|
|
rd(&_prefs.ch_notif_melody_set, sizeof(_prefs.ch_notif_melody_set));
|
|
|
|
|
rd(&_prefs.ch_notif_melody_2, sizeof(_prefs.ch_notif_melody_2));
|
|
|
|
|
rd(_prefs.dm_melody, sizeof(_prefs.dm_melody));
|
|
|
|
|
rd(&_prefs.auto_lock, sizeof(_prefs.auto_lock));
|
|
|
|
|
rd(&_prefs.clock_12h, sizeof(_prefs.clock_12h));
|
|
|
|
|
rd(&_prefs.use_lemon_font, sizeof(_prefs.use_lemon_font));
|
|
|
|
|
rd(&_prefs.display_rotation, sizeof(_prefs.display_rotation));
|
2026-07-02 12:01:40 +02:00
|
|
|
rd(_prefs.page_order, NodePrefs::PAGE_ORDER_LEN_V1); // tail slots read below (append-only)
|
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
|
|
|
rd(&_prefs.joystick_rotation, sizeof(_prefs.joystick_rotation));
|
2026-06-17 21:35:30 +02:00
|
|
|
#if !FEAT_JOYSTICK_ROTATION_SETTING
|
|
|
|
|
// No UI to change it on this build, so force the default — this also corrects
|
|
|
|
|
// a stale value migrated from another build (e.g. e-ink rotation=2). On builds
|
|
|
|
|
// that DO expose the setting the stored value is kept; the old code clobbered
|
|
|
|
|
// it unconditionally (FEAT_* was undefined here because Features.h wasn't
|
|
|
|
|
// included → `#if !FEAT_…` was always true), so the setting never persisted.
|
2026-05-27 23:47:30 +02:00
|
|
|
_prefs.joystick_rotation = 0;
|
|
|
|
|
#endif
|
2026-05-24 10:22:03 +02:00
|
|
|
rd(&_prefs.eink_full_refresh_every, sizeof(_prefs.eink_full_refresh_every));
|
2026-05-24 19:53:05 +02:00
|
|
|
rd(&_prefs.page_order_set, sizeof(_prefs.page_order_set));
|
|
|
|
|
// Migration: pre-magic firmware wrote page_order without a flag. If we see a plausible
|
|
|
|
|
// first entry from such a save, accept it once — savePrefs will then persist the magic.
|
|
|
|
|
if (_prefs.page_order_set != NodePrefs::PAGE_ORDER_MAGIC
|
2026-05-24 22:26:56 +02:00
|
|
|
&& _prefs.page_order[0] >= 1 && _prefs.page_order[0] <= NodePrefs::HPB_COUNT) {
|
2026-05-24 19:53:05 +02:00
|
|
|
_prefs.page_order_set = NodePrefs::PAGE_ORDER_MAGIC;
|
|
|
|
|
}
|
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
|
|
|
|
2026-05-24 23:00:27 +02:00
|
|
|
rd(_prefs.favourite_contacts, sizeof(_prefs.favourite_contacts));
|
2026-05-25 09:10:40 +02:00
|
|
|
rd(&_prefs.trail_interval_idx, sizeof(_prefs.trail_interval_idx));
|
|
|
|
|
rd(&_prefs.trail_min_delta_idx, sizeof(_prefs.trail_min_delta_idx));
|
2026-05-25 13:16:02 +02:00
|
|
|
rd(&_prefs.trail_units_idx, sizeof(_prefs.trail_units_idx));
|
2026-05-27 17:33:41 +02:00
|
|
|
rd(&_prefs.ch_fav_bitmask, sizeof(_prefs.ch_fav_bitmask));
|
|
|
|
|
rd(&_prefs.ch_fav_only, sizeof(_prefs.ch_fav_only));
|
2026-06-04 17:13:49 +02:00
|
|
|
rd(&_prefs.notif_melody_ad, sizeof(_prefs.notif_melody_ad));
|
2026-06-04 00:44:56 +02:00
|
|
|
rd(&_prefs.units_imperial, sizeof(_prefs.units_imperial));
|
|
|
|
|
rd(&_prefs.trail_show_pace, sizeof(_prefs.trail_show_pace));
|
2026-06-07 10:53:17 +02:00
|
|
|
rd(&_prefs.advert_sound_scope, sizeof(_prefs.advert_sound_scope));
|
2026-06-10 23:30:43 +02:00
|
|
|
rd(&_prefs.rx_powersave, sizeof(_prefs.rx_powersave));
|
|
|
|
|
rd(&_prefs.tx_apc, sizeof(_prefs.tx_apc));
|
2026-06-14 23:33:16 +02:00
|
|
|
rd(&_prefs.dm_resend_count, sizeof(_prefs.dm_resend_count));
|
feat(bot): hardening + auto-responder, query commands, quiet hours
Reply-bot overhaul on top of the trigger/reply auto-reply:
Robustness
- single botTriggerMatches() shared by DM/channel paths; named constants
(BOT_REPLY_COOLDOWN_MS / BOT_SCRATCH) replace magic numbers.
- per-contact DM throttle (8-entry ring) so a second sender isn't starved
while one contact is on cooldown.
- channel anti-loop: skip only when an incoming message equals our own reply
(was: any reply containing the trigger word — which silently killed channel
replies for the common "trigger word in reply" setup).
Features
- away / reply-to-all: a lone "*" trigger matches every message.
- independent DM vs channel triggers (bot_trigger / bot_trigger_ch); "*" works
on the channel too, bounded by cooldown + echo guard.
- query commands: a DM or monitored-channel message is scanned for "!word"
tokens — !ping/!batt/!loc/!time/!temp/!hops/!status/!help — merged into one
combined reply. DM = per-contact throttle, ignores quiet hours; channel =
broadcast, per-channel cooldown, respects quiet hours. !hops uses
getPathHashCount() (0 = direct).
- quiet hours (bot_quiet_start/end, local, wraps midnight) silence push replies.
- reply counter shown in the BotScreen header.
UI / storage
- BotScreen: 9 rows, scrolling list (no more cramming), field I/O via
fieldBuf()/fieldCap().
- prefs bot_commands_enabled / bot_quiet_start / bot_quiet_end / bot_trigger_ch
persisted; schema 0xC0DE000A → 0xC0DE000C with clamping + migration
(bot_trigger_ch seeded from bot_trigger on upgrade).
- docs: tools_screen bot section rewritten; FEATURES.md refreshed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 11:57:27 +02:00
|
|
|
rd(&_prefs.bot_commands_enabled, sizeof(_prefs.bot_commands_enabled));
|
|
|
|
|
rd(&_prefs.bot_quiet_start, sizeof(_prefs.bot_quiet_start));
|
|
|
|
|
rd(&_prefs.bot_quiet_end, sizeof(_prefs.bot_quiet_end));
|
|
|
|
|
rd(_prefs.bot_trigger_ch, sizeof(_prefs.bot_trigger_ch));
|
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
|
|
|
rd(_prefs.user_radio_presets, sizeof(_prefs.user_radio_presets));
|
2026-06-21 22:32:59 +02:00
|
|
|
// → 0xC0DE000E: repeater forwarding-filter knobs. On a pre-E file the bytes here are
|
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
|
|
|
// that file's own sentinel tail, so clamp every out-of-range value back to its
|
|
|
|
|
// "off" default (same stray-byte handling as the fields below).
|
|
|
|
|
rd(&_prefs.repeat_skip_adverts, sizeof(_prefs.repeat_skip_adverts));
|
|
|
|
|
rd(&_prefs.repeat_max_hops, sizeof(_prefs.repeat_max_hops));
|
|
|
|
|
rd(&_prefs.repeat_delay_boost, sizeof(_prefs.repeat_delay_boost));
|
|
|
|
|
rd(&_prefs.repeat_min_snr, sizeof(_prefs.repeat_min_snr));
|
|
|
|
|
rd(&_prefs.repeat_suppress_dup, sizeof(_prefs.repeat_suppress_dup));
|
|
|
|
|
if (_prefs.repeat_skip_adverts > 1) _prefs.repeat_skip_adverts = 0;
|
|
|
|
|
if (_prefs.repeat_max_hops > 64) _prefs.repeat_max_hops = 0;
|
|
|
|
|
if (_prefs.repeat_delay_boost > 8) _prefs.repeat_delay_boost = 0;
|
|
|
|
|
if (_prefs.repeat_min_snr != NodePrefs::REPEAT_SNR_DISABLED &&
|
|
|
|
|
(_prefs.repeat_min_snr < -20 || _prefs.repeat_min_snr > 10))
|
|
|
|
|
_prefs.repeat_min_snr = NodePrefs::REPEAT_SNR_DISABLED; // match the UI's -20..10 range
|
|
|
|
|
if (_prefs.repeat_suppress_dup > 1) _prefs.repeat_suppress_dup = 0;
|
|
|
|
|
rd(&_prefs.repeater_use_profile, sizeof(_prefs.repeater_use_profile));
|
|
|
|
|
rd(&_prefs.repeater_freq, sizeof(_prefs.repeater_freq));
|
|
|
|
|
rd(&_prefs.repeater_bw, sizeof(_prefs.repeater_bw));
|
|
|
|
|
rd(&_prefs.repeater_sf, sizeof(_prefs.repeater_sf));
|
|
|
|
|
rd(&_prefs.repeater_cr, sizeof(_prefs.repeater_cr));
|
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
|
|
|
// → 0xC0DE0011: track_shared_loc. Pre-0x11 files leave a stray sentinel byte
|
|
|
|
|
// here; clamp so upgraders fall back to "off".
|
|
|
|
|
rd(&_prefs.track_shared_loc, sizeof(_prefs.track_shared_loc));
|
|
|
|
|
if (_prefs.track_shared_loc > 1) _prefs.track_shared_loc = 0;
|
|
|
|
|
// → 0xC0DE0012: live location sharing. Pre-0x12 files leave stray bytes here;
|
|
|
|
|
// clamp each field back to its default so upgraders start with sharing off.
|
|
|
|
|
rd(&_prefs.loc_share_enabled, sizeof(_prefs.loc_share_enabled));
|
|
|
|
|
rd(&_prefs.loc_share_target_type, sizeof(_prefs.loc_share_target_type));
|
|
|
|
|
rd(&_prefs.loc_share_channel_idx, sizeof(_prefs.loc_share_channel_idx));
|
|
|
|
|
rd(_prefs.loc_share_dm_prefix, sizeof(_prefs.loc_share_dm_prefix));
|
|
|
|
|
rd(&_prefs.loc_share_move_idx, sizeof(_prefs.loc_share_move_idx));
|
|
|
|
|
rd(&_prefs.loc_share_interval_idx, sizeof(_prefs.loc_share_interval_idx));
|
|
|
|
|
rd(&_prefs.loc_share_heartbeat_idx, sizeof(_prefs.loc_share_heartbeat_idx));
|
|
|
|
|
if (_prefs.loc_share_enabled > 1) _prefs.loc_share_enabled = 0;
|
|
|
|
|
if (_prefs.loc_share_target_type > 1) _prefs.loc_share_target_type = 0;
|
|
|
|
|
if (_prefs.loc_share_channel_idx >= MAX_GROUP_CHANNELS) _prefs.loc_share_channel_idx = 0;
|
|
|
|
|
if (_prefs.loc_share_move_idx >= NodePrefs::LOC_SHARE_MOVE_COUNT) _prefs.loc_share_move_idx = 1;
|
|
|
|
|
if (_prefs.loc_share_interval_idx >= NodePrefs::LOC_SHARE_INTERVAL_COUNT) _prefs.loc_share_interval_idx = 1;
|
|
|
|
|
if (_prefs.loc_share_heartbeat_idx >= NodePrefs::LOC_SHARE_HEARTBEAT_COUNT) _prefs.loc_share_heartbeat_idx = 0;
|
|
|
|
|
// → 0xC0DE0013: locator + trail auto-pause. Pre-0x13 files leave stray bytes
|
|
|
|
|
// here; clamp each back to its default so upgraders start with both off.
|
|
|
|
|
rd(&_prefs.locator_enabled, sizeof(_prefs.locator_enabled));
|
|
|
|
|
rd(&_prefs.locator_has_target, sizeof(_prefs.locator_has_target));
|
|
|
|
|
rd(&_prefs.locator_radius_idx, sizeof(_prefs.locator_radius_idx));
|
|
|
|
|
rd(&_prefs.locator_mode, sizeof(_prefs.locator_mode));
|
|
|
|
|
rd(&_prefs.locator_lat_1e6, sizeof(_prefs.locator_lat_1e6));
|
|
|
|
|
rd(&_prefs.locator_lon_1e6, sizeof(_prefs.locator_lon_1e6));
|
|
|
|
|
rd(_prefs.locator_label, sizeof(_prefs.locator_label));
|
|
|
|
|
rd(&_prefs.trail_autopause_idx, sizeof(_prefs.trail_autopause_idx));
|
|
|
|
|
if (_prefs.locator_enabled > 1) _prefs.locator_enabled = 0;
|
|
|
|
|
if (_prefs.locator_has_target > 1) _prefs.locator_has_target = 0;
|
|
|
|
|
if (_prefs.locator_radius_idx >= NodePrefs::LOCATOR_RADIUS_COUNT) _prefs.locator_radius_idx = 1;
|
|
|
|
|
if (_prefs.locator_mode >= NodePrefs::LOCATOR_MODE_COUNT) _prefs.locator_mode = 0;
|
|
|
|
|
if (_prefs.trail_autopause_idx >= NodePrefs::TRAIL_AUTOPAUSE_COUNT) _prefs.trail_autopause_idx = 0;
|
|
|
|
|
_prefs.locator_label[sizeof(_prefs.locator_label) - 1] = '\0';
|
|
|
|
|
// → 0xC0DE0014: locator proximity beeper.
|
|
|
|
|
rd(&_prefs.locator_beeper, sizeof(_prefs.locator_beeper));
|
|
|
|
|
if (_prefs.locator_beeper > 1) _prefs.locator_beeper = 0;
|
|
|
|
|
// → 0xC0DE0015: locator can target a live contact (kind + pubkey prefix).
|
|
|
|
|
rd(&_prefs.locator_target_kind, sizeof(_prefs.locator_target_kind));
|
|
|
|
|
rd(_prefs.locator_key, sizeof(_prefs.locator_key));
|
|
|
|
|
if (_prefs.locator_target_kind > 1) _prefs.locator_target_kind = 0;
|
2026-06-29 19:19:43 +02:00
|
|
|
// → 0xC0DE0016: GPS-averaging duration for waypoint marking.
|
|
|
|
|
rd(&_prefs.gps_avg_idx, sizeof(_prefs.gps_avg_idx));
|
|
|
|
|
if (_prefs.gps_avg_idx >= NodePrefs::GPS_AVG_COUNT) _prefs.gps_avg_idx = 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
|
|
|
// → 0xC0DE0017: one-shot alarm clock (local time-of-day + armed flag).
|
|
|
|
|
rd(&_prefs.alarm_on, sizeof(_prefs.alarm_on));
|
|
|
|
|
rd(&_prefs.alarm_hour, sizeof(_prefs.alarm_hour));
|
|
|
|
|
rd(&_prefs.alarm_min, sizeof(_prefs.alarm_min));
|
|
|
|
|
if (_prefs.alarm_on > 1) _prefs.alarm_on = 0;
|
|
|
|
|
if (_prefs.alarm_hour > 23) _prefs.alarm_hour = 0;
|
|
|
|
|
if (_prefs.alarm_min > 59) _prefs.alarm_min = 0;
|
2026-07-02 18:02:48 +02:00
|
|
|
// → 0xC0DE001A: keyboard type (QWERTY/T9). Pre-0x1A files leave stray sentinel
|
|
|
|
|
// tail bytes here; clamp back to the QWERTY default (0).
|
2026-07-02 12:21:52 +02:00
|
|
|
rd(&_prefs.keyboard_type, sizeof(_prefs.keyboard_type));
|
|
|
|
|
if (_prefs.keyboard_type > 1) _prefs.keyboard_type = 0;
|
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
|
|
|
// Pre-0x10 files leave stray sentinel bytes here, same as a never-configured
|
|
|
|
|
// device. Either way there's no valid saved profile, so default to a profile
|
|
|
|
|
// in the same band as the companion's own network (_prefs.freq, already read
|
|
|
|
|
// above) rather than "Current" — a repeater silently following the companion
|
|
|
|
|
// onto whatever private network it later joins isn't the MeshCore community
|
|
|
|
|
// norm; that stays opt-in. Band-matched rather than a flat frequency so the
|
|
|
|
|
// default can't land outside what's legal where the companion is set up.
|
|
|
|
|
if (_prefs.repeater_use_profile > 1) _prefs.repeater_use_profile = 0;
|
2026-06-21 23:09:27 +02:00
|
|
|
float rpt_lo, rpt_hi; radio_driver.getFreqBounds(rpt_lo, rpt_hi);
|
|
|
|
|
if (!isValidRepeaterProfile(_prefs.repeater_freq, _prefs.repeater_bw, _prefs.repeater_sf, _prefs.repeater_cr, rpt_lo, rpt_hi)) {
|
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
|
|
|
seedDefaultRepeaterProfile(_prefs);
|
|
|
|
|
}
|
feat(bot): hardening + auto-responder, query commands, quiet hours
Reply-bot overhaul on top of the trigger/reply auto-reply:
Robustness
- single botTriggerMatches() shared by DM/channel paths; named constants
(BOT_REPLY_COOLDOWN_MS / BOT_SCRATCH) replace magic numbers.
- per-contact DM throttle (8-entry ring) so a second sender isn't starved
while one contact is on cooldown.
- channel anti-loop: skip only when an incoming message equals our own reply
(was: any reply containing the trigger word — which silently killed channel
replies for the common "trigger word in reply" setup).
Features
- away / reply-to-all: a lone "*" trigger matches every message.
- independent DM vs channel triggers (bot_trigger / bot_trigger_ch); "*" works
on the channel too, bounded by cooldown + echo guard.
- query commands: a DM or monitored-channel message is scanned for "!word"
tokens — !ping/!batt/!loc/!time/!temp/!hops/!status/!help — merged into one
combined reply. DM = per-contact throttle, ignores quiet hours; channel =
broadcast, per-channel cooldown, respects quiet hours. !hops uses
getPathHashCount() (0 = direct).
- quiet hours (bot_quiet_start/end, local, wraps midnight) silence push replies.
- reply counter shown in the BotScreen header.
UI / storage
- BotScreen: 9 rows, scrolling list (no more cramming), field I/O via
fieldBuf()/fieldCap().
- prefs bot_commands_enabled / bot_quiet_start / bot_quiet_end / bot_trigger_ch
persisted; schema 0xC0DE000A → 0xC0DE000C with clamping + migration
(bot_trigger_ch seeded from bot_trigger on upgrade).
- docs: tools_screen bot section rewritten; FEATURES.md refreshed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 11:57:27 +02:00
|
|
|
// → 0xC0DE000B: append bot_commands_enabled + quiet-hours. Older files leave
|
|
|
|
|
// stray bytes here; clamp so upgraders fall back to off / no quiet hours.
|
|
|
|
|
if (_prefs.bot_commands_enabled > 1) _prefs.bot_commands_enabled = 0;
|
|
|
|
|
if (_prefs.bot_quiet_start > 23) _prefs.bot_quiet_start = 0;
|
|
|
|
|
if (_prefs.bot_quiet_end > 23) _prefs.bot_quiet_end = 0;
|
2026-06-10 23:30:43 +02:00
|
|
|
// These fields were appended over successive schema bumps; an older file
|
|
|
|
|
// can leave stray bytes here, so clamp out-of-range values back to defaults.
|
2026-06-07 10:53:17 +02:00
|
|
|
// Values for notif_melody_ad: 0=built-in, 1=melody1, 2=melody2, 3=none.
|
2026-06-07 09:13:02 +02:00
|
|
|
if (_prefs.notif_melody_ad > 3) _prefs.notif_melody_ad = 0;
|
2026-06-16 08:54:11 +02:00
|
|
|
// A stale value >1 from an older multi-font build would read as "Lemon" (all
|
|
|
|
|
// sites test != 0) until the user toggles Font; clamp it to default here.
|
|
|
|
|
if (_prefs.use_lemon_font > 1) _prefs.use_lemon_font = 0;
|
2026-06-04 00:44:56 +02:00
|
|
|
if (_prefs.units_imperial > 1) _prefs.units_imperial = 0;
|
|
|
|
|
if (_prefs.trail_show_pace > 1) _prefs.trail_show_pace = 0;
|
2026-06-07 10:53:17 +02:00
|
|
|
if (_prefs.advert_sound_scope > 1) _prefs.advert_sound_scope = ADVERT_SOUND_SCOPE_ALL;
|
2026-06-10 23:30:43 +02:00
|
|
|
if (_prefs.rx_powersave > 1) _prefs.rx_powersave = 0;
|
|
|
|
|
if (_prefs.tx_apc > 1) _prefs.tx_apc = 0;
|
2026-06-14 23:33:16 +02:00
|
|
|
// An old (0xC0DE0009) file leaves the low byte of its sentinel here (0x09),
|
|
|
|
|
// which is out of range — fall back to the default of 2 resends.
|
|
|
|
|
if (_prefs.dm_resend_count > 5) _prefs.dm_resend_count = 2;
|
2026-05-24 23:00:27 +02:00
|
|
|
|
2026-07-02 12:01:40 +02:00
|
|
|
// → 0xC0DE0019: page_order grew 11 → 13 so Shutdown and Map become reorderable.
|
|
|
|
|
// The extra slots are appended here at the tail (not inline) so a pre-0x19 save,
|
|
|
|
|
// whose order ended right after the old 11 bytes, still loads without shifting
|
|
|
|
|
// every field after it. On such files these bytes are the old sentinel tail or
|
|
|
|
|
// EOF, so clamp anything out of range back to 0 (empty); ensurePageOrderInit
|
|
|
|
|
// then appends the missing pages into the freed slots.
|
|
|
|
|
for (uint8_t i = NodePrefs::PAGE_ORDER_LEN_V1; i < NodePrefs::PAGE_ORDER_LEN; i++) {
|
|
|
|
|
rd(&_prefs.page_order[i], sizeof(_prefs.page_order[i]));
|
|
|
|
|
if (_prefs.page_order[i] > NodePrefs::HPB_COUNT) _prefs.page_order[i] = 0;
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-05 22:21:42 +02:00
|
|
|
// → 0xC0DE001B: append trail_autosave_lowbatt at the tail. A pre-0x1B file has
|
|
|
|
|
// the old sentinel bytes / EOF here; rd() zero-inits when absent and the clamp
|
|
|
|
|
// below turns any stray value into 0 (off), so upgraders default to off.
|
|
|
|
|
rd(&_prefs.trail_autosave_lowbatt, sizeof(_prefs.trail_autosave_lowbatt));
|
|
|
|
|
if (_prefs.trail_autosave_lowbatt > 1) _prefs.trail_autosave_lowbatt = 0;
|
|
|
|
|
|
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
|
|
|
// → 0xC0DE001C: append alarm_repeat_mask at the tail. A pre-0x1C file has the
|
|
|
|
|
// old sentinel bytes / EOF here; any value that isn't one of the four presets
|
|
|
|
|
// clamps to 0 (no repeat / one-shot), matching the original alarm behaviour
|
|
|
|
|
// upgraders already had.
|
|
|
|
|
rd(&_prefs.alarm_repeat_mask, sizeof(_prefs.alarm_repeat_mask));
|
|
|
|
|
if (NodePrefs::alarmRepeatIdxForMask(_prefs.alarm_repeat_mask) == 0) _prefs.alarm_repeat_mask = 0;
|
|
|
|
|
|
|
|
|
|
// → 0xC0DE001D: append keyboard_alt_alphabet at the tail. A pre-0x1D file has
|
|
|
|
|
// the old sentinel bytes / EOF here; clamp anything out of range to 0 (Latin
|
|
|
|
|
// only), matching the keyboard's original (Latin-only) behaviour.
|
|
|
|
|
rd(&_prefs.keyboard_alt_alphabet, sizeof(_prefs.keyboard_alt_alphabet));
|
|
|
|
|
if (_prefs.keyboard_alt_alphabet >= NodePrefs::KB_ALPHABET_COUNT) _prefs.keyboard_alt_alphabet = 0;
|
|
|
|
|
|
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
|
|
|
// → 0xC0DE001E: append bot_dm_scope + the room-server bot fields at the
|
|
|
|
|
// tail. A pre-0x1E file has the old sentinel bytes / EOF here; rd() zero-
|
|
|
|
|
// inits when absent, so upgraders default to bot_dm_scope=0 (all DMs,
|
|
|
|
|
// matching the original bot_enabled behaviour) and the room bot fully
|
|
|
|
|
// disabled (bot_room_enabled clamps to 0; an empty trigger never matches
|
|
|
|
|
// even if somehow set).
|
|
|
|
|
rd(&_prefs.bot_dm_scope, sizeof(_prefs.bot_dm_scope));
|
|
|
|
|
if (_prefs.bot_dm_scope > 1) _prefs.bot_dm_scope = 0;
|
|
|
|
|
|
|
|
|
|
rd(&_prefs.bot_room_enabled, sizeof(_prefs.bot_room_enabled));
|
|
|
|
|
if (_prefs.bot_room_enabled > 1) _prefs.bot_room_enabled = 0;
|
|
|
|
|
rd(_prefs.bot_room_prefix, sizeof(_prefs.bot_room_prefix));
|
|
|
|
|
rd(_prefs.bot_trigger_room, sizeof(_prefs.bot_trigger_room));
|
|
|
|
|
rd(_prefs.bot_reply_room, sizeof(_prefs.bot_reply_room));
|
|
|
|
|
|
|
|
|
|
// → 0xC0DE001F: split the shared bot_commands_enabled into a per-target
|
|
|
|
|
// toggle for channel/room too (DM keeps the original field). A pre-0x1F
|
|
|
|
|
// file has neither new byte; both clamp to 0 here and are seeded from the
|
|
|
|
|
// old shared value in the sentinel-mismatch migration below, so upgraders
|
|
|
|
|
// don't silently lose channel/room commands they already had answering.
|
|
|
|
|
rd(&_prefs.bot_commands_ch, sizeof(_prefs.bot_commands_ch));
|
|
|
|
|
if (_prefs.bot_commands_ch > 1) _prefs.bot_commands_ch = 0;
|
|
|
|
|
rd(&_prefs.bot_commands_room, sizeof(_prefs.bot_commands_room));
|
|
|
|
|
if (_prefs.bot_commands_room > 1) _prefs.bot_commands_room = 0;
|
|
|
|
|
|
2026-07-17 20:50:41 +02:00
|
|
|
// → 0xC0DE0020: append keyboard_main_alphabet at the tail. A pre-0x20 file
|
|
|
|
|
// has the old sentinel bytes / EOF here; clamp anything out of range to 0
|
|
|
|
|
// (Latin), matching the keyboard's original always-Latin-main behaviour.
|
|
|
|
|
rd(&_prefs.keyboard_main_alphabet, sizeof(_prefs.keyboard_main_alphabet));
|
|
|
|
|
if (_prefs.keyboard_main_alphabet >= NodePrefs::KB_ALPHABET_COUNT) _prefs.keyboard_main_alphabet = 0;
|
|
|
|
|
|
2026-07-21 20:30:03 +02:00
|
|
|
// → 0xC0DE0021: append the per-target bot-actions toggles at the tail. A
|
|
|
|
|
// pre-0x21 file has neither byte here; clamp to 0 (off) -- these gate
|
|
|
|
|
// state-changing bot commands (!buzz/!gps/!advert), so an upgrader must
|
|
|
|
|
// opt in deliberately rather than get them silently enabled.
|
|
|
|
|
rd(&_prefs.bot_actions_dm, sizeof(_prefs.bot_actions_dm));
|
|
|
|
|
if (_prefs.bot_actions_dm > 1) _prefs.bot_actions_dm = 0;
|
|
|
|
|
rd(&_prefs.bot_actions_ch, sizeof(_prefs.bot_actions_ch));
|
|
|
|
|
if (_prefs.bot_actions_ch > 1) _prefs.bot_actions_ch = 0;
|
|
|
|
|
rd(&_prefs.bot_actions_room, sizeof(_prefs.bot_actions_room));
|
|
|
|
|
if (_prefs.bot_actions_room > 1) _prefs.bot_actions_room = 0;
|
|
|
|
|
|
|
|
|
|
// → 0xC0DE0022: user-assignable GPIO pin modes (0=Off 1=In 2=Out-low
|
|
|
|
|
// 3=Out-high 4=Analog). A pre-0x22 file has none of these bytes; clamp to
|
|
|
|
|
// 0 (off). gpio1/gpio2 (AIN0/AIN5) allow mode 4; gpio3/gpio4 have no ADC
|
|
|
|
|
// channel, so their clamp stops at 3 -- a stray 4 there (corrupt file,
|
|
|
|
|
// schema mismatch) falls back to Off rather than doing something undefined.
|
|
|
|
|
rd(&_prefs.gpio1_mode, sizeof(_prefs.gpio1_mode));
|
|
|
|
|
if (_prefs.gpio1_mode > 4) _prefs.gpio1_mode = 0;
|
|
|
|
|
rd(&_prefs.gpio2_mode, sizeof(_prefs.gpio2_mode));
|
|
|
|
|
if (_prefs.gpio2_mode > 4) _prefs.gpio2_mode = 0;
|
|
|
|
|
rd(&_prefs.gpio3_mode, sizeof(_prefs.gpio3_mode));
|
|
|
|
|
if (_prefs.gpio3_mode > 3) _prefs.gpio3_mode = 0;
|
|
|
|
|
rd(&_prefs.gpio4_mode, sizeof(_prefs.gpio4_mode));
|
|
|
|
|
if (_prefs.gpio4_mode > 3) _prefs.gpio4_mode = 0;
|
|
|
|
|
|
2026-07-24 15:51:51 +02:00
|
|
|
// → 0xC0DE0023: append the external-keyboard compact-display toggle at the
|
|
|
|
|
// tail. A pre-0x23 file has no byte here; clamp to 0 (full grid, unchanged
|
|
|
|
|
// behaviour for upgraders).
|
|
|
|
|
rd(&_prefs.keyboard_cardkb_compact, sizeof(_prefs.keyboard_cardkb_compact));
|
|
|
|
|
if (_prefs.keyboard_cardkb_compact > 1) _prefs.keyboard_cardkb_compact = 0;
|
|
|
|
|
|
2026-05-24 20:18:50 +02:00
|
|
|
// Schema sentinel: bumped on layout changes. Mismatch means an older file
|
|
|
|
|
// (or a different schema); rd() already zero-inits any fields not present,
|
|
|
|
|
// so we just log it — next savePrefs writes the current sentinel.
|
|
|
|
|
uint32_t sentinel = 0;
|
|
|
|
|
rd(&sentinel, sizeof(sentinel));
|
|
|
|
|
if (sentinel != NodePrefs::SCHEMA_SENTINEL) {
|
|
|
|
|
MESH_DEBUG_PRINTLN("prefs schema sentinel mismatch: got 0x%08X, expected 0x%08X — re-saving on next change",
|
|
|
|
|
(unsigned)sentinel, (unsigned)NodePrefs::SCHEMA_SENTINEL);
|
2026-07-02 10:34:38 +02:00
|
|
|
// 0xC0DE0001 → 0xC0DE0002: FAVOURITES home page added. Only pre-0x0002 saves
|
|
|
|
|
// lack the bit; turn it on once so those upgraders see the new page by
|
|
|
|
|
// default. Must be gated to that transition — running it on every sentinel
|
|
|
|
|
// mismatch (as it did) re-enabled Favourites on each firmware update,
|
|
|
|
|
// clobbering a user who had deliberately hidden it.
|
|
|
|
|
if (sentinel < 0xC0DE0002 && _prefs.home_pages_mask != 0) {
|
2026-05-24 23:17:48 +02:00
|
|
|
_prefs.home_pages_mask |= NodePrefs::HP_FAVOURITES;
|
|
|
|
|
}
|
2026-07-02 10:34:38 +02:00
|
|
|
// 0xC0DE0017 → 0xC0DE0018: MAP home page moved into home_pages_mask (it was
|
|
|
|
|
// always-on before, with no visibility toggle). Turn its bit on once for
|
|
|
|
|
// pre-0x0018 saves so upgraders keep seeing the page; gated to this
|
|
|
|
|
// transition so a user who later hides it isn't overridden on the next update.
|
|
|
|
|
if (sentinel < 0xC0DE0018 && _prefs.home_pages_mask != 0) {
|
|
|
|
|
_prefs.home_pages_mask |= NodePrefs::HP_MAP;
|
|
|
|
|
}
|
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
|
|
|
// 0xC0DE001E → 0xC0DE001F: bot_commands_enabled split per target (see the
|
|
|
|
|
// rd() above). Seed the two new fields from the old shared one so a
|
|
|
|
|
// pre-0x1F upgrader's channel/room commands keep answering exactly as
|
|
|
|
|
// before; gated to this transition so a user who later splits them apart
|
|
|
|
|
// isn't overridden on a later update.
|
|
|
|
|
if (sentinel < 0xC0DE001F) {
|
|
|
|
|
_prefs.bot_commands_ch = _prefs.bot_commands_room = _prefs.bot_commands_enabled;
|
|
|
|
|
}
|
2026-05-26 15:49:50 +02:00
|
|
|
// 0xC0DE0003 → 0xC0DE0004: trail_units_idx added after trail_min_delta_idx.
|
2026-05-30 23:57:12 +02:00
|
|
|
// On a 0xC0DE0003 file the sentinel bytes sit where trail_units_idx is now,
|
|
|
|
|
// so rd() picks up 0x03 (low byte of the old sentinel) — reset just that
|
2026-06-04 08:46:15 +02:00
|
|
|
// case to default 0. Newer mismatches (e.g. 0xC0DE0005 → 0xC0DE0006) had
|
2026-05-30 23:57:12 +02:00
|
|
|
// the field saved correctly and must not be clobbered.
|
|
|
|
|
if (sentinel == 0xC0DE0003) {
|
|
|
|
|
_prefs.trail_units_idx = 0;
|
|
|
|
|
}
|
2026-06-07 10:53:17 +02:00
|
|
|
// → 0xC0DE0008: append advert_sound_scope after the existing 0xC0DE0007
|
|
|
|
|
// tail (notif_melody_ad + units_imperial + trail_show_pace). Older files
|
|
|
|
|
// leave stray/old bytes in these fields; they're clamped above, so
|
|
|
|
|
// upgraders fall back to built-in advert sound + metric + speed + All.
|
2026-06-10 23:30:43 +02:00
|
|
|
// → 0xC0DE0009: append tx_apc after rx_powersave. Clamped above, so
|
|
|
|
|
// upgraders fall back to APC off (fixed tx power).
|
feat(bot): hardening + auto-responder, query commands, quiet hours
Reply-bot overhaul on top of the trigger/reply auto-reply:
Robustness
- single botTriggerMatches() shared by DM/channel paths; named constants
(BOT_REPLY_COOLDOWN_MS / BOT_SCRATCH) replace magic numbers.
- per-contact DM throttle (8-entry ring) so a second sender isn't starved
while one contact is on cooldown.
- channel anti-loop: skip only when an incoming message equals our own reply
(was: any reply containing the trigger word — which silently killed channel
replies for the common "trigger word in reply" setup).
Features
- away / reply-to-all: a lone "*" trigger matches every message.
- independent DM vs channel triggers (bot_trigger / bot_trigger_ch); "*" works
on the channel too, bounded by cooldown + echo guard.
- query commands: a DM or monitored-channel message is scanned for "!word"
tokens — !ping/!batt/!loc/!time/!temp/!hops/!status/!help — merged into one
combined reply. DM = per-contact throttle, ignores quiet hours; channel =
broadcast, per-channel cooldown, respects quiet hours. !hops uses
getPathHashCount() (0 = direct).
- quiet hours (bot_quiet_start/end, local, wraps midnight) silence push replies.
- reply counter shown in the BotScreen header.
UI / storage
- BotScreen: 9 rows, scrolling list (no more cramming), field I/O via
fieldBuf()/fieldCap().
- prefs bot_commands_enabled / bot_quiet_start / bot_quiet_end / bot_trigger_ch
persisted; schema 0xC0DE000A → 0xC0DE000C with clamping + migration
(bot_trigger_ch seeded from bot_trigger on upgrade).
- docs: tools_screen bot section rewritten; FEATURES.md refreshed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 11:57:27 +02:00
|
|
|
// → 0xC0DE000C: split out a per-channel trigger (was shared with the DM
|
|
|
|
|
// trigger). Pre-0x0C files have no bot_trigger_ch; seed it from bot_trigger
|
|
|
|
|
// so an existing channel bot keeps reacting to the same word after upgrade.
|
|
|
|
|
if (_prefs.bot_trigger_ch[0] == '\0')
|
|
|
|
|
strncpy(_prefs.bot_trigger_ch, _prefs.bot_trigger, sizeof(_prefs.bot_trigger_ch) - 1);
|
feat(repeater): politeness controls, dedicated radio profile, and diagnostics
Settings > Radio gains a repeater toggle, 16 community-suggested radio
presets plus manual Freq/SF/BW/CR tuning (digit-by-digit Freq editor),
4 persisted user preset slots, and the packet pool bumped 16->32 so
queued retransmits stop starving incoming-packet allocation while
relaying.
Four opt-in politeness knobs (skip-advert, max-hops, yield, min-SNR),
all flood-only and off by default, plus overhear suppression that
cancels a queued retransmit if a peer relays the same packet first.
Adaptive Power Control is suppressed while relaying (pins TX power to
the ceiling) and duty-cycle RX is forced off, since a repeater needs
to hear and relay at consistent power.
A dedicated Tools > Repeater screen consolidates the toggle, the
politeness knobs, and live forwarding stats, plus an optional "Custom"
radio profile — a dedicated frequency/SF/BW/CR for relaying, separate
from the companion's own network, band-matched to the companion
frequency by default and used everywhere a radio change can happen
(boot, on-device toggle, app-driven CMD_SET_RADIO_PARAMS, Settings
radio edits) so the device never silently falls back to the wrong
params mid-relay.
Diagnostics gains the actually-forwarded packet count, real heap-free
via mallinfo(), a reset-counters popup, and hardened loop-detect
bounds. Also: a frequency-floor fix and a float-equality preset-match
fix in the repeater profile logic, deduped BW-table/valCol/profile-
seeding helpers shared between Settings and the Repeater screen, and
a build.sh fix tolerating control characters in `pio project config`'s
JSON dump.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-20 09:07:00 +02:00
|
|
|
// → 0xC0DE000D: append user_radio_presets. No clamping needed — rd() already
|
|
|
|
|
// zero-inits it on a pre-0x0D file, and name[0]=='\0' is exactly the "empty
|
|
|
|
|
// slot" sentinel the UI already expects.
|
2026-05-24 20:18:50 +02:00
|
|
|
}
|
|
|
|
|
|
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
|
|
|
file.close();
|
2025-06-06 15:30:35 +10:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void DataStore::savePrefs(const NodePrefs& _prefs, double node_lat, double node_lon) {
|
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
|
|
|
// Atomic temp-then-rename (see commitTempFile) so an interrupted save can't
|
|
|
|
|
// wipe settings; loadPrefs() still validates the tail sentinel on read.
|
|
|
|
|
File file = ::openWrite(_fs, "/new_prefs.tmp");
|
2025-06-06 15:30:35 +10:00
|
|
|
if (file) {
|
|
|
|
|
uint8_t pad[8];
|
|
|
|
|
memset(pad, 0, sizeof(pad));
|
|
|
|
|
|
2026-05-12 10:42:19 +02:00
|
|
|
file.write((uint8_t *)&_prefs.airtime_factor, sizeof(float));
|
|
|
|
|
file.write((uint8_t *)_prefs.node_name, sizeof(_prefs.node_name));
|
|
|
|
|
file.write(pad, 4);
|
|
|
|
|
file.write((uint8_t *)&node_lat, sizeof(node_lat));
|
|
|
|
|
file.write((uint8_t *)&node_lon, sizeof(node_lon));
|
|
|
|
|
file.write((uint8_t *)&_prefs.freq, sizeof(_prefs.freq));
|
|
|
|
|
file.write((uint8_t *)&_prefs.sf, sizeof(_prefs.sf));
|
|
|
|
|
file.write((uint8_t *)&_prefs.cr, sizeof(_prefs.cr));
|
|
|
|
|
file.write((uint8_t *)&_prefs.client_repeat, sizeof(_prefs.client_repeat));
|
|
|
|
|
file.write((uint8_t *)&_prefs.manual_add_contacts, sizeof(_prefs.manual_add_contacts));
|
|
|
|
|
file.write((uint8_t *)&_prefs.bw, sizeof(_prefs.bw));
|
|
|
|
|
file.write((uint8_t *)&_prefs.tx_power_dbm, sizeof(_prefs.tx_power_dbm));
|
|
|
|
|
file.write((uint8_t *)&_prefs.telemetry_mode_base, sizeof(_prefs.telemetry_mode_base));
|
|
|
|
|
file.write((uint8_t *)&_prefs.telemetry_mode_loc, sizeof(_prefs.telemetry_mode_loc));
|
|
|
|
|
file.write((uint8_t *)&_prefs.telemetry_mode_env, sizeof(_prefs.telemetry_mode_env));
|
|
|
|
|
file.write((uint8_t *)&_prefs.rx_delay_base, sizeof(_prefs.rx_delay_base));
|
|
|
|
|
file.write((uint8_t *)&_prefs.advert_loc_policy, sizeof(_prefs.advert_loc_policy));
|
|
|
|
|
file.write((uint8_t *)&_prefs.multi_acks, sizeof(_prefs.multi_acks));
|
|
|
|
|
file.write((uint8_t *)&_prefs.path_hash_mode, sizeof(_prefs.path_hash_mode));
|
|
|
|
|
file.write(pad, 1);
|
|
|
|
|
file.write((uint8_t *)&_prefs.ble_pin, sizeof(_prefs.ble_pin));
|
|
|
|
|
file.write((uint8_t *)&_prefs.buzzer_quiet, sizeof(_prefs.buzzer_quiet));
|
|
|
|
|
file.write((uint8_t *)&_prefs.gps_enabled, sizeof(_prefs.gps_enabled));
|
|
|
|
|
file.write((uint8_t *)&_prefs.gps_interval, sizeof(_prefs.gps_interval));
|
|
|
|
|
file.write((uint8_t *)&_prefs.autoadd_config, sizeof(_prefs.autoadd_config));
|
|
|
|
|
file.write((uint8_t *)&_prefs.autoadd_max_hops, sizeof(_prefs.autoadd_max_hops));
|
|
|
|
|
file.write((uint8_t *)&_prefs.rx_boosted_gain, sizeof(_prefs.rx_boosted_gain));
|
|
|
|
|
file.write((uint8_t *)_prefs.default_scope_name, sizeof(_prefs.default_scope_name));
|
|
|
|
|
file.write((uint8_t *)_prefs.default_scope_key, sizeof(_prefs.default_scope_key));
|
|
|
|
|
file.write((uint8_t *)&_prefs.display_brightness, sizeof(_prefs.display_brightness));
|
|
|
|
|
file.write((uint8_t *)&_prefs.auto_off_secs, sizeof(_prefs.auto_off_secs));
|
|
|
|
|
file.write((uint8_t *)&_prefs.tz_offset_hours, sizeof(_prefs.tz_offset_hours));
|
|
|
|
|
file.write((uint8_t *)&_prefs.low_batt_mv, sizeof(_prefs.low_batt_mv));
|
|
|
|
|
file.write((uint8_t *)&_prefs.batt_display_mode, sizeof(_prefs.batt_display_mode));
|
|
|
|
|
file.write((uint8_t *)_prefs.custom_msgs, sizeof(_prefs.custom_msgs));
|
|
|
|
|
file.write((uint8_t *)&_prefs.ch_notif_override, sizeof(_prefs.ch_notif_override));
|
|
|
|
|
file.write((uint8_t *)&_prefs.ch_notif_muted, sizeof(_prefs.ch_notif_muted));
|
|
|
|
|
file.write((uint8_t *)&_prefs.dm_show_all, sizeof(_prefs.dm_show_all));
|
|
|
|
|
file.write((uint8_t *)&_prefs.room_fav_only, sizeof(_prefs.room_fav_only));
|
|
|
|
|
file.write((uint8_t *)&_prefs.buzzer_volume, sizeof(_prefs.buzzer_volume));
|
|
|
|
|
file.write((uint8_t *)&_prefs.ringtone_bpm_idx, sizeof(_prefs.ringtone_bpm_idx));
|
|
|
|
|
file.write((uint8_t *)&_prefs.ringtone_len, sizeof(_prefs.ringtone_len));
|
|
|
|
|
file.write((uint8_t *)_prefs.ringtone_notes, sizeof(_prefs.ringtone_notes));
|
|
|
|
|
file.write((uint8_t *)&_prefs.home_pages_mask, sizeof(_prefs.home_pages_mask));
|
2026-05-12 11:47:33 +02:00
|
|
|
file.write((uint8_t *)&_prefs.bot_enabled, sizeof(_prefs.bot_enabled));
|
2026-05-12 20:12:54 +02:00
|
|
|
file.write((uint8_t *)&_prefs.bot_channel_enabled, sizeof(_prefs.bot_channel_enabled));
|
2026-05-12 11:47:33 +02:00
|
|
|
file.write((uint8_t *)&_prefs.bot_channel_idx, sizeof(_prefs.bot_channel_idx));
|
|
|
|
|
file.write((uint8_t *)_prefs.bot_trigger, sizeof(_prefs.bot_trigger));
|
2026-05-12 20:12:54 +02:00
|
|
|
file.write((uint8_t *)_prefs.bot_reply_dm, sizeof(_prefs.bot_reply_dm));
|
|
|
|
|
file.write((uint8_t *)_prefs.bot_reply_ch, sizeof(_prefs.bot_reply_ch));
|
|
|
|
|
file.write((uint8_t *)&_prefs.clock_hide_seconds, sizeof(_prefs.clock_hide_seconds));
|
|
|
|
|
file.write((uint8_t *)&_prefs.buzzer_auto, sizeof(_prefs.buzzer_auto));
|
2026-05-13 10:48:42 +02:00
|
|
|
file.write((uint8_t *)_prefs.dm_notif, sizeof(_prefs.dm_notif));
|
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
|
|
|
file.write((uint8_t *)_prefs.dashboard_fields, sizeof(_prefs.dashboard_fields));
|
2026-05-14 16:08:33 +02:00
|
|
|
file.write((uint8_t *)&_prefs.advert_auto_interval_sec, sizeof(_prefs.advert_auto_interval_sec));
|
2026-05-15 11:43:38 +02:00
|
|
|
file.write((uint8_t *)&_prefs.ringtone2_bpm_idx, sizeof(_prefs.ringtone2_bpm_idx));
|
|
|
|
|
file.write((uint8_t *)&_prefs.ringtone2_len, sizeof(_prefs.ringtone2_len));
|
|
|
|
|
file.write((uint8_t *)_prefs.ringtone2_notes, sizeof(_prefs.ringtone2_notes));
|
|
|
|
|
file.write((uint8_t *)&_prefs.notif_melody_dm, sizeof(_prefs.notif_melody_dm));
|
|
|
|
|
file.write((uint8_t *)&_prefs.notif_melody_ch, sizeof(_prefs.notif_melody_ch));
|
|
|
|
|
file.write((uint8_t *)&_prefs.ch_notif_melody_set, sizeof(_prefs.ch_notif_melody_set));
|
|
|
|
|
file.write((uint8_t *)&_prefs.ch_notif_melody_2, sizeof(_prefs.ch_notif_melody_2));
|
|
|
|
|
file.write((uint8_t *)_prefs.dm_melody, sizeof(_prefs.dm_melody));
|
2026-05-18 09:39:10 +02:00
|
|
|
file.write((uint8_t *)&_prefs.auto_lock, sizeof(_prefs.auto_lock));
|
2026-05-21 21:04:43 +02:00
|
|
|
file.write((uint8_t *)&_prefs.clock_12h, sizeof(_prefs.clock_12h));
|
2026-05-20 09:49:04 +02:00
|
|
|
file.write((uint8_t *)&_prefs.use_lemon_font, sizeof(_prefs.use_lemon_font));
|
2026-05-20 21:53:40 +02:00
|
|
|
file.write((uint8_t *)&_prefs.display_rotation, sizeof(_prefs.display_rotation));
|
2026-07-02 12:01:40 +02:00
|
|
|
file.write((uint8_t *)_prefs.page_order, NodePrefs::PAGE_ORDER_LEN_V1); // head; tail slots written below
|
2026-05-23 15:20:40 +02:00
|
|
|
file.write((uint8_t *)&_prefs.joystick_rotation, sizeof(_prefs.joystick_rotation));
|
2026-05-24 10:22:03 +02:00
|
|
|
file.write((uint8_t *)&_prefs.eink_full_refresh_every, sizeof(_prefs.eink_full_refresh_every));
|
2026-05-24 19:53:05 +02:00
|
|
|
file.write((uint8_t *)&_prefs.page_order_set, sizeof(_prefs.page_order_set));
|
2026-05-24 23:00:27 +02:00
|
|
|
file.write((uint8_t *)_prefs.favourite_contacts, sizeof(_prefs.favourite_contacts));
|
2026-05-25 09:10:40 +02:00
|
|
|
file.write((uint8_t *)&_prefs.trail_interval_idx, sizeof(_prefs.trail_interval_idx));
|
|
|
|
|
file.write((uint8_t *)&_prefs.trail_min_delta_idx, sizeof(_prefs.trail_min_delta_idx));
|
2026-05-25 13:16:02 +02:00
|
|
|
file.write((uint8_t *)&_prefs.trail_units_idx, sizeof(_prefs.trail_units_idx));
|
2026-05-27 17:33:41 +02:00
|
|
|
file.write((uint8_t *)&_prefs.ch_fav_bitmask, sizeof(_prefs.ch_fav_bitmask));
|
|
|
|
|
file.write((uint8_t *)&_prefs.ch_fav_only, sizeof(_prefs.ch_fav_only));
|
2026-06-04 08:46:15 +02:00
|
|
|
file.write((uint8_t *)&_prefs.notif_melody_ad, sizeof(_prefs.notif_melody_ad));
|
2026-06-04 00:44:56 +02:00
|
|
|
file.write((uint8_t *)&_prefs.units_imperial, sizeof(_prefs.units_imperial));
|
|
|
|
|
file.write((uint8_t *)&_prefs.trail_show_pace, sizeof(_prefs.trail_show_pace));
|
2026-06-07 10:53:17 +02:00
|
|
|
file.write((uint8_t *)&_prefs.advert_sound_scope, sizeof(_prefs.advert_sound_scope));
|
2026-06-10 23:30:43 +02:00
|
|
|
file.write((uint8_t *)&_prefs.rx_powersave, sizeof(_prefs.rx_powersave));
|
|
|
|
|
file.write((uint8_t *)&_prefs.tx_apc, sizeof(_prefs.tx_apc));
|
2026-06-14 23:33:16 +02:00
|
|
|
file.write((uint8_t *)&_prefs.dm_resend_count, sizeof(_prefs.dm_resend_count));
|
feat(bot): hardening + auto-responder, query commands, quiet hours
Reply-bot overhaul on top of the trigger/reply auto-reply:
Robustness
- single botTriggerMatches() shared by DM/channel paths; named constants
(BOT_REPLY_COOLDOWN_MS / BOT_SCRATCH) replace magic numbers.
- per-contact DM throttle (8-entry ring) so a second sender isn't starved
while one contact is on cooldown.
- channel anti-loop: skip only when an incoming message equals our own reply
(was: any reply containing the trigger word — which silently killed channel
replies for the common "trigger word in reply" setup).
Features
- away / reply-to-all: a lone "*" trigger matches every message.
- independent DM vs channel triggers (bot_trigger / bot_trigger_ch); "*" works
on the channel too, bounded by cooldown + echo guard.
- query commands: a DM or monitored-channel message is scanned for "!word"
tokens — !ping/!batt/!loc/!time/!temp/!hops/!status/!help — merged into one
combined reply. DM = per-contact throttle, ignores quiet hours; channel =
broadcast, per-channel cooldown, respects quiet hours. !hops uses
getPathHashCount() (0 = direct).
- quiet hours (bot_quiet_start/end, local, wraps midnight) silence push replies.
- reply counter shown in the BotScreen header.
UI / storage
- BotScreen: 9 rows, scrolling list (no more cramming), field I/O via
fieldBuf()/fieldCap().
- prefs bot_commands_enabled / bot_quiet_start / bot_quiet_end / bot_trigger_ch
persisted; schema 0xC0DE000A → 0xC0DE000C with clamping + migration
(bot_trigger_ch seeded from bot_trigger on upgrade).
- docs: tools_screen bot section rewritten; FEATURES.md refreshed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 11:57:27 +02:00
|
|
|
file.write((uint8_t *)&_prefs.bot_commands_enabled, sizeof(_prefs.bot_commands_enabled));
|
|
|
|
|
file.write((uint8_t *)&_prefs.bot_quiet_start, sizeof(_prefs.bot_quiet_start));
|
|
|
|
|
file.write((uint8_t *)&_prefs.bot_quiet_end, sizeof(_prefs.bot_quiet_end));
|
|
|
|
|
file.write((uint8_t *)_prefs.bot_trigger_ch, sizeof(_prefs.bot_trigger_ch));
|
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
|
|
|
file.write((uint8_t *)_prefs.user_radio_presets, sizeof(_prefs.user_radio_presets));
|
|
|
|
|
file.write((uint8_t *)&_prefs.repeat_skip_adverts, sizeof(_prefs.repeat_skip_adverts));
|
|
|
|
|
file.write((uint8_t *)&_prefs.repeat_max_hops, sizeof(_prefs.repeat_max_hops));
|
|
|
|
|
file.write((uint8_t *)&_prefs.repeat_delay_boost, sizeof(_prefs.repeat_delay_boost));
|
|
|
|
|
file.write((uint8_t *)&_prefs.repeat_min_snr, sizeof(_prefs.repeat_min_snr));
|
|
|
|
|
file.write((uint8_t *)&_prefs.repeat_suppress_dup, sizeof(_prefs.repeat_suppress_dup));
|
|
|
|
|
file.write((uint8_t *)&_prefs.repeater_use_profile, sizeof(_prefs.repeater_use_profile));
|
|
|
|
|
file.write((uint8_t *)&_prefs.repeater_freq, sizeof(_prefs.repeater_freq));
|
|
|
|
|
file.write((uint8_t *)&_prefs.repeater_bw, sizeof(_prefs.repeater_bw));
|
|
|
|
|
file.write((uint8_t *)&_prefs.repeater_sf, sizeof(_prefs.repeater_sf));
|
|
|
|
|
file.write((uint8_t *)&_prefs.repeater_cr, sizeof(_prefs.repeater_cr));
|
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
|
|
|
file.write((uint8_t *)&_prefs.track_shared_loc, sizeof(_prefs.track_shared_loc));
|
|
|
|
|
file.write((uint8_t *)&_prefs.loc_share_enabled, sizeof(_prefs.loc_share_enabled));
|
|
|
|
|
file.write((uint8_t *)&_prefs.loc_share_target_type, sizeof(_prefs.loc_share_target_type));
|
|
|
|
|
file.write((uint8_t *)&_prefs.loc_share_channel_idx, sizeof(_prefs.loc_share_channel_idx));
|
|
|
|
|
file.write((uint8_t *)_prefs.loc_share_dm_prefix, sizeof(_prefs.loc_share_dm_prefix));
|
|
|
|
|
file.write((uint8_t *)&_prefs.loc_share_move_idx, sizeof(_prefs.loc_share_move_idx));
|
|
|
|
|
file.write((uint8_t *)&_prefs.loc_share_interval_idx, sizeof(_prefs.loc_share_interval_idx));
|
|
|
|
|
file.write((uint8_t *)&_prefs.loc_share_heartbeat_idx, sizeof(_prefs.loc_share_heartbeat_idx));
|
|
|
|
|
file.write((uint8_t *)&_prefs.locator_enabled, sizeof(_prefs.locator_enabled));
|
|
|
|
|
file.write((uint8_t *)&_prefs.locator_has_target, sizeof(_prefs.locator_has_target));
|
|
|
|
|
file.write((uint8_t *)&_prefs.locator_radius_idx, sizeof(_prefs.locator_radius_idx));
|
|
|
|
|
file.write((uint8_t *)&_prefs.locator_mode, sizeof(_prefs.locator_mode));
|
|
|
|
|
file.write((uint8_t *)&_prefs.locator_lat_1e6, sizeof(_prefs.locator_lat_1e6));
|
|
|
|
|
file.write((uint8_t *)&_prefs.locator_lon_1e6, sizeof(_prefs.locator_lon_1e6));
|
|
|
|
|
file.write((uint8_t *)_prefs.locator_label, sizeof(_prefs.locator_label));
|
|
|
|
|
file.write((uint8_t *)&_prefs.trail_autopause_idx, sizeof(_prefs.trail_autopause_idx));
|
|
|
|
|
file.write((uint8_t *)&_prefs.locator_beeper, sizeof(_prefs.locator_beeper));
|
|
|
|
|
file.write((uint8_t *)&_prefs.locator_target_kind, sizeof(_prefs.locator_target_kind));
|
|
|
|
|
file.write((uint8_t *)_prefs.locator_key, sizeof(_prefs.locator_key));
|
2026-06-29 19:19:43 +02:00
|
|
|
file.write((uint8_t *)&_prefs.gps_avg_idx, sizeof(_prefs.gps_avg_idx));
|
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
|
|
|
file.write((uint8_t *)&_prefs.alarm_on, sizeof(_prefs.alarm_on));
|
|
|
|
|
file.write((uint8_t *)&_prefs.alarm_hour, sizeof(_prefs.alarm_hour));
|
|
|
|
|
file.write((uint8_t *)&_prefs.alarm_min, sizeof(_prefs.alarm_min));
|
2026-07-02 12:21:52 +02:00
|
|
|
file.write((uint8_t *)&_prefs.keyboard_type, sizeof(_prefs.keyboard_type));
|
2026-07-02 12:01:40 +02:00
|
|
|
// page_order tail slots (see loadPrefsInt): entries beyond PAGE_ORDER_LEN_V1,
|
|
|
|
|
// appended here so the on-disk head stays the original 11 bytes.
|
|
|
|
|
file.write((uint8_t *)&_prefs.page_order[NodePrefs::PAGE_ORDER_LEN_V1],
|
|
|
|
|
NodePrefs::PAGE_ORDER_LEN - NodePrefs::PAGE_ORDER_LEN_V1);
|
2026-07-05 22:21:42 +02:00
|
|
|
file.write((uint8_t *)&_prefs.trail_autosave_lowbatt, sizeof(_prefs.trail_autosave_lowbatt));
|
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
|
|
|
file.write((uint8_t *)&_prefs.alarm_repeat_mask, sizeof(_prefs.alarm_repeat_mask));
|
|
|
|
|
file.write((uint8_t *)&_prefs.keyboard_alt_alphabet, sizeof(_prefs.keyboard_alt_alphabet));
|
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
|
|
|
file.write((uint8_t *)&_prefs.bot_dm_scope, sizeof(_prefs.bot_dm_scope));
|
|
|
|
|
file.write((uint8_t *)&_prefs.bot_room_enabled, sizeof(_prefs.bot_room_enabled));
|
|
|
|
|
file.write((uint8_t *)_prefs.bot_room_prefix, sizeof(_prefs.bot_room_prefix));
|
|
|
|
|
file.write((uint8_t *)_prefs.bot_trigger_room, sizeof(_prefs.bot_trigger_room));
|
|
|
|
|
file.write((uint8_t *)_prefs.bot_reply_room, sizeof(_prefs.bot_reply_room));
|
|
|
|
|
file.write((uint8_t *)&_prefs.bot_commands_ch, sizeof(_prefs.bot_commands_ch));
|
|
|
|
|
file.write((uint8_t *)&_prefs.bot_commands_room, sizeof(_prefs.bot_commands_room));
|
2026-07-17 20:50:41 +02:00
|
|
|
file.write((uint8_t *)&_prefs.keyboard_main_alphabet, sizeof(_prefs.keyboard_main_alphabet));
|
2026-07-21 20:30:03 +02:00
|
|
|
file.write((uint8_t *)&_prefs.bot_actions_dm, sizeof(_prefs.bot_actions_dm));
|
|
|
|
|
file.write((uint8_t *)&_prefs.bot_actions_ch, sizeof(_prefs.bot_actions_ch));
|
|
|
|
|
file.write((uint8_t *)&_prefs.bot_actions_room, sizeof(_prefs.bot_actions_room));
|
|
|
|
|
file.write((uint8_t *)&_prefs.gpio1_mode, sizeof(_prefs.gpio1_mode));
|
|
|
|
|
file.write((uint8_t *)&_prefs.gpio2_mode, sizeof(_prefs.gpio2_mode));
|
|
|
|
|
file.write((uint8_t *)&_prefs.gpio3_mode, sizeof(_prefs.gpio3_mode));
|
|
|
|
|
file.write((uint8_t *)&_prefs.gpio4_mode, sizeof(_prefs.gpio4_mode));
|
2026-07-24 15:51:51 +02:00
|
|
|
file.write((uint8_t *)&_prefs.keyboard_cardkb_compact, sizeof(_prefs.keyboard_cardkb_compact));
|
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
|
|
|
|
|
|
|
|
// Tail sentinel — must be last. See NodePrefs::SCHEMA_SENTINEL. Its write is
|
|
|
|
|
// the one we check: once the flash fills, writes return 0, so a good
|
|
|
|
|
// sentinel write means the whole record fit. Only then swap it in.
|
2026-05-24 20:18:50 +02:00
|
|
|
uint32_t sentinel = NodePrefs::SCHEMA_SENTINEL;
|
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 ok = (file.write((uint8_t *)&sentinel, sizeof(sentinel)) == sizeof(sentinel));
|
2026-05-24 20:18:50 +02:00
|
|
|
|
2025-06-06 15:30:35 +10:00
|
|
|
file.close();
|
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 (ok) {
|
|
|
|
|
commitTempFile(_fs, "/new_prefs.tmp", "/new_prefs");
|
|
|
|
|
} else {
|
|
|
|
|
_fs->remove("/new_prefs.tmp"); // keep the previous good /new_prefs
|
|
|
|
|
}
|
2025-06-06 15:30:35 +10:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-10 18:23:25 +02:00
|
|
|
void DataStore::saveRTCTime() {
|
|
|
|
|
uint32_t t = _clock->getCurrentTime();
|
|
|
|
|
if (t < 1000000000UL) return; // don't save if time not yet synced
|
2026-05-26 08:05:05 +02:00
|
|
|
File file = ::openWrite(_fs, "/rtc_save");
|
2026-05-10 18:23:25 +02:00
|
|
|
if (file) {
|
|
|
|
|
file.write((uint8_t *)&t, sizeof(t));
|
|
|
|
|
file.close();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void DataStore::restoreRTCTime() {
|
|
|
|
|
File file = openRead(_fs, "/rtc_save");
|
|
|
|
|
if (file) {
|
|
|
|
|
uint32_t t = 0;
|
|
|
|
|
file.read((uint8_t *)&t, sizeof(t));
|
|
|
|
|
file.close();
|
|
|
|
|
if (t > 1000000000UL) _clock->setCurrentTime(t);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-06 15:30:35 +10:00
|
|
|
void DataStore::loadContacts(DataStoreHost* host) {
|
2025-10-31 11:42:36 +11:00
|
|
|
File file = openRead(_getContactsChannelsFS(), "/contacts3");
|
2025-06-06 15:30:35 +10:00
|
|
|
if (file) {
|
|
|
|
|
bool full = false;
|
|
|
|
|
while (!full) {
|
|
|
|
|
ContactInfo c;
|
|
|
|
|
uint8_t pub_key[32];
|
|
|
|
|
uint8_t unused;
|
|
|
|
|
|
|
|
|
|
bool success = (file.read(pub_key, 32) == 32);
|
|
|
|
|
success = success && (file.read((uint8_t *)&c.name, 32) == 32);
|
|
|
|
|
success = success && (file.read(&c.type, 1) == 1);
|
|
|
|
|
success = success && (file.read(&c.flags, 1) == 1);
|
|
|
|
|
success = success && (file.read(&unused, 1) == 1);
|
|
|
|
|
success = success && (file.read((uint8_t *)&c.sync_since, 4) == 4); // was 'reserved'
|
|
|
|
|
success = success && (file.read((uint8_t *)&c.out_path_len, 1) == 1);
|
|
|
|
|
success = success && (file.read((uint8_t *)&c.last_advert_timestamp, 4) == 4);
|
|
|
|
|
success = success && (file.read(c.out_path, 64) == 64);
|
|
|
|
|
success = success && (file.read((uint8_t *)&c.lastmod, 4) == 4);
|
|
|
|
|
success = success && (file.read((uint8_t *)&c.gps_lat, 4) == 4);
|
|
|
|
|
success = success && (file.read((uint8_t *)&c.gps_lon, 4) == 4);
|
|
|
|
|
|
|
|
|
|
if (!success) break; // EOF
|
|
|
|
|
|
|
|
|
|
c.id = mesh::Identity(pub_key);
|
|
|
|
|
if (!host->onContactLoaded(c)) full = true;
|
|
|
|
|
}
|
|
|
|
|
file.close();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-01 16:49:31 +10:00
|
|
|
void DataStore::saveContacts(DataStoreHost* host, bool (*filter)(const ContactInfo& c)) {
|
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
|
|
|
FILESYSTEM* fs = _getContactsChannelsFS();
|
|
|
|
|
// Write to a temp file, then atomically rename it over /contacts3 only once
|
|
|
|
|
// every record has written cleanly. The old code truncated /contacts3 up
|
|
|
|
|
// front and wrote in place, so a crash, reset or full flash mid-save wiped
|
|
|
|
|
// the entire contact list. Now an interrupted save leaves the previous good
|
|
|
|
|
// file untouched.
|
|
|
|
|
File file = ::openWrite(fs, "/contacts3.tmp");
|
|
|
|
|
if (!file) return;
|
2025-06-06 15:30:35 +10: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
|
|
|
bool ok = true;
|
|
|
|
|
uint32_t idx = 0;
|
|
|
|
|
ContactInfo c;
|
|
|
|
|
uint8_t unused = 0;
|
|
|
|
|
|
|
|
|
|
while (host->getContactForSave(idx, c)) {
|
|
|
|
|
if (filter && !filter(c)) {
|
2025-06-06 15:30:35 +10:00
|
|
|
idx++; // advance to next contact
|
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
|
|
|
continue;
|
2025-06-06 15:30:35 +10: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
|
|
|
bool success = (file.write(c.id.pub_key, 32) == 32);
|
|
|
|
|
success = success && (file.write((uint8_t *)&c.name, 32) == 32);
|
|
|
|
|
success = success && (file.write(&c.type, 1) == 1);
|
|
|
|
|
success = success && (file.write(&c.flags, 1) == 1);
|
|
|
|
|
success = success && (file.write(&unused, 1) == 1);
|
|
|
|
|
success = success && (file.write((uint8_t *)&c.sync_since, 4) == 4);
|
|
|
|
|
success = success && (file.write((uint8_t *)&c.out_path_len, 1) == 1);
|
|
|
|
|
success = success && (file.write((uint8_t *)&c.last_advert_timestamp, 4) == 4);
|
|
|
|
|
success = success && (file.write(c.out_path, 64) == 64);
|
|
|
|
|
success = success && (file.write((uint8_t *)&c.lastmod, 4) == 4);
|
|
|
|
|
success = success && (file.write((uint8_t *)&c.gps_lat, 4) == 4);
|
|
|
|
|
success = success && (file.write((uint8_t *)&c.gps_lon, 4) == 4);
|
|
|
|
|
|
|
|
|
|
if (!success) { ok = false; break; } // write failed (e.g. flash full)
|
|
|
|
|
|
|
|
|
|
idx++; // advance to next contact
|
|
|
|
|
}
|
|
|
|
|
file.close();
|
|
|
|
|
|
|
|
|
|
if (ok) {
|
|
|
|
|
commitTempFile(fs, "/contacts3.tmp", "/contacts3");
|
|
|
|
|
} else {
|
|
|
|
|
fs->remove("/contacts3.tmp"); // keep the previous good /contacts3
|
2025-06-06 15:30:35 +10:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void DataStore::loadChannels(DataStoreHost* host) {
|
2026-06-26 17:48:16 +02:00
|
|
|
FILESYSTEM* fs = _getContactsChannelsFS();
|
|
|
|
|
File file = openRead(fs, "/channels3");
|
2025-06-06 15:30:35 +10:00
|
|
|
if (file) {
|
2026-06-26 17:48:16 +02:00
|
|
|
// /channels3: the leading 4-byte field's first byte is the channel's
|
|
|
|
|
// original slot index (see saveChannels()) — load it back into that
|
|
|
|
|
// exact slot. The old /channels2 format instead reassigned indices
|
|
|
|
|
// 0,1,2… sequentially on every load, which silently shifted every
|
|
|
|
|
// later channel down a slot once an earlier one was removed — anything
|
|
|
|
|
// that remembers a channel by index (Live Share's target, the bot's
|
|
|
|
|
// channel, per-channel melody) would then point at the wrong channel
|
|
|
|
|
// after the next reboot.
|
2025-06-06 15:30:35 +10:00
|
|
|
bool full = false;
|
2026-05-29 19:56:25 +02:00
|
|
|
uint8_t skipped = 0;
|
2025-06-06 15:30:35 +10:00
|
|
|
while (!full) {
|
|
|
|
|
ChannelDetails ch;
|
2026-06-26 17:48:16 +02:00
|
|
|
uint8_t hdr[4];
|
2025-06-06 15:30:35 +10:00
|
|
|
|
2026-06-26 17:48:16 +02:00
|
|
|
bool success = (file.read(hdr, 4) == 4);
|
2025-06-06 15:30:35 +10:00
|
|
|
success = success && (file.read((uint8_t *)ch.name, 32) == 32);
|
|
|
|
|
success = success && (file.read((uint8_t *)ch.channel.secret, 32) == 32);
|
|
|
|
|
|
|
|
|
|
if (!success) break; // EOF
|
|
|
|
|
|
2026-05-29 19:56:25 +02:00
|
|
|
// Sanity check: an all-zero secret means the entry is uninitialised
|
|
|
|
|
// or the file format was corrupted by a previous firmware (different
|
|
|
|
|
// layout). Loading such a channel makes findChannelIdx() match the
|
|
|
|
|
// wrong slot for incoming messages — drop it. The companion app can
|
|
|
|
|
// re-sync the channel afterwards.
|
|
|
|
|
bool secret_empty = true;
|
|
|
|
|
for (int b = 0; b < 32; b++) {
|
|
|
|
|
if (ch.channel.secret[b] != 0) { secret_empty = false; break; }
|
|
|
|
|
}
|
|
|
|
|
if (secret_empty) {
|
|
|
|
|
skipped++;
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
// Defensive: ensure name is null-terminated so callers can treat it
|
|
|
|
|
// as a C string regardless of how the file was written.
|
|
|
|
|
ch.name[31] = '\0';
|
|
|
|
|
|
2026-06-26 17:48:16 +02:00
|
|
|
if (!host->onChannelLoaded(hdr[0], ch)) full = true;
|
2025-06-06 15:30:35 +10:00
|
|
|
}
|
|
|
|
|
file.close();
|
2026-05-29 19:56:25 +02:00
|
|
|
if (skipped > 0) {
|
|
|
|
|
MESH_DEBUG_PRINTLN("loadChannels: skipped %u corrupted/empty channel entr%s",
|
|
|
|
|
(unsigned)skipped, skipped == 1 ? "y" : "ies");
|
|
|
|
|
}
|
2026-06-26 17:48:16 +02:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// One-time migration from the old /channels2 format (sequential index,
|
|
|
|
|
// reassigned on every load — the bug /channels3 above replaces). Loads
|
|
|
|
|
// with that old semantics once, then resaves as /channels3 so this
|
|
|
|
|
// fallback is never hit again on this device.
|
|
|
|
|
file = openRead(fs, "/channels2");
|
|
|
|
|
if (file) {
|
|
|
|
|
bool full = false;
|
|
|
|
|
uint8_t channel_idx = 0;
|
|
|
|
|
while (!full) {
|
|
|
|
|
ChannelDetails ch;
|
|
|
|
|
uint8_t unused[4];
|
|
|
|
|
|
|
|
|
|
bool success = (file.read(unused, 4) == 4);
|
|
|
|
|
success = success && (file.read((uint8_t *)ch.name, 32) == 32);
|
|
|
|
|
success = success && (file.read((uint8_t *)ch.channel.secret, 32) == 32);
|
|
|
|
|
if (!success) break; // EOF
|
|
|
|
|
|
|
|
|
|
bool secret_empty = true;
|
|
|
|
|
for (int b = 0; b < 32; b++) if (ch.channel.secret[b] != 0) { secret_empty = false; break; }
|
|
|
|
|
if (secret_empty) continue;
|
|
|
|
|
ch.name[31] = '\0';
|
|
|
|
|
|
|
|
|
|
if (host->onChannelLoaded(channel_idx, ch)) channel_idx++;
|
|
|
|
|
else full = true;
|
|
|
|
|
}
|
|
|
|
|
file.close();
|
|
|
|
|
saveChannels(host); // write /channels3 so the migration runs only once
|
2025-06-06 15:30:35 +10:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void DataStore::saveChannels(DataStoreHost* host) {
|
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
|
|
|
FILESYSTEM* fs = _getContactsChannelsFS();
|
|
|
|
|
// Same atomic temp-then-rename pattern as saveContacts() — never truncate the
|
2026-06-26 17:48:16 +02:00
|
|
|
// live /channels3 before the new copy is fully written.
|
|
|
|
|
File file = ::openWrite(fs, "/channels3.tmp");
|
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 (!file) return;
|
|
|
|
|
|
|
|
|
|
bool ok = true;
|
|
|
|
|
uint8_t channel_idx = 0;
|
|
|
|
|
ChannelDetails ch;
|
|
|
|
|
|
|
|
|
|
while (host->getChannelForSave(channel_idx, ch)) {
|
2026-06-26 17:48:16 +02:00
|
|
|
uint8_t idx = channel_idx++;
|
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
|
|
|
// getChannelForSave() returns every slot up to MAX_GROUP_CHANNELS, so skip
|
|
|
|
|
// the unused ones (all-zero secret) rather than writing all 40 — otherwise
|
2026-06-26 17:48:16 +02:00
|
|
|
// the file is always ~2.7 KB and wears the flash needlessly. Unlike the old
|
|
|
|
|
// /channels2 format, loadChannels() no longer compacts: the slot index
|
|
|
|
|
// travels with the record (hdr[0] below) so a removed channel just leaves
|
|
|
|
|
// a hole instead of shifting every later index down a slot.
|
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 empty = true;
|
|
|
|
|
for (int b = 0; b < 32; b++) if (ch.channel.secret[b]) { empty = false; break; }
|
|
|
|
|
if (empty) continue;
|
|
|
|
|
|
2026-06-26 17:48:16 +02:00
|
|
|
uint8_t hdr[4] = { idx, 0, 0, 0 };
|
|
|
|
|
bool success = (file.write(hdr, 4) == 4);
|
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
|
|
|
success = success && (file.write((uint8_t *)ch.name, 32) == 32);
|
|
|
|
|
success = success && (file.write((uint8_t *)ch.channel.secret, 32) == 32);
|
|
|
|
|
if (!success) { ok = false; break; } // write failed
|
|
|
|
|
}
|
|
|
|
|
file.close();
|
|
|
|
|
|
|
|
|
|
if (ok) {
|
2026-06-26 17:48:16 +02:00
|
|
|
commitTempFile(fs, "/channels3.tmp", "/channels3");
|
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 {
|
2026-06-26 17:48:16 +02:00
|
|
|
fs->remove("/channels3.tmp"); // keep the previous good /channels3
|
2025-06-06 15:30:35 +10:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-06 19:50:51 +10:00
|
|
|
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
|
|
|
|
|
|
2025-06-06 21:35:54 +10:00
|
|
|
#define MAX_ADVERT_PKT_LEN (2 + 32 + PUB_KEY_SIZE + 4 + SIGNATURE_SIZE + MAX_ADVERT_DATA_SIZE)
|
|
|
|
|
|
|
|
|
|
struct BlobRec {
|
|
|
|
|
uint32_t timestamp;
|
|
|
|
|
uint8_t key[7];
|
|
|
|
|
uint8_t len;
|
|
|
|
|
uint8_t data[MAX_ADVERT_PKT_LEN];
|
|
|
|
|
};
|
2025-06-06 19:50:51 +10:00
|
|
|
|
|
|
|
|
void DataStore::checkAdvBlobFile() {
|
2025-09-06 14:07:18 +10:00
|
|
|
if (!_getContactsChannelsFS()->exists("/adv_blobs")) {
|
2026-05-26 08:05:05 +02:00
|
|
|
File file = ::openWrite(_getContactsChannelsFS(), "/adv_blobs");
|
2025-06-06 19:50:51 +10:00
|
|
|
if (file) {
|
2025-06-06 21:35:54 +10:00
|
|
|
BlobRec zeroes;
|
|
|
|
|
memset(&zeroes, 0, sizeof(zeroes));
|
2025-07-29 20:06:35 +10:00
|
|
|
for (int i = 0; i < MAX_BLOBRECS; i++) { // pre-allocate to fixed size
|
2025-06-06 21:35:54 +10:00
|
|
|
file.write((uint8_t *) &zeroes, sizeof(zeroes));
|
2025-06-06 19:50:51 +10:00
|
|
|
}
|
|
|
|
|
file.close();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-22 13:09:54 +10:00
|
|
|
void DataStore::migrateToSecondaryFS() {
|
|
|
|
|
// migrate old adv_blobs, contacts3 and channels2 files to secondary FS if they don't already exist
|
|
|
|
|
if (!_fsExtra->exists("/adv_blobs")) {
|
|
|
|
|
if (_fs->exists("/adv_blobs")) {
|
|
|
|
|
File oldAdvBlobs = openRead(_fs, "/adv_blobs");
|
2026-05-26 08:05:05 +02:00
|
|
|
File newAdvBlobs = ::openWrite(_fsExtra, "/adv_blobs");
|
2025-08-22 13:09:54 +10:00
|
|
|
|
|
|
|
|
if (oldAdvBlobs && newAdvBlobs) {
|
|
|
|
|
BlobRec rec;
|
|
|
|
|
size_t count = 0;
|
|
|
|
|
|
|
|
|
|
// Copy 20 BlobRecs from old to new
|
|
|
|
|
while (count < 20 && oldAdvBlobs.read((uint8_t *)&rec, sizeof(rec)) == sizeof(rec)) {
|
|
|
|
|
newAdvBlobs.seek(count * sizeof(BlobRec));
|
|
|
|
|
newAdvBlobs.write((uint8_t *)&rec, sizeof(rec));
|
|
|
|
|
count++;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (oldAdvBlobs) oldAdvBlobs.close();
|
|
|
|
|
if (newAdvBlobs) newAdvBlobs.close();
|
|
|
|
|
_fs->remove("/adv_blobs");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (!_fsExtra->exists("/contacts3")) {
|
|
|
|
|
if (_fs->exists("/contacts3")) {
|
|
|
|
|
File oldFile = openRead(_fs, "/contacts3");
|
2026-05-26 08:05:05 +02:00
|
|
|
File newFile = ::openWrite(_fsExtra, "/contacts3");
|
2025-08-22 13:09:54 +10:00
|
|
|
|
|
|
|
|
if (oldFile && newFile) {
|
|
|
|
|
uint8_t buf[64];
|
|
|
|
|
int n;
|
|
|
|
|
while ((n = oldFile.read(buf, sizeof(buf))) > 0) {
|
|
|
|
|
newFile.write(buf, n);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (oldFile) oldFile.close();
|
|
|
|
|
if (newFile) newFile.close();
|
|
|
|
|
_fs->remove("/contacts3");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (!_fsExtra->exists("/channels2")) {
|
2025-08-25 12:41:59 +10:00
|
|
|
if (_fs->exists("/channels2")) {
|
|
|
|
|
File oldFile = openRead(_fs, "/channels2");
|
2026-05-26 08:05:05 +02:00
|
|
|
File newFile = ::openWrite(_fsExtra, "/channels2");
|
2025-08-22 13:09:54 +10:00
|
|
|
|
|
|
|
|
if (oldFile && newFile) {
|
|
|
|
|
uint8_t buf[64];
|
|
|
|
|
int n;
|
|
|
|
|
while ((n = oldFile.read(buf, sizeof(buf))) > 0) {
|
|
|
|
|
newFile.write(buf, n);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (oldFile) oldFile.close();
|
|
|
|
|
if (newFile) newFile.close();
|
2025-08-25 12:41:59 +10:00
|
|
|
_fs->remove("/channels2");
|
2025-08-22 13:09:54 +10:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// cleanup nodes which have been testing the extra fs, copy _main.id and new_prefs back to primary
|
|
|
|
|
if (_fsExtra->exists("/_main.id")) {
|
|
|
|
|
if (_fs->exists("/_main.id")) {_fs->remove("/_main.id");}
|
|
|
|
|
File oldFile = openRead(_fsExtra, "/_main.id");
|
2026-05-26 08:05:05 +02:00
|
|
|
File newFile = ::openWrite(_fs, "/_main.id");
|
2025-08-22 13:09:54 +10:00
|
|
|
|
|
|
|
|
if (oldFile && newFile) {
|
|
|
|
|
uint8_t buf[64];
|
|
|
|
|
int n;
|
|
|
|
|
while ((n = oldFile.read(buf, sizeof(buf))) > 0) {
|
|
|
|
|
newFile.write(buf, n);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (oldFile) oldFile.close();
|
|
|
|
|
if (newFile) newFile.close();
|
|
|
|
|
_fsExtra->remove("/_main.id");
|
|
|
|
|
}
|
|
|
|
|
if (_fsExtra->exists("/new_prefs")) {
|
|
|
|
|
if (_fs->exists("/new_prefs")) {_fs->remove("/new_prefs");}
|
|
|
|
|
File oldFile = openRead(_fsExtra, "/new_prefs");
|
2026-05-26 08:05:05 +02:00
|
|
|
File newFile = ::openWrite(_fs, "/new_prefs");
|
2025-08-22 13:09:54 +10:00
|
|
|
|
|
|
|
|
if (oldFile && newFile) {
|
|
|
|
|
uint8_t buf[64];
|
|
|
|
|
int n;
|
|
|
|
|
while ((n = oldFile.read(buf, sizeof(buf))) > 0) {
|
|
|
|
|
newFile.write(buf, n);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (oldFile) oldFile.close();
|
|
|
|
|
if (newFile) newFile.close();
|
|
|
|
|
_fsExtra->remove("/new_prefs");
|
|
|
|
|
}
|
|
|
|
|
// remove files from where they should not be anymore
|
|
|
|
|
if (_fs->exists("/adv_blobs")) {
|
|
|
|
|
_fs->remove("/adv_blobs");
|
|
|
|
|
}
|
|
|
|
|
if (_fs->exists("/contacts3")) {
|
|
|
|
|
_fs->remove("/contacts3");
|
|
|
|
|
}
|
2025-08-25 12:41:59 +10:00
|
|
|
if (_fs->exists("/channels2")) {
|
|
|
|
|
_fs->remove("/channels2");
|
|
|
|
|
}
|
2025-08-22 13:09:54 +10:00
|
|
|
if (_fsExtra->exists("/_main.id")) {
|
|
|
|
|
_fsExtra->remove("/_main.id");
|
|
|
|
|
}
|
|
|
|
|
if (_fsExtra->exists("/new_prefs")) {
|
|
|
|
|
_fsExtra->remove("/new_prefs");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-06 19:50:51 +10:00
|
|
|
uint8_t DataStore::getBlobByKey(const uint8_t key[], int key_len, uint8_t dest_buf[]) {
|
2025-10-31 11:42:36 +11:00
|
|
|
File file = openRead(_getContactsChannelsFS(), "/adv_blobs");
|
2025-06-06 19:50:51 +10:00
|
|
|
uint8_t len = 0; // 0 = not found
|
|
|
|
|
if (file) {
|
2025-06-06 21:35:54 +10:00
|
|
|
BlobRec tmp;
|
|
|
|
|
while (file.read((uint8_t *) &tmp, sizeof(tmp)) == sizeof(tmp)) {
|
|
|
|
|
if (memcmp(key, tmp.key, sizeof(tmp.key)) == 0) { // only match by 7 byte prefix
|
|
|
|
|
len = tmp.len;
|
|
|
|
|
memcpy(dest_buf, tmp.data, len);
|
2025-06-06 19:50:51 +10:00
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
file.close();
|
|
|
|
|
}
|
|
|
|
|
return len;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool DataStore::putBlobByKey(const uint8_t key[], int key_len, const uint8_t src_buf[], uint8_t len) {
|
|
|
|
|
if (len < PUB_KEY_SIZE+4+SIGNATURE_SIZE || len > MAX_ADVERT_PKT_LEN) return false;
|
|
|
|
|
checkAdvBlobFile();
|
2025-10-31 15:06:29 +11:00
|
|
|
File file = _getContactsChannelsFS()->open("/adv_blobs", FILE_O_WRITE);
|
2025-06-06 19:50:51 +10:00
|
|
|
if (file) {
|
|
|
|
|
uint32_t pos = 0, found_pos = 0;
|
|
|
|
|
uint32_t min_timestamp = 0xFFFFFFFF;
|
|
|
|
|
|
|
|
|
|
// search for matching key OR evict by oldest timestmap
|
2025-06-06 21:35:54 +10:00
|
|
|
BlobRec tmp;
|
|
|
|
|
file.seek(0);
|
|
|
|
|
while (file.read((uint8_t *) &tmp, sizeof(tmp)) == sizeof(tmp)) {
|
|
|
|
|
if (memcmp(key, tmp.key, sizeof(tmp.key)) == 0) { // only match by 7 byte prefix
|
2025-06-06 19:50:51 +10:00
|
|
|
found_pos = pos;
|
|
|
|
|
break;
|
|
|
|
|
}
|
2025-06-06 21:35:54 +10:00
|
|
|
if (tmp.timestamp < min_timestamp) {
|
|
|
|
|
min_timestamp = tmp.timestamp;
|
2025-06-06 19:50:51 +10:00
|
|
|
found_pos = pos;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pos += sizeof(tmp);
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-06 21:35:54 +10:00
|
|
|
memcpy(tmp.key, key, sizeof(tmp.key)); // just record 7 byte prefix of key
|
|
|
|
|
memcpy(tmp.data, src_buf, len);
|
|
|
|
|
tmp.len = len;
|
|
|
|
|
tmp.timestamp = _clock->getCurrentTime();
|
|
|
|
|
|
2025-06-06 19:50:51 +10:00
|
|
|
file.seek(found_pos);
|
2025-06-06 21:35:54 +10:00
|
|
|
file.write((uint8_t *) &tmp, sizeof(tmp));
|
2025-06-06 19:50:51 +10:00
|
|
|
|
|
|
|
|
file.close();
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
return false; // error
|
|
|
|
|
}
|
2026-01-27 17:51:30 +11:00
|
|
|
bool DataStore::deleteBlobByKey(const uint8_t key[], int key_len) {
|
|
|
|
|
return true; // this is just a stub on NRF52/STM32 platforms
|
|
|
|
|
}
|
2025-06-06 19:50:51 +10:00
|
|
|
#else
|
2026-01-27 19:31:07 +11:00
|
|
|
inline void makeBlobPath(const uint8_t key[], int key_len, char* path, size_t path_size) {
|
2025-06-06 15:30:35 +10:00
|
|
|
char fname[18];
|
|
|
|
|
if (key_len > 8) key_len = 8; // just use first 8 bytes (prefix)
|
|
|
|
|
mesh::Utils::toHex(fname, key, key_len);
|
|
|
|
|
sprintf(path, "/bl/%s", fname);
|
2026-01-27 19:31:07 +11:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
uint8_t DataStore::getBlobByKey(const uint8_t key[], int key_len, uint8_t dest_buf[]) {
|
|
|
|
|
char path[64];
|
|
|
|
|
makeBlobPath(key, key_len, path, sizeof(path));
|
2025-06-06 15:30:35 +10:00
|
|
|
|
|
|
|
|
if (_fs->exists(path)) {
|
2025-10-31 11:42:36 +11:00
|
|
|
File f = openRead(_fs, path);
|
2025-06-06 15:30:35 +10:00
|
|
|
if (f) {
|
|
|
|
|
int len = f.read(dest_buf, 255); // currently MAX 255 byte blob len supported!!
|
|
|
|
|
f.close();
|
|
|
|
|
return len;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return 0; // not found
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-06 19:50:51 +10:00
|
|
|
bool DataStore::putBlobByKey(const uint8_t key[], int key_len, const uint8_t src_buf[], uint8_t len) {
|
2025-06-06 15:30:35 +10:00
|
|
|
char path[64];
|
2026-01-27 19:31:07 +11:00
|
|
|
makeBlobPath(key, key_len, path, sizeof(path));
|
2025-06-06 15:30:35 +10:00
|
|
|
|
2026-05-26 08:05:05 +02:00
|
|
|
File f = ::openWrite(_fs, path);
|
2025-06-06 15:30:35 +10:00
|
|
|
if (f) {
|
|
|
|
|
int n = f.write(src_buf, len);
|
|
|
|
|
f.close();
|
|
|
|
|
if (n == len) return true; // success!
|
|
|
|
|
|
|
|
|
|
_fs->remove(path); // blob was only partially written!
|
|
|
|
|
}
|
|
|
|
|
return false; // error
|
|
|
|
|
}
|
2026-01-27 17:51:30 +11:00
|
|
|
|
|
|
|
|
bool DataStore::deleteBlobByKey(const uint8_t key[], int key_len) {
|
|
|
|
|
char path[64];
|
2026-01-27 19:31:07 +11:00
|
|
|
makeBlobPath(key, key_len, path, sizeof(path));
|
2026-01-27 17:51:30 +11:00
|
|
|
|
|
|
|
|
_fs->remove(path);
|
|
|
|
|
|
|
|
|
|
return true; // return true even if file did not exist
|
|
|
|
|
}
|
2025-06-06 19:50:51 +10:00
|
|
|
#endif
|