diff --git a/MQTT_SNMP.md b/MQTT_SNMP.md index 8274009229f..447e7fdfd46 100644 --- a/MQTT_SNMP.md +++ b/MQTT_SNMP.md @@ -36,6 +36,8 @@ snmpwalk -v2c -c public 1.3.6.1.4.1.99999 All OIDs are under the private enterprise subtree `1.3.6.1.4.1.99999` (`.iso.org.dod.internet.private.enterprises.99999`). +Levels (queue lengths, percentages, voltages) are INTEGER. Running totals added from `.2.13.0` onwards are Counter32, so LibreNMS, Zabbix and similar tools graph them as rates. They start again from 0 after a reboot or `clear stats`. The older totals in `.2.x.0` and `.3.3.0` are still INTEGER. + ### System (.1.x.0) | OID | Type | Description | @@ -43,6 +45,10 @@ All OIDs are under the private enterprise subtree `1.3.6.1.4.1.99999` (`.iso.org | `.1.1.0` | INTEGER | System uptime (seconds) | | `.1.2.0` | STRING | Firmware version | | `.1.3.0` | STRING | Node name | +| `.1.4.0` | INTEGER | Battery voltage (mV), 0 when the board cannot measure it. Read every 30 s | +| `.1.5.0` | INTEGER | MCU die temperature (whole °C), -127 when the board has no sensor. Read every 30 s | +| `.1.6.0` | INTEGER | Reset reason code for the current boot (ESP-IDF `esp_reset_reason_t`: 1 power-on, 3 software, 4 panic, 5/6/7 watchdog, 9 brownout) | +| `.1.7.0` | STRING | Reset reason as text, for example `Brownout (low voltage)` | ### Radio (.2.x.0) @@ -59,6 +65,12 @@ All OIDs are under the private enterprise subtree `1.3.6.1.4.1.99999` (`.iso.org | `.2.9.0` | INTEGER | Flood packets received | | `.2.10.0` | INTEGER | Direct packets received | | `.2.11.0` | INTEGER | Total air time (seconds) | +| `.2.12.0` | INTEGER | Packets waiting in the transmit queue | +| `.2.13.0` | Counter32 | Duplicate flood packets received | +| `.2.14.0` | Counter32 | Duplicate direct packets received | +| `.2.15.0` | Counter32 | Total receive air time (seconds) | +| `.2.16.0` | INTEGER | Transmit duty cycle over the last ~60 s (%) | +| `.2.17.0` | INTEGER | Error flags since boot (bitmask: 1 packet pool full, 2 CAD timeout, 4 start-RX timeout, 8 radio watchdog fired) | ### MQTT (.3.x.0) @@ -67,6 +79,7 @@ All OIDs are under the private enterprise subtree `1.3.6.1.4.1.99999` (`.iso.org | `.3.1.0` | INTEGER | Connected MQTT slot count | | `.3.2.0` | INTEGER | Packet queue depth | | `.3.3.0` | INTEGER | Skipped publishes (memory pressure) | +| `.3.4.0` | INTEGER | Clock set by NTP (1 yes, 0 no) | ### Memory (.4.x.0) @@ -76,6 +89,7 @@ All OIDs are under the private enterprise subtree `1.3.6.1.4.1.99999` (`.iso.org | `.4.2.0` | INTEGER | Max allocatable block (bytes) | | `.4.3.0` | INTEGER | Free internal RAM (bytes) | | `.4.4.0` | INTEGER | Free PSRAM (bytes, 0 if no PSRAM) | +| `.4.5.0` | INTEGER | Lowest free heap since boot (bytes). A value that keeps dropping points to a leak | ### Network (.5.x.0) @@ -83,6 +97,44 @@ All OIDs are under the private enterprise subtree `1.3.6.1.4.1.99999` (`.iso.org |---|---|---| | `.5.1.0` | INTEGER | WiFi RSSI (dBm) | +### Duty gate (.6.x.0, repeater only) + +Region gating state (`set dc.gate on`). Not present on room servers. + +| OID | Type | Description | +|---|---|---| +| `.6.1.0` | INTEGER | Gating enabled (1 yes, 0 no) | +| `.6.2.0` | INTEGER | Outer region layers currently gated (0 = none) | +| `.6.3.0` | INTEGER | Highest layer that can be gated | +| `.6.4.0` | INTEGER | Duty cycle threshold that starts gating (%) | +| `.6.5.0` | INTEGER | Hysteresis: layers come back below threshold minus this (%) | + +### Filter (.7.x.0, repeater only) + +Packet filter drop totals since boot or the last `clear stats`. In dry-run mode the drops are counted but the packets are still forwarded. Not present on room servers. + +| OID | Type | Description | +|---|---|---| +| `.7.1.0` | INTEGER | Filter enabled (1 yes, 0 no) | +| `.7.2.0` | INTEGER | Dry-run mode (1 yes, 0 no) | +| `.7.3.0` | Counter32 | Hop-limit drops, all payload types | +| `.7.4.0` | Counter32 | Rate-limit drops, all payload types | +| `.7.5.0` | Counter32 | Blocked-channel drops | +| `.7.6.0` | Counter32 | Path-hash size drops | +| `.7.7.0` | Counter32 | Malformed packet drops | +| `.7.8.0` | Counter32 | Advert window drops | +| `.7.9.0` | Counter32 | Path-prefix block list drops | +| `.7.10.0` | Counter32 | Sender rule drops | +| `.7.11.0` | Counter32 | Text rule drops | +| `.7.12.0` | Counter32 | Message age drops | +| `.7.13.0` | Counter32 | Estimated transmit air time the drops saved (ms) | + +### Neighbours (.8.x.0) + +| OID | Type | Description | +|---|---|---| +| `.8.1.0` | INTEGER | Neighbours in the neighbour table (always 0 on room servers without PSRAM, which keep no table) | + ## Example Output ``` @@ -91,31 +143,35 @@ $ snmpwalk -v2c -c public 192.168.50.95 1.3.6.1.4.1.99999 SNMPv2-SMI::enterprises.99999.1.1.0 = INTEGER: 18 # uptime 18s SNMPv2-SMI::enterprises.99999.1.2.0 = STRING: "v1.14.1" # firmware SNMPv2-SMI::enterprises.99999.1.3.0 = STRING: "Heltec Repeater" +SNMPv2-SMI::enterprises.99999.1.4.0 = INTEGER: 4105 # battery 4.105 V +SNMPv2-SMI::enterprises.99999.1.5.0 = INTEGER: 41 # MCU 41 °C +SNMPv2-SMI::enterprises.99999.1.6.0 = INTEGER: 1 +SNMPv2-SMI::enterprises.99999.1.7.0 = STRING: "Power-on reset" SNMPv2-SMI::enterprises.99999.2.1.0 = INTEGER: 3 # 3 packets received SNMPv2-SMI::enterprises.99999.2.4.0 = INTEGER: -96 # noise floor -96 dBm SNMPv2-SMI::enterprises.99999.2.5.0 = INTEGER: -25 # last RSSI -25 dBm SNMPv2-SMI::enterprises.99999.4.1.0 = INTEGER: 199612 # ~195 KB free heap SNMPv2-SMI::enterprises.99999.4.2.0 = INTEGER: 188404 # ~184 KB max alloc SNMPv2-SMI::enterprises.99999.5.1.0 = INTEGER: -22 # WiFi RSSI -22 dBm +SNMPv2-SMI::enterprises.99999.7.4.0 = Counter32: 12 # 12 rate-limit drops +SNMPv2-SMI::enterprises.99999.8.1.0 = INTEGER: 7 # 7 neighbours ``` ## Architecture The SNMP agent runs inside the existing MQTT FreeRTOS task on Core 0 (the WiFi/network core). No additional task is created. -- **Radio stats** are pushed from Core 1 (mesh task) every 2 seconds via `updateRadioStats()` -- **MQTT stats** are updated from Core 0 each MQTT task loop iteration +- **Radio, mesh, duty gate, filter and neighbour stats** are pushed from Core 1 (mesh task) every 2 seconds by `MyMesh::pushSNMPStats()` +- **Battery voltage and MCU temperature** are read on Core 1 every 30 seconds (some boards block on the ADC read) +- **Reset reason** is set once at startup +- **MQTT stats and NTP state** are updated from Core 0 each MQTT task loop iteration - **Memory and WiFi stats** are read directly in the SNMP loop on Core 0 The agent starts automatically once WiFi connects and `snmp_enabled = 1` in prefs. It adds approximately 2-3 KB RAM overhead plus per-OID handler storage. ## Build Configuration -SNMP is enabled at compile time with the `WITH_SNMP=1` build flag and included in observer firmware targets: - -- `heltec_v3_repeater_observer_mqtt` -- `heltec_v4_repeater_observer_mqtt` -- `Station_G2_repeater_observer_mqtt` +SNMP is enabled at compile time with the `WITH_SNMP=1` build flag. Most ESP32 observer targets have it; search the `variants/*/platformio.ini` files for `WITH_SNMP=1` to see which. To add SNMP to another observer variant, add the following to its `platformio.ini`: @@ -142,7 +198,8 @@ lib_deps = | `src/helpers/CommonCLI.h` | `NodePrefs` struct (`snmp_enabled`, `snmp_community` fields) | | `src/helpers/CommonCLI.cpp` | CLI command parsing and prefs persistence | | `src/helpers/bridges/MQTTBridge.cpp` | SNMP loop integration in `mqttTaskLoop()` | -| `examples/simple_repeater/MyMesh.cpp` | Radio stats push and agent initialization | +| `examples/simple_repeater/MyMesh.cpp` | Stats push (`pushSNMPStats`) and agent initialization | +| `examples/simple_room_server/MyMesh.cpp` | Stats push for room servers (no duty gate or filter groups) | ## Private Enterprise Number diff --git a/examples/simple_repeater/MyMesh.cpp b/examples/simple_repeater/MyMesh.cpp index 2d6a28655ef..c6c444960b6 100644 --- a/examples/simple_repeater/MyMesh.cpp +++ b/examples/simple_repeater/MyMesh.cpp @@ -1263,6 +1263,9 @@ void MyMesh::begin(FILESYSTEM *fs) { if (_cli.getObserverPrefs()->snmp_enabled) { _snmp_agent.setNodeName(_prefs.node_name); _snmp_agent.setFirmwareVersion(getFirmwareVer()); + uint32_t reset_reason = board.getResetReason(); + _snmp_agent.setResetReason(reset_reason, board.getResetReasonString(reset_reason)); + _snmp_agent.enableRepeaterGroups(); // duty gate + filter bridge->setSNMPAgent(&_snmp_agent); } #endif @@ -2255,24 +2258,78 @@ void MyMesh::loop() { #endif #ifdef WITH_SNMP - // Push radio stats to SNMP agent every 2 seconds - if (_snmp_agent.isRunning()) { - static unsigned long last_snmp_stats = 0; - if (now - last_snmp_stats >= 2000) { - last_snmp_stats = now; - _snmp_agent.updateRadioStats( - radio_driver.getPacketsRecv(), radio_driver.getPacketsSent(), - radio_driver.getPacketsRecvErrors(), - (int16_t)_radio->getNoiseFloor(), - (int16_t)radio_driver.getLastRSSI(), - (int16_t)(radio_driver.getLastSNR() * 4), - getNumSentFlood(), getNumSentDirect(), - getNumRecvFlood(), getNumRecvDirect(), - getTotalAirTime() / 1000, uptime_millis / 1000); - } + if (_snmp_agent.isRunning()) pushSNMPStats(now); +#endif +} + +#ifdef WITH_SNMP +static uint32_t snmpSatAdd(uint32_t a, uint32_t b) { + return a > 0xFFFFFFFFu - b ? 0xFFFFFFFFu : a + b; // filter counters saturate, so the sum does too +} + +// Counters every 2 s; battery and MCU temperature every 30 s, because some boards +// block on the ADC read (Heltec V4 waits 10 ms) and neither value moves quickly. +void MyMesh::pushSNMPStats(uint32_t now) { + static uint32_t last_snmp_stats = 0; + static uint32_t last_snmp_health = 0; + static bool health_pushed = false; + + if (!health_pushed || now - last_snmp_health >= 30000) { + health_pushed = true; + last_snmp_health = now; + _snmp_agent.updateBattery(board.getBattMilliVolts()); + _snmp_agent.updateMcuTemperature(board.getMCUTemperature()); + } + + if (now - last_snmp_stats < 2000) return; + last_snmp_stats = now; + + _snmp_agent.updateRadioStats( + radio_driver.getPacketsRecv(), radio_driver.getPacketsSent(), + radio_driver.getPacketsRecvErrors(), + (int16_t)_radio->getNoiseFloor(), + (int16_t)radio_driver.getLastRSSI(), + (int16_t)(radio_driver.getLastSNR() * 4), + getNumSentFlood(), getNumSentDirect(), + getNumRecvFlood(), getNumRecvDirect(), + getTotalAirTime() / 1000, uptime_millis / 1000); + + int neighbour_count = 0; +#if MAX_NEIGHBOURS + for (int i = 0; i < MAX_NEIGHBOURS; i++) { + if (neighbours[i].heard_timestamp > 0) neighbour_count++; } #endif + _snmp_agent.updateMeshStats( + _mgr->getOutboundCount(0xFFFFFFFF), + ((SimpleMeshTables *)getTables())->getNumFloodDups(), + ((SimpleMeshTables *)getTables())->getNumDirectDups(), + getReceiveAirTime() / 1000, getTxDutyCyclePercent(), _err_flags, + neighbour_count); + + _snmp_agent.updateDutyGate(_prefs.dc_gate_enabled != 0, dc_gate_level, region_map.getMaxGateLevel(), + _prefs.dc_gate_threshold, _prefs.dc_gate_hysteresis); + + const Counters& c = _filter.getCounters(); + SNMPFilterTotals t; + t.enabled = _filter.isEnabled(); + t.dryrun = _filter.getPrefs().dryrun != 0; + for (int i = 0; i < PAYLOAD_TYPE_COUNT; i++) { + t.hops = snmpSatAdd(t.hops, c.hops[i]); + t.rate = snmpSatAdd(t.rate, c.rate[i]); + } + t.channel = c.channel; + t.hash = c.hash; + t.malformed = c.malformed; + t.advert = c.advert; + t.path = c.path; + t.sender = c.sender; + t.text = c.text; + t.age = c.age; + t.air_ms = c.air_ms; + _snmp_agent.updateFilterStats(t); } +#endif #if defined(WITH_MQTT_NEIGHBORS) #include "helpers/MQTTMessageBuilder.h" diff --git a/examples/simple_repeater/MyMesh.h b/examples/simple_repeater/MyMesh.h index 58d01100bd8..7558b5a090d 100644 --- a/examples/simple_repeater/MyMesh.h +++ b/examples/simple_repeater/MyMesh.h @@ -148,6 +148,7 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks #endif #ifdef WITH_SNMP MeshSNMPAgent _snmp_agent; + void pushSNMPStats(uint32_t now); // mesh task: copy stats into the SNMP agent #endif #ifdef WITH_MQTT_BRIDGE AlertReporter _alerter; diff --git a/examples/simple_room_server/MyMesh.cpp b/examples/simple_room_server/MyMesh.cpp index 9d8931392b6..af3736c5538 100644 --- a/examples/simple_room_server/MyMesh.cpp +++ b/examples/simple_room_server/MyMesh.cpp @@ -1785,24 +1785,52 @@ void MyMesh::loop() { #endif #ifdef WITH_SNMP - // Push radio stats to SNMP agent every 2 seconds - if (_snmp_agent.isRunning()) { - static unsigned long last_snmp_stats = 0; - if (now - last_snmp_stats >= 2000) { - last_snmp_stats = now; - _snmp_agent.updateRadioStats( - radio_driver.getPacketsRecv(), radio_driver.getPacketsSent(), - radio_driver.getPacketsRecvErrors(), - (int16_t)_radio->getNoiseFloor(), - (int16_t)radio_driver.getLastRSSI(), - (int16_t)(radio_driver.getLastSNR() * 4), - getNumSentFlood(), getNumSentDirect(), - getNumRecvFlood(), getNumRecvDirect(), - getTotalAirTime() / 1000, uptime_millis / 1000); - } + if (_snmp_agent.isRunning()) pushSNMPStats(now); +#endif +} + +#ifdef WITH_SNMP +// Counters every 2 s; battery and MCU temperature every 30 s, because some boards +// block on the ADC read (Heltec V4 waits 10 ms) and neither value moves quickly. +void MyMesh::pushSNMPStats(uint32_t now) { + static uint32_t last_snmp_stats = 0; + static uint32_t last_snmp_health = 0; + static bool health_pushed = false; + + if (!health_pushed || now - last_snmp_health >= 30000) { + health_pushed = true; + last_snmp_health = now; + _snmp_agent.updateBattery(board.getBattMilliVolts()); + _snmp_agent.updateMcuTemperature(board.getMCUTemperature()); + } + + if (now - last_snmp_stats < 2000) return; + last_snmp_stats = now; + + _snmp_agent.updateRadioStats( + radio_driver.getPacketsRecv(), radio_driver.getPacketsSent(), + radio_driver.getPacketsRecvErrors(), + (int16_t)_radio->getNoiseFloor(), + (int16_t)radio_driver.getLastRSSI(), + (int16_t)(radio_driver.getLastSNR() * 4), + getNumSentFlood(), getNumSentDirect(), + getNumRecvFlood(), getNumRecvDirect(), + getTotalAirTime() / 1000, uptime_millis / 1000); + + int neighbour_count = 0; +#if defined(WITH_MQTT_NEIGHBORS) // the room server keeps no neighbour table without it + for (int i = 0; i < MAX_NEIGHBOURS; i++) { + if (neighbours[i].heard_timestamp > 0) neighbour_count++; } #endif + _snmp_agent.updateMeshStats( + _mgr->getOutboundCount(0xFFFFFFFF), + ((SimpleMeshTables *)getTables())->getNumFloodDups(), + ((SimpleMeshTables *)getTables())->getNumDirectDups(), + getReceiveAirTime() / 1000, getTxDutyCyclePercent(), _err_flags, + neighbour_count); } +#endif #if defined(WITH_MQTT_NEIGHBORS) #include "helpers/MQTTMessageBuilder.h" diff --git a/examples/simple_room_server/MyMesh.h b/examples/simple_room_server/MyMesh.h index 5037bbb3bda..af909670648 100644 --- a/examples/simple_room_server/MyMesh.h +++ b/examples/simple_room_server/MyMesh.h @@ -212,6 +212,7 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks #endif #ifdef WITH_SNMP MeshSNMPAgent _snmp_agent; + void pushSNMPStats(uint32_t now); // mesh task: copy stats into the SNMP agent #endif #ifdef WITH_MQTT_BRIDGE AlertReporter _alerter; @@ -398,6 +399,8 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks if (_cli.getObserverPrefs()->snmp_enabled) { _snmp_agent.setNodeName(_prefs.node_name); _snmp_agent.setFirmwareVersion(getFirmwareVer()); + uint32_t reset_reason = _cli.getBoard()->getResetReason(); + _snmp_agent.setResetReason(reset_reason, _cli.getBoard()->getResetReasonString(reset_reason)); bridge->setSNMPAgent(&_snmp_agent); } #endif diff --git a/src/helpers/SNMPAgent.cpp b/src/helpers/SNMPAgent.cpp index eb29bd688bc..a15c8c70fc8 100644 --- a/src/helpers/SNMPAgent.cpp +++ b/src/helpers/SNMPAgent.cpp @@ -4,23 +4,38 @@ #include "NetworkLink.h" #include #include +#include #define SNMP_PORT 161 MeshSNMPAgent::MeshSNMPAgent() : _snmp("public"), _running(false), + _repeater_groups(false), _uptime_secs(0), + _battery_mv(0), + _mcu_temp_c(SNMP_TEMP_UNAVAILABLE), + _reset_reason(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(-127) + _tx_queue_len(0), _flood_dups(0), _direct_dups(0), _rx_air_time_secs(0), + _tx_duty_percent(0), _err_flags(0), + _mqtt_connected_slots(0), _mqtt_queue_depth(0), _mqtt_skipped_publishes(0), _ntp_synced(0), + _free_heap(0), _max_alloc(0), _internal_free(0), _psram_free(0), _min_free_heap(0), + _wifi_rssi(-127), + _dc_gate_enabled(0), _dc_gate_level(0), _dc_gate_max_level(0), + _dc_gate_threshold(0), _dc_gate_hysteresis(0), + _filter_enabled(0), _filter_dryrun(0), + _filter_hops(0), _filter_rate(0), _filter_channel(0), _filter_hash(0), + _filter_malformed(0), _filter_advert(0), _filter_path(0), _filter_sender(0), + _filter_text(0), _filter_age(0), _filter_air_ms(0), + _neighbour_count(0) { _firmware_version[0] = '\0'; _node_name[0] = '\0'; + _reset_reason_text[0] = '\0'; } void MeshSNMPAgent::begin(const char* community) { @@ -34,6 +49,10 @@ void MeshSNMPAgent::begin(const char* community) { _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)); + _snmp.addIntegerHandler(MESHCORE_OID_BASE ".1.4.0", &_battery_mv); + _snmp.addIntegerHandler(MESHCORE_OID_BASE ".1.5.0", &_mcu_temp_c); + _snmp.addIntegerHandler(MESHCORE_OID_BASE ".1.6.0", &_reset_reason); + _snmp.addReadOnlyStaticStringHandler(MESHCORE_OID_BASE ".1.7.0", _reset_reason_text); // Radio group (.2.x.0) _snmp.addIntegerHandler(MESHCORE_OID_BASE ".2.1.0", &_packets_recv); @@ -47,21 +66,56 @@ void MeshSNMPAgent::begin(const char* community) { _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); + _snmp.addIntegerHandler(MESHCORE_OID_BASE ".2.12.0", &_tx_queue_len); + _snmp.addCounter32Handler(MESHCORE_OID_BASE ".2.13.0", &_flood_dups); + _snmp.addCounter32Handler(MESHCORE_OID_BASE ".2.14.0", &_direct_dups); + _snmp.addCounter32Handler(MESHCORE_OID_BASE ".2.15.0", &_rx_air_time_secs); + _snmp.addIntegerHandler(MESHCORE_OID_BASE ".2.16.0", &_tx_duty_percent); + _snmp.addIntegerHandler(MESHCORE_OID_BASE ".2.17.0", &_err_flags); // 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); + _snmp.addIntegerHandler(MESHCORE_OID_BASE ".3.4.0", &_ntp_synced); // 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); + _snmp.addIntegerHandler(MESHCORE_OID_BASE ".4.5.0", &_min_free_heap); // Network group (.5.x.0) _snmp.addIntegerHandler(MESHCORE_OID_BASE ".5.1.0", &_wifi_rssi); + if (_repeater_groups) { + // Duty gate group (.6.x.0) + _snmp.addIntegerHandler(MESHCORE_OID_BASE ".6.1.0", &_dc_gate_enabled); + _snmp.addIntegerHandler(MESHCORE_OID_BASE ".6.2.0", &_dc_gate_level); + _snmp.addIntegerHandler(MESHCORE_OID_BASE ".6.3.0", &_dc_gate_max_level); + _snmp.addIntegerHandler(MESHCORE_OID_BASE ".6.4.0", &_dc_gate_threshold); + _snmp.addIntegerHandler(MESHCORE_OID_BASE ".6.5.0", &_dc_gate_hysteresis); + + // Filter group (.7.x.0) + _snmp.addIntegerHandler(MESHCORE_OID_BASE ".7.1.0", &_filter_enabled); + _snmp.addIntegerHandler(MESHCORE_OID_BASE ".7.2.0", &_filter_dryrun); + _snmp.addCounter32Handler(MESHCORE_OID_BASE ".7.3.0", &_filter_hops); + _snmp.addCounter32Handler(MESHCORE_OID_BASE ".7.4.0", &_filter_rate); + _snmp.addCounter32Handler(MESHCORE_OID_BASE ".7.5.0", &_filter_channel); + _snmp.addCounter32Handler(MESHCORE_OID_BASE ".7.6.0", &_filter_hash); + _snmp.addCounter32Handler(MESHCORE_OID_BASE ".7.7.0", &_filter_malformed); + _snmp.addCounter32Handler(MESHCORE_OID_BASE ".7.8.0", &_filter_advert); + _snmp.addCounter32Handler(MESHCORE_OID_BASE ".7.9.0", &_filter_path); + _snmp.addCounter32Handler(MESHCORE_OID_BASE ".7.10.0", &_filter_sender); + _snmp.addCounter32Handler(MESHCORE_OID_BASE ".7.11.0", &_filter_text); + _snmp.addCounter32Handler(MESHCORE_OID_BASE ".7.12.0", &_filter_age); + _snmp.addCounter32Handler(MESHCORE_OID_BASE ".7.13.0", &_filter_air_ms); + } + + // Neighbour group (.8.x.0) + _snmp.addIntegerHandler(MESHCORE_OID_BASE ".8.1.0", &_neighbour_count); + _snmp.sortHandlers(); _running = true; } @@ -78,6 +132,7 @@ void MeshSNMPAgent::loop() { #else _psram_free = 0; #endif + _min_free_heap = (int)ESP.getMinFreeHeap(); const int signal = activeNetworkLink().rssi(); _wifi_rssi = signal == INT_MIN ? -127 : signal; @@ -105,10 +160,55 @@ void MeshSNMPAgent::updateRadioStats( _uptime_secs = (int)uptime_secs; } -void MeshSNMPAgent::updateMQTTStats(int connected_slots, int queue_depth, int skipped_publishes) { +void MeshSNMPAgent::updateMeshStats(uint32_t tx_queue_len, uint32_t flood_dups, uint32_t direct_dups, + uint32_t rx_air_time_secs, uint8_t tx_duty_percent, uint16_t err_flags, + int neighbour_count) { + _tx_queue_len = (int)tx_queue_len; + _flood_dups = flood_dups; + _direct_dups = direct_dups; + _rx_air_time_secs = rx_air_time_secs; + _tx_duty_percent = (int)tx_duty_percent; + _err_flags = (int)err_flags; + _neighbour_count = neighbour_count; +} + +void MeshSNMPAgent::updateMQTTStats(int connected_slots, int queue_depth, int skipped_publishes, bool ntp_synced) { _mqtt_connected_slots = connected_slots; _mqtt_queue_depth = queue_depth; _mqtt_skipped_publishes = skipped_publishes; + _ntp_synced = ntp_synced ? 1 : 0; +} + +void MeshSNMPAgent::updateBattery(uint16_t battery_mv) { + _battery_mv = (int)battery_mv; +} + +void MeshSNMPAgent::updateMcuTemperature(float celsius) { + _mcu_temp_c = std::isnan(celsius) ? SNMP_TEMP_UNAVAILABLE : (int)lroundf(celsius); +} + +void MeshSNMPAgent::updateDutyGate(bool enabled, uint8_t level, uint8_t max_level, uint8_t threshold, uint8_t hysteresis) { + _dc_gate_enabled = enabled ? 1 : 0; + _dc_gate_level = level; + _dc_gate_max_level = max_level; + _dc_gate_threshold = threshold; + _dc_gate_hysteresis = hysteresis; +} + +void MeshSNMPAgent::updateFilterStats(const SNMPFilterTotals& t) { + _filter_enabled = t.enabled ? 1 : 0; + _filter_dryrun = t.dryrun ? 1 : 0; + _filter_hops = t.hops; + _filter_rate = t.rate; + _filter_channel = t.channel; + _filter_hash = t.hash; + _filter_malformed = t.malformed; + _filter_advert = t.advert; + _filter_path = t.path; + _filter_sender = t.sender; + _filter_text = t.text; + _filter_age = t.age; + _filter_air_ms = t.air_ms; } void MeshSNMPAgent::setNodeName(const char* name) { @@ -121,4 +221,10 @@ void MeshSNMPAgent::setFirmwareVersion(const char* version) { _firmware_version[sizeof(_firmware_version) - 1] = '\0'; } +void MeshSNMPAgent::setResetReason(uint32_t code, const char* text) { + _reset_reason = (int)code; + strncpy(_reset_reason_text, text ? text : "", sizeof(_reset_reason_text) - 1); + _reset_reason_text[sizeof(_reset_reason_text) - 1] = '\0'; +} + #endif // WITH_SNMP diff --git a/src/helpers/SNMPAgent.h b/src/helpers/SNMPAgent.h index fe94b10fe10..0928e286ef9 100644 --- a/src/helpers/SNMPAgent.h +++ b/src/helpers/SNMPAgent.h @@ -10,11 +10,37 @@ #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 (RSSI, or -127 when disconnected or the medium has no RSSI) +// .1.x.0 = system (uptime, version, node name, battery, MCU temperature, reset reason) +// .2.x.0 = radio (packets, RSSI, SNR, noise floor, air time, TX queue, dups, duty cycle) +// .3.x.0 = mqtt (connected slots, queue depth, skipped publishes, NTP synced) +// .4.x.0 = memory (free heap, max alloc, internal free, PSRAM free, min free heap) +// .5.x.0 = network (RSSI, or -127 when disconnected or the medium has no RSSI) +// .6.x.0 = duty gate (region gating state; repeater only) +// .7.x.0 = filter (packet filter drop totals; repeater only) +// .8.x.0 = neighbours (neighbour count) +// +// Levels are INTEGER. Running totals added after the first release are Counter32, so +// pollers graph them as rates. + +// MCU temperature value when the board has no sensor (or returned NaN). +#define SNMP_TEMP_UNAVAILABLE -127 + +// Packet filter drop totals, as pushed from the repeater's Filter counters. +struct SNMPFilterTotals { + bool enabled = false; + bool dryrun = false; + uint32_t hops = 0; // hop-limit drops, all payload types + uint32_t rate = 0; // rate-limit drops, all payload types + uint32_t channel = 0; + uint32_t hash = 0; + uint32_t malformed = 0; + uint32_t advert = 0; + uint32_t path = 0; + uint32_t sender = 0; + uint32_t text = 0; + uint32_t age = 0; + uint32_t air_ms = 0; // estimated TX airtime the drops saved +}; class MeshSNMPAgent { public: @@ -30,10 +56,30 @@ class MeshSNMPAgent { 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); + // Mesh-side counters that RepeaterStats also reports. Called from the mesh task (Core 1). + void updateMeshStats(uint32_t tx_queue_len, uint32_t flood_dups, uint32_t direct_dups, + uint32_t rx_air_time_secs, uint8_t tx_duty_percent, uint16_t err_flags, + int neighbour_count); + + // Called from the MQTT task (Core 0). + void updateMQTTStats(int connected_slots, int queue_depth, int skipped_publishes, bool ntp_synced); + + // Battery voltage in millivolts, 0 when the board cannot measure it. Pushed from the + // mesh task, so the ADC is only ever read from Core 1. + void updateBattery(uint16_t battery_mv); + // MCU die temperature in whole degrees C. NaN maps to SNMP_TEMP_UNAVAILABLE. + void updateMcuTemperature(float celsius); + + // Repeater-only groups. Registered by begin() only after enableRepeaterGroups(). + void updateDutyGate(bool enabled, uint8_t level, uint8_t max_level, uint8_t threshold, uint8_t hysteresis); + void updateFilterStats(const SNMPFilterTotals& totals); + void enableRepeaterGroups() { _repeater_groups = true; } + + // String OIDs are copied when begin() registers them, so set these first. void setNodeName(const char* name); void setFirmwareVersion(const char* version); + void setResetReason(uint32_t code, const char* text); bool isRunning() const { return _running; } @@ -41,11 +87,16 @@ class MeshSNMPAgent { WiFiUDP _udp; SNMPAgent _snmp; bool _running; + bool _repeater_groups; // System OIDs int _uptime_secs; char _firmware_version[32]; char _node_name[32]; + int _battery_mv; + int _mcu_temp_c; + int _reset_reason; + char _reset_reason_text[32]; // Radio OIDs int _packets_recv; @@ -59,20 +110,53 @@ class MeshSNMPAgent { int _recv_flood; int _recv_direct; int _total_air_time_secs; + int _tx_queue_len; + uint32_t _flood_dups; + uint32_t _direct_dups; + uint32_t _rx_air_time_secs; + int _tx_duty_percent; + int _err_flags; // MQTT OIDs int _mqtt_connected_slots; int _mqtt_queue_depth; int _mqtt_skipped_publishes; + int _ntp_synced; // 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; + int _min_free_heap; // Network OIDs int _wifi_rssi; + + // Duty gate OIDs (repeater only) + int _dc_gate_enabled; + int _dc_gate_level; + int _dc_gate_max_level; + int _dc_gate_threshold; + int _dc_gate_hysteresis; + + // Filter OIDs (repeater only) + int _filter_enabled; + int _filter_dryrun; + uint32_t _filter_hops; + uint32_t _filter_rate; + uint32_t _filter_channel; + uint32_t _filter_hash; + uint32_t _filter_malformed; + uint32_t _filter_advert; + uint32_t _filter_path; + uint32_t _filter_sender; + uint32_t _filter_text; + uint32_t _filter_age; + uint32_t _filter_air_ms; + + // Neighbour OIDs + int _neighbour_count; }; #endif // WITH_SNMP diff --git a/src/helpers/bridges/MQTTBridge.cpp b/src/helpers/bridges/MQTTBridge.cpp index f2235a65646..60d109e08b3 100644 --- a/src/helpers/bridges/MQTTBridge.cpp +++ b/src/helpers/bridges/MQTTBridge.cpp @@ -1619,7 +1619,7 @@ void MQTTBridge::mqttTaskLoop() { for (int i = 0; i < RUNTIME_MQTT_SLOTS; i++) { if (_slots[i].enabled && _slots[i].connected) connected++; } - _snmp_agent->updateMQTTStats(connected, _queue_count, _skipped_publishes); + _snmp_agent->updateMQTTStats(connected, _queue_count, _skipped_publishes, _ntp_synced); _snmp_agent->loop(); } }