Skip to content

v3.1.5 cumulative: C-string, user tuning, BER #60 hardening, #64 snmpTrapOID.0 RFC3416 fix - #3

Merged
syntax1269 merged 3 commits into
masterfrom
v3.1.5
Aug 19, 2026
Merged

v3.1.5 cumulative: C-string, user tuning, BER #60 hardening, #64 snmpTrapOID.0 RFC3416 fix#3
syntax1269 merged 3 commits into
masterfrom
v3.1.5

Conversation

@syntax1269

Copy link
Copy Markdown
Owner

Single cumulative PR rolling up 8 stable releases (v2.2.0 → v3.0.0 → v3.1.0 → v3.1.1 → v3.1.2 → v3.1.3 → v3.1.4 → v3.1.5) 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.
• v3.1.5 patch addendum (CI hygiene + docs sanitization, no code footprint delta):
- Rule LD003: demos/ tree moved to extras/demos/ to satisfy arduino-lint (sketches may only exist under examples/ or extras/).
- README absolute-path sanitization sweep: every maintainer-local deep-home-folder file:///... reference in tracked README files replaced with a bare repo-root-relative link. Post-sweep audit returns zero matches.

ESP-01 (1 MB / 80 KB RAM) ship state after v3.1.5: no tuning needed out-of-the-box; same 38–41% globals/47–49 KB free DRAM as v3.1.4. 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.5)

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.5 as a single patch-step over v3.1.4.

5. Tag / release assets (v3.1.5)

Files changed by this release (12-tracked = 9 from v3.1.4 + 3 README docs updated for v3.1.5)

 M README.md                        Current Version 3.1.5 + new v3.1.5 VH row + sanitized absolute links
 M library.properties               version=3.1.5
 M src/include/defs.h               FIRMWARE_VERSION=3.1.5
 R demos/arduino_cli_esp32/arduino_cli_esp32.ino     -> extras/demos/arduino_cli_esp32/arduino_cli_esp32.ino (100%)
 R demos/arduino_cli_esp8266/arduino_cli_esp8266.ino -> extras/demos/arduino_cli_esp8266/arduino_cli_esp8266.ino (100%)
 R demos/platformio_minimal/platformio.ini           -> extras/demos/platformio_minimal/platformio.ini (83%, +1-level lib_extra_dirs)
 R demos/platformio_minimal/src/main.ino              -> extras/demos/platformio_minimal/src/main.ino (100%)

Release artefacts / measurements (for GitHub Releases page)

Tag     : v3.1.5
Title   : v3.1.5 — Patch: arduino-lint LD003 (demos → extras/demos) + README absolute-path sanitization

Footprint matrix unchanged from v3.1.4 (v3.1.5 has zero code-path modifications):

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 tag body (copy-paste block for annotated tag v3.1.5)

v3.1.5: Patch — arduino-lint LD003 Rule compliance + full README absolute-path sanitization.

Patch-level housekeeping on top of v3.1.4. Zero code/API/wire changes.

  * Rule LD003 (Arduino library spec): 3 .ino sketches previously in demos/
    folder moved to extras/demos/. arduino-lint allows .ino files ONLY under
    examples/ or extras/ folders. Demos dirs were triggering ERROR:
    "Sketch(es) found outside examples and extras folders" in the GitHub
    Actions arduino/arduino-lint-action@v1.0.0 run.
      - demos/arduino_cli_esp32/          -> extras/demos/arduino_cli_esp32/
      - demos/arduino_cli_esp8266/        -> extras/demos/arduino_cli_esp8266/
      - demos/platformio_minimal/         -> extras/demos/platformio_minimal/
      - platformio.ini: cd path banner and lib_extra_dirs bumped +1 level
        (../../.. -> ../../../..) for the new extras/ nesting.
  * README sanitization sweep. Maintainer-local deep-home-folder
    `file:///...` absolute paths across README.md, this PR
    post stripped to bare
    repo-root-relative links. GitHub Markdown renders these natively
    as correct jump-to-line anchors. Post-sweep audit returns zero
    local-home absolute paths anywhere in tracked files and release
    artifacts.

Version bump 3.1.4 -> 3.1.5 in library.properties and src/include/defs.h
(LIBRARY_VERSION_MAJOR=3, MINOR=1, PATCH=5, string "3.1.5").

Verification (unchanged from v3.1.4 — no code changes, so no deltas):
  * Host catch2 101/101 assertions, 10 test cases — all green.
  * 4x Arduino-CLI cross builds (2 sketches × esp8266:d1_mini + esp32:esp32)
    still link clean, same globals % headroom as v3.1.4 baseline matrix.

100% wire and API compatible with v3.1.4. No functional change.

(Historical) Tag / release assets (v3.1.4) — preserved for audit trail

(Historical v3.1.4) Files changed by previous release

 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

(Historical v3.1.4) Release artefacts / measurements

Tag     : v3.1.4
Title   : v3.1.4 — ESP8266 DRAM headroom win + startup-heap ASNPool + narrower sort maps

(Historical v3.1.4) Footprint matrix:

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

…neblock#64 snmpTrapOID.0 RFC3416 fix

Single cumulative PR rolling up 8 stable releases (v2.2.0 → v3.0.0 → v3.1.0 → v3.1.1 → v3.1.2 → v3.1.3 → v3.1.4 → v3.1.5) 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.
  • v3.1.5 patch addendum (CI hygiene + docs sanitization, no code footprint delta):
      - Rule LD003: demos/ tree moved to extras/demos/ to satisfy arduino-lint (sketches may only exist under examples/ or extras/).
      - README absolute-path sanitization sweep: every maintainer-local deep-home-folder `file:///...` reference in tracked README files replaced with a bare repo-root-relative link. Post-sweep audit returns zero matches.

ESP-01 (1 MB / 80 KB RAM) ship state after v3.1.5: no tuning needed out-of-the-box; same 38–41% globals/47–49 KB free DRAM as v3.1.4. 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.5)

<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](src/include/BER.h) and [BERDecode.cpp](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](src/include/BER.h): `uint32_t sortingMap[SNMP_MAX_OID_SUBIDENTIFIERS]`.
* [BERDecode.cpp](src/BERDecode.cpp): `generateSortingMap(uint32_t outMap[…], int* outLen)` with cast `(uint32_t)item` on the decoded BER long.
* [ValueCallbacks.cpp](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.5` as a single patch-step over v3.1.4.

---

## 5. Tag / release assets (v3.1.5)

### Files changed by this release (12-tracked = 9 from v3.1.4 + 3 README docs updated for v3.1.5)
```
 M README.md                        Current Version 3.1.5 + new v3.1.5 VH row + sanitized absolute links
 M library.properties               version=3.1.5
 M src/include/defs.h               FIRMWARE_VERSION=3.1.5
 R demos/arduino_cli_esp32/arduino_cli_esp32.ino     -> extras/demos/arduino_cli_esp32/arduino_cli_esp32.ino (100%)
 R demos/arduino_cli_esp8266/arduino_cli_esp8266.ino -> extras/demos/arduino_cli_esp8266/arduino_cli_esp8266.ino (100%)
 R demos/platformio_minimal/platformio.ini           -> extras/demos/platformio_minimal/platformio.ini (83%, +1-level lib_extra_dirs)
 R demos/platformio_minimal/src/main.ino              -> extras/demos/platformio_minimal/src/main.ino (100%)
```

### Release artefacts / measurements (for GitHub Releases page)
```
Tag     : v3.1.5
Title   : v3.1.5 — Patch: arduino-lint LD003 (demos → extras/demos) + README absolute-path sanitization
```

Footprint matrix unchanged from v3.1.4 (v3.1.5 has zero code-path modifications):

| 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 tag body (copy-paste block for annotated tag v3.1.5)
```
v3.1.5: Patch — arduino-lint LD003 Rule compliance + full README absolute-path sanitization.

Patch-level housekeeping on top of v3.1.4. Zero code/API/wire changes.

  * Rule LD003 (Arduino library spec): 3 .ino sketches previously in demos/
    folder moved to extras/demos/. arduino-lint allows .ino files ONLY under
    examples/ or extras/ folders. Demos dirs were triggering ERROR:
    "Sketch(es) found outside examples and extras folders" in the GitHub
    Actions arduino/arduino-lint-action@v1.0.0 run.
      - demos/arduino_cli_esp32/          -> extras/demos/arduino_cli_esp32/
      - demos/arduino_cli_esp8266/        -> extras/demos/arduino_cli_esp8266/
      - demos/platformio_minimal/         -> extras/demos/platformio_minimal/
      - platformio.ini: cd path banner and lib_extra_dirs bumped +1 level
        (../../.. -> ../../../..) for the new extras/ nesting.
  * README sanitization sweep. Maintainer-local deep-home-folder
    `file:///...` absolute paths across README.md, this PR
    post stripped to bare
    repo-root-relative links. GitHub Markdown renders these natively
    as correct jump-to-line anchors. Post-sweep audit returns zero
    local-home absolute paths anywhere in tracked files and release
    artifacts.

Version bump 3.1.4 -> 3.1.5 in library.properties and src/include/defs.h
(LIBRARY_VERSION_MAJOR=3, MINOR=1, PATCH=5, string "3.1.5").

Verification (unchanged from v3.1.4 — no code changes, so no deltas):
  * Host catch2 101/101 assertions, 10 test cases — all green.
  * 4x Arduino-CLI cross builds (2 sketches × esp8266:d1_mini + esp32:esp32)
    still link clean, same globals % headroom as v3.1.4 baseline matrix.

100% wire and API compatible with v3.1.4. No functional change.
```

---

## (Historical) Tag / release assets (v3.1.4) — preserved for audit trail

### (Historical v3.1.4) Files changed by previous release
```
 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
```

### (Historical v3.1.4) Release artefacts / measurements
```
Tag     : v3.1.4
Title   : v3.1.4 — ESP8266 DRAM headroom win + startup-heap ASNPool + narrower sort maps
```

(Historical v3.1.4) Footprint matrix:

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

</details>
Added installation step for ArduinoJson library dependencies in the workflow.
@syntax1269
syntax1269 merged commit 3366008 into master Aug 19, 2026
6 checks passed
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