Skip to content

PR-06: NVS configuration and LittleFS data storage#9

Merged
cryptotomte merged 26 commits into
mainfrom
003-nvs-littlefs-storage
Jun 13, 2026
Merged

PR-06: NVS configuration and LittleFS data storage#9
cryptotomte merged 26 commits into
mainfrom
003-nvs-littlefs-storage

Conversation

@cryptotomte

Copy link
Copy Markdown
Owner

PR-06: NVS configuration + LittleFS data storage (Phase 2)

Ports the storage layer to ESP-IDF as two redesigned, host-includable contracts.
Spec/plan/contracts: specs/003-nvs-littlefs-storage/. Implements FR-013 (NVS
config + factory defaults); enables FR8 (littlefs assets) and the history part of
FR7. No migration of Arduino-era data (clean start).

What's in it

  • IConfigStoreNvsConfigStore: typed config in the nvs partition
    (namespace wscfg, one entry per item), compiled-in factory defaults applied on
    missing/erased/out-of-range entries, explicit factory reset. Replaces the legacy
    string-keyed JSON blob.
  • IDataStorageLittleFsDataStorage: bounded sensor history (per-metric
    8-byte-record chunk files, 8 KiB chunks, 10-chunk ring eviction, ≥30-day
    retention, 10-metric cap) and a rotating event log (two 16 KiB files, newest
    always retained), plus filesystem usage stats. POSIX stdio with an injectable
    base path — runs on the host; only StorageMount (mount-or-format, usage) is
    esp32-only and excluded from the linux build.
  • LockedConfigStore / LockedDataStorage: mutex decorators for cross-task
    access (FR-013), mirroring the PR-02 LockedWaterPump pattern.
  • Boot wiring in app_main (pumps-OFF-first invariant preserved; all storage
    init non-fatal) and config/storage diagnostic-console commands for HIL.
  • Header-only mocks for both interfaces; littlefs partition image built into the
    flash (build/storage.bin), verified in CI.

Decisions (clarified 2026-06-11)

≥30-day history retention; sensorReadInterval/dataLogInterval are settable;
reservoir flags deferred to PR-05. Deliberate divergences from the Arduino storage
layer are recorded in docs/parity-checklist.md §6.

Verification

  • Both board targets (rev1_devkit + rev2) build green; storage.bin (960 KiB)
    produced on each.
  • Host suite green: 50 tests, 0 failures on the IDF linux target (real NVS +
    POSIX file layer).
  • Reviewed by code-reviewer, silent-failure-hunter, pr-test-analyzer,
    type-design-analyzer (CP3). One real bug fixed (storeEvent torn-tail
    stat-failure), two test gaps closed, three contract-doc clarifications added.
  • Safety invariant verified intact: pumps forced OFF first; every storage failure
    is non-fatal and never touches the pump loop.

Remaining for the bench (not blocking merge)

HIL checklist in specs/003-nvs-littlefs-storage/quickstart.md — config
persistence across power cycle, factory reset, corruption recovery — run on the
rev1 rig.

🤖 Generated with Claude Code

cryptotomte and others added 26 commits June 11, 2026 13:08
…rtifacts

- Feature spec with 2026-06-11 clarifications (>=30-day retention,
  settable interval items, reservoir flags deferred to PR-05)
- Plan with verified research D1-D10 (littlefs image build, NVS on
  linux target, atomicity model), data model, contracts, quickstart
- 36 tasks across 8 phases; cross-artifact analysis findings fixed
  (FR-013 Locked* decorators, 10-metric budget cap, linux-target
  littlefs exclusion, sdkconfig overlay names)

Spec: 003-nvs-littlefs-storage
PR-02 provides the interfaces component and the host test app this
feature extends (T001, research D10).

# Conflicts:
#	.specify/feature.json
Phase 1+2 of the storage feature: storage component skeleton with the
linux-target guard for the upcoming littlefs/StorageMount code, committed
littlefs seed directory, and the host test app extended for real NVS on
the linux preview target (custom partition table with an nvs partition,
nvs_flash REQUIRES). The Unity runner is split into per-suite
run_*_tests() functions in a shared test_main.cpp; the config-store and
data-storage suites are registered empty.

Tasks: T002, T003, T004, T005
Spec: 003-nvs-littlefs-storage

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
User story 1 (P1 MVP) of the storage feature: typed, validated
configuration in NVS with compiled-in factory defaults.

- IConfigStore interface header (host-includable, no IDF includes) with
  the defaults/ranges from data-model.md as the single source of truth
- NvsConfigStore: namespace wscfg, one NVS entry per item, floats as u32
  bit patterns, per-operation RAII handles (factoryReset erases the
  partition, so no handle is ever held), getters shadow missing or
  out-of-range stored values with defaults, setters reject out-of-range
  input, factory reset via nvs_flash_erase_partition + nvs_flash_init,
  credential values never logged
- Header-only MockConfigStore holding the same contract invariants, with
  injectable raw stored state and write-failure simulation for later PRs
- Host suite against the real linux-target NVS: defaults on erased NVS,
  round-trip + persistence across re-init, rejection at every documented
  bound, stored-value shadowing, factory reset, credential set/clear and
  a never-logged capture test (stdout/stderr redirected during the
  credential operations)

Tasks: T006, T007, T008, T009, T010, T011, T012, T013 (T014 run deferred
to the main session)
Spec: 003-nvs-littlefs-storage

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Verified in the pinned espressif/idf:v6.0.1 container on the linux
preview target. Docker cannot mount the OneDrive tree on this machine;
verification runs from an rsync copy under /tmp.

Spec: 003-nvs-littlefs-storage
Sensor history, rotating event log and storage statistics interface
per contracts/IDataStorage.md. Contract-level bounds (kMaxMetrics,
kEventDetailMaxLen, event categories) live here as the single source
of truth for implementations, mocks and tests.

Task: T015
Spec: 003-nvs-littlefs-storage
In-memory IDataStorage holding the same contract invariants as the
real store (FR-012): inclusive chronological queries, distinct-metric
cap, detail truncation, newest-first events, internal bounding.
Rejects metric names that would be unsafe directory names (empty,
'/', '..') to stay conformant with LittleFsDataStorage.

Task: T016
Spec: 003-nvs-littlefs-storage
POSIX-stdio data storage skeleton with injectable base path and
stats provider (no esp_littlefs/IDF includes, builds on the linux
preview target). History methods stubbed for T021, event methods
for T024. Registered in the storage component for both targets.

Task: T017
Spec: 003-nvs-littlefs-storage
Inclusive chronological range queries, empty-result semantics
(no data / unknown metric / t0>t1, FR-009), unsafe-metric-name
rejection, and MockDataStorage held to the same contract (FR-012).
Per-test POSIX temp dirs with RAII cleanup; written against the
T017 stub, red until T021.

Task: T018
Spec: 003-nvs-littlefs-storage
Chunk sealing at 1024 records, ring eviction at the 11th chunk,
30-day retention at the 5-min default interval (8640 records, no
eviction, FR-010), SC-004 endurance at 10x the per-metric bound,
and 11th-distinct-metric rejection. Endurance appends assert on an
aggregate failure count to keep the loop fast.

Task: T019
Spec: 003-nvs-littlefs-storage
A partial trailing record (size % 8 != 0, simulated power loss) is
logically truncated on read with earlier records intact, and the
next append repairs the tail so the chunk stays 8-byte aligned
(research D5, contract invariant 2).

Task: T020
Spec: 003-nvs-littlefs-storage
History per data-model.md: /hist/<metric>/<first_epoch>.dat chunks
of 8-byte little-endian records, sealed at 8 KiB, ring-evicted at
10 chunks per metric, 10-distinct-metric budget guard. Durability
via fflush+fsync per append; torn tails logically truncated on read
and repaired (truncate to valid prefix) before the next append.
Active chunk is derived from the files, so restarts need no
recovery step. Event methods remain T024 stubs.

Verified locally: full US2 suite (12 tests) green against a
functional Unity stand-in; linux-target run deferred to T022.

Task: T021
Spec: 003-nvs-littlefs-storage
Framed-record round-trip, newest-first retrieval with maxCount,
truncate-and-switch rotation (oldest half dropped, newest always
retained), burst within the 32 KiB budget, torn-tail marker/length
detection with repair-on-append, unknown-category passthrough,
>120-byte detail truncated-not-rejected, restart active-file
detection, and mock event-bound conformance. Written first: the
suite fails against the T024 stubs.

Task: T023
Spec: 003-nvs-littlefs-storage

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Rotating two-file event log per data-model.md: /events/0.log +
1.log, 0xE7-framed records {marker, uint32 LE epoch, uint8
category, uint8 detail_len, detail}, 16 KiB per-file cap with
truncate-and-switch rotation (oldest half dropped, newest always
retained), 120-byte detail truncation, fflush+fsync per append,
torn tails skipped on read and repaired before append.

The active file is derived statelessly after restart: the file
whose last valid record carries the newest epoch (ties broken
toward the smaller file; both empty -> 0). getEvents returns
newest-first across both files, bounded by maxCount.

Task: T024
Spec: 003-nvs-littlefs-storage

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Header-only mutex-serializing IConfigStore decorator (FR-013),
mirroring PR-02's LockedWaterPump pattern exactly: composition over
an IConfigStore reference, std::lock_guard around every interface
call, host-testable pure C++ (<mutex> via pthread on both ESP-IDF
and the linux preview target). Base implementations stay
unsynchronized per research.md D9.

Task: T026
Spec: 003-nvs-littlefs-storage

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Header-only mutex-serializing IDataStorage decorator (FR-013),
same LockedWaterPump pattern: composition over an IDataStorage
reference, std::lock_guard around every interface call. Serializes
the stateless-on-disk append paths (active chunk/event-file
derivation, rotation) that would interleave under concurrent task
access. Host-testable pure C++ per research.md D9.

Task: T027
Spec: 003-nvs-littlefs-storage

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Per the mechanism PR-02's LockedWaterPump test established: every
interface method exercised through the wrapper against the
instrumented mock (call counts prove delegation; accepted writes,
validation rejections, truncation, newest-first retrieval, stats
passthrough and simulated persistence failures all behave
identically behind the mutex), plus a real-store spot check each
(LockedConfigStore over NvsConfigStore on linux-target NVS,
LockedDataStorage over LittleFsDataStorage on a temp dir).

Task: T028
Spec: 003-nvs-littlefs-storage

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
esp_vfs_littlefs_register with partition_label="storage",
base_path="/storage" and format_if_mount_failed (research D2 —
legacy LittleFS.begin(true) parity: corrupted FS reformats, never
bricks), plus the esp_littlefs_info stats provider that boot
wiring injects into LittleFsDataStorage (FR-007/FR-008). Built
only in the non-linux CMake branch (esp_littlefs has no linux
port, research D4); littlefs is a PRIV_REQUIRES on the managed
joltwallet__littlefs component already pinned in
main/idf_component.yml — no manifest change, dependencies.lock
stays valid.

Task: T029
Spec: 003-nvs-littlefs-storage

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
littlefs_create_partition_image(storage ../storage_image
FLASH_IN_PROJECT) in main/CMakeLists.txt per research D1 — the
canonical placement from the joltwallet/littlefs example (a
component CMakeLists after idf_component_register; the relative
seed path resolves against firmware/main, landing on the
committed firmware/storage_image). Registers an ALL target:
build/storage.bin is produced on every build and attached to
idf.py flash, giving HIL a deterministic fresh-flash filesystem.

Task: T030
Spec: 003-nvs-littlefs-storage

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
NVS init with the standard ESP_ERR_NVS_NO_FREE_PAGES /
NEW_VERSION_FOUND erase-and-retry recovery, StorageMount
mount-or-format of /storage, and a one-line usage log (parity:
storage usage in the serial status block, FR-008). Store
instances are function-local statics constructed strictly after
pumps_force_off() — the pumps-OFF-first invariant stays the first
action in app_main — and wrapped in the Locked* decorators so
every later consumer (console REPL task, controllers) goes
through the mutex (FR-013). Storage failures are logged and
non-fatal: config falls back to compiled-in defaults and the pump
safety loop never depends on storage. ESP_LOG only, no business
logic in main.

Task: T031
Spec: 003-nvs-littlefs-storage

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… tests, 0 failures)

Spec: 003-nvs-littlefs-storage
Define diag_console_register_storage() and register the config and
storage commands in diag_console_start(), wiring the LockedConfigStore
and LockedDataStorage instances from app_main into the REPL. Completes
the HIL verification path for config persistence, factory reset and
data storage on the rig.

Task: T032
Spec: 003-nvs-littlefs-storage

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- CI verify-binaries step asserts build/storage.bin exists (T033)
- parity-checklist §6: deliberate divergences for the ESP-IDF storage
  port — NVS config, empty-SSID unconfigured state, bounded history,
  new event log, interface split, deferred reservoir flags (T034)
- firmware/CLAUDE.md: storage component, layout, console commands (T035)

Spec: 003-nvs-littlefs-storage
rev1_devkit + rev2 both produce wateringsystem.bin and the 960 KiB
build/storage.bin (FLASH_IN_PROJECT); host suite green (44 tests).
All PR-06 tasks complete.

Spec: 003-nvs-littlefs-storage
…gaps, contract docs

- F1: storeEvent no longer appends when stat() fails on an event file that
  holds valid bytes (would corrupt the frame boundary while returning true);
  an absent not-yet-created file still appends as before.
- F2: add getStorageStats() provider-path test (no provider -> {0,0}; fake
  provider -> injected total/used).
- F3: add non-monotonic-epoch chunk-bump test (sealed chunk + earlier-epoch
  append: no name collision, earlier record retrievable).
- D1: document that NvsConfigStore.factoryReset() erases the entire default
  NVS partition, not just the wscfg namespace.
- D2: note getEvents() newest-first ordering assumes monotonic epochs.
- D3: note LockedConfigStore/LockedDataStorage give per-call atomicity only.

Spec: 003-nvs-littlefs-storage

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@cryptotomte cryptotomte merged commit 5b53a75 into main Jun 13, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant