Skip to content

feat: Partitions and Storage formats migration - #542

Draft
hhvrc wants to merge 10 commits into
developfrom
feat/storage-partition-migration
Draft

hhvrc wants to merge 10 commits into
developfrom
feat/storage-partition-migration

Conversation

@hhvrc

@hhvrc hhvrc commented Oct 27, 2026

Copy link
Copy Markdown
Contributor

No description provided.

hhvrc added 10 commits October 26, 2026 12:38
…board bootloader flash size

Replace the single 1.x table with partitions/ota_4mb.csv and ota_8mb.csv (2.x layouts: bigger
app slots, a dedicated cfg NVS partition, a flat www image of type 0x40 instead of littlefs
static0/config). The old table stays under partitions/legacy/ as the reference for fielded hubs
and as a migration test fixture.

A board's table follows from its flash size (scripts/utils/partitions.py), so board files no longer
name one and the table never splits a build variant. scripts/package_boards.py now writes each
board's partition-table.bin itself (a byte-identical reimplementation of gen_esp32part.py, MD5 entry
included, so CI needs no ESP-IDF for it) and stamps the board's flash size into the bootloader
header: the ROM and the second-stage bootloader size the flash from that header and would refuse
the 8 MB table behind a bootloader compiled at the variant's 4 MB.

gen_staticfs.py packs the frontend into the www format (scripts/utils/wwwimage.py: sorted
directory, 4-byte aligned bodies, truncated sha256 over the body) instead of a littlefs image,
dropping the littlefs-python dependency. One unpadded image serves every board and is checked to
fit the smallest www partition.
components/www parses the flat www image (scripts/utils/wwwimage.py writes it): a sorted directory of
4-byte aligned bodies behind a header with a truncated SHA-256 over everything after it. Every field
is checked before the image is used, so a bad image means "no portal assets", never a partially
trusted one. WwwPartition maps the type-0x40 partition once and serves by pointer.

components/partition_table parses the binary table at 0x8000 (MD5 entry included), tells the 1.x
layout from the 2.x ones, and checks a staged table against every invariant the over-the-air table
rewrite relies on: nvs and otadata unmoved, cfg and www present, geometry the bootloader accepts, a
slot for the running image, a live 1.x table, and a bootloader header that declares enough flash.

Both are pure C++ over byte spans with host suites; their fixtures are produced at configure time by
the same Python that writes the release artifacts, so the two implementations are checked against
each other.
ESP-IDF owns the component name partition_table: a project component of that name shadows it, and every
partition_table_get_partition_info() call in ESP-IDF's own CMake (app_update, esptool_py) fails to configure.
Client::Head reports whether a URL answers and its Content-Length without downloading it: the layout migration
checks that the 2.0.0 artifacts are reachable before it touches flash. The connection setup Download already
had (reuse the kept-alive handle, reconnect once if the server closed it) moves into openRequest so both share it.
A HEAD response has no body to drain, so the connection is dropped rather than kept alive afterwards.
The www host suite packs its fixtures at configure time with the same packer the build uses, so the C++ parser and
the Python writer are checked against each other.
The config component no longer owns any settings. It is a framework for keeping them in NVS:

- config/Nvs.h: typed access to one NVS namespace (tri-state reads, writes that skip unchanged values), plus
  copying and comparing whole namespaces.
- config/Section.h: Section<T>, one module's settings with a RAM cache, writers serialized across sections,
  saves of the changed keys only, and publication after the save. Every section links itself into an ordered
  registry at static initialization.
- config/Store.h: picks the store (the `cfg` partition, or the shared `nvs` on the 1.x layout), moves the
  shared copy into `cfg` key by key and verifies it, fills an empty store from the 1.x littlefs file or the
  defaults, loads every section (one unreadable section is reset alone), factory reset, and the whole-config
  JSON for jsonconfig, which every section parses before any of them saves.
- config/Wire.h: the HubConfig wire form for the local protocol's Ready message and rawconfig, assembled from
  the sections' own ToFlatbuffers, and HubConfig -> jsonconfig JSON, which is also how the 1.x config file is
  imported (with the 1.5.x flipped-default booleans resolved there, in one place).

Each module defines its own section, NVS keys and accessors:
  device_control: rf, estop      core: wifi, backend, otaUpdate      captive_portal: captivePortal
  serial_console: serialInput
otaUpdate lives in core, the lowest component that uses it (the gateway reports the step, the portal edits the
channel, ota runs updates above both). The accessors keep their names in the OpenShock::Config namespace, so
call sites change only their includes. RootConfig, ConfigBase<T> and the sections' FromFlatbuffers are gone.

Chipset.h includes hal/gpio_types.h instead of driver/gpio.h: it only needs the types, and the driver header is
what made the config host suite depend on ESP-IDF's driver mocks.

The config host suite compiles the framework with every module's section and runs them on ESP-IDF's real NVS
over the linux flash emulation: 62 cases, among them the 1.x import of the Arduino fixtures, the move from the
shared nvs, the registry order, saves that write only what changed, and a 40-credential config the 1.x
partition could never hold.
A 1.x hub installs the 1.9.x pivot with its own OTA code, which flashes the staticfs artifact into its littlefs
static0 partition and aborts the update unless that partition then mounts. A flat www image as 1.9.x's staticfs
would make the pivot uninstallable from the field.

gen_staticfs.py --legacy-output packs the same assets as the 1.x littlefs image (/www/<path>.gz, sized for
static0), the build-staticfs action builds both images, and the publish step picks the staticfs file by the
release's major version: staticfs.bin for 1.x, www.bin from 2.0.0 on. littlefs-python comes back as a build
dependency for that output only; both go away with release/1.9.
The captive portal maps the www partition once and serves each asset straight from the flash mapping, with its
gzip flag, a long cache lifetime for content-addressed assets, and an ETag from the image's body hash. On the 1.x
layout, which has no www partition, it falls back to the littlefs static0 image as before.

The OTA update flashes staticfs into whichever of the two the table has and checks it in that format, skips the
flash when the www image on flash already has the release's hash, and parses the optional partitions artifact
(this board's partition table) for the layout migration. TryStartMigrationUpdate lets the hub install a version
on its own initiative: the backend-management and manual-approval settings do not hold it back, disabling OTA
updates still does.
components/migration is the 1.9.x pivot's boot chain (docs/STORAGE.md). On the 1.x table, once online, it checks
that the target release (CONFIG_OPENSHOCK_MIGRATION_TARGET_VERSION, 2.0.0) carries this board's partition table
and that its app and www image answer, downloads the table into RAM, checks its SHA-256 and every invariant in
partition_layout, and on any failure stays on the 1.x layout and retries later. Running from the 1.x app1 it first
hops to app0 (4 MB) or copies itself to where the new table puts app1 (8 MB). It writes the target's www image
where the new table puts www, holds config writes, closes NVS, writes the table sector through the OTA path,
reads it back and restarts. On the 2.x table it drops the WiFi driver's stale store and any 1.x fragment in the
moved slot, then installs the target version; if that rolls back, the chain stops.

A 2.x build only reports the layout. sysinfo prints the layout, the bootloader header's flash size and the config
store. docs/STORAGE.md describes the tables, the config store and its sections, the www image, the migration and
the serial recovery for the one unrecoverable window (the table sector's erase-to-write).
The estop section's namespace keeps a latched (u8) key beside the
section's settings. The E-Stop task writes it on every activation and
every clear, after the change is already in effect, and never waits for
another config writer. Init reads it before anything can transmit and
keeps a latched E-Stop active until it is cleared with the button.

A missing key reads as not latched and a read error as latched. A latch
on a hub whose E-Stop is disabled is dropped, since nothing could clear
it. A factory reset clears it.
@hhvrc hhvrc self-assigned this Oct 27, 2026
@github-project-automation github-project-automation Bot moved this to Todo in Roadmap Oct 27, 2026
@coderabbitai

coderabbitai Bot commented Oct 27, 2026

Copy link
Copy Markdown
Contributor

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

Copy link
Copy Markdown
Contributor

Cpp-Linter Report ⚠️

Some files did not pass the configured checks!

clang-format (v21.1.8) reports: 3 file(s) not formatted
  • components/www/include/www/WwwImage.h
  • components/www/include/www/WwwPartition.h
  • components/www/host_test/main/test_www_image.cpp

Have any feedback or feature suggestions? Share it here.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Todo

Development

Successfully merging this pull request may close these issues.

1 participant