Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 65 additions & 8 deletions MQTT_SNMP.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,13 +36,19 @@ snmpwalk -v2c -c public <device-ip> 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 |
|---|---|---|
| `.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)

Expand All @@ -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)

Expand All @@ -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)

Expand All @@ -76,13 +89,52 @@ 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)

| OID | Type | Description |
|---|---|---|
| `.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

```
Expand All @@ -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`:

Expand All @@ -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

Expand Down
87 changes: 72 additions & 15 deletions examples/simple_repeater/MyMesh.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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"
Expand Down
1 change: 1 addition & 0 deletions examples/simple_repeater/MyMesh.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
58 changes: 43 additions & 15 deletions examples/simple_room_server/MyMesh.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
3 changes: 3 additions & 0 deletions examples/simple_room_server/MyMesh.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down
Loading
Loading