860 lines
42 KiB
C++
860 lines
42 KiB
C++
#pragma once
|
|
|
|
#include "MeshCore.h"
|
|
#include "helpers/AbstractBridge.h"
|
|
#include "helpers/MQTTPrefs.h"
|
|
#include <ArduinoJson.h>
|
|
#include <PsychicMqttClient.h>
|
|
#include <WiFi.h>
|
|
#include <NTPClient.h>
|
|
#include <WiFiUdp.h>
|
|
#include <Timezone.h>
|
|
#include "helpers/JWTHelper.h"
|
|
#include "helpers/MQTTConnectionPolicy.h"
|
|
#include "helpers/MQTTConnectionHealth.h"
|
|
#include "helpers/MQTTTransportHealth.h"
|
|
#include "helpers/MQTTPayloadBuilder.h"
|
|
#include "helpers/MQTTPacketFilter.h"
|
|
#include "helpers/MQTTPresets.h"
|
|
#include "helpers/MQTTLifecycle.h"
|
|
#include "helpers/AlertFaultPolicy.h"
|
|
#include <atomic>
|
|
|
|
#ifdef WITH_SNMP
|
|
class MeshSNMPAgent; // Forward declaration
|
|
#endif
|
|
|
|
#ifdef ESP_PLATFORM
|
|
#include "helpers/esp32/SntpOperationCoordinator.h"
|
|
#include "helpers/WiFiNetworkPowerPolicy.h"
|
|
#include <freertos/FreeRTOS.h>
|
|
#include <freertos/task.h>
|
|
#include <freertos/queue.h>
|
|
#include <freertos/semphr.h>
|
|
#endif
|
|
|
|
#if defined(MQTT_DEBUG) && defined(ARDUINO)
|
|
#include <Arduino.h>
|
|
// USB CDC-aware debug macros: only print if Serial is ready (non-blocking check)
|
|
// Serial.availableForWrite() returns bytes available in write buffer (>0 means ready)
|
|
// This prevents hangs when USB CDC isn't ready yet (e.g., ESP32-S3 native USB)
|
|
#define MQTT_DEBUG_PRINT(F, ...) do { if (mesh::isUsbDebugLoggingEnabled() && mesh::usbLoggingPort().availableForWrite() > 0) { mesh::usbLoggingPort().printf("MQTT: " F, ##__VA_ARGS__); } } while(0)
|
|
#define MQTT_DEBUG_PRINTLN(F, ...) do { if (mesh::isUsbDebugLoggingEnabled() && mesh::usbLoggingPort().availableForWrite() > 0) { mesh::usbLoggingPort().printf("MQTT: " F "\n", ##__VA_ARGS__); } } while(0)
|
|
#else
|
|
#define MQTT_DEBUG_PRINT(...) {}
|
|
#define MQTT_DEBUG_PRINTLN(...) {}
|
|
#endif
|
|
|
|
#ifdef WITH_MQTT_BRIDGE
|
|
|
|
// Live pointers to the small set of role-specific values MQTT publishes. This
|
|
// avoids treating repeater and companion NodePrefs as though they had the same
|
|
// binary layout (they intentionally do not).
|
|
struct MQTTNodeInfo {
|
|
const char* node_name = nullptr;
|
|
const float* freq = nullptr;
|
|
const float* bw = nullptr;
|
|
const uint8_t* sf = nullptr;
|
|
const uint8_t* cr = nullptr;
|
|
const uint8_t* repeat_flag = nullptr;
|
|
bool repeat_when_nonzero = true;
|
|
// ESP32 infrastructure only; Companion and Pico retain their existing owner.
|
|
bool canonical_wifi = false;
|
|
};
|
|
|
|
// Periodic neighbors publication keys off the mesh neighbor cache (sized by
|
|
// MAX_NEIGHBOURS) and needs a persistent JSON buffer plus a second transient one
|
|
// while the mesh builds the table. PSRAM boards get it automatically; non-PSRAM
|
|
// boards must opt in per variant with MQTT_NEIGHBORS_WITHOUT_PSRAM, which spends
|
|
// internal DRAM the TLS stack also needs (see NEIGHBORS_JSON_BUFFER_SIZE). Every
|
|
// neighbors-specific member, method, and code block here is gated on
|
|
// WITH_MQTT_NEIGHBORS.
|
|
#if defined(MAX_NEIGHBOURS) && MAX_NEIGHBOURS > 0 && \
|
|
(defined(BOARD_HAS_PSRAM) || defined(MQTT_NEIGHBORS_WITHOUT_PSRAM))
|
|
#define WITH_MQTT_NEIGHBORS 1
|
|
#endif
|
|
|
|
/**
|
|
* @brief Bridge implementation using MQTT protocol for packet transport
|
|
*
|
|
* This bridge enables mesh packet transport over MQTT, allowing repeaters to
|
|
* uplink packet data to multiple MQTT brokers for monitoring and analysis.
|
|
*
|
|
* Features:
|
|
* - Up to 6 configurable MQTT connection slots (5 active with PSRAM, 2 without)
|
|
* - Built-in presets for LetsMesh Analyzer (US/EU), MeshMapper, MeshRank, Waev, CascadiaMesh
|
|
* - Custom broker support with username/password auth
|
|
* - JWT authentication with Ed25519 device signing
|
|
* - Automatic reconnection with exponential backoff
|
|
* - JSON message formatting for status, packets, and raw data
|
|
* - Packet queuing during connection issues
|
|
*
|
|
* Configuration:
|
|
* - Define WITH_MQTT_BRIDGE to enable this bridge
|
|
* - Configure slots via: set mqtt1.preset <name>, set mqtt2.preset <name>, etc.
|
|
* - Available presets: analyzer-us, analyzer-eu, meshmapper, custom, none
|
|
*/
|
|
class MQTTBridge : public AbstractBridge {
|
|
public:
|
|
// Max NTP servers in a try-list: 1 custom primary + the built-in fallbacks.
|
|
static const int kMaxNtpServers = 6;
|
|
|
|
private:
|
|
static const size_t AUTH_TOKEN_SIZE = 768;
|
|
|
|
bool _initialized = false;
|
|
mesh::RTCClock* _rtc = nullptr;
|
|
|
|
// Connection slot - each slot holds one MQTT connection
|
|
struct MQTTSlot {
|
|
PsychicMqttClient* client;
|
|
const MQTTPresetDef* preset; // Points to MQTT_PRESETS[] entry, nullptr for custom/none
|
|
bool enabled; // true when preset is not "none"
|
|
bool connected; // Updated in callbacks
|
|
bool initial_connect_done; // True after first connect() call
|
|
|
|
// JWT auth state (used by preset JWT slots and custom slots with audience set).
|
|
// nullptr until this slot first creates a token, so a slot that is unconfigured,
|
|
// capped off, or on a non-JWT preset never pays for AUTH_TOKEN_SIZE; slots that do
|
|
// use JWT keep their buffer in PSRAM where the board has it. Allocated by
|
|
// ensureSlotAuthToken() and then held for the client's lifetime -- never freed per
|
|
// reconnect (alloc/free churn is a fragmentation source) and never freed on
|
|
// teardown, because setCredentials() hands this exact pointer to the client and
|
|
// esp-mqtt re-reads it whenever a later connect() re-applies a dirtied config.
|
|
// Freed only alongside the client in destroySlotClients().
|
|
char* auth_token; // nullptr or empty string = no valid token
|
|
unsigned long token_expires_at;
|
|
unsigned long last_token_renewal;
|
|
|
|
// Custom broker settings (only used when preset_name is "custom")
|
|
char host[64];
|
|
uint16_t port;
|
|
char username[32];
|
|
char password[64];
|
|
char audience[64]; // JWT audience for custom JWT slots (empty = username/password)
|
|
char broker_uri[128]; // Persistent URI for custom slots (avoids dangling pointer)
|
|
|
|
// Reconnect backoff
|
|
uint8_t reconnect_backoff; // 0..4 index into backoff table
|
|
uint8_t max_backoff_failures; // consecutive failures at max backoff level
|
|
bool circuit_breaker_tripped; // true = stop reconnecting until reconfigured
|
|
unsigned long connected_at_ms; // millis() of last successful connect (0 = not connected);
|
|
// gates the stability-based backoff reset in maintenance
|
|
unsigned long last_reconnect_attempt;
|
|
unsigned long last_log_time; // Throttle disconnect log messages
|
|
unsigned long last_deferred_log_ms; // Throttle "connect deferred" log spam (Phase 1)
|
|
|
|
// Last error (stored for CLI diagnostics - serial-free debugging)
|
|
int32_t last_tls_err; // esp_tls_last_esp_err (0 = no error)
|
|
int32_t last_tls_stack_err; // mbedTLS stack error
|
|
int last_sock_errno; // socket errno
|
|
unsigned long last_error_time; // millis() of last error
|
|
uint32_t disconnect_count; // Number of disconnect callbacks since boot
|
|
uint32_t connect_failures; // attempts ending before onConnect
|
|
uint32_t start_failures; // rejected starts (not rejected reconnects)
|
|
unsigned long first_disconnect_time; // millis() of first disconnect after boot
|
|
|
|
// Current-outage timer (used by AlertReporter to fire faults after a sustained
|
|
// outage). Reset to 0 on each successful connect, set to millis() on first
|
|
// DISCONNECTED (including a failed initial attempt). first_disconnect_time is intentionally separate
|
|
// so the existing 'mqttN.diag' "first_disc" semantics don't change.
|
|
unsigned long current_outage_started_ms;
|
|
|
|
// Only the bridge worker mutates this state; SDK callbacks raise atomic
|
|
// latches instead. Retained across reconnects to expose recovery history.
|
|
MQTTTransportHealth::State transport_health;
|
|
};
|
|
|
|
MQTTSlot _slots[RUNTIME_MQTT_SLOTS];
|
|
|
|
// JWT username shared across all JWT-auth slots (same device identity)
|
|
char _jwt_username[70]; // Format: v1_{UPPERCASE_PUBLIC_KEY}
|
|
|
|
// Message configuration
|
|
char _origin[32];
|
|
char _iata[8];
|
|
char _device_id[65]; // Device public key (hex string)
|
|
char _firmware_version[64]; // Firmware version string
|
|
char _board_model[64]; // Board model string
|
|
char _build_date[32]; // Build date string
|
|
bool _status_enabled;
|
|
bool _packets_enabled;
|
|
bool _raw_enabled;
|
|
bool _rx_enabled;
|
|
uint8_t _tx_mode; // 0=off, 1=all TX, 2=self-advert only
|
|
unsigned long _last_status_publish;
|
|
unsigned long _status_interval;
|
|
|
|
// Packet queue for offline scenarios
|
|
// NOTE: We store a full copy of the packet (not a pointer) because the
|
|
// Dispatcher frees packets back to the static pool immediately after logRx()
|
|
// returns. Storing only a pointer would be a use-after-free.
|
|
struct QueuedPacket {
|
|
mesh::Packet packet_copy; // ~258 bytes, full value copy
|
|
unsigned long timestamp;
|
|
unsigned long next_retry_ms; // Earliest millis() when a failed send may be retried
|
|
uint8_t retry_attempts; // Bounded resend attempts for transient QoS0 publish failures
|
|
bool is_tx;
|
|
float snr;
|
|
float rssi;
|
|
// Raw radio bytes embedded at enqueue time (Core 1), never shared across cores.
|
|
// On non-PSRAM boards the queue is smaller (6 items) to offset the per-item cost.
|
|
uint8_t raw_data[256];
|
|
uint8_t raw_len;
|
|
bool has_raw_data;
|
|
};
|
|
|
|
#if defined(BOARD_HAS_PSRAM)
|
|
static const int MAX_QUEUE_SIZE = 50;
|
|
#else
|
|
// Reduced from 10: raw_data[256] adds ~256 bytes/item; 6x543 ~ 3.3 KB vs old 10x282 ~ 2.8 KB.
|
|
// Net increase is <500 bytes; _last_raw_data (256 bytes) is eliminated to offset further.
|
|
static const int MAX_QUEUE_SIZE = 6;
|
|
#endif
|
|
|
|
// FreeRTOS queue for thread-safe packet queuing
|
|
#ifdef ESP_PLATFORM
|
|
QueueHandle_t _packet_queue_handle;
|
|
TaskHandle_t _mqtt_task_handle;
|
|
// Packet queue storage: PSRAM heap on PSRAM boards, inline array on non-PSRAM boards.
|
|
// Using xQueueCreateStatic with inline storage eliminates a separate heap allocation.
|
|
uint8_t* _packet_queue_storage;
|
|
StaticQueue_t _packet_queue_struct;
|
|
#if !defined(BOARD_HAS_PSRAM)
|
|
uint8_t _packet_queue_inline[MAX_QUEUE_SIZE * sizeof(QueuedPacket)];
|
|
#endif
|
|
#else
|
|
// Fallback to circular buffer for non-ESP32 platforms
|
|
QueuedPacket _packet_queue[MAX_QUEUE_SIZE];
|
|
int _queue_head;
|
|
int _queue_tail;
|
|
#endif
|
|
int _queue_count; // Protected by queue operations or mutex
|
|
|
|
// NTP time sync
|
|
WiFiUDP _ntp_udp;
|
|
NTPClient _ntp_client;
|
|
unsigned long _last_ntp_sync;
|
|
unsigned long _ntp_refresh_retry_at;
|
|
unsigned long _ntp_refresh_started_at;
|
|
unsigned long _ntp_refresh_previous_epoch;
|
|
#ifdef ESP_PLATFORM
|
|
mesh::sntp_coord::OperationLease _ntp_refresh_operation;
|
|
#endif
|
|
bool _ntp_refresh_pending;
|
|
std::atomic<bool> _ntp_synced;
|
|
std::atomic<bool> _fresh_ntp_this_boot;
|
|
std::atomic<uint32_t> _backward_clock_reset_epoch;
|
|
bool _ntp_sync_pending; // Owned by the MQTT task; the WiFi callback only records a GOT_IP edge
|
|
MQTTConnectionPolicy::NtpReconnectLatch _ntp_reconnect_latch;
|
|
bool _slots_setup_done; // Deferred: slots set up after NTP sync
|
|
// WiFi.onEvent() handler registered once and never removed by end(); the bridge
|
|
// object is reused across restarts, so re-registering would leak handlers and
|
|
// duplicate every connect/disconnect log line. Inline-initialised so it survives
|
|
// construction and is NOT reset by end().
|
|
bool _wifi_event_registered = false;
|
|
int _max_active_slots; // Runtime limit: 5 with PSRAM, 2 without
|
|
|
|
// Pending slot reconfigure: set from CLI (Core 1), processed by MQTT task (Core 0)
|
|
volatile bool _slot_reconfigure_pending[RUNTIME_MQTT_SLOTS];
|
|
|
|
// A broker refusal can invalidate an otherwise clock-valid JWT. The event
|
|
// callback sets this byte and the bridge task consumes it.
|
|
volatile bool _slot_force_jwt_mint[RUNTIME_MQTT_SLOTS];
|
|
|
|
// Set before starting/reconnecting, cleared by connect/failed-attempt/stop.
|
|
// Must be set before the SDK call because its event task can callback at once.
|
|
std::atomic<bool> _slot_attempt_pending[RUNTIME_MQTT_SLOTS];
|
|
std::atomic<bool> _slot_transport_fault_pending[RUNTIME_MQTT_SLOTS];
|
|
std::atomic<bool> _slot_health_reset_pending[RUNTIME_MQTT_SLOTS];
|
|
// Read-only CLI snapshots: never read the owner-only State across tasks.
|
|
std::atomic<uint32_t> _slot_health_recoveries[RUNTIME_MQTT_SLOTS];
|
|
std::atomic<uint8_t> _slot_health_reason[RUNTIME_MQTT_SLOTS];
|
|
std::atomic<bool> _slot_health_recovering[RUNTIME_MQTT_SLOTS];
|
|
std::atomic<uint32_t> _slot_health_ack_wait_secs[RUNTIME_MQTT_SLOTS];
|
|
|
|
// Pending on-connect status publish: set from the onConnect callback (which
|
|
// runs on the esp-mqtt event task, NOT this bridge task), consumed by the MQTT
|
|
// task (Core 0). publishStatusToSlot() touches the shared status doc/buffer/
|
|
// origin that publishStatus() also uses, so it must run only on the bridge
|
|
// task -- the callback just raises this flag. Same idiom as
|
|
// _slot_reconfigure_pending; a single-byte volatile store/load is atomic.
|
|
volatile bool _status_publish_pending[RUNTIME_MQTT_SLOTS];
|
|
|
|
// CLI-requested forced NTP sync, marshalled onto the MQTT task (Core 0).
|
|
// All NTP I/O (_ntp_client, configTime) must run on Core 0; the CLI thread
|
|
// (Core 1) sets _ntp_force_requested and blocks in requestForcedNtpSync()
|
|
// until the task publishes the outcome via _ntp_force_result/_ntp_force_done.
|
|
// Single-requester assumption: CLI commands are serialized, so at most one
|
|
// forced sync is outstanding at a time.
|
|
volatile bool _ntp_force_requested;
|
|
volatile bool _ntp_force_done;
|
|
volatile bool _ntp_force_result;
|
|
|
|
// CLI-requested NTP connectivity diagnostic, marshalled onto the MQTT task (Core 0)
|
|
// with the same handshake as the forced sync. Probe-only: it queries each server and
|
|
// records the reported time but never calls configTime()/setCurrentTime(), so the
|
|
// system clock is left untouched. Results are written by the task and read by the CLI
|
|
// thread once _ntp_diag_done is set.
|
|
volatile bool _ntp_diag_requested;
|
|
volatile bool _ntp_diag_done;
|
|
struct NtpDiagResult {
|
|
char server[64];
|
|
bool ok;
|
|
uint32_t epoch; // server-reported UTC epoch when ok
|
|
};
|
|
NtpDiagResult _ntp_diag_results[kMaxNtpServers];
|
|
int _ntp_diag_count;
|
|
// Non-blocking, read-only NTP estimate used by the delayed clock-bootstrap
|
|
// policy. The MQTT task performs network I/O; the radio/main task only polls.
|
|
volatile bool _ntp_estimate_requested;
|
|
volatile bool _ntp_estimate_done;
|
|
volatile bool _ntp_estimate_ok;
|
|
volatile uint32_t _ntp_estimate_epoch;
|
|
|
|
// Cooperative-shutdown handshake (Phase 5). The loop task (Core 1) raises
|
|
// _stop_requested through the lifecycle Coordinator; the MQTT task (Core 0)
|
|
// sees it, tears down its own clients on Core 0 (where the mbedTLS contexts
|
|
// live), sets _stop_acked LAST, and self-terminates. end() waits for the ack;
|
|
// loop() can instead reap it without blocking. Atomic release/acquire ordering
|
|
// makes client teardown visible before Core 1 frees the queue/buffers.
|
|
std::atomic<bool> _stop_requested{false};
|
|
std::atomic<bool> _stop_acked{false};
|
|
|
|
// Timezone handling.
|
|
// _timezone_storage is inline class storage (zero heap) that is reconfigured
|
|
// via setRules() whenever the preferred timezone string changes. _timezone
|
|
// is a stable alias pointer to &_timezone_storage so existing call sites
|
|
// that accept a Timezone* keep working without modification.
|
|
Timezone _timezone_storage;
|
|
Timezone* _timezone;
|
|
|
|
// Core 1-only staging: written by storeRawRadioData(), consumed by queuePacket().
|
|
// No mutex needed - both call sites run on Core 1 in guaranteed sequence.
|
|
static const size_t LAST_RAW_DATA_SIZE = 256;
|
|
uint8_t _staged_raw[LAST_RAW_DATA_SIZE];
|
|
int _staged_raw_len = 0;
|
|
float _staged_snr = 0.0f;
|
|
float _staged_rssi = 0.0f;
|
|
bool _staged_raw_valid = false;
|
|
|
|
// Core 0-owned copy of the most recent raw data - written only by processPacketQueue()
|
|
// on Core 0, read only by publishStatus() on Core 0. No mutex required.
|
|
// On PSRAM boards: heap pointer into PSRAM. On non-PSRAM: inline array in class object.
|
|
static const size_t LAST_RAW_DATA_SIZE_MEMBER = 256; // mirrors LAST_RAW_DATA_SIZE
|
|
#if defined(BOARD_HAS_PSRAM)
|
|
uint8_t* _last_raw_data;
|
|
#else
|
|
uint8_t _last_raw_data[LAST_RAW_DATA_SIZE_MEMBER];
|
|
#endif
|
|
int _last_raw_len;
|
|
float _last_snr;
|
|
float _last_rssi;
|
|
unsigned long _last_raw_timestamp;
|
|
|
|
// One JSON serialization buffer shared by every publish path - packet, raw, and
|
|
// status all serialize on the bridge task (Core 0), so they are never in flight at
|
|
// the same time and a second buffer bought nothing. Reused rather than reallocated
|
|
// per publish (no alloc/free churn). On PSRAM boards: heap pointer into PSRAM to save
|
|
// internal heap. On non-PSRAM: inline in the class object so the allocation doesn't
|
|
// interleave with large TLS buffers at startup.
|
|
static const size_t PUBLISH_JSON_BUFFER_SIZE = 2048;
|
|
// Actual field maxima (including JSON-escaped node names) plus slot health
|
|
// exceed 768. Reuse the existing 2048-byte scratch; only the PSRAM-failure
|
|
// stack fallback grows by 256 bytes.
|
|
static const size_t STATUS_JSON_BUFFER_SIZE = 1024;
|
|
static_assert(STATUS_JSON_BUFFER_SIZE <= PUBLISH_JSON_BUFFER_SIZE,
|
|
"status payloads serialize into the shared publish buffer");
|
|
#if defined(BOARD_HAS_PSRAM)
|
|
char* _json_scratch_buffer;
|
|
#else
|
|
char _json_scratch_buffer[PUBLISH_JSON_BUFFER_SIZE];
|
|
#endif
|
|
|
|
#if defined(WITH_MQTT_NEIGHBORS)
|
|
// Persistent PSRAM copy of the neighbors-table JSON. The mesh (Core 1) builds
|
|
// the payload into its own transient buffer, hands it here via
|
|
// requestPublishNeighbors(), and the MQTT task (Core 0) publishes this copy.
|
|
// Allocated in allocateRuntimeBuffers()/freed in releaseRuntimeBuffers() like
|
|
// the other PSRAM buffers (nullptr if the allocation failed).
|
|
char* _neighbors_json_buffer;
|
|
size_t _neighbors_publish_len;
|
|
// Release/acquire handoff from the mesh loop (Core 1) to the MQTT task (Core 0).
|
|
// A second snapshot is dropped while the current one is still publishing.
|
|
std::atomic<bool> _neighbors_publish_pending;
|
|
// Written by the MQTT task (Core 0), read by the CLI (Core 1) for `get mqtt.status`.
|
|
enum NeighborsResult : uint8_t { NBR_RESULT_NONE, NBR_RESULT_OK, NBR_RESULT_FAIL };
|
|
std::atomic<uint8_t> _neighbors_last_result;
|
|
// Written by the mesh loop (Core 1), read by the CLI (Core 1). Cached schedule
|
|
// summary so the wrap-safe millis math stays on the mesh side that owns the timer.
|
|
std::atomic<uint8_t> _neighbors_phase;
|
|
std::atomic<uint32_t> _neighbors_secs_until_next;
|
|
#endif
|
|
|
|
// Routes the shared document's pools to PSRAM where the board has it, matching the
|
|
// neighbors document's allocator in MyMesh.cpp. ArduinoJson's default allocator is
|
|
// plain malloc(), which puts every per-publish pool block in internal DRAM next to
|
|
// the mbedTLS working set. A block is ARDUINOJSON_POOL_CAPACITY slots: these targets
|
|
// are 32-bit, so ARDUINOJSON_SLOT_ID_SIZE is 2 and that resolves to 128 slots =
|
|
// 1024 bytes per block, not the 4096 quoted near NEIGHBORS_DOC_POOL_BUDGET below
|
|
// (which describes a 64-bit configuration; its own byte measurements still stand).
|
|
struct JsonScratchAllocator : ArduinoJson::Allocator {
|
|
void* allocate(size_t size) override;
|
|
void deallocate(void* ptr) override;
|
|
void* reallocate(void* ptr, size_t new_size) override;
|
|
};
|
|
JsonScratchAllocator _json_allocator;
|
|
|
|
// Shared by the packet/raw/status builders, like _json_scratch_buffer above.
|
|
// Declared after _json_allocator so the allocator is constructed first.
|
|
// This was a StaticJsonDocument<N> described as an inline pool; under ArduinoJson 7
|
|
// that is a deprecated empty subclass of JsonDocument whose template argument only
|
|
// feeds capacity(), so the object is 64 bytes and every pool comes from the allocator.
|
|
JsonDocument _json_scratch_doc{&_json_allocator};
|
|
|
|
// Memory pressure monitoring (per-publish skip; see publishPacket()).
|
|
// The broader fragmentation-recovery machinery was removed in Phase 4 of
|
|
// the MQTT memory-defrag work. Persistent clients reduce MQTT storage churn;
|
|
// ESP-IDF still reallocates TLS sessions when a connection closes.
|
|
unsigned long _last_memory_check;
|
|
bool _memory_pressure = false; // Cached max-alloc verdict; re-sampled at most once per interval in publishPacket() so the heap walk isn't paid per-packet under pressure
|
|
int _skipped_publishes; // Exposed via SNMP; count of publishes skipped when max_alloc is too low
|
|
// Packets rejected by the per-slot filters before reaching the queue. Written
|
|
// on Core 1 (radio callbacks), read on Core 0 for `mqtt.stats`; a torn read of
|
|
// a diagnostic counter is harmless, so no atomic is warranted.
|
|
unsigned long _filtered_packets = 0;
|
|
|
|
// Status publish retry tracking
|
|
unsigned long _last_status_retry; // Track last retry attempt (separate from successful publish)
|
|
static const unsigned long STATUS_RETRY_INTERVAL = 30000; // Retry every 30 seconds if failed
|
|
|
|
// Device identity for JWT token creation
|
|
mesh::LocalIdentity *_identity;
|
|
|
|
// Cached connection status (updated in callbacks to avoid redundant checks)
|
|
bool _cached_has_connected_slots;
|
|
|
|
// Queue staleness tracking
|
|
unsigned long _queue_disconnected_since; // 0 = has connected slots
|
|
|
|
#ifdef WITH_SNMP
|
|
MeshSNMPAgent* _snmp_agent;
|
|
#endif
|
|
|
|
// Throttle logging
|
|
unsigned long _last_no_broker_log;
|
|
static const unsigned long NO_BROKER_LOG_INTERVAL = 30000; // Log every 30 seconds max
|
|
static const unsigned long SLOT_LOG_INTERVAL = 30000; // Log every 30 seconds max
|
|
// Retry cadence for a slot whose setup failed on an allocation. Deliberately slower
|
|
// than the first backoff rung: the failure means internal heap is exhausted, and a
|
|
// retry that succeeds immediately launches a TLS handshake.
|
|
static const unsigned long SLOT_SETUP_RETRY_INTERVAL = 60000;
|
|
unsigned long _last_config_warning; // Throttle configuration mismatch warnings
|
|
static const unsigned long CONFIG_WARNING_INTERVAL = 300000; // Log every 5 minutes max
|
|
|
|
// WiFi connection state and exponential backoff
|
|
unsigned long _last_wifi_check;
|
|
wl_status_t _last_wifi_status;
|
|
bool _wifi_status_initialized;
|
|
std::atomic<uint64_t> _wifi_outage_bits;
|
|
unsigned long _last_wifi_reconnect_attempt;
|
|
uint8_t _wifi_reconnect_backoff_attempt; // 0..5 -> 15s, 30s, 60s, 120s, 300s; reset on connect
|
|
unsigned long _last_slot_reconnect_ms; // guards against concurrent TLS handshakes (15 s inter-slot gap)
|
|
#ifdef ESP_PLATFORM
|
|
mesh::wifi::NetworkPowerPolicy _network_power_policy;
|
|
std::atomic<bool> _network_suspended{false};
|
|
uint8_t _background_ntp_server = 0;
|
|
uint32_t _network_publish_acks[RUNTIME_MQTT_SLOTS] = {};
|
|
bool _network_espnow_restore_pending = false;
|
|
uint32_t _network_espnow_restore_at = 0;
|
|
bool serviceNetworkPower(uint32_t now);
|
|
void serviceEspNowChannelRestore(uint32_t now);
|
|
void serviceBackgroundNtp(uint32_t now);
|
|
#endif
|
|
|
|
AlertFaultPolicy::OutageSnapshot wifiOutage() const {
|
|
return AlertFaultPolicy::unpackOutageSnapshot(
|
|
_wifi_outage_bits.load(std::memory_order_acquire));
|
|
}
|
|
void setWifiOutage(AlertFaultPolicy::OutageSnapshot snapshot) {
|
|
_wifi_outage_bits.store(AlertFaultPolicy::packOutageSnapshot(snapshot),
|
|
std::memory_order_release);
|
|
}
|
|
|
|
// Optional pointers for collecting stats internally (set by mesh if available)
|
|
mesh::Dispatcher* _dispatcher; // For air times and errors
|
|
mesh::Radio* _radio; // For noise floor
|
|
mesh::MainBoard* _board; // For battery voltage
|
|
mesh::MillisecondClock* _ms; // For uptime
|
|
|
|
// Topic building
|
|
enum MQTTMessageType { MSG_STATUS, MSG_PACKETS, MSG_RAW, MSG_NEIGHBORS };
|
|
bool buildTopicForSlot(int index, MQTTMessageType type, char* topic_buf, size_t buf_size);
|
|
bool substituteTopicTemplate(const char* tmpl, MQTTMessageType type, int slot_index, char* buf, size_t buf_size);
|
|
uint8_t eligiblePacketSlots(uint8_t packet_type, MQTTMessageType type);
|
|
bool shouldQueuePacketType(uint8_t packet_type, bool& filtered);
|
|
|
|
// Internal methods - slot management
|
|
// Lifetime model (Phase 1 of MQTT memory-defrag):
|
|
// - ensureSlotClient() allocates this slot's PsychicMqttClient and registers its
|
|
// persistent callbacks. Called from setupSlot() on a slot's first setup, so an
|
|
// unconfigured or capped-off slot never pays for a client it cannot use.
|
|
// - destroySlotClients() disconnects and deletes each client. Runs once in end().
|
|
// - setupSlot() ensures the client exists, then configures it (server,
|
|
// credentials, CA) and calls connect(). Safe to call again to reconfigure.
|
|
// - teardownSlot() only disconnects - it never deletes the client. Leaves
|
|
// MQTT storage and transport handles ready for a subsequent setupSlot().
|
|
// This avoids wrapper/configuration allocation cycles. TLS sessions and their
|
|
// record buffers are still freed and recreated by ESP-IDF on reconnect.
|
|
bool ensureSlotClient(int index); // Allocate this slot's persistent client + callbacks on first use
|
|
bool ensureSlotAuthToken(int index); // Allocate this slot's JWT token buffer on first token creation
|
|
void releaseSlotAuthToken(int index);// Free the token buffer (only with the client -- see MQTTSlot)
|
|
void destroySlotClients(bool force = false);
|
|
bool setupSlot(int index); // Configure and connect the slot; false = not activated
|
|
// Single definition of "this slot holds one of the _max_active_slots positions":
|
|
// it is enabled and has been through a successful setupSlot(). Startup, the
|
|
// setup-retry path, and live reconfigure all gate on these so the cap cannot be
|
|
// exceeded by one route while another enforces it.
|
|
int activatedSlotCount() const;
|
|
bool canActivateSlot(int index) const;
|
|
bool hasPendingSlotConnection() const;
|
|
bool canStartSlotConnection(int index) const;
|
|
void teardownSlot(int index, bool force = false);
|
|
esp_err_t reconnectSlotClient(int index);
|
|
bool recoverSlotTransport(int index, uint32_t now);
|
|
void serviceSlotTransportHealth(uint32_t now);
|
|
void updateSlotTransportDiag(int index, uint32_t now);
|
|
void maintainSlotConnections(); // Maintain all slot connections (token renewal, reconnect)
|
|
void maintainSlotConnection(int index, unsigned long now_millis, unsigned long current_time, bool time_synced, bool& reconnect_attempted, bool& teardown_attempted);
|
|
bool createSlotAuthToken(int index); // Create/renew JWT token for a slot
|
|
unsigned long slotTokenLifetime(int index) const; // effective JWT lifetime (preset/default minus slot stagger), seconds
|
|
// payload_len is the serialized length the builder already returned. Every caller
|
|
// knows it, and passing it avoids re-scanning up to 2 KB of JSON per destination
|
|
// slot (and up to NEIGHBORS_JSON_BUFFER_SIZE per neighbor snapshot).
|
|
bool publishToSlot(int index, const char* topic, const char* payload, size_t payload_len, bool retained = false, uint8_t qos = 0);
|
|
bool publishToAllSlots(const char* topic, const char* payload, size_t payload_len, bool retained = false, uint8_t qos = 0);
|
|
void publishStatusToSlot(int index);
|
|
MQTTConnHealth collectConnHealth() const;
|
|
void updateCachedConnectionStatus();
|
|
|
|
void processPacketQueue();
|
|
bool publishStatus(); // Returns true if status was successfully published
|
|
bool handleWiFiConnection(unsigned long now);
|
|
|
|
// FreeRTOS task function (runs on Core 0)
|
|
#ifdef ESP_PLATFORM
|
|
static void mqttTask(void* parameter);
|
|
void mqttTaskLoop(); // Main loop for MQTT task
|
|
bool waitUnlessStopping(uint32_t delay_ms);
|
|
bool initializeWiFiInTask(); // False while an AP owns the shared driver.
|
|
#endif
|
|
bool publishPacket(mesh::Packet* packet, bool is_tx, bool& has_eligible_target,
|
|
const uint8_t* raw_data = nullptr, int raw_len = 0,
|
|
float snr = 0.0f, float rssi = 0.0f);
|
|
bool publishRaw(mesh::Packet* packet, bool& has_eligible_target);
|
|
#if defined(WITH_MQTT_NEIGHBORS)
|
|
// Publishes the pending _neighbors_json_buffer to every connected slot's
|
|
// neighbors topic. Runs on the MQTT task (Core 0) only.
|
|
bool publishNeighbors();
|
|
#endif
|
|
void queuePacket(mesh::Packet* packet, bool is_tx);
|
|
void dequeuePacket();
|
|
bool isAnySlotConnected();
|
|
void refreshNTP(); // Start a lightweight scheduled NTP refresh (non-blocking)
|
|
void pollNtpRefresh(uint32_t now); // Copy a completed async refresh into MeshCore's RTC
|
|
void cancelNtpRefresh();
|
|
void runNtpDiagProbe(); // Probe every server for connectivity; never sets the clock. Core 0 only.
|
|
void runNtpEstimateProbe(); // Query the first usable server without changing any clock. Core 0 only.
|
|
// Populates dst_out/std_out with TimeChangeRules for the given IANA or
|
|
// abbreviation string. Returns false if the string is not recognized
|
|
// (callers should fall back to UTC). Zero-allocation.
|
|
static bool timezoneRulesFromString(const char* tz_string, TimeChangeRule& dst_out, TimeChangeRule& std_out);
|
|
void checkConfigurationMismatch();
|
|
bool isIATAValid() const;
|
|
bool isSlotReady(int index, char* reason_buf = nullptr, size_t reason_size = 0) const;
|
|
|
|
void optimizeMqttClientConfig(PsychicMqttClient* client, bool needs_large_buffer = false);
|
|
void getClientVersion(char* buffer, size_t buffer_size) const;
|
|
void logMemoryStatus();
|
|
void refreshOriginFromPrefs();
|
|
void applyTimezoneRules();
|
|
// begin()/end()-scoped PSRAM buffers. Each allocation is independent so a
|
|
// transient heap shortage degrades to the existing stack fallback instead
|
|
// of making the bridge unusable.
|
|
void allocateRuntimeBuffers();
|
|
void releaseRuntimeBuffers();
|
|
void finishStopped(); // Core 1 only, after lifecycle resource release
|
|
|
|
// --- Cooperative lifecycle (Phase 5) ---------------------------------------
|
|
// The pure state machine, bounded stop timeout, and OTA barrier live in
|
|
// src/helpers/MQTTLifecycle.h and are host-tested by test/test_mqtt_lifecycle/.
|
|
// This nested Ops binds that spec to FreeRTOS/PsychicMqttClient. The
|
|
// Coordinator is owned and driven ONLY by the loop task (Core 1) from
|
|
// begin()/requestStop()/end()/loop(); the MQTT task (Core 0) communicates solely through the
|
|
// _stop_requested/_stop_acked flags above. Methods are defined in the .cpp.
|
|
class LifecycleOps : public MQTTLifecycle::Ops {
|
|
public:
|
|
explicit LifecycleOps(MQTTBridge* bridge) : _b(bridge) {}
|
|
uint32_t nowMs() override;
|
|
void startTask() override;
|
|
void deliverStop() override;
|
|
void releaseResources() override;
|
|
void onStopComplete(bool clean) override;
|
|
private:
|
|
MQTTBridge* _b;
|
|
};
|
|
LifecycleOps _lifecycle_ops;
|
|
MQTTLifecycle::Coordinator _lifecycle;
|
|
|
|
// Observer config (MQTT/WiFi/timezone/SNMP/alert), persisted to /mqtt_prefs.
|
|
MQTTPrefs* _obs = nullptr;
|
|
MQTTNodeInfo _node_info;
|
|
bool _manage_wifi;
|
|
#ifdef ESP_PLATFORM
|
|
// Frozen before Core 0 starts; changed only after its stop barrier.
|
|
char _wifi_ssid[32] = {};
|
|
char _wifi_password[65] = {};
|
|
uint8_t _wifi_power_save = 0;
|
|
bool _wifi_configured = false;
|
|
bool prepareWiFiCredentials();
|
|
void beginWiFiStation();
|
|
#endif
|
|
|
|
const char* repeatStatus() const;
|
|
|
|
public:
|
|
MQTTBridge(const MQTTNodeInfo& node_info, MQTTPrefs *obs,
|
|
mesh::RTCClock *rtc, mesh::LocalIdentity *identity,
|
|
bool manage_wifi = true);
|
|
|
|
void begin() override;
|
|
void end() override;
|
|
// Core 1: request cooperative teardown without waiting or freeing resources.
|
|
// Call loop() to reap the acknowledgment; end() remains the OTA stop barrier.
|
|
void requestStop();
|
|
bool isStopping() const { return _lifecycle.isStopInProgress(); }
|
|
bool isRunning() const override { return _initialized; }
|
|
// Configured and still scheduling recovery, but transports are quiesced.
|
|
// Do not conflate automatic suspension with a persisted bridge disable.
|
|
bool isNetworkSuspended() const {
|
|
#ifdef ESP_PLATFORM
|
|
return _network_suspended.load(std::memory_order_acquire);
|
|
#else
|
|
return false;
|
|
#endif
|
|
}
|
|
void loop() override;
|
|
void onPacketReceived(mesh::Packet *packet) override;
|
|
void sendPacket(mesh::Packet *packet) override;
|
|
|
|
/**
|
|
* Configure a slot with a preset name. Call this when the user runs
|
|
* "set mqttN.preset <name>". Handles teardown of old connection and
|
|
* setup of new one.
|
|
*
|
|
* @param slot_index Slot index (0 to RUNTIME_MQTT_SLOTS-1)
|
|
* @param preset_name Preset name: "analyzer-us", "analyzer-eu", "nz-analyzer", "meshmapper", "custom", "none"
|
|
*/
|
|
void setSlotPreset(int slot_index, const char* preset_name);
|
|
void applySlotPreset(int slot_index, const char* preset_name);
|
|
|
|
/**
|
|
* Configure custom broker settings for a slot. Only applies when the
|
|
* slot's preset is "custom".
|
|
*
|
|
* @param slot_index Slot index (0 to RUNTIME_MQTT_SLOTS-1)
|
|
* @param host Broker hostname
|
|
* @param port Broker port
|
|
* @param username MQTT username (empty for anonymous)
|
|
* @param password MQTT password (empty for anonymous)
|
|
*/
|
|
void setSlotCustomBroker(int slot_index, const char* host, uint16_t port,
|
|
const char* username, const char* password);
|
|
|
|
void setOrigin(const char* origin);
|
|
void setIATA(const char* iata);
|
|
void setDeviceID(const char* device_id);
|
|
void setFirmwareVersion(const char* firmware_version);
|
|
void setBoardModel(const char* board_model);
|
|
void setBuildDate(const char* build_date);
|
|
void storeRawRadioData(const uint8_t* raw_data, int len, float snr, float rssi);
|
|
void setMessageTypes(bool status, bool packets, bool raw);
|
|
|
|
#if defined(WITH_MQTT_NEIGHBORS)
|
|
// Single source of truth for the neighbors JSON size, used by both the bridge's
|
|
// persistent buffer and the mesh's transient build buffer.
|
|
#if defined(BOARD_HAS_PSRAM)
|
|
static const size_t NEIGHBORS_JSON_BUFFER_SIZE = 10240;
|
|
#else
|
|
// Without PSRAM all three neighbors allocations (persistent buffer, transient
|
|
// build buffer, ArduinoJson pool) come out of internal DRAM, which each TLS
|
|
// slot also needs ~40 KB of. A quarter-size buffer keeps the peak near 13 KB
|
|
// instead of ~35 KB; the builder drops the tail and sets "truncated" once the
|
|
// next entry will not fit, so the table degrades instead of failing.
|
|
static const size_t NEIGHBORS_JSON_BUFFER_SIZE = 4096;
|
|
#endif
|
|
|
|
// The ArduinoJson pool is NOT bounded by the text buffer: v7 hands out pool
|
|
// blocks in fixed 4096-byte chunks, so a table that just fits the text buffer
|
|
// can still need well over it in pool. Budgeting the pool at the text size
|
|
// starves it, and a starved pool sets doc.overflowed() -- which drops the whole
|
|
// publish instead of truncating. Measured need is 12541 B for 50 entries and
|
|
// 4286 B for 21, so allow a spare block over each.
|
|
#if defined(BOARD_HAS_PSRAM)
|
|
static const size_t NEIGHBORS_DOC_POOL_BUDGET = 16384;
|
|
#else
|
|
static const size_t NEIGHBORS_DOC_POOL_BUDGET = 12288;
|
|
#endif
|
|
|
|
// Entries per publish. One 4096-byte pool block holds 21, so staying under
|
|
// that keeps the non-PSRAM pool to a single block (~4.3 KB rather than
|
|
// ~8.4 KB) and the whole publish peak near 13 KB of internal DRAM. PSRAM
|
|
// boards are limited only by the text buffer and the neighbour cache.
|
|
#if defined(BOARD_HAS_PSRAM)
|
|
static const int NEIGHBORS_MAX_PUBLISH_ENTRIES = MAX_NEIGHBOURS;
|
|
#else
|
|
static const int NEIGHBORS_MAX_PUBLISH_ENTRIES = 20;
|
|
#endif
|
|
|
|
// Discovery budgets each row with a 64-character public key, UINT32_MAX
|
|
// heard age and a status of at least "timeout". Its shortest measured JSON
|
|
// is 144 bytes. Reserve scope results only for rows that could fit the
|
|
// existing JSON buffer, plus one overflow sentinel. Serializer tests keep
|
|
// this lower bound tied to completeNeighborDiscoverEntry's measurement.
|
|
// The neighbor cache and actual per-publish entry limit remain unchanged.
|
|
static const size_t NEIGHBORS_MIN_DISCOVERY_ENTRY_JSON_BYTES = 144;
|
|
static const size_t NEIGHBORS_SCOPE_RESULT_CAPACITY =
|
|
(NEIGHBORS_MAX_PUBLISH_ENTRIES < NEIGHBORS_JSON_BUFFER_SIZE / NEIGHBORS_MIN_DISCOVERY_ENTRY_JSON_BYTES
|
|
? NEIGHBORS_MAX_PUBLISH_ENTRIES
|
|
: NEIGHBORS_JSON_BUFFER_SIZE / NEIGHBORS_MIN_DISCOVERY_ENTRY_JSON_BYTES) + 1;
|
|
|
|
// Called by the mesh (Core 1) once a neighbor-discovery pass has built the
|
|
// table JSON. Copies it into the persistent PSRAM buffer and raises the
|
|
// publish-pending flag for the MQTT task; a request is dropped if one is
|
|
// already in flight or the buffer is unavailable.
|
|
void requestPublishNeighbors(const char* json, size_t len);
|
|
|
|
// Periodic-neighbors schedule, reported by the mesh loop for `get mqtt.status`.
|
|
// The mesh owns the timer; the bridge only caches the summary so the wrap-safe
|
|
// millis math stays on the side that already has those helpers.
|
|
enum NeighborsPhase : uint8_t {
|
|
NBR_SCHEDULED, // waiting for the next publish; secs_until_next is valid
|
|
NBR_ACTIVE, // zero-hop refresh or scope queries in flight
|
|
NBR_DUE, // publish is due, waiting on the bridge/WiFi to come up
|
|
};
|
|
void setNeighborsSchedule(NeighborsPhase phase, uint32_t secs_until_next);
|
|
#endif
|
|
|
|
int getConnectedBrokers() const;
|
|
int getQueueSize() const;
|
|
bool isReady() const;
|
|
/** True only after a CLEAN cooperative stop -- the MQTT task acknowledged
|
|
* its teardown before resources were released. A timed-out/forced stop returns false so
|
|
* OTA flashing is withheld until a clean start/stop cycle. Mirrors
|
|
* MQTTLifecycle::mayBeginFlash(); read on the loop task (Core 1). */
|
|
bool canFlashAfterStop() const { return _lifecycle.mayBeginFlash(); }
|
|
|
|
static unsigned long getWifiConnectedAtMillis();
|
|
|
|
AlertFaultPolicy::OutageSnapshot getWifiOutageSnapshot() const {
|
|
return wifiOutage();
|
|
}
|
|
|
|
/**
|
|
* Per-slot outage accessors used by AlertReporter to detect prolonged
|
|
* MQTT broker outages. Indices are 0..RUNTIME_MQTT_SLOTS-1.
|
|
*
|
|
* - getSlotCurrentOutageStartMs(): millis() of the current outage start
|
|
* (0 when the slot is connected). Reset on each reconnect.
|
|
* - isSlotEnabledAndAttempted(): true when the slot is enabled (preset
|
|
* != "none") and has reached at least one connect attempt - i.e. it is
|
|
* meaningful to alarm on its connection state.
|
|
* - getSlotPresetName(): preset name for friendly status text. Returns
|
|
* "custom"/"none"/preset->name; never null.
|
|
*/
|
|
unsigned long getSlotCurrentOutageStartMs(int slot_index) const;
|
|
bool isSlotEnabledAndAttempted(int slot_index) const;
|
|
const char* getSlotPresetName(int slot_index) const;
|
|
static int getRuntimeSlotCount() { return RUNTIME_MQTT_SLOTS; }
|
|
/** Max slots that can be connected at once: 5 with PSRAM, 2 without (each
|
|
* WSS/TLS connection needs ~40KB for mbedTLS buffers). This is the number of
|
|
* usefully-configurable servers; RUNTIME_MQTT_SLOTS carries a spare for
|
|
* reconfiguration. Safe to call before begin(). */
|
|
static int getMaxActiveSlots();
|
|
/** Resolved origin for MQTT JSON: node_name when mqtt_origin is empty, else mqtt_origin (with quote stripping). */
|
|
static void getEffectiveMqttOrigin(const char* node_name, const MQTTPrefs* obs,
|
|
char* buf, size_t buf_size);
|
|
static const char* effectiveNtpPrimary(const MQTTPrefs* obs);
|
|
/** Sync system clock via NTP. force=true bypasses the 5s post-sync rate limit.
|
|
* primary_only=true tests just the effective primary server (no fallback walk) so a
|
|
* mistyped hostname fails fast instead of blocking through the whole fallback list.
|
|
* Performs blocking NTP I/O and must only be called from the MQTT task (Core 0).
|
|
* Other tasks (e.g. the CLI on Core 1) must use requestForcedNtpSync() instead. */
|
|
bool syncTimeWithNTP(bool force = false, bool primary_only = false);
|
|
/** Request a forced clock refresh from another task (e.g. CLI on Core 1).
|
|
* Marshals NTP I/O onto the MQTT task, then blocks up to timeout_ms for the
|
|
* result. This validation request tests only the configured primary server
|
|
* and never accepts a retained RTC/system clock as success, so a true result
|
|
* from a nonzero-timeout call proves a fresh NTP response. A zero timeout is
|
|
* fire-and-forget and proves only that the request was queued. */
|
|
bool requestForcedNtpSync(uint32_t timeout_ms = 30000);
|
|
/** Probe every configured NTP server (custom primary + built-in fallbacks) for
|
|
* connectivity and report each server's reported time WITHOUT touching the system
|
|
* clock. Marshals the probe onto the MQTT task (Core 0), then formats on the caller's
|
|
* thread. verbose=true prints a detailed table to the serial console and leaves a short
|
|
* summary in reply; verbose=false fills reply with a compact "<server> ok|fail" list
|
|
* (for LoRa). Returns false if the bridge is not running. */
|
|
bool ntpDiag(char* reply, size_t reply_size, bool verbose);
|
|
/** Queue a read-only NTP query on the MQTT task without blocking the radio loop. */
|
|
bool requestNtpTimeEstimate();
|
|
/** Poll/consume the queued estimate. finished=false means it is still pending. */
|
|
bool takeNtpTimeEstimate(uint32_t& epoch, bool& finished);
|
|
/** True after the bridge established a usable clock this boot. This may be a
|
|
* plausible retained RTC/system clock accepted after NTP was unavailable. */
|
|
bool hasNtpTime() const {
|
|
return _ntp_synced.load(std::memory_order_acquire);
|
|
}
|
|
/** True only after a real NTP/SNTP reply was accepted this boot. */
|
|
bool hasFreshNtpThisBoot() const {
|
|
return _fresh_ntp_this_boot.load(std::memory_order_acquire);
|
|
}
|
|
/** Apply a queued backward-clock uniqueness repair from the role's main
|
|
* loop. MQTT performs network work on another core on ESP32, while
|
|
* RTCClock::getCurrentTimeUnique() is main-loop owned. */
|
|
bool servicePendingClockCorrection();
|
|
static void formatMqttStatusReply(char* buf, size_t bufsize, const MQTTPrefs* obs);
|
|
/** On-demand publish-health + heap snapshot for `get mqtt.stats` (per-slot ok/err,
|
|
* outbox size, free/max heap, queue depth). */
|
|
static void formatMqttStatsReply(char* buf, size_t bufsize);
|
|
// Structured status used by the browser configuration page. Publish
|
|
// counters are optional because not every supported PsychicMqttClient
|
|
// version exposes them.
|
|
struct SlotStatusSnapshot {
|
|
const char* name;
|
|
const char* state;
|
|
bool has_publish_counts;
|
|
unsigned long publish_ok;
|
|
unsigned long publish_err;
|
|
// Raw packet-type allowlist. Kept as the mask rather than canonical text so
|
|
// the 1 KB stats JSON stays compact; the portal renders it.
|
|
uint16_t filter_mask;
|
|
};
|
|
static bool getSlotStatusSnapshot(int slot_index, SlotStatusSnapshot* out);
|
|
/** True when WiFi is set and at least one MQTT slot can run (preset + custom host if needed). */
|
|
static bool isConfigValid(const MQTTPrefs* obs, bool canonical_wifi = false);
|
|
static void formatSlotDiagReply(char* buf, size_t bufsize, int slot_index);
|
|
static uint8_t getLastWifiDisconnectReason();
|
|
static unsigned long getLastWifiDisconnectTime();
|
|
static const char* wifiReasonStr(uint8_t reason);
|
|
static const char* tlsErrorStr(int32_t err);
|
|
|
|
void setStatsSources(mesh::Dispatcher* dispatcher, mesh::Radio* radio,
|
|
mesh::MainBoard* board, mesh::MillisecondClock* ms);
|
|
|
|
#ifdef WITH_SNMP
|
|
void setSNMPAgent(MeshSNMPAgent* agent) { _snmp_agent = agent; }
|
|
#endif
|
|
};
|
|
|
|
#endif
|