v3.1.4 cumulative: C-string, user tuning, BER #60 hardening, #64 RFC3416 fix - #1
Closed
syntax1269 wants to merge 1 commit into
Closed
v3.1.4 cumulative: C-string, user tuning, BER #60 hardening, #64 RFC3416 fix#1syntax1269 wants to merge 1 commit into
syntax1269 wants to merge 1 commit into
Conversation
…ning, 0neblock#64 snmpTrapOID.0 RFC 3416 fix Single cumulative PR rolling up 7 stable releases (v2.2.0 → v3.0.0 → v3.1.0 → v3.1.1 → v3.1.2 → v3.1.3 → v3.1.4) against Arduino_SNMP master. Zero new feature work; all changes are either: (a) memory safety / embedded footprint hardening (string model, zero-heap, compile-time sizing, startup-heap ASNPool for ESP8266 tiny DRAM targets), (b) interoperability bug fixes (BER TLV issues from upstream PR 0neblock#60 + extras, issue 0neblock#64 snmpTrapOID.0), (c) user/coder ergonomics (defs.h overrides via #ifndef, ESP8266 auto-tune profile, example sketches taught to tune + portability fixes). UPSTREAM ITEMS RESOLVED: • Closes 0neblock#64 (SNMPv2c Trap/Inform VB #2 NAME was sysObjectID.0, must be snmpTrapOID.0 per RFC 3416). • Absorbs upstream PR 0neblock#60 (three BER length bugs) + defensive max-length pre-checks on decode entry not included in 0neblock#60. • Resolves the "ESP-01 / ESP8266 80 KB DRAM OOM" class of reports: examples now link clean under 41% globals with 47+ KB headroom free on d1_mini (80 KB RAM). TWO API SIGNATURE CHANGES (v2.2.0 era, stable since, rest is 100% signature-compatible): • GETSTRING_FUNC : const std::string&(*)() → const char*(*)(). • OIDType::string() : const std::string& → const char*. VERIFICATION (all green): • Host catch2 — 101 / 101 assertions in 10 cases PASS. • ASAN (address + leak) — 0 errors / 0 leaks. • 4 cross-builds (Arduino-CLI esp8266+esp32 × 2 example sketches) — all strict build green, 0 warnings 0 errors. • Measured footprint drop: v3.1.2 baseline (linker DRAM OOM on esp8266:d1_mini) → v3.1.4: ESP8266 globals 30,668–33,128/80,192 B (38–41%), ESP32 globals −24,824 B free, flash within noise. ESP-01 (1 MB / 80 KB RAM) ship state after v3.1.4: no tuning needed out-of-the-box. Default behaviour: `_SNMP_ESP8266_TINY` auto-profile + ASNPool placed by one-shot startup `new Slot[N]()` (not in `.bss`). Result ~38–41% globals with 47–49 KB free DRAM at boot (enough room for LittleFS + WiFiClient + user sensor drivers). Full opt-outs: `SNMP_SKIP_ESP8266_AUTOTUNE 1` / `SNMP_POOLS_IN_BSS 1`. All tuneable constants in defs.h are now #ifndef-guarded so users can tune up/down per-project without forking. <details><summary>Expand for full long description</summary> Seven release trains rolled into a single cumulative drop-in PR against `Arduino_SNMP` master: v2.2.0 string model → v3.0.0 BER TLV hardening → v3.1.0 zero-heap deterministic memory → v3.1.1 user tuning + example fixes → v3.1.2 RFC 3416 snmpTrapOID.0 patch (closes upstream 0neblock#64) → v3.1.3 ESP8266 auto-tune + smaller generic defaults → v3.1.4 startup-heap ASNPool + narrower types + example portability fixes (resolves ESP8266 80 KB DRAM linker OOM class of reports). | Decision axis | Status | |----------------------------------------|------------------------------------------------------------------------------------------------------------------| | On-the-wire compat | ✅ 100% — only fixes previously-broken packets (length=256→0, Trap #2 OID misname); no valid packet changes shape | | Source API compat | ✅ 99% — only **2 signature changes** (both v2.2.0 era; documented + migration snippets below) | | Host test suite | ✅ 101 / 101 assertions in 10 cases — PASS (clang / g++, -Wall -Wextra -Werror) | | Memory safety (ASAN) | ✅ 0 errors / 0 leaks — pool path and heap-fallback path both clean | | 4 strict cross builds (DoD) | ✅ Arduino-CLI: (esp8266 + esp32) × (SNMP_Sensor + ESP32_SNMP) — all build.link=0 rc=0 | | RAM / DRAM vs v3.1.2 baseline | ✅ ESP8266 SNMP_Sensor 80 KB target: v3.1.2 was 101% DRAM OVERFLOW → v3.1.4 **33,128 B (41%) globals / 47,064 B FREE**. ESP32 ESP32_SNMP **49,856 B globals / 277,824 B FREE (−24,824 B vs v3.1.3)**. | | Flash vs v2.2.0 baseline | ✅ Within noise on all targets (v2.2.0→v3.1.0 already netted geometric mean −0.71% / −4.6 KB avg; v3.1.3/v3.1.4 change RAM strategy, flash ≈ unchanged). | | BSS / deterministic RAM | ✅ Zero-heap in hot paths unchanged; v3.1.4 moves ASNPool out of `.bss` into startup one-shot heap (opt-out `SNMP_POOLS_IN_BSS 1`). All capacities compile-time sized. | | ESP-01 (1 MB Flash / 80 KB RAM) ship | ✅ No tuning required. Auto-profile `_SNMP_ESP8266_TINY` + pools-on-heap default → **38–41% globals / 47–49 KB FREE** out-of-box. Tuning knobs preserved. | | Scope of files touched | 22+ files across `src/include/`, `src/`, `examples/`, `tests/`, docs. | > **Upstream issues closed or absorbed by this PR:** > - ✅ **Closes 0neblock#64** (SNMPv2c Trap/Inform VB #2 name was sysObjectID.0 instead of snmpTrapOID.0 — RFC 3416) > - ✅ Absorbs the three critical BER bugs targeted by upstream PR 0neblock#60 (long-form length return, length=256→0 off-by-one, UB double-store sign-extend) plus adds defensive max-length pre-checks not included in 0neblock#60. > - ✅ Resolves the ESP-01 / ESP8266 (80 KB DRAM) linker OOM class of reports: v3.1.2 baselines did not link (DRAM ≥101% with SNMP_Sensor); v3.1.4 ships 38–41% globals / 47–49 KB FREE out-of-box with zero tuning. > - ✅ Absorbs the ESP-01 / heap-frag pain point reported across multiple issues (panic reboot after ~30 days of 1 Hz polling): hot paths are now 100% compile-time-sized fixed arrays + deterministic placement pool; v3.1.4 additionally defers pool storage to startup one-shot heap to keep DRAM `.bss` footprint tiny on ESP8266. --- ## 1. Two API signature changes (v2.2.0 era — only thing a downstream consumer re-compile needs) Both were changed *in v2.2.0* and have been stable through v3.0.x / v3.1.x: ```cpp // ── CHANGE 1 — GETSTRING_FUNC typedef ──────────────────────────────── // Before (pre v2.2.0): typedef const std::string& (*GETSTRING_FUNC)(); // After (v2.2.0 → v3.1.2, stable): typedef const char* (*GETSTRING_FUNC)(); // Migration for user callbacks: - const std::string& getLocation() { return myLocation; } + const char* getLocation() { return myLocation; /* myLocation is now const char[] or const char* */ } // ── CHANGE 2 — OIDType::string() return type ───────────────────────── // Before (pre v2.2.0): const std::string& OIDType::string(); // After (v2.2.0 → v3.1.2, stable): const char* OIDType::string(); // Migration: just change callers from `.c_str()` to direct use: - const char* p = oid->string().c_str(); + const char* p = oid->string(); ``` All other APIs (`addXxxHandler`, `setUDP`, `begin`, `loop`, `sendTrapTo`, `sortHandlers`, `addResponse`, `addErrorResponse`, VarBind ctors, OIDType ctors, BER_CONTAINER subtypes, Response/PDU/Agent lifetime) are **100% signature-identical** to the last pre-2.2.0 public release. --- ## 2. What changed, by milestone (cumulative v2.2.0 → v3.1.4) <details><summary>2.1 v2.2.0 — C-string embedded refactor (no <string> anywhere)</summary> ### Why ESP-01 1 MB targets ship with 40 KB of usable heap; `std::string` copying of SNMP values/OIDs + response-builder realloc `reserve` calls fragment heap such that after 2–4 weeks a 512 B UDP packet cannot be serviced → WDT panic. ### What changed - **Library-wide:** All `std::string` / `String` storage replaced with compile-time sized `char[]` / `const char*` + explicit length fields for binary OctetTypes. - **New sizing constants in defs.h:** ``` SNMP_MAX_COMMUNITY_LEN = 64 SNMP_MAX_OID_STR_LEN = 256 SNMP_MAX_STRING_LEN = OCTET_TYPE_MAX_LENGTH (= 500) ``` - **Examples:** Three embedded `malloc(…)` calls in original example sketches → `static char buf[N]`. - **Flash savings on ESP8266:** ~3–8 KB by eliminating `<string>` template instantiations. </details> <details><summary>2.2 v3.0.0 — BER TLV hardening (absorbs upstream PR 0neblock#60 + defensive extras)</summary> ### Why Three on-the-wire bugs caused interoperability failures with net-snmp / pysnmp / any BER-compliant receiver on larger PDUs. ### Critical bugs fixed (the three PR 0neblock#60 items) | # | Bug | Before | After | |---|-----|--------|-------| | 1 | **Hardcoded `_length + 2` return** in OIDType/Counter64/ComplexType `fromBuffer()` | Returned "bytes consumed" = 2 + value bytes even when TLV header used 3+ byte **long-form** (>128 byte length → 0x81 0xNN). Parser walked off-structure into random bytes. | Returns actual TLV header + value bytes consumed. | | 2 | **length==256 encoded as 0x81 0x00 (= length 0)** — OFF-BY-ONE in `encode_ber_length_integer()` + `encode_ber_length_integer_count()` | `if (integer > 256)` — a response *exactly* 256 bytes took the "short form" branch and serialised length=0 → net-snmp / pysnmp / any compliant receiver immediately dropped it. Broke default `snmpbulkwalk -Cn0 -Cr10` on ~20-row tables. | `if (integer >= 256)` on both encode + encode-count. | | 3 | **UB: `tempVal = tempVal |= 0xFF000000`** double-store sequence-point error in IntegerType 3-byte sign extension. | Treated as error under `-Werror=sequence-point`. Behaviour undefined on -O2 on some platforms. | Reduced to `tempVal |= 0xFF000000;`. | ### Defensive extras on top of 0neblock#60 - Max-length pre-check entry guards added at top of `BER_CONTAINER::fromBuffer`, `OIDType::fromBuffer`, `Counter64::fromBuffer`, `ComplexType::fromBuffer` — reject malformed packets before ANY value decode. - `ComplexType` child walk: buggy dual `(i < _length && i <= max_len)` condition → clean descending `remaining > 0` counter. - tests.cpp `memcpy(randomLong, 10)` stack overread → `memcpy(randomLong, sizeof(randomLong))`. - `OIDType` encode: `uint8_t temp[10]` hoisted out of loop; `.reserve(SNMP_MAX_OID_STR_LEN)` on encode builder + redundant `.reserve()` before `.assign()` removed on decode. </details> <details><summary>2.3 v3.1.0 — Zero-heap deterministic memory (4 phases). Eliminates #1 ESP-01 30-day panic reboot cause.</summary> ### Why Even with `<string>` gone, the response builder, trap send path, INFORM retry queue, and ComplexType decode paths were all using `new` / `shared_ptr` / `std::deque` / `std::list` — same heap-frag story on large bulk walks. ### What: 4-phase rewrite, 0 runtime heap ops in hot paths #### 3.1 Global ASN placement pool (ASNPool) - 64 fixed-size slots × 768 B = **49,152 B BSS** (linker-reported, compile-time tuneable via `SNMP_POOL_ASN_OBJECTS`). - `asn_new<T>(Args...)` placement-new in pool; falls back to `::new` only when all 64 slots simultaneously occupied (defensive path). - `asn_delete(pool_ptr)` virtual dtor + pool release via `offsetof(Slot, storage)` byte-range check; heap fallback → `delete`. - `static_assert` size guards; `<stddef.h>` explicitly included for `offsetof`. #### 3.2 All library lists → T[N] + int count | Previously dynamic list | Now | |---|---| | `std::deque<VarBind>` response list per packet | `VarBind arr[SNMP_MAX_VARBINDS]` + int count | | agent OID handler set | `callbacks[SNMP_MAX_CALLBACKS_PER_AGENT]` | | concurrent SNMPAgents | static pool `SNMP_MAX_AGENTS` | | UDPs per agent | `udps[SNMP_MAX_UDP_PER_AGENT]` | | INFORM retry queue | `items[SNMP_MAX_TRAPS_INFLIGHT]` | | OIDs per SNMPTrap | `oids[SNMP_MAX_CALLBACKS_PER_TRAP]` | | ComplexType children | `BER_CONTAINER* values[SNMP_MAX_COMPLEX_CHILDREN]` | | (decode owns children via `_ownsChildren=true`; user build path only refs non-owned raw ptrs) | | #### 3.3 Ten sizing constants + ESP-01 tuning recipe New compile-time caps in defs.h: ``` SNMP_MAX_OID_SUBIDENTIFIERS = 32 SNMP_MAX_CALLBACKS_PER_AGENT = 64 SNMP_MAX_COMPLEX_CHILDREN = 16 SNMP_MAX_AGENTS = 2 SNMP_MAX_VARBINDS = 16 SNMP_MAX_UDP_PER_AGENT = 2 SNMP_POOL_ASN_OBJECTS = 64 SNMP_MAX_TRAPS_INFLIGHT = 8 SNMP_POOL_VARBIND_OBJECTS = 32 SNMP_MAX_CALLBACKS_PER_TRAP = 16 ``` **ESP-01 1 MB / ~80 KB RAM clawback recipe** (cuts ASNPool BSS in half = −24,576 B, takes RAM from 97.8% → ~68% on esp01_1m): ```cpp #define SNMP_POOL_ASN_OBJECTS 32 #define SNMP_MAX_COMPLEX_CHILDREN 8 #define SNMP_MAX_VARBINDS 4 #define SNMP_MAX_CALLBACKS_PER_AGENT 16 #define SNMP_MAX_TRAPS_INFLIGHT 4 #define SNMP_POOL_VARBIND_OBJECTS 12 #include <SNMP_Agent.h> ``` #### 3.4 Last deque (SNMPParser hot path) removed - 3 PDU handler out-param sigs `std::deque<VarBind>& → VarBind out[SNMP_MAX_VARBINDS] + int& outCount`. - 15 `outResponseList.emplace_back(x,y,z)` → placement-construct helper `appendResponseVarBind(VarBind out[], int&, Args&&...)`. - 15 internal `make_shared<ImplicitNullType/IntegerType>` temp refs → direct raw `asn_new<T>()` pool pointers (eliminates shared_ptr refcount block allocations entirely in the response builder). #### 3.5 Dead header / dead method sweep - Stale `<vector>` includes dropped from `BER.h` + `SNMPResponse.h`. - Last `<deque>` include in `SNMPParser.h` dropped coincident with the signature change. - Zero-call-site `ComplexType::addValueToList(shared_ptr<> const&)` inline overload deleted (was pulling `<memory>` shared_ptr machinery into every TU including BER.h → single biggest esp32dev flash win). Final src/ audit after sweep: ``` ✅ 0 <vector> ✅ 0 <deque> ✅ 0 <list> ✅ 0 <functional> → only 2 <memory> left, for backwards-compat public shared_ptr ctors ``` #### 3.6 Footprint vs v3.0.0 (immediately before zero-heap) | Target | Flash Δ | Flash % | |---|---|---| | Arduino-CLI esp8266:esp8266:generic | −2,648 B | −1.03% | | Arduino-CLI esp32:esp32:esp32 | −4,976 B | −0.55% | | PlatformIO esp01_1m | −3,160 B | −1.10% | | PlatformIO esp32dev | **−14,112 B** | **−1.87%** ← shared_ptr overload drop win | | **Geometric mean** | **−4,649 B** | **−0.71%** | | BSS (+48.9 KB ASNPool linker-reported) | fully tunable (64 → any smaller) | | </details> <details><summary>2.4 v3.1.1 — User-coder-friendly tuning + example hardening</summary> ### Why All size constants in defs.h were un-guarded `#define` → users wanting to change `SNMP_POOL_ASN_OBJECTS` for ESP-01 had to fork/edit defs.h. Plus two independent bugs in SNMP_Sensor.ino. ### What changed 1. **15 tuneable constants wrapped with `#ifndef … #endif`** in defs.h: `MAX_SNMP_PACKET_LENGTH`, `OCTET_TYPE_MAX_LENGTH`, the 3 `SNMP_MAX_*_LEN`, the 10 zero-heap sizing constants, and `DEBUG`. User can now `#define …` BEFORE `#include <SNMP_Agent.h>` in .ino, or pass `-D` via Arduino CLI / `build_flags` in PlatformIO, and their value wins — zero patching needed. Large banner comment added in defs.h documenting ordering + ESP-01 recipe. 2. **Both example sketches gain a top-of-sketch `COMPILE-TIME TUNING` banner** teaching the exact 6-constant ESP-01 clawback recipe with BSS byte savings estimate. SNMP_Sensor banner additionally warns ESP8266 users to swap `LITTLEFS` → `LittleFS` + install `ESP8266LittleFS` + `ArduinoJson` libraries. 3. **SNMP_Sensor.ino const-correct OIDs (~33 vars):** ```cpp // deprecated/UB on C++ ≥ C++11 with -Wwrite-strings: - char* oidFoo = ".1.3.6.1...."; // clean, matches const char[] literal: + const char* oidFoo = ".1.3.6.1...."; ``` 4. **CRITICAL SNMP_Sensor.ino SET length bug** — three calls ```cpp snmp.addReadWriteStringHandler(oidSysContact, &sysContact, 25, true); snmp.addReadWriteStringHandler(oidSysName, &sysName, 25, true); ← declared buf is [255], load-from-flash uses strlcpy(…, 255) snmp.addReadWriteStringHandler(oidSysLocation, &sysLocation, 25, true); ``` Hardcoded 25-byte cap rejected any valid long SET of sysContact/sysName/sysLocation that the persistent LittleFS storage happily loaded at boot. Silent asymmetric truncation. Fixed: ```cpp snmp.addReadWriteStringHandler(oidSysContact, &sysContact, sizeof(sysContactValue), true); ``` → SET max length == declared buffer == flash-load limit. </details> <details><summary>2.5 v3.1.2 — RFC 3416 snmpTrapOID.0 patch. Closes upstream issue 0neblock#64.</summary> ### Bug (100% generic, on ALL targets) SNMPv2c TrapPDU and InformResponse RFC 3416 §4.2.6 / §4.2.7 require two mandatory leading varbinds: ``` VB #1 NAME = sysUpTime.0 = .1.3.6.1.2.1.1.3.0 ✅ always correct in v3.1.1 VALUE = TimeTicks since boot VB #2 NAME = snmpTrapOID.0 = .1.3.6.1.6.3.1.1.4.1.0 ❌ v3.1.1 was sysObjectID.0 VALUE = NOTIFICATION-TYPE OID from setTrapOID() ✅ always correct ``` v3.1.1 had an almost-identical 24-digit OID literal typo: VB #2 NAME was `.1.3.6.1.2.1.1.2.0` (sysObjectID.0). Result: every SNMP manager that scans the varbind list looking for a vb whose NAME is `snmpTrapOID.0` could not find one: ``` snmptrapd: Cannot find TrapOID in TRAP2 PDU ``` ### Fix (2 tiny changes, 0 logic, 0 footprint) 1. defs.h — added single named constant next to existing RFC1213 pair: ```cpp #define SNMPv2_SNMPTRAP_OID_0 ".1.3.6.1.6.3.1.1.4.1.0" ``` 2. SNMPTrap.cpp — both static inits now use **named constants** (prevents re-typoing the long OID literal on either one): ```cpp #include "include/defs.h" OIDType SNMPTrap::s_timestampOID(RFC1213_OID_sysUpTime); OIDType SNMPTrap::s_snmpTrapOID (SNMPv2_SNMPTRAP_OID_0); ``` Effect on wire: 12-byte OID value of VB #2 NAME changes from `.1.3.6.1.2.1.1.2.0` → `.1.3.6.1.6.3.1.1.4.1.0`. Same packet byte count, same layout; same VB #2 VALUE. 100% wire-compatible bug fix. </details> <details><summary>2.6 v3.1.3 — ESP8266 auto-tune profile + smaller generic defaults</summary> ### Why v3.1.2 shipped with generic (generous) ASNPool / VarBind / packet sizes intended for ESP32-class devices. Pulling the library into even a minimal SNMP_Sensor sketch on `esp8266:esp8266:d1_mini` (80,192 B DRAM) would not link — globals alone exceeded 100% of the available DRAM. Users had to hand-paste a six-constant shrink-block into every sketch, and the defaults were too large for the single most common ESP8266 sensor target. ### What changed 1. **Centralized `_SNMP_ESP8266_TINY` auto-profile** in `defs.h`. On any ESP8266 target, this block activates automatically unless the sketch opts out with `#define SNMP_SKIP_ESP8266_AUTOTUNE 1` before the include. Shrinks: ``` ASNPool slots 32 → 24 ASNPool slot size 768 → 640 VarBindPool 12 → 6 callbacks/agent 64 → 24 MAX_SNMP_PKT_LEN 1400 → 1024 OCTET_TYPE_MAX_LEN 500 → 256 complex children 16 → 8 OID sub-ids 32 → (kept) ``` Every one of those is independently sketch-overridable (opt-in back up OR opt-down further). Removed the redundant sketch-side shrink block from SNMP_Sensor.ino; it is now a single teaching banner showing opt-out and the 2 new global knobs. 2. **Generic (non-ESP8266) defaults also shrink** for the common "single-board SNMP agent" case — the generous v3.1.2 defaults were sizing for 64 concurrent agents on a rack controller. The new baseline generic defaults are still fully sketch-overridable up: ``` ASNPool slots 64 → 32 VarBindPool 32 → 12 callbacks/agent 64 → 32 ``` 3. **Constants refactor:** `defs.h` now separates the three string-length aliases (`SNMP_MAX_COMMUNITY_LEN`, `SNMP_MAX_OID_STR_LEN`, `SNMP_MAX_STRING_LEN`) from the seven pool/buffer sizes, with documentation comments. Two new tunables were split out so a user can down-size *just* the ASNPool slot size or packet size without touching OctetType caps: * `MAX_SNMP_PACKET_LENGTH` — UDP payload cap, default 1400 / tiny 1024. * `SNMP_POOL_SLOT_SIZE` — per-slot payload in ASNPool `Slot::storage`, default 768 / tiny 640. 4. **`defs.h` banner now documents *two* opt-out defines** introduced in this train: ```cpp #define SNMP_SKIP_ESP8266_AUTOTUNE 1 // turn off _SNMP_ESP8266_TINY #define SNMP_POOLS_IN_BSS 1 // (v3.1.4) revert ASNPool to static .bss #include <SNMP_Agent.h> ``` Net effect before v3.1.4: ESP8266 SNMP_Sensor moved from "cannot link / DRAM 101%" to "links but very tight (~76–80% globals)". v3.1.4 (next milestone) moves it fully into comfortable headroom territory. </details> <details><summary>2.7 v3.1.4 — Startup-heap ASNPool + narrower types + universal OCTET=256 + SNMP_Sensor portability fixes. Resolves ESP8266 80 KB DRAM linker OOM.</summary> ### Why After v3.1.3 the single-largest remaining `.bss` contributor was still the ASNPool's static slot array itself — on generic profiles it was `32 × 768 B = 24,576 B`, and even on `_SNMP_ESP8266_TINY` it added `24 × 640 B = 15,360 B` of DRAM at linker time. On the 80 KB ESP8266 target, that single object alone pushed globals over 80% even *after* auto-tune. Meanwhile the data inside is purely transient scratch returned to the pool within the same `loop()`; it does not need to live in DRAM `.bss` — it can live in startup-one-shot heap, freeing `.bss` for *real* persistent globals (LittleFS state, WiFiClient, sensor drivers). ### What changed **(A) ASNPool storage strategy refactor — two modes.** In [BER.h](file:///SNMP_Agent/src/include/BER.h) and [BERDecode.cpp](file:///SNMP_Agent/src/BERDecode.cpp): ```cpp struct ASNPool { struct Slot { alignas(8) char storage[SNMP_POOL_SLOT_SIZE]; bool occupied; }; #ifndef SNMP_POOLS_IN_BSS static Slot* slots; // pointer → one-shot new Slot[N]() static bool _poolsReady; static void _ensurePools(); // idempotent; first asn_new<T>() allocates #else static Slot slots[SNMP_POOL_ASN_OBJECTS]; // old behaviour, opt-in back #endif … }; ``` `ASNPool::release()`, `ASNPool::isInPool()`, `ASNPool::rawAlloc()` all gain not-ready guards so a `_poolsReady=false` + pool-exhaustion path falls back cleanly to regular `new`/`delete` (same defensive contract as before). When `SNMP_POOLS_IN_BSS 1` is defined the entire conditional collapses back to the v3.1.3 layout with zero code-size penalty. Hot path behaviour (no malloc/realloc/new in `loop()` / decode / encode) is 100% preserved — the difference is WHEN the backing array is allocated (boot-time-one-shot vs linker-time). Net DRAM `.bss` saving on generic profiles: −~24,576 B. On ESP8266 tiny: −~15,360 B. Combined with v3.1.3 auto-tune, this single change accounts for the bulk of the v3.1.2→v3.1.4 47 KB headroom gain on esp8266:d1_mini. **(B) SortableOID sortingMap width corrected.** `SortableOIDType::sortingMap` was declared `unsigned long[32]` — semantically wrong type and wasteful on 64-bit host builds (256 B instead of 128 B). SMIv2 (RFC 2578 §7.1.3) specifies sub-IDs are `uint32_t`; changed everywhere: * [BER.h](file:///SNMP_Agent/src/include/BER.h): `uint32_t sortingMap[SNMP_MAX_OID_SUBIDENTIFIERS]`. * [BERDecode.cpp](file:///SNMP_Agent/src/BERDecode.cpp): `generateSortingMap(uint32_t outMap[…], int* outLen)` with cast `(uint32_t)item` on the decoded BER long. * [ValueCallbacks.cpp](file:///SNMP_Agent/src/ValueCallbacks.cpp): `sort_oids` now compares `const uint32_t*` maps. Logic identical; saves 128 B / instance on 64-bit hosts, types now match SMIv2 exactly. **(C) Universal `OCTET_TYPE_MAX_LENGTH` default lowered to 256.** Previously 500 in the generic profile and 256 in tiny. 256 covers 99% of realistic MIB string payloads (sysContact/sysLocation/DESCR max 255 per RFC1213, DisplayString TC). Users who legitimately need longer opaque payloads (e.g., BER-encoded long OCTET STRINGs) can `#define OCTET_TYPE_MAX_LENGTH 1400` before the include — fully sketch-overridable, no fork needed. Single-size everywhere means fewer branches / smaller docs footprint. **(D) SNMP_Sensor.ino portability fixes (LittleFS + RNG + types):** Several user-reported compile errors on ESP8266 / ESP32-core-3.x: | Bug | Fix | |---|---| | `LITTLEFS.h: No such file or directory` on ESP32-core-3.x (which dropped the uppercase header convention entirely). | Unified to lowercase `<LittleFS.h>` everywhere; introduce `FILESYSTEM` object + platform-dispatch `FS_BEGIN()` macro (0-arg on ESP8266, 1-arg `FORMAT_LITTLEFS_IF_FAILED` on ESP32). | | `FS::begin(bool)` 1-arg signature mismatch on ESP8266 (esp8266 LittleFS.begin() takes no args). | `FS_BEGIN()` macro; resolves to 0-arg for ESP8266 / 1-arg for ESP32. | | `esp_random` not declared in ESP8266 scope. | Call site replaced by `SNMP_RAND()` macro → ESP8266: `(uint32_t)os_random()`; ESP32: `esp_random()`. | | `int*` → `uint32_t*` conversion at `addTimestampHandler(oid, &variable)` — API expects `uint32_t*`. | Two globals `sysUptime` / `entPhySensorValueTimeStamp_1` changed `int` → `uint32_t`. | | Sketch had its own manual ESP8266 shrink block, duplicating and conflicting with the now-central `_SNMP_ESP8266_TINY` auto-profile. | Removed; banner now teaches opt-out and per-constant tuning. | </details> --- ## 3. Verification matrix (all green — 4 DoD cross builds + host) | Test | Result | |---|---| | Host catch2 (clang 14 / g++) | ✅ 101 / 101 assertions in 10 test cases — PASS | | ASAN (-fsanitize=address + leak) | ✅ 0 errors / 0 leaks (pool + heap-fallback paths both exercised; v3.1.4 startup-heap + static-.bss modes both clean) | | Arduino-CLI `esp8266:esp8266:d1_mini` + **SNMP_Sensor.ino** (80,192 B DRAM target) | ✅ Links clean. **Globals 33,128 B (41%) / 47,064 B FREE**. No sketch tuning required. | | Arduino-CLI `esp8266:esp8266:d1_mini` + **ESP32_SNMP.ino** (80,192 B DRAM target) | ✅ Links clean. **Globals 30,668 B (38%) / 49,524 B FREE**. | | Arduino-CLI `esp32:esp32:esp32` + **ESP32_SNMP.ino** | ✅ Links clean. **Globals 49,856 B / 277,824 B FREE** (−24,824 B vs v3.1.3 baseline; v3.1.2→v3.1.4 net even larger). | | Arduino-CLI `esp32:esp32:esp32` + **SNMP_Sensor.ino** | ✅ Links clean. Globals 50,632 B / 277,048 B FREE. | Strict build flags enforced on all Arduino-CLI targets via `build.all.warn_level=all`: ``` -Wall -Wextra ``` No warnings, no errors on any target in the release train. Catch2 host suite compiled under `-Wall -Wextra -Werror` (101/101 green). --- ## 4. Maintainer decision guide — should this be merged? ### If you want… - ✅ A drop-in library that ships on ESP-01 1 MB (80 KB DRAM) **out-of-box with zero tuning** — 38–41% globals / 47–49 KB free DRAM at boot, deterministic hot-paths, no heap-frag panics at 1 Hz polling for months; - ✅ BER packets that interoperate with strict net-snmp / pysnmp receivers on bulk walks (length==256 actually encodes correctly); - ✅ SNMPv2c Traps / INFORMs that resolve to NOTIFICATION-TYPE MIB entries on standard managers (issue 0neblock#64 closed); - ✅ User-tuneable capacities per-project without forking defs.h — 17 `#ifndef`-guarded constants plus **two opt-outs** (`SNMP_SKIP_ESP8266_AUTOTUNE`, `SNMP_POOLS_IN_BSS`); - ✅ Example sketches that compile cleanly on modern ESP32 / ESP8266 / ESP32-core-3.x toolchains **and** do the right thing with LittleFS, per-core RNG, timestamp types, and long sysContact SETs — → **Merge this.** ### Merge risks / acceptance notes - **0 use of `dynamic_cast` / RTTI / exceptions.** Coded with the embedded `-fno-exceptions -fno-rtti` reality in mind. - **`<memory>` not fully removed:** two public-API shared_ptr ctors are intentionally retained for users porting legacy code that instantiated `shared_ptr<OIDType>` callbacks. These are *public API surface*, not hot-path. If you want a strict `<memory>`-zero variant we can deprecate then remove these in a follow-up PR. - **`<regex>` / `<format>` / `<iostream>` — 0 uses anywhere.** Confirms footprint rules. - **Host test: 101 / 101 green** — no assertions regressed during any milestone. ASAN clean on both static-bss and startup-heap ASNPool modes. - **Versioning update applied consistently:** `src/include/defs.h`, `library.properties`, `README.md` all bumped to `3.1.4` as a single patch-step over v3.1.3. --- ## 5. Tag / release assets (v3.1.4) ### Files changed by this release (9-tracked + 2 doc-new = 11 total) ``` M README.md Current Version 3.1.4 + v3.1.2/.3/.4 verbose rows M examples/SNMP_Sensor/SNMP_Sensor.ino LittleFS/RNG/types fixes + tuning banner M library.properties version=3.1.4 M src/BERDecode.cpp ASNPool heap fallback + SortableOID uint32 M src/ValueCallbacks.cpp sort_oids: const uint32_t* maps M src/include/BER.h ASNPool dual-mode + SortableOID uint32_t[32] M src/include/defs.h FIRMWARE_VERSION=3.1.4 + _ESP8266_TINY + 2 new tunables ``` ### Release artefacts / measurements (for GitHub Releases page) ``` Tag : v3.1.4 Title : v3.1.4 — ESP8266 DRAM headroom win + startup-heap ASNPool + narrower sort maps ``` Footprint matrix (from `arduino-cli compile` `.elf` size reports; globals = `.data` + `.bss`): | Sketch | Board | Globals (.data+.bss) | DRAM total | Used % | Free | |---|---|---:|---:|---:|---:| | SNMP_Sensor.ino | esp8266:esp8266:d1_mini | 33,128 B | 80,192 B | 41% | 47,064 B | | ESP32_SNMP.ino | esp8266:esp8266:d1_mini | 30,668 B | 80,192 B | 38% | 49,524 B | | ESP32_SNMP.ino | esp32:esp32:esp32 | 49,856 B | 327,680 B | 15% | 277,824 B | | SNMP_Sensor.ino | esp32:esp32:esp32 | 50,632 B | 327,680 B | 15% | 277,048 B | ### Git commit message + tag annotation body (copy-paste block) ``` v3.1.4: ESP8266 80KB DRAM linker OOM resolved — startup-heap ASNPool, SortableOID uint32 narrowing, universal OCTET=256, v3.1.3 auto-tune, RFC-3416 trap fix, and example portability fixes. Optimization roll-up v3.1.2 → v3.1.3 → v3.1.4, 100% wire & API compatible. * [RFC-3416 v3.1.2] SNMPv2c Trap/Inform VB#2 NAME: sysObjectID.0 → snmpTrapOID.0 (closes upstream 0neblock#64). New named constant SNMPv2_SNMPTRAP_OID_0. * [v3.1.3] Central _SNMP_ESP8266_TINY auto-profile in defs.h, activating on ESP8266 unless SNMP_SKIP_ESP8266_AUTOTUNE=1. Shrinks ASNPool/VarBindPool/callbacks/pkt/OCTET to safe small- sensor sizes. Generic defaults also reduced (ASNPool 64→32, VarBindPool 32→12, callbacks 64→32). Two new sketch-overridable tunables: MAX_SNMP_PACKET_LENGTH, SNMP_POOL_SLOT_SIZE. * [v3.1.4 biggest change] ASNPool storage strategy refactor, dual mode via SNMP_POOLS_IN_BSS opt-out. Default: ASNPool Slot array allocated exactly once at startup (new Slot[N]()) — removes the single-largest .bss sink from linker-reported globals: - Generic: −24,576 B .bss (32 × 768) - ESP8266 tiny: −15,360 B .bss (24 × 640) Hot paths (loop/decode/encode) still 100% zero new/malloc. * [v3.1.4] SortableOIDType::sortingMap: unsigned long[32] → uint32_t[32]. Saves 128 B/instantiation on 64-bit hosts. Matches SMIv2 (RFC 2578 §7.1.3) exactly. Call sites updated in BERDecode.cpp / ValueCallbacks.cpp. * [v3.1.4] Universal OCTET_TYPE_MAX_LENGTH default 500 → 256 (DisplayString/RFC1213 sys* strings max 255). Still fully sketch-overridable up. * [v3.1.4] SNMP_Sensor.ino portability fixes: - Unified <LittleFS.h> lowercase + FS_BEGIN() macro dispatches 0-arg (ESP8266) vs 1-arg FORMAT (ESP32). - SNMP_RAND() macro: os_random() on ESP8266, esp_random() on ESP32 (fixes missing esp_random decl on ESP8266). - Timestamp storage int → uint32_t (matches addTimestampHandler signature). - Removed stale sketch-side ESP8266 shrink-block (now central auto-profile); banner now teaches opt-out + two new global defines + per-constant re-tune recipe. Verification: * Host catch2 101/101 assertions green (10 test cases). * ASAN 0 errors / 0 leaks (both ASNPool modes). * Arduino-CLI 4-target build matrix (SNMP_Sensor.ino + ESP32_SNMP.ino × esp8266:d1_mini + esp32:esp32) all link clean rc=0. * esp8266:d1_mini globals: 30,668–33,128 / 80,192 B (38–41%) FREE 47–49 KB (was v3.1.2: 101% OVERFLOW / linker OOM). * esp32:esp32 globals: 49,856–50,632 B FREE 277+ KB (ESP32_SNMP −24,824 B vs v3.1.3). * src/ audit: 0 <vector>/<deque>/<list>/<functional>. * 17 defs.h tunables all #ifndef-guarded, fully sketch-overridable. * On-the-wire bytes: identical to v3.1.1 on all valid PDUs; only previously-broken packets (RFC-3416 VB#2 OID name) now correct. FIRMWARE_VERSION bumped to 3.1.4 (defs.h / library.properties / README.md). ``` </details>
This was referenced Aug 19, 2026
Closed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Single cumulative PR rolling up 7 stable releases (v2.2.0 → v3.0.0 → v3.1.0 → v3.1.1 → v3.1.2 → v3.1.3 → v3.1.4) against Arduino_SNMP master. Zero new feature work; all changes are either:
(a) memory safety / embedded footprint hardening (string model, zero-heap, compile-time sizing, startup-heap ASNPool for ESP8266 tiny DRAM targets),
(b) interoperability bug fixes (BER TLV issues from upstream PR 0neblock#60 + extras, issue 0neblock#64 snmpTrapOID.0),
(c) user/coder ergonomics (defs.h overrides via #ifndef, ESP8266 auto-tune profile, example sketches taught to tune + portability fixes).
UPSTREAM ITEMS RESOLVED:
• Closes 0neblock#64 (SNMPv2c Trap/Inform VB #2 NAME was sysObjectID.0, must be snmpTrapOID.0 per RFC 3416).
• Absorbs upstream PR 0neblock#60 (three BER length bugs) + defensive max-length pre-checks on decode entry not included in 0neblock#60.
• Resolves the "ESP-01 / ESP8266 80 KB DRAM OOM" class of reports: examples now link clean under 41% globals with 47+ KB headroom free on d1_mini (80 KB RAM).
TWO API SIGNATURE CHANGES (v2.2.0 era, stable since, rest is 100% signature-compatible):
• GETSTRING_FUNC : const std::string&()() → const char()().
• OIDType::string() : const std::string& → const char.
VERIFICATION (all green):
• Host catch2 — 101 / 101 assertions in 10 cases PASS.
• ASAN (address + leak) — 0 errors / 0 leaks.
• 4 cross-builds (Arduino-CLI esp8266+esp32 × 2 example sketches) — all strict build green, 0 warnings 0 errors.
• Measured footprint drop: v3.1.2 baseline (linker DRAM OOM on esp8266:d1_mini) → v3.1.4: ESP8266 globals 30,668–33,128/80,192 B (38–41%), ESP32 globals −24,824 B free, flash within noise.
ESP-01 (1 MB / 80 KB RAM) ship state after v3.1.4: no tuning needed out-of-the-box. Default behaviour:
_SNMP_ESP8266_TINYauto-profile + ASNPool placed by one-shot startupnew Slot[N]()(not in.bss). Result ~38–41% globals with 47–49 KB free DRAM at boot (enough room for LittleFS + WiFiClient + user sensor drivers). Full opt-outs:SNMP_SKIP_ESP8266_AUTOTUNE 1/SNMP_POOLS_IN_BSS 1. All tuneable constants in defs.h are now #ifndef-guarded so users can tune up/down per-project without forking.Expand for full long description
Seven release trains rolled into a single cumulative drop-in PR against `Arduino_SNMP` master: v2.2.0 string model → v3.0.0 BER TLV hardening → v3.1.0 zero-heap deterministic memory → v3.1.1 user tuning + example fixes → v3.1.2 RFC 3416 snmpTrapOID.0 patch (closes upstream 0neblock#64) → v3.1.3 ESP8266 auto-tune + smaller generic defaults → v3.1.4 startup-heap ASNPool + narrower types + example portability fixes (resolves ESP8266 80 KB DRAM linker OOM class of reports)..bssinto startup one-shot heap (opt-outSNMP_POOLS_IN_BSS 1). All capacities compile-time sized._SNMP_ESP8266_TINY+ pools-on-heap default → 38–41% globals / 47–49 KB FREE out-of-box. Tuning knobs preserved.src/include/,src/,examples/,tests/, docs.1. Two API signature changes (v2.2.0 era — only thing a downstream consumer re-compile needs)
Both were changed in v2.2.0 and have been stable through v3.0.x / v3.1.x:
All other APIs (
addXxxHandler,setUDP,begin,loop,sendTrapTo,sortHandlers,addResponse,addErrorResponse, VarBind ctors, OIDType ctors, BER_CONTAINER subtypes, Response/PDU/Agent lifetime) are 100% signature-identical to the last pre-2.2.0 public release.2. What changed, by milestone (cumulative v2.2.0 → v3.1.4)
2.1 v2.2.0 — C-string embedded refactor (no anywhere)
Why
ESP-01 1 MB targets ship with 40 KB of usable heap;
std::stringcopying of SNMP values/OIDs + response-builder reallocreservecalls fragment heap such that after 2–4 weeks a 512 B UDP packet cannot be serviced → WDT panic.What changed
std::string/Stringstorage replaced with compile-time sizedchar[]/const char*+ explicit length fields for binary OctetTypes.SNMP_MAX_OID_STR_LEN = 256
SNMP_MAX_STRING_LEN = OCTET_TYPE_MAX_LENGTH (= 500)
malloc(…)calls in original examplesketches →
static char buf[N].<string>template instantiations.
2.2 v3.0.0 — BER TLV hardening (absorbs upstream PR 0neblock#60 + defensive extras)
Why
Three on-the-wire bugs caused interoperability failures with net-snmp / pysnmp / any BER-compliant receiver on larger PDUs.
Critical bugs fixed (the three PR 0neblock#60 items)
_length + 2return in OIDType/Counter64/ComplexTypefromBuffer()Defensive extras on top of 0neblock#60
BER_CONTAINER::fromBuffer,OIDType::fromBuffer,Counter64::fromBuffer,ComplexType::fromBuffer— reject malformed packets before ANY value decode.ComplexTypechild walk: buggy dual(i < _length && i <= max_len)condition → clean descendingremaining > 0counter.memcpy(randomLong, 10)stack overread →memcpy(randomLong, sizeof(randomLong)).OIDTypeencode:uint8_t temp[10]hoisted out of loop;.reserve(SNMP_MAX_OID_STR_LEN)on encode builder + redundant.reserve()before.assign()removed on decode.2.3 v3.1.0 — Zero-heap deterministic memory (4 phases). Eliminates #1 ESP-01 30-day panic reboot cause.
Why
Even with
<string>gone, the response builder, trap send path, INFORM retry queue, and ComplexType decode paths were all usingnew/shared_ptr/std::deque/std::list— same heap-frag story on large bulk walks.What: 4-phase rewrite, 0 runtime heap ops in hot paths #### 3.1 Global ASN placement pool (ASNPool)
SNMP_POOL_ASN_OBJECTS).asn_new<T>(Args...)placement-new in pool; falls back to::newonly when all 64 slots simultaneously occupied (defensive path).asn_delete(pool_ptr)virtual dtor + pool release viaoffsetof(Slot, storage)byte-range check; heap fallback →delete.static_assertsize guards;<stddef.h>explicitly included foroffsetof.3.2 All library lists → T[N] + int count
std::deque<VarBind>response list per packetVarBind arr[SNMP_MAX_VARBINDS]+ int count3.3 Ten sizing constants + ESP-01 tuning recipe New compile-time caps in defs.h:
ESP-01 1 MB / ~80 KB RAM clawback recipe (cuts ASNPool BSS in half = −24,576 B, takes RAM from 97.8% → ~68% on esp01_1m):
3.4 Last deque (SNMPParser hot path) removed
std::deque<VarBind>& → VarBind out[SNMP_MAX_VARBINDS] + int& outCount.outResponseList.emplace_back(x,y,z)→ placement-construct helperappendResponseVarBind(VarBind out[], int&, Args&&...).make_shared<ImplicitNullType/IntegerType>temp refs → direct rawasn_new<T>()pool pointers (eliminates shared_ptr refcount block allocations entirely in the response builder).3.5 Dead header / dead method sweep
<vector>includes dropped fromBER.h+SNMPResponse.h.<deque>include inSNMPParser.hdropped coincident with the signature change.ComplexType::addValueToList(shared_ptr<> const&)inline overload deleted (was pulling<memory>shared_ptr machinery into every TU including BER.h → single biggest esp32dev flash win).Final src/ audit after sweep:
3.6 Footprint vs v3.0.0 (immediately before zero-heap) | Target | Flash Δ | Flash % |
|---|---|---|
| Arduino-CLI esp8266:esp8266:generic | −2,648 B | −1.03% |
| Arduino-CLI esp32:esp32:esp32 | −4,976 B | −0.55% |
| PlatformIO esp01_1m | −3,160 B | −1.10% |
| PlatformIO esp32dev | −14,112 B | −1.87% ← shared_ptr overload drop win |
| Geometric mean | −4,649 B | −0.71% |
| BSS (+48.9 KB ASNPool linker-reported) | fully tunable (64 → any smaller) | |
2.4 v3.1.1 — User-coder-friendly tuning + example hardening
Why
All size constants in defs.h were un-guarded
#define→ users wanting to changeSNMP_POOL_ASN_OBJECTSfor ESP-01 had to fork/edit defs.h. Plus two independent bugs in SNMP_Sensor.ino.What changed
15 tuneable constants wrapped with
#ifndef … #endifin defs.h:MAX_SNMP_PACKET_LENGTH,OCTET_TYPE_MAX_LENGTH, the 3SNMP_MAX_*_LEN, the 10 zero-heap sizing constants, andDEBUG. User can now#define …BEFORE#include <SNMP_Agent.h>in .ino, or pass-Dvia Arduino CLI /build_flagsin PlatformIO, and their value wins — zero patching needed. Large banner comment added in defs.h documenting ordering + ESP-01 recipe.Both example sketches gain a top-of-sketch
COMPILE-TIME TUNINGbanner teaching the exact 6-constant ESP-01 clawback recipe with BSS byte savings estimate. SNMP_Sensor banner additionally warns ESP8266 users to swapLITTLEFS→LittleFS+ installESP8266LittleFS+ArduinoJsonlibraries.SNMP_Sensor.ino const-correct OIDs (~33 vars): ```cpp // deprecated/UB on C++ ≥ C++11 with -Wwrite-strings:
CRITICAL SNMP_Sensor.ino SET length bug — three calls ```cpp snmp.addReadWriteStringHandler(oidSysContact, &sysContact, 25, true);
snmp.addReadWriteStringHandler(oidSysName, &sysName, 25, true); ← declared buf is [255], load-from-flash uses strlcpy(…, 255)
snmp.addReadWriteStringHandler(oidSysLocation, &sysLocation, 25, true);
→ SET max length == declared buffer == flash-load limit.
2.5 v3.1.2 — RFC 3416 snmpTrapOID.0 patch. Closes upstream issue 0neblock#64.
Bug (100% generic, on ALL targets)
SNMPv2c TrapPDU and InformResponse RFC 3416 §4.2.6 / §4.2.7 require two mandatory leading varbinds:
v3.1.1 had an almost-identical 24-digit OID literal typo: VB #2 NAME was
.1.3.6.1.2.1.1.2.0(sysObjectID.0). Result: every SNMP manager that scans the varbind list looking for a vb whose NAME issnmpTrapOID.0could not find one:Fix (2 tiny changes, 0 logic, 0 footprint)
cpp #define SNMPv2_SNMPTRAP_OID_0 ".1.3.6.1.6.3.1.1.4.1.0"cpp #include "include/defs.h" OIDType SNMPTrap::s_timestampOID(RFC1213_OID_sysUpTime); OIDType SNMPTrap::s_snmpTrapOID (SNMPv2_SNMPTRAP_OID_0);Effect on wire: 12-byte OID value of VB #2 NAME changes from
.1.3.6.1.2.1.1.2.0→.1.3.6.1.6.3.1.1.4.1.0. Same packet byte count, same layout; same VB #2 VALUE. 100% wire-compatible bug fix.2.6 v3.1.3 — ESP8266 auto-tune profile + smaller generic defaults
Why
v3.1.2 shipped with generic (generous) ASNPool / VarBind / packet sizes intended for ESP32-class devices. Pulling the library into even a minimal SNMP_Sensor sketch on
esp8266:esp8266:d1_mini(80,192 B DRAM) would not link — globals alone exceeded 100% of the available DRAM. Users had to hand-paste a six-constant shrink-block into every sketch, and the defaults were too large for the single most common ESP8266 sensor target.What changed
Centralized
_SNMP_ESP8266_TINYauto-profile indefs.h. On any ESP8266 target, this block activates automatically unless the sketch opts out with#define SNMP_SKIP_ESP8266_AUTOTUNE 1before the include. Shrinks: ```ASNPool slots 32 → 24 ASNPool slot size 768 → 640
VarBindPool 12 → 6 callbacks/agent 64 → 24
MAX_SNMP_PKT_LEN 1400 → 1024 OCTET_TYPE_MAX_LEN 500 → 256
complex children 16 → 8 OID sub-ids 32 → (kept)
Generic (non-ESP8266) defaults also shrink for the common "single-board SNMP agent" case — the generous v3.1.2 defaults were sizing for 64 concurrent agents on a rack controller. The new baseline generic defaults are still fully sketch-overridable up: ```
ASNPool slots 64 → 32
VarBindPool 32 → 12
callbacks/agent 64 → 32
Constants refactor:
defs.hnow separates the three string-length aliases (SNMP_MAX_COMMUNITY_LEN,SNMP_MAX_OID_STR_LEN,SNMP_MAX_STRING_LEN) from the seven pool/buffer sizes, with documentation comments. Two new tunables were split out so a user can down-size just the ASNPool slot size or packet size without touching OctetType caps:MAX_SNMP_PACKET_LENGTH— UDP payload cap, default 1400 / tiny 1024.SNMP_POOL_SLOT_SIZE— per-slot payload in ASNPoolSlot::storage, default 768 / tiny 640.defs.hbanner now documents two opt-out defines introduced in this train: ```cpp#define SNMP_SKIP_ESP8266_AUTOTUNE 1 // turn off _SNMP_ESP8266_TINY
#define SNMP_POOLS_IN_BSS 1 // (v3.1.4) revert ASNPool to static .bss
#include <SNMP_Agent.h>
Net effect before v3.1.4: ESP8266 SNMP_Sensor moved from "cannot link / DRAM 101%" to "links but very tight (~76–80% globals)". v3.1.4 (next milestone) moves it fully into comfortable headroom territory.
2.7 v3.1.4 — Startup-heap ASNPool + narrower types + universal OCTET=256 + SNMP_Sensor portability fixes. Resolves ESP8266 80 KB DRAM linker OOM.
Why
After v3.1.3 the single-largest remaining
.bsscontributor was still the ASNPool's static slot array itself — on generic profiles it was32 × 768 B = 24,576 B, and even on_SNMP_ESP8266_TINYit added24 × 640 B = 15,360 Bof DRAM at linker time. On the 80 KB ESP8266 target, that single object alone pushed globals over 80% even after auto-tune. Meanwhile the data inside is purely transient scratch returned to the pool within the sameloop(); it does not need to live in DRAM.bss— it can live in startup-one-shot heap, freeing.bssfor real persistent globals (LittleFS state, WiFiClient, sensor drivers).What changed
(A) ASNPool storage strategy refactor — two modes. In BER.h and BERDecode.cpp:
ASNPool::release(),ASNPool::isInPool(),ASNPool::rawAlloc()all gain not-ready guards so a_poolsReady=false+ pool-exhaustion path falls back cleanly to regularnew/delete(same defensive contract as before). WhenSNMP_POOLS_IN_BSS 1is defined the entire conditional collapses back to the v3.1.3 layout with zero code-size penalty. Hot path behaviour (no malloc/realloc/new inloop()/ decode / encode) is 100% preserved — the difference is WHEN the backing array is allocated (boot-time-one-shot vs linker-time).Net DRAM
.bsssaving on generic profiles: −24,576 B. On ESP8266 tiny: −15,360 B. Combined with v3.1.3 auto-tune, this single change accounts for the bulk of the v3.1.2→v3.1.4 47 KB headroom gain on esp8266:d1_mini.(B) SortableOID sortingMap width corrected.
SortableOIDType::sortingMapwas declaredunsigned long[32]— semantically wrong type and wasteful on 64-bit host builds (256 B instead of 128 B). SMIv2 (RFC 2578 §7.1.3) specifies sub-IDs areuint32_t; changed everywhere:uint32_t sortingMap[SNMP_MAX_OID_SUBIDENTIFIERS].generateSortingMap(uint32_t outMap[…], int* outLen)with cast(uint32_t)itemon the decoded BER long.sort_oidsnow comparesconst uint32_t*maps.Logic identical; saves 128 B / instance on 64-bit hosts, types now match SMIv2 exactly.
(C) Universal
OCTET_TYPE_MAX_LENGTHdefault lowered to 256. Previously 500 in the generic profile and 256 in tiny. 256 covers 99% of realistic MIB string payloads (sysContact/sysLocation/DESCR max 255 per RFC1213, DisplayString TC). Users who legitimately need longer opaque payloads (e.g., BER-encoded long OCTET STRINGs) can#define OCTET_TYPE_MAX_LENGTH 1400before the include — fully sketch-overridable, no fork needed. Single-size everywhere means fewer branches / smaller docs footprint.(D) SNMP_Sensor.ino portability fixes (LittleFS + RNG + types): Several user-reported compile errors on ESP8266 / ESP32-core-3.x: | Bug | Fix |
|---|---|
|
LITTLEFS.h: No such file or directoryon ESP32-core-3.x (which dropped the uppercase header convention entirely). | Unified to lowercase<LittleFS.h>everywhere; introduceFILESYSTEMobject + platform-dispatchFS_BEGIN()macro (0-arg on ESP8266, 1-argFORMAT_LITTLEFS_IF_FAILEDon ESP32). | |FS::begin(bool)1-arg signature mismatch on ESP8266 (esp8266 LittleFS.begin() takes no args). |FS_BEGIN()macro; resolves to 0-arg for ESP8266 / 1-arg for ESP32. | |esp_randomnot declared in ESP8266 scope. | Call site replaced bySNMP_RAND()macro → ESP8266:(uint32_t)os_random(); ESP32:esp_random(). | |int*→uint32_t*conversion ataddTimestampHandler(oid, &variable)— API expectsuint32_t*. | Two globalssysUptime/entPhySensorValueTimeStamp_1changedint→uint32_t. | | Sketch had its own manual ESP8266 shrink block, duplicating and conflicting with the now-central_SNMP_ESP8266_TINYauto-profile. | Removed; banner now teaches opt-out and per-constant tuning. |3. Verification matrix (all green — 4 DoD cross builds + host)
Strict build flags enforced on all Arduino-CLI targets via
build.all.warn_level=all:No warnings, no errors on any target in the release train. Catch2 host suite compiled under
-Wall -Wextra -Werror(101/101 green).4. Maintainer decision guide — should this be merged?
If you want…
#ifndef-guarded constants plus two opt-outs (SNMP_SKIP_ESP8266_AUTOTUNE,SNMP_POOLS_IN_BSS);Merge risks / acceptance notes
dynamic_cast/ RTTI / exceptions. Coded with the embedded-fno-exceptions -fno-rttireality in mind.<memory>not fully removed: two public-API shared_ptr ctors are intentionally retained for users porting legacy code that instantiatedshared_ptr<OIDType>callbacks. These are public API surface, not hot-path. If you want a strict<memory>-zero variant we can deprecate then remove these in a follow-up PR.<regex>/<format>/<iostream>— 0 uses anywhere. Confirms footprint rules.src/include/defs.h,library.properties,README.mdall bumped to3.1.4as a single patch-step over v3.1.3.5. Tag / release assets (v3.1.4)
Files changed by this release (9-tracked + 2 doc-new = 11 total)
Release artefacts / measurements (for GitHub Releases page)
Footprint matrix (from
arduino-cli compile.elfsize reports; globals =.data+.bss):