Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
239153c
chore: remove 1.x support and the one-time storage migration
hhvrc Nov 1, 2026
8ec6d37
style: format the dependency graph test
hhvrc Nov 1, 2026
1f8c39a
feat: remove the FlatBuffers form of the config
hhvrc Nov 1, 2026
8790b49
fix: include FreeRTOS.h first in WSLocal.cpp
hhvrc Nov 1, 2026
d1b8325
style: apply clang-format to the config headers and store
hhvrc Nov 1, 2026
96d970b
chore: pin the schemas submodule to develop
hhvrc Nov 1, 2026
11b0793
chore: track master for the schemas submodule
hhvrc Nov 1, 2026
732c40f
refactor: stop filling the config store on first boot
hhvrc Nov 1, 2026
29619a9
fix: move hubs off the 1.x CDN domain for OTA updates
hhvrc Nov 1, 2026
f3bd23e
refactor: delete TinyVec and port Base64 to std::vector
hhvrc Nov 1, 2026
58b7d54
refactor: drop the littlefs branches from the static file server
hhvrc Nov 1, 2026
37eccfd
refactor: fold hwutil into ota and rename its filesystem code to www
hhvrc Nov 1, 2026
845a485
refactor: give the www image one size limit and one partition table s…
hhvrc Nov 1, 2026
1a132f2
refactor: finish removing the schema from the config model
hhvrc Nov 1, 2026
983eb14
chore: remove a dead constant and correct comments left by the removal
hhvrc Nov 1, 2026
247ea97
fix: move hubs off fw.openshock.org for OTA updates too
hhvrc Nov 1, 2026
3f78f60
style: format OtaUpdateConfig.cpp with clang-format 21
hhvrc Nov 1, 2026
6c58f76
chore: pin the change files this branch touches to their originating PRs
hhvrc Nov 1, 2026
7290bd1
fix: leave the pr field off the change files this branch adds
hhvrc Nov 1, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .changes/board-descriptor.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
---
kind: changed
pr: 530
---
Stamp a board descriptor into the firmware image instead of compiling each board in

Expand All @@ -8,7 +9,7 @@ Firmware images now carry a small board descriptor: which board they are for, wh

- Add a 256-byte board descriptor at a fixed offset of the app image, written by `scripts/stamp_board.py` after linking and validated on the device with a CRC
- Read pins, LED flags, pin bypasses and the OTA board name from the descriptor at startup; the compiled image describes no board and has no pins until it is stamped
- Refuse an OTA download whose descriptor names a different board before it is made bootable, and roll back on first boot if the previous slot disagrees
- Refuse an OTA download whose descriptor is missing or names a different board before it is made bootable, and roll back on first boot if the previous slot disagrees
- Boot into the captive portal with no pins, and say so on the serial console, when an image was never stamped or its descriptor is corrupt
- Validate the descriptor at boot: chip matches the build, every pin is usable on the chip, no two features share a pin; an unusable LED pin disables that LED instead of failing startup
- Move per-board pin bypass lists out of `Chipset.h` into an `OPENSHOCK_GPIO_BYPASS` line in each board fragment
Expand Down
15 changes: 6 additions & 9 deletions .changes/config-nvs.md
Original file line number Diff line number Diff line change
@@ -1,26 +1,23 @@
---
kind: changed
pr: 548
---
Keep the configuration in NVS instead of a littlefs file

## Release Note
Settings are now stored in NVS

The hub keeps its settings in the chip's NVS key-value store instead of a single config file, and saves each setting on its own when it changes. Your WiFi networks, account link and other settings from 1.x are imported automatically on the first boot after updating.
The hub keeps its settings in the chip's NVS key-value store instead of a single config file, and saves each setting on its own when it changes. Hubs on 1.x get here through the migration firmware, which carries your WiFi networks, account link and other settings over.

- Each module owns its section of the config (`config/Section.h`); every section type meets the `SectionValue` concept, with its defaults defined in one place and its validation run on every value the section takes in
- On the 2.x partition table the store is the `cfg` partition (the 1.x littlefs `config` partition retyped in place); on the 1.x table it lives in the shared `nvs` partition until the layout is migrated, and then moves into `cfg`
- An empty store is filled once from the 1.x config file, which is never modified, otherwise from the defaults
- The store is the `cfg` partition; an empty one reads as the defaults, and a setting is only written once it changes
- WiFi credentials are stored one per key; the WiFi driver's own NVS store is compiled out
- A section that can't be read runs on its defaults without erasing what is stored, and the save after a failed one rewrites every key; a single WiFi credential that doesn't read is dropped on its own, so the rest still load
- The store is marked filled only once every section reads back from flash as written; a first fill cut short is redone on the next boot
- A store written by newer firmware is loaded instead of reset, and a `cfg` partition in a newer NVS format is never erased
- A section whose NVS namespace fails to open is a failed load, not a load of the defaults: the defaults are only taken when the namespace was never written. The store likewise never fills or cleans up when its own marker cannot be read
- A `cfg` partition in a newer NVS format is never erased
- A section whose NVS namespace fails to open is a failed load, not a load of the defaults: the defaults are only taken when the namespace was never written
- A boolean field of the wrong type in a `jsonconfig set` document rejects the document instead of storing the default, in every section
- The config accessors return the value itself (a section always has one); the WiFi section hands a network saved without an id the lowest free one on every path, so `networks` over serial no longer allocates ids itself
- An OTA check interval of zero and a hostname esp_netif would refuse become their defaults on every path (NVS, JSON and the setters), not only through the captive portal
- `Config::MigrateToCfgPartition` retypes the `config` partition as `cfg`; nothing calls it yet

## Notices
- warning: **1.x firmware does not see settings changed after this update.** Downgrading to 1.x brings back the settings the hub had before updating, as long as the partition layout has not been migrated yet. Re-enter any WiFi networks or pairing you changed since.
- warning: Until the partition layout is migrated the settings live in the chip's shared NVS partition. If that partition has to be erased (it fills up, or a newer firmware wrote it in a format this one cannot read), the hub imports the 1.x settings again and anything changed since the update is lost.
- important: **Hubs on 1.x must install the migration firmware first.** This release only runs on the new partition table, so a 1.x hub cannot update to it directly. Install the migration firmware, then update as usual.
9 changes: 9 additions & 0 deletions .changes/ota-repository-domain.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
kind: changed
---
Move hubs from the 1.x firmware CDN domains to the repository server for OTA updates

## Release Note
Hubs that stored a 1.x update domain now look for updates on the repository server

The 1.x domains `firmware.openshock.org` and `fw.openshock.org` serve flat files for 1.x hubs and have no repository API, so a hub that kept one could not update again. A hub storing either now uses `repo.openshock.dev`, which is also the new default. A custom domain is left as it is.
5 changes: 3 additions & 2 deletions .changes/plain-build-partition-table.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
---
kind: fixed
pr: 536
---
Build without a board fragment with the 4 MB OTA partition table, and skip staticfs cleanly without one
Build without a board fragment with the 4 MB OTA partition table

## Release Note
A build without a board fragment (the VS Code ESP-IDF extension, a plain `idf.py build`) fell back to ESP-IDF's stock 2 MB flash and 1 MB single-app partition table. The app overflowed it, and the staticfs target then tried to size an image from a `static0` partition the table did not have, because its guard compared an undefined CMake variable name to the empty string. `sdkconfig.defaults` now carries the 4 MB flash and the `partitions/ota_4mb.csv` table every board already uses, and a table without `static0` skips the staticfs target with the intended warning.
A build without a board fragment (the VS Code ESP-IDF extension, a plain `idf.py build`) fell back to ESP-IDF's stock 2 MB flash and 1 MB single-app partition table. The app overflowed it, and the staticfs target then tried to size an image from a partition the table did not have, because its guard compared an undefined CMake variable name to the empty string. `sdkconfig.defaults` now carries the 4 MB flash and the `partitions/ota_4mb.csv` table every board already uses, and a table without that partition now stops the build with a clear error.
12 changes: 12 additions & 0 deletions .changes/remove-hubconfig.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
kind: removed
---
Remove the FlatBuffers form of the config and the rawconfig serial command

## Release Note
The rawconfig serial command is gone

Use `jsonconfig` to read or set the whole config over serial.

- The HubConfig schema, `Config::Wire` and every section's `ToFlatbuffers` are removed
- The portal's Ready message carries only what it shows: the RF TX pin, the E-Stop state, the saved SSIDs and the OTA settings
16 changes: 0 additions & 16 deletions .changes/storage-migration.md

This file was deleted.

12 changes: 5 additions & 7 deletions .changes/www-image.md
Original file line number Diff line number Diff line change
@@ -1,15 +1,13 @@
---
kind: changed
pr: 549
---
Serve the setup portal from a flat www image where the partition table has a www partition
Serve the setup portal from a flat www image

## Release Note
Setup portal assets get their own www partition

The firmware's partition table replaces the 1.x littlefs `static0` partition with a `www` partition (type 0x40, same offset and size) that holds the setup portal's assets as one flat, read-only image. The firmware maps the image once and serves it by pointer, checked against a SHA-256 before anything is served. The image is identical for every board, so an update skips re-flashing it when the one on flash already matches.
The firmware's partition table has a `www` partition (type 0x40) that holds the setup portal's assets as one flat, read-only image. The firmware maps the image once and serves it by pointer, checked against a SHA-256 before anything is served. The image is identical for every board, so an update skips re-flashing it when the one on flash already matches.

Hubs in the field keep their 1.x partition table for now: this release installs over the air like any other, keeps serving the portal from the littlefs `static0`, and keeps updating it the way 1.x firmware does. The in-place move onto the `www` partition ships with the release that runs the storage migrations.
- `partitions/ota_4mb.csv` turns `static0` into `www`; the 1.x table is kept as `partitions/legacy/ota_4mb_v1.csv`.
- `scripts/gen_staticfs.py` packs the frontend build into `www.bin` (`scripts/utils/wwwimage.py`) and, with `--legacy-output`, into the 1.x littlefs `staticfs.bin`. A 1.x-numbered release publishes the littlefs image as its `staticfs` artifact, since 1.x firmware's own update code installs it by mounting it; from 2.0.0 the artifact is `www.bin`.
- The over-the-air update flashes whichever of `www` or `static0` the running table has and verifies it in that partition's format.
- `components/www` adds the in-place migration (`Www::MigrateToWwwPartition`) that retypes `static0` as `www` and downloads the image over it; nothing calls it yet.
- `scripts/gen_staticfs.py` packs the frontend build into `www.bin` (`scripts/utils/wwwimage.py`), published as the release's `staticfs` artifact.
- The over-the-air update flashes the `www` partition and verifies the image there, and refuses a download that is not a www image before erasing anything.
12 changes: 4 additions & 8 deletions .github/actions/build-staticfs/action.yml
Original file line number Diff line number Diff line change
@@ -1,29 +1,25 @@
name: build-staticfs
description: Pack the frontend build into the board-independent www asset image, and the 1.x littlefs staticfs image
description: Pack the frontend build into the board-independent www asset image

runs:
using: composite
steps:
- uses: ./.github/actions/install-python-deps

# One flat image serves every board; checked to fit the www partition of partitions/ota_4mb.csv. The littlefs
# staticfs.bin is what a 1.x hub's OTA installs, published for 1.x-numbered releases only.
- name: Build asset images
# One flat image serves every board; checked to fit the www partition of partitions/ota_4mb.csv.
- name: Build asset image
shell: bash
run: |
python scripts/gen_staticfs.py \
--frontend-dir frontend \
--output www.bin \
--legacy-output staticfs.bin \
--partition-csv partitions/ota_4mb.csv \
--skip-frontend-build

- name: Upload asset image artifact
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: firmware_staticfs
path: |
www.bin
staticfs.bin
path: www.bin
retention-days: 7
if-no-files-found: error
2 changes: 1 addition & 1 deletion .github/actions/repo-server-publish/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ inputs:
description: 'init opens the release, publish uploads and promotes it, abort closes one the build never reached'
version:
required: false
description: 'Firmware version, strict semver (init; publish, where it picks the staticfs image)'
description: 'Firmware version, strict semver (init)'
channel:
required: false
description: 'Release channel: stable, beta or develop (init)'
Expand Down
1 change: 0 additions & 1 deletion .github/workflows/ci-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -295,7 +295,6 @@ jobs:
token: ${{ steps.token.outputs.token }}
release-id: ${{ needs.stage.outputs.release-id }}
boards: ${{ needs.plan.outputs.board-list }}
version: ${{ needs.plan.outputs.version }}
mode: publish

# Hands the staged version back when the run fails, is cancelled or never publishes; the server's TTL is only a backstop.
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/cpp-linter.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ jobs:
version: 21
tidy-checks: '-*' # clang-tidy rejects the gcc-only flags of the ESP-IDF compile DB, CodeQL covers static analysis
files-changed-only: false # GitHub returns 406 for very large PR diffs
ignore: '.github|build|managed_components|components/littlefs|components/flatbuffers|components/serialization/include/serialization/_fbs'
ignore: '.github|build|managed_components|components/flatbuffers|components/serialization/include/serialization/_fbs'
thread-comments: ${{ github.event_name == 'pull_request' && 'update' || 'false' }} # Commit comments on push need contents:write

# Advisory on beta and master (pushes and PRs targeting them): findings are still reported, but they don't fail the job
Expand Down
2 changes: 1 addition & 1 deletion .gitmodules
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[submodule "schemas"]
path = schemas
url = https://github.com/OpenShock/flatbuffers-schemas
branch = local-comms-revamp
branch = master
[submodule "frontend/packages/svelte-core"]
path = frontend/packages/svelte-core
url = git@github.com:OpenShock/svelte-core.git
Expand Down
40 changes: 19 additions & 21 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -187,27 +187,25 @@ if(NOT CMAKE_BINARY_DIR STREQUAL _os_compile_db_dir)
endif()

# --- Captive-portal asset image (www) ----------------------------------------
# gen_staticfs.py packs the frontend into the flat `www` image; skipped when the partition table has no such partition.
# gen_staticfs.py packs the frontend into the flat `www` image, which the firmware needs for its captive portal.
partition_table_get_partition_info(_www_offset "--partition-name www" "offset")
partition_table_get_partition_info(_www_size "--partition-name www" "size")

if("${_www_offset}" STREQUAL "" OR "${_www_size}" STREQUAL "")
message(WARNING "No `www` partition in the active partition table; "
"skipping asset image generation.")
else()
set(_www_image ${CMAKE_BINARY_DIR}/www.bin)

# CI never builds this target: its staticfs job runs gen_staticfs.py --skip-frontend-build once for every variant.
add_custom_target(www ALL
BYPRODUCTS ${_www_image}
COMMAND ${_os_python} ${CMAKE_CURRENT_LIST_DIR}/scripts/gen_staticfs.py
--frontend-dir ${CMAKE_CURRENT_LIST_DIR}/frontend
--output ${_www_image}
--max-size ${_www_size}
COMMENT "Building captive-portal asset image (www.bin) for www"
VERBATIM USES_TERMINAL)

# Register the image with the flasher so `idf.py flash` writes it to the
# www offset alongside the app and partition table.
esptool_py_flash_target_image(flash "www" "${_www_offset}" "${_www_image}")
if("${_www_offset}" STREQUAL "")
message(FATAL_ERROR "No `www` partition in the active partition table.")
endif()

set(_www_image ${CMAKE_BINARY_DIR}/www.bin)

# CI never builds this target: its staticfs job runs gen_staticfs.py --skip-frontend-build once for every variant.
add_custom_target(www ALL
BYPRODUCTS ${_www_image}
COMMAND ${_os_python} ${CMAKE_CURRENT_LIST_DIR}/scripts/gen_staticfs.py
--frontend-dir ${CMAKE_CURRENT_LIST_DIR}/frontend
--output ${_www_image}
--partition-csv ${CMAKE_CURRENT_LIST_DIR}/${CONFIG_PARTITION_TABLE_CUSTOM_FILENAME}
COMMENT "Building captive-portal asset image (www.bin) for www"
VERBATIM USES_TERMINAL)

# Register the image with the flasher so `idf.py flash` writes it to the
# www offset alongside the app and partition table.
esptool_py_flash_target_image(flash "www" "${_www_offset}" "${_www_image}")
4 changes: 0 additions & 4 deletions boards/DFRobot-Firebeetle2-ESP32E.defaults
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,6 @@
CONFIG_IDF_TARGET="esp32"
CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y

# OpenShock uses a single 4 MB OTA partition layout (fits every board's flash).
CONFIG_PARTITION_TABLE_CUSTOM=y
CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions/ota_4mb.csv"

# Hardware pins. Not Kconfig: bare OPENSHOCK_* lines, stripped from the sdkconfig fragment by
# scripts/build.py and stamped into the app image after linking by scripts/stamp_board.py,
# so every board on a chip compiles identical objects and one image can serve them all.
Expand Down
4 changes: 0 additions & 4 deletions boards/NodeMCU-32S.defaults
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,6 @@
CONFIG_IDF_TARGET="esp32"
CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y

# OpenShock uses a single 4 MB OTA partition layout (fits every board's flash).
CONFIG_PARTITION_TABLE_CUSTOM=y
CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions/ota_4mb.csv"

# Hardware pins. Not Kconfig: bare OPENSHOCK_* lines, stripped from the sdkconfig fragment by
# scripts/build.py and stamped into the app image after linking by scripts/stamp_board.py,
# so every board on a chip compiles identical objects and one image can serve them all.
Expand Down
4 changes: 0 additions & 4 deletions boards/OpenShock-Core-V1.defaults
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,6 @@
CONFIG_IDF_TARGET="esp32s3"
CONFIG_ESPTOOLPY_FLASHSIZE_8MB=y

# OpenShock uses a single 4 MB OTA partition layout (fits every board's flash).
CONFIG_PARTITION_TABLE_CUSTOM=y
CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions/ota_4mb.csv"

# Hardware pins. Not Kconfig: bare OPENSHOCK_* lines, stripped from the sdkconfig fragment by
# scripts/build.py and stamped into the app image after linking by scripts/stamp_board.py,
# so every board on a chip compiles identical objects and one image can serve them all.
Expand Down
5 changes: 0 additions & 5 deletions boards/OpenShock-Core-V2.defaults
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,6 @@
CONFIG_IDF_TARGET="esp32s3"
CONFIG_ESPTOOLPY_FLASHSIZE_8MB=y

# OpenShock custom OTA partition layout. There is no dedicated 8 MB table yet, so
# reuse the 4 MB OTA layout; it fits within 8 MB (upper flash currently unused).
CONFIG_PARTITION_TABLE_CUSTOM=y
CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions/ota_4mb.csv"

# Native USB only (no USB-UART bridge): console on the built-in USB-Serial-JTAG, so the
# serial console reads commands from the port the host actually sees.
CONFIG_ESP_CONSOLE_USB_SERIAL_JTAG=y
Expand Down
4 changes: 0 additions & 4 deletions boards/Pishock-2023.defaults
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,6 @@
CONFIG_IDF_TARGET="esp32"
CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y

# OpenShock uses a single 4 MB OTA partition layout (fits every board's flash).
CONFIG_PARTITION_TABLE_CUSTOM=y
CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions/ota_4mb.csv"

# Hardware pins. Not Kconfig: bare OPENSHOCK_* lines, stripped from the sdkconfig fragment by
# scripts/build.py and stamped into the app image after linking by scripts/stamp_board.py,
# so every board on a chip compiles identical objects and one image can serve them all.
Expand Down
4 changes: 0 additions & 4 deletions boards/Pishock-Lite-2021.defaults
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,6 @@
CONFIG_IDF_TARGET="esp32"
CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y

# OpenShock uses a single 4 MB OTA partition layout (fits every board's flash).
CONFIG_PARTITION_TABLE_CUSTOM=y
CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions/ota_4mb.csv"

# Hardware pins. Not Kconfig: bare OPENSHOCK_* lines, stripped from the sdkconfig fragment by
# scripts/build.py and stamped into the app image after linking by scripts/stamp_board.py,
# so every board on a chip compiles identical objects and one image can serve them all.
Expand Down
Loading
Loading