2025-06-01 09:25:17 -07:00
# pragma once
2025-05-29 13:06:24 -07:00
# include <Arduino.h>
# include <Mesh.h>
2025-08-16 20:04:54 +10:00
# include "AbstractUITask.h"
2026-05-26 21:26:07 +02:00
# include <helpers/ui/DisplayDriver.h>
// Forward declaration for UITask
class UITask ;
2025-05-29 13:06:24 -07:00
/*------------ Frame Protocol --------------*/
2026-06-01 16:49:31 +10:00
# define FIRMWARE_VER_CODE 13
2025-05-29 13:06:24 -07:00
# ifndef FIRMWARE_BUILD_DATE
2026-06-06 21:07:03 +10:00
# define FIRMWARE_BUILD_DATE "6 Jun 2026"
2025-05-29 13:06:24 -07:00
# endif
2026-05-29 12:25:34 +02:00
// Versioning: vX.Y = upstream base, solo.N = fork revision
2025-05-29 13:06:24 -07:00
# ifndef FIRMWARE_VERSION
Merge upstream/main (v1.16.0) into wio-unified
Merge 254 upstream commits. Highlights relevant to the Wio fork: native
NRF52 companion power-saving (sleep when !hasPendingWork), preamble 16->32
for SF<9, generic sensor-registration model, CMD_SEND_RAW_PACKET, isEink()
display abstraction, KEEP_DISPLAY_ON_USB, contacts-sync/transient fixes.
Conflicts resolved (11 files):
- buzzer.h/.cpp: keep NRF52 include guard + our begin()->startup(); fold in
upstream doc comment.
- platformio (wio + eink): keep our slim env layout, add upstream kiss_modem
env (+ debug_tool=stlink on eink ble).
- GxEPDDisplay.h: take upstream isEink() override.
- EnvironmentSensorManager.h: take upstream's wider #if guard.
- DataStore.cpp: take upstream saveContacts(filter) signature, keep our
::openWrite (member openWrite would shadow the global 2-arg overload).
- main.cpp: keep our watchdog init + add board.onBootComplete(); adopt
upstream's `if(!hasPendingWork()) sleep` loop.
- MyMesh.h: version -> v1.16-solo.0, build date 6 Jun 2026.
- MyMesh.cpp: keep both CMD codes (SCREENSHOT 66 + RAW_PACKET 65), both field
inits, both handlers, our screenshot fns + upstream saveContacts/save_filter,
upstream hasPendingWork() + our MyMeshBot include.
- UITask.cpp: keep our solo splash/clock page/snprintf/buzzer init; merge
auto-off (+ opt-in KEEP_DISPLAY_ON_USB); merge low-batt shutdown (our EMA +
prefs threshold + upstream's !isExternalPowered() guard and eink-aware delay).
Post-merge drift fixes (upstream API/architecture changes, not conflicts):
- applyTxPower(): radio_set_tx_power() removed upstream -> radio_driver.setTxPower().
- Sensor refactor dropped the per-sensor *_initialized flags our
getAvailableLPPTypes() relied on; removed that override and the SENSORS page
now derives available LPP types from the populated sensors_lpp buffer.
README kept ours via .gitattributes merge=ours. Builds verified on all 5 Wio
envs (dual, eink dual, radio_ble, dual_dev, eink_dual_dev). Not yet
hardware-verified.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-06 17:21:41 +02:00
# define FIRMWARE_VERSION "v1.16-solo.0"
2025-05-29 13:06:24 -07:00
# endif
# if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
# include <InternalFileSystem.h>
# elif defined(RP2040_PLATFORM)
# include <LittleFS.h>
# elif defined(ESP32)
# include <SPIFFS.h>
# endif
2025-06-06 15:30:35 +10:00
# include "DataStore.h"
2025-05-29 13:06:24 -07:00
# include "NodePrefs.h"
2025-06-01 09:25:17 -07:00
2025-05-29 13:06:24 -07:00
# include <RTClib.h>
2025-06-01 09:25:17 -07:00
# include <helpers/ArduinoHelpers.h>
# include <helpers/BaseSerialInterface.h>
# include <helpers/IdentityStore.h>
# include <helpers/SimpleMeshTables.h>
# include <helpers/StaticPoolPacketManager.h>
2025-05-29 13:06:24 -07:00
# include <target.h>
/* ---------------------------------- CONFIGURATION ------------------------------------- */
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
// LORA_FREQ/BW/SF/CR fallbacks now live in NodePrefs.h (DataStore.cpp needs them too).
2025-05-29 13:06:24 -07:00
# ifndef LORA_TX_POWER
2025-06-01 09:25:17 -07:00
# define LORA_TX_POWER 20
2025-05-29 13:06:24 -07:00
# endif
# ifndef MAX_LORA_TX_POWER
2025-06-01 09:25:17 -07:00
# define MAX_LORA_TX_POWER LORA_TX_POWER
2025-05-29 13:06:24 -07:00
# endif
# ifndef MAX_CONTACTS
2025-06-01 09:25:17 -07:00
# define MAX_CONTACTS 100
2025-05-29 13:06:24 -07:00
# endif
# ifndef OFFLINE_QUEUE_SIZE
2025-06-01 09:25:17 -07:00
# define OFFLINE_QUEUE_SIZE 16
2025-05-29 13:06:24 -07:00
# endif
# ifndef BLE_NAME_PREFIX
2025-06-01 09:25:17 -07:00
# define BLE_NAME_PREFIX "MeshCore-"
2025-05-29 13:06:24 -07:00
# endif
# include <helpers/BaseChatMesh.h>
2025-11-04 01:21:56 +11:00
# include <helpers/TransportKeyStore.h>
2025-05-29 13:06:24 -07:00
/* -------------------------------------------------------------------------------------- */
2025-06-01 09:25:17 -07:00
# define REQ_TYPE_GET_STATUS 0x01 // same as _GET_STATS
# define REQ_TYPE_KEEP_ALIVE 0x02
# define REQ_TYPE_GET_TELEMETRY_DATA 0x03
2025-05-29 13:06:24 -07:00
2025-08-08 20:01:31 +10:00
struct AdvertPath {
uint8_t pubkey_prefix [ 7 ] ;
uint8_t path_len ;
char name [ 32 ] ;
uint32_t recv_timestamp ;
uint8_t path [ MAX_PATH_SIZE ] ;
} ;
2026-05-21 07:57:21 +02:00
struct DiscoverResult {
char name [ 32 ] ; // contact name if known, "" if unknown (use type label)
2026-05-21 00:13:36 +02:00
uint8_t type ; // ADV_TYPE_REPEATER / ADV_TYPE_SENSOR / ADV_TYPE_ROOM
2026-05-21 07:57:21 +02:00
bool is_known ; // true = in contacts[], false = new unknown node
2026-05-21 08:51:43 +02:00
int8_t rssi ; // RSSI of the response as received by us (dBm)
2026-05-21 08:56:54 +02:00
int8_t snr_x4 ; // SNR of the response as received by us (dB × 4)
int8_t remote_snr_x4 ; // SNR at which responder heard our request (dB × 4)
2026-05-21 09:11:32 +02:00
uint8_t pub_key [ PUB_KEY_SIZE ] ;
2026-05-21 07:57:21 +02:00
uint32_t timestamp ;
2026-05-21 00:13:36 +02:00
} ;
2026-05-14 11:30:43 +02:00
# define EXPECTED_ACK_TABLE_SIZE 8
2025-06-06 15:30:35 +10:00
class MyMesh : public BaseChatMesh , public DataStoreHost {
2025-05-29 13:06:24 -07:00
public :
2025-08-16 20:04:54 +10:00
MyMesh ( mesh : : Radio & radio , mesh : : RNG & rng , mesh : : RTCClock & rtc , SimpleMeshTables & tables , DataStore & store , AbstractUITask * ui = NULL ) ;
2025-06-01 09:25:17 -07:00
2025-06-06 15:30:35 +10:00
void begin ( bool has_display ) ;
2025-06-01 09:25:17 -07:00
void startInterface ( BaseSerialInterface & serial ) ;
const char * getNodeName ( ) ;
NodePrefs * getNodePrefs ( ) ;
uint32_t getBLEPin ( ) ;
void loop ( ) ;
void handleCmdFrame ( size_t len ) ;
bool advert ( ) ;
2026-05-21 00:13:36 +02:00
void sendNodeDiscoverReq ( ) ;
2025-06-04 21:33:48 +10:00
void enterCLIRescue ( ) ;
2025-05-29 13:06:24 -07:00
2025-08-08 20:01:31 +10:00
int getRecentlyHeard ( AdvertPath dest [ ] , int max_num ) ;
2026-05-21 07:57:21 +02:00
int getDiscoverResults ( DiscoverResult dest [ ] , int max_count ) ;
2025-08-08 20:01:31 +10:00
2026-07-07 01:52:51 +02:00
// On-device contact management — lets Nearby Nodes add a discovered node or
// delete a contact without the phone app. Mirrors the CMD_ADD/REMOVE paths.
bool addDiscoveredContact ( const uint8_t * pub_key , const char * name , uint8_t type ) ;
bool deleteContactByKey ( const uint8_t * pub_key ) ;
2026-05-29 21:49:48 +02:00
// Ping/Trace functionality
# define PING_RESULT_MAX 4
typedef void ( * PingCallback ) ( uint32_t tag , int16_t snr_out_x4 , int16_t snr_back_x4 , uint32_t rtt_ms ) ;
struct PingResult {
uint32_t tag ;
uint32_t auth_code ;
int16_t snr_out_x4 ; // SNR out (to first hop) × 4
int16_t snr_back_x4 ; // SNR back (from last hop to us) × 4
uint32_t rtt_ms ; // Round-trip time in milliseconds
bool received ;
unsigned long sent_ms ;
} ;
uint32_t sendPing ( const uint8_t * dest_pubkey , uint8_t hash_width = 1 ) ;
void setPingCallback ( PingCallback cb , void * arg ) ;
void clearPingResult ( uint32_t tag ) ;
PingResult * getPingResult ( uint32_t tag ) ;
PingCallback getPingCallback ( ) const { return _ping_callback ; }
AbstractUITask * getUITask ( ) const { return _ui ; }
2025-05-29 13:06:24 -07:00
protected :
2025-06-01 09:25:17 -07:00
float getAirtimeBudgetFactor ( ) const override ;
int getInterferenceThreshold ( ) const override ;
int calcRxDelay ( float score , uint32_t air_time ) const override ;
2026-02-17 20:25:56 +11:00
uint32_t getRetransmitDelay ( const mesh : : Packet * packet ) override ;
uint32_t getDirectRetransmitDelay ( const mesh : : Packet * packet ) override ;
2025-07-16 19:25:28 +10:00
uint8_t getExtraAckTransmitCount ( ) const override ;
2025-11-05 14:34:44 +11:00
bool filterRecvFloodPacket ( mesh : : Packet * packet ) override ;
2026-02-14 15:50:06 +11:00
bool allowPacketForward ( const mesh : : Packet * packet ) override ;
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
bool isRepeatLooped ( const mesh : : Packet * packet ) const ;
// Overhear suppression only makes sense while repeating; gated behind its own
2026-06-21 22:32:59 +02:00
// opt-in pref (Tools > Repeater > Suppress dup).
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
bool wantsOverhearSuppress ( ) const override { return _prefs . client_repeat & & _prefs . repeat_suppress_dup ; }
2025-06-01 09:25:17 -07:00
2026-04-10 17:01:41 +10:00
void sendFloodScoped ( const TransportKey & scope , mesh : : Packet * pkt , uint32_t delay_millis ) ;
2025-11-04 01:21:56 +11:00
void sendFloodScoped ( const ContactInfo & recipient , mesh : : Packet * pkt , uint32_t delay_millis = 0 ) override ;
void sendFloodScoped ( const mesh : : GroupChannel & channel , mesh : : Packet * pkt , uint32_t delay_millis = 0 ) override ;
2025-06-01 09:25:17 -07:00
void logRxRaw ( float snr , float rssi , const uint8_t raw [ ] , int len ) override ;
bool isAutoAddEnabled ( ) const override ;
2026-01-13 00:38:20 +11:00
bool shouldAutoAddContactType ( uint8_t type ) const override ;
bool shouldOverwriteWhenFull ( ) const override ;
2026-03-02 20:41:41 +01:00
uint8_t getAutoAddMaxHops ( ) const override ;
2026-01-13 00:38:20 +11:00
void onContactsFull ( ) override ;
void onContactOverwrite ( const uint8_t * pub_key ) override ;
2025-07-31 13:04:16 +10:00
bool onContactPathRecv ( ContactInfo & from , uint8_t * in_path , uint8_t in_path_len , uint8_t * out_path , uint8_t out_path_len , uint8_t extra_type , uint8_t * extra , uint8_t extra_len ) override ;
2026-06-08 20:37:22 +02:00
void onDiscoveredContact ( ContactInfo & contact , bool is_new , uint8_t path_len , const uint8_t * path ) override ;
void onDiscoveredAdvert ( bool was_flood ) override ;
2025-06-01 09:25:17 -07:00
void onContactPathUpdated ( const ContactInfo & contact ) override ;
2025-09-08 19:22:59 +10:00
ContactInfo * processAck ( const uint8_t * data ) override ;
2025-06-01 09:25:17 -07:00
void queueMessage ( const ContactInfo & from , uint8_t txt_type , mesh : : Packet * pkt , uint32_t sender_timestamp ,
const uint8_t * extra , int extra_len , const char * text ) ;
void onMessageRecv ( const ContactInfo & from , mesh : : Packet * pkt , uint32_t sender_timestamp ,
const char * text ) override ;
void onCommandDataRecv ( const ContactInfo & from , mesh : : Packet * pkt , uint32_t sender_timestamp ,
const char * text ) override ;
void onSignedMessageRecv ( const ContactInfo & from , mesh : : Packet * pkt , uint32_t sender_timestamp ,
const uint8_t * sender_prefix , const char * text ) override ;
void onChannelMessageRecv ( const mesh : : GroupChannel & channel , mesh : : Packet * pkt , uint32_t timestamp ,
const char * text ) override ;
2026-03-19 09:25:42 +01:00
void onChannelDataRecv ( const mesh : : GroupChannel & channel , mesh : : Packet * pkt , uint16_t data_type ,
2026-03-05 13:23:23 +01:00
const uint8_t * data , size_t data_len ) override ;
2025-06-01 09:25:17 -07:00
uint8_t onContactRequest ( const ContactInfo & contact , uint32_t sender_timestamp , const uint8_t * data ,
uint8_t len , uint8_t * reply ) override ;
void onContactResponse ( const ContactInfo & contact , const uint8_t * data , uint8_t len ) override ;
2025-11-06 22:10:20 +11:00
void onControlDataRecv ( mesh : : Packet * packet ) override ;
2025-06-01 09:25:17 -07:00
void onRawDataRecv ( mesh : : Packet * packet ) override ;
void onTraceRecv ( mesh : : Packet * packet , uint32_t tag , uint32_t auth_code , uint8_t flags ,
const uint8_t * path_snrs , const uint8_t * path_hashes , uint8_t path_len ) override ;
uint32_t calcFloodTimeoutMillisFor ( uint32_t pkt_airtime_millis ) const override ;
uint32_t calcDirectTimeoutMillisFor ( uint32_t pkt_airtime_millis , uint8_t path_len ) const override ;
void onSendTimeout ( ) override ;
2026-06-10 23:30:43 +02:00
void onAckRecv ( mesh : : Packet * packet , uint32_t ack_crc ) override ; // APC: ACK SNR sample
// APC internals (see MyMesh.cpp): one reverse-link SNR sample, a lost-confirmation
// ramp-up, and tracking an originated flood so its echo (or absence) can be scored.
// The heard-echo sampling itself lives in filterRecvFloodPacket() (declared below).
void apcSampleSnr ( float snr ) ;
void apcOnFailure ( ) ;
void apcTrackFloodSend ( const mesh : : Packet * pkt ) ;
2026-06-14 23:33:16 +02:00
void trackRelaySend ( const mesh : : Packet * pkt ) ; // arm the UI relayed-into-mesh tracker
public :
// Seq of the most recently tracked channel send — the UI records it on the
// outgoing history entry so a heard echo (onChannelRelayed) can match it back.
uint32_t lastChannelRelaySeq ( ) const { return _last_relay_seq ; }
private :
2025-05-29 13:06:24 -07:00
2025-06-06 15:30:35 +10:00
// DataStoreHost methods
bool onContactLoaded ( const ContactInfo & contact ) override { return addContact ( contact ) ; }
bool getContactForSave ( uint32_t idx , ContactInfo & contact ) override { return getContactByIdx ( idx , contact ) ; }
bool onChannelLoaded ( uint8_t channel_idx , const ChannelDetails & ch ) override { return setChannel ( channel_idx , ch ) ; }
bool getChannelForSave ( uint8_t channel_idx , ChannelDetails & ch ) override { return getChannel ( channel_idx , ch ) ; }
2025-07-31 13:45:53 +10:00
void clearPendingReqs ( ) {
2026-06-26 10:55:24 +02:00
pending_login = pending_status = pending_telemetry = pending_discovery = pending_req = ui_pending_login = 0 ;
2025-07-31 13:45:53 +10:00
}
2025-11-20 18:55:39 -08:00
public :
2026-06-26 10:55:24 +02:00
// On-device UI login to a room/repeater contact — no phone app required.
// The room server's ACL grants permission per-identity (self_id), not per
// command source, so this reuses the same sendLogin() the BLE CMD_SEND_LOGIN
// path uses; the async result lands in onContactResponse() and is pushed to
// the UI via AbstractUITask::onRoomLoginResult().
2026-07-24 17:56:46 +02:00
bool sendRoomLogin ( const ContactInfo & contact , const char * password , uint32_t & est_timeout ) {
2026-06-26 10:55:24 +02:00
if ( sendLogin ( contact , password , est_timeout ) = = MSG_SEND_FAILED ) return false ;
clearPendingReqs ( ) ;
memcpy ( & ui_pending_login , contact . id . pub_key , 4 ) ; // match this in onContactResponse()
return true ;
}
2026-07-10 16:01:14 +02:00
// On-device UI logout: drops the local keep-alive tracking (mirrors the app's
// CMD_LOGOUT) and forgets the saved password, so the next room open prompts
// for credentials again instead of silently re-using them. No packet is sent
// to the server -- the room ACL has no session state to tear down, this is
// purely local "forget this login" bookkeeping.
void logoutRoom ( const uint8_t * pub_key ) {
stopConnection ( pub_key ) ;
forgetRoomPassword ( pub_key ) ;
}
2026-06-26 10:55:24 +02:00
// On-device-saved room/repeater login passwords, persisted to flash (own
// small file, independent of /contacts3) so a room that's already been
// logged into doesn't need its password retyped after reboot.
bool saveRoomPassword ( const uint8_t * pub_key , const char * password ) ;
bool getRoomPassword ( const uint8_t * pub_key , char * out_password , uint8_t max_len ) ;
void forgetRoomPassword ( const uint8_t * pub_key ) ;
feat(ui): on-device channel management, remote admin tool, per-language keyboards
Messages:
- Add/edit/delete channels on-device (new ChannelsView, owned by the renamed
MessagesScreen — was QuickMsgScreen, whose name no longer matched its scope).
Channel secret entry supports a typed passphrase (SHA-256'd, same primitive
the library already uses for the routing hash) or a raw 32-hex-char key.
- MyMesh::setChannelLocal() factors out the setChannel/saveChannels/
onChannelRemoved sequence previously duplicated across the two
CMD_SET_CHANNEL branches, shared now by the BLE and on-device paths.
Tools > Admin (new):
- Log into a repeater/room server's admin account and send CLI commands,
the on-device equivalent of the app's repeater-admin feature.
- Commands are organised into category tabs (System/Radio/Routing/Actions)
with common get/set fields (name, radio profile, tx power, repeat, advert
intervals, ...) plus a free-text "Custom command..." fallback for anything
else. A field row fetches the current value, opens it pre-filled for
editing, and sends the change — falling back to a blank editor if the
fetch fails or times out.
- The admin password persists and self-heals exactly like room logins in
Messages: saved on a confirmed admin-level login, forgotten on a failed
one, left alone if merely under-privileged.
- New MyMesh::sendAdminCommand()/AbstractUITask::onAdminReply() plumbing so
a reply reaches the UI without touching the existing BLE/app CLI-terminal
path (queueMessage's should_display gate is untouched).
Shared TabBar.h extracted from NearbyScreen/BotScreen's independently
duplicated tab-carousel rendering (now a third consumer via Admin) — also
fixes neighbouring tabs vanishing outright when they didn't fully fit;
they now truncate with an ellipsis instead.
Keyboard: the combined "Ext.Latin" alphabet split into 8 separate,
linguistically complete per-language keyboards (Polish, Czech, Slovak,
German, French, Spanish, Portuguese, Nordic), and fixed an OLED-only bug
where tall accented glyphs overlapped the keyboard's separator line
(SH1106's Lemon-font ascent constant was 2-3px short for them).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-12 19:40:53 +02:00
// On-device channel add/edit/delete (Messages > Channels). Shares the exact
// setChannel + saveChannels + onChannelRemoved-cleanup sequence the
// CMD_SET_CHANNEL BLE handler already performs, so both paths stay in sync.
bool setChannelLocal ( uint8_t idx , const ChannelDetails & ch ) ;
// On-device "remote admin" (Tools > Admin): send a CLI command to a node
// you're logged into with admin permission (see ClientACL::isAdmin()). The
// reply is a text frame (TXT_TYPE_CLI_DATA) delivered via onCommandDataRecv();
// this just tracks which contact's reply AdminScreen is currently waiting on.
bool sendAdminCommand ( const ContactInfo & contact , const char * cmd_text , uint32_t & est_timeout ) {
if ( sendCommandData ( contact , rtc_clock . getCurrentTime ( ) , 0 , cmd_text , est_timeout ) = = MSG_SEND_FAILED )
return false ;
memcpy ( & ui_pending_admin_reply , contact . id . pub_key , 4 ) ;
return true ;
}
2025-11-20 18:55:39 -08:00
void savePrefs ( ) { _store - > savePrefs ( _prefs , sensors . node_lat , sensors . node_lon ) ; }
2026-05-10 18:23:25 +02:00
void saveRTCTime ( ) { _store - > saveRTCTime ( ) ; }
2026-05-26 07:49:17 +02:00
DataStore * getDataStore ( ) const { return _store ; }
2026-06-10 23:30:43 +02:00
void applyApc ( ) ; // (re)initialise Adaptive Power Control from prefs
feat(repeater): politeness controls, dedicated radio profile, and diagnostics
Settings > Radio gains a repeater toggle, 16 community-suggested radio
presets plus manual Freq/SF/BW/CR tuning (digit-by-digit Freq editor),
4 persisted user preset slots, and the packet pool bumped 16->32 so
queued retransmits stop starving incoming-packet allocation while
relaying.
Four opt-in politeness knobs (skip-advert, max-hops, yield, min-SNR),
all flood-only and off by default, plus overhear suppression that
cancels a queued retransmit if a peer relays the same packet first.
Adaptive Power Control is suppressed while relaying (pins TX power to
the ceiling) and duty-cycle RX is forced off, since a repeater needs
to hear and relay at consistent power.
A dedicated Tools > Repeater screen consolidates the toggle, the
politeness knobs, and live forwarding stats, plus an optional "Custom"
radio profile — a dedicated frequency/SF/BW/CR for relaying, separate
from the companion's own network, band-matched to the companion
frequency by default and used everywhere a radio change can happen
(boot, on-device toggle, app-driven CMD_SET_RADIO_PARAMS, Settings
radio edits) so the device never silently falls back to the wrong
params mid-relay.
Diagnostics gains the actually-forwarded packet count, real heap-free
via mallinfo(), a reset-counters popup, and hardened loop-detect
bounds. Also: a frequency-floor fix and a float-equality preset-match
fix in the repeater profile logic, deduped BW-table/valCol/profile-
seeding helpers shared between Settings and the Repeater screen, and
a build.sh fix tolerating control characters in `pio project config`'s
JSON dump.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-20 09:07:00 +02:00
// Adaptive Power Control is suppressed while repeating: a repeater wants full,
// consistent TX power for relay reach, and its feedback sources (own ACKs /
// own flood echoes) don't fire on forwarded traffic anyway. applyApc() then
// pins power to the ceiling.
bool apcActive ( ) const { return _prefs . tx_apc & & ! _prefs . client_repeat ; }
2026-06-21 23:09:27 +02:00
// True when the optional repeater radio profile is a valid LoRa config for
// this radio (freq bounds come from the chip's own validated range).
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
bool repeaterProfileValid ( ) const {
2026-06-21 23:09:27 +02:00
float lo , hi ; radio_driver . getFreqBounds ( lo , hi ) ;
return isValidRepeaterProfile ( _prefs . repeater_freq , _prefs . repeater_bw , _prefs . repeater_sf , _prefs . repeater_cr , lo , 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
}
// Load the radio with the correct params for the current mode: the repeater
// profile when relaying with a valid dedicated profile, otherwise the
// companion's own params. Single source of truth, used at boot and whenever
// the repeater toggle / network / profile changes.
void applyRepeaterRadio ( ) ;
2025-11-20 18:55:39 -08:00
feat: NearbyScreen, configurable clock dashboard, memory optimizations
- Add NearbyScreen: GPS contact list with haversine distance sort,
type filter (ALL/Chat/Rpt/Room/Snsr), detail view with ping (RTT via
ACK poll), node scan via advert(), context menu
- Add DashboardConfigScreen: configure up to 3 data fields on the clock
page (Battery, Temp, Humidity, Pressure, GPS, Altitude, Lux, CO2,
Contacts); entered via long-press ENTER on clock page
- Rework HP_CLOCK layout: full-screen clock (size 2 at y=0), date at
y=19, separator at y=28, three data field rows at y=31/41/51;
hide node name/battery indicator on clock page
- Persist dashboard_fields[3] in NodePrefs and DataStore
- Add isAckPending() to MyMesh for NearbyScreen ping detection
- Wire NearbyScreen into ToolsScreen and UITask
- Reduce NearbyScreen entry buffer from MAX_CONTACTS(100) to 32,
saving ~3.3 KB RAM
- Switch haversine/bearing math from double to float (sinf/cosf/atan2f),
eliminating double libm and saving ~5-10 KB flash
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 11:28:36 +02:00
bool isAckPending ( uint32_t expected_ack ) const {
for ( int i = 0 ; i < EXPECTED_ACK_TABLE_SIZE ; i + + )
if ( expected_ack_table [ i ] . ack = = expected_ack ) return true ;
return false ;
}
2026-05-14 15:57:00 +02:00
2026-03-13 15:12:59 -05:00
# if ENV_INCLUDE_GPS == 1
void applyGpsPrefs ( ) {
sensors . setSettingValue ( " gps " , _prefs . gps_enabled ? " 1 " : " 0 " ) ;
2026-03-25 16:26:51 +11:00
if ( _prefs . gps_interval > 0 ) {
char interval_str [ 12 ] ; // Max: 24 hours = 86400 seconds (5 digits + null)
sprintf ( interval_str , " %u " , _prefs . gps_interval ) ;
sensors . setSettingValue ( " gps_interval " , interval_str ) ;
}
2026-03-13 15:12:59 -05:00
}
# endif
2026-04-10 10:45:26 +07:00
// To check if there is pending work
bool hasPendingWork ( ) const ;
2026-03-13 15:12:59 -05:00
feat(admin): typed Radio/Routing field editors; pre-release audit fixes
Tools > Admin: split the Radio tab's single "f,bw,sf,cr" comma-string
field (free-text keyboard) into 4 independent, type-appropriate rows --
Frequency (same digit-cursor DigitEditor Settings/Repeater use locally),
Bandwidth/Spreading factor/Coding rate (discrete-set LEFT/RIGHT stepping),
plus TX power and the 3 Routing numeric fields as number steppers and
Repeat as an ON/OFF toggle. Edits happen locally (zero mesh traffic per
keystroke); only Enter sends one combined `set`, Cancel sends nothing.
Full pre-release audit of all 16 commits since v1.22 (5 parallel focus
areas: display/font, keyboard/messages, Nodes/Admin, Bot/channels,
UITask+NodePrefs schema) turned up and fixed:
- Admin: fetched FK_NUMBER values weren't clamped to the field's range,
so a value already out-of-range could get stuck unreachable; fixed
commit-time float format noise (%.6f -> %.3f); Cancel/failed-login
always returned to the Nodes picker even when Admin was opened
directly from a node's Hold-Enter action -- now returns to wherever
it was actually opened from (AdminScreen::_from_picker).
- Keyboard: one-shot Shift was consumed after the *first* T9 multi-tap,
so cycling to the 2nd/3rd candidate always came out lowercase --
fixed by caching the cycle's caps state (t9_caps).
- Messages: history is numbered newest-first, so a message arriving
while scrolled up to an older one silently relabeled the view onto a
different message -- selection now shifts with the insert.
- Channels: onChannelRemoved() didn't clear ch_notif_override/
ch_notif_muted/ch_fav_bitmask despite its own contract comment
requiring it (now far more reachable via the on-device Delete);
an all-zero hex secret silently self-deleted the channel it was
just saved into (collides with the empty-slot sentinel) -- rejected.
- Room login: isRoomLoggedIn() indexed the login-tracking ring
directly instead of via its head offset, silently wrong once the
ring wraps (8+ rooms/session) -- the new on-device Logout depends
on this being right.
- Font/display: removed the now fully-inert Settings > Display > Font
toggle and the dead LemonFont.h (retired by the earlier misc-fixed
font unification, zero remaining includes); fixed a copy-pasted
"5x7" comment (font is 6x9), two meaningless dead ternaries, and an
OLED/e-ink inconsistency in the undefined-glyph fallback box offset.
- AdminField's `kind`/bounds fields no longer rely on default member
initializers inside aggregate-init: this toolchain's actual nRF52
build (unlike env:native) has no explicit -std= override, so it
predates C++14's aggregate-with-default-member-initializer rule.
Given an explicit constructor instead -- portable regardless of
standard, all existing field-table literals unchanged.
Docs updated to match: tools_screen.md (Diagnostics as a 3-tab
carousel, was documented as one flat screen), message_screen.md
(chat bubbles, newest-at-bottom, cursor mode, secret validation),
settings_screen.md (dropped the dead Font row), solo_ui_framework.md
(header menu_hint signatures, icon priority-drop, KeyboardWidget's
T9/alphabets/cursor-mode). release-notes.md gains the v1.23 section
covering all of the above plus the other 15 commits since v1.22.
Build-verified on WioTrackerL1_companion_solo_dual.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-17 19:38:51 +02:00
// Number of auto-replies sent since boot (DM + channel + room). Shown on BotScreen.
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
uint16_t botReplyCount ( ) const { return _bot_reply_count ; }
2025-05-29 13:06:24 -07:00
private :
feat(bot): room-server target, tab-carousel UI, per-target independence
Auto-Reply Bot gains a third target (room servers, alongside DM and
channel), new {name}/{hops} reply placeholders, and a DM all/favourites
allow-list. BotScreen is redesigned as a circular tab carousel (Direct /
Channel / Room / Other, same interaction as Nearby Nodes' filter tabs)
instead of one long scrolling list, which also exposed and fixed a
leftover coupling where Channel/Room trigger-replies and their !command
handling secretly depended on the DM tab's Enable/Commands toggles —
each target's Enable and Commands are now fully independent.
- NodePrefs: bot_room_enabled/prefix/trigger/reply, bot_dm_scope,
bot_commands_ch/bot_commands_room (bot_commands_enabled repurposed as
DM-only). SCHEMA_SENTINEL 0xC0DE001D -> 0xC0DE001F; sizeof unchanged
at 2712 (new bytes absorbed existing padding, verified via a
standalone host compile + offsetof check).
- DataStore: persists all new fields; seeds bot_commands_ch/room from
the old shared bot_commands_enabled on upgrade so existing
channel/room command behaviour isn't silently lost.
- MyMeshBot: tryBotReplyRoom/tryBotRoomCommand mirror the channel bot's
shape but post via sendMessage (room relays to members itself);
requires an existing login session with that room, same as a manual
post would. botDmSenderAllowed() gates DM trigger-reply/commands on
the favourites bit when bot_dm_scope=Fav.
- MsgExpand: {name}/{hops} as optional trailing params (default
nullptr/-1, no existing caller affected) — deliberately not exposed
on the general compose keyboard, only on bot Reply fields.
- QuickMsgScreen/UITask: room-target picker (mirrors the channel
picker), routing through the existing room-login prompt when there's
no saved password yet.
- BotScreen: tab carousel (LEFT/RIGHT switches tabs, UP/DOWN moves
rows, Enter is now the only way to change a value); Enable split out
of Channel/Room's combo row; Quiet Hours gained a stepper sub-mode;
Commands moved from a shared toggle into each tab.
- docs/tools_screen.md updated for the new tab layout and behaviour.
Not build-verified — no PlatformIO toolchain in this environment.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-10 20:16:59 +02:00
void tryBotReplyDM ( const ContactInfo & from , const char * text , uint8_t hops ) ;
void tryBotReplyChannel ( uint8_t channel_idx , const char * text , uint8_t hops ) ;
void tryBotReplyRoom ( const ContactInfo & from , const uint8_t * sender_prefix , const char * text , uint8_t hops ) ;
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
bool tryBotCommand ( const ContactInfo & from , const char * text , uint8_t hops ) ; // DM commands
bool tryBotChannelCommand ( uint8_t channel_idx , const char * text , uint8_t hops ) ; // channel commands
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
bool tryBotRoomCommand ( const ContactInfo & from , const uint8_t * sender_prefix , const char * text , uint8_t hops ) ; // room commands
2026-07-21 20:30:03 +02:00
bool botCommandReply ( const char * cmd , const char * arg , bool actions_allowed , uint8_t hops , uint32_t ts , char * out , int out_len , const char * sender_name ) ; // one command → reply text
int botScanCommands ( const char * body , uint8_t hops , uint32_t ts , char * out , int out_len , const char * sender_name , bool actions_allowed ) ; // scan "!word"s → combined reply, returns 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
bool botTriggerMatches ( const char * trigger , const char * body , bool allow_wildcard ) const ;
bool botInQuietHours ( ) const ; // true when auto-replies should stay silent
bool botDmAllowed ( const uint8_t * pubkey ) ; // per-contact DM throttle: ok to reply?
void botDmRecord ( const uint8_t * pubkey ) ; // remember we just replied to this contact
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
// DM allow-list gate (bot_dm_scope): true unless scope is favourites-only
// and `from` isn't starred. Shared by the DM trigger-reply and command paths.
bool botDmSenderAllowed ( const ContactInfo & from ) const ;
// Resolves a room post's signed author prefix to a display name for {name};
// falls back to a generic label when the poster isn't a known contact.
void botRoomSenderName ( const uint8_t * sender_prefix , char * out , int out_len ) ;
2026-07-17 21:02:33 +02:00
// Splits a channel message's leading "SenderName: " text convention off its
// body, for the {name} placeholder and to match triggers/commands against
// the body only. `*msg_out` points into `text` (no copy); `sender_name`
// defaults to "someone" if there's no ": " separator.
void botChannelSenderSplit ( const char * text , char * sender_name , int sender_name_len , const char * * msg_out ) ;
2026-05-12 14:51:19 +02:00
2025-06-01 09:25:17 -07:00
void writeOKFrame ( ) ;
void writeErrFrame ( uint8_t err_code ) ;
void writeDisabledFrame ( ) ;
void writeContactRespFrame ( uint8_t code , const ContactInfo & contact ) ;
2025-07-25 22:59:48 +10:00
void updateContactFromFrame ( ContactInfo & contact , uint32_t & last_mod , const uint8_t * frame , int len ) ;
2025-06-01 09:25:17 -07:00
void addToOfflineQueue ( const uint8_t frame [ ] , int len ) ;
int getFromOfflineQueue ( uint8_t frame [ ] ) ;
2025-06-06 21:35:54 +10:00
int getBlobByKey ( const uint8_t key [ ] , int key_len , uint8_t dest_buf [ ] ) override {
return _store - > getBlobByKey ( key , key_len , dest_buf ) ;
}
bool putBlobByKey ( const uint8_t key [ ] , int key_len , const uint8_t src_buf [ ] , int len ) override {
return _store - > putBlobByKey ( key , key_len , src_buf , len ) ;
}
2025-05-29 13:06:24 -07:00
2025-06-04 21:33:48 +10:00
void checkCLIRescueCmd ( ) ;
void checkSerialInterface ( ) ;
2026-02-14 15:50:06 +11:00
bool isValidClientRepeatFreq ( uint32_t f ) const ;
2026-05-26 21:26:07 +02:00
# ifdef ENABLE_SCREENSHOT
void handleScreenshotRequest ( ) ;
2026-05-28 22:58:26 +02:00
void sendScreenshotResponse ( DisplayDriver * display , const uint8_t * buffer , uint16_t bufferSize ) ;
2026-05-26 21:26:07 +02:00
# endif
2025-06-06 15:30:35 +10:00
// helpers, short-cuts
void saveChannels ( ) { _store - > saveChannels ( this ) ; }
2026-06-01 16:49:31 +10:00
void saveContacts ( ) ;
2025-06-04 21:33:48 +10:00
2025-06-06 15:30:35 +10:00
DataStore * _store ;
2025-06-01 09:25:17 -07:00
NodePrefs _prefs ;
uint32_t pending_login ;
2026-06-26 10:55:24 +02:00
uint32_t ui_pending_login ; // like pending_login, but triggered by on-device UI instead of BLE/USB app
2026-06-26 11:34:08 +02:00
char pending_login_pw [ 16 ] ; // password of the in-flight app/USB login, persisted on success for ADV_TYPE_ROOM (see saveRoomPassword)
feat(ui): on-device channel management, remote admin tool, per-language keyboards
Messages:
- Add/edit/delete channels on-device (new ChannelsView, owned by the renamed
MessagesScreen — was QuickMsgScreen, whose name no longer matched its scope).
Channel secret entry supports a typed passphrase (SHA-256'd, same primitive
the library already uses for the routing hash) or a raw 32-hex-char key.
- MyMesh::setChannelLocal() factors out the setChannel/saveChannels/
onChannelRemoved sequence previously duplicated across the two
CMD_SET_CHANNEL branches, shared now by the BLE and on-device paths.
Tools > Admin (new):
- Log into a repeater/room server's admin account and send CLI commands,
the on-device equivalent of the app's repeater-admin feature.
- Commands are organised into category tabs (System/Radio/Routing/Actions)
with common get/set fields (name, radio profile, tx power, repeat, advert
intervals, ...) plus a free-text "Custom command..." fallback for anything
else. A field row fetches the current value, opens it pre-filled for
editing, and sends the change — falling back to a blank editor if the
fetch fails or times out.
- The admin password persists and self-heals exactly like room logins in
Messages: saved on a confirmed admin-level login, forgotten on a failed
one, left alone if merely under-privileged.
- New MyMesh::sendAdminCommand()/AbstractUITask::onAdminReply() plumbing so
a reply reaches the UI without touching the existing BLE/app CLI-terminal
path (queueMessage's should_display gate is untouched).
Shared TabBar.h extracted from NearbyScreen/BotScreen's independently
duplicated tab-carousel rendering (now a third consumer via Admin) — also
fixes neighbouring tabs vanishing outright when they didn't fully fit;
they now truncate with an ellipsis instead.
Keyboard: the combined "Ext.Latin" alphabet split into 8 separate,
linguistically complete per-language keyboards (Polish, Czech, Slovak,
German, French, Spanish, Portuguese, Nordic), and fixed an OLED-only bug
where tall accented glyphs overlapped the keyboard's separator line
(SH1106's Lemon-font ascent constant was 2-3px short for them).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-12 19:40:53 +02:00
uint32_t ui_pending_admin_reply ; // pub_key prefix of the contact AdminScreen's sendAdminCommand() is awaiting a CLI reply from
2025-06-01 09:25:17 -07:00
uint32_t pending_status ;
2025-07-31 13:04:16 +10:00
uint32_t pending_telemetry , pending_discovery ; // pending _TELEMETRY_REQ
2025-07-14 12:46:51 +10:00
uint32_t pending_req ; // pending _BINARY_REQ
2025-06-01 09:25:17 -07:00
BaseSerialInterface * _serial ;
2025-08-16 20:04:54 +10:00
AbstractUITask * _ui ;
2025-06-01 09:25:17 -07:00
ContactsIterator _iter ;
uint32_t _iter_filter_since ;
uint32_t _most_recent_lastmod ;
uint32_t _active_ble_pin ;
bool _iter_started ;
2025-06-04 21:33:48 +10:00
bool _cli_rescue ;
2026-06-10 23:30:43 +02:00
int8_t _apc_cur_dbm ; // APC current TX power (≤ tx_power_dbm ceiling) when tx_apc on
float _apc_margin_ewma ; // APC smoothed reverse-link SNR margin above the SF demod floor
uint8_t _apc_fail_count ; // APC consecutive lost-confirmation count (graduated ramp-up)
uint8_t _apc_flood_hash [ MAX_HASH_SIZE ] ; // hash of the channel/flood send awaiting a repeater echo
uint16_t _apc_flood_len ; // its payload length — cheap pre-filter before hashing
uint32_t _apc_flood_deadline ; // echo-wait deadline for that send
bool _apc_flood_pending ; // a tracked flood send is awaiting its echo
2026-06-14 23:33:16 +02:00
// UI "relayed into mesh" tracker for channel sends — independent of APC. A small
// ring so a quick burst of channel sends are each tracked (not just the latest).
// Hashing on receive only runs while at least one slot is pending, so the hot
// flood-recv path is untouched otherwise.
static const int RELAY_RING = 4 ;
struct RelaySlot {
uint8_t hash [ MAX_HASH_SIZE ] ;
uint16_t len ;
uint32_t deadline ;
uint32_t seq ;
bool pending ;
} ;
RelaySlot _relay [ RELAY_RING ] ;
int _relay_head ; // next ring slot to overwrite
int _relay_active ; // number of slots currently pending (cheap gate)
uint32_t _relay_seq ; // monotonic id counter for tracked sends
uint32_t _last_relay_seq ; // seq of the most recent tracked send (for the UI to record)
2026-05-07 16:51:03 +10:00
bool send_unscoped ; // force un-scoped flood (instead of using send_scope)
2025-06-04 21:33:48 +10:00
char cli_command [ 80 ] ;
2025-06-01 09:25:17 -07:00
uint8_t app_target_ver ;
uint8_t * sign_data ;
uint32_t sign_data_len ;
unsigned long dirty_contacts_expiry ;
2026-05-16 23:26:31 +02:00
unsigned long _bot_last_ch_reply_ms ;
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
unsigned long _bot_last_room_reply_ms ;
2026-05-14 16:08:33 +02:00
unsigned long _next_auto_advert_ms ;
2025-06-01 09:25:17 -07:00
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
// Per-contact DM reply throttle: a small ring of the most recent recipients so
// one chatty contact can't be spammed while a different sender is still served.
struct BotReplyLog { uint8_t key [ 4 ] ; unsigned long t_ms ; bool used ; } ;
static const int BOT_DM_LOG_SIZE = 8 ;
BotReplyLog _bot_dm_log [ BOT_DM_LOG_SIZE ] ;
uint16_t _bot_reply_count ; // total auto-replies sent since boot
2026-04-13 23:11:21 +10:00
TransportKey send_scope ;
2025-11-04 01:21:56 +11:00
2025-06-01 09:25:17 -07:00
uint8_t cmd_frame [ MAX_FRAME_SIZE + 1 ] ;
uint8_t out_frame [ MAX_FRAME_SIZE + 1 ] ;
CayenneLPP telemetry ;
struct Frame {
uint8_t len ;
uint8_t buf [ MAX_FRAME_SIZE ] ;
2025-09-22 15:01:28 +10:00
bool isChannelMsg ( ) const ;
2025-06-01 09:25:17 -07:00
} ;
int offline_queue_len ;
Frame offline_queue [ OFFLINE_QUEUE_SIZE ] ;
struct AckTableEntry {
unsigned long msg_sent ;
uint32_t ack ;
2025-09-08 19:22:59 +10:00
ContactInfo * contact ;
2025-06-01 09:25:17 -07:00
} ;
AckTableEntry expected_ack_table [ EXPECTED_ACK_TABLE_SIZE ] ; // circular table
int next_ack_idx ;
2025-06-18 16:02:09 +10:00
# define ADVERT_PATH_TABLE_SIZE 16
AdvertPath advert_paths [ ADVERT_PATH_TABLE_SIZE ] ; // circular table
2026-05-21 00:13:36 +02:00
2026-05-21 07:57:21 +02:00
# define DISCOVER_RESULTS_MAX 16
DiscoverResult _discover_results [ DISCOVER_RESULTS_MAX ] ;
int _discover_count ;
2026-05-21 00:13:36 +02:00
uint32_t _pending_node_discover_tag ;
unsigned long _pending_node_discover_until ;
2026-05-29 21:49:48 +02:00
2026-07-05 17:00:52 +02:00
// Dedup for NODE_DISCOVER_RESP copies heard more than once: the responder's
// zero-hop direct copy and a re-flooded copy relayed by another repeater
// carry different packet hashes, so the mesh duplicate filter passes both.
// Keyed by (tag, responder pubkey prefix) with a short expiry — covers both
// the standalone on-device scan and responses forwarded to the app (which
// would otherwise list the same repeater twice).
# define DISCOVER_SEEN_MAX 16
struct DiscoverSeen { uint32_t tag ; uint8_t pk [ 6 ] ; unsigned long until ; } ;
DiscoverSeen _disc_seen [ DISCOVER_SEEN_MAX ] ;
uint8_t _disc_seen_head ;
bool isDupDiscoverResp ( uint32_t tag , const uint8_t * pub_key ) ; // records when new
2026-05-29 21:49:48 +02:00
// ── Ping/Trace state ──────────────────────────────────────────────────────
PingResult _ping_results [ PING_RESULT_MAX ] ;
PingCallback _ping_callback ;
void * _ping_callback_arg ;
2025-05-29 13:06:24 -07:00
} ;
extern MyMesh the_mesh ;