Skip to content

v3.1.4 cumulative: C-string, user tuning, BER #60 hardening, #64 RFC3416 fix - #1

Closed
syntax1269 wants to merge 1 commit into
masterfrom
v3.1.4
Closed

v3.1.4 cumulative: C-string, user tuning, BER #60 hardening, #64 RFC3416 fix#1
syntax1269 wants to merge 1 commit into
masterfrom
v3.1.4

Conversation

@syntax1269

Copy link
Copy Markdown
Owner

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.

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).
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:


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:

// ── 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)

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::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.
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)

# 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.

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.
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 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

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):

#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) | |

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 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 LITTLEFSLittleFS + 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.

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:

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.

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

  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.

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 .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 and BERDecode.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: uint32_t sortingMap[SNMP_MAX_OID_SUBIDENTIFIERS].
  • BERDecode.cpp: generateSortingMap(uint32_t outMap[…], int* outLen) with cast (uint32_t)item on the decoded BER long.
  • 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 intuint32_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. |


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

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 SNMPv2c Trap/Inform uses sysObjectID.0 instead of snmpTrapOID.0 0neblock/Arduino_SNMP#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

…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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

SNMPv2c Trap/Inform uses sysObjectID.0 instead of snmpTrapOID.0

1 participant