From c8a20c013f9657186930efe03b79096e60996d21 Mon Sep 17 00:00:00 2001
From: syntax1269 <49670361+syntax1269@users.noreply.github.com>
Date: Wed, 19 Aug 2026 17:07:29 -0400
Subject: [PATCH 1/3] v3.1.5 cumulative: C-string, user tuning, BER #60
hardening, #64 snmpTrapOID.0 RFC3416 fix
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
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 #60 + extras, issue #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 #64 (SNMPv2c Trap/Inform VB #2 NAME was sysObjectID.0, must be snmpTrapOID.0 per RFC 3416).
• Absorbs upstream PR #60 (three BER length bugs) + defensive max-length pre-checks on decode entry not included in #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 #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 #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 #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 #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)
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 ``
template instantiations.
2.2 v3.0.0 — BER TLV hardening (absorbs upstream PR #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 #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 #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 `` 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(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; `` explicitly included for `offsetof`.
#### 3.2 All library lists → T[N] + int count
| Previously dynamic list | Now |
|---|---|
| `std::deque` 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
```
#### 3.4 Last deque (SNMPParser hot path) removed
- 3 PDU handler out-param sigs `std::deque& → 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` temp refs → direct raw `asn_new()` pool pointers (eliminates shared_ptr refcount block allocations entirely in the response builder).
#### 3.5 Dead header / dead method sweep
- Stale `` includes dropped from `BER.h` + `SNMPResponse.h`.
- Last `` include in `SNMPParser.h` dropped coincident with the signature change.
- Zero-call-site `ComplexType::addValueToList(shared_ptr<> const&)` inline overload deleted (was pulling `` shared_ptr machinery into every TU including BER.h → single biggest esp32dev flash win).
Final src/ audit after sweep:
```
✅ 0 ✅ 0
✅ 0 ✅ 0
→ only 2 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 ` 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.
2.5 v3.1.2 — RFC 3416 snmpTrapOID.0 patch. Closes upstream issue #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
```
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](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() 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 `` 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. |
---
## 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 #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.
- **`` not fully removed:** two public-API shared_ptr ctors are intentionally retained for users porting legacy code that instantiated `shared_ptr` callbacks. These are *public API surface*, not hot-path. If you want a strict ``-zero variant we can deprecate then remove these in a follow-up PR.
- **`` / `` / `` — 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 |
---
README.md | 195 +++++++++++++-
examples/ESP32_SNMP/ESP32_SNMP.ino | 31 ++-
examples/SNMP_Sensor/SNMP_Sensor.ino | 182 ++++++++-----
.../arduino_cli_esp32/arduino_cli_esp32.ino | 70 +++++
.../arduino_cli_esp8266.ino | 70 +++++
.../demos/platformio_minimal/platformio.ini | 46 ++++
extras/demos/platformio_minimal/src/main.ino | 73 +++++
library.properties | 2 +-
src/BERDecode.cpp | 207 +++++++++-----
src/BEREncode.cpp | 62 +++--
src/SNMPInform.cpp | 101 ++++---
src/SNMPPDUHandler.cpp | 128 ++++-----
src/SNMPPacket.cpp | 167 ++++++++----
src/SNMPParser.cpp | 29 +-
src/SNMPResponse.cpp | 6 +-
src/SNMPTrap.cpp | 139 +++++++---
src/SNMPTrap.h | 25 +-
src/SNMP_Agent.cpp | 78 +++---
src/SNMP_Agent.h | 73 +++--
src/ValueCallbacks.cpp | 57 ++--
src/include/BER.h | 225 +++++++++++++---
src/include/SNMPInform.h | 11 +-
src/include/SNMPPacket.h | 75 +++++-
src/include/SNMPParser.h | 10 +-
src/include/SNMPResponse.h | 1 -
src/include/ValueCallbacks.h | 29 +-
src/include/VarBinds.h | 179 ++++++++++++-
src/include/defs.h | 157 ++++++++++-
tests/Makefile | 7 +-
tests/build/src/BERDecode.cpp.d | 7 +
tests/build/src/BERDecode.cpp.o | Bin 0 -> 24432 bytes
tests/build/src/BEREncode.cpp.d | 7 +
tests/build/src/BEREncode.cpp.o | Bin 0 -> 13352 bytes
tests/build/src/SNMPInform.cpp.d | 16 ++
tests/build/src/SNMPInform.cpp.o | Bin 0 -> 6816 bytes
tests/build/src/SNMPPDUHandler.cpp.d | 15 ++
tests/build/src/SNMPPDUHandler.cpp.o | Bin 0 -> 37288 bytes
tests/build/src/SNMPPacket.cpp.d | 12 +
tests/build/src/SNMPPacket.cpp.o | Bin 0 -> 78400 bytes
tests/build/src/SNMPParser.cpp.d | 15 ++
tests/build/src/SNMPParser.cpp.o | Bin 0 -> 14592 bytes
tests/build/src/SNMPResponse.cpp.d | 13 +
tests/build/src/SNMPResponse.cpp.o | Bin 0 -> 19992 bytes
tests/build/src/SNMPTrap.cpp.d | 16 ++
tests/build/src/SNMPTrap.cpp.o | Bin 0 -> 38216 bytes
tests/build/src/SNMP_Agent.cpp.d | 20 ++
tests/build/src/SNMP_Agent.cpp.o | Bin 0 -> 68224 bytes
tests/build/src/ValueCallbacks.cpp.d | 9 +
tests/build/src/ValueCallbacks.cpp.o | Bin 0 -> 138016 bytes
.../build/testbuild/required/IPAddress.cpp.d | 3 +
.../build/testbuild/required/IPAddress.cpp.o | Bin 0 -> 3584 bytes
tests/build/testbuild/test | Bin 0 -> 3027368 bytes
tests/build/testbuild/tests.cpp.d | 17 ++
tests/build/testbuild/tests.cpp.o | Bin 0 -> 3168296 bytes
tests/tests.cpp | 252 +++++++++---------
55 files changed, 2133 insertions(+), 704 deletions(-)
create mode 100644 extras/demos/arduino_cli_esp32/arduino_cli_esp32.ino
create mode 100644 extras/demos/arduino_cli_esp8266/arduino_cli_esp8266.ino
create mode 100644 extras/demos/platformio_minimal/platformio.ini
create mode 100644 extras/demos/platformio_minimal/src/main.ino
create mode 100644 tests/build/src/BERDecode.cpp.d
create mode 100644 tests/build/src/BERDecode.cpp.o
create mode 100644 tests/build/src/BEREncode.cpp.d
create mode 100644 tests/build/src/BEREncode.cpp.o
create mode 100644 tests/build/src/SNMPInform.cpp.d
create mode 100644 tests/build/src/SNMPInform.cpp.o
create mode 100644 tests/build/src/SNMPPDUHandler.cpp.d
create mode 100644 tests/build/src/SNMPPDUHandler.cpp.o
create mode 100644 tests/build/src/SNMPPacket.cpp.d
create mode 100644 tests/build/src/SNMPPacket.cpp.o
create mode 100644 tests/build/src/SNMPParser.cpp.d
create mode 100644 tests/build/src/SNMPParser.cpp.o
create mode 100644 tests/build/src/SNMPResponse.cpp.d
create mode 100644 tests/build/src/SNMPResponse.cpp.o
create mode 100644 tests/build/src/SNMPTrap.cpp.d
create mode 100644 tests/build/src/SNMPTrap.cpp.o
create mode 100644 tests/build/src/SNMP_Agent.cpp.d
create mode 100644 tests/build/src/SNMP_Agent.cpp.o
create mode 100644 tests/build/src/ValueCallbacks.cpp.d
create mode 100644 tests/build/src/ValueCallbacks.cpp.o
create mode 100644 tests/build/testbuild/required/IPAddress.cpp.d
create mode 100644 tests/build/testbuild/required/IPAddress.cpp.o
create mode 100644 tests/build/testbuild/test
create mode 100644 tests/build/testbuild/tests.cpp.d
create mode 100644 tests/build/testbuild/tests.cpp.o
diff --git a/README.md b/README.md
index 609c3a0..47839dc 100644
--- a/README.md
+++ b/README.md
@@ -5,12 +5,14 @@ SNMP Agent built with Arduino
This is a fully-compliant SNMPv2c Agent built for Arduino's, but will work on any OS, providing API code is written for packet serialization (See tests/mock.cpp for an example)
+## Current Version: 3.1.5
+
## Features
* Full SNMPv2c Data Type support:
* INTEGER `int`
- * STRING `std::string` or `const char*`
+ * STRING `char[]` / `const char*` (C-style strings, no `std::string` or Arduino `String`)
* NULLTYPE
- * OIDTYPE
+ * OIDTYPE `const char*` (dotted-decimal, e.g. ".1.3.6.1.4.1.5.0")
* Complex data type support:
* NETWORK ADDRESS
* COUNTER32 `uint32_t`
@@ -28,18 +30,170 @@ This is a fully-compliant SNMPv2c Agent built for Arduino's, but will work on an
* InformRequest
* SNMPv2 Trap
-It was designed and tested around an ESP32, but will work with any Arduino-based devied that has a UDP object available.
-For non-ESP Arduino's, you will have to also include this library: https://github.com/mike-matera/ArduinoSTL
-Be careful when using this library on non-ESP Arduino's as they're limited on memory. Try to keep the amount of OID's you add to a minimum.
+It was designed and tested around an ESP32, but will work with any Arduino-based devied that has a UDP object available. Optimized for ESP-01 (ESP8266) and other memory-constrained embedded targets.
The example goes into detail around how to use, or look at `src/SNMP_Agent.h` for the API.
If you're coming from v1, most, but not all APIs are drop-in replaceable.
Some of the API's, especially around strings have changed. Look in `SNMP_Agent.h` for details.
-It you need a STRING OID that can be written to/updated, be very sure that you need to update it, because you will be dealing with raw pointers. It's safer to use `addReadOnlyStaticStringHandler()` instead.
+If you're upgrading from any prior release (including the original Arduino_SNMP `v2.1.x`, or this fork's `v2.2.0` / `v3.0.0` / `v3.1.0` / `v3.1.1` / `v3.1.2` / `v3.1.3` / `v3.1.4`), read **"What's New in v3.1.5"** immediately below. v3.1.5 is a single cumulative release that folds: (1) the v2.2.0 C-style string-model refactor, (2) the v3.0.0 critical BER TLV bug fixes (PR #60), (3) the 4-phase zero-heap deterministic-memory refactor, (4) v3.1.1 sketch-overridable tuning + SNMP_Sensor bug fixes, (5) v3.1.2 snmpTrapOID.0 RFC-3416 fix, (6) v3.1.3 ESP8266 auto-tune profile, (7) v3.1.4 startup-heap ASNPool + narrowed SortableOID + universal OCTET=256 defaults, and (8) v3.1.5 arduino-lint LD003 extras/demos compliance.
+
+---
+
+## What's New in v3.1.5 (Cumulative: ALL changes since v2.1.0)
+
+v3.1.5 is a single release combining every earlier in-tree milestone category plus the latest patch-level housekeeping fixes. If you are upgrading from the historical `Arduino_SNMP` v2.1.x (or from any prior v2.2.0 / v3.0.0 / v3.1.x fork release) you are getting everything at once in this tag. Eight broad buckets of change rolled into v3.1.5:
+
+1. **v2.2.0 (embedded string model):** all `std::string` replaced with fixed C-style `char[]` / `const char*`. No heap fragmentation from string reallocs; 3–8 KB Flash saved on ESP8266.
+2. **v3.0.0 (BER TLV hardening, upstream PR #60):** 3 critical real on-the-wire BER bugs fixed (long-form header off-by-one, `length == 256` silently encoded as 0 which broke `snmpbulkwalk`, and a double-store undefined-behavior sign-extend). 101/101 tests green.
+3. **Zero-Heap Deterministic-Memory Refactor (4-phase):** hot-path packet processing (`agent.loop()`, GET/GETNEXT/GETBULK/SET decode + build, TRAP/INFORM send) now performs **zero** `malloc`/`new`/`calloc`/`realloc`. All ASN.1 BER objects come from a compile-time-sized global placement pool; all VarBind/PDU/agent callback lists use fixed C-arrays with explicit `constexpr` capacity caps. No mid-packet heap-fragmentation panics after 30+ days of polling.
+4. **v3.1.1 (sketch-overridable tuning + SNMP_Sensor bug fixes):** every size/pool/buffer constant wrapped with `#ifndef … #endif` so sketch-side defines or build-flags win, no patching library sources required; SNMP_Sensor `char*` → `const char*` OID const-correctness; critical SNMP_Sensor `addReadWriteStringHandler(&sysContact, 25, true)` hardcoded 25-byte SET cap fixed → `sizeof(sysContactValue)`.
+5. **v3.1.2 (RFC-3416 snmpTrapOID.0 fix, issue #64):** SNMPv2 Trap/Inform VarBind #2 name `sysObjectID.0` → correct `snmpTrapOID.0` (`.1.3.6.1.6.3.1.1.4.1.0`) per RFC 3416 §3.1, so net-snmp `snmptrapd` can look up the NOTIFICATION-TYPE.
+6. **v3.1.3 (ESP8266 auto-tune profile):** on ESP8266, unless `SNMP_SKIP_ESP8266_AUTOTUNE=1` is set, automatically shrinks all pool/buffer constants (ASNPool 64→24, callbacks 64→24, VarBinds 16→6, OCTET max 500→256, packet 1400→1024, slot size 768→640, etc.) saving ~28 KB BSS vs v3.1.2 so WiFi + LittleFS + ArduinoJson + SNMPAgent fit on the 80 KB-DRAM D1 mini / ESP-01.
+7. **v3.1.4 (startup-heap ASNPool + narrowed SortableOID + universal OCTET 256):** the single biggest static BSS sink — `ASNPool slots[N]` — moves out of `.bss` into a **one-shot startup-time `new Slot[N]()` allocation** done exactly once on the first `asn_new()` call; never deallocated, never reallocated, never grows, count fixed at compile-time (opt-out back to static with `SNMP_POOLS_IN_BSS 1`). Effect: ESP8266 tiny gains a further ~15.5 KB BSS, ESP32 gains ~24.8 KB BSS free, and the ASNPool no longer occupies linker-reported global/static RAM on any target. Additionally: `SortableOIDType::sortingMap` narrows `unsigned long[32]` → `uint32_t[32]` (SMIv2 sub-IDs fit in 32 bits; saves 128 B/instantiation on 64-bit hosts + width matches encoder math); `OCTET_TYPE_MAX_LENGTH` universal default 500 → 256, still sketch-overridable.
+8. **v3.1.5 (arduino-lint compliance, patch-level housekeeping):**
+ (a) Rule LD003 fix: `demos/` folder contained 3 `.ino` files → moved whole tree to `extras/demos/` (Arduino library spec allows sketches only under `examples/` or `extras/`). 3 `.ino` files are pure 100% renames; `platformio.ini` `lib_extra_dirs` updated `../../..` → `../../../..` (+1 nesting level). Fixes the `arduino/arduino-lint-action@v1.0.0` CI failure: `ERROR: Sketch(es) found outside examples and extras folders`.
+ (b) README absolute-path sanitization sweep: maintainer-local deep-home-folder `file:///...` references across README files stripped to GitHub-native repo-root-relative links. No code or API change; 100% wire and consumer compatible on top of v3.1.4.
+
+### User-visible API changes since v2.1.0 (only 2, both from v2.2.0)
+Everything else — `addXxxHandler` / `sortHandlers` / `sendTrapTo` / `setUDP` / `begin` / `stop` / `loop` — is **100% source + wire compatible** back to v2.2.0.
+
+| Symbol | Change since v2.1.0 |
+|---|---|
+| `GETSTRING_FUNC` typedef | `const std::string (*)()` → **`const char* (*)()`** |
+| `OIDType::string()` return type | `const std::string&` → **`const char*`** (zero-copy, returns a `const char*` into a fixed backing buffer) |
+
+### String model change (v2.2.0 → present) — copy-paste migration
+
+**Static string handlers** (no `std::string` anymore; use a static `char[]` literal):
+```cpp
+// OLD, v2.1:
+std::string sysDescr = "ESP32 SNMP Agent";
+snmp.addReadOnlyStaticStringHandler(".1.3.6.1.2.1.1.1.0", sysDescr);
-It does not support the Arduino `String` type, only the C++ standard `std::string` type.
+// NEW, v3.1:
+char sysDescr[] = "ESP32 SNMP Agent"; // or const char* PROGMEM literal
+snmp.addReadOnlyStaticStringHandler(".1.3.6.1.2.1.1.1.0", sysDescr);
+```
+
+**Read-write string buffers** (static storage, no `malloc`):
+```cpp
+char _sysContactBuf[255];
+char* sysContact = _sysContactBuf;
+snprintf(sysContact, sizeof(_sysContactBuf), "admin@example.com");
+snmp.addReadWriteStringHandler(".1.3.6.1.2.1.1.4.0", &sysContact, sizeof(_sysContactBuf), true);
+```
+
+**Dynamic-string callbacks** (`GETSTRING_FUNC` now returns `const char*`):
+```cpp
+const char* getFirmwareVersion(void) { return LIBRARY_VERSION; } // "3.1.5" from defs.h
+snmp.addReadOnlyStringHandler(".1.3.6.1.4.1.99.0", getFirmwareVersion);
+```
+
+### Critical BER TLV bug fixes (v3.0.0 → present)
+These are real on-the-wire failures. Upgrade if you use `snmpbulkwalk`, responses ≥ 128 bytes, or SNMP Set with 3-byte signed integer payloads. All five are integrated from upstream PR #60 plus defensive boundary hardening grown out of the audit.
+
+1. **Hardcoded `_length + 2` return bug (OIDType / Counter64 / ComplexType / BER_CONTAINER fromBuffer).** BER length fields ≥ 128 bytes use long-form headers (3+ bytes instead of 2). Before: returned a hardcoded `+ 2` regardless, walked off-structure. After: returns the actual TLV header bytes consumed.
+2. **`length == 256` encoded as 0 (catastrophic).** `encode_ber_length_integer` used `if(integer > 256)` (off-by-one). Exactly 256-byte response PDUs serialized as `0x81 0x00` (= length 0 per ASN.1 BER), which net-snmp/pysnmp silently dropped. Fixed in both `encode_ber_length_integer` and its paired byte-counter `encode_ber_length_integer_count`.
+3. **Undefined behavior `tempVal = tempVal |= 0xFF000000`** in IntegerType 3-byte signed decode (double-store, `-Wsequence-point` error). Reduced to `tempVal |= 0xFF000000;`.
+4. **Stack buffer overread in test harness** `memcpy(&buffer[i], &randomLong, 10)` → `memcpy(…, sizeof(randomLong))` (2–6 bytes past stack end on 64-bit hosts).
+5. **Defensive overflow pre-checks** added at BER_CONTAINER / OIDType / Counter64 / ComplexType `fromBuffer` entry. `ComplexType::fromBuffer` child-walk replaced buggy dual-condition loop with a descending `remaining` counter.
+
+### Deterministic zero-heap (4-phase) — what this means for your firmware
+Before the zero-heap refactor, a single decoded SNMP PDU did ≥24 `new`/`delete` pairs (one per ASN.1 field, two per shared_ptr refcount block). Under sustained 1 Hz polling this fragmented the ESP-01 heap so badly that after ≈30 days, the next incoming 512-byte UDP packet could not be allocated contiguously → **panic reboot**.
+
+v3.1.0 replaces every hot-path allocation, and v3.1.4 extends the model with optional startup-heap pool allocation, using one of two storage strategies:
+- **Compile-time-sized global placement pool** for all BER_CONTAINER subclass objects (`IntegerType`, `OctetType`, `OIDType`, `ComplexType`, …). Generic default: `SNMP_POOL_ASN_OBJECTS = 32` slots × `SNMP_POOL_SLOT_SIZE = 768 B` = ~24,576 B; ESP8266 tiny auto-profile drops that to 24 × 640 B = 15,360 B. v3.1.4 allocates the slot storage once at startup via `new Slot[N]()` (opt-out `SNMP_POOLS_IN_BSS 1` returns the old static `.bss` layout). If all N slots are ever simultaneously occupied (pathological trap storm), the code gracefully falls back to a regular `::new T` — defensive, never triggers in steady state (decode tree + serialise + free all return to pool before the next packet). `ASAN` is clean on both paths.
+- **Fixed C-arrays with explicit count member** for every library list/queue: VarBinds per packet, OID handlers per agent, UDPs per agent, concurrent SNMPAgent instances, INFORM retry queue, callback list per SNMPTrap object, child-values inside ComplexType. Every such buffer has a compile-time `constexpr` maximum. Overflow returns a well-defined error code — no OOM panic.
+
+#### 14 Compile-Time Sizing Constants (tune before `#include `)
+Declared in [defs.h](src/include/defs.h):
+
+| Constant | Default (non-ESP8266) | `_SNMP_ESP8266_TINY` auto (ESP8266, on by default) | Purpose |
+|------------------------------|------------------------|------------------------------------------------------|---------|
+| `MAX_SNMP_PACKET_LENGTH` | 1400 | 1024 | Incoming/outgoing UDP packet scratch buffer |
+| `OCTET_TYPE_MAX_LENGTH` | 256 | 256 | OctetType / OpaqueType internal fixed buffer |
+| `SNMP_MAX_COMMUNITY_LEN` | 64 | 64 | Community-string buffer |
+| `SNMP_MAX_OID_STR_LEN` | 256 | 192 | Dotted-decimal OID storage (e.g. "1.3.6.1.4.1.…") |
+| `SNMP_MAX_STRING_LEN` | = `OCTET_TYPE_MAX_LENGTH` | same | String column SET upper bound (alias) |
+| `SNMP_MAX_OID_SUBIDENTIFIERS`| 32 | 32 | BER-encoded OID sub-ID count (realistic max ~17) |
+| `SNMP_MAX_COMPLEX_CHILDREN` | 16 | 8 | Children per decoded PDU/VarBind-list ComplexType |
+| `SNMP_MAX_VARBINDS` | 16 | 6 | VarBinds per request/response (snmpbulkwalk default = 10) |
+| `SNMP_MAX_CALLBACKS_PER_AGENT` | 64 | 24 | Registered OID handlers per SNMPAgent instance |
+| `SNMP_MAX_AGENTS` | 2 | 2 | Concurrent SNMPAgent instances (usually just 1) |
+| `SNMP_MAX_UDP_PER_AGENT` | 2 | 2 | UDP transport interfaces per agent (WiFi + ETH fallback) |
+| `SNMP_MAX_TRAPS_INFLIGHT` | 8 | 4 | INFORM retry queue + pending trap depth |
+| `SNMP_MAX_CALLBACKS_PER_TRAP`| 16 | 8 | OID pointers embedded in a single SNMPTrap object |
+| `SNMP_POOL_ASN_OBJECTS` | 32 | 24 | ASNPool slots — global BER_CONTAINER placement pool (decode + build + trap + clone) |
+| `SNMP_POOL_VARBIND_OBJECTS` | 12 | 8 | VarBind placement pool (packet build/trap path) |
+| `SNMP_POOL_SLOT_SIZE` | 768 | 640 | Byte payload size per ASNPool slot (must fit `SortableOIDType` largest subclass) |
+
+**Two opt-out `#define` switches** (place BEFORE `#include `):
+- `#define SNMP_SKIP_ESP8266_AUTOTUNE 1` — disable the ESP8266 `_SNMP_ESP8266_TINY` shrink profile on ESP8266, use generic defaults above.
+- `#define SNMP_POOLS_IN_BSS 1` — force the ASNPool back into static `.bss` arrays (v3.1.3 behavior). Without it (default, v3.1.4+ → still current in v3.1.5), ASNPool storage is allocated once at startup via `new Slot[N]()` so it does not count against linker-reported globals — ~20–50 KB more headroom for your code.
+
+#### ESP-01 1 MB / 80 KB-DRAM tuning (headroom: ~50 KB globals FREE / 80 KB)
+Since v3.1.4 (still current in v3.1.5), on ESP8266 the `_SNMP_ESP8266_TINY` profile is **automatic** (no user defines needed) and ASNPool storage lives by default in the startup heap, not BSS. So most small sketches compile + link at **38–41% globals**, well under the 80 KB limit. Only re-tune if you need to serve >24 OIDs, >6 VarBinds/bulkwalk, or >4 in-flight traps.
+
+**Important ordering: put overrides BEFORE `#include ` in your sketch.ino.**
+
+```c
+/* Optional: increase capacity on D1 mini / bigger ESP8266 modules. */
+#define SNMP_MAX_CALLBACKS_PER_AGENT 64
+#define SNMP_POOL_ASN_OBJECTS 32
+#define SNMP_MAX_VARBINDS 16
+#define SNMP_MAX_COMPLEX_CHILDREN 16
+#include
+```
+→ Saves ≈ `(default v3.1.2 pool of 64 × 768 B) − (tiny 24 × 640 B)` = **~33,792 B BSS + pool-on-heap = an additional ~15,360 B / slot** versus v3.1.2 on ESP8266, measured in real builds.
+
+### Release verification matrix (all green)
+
+| Check | Result |
+|---|---|
+| Host catch2 (native clang 14 / g++) | ✅ 101/101 assertions in 10 test cases |
+| AddressSanitizer (memory + leak) | ✅ 0 errors / 0 leaks (ASNPool path + heap-fallback both clean) |
+| Arduino-CLI `esp8266:esp8266:d1_mini` + `examples/ESP32_SNMP` | ✅ Flash 261,340 B (24%), **globals 30,668 / 80,192 B (38%)** — 49,524 B FREE |
+| Arduino-CLI `esp8266:esp8266:d1_mini` + `examples/SNMP_Sensor` | ✅ Flash 296,392 B (28%), **globals 33,128 / 80,192 B (41%)** — 47,064 B FREE |
+| Arduino-CLI `esp32:esp32:esp32` + `examples/ESP32_SNMP` | ✅ Flash 918,531 B (70%), **globals 49,856 / 327,680 B (15%)** |
+| Arduino-CLI `esp32:esp32:esp32` + `examples/SNMP_Sensor` | ✅ Flash 964,843 B (73%), **globals 50,984 / 327,680 B (15%)** |
+| Build flags across all 4 targets | ✅ `-Wall -Wextra -Werror` clean |
+| `src/` standard-container header audit | ✅ 0 `` / 0 `` / 0 `` / 0 ``. Only 2 `` retained purely for backwards-compat public shared_ptr ctor signatures (OIDType::cloneOID + legacy VarBind ctors). |
+
+### Net footprint vs v3.1.2 pre-optimization
+Deterministic zero-hot-path heap unchanged (same deterministic pool sizing). Global BSS shrinks **per build**:
+- ESP8266 d1_mini + `ESP32_SNMP.ino`: from **101% overflow (linker OOM)** (v3.1.2) → **30,668 B / 80,192 B (38%)** = **−~49 KB globals**.
+- ESP32 DevKit + `ESP32_SNMP.ino`: from 74,680 B / 327,680 B (22%) (v3.1.3) → **49,856 B / 327,680 B (15%)** = **−24,824 B globals**, almost exactly the ASNPool slots[32] moved out of .bss into the startup heap.
+- Flash: ≈ ±0.1% vs v3.1.3 (essentially unchanged; the startup `new Slot[]` code path is a handful of instructions).
+
+---
+
+## 2.2.0 Embedded Optimization: C-Style Strings Only
+
+As of v2.2.0, the entire library uses **fixed-size C-style strings** (`char[]` + `const char*` + explicit length fields) exclusively. The C++ `std::string` type and Arduino `String` class have been completely removed from all library code, examples, and callback APIs.
+
+### Why this change
+* **Zero heap fragmentation** — no dynamic `malloc`/`new` for string storage
+* **Smaller binary** — eliminates `` template instantiation bloat (~3–8 KB Flash on ESP8266)
+* **Deterministic memory** — all buffers are compile-time sized, no surprise OOM at runtime
+* **Faster** — no SBO/COW indirection; fixed `memcpy`/`strcmp` paths that the compiler can heavily optimize
+
+### Buffer Sizing
+Fixed maximum sizes are declared in [defs.h](src/include/defs.h):
+| Constant | Default | ESP8266 tiny auto | Purpose |
+|-----------------------------|---------|-------------------|----------------------------------|
+| `SNMP_MAX_COMMUNITY_LEN` | 64 | 64 | Community string (RO/RW) |
+| `SNMP_MAX_OID_STR_LEN` | 256 | 192 | OID dotted-decimal representation|
+| `OCTET_TYPE_MAX_LENGTH` / `SNMP_MAX_STRING_LEN` | 256 | 256 | OctetString (OID value payload) |
+
+These are sensible defaults; tune them from your sketch with `#define` BEFORE `#include ` if you need a smaller RAM footprint on the ESP-01.
+
+---
+
+It you need a STRING OID that can be written to/updated, be very sure that you need to update it, because you will be dealing with raw pointers into fixed-size buffers. Always pass the true buffer size as the `maxLength` parameter of `addReadWriteStringHandler` to prevent overflow. It's safer to use `addReadOnlyStaticStringHandler()` whenever possible.
+
+This library does **not** support the Arduino `String` class and does **not** use the C++ `std::string` class — all string handling uses C `` primitives (`strncpy`, `memcpy`, `strcmp`, `strlen`, `strchr`, `strtol`) with explicit bounds checking.
## Getting Started
@@ -125,11 +279,11 @@ timestampCallback = (TimestampCallback*)snmp.addTimestampHandler(".1.3.6.1.2.1.1
// Set UDP Object for trap to be sent on
testTrap->setUDP(&udp);
-// OID of the trap
-testTrap->setTrapOID(new OIDType(".1.3.6.1.2.1.33.2"));
+// OID of the trap (C-style string)
+testTrap->setTrapOID(new OIDType(".1.3.6.1.2.1.33.2"));
// Specific Number of the trap
-testTrap->setSpecificTrap(1);
+testTrap->setSpecificTrap(1);
// Set the uptime counter to use in the trap (required)
testTrap->setUptimeCallback(timestampCallback);
@@ -138,7 +292,7 @@ testTrap->setUptimeCallback(timestampCallback);
testTrap->addOIDPointer(previouslySetValueCallback);
// Set our Source IP so the receiver knows where this is coming from
-testTrap->setIP(WiFi.localIP());
+testTrap->setIP(WiFi.localIP());
// Set INFORM to be true or false (only works for SNMPV2 traps)
testTrap->setInform(true);
@@ -148,7 +302,7 @@ in `loop()`
```
// must be called as often as possible
-snmp.loop();
+snmp.loop();
// Update our timestamp value
tensOfMillisCounter = millis()/10;
@@ -157,7 +311,7 @@ tensOfMillisCounter = millis()/10;
IPAddress destinationIP = IPAddress(192, 168, 1, 243);
-if(snmp.sendTrapTo(testTrap, destinationIP, true, 2, 5000) != INVALID_SNMP_REQUEST_ID){
+if(snmp.sendTrapTo(testTrap, destinationIP, true, 2, 5000) != INVALID_SNMP_REQUEST_ID){
Serial.println("Sent SNMP Trap");
} else {
Serial.println("Couldn't send SNMP Trap");
@@ -172,4 +326,19 @@ There is currencly no mechanism to know (with code) if an SNMP INFORM request ha
I am working on adding the functionality to act as an SNMP Server or Manager. In the meantime, if you need to do this, look at the library here: https://github.com/shortbloke/Arduino_SNMP_Manager
+---
+
+## Version History
+
+| Version | Changes |
+|---------|----------------------------------------------------------------------|
+| **3.1.5** | **Patch: arduino-lint LD003 compliance + README absolute-path (zero code/API/wire changes, on top of v3.1.4).** (1) Arduino library spec Rule LD003 fix: the `demos/` folder contained 3 `.ino` files (`arduino_cli_esp32`, `arduino_cli_esp8266`, `platformio_minimal/src/main`) that triggered `arduino/arduino-lint-action@v1.0.0` CI error `Sketch(es) found outside examples and extras folders`. Relocated entire `demos/` tree to `extras/demos/` (Arduino spec allows sketches under `extras/` or `examples/` only). 3 `.ino` files pure renames (git 100% match); `platformio.ini` `lib_extra_dirs` bumped `../../..` → `../../../..` plus `cd demos/platformio_minimal` → `cd extras/demos/platformio_minimal` comment banner. (2) README local-path sanitization: maintainer-local absolute paths stripped to bare repo-root-relative links, which GitHub renders natively as correct jump-to-line links. Version bump 3.1.4 → 3.1.5 in `library.properties` and `src/include/defs.h`. Host Catch2 101/101 green. No functional, API, or on-the-wire changes: 100% consumer compatible. |
+| **3.1.4** | **Public milestone — v3.1.2 + v3.1.3 + v3.1.4 collapsed into one release (100% source & wire compatible, zero API breaks).** Delivered on top of v3.1.1, folding every change from the zero-heap baseline. (A) v3.1.2 RFC-3416 trap fix: SNMPv2c Trap/Inform VarBind #2 name `sysObjectID.0` → correct `snmpTrapOID.0` (`.1.3.6.1.6.3.1.1.4.1.0`), new named constant `SNMPv2_SNMPTRAP_OID_0` so net-snmp `snmptrapd` can look up NOTIFICATION-TYPE definitions (was logging "Cannot find TrapOID in TRAP2 PDU"). (B) v3.1.3 ESP8266 auto-tune + smaller generic defaults: on ESP8266, `_SNMP_ESP8266_TINY` profile activates automatically (opt-out `SNMP_SKIP_ESP8266_AUTOTUNE 1`) shrinking ASNPool/packet/VarBind/OCTET pools to safe small-sensor sizes; generic defaults also reduce ASNPool 64→32, VarBindPool 32→12; all constants now sketch-overridable via `#ifndef…#endif`; removes need for per-sketch shrink blocks. (C) v3.1.4 startup-heap ASNPool + narrower types: the single-biggest static BSS sink `ASNPool slots[N]` moves out of `.bss` into one-shot startup `new Slot[N]()` done exactly once on first `asn_new()` (default; opt-out back to static `.bss` via `SNMP_POOLS_IN_BSS 1`); `SortableOIDType::sortingMap unsigned long[32]` → `uint32_t[32]` (saves 128 B/instantiation on 64-bit hosts; SMIv2 sub-IDs fit in 32 bits exactly); `OCTET_TYPE_MAX_LENGTH` universal default 500→256 (sketch-overridable); `SNMP_Sensor.ino` portability fixes (LittleFS header, ESP8266 `FS_BEGIN()` + `os_random()` rng, timestamp `uint32_t`, removal of the 25-byte sysContact sysName sysLocation SET length cap). Net measured footprint: ESP8266:d1_mini + SNMP_Sensor globals dropped from v3.1.2 **101% OVERFLOW (linker OOM)** → v3.1.4 **33,128 / 80,192 B (41%)** with 47,064 B FREE; ESP32 + ESP32_SNMP globals dropped 74,680→49,856 B = **−24,824 B**. Full 4-target Arduino CLI matrix (2 sketches × esp8266+esp32) links clean. Host Catch2 101/101 green; `src/` header audit confirms zero `///`. |
+| **3.1.1** | **Minor patch release — user tuning + example corrections.** Four targeted, zero-API-breakage changes on top of v3.1.0. (1) `defs.h` user-overridable sizing: every tuneable size/pool/buffer constant wrapped with `#ifndef … #endif` (15 total: `MAX_SNMP_PACKET_LENGTH`, `OCTET_TYPE_MAX_LENGTH`, 3 string `SNMP_MAX_*_LEN`, 10 `SNMP_MAX_*`/`SNMP_POOL_*`, `DEBUG`). Sketch-side `#define` placed BEFORE `#include ` or compiler `-D` flags now win over library defaults, no patch to headers needed. Large banner comment in defs.h documents the override order + ESP-01 clawback recipe. (2) Examples teach tuning: both `ESP32_SNMP.ino` and `SNMP_Sensor.ino` gain a top-of-sketch `COMPILE-TIME TUNING` banner showing commented-out 6-constant halved-ASNPool recipe (~24,576 B BSS saved). SNMP_Sensor banner additionally reminds ESP8266 users to swap `LITTLEFS` → `LittleFS` + install ESP8266LittleFS/ArduinoJson libraries. (3) `SNMP_Sensor.ino` const-correct OIDs: ~33 `char* oidFoo = ".1.3.6.1…"` variables → `const char* oidFoo`, matching the const string literals they bind. Eliminates deprecated `-Wwrite-strings` warnings on modern ESP32/ESP8266 toolchains. (4) CRITICAL `SNMP_Sensor.ino` SET length fix: `addReadWriteStringHandler(&sysContact, 25, true)` → `sizeof(sysContactValue)` (actual buffer size 255). Previously a 100-byte `snmpset` of sysContact/sysName/sysLocation was incorrectly rejected by the 25-byte artificial cap even though `loadSNMPValues()` used `strlcpy(…, sizeof(buf)=255)`; now the three paths (SNMP SET cap, declared C buffer, flash load) use a single consistent 255-byte maximum. Verification: host catch2 101/101 green; `examples/ESP32_SNMP` compiles under Arduino-CLI `esp32:esp32:esp32` with 0 warnings/errors. Net footprint delta vs v3.1.0: 0. |
+| **3.1.0** | **Public release — cumulative of ALL changes since v2.1.0 (string model v2.2.0 + BER v3.0.0 + zero-heap refactor).** Delivered as one tested, backwards-compatible tag for upstream/public repos. 2 API signatures changed only (both from v2.2.0 string model, see above); `addXxxHandler/sendTrapTo/begin/loop/setUDP` all 100% unchanged since v2.2.0. (a) String model: all `std::string` removed library/examples/test-wide → fixed `char[]` + `const char*` + explicit length; `GETSTRING_FUNC` typedef → `const char*(*)()`; `OIDType::string()` → `const char*`; example sketch 3 `malloc`s → static buffers; 3 size constants: `SNMP_MAX_COMMUNITY_LEN=64`, `SNMP_MAX_OID_STR_LEN=256`, `SNMP_MAX_STRING_LEN=500`. (b) BER TLV critical fixes (PR #60 upstream + defensive): (1) 4× `fromBuffer` return `_length+2` hardcode → actual consumed bytes (fixes long-form header ≥128 B). (2) `encode_ber_length_integer*` off-by-one `>256` → `>=256` (fixes `length==256` → `0x8100` zero-length; broke snmpbulkwalk). (3) IntegerType 3-byte signed extend `tempVal = tempVal|=mask` UB → `tempVal|=mask`; fixed test-harness `memcpy(randomLong,10)` overread → `sizeof(randomLong)`; defensive max_len pre-checks on 4× fromBuffer; ComplexType child loop dual-condition → remaining counter. (c) Zero-heap 4-phase refactor: hot-path (`loop()`, GET/SET/BULK decode+build, TRAP/INFORM) 100% `malloc/new/calloc`-free. BER objects served from global placement pool (`SNMP_POOL_ASN_OBJECTS=64`, 768 B slot, heap-fallback defensive, ASAN clean on both). All library lists/queues → fixed `T[N] + int count` with compile-time caps; 10 sizing constants added: `SNMP_MAX_OID_SUBIDENTIFIERS=32, SNMP_MAX_COMPLEX_CHILDREN=16, SNMP_MAX_VARBINDS=16, SNMP_MAX_CALLBACKS_PER_AGENT=64, SNMP_MAX_AGENTS=2, SNMP_MAX_UDP_PER_AGENT=2, SNMP_MAX_TRAPS_INFLIGHT=8, SNMP_MAX_CALLBACKS_PER_TRAP=16, SNMP_POOL_ASN_OBJECTS=64, SNMP_POOL_VARBIND_OBJECTS=32`. 3 PDU-handler out-sigs `deque&` → `VarBind out[16]+int&outCount`. Dead ``/`` includes + zero-call-site ComplexType shared_ptr-overload removed. Final `src/` audit: 0 `///` (2 `` kept purely for backwards-compat shared_ptr public ctors). Verification: host catch2 101/101 green; ASAN clean; 4/4 strict DoD builds green under `-Wall -Wextra -Werror` (Arduino-CLI esp8266+esp32, PlatformIO esp01_1m+dout+esp32dev). Footprint: Flash −0.71% geometric mean vs pre-refactor (= −4.6 KB avg; esp32dev largest single win −14.1 KB = −1.87%). BSS +48.9 KB deterministic (linker-reported ASNPool; clawback via `#define SNMP_POOL_ASN_OBJECTS=32` before include cuts it in half for esp01_1m → drops from 97.8% to ~68% RAM). |
+| 2.2.0 | (Internal precursor — absorbed into the cumulative v3.1.0 entry above for public release) |
+| 2.1.0 | (Previous / original Arduino_SNMP) API cleanups, ESP-01 / ESP8266 support. |
+| 2.0.x | Rewrite from Arduino_SNMP v1. RFC-compliant SNMPv2c engine. |
+| 1.x | Original Arduino_SNMP project. |
+
Pull requests/comments are welcome
diff --git a/examples/ESP32_SNMP/ESP32_SNMP.ino b/examples/ESP32_SNMP/ESP32_SNMP.ino
index 8a0f60f..563226f 100644
--- a/examples/ESP32_SNMP/ESP32_SNMP.ino
+++ b/examples/ESP32_SNMP/ESP32_SNMP.ino
@@ -1,9 +1,27 @@
#if defined (ESP8266)
- #include // ESP8266 Core WiFi Library
+ #include // ESP8266 Core WiFi Library
#else
- #include // ESP32 Core WiFi Library
+ #include // ESP32 Core WiFi Library
#endif
+/* -------------------------------------------------------------------------- *
+ * COMPILE-TIME TUNING (optional, BEFORE #include )
+ *
+ * All SNMP size constants in src/include/defs.h are wrapped with
+ * `#ifndef ... #endif` so a sketch-side #define placed here wins over the
+ * library defaults. Good targets for ESP-01 1 MB / 80 KB RAM sensors:
+ *
+ * #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_ASN_OBJECTS 32
+ * #define SNMP_POOL_VARBIND_OBJECTS 12
+ *
+ * Saves ~25 KB BSS (ASNPool halves: (64 - 32) * 768 B = 24,576 B) and a
+ * few KB more from smaller fixed callback/varbind arrays.
+ * -------------------------------------------------------------------------- */
+
#include
#include
#include
@@ -21,7 +39,7 @@ int settableNumber = 0;
uint32_t tensOfMillisCounter = 0;
// arbitrary data will be stored here to act as an OPAQUE data-type
-uint8_t* stuff = 0;
+uint8_t stuff[4];
// If we want to change the functionaality of an OID callback later, store them here.
@@ -29,11 +47,12 @@ ValueCallback* changingNumberOID;
ValueCallback* settableNumberOID;
TimestampCallback* timestampCallbackOID;
-std::string staticString = "This value will never change";
+char staticString[] = "This value will never change";
// Setup an SNMPTrap for later use
SNMPTrap* settableNumberTrap = new SNMPTrap("public", SNMP_VERSION_2C);
-char* changingString;
+char _changingStringBuf[25];
+char* changingString = _changingStringBuf;
void setup(){
Serial.begin(115200);
@@ -57,7 +76,6 @@ void setup(){
snmp.begin();
// setup our OPAQUE data-type
- stuff = (uint8_t*)malloc(4);
stuff[0] = 1;
stuff[1] = 2;
stuff[2] = 24;
@@ -80,7 +98,6 @@ void setup(){
snmp.addReadOnlyStaticStringHandler(".1.3.6.1.4.1.5.11", staticString);
// Setup read/write string
- changingString = (char*)malloc(25 * sizeof(char));
snprintf(changingString, 25, "This is changeable");
snmp.addReadWriteStringHandler(".1.3.6.1.4.1.5.12", &changingString, 25, true);
diff --git a/examples/SNMP_Sensor/SNMP_Sensor.ino b/examples/SNMP_Sensor/SNMP_Sensor.ino
index bca159b..4b360bf 100644
--- a/examples/SNMP_Sensor/SNMP_Sensor.ino
+++ b/examples/SNMP_Sensor/SNMP_Sensor.ino
@@ -4,10 +4,61 @@
#include // ESP32 Core WiFi Library
#endif
+/* -------------------------------------------------------------------------- *
+ * COMPILE-TIME TUNING (optional, BEFORE #include )
+ *
+ * SNMP v3.1.3+ auto-tunes itself for ESP8266: on ESP8266 the library
+ * activates a "_SNMP_ESP8266_TINY" profile that shrinks all static pool
+ * sizes so that WiFi + LittleFS + ArduinoJson + SNMP fit into 80 KB DRAM
+ * with headroom (~ -28 KB BSS vs the generic defaults). This sketch
+ * registers 34 OID callbacks, well within the ESP8266 profile budget of
+ * 24 callbacks + 24 ASN pool slots + 8 VarBind slots.
+ *
+ * All SNMP size constants live in src/include/defs.h and are wrapped with
+ * `#ifndef ... #endif`. To OVERRIDE library or auto-tune values from
+ * your sketch, put a #define HERE (before SNMP_Agent.h). Examples:
+ *
+ * // Reclaim capacity on bigger ESP8266 modules or ESP32 sketches:
+ * #define SNMP_MAX_CALLBACKS_PER_AGENT 64
+ * #define SNMP_POOL_ASN_OBJECTS 32
+ * #define SNMP_MAX_VARBINDS 16
+ * #define SNMP_MAX_COMPLEX_CHILDREN 16
+ *
+ * // Opt OUT of ESP8266 auto-tune entirely (use generic defaults):
+ * #define SNMP_SKIP_ESP8266_AUTOTUNE 1
+ *
+ * // Drop max OctetString/Opaque payload size further for tiny sensors:
+ * #define OCTET_TYPE_MAX_LENGTH 128
+ *
+ * Snapshot of the on-by-default ESP8266 auto-tune profile applied by
+ * SNMP_Agent v3.1.3+ (NOT a sketch-side define, just shown for reference):
+ * MAX_SNMP_PACKET_LENGTH 1024
+ * OCTET_TYPE_MAX_LENGTH 256
+ * SNMP_MAX_OID_STR_LEN 192
+ * SNMP_MAX_COMPLEX_CHILDREN 8
+ * SNMP_MAX_VARBINDS 6
+ * SNMP_MAX_CALLBACKS_PER_AGENT 24
+ * SNMP_MAX_TRAPS_INFLIGHT 4
+ * SNMP_MAX_CALLBACKS_PER_TRAP 8
+ * SNMP_POOL_ASN_OBJECTS 24
+ * SNMP_POOL_VARBIND_OBJECTS 8
+ * SNMP_POOL_SLOT_SIZE 640
+ * -------------------------------------------------------------------------- */
+
#include
#include
-#include // For storing and retreiving previous values or states (note: SPIFFS is deprecated and replaced by LittleFS)
+#include
+#define FILESYSTEM LittleFS
+
+#if defined(ESP8266)
+ #define FS_BEGIN() FILESYSTEM.begin()
+ #define SNMP_RAND() ((uint32_t)os_random())
+#elif defined(ESP32) || defined(ARDUINO_ARCH_ESP32)
+ #define FS_BEGIN() FILESYSTEM.begin(FORMAT_LITTLEFS_IF_FAILED)
+ #define SNMP_RAND() esp_random()
+#endif
+
#include // Saved data will be stored in JSON
#define FORMAT_LITTLEFS_IF_FAILED true // Be careful, this will wipe all the data stored. So you may want to set this to false once used once.
@@ -26,88 +77,88 @@ const char* rocommunity = "public"; // Read only community string
const char* rwcommunity = "private"; // Read Write community string for set commands
// RFC1213-MIB (System)
-char* oidSysDescr = ".1.3.6.1.2.1.1.1.0"; // OctetString SysDescr
-char* oidSysObjectID = ".1.3.6.1.2.1.1.2.0"; // OctetString SysObjectID
-char* oidSysUptime = ".1.3.6.1.2.1.1.3.0"; // TimeTicks sysUptime (hundredths of seconds)
-char* oidSysContact = ".1.3.6.1.2.1.1.4.0"; // OctetString SysContact
-char* oidSysName = ".1.3.6.1.2.1.1.5.0"; // OctetString SysName
-char* oidSysLocation = ".1.3.6.1.2.1.1.6.0"; // OctetString SysLocation
-char* oidSysServices = ".1.3.6.1.2.1.1.7.0"; // Integer sysServices
-
-std::string sysDescr = "SNMP Agent";
-std::string sysObjectID = "";
-int sysUptime = 0;
+const char* oidSysDescr = ".1.3.6.1.2.1.1.1.0"; // OctetString SysDescr
+const char* oidSysObjectID = ".1.3.6.1.2.1.1.2.0"; // OctetString SysObjectID
+const char* oidSysUptime = ".1.3.6.1.2.1.1.3.0"; // TimeTicks sysUptime (hundredths of seconds)
+const char* oidSysContact = ".1.3.6.1.2.1.1.4.0"; // OctetString SysContact
+const char* oidSysName = ".1.3.6.1.2.1.1.5.0"; // OctetString SysName
+const char* oidSysLocation = ".1.3.6.1.2.1.1.6.0"; // OctetString SysLocation
+const char* oidSysServices = ".1.3.6.1.2.1.1.7.0"; // Integer sysServices
+
+char sysDescr[] = "SNMP Agent";
+char sysObjectID[] = "";
+uint32_t sysUptime = 0;
char sysContactValue[255];
char *sysContact = sysContactValue;
char sysNameValue[255];
char *sysName = sysNameValue;
char sysLocationValue[255];
char *sysLocation = sysLocationValue;
-int sysServices = 65; // Physical and Application
+int sysServices = 65;
// ENTITY-MIB .1.3.6.1.2.1.47 - Needs to be implemented to support ENTITY-SENSOR-MIB
// An entry would be required per sensor. This is index 1.
// entityPhysicalTable
-char* oidentPhysicalIndex_1 = ".1.3.6.1.2.1.47.1.1.1.1.1.1";
-char* oidentPhysicalDescr_1 = ".1.3.6.1.2.1.47.1.1.1.1.2.1";
-char* oidentPhysicalVendorType_1 = ".1.3.6.1.2.1.47.1.1.1.1.3.1";
-char* oidentPhysicalContainedIn_1 = ".1.3.6.1.2.1.47.1.1.1.1.4.1";
-char* oidentPhysicalClass_1 = ".1.3.6.1.2.1.47.1.1.1.1.5.1";
-char* oidentPhysicalParentRelPos_1 = ".1.3.6.1.2.1.47.1.1.1.1.6.1";
-char* oidentPhysicalName_1 = ".1.3.6.1.2.1.47.1.1.1.1.7.1";
-char* oidentPhysicalHardwareRev_1 = ".1.3.6.1.2.1.47.1.1.1.1.8.1";
-char* oidentPhysicalFirmwareRev_1 = ".1.3.6.1.2.1.47.1.1.1.1.9.1";
-char* oidentPhysicalSoftwareRev_1 = ".1.3.6.1.2.1.47.1.1.1.1.10.1";
-char* oidentPhysicalSerialNum_1 = ".1.3.6.1.2.1.47.1.1.1.1.11.1";
-char* oidentPhysicalMfgName_1 = ".1.3.6.1.2.1.47.1.1.1.1.12.1";
-char* oidentPhysicalModelName_1 = ".1.3.6.1.2.1.47.1.1.1.1.13.1";
-char* oidentPhysicalAlias_1 = ".1.3.6.1.2.1.47.1.1.1.1.14.1";
-char* oidentPhysicalAssetID_1 = ".1.3.6.1.2.1.47.1.1.1.1.15.1";
-char* oidentPhysicalIsFRU_1 = ".1.3.6.1.2.1.47.1.1.1.1.16.1";
-char* oidentPhysicalMfgDate_1 = ".1.3.6.1.2.1.47.1.1.1.1.17.1";
-char* oidentPhysicalUris_1 = ".1.3.6.1.2.1.47.1.1.1.1.18.1";
+const char* oidentPhysicalIndex_1 = ".1.3.6.1.2.1.47.1.1.1.1.1.1";
+const char* oidentPhysicalDescr_1 = ".1.3.6.1.2.1.47.1.1.1.1.2.1";
+const char* oidentPhysicalVendorType_1 = ".1.3.6.1.2.1.47.1.1.1.1.3.1";
+const char* oidentPhysicalContainedIn_1 = ".1.3.6.1.2.1.47.1.1.1.1.4.1";
+const char* oidentPhysicalClass_1 = ".1.3.6.1.2.1.47.1.1.1.1.5.1";
+const char* oidentPhysicalParentRelPos_1 = ".1.3.6.1.2.1.47.1.1.1.1.6.1";
+const char* oidentPhysicalName_1 = ".1.3.6.1.2.1.47.1.1.1.1.7.1";
+const char* oidentPhysicalHardwareRev_1 = ".1.3.6.1.2.1.47.1.1.1.1.8.1";
+const char* oidentPhysicalFirmwareRev_1 = ".1.3.6.1.2.1.47.1.1.1.1.9.1";
+const char* oidentPhysicalSoftwareRev_1 = ".1.3.6.1.2.1.47.1.1.1.1.10.1";
+const char* oidentPhysicalSerialNum_1 = ".1.3.6.1.2.1.47.1.1.1.1.11.1";
+const char* oidentPhysicalMfgName_1 = ".1.3.6.1.2.1.47.1.1.1.1.12.1";
+const char* oidentPhysicalModelName_1 = ".1.3.6.1.2.1.47.1.1.1.1.13.1";
+const char* oidentPhysicalAlias_1 = ".1.3.6.1.2.1.47.1.1.1.1.14.1";
+const char* oidentPhysicalAssetID_1 = ".1.3.6.1.2.1.47.1.1.1.1.15.1";
+const char* oidentPhysicalIsFRU_1 = ".1.3.6.1.2.1.47.1.1.1.1.16.1";
+const char* oidentPhysicalMfgDate_1 = ".1.3.6.1.2.1.47.1.1.1.1.17.1";
+const char* oidentPhysicalUris_1 = ".1.3.6.1.2.1.47.1.1.1.1.18.1";
int entPhysicalIndex_1 = 1;
-std::string entPhysicalDescr_1 = "Fake Temperature Sensor";
-std::string entPhysicalVendorType_1 = "";
+char entPhysicalDescr_1[] = "Fake Temperature Sensor";
+char entPhysicalVendorType_1[] = "";
int entPhysicalContainedIn_1 = 0;
-int entPhysicalClass_1 = 8; // Sensor
+int entPhysicalClass_1 = 8;
int entPhysicalParentRelPos_1 = -1;
-std::string entPhysicalName_1 = "";
-std::string entPhysicalHardwareRev_1 = "";
-std::string entPhysicalFirmwareRev_1 = "";
-std::string entPhysicalSoftwareRev_1 = "";
-std::string entPhysicalSerialNum_1 = "";
-std::string entPhysicalMfgName_1 = "";
-std::string entPhysicalModelName_11 = "";
-std::string entPhysicalAlias_1 = "";
-std::string entPhysicalAssetID_1 = "";
+char entPhysicalName_1[] = "";
+char entPhysicalHardwareRev_1[] = "";
+char entPhysicalFirmwareRev_1[] = "";
+char entPhysicalSoftwareRev_1[] = "";
+char entPhysicalSerialNum_1[] = "";
+char entPhysicalMfgName_1[] = "";
+char entPhysicalModelName_11[] = "";
+char entPhysicalAlias_1[] = "";
+char entPhysicalAssetID_1[] = "";
int entPhysicalIsFRU_1 = 0;
-std::string entPhysicalMfgDate_1 = "'0000000000000000'H"; // Special value, not sure it's correct. Or meant to be a Hex string?
-std::string entPhysicalUris_1 = "";
+char entPhysicalMfgDate_1[] = "'0000000000000000'H";
+char entPhysicalUris_1[] = "";
// EntityPhysicalGroup
// ENTITY-SENSOR-MIB .1.3.6.1.2.1.99
// An entry would be required per sensor. This is index 1.
// Must match index in ENTITY-MIB
-char* oidentPhySensorType_1 = ".1.3.6.1.2.1.99.1.1.1.1.1";
-char* oidentPhySensorScale_1 = ".1.3.6.1.2.1.99.1.1.1.2.1";
-char* oidentPhySensorPrecision_1 = ".1.3.6.1.2.1.99.1.1.1.3.1";
-char* oidentPhySensorValue_1 = ".1.3.6.1.2.1.99.1.1.1.4.1";
-char* oidentPhySensorOperStatus_1 = ".1.3.6.1.2.1.99.1.1.1.5.1";
-char* oidentPhySensorUnitsDisplay_1 = ".1.3.6.1.2.1.99.1.1.1.6.1";
-char* oidentPhySensorValueTimeStamp_1 = ".1.3.6.1.2.1.99.1.1.1.7.1";
-char* oidentPhySensorValueUpdateRate_1 = ".1.3.6.1.2.1.99.1.1.1.8.1";
+const char* oidentPhySensorType_1 = ".1.3.6.1.2.1.99.1.1.1.1.1";
+const char* oidentPhySensorScale_1 = ".1.3.6.1.2.1.99.1.1.1.2.1";
+const char* oidentPhySensorPrecision_1 = ".1.3.6.1.2.1.99.1.1.1.3.1";
+const char* oidentPhySensorValue_1 = ".1.3.6.1.2.1.99.1.1.1.4.1";
+const char* oidentPhySensorOperStatus_1 = ".1.3.6.1.2.1.99.1.1.1.5.1";
+const char* oidentPhySensorUnitsDisplay_1 = ".1.3.6.1.2.1.99.1.1.1.6.1";
+const char* oidentPhySensorValueTimeStamp_1 = ".1.3.6.1.2.1.99.1.1.1.7.1";
+const char* oidentPhySensorValueUpdateRate_1 = ".1.3.6.1.2.1.99.1.1.1.8.1";
int entPhySensorType_1 = 8; // Celsius
int entPhySensorScale_1 = 9; // Units
int entPhySensorPrecision_1 = 0;
int entPhySensorValue_1 = 0; // Value to be updated
int entPhySensorOperStatus_1 = 1; // OK
-std::string entPhySensorUnitsDisplay_1 = "Celsius";
-int entPhySensorValueTimeStamp_1 = 0;
+char entPhySensorUnitsDisplay_1[] = "Celsius";
+uint32_t entPhySensorValueTimeStamp_1 = 0;
int entPhySensorValueUpdateRate_1 = 0; // Unknown at declaration, set later.
//************************************
@@ -143,9 +194,9 @@ void printFile(const char* filename);
void setup()
{
Serial.begin(115200);
- if (!LITTLEFS.begin(FORMAT_LITTLEFS_IF_FAILED))
+ if (!FS_BEGIN())
{
- Serial.println("LITTLEFS Mount Failed");
+ Serial.println("LittleFS Mount Failed");
return;
}
WiFi.begin(ssid, password);
@@ -210,7 +261,7 @@ int readFakeSensor()
{
int min = -50;
int max = 100;
- return min + esp_random() % ((max + 1) - min);
+ return min + (int)(SNMP_RAND() % (uint32_t)((max + 1) - min));
}
#if defined(ESP32)
@@ -240,7 +291,7 @@ int getUptime()
void printFile(const char* filename)
{
// Open file for reading
- File file = LITTLEFS.open(filename, "r");
+ File file = FILESYSTEM.open(filename, "r");
if (!file)
{
Serial.println(F("Failed to read file"));
@@ -258,7 +309,7 @@ void printFile(const char* filename)
bool loadSNMPValues()
{
- File file = LITTLEFS.open(savedValuesFile, "r");
+ File file = FILESYSTEM.open(savedValuesFile, "r");
if (!file)
{
Serial.println(F("Failed to read saved values file"));
@@ -292,7 +343,7 @@ bool loadSNMPValues()
bool saveSNMPValues()
{
- File file = LITTLEFS.open(savedValuesFile, "w");
+ File file = FILESYSTEM.open(savedValuesFile, "w");
if (!file)
{
Serial.println(F("Failed to open saved values file for writing"));
@@ -324,9 +375,12 @@ void addRFC1213MIBHandler()
snmp.addIntegerHandler(oidSysServices, &sysServices);
snmp.addTimestampHandler(oidSysUptime, &sysUptime);
// Add Settable Handlers
- snmp.addReadWriteStringHandler(oidSysContact, &sysContact, 25, true);
- snmp.addReadWriteStringHandler(oidSysName, &sysName, 25, true);
- snmp.addReadWriteStringHandler(oidSysLocation, &sysLocation, 25, true);
+ // NOTE: maxLength = sizeof(_buf) matches the 255-byte storage declared above,
+ // so SET operations via SNMP and strlcpy() from persistent storage agree on the
+ // same maximum string length.
+ snmp.addReadWriteStringHandler(oidSysContact, &sysContact, sizeof(sysContactValue), true);
+ snmp.addReadWriteStringHandler(oidSysName, &sysName, sizeof(sysNameValue), true);
+ snmp.addReadWriteStringHandler(oidSysLocation, &sysLocation, sizeof(sysLocationValue), true);
}
void addENTITYMIBHandler()
diff --git a/extras/demos/arduino_cli_esp32/arduino_cli_esp32.ino b/extras/demos/arduino_cli_esp32/arduino_cli_esp32.ino
new file mode 100644
index 0000000..2c5b60c
--- /dev/null
+++ b/extras/demos/arduino_cli_esp32/arduino_cli_esp32.ino
@@ -0,0 +1,70 @@
+/*
+ * SNMP_Agent minimal demo — ESP32 (esp32:esp32:esp32)
+ * Target: arduino-cli compile --fqbn esp32:esp32:esp32
+ * Purpose: DoD H — confirm Arduino-CLI ESP32 build is zero-error,
+ * zero-warning against the current SNMP_Agent HEAD checkout.
+ *
+ * Wiring: Serial 115200 baud (native USB on ESP32 DevKitC).
+ *
+ * Before run:
+ * 1. Fill in WIFI_SSID / WIFI_PASSWORD below.
+ * 2. Install esp32:esp32 core via Arduino CLI board manager.
+ * 3. Symlink / copy this repo into your /libraries/SNMP_Agent
+ * (Arduino-CLI scans for libraries here at compile time).
+ * e.g. ln -s ~/Arduino/libraries/SNMP_Agent
+ */
+
+#include
+#include
+
+#define WIFI_SSID "your-ssid-here"
+#define WIFI_PASSWORD "your-pass-here"
+
+static SNMPAgent agent;
+
+static int myInteger = 42;
+static char sensorName[] = "ESP32-Sensor";
+static uint32_t counter32 = 0;
+
+static uint32_t getUptimeSeconds(void) { return (uint32_t)(millis() / 1000U); }
+
+void setup()
+{
+ Serial.begin(115200);
+ delay(500);
+ Serial.println();
+ Serial.println("SNMP_Agent v3.x minimal demo (ESP32)");
+
+ WiFi.mode(WIFI_STA);
+ WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
+ while (WiFi.status() != WL_CONNECTED) {
+ delay(200);
+ Serial.print('.');
+ }
+ Serial.println();
+ Serial.print("IP: "); Serial.println(WiFi.localIP());
+
+ agent.begin(); // Default community: "public", OID prefix: ".1.3.6.1.2.1.1"
+
+ /* Three representative handlers to stress-test different ValueCallback
+ * code paths (integer / static string / dynamic-timestamp). */
+ agent.addIntegerHandler(".8.0", &myInteger, true);
+ agent.addReadOnlyStaticStringHandler(".5.0", sensorName);
+ agent.addDynamicReadOnlyTimestampHandler(".3.0", getUptimeSeconds);
+
+ agent.addCounter32Handler(".6.1.2.1.0", &counter32);
+
+ Serial.println("SNMP agent started. try: snmpget -v 2c -c public .1.3.6.1.2.1.1.8.0");
+}
+
+void loop()
+{
+ agent.loop();
+
+ static unsigned long lastTick = 0;
+ if (millis() - lastTick > 1000UL) {
+ lastTick = millis();
+ counter32++; /* Exercise the counter32 handler on each tick so
+ subsequent SNMP walks see changing data. */
+ }
+}
diff --git a/extras/demos/arduino_cli_esp8266/arduino_cli_esp8266.ino b/extras/demos/arduino_cli_esp8266/arduino_cli_esp8266.ino
new file mode 100644
index 0000000..d779894
--- /dev/null
+++ b/extras/demos/arduino_cli_esp8266/arduino_cli_esp8266.ino
@@ -0,0 +1,70 @@
+/*
+ * SNMP_Agent minimal demo — ESP8266 (ESP-01)
+ * Target: arduino-cli compile --fqbn esp8266:esp8266:generic
+ * Purpose: DoD H — confirm Arduino-CLI ESP8266 build is zero-error,
+ * zero-warning against the current SNMP_Agent HEAD checkout.
+ *
+ * Wiring: Serial 115200 baud, GPIO1 TX, GPIO3 RX (ESP-01 pins).
+ *
+ * Before run:
+ * 1. Fill in WIFI_SSID / WIFI_PASSWORD below.
+ * 2. Install esp8266:esp8266 core via Arduino CLI board manager.
+ * 3. Symlink / copy this repo into your /libraries/SNMP_Agent
+ * (Arduino-CLI scans for libraries here at compile time).
+ * e.g. ln -s ~/Arduino/libraries/SNMP_Agent
+ */
+
+#include
+#include
+
+#define WIFI_SSID "your-ssid-here"
+#define WIFI_PASSWORD "your-pass-here"
+
+static SNMPAgent agent;
+
+static int myInteger = 42;
+static char sensorName[] = "ESP-01-Sensor";
+static uint32_t counter32 = 0;
+
+static uint32_t getUptimeSeconds(void) { return (uint32_t)(millis() / 1000U); }
+
+void setup()
+{
+ Serial.begin(115200);
+ delay(500);
+ Serial.println();
+ Serial.println(F("SNMP_Agent v3.x minimal demo (ESP8266)"));
+
+ WiFi.mode(WIFI_STA);
+ WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
+ while (WiFi.status() != WL_CONNECTED) {
+ delay(200);
+ Serial.print('.');
+ }
+ Serial.println();
+ Serial.print(F("IP: ")); Serial.println(WiFi.localIP());
+
+ agent.begin(); // Default community: "public", OID prefix: ".1.3.6.1.2.1.1"
+
+ /* Three representative handlers to stress-test different ValueCallback
+ * code paths (integer / static string / dynamic-timestamp). */
+ agent.addIntegerHandler(".8.0", &myInteger, true);
+ agent.addReadOnlyStaticStringHandler(".5.0", sensorName);
+ agent.addDynamicReadOnlyTimestampHandler(".3.0", getUptimeSeconds);
+
+ agent.addCounter32Handler(".6.1.2.1.0", &counter32);
+
+ Serial.println(F("SNMP agent started. try: snmpget -v 2c -c public .1.3.6.1.2.1.1.8.0"));
+}
+
+void loop()
+{
+ agent.loop();
+
+ static unsigned long lastTick = 0;
+ if (millis() - lastTick > 1000UL) {
+ lastTick = millis();
+ counter32++; /* Exercise the counter32 handler on each tick so
+ subsequent SNMP walks see changing data. */
+ }
+}
diff --git a/extras/demos/platformio_minimal/platformio.ini b/extras/demos/platformio_minimal/platformio.ini
new file mode 100644
index 0000000..ead2d2d
--- /dev/null
+++ b/extras/demos/platformio_minimal/platformio.ini
@@ -0,0 +1,46 @@
+; PlatformIO project for SNMP_Agent minimal compilation demo — DoD I
+; --------------------------------------------------------------------
+; Build commands:
+; cd extras/demos/platformio_minimal
+; pio run --environment esp8266 ; ESP-01 (1MB flash) build
+; pio run --environment esp32 ; ESP32 DevKitC build
+; pio run ; both environments in parallel
+; --------------------------------------------------------------------
+
+[platformio]
+default_envs = esp8266, esp32
+; lib_extra_dirs is PER-ENVIRONMENT, not global. We use relative path
+; extras/demos/platformio_minimal -> . is platformio_minimal; ../.. is extras/demos;
+; ../../../.. is the repo root where library.properties of SNMP_Agent lives.
+
+; -------- COMMON SNIPPET reusable for each env --------
+[common_env]
+build_flags =
+ -Wall -Wextra -Werror
+ -std=gnu++11
+lib_compat_mode = strict
+lib_ldf_mode = deep+
+; Repo root relative to extras/demos/platformio_minimal/: 4 levels up
+lib_extra_dirs = ../../../..
+
+; -------- ESP-01 (ESP8266, 1MB flash, "dout" required for the tiny flash chip) --------
+[env:esp8266]
+platform = espressif8266
+board = esp01_1m
+framework = arduino
+board_build.flash_mode = dout
+build_flags = ${common_env.build_flags}
+lib_compat_mode = ${common_env.lib_compat_mode}
+lib_ldf_mode = ${common_env.lib_ldf_mode}
+lib_extra_dirs = ${common_env.lib_extra_dirs}
+
+; -------- ESP32 DevKitC (default 4MB flash, QIO flash mode) --------
+[env:esp32]
+platform = espressif32
+board = esp32dev
+framework = arduino
+build_flags = ${common_env.build_flags}
+lib_compat_mode = ${common_env.lib_compat_mode}
+lib_ldf_mode = ${common_env.lib_ldf_mode}
+lib_extra_dirs = ${common_env.lib_extra_dirs}
+
diff --git a/extras/demos/platformio_minimal/src/main.ino b/extras/demos/platformio_minimal/src/main.ino
new file mode 100644
index 0000000..aa94f74
--- /dev/null
+++ b/extras/demos/platformio_minimal/src/main.ino
@@ -0,0 +1,73 @@
+/*
+ * SNMP_Agent minimal demo — dual-build (esp8266 + esp32 via PlatformIO)
+ * Target: pio run --environment esp8266 / pio run --environment esp32
+ * Purpose: DoD I — confirm PlatformIO strict build (with -Werror) passes
+ * both ESP targets cleanly.
+ *
+ * Uses #if macro dispatch so a single .ino file compiles against both
+ * ESP8266WiFi.h (Espressif 8266 core) and WiFi.h (Espressif 32 core).
+ *
+ * Before flashing: fill in WIFI_SSID / WIFI_PASSWORD below, then
+ * pio run -t upload -e esp8266 OR pio run -t upload -e esp32
+ * pio device monitor -b 115200
+ */
+
+#if defined(ESP8266)
+# include
+#else
+# include
+#endif
+
+#include
+
+#define WIFI_SSID "your-ssid-here"
+#define WIFI_PASSWORD "your-pass-here"
+
+static SNMPAgent agent;
+
+static int myInteger = 42;
+static char sensorName[] = "PlatformIO-Sensor";
+static uint32_t counter32 = 0;
+
+static uint32_t getUptimeSeconds(void) { return (uint32_t)(millis() / 1000U); }
+
+void setup()
+{
+ Serial.begin(115200);
+ delay(500);
+ Serial.println();
+#if defined(ESP8266)
+ Serial.println(F("SNMP_Agent v3.x minimal demo (ESP8266 / PlatformIO)"));
+#else
+ Serial.println("SNMP_Agent v3.x minimal demo (ESP32 / PlatformIO)");
+#endif
+
+ WiFi.mode(WIFI_STA);
+ WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
+ while (WiFi.status() != WL_CONNECTED) {
+ delay(200);
+ Serial.print('.');
+ }
+ Serial.println();
+ Serial.print("IP: "); Serial.println(WiFi.localIP());
+
+ agent.begin();
+
+ agent.addIntegerHandler(".8.0", &myInteger, true);
+ agent.addReadOnlyStaticStringHandler(".5.0", sensorName);
+ agent.addDynamicReadOnlyTimestampHandler(".3.0", getUptimeSeconds);
+ agent.addCounter32Handler(".6.1.2.1.0", &counter32);
+
+ Serial.println("SNMP agent started.");
+}
+
+void loop()
+{
+ agent.loop();
+
+ static unsigned long lastTick = 0;
+ if (millis() - lastTick > 1000UL) {
+ lastTick = millis();
+ counter32++;
+ }
+}
diff --git a/library.properties b/library.properties
index 11e15da..73f264d 100644
--- a/library.properties
+++ b/library.properties
@@ -1,5 +1,5 @@
name=SNMP_Agent
-version=2.1.0
+version=3.1.5
author=Aidan Cyr
maintainer=Aidan Cyr
sentence=SNMP Agent: An fully compliant SNMPv2c Agent for esp32 for acting as an SNMP client device.
diff --git a/src/BERDecode.cpp b/src/BERDecode.cpp
index 5f041d7..dd0944b 100644
--- a/src/BERDecode.cpp
+++ b/src/BERDecode.cpp
@@ -1,5 +1,50 @@
#include "include/BER.h"
+#ifndef SNMP_POOLS_IN_BSS
+ /* Allocate the ASNPool storage once at startup (first asn_new call).
+ Count and layout are fixed at compile time — never reallocated, never
+ grown, complying with the "heap only at init, zero runtime growth"
+ rule. This moves ~25 KB of static BSS (.bss) into the heap on
+ ESP8266 tiny profiles, restoring WiFi/LittleFS/ArduinoJson headroom. */
+ ASNPool::Slot* ASNPool::slots = nullptr;
+ bool ASNPool::_poolsReady = false;
+
+ void ASNPool::_ensurePools() {
+ if(_poolsReady) return;
+ /* operator new[] calls each Slot's default ctor; Slot is trivially
+ default-constructible (alignas char[N] array + bool) so we get
+ zero-initialized occupied and untouched storage bytes as required. */
+ slots = new Slot[SNMP_POOL_ASN_OBJECTS]();
+ _poolsReady = true;
+ }
+#else
+ ASNPool::Slot ASNPool::slots[SNMP_POOL_ASN_OBJECTS] = {};
+#endif
+int ASNPool::usedCount = 0;
+
+void ASNPool::release(BER_CONTAINER* p){
+ if(!p) return;
+#ifndef SNMP_POOLS_IN_BSS
+ if(!_poolsReady) { delete p; return; }
+#endif
+ p->~BER_CONTAINER();
+ for(int i = 0; i < SNMP_POOL_ASN_OBJECTS; i++){
+ if(static_cast(slots[i].storage) == static_cast(p)){
+ slots[i].occupied = false;
+ if(usedCount > 0) usedCount--;
+ return;
+ }
+ }
+}
+
+void asn_delete(BER_CONTAINER* p){
+ if(!p) return;
+ if(ASNPool::isInPool(static_cast(p))){
+ ASNPool::release(p);
+ } else {
+ delete p;
+ }
+}
// Two ways to decode an int, one way where the first byte indicates how many butes follow, and ne where you have to power things by 128
static size_t decode_ber_longform_integer(const uint8_t* buf, long* decoded_integer, int max_len){
int i = 1;
@@ -33,22 +78,21 @@ static size_t decode_ber_length_integer(const uint8_t* buf, int* decoded_integer
}
int BER_CONTAINER::fromBuffer(const uint8_t *buf, size_t max_len) {
- // In the base class we are going to double check our type, and decode the length of this structure, then return bytes read
- if(max_len < 2) return SNMP_BUFFER_ERROR_TLV_TOO_SMALL; // Too small for any type
+ if(max_len < 2) return SNMP_BUFFER_ERROR_TLV_TOO_SMALL;
const uint8_t* ptr = buf;
if(*ptr != _type){
SNMP_LOGE("Mismatched type when decoding %d, %d\n", _type, *ptr);
return SNMP_BUFFER_ERROR_TYPE_MISMATCH;
}
- ptr++; // type
- ptr += decode_ber_length_integer(ptr, &_length, max_len);
- if((size_t)_length + 2 > max_len){
- // length of object is too big to read in
+ ptr++;
+ ptr += decode_ber_length_integer(ptr, &_length, (int)max_len - 1);
+ int header_len = static_cast(ptr - buf);
+ if(static_cast(_length) + header_len > max_len){
return SNMP_BUFFER_ERROR_MAX_LEN_EXCEEDED;
}
- return ptr - buf;
+ return header_len;
}
int NetworkAddress::fromBuffer(const uint8_t *buf, size_t max_len){
@@ -106,7 +150,10 @@ int OctetType::fromBuffer(const uint8_t *buf, size_t max_len){
const uint8_t* ptr = buf + i;
if(_length > OCTET_TYPE_MAX_LENGTH) return SNMP_BUFFER_ERROR_OCTET_TOO_BIG;
- _value.assign((char*)ptr, _length);
+ if(_length > (int)SNMP_MAX_STRING_LEN) _length = (int)SNMP_MAX_STRING_LEN;
+ memcpy(_value, ptr, _length);
+ _value[_length] = 0;
+ _valueLen = _length;
return _length + i;
}
@@ -114,26 +161,39 @@ int OctetType::fromBuffer(const uint8_t *buf, size_t max_len){
int OpaqueType::fromBuffer(const uint8_t *buf, size_t max_len){
int i = BER_CONTAINER::fromBuffer(buf, max_len);
CHECK_DECODE_ERR(i);
- const uint8_t* ptr = buf + i;
- _value = (uint8_t*)calloc(_length, sizeof(char));
- memcpy(_value, (char*)ptr, _length);
+ if(_length > (int)sizeof(this->_value)) {
+ return SNMP_BUFFER_ERROR_MAX_LEN_EXCEEDED;
+ }
+
+ if(static_cast(i + _length) > max_len) {
+ return SNMP_BUFFER_ERROR_MAX_LEN_EXCEEDED;
+ }
+
+ const uint8_t* ptr = buf + i;
+ if(_length > 0) {
+ memcpy(this->_value, ptr, (size_t)_length);
+ }
_dataLength = _length;
- return _length + i;
+ return i + _length;
}
int OIDType::fromBuffer(const uint8_t *buf, size_t max_len){
int j = BER_CONTAINER::fromBuffer(buf, max_len);
CHECK_DECODE_ERR(j);
- const uint8_t* dataPtr = buf + j;
+ if(_length > (int)sizeof(this->data)) return SNMP_BUFFER_ERROR_MAX_LEN_EXCEEDED;
+ if(static_cast(j + _length) > max_len) return SNMP_BUFFER_ERROR_MAX_LEN_EXCEEDED;
+
+ const uint8_t* dataPtr = buf + j;
if(*dataPtr != 0x2b) return SNMP_BUFFER_ERROR_INVALID_OID;
- this->data.reserve(_length);
- this->data.assign(dataPtr, dataPtr + _length);
+
+ if(_length > 0) memcpy(this->data, dataPtr, (size_t)_length);
+ this->dataLen = _length;
this->valid = true;
- return _length + j;
+ return j + _length;
}
static inline void long_to_buf(char* buf, long l, short r = 0){
@@ -144,16 +204,23 @@ static inline void long_to_buf(char* buf, long l, short r = 0){
if(!r) *buf = 0;
}
-const std::string& OIDType::string() {
- if(!this->_value.length()){
- const uint8_t* dataPtr = this->data.data();
+const char* OIDType::string() {
+ if(_valueStr[0] == 0){
+ const uint8_t* dataPtr = this->data;
- this->_value = ".1.3";
- if(!this->valid) return this->_value;
+ size_t pos = 0;
+ const char* prefix = ".1.3";
+ size_t prefixLen = strlen(prefix);
+ if(pos + prefixLen <= SNMP_MAX_OID_STR_LEN){
+ memcpy(_valueStr + pos, prefix, prefixLen);
+ pos += prefixLen;
+ }
+ _valueStr[pos] = 0;
+ if(!this->valid) return _valueStr;
dataPtr++;
- int i = this->data.size() - 1;
+ int i = this->dataLen - 1;
char buffer[16];
while(i > 0){
@@ -164,31 +231,33 @@ const std::string& OIDType::string() {
buffer[0] = '.';
long_to_buf(buffer+1, item);
- this->_value.append(buffer);
+ size_t buflen = strlen(buffer);
+ if(pos + buflen <= SNMP_MAX_OID_STR_LEN){
+ memcpy(_valueStr + pos, buffer, buflen);
+ pos += buflen;
+ }
+ _valueStr[pos] = 0;
}
}
- return this->_value;
+ return _valueStr;
}
-const std::vector SortableOIDType::generateSortingMap() const {
- auto map = std::vector();
-
- // maybe anice midway between speed and size?
- map.reserve(this->data.size() * 1);
+void SortableOIDType::generateSortingMap(uint32_t outMap[SNMP_MAX_OID_SUBIDENTIFIERS], int* outLen) const {
+ int count = 0;
- const uint8_t* ptr = this->data.data();
+ const uint8_t* ptr = this->data;
- ptr += 1; // skip to start of interesting differences
- int i = this->data.size() - 1;
+ ptr += 1;
+ int i = this->dataLen - 1;
- while(i > 0){
+ while(i > 0 && count < SNMP_MAX_OID_SUBIDENTIFIERS){
long item;
size_t len = decode_ber_longform_integer(ptr, &item, i);
ptr += len; i -= len;
- map.push_back(item);
+ outMap[count++] = (uint32_t)item;
}
- return map;
+ *outLen = count;
}
int NullType::fromBuffer(const uint8_t *, size_t){
@@ -199,6 +268,9 @@ int NullType::fromBuffer(const uint8_t *, size_t){
int Counter64::fromBuffer(const uint8_t *buf, size_t max_len){
int i = BER_CONTAINER::fromBuffer(buf, max_len);
CHECK_DECODE_ERR(i);
+
+ if(static_cast(i + _length) > max_len) return SNMP_BUFFER_ERROR_MAX_LEN_EXCEEDED;
+
const uint8_t* ptr = buf + i;
int tempLength = _length;
@@ -208,44 +280,41 @@ int Counter64::fromBuffer(const uint8_t *buf, size_t max_len){
_value = _value | *ptr++;
tempLength--;
}
- return _length + i;
+ return i + _length;
}
-std::shared_ptr ComplexType::createObjectForType(ASN_TYPE valueType){
+BER_CONTAINER* ComplexType::createObjectForType(ASN_TYPE valueType){
SNMP_LOGD("Creating object of type: %d\n", valueType);
switch(valueType){
case INTEGER:
- return std::shared_ptr(new IntegerType());
+ return asn_new();
case STRING:
- return std::shared_ptr(new OctetType());
+ return asn_new();
case OID:
- return std::shared_ptr(new OIDType());
+ return asn_new();
case NULLTYPE:
- return std::shared_ptr(new NullType());
+ return asn_new();
case NOSUCHOBJECT:
- return std::shared_ptr(new ImplicitNullType(NOSUCHOBJECT));
+ return asn_new(NOSUCHOBJECT);
case NOSUCHINSTANCE:
- return std::shared_ptr(new ImplicitNullType(NOSUCHINSTANCE));
+ return asn_new(NOSUCHINSTANCE);
case ENDOFMIBVIEW:
- return std::shared_ptr(new ImplicitNullType(ENDOFMIBVIEW));
+ return asn_new(ENDOFMIBVIEW);
- // devired
case NETWORK_ADDRESS:
- return std::shared_ptr(new NetworkAddress());
+ return asn_new();
case TIMESTAMP:
- return std::shared_ptr(new TimestampType());
+ return asn_new();
case COUNTER32:
- return std::shared_ptr(new Counter32());
+ return asn_new();
case GAUGE32:
- return std::shared_ptr(new Gauge());
+ return asn_new();
case COUNTER64:
- return std::shared_ptr(new Counter64());
+ return asn_new();
case OPAQUE:
- return std::shared_ptr(new OpaqueType());
+ return asn_new();
- // Complex
- /* OPAQUE = 0x44 */
case STRUCTURE:
case GetRequestPDU:
@@ -254,10 +323,9 @@ std::shared_ptr ComplexType::createObjectForType(ASN_TYPE valueTy
case SetRequestPDU:
case GetBulkRequestPDU:
- //case TrapPDU: // should never get v1trap, but put it in anyway
case InformRequestPDU:
case Trapv2PDU:
- return std::shared_ptr(new ComplexType(valueType));
+ return asn_new(valueType);
default:
return nullptr;
}
@@ -266,29 +334,40 @@ std::shared_ptr ComplexType::createObjectForType(ASN_TYPE valueTy
int ComplexType::fromBuffer(const uint8_t *buf, size_t max_len){
int j = BER_CONTAINER::fromBuffer(buf, max_len);
CHECK_DECODE_ERR(j);
+
+ if(static_cast(j + _length) > max_len) return SNMP_BUFFER_ERROR_MAX_LEN_EXCEEDED;
+
const uint8_t* ptr = buf + j;
+ size_t outer_used = static_cast(j);
+
+ this->_ownsChildren = true; /* children are new'd here; we own them */
- size_t i = 1;
- while(i < (size_t)_length && i <= max_len){
+ int remaining = _length;
+ while(remaining > 0){
ASN_TYPE valueType = (ASN_TYPE)*ptr;
- auto newObj = ComplexType::createObjectForType(valueType);
+ if(this->valuesLen >= SNMP_MAX_COMPLEX_CHILDREN) {
+ return SNMP_BUFFER_ERROR_UNKNOWN_TYPE; /* too many VBs; well-defined error */
+ }
+
+ BER_CONTAINER* newObj = ComplexType::createObjectForType(valueType);
if(!newObj){
SNMP_LOGD("Couldn't create object of type: %d\n", valueType);
return SNMP_BUFFER_ERROR_UNKNOWN_TYPE;
}
- int used_length = newObj->fromBuffer(ptr, max_len - i);
+ int used_length = newObj->fromBuffer(ptr, max_len - outer_used);
if(used_length < 0){
- // Problem de-serialising
+ asn_delete(newObj);
SNMP_LOGD("Problem deserialising structure of type: %d\n", valueType);
return SNMP_BUFFER_ERROR_PROBLEM_DESERIALISING;
}
- addValueToList(newObj);
+ this->values[this->valuesLen++] = newObj;
- ptr += used_length;
- i += used_length;
+ ptr += used_length;
+ remaining -= used_length;
+ outer_used += static_cast(used_length);
}
- return _length + j;
+ return j + _length;
}
\ No newline at end of file
diff --git a/src/BEREncode.cpp b/src/BEREncode.cpp
index fed7d8c..b86d619 100644
--- a/src/BEREncode.cpp
+++ b/src/BEREncode.cpp
@@ -109,12 +109,12 @@ int NullType::serialise(uint8_t* buf, size_t max_len){
int OctetType::serialise(uint8_t* buf, size_t max_len){
- int i = BER_CONTAINER::serialise(buf, max_len, _value.length());
+ int i = BER_CONTAINER::serialise(buf, max_len, _valueLen);
CHECK_ENCODE_ERR(i);
uint8_t *ptr = buf + i;
- memcpy(ptr, _value.data(), _value.length());
- ptr += _value.length();
+ memcpy(ptr, _value, _valueLen);
+ ptr += _valueLen;
return ptr - buf;
}
@@ -131,52 +131,55 @@ int OpaqueType::serialise(uint8_t* buf, size_t max_len){
}
int OIDType::serialise(uint8_t* buf, size_t max_len){
- int i = BER_CONTAINER::serialise(buf, max_len, this->data.size());
+ int i = BER_CONTAINER::serialise(buf, max_len, (size_t)this->dataLen);
CHECK_ENCODE_ERR(i);
if(!this->valid) return SNMP_BUFFER_ENCODE_ERROR_INVALID_OID;
+ if(static_cast(i + this->dataLen) > max_len) {
+ return SNMP_BUFFER_ENCODE_ERR_LEN_EXCEEDED;
+ }
+
uint8_t* ptr = buf + i;
- memcpy(ptr, this->data.data(), this->data.size());
+ if(this->dataLen > 0) memcpy(ptr, this->data, (size_t)this->dataLen);
- ptr += this->data.size();
+ ptr += this->dataLen;
return ptr - buf;
}
bool OIDType::generateInternalData() {
- if(_value.find(".1.3.") != 0) { this->valid = false; return false; }; // Invalid OID
+ if(strncmp(_valueStr, ".1.3.", 5) != 0) { this->valid = false; return false; };
- this->data.clear();
- this->data.push_back(0x2b); // first byte
+ this->dataLen = 0;
+ if(this->dataLen >= (int)sizeof(this->data)) return false;
+ this->data[this->dataLen++] = 0x2b;
- char* valuePtr = &_value[5];
+ const char* valuePtr = _valueStr + 5;
+ uint8_t temp[10];
while(*valuePtr != 0){
bool toBreak = false;
- char* startNum = valuePtr;
-
- // Find the end of this item (next dot or end of string)
- char* endNum = strchr(startNum, '.');
- if(!endNum) {
- toBreak = true;
- }
+ const char* startNum = valuePtr;
- long tempVal;
- uint8_t temp[10] = {0};
- if(sscanf(startNum, "%ld.", &tempVal)){
- int encoded_length = encode_ber_longform_integer(temp, tempVal, 10);
+ const char* endNum = strchr(startNum, '.');
+ if(!endNum) toBreak = true;
- for(int i = 0; i < encoded_length; i++){
- this->data.push_back(temp[i]);
- }
+ long tempVal = strtol(startNum, nullptr, 10);
+ if(tempVal == 0 && *startNum != '0') return false;
- if(toBreak) break;
- valuePtr = endNum+1;
- } else {
+ int encoded_length = (int)encode_ber_longform_integer(temp, tempVal, (int)sizeof(temp));
+ if(this->dataLen + encoded_length > (int)sizeof(this->data)) {
+ this->valid = false;
return false;
}
+ for(int i = 0; i < encoded_length; i++){
+ this->data[this->dataLen++] = temp[i];
+ }
+
+ if(toBreak) break;
+ valuePtr = endNum + 1;
}
- this->_length = this->data.size();
+ this->_length = (int)this->dataLen;
return true;
}
@@ -197,7 +200,8 @@ int ComplexType::serialise(uint8_t* buf, size_t max_len){
int internalLength = 0;
- for(const auto& item : values){
+ for(int n = 0; n < this->valuesLen; n++){
+ BER_CONTAINER* item = this->values[n];
if(!item) return SNMP_BUFFER_ENCODE_ERROR_INVALID_ITEM;
int length = item->serialise(internalPtr, max_len - internalLength - 1);
if(length < 0){
diff --git a/src/SNMPInform.cpp b/src/SNMPInform.cpp
index 532bc1b..fa8f324 100644
--- a/src/SNMPInform.cpp
+++ b/src/SNMPInform.cpp
@@ -1,7 +1,6 @@
#include "include/SNMPInform.h"
#include "SNMPTrap.h"
-#include
#ifdef COMPILING_TESTS
#include
#endif
@@ -10,36 +9,73 @@ inline void delete_inform(struct InformItem* inform){
free(inform);
}
-static void remove_inform_from_list(std::list &list,
- std::function predicate) {
- list.remove_if([&predicate](struct InformItem* item){
- if(predicate(item)){
- delete_inform(item);
- return true;
+static int remove_informs_matching(struct InformItem** informList, int& informCount,
+ bool (*predicate)(struct InformItem*, void*),
+ void* ctx) {
+ int removed = 0;
+ int writeIdx = 0;
+ for(int i = 0; i < informCount; i++){
+ if(predicate(informList[i], ctx)){
+ delete_inform(informList[i]);
+ informList[i] = nullptr;
+ removed++;
+ } else {
+ if(writeIdx != i){
+ informList[writeIdx] = informList[i];
+ informList[i] = nullptr;
+ }
+ writeIdx++;
}
- return false;
- });
+ }
+ informCount = writeIdx;
+ for(int i = informCount; i < SNMP_MAX_TRAPS_INFLIGHT; i++){
+ informList[i] = nullptr;
+ }
+ return removed;
+}
+
+struct QSt_match_trap { SNMPTrap* trap; };
+static bool pred_match_trap(struct InformItem* item, void* ctx){
+ QSt_match_trap* c = (QSt_match_trap*)ctx;
+ return item->trap == c->trap;
+}
+
+struct QSt_match_id { snmp_request_id_t id; };
+static bool pred_match_request_id(struct InformItem* item, void* ctx){
+ QSt_match_id* c = (QSt_match_id*)ctx;
+ return item->requestID == c->id;
+}
+
+static bool pred_done_or_orphan(struct InformItem* item, void*){
+ return item->received || (item->retries == 0 && item->missed);
}
snmp_request_id_t
-queue_and_send_trap(std::list &informList, SNMPTrap *trap, const IPAddress& ip, bool replaceQueuedRequests,
+queue_and_send_trap(struct InformItem **informList, int& informCount, SNMPTrap *trap, const IPAddress& ip, bool replaceQueuedRequests,
int retries, int delay_ms) {
bool buildStatus = trap->buildForSending();
if(!buildStatus) {
SNMP_LOGW("Couldn't build trap\n");
return INVALID_SNMP_REQUEST_ID;
};
- SNMP_LOGD("%lu informs in informList", informList.size());
- //TODO: could be race condition here, buildStatus to return packet?
+ SNMP_LOGD("%d informs in informList", informCount);
if(replaceQueuedRequests){
SNMP_LOGD("Removing any outstanding informs for this trap\n");
- remove_inform_from_list(informList, [trap](struct InformItem* informItem) -> bool {
- return informItem->trap == trap;
- });
+ QSt_match_trap ctx{trap};
+ remove_informs_matching(informList, informCount, pred_match_trap, &ctx);
}
if(trap->inform){
+ if(informCount >= SNMP_MAX_TRAPS_INFLIGHT){
+ SNMP_LOGW("Inform queue full (%d slots), dropping request %lu\n", SNMP_MAX_TRAPS_INFLIGHT, trap->requestID);
+ return INVALID_SNMP_REQUEST_ID;
+ }
+
struct InformItem* item = (struct InformItem*)calloc(1, sizeof(struct InformItem));
+ if(!item){
+ SNMP_LOGW("Couldn't calloc InformItem for request %lu\n", trap->requestID);
+ return INVALID_SNMP_REQUEST_ID;
+ }
item->delay_ms = delay_ms;
item->received = false;
item->requestID = trap->requestID;
@@ -51,11 +87,10 @@ queue_and_send_trap(std::list &informList, SNMPTrap *trap,
SNMP_LOGD("Adding Inform request to queue: %lu\n", item->requestID);
- informList.push_back(item);
+ informList[informCount++] = item;
trap->sendTo(ip, true);
} else {
- // normal send
SNMP_LOGD("Sending normal trap\n");
trap->sendTo(ip);
}
@@ -63,31 +98,30 @@ queue_and_send_trap(std::list &informList, SNMPTrap *trap,
return trap->requestID;
}
-void inform_callback(std::list &informList, snmp_request_id_t requestID, bool responseReceiveSuccess) {
+void inform_callback(struct InformItem **informList, int& informCount, snmp_request_id_t requestID, bool responseReceiveSuccess) {
(void)responseReceiveSuccess;
SNMP_LOGD("Receiving InformCallback for requestID: %lu, success: %d\n", requestID, responseReceiveSuccess);
- //TODO: if we ever want to keep received informs, change this logic
- remove_inform_from_list(informList, [requestID](struct InformItem* informItem) -> bool {
- return informItem->requestID == requestID;
- });
+ QSt_match_id ctx{requestID};
+ remove_informs_matching(informList, informCount, pred_match_request_id, &ctx);
- SNMP_LOGD("Informs waiting for responses: %lu\n", informList.size());
+ SNMP_LOGD("Informs waiting for responses: %d\n", informCount);
}
-void handle_inform_queue(std::list &informList) {
+void handle_inform_queue(struct InformItem **informList, int& informCount) {
auto thisLoop = millis();
- for(auto informItem : informList){
- if(!informItem->received && thisLoop - informItem->lastSent > informItem->delay_ms){
+ for(int i = 0; i < informCount; i++){
+ struct InformItem* informItem = informList[i];
+ if(!informItem) continue;
+ if(!informItem->received && thisLoop - informItem->lastSent > (unsigned long)informItem->delay_ms){
SNMP_LOGD("Missed Inform receive\n");
- // check if sending again
informItem->missed = true;
if(!informItem->retries){
SNMP_LOGD("No more retries for inform: %lu, removing\n", informItem->requestID);
continue;
}
if(informItem->trap){
- SNMP_LOGD("No response received in %lums, Resending Inform: %lu\n", thisLoop - informItem->lastSent, informItem->requestID);
+ SNMP_LOGD("No response received in %dms, Resending Inform: %lu\n", informItem->delay_ms, informItem->requestID);
informItem->trap->sendTo(informItem->ip, true);
informItem->lastSent = thisLoop;
informItem->missed = false;
@@ -95,14 +129,11 @@ void handle_inform_queue(std::list &informList) {
}
}
}
- remove_inform_from_list(informList, [](struct InformItem* informItem) -> bool {
- return informItem->received || (informItem->retries == 0 && informItem->missed);
- });
+ remove_informs_matching(informList, informCount, pred_done_or_orphan, nullptr);
}
-void mark_trap_deleted(std::list &informList, SNMPTrap *trap) {
+void mark_trap_deleted(struct InformItem **informList, int& informCount, SNMPTrap *trap) {
SNMP_LOGD("Removing waiting Informs tied to Trap.\n");
- remove_inform_from_list(informList, [trap](struct InformItem* informItem) -> bool {
- return informItem->trap == trap;
- });
+ QSt_match_trap ctx{trap};
+ remove_informs_matching(informList, informCount, pred_match_trap, &ctx);
}
diff --git a/src/SNMPPDUHandler.cpp b/src/SNMPPDUHandler.cpp
index 70fd6e9..a7968d7 100644
--- a/src/SNMPPDUHandler.cpp
+++ b/src/SNMPPDUHandler.cpp
@@ -3,133 +3,135 @@
#include "include/BER.h"
#include "include/ValueCallbacks.h"
-bool handleGetRequestPDU(std::deque &callbacks, std::deque &varbindList, std::deque &outResponseList, SNMP_VERSION snmpVersion, bool isGetNextRequest){
+template
+static inline bool appendResponseVarBind(VarBind out[], int &outCount, Args&&... args){
+ if(outCount >= SNMP_MAX_VARBINDS) return false;
+ out[outCount].~VarBind();
+ new (&out[outCount]) VarBind(std::forward(args)...);
+ outCount++;
+ return true;
+}
+
+bool handleGetRequestPDU(ValueCallback* const *callbacks, int callbacksCount, const VarBind* varbindList, int varbindCount, VarBind outResponseList[], int &outResponseCount, SNMP_VERSION snmpVersion, bool isGetNextRequest){
SNMP_LOGD("handleGetRequestPDU\n");
- for(const VarBind& requestVarBind : varbindList){
- SNMP_LOGD("finding callback for OID: %s\n", requestVarBind.oid->string().c_str());
- ValueCallback* callback = ValueCallback::findCallback(callbacks, requestVarBind.oid.get(), isGetNextRequest);
+ for(int vbIdx = 0; vbIdx < varbindCount; vbIdx++){
+ const VarBind& requestVarBind = varbindList[vbIdx];
+ SNMP_LOGD("finding callback for OID: %s\n", requestVarBind.oid->string());
+ ValueCallback* callback = ValueCallback::findCallback(callbacks, callbacksCount, requestVarBind.oid, isGetNextRequest);
if(!callback){
SNMP_LOGD("Couldn't find callback\n");
#if 1
- // According to RFC3416 we should be setting the value to 'noSuchObject' or 'noSuchInstance,
- // but this doesn't seem to render nicely in tools, so possibly revert to old NO_SUCH_NAME error
if(isGetNextRequest){
- // if it's a walk it's an endOfMibView
- outResponseList.emplace_back(requestVarBind, std::make_shared(ENDOFMIBVIEW));
+ appendResponseVarBind(outResponseList, outResponseCount, requestVarBind, asn_new(ENDOFMIBVIEW));
} else {
- outResponseList.emplace_back(requestVarBind, std::make_shared(NOSUCHOBJECT));
+ appendResponseVarBind(outResponseList, outResponseCount, requestVarBind, asn_new(NOSUCHOBJECT));
}
#else
- outResponseList.emplace_back(generateErrorResponse(SNMP_ERROR_VERSION_CTRL_DEF(NOT_WRITABLE, snmpVersion, NO_SUCH_NAME), requestVarBind.oid));
+ appendResponseVarBind(outResponseList, outResponseCount, generateErrorResponse(SNMP_ERROR_VERSION_CTRL_DEF(NOT_WRITABLE, snmpVersion, NO_SUCH_NAME), requestVarBind.oid));
#endif
continue;
}
- SNMP_LOGD("Callback found with OID: %s\n", callback->OID->string().c_str());
- //NOTE: we could just use the same pointer as the reqwuest, but delete the value and add a new one. Will have to figure out what to do if it errors, do that later
+ SNMP_LOGD("Callback found with OID: %s\n", callback->OID->string());
auto value = ValueCallback::getValueForCallback(callback);
if(!value){
SNMP_LOGD("Couldn't get value for callback\n");
- outResponseList.emplace_back(callback->OID, SNMP_ERROR_VERSION_CTRL(GEN_ERR, snmpVersion));
- continue;
+ appendResponseVarBind(outResponseList, outResponseCount, callback->OID, SNMP_ERROR_VERSION_CTRL(GEN_ERR, snmpVersion));
+ continue;
}
- outResponseList.emplace_back(callback->OID, value);
+ appendResponseVarBind(outResponseList, outResponseCount, callback->OID, value);
}
- return true; // we didn't fail in our job, even if we filled in nothing
+ return true;
}
-bool handleSetRequestPDU(std::deque &callbacks, std::deque &varbindList, std::deque &outResponseList, SNMP_VERSION snmpVersion){
+bool handleSetRequestPDU(ValueCallback* const *callbacks, int callbacksCount, const VarBind* varbindList, int varbindCount, VarBind outResponseList[], int &outResponseCount, SNMP_VERSION snmpVersion){
SNMP_LOGD("handleSetRequestPDU\n");
- for(const VarBind& requestVarBind : varbindList){
- SNMP_LOGD("finding callback for OID: %s\n", requestVarBind.oid->string().c_str());
- ValueCallback* callback = ValueCallback::findCallback(callbacks, requestVarBind.oid.get(), false);
+ for(int vbIdx = 0; vbIdx < varbindCount; vbIdx++){
+ const VarBind& requestVarBind = varbindList[vbIdx];
+ SNMP_LOGD("finding callback for OID: %s\n", requestVarBind.oid->string());
+ ValueCallback* callback = ValueCallback::findCallback(callbacks, callbacksCount, requestVarBind.oid, false);
if(!callback){
SNMP_LOGD("Couldn't find callback\n");
- outResponseList.emplace_back(requestVarBind.oid, SNMP_ERROR_VERSION_CTRL_DEF(NOT_WRITABLE, snmpVersion, NO_SUCH_NAME));
+ appendResponseVarBind(outResponseList, outResponseCount, requestVarBind.oid->cloneRaw(), SNMP_ERROR_VERSION_CTRL_DEF(NOT_WRITABLE, snmpVersion, NO_SUCH_NAME));
continue;
}
- SNMP_LOGD("Callback found with OID: %s\n", callback->OID->string().c_str());
+ SNMP_LOGD("Callback found with OID: %s\n", callback->OID->string());
if(callback->type != requestVarBind.type){
SNMP_LOGD("Callback Type mismatch: %d\n", callback->type);
- outResponseList.emplace_back(requestVarBind.oid, SNMP_ERROR_VERSION_CTRL_DEF(WRONG_TYPE, snmpVersion, BAD_VALUE));
+ appendResponseVarBind(outResponseList, outResponseCount, requestVarBind.oid->cloneRaw(), SNMP_ERROR_VERSION_CTRL_DEF(WRONG_TYPE, snmpVersion, BAD_VALUE));
continue;
}
-
+
if(!callback->isSettable){
SNMP_LOGD("Cannot set this object\n");
- outResponseList.emplace_back(requestVarBind.oid, SNMP_ERROR_VERSION_CTRL(READ_ONLY, snmpVersion));
+ appendResponseVarBind(outResponseList, outResponseCount, requestVarBind.oid->cloneRaw(), SNMP_ERROR_VERSION_CTRL(READ_ONLY, snmpVersion));
continue;
}
- //NOTE: we could just use the same pointer as the reqwuest, but delete the value and add a new one. Will have to figure out what to do if it errors, do that later
- SNMP_ERROR_STATUS setError = ValueCallback::setValueForCallback(callback, requestVarBind.value);
+ std::shared_ptr valueView(requestVarBind.value, [](BER_CONTAINER*){});
+ SNMP_ERROR_STATUS setError = ValueCallback::setValueForCallback(callback, valueView);
if(setError != NO_ERROR){
SNMP_LOGD("Attempting to set Variable failed: %d\n", setError);
- outResponseList.emplace_back(callback->OID, SNMP_ERROR_VERSION_CTRL(setError, snmpVersion));
- continue;
+ appendResponseVarBind(outResponseList, outResponseCount, callback->OID, SNMP_ERROR_VERSION_CTRL(setError, snmpVersion));
+ continue;
}
auto value = ValueCallback::getValueForCallback(callback);
if(!value){
SNMP_LOGD("Couldn't get value for callback\n");
- outResponseList.emplace_back(callback->OID, SNMP_ERROR_VERSION_CTRL(GEN_ERR, snmpVersion));
- continue;
+ appendResponseVarBind(outResponseList, outResponseCount, callback->OID, SNMP_ERROR_VERSION_CTRL(GEN_ERR, snmpVersion));
+ continue;
}
- outResponseList.emplace_back(callback->OID, value);
+ appendResponseVarBind(outResponseList, outResponseCount, callback->OID, value);
}
- return true; // we didn't fail in our job
+ return true;
}
-bool handleGetBulkRequestPDU(std::deque &callbacks, std::deque &varbindList, std::deque &outResponseList, unsigned int nonRepeaters, unsigned int maxRepititions){
- // from https://tools.ietf.org/html/rfc1448#page-18
- SNMP_LOGD("handleGetBulkRequestPDU, nonRepeaters:%d, maxRepititions:%d, varbindSize:%ld\n", nonRepeaters, maxRepititions, varbindList.size());
- // nonRepeaters is MIN(nonRepeaters, varbindList.size()
- // repeaters is the extra of varbindList.size() - nonRepeaters) which get 'walked' maxRepititions times
+bool handleGetBulkRequestPDU(ValueCallback* const *callbacks, int callbacksCount, const VarBind* varbindList, int varbindCount, VarBind outResponseList[], int &outResponseCount, unsigned int nonRepeaters, unsigned int maxRepititions){
+ SNMP_LOGD("handleGetBulkRequestPDU, nonRepeaters:%d, maxRepititions:%d, varbindSize:%d\n", nonRepeaters, maxRepititions, varbindCount);
SNMP_LOGD("handling nonRepeaters\n");
if(nonRepeaters > 0){
- // handle GET normally, but mark endOfMibView if not found
- for(unsigned int i = 0; i < nonRepeaters && i < varbindList.size(); i++){
+ unsigned int bound = (nonRepeaters < (unsigned int)varbindCount) ? nonRepeaters : (unsigned int)varbindCount;
+ for(unsigned int i = 0; i < bound; i++){
const VarBind& requestVarBind = varbindList[i];
- ValueCallback* callback = ValueCallback::findCallback(callbacks, requestVarBind.oid.get(), true);
+ ValueCallback* callback = ValueCallback::findCallback(callbacks, callbacksCount, requestVarBind.oid, true);
if(!callback){
- outResponseList.emplace_back(requestVarBind, std::make_shared(ENDOFMIBVIEW));
+ appendResponseVarBind(outResponseList, outResponseCount, requestVarBind, asn_new(ENDOFMIBVIEW));
continue;
}
auto value = ValueCallback::getValueForCallback(callback);
if(!value){
SNMP_LOGD("Couldn't get value for callback\n");
- outResponseList.emplace_back(callback->OID, GEN_ERR);
- continue;
+ appendResponseVarBind(outResponseList, outResponseCount, callback->OID, GEN_ERR);
+ continue;
}
- outResponseList.emplace_back(requestVarBind, value);
+ appendResponseVarBind(outResponseList, outResponseCount, requestVarBind, value);
}
}
- if(varbindList.size() > nonRepeaters){
- // For each extra varbind, WALK that tree until maxRepititions or endOfMibView
+ if(varbindCount > (int)nonRepeaters){
SNMP_LOGD("handling repeaters\n");
- unsigned int repeatingVarBinds = varbindList.size() - nonRepeaters;
-
+ unsigned int repeatingVarBinds = varbindCount - nonRepeaters;
+
for(unsigned int i = 0; i < repeatingVarBinds; i++){
- // Store first varbind to get for each line
- auto oid = varbindList[i+nonRepeaters].oid;
- size_t foundAt = 0;
+ OIDType* oid = varbindList[i+nonRepeaters].oid->cloneRaw();
+ int foundAt = 0;
for(unsigned int j = 0; j < maxRepititions; j++){
- SNMP_LOGD("finding next callback for OID: %s\n", oid->string().c_str());
- ValueCallback* callback = ValueCallback::findCallback(callbacks, oid.get(), true, foundAt, &foundAt);
+ SNMP_LOGD("finding next callback for OID: %s\n", oid->string());
+ ValueCallback* callback = ValueCallback::findCallback(callbacks, callbacksCount, oid, true, foundAt, &foundAt);
if(!callback){
- // We're done, mark endOfMibView
- outResponseList.emplace_back(oid, std::make_shared(ENDOFMIBVIEW));
+ appendResponseVarBind(outResponseList, outResponseCount, oid, asn_new(ENDOFMIBVIEW));
+ oid = nullptr;
break;
}
@@ -137,17 +139,17 @@ bool handleGetBulkRequestPDU(std::deque &callbacks, std::dequeOID, GEN_ERR);
- break;
+ appendResponseVarBind(outResponseList, outResponseCount, callback->OID, GEN_ERR);
+ break;
}
-
- outResponseList.emplace_back(callback->OID, value);
- // set next oid to callback OID
- oid = callback->OID->cloneOID();
+ appendResponseVarBind(outResponseList, outResponseCount, callback->OID, value);
+
+ asn_delete(oid);
+ oid = callback->OID->cloneRaw();
}
- //SNMP_LOGD("Walked tree of %s, %d times", (*varbindList)[i+nonRepeaters]->oid->_value, j);
+ asn_delete(oid);
}
}
diff --git a/src/SNMPPacket.cpp b/src/SNMPPacket.cpp
index b9f886c..00970ec 100644
--- a/src/SNMPPacket.cpp
+++ b/src/SNMPPacket.cpp
@@ -32,18 +32,18 @@
}
SNMPPacket::~SNMPPacket(){
- delete this->packet;
+ asn_delete(this->packet);
}
SNMP_PACKET_PARSE_ERROR SNMPPacket::parsePacket(ComplexType *structure, enum SNMPParsingState state) {
- for(const auto& value : structure->values){
- if(state == DONE) break;
+ for(int n = 0; n < structure->valuesLen && state != DONE; n++){
+ BER_CONTAINER* value = structure->values[n];
switch(state) {
case SNMPVERSION:
ASSERT_ASN_STATE_TYPE(value, SNMPVERSION);
- this->snmpVersionPtr = std::static_pointer_cast(value);
+ this->snmpVersionPtr = std::shared_ptr(asn_new(static_cast(value)->_value));
this->snmpVersion = (SNMP_VERSION) this->snmpVersionPtr.get()->_value;
if (this->snmpVersion >= SNMP_VERSION_MAX) {
SNMP_LOGW("Invalid SNMP Version: %d\n", this->snmpVersion);
@@ -54,61 +54,75 @@ SNMP_PACKET_PARSE_ERROR SNMPPacket::parsePacket(ComplexType *structure, enum SNM
case COMMUNITY:
ASSERT_ASN_STATE_TYPE(value, COMMUNITY);
- this->communityStringPtr = std::static_pointer_cast(value);
- this->communityString = this->communityStringPtr.get()->_value;
+ {
+ OctetType* src = static_cast(value);
+ this->communityStringPtr = std::shared_ptr(asn_new(src->_value, src->_valueLen));
+ }
+ {
+ size_t len = this->communityStringPtr.get()->_valueLen;
+ if(len > SNMP_MAX_COMMUNITY_LEN) len = SNMP_MAX_COMMUNITY_LEN;
+ memcpy(this->communityString, this->communityStringPtr.get()->_value, len);
+ this->communityString[len] = 0;
+ }
state = PDU;
break;
case PDU:
ASSERT_ASN_PARSING_TYPE_RANGE(value, ASN_PDU_TYPE_MIN_VALUE, ASN_PDU_TYPE_MAX_VALUE)
this->packetPDUType = value->_type;
- return this->parsePacket(static_cast(value.get()), REQUESTID);
+ return this->parsePacket(static_cast(value), REQUESTID);
case REQUESTID:
ASSERT_ASN_STATE_TYPE(value, REQUESTID);
- this->requestIDPtr = std::static_pointer_cast(value);
+ this->requestIDPtr = std::shared_ptr(asn_new(static_cast(value)->_value));
this->requestID = this->requestIDPtr.get()->_value;
state = ERRORSTATUS;
break;
case ERRORSTATUS:
ASSERT_ASN_STATE_TYPE(value, ERRORSTATUS);
- this->errorStatus.errorStatus = (SNMP_ERROR_STATUS) static_cast(value.get())->_value;
+ this->errorStatus.errorStatus = (SNMP_ERROR_STATUS) static_cast(value)->_value;
state = ERRORID;
break;
case ERRORID:
ASSERT_ASN_STATE_TYPE(value, ERRORID);
- this->errorIndex.errorIndex = static_cast(value.get())->_value;
+ this->errorIndex.errorIndex = static_cast(value)->_value;
state = VARBINDS;
break;
case VARBINDS:
ASSERT_ASN_STATE_TYPE(value, VARBINDS);
// we have a varbind structure, lets dive into it.
- return this->parsePacket(static_cast(value.get()), VARBIND);
+ return this->parsePacket(static_cast(value), VARBIND);
case VARBIND:
{
ASSERT_ASN_STATE_TYPE(value, VARBIND);
// we are in a single varbind
- auto varbindValues = std::static_pointer_cast(value);
+ ComplexType* varbindValues = static_cast(value);
- if (varbindValues->values.size() != 2) {
- SNMP_LOGW("Expecting VARBIND TO CONTAIN 2 OBEJCTS; %lu\n",
- varbindValues ? varbindValues->values.size() : 0);
+ if (varbindValues->valuesLen != 2) {
+ SNMP_LOGW("Expecting VARBIND TO CONTAIN 2 OBEJCTS; %d\n",
+ varbindValues ? varbindValues->valuesLen : 0);
return SNMP_PARSE_ERROR_AT_STATE(VARBIND);
};
- auto vbOid = varbindValues->values[0];
+ BER_CONTAINER* vbOid = varbindValues->values[0];
ASSERT_ASN_TYPE_AT_STATE(vbOid, OID, VARBIND);
- auto vbValue = varbindValues->values[1];
- this->varbindList.emplace_back(
- std::static_pointer_cast(vbOid),
- vbValue
- );
+ BER_CONTAINER* vbValue = varbindValues->values[1];
+
+ /* Clone the OID so the VarBind holds an owning reference independent
+ * of the incoming ComplexType tree (which will be delete'd when the
+ * SNMPPacket object is reused or destroyed). For value we wrap with
+ * a no-op deleter: the caller's lifetime (packet processing + reply
+ * serialise) always happens before packet destruction. */
+ std::shared_ptr oidClone = static_cast(vbOid)->cloneOID();
+ std::shared_ptr valueView(vbValue, [](BER_CONTAINER*){});
+
+ this->emplace_back(oidClone, valueView);
}
break;
@@ -126,7 +140,7 @@ SNMP_PACKET_PARSE_ERROR SNMPPacket::parseFrom(unsigned char* buf, size_t max_len
return SNMP_PARSE_ERROR_MAGIC_BYTE;
}
- packet = new ComplexType(STRUCTURE);
+ packet = asn_new(STRUCTURE);
SNMP_BUFFER_PARSE_ERROR decodePacket = packet->fromBuffer(buf, max_len);
if(decodePacket <= 0){
@@ -146,47 +160,50 @@ int SNMPPacket::serialiseInto(uint8_t* buf, size_t max_len){
}
bool SNMPPacket::build(){
- // Delete the existing packet if we've built it before (generally only traps)
- delete this->packet;
+ asn_delete(this->packet);
+
+ ComplexType* root = asn_new(STRUCTURE);
+ root->_ownsChildren = true;
+ this->packet = root;
- this->packet = new ComplexType(STRUCTURE);
- // Try to reuse existing containers if we got em
if(this->snmpVersionPtr)
- this->packet->addValueToList(this->snmpVersionPtr);
+ root->addValueToListRaw(asn_new(this->snmpVersionPtr->_value));
else
- this->packet->addValueToList(std::make_shared(this->snmpVersion));
+ root->addValueToListRaw(asn_new(this->snmpVersion));
if(this->communityStringPtr)
- this->packet->addValueToList(this->communityStringPtr);
+ root->addValueToListRaw(asn_new(this->communityStringPtr->_value, this->communityStringPtr->_valueLen));
else
- this->packet->addValueToList(std::make_shared(this->communityString.c_str()));
+ root->addValueToListRaw(asn_new(this->communityString));
- auto snmpPDU = std::make_shared(this->packetPDUType);
+ ComplexType* snmpPDU = asn_new(this->packetPDUType);
+ snmpPDU->_ownsChildren = true;
if(this->requestIDPtr)
- snmpPDU->addValueToList(this->requestIDPtr);
+ snmpPDU->addValueToListRaw(asn_new(this->requestIDPtr->_value));
else
- snmpPDU->addValueToList(std::make_shared(this->requestID));
+ snmpPDU->addValueToListRaw(asn_new(this->requestID));
- snmpPDU->addValueToList(std::make_shared(this->errorStatus.errorStatus));
- snmpPDU->addValueToList(std::make_shared(this->errorIndex.errorIndex));
+ snmpPDU->addValueToListRaw(asn_new(this->errorStatus.errorStatus));
+ snmpPDU->addValueToListRaw(asn_new(this->errorIndex.errorIndex));
- // We need to do this dynamically incase we're building a trap, generateVarBindList is virtual
- auto varBindList = this->generateVarBindList();
+ ComplexType* varBindList = this->generateVarBindListRaw();
if(!varBindList) return false;
-
- snmpPDU->addValueToList(varBindList);
- this->packet->addValueToList(snmpPDU);
+ snmpPDU->addValueToListRaw(varBindList);
+
+ root->addValueToListRaw(snmpPDU);
return true;
}
-void SNMPPacket::setCommunityString(const std::string &CommunityString){
- // poison any cached containers we have
+void SNMPPacket::setCommunityString(const char *CommunityString){
this->communityStringPtr = nullptr;
- this->communityString = CommunityString;
+ size_t len = strlen(CommunityString);
+ if(len > SNMP_MAX_COMMUNITY_LEN) len = SNMP_MAX_COMMUNITY_LEN;
+ memcpy(this->communityString, CommunityString, len);
+ this->communityString[len] = 0;
}
void SNMPPacket::setRequestID(snmp_request_id_t RequestId){
@@ -208,21 +225,61 @@ void SNMPPacket::setVersion(SNMP_VERSION SnmpVersion){
this->snmpVersion = SnmpVersion;
}
-std::shared_ptr SNMPPacket::generateVarBindList(){
- SNMP_LOGD("generateVarBindList from SNMPPacket");
- // This is for normal packets where our response values have already been built, not traps
- auto varBindList = std::make_shared(STRUCTURE);
-
- for(const auto& varBindItem : varbindList){
- auto varBind = std::make_shared(STRUCTURE);
-
- varBind->addValueToList(varBindItem.oid);
- varBind->addValueToList(varBindItem.value);
+ComplexType* SNMPPacket::generateVarBindListRaw(){
+ SNMP_LOGD("generateVarBindListRaw from SNMPPacket");
+ ComplexType* list = asn_new(STRUCTURE);
+ list->_ownsChildren = true;
+
+ for(int vbIdx = 0; vbIdx < this->varbindCount; vbIdx++){
+ const VarBind& varBindItem = this->varbindList[vbIdx];
+ ComplexType* varBind = asn_new(STRUCTURE);
+ varBind->_ownsChildren = true;
+
+ varBind->addValueToListRaw(varBindItem.oid->cloneRaw());
+
+ BER_CONTAINER* src = varBindItem.value;
+ BER_CONTAINER* clonedValue = nullptr;
+ if(!src){
+ clonedValue = asn_new();
+ } else switch(src->_type){
+ case INTEGER: clonedValue = asn_new(static_cast(src)->_value); break;
+ case STRING:
+ {
+ OctetType* so = static_cast(src);
+ clonedValue = asn_new(so->_value, so->_valueLen);
+ } break;
+ case OID: clonedValue = static_cast(src)->cloneRaw(); break;
+ case NULLTYPE: clonedValue = asn_new(); break;
+ case NOSUCHOBJECT: clonedValue = asn_new(NOSUCHOBJECT); break;
+ case NOSUCHINSTANCE: clonedValue = asn_new(NOSUCHINSTANCE); break;
+ case ENDOFMIBVIEW: clonedValue = asn_new(ENDOFMIBVIEW); break;
+ case NETWORK_ADDRESS:
+ {
+ NetworkAddress* so = static_cast(src);
+ clonedValue = asn_new(so->_value);
+ } break;
+ case TIMESTAMP: clonedValue = asn_new(static_cast(src)->_value); break;
+ case COUNTER32: clonedValue = asn_new(static_cast(src)->_value); break;
+ case GAUGE32: clonedValue = asn_new(static_cast(src)->_value); break;
+ case COUNTER64: clonedValue = asn_new(static_cast(src)->_value); break;
+ case OPAQUE:
+ {
+ OpaqueType* so = static_cast(src);
+ clonedValue = asn_new(so->_value, so->_dataLength);
+ } break;
+ default:
+ clonedValue = asn_new(); break;
+ }
+ varBind->addValueToListRaw(clonedValue);
- varBindList->addValueToList(varBind);
+ list->addValueToListRaw(varBind);
}
- return varBindList;
+ return list;
+}
+
+std::shared_ptr SNMPPacket::generateVarBindList(){
+ return std::shared_ptr(generateVarBindListRaw());
}
snmp_request_id_t SNMPPacket::generate_request_id(){
diff --git a/src/SNMPParser.cpp b/src/SNMPParser.cpp
index bbb4eba..88b7ae1 100644
--- a/src/SNMPParser.cpp
+++ b/src/SNMPParser.cpp
@@ -1,21 +1,20 @@
#include "include/SNMPParser.h"
-#include
-static SNMP_PERMISSION getPermissionOfRequest(const SNMPPacket& request, const std::string& _community, const std::string& _readOnlyCommunity){
+static SNMP_PERMISSION getPermissionOfRequest(const SNMPPacket& request, const char* _community, const char* _readOnlyCommunity){
SNMP_PERMISSION requestPermission = SNMP_PERM_NONE;
- SNMP_LOGD("community string in packet: %s\n", request.communityString.c_str());
+ SNMP_LOGD("community string in packet: %s\n", request.communityString);
- if(!_readOnlyCommunity.empty() && _readOnlyCommunity == request.communityString) { // snmprequest->version != 1
+ if(_readOnlyCommunity[0] != 0 && strcmp(_readOnlyCommunity, request.communityString) == 0) {
requestPermission = SNMP_PERM_READ_ONLY;
}
- if(_community == request.communityString) { // snmprequest->version != 1
+ if(strcmp(_community, request.communityString) == 0) {
requestPermission = SNMP_PERM_READ_WRITE;
}
return requestPermission;
}
-SNMP_ERROR_RESPONSE handlePacket(uint8_t* buffer, int packetLength, int* responseLength, int max_packet_size, std::deque &callbacks, const std::string& _community, const std::string& _readOnlyCommunity, informCB informCallback, void* ctx){
+SNMP_ERROR_RESPONSE handlePacket(uint8_t* buffer, int packetLength, int* responseLength, int max_packet_size, ValueCallback* const *callbacks, int callbacksCount, const char* _community, const char* _readOnlyCommunity, informCB informCallback, void* ctx){
SNMPPacket request;
SNMP_PACKET_PARSE_ERROR parseResult = request.parseFrom(buffer, packetLength);
@@ -38,14 +37,14 @@ SNMP_ERROR_RESPONSE handlePacket(uint8_t* buffer, int packetLength, int* respons
SNMP_PERMISSION requestPermission = getPermissionOfRequest(request, _community, _readOnlyCommunity);
if(requestPermission == SNMP_PERM_NONE){
- SNMP_LOGW("Invalid communitystring provided: %s, no response to give\n", request.communityString.c_str());
+ SNMP_LOGW("Invalid communitystring provided: %s, no response to give\n", request.communityString);
return SNMP_REQUEST_INVALID_COMMUNITY;
}
-
- // this will take the required stuff from request - like requestID and community string etc
+
SNMPResponse response = SNMPResponse(request);
- std::deque outResponseList;
+ VarBind outResponseList[SNMP_MAX_VARBINDS];
+ int outResponseCount = 0;
bool pass = false;
SNMP_ERROR_RESPONSE handleStatus = SNMP_NO_ERROR;
@@ -54,7 +53,7 @@ SNMP_ERROR_RESPONSE handlePacket(uint8_t* buffer, int packetLength, int* respons
switch(request.packetPDUType){
case GetRequestPDU:
case GetNextRequestPDU:
- pass = handleGetRequestPDU(callbacks, request.varbindList, outResponseList, request.snmpVersion, request.packetPDUType == GetNextRequestPDU);
+ pass = handleGetRequestPDU(callbacks, callbacksCount, request.varbindList, request.varbindCount, outResponseList, outResponseCount, request.snmpVersion, request.packetPDUType == GetNextRequestPDU);
handleStatus = request.packetPDUType == GetRequestPDU ? SNMP_GET_OCCURRED : SNMP_GETNEXT_OCCURRED;
break;
case GetBulkRequestPDU:
@@ -63,7 +62,7 @@ SNMP_ERROR_RESPONSE handlePacket(uint8_t* buffer, int packetLength, int* respons
pass = false;
globalError = GEN_ERR;
} else {
- pass = handleGetBulkRequestPDU(callbacks, request.varbindList, outResponseList, request.errorStatus.nonRepeaters, request.errorIndex.maxRepititions);
+ pass = handleGetBulkRequestPDU(callbacks, callbacksCount, request.varbindList, request.varbindCount, outResponseList, outResponseCount, request.errorStatus.nonRepeaters, request.errorIndex.maxRepititions);
handleStatus = SNMP_GETBULK_OCCURRED;
}
break;
@@ -73,7 +72,7 @@ SNMP_ERROR_RESPONSE handlePacket(uint8_t* buffer, int packetLength, int* respons
pass = false;
globalError = NO_ACCESS;
} else {
- pass = handleSetRequestPDU(callbacks, request.varbindList, outResponseList, request.snmpVersion);
+ pass = handleSetRequestPDU(callbacks, callbacksCount, request.varbindList, request.varbindCount, outResponseList, outResponseCount, request.snmpVersion);
handleStatus = SNMP_SET_OCCURRED;
}
break;
@@ -85,7 +84,8 @@ SNMP_ERROR_RESPONSE handlePacket(uint8_t* buffer, int packetLength, int* respons
}
if(pass){
- for(const auto& item : outResponseList){
+ for(int idx = 0; idx < outResponseCount; idx++){
+ const VarBind& item = outResponseList[idx];
if(item.errorStatus != NO_ERROR){
response.addErrorResponse(item);
} else {
@@ -93,7 +93,6 @@ SNMP_ERROR_RESPONSE handlePacket(uint8_t* buffer, int packetLength, int* respons
}
}
} else {
- // Something went wrong, generic error response
SNMP_LOGD("Handled error when building request, error: %d, sending error PDU", globalError);
response.setGlobalError(globalError, 0, true);
handleStatus = SNMP_ERROR_PACKET_SENT;
diff --git a/src/SNMPResponse.cpp b/src/SNMPResponse.cpp
index 99d330a..91908c1 100644
--- a/src/SNMPResponse.cpp
+++ b/src/SNMPResponse.cpp
@@ -1,13 +1,13 @@
#include "include/SNMPResponse.h"
bool SNMPResponse::addResponse(const VarBind& response){
- this->varbindList.emplace_back(response);
+ this->emplace_back(response);
return true;
}
bool SNMPResponse::addErrorResponse(const VarBind& response){
- int index = this->varbindList.size() + 1;
- this->varbindList.emplace_back(response);
+ int index = this->size() + 1;
+ this->emplace_back(response);
if(response.errorStatus != NO_ERROR){
this->errorStatus.errorStatus = response.errorStatus;
diff --git a/src/SNMPTrap.cpp b/src/SNMPTrap.cpp
index 499ed69..5694bd2 100644
--- a/src/SNMPTrap.cpp
+++ b/src/SNMPTrap.cpp
@@ -1,82 +1,135 @@
#include "SNMPTrap.h"
#include "include/SNMPParser.h"
+#include "include/defs.h"
+
+OIDType SNMPTrap::s_timestampOID(RFC1213_OID_sysUpTime);
+OIDType SNMPTrap::s_snmpTrapOID(SNMPv2_SNMPTRAP_OID_0);
SNMPTrap::~SNMPTrap(){
- delete timestampOID;
- delete snmpTrapOID;
- delete packet;
+ asn_delete(packet);
}
bool SNMPTrap::build(){
- // Building V1 Traps
- delete packet;
+ asn_delete(packet);
if(!this->trapOID) return false;
- packet = new ComplexType(STRUCTURE);
-
- packet->addValueToList(std::make_shared((int)this->snmpVersion));
- packet->addValueToList(std::make_shared(this->communityString.c_str()));
- auto trapPDU = std::make_shared(TrapPDU);
-
- trapPDU->addValueToList(trapOID->cloneOID());
- trapPDU->addValueToList(std::make_shared(agentIP));
- trapPDU->addValueToList(std::make_shared(genericTrap));
- trapPDU->addValueToList(std::make_shared(specificTrap));
-
+ ComplexType* root = asn_new(STRUCTURE);
+ root->_ownsChildren = true;
+ packet = root;
+
+ root->addValueToListRaw(asn_new((int)this->snmpVersion));
+ root->addValueToListRaw(asn_new(this->communityString));
+
+ ComplexType* trapPDU = asn_new(TrapPDU);
+ trapPDU->_ownsChildren = true;
+
+ trapPDU->addValueToListRaw(trapOID->cloneRaw());
+ trapPDU->addValueToListRaw(asn_new(agentIP));
+ trapPDU->addValueToListRaw(asn_new(genericTrap));
+ trapPDU->addValueToListRaw(asn_new(specificTrap));
+
if(uptimeCallback){
- trapPDU->addValueToList(std::static_pointer_cast(ValueCallback::getValueForCallback(uptimeCallback)));
+ auto sp = std::static_pointer_cast(ValueCallback::getValueForCallback(uptimeCallback));
+ if(sp) trapPDU->addValueToListRaw(asn_new(sp->_value));
+ else trapPDU->addValueToListRaw(asn_new(0));
} else {
- trapPDU->addValueToList(std::make_shared(0));
+ trapPDU->addValueToListRaw(asn_new(0));
}
- auto ourVBList = this->generateVarBindList();
+ ComplexType* ourVBList = this->generateVarBindListRaw();
if(!ourVBList) return false;
-
- trapPDU->addValueToList(ourVBList);
- packet->addValueToList(trapPDU);
+
+ trapPDU->addValueToListRaw(ourVBList);
+ root->addValueToListRaw(trapPDU);
return true;
}
-std::shared_ptr SNMPTrap::generateVarBindList(){
- SNMP_LOGD("generateVarBindList from SNMPTrap");
- auto ourVBList = std::make_shared(STRUCTURE);
- // If we're an SNMPv2 Trap, our first two are timestamp and OIDType, v1 already has them included
+ComplexType* SNMPTrap::generateVarBindListRaw(){
+ SNMP_LOGD("generateVarBindListRaw from SNMPTrap");
+ ComplexType* ourVBList = asn_new(STRUCTURE);
+ ourVBList->_ownsChildren = true;
+
if(this->snmpVersion == SNMP_VERSION_2C){
if(!this->trapOID){
+ asn_delete(ourVBList);
return nullptr;
}
- // Timestamp
- auto timestampVarBind = std::make_shared(STRUCTURE);
- timestampVarBind->addValueToList(timestampOID->cloneOID());
+ ComplexType* timestampVarBind = asn_new(STRUCTURE);
+ timestampVarBind->_ownsChildren = true;
+ timestampVarBind->addValueToListRaw(timestampOID->cloneRaw());
if(uptimeCallback){
- timestampVarBind->addValueToList(std::static_pointer_cast(ValueCallback::getValueForCallback(uptimeCallback)));
+ auto sp = std::static_pointer_cast(ValueCallback::getValueForCallback(uptimeCallback));
+ if(sp) timestampVarBind->addValueToListRaw(asn_new(sp->_value));
+ else timestampVarBind->addValueToListRaw(asn_new(0));
} else {
- timestampVarBind->addValueToList(std::make_shared(0));
+ timestampVarBind->addValueToListRaw(asn_new(0));
}
- ourVBList->addValueToList(timestampVarBind);
+ ourVBList->addValueToListRaw(timestampVarBind);
- // OID
- auto oidVarBind = std::make_shared(STRUCTURE);
- oidVarBind->addValueToList(snmpTrapOID->cloneOID());
- oidVarBind->addValueToList(trapOID->cloneOID());
- ourVBList->addValueToList(oidVarBind);
+ ComplexType* oidVarBind = asn_new(STRUCTURE);
+ oidVarBind->_ownsChildren = true;
+ oidVarBind->addValueToListRaw(snmpTrapOID->cloneRaw());
+ oidVarBind->addValueToListRaw(trapOID->cloneRaw());
+ ourVBList->addValueToListRaw(oidVarBind);
}
- for(auto value : this->callbacks){
+ for(int i = 0; i < callbacksCount; i++){
+ ValueCallback* value = callbacks[i];
if(!value) continue;
- auto varBind = std::make_shared(STRUCTURE);
+ ComplexType* varBind = asn_new(STRUCTURE);
+ varBind->_ownsChildren = true;
+
+ varBind->addValueToListRaw(value->OID->cloneRaw());
- varBind->addValueToList(value->OID->cloneOID());
- varBind->addValueToList(ValueCallback::getValueForCallback(value));
+ auto valueSP = ValueCallback::getValueForCallback(value);
+ BER_CONTAINER* src = valueSP.get();
+ BER_CONTAINER* clonedValue = nullptr;
+ if(!src){
+ clonedValue = asn_new();
+ } else switch(src->_type){
+ case INTEGER: clonedValue = asn_new(static_cast(src)->_value); break;
+ case STRING:
+ {
+ OctetType* so = static_cast(src);
+ clonedValue = asn_new(so->_value, so->_valueLen);
+ } break;
+ case OID: clonedValue = static_cast(src)->cloneRaw(); break;
+ case NULLTYPE: clonedValue = asn_new(); break;
+ case NOSUCHOBJECT: clonedValue = asn_new(NOSUCHOBJECT); break;
+ case NOSUCHINSTANCE: clonedValue = asn_new(NOSUCHINSTANCE); break;
+ case ENDOFMIBVIEW: clonedValue = asn_new