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().
|
|
|
|
|
|
bool sendRoomLogin(const ContactInfo& contact, const char* password) {
|
|
|
|
|
|
uint32_t est_timeout;
|
|
|
|
|
|
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);
|
|
|
|
|
|
|
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(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
|
|
|
|
// Number of auto-replies sent since boot (DM + channel). Shown on BotScreen.
|
|
|
|
|
|
uint16_t botReplyCount() const { return _bot_reply_count; }
|
|
|
|
|
|
|
2025-05-29 13:06:24 -07:00
|
|
|
|
private:
|
2026-05-12 14:51:19 +02:00
|
|
|
|
void tryBotReplyDM(const ContactInfo& from, const char* text);
|
|
|
|
|
|
void tryBotReplyChannel(uint8_t channel_idx, const char* text);
|
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
|
|
|
|
|
|
bool botCommandReply(const char* cmd, uint8_t hops, uint32_t ts, char* out, int out_len); // one command → reply text
|
|
|
|
|
|
int botScanCommands(const char* body, uint8_t hops, uint32_t ts, char* out, int out_len); // scan "!word"s → combined reply, returns count
|
|
|
|
|
|
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-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)
|
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;
|
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;
|