Enhance MQTT implementation by adding optional SNMP support, allowing for monitoring and management of MQTT statistics. Update configuration options to include SNMP settings, and modify the MQTTBridge to integrate SNMP agent functionality. Additionally, improve documentation for the MQTT observer setup and expand the number of concurrent MQTT connection slots from 3 to 5, enhancing overall flexibility and performance.

This commit is contained in:
agessaman committed 2026-03-28 17:08:32 -07:00
1 parent cc17c6b745
commit 202acacfe1
13 files changed
+706 -96

No files matched your search

+21 -7
View File
@@ -159,7 +159,9 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) {
remaining -= to_read;
}
file.read((uint8_t *)&_prefs->rx_boosted_gain, sizeof(_prefs->rx_boosted_gain)); // 290
// next: 291
file.read((uint8_t *)&_prefs->snmp_enabled, sizeof(_prefs->snmp_enabled)); // 291
file.read((uint8_t *)&_prefs->snmp_community, sizeof(_prefs->snmp_community)); // 292
// next: 316
// sanitise bad pref values
_prefs->rx_delay_base = constrain(_prefs->rx_delay_base, 0, 20.0f);
@@ -189,6 +191,8 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) {
_prefs->advert_loc_policy = constrain(_prefs->advert_loc_policy, 0, 2);
_prefs->rx_boosted_gain = constrain(_prefs->rx_boosted_gain, 0, 1); // boolean
_prefs->snmp_enabled = constrain(_prefs->snmp_enabled, 0, 1);
_prefs->snmp_community[sizeof(_prefs->snmp_community) - 1] = '\0'; // ensure null terminated
file.close();
}
@@ -274,7 +278,9 @@ void CommonCLI::savePrefs(FILESYSTEM* fs) {
remaining -= to_write;
}
file.write((uint8_t *)&_prefs->rx_boosted_gain, sizeof(_prefs->rx_boosted_gain)); // 290
// next: 291
file.write((uint8_t *)&_prefs->snmp_enabled, sizeof(_prefs->snmp_enabled)); // 291
file.write((uint8_t *)&_prefs->snmp_community, sizeof(_prefs->snmp_community)); // 292
// next: 316
file.close();
}
@@ -304,11 +310,7 @@ static void setMQTTPrefsDefaults(MQTTPrefs* prefs) {
strncpy(prefs->mqtt_slot_preset[i], "none", sizeof(prefs->mqtt_slot_preset[i]) - 1);
prefs->mqtt_slot_preset[i][sizeof(prefs->mqtt_slot_preset[i]) - 1] = '\0';
}
#ifdef MQTT_WIFI_POWER_SAVE_DEFAULT
prefs->wifi_power_save = MQTT_WIFI_POWER_SAVE_DEFAULT; // 0=min, 1=none, 2=max
#else
prefs->wifi_power_save = 0; // Default to WIFI_PS_MIN_MODEM (0=min)
#endif
prefs->wifi_power_save = 1; // Default to none (0=min, 1=none, 2=max)
// String fields are already zero-initialized by memset
}
@@ -714,6 +716,10 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch
} else {
strcpy(reply, "> strict");
}
} else if (memcmp(config, "snmp.community", 14) == 0) {
sprintf(reply, "> %s", _prefs->snmp_community);
} else if (memcmp(config, "snmp", 4) == 0 && (config[4] == '\0' || config[4] == '\n' || config[4] == '\r')) {
strcpy(reply, _prefs->snmp_enabled ? "> on" : "> off");
} else if (memcmp(config, "tx", 2) == 0 && (config[2] == 0 || config[2] == ' ')) {
sprintf(reply, "> %d", (int32_t) _prefs->tx_power_dbm);
} else if (memcmp(config, "freq", 4) == 0) {
@@ -1104,6 +1110,14 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch
savePrefs();
strcpy(reply, "OK");
}
} else if (memcmp(config, "snmp.community ", 15) == 0) {
StrHelper::strncpy(_prefs->snmp_community, &config[15], sizeof(_prefs->snmp_community));
savePrefs();
strcpy(reply, "OK - restart to apply");
} else if (memcmp(config, "snmp ", 5) == 0) {
_prefs->snmp_enabled = memcmp(&config[5], "on", 2) == 0;
savePrefs();
strcpy(reply, "OK - restart to apply");
} else if (memcmp(config, "tx ", 3) == 0) {
_prefs->tx_power_dbm = atoi(&config[3]);
savePrefs();
+6 -2
View File
@@ -72,7 +72,7 @@ struct NodePrefs { // persisted to file
// WiFi settings
char wifi_ssid[32]; // WiFi SSID
char wifi_password[64]; // WiFi password
uint8_t wifi_power_save; // WiFi power save mode: 0=min, 1=none, 2=max (default: 0=min)
uint8_t wifi_power_save; // WiFi power save mode: 0=min, 1=none, 2=max (default: 1=none)
// Timezone settings
char timezone_string[32]; // Timezone string (e.g., "America/Los_Angeles")
@@ -96,6 +96,10 @@ struct NodePrefs { // persisted to file
char mqtt_slot_topic[MAX_MQTT_SLOTS][96]; // Per-slot custom topic template (custom preset only)
uint8_t loop_detect;
// SNMP settings (optional, only used when WITH_SNMP is defined)
uint8_t snmp_enabled; // boolean: 0=off, 1=on
char snmp_community[24]; // community string (default "public")
};
#ifdef WITH_MQTT_BRIDGE
@@ -137,7 +141,7 @@ struct MQTTPrefs {
// WiFi settings
char wifi_ssid[32]; // WiFi SSID
char wifi_password[64]; // WiFi password
uint8_t wifi_power_save; // WiFi power save mode: 0=min, 1=none, 2=max (default: 0=min)
uint8_t wifi_power_save; // WiFi power save mode: 0=min, 1=none, 2=max (default: 1=none)
// Timezone settings
char timezone_string[32]; // Timezone string (e.g., "America/Los_Angeles")
+123
View File
@@ -0,0 +1,123 @@
#ifdef WITH_SNMP
#include "SNMPAgent.h"
#include <esp_heap_caps.h>
#define SNMP_PORT 161
MeshSNMPAgent::MeshSNMPAgent()
: _snmp("public"),
_running(false),
_uptime_secs(0),
_packets_recv(0), _packets_sent(0), _recv_errors(0),
_noise_floor(0), _last_rssi(0), _last_snr(0),
_sent_flood(0), _sent_direct(0), _recv_flood(0), _recv_direct(0),
_total_air_time_secs(0),
_mqtt_connected_slots(0), _mqtt_queue_depth(0), _mqtt_skipped_publishes(0),
_free_heap(0), _max_alloc(0), _internal_free(0), _psram_free(0),
_wifi_rssi(0)
{
_firmware_version[0] = '\0';
_node_name[0] = '\0';
}
void MeshSNMPAgent::begin(const char* community) {
if (_running) return;
_snmp = SNMPAgent(community);
_snmp.setUDP(&_udp);
_snmp.begin();
// System group (.1.x.0)
_snmp.addIntegerHandler(MESHCORE_OID_BASE ".1.1.0", &_uptime_secs);
_snmp.addReadOnlyStaticStringHandler(MESHCORE_OID_BASE ".1.2.0", _firmware_version, sizeof(_firmware_version));
_snmp.addReadOnlyStaticStringHandler(MESHCORE_OID_BASE ".1.3.0", _node_name, sizeof(_node_name));
// Radio group (.2.x.0)
_snmp.addIntegerHandler(MESHCORE_OID_BASE ".2.1.0", &_packets_recv);
_snmp.addIntegerHandler(MESHCORE_OID_BASE ".2.2.0", &_packets_sent);
_snmp.addIntegerHandler(MESHCORE_OID_BASE ".2.3.0", &_recv_errors);
_snmp.addIntegerHandler(MESHCORE_OID_BASE ".2.4.0", &_noise_floor);
_snmp.addIntegerHandler(MESHCORE_OID_BASE ".2.5.0", &_last_rssi);
_snmp.addIntegerHandler(MESHCORE_OID_BASE ".2.6.0", &_last_snr);
_snmp.addIntegerHandler(MESHCORE_OID_BASE ".2.7.0", &_sent_flood);
_snmp.addIntegerHandler(MESHCORE_OID_BASE ".2.8.0", &_sent_direct);
_snmp.addIntegerHandler(MESHCORE_OID_BASE ".2.9.0", &_recv_flood);
_snmp.addIntegerHandler(MESHCORE_OID_BASE ".2.10.0", &_recv_direct);
_snmp.addIntegerHandler(MESHCORE_OID_BASE ".2.11.0", &_total_air_time_secs);
// MQTT group (.3.x.0)
_snmp.addIntegerHandler(MESHCORE_OID_BASE ".3.1.0", &_mqtt_connected_slots);
_snmp.addIntegerHandler(MESHCORE_OID_BASE ".3.2.0", &_mqtt_queue_depth);
_snmp.addIntegerHandler(MESHCORE_OID_BASE ".3.3.0", &_mqtt_skipped_publishes);
// Memory group (.4.x.0)
_snmp.addIntegerHandler(MESHCORE_OID_BASE ".4.1.0", &_free_heap);
_snmp.addIntegerHandler(MESHCORE_OID_BASE ".4.2.0", &_max_alloc);
_snmp.addIntegerHandler(MESHCORE_OID_BASE ".4.3.0", &_internal_free);
_snmp.addIntegerHandler(MESHCORE_OID_BASE ".4.4.0", &_psram_free);
// Network group (.5.x.0)
_snmp.addIntegerHandler(MESHCORE_OID_BASE ".5.1.0", &_wifi_rssi);
_snmp.sortHandlers();
_running = true;
}
void MeshSNMPAgent::loop() {
if (!_running) return;
// Update memory and network stats locally (we're on Core 0 with WiFi)
_free_heap = (int)ESP.getFreeHeap();
_max_alloc = (int)ESP.getMaxAllocHeap();
_internal_free = (int)heap_caps_get_free_size(MALLOC_CAP_INTERNAL);
#ifdef BOARD_HAS_PSRAM
_psram_free = (int)heap_caps_get_free_size(MALLOC_CAP_SPIRAM);
#else
_psram_free = 0;
#endif
if (WiFi.isConnected()) {
_wifi_rssi = (int)WiFi.RSSI();
}
_snmp.loop();
}
void MeshSNMPAgent::updateRadioStats(
uint32_t packets_recv, uint32_t packets_sent, uint32_t recv_errors,
int16_t noise_floor, int16_t last_rssi, int16_t last_snr,
uint32_t sent_flood, uint32_t sent_direct,
uint32_t recv_flood, uint32_t recv_direct,
uint32_t total_air_time_secs, uint32_t uptime_secs) {
_packets_recv = (int)packets_recv;
_packets_sent = (int)packets_sent;
_recv_errors = (int)recv_errors;
_noise_floor = (int)noise_floor;
_last_rssi = (int)last_rssi;
_last_snr = (int)last_snr;
_sent_flood = (int)sent_flood;
_sent_direct = (int)sent_direct;
_recv_flood = (int)recv_flood;
_recv_direct = (int)recv_direct;
_total_air_time_secs = (int)total_air_time_secs;
_uptime_secs = (int)uptime_secs;
}
void MeshSNMPAgent::updateMQTTStats(int connected_slots, int queue_depth, int skipped_publishes) {
_mqtt_connected_slots = connected_slots;
_mqtt_queue_depth = queue_depth;
_mqtt_skipped_publishes = skipped_publishes;
}
void MeshSNMPAgent::setNodeName(const char* name) {
strncpy(_node_name, name, sizeof(_node_name) - 1);
_node_name[sizeof(_node_name) - 1] = '\0';
}
void MeshSNMPAgent::setFirmwareVersion(const char* version) {
strncpy(_firmware_version, version, sizeof(_firmware_version) - 1);
_firmware_version[sizeof(_firmware_version) - 1] = '\0';
}
#endif // WITH_SNMP
+79
View File
@@ -0,0 +1,79 @@
#pragma once
#ifdef WITH_SNMP
#include <WiFi.h>
#include <WiFiUdp.h>
#include <SNMP_Agent.h>
// Temporary private enterprise OID base — replace with registered PEN when available.
// All MeshCore OIDs live under this subtree.
#define MESHCORE_OID_BASE ".1.3.6.1.4.1.99999"
// OID layout:
// .1.x.0 = system (uptime, version, node name)
// .2.x.0 = radio (packets, RSSI, SNR, noise floor, air time)
// .3.x.0 = mqtt (connected slots, queue depth, skipped publishes)
// .4.x.0 = memory (free heap, max alloc, internal free, PSRAM free)
// .5.x.0 = network (WiFi RSSI)
class MeshSNMPAgent {
public:
MeshSNMPAgent();
void begin(const char* community);
void loop();
// Called from the mesh task (Core 1) to push fresh stats into SNMP-visible variables.
// Copies are atomic for 32-bit aligned ints on ESP32, so no mutex needed.
void updateRadioStats(uint32_t packets_recv, uint32_t packets_sent, uint32_t recv_errors,
int16_t noise_floor, int16_t last_rssi, int16_t last_snr,
uint32_t sent_flood, uint32_t sent_direct,
uint32_t recv_flood, uint32_t recv_direct,
uint32_t total_air_time_secs, uint32_t uptime_secs);
void updateMQTTStats(int connected_slots, int queue_depth, int skipped_publishes);
void setNodeName(const char* name);
void setFirmwareVersion(const char* version);
bool isRunning() const { return _running; }
private:
WiFiUDP _udp;
SNMPAgent _snmp;
bool _running;
// System OIDs
int _uptime_secs;
char _firmware_version[32];
char _node_name[32];
// Radio OIDs
int _packets_recv;
int _packets_sent;
int _recv_errors;
int _noise_floor;
int _last_rssi;
int _last_snr;
int _sent_flood;
int _sent_direct;
int _recv_flood;
int _recv_direct;
int _total_air_time_secs;
// MQTT OIDs
int _mqtt_connected_slots;
int _mqtt_queue_depth;
int _mqtt_skipped_publishes;
// Memory OIDs (updated in loop() since we're on Core 0 with WiFi)
int _free_heap;
int _max_alloc;
int _internal_free;
int _psram_free;
// Network OIDs
int _wifi_rssi;
};
#endif // WITH_SNMP
+98 -10
View File
@@ -4,6 +4,10 @@
#include <WiFiUdp.h>
#include <Timezone.h>
#ifdef WITH_SNMP
#include "../SNMPAgent.h"
#endif
#ifdef ESP_PLATFORM
#include <esp_wifi.h>
#include <esp_heap_caps.h>
@@ -168,8 +172,12 @@ MQTTBridge::MQTTBridge(NodePrefs *prefs, mesh::PacketManager *mgr, mesh::RTCCloc
_cached_has_connected_slots(false),
_last_memory_check(0), _skipped_publishes(0), _last_fragmentation_recovery(0),
_fragmentation_pressure_since(0), _last_critical_check_run(0),
_last_no_broker_log(0), _last_config_warning(0),
_last_no_broker_log(0), _queue_disconnected_since(0), _all_tripped_since(0),
_last_config_warning(0),
_dispatcher(nullptr), _radio(nullptr), _board(nullptr), _ms(nullptr),
#ifdef WITH_SNMP
_snmp_agent(nullptr),
#endif
_last_wifi_check(0), _last_wifi_status(WL_DISCONNECTED), _wifi_status_initialized(false),
_wifi_disconnected_time(0), _last_wifi_reconnect_attempt(0), _wifi_reconnect_backoff_attempt(0)
#ifdef ESP_PLATFORM
@@ -673,6 +681,25 @@ void MQTTBridge::mqttTaskLoop() {
// Process packet queue
processPacketQueue();
#ifdef WITH_SNMP
// SNMP agent loop — process incoming UDP requests
if (_snmp_agent) {
if (!_snmp_agent->isRunning() && WiFi.isConnected() && _prefs->snmp_enabled) {
_snmp_agent->begin(_prefs->snmp_community);
MQTT_DEBUG_PRINTLN("SNMP agent started on port 161 (community: %s)", _prefs->snmp_community);
}
if (_snmp_agent->isRunning()) {
// Update MQTT stats from this core
int connected = 0;
for (int i = 0; i < MAX_MQTT_SLOTS; i++) {
if (_slots[i].enabled && _slots[i].connected) connected++;
}
_snmp_agent->updateMQTTStats(connected, _queue_count, _skipped_publishes);
_snmp_agent->loop();
}
}
#endif
// Periodic configuration check (throttled to avoid spam)
checkConfigurationMismatch();
@@ -937,6 +964,9 @@ void MQTTBridge::maintainSlotConnections() {
// Only allow one reconnect attempt per maintenance cycle to avoid
// multiple simultaneous TLS handshakes blocking the network stack
bool reconnect_attempted_this_cycle = false;
// Only allow one full teardown+setup per cycle to limit heap fragmentation
// when multiple slots fail simultaneously
bool teardown_attempted_this_cycle = false;
for (int i = 0; i < MAX_MQTT_SLOTS; i++) {
if (!_slots[i].enabled || !_slots[i].client) continue;
@@ -946,11 +976,11 @@ void MQTTBridge::maintainSlotConnections() {
continue;
}
maintainSlotConnection(i, now_millis, current_time, time_synced, reconnect_attempted_this_cycle);
maintainSlotConnection(i, now_millis, current_time, time_synced, reconnect_attempted_this_cycle, teardown_attempted_this_cycle);
}
}
void MQTTBridge::maintainSlotConnection(int index, unsigned long now_millis, unsigned long current_time, bool time_synced, bool& reconnect_attempted) {
void MQTTBridge::maintainSlotConnection(int index, unsigned long now_millis, unsigned long current_time, bool time_synced, bool& reconnect_attempted, bool& teardown_attempted) {
MQTTSlot& slot = _slots[index];
if (slot.connected) {
@@ -1037,13 +1067,23 @@ void MQTTBridge::maintainSlotConnection(int index, unsigned long now_millis, uns
}
slot.client->connect();
} else {
// Token expired or near expiry — full teardown for fresh TLS + new token
bool saved_tripped = slot.circuit_breaker_tripped;
MQTT_DEBUG_PRINTLN("MQTT%d token expired/near expiry, full teardown+setup for probe", index + 1);
teardownSlot(index);
setupSlot(index);
_slots[index].circuit_breaker_tripped = saved_tripped;
_slots[index].last_reconnect_attempt = now_millis;
// Token expired — regenerate token but avoid teardown+setup
// which would allocate new TLS context on potentially fragmented heap
if (slot.client) {
if (createSlotAuthToken(index)) {
slot.client->setCredentials(_jwt_username, slot.auth_token);
MQTT_DEBUG_PRINTLN("MQTT%d circuit breaker probe (regenerated expired token)", index + 1);
}
slot.client->connect();
} else {
// Client was destroyed — must do full setup
bool saved_tripped = slot.circuit_breaker_tripped;
MQTT_DEBUG_PRINTLN("MQTT%d circuit breaker probe (full setup, no client)", index + 1);
teardownSlot(index);
setupSlot(index);
_slots[index].circuit_breaker_tripped = saved_tripped;
_slots[index].last_reconnect_attempt = now_millis;
}
}
} else {
slot.client->connect();
@@ -1094,6 +1134,12 @@ void MQTTBridge::maintainSlotConnection(int index, unsigned long now_millis, uns
slot.client->reconnect();
} else {
// Full teardown: token expired/near expiry, or lightweight reconnect failed (backoff 3+)
if (teardown_attempted) {
// Defer to next cycle to limit heap fragmentation from simultaneous teardowns
slot.last_reconnect_attempt = now_millis;
return;
}
teardown_attempted = true;
uint8_t saved_backoff = slot.reconnect_backoff;
uint8_t saved_failures = slot.max_backoff_failures;
MQTT_DEBUG_PRINTLN("MQTT%d full teardown+setup for reconnect (backoff %d)", index + 1, saved_backoff);
@@ -1110,6 +1156,11 @@ void MQTTBridge::maintainSlotConnection(int index, unsigned long now_millis, uns
slot.client->reconnect();
} else {
// Full teardown after lightweight reconnect failed (backoff 3+)
if (teardown_attempted) {
slot.last_reconnect_attempt = now_millis;
return;
}
teardown_attempted = true;
uint8_t saved_backoff = slot.reconnect_backoff;
uint8_t saved_failures = slot.max_backoff_failures;
MQTT_DEBUG_PRINTLN("MQTT%d full teardown+setup for reconnect (backoff %d)", index + 1, saved_backoff);
@@ -1769,6 +1820,7 @@ void MQTTBridge::processPacketQueue() {
_queue_count = uxQueueMessagesWaiting(_packet_queue_handle);
if (_queue_count == 0) {
_queue_disconnected_since = 0;
return;
}
@@ -1782,10 +1834,21 @@ void MQTTBridge::processPacketQueue() {
MQTT_DEBUG_PRINTLN("Queue has %d packets but no slots connected", _queue_count);
_last_no_broker_log = now;
}
// Flush stale packets after extended disconnect
if (_queue_disconnected_since == 0) {
_queue_disconnected_since = now;
} else if ((now - _queue_disconnected_since) >= QUEUE_STALE_MS) {
QueuedPacket discard;
while (xQueueReceive(_packet_queue_handle, &discard, 0) == pdTRUE) {}
_queue_count = 0;
MQTT_DEBUG_PRINTLN("Flushed stale packet queue after %lu ms disconnected", now - _queue_disconnected_since);
_queue_disconnected_since = now;
}
}
return;
}
_queue_disconnected_since = 0;
_last_no_broker_log = 0;
// Adaptive drain: burst-process when queue has backlog, gentle otherwise
@@ -2273,6 +2336,31 @@ void MQTTBridge::runCriticalMemoryCheckAndRecovery() {
MQTT_DEBUG_PRINTLN("Fragmentation recovery: recreating MQTT clients (max_alloc=%d, pressure %lu min)", (int)max_alloc, (unsigned long)(required_window_ms / 60000));
recreateMqttClientsForFragmentationRecovery();
}
// Last resort: if ALL enabled slots have circuit breakers tripped for >1 hour,
// heap is likely too fragmented for TLS to ever succeed — reboot.
bool all_tripped = true;
int enabled_count = 0;
for (int i = 0; i < MAX_MQTT_SLOTS; i++) {
if (_slots[i].enabled) {
enabled_count++;
if (!_slots[i].circuit_breaker_tripped) {
all_tripped = false;
break;
}
}
}
if (enabled_count > 0 && all_tripped) {
if (_all_tripped_since == 0) {
_all_tripped_since = now;
} else if ((now - _all_tripped_since) >= 3600000UL) {
MQTT_DEBUG_PRINTLN("All MQTT slots circuit-breaker tripped for >1 hour. Restarting ESP.");
delay(100);
ESP.restart();
}
} else {
_all_tripped_since = 0;
}
}
#endif
+20 -1
View File
@@ -10,6 +10,10 @@
#include "helpers/JWTHelper.h"
#include "helpers/MQTTPresets.h"
#ifdef WITH_SNMP
class MeshSNMPAgent; // Forward declaration
#endif
#ifdef ESP_PLATFORM
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
@@ -186,6 +190,17 @@ private:
// 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
static const unsigned long QUEUE_STALE_MS = 300000UL; // Flush queue after 5 min disconnected
// Recovery: restart ESP after prolonged total failure
unsigned long _all_tripped_since; // 0 = not all tripped
#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
@@ -216,7 +231,7 @@ private:
void setupSlot(int index); // Create/destroy client for a slot based on its preset
void teardownSlot(int index); // Disconnect and free slot resources
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);
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
bool publishToSlot(int index, const char* topic, const char* payload, bool retained = false);
bool publishToAllSlots(const char* topic, const char* payload, bool retained = false);
@@ -304,6 +319,10 @@ public:
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