Temporarily disable incompatible cached side detectors while applying a temporary primary tuple, preserving their saved configuration for normal restoration. Use primary-only CAD while those detectors are suspended. Keep compatible temporary detectors and real hardware errors intact. Report pending TempRadio starts instead of off, preserve live-tuple status during replacements and future schedules, and include the two-second delay in the accepted reply. Share primary validation to retain STM32 Mini flash capacity without dropping features. Validated production CLI status, scheduler and persistence cases; LR2021 detector suspension, restoration, CAD and hardware-error paths; reply handoff, room/sensor recovery, Morse and OTA automation regressions. W12 repeater, room-server and sensor builds pass. Canonical STM32 Mini Repeater passes flash and runtime RAM admission with 436 bytes of flash remaining. Physical W12 RF testing remains unverified.
1165 lines
43 KiB
C++
1165 lines
43 KiB
C++
#pragma once
|
|
|
|
#include <Arduino.h> // needed for PlatformIO
|
|
#include <Mesh.h>
|
|
#include <helpers/PowerSavingDiagnostics.h>
|
|
#include "RoomServerFeatures.h"
|
|
|
|
#if defined(NRF52_PLATFORM)
|
|
#include <InternalFileSystem.h>
|
|
#elif defined(RP2040_PLATFORM)
|
|
#include <LittleFS.h>
|
|
#elif defined(ESP32)
|
|
#include <SPIFFS.h>
|
|
#endif
|
|
|
|
#include <helpers/ArduinoHelpers.h>
|
|
#include <helpers/StaticPoolPacketManager.h>
|
|
#include <helpers/SimpleMeshTables.h>
|
|
#include <helpers/IdentityStore.h>
|
|
#ifdef DISPLAY_ACTIVITY_DASHBOARD
|
|
#include <helpers/RadioActivityWindow.h>
|
|
#endif
|
|
#include <helpers/AdvertDataHelpers.h>
|
|
#include <helpers/AlertReporter.h>
|
|
#include <helpers/TxtDataHelpers.h>
|
|
#include <helpers/CommonCLI.h>
|
|
#include <helpers/MeshClockSync.h>
|
|
#if defined(ESP32_PLATFORM) || defined(USER_GPIO_CONTROL)
|
|
#include <helpers/UserGpioReplyTracker.h>
|
|
#endif
|
|
#include <helpers/StatsFormatHelper.h>
|
|
#include <helpers/LocalCliOutput.h>
|
|
#include <helpers/ClientACL.h>
|
|
#include <helpers/RoomAccessPolicy.h>
|
|
#include <helpers/RoomHistoryStore.h>
|
|
#include <helpers/RoomBoardStore.h>
|
|
#include <helpers/LogicalMessageCache.h>
|
|
#include <helpers/RemoteCliReplyCache.h>
|
|
#include <helpers/RemoteCliRequest.h>
|
|
#include <helpers/RoomClientPathCommand.h>
|
|
#include <helpers/RegionMap.h>
|
|
#include "FloodRuleEngine.h"
|
|
#include <helpers/RoutingPolicy.h>
|
|
#include <RTClib.h>
|
|
#include <target.h>
|
|
|
|
namespace mesh { class RoomJsonWriter; }
|
|
|
|
#include <helpers/TelemetryHistoryConfig.h>
|
|
|
|
#ifndef MESH_ENABLE_TELEMETRY_GPS_HISTORY
|
|
#if ENV_INCLUDE_GPS == 1
|
|
#define MESH_ENABLE_TELEMETRY_GPS_HISTORY 1
|
|
#else
|
|
#define MESH_ENABLE_TELEMETRY_GPS_HISTORY 0
|
|
#endif
|
|
#endif
|
|
|
|
#if MESH_ENABLE_TELEMETRY_HISTORY
|
|
#include <helpers/ExternalVoltageHistory.h>
|
|
#include <helpers/TelemetryHistory.h>
|
|
#include <helpers/NoiseFloorHistorySampler.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
|
|
|
|
#ifdef WITH_ESPNOW_BRIDGE
|
|
#include "helpers/bridges/ESPNowBridge.h"
|
|
#define WITH_BRIDGE
|
|
#endif
|
|
|
|
#ifdef WITH_SNMP
|
|
#include "helpers/SNMPAgent.h"
|
|
#endif
|
|
|
|
|
|
#if defined(ESP_PLATFORM) && defined(ADMIN_PASSWORD) && !defined(WEBCONFIG_DISABLED)
|
|
#include "helpers/esp32/WebConfigServer.h"
|
|
#endif
|
|
#ifdef WITH_WEBCONFIG
|
|
#include <ArduinoJson.h>
|
|
#endif
|
|
|
|
/* ------------------------------ Config -------------------------------- */
|
|
|
|
#ifndef FIRMWARE_BUILD_DATE
|
|
#define FIRMWARE_BUILD_DATE "15 Oct 2026"
|
|
#endif
|
|
|
|
#ifndef FIRMWARE_VERSION
|
|
#define FIRMWARE_VERSION "v1.18.0"
|
|
#endif
|
|
|
|
#ifndef LORA_FREQ
|
|
#define LORA_FREQ 915.0
|
|
#endif
|
|
#ifndef LORA_BW
|
|
#define LORA_BW 250
|
|
#endif
|
|
#ifndef LORA_SF
|
|
#define LORA_SF 10
|
|
#endif
|
|
#ifndef LORA_CR
|
|
#define LORA_CR 5
|
|
#endif
|
|
#ifndef LORA_TX_POWER
|
|
#define LORA_TX_POWER 20
|
|
#endif
|
|
|
|
#ifndef ADVERT_NAME
|
|
#define ADVERT_NAME "Test BBS"
|
|
#endif
|
|
#ifndef ADVERT_LAT
|
|
#define ADVERT_LAT 0.0
|
|
#endif
|
|
#ifndef ADVERT_LON
|
|
#define ADVERT_LON 0.0
|
|
#endif
|
|
|
|
#ifndef ADMIN_PASSWORD
|
|
#define ADMIN_PASSWORD "password"
|
|
#endif
|
|
|
|
#ifndef MAX_UNSYNCED_POSTS
|
|
#define MAX_UNSYNCED_POSTS 32
|
|
#endif
|
|
|
|
#ifndef ROOM_MESSAGE_CACHE_SIZE
|
|
#define ROOM_MESSAGE_CACHE_SIZE 32
|
|
#endif
|
|
|
|
#ifndef SERVER_RESPONSE_DELAY
|
|
#define SERVER_RESPONSE_DELAY 300
|
|
#endif
|
|
|
|
#ifndef TXT_ACK_DELAY
|
|
#define TXT_ACK_DELAY 200
|
|
#endif
|
|
|
|
#define FIRMWARE_ROLE "room_server"
|
|
|
|
#define PACKET_LOG_FILE "/packet_log"
|
|
|
|
#define MAX_POST_TEXT_LEN (160-9)
|
|
|
|
#ifndef ROOM_POSTS_RTC_SLOW_STORAGE
|
|
#define ROOM_POSTS_RTC_SLOW_STORAGE 0
|
|
#endif
|
|
#if ROOM_POSTS_RTC_SLOW_STORAGE != 0 && ROOM_POSTS_RTC_SLOW_STORAGE != 1
|
|
#error "ROOM_POSTS_RTC_SLOW_STORAGE must be 0 or 1"
|
|
#endif
|
|
#if ROOM_POSTS_RTC_SLOW_STORAGE && (!defined(ESP32_PLATFORM) \
|
|
|| (!defined(LILYGO_TLORA) && !defined(TBEAM_SX1262) && !defined(TBEAM_SX1276)) \
|
|
|| !defined(MESHCORE_EXPANDED_PARTITION_PROFILE))
|
|
#error "RTC slow Room posts require a qualified Full TLora or Tbeam ESP32 recipe"
|
|
#endif
|
|
|
|
struct PostInfo {
|
|
mesh::Identity author;
|
|
uint32_t post_timestamp; // by OUR clock
|
|
char text[MAX_POST_TEXT_LEN+1];
|
|
};
|
|
|
|
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
|
|
int16_t rssi; // dBm from the last packet heard from this neighbour
|
|
};
|
|
|
|
class MyMesh : public mesh::Mesh, public CommonCLICallbacks,
|
|
public mesh::MeshClockSyncCallbacks
|
|
#ifdef WITH_WEBCONFIG
|
|
, public WebConfigServer::Callbacks
|
|
#endif
|
|
{
|
|
FILESYSTEM* _fs;
|
|
uint16_t pending_preamble = 0;
|
|
mesh::StaticFloodAdvertLimiter<> flood_advert_limiter;
|
|
mesh::FloodAdvertLimiter* getFloodAdvertLimiter() override { return &flood_advert_limiter; }
|
|
#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;
|
|
#endif
|
|
uint32_t last_millis;
|
|
uint64_t uptime_millis;
|
|
unsigned long next_local_advert, next_flood_advert;
|
|
bool _logging;
|
|
#ifdef DISPLAY_ACTIVITY_DASHBOARD
|
|
RadioActivityWindow _activity;
|
|
#endif
|
|
bool region_load_active;
|
|
NodePrefs _prefs;
|
|
TransportKeyStore key_store;
|
|
RegionMap region_map, temp_map;
|
|
#if MESH_ENABLE_ROOM_FLOOD_RULE_ENGINE
|
|
FloodRuleEngine flood_rules;
|
|
uint32_t recv_pkt_rule_match_mask;
|
|
bool recv_pkt_regionless_scope_set;
|
|
#endif
|
|
ClientACL acl;
|
|
mesh::LogicalMessageCache<ROOM_MESSAGE_CACHE_SIZE> recent_room_posts;
|
|
mesh::LogicalMessageCache<16> recent_room_polls;
|
|
mesh::RoomAccessPolicy room_access;
|
|
mesh::RemoteCliReplyCache remote_cli_reply_cache;
|
|
CommonCLI _cli;
|
|
mesh::MeshClockSync _clock_sync;
|
|
#if defined(ESP32_PLATFORM) || defined(USER_GPIO_CONTROL)
|
|
UserGpioReplyTracker _gpio_reply_tracker;
|
|
#endif
|
|
unsigned long dirty_contacts_expiry;
|
|
uint8_t contacts_save_failures;
|
|
uint8_t reply_data[MAX_PACKET_PAYLOAD];
|
|
unsigned long next_push;
|
|
uint16_t _num_posted, _num_post_pushes;
|
|
int next_client_idx; // for round-robin polling
|
|
int next_post_idx;
|
|
#if ROOM_POSTS_RTC_SLOW_STORAGE
|
|
PostInfo (&posts)[MAX_UNSYNCED_POSTS];
|
|
#else
|
|
PostInfo posts[MAX_UNSYNCED_POSTS]; // cyclic queue
|
|
#endif
|
|
static_assert(MAX_UNSYNCED_POSTS <= 32, "Room readiness bitmap holds 32 posts");
|
|
uint32_t post_ready_at[MAX_UNSYNCED_POSTS] = {};
|
|
uint32_t post_ready_mask = UINT32_MAX;
|
|
uint32_t last_room_timestamp = 0;
|
|
mesh::RoomHistoryState room_history_state;
|
|
bool room_history_enabled = false;
|
|
bool room_history_available = true;
|
|
char room_topic[MAX_POST_TEXT_LEN + 1] = {};
|
|
uint32_t room_topic_revision = 0;
|
|
uint32_t room_topic_timestamp = 0;
|
|
unsigned long room_topic_ready_at = 0;
|
|
bool room_topic_ready = true;
|
|
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;
|
|
#endif
|
|
RegionEntry* load_stack[8];
|
|
RegionEntry* recv_pkt_region;
|
|
TransportKey default_scope;
|
|
unsigned long set_radio_at, revert_radio_at;
|
|
unsigned long _ota_update_at = 0; // deferred `ota update` fire time (0 = none scheduled)
|
|
uint8_t _ota_update_channel = 0; // channel `ota update` checked; a later `ota branch` cannot retarget it
|
|
float pending_freq;
|
|
float pending_bw;
|
|
uint8_t pending_sf;
|
|
uint8_t pending_cr;
|
|
uint8_t active_cr;
|
|
bool temp_radio_applied;
|
|
bool saved_radio_apply_pending;
|
|
unsigned long radio_apply_retry_at;
|
|
uint8_t radio_apply_failures;
|
|
int matching_peer_indexes[MAX_CLIENTS];
|
|
#if defined(WITH_MQTT_NEIGHBORS)
|
|
NeighbourInfo neighbours[MAX_NEIGHBOURS];
|
|
uint32_t pending_discover_tag;
|
|
unsigned long pending_discover_until;
|
|
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;
|
|
uint8_t neighbor_discover_publish_count;
|
|
uint8_t neighbor_discover_queried_count; // requests confirmed transmitted
|
|
size_t neighbor_discover_json_size;
|
|
bool neighbor_discover_truncated;
|
|
bool neighbor_discover_active;
|
|
bool neighbor_table_refresh_active;
|
|
bool neighbor_table_refresh_periodic;
|
|
unsigned long neighbor_discover_until;
|
|
mesh::Packet* neighbor_discover_request;
|
|
unsigned long next_neighbors_publish;
|
|
char self_scopes_buf[96];
|
|
char self_default_scope_buf[31];
|
|
char neighbor_discover_origin[32];
|
|
|
|
void putNeighbour(const mesh::Identity& id, uint32_t timestamp, float snr,
|
|
int16_t rssi);
|
|
void sendNodeDiscoverReq();
|
|
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);
|
|
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
|
|
#ifdef WITH_MQTT_BRIDGE
|
|
MQTTBridge* bridge;
|
|
#if defined(WITH_ESPNOW_BRIDGE)
|
|
ESPNowBridge espnow_bridge;
|
|
uint32_t shared_espnow_retry_at = 0;
|
|
#endif
|
|
#endif
|
|
#ifdef WITH_SNMP
|
|
MeshSNMPAgent _snmp_agent;
|
|
#endif
|
|
#ifdef WITH_MQTT_BRIDGE
|
|
AlertReporter _alerter;
|
|
#endif
|
|
#ifdef WITH_WEBCONFIG
|
|
WebConfigServer* _webconfig = nullptr;
|
|
bool room_web_enabled = false;
|
|
bool room_web_request_authenticated = false;
|
|
uint32_t room_web_boot_id = 0;
|
|
struct RoomWebUser {
|
|
mesh::Identity id;
|
|
uint32_t last_seen = 0, last_sequence = 0;
|
|
uint16_t posts = 0, polls = 0;
|
|
} room_web_users[8] = {};
|
|
// Wrong room-browser passwords, shared by every browser token: when the
|
|
// current thirty-second window began, how many distinct ones failed in it,
|
|
// and digests of the last four. RAM only; a reboot starts afresh.
|
|
struct RoomWebPasswordLimit {
|
|
uint32_t window = 0;
|
|
uint8_t failures = 0, recent_next = 0;
|
|
uint8_t recent[4][8] = {};
|
|
} room_web_password_limit;
|
|
bool handleRoomWebCommand(const char* command, char* reply);
|
|
bool processRoomWebMail(const mesh::Identity& author, uint8_t role, uint32_t sequence,
|
|
JsonObjectConst input, mesh::RoomJsonWriter& json) __attribute__((noinline));
|
|
void processRoomWebAdmin(const char* operation, JsonObjectConst input,
|
|
mesh::RoomJsonWriter& json) __attribute__((noinline));
|
|
void processRoomWebBoard(const char* operation, JsonObjectConst input,
|
|
mesh::RoomJsonWriter& json) __attribute__((noinline));
|
|
void processRoomWebBoardRead(JsonObjectConst input, const mesh::RoomBoardIndex& board,
|
|
mesh::RoomJsonWriter& json) __attribute__((noinline));
|
|
void processRoomWebBoardWrite(const char* operation, JsonObjectConst input,
|
|
const mesh::RoomBoardIndex& board,
|
|
mesh::RoomJsonWriter& json) __attribute__((noinline));
|
|
bool _unconfigured_setup_espnow_suspended = false;
|
|
void suspendUnconfiguredSetupBridges();
|
|
bool startWebConfigImpl(bool force_ap, char* reply, bool automatic_setup);
|
|
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
|
|
|
|
bool addPost(ClientInfo* client, const char* postData);
|
|
bool applySavedRadioParams();
|
|
void servicePendingPrimaryRadioSettings(bool tx_gap = false);
|
|
bool storePost(const mesh::Identity& author, const char* postData);
|
|
bool snapshotRoomHistory();
|
|
void loadRoomHistory();
|
|
bool handleRoomHistoryCommand(const char* command, char* reply);
|
|
void serviceRoomQuotas();
|
|
bool pushPostToClient(ClientInfo* client, PostInfo& post);
|
|
bool pushRoomTextToClient(ClientInfo* client, uint32_t timestamp,
|
|
const mesh::Identity& author, const char* text,
|
|
uint32_t topic_revision = 0);
|
|
bool handleRoomTopicCommand(const char* command, char* reply);
|
|
void activateRoomTopic();
|
|
void serviceRoomPush();
|
|
uint8_t getUnsyncedCount(ClientInfo* client);
|
|
bool processAck(const uint8_t *data);
|
|
mesh::Packet* createSelfAdvert();
|
|
File openAppend(const char* fname);
|
|
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));
|
|
#if MESH_ENABLE_ROOM_FLOOD_RULE_ENGINE
|
|
bool evaluateFloodRuleTiming(const mesh::Packet* packet,
|
|
bool& fast_track);
|
|
bool isLooped(const mesh::Packet* packet,
|
|
const uint8_t max_counters[]) const;
|
|
uint32_t getSlowFloodRuleRetransmitDelay(
|
|
const mesh::Packet* packet);
|
|
#endif
|
|
|
|
protected:
|
|
bool isTempRadioActive() const override {
|
|
return temp_radio_applied && revert_radio_at != 0 && !millisHasNowPassed(revert_radio_at);
|
|
}
|
|
float getAirtimeBudgetFactor() const override {
|
|
return _prefs.airtime_factor;
|
|
}
|
|
|
|
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;
|
|
#if MESH_ENABLE_ROOM_FLOOD_RULE_ENGINE
|
|
int calcRxDelayForPacket(const mesh::Packet* packet, float score,
|
|
uint32_t air_time) override;
|
|
#endif
|
|
const char* getLogDateTime() override;
|
|
uint32_t getRetransmitDelay(const mesh::Packet* packet) override;
|
|
uint32_t getDirectRetransmitDelay(const mesh::Packet* packet) override;
|
|
bool allowDirectRetry(const mesh::Packet* packet, const uint8_t* next_hop_hash,
|
|
uint8_t next_hop_hash_len) const 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;
|
|
bool allowFloodRetry(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 supportsBasicRetryConfig() const override { return true; }
|
|
void onRetryConfigChanged() 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; }
|
|
uint8_t getExtraAckTransmitCount() const override {
|
|
return _prefs.multi_acks;
|
|
}
|
|
uint8_t getDefaultTxCodingRate() const override {
|
|
return active_cr;
|
|
}
|
|
|
|
mesh::DispatcherAction onRecvPacket(mesh::Packet* pkt) override;
|
|
|
|
bool allowPacketForward(const mesh::Packet* packet) override;
|
|
bool allowPacketTransmit(const mesh::Packet* packet) const override;
|
|
void onAnonDataRecv(mesh::Packet* packet, const uint8_t* secret, const mesh::Identity& sender, uint8_t* data, size_t len) override;
|
|
void sendDefaultPasswordWarning(ClientInfo* client, uint32_t sender_timestamp,
|
|
uint8_t path_hash_size);
|
|
int searchPeersByHash(const uint8_t* hash) override ;
|
|
void getPeerSharedSecret(uint8_t* dest_secret, int peer_idx) 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 onAckRecv(mesh::Packet* packet, uint32_t ack_crc) 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 onSendComplete(mesh::Packet* packet) override;
|
|
void onSendFail(mesh::Packet* packet) override;
|
|
void onRadioProfileCopyQueued(mesh::Packet* packet, const mesh::Packet* original,
|
|
uint8_t priority) override;
|
|
#if defined(WITH_MQTT_NEIGHBORS)
|
|
void onControlDataRecv(mesh::Packet* packet) override;
|
|
#endif
|
|
|
|
#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
|
|
|
|
bool sendFloodReply(mesh::Packet* packet, unsigned long delay_millis, uint8_t path_hash_size);
|
|
#if MESH_ENABLE_TELEMETRY_HISTORY
|
|
bool handleTelemetryHistoryCommand(const char* command, char* reply,
|
|
size_t reply_capacity = 160, bool include_prompt = true);
|
|
void sampleTelemetryHistory();
|
|
void sampleNoiseFloorHistory();
|
|
#endif
|
|
|
|
public:
|
|
bool supportsFleetControl() const override { return true; }
|
|
#if MESH_ENABLE_FLEET_CONTROL
|
|
bool getFleetLocation(int32_t& latitude_e6, int32_t& longitude_e6) const override;
|
|
#endif
|
|
MyMesh(mesh::MainBoard& board, mesh::Radio& radio, mesh::MillisecondClock& ms, mesh::RNG& rng, mesh::RTCClock& rtc, mesh::MeshTables& tables);
|
|
#if ROOM_POSTS_RTC_SLOW_STORAGE
|
|
~MyMesh();
|
|
MyMesh(const MyMesh&) = delete;
|
|
MyMesh& operator=(const MyMesh&) = delete;
|
|
MyMesh(MyMesh&&) = delete;
|
|
MyMesh& operator=(MyMesh&&) = delete;
|
|
#endif
|
|
|
|
void begin(FILESYSTEM* fs);
|
|
bool addSystemPost(const char* postData);
|
|
|
|
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 { return hasAuthoritativeClock(); }
|
|
const char* getNodeName() { return _prefs.node_name; }
|
|
NodePrefs* getNodePrefs() {
|
|
return &_prefs;
|
|
}
|
|
|
|
#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 { _clock_sync.onManualClockSet(); }
|
|
bool hasAuthoritativeClock() const override {
|
|
#ifdef WITH_MQTT_BRIDGE
|
|
return bridge != nullptr && bridge->hasFreshNtpThisBoot();
|
|
#else
|
|
return false;
|
|
#endif
|
|
}
|
|
|
|
bool sendFloodScoped(const TransportKey& scope, mesh::Packet* pkt,
|
|
uint32_t delay_millis, uint8_t path_hash_size);
|
|
|
|
// CommonCLICallbacks
|
|
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 isTempRadioPending() const override { return set_radio_at != 0 && !temp_radio_applied; }
|
|
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;
|
|
bool formatFileSystem() override;
|
|
void sendSelfAdvertisement(int delay_millis, bool flood) override;
|
|
void updateAdvertTimer() override;
|
|
void updateFloodAdvertTimer() override;
|
|
|
|
void setLoggingOn(bool enable) override { _logging = enable; }
|
|
|
|
void eraseLogFile() override {
|
|
_fs->remove(PACKET_LOG_FILE);
|
|
}
|
|
|
|
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;
|
|
mesh::RadioParamApplyResult trySetTxPower(int8_t power_dbm) override;
|
|
#if defined(MESH_HIL_TX_FREQ_MHZ)
|
|
bool isTxPowerApplied(int8_t power_dbm) const override;
|
|
#endif
|
|
bool setRxPowerSaving(bool enable, uint32_t rx_us, uint32_t sleep_us) override;
|
|
bool supportsRxPowerSaving() const override;
|
|
mesh::RadioParamApplyResult trySetRxPowerSaving(bool enable, uint32_t rx_us, uint32_t sleep_us) override;
|
|
bool supportsRxPowerSavingRfRxDisable() const override;
|
|
mesh::RadioParamApplyResult trySetRxPowerSavingRfRxDisabled(bool disabled) override;
|
|
bool isRxPowerSavingRfRxDisabled() const override;
|
|
void recalibrateNoiseFloor() override { _radio->recalibrateNoiseFloor(); }
|
|
void getRxPsWatchdogCounts(uint32_t* soft, uint32_t* hard) override;
|
|
bool setRxBoostedGain(bool enable) override;
|
|
bool supportsRxBoostedGain() const override;
|
|
mesh::RadioParamApplyResult trySetRxBoostedGain(bool enable) override;
|
|
#if defined(USE_LR2021)
|
|
mesh::RadioParamApplyResult tryConfigSideDetectors(const uint8_t sideDetSFs[], uint8_t num, float bw) override;
|
|
#endif
|
|
|
|
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 startRegionsLoad() override;
|
|
bool saveRegions() override;
|
|
void onDefaultRegionChanged(const RegionEntry* r) override;
|
|
|
|
mesh::LocalIdentity& getSelfId() override { return self_id; }
|
|
|
|
static bool saveFilter(ClientInfo* client);
|
|
|
|
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, char* command, char* reply,
|
|
int gpio_client_index = -1,
|
|
uint8_t gpio_path_hash_size = 1);
|
|
bool handleClientPathCommand(ClientInfo* client, char* command, char* reply);
|
|
bool roomClientChatEnabled(ClientInfo* client);
|
|
bool handleRoomMailClientCommand(ClientInfo* client, const char* text, uint64_t request_id,
|
|
char* reply, size_t capacity);
|
|
bool handleRoomMailText(ClientInfo* client, mesh::Packet* packet, const uint8_t* secret,
|
|
uint8_t* data, size_t len);
|
|
bool executeClientPathCommand(ClientInfo* client, mesh::RoomClientPathCommand operation,
|
|
const char* body, char* reply);
|
|
bool setRoomClientPath(ClientInfo* client, const char* which, const char* value, char* reply);
|
|
bool handleRoomCatchUpCommand(ClientInfo* client, char* command, char* reply);
|
|
bool applyRoomCatchUpCommand(ClientInfo* client, const char* mode, uint32_t value, char* reply);
|
|
bool handleRoomManagementCommand(char* command, char* reply);
|
|
#ifdef WITH_WEBCONFIG
|
|
void writeRoomClientJson(mesh::RoomJsonWriter& json, ClientInfo* client, bool detail);
|
|
#endif
|
|
bool sendClientReply(ClientInfo* client, mesh::Packet* packet,
|
|
unsigned long delay_millis, uint8_t path_hash_size);
|
|
bool _wireless_usb_command = false;
|
|
void handleUsbCommand(char* command, char* reply) {
|
|
_wireless_usb_command = true;
|
|
handleCommand(0, command, reply);
|
|
_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 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 && !_cli.hasPendingRadioSettings()
|
|
&& set_radio_at == 0 && _ota_update_at == 0;
|
|
}
|
|
#if MESH_ENABLE_ROOM_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
|
|
|
|
|
|
#if defined(WITH_BRIDGE)
|
|
#if defined(WITH_MQTT_BRIDGE) && defined(WITH_ESPNOW_BRIDGE)
|
|
void formatEspNowStatus(char* reply, size_t capacity) override {
|
|
espnow_bridge.formatStatus(reply, capacity);
|
|
}
|
|
void formatEspNowStats(const char* page, char* reply, size_t capacity) override {
|
|
espnow_bridge.formatStats(page, reply, capacity);
|
|
}
|
|
bool sendEspNowTest(uint32_t nonce) override {
|
|
return espnow_bridge.sendTest(nonce);
|
|
}
|
|
bool resetEspNowStats() override {
|
|
espnow_bridge.resetStats();
|
|
return true;
|
|
}
|
|
#endif
|
|
|
|
#if defined(WITH_MQTT_BRIDGE) && defined(WITH_ESPNOW_BRIDGE)
|
|
// MQTT's WiFi task associates asynchronously. ESP-NOW must wait for that
|
|
// station interface so it can share the AP-selected channel safely.
|
|
bool startSharedEspNowBridgeIfReady() {
|
|
#ifdef WITH_WEBCONFIG
|
|
if (_unconfigured_setup_espnow_suspended) return false;
|
|
#endif
|
|
if (espnow_bridge.isRunning()) return true;
|
|
if (!_prefs.espnow_bridge_enabled) return false;
|
|
// Keep the browser uploader's bridge pause in effect until OTA stops.
|
|
if (_cli.getBoard()->isOTAUpdateRunning()) return false;
|
|
if (bridge && bridge->isRunning() && !WiFi.isConnected()) return false;
|
|
if (!millisHasNowPassed(shared_espnow_retry_at)) return false;
|
|
shared_espnow_retry_at = millis() + 5000;
|
|
configureBridgeFilter(&espnow_bridge);
|
|
espnow_bridge.begin();
|
|
return espnow_bridge.isRunning();
|
|
}
|
|
#endif
|
|
|
|
bool isBridgeRunning() const override {
|
|
#if defined(WITH_MQTT_BRIDGE) && defined(WITH_ESPNOW_BRIDGE)
|
|
return (bridge && bridge->isRunning()) || espnow_bridge.isRunning();
|
|
#else
|
|
return bridge != nullptr && bridge->isRunning();
|
|
#endif
|
|
}
|
|
|
|
#if defined(WITH_MQTT_BRIDGE) && defined(WITH_ESPNOW_BRIDGE)
|
|
bool isEspNowBridgeRunning() override {
|
|
return espnow_bridge.isRunning();
|
|
}
|
|
|
|
bool setMqttBridgeState(bool enable) override {
|
|
if (!enable) {
|
|
if (bridge) bridge->end();
|
|
_alerter.setBridge(nullptr);
|
|
return !bridge || !bridge->isRunning();
|
|
}
|
|
if (bridge && bridge->isStopping()) {
|
|
bridge->begin();
|
|
return bridge->isStartPending();
|
|
}
|
|
// 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 (!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
|
|
bridge = new MQTTBridge(node_info, _cli.getObserverPrefs(),
|
|
getRTCClock(), &self_id);
|
|
if (!bridge) return false;
|
|
}
|
|
if (!bridge->isRunning()) {
|
|
char device_id[65];
|
|
mesh::LocalIdentity self_id = getSelfId();
|
|
mesh::Utils::toHex(device_id, self_id.pub_key, PUB_KEY_SIZE);
|
|
bridge->setDeviceID(device_id);
|
|
bridge->setFirmwareVersion(getFirmwareVer());
|
|
bridge->setBoardModel(_cli.getBoard()->getManufacturerName());
|
|
bridge->setBuildDate(getBuildDate());
|
|
bridge->setStatsSources(this, _radio, _cli.getBoard(), _ms);
|
|
#ifdef WITH_SNMP
|
|
if (_cli.getObserverPrefs()->snmp_enabled) {
|
|
_snmp_agent.setNodeName(_prefs.node_name);
|
|
_snmp_agent.setFirmwareVersion(getFirmwareVer());
|
|
bridge->setSNMPAgent(&_snmp_agent);
|
|
}
|
|
#endif
|
|
configureBridgeFilter(bridge);
|
|
bridge->begin();
|
|
_alerter.setBridge(bridge);
|
|
}
|
|
if (_prefs.espnow_bridge_enabled) startSharedEspNowBridgeIfReady();
|
|
return bridge->isRunning() || bridge->isStartPending();
|
|
}
|
|
|
|
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;
|
|
if (bridge && bridge->isRunning()) {
|
|
startSharedEspNowBridgeIfReady();
|
|
return true;
|
|
}
|
|
configureBridgeFilter(&espnow_bridge);
|
|
espnow_bridge.begin();
|
|
return espnow_bridge.isRunning();
|
|
}
|
|
|
|
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 (bridge && bridge->isRunning()) bridge->end();
|
|
shared_espnow_retry_at = 0;
|
|
_alerter.setBridge(nullptr);
|
|
return !_prefs.bridge_enabled || setMqttBridgeState(true);
|
|
}
|
|
|
|
bool restartEspNowBridge() override {
|
|
if (espnow_bridge.isRunning()) espnow_bridge.end();
|
|
shared_espnow_retry_at = 0;
|
|
return !_prefs.espnow_bridge_enabled || setEspNowBridgeState(true);
|
|
}
|
|
#endif
|
|
|
|
|
|
void configureBridgeFilter(AbstractBridge* active_bridge) {
|
|
#if MESH_ENABLE_ROOM_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
|
|
}
|
|
|
|
bool setBridgeState(bool enable) override {
|
|
#ifdef WITH_MQTT_BRIDGE
|
|
if (!enable && bridge && bridge->isStartPending()) bridge->end();
|
|
if (enable && bridge && bridge->isStopping()) {
|
|
bridge->begin();
|
|
return bridge->isStartPending();
|
|
}
|
|
#endif
|
|
#if defined(WITH_MQTT_BRIDGE) && defined(WITH_ESPNOW_BRIDGE)
|
|
if (!enable) return setEspNowBridgeState(false) && setMqttBridgeState(false);
|
|
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
|
|
// An absent MQTT bridge is already stopped. Do not allocate one solely to
|
|
// satisfy an idempotent disable request.
|
|
if (!enable && !bridge) return true;
|
|
if (!bridge) {
|
|
#ifdef WITH_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
|
|
bridge = new MQTTBridge(node_info, _cli.getObserverPrefs(),
|
|
getRTCClock(), &self_id);
|
|
#endif
|
|
if (!bridge) return false;
|
|
}
|
|
if (enable == bridge->isRunning()) return true;
|
|
if (enable)
|
|
{
|
|
char device_id[65];
|
|
mesh::LocalIdentity self_id = getSelfId();
|
|
mesh::Utils::toHex(device_id, self_id.pub_key, PUB_KEY_SIZE);
|
|
bridge->setDeviceID(device_id);
|
|
bridge->setFirmwareVersion(getFirmwareVer());
|
|
bridge->setBoardModel(_cli.getBoard()->getManufacturerName());
|
|
bridge->setBuildDate(getBuildDate());
|
|
#ifdef WITH_MQTT_BRIDGE
|
|
bridge->setStatsSources(this, _radio, _cli.getBoard(), _ms);
|
|
#ifdef WITH_SNMP
|
|
if (_cli.getObserverPrefs()->snmp_enabled) {
|
|
_snmp_agent.setNodeName(_prefs.node_name);
|
|
_snmp_agent.setFirmwareVersion(getFirmwareVer());
|
|
bridge->setSNMPAgent(&_snmp_agent);
|
|
}
|
|
#endif
|
|
#endif
|
|
configureBridgeFilter(bridge);
|
|
bridge->begin();
|
|
#ifdef WITH_MQTT_BRIDGE
|
|
_alerter.setBridge(bridge);
|
|
#endif
|
|
}
|
|
else
|
|
{
|
|
bridge->end();
|
|
#ifdef WITH_MQTT_BRIDGE
|
|
_alerter.setBridge(nullptr);
|
|
#endif
|
|
}
|
|
return enable ? (bridge->isRunning() || bridge->isStartPending())
|
|
: !bridge->isRunning();
|
|
#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
|
|
return restartEspNowBridge() && restartMqttBridge();
|
|
#else
|
|
if (!bridge) return false;
|
|
#ifdef WITH_WEBCONFIG
|
|
if (_wc_batch_active) { // coalesced: applied once in onConfigBatchEnd()
|
|
_wc_restart_pending = true;
|
|
return true;
|
|
}
|
|
#endif
|
|
if (bridge->isRunning()) bridge->end();
|
|
char device_id[65];
|
|
mesh::LocalIdentity self_id = getSelfId();
|
|
mesh::Utils::toHex(device_id, self_id.pub_key, PUB_KEY_SIZE);
|
|
bridge->setDeviceID(device_id);
|
|
bridge->setFirmwareVersion(getFirmwareVer());
|
|
bridge->setBoardModel(_cli.getBoard()->getManufacturerName());
|
|
bridge->setBuildDate(getBuildDate());
|
|
#ifdef WITH_MQTT_BRIDGE
|
|
bridge->setStatsSources(this, _radio, _cli.getBoard(), _ms);
|
|
#endif
|
|
configureBridgeFilter(bridge);
|
|
bridge->begin();
|
|
return bridge->isRunning() || bridge->isStartPending();
|
|
#endif
|
|
}
|
|
|
|
void restartBridgeSlot(int slot) override {
|
|
#ifdef WITH_MQTT_BRIDGE
|
|
if (!bridge || !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
|
|
bridge->setSlotPreset(slot, _cli.getObserverPrefs()->mqtt_slot_preset[slot]);
|
|
#else
|
|
(void)slot;
|
|
#endif
|
|
}
|
|
|
|
#if defined(WITH_MQTT_BRIDGE)
|
|
// Pump already-queued mesh traffic before OTA blocks the loop and reboots.
|
|
// This is deliberately bounded: a jammed or duty-limited channel must not
|
|
// prevent an update, and Mesh::loop() continues to respect TX constraints.
|
|
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();
|
|
delay(1);
|
|
}
|
|
}
|
|
#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;
|
|
return true;
|
|
}
|
|
|
|
int getQueueSize() override {
|
|
return bridge ? bridge->getQueueSize() : 0;
|
|
}
|
|
|
|
bool isMqttBridgeRunning() override {
|
|
return bridge && bridge->isRunning();
|
|
}
|
|
|
|
bool requestMqttBridgeStop() override {
|
|
_alerter.setBridge(nullptr);
|
|
if (bridge) bridge->requestStop();
|
|
return true;
|
|
}
|
|
|
|
bool isMqttBridgeStopping() override {
|
|
return bridge && bridge->isStopping();
|
|
}
|
|
|
|
bool isMqttBridgeStartPending() override {
|
|
return bridge && bridge->isStartPending();
|
|
}
|
|
|
|
bool syncMqttNtp() override {
|
|
if (!bridge || !bridge->isRunning()) return false;
|
|
// Queue the sync onto the MQTT task (Core 0) without blocking: this runs on
|
|
// the Arduino loop task (serial CLI and the web config batch both drain
|
|
// here), and blocking up to 30 s would stall mesh/radio forwarding. Returns
|
|
// true once queued; verify with `get mqtt.ntp.diag`.
|
|
return bridge->requestForcedNtpSync(0);
|
|
}
|
|
|
|
bool runMqttNtpDiag(char* reply, size_t reply_size, bool verbose) override {
|
|
if (!bridge || !bridge->isRunning()) return false;
|
|
return bridge->ntpDiag(reply, reply_size, verbose);
|
|
}
|
|
#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()
|
|
|| _webconfig->isStartPending());
|
|
}
|
|
void getNodeSnapshot(WebConfigServer::NodeSnapshot& snapshot) override;
|
|
void execCommand(char* cmd, char* reply) override { handleCommand(0, cmd, reply); }
|
|
bool supportsRoomService() const override { return true; }
|
|
const uint8_t* roomPageGzip(size_t& length) const override;
|
|
bool roomRequestAuthenticated() const override { return room_web_request_authenticated; }
|
|
void processRoomRequest(const uint8_t* token, uint32_t sequence, char* request,
|
|
char* response, size_t capacity) override;
|
|
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
|
|
uint32_t getPowerSaveSleepSeconds(uint32_t max_secs) const;
|
|
uint32_t getPowerSaveSleepMilliseconds(uint32_t max_ms) const;
|
|
|
|
// To check if there is pending work
|
|
bool hasPendingWork() const;
|
|
mesh::powerdiag::Mask pendingWorkReason() const;
|
|
|
|
private:
|
|
bool isMillisTimerDue(unsigned long timestamp) const;
|
|
uint32_t limitSleepToMillisTimer(unsigned long timestamp, uint32_t sleep_ms) const;
|
|
};
|