Files
MeshCore/examples/simple_repeater/MyMesh.h
T
mikecarper 21194b9825 Fix compile and capacity blockers in the full release matrix
Resolve Serial macro and ESP-NOW wrapper compilation errors. Preserve queue, neighbor, advert-history and Room post capacities while using qualified RTC, PSRAM and word-only IRAM storage on constrained ESP32 targets. Reduce Full infrastructure flash through lossless Room web compression, compact crypto and unused exception metadata removal.

Add focused native regressions for storage ownership, exact protocol output, OTA proofs and build policy. Keep physical flash sizes, OTA slots and RAM guard formulas unchanged.
2026-10-24 05:38:23 -07:00

2076 lines
84 KiB
C++

#pragma once
#include <Arduino.h>
#include <new>
#include <Mesh.h>
#include <helpers/PacketFileLoggingConfig.h>
#include <helpers/PowerSavingDiagnostics.h>
#include <helpers/FloodGroupPlainText.h>
#include <helpers/FloodRuleExtensions.h>
#include <helpers/GuestLoginSettings.h>
#include <helpers/LoginHistory.h>
#if defined(ENABLE_OTA)
#include <helpers/ota/OtaContext.h>
#endif
#include <RTClib.h>
#include <CayenneLPP.h>
#include <target.h>
#include <helpers/TelemetryHistoryConfig.h>
#ifndef MESH_ENABLE_TELEMETRY_GPS_HISTORY
// Builds without a GPS provider only collect missing GPS positions. Omit
// their unused decoder and resize CLI to preserve flash for useful history.
#if ENV_INCLUDE_GPS == 1
#define MESH_ENABLE_TELEMETRY_GPS_HISTORY 1
#else
#define MESH_ENABLE_TELEMETRY_GPS_HISTORY 0
#endif
#endif
#ifndef MESH_ENABLE_RECENT_REPEATERS
#define MESH_ENABLE_RECENT_REPEATERS 1
#endif
#ifndef MAX_RECENT_REPEATERS
// Only repeater firmware supplies this RAM-heavy history storage.
#if !MESH_ENABLE_RECENT_REPEATERS
#define MAX_RECENT_REPEATERS 0
#elif defined(CONFIG_IDF_TARGET_ESP32)
// This history is allocated before setup(), when classic ESP32 must also
// retain internal heap for startup, the packet pool, and WiFi. Moving the
// old 24 KiB table off .bss alone does not reduce that heap pressure.
#define MAX_RECENT_REPEATERS 256
#elif defined(ESP32) || defined(ESP32_PLATFORM)
#define MAX_RECENT_REPEATERS 2048
#elif defined(NRF52_PLATFORM)
#define MAX_RECENT_REPEATERS 512
#else
#define MAX_RECENT_REPEATERS 64
#endif
#endif
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
#include <InternalFileSystem.h>
#elif defined(RP2040_PLATFORM)
#include <LittleFS.h>
#elif defined(ESP32)
#include <SPIFFS.h>
using File = fs::File;
#endif
#ifdef WITH_RS232_BRIDGE
#include "helpers/bridges/RS232Bridge.h"
#define WITH_BRIDGE
#endif
#ifdef WITH_ESPNOW_BRIDGE
#include "helpers/bridges/ESPNowBridge.h"
#define WITH_BRIDGE
#endif
#ifdef WITH_UDP_BRIDGE
#include "helpers/bridges/UDPBridge.h"
#include "helpers/UDPBridgeSettings.h"
#endif
#ifdef WITH_MQTT_BRIDGE
#include "helpers/bridges/MQTTBridge.h"
#define WITH_BRIDGE
#include "helpers/esp32/WebConfigServer.h" // defines WITH_WEBCONFIG on ESP32
#endif
#if defined(ESP_PLATFORM) && defined(ADMIN_PASSWORD) && !defined(WEBCONFIG_DISABLED)
#include "helpers/esp32/WebConfigServer.h"
#endif
#ifdef WITH_SNMP
#include "helpers/SNMPAgent.h"
#endif
#ifdef DISPLAY_ACTIVITY_DASHBOARD
#include <helpers/RadioActivityWindow.h>
#endif
#include <helpers/AdvertDataHelpers.h>
#include <helpers/AlertReporter.h>
#include <helpers/ArduinoHelpers.h>
#include <helpers/ClientACL.h>
#include <helpers/CommonCLI.h>
#include <helpers/DeferredCliCommand.h>
#ifndef MESH_ENABLE_HOST_CLI
#define MESH_ENABLE_HOST_CLI 1
#endif
#if MESH_ENABLE_HOST_CLI
#include <helpers/HostCliBridge.h>
#endif
#include <helpers/RemoteCliReplyCache.h>
#include <helpers/ReplayResetCommand.h>
#include <helpers/RemoteCliRequest.h>
#include <helpers/TempRadioReplyBarrier.h>
#include <helpers/TempRadioLeaseDeadline.h>
#if defined(ESP32_PLATFORM) || defined(USER_GPIO_CONTROL)
#include <helpers/UserGpioReplyTracker.h>
#endif
#include <helpers/IdentityStore.h>
#include <helpers/SimpleMeshTables.h>
#include <helpers/StaticPoolPacketManager.h>
#include <helpers/StatsFormatHelper.h>
#include <helpers/LocalCliOutput.h>
#if MESH_ENABLE_TELEMETRY_HISTORY
#include <helpers/ExternalVoltageHistory.h>
#include <helpers/TelemetryHistory.h>
#include <helpers/NoiseFloorHistorySampler.h>
#endif
#include <helpers/TxtDataHelpers.h>
#include <helpers/RegionMap.h>
#include <helpers/RoutingPolicy.h>
#include "RateLimiter.h"
struct RepeaterStats {
uint16_t batt_milli_volts;
uint16_t curr_tx_queue_len;
int16_t noise_floor;
int16_t last_rssi;
uint32_t n_packets_recv;
uint32_t n_packets_sent;
uint32_t total_air_time_secs;
uint32_t total_up_time_secs;
uint32_t n_sent_flood, n_sent_direct;
uint32_t n_recv_flood, n_recv_direct;
uint16_t err_events; // was 'n_full_events'
int16_t last_snr; // x 4
uint16_t n_direct_dups, n_flood_dups;
uint32_t total_rx_air_time_secs;
uint32_t n_recv_errors;
};
struct NeighbourInfo {
mesh::Identity id;
uint32_t advert_timestamp;
uint32_t heard_timestamp;
int8_t snr; // multiplied by 4, user should divide to get float value
#if defined(WITH_MQTT_NEIGHBORS)
int16_t rssi; // dBm from the last packet heard from this neighbour
#endif
};
#ifndef FIRMWARE_BUILD_DATE
#define FIRMWARE_BUILD_DATE "15 Oct 2026"
#endif
#ifndef FIRMWARE_BUILD_EPOCH
#define FIRMWARE_BUILD_EPOCH 0UL
#endif
#ifndef FIRMWARE_VERSION
#define FIRMWARE_VERSION "v1.18.0"
#endif
#define FIRMWARE_ROLE "repeater"
#define PACKET_LOG_FILE "/packet_log"
#ifndef MAX_SCHEDULED_RADIO_SETTINGS_PER_TYPE
#define MAX_SCHEDULED_RADIO_SETTINGS_PER_TYPE 3
#endif
#define MAX_SCHEDULED_RADIO_SETTINGS (MAX_SCHEDULED_RADIO_SETTINGS_PER_TYPE * 2)
#ifndef MESH_ENABLE_FLOOD_RULE_ENGINE
// STM32WL repeater images have only 240 KB of application flash. They keep
// the established dynamic flood.filter table unless a larger target profile
// explicitly opts into the generalized rule parser and persistence format.
#if defined(STM32_PLATFORM)
#define MESH_ENABLE_FLOOD_RULE_ENGINE 0
#else
#define MESH_ENABLE_FLOOD_RULE_ENGINE 1
#endif
#endif
#ifndef FLOOD_PACKET_FILTER_SLOTS
#if MESH_ENABLE_FLOOD_RULE_ENGINE
#define FLOOD_PACKET_FILTER_SLOTS 63
#else
#define FLOOD_PACKET_FILTER_SLOTS 16
#endif
#endif
#define FLOOD_PACKET_FILTER_ANY_TYPE 0xFF
#define FLOOD_PACKET_FILTER_MAX_HOPS 63
#define FLOOD_PACKET_FILTER_SCOPE_NAME_LEN 32
#if defined(ESP32)
#define FLOOD_PACKET_FILTER_BLACKLIST_MAX 255
#else
#define FLOOD_PACKET_FILTER_BLACKLIST_MAX 18
#endif
#define FLOOD_PACKET_FILTER_BLACKLIST_REPLACE_MAX 18
#define FLOOD_PACKET_FILTER_PATH_ID_SIZE 3
#define FLOOD_PACKET_FILTER_PATH_PREFIX_HOPS_MAX 3
#define FLOOD_PACKET_FILTER_PATH_PREFIX_BYTES_MAX \
(FLOOD_PACKET_FILTER_PATH_PREFIX_HOPS_MAX * 3)
#ifndef FLOOD_CHANNEL_SCOPE_SLOTS
#if defined(CONFIG_IDF_TARGET_ESP32)
// The rewrite and require tables share this capacity. Their 255-slot
// default leaves almost no static DRAM headroom in classic ESP32 builds;
// PSRAM cannot hold these inline members of the global MyMesh object.
#define FLOOD_CHANNEL_SCOPE_SLOTS 31
#elif defined(ESP32)
#define FLOOD_CHANNEL_SCOPE_SLOTS 255
#elif defined(STM32_PLATFORM)
#define FLOOD_CHANNEL_SCOPE_SLOTS 15
#else
#define FLOOD_CHANNEL_SCOPE_SLOTS 31
#endif
#endif
#define FLOOD_CHANNEL_SCOPE_TXT_ANY 0
#define FLOOD_CHANNEL_SCOPE_LOGIN_ANY 1
#define FLOOD_CHANNEL_SCOPE_OTHER_ANY 2
#ifndef FLOOD_CHANNEL_DIRECT_SCOPE_SLOTS
// STM32WL repeater images and RAM are both exceptionally tight. Keep one
// reusable regionless target there; other platforms scale with the rule
// table up to the region-map capacity.
#if defined(STM32_PLATFORM)
#define FLOOD_CHANNEL_DIRECT_SCOPE_SLOTS 1
#elif FLOOD_CHANNEL_SCOPE_SLOTS < MAX_REGION_ENTRIES
#define FLOOD_CHANNEL_DIRECT_SCOPE_SLOTS FLOOD_CHANNEL_SCOPE_SLOTS
#else
#define FLOOD_CHANNEL_DIRECT_SCOPE_SLOTS MAX_REGION_ENTRIES
#endif
#endif
#ifndef FLOOD_CHANNEL_SCOPE_REQUIRE_SLOTS
#define FLOOD_CHANNEL_SCOPE_REQUIRE_SLOTS FLOOD_CHANNEL_SCOPE_SLOTS
#endif
#ifndef MESH_ENABLE_FLOOD_GROUP_MODERATION
#define MESH_ENABLE_FLOOD_GROUP_MODERATION 1
#endif
#ifndef FLOOD_GROUP_MODERATION_SLOTS
#define FLOOD_GROUP_MODERATION_SLOTS 16
#endif
#define FLOOD_GROUP_MODERATION_NAME_LEN 32
#define FLOOD_GROUP_MODERATION_PATH_HOPS_MAX 3
#define FLOOD_GROUP_MODERATION_PATH_BYTES_MAX (FLOOD_GROUP_MODERATION_PATH_HOPS_MAX * 3)
#define FLOOD_GROUP_MODERATION_HOPS_ALL 0xFF
#define FLOOD_GROUP_MODERATION_RATE_UNLIMITED 0xFFFF
#ifndef MESH_ENABLE_CLOCK_SYNC
#define MESH_ENABLE_CLOCK_SYNC 1
#endif
#ifndef CLOCK_SYNC_SAMPLE_SLOTS
#define CLOCK_SYNC_SAMPLE_SLOTS 16
#endif
#ifndef CLOCK_SYNC_MESH_DEFAULT_ENABLED
#define CLOCK_SYNC_MESH_DEFAULT_ENABLED 1
#endif
#ifndef CLOCK_SYNC_MESH_EDGE_DEFAULT_ENABLED
#define CLOCK_SYNC_MESH_EDGE_DEFAULT_ENABLED 1
#endif
#define CLOCK_SYNC_REQUIRED_SAMPLES_MIN 3
#define CLOCK_SYNC_REQUIRED_SAMPLES_MAX CLOCK_SYNC_SAMPLE_SLOTS
#define CLOCK_SYNC_REQUIRED_SAMPLES_DEFAULT 9
#define CLOCK_SYNC_PATH_ID_SIZE 8
#define CLOCK_SYNC_STARTUP_DELAY_MILLIS (30ULL * 60ULL * 1000ULL)
#define CLOCK_SYNC_RETRY_INTERVAL_MILLIS (30ULL * 60ULL * 1000ULL)
#define CLOCK_SYNC_RESYNC_INTERVAL_MILLIS (7ULL * 24ULL * 60ULL * 60ULL * 1000ULL)
#define CLOCK_SYNC_SAMPLE_MAX_AGE_MILLIS (2UL * 60UL * 60UL * 1000UL)
#define CLOCK_SYNC_CONSENSUS_WINDOW_SECONDS 600UL
#define CLOCK_SYNC_DRIFT_MIN_SECONDS 30UL
#define CLOCK_SYNC_DRIFT_MAX_SECONDS 86400UL
#define CLOCK_SYNC_MESH_SUPPRESS_NONE 0
#define CLOCK_SYNC_MESH_SUPPRESS_CLI 1
#define CLOCK_SYNC_MESH_SUPPRESS_GPS 2
#define CLOCK_SYNC_MESH_SUPPRESS_INTERNET 3
#define RECENT_REPEATER_MAX_AGE_MILLIS (24UL * 60UL * 60UL * 1000UL)
#define RECENT_REPEATER_SWEEP_INTERVAL_MILLIS (3UL * 60UL * 60UL * 1000UL)
#define RADIO_APPLY_RETRY_INTERVAL_MILLIS 1000UL
#define SCHEDULED_RADIO_CLOCK_CHECKPOINT_SECS 60UL
#if FLOOD_ADVERT_RTC_FAST_SLOTS || FLOOD_ADVERT_RTC_SLOW_SLOTS
mesh::FloodAdvertLimiter::Entry* repeaterRtcAdvertFastBank();
mesh::FloodAdvertLimiter::Entry* repeaterRtcAdvertSlowBank();
#endif
class MyMesh : public mesh::Mesh, public CommonCLICallbacks
#ifdef WITH_WEBCONFIG
, public WebConfigServer::Callbacks
#endif
{
struct ScheduledRadioSetting {
// RAM only: group aligned fields to keep each schedule within 32 bytes.
uint64_t hard_end_uptime_millis;
float freq;
float bw;
uint32_t start_time;
uint32_t end_time;
uint16_t preamble = 0;
uint8_t sf;
uint8_t cr;
bool active;
bool temporary;
bool started;
};
static_assert(sizeof(ScheduledRadioSetting) <= 32,
"Scheduled radio rows must retain their compact RAM layout");
static_assert(alignof(ScheduledRadioSetting) >= alignof(uint64_t),
"Scheduled radio uptime deadlines must remain naturally aligned");
FILESYSTEM* _fs;
#ifndef PORTABLE_MQTT_OBSERVER
#if FLOOD_ADVERT_RTC_FAST_SLOTS || FLOOD_ADVERT_RTC_SLOW_SLOTS
mesh::StaticFloodAdvertLimiter<> flood_advert_limiter{
repeaterRtcAdvertFastBank(), repeaterRtcAdvertSlowBank()};
#else
mesh::StaticFloodAdvertLimiter<> flood_advert_limiter;
#endif
mesh::FloodAdvertLimiter* getFloodAdvertLimiter() override { return &flood_advert_limiter; }
#endif
#if defined(WITH_WEBCONFIG) || defined(ETHERNET_ENABLED)
#ifdef NRF52_PLATFORM
mesh::LocalCliOutput<File> _local_cli_output{File(InternalFS)};
#else
mesh::LocalCliOutput<File> _local_cli_output;
#endif
Stream* _command_output = nullptr;
Stream* _web_terminal = nullptr;
#endif
#if MESH_USB_CONSOLE_COOPERATIVE
#if defined(NRF52_PLATFORM)
File serial_log_dump{InternalFS};
#else
File serial_log_dump;
#endif
size_t serial_log_remaining = 0;
size_t serial_log_pending_size = 0;
char serial_log_pending[640];
bool serial_log_active = false;
bool serial_log_eof_pending = false;
bool serial_log_skip_line = false;
int serial_acl_next = -1;
int serial_acl_count = 0;
bool serial_acl_header = false;
int serial_recent_next = -1;
int serial_recent_count = 0;
bool serial_recent_header = false;
bool serial_recent_has_cursor = false;
SimpleMeshTables::RecentRepeaterInfo serial_recent_cursor;
int serial_recent_cursor_index = -1;
#endif
uint32_t last_millis;
uint64_t uptime_millis;
unsigned long next_local_advert, next_flood_advert;
mesh::DeferredCliCommand deferred_cli_command;
mesh::RemoteCliReplyCache remote_cli_reply_cache;
mesh::ReplayResetNonce replay_reset_nonce;
bool replay_clock_set = false;
mesh::TempRadioReplyBarrier temp_radio_reply_barrier;
uint32_t primary_radio_mutation_generation = 0;
bool primary_radio_mutation_starts_temp = false;
uint32_t radio_reply_deadline = 0;
bool radio_reply_secondary = false;
void finishRadioReply(bool delivered);
void serviceRadioReplyDeadline();
TransportKey deferred_cli_reply_scope;
bool deferred_cli_reply_scoped;
#if MESH_ENABLE_HOST_CLI
bool host_cli_waiting;
bool host_cli_claimed;
bool host_cli_claim_emit;
unsigned long host_cli_deadline;
unsigned long host_cli_claim_emit_at;
uint64_t host_cli_nonce;
uint64_t host_cli_claim_challenge;
#endif
uint32_t pending_self_advert_delay;
bool pending_self_advert;
bool pending_self_advert_flood;
unsigned long next_battery_alert_check;
mesh::RxInactivityWatchdog rx_inactivity_watchdog;
mesh::RepeaterRadioTiming radio_timing;
unsigned long next_recent_repeater_sweep;
uint64_t last_battery_alert_sent;
mesh::Packet* pending_battery_alert_packet;
bool battery_alert_sent;
bool _logging;
#ifdef DISPLAY_ACTIVITY_DASHBOARD
RadioActivityWindow _activity;
#endif
NodePrefs _prefs;
mesh::GuestLoginSettings guest_login;
mesh::LoginHistory login_history;
bool handleGuestCommand(const char* command, char* reply, size_t capacity);
ClientACL acl;
CommonCLI _cli;
#if defined(ESP32_PLATFORM) || defined(USER_GPIO_CONTROL)
UserGpioReplyTracker _gpio_reply_tracker;
#endif
uint8_t reply_data[MAX_PACKET_PAYLOAD];
uint8_t reply_path[MAX_PATH_SIZE];
uint8_t reply_path_len;
TransportKeyStore key_store;
RegionMap region_map, temp_map;
RegionEntry* load_stack[8];
RegionEntry* recv_pkt_region;
bool recv_pkt_regionless_scope_set;
bool recv_pkt_channel_scope_bypass;
bool recv_pkt_channel_scope_rejected;
TransportKey default_scope;
RateLimiter discover_limiter, anon_limiter;
struct FloodRetryBridgeState {
uint8_t key[MAX_HASH_SIZE];
uint8_t source_mask;
uint8_t target_mask;
uint8_t heard_mask;
uint8_t progress_marker;
bool active;
};
struct FloodRetryBridgeReachability {
uint8_t prefix[MAX_ROUTE_HASH_BYTES];
uint8_t prefix_len;
uint32_t last_heard_millis;
};
struct FloodPacketFilterEntry {
#if MESH_ENABLE_FLOOD_RULE_ENGINE
// Keep multibyte counters naturally aligned. These eight independent
// flags share a byte; their bit order is never persisted (FPF7 serializes
// each field explicitly). active/retry remain addressable for the stored
// rule decoders. Rule mutations run on the mesh loop.
uint32_t rate_window_started;
uint16_t rate_per_minute;
uint16_t rate_window_count;
bool suspend_on_temp_radio : 1;
bool match_blacklisted_path : 1;
bool scope_uses_slow_timing : 1;
bool drop_on_match : 1;
bool rate_limit_enabled : 1;
bool stop_on_match : 1;
bool rate_window_active : 1;
bool target_is_region : 1;
bool active;
bool retry_on_match;
uint8_t payload_type;
uint8_t min_hops;
uint8_t max_hops;
// scope= and region= cannot coexist; retain their full name capacity.
char target_name[FLOOD_PACKET_FILTER_SCOPE_NAME_LEN];
const char (&scopeName() const)[FLOOD_PACKET_FILTER_SCOPE_NAME_LEN] {
static const char empty[FLOOD_PACKET_FILTER_SCOPE_NAME_LEN] = {};
return target_is_region ? empty : target_name;
}
const char (&regionName() const)[FLOOD_PACKET_FILTER_SCOPE_NAME_LEN] {
static const char empty[FLOOD_PACKET_FILTER_SCOPE_NAME_LEN] = {};
return target_is_region ? target_name : empty;
}
uint8_t incoming_scope_kind;
char incoming_scope_name[FLOOD_PACKET_FILTER_SCOPE_NAME_LEN];
uint8_t channel_key_len;
uint8_t channel_hash;
uint8_t channel_secret[PUB_KEY_SIZE];
char channel_name[FLOOD_GROUP_MODERATION_NAME_LEN];
uint8_t path_hash_size;
uint8_t path_hops;
uint8_t path[FLOOD_PACKET_FILTER_PATH_PREFIX_BYTES_MAX];
uint8_t priority;
uint8_t transport_modes;
FloodRuleExtensions::Fields extra;
#else
bool active;
uint8_t payload_type;
uint8_t min_hops;
uint8_t max_hops;
bool suspend_on_temp_radio;
char scope_name[FLOOD_PACKET_FILTER_SCOPE_NAME_LEN];
char (&scopeName())[FLOOD_PACKET_FILTER_SCOPE_NAME_LEN] { return scope_name; }
const char (&scopeName() const)[FLOOD_PACKET_FILTER_SCOPE_NAME_LEN] { return scope_name; }
bool match_blacklisted_path;
bool scope_requires_region_match;
bool scope_uses_slow_timing;
#endif
};
struct FloodChannelScopeEntry {
// Zero means unused. Otherwise this is either a region ID or a one-based
// flood_channel_direct_scopes index, as selected by selector.
uint16_t target_id;
// Encodes txt:*/login:*/other:* or an exact key length, target kind,
// optional path selector, and fast/slow timing.
uint8_t selector;
uint8_t channel_hash;
uint8_t secret[PUB_KEY_SIZE];
};
struct FloodChannelScopeRequireEntry {
uint8_t key_len; // zero means unused
uint8_t channel_hash;
uint8_t secret[PUB_KEY_SIZE];
};
struct FloodGroupModerationEntry {
bool active;
uint8_t key_len;
uint8_t hash_prefix[FLOOD_CHANNEL_KEY_PREFIX_LEN];
uint8_t secret[PUB_KEY_SIZE];
char channel_name[FLOOD_GROUP_MODERATION_NAME_LEN];
char sender[FLOOD_GROUP_MODERATION_NAME_LEN];
uint8_t path_hash_size;
uint8_t path_hops;
uint8_t path[FLOOD_GROUP_MODERATION_PATH_BYTES_MAX];
uint8_t max_hops;
uint16_t rate_per_minute;
uint32_t rate_window_started;
uint16_t rate_window_count;
bool rate_window_active;
};
struct ClockSyncSample {
bool active;
uint8_t source_kind;
uint8_t source_id[4];
uint8_t path_id[CLOCK_SYNC_PATH_ID_SIZE];
uint32_t epoch;
uint32_t received_millis;
};
mutable FloodRetryBridgeState flood_retry_bridge_states[TOTAL_FLOOD_RETRY_SLOTS];
FloodRetryBridgeReachability flood_retry_bridge_reachability[FLOOD_RETRY_BRIDGE_BUCKETS + 1];
static_assert(sizeof(FloodPacketFilterEntry) <= (MESH_ENABLE_FLOOD_RULE_ENGINE ? 304 : 40),
"Update the flood-table runtime RAM budget in check_firmware_ram.py");
// The rule table is the single largest member of this object. It is heap
// allocated in the constructor (before setup(), while the heap is still
// unfragmented) so classic ESP32's small link-time static DRAM window does
// not have to hold it. flood_packet_filter_slots is the live capacity, and
// is 0 when the allocation failed: every loop below is bounded by it, so a
// zero-capacity node simply forwards without rule filtering.
FloodPacketFilterEntry* flood_packet_filters;
uint8_t flood_packet_filter_slots;
#if MESH_ENABLE_FLOOD_RULE_ENGINE
uint32_t flood_filter_probability_seed;
#endif
uint8_t flood_packet_filter_blacklist_count;
uint8_t flood_packet_filter_blacklist[FLOOD_PACKET_FILTER_BLACKLIST_MAX]
[FLOOD_PACKET_FILTER_PATH_ID_SIZE];
FloodChannelScopeEntry flood_channel_scopes[FLOOD_CHANNEL_SCOPE_SLOTS];
char flood_channel_direct_scopes[FLOOD_CHANNEL_DIRECT_SCOPE_SLOTS]
[FLOOD_PACKET_FILTER_SCOPE_NAME_LEN];
FloodChannelScopeRequireEntry
flood_channel_scope_requirements[FLOOD_CHANNEL_SCOPE_REQUIRE_SLOTS];
#if MESH_ENABLE_FLOOD_GROUP_MODERATION
FloodGroupModerationEntry flood_group_moderation[FLOOD_GROUP_MODERATION_SLOTS];
#endif
uint64_t recv_pkt_filter_match_mask;
#if MESH_ENABLE_FLOOD_RULE_ENGINE
bool flood_policy_has_embedded_sections;
// A valid stored ruleset can exceed this image's runtime table. Preserve
// it until compatible firmware is installed; do not publish an empty set.
bool flood_policy_capacity_limited;
// Zero-based forward-row slot owned by the flood.channel.data compatibility
// facade. 0xFF means forwarding is enabled and no compatibility row exists.
uint8_t flood_channel_data_rule_slot;
uint8_t flood_channel_data_rule_max_hops;
#endif
ClockSyncSample clock_sync_samples[CLOCK_SYNC_SAMPLE_SLOTS];
bool clock_sync_mesh_enabled;
bool clock_sync_mesh_edge_enabled;
bool clock_sync_internet_enabled;
bool clock_sync_complete;
bool clock_sync_internet_pending;
bool clock_sync_force_mesh_pending;
uint8_t clock_sync_mesh_suppressed_by;
uint8_t clock_sync_last_result;
uint8_t clock_sync_last_source;
uint8_t clock_sync_last_sample_count;
uint8_t clock_sync_last_fresh_count;
uint8_t clock_sync_last_required_count;
uint8_t clock_sync_required_samples;
uint32_t clock_sync_drift_seconds;
uint32_t clock_sync_last_estimate;
uint32_t clock_sync_last_abs_drift;
uint32_t clock_sync_internet_requested_millis;
uint64_t clock_sync_next_attempt_uptime;
uint32_t pending_discover_tag;
unsigned long pending_discover_until;
bool region_load_active;
unsigned long dirty_contacts_expiry;
uint8_t contacts_save_failures;
#if MAX_NEIGHBOURS
NeighbourInfo neighbours[MAX_NEIGHBOURS];
#endif
CayenneLPP telemetry;
#if MESH_ENABLE_TELEMETRY_HISTORY
mesh::TelemetryHistory telemetry_history;
mesh::ExternalVoltageHistory external_voltage_history;
mesh::NoiseFloorHistory noise_floor_history;
mesh::NoiseFloorHistorySampler noise_floor_sampler;
bool telemetry_history_tx_prefs_healthy;
bool telemetry_history_tx_enabled;
uint8_t telemetry_history_tx_interval_days;
uint8_t telemetry_history_tx_pending;
bool telemetry_history_tx_manual;
uint8_t telemetry_history_tx_external_channel;
uint8_t telemetry_history_tx_external_chunk;
uint8_t telemetry_history_tx_types;
uint64_t telemetry_history_next_tx_uptime;
uint64_t telemetry_history_tx_resume_uptime;
uint64_t telemetry_history_last_tx_uptime;
#endif
unsigned long _ota_update_at = 0; // deferred `ota update` fire time (0 = none scheduled)
float active_bw; // live BW, including temporary radio overrides
uint8_t active_sf; // live SF, including temporary radio overrides
uint8_t active_cr; // live CR, including temporary radio overrides
bool saved_radio_apply_pending;
bool temp_radio_handoff_pending;
bool temp_radio_applied;
bool scheduled_temp_radio_started;
uint32_t next_scheduled_radio_time;
unsigned long next_scheduled_radio_check_at;
uint32_t scheduled_temp_radio_end_time;
unsigned long scheduled_temp_radio_end_check_at;
bool scheduled_temp_radio_end_check_final;
unsigned long scheduled_radio_retry_at;
unsigned long scheduled_radio_save_retry_at;
uint8_t scheduled_radio_retry_failures;
ScheduledRadioSetting scheduled_radio_settings[MAX_SCHEDULED_RADIO_SETTINGS];
#if MESH_ENABLE_FLEET_CONTROL
// One verified fleet schedule waits for its own physical acknowledgement.
ScheduledRadioSetting fleet_schedule_setting_{};
uint8_t fleet_schedule_delete_mask_[(MAX_SCHEDULED_RADIO_SETTINGS + 7) / 8]{};
int fleet_schedule_slot_ = -1;
bool fleet_command_ = false, fleet_schedule_pending_ = false;
bool fleet_schedule_delivered_ = false, fleet_schedule_delete_all_temp_ = false;
void serviceFleetScheduleReply();
#endif
uint8_t _ota_update_channel = 0; // Snapshot: later ota branch commands cannot retarget a queued update
int matching_peer_indexes[MAX_CLIENTS];
#if defined(WITH_MQTT_BRIDGE)
MQTTBridge* mqtt_bridge;
// Expanded ESP32 Full builds run MQTT and ESP-NOW on the same 2.4 GHz
// station interface. MQTT owns the WiFi connection; ESP-NOW must therefore
// use that AP's channel (ESPNowBridge rejects a mismatch at start).
#if defined(WITH_ESPNOW_BRIDGE)
ESPNowBridge espnow_bridge;
uint32_t shared_espnow_retry_at = 0;
#endif
#endif
#if defined(WITH_RS232_BRIDGE)
RS232Bridge* bridge;
uint8_t active_rs232_bridge_uart = 0;
#if defined(WITH_ESPNOW_BRIDGE) && !defined(WITH_MQTT_BRIDGE)
ESPNowBridge espnow_bridge;
uint32_t shared_espnow_retry_at = 0;
#endif
#elif defined(WITH_ESPNOW_BRIDGE) && !defined(WITH_MQTT_BRIDGE)
ESPNowBridge bridge;
uint32_t shared_espnow_retry_at = 0;
#endif
#ifdef WITH_UDP_BRIDGE
mesh::bridge::udp::Settings udp_bridge_settings;
UDPBridge* udp_bridge = nullptr;
uint32_t udp_station_retry_at = 0;
bool udp_network_paused = false;
void serviceUDPBridge();
bool ensureUDPBridge();
const mesh::bridge::udp::Prefs* getUDPBridgePrefs() const override {
return &udp_bridge_settings.prefs();
}
bool setUDPBridgePrefs(const mesh::bridge::udp::Prefs& value) override;
void formatUDPBridgeStatus(char* reply, size_t capacity) override;
void formatUDPBridgeStats(const char* page, char* reply, size_t capacity) override;
bool sendUDPBridgeTest(uint8_t peer_index, uint32_t nonce) override;
bool resetUDPBridgeStats() override;
#endif
#ifdef WITH_BRIDGE
AbstractBridge* activeBridge() {
#ifdef WITH_MQTT_BRIDGE
return mqtt_bridge;
#elif defined(WITH_RS232_BRIDGE)
return bridge;
#else
return &bridge;
#endif
}
const AbstractBridge* activeBridge() const {
#ifdef WITH_MQTT_BRIDGE
return mqtt_bridge;
#elif defined(WITH_RS232_BRIDGE)
return bridge;
#else
return &bridge;
#endif
}
#endif
#if defined(WITH_ESPNOW_BRIDGE)
ESPNowBridge& diagnosticEspNowBridge() {
#if defined(WITH_MQTT_BRIDGE) || defined(WITH_RS232_BRIDGE)
return espnow_bridge;
#else
return bridge;
#endif
}
void formatEspNowStatus(char* reply, size_t capacity) override {
diagnosticEspNowBridge().formatStatus(reply, capacity);
}
void formatEspNowStats(const char* page, char* reply, size_t capacity) override {
diagnosticEspNowBridge().formatStats(page, reply, capacity);
}
bool sendEspNowTest(uint32_t nonce) override {
return diagnosticEspNowBridge().sendTest(nonce);
}
bool resetEspNowStats() override {
diagnosticEspNowBridge().resetStats();
return true;
}
// Retry ESP-NOW without taking over an active OTA session. MQTT creates its
// WiFi task asynchronously, so it must first associate on the shared channel.
bool startSharedEspNowBridgeIfReady() {
#ifdef WITH_WEBCONFIG
if (_unconfigured_setup_espnow_suspended) return false;
#endif
#if defined(WITH_MQTT_BRIDGE) || defined(WITH_RS232_BRIDGE)
ESPNowBridge& transport = espnow_bridge;
if (!_prefs.espnow_bridge_enabled) return false;
#else
ESPNowBridge& transport = bridge;
if (!_prefs.bridge_enabled) return false;
#endif
if (transport.isRunning()) return true;
// Browser OTA pauses the bridges for the entire upload session. The
// periodic retry must not undo that pause on its next loop iteration.
if (_cli.getBoard()->isOTAUpdateRunning()) return false;
#ifdef WITH_MQTT_BRIDGE
if (mqtt_bridge && mqtt_bridge->isRunning() && !WiFi.isConnected()) return false;
#endif
if (!millisHasNowPassed(shared_espnow_retry_at)) return false;
shared_espnow_retry_at = millis() + 5000;
configureBridgeFilter(&transport);
transport.begin();
return transport.isRunning();
}
#endif
#ifdef WITH_SNMP
MeshSNMPAgent _snmp_agent;
#endif
#ifdef WITH_MQTT_BRIDGE
AlertReporter _alerter;
#endif
#ifdef WITH_WEBCONFIG
WebConfigServer* _webconfig = nullptr;
// A first-boot setup session may pause saved bridge intent for this boot.
// Its retry loop must not revive ESP-NOW after the AP window ends.
bool _unconfigured_setup_espnow_suspended = false;
void suspendUnconfiguredSetupBridges();
bool startWebConfigImpl(bool force_ap, char* reply, bool automatic_setup);
bool webConfigOwnsWiFi() const;
bool _wc_batch_active = false;
bool _wc_restart_pending = false;
uint8_t _wc_slot_restart_mask = 0;
#endif
#if defined(ESP32_PLATFORM)
void serviceIdleWiFi();
#endif
#if defined(WITH_MQTT_NEIGHBORS)
// Neighbor-scope discovery: a snapshot of the neighbor table overlaid with an
// anon-regions query per neighbor, published to the MQTT neighbors topic once
// every neighbor has responded or timed out.
enum NeighborDiscoverStatus : uint8_t {
ND_UNSENT = 0,
ND_QUEUED = 1,
ND_PENDING = 2,
ND_RESPONDED = 3,
ND_TIMEOUT = 4,
ND_SEND_FAILED = 5,
};
struct NeighborDiscoverEntry {
// Keep every full key in the immutable crypto overlay, even when the
// existing JSON/query budget cannot publish that neighbor this pass.
mesh::Identity id;
};
struct NeighborDiscoverMetadata {
uint32_t heard_timestamp;
uint32_t tag;
int16_t rssi;
int8_t snr;
uint8_t status;
};
static_assert(sizeof(NeighborDiscoverMetadata) == 12,
"Discovery metadata must retain its complete compact layout");
NeighborDiscoverEntry neighbor_discover[MAX_NEIGHBOURS];
// Only the publishable prefix (plus the result that detects truncation)
// receives scope queries. Keep every neighbor snapshot, without reserving
// 96 response bytes for each entry that will never be queried.
static constexpr size_t NEIGHBOR_SCOPE_RESULTS =
MAX_NEIGHBOURS < MQTTBridge::NEIGHBORS_SCOPE_RESULT_CAPACITY
? MAX_NEIGHBOURS : MQTTBridge::NEIGHBORS_SCOPE_RESULT_CAPACITY;
NeighborDiscoverMetadata neighbor_discover_metadata[NEIGHBOR_SCOPE_RESULTS];
char neighbor_discover_scopes[NEIGHBOR_SCOPE_RESULTS][96];
uint8_t neighbor_discover_count;
uint8_t neighbor_discover_next; // newest-first entry currently being queried
uint8_t neighbor_discover_publish_count; // completed prefix that fits the JSON buffer
uint8_t neighbor_discover_queried_count; // requests confirmed transmitted
size_t neighbor_discover_json_size;
bool neighbor_discover_truncated;
bool neighbor_discover_active; // scope-query phase in flight
bool neighbor_table_refresh_active; // zero-hop table refresh (stage 1) in flight
bool neighbor_table_refresh_periodic; // that refresh was kicked by the periodic timer
unsigned long neighbor_discover_until; // current queue or response deadline
mesh::Packet* neighbor_discover_request; // request awaiting TX completion
unsigned long next_neighbors_publish; // periodic publish deadline (0 = fire ASAP)
char self_scopes_buf[96];
char self_default_scope_buf[31];
char neighbor_discover_origin[32];
mesh::Packet* sendAnonRegionsReq(const mesh::Identity& target, uint32_t& tag);
bool cancelNeighborDiscoverRequest();
uint32_t neighborDiscoverQueryTimeoutMs() const;
bool completeNeighborDiscoverEntry();
void resetNeighborDiscoverJsonBudget();
bool neighborDiscoverReady(char* reply);
bool startNeighborDiscover(char* reply);
void loopNeighborDiscover();
void finishNeighborDiscover();
bool handleNeighborDiscoverResponse(int overlay_idx, const uint8_t* data,
size_t len, float snr, int16_t rssi);
void touchNeighbourHeard(const mesh::Identity& id, uint32_t heard_timestamp,
float snr, int16_t rssi);
void getLocalScopes(char* buf, size_t len);
// Overlay peer indices are offset by this base so onPeerDataRecv can tell a
// discovery response apart from a normal ACL-client index.
static const int NEIGHBOR_DISCOVER_PEER_BASE = 1000;
static const unsigned long NEIGHBOR_DISCOVER_QUEUE_TIMEOUT_MS = 29000;
static const int NEIGHBOR_DISCOVER_MIN_FREE_PACKETS = 5;
#endif
bool extractDirectRetryPrefix(const mesh::Packet* packet, uint8_t* prefix, uint8_t& prefix_len) const;
int8_t getDirectRetryMinSNRX4() const;
uint8_t getDirectRetryCodingRateForSNR(int8_t snr_x4) const;
uint8_t getDirectRetryConfiguredMaxAttempts() const;
uint32_t getDirectRetryAttemptStepMillis() const;
bool hasFloodRetryPrefixes() const;
bool floodRetryPrefixMatches(const mesh::Packet* packet) const;
bool floodRetryLastHopMatches(const mesh::Packet* packet) const;
bool floodRetryPrefixIgnored(const uint8_t* prefix, uint8_t prefix_len) const;
uint8_t floodRetryEffectivePathLength(const mesh::Packet* packet, uint8_t max_hops = 0xFF) const;
bool floodRetryPrefixFresh(const uint8_t* prefix, uint8_t prefix_len) const;
uint8_t floodRetryBucketMaskForPrefix(const uint8_t* prefix, uint8_t prefix_len, bool require_fresh) const;
uint8_t floodRetryBucketMaskForPathHop(const uint8_t* prefix, uint8_t prefix_len, uint8_t hop,
uint8_t progress_marker) const;
uint8_t floodRetrySourceMask(const mesh::Packet* packet) const;
bool floodRetryBridgeBucketFresh(uint8_t bucket) const;
void recordFloodRetryBridgeReachability(const uint8_t* prefix, uint8_t prefix_len, uint8_t bucket_mask);
uint8_t floodRetryBridgeTargetMask(uint8_t source_mask) const;
uint8_t floodRetryBridgeHeardMask(const mesh::Packet* packet, uint8_t source_mask,
uint8_t progress_marker) const;
bool floodRetryBridgeEligible(const mesh::Packet* packet) const;
FloodRetryBridgeState* floodRetryBridgeStateFor(const mesh::Packet* packet, bool create) const;
void clearFloodRetryBridgeStateByKey(const uint8_t* retry_key, uint8_t radio_profile);
void refreshFloodRetryReachability(const mesh::Packet* packet);
bool hasFloodRetryHeard(const mesh::Packet* packet) const;
void formatFloodRetryPath(char* dest, size_t dest_len, const mesh::Packet* packet) const;
bool handleClientPathCommand(ClientInfo* sender, char* command, char* reply);
bool formatFloodRetryHeard(char* dest, size_t dest_len, const mesh::Packet* packet) const;
void putNeighbour(const mesh::Identity& id, uint32_t timestamp, float snr,
int16_t rssi);
uint8_t handleLoginReq(const mesh::Identity& sender, const uint8_t* secret, uint32_t sender_timestamp, const uint8_t* data, bool is_flood);
void sendDefaultPasswordWarning(ClientInfo* client, uint32_t sender_timestamp,
uint8_t path_hash_size);
uint8_t handleAnonRegionsReq(const mesh::Identity& sender, uint32_t sender_timestamp, const uint8_t* data, size_t data_len);
uint8_t handleAnonOwnerReq(const mesh::Identity& sender, uint32_t sender_timestamp, const uint8_t* data, size_t data_len);
uint8_t handleAnonClockReq(const mesh::Identity& sender, uint32_t sender_timestamp, const uint8_t* data, size_t data_len);
int handleRequest(ClientInfo* sender, uint32_t sender_timestamp, uint8_t* payload, size_t payload_len,
size_t reply_capacity = MAX_PACKET_PAYLOAD - CIPHER_MAC_SIZE - (CIPHER_BLOCK_SIZE - 1));
mesh::Packet* createSelfAdvert();
void processDeferredCliCommand();
void clearDeferredCliCommand();
#if MESH_ENABLE_HOST_CLI
bool completeHostCliRequest(const char* service_reply);
#endif
bool sendRemoteCliReply(ClientInfo* client, const uint8_t* secret,
uint8_t path_hash_size, uint32_t sender_timestamp,
const char* reply, const TransportKey* fallback_scope,
mesh::Packet** queued_packet = NULL);
bool sendClientReplyWithFallbackScope(ClientInfo* client, mesh::Packet* packet,
unsigned long delay_millis, uint8_t path_hash_size,
const TransportKey* fallback_scope,
bool allow_redundant_copies = true);
void servicePostMeshLoop();
#if MESH_ENABLE_TELEMETRY_HISTORY
void sampleTelemetryHistory();
void sampleNoiseFloorHistory();
#if MESH_ENABLE_TELEMETRY_GPS_HISTORY
uint8_t resizeTelemetryGpsDays(uint8_t requested_days);
#endif
void loadTelemetryHistoryTxPrefs();
bool saveTelemetryHistoryTxPrefs();
uint8_t availableTelemetryHistoryTxTypes() const;
void finishTelemetryHistoryTxBatch(uint64_t now);
bool handleTelemetryHistoryTxTypesCommand(char* command, char* reply,
bool permitted);
void serviceTelemetryHistoryTx();
bool sendTelemetryHistorySnapshot(mesh::TelemetryHistory::Series series);
bool sendNoiseFloorHistorySnapshot(uint8_t profile);
bool sendExternalVoltageHistorySnapshot(uint8_t channel_index,
uint8_t chunk_index);
void formatTelemetryHistoryTxStatus(char* reply, size_t reply_size) const;
void recordTelemetryHistoryTx(const mesh::Packet* packet);
#endif
void sendSelfAdvertisementNow(uint32_t delay_millis, bool flood);
bool sendRepeatersFloodText(const char* text, const TransportKey* scope = nullptr,
mesh::Packet** queued_packet = nullptr);
uint8_t getRegionDepth(const RegionEntry* region);
const RegionEntry* findNarrowestBatteryAlertRegion(bool& ambiguous);
bool getBatteryAlertScopeForRegion(const RegionEntry& region, TransportKey& scope);
bool resolveBatteryAlertScope(TransportKey& scope);
void checkBatteryAlert();
void checkRxInactivityWatchdog();
void setTempRadioTiming(uint32_t duration_seconds);
void expireRecentRepeatersIfDue();
void printRecentRepeatersSerial();
File openAppend(const char* fname);
bool isLooped(const mesh::Packet* packet, const uint8_t max_counters[]);
bool applyRadioParams(float freq, float bw, uint8_t sf, uint8_t cr, uint16_t preamble = 0, bool temporary = false);
bool applySavedRadioParams();
void queueSavedRadioApply();
void refreshScheduledRadioState();
void processScheduledRadioSettings();
bool isMillisTimerDue(unsigned long timestamp) const;
bool floodChannelDataHopApplies(const mesh::Packet* packet) const;
bool loadFloodPacketFilters();
bool saveFloodPacketFilters(bool empty_scope_phase = false,
bool empty_forward_phase = false);
#if MESH_ENABLE_FLOOD_RULE_ENGINE
bool migrateLegacyFloodChannelBlocks();
bool migrateLegacyFloodChannelData();
bool importLegacyFloodPolicySections();
int findFloodChannelDataRule() const;
bool isFloodChannelDataRule(const FloodPacketFilterEntry& entry) const;
void formatFloodChannelData(char* reply) const;
void formatFloodChannelDataHops(char* reply) const;
void setFloodChannelData(const char* value, char* reply);
void setFloodChannelDataHops(const char* value, char* reply);
#endif
bool loadFloodPacketFilterBlacklist();
bool saveFloodPacketFilterBlacklist();
void seedDefaultFloodPacketFilters();
bool floodPacketFilterBlacklistMatches(const mesh::Packet* packet) const;
bool floodPacketFilterFieldsMatch(const FloodPacketFilterEntry& entry,
const mesh::Packet* packet,
bool incoming_is_scoped,
uint16_t incoming_transport_code,
bool incoming_region_allowed,
const RegionEntry* incoming_region, uint8_t context = 0) const;
bool authenticateFloodPacketFilterChannel(
const FloodPacketFilterEntry& entry,
const mesh::Packet* packet) const;
bool hasFloodPacketFilterRetryRules() const;
bool floodPacketFilterAllowsRetry(uint64_t match_mask) const;
int nextFloodPacketFilterMatch(uint64_t match_mask,
uint64_t visited_mask) const;
bool resolveFloodPacketFilterTargetRegion(
const char* name, TransportKey& scope,
const char*& canonical_name);
uint64_t applyFloodPacketFilterStop(uint64_t match_mask);
uint64_t evaluateFloodPacketFilterMatches(
const mesh::Packet* packet, bool incoming_region_allowed,
const RegionEntry* incoming_region, uint8_t context = 0);
bool applyFloodPacketFilterScope(mesh::Packet* packet, uint64_t match_mask,
bool& scope_set, bool& fast_track,
bool log_change = true);
bool shouldBlockFloodPacketForward(const mesh::Packet* packet,
uint64_t match_mask) const;
void commitFloodPacketFilterRates(const mesh::Packet* packet,
uint64_t match_mask);
void formatFloodPacketFilters(const char* args, char* reply, bool compact = false) const;
void formatFloodPacketFilterDetail(int index, char* reply, size_t reply_len, bool compact = false) const;
void setFloodPacketFilter(const char* args, char* reply,
bool require_explicit_action = false);
void deleteFloodPacketFilter(const char* args, char* reply);
void formatFloodPacketFilterBlacklist(const char* args, char* reply) const;
void setFloodPacketFilterBlacklist(const char* args, char* reply);
void deleteFloodPacketFilterBlacklist(const char* args, char* reply);
bool loadFloodChannelScopes();
bool saveFloodChannelScopes(bool empty_table = false);
bool applyFloodChannelScopeTarget(mesh::Packet* packet, const FloodChannelScopeEntry& entry,
bool& scope_changed, bool& fast_track,
bool& regionless_scope_set,
bool log_change = true);
bool applyFloodChannelScope(mesh::Packet* packet, bool& fast_track,
bool& regionless_scope_set,
bool log_change = true);
static uint8_t scoreFloodTransportScope(const mesh::Packet* packet, void* context);
uint8_t getFloodTransportScopeDepth(const mesh::Packet* packet);
void formatFloodChannelScopes(const char* args, char* reply);
void formatFloodChannelScopeDetail(int index, char* reply, size_t reply_len);
void setFloodChannelScope(const char* args, char* reply);
void deleteFloodChannelScope(const char* args, char* reply);
void loadFloodChannelScopeRequirements();
bool saveFloodChannelScopeRequirements(bool empty_table = false);
bool findFloodChannelScopeRequirementMatch(const mesh::Packet* packet,
bool& table_active) const;
void formatFloodChannelScopeRequirements(const char* args, char* reply);
void formatFloodChannelScopeRequirementDetail(int index, char* reply,
size_t reply_len) const;
void setFloodChannelScopeRequirement(const char* args, char* reply);
void deleteFloodChannelScopeRequirement(const char* args, char* reply);
void loadFloodGroupModeration();
bool saveFloodGroupModeration();
bool readFloodGroupModerationFile(const char* path, FloodGroupModerationEntry* entries,
bool* readable = nullptr) const;
bool recoverFloodGroupModeration();
bool shouldBlockFloodGroupTextForward(const mesh::Packet* packet);
void formatFloodGroupModeration(const char* args, char* reply) const;
void formatFloodGroupModerationDetail(int index, char* reply, size_t reply_len) const;
void setFloodGroupModeration(const char* args, char* reply);
void deleteFloodGroupModeration(const char* args, char* reply);
bool decodeFloodGroupPlainText(const mesh::Packet* packet, const uint8_t* secret, uint8_t key_len,
uint32_t& timestamp, char* sender, size_t sender_len) const;
bool decodeFloodGroupPlainTextSpan(const mesh::Packet* packet, const uint8_t* secret,
uint8_t key_len, uint8_t* buffer, size_t capacity,
mesh::FloodGroupPlainText::View& view) const;
void loadClockSyncPrefs();
bool saveClockSyncPrefs();
void resetClockSyncAttempt();
void suppressMeshClockSyncForBoot(uint8_t source);
void checkGpsClockSyncOverride();
bool isClockSyncCollectionActive() const;
uint32_t estimateClockTransitMillis(const mesh::Packet* packet) const;
void recordClockSyncSample(uint8_t source_kind, const uint8_t source_id[4], uint32_t epoch,
const mesh::Packet* packet);
void recordAcceptedFloodClockSample(const mesh::Packet* packet);
void recordPublicChannelClockSample(const mesh::Packet* packet);
bool estimateMeshClock(uint32_t& estimate, uint8_t& fresh_count,
uint8_t& agreeing_count, uint8_t& required_count) const;
bool applyClockEstimate(uint32_t estimate, uint8_t source, uint8_t sample_count);
void checkClockSync();
void formatClockSyncStatus(const char* args, char* reply, size_t reply_len) const;
void formatClockSyncTable(char* reply, size_t reply_len) const;
void formatClockSyncSampleDetail(int index, char* reply, size_t reply_len) const;
bool hasScheduledRadioWorkDue() const;
uint32_t limitSleepToMillisTimer(unsigned long timestamp, uint32_t sleep_ms) const;
uint32_t limitSleepToScheduledRadioWork(uint32_t sleep_ms) const;
bool hasStartedScheduledTempRadio() const;
int findFreeScheduledRadioSlot() const;
int countScheduledRadioSettings(bool temporary) const;
int findScheduledRadioSettingByIndex(bool temporary, int wanted) const;
int getScheduledRadioSettingIndex(bool temporary, int slot_idx) const;
bool scheduledRadioConflicts(bool temporary, uint32_t start_time, uint32_t end_time) const;
void clearScheduledRadioSetting(int idx, bool restore_if_started);
void formatScheduledRadioDuration(char* dest, size_t dest_len, uint32_t target_time) const;
void formatRadioParamTuple(char* dest, size_t dest_len, const ScheduledRadioSetting& setting) const;
void formatScheduledRadioSetting(char* reply, int setting_idx, int display_idx) const;
protected:
uint32_t getTempRadioDurationSeconds() const override { return radio_timing.tempDuration(); }
void appendTempRadioTimingNote(char* reply, size_t size, uint32_t seconds) const override {
mesh::RepeaterRadioTiming::appendTempWatchdogNote(reply, size, seconds);
}
bool isTempRadioActive() const override;
float getAirtimeBudgetFactor() const override {
// A bounded TempRadio window is an explicitly coordinated private OTA
// channel. Use its full TX budget without changing the saved normal-radio
// duty factor; restoring the ordinary tuple restores ordinary pacing too.
return isTempRadioActive() ? 0.0f : _prefs.airtime_factor;
}
bool getCADEnabled() const override {
return _prefs.cad_enabled;
}
bool allowPacketForward(const mesh::Packet* packet) override;
bool allowPacketTransmit(const mesh::Packet* packet) const override;
#if MESH_DEBUG || MESH_ENABLE_PACKET_FILE_LOGGING \
|| (MESH_PACKET_LOGGING && !MESH_PACKET_LOGGING_COMPACT)
const char* getLogDateTime() override;
#endif
void logRxRaw(float snr, float rssi, const uint8_t raw[], int len) override;
void logRx(mesh::Packet* pkt, int len, float score) override;
void logTx(mesh::Packet* pkt, int len) override;
void logTxFail(mesh::Packet* pkt, int len) override;
int calcRxDelay(float score, uint32_t air_time) const override;
int calcRxDelayForPacket(const mesh::Packet* packet, float score,
uint32_t air_time) override;
bool shouldBypassRxDelay(const mesh::Packet* packet) override;
bool evaluateScopeRewriteTiming(const mesh::Packet* packet,
bool& fast_track);
uint32_t getRetransmitDelay(const mesh::Packet* packet) override;
uint32_t getSlowScopeRetransmitDelay(const mesh::Packet* packet);
uint32_t getDirectRetransmitDelay(const mesh::Packet* packet) override;
bool supportsBasicRetryConfig() const override { return true; }
bool supportsAdvancedRetryConfig() const override { return true; }
void onRetryConfigChanged() override;
uint8_t getDefaultTxCodingRate() const override { return active_cr; }
bool allowDirectRetry(const mesh::Packet* packet, const uint8_t* next_hop_hash, uint8_t next_hop_hash_len) const override;
bool maybeShortCircuitDirect(mesh::Packet* packet) override;
void configureDirectRetryPacket(mesh::Packet* retry, const mesh::Packet* original, uint8_t retry_attempt) override;
uint32_t getDirectRetryEchoDelay(const mesh::Packet* packet) const override;
uint8_t getDirectRetryMaxAttempts(const mesh::Packet* packet) const override;
uint32_t getDirectRetryAttemptDelay(const mesh::Packet* packet, uint8_t attempt_idx) override;
void onDirectRetryEvent(const char* event, const mesh::Packet* packet, uint32_t delay_millis, uint8_t retry_attempt,
const uint8_t* target_hash = NULL, uint8_t target_hash_len = 0,
int16_t payload_type = -1) override;
void onDirectRetryFailed(const uint8_t* next_hop_hash, uint8_t next_hop_hash_len) override;
void onDirectRetrySucceeded(const uint8_t* next_hop_hash, uint8_t next_hop_hash_len, int8_t snr_x4) override;
bool allowFloodRetry(const mesh::Packet* packet) const override;
bool prepareFloodRetry(const mesh::Packet* packet) const override;
void onFloodRetryEvent(const char* event, const mesh::Packet* packet, uint32_t delay_millis, uint8_t retry_attempt) override;
void onFloodRetrySlotReleased(const uint8_t* retry_key, uint8_t radio_profile) override;
bool hasFloodRetryTargetPrefix(const mesh::Packet* packet) const override;
uint8_t getFloodRetryMaxPathLength(const mesh::Packet* packet) const override;
uint8_t getFloodRetryMaxAttempts(const mesh::Packet* packet) const override;
bool isFloodRetryEchoTarget(const mesh::Packet* packet, uint8_t progress_marker) const override;
int getInterferenceThreshold() const override {
return _prefs.interference_threshold;
}
int getAGCResetInterval() const override {
return ((int)_prefs.agc_reset_interval) * 4000; // milliseconds
}
bool useAdaptiveAGC() const override { return _prefs.powersaving_enabled != 0; }
#ifdef WITH_MQTT_BRIDGE
uint32_t getRadioWatchdogMillis() const override {
return ((uint32_t)_cli.getObserverPrefs()->radio_watchdog_minutes) * 60000UL;
}
#endif
uint8_t getExtraAckTransmitCount() const override {
return _prefs.multi_acks;
}
#if ENV_INCLUDE_GPS == 1
void applyGpsPrefs() {
sensors.setPowerSavingEnabled(_prefs.powersaving_enabled != 0);
sensors.setSettingValue("gps", _prefs.gps_enabled?"1":"0");
char interval_str[12];
sprintf(interval_str, "%u", _prefs.gps_interval);
sensors.setSettingValue("gps_interval", interval_str);
sensors.applyGpsTimeSyncInterval(_prefs.gps_sync_interval_hours);
}
#endif
mesh::DispatcherAction onRecvPacket(mesh::Packet* pkt) override;
void onSendComplete(mesh::Packet* packet) override;
void onRadioProfileCopyQueued(mesh::Packet* packet, const mesh::Packet* original,
uint8_t priority) override;
void onSendFail(mesh::Packet* packet) override;
void onAnonDataRecv(mesh::Packet* packet, const uint8_t* secret, const mesh::Identity& sender, uint8_t* data, size_t len) override;
int searchPeersByHash(const uint8_t* hash) override;
void getPeerSharedSecret(uint8_t* dest_secret, int peer_idx) override;
void onAdvertRecv(mesh::Packet* packet, const mesh::Identity& id, uint32_t timestamp, const uint8_t* app_data, size_t app_data_len) override;
void onGroupPacketRecv(mesh::Packet* packet) override;
void onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender_idx, const uint8_t* secret, uint8_t* data, size_t len) override;
bool onPeerPathRecv(mesh::Packet* packet, int sender_idx, const uint8_t* secret, uint8_t* path, uint8_t path_len, uint8_t extra_type, uint8_t* extra, uint8_t extra_len) override;
void onControlDataRecv(mesh::Packet* packet) override;
// OTA mesh-integration is centralized in mesh::Mesh (no per-example onOtaRecv / send adapter / tick).
bool sendFloodReply(mesh::Packet* packet, unsigned long delay_millis, uint8_t path_hash_size);
bool sendClientReply(ClientInfo* client, mesh::Packet* packet, unsigned long delay_millis, uint8_t path_hash_size);
public:
#ifdef WITH_UDP_BRIDGE
bool setUDPBridgeNetworkState(bool enabled) override;
bool isUDPBridgeNetworkEnabled() const override;
bool isUDPBridgeConfigured() const { return udp_bridge_settings.prefs().enabled != 0; }
bool isUDPBridgeRunning() const { return udp_bridge && udp_bridge->isRunning(); }
#endif
MyMesh(mesh::MainBoard& board, mesh::Radio& radio, mesh::MillisecondClock& ms, mesh::RNG& rng, mesh::RTCClock& rtc, mesh::MeshTables& tables);
~MyMesh() {
#ifdef WITH_UDP_BRIDGE
delete udp_bridge;
#endif
delete[] flood_packet_filters;
}
MyMesh(const MyMesh&) = delete;
MyMesh& operator=(const MyMesh&) = delete;
void begin(FILESYSTEM* fs);
void sendNodeDiscoverReq();
const char* getFirmwareVer() override { return FIRMWARE_VERSION; }
const char* getBuildDate() override { return FIRMWARE_BUILD_DATE; }
const char* getRole() override { return FIRMWARE_ROLE; }
bool managementNtpSynced() const override {
#ifdef WITH_MQTT_BRIDGE
return mqtt_bridge && mqtt_bridge->hasFreshNtpThisBoot();
#else
return false;
#endif
}
const char* getNodeName() { return _prefs.node_name; }
NodePrefs* getNodePrefs() {
return &_prefs;
}
bool isHiddenNode() const {
return _prefs.advert_interval == 0 && _prefs.flood_advert_interval == 0;
}
#ifdef DISPLAY_ACTIVITY_DASHBOARD
RadioActivityWindow* getActivityWindow() { return &_activity; }
#endif
#ifdef WITH_MQTT_BRIDGE
MQTTPrefs* getObserverPrefs() { return _cli.getObserverPrefs(); }
#endif
void savePrefs(
PrefsSaveRouting::Scope scope = PrefsSaveRouting::Scope::Common) override {
_cli.savePrefs(_fs, scope);
}
void onManualClockSet() override;
bool sendFloodScoped(const TransportKey& scope, mesh::Packet* pkt, uint32_t delay_millis, uint8_t path_hash_size);
// CommonCLICallbacks
bool supportsFleetControl() const override { return true; }
#if MESH_ENABLE_FLEET_CONTROL
bool getFleetLocation(int32_t& latitude_e6, int32_t& longitude_e6) const override;
void beginFleetCommand() override { fleet_command_ = true; }
void endFleetCommand() override { fleet_command_ = false; }
bool hasFleetReplyMutation() const override { return fleet_schedule_pending_; }
void finishFleetReplyMutation(bool delivered) override;
#endif
mesh::Radio* getProfileRadio() override { return _radio; }
void applyTempRadioParams(float freq, float bw, uint8_t sf, uint8_t cr, int timeout_mins, uint16_t preamble = 0) override;
bool scheduleNormalRadio() override;
#if defined(ESP32_PLATFORM) || defined(USER_GPIO_CONTROL)
uint32_t getUserGpioRequestSource() const override {
return _gpio_reply_tracker.requestSource();
}
void onUserGpioTimerScheduled(uint8_t pin, uint32_t request_id) override {
_gpio_reply_tracker.timerScheduled(pin, request_id);
}
void onUserGpioTimerCancelled(uint8_t pin) override {
_gpio_reply_tracker.timerCancelled(pin);
}
void onUserGpioTimerCompleted(uint8_t pin, uint8_t state,
uint32_t request_id) override;
#endif
#ifdef WITH_MQTT_BRIDGE
void onAlertConfigChanged() override { _alerter.onConfigChanged(); }
bool sendAlertText(const char* text) override { return _alerter.sendText(text); }
#endif
bool resolveAlertScope(TransportKey& dest) override;
void addScheduledRadioParams(bool temporary, float freq, float bw, uint8_t sf, uint8_t cr,
uint32_t start_time, uint32_t end_time, char* reply, uint16_t preamble = 0) override;
void formatScheduledRadioParams(bool temporary, const char* selector, char* reply) override;
void deleteScheduledRadioParams(bool temporary, const char* selector, char* reply) override;
bool formatFileSystem() override;
void sendSelfAdvertisement(int delay_millis, bool flood) override;
void updateAdvertTimer() override;
void updateFloodAdvertTimer() override;
void setLoggingOn(bool enable) override {
#if MESH_ENABLE_PACKET_FILE_LOGGING
_logging = enable;
#else
(void)enable;
_logging = false;
#endif
}
void eraseLogFile() override {
#if MESH_ENABLE_PACKET_FILE_LOGGING
_fs->remove(PACKET_LOG_FILE);
#endif
}
void dumpLogFile() override;
void printAclSerial();
#if MESH_USB_CONSOLE_COOPERATIVE
// Large local-only replies advance between radio service passes.
bool hasPendingSerialOutput() const;
void servicePendingSerialOutput();
void cancelPendingSerialOutput();
#endif
bool setTxPower(int8_t power_dbm) override;
bool setRxPowerSaving(bool enable, uint32_t rx_us, uint32_t sleep_us) override;
bool supportsRxPowerSavingRfRxDisable() const override;
bool setRxPowerSavingRfRxDisabled(bool disabled) override;
bool isRxPowerSavingRfRxDisabled() const override;
void getRxPsWatchdogCounts(uint32_t* soft, uint32_t* hard) override;
void formatNeighborsReply(char *reply) override;
void removeNeighbor(const uint8_t* pubkey, int key_len) override;
void formatStatsReply(char *reply) override;
void formatRadioStatsReply(char *reply) override;
void formatRadioDiagReply(char *reply) override;
void formatPacketStatsReply(char *reply) override;
void formatRecentRepeatersReply(char *reply, int page,
const uint8_t* search_prefix,
uint8_t search_prefix_len) override;
bool setRecentRepeater(const uint8_t* prefix, uint8_t prefix_len, int8_t snr_x4) override;
void clearRecentRepeaters() override;
void startRegionsLoad() override;
bool saveRegions() override;
void onDefaultRegionChanged(const RegionEntry* r) override;
mesh::LocalIdentity& getSelfId() override { return self_id; }
bool saveIdentity(const mesh::LocalIdentity& new_id) override;
void clearStats() override;
#if defined(WITH_WEBCONFIG) || defined(ETHERNET_ENABLED)
void handleLocalCommand(char* command, char* reply, Stream& output) {
if (_local_cli_output.busy()) {
strcpy(reply, "Err - local listing in progress");
return;
}
_command_output = &output;
handleCommand(0, command, reply);
_command_output = nullptr;
}
bool hasPendingLocalOutput() const { return _local_cli_output.busy(); }
void cancelLocalOutput(Stream& output) {
if (_local_cli_output.owns(output)) _local_cli_output.cancel();
}
#endif
void handleCommand(uint32_t sender_timestamp, ClientInfo* sender, char* command,
char* reply, int gpio_client_index = -1,
uint8_t gpio_path_hash_size = 1, bool usb_origin = false);
bool _wireless_usb_command = false;
uint8_t wirelessCommandSource(uint32_t sender_timestamp) const override {
if (sender_timestamp) return CommonCLICallbacks::wirelessCommandSource(sender_timestamp);
if (_wireless_usb_command) return mesh::wireless::Independent;
#if defined(WITH_WEBCONFIG) || defined(ETHERNET_ENABLED)
if (_command_output && _command_output != _web_terminal) return mesh::wireless::Independent;
#endif
return mesh::wireless::WiFi;
}
void handleCommand(uint32_t sender_timestamp, char* command, char* reply) {
handleCommand(sender_timestamp, NULL, command, reply);
}
// Only the physical console input loop may grant USB-only recovery access.
// Web, Ethernet and execCommand callbacks deliberately keep the default.
void handleUsbCommand(char* command, char* reply) {
handleCommand(0, NULL, command, reply, -1, 1, true);
}
bool handleReplayResetCommand(ClientInfo* sender, const char* command,
char* reply, bool usb_origin);
#if MESH_ENABLE_HOST_CLI
bool handleHostCliSerialReply(const char* command, char* reply);
#endif
void loop();
#if defined(OTA_BOOTLOADER_MIGRATION)
bool prepareForOtaReboot() override {
// Let the normal ACL save finish, including its failure backoff.
return dirty_contacts_expiry == 0;
}
#endif
bool canRecoverUsbLogging() const {
return dirty_contacts_expiry == 0 && !hasOutbound() && !isAnyTempRadioActive()
&& !hasPendingOtaApply() && !saved_radio_apply_pending
&& !temp_radio_handoff_pending && _ota_update_at == 0;
}
#if MESH_ENABLE_FLOOD_RULE_ENGINE
bool allowTransportPacket(const mesh::Packet* packet, uint8_t context);
bool allowRadioProfileCross(const mesh::Packet* packet) override {
return allowTransportPacket(packet, 4); // RULE_MODE_CROSS
}
#endif
uint32_t getPowerSaveSleepSeconds(uint32_t max_secs) const;
uint32_t getPowerSaveSleepMilliseconds(uint32_t max_ms) const;
#if defined(WITH_BRIDGE)
void configureBridgeFilter(AbstractBridge* active_bridge) {
active_bridge->setIngressProfiles(_radio->profiles());
#if MESH_ENABLE_FLOOD_RULE_ENGINE
active_bridge->setPacketFilter([](void* context, const mesh::Packet* packet) {
return static_cast<MyMesh*>(context)->allowTransportPacket(packet, 2); // RULE_MODE_BRIDGE
}, this);
#else
(void)active_bridge;
#endif
}
#ifdef WITH_RS232_BRIDGE
RS232Bridge* createRS232Bridge() {
RS232Bridge* created;
#ifdef WITH_RS232_BRIDGE_ALT
if (_prefs.bridge_uart == WITH_RS232_BRIDGE_ALT_UART) {
created = new (std::nothrow) RS232Bridge(&_prefs, WITH_RS232_BRIDGE_ALT,
WITH_RS232_BRIDGE_ALT_RX,
WITH_RS232_BRIDGE_ALT_TX, _mgr, getRTCClock());
} else
#endif
{
created = new (std::nothrow) RS232Bridge(&_prefs, WITH_RS232_BRIDGE,
WITH_RS232_BRIDGE_RX, WITH_RS232_BRIDGE_TX,
_mgr, getRTCClock());
}
if (created) {
// A new physical owner gets a new control session. Soft UART standby
// retains this object/session; a manual off/restart does not.
uint64_t session = 0;
getRNG()->random(reinterpret_cast<uint8_t*>(&session), sizeof(session));
created->setHeartbeatSession(session ? session : 1);
}
return created;
}
bool beginRS232Bridge() {
if (!bridge) return false;
const uint8_t selected_uart = _prefs.bridge_uart;
bool gps_uart_blocked = false;
#if ENV_INCLUDE_GPS == 1
if (sensors.gpsSerialTransportMayConflict(selected_uart)) {
if (!sensors.gpsUsesSerialUart(selected_uart)
|| !sensors.gpsSerialTransportCanYield(selected_uart)) {
return false;
}
if (!sensors.setGpsSerialTransportBlocked(selected_uart, true)) {
return false;
}
gps_uart_blocked = true;
// Record ownership as soon as the GPS yields. If bridge startup then
// fails and the first release attempt also fails, normal bridge cleanup
// still knows which UART must be released and can retry safely.
active_rs232_bridge_uart = selected_uart;
}
#endif
configureBridgeFilter(bridge);
bridge->begin();
if (!bridge->isRunning()) {
#if ENV_INCLUDE_GPS == 1
if (gps_uart_blocked) {
if (sensors.setGpsSerialTransportBlocked(selected_uart, false)) {
active_rs232_bridge_uart = 0;
}
}
#else
(void)gps_uart_blocked;
#endif
return false;
}
active_rs232_bridge_uart = selected_uart;
return true;
}
bool endRS232Bridge() {
#if ENV_INCLUDE_GPS == 1
const uint8_t released_uart = active_rs232_bridge_uart;
#endif
bool stopped = true;
// A fail-closed preflight can reject this object before begin() touches
// its UART. Adafruit nRF Uart::end() is not safe as a generic cleanup for
// an unstarted peripheral: it can stop a GPS-owned Serial1 and wait on
// hardware events which will never arrive. End only a bridge which proved
// that it reached running state.
if (bridge && bridge->isRunning()) {
bridge->end();
stopped = !bridge->isRunning();
}
bool gps_released = true;
#if ENV_INCLUDE_GPS == 1
// Release the bridge first, then restore a UART GPS. I2C GPS providers do
// not claim any UART and therefore never enter this path.
if (released_uart != 0 && sensors.gpsUsesSerialUart(released_uart)) {
gps_released = sensors.setGpsSerialTransportBlocked(released_uart, false);
}
#endif
if (stopped && gps_released) active_rs232_bridge_uart = 0;
return stopped && gps_released;
}
bool rs232BridgeEnabled() const {
#ifdef WITH_MQTT_BRIDGE
return _prefs.rs232_bridge_enabled != 0;
#else
return _prefs.bridge_enabled != 0;
#endif
}
bool isRs232BridgeRunning() const override {
return bridge && bridge->isRunning();
}
void serviceRs232BridgePower() {
if (!bridge) return;
// Poll only once, before evaluating liveness. The policy must still run
// when UART is stopped, or two sleeping peers can never rendezvous.
bridge->setHeartbeatEnabled(_prefs.rs232_heartbeat == 1);
if (bridge->isRunning()) bridge->loop();
if (!rs232BridgeEnabled()) return;
const uint32_t now = millis();
const RS232Bridge::PowerAction action = bridge->servicePowerPolicy(
(_prefs.rs232_autosleep == 1 || _prefs.rs232_autosleep == 2)
&& _prefs.powersaving_enabled != 0
&& _prefs.rs232_heartbeat == 1,
static_cast<uint32_t>(_prefs.rs232_idle_seconds) * 1000UL, now,
_prefs.rs232_autosleep == 2);
if (action == RS232Bridge::PowerAction::None) return;
bool success;
if (action == RS232Bridge::PowerAction::Suspend) {
// An end may stop UART but fail to release GPS ownership. The driver
// stays in cleanup/retry until both physical owners confirm success.
success = endRS232Bridge();
} else {
// A failed earlier start may have yielded GPS before UART begin failed.
// Release any partial ownership before trying to claim it again.
success = (active_rs232_bridge_uart == 0 || endRS232Bridge())
&& beginRS232Bridge();
}
bridge->completePowerAction(action, success, millis());
}
void formatRs232Status(char* reply) override {
char heartbeat[84];
if (bridge) bridge->formatHeartbeatStatus(heartbeat, sizeof(heartbeat), millis());
else StrHelper::strncpy(heartbeat, "phase=off peer=unknown", sizeof(heartbeat));
snprintf(reply, 160, "saved=%s running=%s heartbeat=%s autosleep=%s idle=%us %s",
rs232BridgeEnabled() ? "on" : "off",
isRs232BridgeRunning() ? "on" : "off",
_prefs.rs232_heartbeat == 1 ? "on" : "off",
_prefs.rs232_autosleep == 2 ? "force" : _prefs.rs232_autosleep == 1 ? "on" : "off",
static_cast<unsigned>(_prefs.rs232_idle_seconds), heartbeat);
}
// Diagnostics address the UART transport even when MQTT and ESP-NOW also run.
void formatRs232Transport(char* reply, size_t capacity) override {
if (bridge) bridge->formatStatus(reply, capacity);
else snprintf(reply, capacity, "Error: RS232 diagnostics unavailable");
}
void formatRs232Stats(const char* page, char* reply, size_t capacity) override {
if (bridge) bridge->formatStats(page, reply, capacity);
else snprintf(reply, capacity, "Error: RS232 diagnostics unavailable");
}
bool sendRs232Test(uint32_t nonce) override {
return bridge && bridge->sendTest(nonce);
}
bool resetRs232Stats() override {
if (!bridge) return false;
bridge->resetStats();
return true;
}
bool setRs232BridgeState(bool enable) override {
if (!enable) {
const bool stopped = endRS232Bridge();
if (stopped) {
delete bridge;
bridge = nullptr;
}
return stopped;
}
if (isRs232BridgeRunning()) return true;
// Retry any tracked GPS release before constructing a new UART owner.
if (active_rs232_bridge_uart != 0 && !endRS232Bridge()) return false;
// Explicit enable is a fresh owner, including when soft standby retained
// the previous one. It must not inherit a dormant heartbeat deadline.
delete bridge;
bridge = createRS232Bridge();
return bridge && beginRS232Bridge();
}
bool restartRs232Bridge() override {
// Apply UART changes synchronously so a failed CLI change can restore
// the previous pins/baud without disturbing MQTT or ESP-NOW.
if (!endRS232Bridge()) return false;
delete bridge;
bridge = nullptr;
return !rs232BridgeEnabled() || setRs232BridgeState(true);
}
#endif
bool isBridgeRunning() const override {
#ifdef WITH_UDP_BRIDGE
if (isUDPBridgeRunning()) return true;
#endif
#if defined(WITH_MQTT_BRIDGE)
return (mqtt_bridge && mqtt_bridge->isRunning())
#ifdef WITH_ESPNOW_BRIDGE
|| espnow_bridge.isRunning()
#endif
#ifdef WITH_RS232_BRIDGE
|| isRs232BridgeRunning()
#endif
;
#else
const AbstractBridge* active_bridge = activeBridge();
return active_bridge != nullptr && active_bridge->isRunning();
#endif
}
#if defined(WITH_ESPNOW_BRIDGE)
// The wireless backend queries this public transport API in standalone
// Full repeaters as well as MQTT/UART combinations. UDP can be running
// independently, so the generic isBridgeRunning() result is insufficient.
bool isEspNowBridgeRunning() override {
#if defined(WITH_MQTT_BRIDGE) || defined(WITH_RS232_BRIDGE)
return espnow_bridge.isRunning();
#else
return bridge.isRunning();
#endif
}
#endif
#if defined(WITH_ESPNOW_BRIDGE) \
&& (defined(WITH_MQTT_BRIDGE) || defined(WITH_RS232_BRIDGE))
#ifdef WITH_MQTT_BRIDGE
bool setMqttBridgeState(bool enable) override {
if (!enable) {
if (mqtt_bridge && mqtt_bridge->isRunning()) mqtt_bridge->end();
_alerter.setBridge(nullptr);
return !mqtt_bridge || !mqtt_bridge->isRunning();
}
if (mqtt_bridge && mqtt_bridge->isStopping()) return false;
// Give the WiFi station to MQTT while it associates. ESP-NOW is restarted
// after the AP channel is known, which avoids a raw ESP-NOW-only session
// pinning the station to a different channel.
if (espnow_bridge.isRunning()) espnow_bridge.end();
shared_espnow_retry_at = 0;
if (!mqtt_bridge) {
MQTTNodeInfo node_info;
node_info.node_name = _prefs.node_name;
node_info.freq = &_prefs.freq;
node_info.bw = &_prefs.bw;
node_info.sf = &_prefs.sf;
node_info.cr = &_prefs.cr;
node_info.repeat_flag = &_prefs.disable_fwd;
node_info.repeat_when_nonzero = false;
#ifdef WITH_WEBCONFIG
node_info.canonical_wifi = true;
#endif
mqtt_bridge = new MQTTBridge(node_info, _cli.getObserverPrefs(),
getRTCClock(), &self_id);
if (!mqtt_bridge) return false;
}
if (!mqtt_bridge->isRunning()) {
char device_id[65];
mesh::LocalIdentity self_id = getSelfId();
mesh::Utils::toHex(device_id, self_id.pub_key, PUB_KEY_SIZE);
mqtt_bridge->setDeviceID(device_id);
mqtt_bridge->setFirmwareVersion(getFirmwareVer());
mqtt_bridge->setBoardModel(_cli.getBoard()->getManufacturerName());
mqtt_bridge->setBuildDate(getBuildDate());
mqtt_bridge->setStatsSources(this, _radio, _cli.getBoard(), _ms);
configureBridgeFilter(mqtt_bridge);
mqtt_bridge->begin();
_alerter.setBridge(mqtt_bridge);
}
if (_prefs.espnow_bridge_enabled) startSharedEspNowBridgeIfReady();
return mqtt_bridge->isRunning();
}
#endif
bool setEspNowBridgeState(bool enable) override {
if (!enable) {
if (espnow_bridge.isRunning()) espnow_bridge.end();
shared_espnow_retry_at = 0;
return !espnow_bridge.isRunning();
}
if (espnow_bridge.isRunning()) return true;
if (_cli.getBoard()->isOTAUpdateRunning()) return false;
#ifdef WITH_WEBCONFIG
_unconfigured_setup_espnow_suspended = false;
#endif
shared_espnow_retry_at = 0;
#ifdef WITH_MQTT_BRIDGE
if (mqtt_bridge && mqtt_bridge->isRunning()) {
// Association is asynchronous; servicePostMeshLoop retries once the
// MQTT station has joined the access point on its fixed channel.
startSharedEspNowBridgeIfReady();
return true;
}
#endif
configureBridgeFilter(&espnow_bridge);
espnow_bridge.begin();
return espnow_bridge.isRunning();
}
#ifdef WITH_MQTT_BRIDGE
bool restartMqttBridge() override {
#ifdef WITH_WEBCONFIG
if (_wc_batch_active) {
_wc_restart_pending = true;
return true;
}
#endif
if (espnow_bridge.isRunning()) espnow_bridge.end();
if (mqtt_bridge && mqtt_bridge->isRunning()) mqtt_bridge->end();
shared_espnow_retry_at = 0;
_alerter.setBridge(nullptr);
return !_prefs.bridge_enabled || setMqttBridgeState(true);
}
#endif
bool restartEspNowBridge() override {
if (espnow_bridge.isRunning()) espnow_bridge.end();
shared_espnow_retry_at = 0;
return !_prefs.espnow_bridge_enabled || setEspNowBridgeState(true);
}
#endif
bool setBridgeState(bool enable) override {
#ifdef WITH_MQTT_BRIDGE
if (enable && mqtt_bridge && mqtt_bridge->isStopping()) return false;
#endif
#if defined(WITH_MQTT_BRIDGE) && defined(WITH_ESPNOW_BRIDGE)
if (!enable) {
const bool espnow_stopped = setEspNowBridgeState(false);
const bool mqtt_stopped = setMqttBridgeState(false);
return espnow_stopped && mqtt_stopped;
}
const bool mqtt_ok = _prefs.bridge_enabled
? setMqttBridgeState(true) : setMqttBridgeState(false);
const bool espnow_ok = _prefs.espnow_bridge_enabled
? setEspNowBridgeState(true) : setEspNowBridgeState(false);
return mqtt_ok && espnow_ok;
#else
#if defined(WITH_ESPNOW_BRIDGE) && !defined(WITH_RS232_BRIDGE)
// A standalone bridge shares its WiFi driver with the browser uploader.
// Preserve saved intent, but do not restart it until OTA has stopped.
if (enable && _cli.getBoard()->isOTAUpdateRunning()) return false;
#ifdef WITH_WEBCONFIG
if (enable) _unconfigured_setup_espnow_suspended = false;
#endif
shared_espnow_retry_at = 0;
#endif
// Disabling an already-absent heap-backed bridge is successful and must
// not allocate an instance merely to stop it. The embedded ESP-NOW bridge
// is always present, so it continues through the normal state check.
if (!enable && !activeBridge()) return true;
#ifdef WITH_MQTT_BRIDGE
if (!mqtt_bridge) {
MQTTNodeInfo node_info;
node_info.node_name = _prefs.node_name;
node_info.freq = &_prefs.freq;
node_info.bw = &_prefs.bw;
node_info.sf = &_prefs.sf;
node_info.cr = &_prefs.cr;
node_info.repeat_flag = &_prefs.disable_fwd;
node_info.repeat_when_nonzero = false;
#ifdef WITH_WEBCONFIG
node_info.canonical_wifi = true;
#endif
mqtt_bridge = new MQTTBridge(node_info, _cli.getObserverPrefs(),
getRTCClock(), &self_id);
if (!mqtt_bridge) return false;
}
#endif
#if defined(WITH_RS232_BRIDGE) && !defined(WITH_MQTT_BRIDGE)
if (enable) return setRs232BridgeState(true);
if (!enable) {
const bool stopped = endRS232Bridge();
if (stopped) {
delete bridge;
bridge = nullptr;
}
return stopped;
}
#endif
AbstractBridge* active_bridge = activeBridge();
if (!active_bridge) return false;
if (enable == active_bridge->isRunning()) {
return true;
}
if (enable)
{
#ifdef WITH_MQTT_BRIDGE
// Set device metadata before starting bridge (same as in begin())
char device_id[65];
mesh::LocalIdentity self_id = getSelfId();
mesh::Utils::toHex(device_id, self_id.pub_key, PUB_KEY_SIZE);
mqtt_bridge->setDeviceID(device_id);
mqtt_bridge->setFirmwareVersion(getFirmwareVer());
mqtt_bridge->setBoardModel(_cli.getBoard()->getManufacturerName());
mqtt_bridge->setBuildDate(getBuildDate());
mqtt_bridge->setStatsSources(this, _radio, _cli.getBoard(), _ms);
#endif
#if defined(WITH_RS232_BRIDGE) && !defined(WITH_MQTT_BRIDGE)
const bool started = beginRS232Bridge();
#else
configureBridgeFilter(active_bridge);
active_bridge->begin();
const bool started = active_bridge->isRunning();
#endif
#ifdef WITH_MQTT_BRIDGE
_alerter.setBridge(mqtt_bridge);
#endif
return started;
}
else
{
#if defined(WITH_RS232_BRIDGE) && !defined(WITH_MQTT_BRIDGE)
const bool stopped = endRS232Bridge();
delete bridge;
bridge = nullptr;
#else
active_bridge->end();
const bool stopped = !active_bridge->isRunning();
#endif
#ifdef WITH_MQTT_BRIDGE
_alerter.setBridge(nullptr);
#endif
return stopped;
}
#endif
}
bool restartBridge() override {
#if defined(WITH_MQTT_BRIDGE) && defined(WITH_ESPNOW_BRIDGE)
#ifdef WITH_WEBCONFIG
if (_wc_batch_active) {
_wc_restart_pending = true;
return true;
}
#endif
const bool mqtt_ok = restartMqttBridge();
const bool espnow_ok = restartEspNowBridge();
return mqtt_ok && espnow_ok;
#else
#if defined(WITH_ESPNOW_BRIDGE) && !defined(WITH_RS232_BRIDGE)
if (_cli.getBoard()->isOTAUpdateRunning()) return false;
#ifdef WITH_WEBCONFIG
_unconfigured_setup_espnow_suspended = false;
#endif
shared_espnow_retry_at = 0;
#endif
#if defined(WITH_RS232_BRIDGE) && !defined(WITH_MQTT_BRIDGE)
// RS-232 changes must be applied synchronously so the CLI can commit or
// roll back the selected pins/baud. This branch also reconstructs a bridge
// after a prior allocation/start failure and is not WebConfig-coalesced.
if (bridge && (bridge->isRunning() || active_rs232_bridge_uart != 0)
&& !endRS232Bridge()) {
return false;
}
delete bridge;
bridge = createRS232Bridge();
return bridge && beginRS232Bridge();
#else
AbstractBridge* active_bridge = activeBridge();
if (!active_bridge) return false;
#ifdef WITH_WEBCONFIG
if (_wc_batch_active) { // coalesced: applied once in onConfigBatchEnd()
_wc_restart_pending = true;
return true;
}
#endif
if (active_bridge->isRunning()) active_bridge->end();
#ifdef WITH_MQTT_BRIDGE
// Set device metadata before restarting bridge (same as in begin())
char device_id[65];
mesh::LocalIdentity self_id = getSelfId();
mesh::Utils::toHex(device_id, self_id.pub_key, PUB_KEY_SIZE);
mqtt_bridge->setDeviceID(device_id);
mqtt_bridge->setFirmwareVersion(getFirmwareVer());
mqtt_bridge->setBoardModel(_cli.getBoard()->getManufacturerName());
mqtt_bridge->setBuildDate(getBuildDate());
mqtt_bridge->setStatsSources(this, _radio, _cli.getBoard(), _ms);
#endif
configureBridgeFilter(active_bridge);
active_bridge->begin();
return active_bridge->isRunning();
#endif
#endif
}
void restartBridgeSlot(int slot) override {
#ifdef WITH_MQTT_BRIDGE
if (!mqtt_bridge || !mqtt_bridge->isRunning()) return;
#ifdef WITH_WEBCONFIG
if (_wc_batch_active && slot >= 0 && slot < MAX_MQTT_SLOTS) {
_wc_slot_restart_mask |= static_cast<uint8_t>(1U << slot);
return;
}
#endif
mqtt_bridge->setSlotPreset(slot, _cli.getObserverPrefs()->mqtt_slot_preset[slot]);
#else
(void)slot;
#endif
}
#if defined(WITH_MQTT_BRIDGE)
// Broadcast a key OTA milestone (start/fail only) on the configured alert
// channel, in addition to the Serial log -- so an operator who triggered
// `ota update` via remote management still gets feedback that lands well after
// the command's reply window. Respects the `alert on/off` master switch and
// rides the configured alert scope (sendChannel -> resolveAlertScope); a no-op
// when alerts are off or no channel is set. Deliberately NOT wired to routine
// slot connect/disconnect -- those remain in AlertReporter's fault logic.
void otaAlert(const char* msg) {
auto* obs = _cli.getObserverPrefs();
if (obs && obs->alert_enabled) _alerter.sendText(msg);
}
// Best-effort flush of the outbound packet queue before an OTA teardown that
// blocks the loop until reboot. The START alert (otaAlert) and the CLI reply
// are queued fire-and-forget (delay 0 / CLI_REPLY_DELAY_MILLIS); once
// setBridgeState(false) + otaFromManifest() run they spin the loop task until
// the chip reboots, so anything still in the send queue at that point is
// silently lost -- the observed "OTA update starting never arrives" case on a
// busy / duty-limited channel where the packet can't win a TX slot inside the
// 2.5 s window. Pump the mesh loop so already-queued packets get their airtime,
// bounded by timeout_ms so a jammed or budget-exhausted channel can't stall the
// update. Respects duty cycle / CAD: it only drains what is queued, it does not
// force a transmit. Returns instantly on a healthy node (queue already empty).
void drainOutbound(uint32_t timeout_ms) {
unsigned long start = millis();
while (hasOutbound() || _mgr->getOutboundCount(millis()) > 0) {
if (millis() - start >= timeout_ms) break;
mesh::Mesh::loop(); // base dispatcher only -- drives RX + checkSend()/TX
delay(1); // yield to the radio ISR / other FreeRTOS tasks
}
}
#endif
// Schedule the pull-OTA flash to run from loop() in ~2.5 s, leaving time for the
// "Beginning update..." CLI reply (CLI_REPLY_DELAY_MILLIS = 600 ms) to transmit
// before the flash blocks the loop and reboots.
bool beginDeferredOtaUpdate() override {
_ota_update_at = millis() + 2500;
if (_ota_update_at == 0) _ota_update_at = 1; // 0 means "none"
_ota_update_channel = _prefs.ota_channel;
#if defined(WITH_MQTT_BRIDGE)
// Broadcast START now, while the loop still runs (the 2.5 s reply window):
// the deferred flash blocks the loop and, on success, reboots -- so a start
// alert queued at fire time could never transmit. See otaAlert().
otaAlert("OTA update starting");
#endif
return true;
}
int getQueueSize() override {
#ifdef WITH_MQTT_BRIDGE
return mqtt_bridge ? mqtt_bridge->getQueueSize() : 0;
#else
return 0;
#endif
}
bool isMqttBridgeRunning() override {
#ifdef WITH_MQTT_BRIDGE
return mqtt_bridge && mqtt_bridge->isRunning();
#else
return false;
#endif
}
bool requestMqttBridgeStop() override {
#ifdef WITH_MQTT_BRIDGE
_alerter.setBridge(nullptr);
if (mqtt_bridge) mqtt_bridge->requestStop();
return true;
#else
return false;
#endif
}
bool isMqttBridgeStopping() override {
#ifdef WITH_MQTT_BRIDGE
return mqtt_bridge && mqtt_bridge->isStopping();
#else
return false;
#endif
}
bool syncMqttNtp() override {
#ifdef WITH_MQTT_BRIDGE
if (!mqtt_bridge || !mqtt_bridge->isRunning()) return false;
// Marshal onto the MQTT task (Core 0); this runs on the CLI thread (Core 1).
return mqtt_bridge->requestForcedNtpSync(0);
#else
return false;
#endif
}
bool runMqttNtpDiag(char* reply, size_t reply_size, bool verbose) override {
#ifdef WITH_MQTT_BRIDGE
if (!mqtt_bridge || !mqtt_bridge->isRunning()) return false;
return mqtt_bridge->ntpDiag(reply, reply_size, verbose);
#else
(void)reply;
(void)reply_size;
(void)verbose;
return false;
#endif
}
#endif
#ifdef WITH_WEBCONFIG
bool startWebConfig(bool force_ap, char* reply) override;
bool isWebConfigStopping() const { return _webconfig && _webconfig->isStopping(); }
bool hasWirelessNetworkClient() const { return _web_terminal != nullptr; }
bool stopWebConfig(char* reply) override;
bool stopWebConfigForOTA(char* reply) override {
return !_webconfig || _webconfig->stopForOTA(reply);
}
bool setWebUIEnabled(bool enabled, char* reply) override;
bool getWebUIStatus(char* reply) const override;
bool usesCanonicalWiFi() const override { return true; }
const void* canonicalWiFiLegacyPrefs() const {
#ifdef WITH_MQTT_BRIDGE
return _cli.getObserverPrefs();
#else
return nullptr;
#endif
}
bool getWiFiSSID(char* reply) const override;
bool getWiFiPassword(char* reply) const override {
return WebConfigServer::formatWiFiPassword(reply, 160, canonicalWiFiLegacyPrefs());
}
bool getWiFiStatus(char* reply) const override;
bool getWiFiPowerSave(char* reply) const override;
bool getWiFiCLI(char* reply) const override;
bool setWiFiSSID(const char* value, char* reply) override;
bool setWiFiPassword(const char* value, char* reply) override;
bool setWiFiPowerSave(const char* value, char* reply) override;
bool setWiFiCLI(const char* value, char* reply) override;
bool isWebConfigActive() const override {
return _webconfig && (_webconfig->isRunning() || _webconfig->isStopping());
}
void getNodeSnapshot(WebConfigServer::NodeSnapshot& snapshot) override;
void execCommand(char* cmd, char* reply) override { handleCommand(0, cmd, reply); }
bool supportsCliTerminal() const override { return true; }
void execAdminCommand(char* cmd, char* reply) override {
if (strcmp(cmd, "get acl") == 0 || strcmp(cmd, "log") == 0
|| strcmp(cmd, "get recent.repeater") == 0
|| strcmp(cmd, "get recent.repeaters") == 0) {
strcpy(reply, "Error: use /api/terminal for streamed listings");
return;
}
handleCommand(0, cmd, reply);
}
bool supportsStreamTerminal() const override { return true; }
bool beginStreamTerminal(Stream& output) override {
if (_web_terminal || _local_cli_output.busy()) return false;
_web_terminal = &output;
return true;
}
bool ownsStreamTerminal(const Stream& output) const override {
return _web_terminal == &output;
}
void runStreamTerminal(char* command) override {
if (!_web_terminal) return;
char reply[160] = {};
handleLocalCommand(command, reply, *_web_terminal);
if (reply[0]) _web_terminal->println(reply);
}
bool streamTerminalBusy() const override {
return _web_terminal && _local_cli_output.owns(*_web_terminal);
}
void endStreamTerminal(Stream& output) override {
cancelLocalOutput(output);
if (_web_terminal == &output) _web_terminal = nullptr;
}
void rebootNow() override { _cli.getBoard()->reboot(); }
void onConfigBatchStart() override {
_wc_batch_active = true;
_wc_restart_pending = false;
_wc_slot_restart_mask = 0;
}
void onConfigBatchEnd() override;
void buildStatsJson(char* buf, size_t buf_size) override;
#endif
// To check if there is pending work
bool hasPendingWork() const;
mesh::powerdiag::Mask pendingWorkReason() const;
bool setRxBoostedGain(bool enable) override;
void recalibrateNoiseFloor() override { _radio->recalibrateNoiseFloor(); }
#if defined(USE_LR2021)
virtual bool configSideDetectors(const uint8_t sideDetSFs[], uint8_t num, float bw) override;
#endif
};