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-08-11 17:25:35 +02:00
#define FIRMWARE_BUILD_DATE "11 Aug 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
2026-08-11 17:25:35 +02:00
#define FIRMWARE_VERSION "v1.17-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 ------------------------------------- */
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-05-29 13:06:24 -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 ;
2026-06-03 10:13:10 +02:00
bool getCADEnabled () const override ;
2025-06-01 09:25:17 -07:00
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 ;
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).
2026-06-20 09:07:00 +02:00
bool wantsOverhearSuppress () const override { return _prefs . client_repeat && _prefs . repeat_suppress_dup ; }
2025-05-29 13:06:24 -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 );
2025-05-29 13:06:24 -07:00
2025-06-01 09:25:17 -07:00
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-05-29 13:06:24 -07:00
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 ;
2025-05-29 13:06:24 -07:00
2025-06-01 09:25:17 -07:00
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-24 18:21:31 +02:00
// Called by a screen that gave up waiting on its own sendRoomLogin() (Cancel
// or a timeout) so a reply that arrives after doesn't get misrouted:
// UITask::onRoomLoginResult() dispatches by whichever screen is *currently*
// showing, not by who actually sent the request, so a late reply landing
// after the requester navigated away is delivered to whatever screen the
// user is on instead -- which then persists its own unrelated login state
// (e.g. a different screen's _login_pw) as if it were that reply's answer.
// Pubkey-guarded so this is a no-op if a newer request (this screen retrying,
// or a different screen entirely) has since overwritten ui_pending_login.
void cancelUiPendingLogin ( const uint8_t * pub_key ) {
if ( ui_pending_login && memcmp ( & ui_pending_login , pub_key , 4 ) == 0 ) ui_pending_login = 0 ;
}
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 );
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
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).
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 );
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
2026-05-14 11:28:36 +02:00
bool isAckPending ( uint32_t expected_ack ) const {
2026-07-27 22:05:52 +02:00
if ( expected_ack == 0 ) return false ; // 0 marks an empty/cleared slot, not a real ACK
2026-05-14 11:28:36 +02:00
for ( int i = 0 ; i < EXPECTED_ACK_TABLE_SIZE ; i ++ )
2026-07-27 22:05:52 +02:00
if ( expected_ack_table [ i ]. ack != 0 && expected_ack_table [ i ]. ack == expected_ack ) return true ;
2026-05-14 11:28:36 +02:00
return false ;
}
2026-03-13 15:12:59 -05:00
#if ENV_INCLUDE_GPS == 1
void applyGpsPrefs () {
sensors . setSettingValue ( "gps" , _prefs . gps_enabled ? "1" : "0" );
2026-08-07 22:14:19 +02:00
// gps_interval doubles as the GPS duty-cycle sleep window in seconds
// (0 = disabled, GPS stays continuous) -- see EnvironmentSensorManager::
// gpsDutyCycleLoop(). Max: 24 hours = 86400 seconds (5 digits + null).
char interval_str [ 12 ];
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
2026-07-17 19:38:51 +02:00
// Number of auto-replies sent since boot (DM + channel + room). Shown on BotScreen.
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 :
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 );
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
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-24 23:51:31 +02:00
bool botCommandReply ( const char * cmd , const char * arg , const char * arg2 , bool actions_allowed , uint8_t hops , uint32_t ts , char * out , int out_len , const char * sender_name ); // one command → reply text
2026-07-21 20:30:03 +02:00
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
2026-07-24 17:57:05 +02:00
// !gps fix -- single-shot "wait for a stabilised GPS fix, then push a follow-up
// message" action. botCommandReply() only sets _locfix_requested (it doesn't know
// the destination); the tryBot*Command() wrappers call startLocFix() with the
// destination they each already have, but only once the immediate ack actually
// sent (so a throttled/suppressed ack never starts a fix nobody will hear about).
void tickLocFix (); // ticked every loop() while _loc_fix.active
void startLocFix ( uint8_t dest_type , const uint8_t * pub_key , uint8_t channel_idx );
void sendLocFixResult ( const char * msg );
2026-07-24 22:49:17 +02:00
static bool isLocFixReady ( LocationProvider * loc ); // HDOP if the provider has it, else satellite count
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
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)
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-05-29 13:06:24 -07:00
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 ;
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-05-29 13:06:24 -07:00
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-07-24 17:57:05 +02:00
// !gps fix state -- one global slot (one physical GPS): botCommandReply()
// rejects a second request outright while one is active, so this never
// needs to be an array. See tickLocFix()/startLocFix() in MyMeshBot.h.
2026-07-24 22:49:17 +02:00
// Readiness: HDOP (tenths, lower=better) when the provider exposes it --
// 20 == HDOP 2.0, the usual "good fix" cutoff -- else satellite count as a
// cruder fallback (see LocationProvider::getHDOP()'s -1 = "not available").
static const int LOCFIX_MAX_HDOP = 20 ;
static const int LOCFIX_MIN_SATS = 8 ; // readiness threshold (HDOP-less fallback)
2026-07-24 17:57:05 +02:00
static const uint32_t LOCFIX_AVERAGE_MS = 10000 ; // once ready, keep averaging this long
static const uint32_t LOCFIX_TIMEOUT_MS = 90000 ; // hard stop covering both phases
enum { LOCFIX_DEST_CONTACT = 0 , LOCFIX_DEST_CHANNEL = 1 }; // CONTACT covers DM and room alike (both reply via sendMessage)
struct PendingLocFix {
bool active ;
bool gps_was_on ; // restore to this when done, not unconditionally "off"
uint32_t deadline_ms ;
uint32_t averaging_until_ms ; // 0 while still acquiring; set once the sat threshold is first met
double sum_lat , sum_lon ;
int sample_count ;
uint8_t dest_type ;
uint8_t pub_key [ PUB_KEY_SIZE ]; // dest_type == LOCFIX_DEST_CONTACT
uint8_t channel_idx ; // dest_type == LOCFIX_DEST_CHANNEL
};
PendingLocFix _loc_fix ;
bool _locfix_requested ; // transient: set by botCommandReply() when "!gps fix" was
// seen this scan, cleared by the tryBot*Command() wrapper
2026-07-24 23:51:31 +02:00
uint32_t _locfix_requested_timeout_ms ; // "!gps fix [seconds]" override, see startLocFix()
// Deferred bot actions (!gps on|off, !buzz, !advert, !gpio1..4 on|off) --
// botCommandReply() only records what was requested; the actual hardware/
// radio side effect happens in applyPendingBotActions(), called by the
// tryBot*Command() wrappers only once quiet-hours/cooldown/per-contact
// throttle have passed and the ack actually sent. Otherwise those gates
// would only suppress the reply text while the action fired unconditionally
// on every matching message (e.g. !buzz still buzzing during quiet hours,
// or an unthrottled !advert flooding the mesh). Mirrors the _locfix_requested
// pattern above; resetPendingBotActions() is the throttled/aborted-path
// twin of applyPendingBotActions(), used wherever _locfix_requested used to
// be cleared alone.
bool _bot_gps_action_pending ;
bool _bot_gps_action_on ;
int _bot_buzz_action_secs ; // 0 = no !buzz requested this scan
bool _bot_advert_action_pending ;
int8_t _bot_gpio_action [ 4 ]; // per pin: -1 none requested, 0 off, 1 on
void applyPendingBotActions ();
void resetPendingBotActions ();
2026-07-24 17:57:05 +02:00
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 ;
2025-05-29 13:06:24 -07:00
2025-06-01 09:25:17 -07:00
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 ];
2025-05-29 13:06:24 -07:00
2025-06-01 09:25:17 -07:00
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 ;