diff --git a/.github/workflows/no-std.yaml b/.github/workflows/no-std.yaml index 29beeba8..9906370c 100644 --- a/.github/workflows/no-std.yaml +++ b/.github/workflows/no-std.yaml @@ -1,10 +1,10 @@ --- name: no-std -# The crates that build without std, built where there is none, and et-ws-server as Raspberry Pi Pico 2 W firmware. -# The firmware is linted bare, and built as a generated scenario's, whose modules its host build then serves to a -# test that fetches each. One Linux job: a bare-metal target compiles the same on every host, so another platform -# would only repeat it. The board is not here, so the firmware itself is built and linted, not run; nothing is +# The crates that build without std, built where there is none, and et-ws-server as firmware for every board. +# Each board's firmware is linted bare, and built as a generated scenario's, whose modules the shared host build then +# serves to a test that fetches each. One Linux job: a bare-metal target compiles the same on every host, so another +# platform would only repeat it. No board is here, so the firmware itself is built and linted, not run; nothing is # uploaded or published. # # Not path-gated: what breaks a no_std build is a dependency or a feature gaining std somewhere in the tree, which a @@ -79,3 +79,18 @@ jobs: cd "$dst" MISE_ENV="$fmt" mise run firmware-build MISE_ENV="$fmt" mise run firmware-host-test + + # The host build the step above tested is the one both boards share, so only the board's own build is left. + - name: Lint the ESP32-C6 firmware, then build a generated scenario's + timeout-minutes: 30 + env: + WIFI_NETWORK: edge-toolkit-ci + WIFI_PASSWORD: not-a-real-passphrase + run: | + mise run cargo-clippy-check-esp32c6 + src=verification/local/input/default.yaml + dst=target/firmware-default + fmt=firmware-esp32c6 + cargo run -q -p et-cli -- generate-deployment --input-file "$src" --output-dir "$dst" --output-type "$fmt" + cd "$dst" + MISE_ENV="$fmt" mise run firmware-build diff --git a/.mise/config.rust.toml b/.mise/config.rust.toml index 7f5e2f3a..adf1a7c6 100644 --- a/.mise/config.rust.toml +++ b/.mise/config.rust.toml @@ -16,7 +16,7 @@ depends = ["fetch-cyw43-firmware-rclone"] description = "Build et-ws-server as Raspberry Pi Pico 2 W firmware" env = { CYW43_FIRMWARE_DIR = "{{ config_root }}/{{ vars.cyw43_dir }}" } -run = "cargo build --release -p et-int-ws-server-pico2w --target thumbv8m.main-none-eabihf" +run = "cargo build --release -p et-int-ws-server-firmware --target thumbv8m.main-none-eabihf" # The firmware's body compiles only for the board, so the workspace's host clippy never reaches it; this does. # It needs what build-firmware-pico2w needs. @@ -24,7 +24,19 @@ run = "cargo build --release -p et-int-ws-server-pico2w --target thumbv8m.main-n depends = ["fetch-cyw43-firmware-rclone"] description = "Lint the Raspberry Pi Pico 2 W firmware for its own target" env = { CYW43_FIRMWARE_DIR = "{{ config_root }}/{{ vars.cyw43_dir }}" } -run = "cargo clippy --release -p et-int-ws-server-pico2w --target thumbv8m.main-none-eabihf" +run = "cargo clippy --release -p et-int-ws-server-firmware --target thumbv8m.main-none-eabihf" + +# et-ws-server as ESP32-C6 firmware: an ELF that espflash writes to the board, with esp-hal's bootloader before it. +# Its radio's blobs come with esp-radio, so the build needs only the network to join, from WIFI_NETWORK and +# WIFI_PASSWORD. +[tasks.build-firmware-esp32c6] +description = "Build et-ws-server as ESP32-C6 firmware" +run = "cargo build --release -p et-int-ws-server-firmware --target riscv32imac-unknown-none-elf" + +# As cargo-clippy-check-pico2w, for the ESP32-C6's body. +[tasks.cargo-clippy-check-esp32c6] +description = "Lint the ESP32-C6 firmware for its own target" +run = "cargo clippy --release -p et-int-ws-server-firmware --target riscv32imac-unknown-none-elf" # The Pico 2 W radio's firmware, CLM and NVRAM, from embassy's cyw43-firmware directory at the pinned commit. # They land in the gitignored target/cyw43-firmware, for build-firmware-pico2w to read as CYW43_FIRMWARE_DIR, and diff --git a/.mise/config.toml b/.mise/config.toml index 49465c37..5c9a8b08 100644 --- a/.mise/config.toml +++ b/.mise/config.toml @@ -230,7 +230,7 @@ zizmor = "latest" components = "clippy,rust-analyzer" os = ["linux", "macos"] profile = "minimal" -targets = "thumbv8m.main-none-eabihf,wasm32-unknown-unknown,wasm32-wasip2" +targets = "riscv32imac-unknown-none-elf,thumbv8m.main-none-eabihf,wasm32-unknown-unknown,wasm32-wasip2" # Use `stable`, not `latest`, so every lane resolves through the same rustup channel. # mise's `latest` picks the max of rust-lang/rust's GitHub Releases, which are published by separate # automation that lags the `stable` rustup channel the Windows [[tools.rust]] entries resolve through. That @@ -317,6 +317,13 @@ version = "0.10.0" asset_pattern = "{{ vars.musl_pattern }}" version = "latest" +# espflash writes ESP32 firmware to a board over USB and shows its defmt log, for a firmware-esp32c6 deployment. +# Its release holds cargo-espflash beside it, so the pattern names espflash's own assets; on Linux its musl build, for +# the same reason as coreutils above. +[tools."github:esp-rs/espflash"] +asset_pattern = "{{ vars.espflash_glob }}" +version = "latest" + # The feature-powerset driver behind cargo-hack-check; musl on Linux so the older-glibc image bases can run it. [tools."github:taiki-e/cargo-hack"] asset_pattern = "{{ vars.musl_pattern }}" @@ -524,6 +531,7 @@ win_rust_host = "{% if arch() == 'arm64' %}aarch64-pc-windows-msvc{% else %}x86_ # `raw` keeps the config's own render from evaluating it, so the backend renders it per target platform and the # lock records the one template rather than a host-specific string. The pattern is anchored and the shortest match # wins, which is what the trailing `*` leans on to take `.tar.gz` / `.zip` over their `.sha256` sidecars. +espflash_glob = '{% raw %}espflash-{{ x86_64_arch }}*{{ os(linux="musl", macos="apple", windows="pc") }}*{% endraw %}' musl_pattern = '{% raw %}*-{{ x86_64_arch }}-*{{ os(linux="musl.t", macos="darwin.t", windows="msvc.z") }}*{% endraw %}' # Tier-`a_` prefix marks leaf values referenced by later vars. # a_* vars are referenced by later vars (face1_src), and mise renders [vars] in alphabetical order; `a_` sorts @@ -1909,6 +1917,14 @@ other_hubs=$(server="$server_machine" yq '(.services.hub.machines // [])[] | sel if [ "$source" = "published" ]; then runner_image="ghcr.io/edge-toolkit/core/et-ws-web-runner:latest" docker pull "$runner_image" + # The released runner is rebuilt as an image of this platform alone, under the same name, before it is imported. + # Pulled into docker's containerd image store, it keeps its multi-platform index while holding only this + # platform's content, so the saved tarball names the arm64 manifest without carrying it. containerd then refuses + # the whole import, the hub image included, while k3d reports success: + # ctr: content digest sha256:91389a84eedc9f98e62417d64d2cc7146be16b1e2027bca33919040740ec1a5b: not found + # on https://github.com/edge-toolkit/core/commit/bf8e71a0565e58c8fabb4d53640970eb181c5726 at + # https://github.com/edge-toolkit/core/actions/runs/41288288633/job/124794004602, the first ubuntu-26.04 runners. + printf 'FROM %s\\n' "$runner_image" | docker build --provenance=false -t "$runner_image" - else runner_image="et-ws-web-runner:latest" fi @@ -2002,6 +2018,22 @@ done kubectl wait --for=condition=Ready node --all --timeout="$manifest_wait" # `k3d image import` loads every node, so a pod finds its image whichever machine it is pinned to. k3d image import -c "$cluster" "$hub_image" "$runner_image" +# Every node is checked for both, since the import reports success even when a node's containerd drops an image. +# k3d names each node's container as Kubernetes names the node, and a bare image name is docker.io's library one. +for node in $(kubectl get nodes -o name); do + node=${node#node/} + held=$(docker exec "$node" ctr -n k8s.io images ls -q) + for image in "$hub_image" "$runner_image"; do + case "$image" in + */*) ref=$image ;; + *) ref="docker.io/library/$image" ;; + esac + if ! printf '%s\\n' "$held" | rg -qxF "$ref"; then + echo "k3s-verify: node $node holds no $ref after the import" >&2 + exit 1 + fi + done +done # --save-config so the `kubectl apply` below does not warn about adopting a namespace it did not create. kubectl create namespace "$ns" --save-config @@ -3227,7 +3259,7 @@ run = "cargo nextest run --workspace {{ vars.cargo_ws_excludes }} --config-file [tasks.cargo-test-pkg] description = "Run one workspace package's Rust tests via nextest" run = 'cargo nextest run --config-file config/nextest.toml -p "$usage_package"' -usage = 'arg "" help="Workspace package name (e.g. et-int-ws-server-pico2w)"' +usage = 'arg "" help="Workspace package name (e.g. et-int-ws-server-firmware)"' [tasks.test-recovery-supervisor] description = "Test recovery policy, Kubernetes observations, persistence, and hub lifecycle transport" diff --git a/.mise/config.windows.toml b/.mise/config.windows.toml index 5cd89ddb..0ad5b202 100644 --- a/.mise/config.windows.toml +++ b/.mise/config.windows.toml @@ -106,7 +106,7 @@ url = "https://github.com/edge-toolkit/core/releases/download/gnupg-w32-v1/2.5.2 [[tools.rust]] components = "clippy,rust-analyzer" profile = "minimal" -targets = "thumbv8m.main-none-eabihf,wasm32-unknown-unknown,wasm32-wasip2" +targets = "riscv32imac-unknown-none-elf,thumbv8m.main-none-eabihf,wasm32-unknown-unknown,wasm32-wasip2" version = "stable-{{ vars.win_rust_host }}" [[tools.rust]] diff --git a/.mise/mise.lock b/.mise/mise.lock index 259d410d..1aafffba 100644 --- a/.mise/mise.lock +++ b/.mise/mise.lock @@ -973,6 +973,39 @@ url = "https://github.com/caldempsey/parfit/releases/download/v0.4.4/parfit-x86_ url_api = "https://api.github.com/repos/caldempsey/parfit/releases/assets/404472615" provenance = "github-attestations" +[[tools."github:esp-rs/espflash"]] +version = "4.7.0" +backend = "github:esp-rs/espflash" +specifiers = ["latest"] + +[tools."github:esp-rs/espflash".options] +asset_pattern = 'espflash-{{ x86_64_arch }}*{{ os(linux="musl", macos="apple", windows="pc") }}*' + +[tools."github:esp-rs/espflash"."platforms.linux-arm64"] +checksum = "sha256:a2081c002f6050a287007a93ba01cdd4c9d111181b69d5ddf7bdb71d33841c65" +url = "https://github.com/esp-rs/espflash/releases/download/v4.7.0/espflash-aarch64-unknown-linux-musl.zip" +url_api = "https://api.github.com/repos/esp-rs/espflash/releases/assets/688829879" + +[tools."github:esp-rs/espflash"."platforms.linux-x64"] +checksum = "sha256:a9bf20c76ab42d0d4330bbc33707c4424d5541f5129149d889dbfa8dab476910" +url = "https://github.com/esp-rs/espflash/releases/download/v4.7.0/espflash-x86_64-unknown-linux-musl.zip" +url_api = "https://api.github.com/repos/esp-rs/espflash/releases/assets/688825584" + +[tools."github:esp-rs/espflash"."platforms.macos-arm64"] +checksum = "sha256:4313a4cf3e05036d4e1d8edeef938a11661eb5b9ba5759cabbf48ad4f6fbc180" +url = "https://github.com/esp-rs/espflash/releases/download/v4.7.0/espflash-aarch64-apple-darwin.zip" +url_api = "https://api.github.com/repos/esp-rs/espflash/releases/assets/688815679" + +[tools."github:esp-rs/espflash"."platforms.macos-x64"] +checksum = "sha256:3e3ca415f15f58000d351ca8dd22137c2d04384a8153f65b831553074601ee03" +url = "https://github.com/esp-rs/espflash/releases/download/v4.7.0/espflash-x86_64-apple-darwin.zip" +url_api = "https://api.github.com/repos/esp-rs/espflash/releases/assets/688906558" + +[tools."github:esp-rs/espflash"."platforms.windows-x64"] +checksum = "sha256:2b10333e4f4f1a26024259b0cc1942ba17f27b3c530621c6111f4c10ac70213e" +url = "https://github.com/esp-rs/espflash/releases/download/v4.7.0/espflash-x86_64-pc-windows-msvc.zip" +url_api = "https://api.github.com/repos/esp-rs/espflash/releases/assets/688833427" + [[tools."github:grok-rs/waitup"]] version = "2.0.0" backend = "github:grok-rs/waitup" @@ -1965,7 +1998,7 @@ specifiers = ["stable"] [tools.rust.options] components = "clippy,rust-analyzer" profile = "minimal" -targets = "thumbv8m.main-none-eabihf,wasm32-unknown-unknown,wasm32-wasip2" +targets = "riscv32imac-unknown-none-elf,thumbv8m.main-none-eabihf,wasm32-unknown-unknown,wasm32-wasip2" [[tools.taplo]] version = "0.10.0" diff --git a/AGENTS.md b/AGENTS.md index fbf4ebfe..fb940058 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -168,6 +168,11 @@ its failing step with `gh api repos///actions/jobs//logs` - even while the parent run is still `in_progress` (whereas `gh run view --log-failed` refuses until the whole run finishes). Only after `gh pr checks` is clean is the PR actually green. +**Restart a failed job only once every job in its workflow run has finished.** A failure is reported as soon as its job +ends, but `gh run rerun --job ` is refused while any other job in the same run is still going, and a rerun of the +whole run would throw away the jobs still in flight. So wait for the run to complete, read every failure it produced, +and only then restart the failed jobs, together. + **Ignore `DeepSource: Test coverage`; `codecov/patch` is the coverage check that has to pass.** The repo's line coverage sits under the threshold set in the DeepSource dashboard, so that one check reports `failure` on every PR and on every `main` commit alike -- it carries no information about the change under review, and no diff can turn it @@ -822,7 +827,7 @@ fedora and ubuntu:24.04 both passed in the run that killed bookworm -- and the s and workload go green in the same run. A ceiling would hit the same heaviest lanes every time. Duration is the only correlate: every sighting was a long job. -So the remedy is `gh run rerun --job ` once the parent run completes, and there is nothing to fix. Do not +So the remedy is `gh run rerun --job `, and there is nothing to fix. Do not spend a diagnosis on it; check for the empty log first, and if the siblings passed, re-run and move on. The `ubuntu-24.04-arm` lane of test.yaml is the exception, and it disproves the rotation argument for that lane: diff --git a/CHANGELOG.md b/CHANGELOG.md index 22357962..e3b894e0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,18 +22,21 @@ Changelog 1.1.0. default, adds `picoserve_server::serve`: the hub's `/ws` with `/health` and `/favicon.ico` beside it, over picoserve, and the modules a `picoserve_modules::Modules` builds into the binary under `/modules` and `/`, as the `std` server serves them from disk. A module listed as served elsewhere is redirected to a base URL instead. -- The `et-ws-server-pico2w` firmware runs et-ws-server on a Raspberry Pi Pico 2 W, over the board's Wi-Fi. - `MISE_ENV=rust mise run build-firmware-pico2w` fetches the radio's firmware blobs and builds it for the network - `WIFI_NETWORK` and `WIFI_PASSWORD` name, embedding the modules `MODULES_PATHS` names and serving `MODULES_ROOT` at - `/`. -- `et-cli generate-deployment --output-type firmware-pico2` writes a `mise.firmware-pico2.toml` whose - `firmware-flash` task builds that firmware with the scenario's modules in it and loads it onto a board. The npm +- The `et-ws-server-firmware` firmware runs et-ws-server on a Raspberry Pi Pico 2 W or an ESP32-C6, over the board's + Wi-Fi. `MISE_ENV=rust mise run build-firmware-pico2w` fetches the Pico's radio blobs and builds it for the Pico, and + `build-firmware-esp32c6` for the ESP32-C6, each for the network `WIFI_NETWORK` and `WIFI_PASSWORD` name, embedding + the modules `MODULES_PATHS` names and serving `MODULES_ROOT` at `/`. +- `et-cli generate-deployment --output-type firmware-pico2` writes a `mise.firmware-pico2.toml`, and + `firmware-esp32c6` a `mise.firmware-esp32c6.toml`, whose `firmware-flash` task builds that firmware with the + scenario's modules in it and loads it onto a board, with picotool or espflash. The npm packages the modules use, such as onnxruntime-web, are redirected to jsDelivr rather than built in. A scenario with a runner, `machines:` or published artifacts is refused, since the board runs one hub and nothing else. Its `firmware-host-test` task serves the same modules from the host, as the firmware's host build does, and fetches every module it lists, with no board needed. - `et-firmware-modules` generates the source a firmware's build script writes to build ws-modules into its image, - from the same `MODULES_*` variables, so every board's firmware embeds modules the same way. + from the same `MODULES_*` variables, so every board's firmware embeds modules the same way. Its `board` writes what + the board a target picks bakes in -- the Wi-Fi network, and the Pico 2 W's radio blobs and memory layout -- and + returns the directives that link it. - An opt-in `et-recovery-supervisor` service observes application progress and coordinates bounded Kubernetes recovery. - The Python SDK provides recovery lifecycle messages and integrity-checked checkpoint storage helpers. - A scenario can list `machines:` to span several hosts. The first is the server, running the hub and the collector, diff --git a/CHECKS.md b/CHECKS.md index f334eb93..0392c218 100644 --- a/CHECKS.md +++ b/CHECKS.md @@ -1,7 +1,7 @@ # Checks -- **Generated:** 2026-10-31 -- **Checks:** 79 +- **Generated:** 2026-11-01 +- **Checks:** 80 - **Tools:** 47 - **Shell-script checks:** 10 - **Checks with a repair pass:** 21 @@ -332,6 +332,8 @@ Rules, from `config/ast-grep/rules`: - `cargo-clippy-check` (`default` env; run by `check:rust`). Repaired by `cargo-clippy-fix`. Config: `Cargo.toml`, `config/clippy.toml`. +- `cargo-clippy-check-esp32c6` (`rust` env; not run by `mise run check`) -- Lint the ESP32-C6 firmware for its own + target. Config: `Cargo.toml`, `config/clippy.toml`. - `cargo-clippy-check-pico2w` (`rust` env; not run by `mise run check`) -- Lint the Raspberry Pi Pico 2 W firmware for its own target. Config: `Cargo.toml`, `config/clippy.toml`. - `cargo-clippy-check-pkg` (`default` env; not run by `mise run check`) -- cargo clippy on one or more workspace diff --git a/Cargo.lock b/Cargo.lock index 5874a6a9..03d4aec7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -289,6 +289,15 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" +[[package]] +name = "aead" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b613b8e1e3cf911a086f53f03bf286f52fd7a7258e4fa606f0ef220d39d8877" +dependencies = [ + "generic-array", +] + [[package]] name = "aead" version = "0.5.2" @@ -305,10 +314,10 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2eb72ea190aaa8f0816b548201c4e178e7379a6a8d4c0a0dfd950529adfae9dc" dependencies = [ - "aead", + "aead 0.5.2", "aes", - "cipher", - "ctr", + "cipher 0.4.4", + "ctr 0.9.2", "ghash", "subtle", ] @@ -320,7 +329,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac1f845298e95f983ff1944b728ae08b8cebab80d684f0a832ed0fc74dfa27e2" dependencies = [ "cfg-if", - "cipher", + "cipher 0.4.4", "cpufeatures 0.2.17", ] @@ -330,10 +339,10 @@ version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" dependencies = [ - "aead", + "aead 0.5.2", "aes", - "cipher", - "ctr", + "cipher 0.4.4", + "ctr 0.9.2", "ghash", "subtle", ] @@ -413,6 +422,12 @@ version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" +[[package]] +name = "allocator-api2" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c583acf993cf4245c4acb0a2cc2ab1f9cc097de73411bb6d3647ff6af2b1013d" + [[package]] name = "ambient-authority" version = "0.0.2" @@ -1065,6 +1080,26 @@ version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "46afbd2983a5d5a7bd740ccb198caf5b82f45c40c09c0eed36052d91cb92e719" +[[package]] +name = "bitfield" +version = "0.19.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b45721c9db4c7a20899d05efb7ad9235f50b256e980db30ffb229abf732934c3" +dependencies = [ + "bitfield-macros", +] + +[[package]] +name = "bitfield-macros" +version = "0.19.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0cb6f3d4773a2107b94cbeccaa5b5f0b35a88389b5d522d13d659f64317b22d" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + [[package]] name = "bitflags" version = "1.3.2" @@ -1216,13 +1251,39 @@ checksum = "211713d2e9fb4793ce4360a712c0764264aff6be48932ccf02ca2a331c0436a9" dependencies = [ "btuuid", "defmt 1.2.0", - "embassy-sync", + "embassy-sync 0.8.0", + "embedded-io 0.7.1", + "embedded-io-async 0.7.0", + "futures-intrusive", + "heapless 0.9.3", +] + +[[package]] +name = "bt-hci" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f3c66fd0a41a3b3a5906847b1bf5516048ce083e24fa9341a0fa31d3b4cd73d" +dependencies = [ + "btuuid", + "defmt 1.2.0", + "embassy-sync 0.8.0", "embedded-io 0.7.1", "embedded-io-async 0.7.0", "futures-intrusive", "heapless 0.9.3", ] +[[package]] +name = "bt-hci-transport" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ffb9819c69cf88877f5eebc8c5ade0ab50f2bf9475bbb3eb714d66c55b0cb64" +dependencies = [ + "defmt 1.2.0", + "embedded-io 0.7.1", + "embedded-io-async 0.7.0", +] + [[package]] name = "btuuid" version = "0.1.1" @@ -1238,9 +1299,15 @@ version = "3.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" dependencies = [ - "allocator-api2", + "allocator-api2 0.2.21", ] +[[package]] +name = "byte" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21c7ab3e4ae80853c7f8dcdcd904dfa25c02cc373534b8d165194325a088a7cc" + [[package]] name = "bytemuck" version = "1.25.2" @@ -1399,7 +1466,7 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6" dependencies = [ - "cipher", + "cipher 0.4.4", ] [[package]] @@ -1414,6 +1481,18 @@ dependencies = [ "shlex 2.0.1", ] +[[package]] +name = "ccm" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fba6b0217c41717152d53ae88cb2d32cdeed5c2ac5dc43e0f9e3daff2d5bdf2" +dependencies = [ + "aead 0.4.3", + "cipher 0.3.0", + "ctr 0.8.0", + "subtle", +] + [[package]] name = "cdivsufsort" version = "2.0.0" @@ -1472,6 +1551,15 @@ dependencies = [ "windows-link 0.2.1", ] +[[package]] +name = "cipher" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ee52072ec15386f770805afd189a01c8841be8696bed250fa2f13c4c0d6dfb7" +dependencies = [ + "generic-array", +] + [[package]] name = "cipher" version = "0.4.4" @@ -1701,6 +1789,12 @@ version = "0.4.34" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2ebdda89edc534bf8d5dd2857562633b6495557e5846552ade994b99ed13c36a" +[[package]] +name = "const-default" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b396d1f76d455557e1218ec8066ae14bba60b4b36ecd55577ba979f5db7ecaa" + [[package]] name = "const-hex" version = "1.19.3" @@ -1824,7 +1918,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "408ca8c6d703a196b73219fdb68fd8914d4fda90c34e2bd6859d4842f761bd5f" dependencies = [ "bare-metal", - "bitfield", + "bitfield 0.13.2", "cortex-m-macros", "critical-section", "embedded-hal 0.2.7", @@ -2368,13 +2462,22 @@ dependencies = [ "syn 2.0.119", ] +[[package]] +name = "ctr" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "049bb91fb4aaf0e3c7efa6cd5ef877dbbbd15b39dad06d9948de4ec8a75761ea" +dependencies = [ + "cipher 0.3.0", +] + [[package]] name = "ctr" version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" dependencies = [ - "cipher", + "cipher 0.4.4", ] [[package]] @@ -2421,13 +2524,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "033e856bf6f38c96fee3e3f398cceff0ca2c2fae5db255bcd23495216c7047da" dependencies = [ "aligned", - "bt-hci", + "bt-hci 0.8.1", "cortex-m", "cortex-m-rt", "defmt 1.2.0", "embassy-futures", "embassy-net-driver-channel", - "embassy-sync", + "embassy-sync 0.8.0", "embassy-time", "embedded-hal 1.0.0", "embedded-hal-async", @@ -2458,6 +2561,16 @@ dependencies = [ "darling_macro 0.20.11", ] +[[package]] +name = "darling" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0" +dependencies = [ + "darling_core 0.21.3", + "darling_macro 0.21.3", +] + [[package]] name = "darling" version = "0.24.1" @@ -2482,6 +2595,19 @@ dependencies = [ "syn 2.0.119", ] +[[package]] +name = "darling_core" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1247195ecd7e3c85f83c8d2a366e4210d588e802133e1e355180a9870b517ea4" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "darling_core" version = "0.24.1" @@ -2506,6 +2632,17 @@ dependencies = [ "syn 2.0.119", ] +[[package]] +name = "darling_macro" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" +dependencies = [ + "darling_core 0.21.3", + "quote", + "syn 2.0.119", +] + [[package]] name = "darling_macro" version = "0.24.1" @@ -2608,6 +2745,17 @@ dependencies = [ "defmt 1.2.0", ] +[[package]] +name = "delegate" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "780eb241654bf097afb00fc5f054a09b687dad862e485fdcf8399bb056565370" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "delegate-attr" version = "0.3.1" @@ -2846,7 +2994,7 @@ dependencies = [ "base64 0.22.1", "cbc", "const-oid 0.9.6", - "ctr", + "ctr 0.9.2", "curve25519-dalek", "deno_core", "deno_crypto_provider", @@ -3341,7 +3489,7 @@ dependencies = [ "blake2 0.10.6", "cbc", "const-oid 0.9.6", - "ctr", + "ctr 0.9.2", "data-encoding", "deno_core", "deno_crypto_provider", @@ -4165,7 +4313,7 @@ version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ffdd80ce8ce993de27e9f063a444a4d53ce8e8db4c1f00cc03af5ad5a9867a1e" dependencies = [ - "cipher", + "cipher 0.4.4", ] [[package]] @@ -4322,6 +4470,26 @@ dependencies = [ "syn 2.0.119", ] +[[package]] +name = "docsplay" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8547ea80db62c5bb9d7796fcce5e6e07d1136bdc1a02269095061e806758fab4" +dependencies = [ + "docsplay-macros", +] + +[[package]] +name = "docsplay-macros" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11772ed3eb3db124d826f3abeadf5a791a557f62c19b123e3f07288158a71fdd" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "document-features" version = "0.2.12" @@ -4396,7 +4564,7 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a8bfa975b1aec2145850fcaa1c6fe269a16578c44705a532ae3edc92b8881c7" dependencies = [ - "cipher", + "cipher 0.4.4", ] [[package]] @@ -4563,7 +4731,7 @@ checksum = "b0641612053b2f34fc250bb63f6630ae75de46e02ade7f457268447081d709ce" dependencies = [ "embassy-futures", "embassy-hal-internal 0.4.0", - "embassy-sync", + "embassy-sync 0.8.0", "embedded-hal 0.2.7", "embedded-hal 1.0.0", "embedded-hal-async", @@ -4610,6 +4778,9 @@ name = "embassy-futures" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc2d050bdc5c21e0862a89256ed8029ae6c290a93aecefc73084b3002cdebb01" +dependencies = [ + "defmt 1.2.0", +] [[package]] name = "embassy-hal-internal" @@ -4641,7 +4812,7 @@ dependencies = [ "defmt 1.2.0", "document-features", "embassy-net-driver", - "embassy-sync", + "embassy-sync 0.8.0", "embassy-time", "embedded-io-async 0.7.0", "embedded-nal-async", @@ -4667,7 +4838,7 @@ checksum = "a07d2eb9f05a6fc876500949856ea1be40773d866d8cb99384f72d0ae4568c16" dependencies = [ "embassy-futures", "embassy-net-driver", - "embassy-sync", + "embassy-sync 0.8.0", ] [[package]] @@ -4686,7 +4857,7 @@ dependencies = [ "embassy-embedded-hal", "embassy-futures", "embassy-hal-internal 0.5.0", - "embassy-sync", + "embassy-sync 0.8.0", "embassy-time", "embassy-time-driver", "embassy-time-queue-utils", @@ -4710,6 +4881,34 @@ dependencies = [ "smart-leds", ] +[[package]] +name = "embassy-sync" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d2c8cdff05a7a51ba0087489ea44b0b1d97a296ca6b1d6d1a33ea7423d34049" +dependencies = [ + "cfg-if", + "critical-section", + "embedded-io-async 0.6.1", + "futures-sink", + "futures-util", + "heapless 0.8.0", +] + +[[package]] +name = "embassy-sync" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73974a3edbd0bd286759b3d483540f0ebef705919a5f56f4fc7709066f71689b" +dependencies = [ + "cfg-if", + "critical-section", + "embedded-io-async 0.6.1", + "futures-core", + "futures-sink", + "heapless 0.8.0", +] + [[package]] name = "embassy-sync" version = "0.8.0" @@ -4718,6 +4917,7 @@ checksum = "7bbd85cf5a5ae56bdf26f618364af642d1d0a4e245cdd75cd9aabda382f65a81" dependencies = [ "cfg-if", "critical-section", + "defmt 1.2.0", "embedded-io-async 0.7.0", "futures-core", "futures-sink", @@ -4781,6 +4981,15 @@ dependencies = [ "linked_list_allocator", ] +[[package]] +name = "embedded-can" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9d2e857f87ac832df68fa498d18ddc679175cf3d2e4aa893988e5601baf9438" +dependencies = [ + "nb 1.1.0", +] + [[package]] name = "embedded-hal" version = "0.2.7" @@ -4827,6 +5036,9 @@ name = "embedded-io" version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d" +dependencies = [ + "defmt 0.3.101", +] [[package]] name = "embedded-io" @@ -4843,6 +5055,7 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3ff09972d4073aa8c299395be75161d582e7629cd663171d62af73c8d50dba3f" dependencies = [ + "defmt 0.3.101", "embedded-io 0.6.1", ] @@ -4882,77 +5095,598 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c815b3ed4213d85d6cfd274b871f430c0681084e28dfd4a537877f47f844ec83" [[package]] -name = "embedded-storage-async" -version = "0.4.2" +name = "embedded-storage-async" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69aa7787b8ba0cf4e626c95839f811d853b75e24a0f7312a1eef324fedc13b21" +dependencies = [ + "embedded-storage", +] + +[[package]] +name = "ena" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eabffdaee24bd1bf95c5ef7cec31260444317e72ea56c4c91750e8b7ee58d5f1" +dependencies = [ + "log", +] + +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "endian-type" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c34f04666d835ff5d62e058c3995147c06f42fe86ff053337632bca83e42702d" + +[[package]] +name = "enum-as-inner" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1e6a265c649f3f5979b601d26f1d05ada116434c87741c9493cb56218f76cbc" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "enumset" +version = "1.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e6bd2d7c76d078cd7a460fa169f778c42c026fe16d90e37d7a45d37181bdce5" +dependencies = [ + "defmt 1.2.0", + "enumset_derive", +] + +[[package]] +name = "enumset_derive" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60587c2044cabd91c4d4c73e3912e9f1ea7d76905f31c7eec51869305dae044" +dependencies = [ + "darling 0.21.3", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "equivalent" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00d174d5400e5e8fd687ad1049e2f578285fa914201b1af7e8b112a4546bd826" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "error-code" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5343afd4a8365a643ac588dab4cf234a190c7f6c88c9f6dd6ffe00837661b7" + +[[package]] +name = "error_reporter" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31ae425815400e5ed474178a7a22e275a9687086a12ca63ec793ff292d8fdae8" + +[[package]] +name = "esp-alloc" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f73e8d355019c740f6a5a78429633040617d28a378c271464caadc52c78216e3" +dependencies = [ + "allocator-api2 0.3.1", + "defmt 1.2.0", + "document-features", + "enumset", + "esp-config", + "esp-metadata-generated", + "esp-sync", + "linked_list_allocator", + "rlsf", +] + +[[package]] +name = "esp-backtrace" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fbfed00e884572d620a6958b13456fc2bb8ab75802b5adc8afc224f97cdebad" +dependencies = [ + "defmt 1.2.0", + "document-features", + "esp-config", + "esp-metadata-generated", + "esp-println", + "heapless 0.9.3", + "riscv", + "xtensa-lx", +] + +[[package]] +name = "esp-bootloader-esp-idf" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba0e487f6843419bb085d871f6e860da1bb8c267ce1c5db1051b83882e943054" +dependencies = [ + "defmt 1.2.0", + "document-features", + "esp-config", + "esp-hal-procmacros", + "esp-metadata-generated", + "esp-rom-sys", + "esp-storage", + "jiff", + "sha2 0.11.1", + "strum 0.27.2", +] + +[[package]] +name = "esp-config" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcd577a122ec917691f6820a3df54388ecebc750f071b060f5e83707bc415512" +dependencies = [ + "document-features", + "esp-metadata-generated", + "serde", + "serde_yaml", + "somni-expr", +] + +[[package]] +name = "esp-hal" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9e3dfd511fed638fe41fdb4595f8c7f0cbb488d0f0cf2bbc063d76d805057cf" +dependencies = [ + "bitfield 0.19.5", + "bytemuck", + "critical-section", + "defmt 1.2.0", + "delegate", + "digest 0.10.7", + "digest 0.11.3", + "document-features", + "embassy-embedded-hal", + "embassy-futures", + "embassy-sync 0.8.0", + "embedded-can", + "embedded-hal 1.0.0", + "embedded-hal-async", + "embedded-io 0.6.1", + "embedded-io 0.7.1", + "embedded-io-async 0.6.1", + "embedded-io-async 0.7.0", + "enumset", + "esp-config", + "esp-hal-procmacros", + "esp-metadata-generated", + "esp-riscv-rt", + "esp-rom-sys", + "esp-sync", + "esp32", + "esp32c2", + "esp32c3", + "esp32c5", + "esp32c6 0.24.0", + "esp32c61", + "esp32h2", + "esp32p4", + "esp32s2", + "esp32s3", + "esp32s31", + "fugit", + "instability", + "nb 1.1.0", + "paste", + "portable-atomic", + "rand_core 0.10.1", + "rand_core 0.6.4", + "rand_core 0.9.5", + "riscv", + "somni-template", + "static_cell", + "strum 0.27.2", + "ufmt-write", + "xtensa-lx", + "xtensa-lx-rt", +] + +[[package]] +name = "esp-hal-procmacros" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a8733fdeadc349ba1e4a345c7bfb74d195d8c98fd3be9d2c15bfd78601cbc44" +dependencies = [ + "document-features", + "object 0.37.3", + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.119", + "termcolor", +] + +[[package]] +name = "esp-metadata-generated" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "911aadae41ec99104efa865bd0b6e486b4b735999f9b571963748a46d16495ab" + +[[package]] +name = "esp-phy" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08a925ab42e72eaaa77167352bab7c2bd528ea962a0eb40501324f6db594f88c" +dependencies = [ + "defmt 1.2.0", + "document-features", + "embassy-sync 0.8.0", + "esp-config", + "esp-hal", + "esp-metadata-generated", + "esp-sync", + "esp-wifi-sys-esp32c6", + "esp32c6 0.25.0", +] + +[[package]] +name = "esp-println" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebf772a41bc642b57399632aa06ab55391de35b2b8fcbb1b270d282fbd0fe5ff" +dependencies = [ + "defmt 1.2.0", + "document-features", + "esp-metadata-generated", + "esp-sync", + "log", + "portable-atomic", +] + +[[package]] +name = "esp-radio" +version = "1.0.0-beta.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50247f4e91000fc87661e77254126c74acc4535f822ee247f7778807202dc938" +dependencies = [ + "allocator-api2 0.3.1", + "bt-hci 0.9.0", + "bt-hci-transport", + "defmt 1.2.0", + "docsplay", + "document-features", + "embassy-net-driver", + "embassy-sync 0.8.0", + "enumset", + "esp-alloc", + "esp-config", + "esp-hal", + "esp-hal-procmacros", + "esp-metadata-generated", + "esp-phy", + "esp-radio-rtos-driver", + "esp-rom-sys", + "esp-sync", + "esp-wifi-sys-esp32", + "esp-wifi-sys-esp32c2", + "esp-wifi-sys-esp32c3", + "esp-wifi-sys-esp32c6", + "esp-wifi-sys-esp32h2", + "esp-wifi-sys-esp32s2", + "esp-wifi-sys-esp32s3", + "esp-wifi-sys-esp32s31", + "esp32c6 0.25.0", + "heapless 0.9.3", + "ieee802154", + "instability", + "num-derive", + "num-traits", + "portable-atomic", + "portable_atomic_enum", + "xarxa-driver", +] + +[[package]] +name = "esp-radio-rtos-driver" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27d3ad9d7ed96260566acd1a69a908ff805cfaa73a936ae445c1252e1ec983be" +dependencies = [ + "esp-sync", + "portable-atomic", +] + +[[package]] +name = "esp-riscv-rt" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "626c6b1477224c46fb27919d53ac5bf4ef6f7f70344ee26a720373761aaef531" +dependencies = [ + "defmt 1.2.0", + "document-features", + "riscv", + "riscv-rt", +] + +[[package]] +name = "esp-rom-sys" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e933ff78374b763b65998b64ad671b4890ed4ba13a80deff9f53afe95ede289d" +dependencies = [ + "document-features", + "esp-metadata-generated", + "esp32c6 0.24.0", +] + +[[package]] +name = "esp-rtos" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f8e99f512a839c78a77c2e55e1f0d208851cfa48935847d5e0b5e8b2c5a6289" +dependencies = [ + "allocator-api2 0.3.1", + "defmt 1.2.0", + "document-features", + "embassy-executor", + "embassy-sync 0.8.0", + "embassy-time-driver", + "embassy-time-queue-utils", + "esp-alloc", + "esp-config", + "esp-hal", + "esp-hal-procmacros", + "esp-metadata-generated", + "esp-radio-rtos-driver", + "esp-rom-sys", + "esp-sync", + "portable-atomic", + "riscv", + "xtensa-lx", +] + +[[package]] +name = "esp-storage" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ef0e785f54e316f64a1e14a7a2f061a2ea0b038b284902ca71e1e1f5574cf47" +dependencies = [ + "document-features", + "esp-hal", + "esp-hal-procmacros", + "esp-metadata-generated", + "esp-rom-sys", + "esp-sync", + "esp32c6 0.24.0", +] + +[[package]] +name = "esp-sync" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "064fd57eed12b5cbb669b3935e1f647fd6945b89938116d61153fb45fd703422" +dependencies = [ + "defmt 1.2.0", + "document-features", + "embassy-sync 0.6.2", + "embassy-sync 0.7.2", + "embassy-sync 0.8.0", + "esp-metadata-generated", + "riscv", + "xtensa-lx", +] + +[[package]] +name = "esp-wifi-sys-esp32" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4644d49bde1bdeb3cd20fcc8c08be09f8f67af4b80123e54f665fdebd9a3358" +dependencies = [ + "defmt 1.2.0", +] + +[[package]] +name = "esp-wifi-sys-esp32c2" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57b97263761d6fc207eeffa1f4330b7b80dbf78e5ebe201aa32bf9d37d2ee5cc" +dependencies = [ + "defmt 1.2.0", +] + +[[package]] +name = "esp-wifi-sys-esp32c3" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1345555a7856033077e8ca26572e4739a1b43f8d52daaa0fdf6a1497fe88845" +dependencies = [ + "defmt 1.2.0", +] + +[[package]] +name = "esp-wifi-sys-esp32c6" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5bb01c06a3dbd75121146f619513ee610505b2d80ce3c421ed0c840a19e12f67" +dependencies = [ + "defmt 1.2.0", +] + +[[package]] +name = "esp-wifi-sys-esp32h2" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d88f3ed22082eae3d9ec3d98723040201f5825b9a707475000a7fda62ab2d9e" +dependencies = [ + "defmt 1.2.0", +] + +[[package]] +name = "esp-wifi-sys-esp32s2" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f79fe19c5ce6b1eafa6ce7f022e5cd0aca8e6d01c9d1e29b4c9ad62b1f5f82a" +dependencies = [ + "defmt 1.2.0", +] + +[[package]] +name = "esp-wifi-sys-esp32s3" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1e227f3f97c28c6f52bd28758ddb544854fe52d48b3219a5194846ec982fcfb" +dependencies = [ + "defmt 1.2.0", +] + +[[package]] +name = "esp-wifi-sys-esp32s31" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce987bf209ad4d95810c206acce9505d34056758ab5537ed8ced798945dc4131" +dependencies = [ + "defmt 1.2.0", +] + +[[package]] +name = "esp32" +version = "0.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2f9714c79477940e5c7848b3832af89cd3bcd79abf4067e6938ae90de187a45" +dependencies = [ + "defmt 1.2.0", + "vcell", +] + +[[package]] +name = "esp32c2" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "496afd4a776ec7c96c90fb665894895a1a9f8a7fd5a9d75779fa09d63f9d40a1" +dependencies = [ + "defmt 1.2.0", + "vcell", +] + +[[package]] +name = "esp32c3" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffb892f670ff857033c1624f99ecba1a3f3cafa05c45d734bc129388276715cf" +dependencies = [ + "defmt 1.2.0", + "vcell", +] + +[[package]] +name = "esp32c5" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69aa7787b8ba0cf4e626c95839f811d853b75e24a0f7312a1eef324fedc13b21" +checksum = "97afa80d8110c7234f840252baa8ee8de28f78e8dc233d2d430c0ccfbe59a2b5" dependencies = [ - "embedded-storage", + "defmt 1.2.0", + "vcell", ] [[package]] -name = "ena" -version = "0.14.4" +name = "esp32c6" +version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eabffdaee24bd1bf95c5ef7cec31260444317e72ea56c4c91750e8b7ee58d5f1" +checksum = "a207a5d6886ca89b4f220606dcf3779ca869803df192508314939186ae8de1ee" dependencies = [ - "log", + "defmt 1.2.0", + "vcell", ] [[package]] -name = "encoding_rs" -version = "0.8.35" +name = "esp32c6" +version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +checksum = "f90472d3e6a752e5d20533796f649760103ea449319f14d2a26b2fdff95633bb" dependencies = [ - "cfg-if", + "vcell", ] [[package]] -name = "endian-type" -version = "0.1.2" +name = "esp32c61" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c34f04666d835ff5d62e058c3995147c06f42fe86ff053337632bca83e42702d" +checksum = "f8690ba8b6292db301db513bb9b24263499c9a1ecd22b263dd4210ef12d47fe1" +dependencies = [ + "defmt 1.2.0", + "vcell", +] [[package]] -name = "enum-as-inner" -version = "0.6.1" +name = "esp32h2" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1e6a265c649f3f5979b601d26f1d05ada116434c87741c9493cb56218f76cbc" +checksum = "083a75597f85acaabc0b05998f22455d78c33c4d23ff5f12df2b9590e46f571b" dependencies = [ - "heck", - "proc-macro2", - "quote", - "syn 2.0.119", + "defmt 1.2.0", + "vcell", ] [[package]] -name = "equivalent" -version = "1.0.3" +name = "esp32p4" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00d174d5400e5e8fd687ad1049e2f578285fa914201b1af7e8b112a4546bd826" +checksum = "4c4b1a8cb0d4831d710437f72a73246682919c16ef37a99788f1e26d9f49fe20" +dependencies = [ + "critical-section", + "vcell", +] [[package]] -name = "errno" -version = "0.3.14" +name = "esp32s2" +version = "0.32.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +checksum = "dbeb62af2f2ca94a85be9ab22f8ad97c23ba79b9b3d3e4e20f3e0957bc5f2b2d" dependencies = [ - "libc", - "windows-sys 0.61.2", + "defmt 1.2.0", + "vcell", ] [[package]] -name = "error-code" -version = "3.4.0" +name = "esp32s3" +version = "0.36.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b5343afd4a8365a643ac588dab4cf234a190c7f6c88c9f6dd6ffe00837661b7" +checksum = "ab08eb1f516ba72b7eb272cdd88cd74899552292a294b831b2143a3e60f1666a" +dependencies = [ + "defmt 1.2.0", + "vcell", +] [[package]] -name = "error_reporter" -version = "1.0.0" +name = "esp32s31" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31ae425815400e5ed474178a7a22e275a9687086a12ca63ec793ff292d8fdae8" +checksum = "d8b4f101d37c233fe88b2b5e6d27fedab934a52f804e34f52b4b9d4d5ba2ea63" +dependencies = [ + "critical-section", + "vcell", +] [[package]] name = "et-actix-web-config" @@ -5096,7 +5830,7 @@ dependencies = [ ] [[package]] -name = "et-int-ws-server-pico2w" +name = "et-int-ws-server-firmware" version = "0.1.0" dependencies = [ "chrono", @@ -5111,6 +5845,13 @@ dependencies = [ "embassy-rp", "embassy-time", "embedded-alloc", + "esp-alloc", + "esp-backtrace", + "esp-bootloader-esp-idf", + "esp-hal", + "esp-println", + "esp-radio", + "esp-rtos", "et-firmware-modules", "et-ws-server", "et-ws-service", @@ -6217,6 +6958,16 @@ dependencies = [ "winapi", ] +[[package]] +name = "fugit" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e88e2d6927cbdbbbdc85fc550cee539e63ae0f76445f13094833f63bb65b62f5" +dependencies = [ + "defmt 1.2.0", + "gcd", +] + [[package]] name = "funty" version = "2.0.0" @@ -6341,6 +7092,12 @@ dependencies = [ "serde_json", ] +[[package]] +name = "gcd" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d758ba1b47b00caf47f24925c0074ecb20d6dfcffe7f6d53395c0465674841a" + [[package]] name = "generator" version = "0.8.11" @@ -6637,6 +7394,15 @@ dependencies = [ "elliptic-curve 0.14.3", ] +[[package]] +name = "hash32" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0c35f58762feb77d74ebe43bdbc3210f09be9fe6742234d573bacc26ed92b67" +dependencies = [ + "byteorder", +] + [[package]] name = "hash32" version = "0.3.1" @@ -6646,6 +7412,17 @@ dependencies = [ "byteorder", ] +[[package]] +name = "hash32-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59d2aba832b60be25c1b169146b27c64115470981b128ed84c8db18c1b03c6ff" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "hashbrown" version = "0.14.5" @@ -6653,7 +7430,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" dependencies = [ "ahash", - "allocator-api2", + "allocator-api2 0.2.21", ] [[package]] @@ -6671,7 +7448,7 @@ version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" dependencies = [ - "allocator-api2", + "allocator-api2 0.2.21", "equivalent", "foldhash 0.2.0", "serde", @@ -6718,7 +7495,7 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0bfb9eb618601c89945a70e254898da93b13be0388091d42117462b265bb3fad" dependencies = [ - "hash32", + "hash32 0.3.1", "serde", "stable_deref_trait", ] @@ -6730,7 +7507,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "25ba4bd83f9415b58b4ed8dc5714c76e626a105be4646c02630ad730ad3b5aa4" dependencies = [ "defmt 1.2.0", - "hash32", + "hash32 0.3.1", "serde_core", "stable_deref_trait", ] @@ -7219,6 +7996,20 @@ dependencies = [ "icu_properties", ] +[[package]] +name = "ieee802154" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fcb6de62f20180795db19ae2ab338852a66f8576581554fa8a730e437b450a5" +dependencies = [ + "byte", + "ccm", + "cipher 0.3.0", + "defmt 0.3.101", + "hash32 0.2.1", + "hash32-derive", +] + [[package]] name = "if_chain" version = "1.0.3" @@ -7314,6 +8105,15 @@ dependencies = [ "serde_core", ] +[[package]] +name = "indoc" +version = "2.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a37b2691796cffeb8a8cd305ac66e65841559f147f4e63231d0eafa4db5384d1" +dependencies = [ + "rustversion", +] + [[package]] name = "inotify" version = "0.11.5" @@ -7344,6 +8144,19 @@ dependencies = [ "generic-array", ] +[[package]] +name = "instability" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c3b5acc1e2fd9375041a388da33d1eb8aed5f7a8c0dd3543e3ea2805adfbe20" +dependencies = [ + "darling 0.24.1", + "indoc", + "proc-macro2", + "quote", + "syn 3.0.9", +] + [[package]] name = "int-wasm-cov-wrapper" version = "0.1.0" @@ -8629,6 +9442,17 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" +[[package]] +name = "num-derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "num-integer" version = "0.1.47" @@ -8794,6 +9618,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "object" +version = "0.37.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" +dependencies = [ + "memchr", +] + [[package]] name = "object" version = "0.39.1" @@ -8863,9 +9696,9 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c196e0276c471c843dd5777e7543a36a298a4be942a2a688d8111cd43390dedb" dependencies = [ - "aead", - "cipher", - "ctr", + "aead 0.5.2", + "cipher 0.4.4", + "ctr 0.9.2", "subtle", ] @@ -9099,7 +9932,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4873306de53fe82e7e484df31e1e947d61514b6ea2ed6cd7b45d63006fd9224" dependencies = [ "cbc", - "cipher", + "cipher 0.4.4", "des", "getrandom 0.2.17", "hmac", @@ -9622,6 +10455,27 @@ dependencies = [ "portable-atomic", ] +[[package]] +name = "portable_atomic_enum" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30d48f60c43e0120bb2bb48589a16d4bed2f4b911be41e299f2d0fc0e0e20885" +dependencies = [ + "portable-atomic", + "portable_atomic_enum_macros", +] + +[[package]] +name = "portable_atomic_enum_macros" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a33fa6ec7f2047f572d49317cca19c87195de99c6e5b6ee492da701cfe02b053" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "postcard" version = "1.1.3" @@ -10311,7 +11165,7 @@ version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "62c64daa8e9438b84aaae55010a93f396f8e60e3911590fcba770d04643fc1dd" dependencies = [ - "cipher", + "cipher 0.4.4", ] [[package]] @@ -10383,7 +11237,7 @@ version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc06e6b318142614e4a48bc725abbf08ff166694835c43c9dae5a9009704639a" dependencies = [ - "allocator-api2", + "allocator-api2 0.2.21", "bumpalo", "hashbrown 0.15.5", "log", @@ -10397,7 +11251,7 @@ version = "0.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "757712e8e61590d6d4f5d563483755538b5aa13467837a3b41cd9832509a7f85" dependencies = [ - "allocator-api2", + "allocator-api2 0.2.21", "bumpalo", "hashbrown 0.17.1", "log", @@ -10569,6 +11423,78 @@ dependencies = [ "digest 0.10.7", ] +[[package]] +name = "riscv" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05cfa3f7b30c84536a9025150d44d26b8e1cc20ddf436448d74cd9591eefb25" +dependencies = [ + "critical-section", + "embedded-hal 1.0.0", + "paste", + "riscv-macros", + "riscv-pac", +] + +[[package]] +name = "riscv-macros" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d323d13972c1b104aa036bc692cd08b822c8bbf23d79a27c526095856499799" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "riscv-pac" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8188909339ccc0c68cfb5a04648313f09621e8b87dc03095454f1a11f6c5d436" + +[[package]] +name = "riscv-rt" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d07b9f3a0eff773fc4df11f44ada4fa302e529bff4b7fe7e6a4b98a65ce9174" +dependencies = [ + "defmt 1.2.0", + "riscv", + "riscv-pac", + "riscv-rt-macros", + "riscv-target-parser", +] + +[[package]] +name = "riscv-rt-macros" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "def519ddeeb5e43c2b4fc3952c27b3a86782fc05192f322b2309125cd85b1fc3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "riscv-target-parser" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1376b15f3ff160e9b1e8ea564ce427f2f6fcf77528cc0a8bf405cb476f9cea7" + +[[package]] +name = "rlsf" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07393724337be2ee43a9d86164df4505746874a3fa65913374bc6d6a92314362" +dependencies = [ + "cfg-if", + "const-default", + "libc", + "rustversion", +] + [[package]] name = "ron" version = "0.12.3" @@ -10956,7 +11882,7 @@ version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97a22f5af31f73a954c10289c93e8a50cc23d971e80ee446f1f6f7137a088213" dependencies = [ - "cipher", + "cipher 0.4.4", ] [[package]] @@ -11710,6 +12636,32 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "somni-expr" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f024812d1302010ff9227b80650469e58a3ef129ec5fda36c7a0b6b14ce07371" +dependencies = [ + "indexmap", + "somni-parser", +] + +[[package]] +name = "somni-parser" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ba0fb59df288988d1f80eab8a0a32b18222e87b48d07b065c2f69cfc54d946c" + +[[package]] +name = "somni-template" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f39bb07107650fb128f05f033a97212e946a3c0625cc729c97ed586bf7f226a3" +dependencies = [ + "somni-expr", + "somni-parser", +] + [[package]] name = "sourcemap" version = "9.3.2" @@ -11946,7 +12898,7 @@ version = "4.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d7eefd2c8b228a8c73056482b2ae4b3a1071fbe07638e3b55ceca8570cc48bb" dependencies = [ - "allocator-api2", + "allocator-api2 0.2.21", "bumpalo", "hashbrown 0.14.5", "rustc-hash 2.1.3", @@ -13225,6 +14177,12 @@ dependencies = [ "typify-impl", ] +[[package]] +name = "ufmt-write" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e87a2ed6b42ec5e28cc3b94c09982969e9227600b2e3dcbc1db927a84c06bd69" + [[package]] name = "unarray" version = "0.1.4" @@ -15146,12 +16104,55 @@ dependencies = [ "time", ] +[[package]] +name = "xarxa-driver" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "241eb997638d080753b73e5335427d3a27568678aafa6c0e0564485257e90738" +dependencies = [ + "critical-section", + "defmt 1.2.0", + "document-features", +] + [[package]] name = "xml-rs" version = "0.8.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e450f9b2ed1dff33c94c12589a87338689467b9c4f5d8a5710bd09a847d2c8a7" +[[package]] +name = "xtensa-lx" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e012d667b0aa6d2592ace8ef145a98bff3e76cca7a644f4181ecd7a916ed289b" +dependencies = [ + "critical-section", +] + +[[package]] +name = "xtensa-lx-rt" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07933551193a68b5b0a94a7c428e9626adb559661e0f4b048ac5ec1fa4bed334" +dependencies = [ + "defmt 1.2.0", + "document-features", + "xtensa-lx", + "xtensa-lx-rt-proc-macros", +] + +[[package]] +name = "xtensa-lx-rt-proc-macros" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96fb42cd29c42f8744c74276e9f5bee7b06685bbe5b88df891516d72cb320450" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "yaml_parser" version = "0.2.1" diff --git a/Cargo.toml b/Cargo.toml index 1b13f28b..a57e1e05 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -49,7 +49,7 @@ members = [ "services/ws", "services/ws-pyo3-runner", "services/ws-server", - "services/ws-server-pico2w", + "services/ws-server-firmware", "services/ws-wasm-agent", "services/ws-wasi-runner", "services/ws-web-runner", @@ -110,6 +110,14 @@ embassy-net = "0.9" embassy-rp = "0.10" embassy-time = "0.5" embedded-alloc = { version = "0.7", default-features = false, features = ["llff"] } +esp-alloc = "0.11" +esp-backtrace = "0.20" +esp-bootloader-esp-idf = "0.6" +esp-hal = "1.2" +esp-println = "0.18" +# The 1.0 beta, as the only esp-radio built against the esp-hal 1.2 that esp-rtos 0.4 needs. +esp-radio = "1.0.0-beta.1" +esp-rtos = "0.4" et-actix-web-config = { path = "libs/actix-web-config", version = "0.1.0" } et-cli = { path = "utilities/cli", version = "0.2.0" } et-firmware-modules = { path = "services/firmware-modules", version = "0.1.0" } diff --git a/services/firmware-modules/src/lib.rs b/services/firmware-modules/src/lib.rs index 940b330b..21516628 100644 --- a/services/firmware-modules/src/lib.rs +++ b/services/firmware-modules/src/lib.rs @@ -1,17 +1,28 @@ -//! The source of a firmware's `modules.rs`: every ws-module package it serves, built in with `include_bytes!`. +//! What a firmware's build script generates: `modules.rs`, every ws-module package it serves, built in with +//! `include_bytes!`, and what the board it is built for bakes in and links with. //! //! A firmware's build script reads the variables the `std` server's modules service reads -- the module directories, //! the one served at `/`, and the packages served from elsewhere -- and writes what [`generate`] returns. Each //! directory is a module the way that service finds one: its `pkg/` when it has one, otherwise the directory itself, //! named by the `package.json` there. Its files are the ones npm would publish, each served as its extension says and -//! tagged with a hash of its contents. +//! tagged with a hash of its contents. Built for a board, it also hands [`board`] the board's variables. +use std::env::VarError; use std::fmt::Write as _; use std::hash::{DefaultHasher, Hash as _, Hasher as _}; use std::path::{Component, Path, PathBuf}; use fs_err as fs; +/// The variables [`board`] reads, which a build script has to rerun when one changes. +pub const BOARD_VARIABLES: [&str; 3] = [CYW43_FIRMWARE_DIR, WIFI_NETWORK, WIFI_PASSWORD]; +/// The directory holding the Pico 2 W radio's blobs. +const CYW43_FIRMWARE_DIR: &str = "CYW43_FIRMWARE_DIR"; +/// The network a board joins. +const WIFI_NETWORK: &str = "WIFI_NETWORK"; +/// The network's passphrase. +const WIFI_PASSWORD: &str = "WIFI_PASSWORD"; + /// The directory a module's build leaves its package in, when the module's own directory is its source. const PACKAGE_DIR: &str = "pkg"; /// The manifest naming a module, and listing its files. @@ -21,7 +32,7 @@ const MODULES_API: &str = "et_ws_server::picoserve_modules"; /// The variable the module directories come from, which errors about them name. const MODULES_PATHS: &str = "MODULES_PATHS"; -/// Why a firmware's modules cannot be built in. +/// Why a firmware's modules cannot be built in, or its board's build cannot be generated. #[derive(Debug, thiserror::Error)] #[non_exhaustive] pub enum ModulesError { @@ -49,6 +60,12 @@ pub enum ModulesError { UnknownRoot { root: String, names: Vec }, #[error("MODULES_ELSEWHERE needs MODULES_ELSEWHERE_URL to say where they are")] ElsewhereWithoutUrl, + #[error("a board's firmware needs {0} set when it is built")] + Unset(String), + #[error("{variable} holds no {file}")] + MissingBlob { variable: String, file: String }, + #[error("no board is built for {0}")] + NoBoard(String), } /// The source of `modules.rs`, and every file it builds in, which the build has to rerun when one changes. @@ -157,6 +174,85 @@ pub fn include_path(variable: &str, path: &Path) -> Result Ok(format!("{path:?}")) } +/// Write what a firmware for the board `arch` picks bakes in to `out_dir`, and return the directives that link it. +/// +/// Every board gets `wifi.rs`, the network `WIFI_NETWORK` and `WIFI_PASSWORD` name, each read through `var` as from +/// the environment. Bare-metal Arm is the Raspberry Pi Pico 2 W, which also gets its `memory_x` layout and `cyw43.rs`, +/// its radio's blobs from `CYW43_FIRMWARE_DIR`, and links with cortex-m-rt's script; bare-metal RISC-V is the +/// ESP32-C6, which links with esp-hal's. Both link defmt's too. +/// +/// # Errors +/// +/// When `arch` is no board's, a variable the board needs is unset, `CYW43_FIRMWARE_DIR` lacks a blob or is not an +/// absolute UTF-8 path, or a file cannot be written. +pub fn board(arch: &str, out_dir: &Path, memory_x: &[u8], var: V) -> Result, ModulesError> +where + V: Fn(&str) -> Result, +{ + let required = |name: &str| { + let Ok(value) = var(name) else { + return Err(ModulesError::Unset(name.to_owned())); + }; + Ok(value) + }; + let mut directives = Vec::new(); + let script = match arch { + "arm" => { + fs::write(out_dir.join("memory.x"), memory_x)?; + let blobs = cyw43(Path::new(&required(CYW43_FIRMWARE_DIR)?))?; + fs::write(out_dir.join("cyw43.rs"), blobs)?; + directives.push(format!("cargo:rustc-link-search={}", out_dir.display())); + directives.push("cargo:rustc-link-arg-bins=--nmagic".to_owned()); + "link.x" + } + "riscv32" => "linkall.x", + _ => return Err(ModulesError::NoBoard(arch.to_owned())), + }; + let network = required(WIFI_NETWORK)?; + let password = required(WIFI_PASSWORD)?; + let wifi = format!( + "/// The network the board joins. +const WIFI_NETWORK: &str = {network:?}; +/// The network's passphrase. +const WIFI_PASSWORD: &str = {password:?}; +" + ); + fs::write(out_dir.join("wifi.rs"), wifi)?; + directives.push(format!("cargo:rustc-link-arg-bins=-T{script}")); + directives.push("cargo:rustc-link-arg-bins=-Tdefmt.x".to_owned()); + Ok(directives) +} + +/// The source of `cyw43.rs`: the Pico 2 W radio's blobs, from `dir`. +#[expect( + clippy::single_call_fn, + reason = "the radio step of board, the one board with blobs of its own" +)] +fn cyw43(dir: &Path) -> Result { + let blob = |file: &str| { + let path = dir.join(file); + if !path.is_file() { + return Err(ModulesError::MissingBlob { + variable: CYW43_FIRMWARE_DIR.to_owned(), + file: file.to_owned(), + }); + } + Ok(format!("include_bytes!({})", include_path(CYW43_FIRMWARE_DIR, &path)?)) + }; + let firmware = blob("43439A0.bin")?; + let clm = blob("43439A0_clm.bin")?; + let nvram = blob("nvram_rp2040.bin")?; + Ok(format!( + "/// The CYW43439's firmware. +static RADIO_FIRMWARE: &cyw43::Aligned = &cyw43::Aligned(*{firmware}); +/// The CYW43439's country locale matrix. +static RADIO_CLM: &[u8] = {clm}; +/// The CYW43439's configuration for the Pico 2 W's module. +static RADIO_NVRAM: &cyw43::Aligned = &cyw43::Aligned(*{nvram}); +" + )) +} + /// The comma-separated items of `list`, trimmed, with empty ones dropped. fn items(list: &str) -> impl Iterator { list.split(',').map(str::trim).filter(|item| !item.is_empty()) diff --git a/services/firmware-modules/tests/board.rs b/services/firmware-modules/tests/board.rs new file mode 100644 index 00000000..5b35aede --- /dev/null +++ b/services/firmware-modules/tests/board.rs @@ -0,0 +1,119 @@ +//! Covers what a board's build writes and links with: each board's files and directives, and every way a build for +//! one is refused. +#![cfg(test)] + +use std::collections::BTreeMap; +use std::env::VarError; +use std::path::Path; + +use et_firmware_modules::{BOARD_VARIABLES, ModulesError, board}; +use fs_err as fs; +use tempfile::{TempDir, tempdir}; + +/// The layout handed over as the Pico 2 W's `memory.x`. +const MEMORY_X: &[u8] = b"MEMORY {}"; + +/// The radio's blobs, each with something in it, in a directory of their own. +fn blobs() -> TempDir { + let dir = tempdir().unwrap(); + for file in ["43439A0.bin", "43439A0_clm.bin", "nvram_rp2040.bin"] { + fs::write(dir.path().join(file), file).unwrap(); + } + dir +} + +/// What `board` returns for `arch`, reading only `vars`, and the directory it wrote to. +fn built(arch: &str, vars: &[(&str, &str)]) -> (TempDir, Result, ModulesError>) { + let vars: BTreeMap = vars + .iter() + .map(|(name, value)| ((*name).to_owned(), (*value).to_owned())) + .collect(); + let out_dir = tempdir().unwrap(); + let var = |name: &str| vars.get(name).cloned().ok_or(VarError::NotPresent); + let directives = board(arch, out_dir.path(), MEMORY_X, var); + (out_dir, directives) +} + +/// The file `name` that `board` wrote to `out_dir`. +fn written(out_dir: &Path, name: &str) -> String { + fs::read_to_string(out_dir.join(name)).unwrap() +} + +#[test] +fn an_esp32c6_joins_the_network_and_links_with_esp_hal() { + let (out_dir, directives) = built( + "riscv32", + &[("WIFI_NETWORK", "home"), ("WIFI_PASSWORD", "pass \"word\"")], + ); + assert_eq!( + directives.unwrap(), + [ + "cargo:rustc-link-arg-bins=-Tlinkall.x", + "cargo:rustc-link-arg-bins=-Tdefmt.x" + ] + ); + let wifi = written(out_dir.path(), "wifi.rs"); + assert!(wifi.contains(r#"const WIFI_NETWORK: &str = "home";"#)); + assert!(wifi.contains(r#"const WIFI_PASSWORD: &str = "pass \"word\"";"#)); + assert!(!out_dir.path().join("cyw43.rs").exists()); + assert!(!out_dir.path().join("memory.x").exists()); +} + +#[test] +fn a_pico_2_w_also_bakes_in_its_radio_and_its_memory_layout() { + let blobs = blobs(); + let dir = blobs.path().to_str().unwrap(); + let (out_dir, directives) = built( + "arm", + &[ + ("WIFI_NETWORK", "home"), + ("WIFI_PASSWORD", "pass"), + ("CYW43_FIRMWARE_DIR", dir), + ], + ); + assert_eq!( + directives.unwrap(), + [ + format!("cargo:rustc-link-search={}", out_dir.path().display()), + "cargo:rustc-link-arg-bins=--nmagic".to_owned(), + "cargo:rustc-link-arg-bins=-Tlink.x".to_owned(), + "cargo:rustc-link-arg-bins=-Tdefmt.x".to_owned(), + ] + ); + assert_eq!(fs::read(out_dir.path().join("memory.x")).unwrap(), MEMORY_X); + let radio = written(out_dir.path(), "cyw43.rs"); + for file in ["43439A0.bin", "43439A0_clm.bin", "nvram_rp2040.bin"] { + let literal = format!("{:?}", blobs.path().join(file).to_str().unwrap()); + assert!(radio.contains(&format!("include_bytes!({literal})"))); + } + assert!(written(out_dir.path(), "wifi.rs").contains(r#"const WIFI_NETWORK: &str = "home";"#)); + assert!(BOARD_VARIABLES.contains(&"CYW43_FIRMWARE_DIR")); +} + +#[test] +fn a_build_for_no_board_or_without_what_its_board_needs_is_refused() { + let blobs = blobs(); + let dir = blobs.path().to_str().unwrap(); + let wifi = [("WIFI_NETWORK", "home"), ("WIFI_PASSWORD", "pass")]; + let (_out, no_board) = built("x86_64", &wifi); + assert_eq!(no_board.unwrap_err().to_string(), "no board is built for x86_64"); + let (_out, no_password) = built("riscv32", &[("WIFI_NETWORK", "home")]); + assert_eq!( + no_password.unwrap_err().to_string(), + "a board's firmware needs WIFI_PASSWORD set when it is built" + ); + let (_out, no_blobs) = built("arm", &wifi); + assert_eq!( + no_blobs.unwrap_err().to_string(), + "a board's firmware needs CYW43_FIRMWARE_DIR set when it is built" + ); + let (_out, relative) = built("arm", &[wifi[0], wifi[1], ("CYW43_FIRMWARE_DIR", "relative")]); + assert!(matches!(relative.unwrap_err(), ModulesError::MissingBlob { .. })); + + fs::remove_file(blobs.path().join("43439A0_clm.bin")).unwrap(); + let (_out, missing) = built("arm", &[wifi[0], wifi[1], ("CYW43_FIRMWARE_DIR", dir)]); + assert_eq!( + missing.unwrap_err().to_string(), + "CYW43_FIRMWARE_DIR holds no 43439A0_clm.bin" + ); +} diff --git a/services/ws-server-pico2w/Cargo.toml b/services/ws-server-firmware/Cargo.toml similarity index 59% rename from services/ws-server-pico2w/Cargo.toml rename to services/ws-server-firmware/Cargo.toml index 3fe2d178..2bc654d4 100644 --- a/services/ws-server-pico2w/Cargo.toml +++ b/services/ws-server-firmware/Cargo.toml @@ -1,7 +1,7 @@ [package] -name = "et-int-ws-server-pico2w" +name = "et-int-ws-server-firmware" publish = false -description = "et-ws-server as firmware for the Raspberry Pi Pico 2 W" +description = "et-ws-server as firmware for the Raspberry Pi Pico 2 W and the ESP32-C6" version = "0.1.0" edition.workspace = true license.workspace = true @@ -14,7 +14,7 @@ ignored = ["cortex-m-rt"] [[bin]] bench = false doctest = false -name = "et-ws-server-pico2w" +name = "et-ws-server-firmware" path = "src/main.rs" test = false @@ -26,24 +26,38 @@ et-ws-server = { workspace = true, features = ["picoserve-server"] } et-ws-service = { workspace = true, features = ["picoserve-hub"] } picoserve.workspace = true -# The board's network stack and executor, scoped to bare-metal Arm as the firmware body is. +# What every board shares once its radio is up: embassy's network stack, executor and clock, and the defmt log. +[target.'cfg(target_os = "none")'.dependencies] +defmt.workspace = true +embassy-executor = { workspace = true, features = ["defmt"] } +embassy-net = { workspace = true, features = ["defmt", "dhcpv4", "medium-ethernet", "proto-ipv4", "tcp"] } +embassy-time = { workspace = true, features = ["defmt"] } +picoserve = { workspace = true, features = ["embassy"] } +static_cell.workspace = true + +# The Raspberry Pi Pico 2 W, bare-metal Arm: the RP2350 and its CYW43439 radio. [target.'cfg(all(target_arch = "arm", target_os = "none"))'.dependencies] cortex-m-rt.workspace = true cyw43 = { workspace = true, features = ["defmt"] } cyw43-pio = { workspace = true, features = ["defmt"] } -defmt.workspace = true defmt-rtt.workspace = true -embassy-executor = { workspace = true, features = ["defmt", "executor-thread", "platform-cortex-m"] } -embassy-net = { workspace = true, features = ["defmt", "dhcpv4", "medium-ethernet", "proto-ipv4", "tcp"] } +embassy-executor = { workspace = true, features = ["executor-thread", "platform-cortex-m"] } embassy-rp = { workspace = true, features = ["critical-section-impl", "defmt", "rp235xa", "time-driver"] } -embassy-time = { workspace = true, features = ["defmt"] } embedded-alloc.workspace = true panic-probe = { workspace = true, features = ["defmt"] } -picoserve = { workspace = true, features = ["embassy"] } -static_cell.workspace = true + +# The ESP32-C6, bare-metal RISC-V: its own radio, and esp-rtos running embassy's executor and clock. +[target.'cfg(all(target_arch = "riscv32", target_os = "none"))'.dependencies] +esp-alloc = { workspace = true, features = ["defmt", "esp32c6"] } +esp-backtrace = { workspace = true, features = ["defmt", "esp32c6", "panic-handler"] } +esp-bootloader-esp-idf = { workspace = true, features = ["defmt", "esp32c6"] } +esp-hal = { workspace = true, features = ["defmt", "esp32c6", "unstable"] } +esp-println = { workspace = true, features = ["defmt-espflash", "esp32c6"] } +esp-radio = { workspace = true, features = ["defmt", "esp-alloc", "esp32c6", "wifi"] } +esp-rtos = { workspace = true, features = ["defmt", "embassy", "esp-alloc", "esp-radio", "esp32c6"] } # Anywhere else, the host's: tokio, and a clock that knows the date. -[target.'cfg(not(all(target_arch = "arm", target_os = "none")))'.dependencies] +[target.'cfg(not(target_os = "none"))'.dependencies] chrono = { workspace = true, features = ["std"] } picoserve = { workspace = true, features = ["tokio"] } tokio = { workspace = true, features = ["net", "rt", "time"] } diff --git a/services/ws-server-firmware/README.md b/services/ws-server-firmware/README.md new file mode 100644 index 00000000..07e588c0 --- /dev/null +++ b/services/ws-server-firmware/README.md @@ -0,0 +1,81 @@ +# et-int-ws-server-firmware + +et-ws-server as firmware, built as the `et-ws-server-firmware` binary: the `no_std` hub's `/ws`, with `/health`, +`/favicon.ico` and the modules built into it beside it, served over a board's own Wi-Fi on port 8080. It joins the +network it was built for, takes an address over DHCP, and serves four connections at once. + +The target it is built for picks the board: + +| Board | Target | Build task | Loaded with | +| --------------------- | ------------------------------ | ------------------------ | ----------- | +| Raspberry Pi Pico 2 W | `thumbv8m.main-none-eabihf` | `build-firmware-pico2w` | picotool | +| ESP32-C6 | `riscv32imac-unknown-none-elf` | `build-firmware-esp32c6` | espflash | + +Everything but bringing up the radio is shared, so both serve the same hub the same way. It is a local hub, with no +backplane, admin API, idle timeout or registry file. A board has no clock, so the times it stamps on messages are its +uptime counted from the Unix epoch. + +## Building + +The build bakes in the network to join, from `WIFI_NETWORK` and `WIFI_PASSWORD`. + +The Pico 2 W build also bakes in the CYW43439 radio's firmware. Its build task fetches the radio's `43439A0.bin`, +`43439A0_clm.bin` and `nvram_rp2040.bin` from the embassy repository's `cyw43-firmware` directory into +`target/cyw43-firmware`, and points `CYW43_FIRMWARE_DIR` at them. Infineon licenses those blobs separately from this +project, so they are not part of it. The ESP32-C6's radio blobs come with the `esp-radio` crate. + +It serves no modules unless told which to build in, through the variables the `std` server reads, each optional: + +- `MODULES_PATHS`: the module directories, comma-separated and absolute. Each is built in from its `pkg/` when it has + one, otherwise from the directory itself: every file its `package.json` lists under `files`, or every file there. +- `MODULES_ROOT`: the name of the module served at `/`, one of those. +- `MODULES_ELSEWHERE` and `MODULES_ELSEWHERE_URL`: module names too big for a board's 4 MiB of flash, listed with the + rest at `/modules/`, and the base URL a request for one of their files is redirected to. + +`et-cli generate-deployment --output-type firmware-pico2` or `firmware-esp32c6` writes the tasks that set all of +these for a scenario. + +### Raspberry Pi Pico 2 W + +```sh +MISE_ENV=rust mise run build-firmware-pico2w +``` + +The firmware is `target/thumbv8m.main-none-eabihf/release/et-ws-server-firmware`, an ELF. Load it with picotool +while the board is in BOOTSEL mode, or with probe-rs over a debug probe, which also shows its defmt log: + +```sh +picotool load -x -t elf target/thumbv8m.main-none-eabihf/release/et-ws-server-firmware +``` + +```sh +probe-rs run --chip RP235x target/thumbv8m.main-none-eabihf/release/et-ws-server-firmware +``` + +### ESP32-C6 + +```sh +MISE_ENV=rust mise run build-firmware-esp32c6 +``` + +The firmware is `target/riscv32imac-unknown-none-elf/release/et-ws-server-firmware`, an ELF. espflash writes it to +the board over USB, with esp-hal's bootloader before it, then shows its defmt log: + +```sh +espflash flash --monitor --log-format defmt target/riscv32imac-unknown-none-elf/release/et-ws-server-firmware +``` + +Either board's log names the address it serves on, as `ws://
:8080/ws`. + +## Serving from the host + +Built for a target with an operating system, the binary is the same hub serving the same modules from the host over +tokio, read from the same `MODULES_*` at build time, so what a board would serve can be explored without one. It +listens on the address its one argument names, or on port 8080 on every interface, and prints where: + +```sh +cargo run -p et-int-ws-server-firmware -- 127.0.0.1:8080 +``` + +The package's test starts it on a free port and fetches every module it lists, so run with the `MODULES_*` a +scenario's deployment sets, as its `firmware-host-test` task does, it checks what a board would serve. diff --git a/services/ws-server-firmware/build.rs b/services/ws-server-firmware/build.rs new file mode 100644 index 00000000..bc746c36 --- /dev/null +++ b/services/ws-server-firmware/build.rs @@ -0,0 +1,60 @@ +//! Generates what the firmware bakes in, read here from the build's environment, and links it for its board. +//! +//! Every build gets `modules.rs`, since the host serves the same modules a board would; a board's build also gets +//! what [`board`] writes for it, and links as that says. + +use std::env; +use std::error::Error; +use std::path::PathBuf; + +use et_firmware_modules::{BOARD_VARIABLES, board, generate}; +use fs_err as fs; + +/// The variables `modules.rs` is generated from, each named as the `std` server's modules service or `MODULES_*` reads. +const MODULE_VARIABLES: [&str; 4] = [MODULES_PATHS, MODULES_ROOT, MODULES_ELSEWHERE, MODULES_ELSEWHERE_URL]; +/// The module directories to build in, comma-separated, as the `std` server's `MODULES_PATHS` lists them. +const MODULES_PATHS: &str = "MODULES_PATHS"; +/// The name of the module served at `/`, as the `std` server's `MODULES_ROOT`. +const MODULES_ROOT: &str = "MODULES_ROOT"; +/// The names of the modules served from `MODULES_ELSEWHERE_URL` rather than built in, comma-separated. +const MODULES_ELSEWHERE: &str = "MODULES_ELSEWHERE"; +/// Where a request for a module in `MODULES_ELSEWHERE` is redirected, its path under `/modules` appended. +const MODULES_ELSEWHERE_URL: &str = "MODULES_ELSEWHERE_URL"; +/// The operating system cargo is building for, `none` for a board. +const CARGO_CFG_TARGET_OS: &str = "CARGO_CFG_TARGET_OS"; +/// The architecture cargo is building for, which picks the board. +const CARGO_CFG_TARGET_ARCH: &str = "CARGO_CFG_TARGET_ARCH"; +/// The directory cargo gives this build script for what it generates. +const OUT_DIR: &str = "OUT_DIR"; + +/// Write `modules.rs`, and for a board what [`board`] writes, rerunning whenever a variable or file they read changes. +fn main() -> Result<(), Box> { + println!("cargo:rerun-if-changed=memory.x"); + for variable in BOARD_VARIABLES.into_iter().chain(MODULE_VARIABLES) { + println!("cargo:rerun-if-env-changed={variable}"); + } + let out_dir = PathBuf::from(env::var(OUT_DIR)?); + // The modules are served on the host too, so what a board would serve can be explored without one. + let modules = generate( + &optional(MODULES_PATHS), + &optional(MODULES_ROOT), + &optional(MODULES_ELSEWHERE), + &optional(MODULES_ELSEWHERE_URL), + )?; + for file in &modules.files { + println!("cargo:rerun-if-changed={}", file.display()); + } + fs::write(out_dir.join("modules.rs"), modules.source)?; + if env::var(CARGO_CFG_TARGET_OS)? == "none" { + let arch = env::var(CARGO_CFG_TARGET_ARCH)?; + for directive in board(&arch, &out_dir, include_bytes!("memory.x"), |name| env::var(name))? { + println!("{directive}"); + } + } + Ok(()) +} + +/// The variable `name`, empty when it is unset, since every module variable may be. +fn optional(name: &str) -> String { + env::var(name).unwrap_or_default() +} diff --git a/services/ws-server-pico2w/memory.x b/services/ws-server-firmware/memory.x similarity index 100% rename from services/ws-server-pico2w/memory.x rename to services/ws-server-firmware/memory.x diff --git a/services/ws-server-firmware/src/board/esp32c6.rs b/services/ws-server-firmware/src/board/esp32c6.rs new file mode 100644 index 00000000..5066a44f --- /dev/null +++ b/services/ws-server-firmware/src/board/esp32c6.rs @@ -0,0 +1,88 @@ +//! The ESP32-C6: bring up its Wi-Fi, join the network named at build time, rejoin it whenever it is lost, and serve the +//! hub on it. + +#![expect( + clippy::future_not_send, + clippy::single_call_fn, + clippy::unwrap_used, + reason = "esp-rtos runs embassy's executor on the one core; each task is spawned once, or bring-up panics" +)] + +use core::future; + +use defmt::{Debug2Format, warn}; +use embassy_executor::Spawner; +use embassy_net::{Config as NetConfig, DhcpConfig, Runner}; +use embassy_time::{Duration, Timer}; +use esp_hal::clock::CpuClock; +use esp_hal::rng::Rng; +use esp_hal::timer::timg::TimerGroup; +use esp_radio::wifi::sta::StationConfig; +use esp_radio::wifi::{AuthenticationMethodConfig, Config, ControllerConfig, Interface, WifiController}; +use {esp_backtrace as _, esp_println as _}; + +use super::hub::{HEAP_SIZE, serve, stack_resources}; + +include!(concat!(env!("OUT_DIR"), "/wifi.rs")); + +/// How long to wait before joining again, after failing to join or losing the network. +const REJOIN_DELAY: Duration = Duration::from_secs(5); +/// The heap the radio's driver allocates from, in the RAM the bootloader is done with once the firmware runs. +const RADIO_HEAP_SIZE: usize = 64 * 1024; + +esp_bootloader_esp_idf::esp_app_desc!(); + +/// Join the network, and join it again each time it is lost or a join fails. +#[embassy_executor::task] +async fn wifi_task(mut controller: WifiController<'static>) -> ! { + loop { + match controller.connect_async().await { + Ok(_) => { + let lost = controller.wait_for_disconnect_async().await; + warn!("Lost {}: {}; rejoining", WIFI_NETWORK, Debug2Format(&lost)); + } + Err(error) => warn!("Failed to join {}: {}; retrying", WIFI_NETWORK, Debug2Format(&error)), + } + Timer::after(REJOIN_DELAY).await; + } +} + +/// Drive the network stack over the station interface: DHCP, TCP, and every socket the hub serves on. +#[embassy_executor::task] +async fn net_task(mut runner: Runner<'static, Interface>) -> ! { + runner.run().await +} + +/// Bring the board up, start joining the network, and serve the hub. +#[esp_hal::main] +async fn main(spawner: Spawner) -> ! { + let peripherals = esp_hal::init(esp_hal::Config::default().with_cpu_clock(CpuClock::max())); + esp_alloc::heap_allocator!(#[esp_hal::ram(reclaimed)] size: RADIO_HEAP_SIZE); + esp_alloc::heap_allocator!(size: HEAP_SIZE); + let timers = TimerGroup::new(peripherals.TIMG0); + esp_rtos::start(timers.timer0, peripherals.FROM_CPU_INTR0); + + let station = StationConfig::default() + .with_ssid(WIFI_NETWORK.try_into().unwrap()) + .with_authentication(AuthenticationMethodConfig::Wpa2Personal( + WIFI_PASSWORD.try_into().unwrap(), + )); + let controller = WifiController::new( + peripherals.WIFI, + ControllerConfig::default().with_initial_config(Config::Station(station)), + ) + .unwrap(); + + let rng = Rng::new(); + let seed = (u64::from(rng.random()) << 32_u32) | u64::from(rng.random()); + let (stack, network) = embassy_net::new( + Interface::station(), + NetConfig::dhcpv4(DhcpConfig::default()), + stack_resources(), + seed, + ); + spawner.spawn(wifi_task(controller).unwrap()); + spawner.spawn(net_task(network).unwrap()); + serve(spawner, stack, rng.random()).await; + future::pending().await +} diff --git a/services/ws-server-firmware/src/board/hub.rs b/services/ws-server-firmware/src/board/hub.rs new file mode 100644 index 00000000..b08c2195 --- /dev/null +++ b/services/ws-server-firmware/src/board/hub.rs @@ -0,0 +1,113 @@ +//! What every board runs once it is on the network: the hub, served on as many sockets at once as there are connection +//! tasks, from the network stack a board hands to [`serve`]. + +#![expect( + clippy::future_not_send, + clippy::single_call_fn, + reason = "embassy's executor runs every task on the one core; each board calls each step once" +)] + +use alloc::format; +use alloc::string::String; +use core::net::{IpAddr, Ipv4Addr}; + +use chrono::DateTime; +use defmt::{Debug2Format, info, warn}; +use edge_toolkit::ws_server::MessageRetention; +use embassy_executor::Spawner; +use embassy_net::tcp::TcpSocket; +use embassy_net::{Stack, StackResources}; +use embassy_time::Instant; +use et_ws_server::picoserve_server::serve as serve_connection; +use et_ws_service::hub::Stamps; +use et_ws_service::picoserve_hub::{self, Hub}; +use picoserve::time::EmbassyTimer; +use static_cell::StaticCell; + +use crate::{HTTP_BUFFER, MAX_FRAME_SIZE, MAX_QUEUED, MODULES, PORT}; + +/// How many connections the hub serves at once: one task, and one socket's buffers, each. +const CONNECTIONS: usize = 4; +/// Each connection's TCP receive and transmit buffer. +const TCP_BUFFER: usize = 4 * 1024; +/// The heap the hub's registry, routing and frame buffers allocate from. +pub(super) const HEAP_SIZE: usize = 160 * 1024; + +/// Ids unique across boots -- a random word drawn at boot, then a count -- and the time since boot. +/// +/// A board has no clock, so a time is its uptime counted from the Unix epoch. +struct BoardStamps { + boot: u32, + issued: u32, +} + +impl Stamps for BoardStamps { + fn new_id(&mut self) -> String { + self.issued = self.issued.wrapping_add(1); + format!("{:08x}-{}", self.boot, self.issued) + } + + fn now(&mut self) -> String { + let millis = i64::try_from(Instant::now().as_millis()).unwrap_or(i64::MAX); + DateTime::from_timestamp_millis(millis).unwrap_or_default().to_rfc3339() + } +} + +/// The network stack's resources: a socket for each connection, and one for DHCP. +pub(super) fn stack_resources() -> &'static mut StackResources<{ CONNECTIONS + 1 }> { + static RESOURCES: StaticCell> = StaticCell::new(); + RESOURCES.init(StackResources::new()) +} + +/// Accept connections on [`PORT`] one at a time, serving each until it closes. +#[embassy_executor::task(pool_size = CONNECTIONS)] +async fn connection_task(stack: Stack<'static>, hub: &'static Hub) -> ! { + let mut rx_buffer = [0; TCP_BUFFER]; + let mut tx_buffer = [0; TCP_BUFFER]; + let mut http_buffer = [0; HTTP_BUFFER]; + let config = picoserve_hub::config(); + loop { + let mut socket = TcpSocket::new(stack, &mut rx_buffer, &mut tx_buffer); + if let Err(error) = socket.accept(PORT).await { + warn!("Failed to accept a connection: {}", error); + continue; + } + let client_ip = socket + .remote_endpoint() + .map_or(IpAddr::V4(Ipv4Addr::UNSPECIFIED), |remote| remote.addr.into()); + let served = serve_connection( + hub, + &MODULES, + socket, + client_ip, + EmbassyTimer, + &config, + &mut http_buffer, + ); + if let Err(error) = served.await { + warn!( + "Connection from {} ended: {}", + Debug2Format(&client_ip), + Debug2Format(&error) + ); + } + } +} + +/// Once `stack` has an address, serve the hub on it, its ids drawn from `boot`, a random word. +pub(super) async fn serve(spawner: Spawner, stack: Stack<'static>, boot: u32) { + static HUB: StaticCell> = StaticCell::new(); + stack.wait_config_up().await; + if let Some(config) = stack.config_v4() { + info!("Serving ws://{}:{}/ws", Debug2Format(&config.address.address()), PORT); + } + let stamps = BoardStamps { boot, issued: 0 }; + let hub = Hub::new(stamps, MessageRetention::LatestPerSender, MAX_FRAME_SIZE, MAX_QUEUED); + let hub: &'static Hub = HUB.init(hub); + for _ in 0..CONNECTIONS { + match connection_task(stack, hub) { + Ok(task) => spawner.spawn(task), + Err(error) => warn!("Failed to start a connection task: {}", Debug2Format(&error)), + } + } +} diff --git a/services/ws-server-firmware/src/board/mod.rs b/services/ws-server-firmware/src/board/mod.rs new file mode 100644 index 00000000..4bcabee3 --- /dev/null +++ b/services/ws-server-firmware/src/board/mod.rs @@ -0,0 +1,8 @@ +//! The firmware on a board: each board's module brings up its radio and joins the network named at build time, and +//! [`hub`] serves the hub on it. + +#[cfg(target_arch = "riscv32")] +mod esp32c6; +mod hub; +#[cfg(target_arch = "arm")] +mod pico2w; diff --git a/services/ws-server-firmware/src/board/pico2w.rs b/services/ws-server-firmware/src/board/pico2w.rs new file mode 100644 index 00000000..77cac655 --- /dev/null +++ b/services/ws-server-firmware/src/board/pico2w.rs @@ -0,0 +1,104 @@ +//! The Raspberry Pi Pico 2 W: bring up the CYW43439's Wi-Fi, join the network named at build time, and serve the hub +//! on it. + +#![expect( + clippy::future_not_send, + clippy::single_call_fn, + clippy::unwrap_used, + reason = "embassy's thread-mode executor runs every task on the one core; each is spawned once, or bring-up panics" +)] + +use cyw43::JoinOptions; +use cyw43_pio::{DEFAULT_CLOCK_DIVIDER, PioSpi}; +use defmt::{Debug2Format, warn}; +use embassy_executor::Spawner; +use embassy_net::{Config as NetConfig, DhcpConfig}; +use embassy_rp::gpio::{Level, Output}; +use embassy_rp::peripherals::{DMA_CH0, PIO0, TRNG}; +use embassy_rp::pio::{self, Pio}; +use embassy_rp::trng::{self, Trng}; +use embassy_rp::{bind_interrupts, dma}; +use embedded_alloc::LlffHeap as Heap; +use static_cell::StaticCell; +use {defmt_rtt as _, panic_probe as _}; + +use super::hub::{HEAP_SIZE, serve, stack_resources}; + +include!(concat!(env!("OUT_DIR"), "/wifi.rs")); +include!(concat!(env!("OUT_DIR"), "/cyw43.rs")); + +#[global_allocator] +static HEAP: Heap = Heap::empty(); + +bind_interrupts!(struct Irqs { + DMA_IRQ_0 => dma::InterruptHandler; + PIO0_IRQ_0 => pio::InterruptHandler; + TRNG_IRQ => trng::InterruptHandler; +}); + +type Radio = cyw43::Runner<'static, cyw43::SpiBus, PioSpi<'static, PIO0, 0>>>; + +/// Drive the CYW43439, which every frame the board sends or receives passes through. +#[embassy_executor::task] +async fn radio_task(runner: Radio) -> ! { + runner.run().await +} + +/// Drive the network stack over the radio: DHCP, TCP, and every socket the hub serves on. +#[embassy_executor::task] +async fn net_task(mut runner: embassy_net::Runner<'static, cyw43::NetDriver<'static>>) -> ! { + runner.run().await +} + +/// Bring the board up, join the network, and serve the hub. +#[embassy_executor::main] +async fn main(spawner: Spawner) { + #[expect( + unsafe_code, + reason = "handing the allocator its memory is unsafe by nature: nothing else may own that memory" + )] + // SAFETY: this runs once, first, before anything allocates, and `init!` declares the memory it hands over. + unsafe { + embedded_alloc::init!(HEAP, HEAP_SIZE); + } + let peripherals = embassy_rp::init(embassy_rp::config::Config::default()); + let mut trng = Trng::new(peripherals.TRNG, Irqs, trng::Config::default()); + + let power = Output::new(peripherals.PIN_23, Level::Low); + let chip_select = Output::new(peripherals.PIN_25, Level::High); + let mut pio = Pio::new(peripherals.PIO0, Irqs); + let spi = PioSpi::new( + &mut pio.common, + pio.sm0, + DEFAULT_CLOCK_DIVIDER, + pio.irq0, + chip_select, + peripherals.PIN_24, + peripherals.PIN_29, + dma::Channel::new(peripherals.DMA_CH0, Irqs), + ); + static RADIO: StaticCell = StaticCell::new(); + let radio_state = RADIO.init(cyw43::State::new()); + let (device, mut control, radio) = cyw43::new(radio_state, power, spi, RADIO_FIRMWARE, RADIO_NVRAM).await; + spawner.spawn(radio_task(radio).unwrap()); + control.init(RADIO_CLM).await; + control + .set_power_management(cyw43::PowerManagementMode::PowerSave) + .await; + + let (stack, network) = embassy_net::new( + device, + NetConfig::dhcpv4(DhcpConfig::default()), + stack_resources(), + trng.blocking_next_u64(), + ); + spawner.spawn(net_task(network).unwrap()); + + while let Err(error) = control + .join(WIFI_NETWORK, JoinOptions::new(WIFI_PASSWORD.as_bytes())) + .await + { + warn!("Failed to join {}: {}; retrying", WIFI_NETWORK, Debug2Format(&error)); + } + serve(spawner, stack, trng.blocking_next_u32()).await; +} diff --git a/services/ws-server-pico2w/src/host.rs b/services/ws-server-firmware/src/host.rs similarity index 100% rename from services/ws-server-pico2w/src/host.rs rename to services/ws-server-firmware/src/host.rs diff --git a/services/ws-server-pico2w/src/main.rs b/services/ws-server-firmware/src/main.rs similarity index 51% rename from services/ws-server-pico2w/src/main.rs rename to services/ws-server-firmware/src/main.rs index 28dcdc47..158417bf 100644 --- a/services/ws-server-pico2w/src/main.rs +++ b/services/ws-server-firmware/src/main.rs @@ -1,22 +1,23 @@ -//! et-ws-server as firmware for the Raspberry Pi Pico 2 W: the hub's `/ws`, `/health`, `/favicon.ico` and the modules -//! built into it, on port 8080, over the board's own Wi-Fi. +//! et-ws-server as firmware: the hub's `/ws`, `/health`, `/favicon.ico` and the modules built into it, on port 8080, +//! over a board's own Wi-Fi. //! -//! Built for any other target, the same hub serves the same modules from the host, over tokio, so what the board would -//! serve can be explored without one. -#![cfg_attr(all(target_arch = "arm", target_os = "none"), no_std, no_main)] +//! The target picks the board: bare-metal Arm is the Raspberry Pi Pico 2 W, and bare-metal RISC-V the ESP32-C6. Built +//! for any target with an operating system, the same hub serves the same modules from the host, over tokio, so what a +//! board would serve can be explored without one. +#![cfg_attr(target_os = "none", no_std, no_main)] #![recursion_limit = "256"] -#[cfg(all(target_arch = "arm", target_os = "none"))] +#[cfg(target_os = "none")] extern crate alloc; use edge_toolkit::ports::Services; -#[cfg(all(target_arch = "arm", target_os = "none"))] -mod firmware; -#[cfg(not(all(target_arch = "arm", target_os = "none")))] +#[cfg(target_os = "none")] +mod board; +#[cfg(not(target_os = "none"))] mod host; -#[cfg(not(all(target_arch = "arm", target_os = "none")))] +#[cfg(not(target_os = "none"))] use self::host::main; /// The port the hub serves on, as the `std` server's plain-HTTP listener does. diff --git a/services/ws-server-pico2w/tests/serves_modules.rs b/services/ws-server-firmware/tests/serves_modules.rs similarity index 99% rename from services/ws-server-pico2w/tests/serves_modules.rs rename to services/ws-server-firmware/tests/serves_modules.rs index 246f4bb5..28ae5c47 100644 --- a/services/ws-server-pico2w/tests/serves_modules.rs +++ b/services/ws-server-firmware/tests/serves_modules.rs @@ -23,7 +23,7 @@ impl Server { /// The binary, started on a free loopback port, once it has said which. fn start() -> Self { // The raw child, since the server is killed rather than left to exit with a status to check. - let mut child = std::process::Command::new(env!("CARGO_BIN_EXE_et-ws-server-pico2w")) + let mut child = std::process::Command::new(env!("CARGO_BIN_EXE_et-ws-server-firmware")) .arg("127.0.0.1:0") .stdout(Stdio::piped()) .spawn() diff --git a/services/ws-server-pico2w/README.md b/services/ws-server-pico2w/README.md deleted file mode 100644 index c13ad3c5..00000000 --- a/services/ws-server-pico2w/README.md +++ /dev/null @@ -1,56 +0,0 @@ -# et-int-ws-server-pico2w - -et-ws-server as firmware for the Raspberry Pi Pico 2 W, built as the `et-ws-server-pico2w` binary: the `no_std` -hub's `/ws`, with `/health`, `/favicon.ico` and the modules built into it beside it, served over the board's own -Wi-Fi on port 8080. It joins the network it was built for, takes an address over DHCP, and serves four connections -at once. - -It is a local hub, with no backplane, admin API, idle timeout or registry file. The board has no clock, so the times -it stamps on messages are its uptime counted from the Unix epoch. - -## Building - -The build bakes in the network to join, from `WIFI_NETWORK` and `WIFI_PASSWORD`, and the CYW43439 radio's firmware. -The build task fetches the radio's `43439A0.bin`, `43439A0_clm.bin` and `nvram_rp2040.bin` from the embassy -repository's `cyw43-firmware` directory into `target/cyw43-firmware`, and points `CYW43_FIRMWARE_DIR` at them. -Infineon licenses those blobs separately from this project, so they are not part of it. - -It serves no modules unless told which to build in, through the variables the `std` server reads, each optional: - -- `MODULES_PATHS`: the module directories, comma-separated and absolute. Each is built in from its `pkg/` when it has - one, otherwise from the directory itself: every file its `package.json` lists under `files`, or every file there. -- `MODULES_ROOT`: the name of the module served at `/`, one of those. -- `MODULES_ELSEWHERE` and `MODULES_ELSEWHERE_URL`: module names too big for the board's 4 MiB of flash, listed with - the rest at `/modules/`, and the base URL a request for one of their files is redirected to. - -`et-cli generate-deployment --output-type firmware-pico2` writes the task that sets all of these for a scenario. - -```sh -MISE_ENV=rust mise run build-firmware-pico2w -``` - -The firmware is `target/thumbv8m.main-none-eabihf/release/et-ws-server-pico2w`, an ELF. Load it with picotool while -the board is in BOOTSEL mode, or with probe-rs over a debug probe, which also shows its defmt log: - -```sh -picotool load -x -t elf target/thumbv8m.main-none-eabihf/release/et-ws-server-pico2w -``` - -```sh -probe-rs run --chip RP235x target/thumbv8m.main-none-eabihf/release/et-ws-server-pico2w -``` - -The log names the address it serves on, as `ws://
:8080/ws`. - -## Serving from the host - -Built for any other target, the binary is the same hub serving the same modules from the host over tokio, read from -the same `MODULES_*` at build time, so what a board would serve can be explored without one. It listens on the -address its one argument names, or on port 8080 on every interface, and prints where: - -```sh -cargo run -p et-int-ws-server-pico2w -- 127.0.0.1:8080 -``` - -The package's test starts it on a free port and fetches every module it lists, so run with the `MODULES_*` a -scenario's deployment sets, as its `firmware-host-test` task does, it checks what that board would serve. diff --git a/services/ws-server-pico2w/build.rs b/services/ws-server-pico2w/build.rs deleted file mode 100644 index 3e5677fe..00000000 --- a/services/ws-server-pico2w/build.rs +++ /dev/null @@ -1,106 +0,0 @@ -//! Puts the Pico 2 W's memory layout where the linker looks, links the firmware with cortex-m-rt's and defmt's -//! linker scripts, and generates `board.rs` and `modules.rs`: what the firmware bakes in, read here from the build's -//! environment. On any other target the binary serves the same modules from the host, so only `modules.rs` is made. - -use std::env; -use std::error::Error; -use std::path::PathBuf; - -use et_firmware_modules::{generate, include_path}; -use fs_err as fs; - -/// The variables `board.rs` is generated from. -const BOARD_VARIABLES: [&str; 3] = ["CYW43_FIRMWARE_DIR", "WIFI_NETWORK", "WIFI_PASSWORD"]; -/// The variables `modules.rs` is generated from, each named as the `std` server's modules service or `MODULES_*` reads. -const MODULE_VARIABLES: [&str; 4] = [MODULES_PATHS, MODULES_ROOT, MODULES_ELSEWHERE, MODULES_ELSEWHERE_URL]; -/// The module directories to build in, comma-separated, as the `std` server's `MODULES_PATHS` lists them. -const MODULES_PATHS: &str = "MODULES_PATHS"; -/// The name of the module served at `/`, as the `std` server's `MODULES_ROOT`. -const MODULES_ROOT: &str = "MODULES_ROOT"; -/// The names of the modules served from `MODULES_ELSEWHERE_URL` rather than built in, comma-separated. -const MODULES_ELSEWHERE: &str = "MODULES_ELSEWHERE"; -/// Where a request for a module in `MODULES_ELSEWHERE` is redirected, its path under `/modules` appended. -const MODULES_ELSEWHERE_URL: &str = "MODULES_ELSEWHERE_URL"; -/// The operating system cargo is building for, `none` for the board. -const CARGO_CFG_TARGET_OS: &str = "CARGO_CFG_TARGET_OS"; -/// The directory cargo gives this build script for what it generates. -const OUT_DIR: &str = "OUT_DIR"; - -fn main() -> Result<(), Box> { - println!("cargo:rerun-if-changed=memory.x"); - for variable in BOARD_VARIABLES.into_iter().chain(MODULE_VARIABLES) { - println!("cargo:rerun-if-env-changed={variable}"); - } - let out_dir = PathBuf::from(env::var(OUT_DIR)?); - // The modules are served on the host too, so what the board would serve can be explored without one. - let modules = generate( - &optional(MODULES_PATHS), - &optional(MODULES_ROOT), - &optional(MODULES_ELSEWHERE), - &optional(MODULES_ELSEWHERE_URL), - )?; - for file in &modules.files { - println!("cargo:rerun-if-changed={}", file.display()); - } - fs::write(out_dir.join("modules.rs"), modules.source)?; - if env::var(CARGO_CFG_TARGET_OS)? != "none" { - return Ok(()); - } - fs::write(out_dir.join("memory.x"), include_bytes!("memory.x"))?; - fs::write(out_dir.join("board.rs"), board()?)?; - println!("cargo:rustc-link-search={}", out_dir.display()); - println!("cargo:rustc-link-arg-bins=--nmagic"); - println!("cargo:rustc-link-arg-bins=-Tlink.x"); - println!("cargo:rustc-link-arg-bins=-Tdefmt.x"); - Ok(()) -} - -/// The variable `name`, which the firmware cannot be built without. -fn required(name: &str) -> Result> { - let Ok(value) = env::var(name) else { - return Err(format!("the Pico 2 W firmware needs {name} set when it is built").into()); - }; - Ok(value) -} - -/// The variable `name`, empty when it is unset, since every module variable may be. -fn optional(name: &str) -> String { - env::var(name).unwrap_or_default() -} - -/// The source of `board.rs`: the network to join, and the radio's blobs from `CYW43_FIRMWARE_DIR`. -#[expect( - clippy::single_call_fn, - reason = "the board generation step of main, kept apart from its linker directives" -)] -fn board() -> Result> { - let firmware_dir = PathBuf::from(required("CYW43_FIRMWARE_DIR")?); - let blob = |file: &str| -> Result> { - let path = firmware_dir.join(file); - if !path.is_file() { - return Err(format!("CYW43_FIRMWARE_DIR holds no {file}").into()); - } - Ok(format!( - "include_bytes!({})", - include_path("CYW43_FIRMWARE_DIR", &path)? - )) - }; - let network = required("WIFI_NETWORK")?; - let password = required("WIFI_PASSWORD")?; - let firmware = blob("43439A0.bin")?; - let clm = blob("43439A0_clm.bin")?; - let nvram = blob("nvram_rp2040.bin")?; - Ok(format!( - "/// The network the board joins. -const WIFI_NETWORK: &str = {network:?}; -/// The network's passphrase. -const WIFI_PASSWORD: &str = {password:?}; -/// The CYW43439's firmware. -static RADIO_FIRMWARE: &cyw43::Aligned = &cyw43::Aligned(*{firmware}); -/// The CYW43439's country locale matrix. -static RADIO_CLM: &[u8] = {clm}; -/// The CYW43439's configuration for the Pico 2 W's module. -static RADIO_NVRAM: &cyw43::Aligned = &cyw43::Aligned(*{nvram}); -" - )) -} diff --git a/services/ws-server-pico2w/src/firmware.rs b/services/ws-server-pico2w/src/firmware.rs deleted file mode 100644 index d53d6071..00000000 --- a/services/ws-server-pico2w/src/firmware.rs +++ /dev/null @@ -1,189 +0,0 @@ -//! The firmware: bring up the CYW43439's Wi-Fi, join the network named at build time, take an address over DHCP, and -//! serve the hub on as many sockets at once as there are connection tasks. - -#![expect( - clippy::future_not_send, - clippy::single_call_fn, - clippy::unwrap_used, - reason = "embassy's thread-mode executor runs every task on the one core; each is spawned once, or bring-up panics" -)] - -use alloc::format; -use alloc::string::String; -use core::net::{IpAddr, Ipv4Addr}; - -use chrono::DateTime; -use cyw43::JoinOptions; -use cyw43_pio::{DEFAULT_CLOCK_DIVIDER, PioSpi}; -use defmt::{Debug2Format, info, warn}; -use edge_toolkit::ws_server::MessageRetention; -use embassy_executor::Spawner; -use embassy_net::tcp::TcpSocket; -use embassy_net::{Config as NetConfig, DhcpConfig, Stack, StackResources}; -use embassy_rp::gpio::{Level, Output}; -use embassy_rp::peripherals::{DMA_CH0, PIO0, TRNG}; -use embassy_rp::pio::{self, Pio}; -use embassy_rp::trng::{self, Trng}; -use embassy_rp::{bind_interrupts, dma}; -use embassy_time::Instant; -use embedded_alloc::LlffHeap as Heap; -use et_ws_server::picoserve_server::serve; -use et_ws_service::hub::Stamps; -use et_ws_service::picoserve_hub::{self, Hub}; -use picoserve::time::EmbassyTimer; -use static_cell::StaticCell; -use {defmt_rtt as _, panic_probe as _}; - -use crate::{HTTP_BUFFER, MAX_FRAME_SIZE, MAX_QUEUED, MODULES, PORT}; - -/// How many connections the hub serves at once: one task, and one socket's buffers, each. -const CONNECTIONS: usize = 4; -/// Each connection's TCP receive and transmit buffer. -const TCP_BUFFER: usize = 4 * 1024; -/// The heap the hub's registry, routing and frame buffers allocate from. -const HEAP_SIZE: usize = 160 * 1024; - -include!(concat!(env!("OUT_DIR"), "/board.rs")); - -#[global_allocator] -static HEAP: Heap = Heap::empty(); - -bind_interrupts!(struct Irqs { - DMA_IRQ_0 => dma::InterruptHandler; - PIO0_IRQ_0 => pio::InterruptHandler; - TRNG_IRQ => trng::InterruptHandler; -}); - -/// Ids unique across boots -- a word the TRNG draws at boot, then a count -- and the time since boot. -/// -/// The board has no clock, so a time is its uptime counted from the Unix epoch. -struct BoardStamps { - boot: u32, - issued: u32, -} - -impl Stamps for BoardStamps { - fn new_id(&mut self) -> String { - self.issued = self.issued.wrapping_add(1); - format!("{:08x}-{}", self.boot, self.issued) - } - - fn now(&mut self) -> String { - let millis = i64::try_from(Instant::now().as_millis()).unwrap_or(i64::MAX); - DateTime::from_timestamp_millis(millis).unwrap_or_default().to_rfc3339() - } -} - -type Radio = cyw43::Runner<'static, cyw43::SpiBus, PioSpi<'static, PIO0, 0>>>; - -#[embassy_executor::task] -async fn radio_task(runner: Radio) -> ! { - runner.run().await -} - -#[embassy_executor::task] -async fn net_task(mut runner: embassy_net::Runner<'static, cyw43::NetDriver<'static>>) -> ! { - runner.run().await -} - -/// Accept connections on [`PORT`] one at a time, serving each until it closes. -#[embassy_executor::task(pool_size = CONNECTIONS)] -async fn connection_task(stack: Stack<'static>, hub: &'static Hub) -> ! { - let mut rx_buffer = [0; TCP_BUFFER]; - let mut tx_buffer = [0; TCP_BUFFER]; - let mut http_buffer = [0; HTTP_BUFFER]; - let config = picoserve_hub::config(); - loop { - let mut socket = TcpSocket::new(stack, &mut rx_buffer, &mut tx_buffer); - if let Err(error) = socket.accept(PORT).await { - warn!("Failed to accept a connection: {}", error); - continue; - } - let client_ip = socket - .remote_endpoint() - .map_or(IpAddr::V4(Ipv4Addr::UNSPECIFIED), |remote| remote.addr.into()); - let served = serve( - hub, - &MODULES, - socket, - client_ip, - EmbassyTimer, - &config, - &mut http_buffer, - ); - if let Err(error) = served.await { - warn!( - "Connection from {} ended: {}", - Debug2Format(&client_ip), - Debug2Format(&error) - ); - } - } -} - -#[embassy_executor::main] -async fn main(spawner: Spawner) { - #[expect( - unsafe_code, - reason = "handing the allocator its memory is unsafe by nature: nothing else may own that memory" - )] - // SAFETY: this runs once, first, before anything allocates, and `init!` declares the memory it hands over. - unsafe { - embedded_alloc::init!(HEAP, HEAP_SIZE); - } - let peripherals = embassy_rp::init(embassy_rp::config::Config::default()); - let mut trng = Trng::new(peripherals.TRNG, Irqs, trng::Config::default()); - - let power = Output::new(peripherals.PIN_23, Level::Low); - let chip_select = Output::new(peripherals.PIN_25, Level::High); - let mut pio = Pio::new(peripherals.PIO0, Irqs); - let spi = PioSpi::new( - &mut pio.common, - pio.sm0, - DEFAULT_CLOCK_DIVIDER, - pio.irq0, - chip_select, - peripherals.PIN_24, - peripherals.PIN_29, - dma::Channel::new(peripherals.DMA_CH0, Irqs), - ); - static RADIO: StaticCell = StaticCell::new(); - let radio_state = RADIO.init(cyw43::State::new()); - let (device, mut control, radio) = cyw43::new(radio_state, power, spi, RADIO_FIRMWARE, RADIO_NVRAM).await; - spawner.spawn(radio_task(radio).unwrap()); - control.init(RADIO_CLM).await; - control - .set_power_management(cyw43::PowerManagementMode::PowerSave) - .await; - - static NETWORK: StaticCell> = StaticCell::new(); - let (stack, network) = embassy_net::new( - device, - NetConfig::dhcpv4(DhcpConfig::default()), - NETWORK.init(StackResources::new()), - trng.blocking_next_u64(), - ); - spawner.spawn(net_task(network).unwrap()); - - while let Err(error) = control - .join(WIFI_NETWORK, JoinOptions::new(WIFI_PASSWORD.as_bytes())) - .await - { - warn!("Failed to join {}: {}; retrying", WIFI_NETWORK, Debug2Format(&error)); - } - stack.wait_config_up().await; - if let Some(config) = stack.config_v4() { - info!("Serving ws://{}:{}/ws", Debug2Format(&config.address.address()), PORT); - } - - let stamps = BoardStamps { - boot: trng.blocking_next_u32(), - issued: 0, - }; - static HUB: StaticCell> = StaticCell::new(); - let hub = Hub::new(stamps, MessageRetention::LatestPerSender, MAX_FRAME_SIZE, MAX_QUEUED); - let hub: &'static Hub = HUB.init(hub); - for _ in 0..CONNECTIONS { - spawner.spawn(connection_task(stack, hub).unwrap()); - } -} diff --git a/utilities/cli/HELP.md b/utilities/cli/HELP.md index f11bf904..afafb766 100644 --- a/utilities/cli/HELP.md +++ b/utilities/cli/HELP.md @@ -47,6 +47,8 @@ Generate deployment config from a cluster input YAML - `k3s` - `firmware-pico2`: The hub alone, as firmware for a Raspberry Pi Pico 2 W + - `firmware-esp32c6`: + The hub alone, as firmware for an ESP32-C6 ## `et-cli regen-verification` diff --git a/utilities/cli/src/deployment_types/firmware.rs b/utilities/cli/src/deployment_types/firmware.rs index 9ed138c1..00fad4ff 100644 --- a/utilities/cli/src/deployment_types/firmware.rs +++ b/utilities/cli/src/deployment_types/firmware.rs @@ -86,7 +86,7 @@ pub(crate) fn generate_firmware_deployment( Value::Array(vec![Value::String(BUILD_TASK.to_owned())]), ); let flash_run = format!("{} {}", board.load_command, board.image); - let flash_description = format!("Load the firmware onto a {}, and start it", board.title); + let flash_description = format!("Load the {} firmware onto the board, and start it", board.title); let flash = mise_task( None, Some(&flash_description), diff --git a/utilities/cli/src/input.rs b/utilities/cli/src/input.rs index ad04ce38..807ff584 100644 --- a/utilities/cli/src/input.rs +++ b/utilities/cli/src/input.rs @@ -18,6 +18,11 @@ const REPOSITORY_URL: &str = et_org::REPOSITORY_URL; /// The name a cluster takes when its input does not choose one. pub const DEFAULT_CLUSTER_NAME: &str = "default"; +/// The package every board's firmware is built from, its target picking the board. +const FIRMWARE_PACKAGE: &str = "et-int-ws-server-firmware"; +/// What every board's build needs told about the network it joins. +const WIFI_SETUP: &str = "Set `WIFI_NETWORK` and `WIFI_PASSWORD` to the Wi-Fi network the board joins."; + /// The deployment format a scenario is rendered into. /// /// An enum rather than the string it is written as, because the set is closed and every consumer switches on it: as @@ -42,6 +47,9 @@ pub enum OutputType { /// The hub alone, as firmware for a Raspberry Pi Pico 2 W. #[serde(rename = "firmware-pico2", alias = "firmware_pico2")] FirmwarePico2, + /// The hub alone, as firmware for an ESP32-C6. + #[serde(rename = "firmware-esp32c6", alias = "firmware_esp32c6")] + FirmwareEsp32c6, } /// A board a `firmware-*` format builds the hub for, each the board of exactly one format. @@ -52,6 +60,8 @@ pub enum OutputType { pub(crate) enum Board { /// The Raspberry Pi Pico 2 W, over its own Wi-Fi. Pico2, + /// Any ESP32-C6 board, over the chip's own Wi-Fi. + Esp32c6, } /// What building, loading and describing one board's firmware takes. @@ -79,7 +89,7 @@ pub(crate) struct BoardSpec { impl Board { /// Every board, each with a `firmware-*` format of its own. - pub(crate) const ALL: &'static [Self] = &[Self::Pico2]; + pub(crate) const ALL: &'static [Self] = &[Self::Pico2, Self::Esp32c6]; /// What building, loading and describing this board's firmware takes. #[must_use] @@ -89,11 +99,22 @@ impl Board { output_type: OutputType::FirmwarePico2, title: "Raspberry Pi Pico 2 W", build_task: "build-firmware-pico2w", - package: "et-int-ws-server-pico2w", - image: "target/thumbv8m.main-none-eabihf/release/et-ws-server-pico2w", + package: FIRMWARE_PACKAGE, + image: "target/thumbv8m.main-none-eabihf/release/et-ws-server-firmware", load_command: "picotool load -x -t elf", load_preparation: "hold the board's BOOTSEL button while plugging it in", - network_setup: "Set `WIFI_NETWORK` and `WIFI_PASSWORD` to the Wi-Fi network the board joins.", + network_setup: WIFI_SETUP, + flash: "4 MiB", + }, + Self::Esp32c6 => BoardSpec { + output_type: OutputType::FirmwareEsp32c6, + title: "ESP32-C6", + build_task: "build-firmware-esp32c6", + package: FIRMWARE_PACKAGE, + image: "target/riscv32imac-unknown-none-elf/release/et-ws-server-firmware", + load_command: "espflash flash --monitor --log-format defmt", + load_preparation: "plug the board in over USB, where espflash finds it", + network_setup: WIFI_SETUP, flash: "4 MiB", }, } @@ -114,6 +135,7 @@ impl OutputType { Self::DockerCompose, Self::K3s, Self::FirmwarePico2, + Self::FirmwareEsp32c6, ]; /// The file this format is written to, beside the others in the deployment directory. @@ -126,6 +148,7 @@ impl OutputType { Self::Dekit => "dekit.yaml", Self::ProcessCompose => "process-compose.yaml", Self::FirmwarePico2 => "mise.firmware-pico2.toml", + Self::FirmwareEsp32c6 => "mise.firmware-esp32c6.toml", } } @@ -139,6 +162,7 @@ impl OutputType { Self::Dekit => "dekit", Self::ProcessCompose => "process-compose", Self::FirmwarePico2 => "firmware-pico2", + Self::FirmwareEsp32c6 => "firmware-esp32c6", } } @@ -148,7 +172,10 @@ impl OutputType { /// the board runs no agents at all. #[must_use] pub const fn runs_schedules(self) -> bool { - !matches!(self, Self::Mise | Self::Dekit | Self::FirmwarePico2) + !matches!( + self, + Self::Mise | Self::Dekit | Self::FirmwarePico2 | Self::FirmwareEsp32c6 + ) } /// Whether this format runs the tasks of a `mise.toml`, so needs what that file names installed first. diff --git a/utilities/cli/src/lib.rs b/utilities/cli/src/lib.rs index 3c9456ed..c9d29a52 100644 --- a/utilities/cli/src/lib.rs +++ b/utilities/cli/src/lib.rs @@ -654,6 +654,7 @@ fn generate_format( generate_process_compose_deployment(cluster, output_dir) } OutputType::FirmwarePico2 => generate_firmware_deployment(cluster, output_dir, Board::Pico2), + OutputType::FirmwareEsp32c6 => generate_firmware_deployment(cluster, output_dir, Board::Esp32c6), } } @@ -858,7 +859,13 @@ fn output_summary(cluster: &ClusterInput, output_types: &[OutputType]) -> String .map(|output_file| format!("`{output_file}`")) .collect::>() .join(", "); - format!("This directory contains generated deployment configs for the `{cluster_name}` scenario.\nFiles: {listed}.") + // Broken only between names, so the list fits the line length however many formats it holds. + let files = textwrap::Options::new(120) + .break_words(false) + .word_separator(textwrap::WordSeparator::AsciiSpace) + .word_splitter(textwrap::WordSplitter::NoHyphenation); + let files = textwrap::fill(&format!("Files: {listed}."), files); + format!("This directory contains generated deployment configs for the `{cluster_name}` scenario.\n{files}") } /// How the README's `docker build` of the scenario image runs: what it needs, and the paths it names. @@ -1126,15 +1133,15 @@ fn generated_run_instructions( OutputType::DockerCompose => docker_compose_run_instructions(cluster, artifacts), OutputType::K3s => k3s_run_instructions(cluster, image_build, runners, artifacts, build_contexts), OutputType::FirmwarePico2 => firmware_run_instructions(Board::Pico2.spec()), + OutputType::FirmwareEsp32c6 => firmware_run_instructions(Board::Esp32c6.spec()), } } /// Render the README section that builds the hub as firmware for `board` and loads it onto one. -#[expect(clippy::single_call_fn, reason = "the firmware step of generated_run_instructions")] fn firmware_run_instructions(board: BoardSpec) -> String { format!( concat!( - "## Run As Firmware On A {title}\n\n", + "## Run As {title} Firmware\n\n", "The board runs the hub alone and serves this scenario's modules from its {flash} of flash.\n", "Build those modules first, since the firmware takes each one's built package.\n", "{network}\n", @@ -1266,7 +1273,7 @@ fn machine_instructions(output_type: OutputType, cluster: &ClusterInput) -> Stri ), OutputType::K3s => return k3s_machine_instructions(cluster, server), // Refused before anything is written: the board is one machine. - OutputType::FirmwarePico2 => return String::default(), + OutputType::FirmwarePico2 | OutputType::FirmwareEsp32c6 => return String::default(), }; let roles = machine_roles(cluster, server); let loopback = loopback_note(cluster); diff --git a/utilities/cli/src/limits.rs b/utilities/cli/src/limits.rs index d7c74e8c..d96067ea 100644 --- a/utilities/cli/src/limits.rs +++ b/utilities/cli/src/limits.rs @@ -541,7 +541,9 @@ pub(crate) fn limits_note(output_type: OutputType, cluster: &ClusterInput) -> St // heading. let heading = match output_type { // The board runs no agent, and only the one hub, which has nothing else to be limited against. - OutputType::DockerCompose | OutputType::K3s | OutputType::FirmwarePico2 => return String::default(), + OutputType::DockerCompose | OutputType::K3s | OutputType::FirmwarePico2 | OutputType::FirmwareEsp32c6 => { + return String::default(); + } OutputType::Mise => "Mise", OutputType::Dekit => "dekit", OutputType::ProcessCompose => "process-compose", diff --git a/utilities/cli/tests/firmware.rs b/utilities/cli/tests/firmware.rs index 47c13cf6..81d4e4b3 100644 --- a/utilities/cli/tests/firmware.rs +++ b/utilities/cli/tests/firmware.rs @@ -1,5 +1,6 @@ -//! Covers the firmware formats through `firmware-pico2`: which modules a firmware build embeds and which it -//! redirects, and the scenarios it refuses because a board runs one hub and nothing else. +//! Covers the firmware formats, mostly through `firmware-pico2`: which modules a firmware build embeds and which it +//! redirects, the scenarios it refuses because a board runs one hub and nothing else, and what `firmware-esp32c6` +//! builds and loads differently. #![cfg(test)] use std::path::PathBuf; @@ -44,29 +45,28 @@ struct Task { run: String, } -/// What generating `input` in `test_root` as `firmware-pico2` returns, the error's text or empty, and its directory. -fn generate_in(test_root: TempDir, input: &str) -> (TempDir, PathBuf, String) { +/// What generating `input` in `test_root` as `format` returns, the error's text or empty, and its directory. +fn generate_in(test_root: TempDir, input: &str, format: OutputType) -> (TempDir, PathBuf, String) { let input_file = test_root.path().join("board.yaml"); fs::write(&input_file, input).unwrap(); let output_dir = test_root.path().join("output"); - let outcome = generate_deployment(&input_file, &output_dir, Some(OutputType::FirmwarePico2)) + let outcome = generate_deployment(&input_file, &output_dir, Some(format)) .map_or_else(|error| error.to_string(), |_summary| String::default()); (test_root, output_dir, outcome) } -/// The deployment `input` generates, which has to succeed. +/// The `firmware-pico2` deployment `input` generates, which has to succeed. fn generated(input: &str) -> (TempDir, FirmwareConfig, String) { - generated_in(tempdir().unwrap(), input) + generated_in(tempdir().unwrap(), input, OutputType::FirmwarePico2) } -/// The deployment `input`, written to a scenario in `test_root`, generates, which has to succeed. -fn generated_in(test_root: TempDir, input: &str) -> (TempDir, FirmwareConfig, String) { - let (test_root, output_dir, outcome) = generate_in(test_root, input); +/// The deployment `input`, written to a scenario in `test_root`, generates as `format`, which has to succeed. +fn generated_in(test_root: TempDir, input: &str, format: OutputType) -> (TempDir, FirmwareConfig, String) { + let (test_root, output_dir, outcome) = generate_in(test_root, input, format); assert_eq!(outcome, ""); - assert_eq!(OutputType::FirmwarePico2.output_file_name(), CONFIG_FILE); - let config = toml::from_str(&fs::read_to_string(output_dir.join(CONFIG_FILE)).unwrap()).unwrap(); + let config = fs::read_to_string(output_dir.join(format.output_file_name())).unwrap(); let readme = fs::read_to_string(output_dir.join("README.md")).unwrap(); - (test_root, config, readme) + (test_root, toml::from_str(&config).unwrap(), readme) } #[test] @@ -89,10 +89,26 @@ fn it_embeds_the_repository_modules_and_redirects_the_npm_packages() { assert_eq!(config.tasks.flash.depends, ["firmware-build"]); assert_eq!(config.tasks.flash.dir, build.dir); assert!(config.tasks.flash.run.starts_with("picotool load")); - assert!(readme.contains("## Run As Firmware On A Raspberry Pi Pico 2 W")); + assert!(readme.contains("## Run As Raspberry Pi Pico 2 W Firmware")); assert!(readme.contains("MISE_ENV=firmware-pico2 mise run firmware-flash")); } +#[test] +fn an_esp32c6_is_built_from_the_same_package_and_loaded_with_espflash() { + let (_root, pico, _readme) = generated(BROWSER); + let (_root, esp, readme) = generated_in(tempdir().unwrap(), BROWSER, OutputType::FirmwareEsp32c6); + assert_eq!(esp.tasks.build.env, pico.tasks.build.env); + assert!(esp.tasks.build.run.contains("mise -E rust run build-firmware-esp32c6")); + assert_eq!(esp.tasks.host_test.run, pico.tasks.host_test.run); + let image = "target/riscv32imac-unknown-none-elf/release/et-ws-server-firmware"; + assert_eq!( + esp.tasks.flash.run, + format!("espflash flash --monitor --log-format defmt {image}") + ); + assert!(readme.contains("## Run As ESP32-C6 Firmware")); + assert!(readme.contains("MISE_ENV=firmware-esp32c6 mise run firmware-flash")); +} + #[test] fn its_host_test_serves_the_modules_the_build_embeds() { let (_root, config, readme) = generated(BROWSER); @@ -102,7 +118,7 @@ fn its_host_test_serves_the_modules_the_build_embeds() { let (paths, _) = build.run.split_once("mise -E rust").unwrap(); let (host_paths, command) = host_test.run.split_once("mise run").unwrap(); assert_eq!(host_paths, paths); - assert_eq!(command, " cargo-test-pkg et-int-ws-server-pico2w\n"); + assert_eq!(command, " cargo-test-pkg et-int-ws-server-firmware\n"); assert!(readme.contains("MISE_ENV=firmware-pico2 mise run firmware-host-test")); } @@ -121,7 +137,7 @@ fn it_embeds_a_module_the_scenario_keeps_beside_it() { let input = format!("{BROWSER}module_paths: [\"mine\"]\n").replace(r#"type: "math1""#, r#"type: "mine""#); let module_path = module_dir.display().to_string(); - let (_root, config, _readme) = generated_in(test_root, &input); + let (_root, config, _readme) = generated_in(test_root, &input, OutputType::FirmwarePico2); assert!(config.tasks.build.run.contains(&module_path)); } @@ -135,7 +151,7 @@ fn it_refuses_what_the_board_cannot_run() { (machines, "spans several machines"), (published, "uses published artifacts"), ] { - let (_root, output_dir, outcome) = generate_in(tempdir().unwrap(), &input); + let (_root, output_dir, outcome) = generate_in(tempdir().unwrap(), &input, OutputType::FirmwarePico2); assert!(outcome.starts_with("The Raspberry Pi Pico 2 W firmware runs one hub and nothing else")); assert!(outcome.contains(reason)); assert!(!output_dir.join(CONFIG_FILE).exists()); @@ -154,8 +170,11 @@ fn verification_renders_it_only_for_a_scenario_the_board_can_run() { let _regenerated = regenerate_verification(&verification_root, None).unwrap(); let output = verification_root.join("local/output"); + let esp_file = OutputType::FirmwareEsp32c6.output_file_name(); assert!(output.join("browser").join(CONFIG_FILE).is_file()); + assert!(output.join("browser").join(esp_file).is_file()); assert!(output.join("browser/mise.toml").is_file()); assert!(!output.join("headless").join(CONFIG_FILE).exists()); + assert!(!output.join("headless").join(esp_file).exists()); assert!(output.join("headless/mise.toml").is_file()); } diff --git a/utilities/cli/tests/input.rs b/utilities/cli/tests/input.rs index 775e685b..88f63c4b 100644 --- a/utilities/cli/tests/input.rs +++ b/utilities/cli/tests/input.rs @@ -111,6 +111,16 @@ fn every_deployment_type_spelling_deserializes_to_its_own_output_file() { ("process_compose", OutputType::ProcessCompose, "process-compose.yaml"), ("firmware-pico2", OutputType::FirmwarePico2, "mise.firmware-pico2.toml"), ("firmware_pico2", OutputType::FirmwarePico2, "mise.firmware-pico2.toml"), + ( + "firmware-esp32c6", + OutputType::FirmwareEsp32c6, + "mise.firmware-esp32c6.toml", + ), + ( + "firmware_esp32c6", + OutputType::FirmwareEsp32c6, + "mise.firmware-esp32c6.toml", + ), ] { let cluster = parse(&format!("{ONLY_A_NAME}deployment_type: {spelling}\n")); @@ -121,11 +131,11 @@ fn every_deployment_type_spelling_deserializes_to_its_own_output_file() { cluster.deployment_type.runs_mise_tasks(), !matches!( expected, - OutputType::DockerCompose | OutputType::K3s | OutputType::FirmwarePico2 + OutputType::DockerCompose | OutputType::K3s | OutputType::FirmwarePico2 | OutputType::FirmwareEsp32c6 ) ); } - assert_eq!(OutputType::ALL.len(), 6, "and the list of them is complete"); + assert_eq!(OutputType::ALL.len(), 7, "and the list of them is complete"); } #[test] diff --git a/utilities/cli/tests/machines.rs b/utilities/cli/tests/machines.rs index c07151e9..7832d4ac 100644 --- a/utilities/cli/tests/machines.rs +++ b/utilities/cli/tests/machines.rs @@ -287,12 +287,15 @@ fn the_readme_says_how_to_run_each_machine_share() { let (_root, dir) = generated(&format!("{AGENTS}{MACHINES}")); let readme = read(&dir, "README.md"); - // Every format but the board's, which refuses a scenario spanning machines. + // Every format but the boards', which refuse a scenario spanning machines. + let boards = [OutputType::FirmwarePico2, OutputType::FirmwareEsp32c6]; assert_eq!( readme.matches("### Run Across Machines With").count(), - OutputType::ALL.len() - 1 + OutputType::ALL.len() - boards.len() ); - assert!(!dir.join(OutputType::FirmwarePico2.output_file_name()).exists()); + for board in boards { + assert!(!dir.join(board.output_file_name()).exists()); + } assert!(readme.contains("mise run machine-edge")); assert!(readme.contains("docker compose --profile edge up --build")); assert!(readme.contains("dekit start +edge")); diff --git a/utilities/cli/tests/recovery_regeneration.rs b/utilities/cli/tests/recovery_regeneration.rs index faa7e6ae..dea5b670 100644 --- a/utilities/cli/tests/recovery_regeneration.rs +++ b/utilities/cli/tests/recovery_regeneration.rs @@ -38,11 +38,11 @@ fn verification_generation_limits_recovery_scenarios_to_supported_formats() { assert!(readme.contains("services/recovery-supervisor/Dockerfile")); } -/// Every format a scenario with runners renders: all but the board's, which runs none. +/// Every format a scenario with runners renders: all but the boards', which run none. fn runner_formats() -> impl Iterator { OutputType::ALL .iter() - .filter(|kind| **kind != OutputType::FirmwarePico2) + .filter(|kind| !matches!(kind, OutputType::FirmwarePico2 | OutputType::FirmwareEsp32c6)) } /// Start with actual outputs from every generator, rather than fabricated files that may not match their layout. diff --git a/verification/local/output/default/README.md b/verification/local/output/default/README.md index 964ec8bc..7eae0e29 100644 --- a/verification/local/output/default/README.md +++ b/verification/local/output/default/README.md @@ -1,7 +1,8 @@ # default This directory contains generated deployment configs for the `default` scenario. -Files: `mise.toml`, `dekit.yaml`, `process-compose.yaml`, `compose.yaml`, `k3s.yaml`, `mise.firmware-pico2.toml`. +Files: `mise.toml`, `dekit.yaml`, `process-compose.yaml`, `compose.yaml`, `k3s.yaml`, `mise.firmware-pico2.toml`, +`mise.firmware-esp32c6.toml`. No workflow modules were selected in the scenario input. @@ -153,7 +154,7 @@ kubectl get pods -n "$ns" --watch Nothing outside the cluster can reach `ws-server`, so no browser can open its page. List `ws` and `wss` under `services.hub.expose` in the scenario input and regenerate to publish them. -## Run As Firmware On A Raspberry Pi Pico 2 W +## Run As Raspberry Pi Pico 2 W Firmware The board runs the hub alone and serves this scenario's modules from its 4 MiB of flash. Build those modules first, since the firmware takes each one's built package. @@ -176,3 +177,27 @@ The board takes an address over DHCP and serves the page at port 8080 of it. npm modules use that are too big for its flash, such as onnxruntime-web, are not built in: the board redirects a request for one to the same path under `https://cdn.jsdelivr.net/npm`, which the browser has to be able to reach. + +## Run As ESP32-C6 Firmware + +The board runs the hub alone and serves this scenario's modules from its 4 MiB of flash. +Build those modules first, since the firmware takes each one's built package. +Set `WIFI_NETWORK` and `WIFI_PASSWORD` to the Wi-Fi network the board joins. +Then plug the board in over USB, where espflash finds it. +From this directory, build the firmware and load it with: + +```bash +MISE_ENV=firmware-esp32c6 mise run firmware-flash +``` + +To check what the board will serve without one, serve the same modules from this host and fetch +each, as a page would: + +```bash +MISE_ENV=firmware-esp32c6 mise run firmware-host-test +``` + +The board takes an address over DHCP and serves the page at port 8080 of it. npm packages the +modules use that are too big for its flash, such as onnxruntime-web, are not built in: the board +redirects a request for one to the same path under `https://cdn.jsdelivr.net/npm`, which the browser has to be +able to reach. diff --git a/verification/local/output/default/mise.firmware-esp32c6.toml b/verification/local/output/default/mise.firmware-esp32c6.toml new file mode 100644 index 00000000..265cd76f --- /dev/null +++ b/verification/local/output/default/mise.firmware-esp32c6.toml @@ -0,0 +1,33 @@ +[tasks.firmware-build] +description = "Build the ESP32-C6 firmware serving default" +dir = "../../../.." +run = """ +MODULES_PATHS="$PWD/services/ws-server/static, $PWD/services/ws-wasm-agent" +export MODULES_PATHS +mise -E rust run build-firmware-esp32c6 +""" + +[tasks.firmware-build.env] +MODULES_ELSEWHERE = "onnxruntime-web, stats-gl" +MODULES_ELSEWHERE_URL = "https://cdn.jsdelivr.net/npm" +MODULES_ROOT = "@edge-toolkit/et-ws-server-static" + +[tasks.firmware-flash] +depends = ["firmware-build"] +description = "Load the ESP32-C6 firmware onto the board, and start it" +dir = "../../../.." +run = "espflash flash --monitor --log-format defmt target/riscv32imac-unknown-none-elf/release/et-ws-server-firmware" + +[tasks.firmware-host-test] +description = "Serve default's modules from this host as the board would, and fetch each" +dir = "../../../.." +run = """ +MODULES_PATHS="$PWD/services/ws-server/static, $PWD/services/ws-wasm-agent" +export MODULES_PATHS +mise run cargo-test-pkg et-int-ws-server-firmware +""" + +[tasks.firmware-host-test.env] +MODULES_ELSEWHERE = "onnxruntime-web, stats-gl" +MODULES_ELSEWHERE_URL = "https://cdn.jsdelivr.net/npm" +MODULES_ROOT = "@edge-toolkit/et-ws-server-static" diff --git a/verification/local/output/default/mise.firmware-pico2.toml b/verification/local/output/default/mise.firmware-pico2.toml index 02048ffe..1cce7973 100644 --- a/verification/local/output/default/mise.firmware-pico2.toml +++ b/verification/local/output/default/mise.firmware-pico2.toml @@ -14,9 +14,9 @@ MODULES_ROOT = "@edge-toolkit/et-ws-server-static" [tasks.firmware-flash] depends = ["firmware-build"] -description = "Load the firmware onto a Raspberry Pi Pico 2 W, and start it" +description = "Load the Raspberry Pi Pico 2 W firmware onto the board, and start it" dir = "../../../.." -run = "picotool load -x -t elf target/thumbv8m.main-none-eabihf/release/et-ws-server-pico2w" +run = "picotool load -x -t elf target/thumbv8m.main-none-eabihf/release/et-ws-server-firmware" [tasks.firmware-host-test] description = "Serve default's modules from this host as the board would, and fetch each" @@ -24,7 +24,7 @@ dir = "../../../.." run = """ MODULES_PATHS="$PWD/services/ws-server/static, $PWD/services/ws-wasm-agent" export MODULES_PATHS -mise run cargo-test-pkg et-int-ws-server-pico2w +mise run cargo-test-pkg et-int-ws-server-firmware """ [tasks.firmware-host-test.env] diff --git a/verification/local/output/facility-security-scenario/README.md b/verification/local/output/facility-security-scenario/README.md index f9924ba8..fe07f144 100644 --- a/verification/local/output/facility-security-scenario/README.md +++ b/verification/local/output/facility-security-scenario/README.md @@ -1,7 +1,8 @@ # facility-security-scenario This directory contains generated deployment configs for the `facility-security-scenario` scenario. -Files: `mise.toml`, `dekit.yaml`, `process-compose.yaml`, `compose.yaml`, `k3s.yaml`, `mise.firmware-pico2.toml`. +Files: `mise.toml`, `dekit.yaml`, `process-compose.yaml`, `compose.yaml`, `k3s.yaml`, `mise.firmware-pico2.toml`, +`mise.firmware-esp32c6.toml`. The scenario exposes these workflow modules: face-detection, har1, pyface1. @@ -153,7 +154,7 @@ kubectl get pods -n "$ns" --watch Nothing outside the cluster can reach `ws-server`, so no browser can open its page. List `ws` and `wss` under `services.hub.expose` in the scenario input and regenerate to publish them. -## Run As Firmware On A Raspberry Pi Pico 2 W +## Run As Raspberry Pi Pico 2 W Firmware The board runs the hub alone and serves this scenario's modules from its 4 MiB of flash. Build those modules first, since the firmware takes each one's built package. @@ -176,3 +177,27 @@ The board takes an address over DHCP and serves the page at port 8080 of it. npm modules use that are too big for its flash, such as onnxruntime-web, are not built in: the board redirects a request for one to the same path under `https://cdn.jsdelivr.net/npm`, which the browser has to be able to reach. + +## Run As ESP32-C6 Firmware + +The board runs the hub alone and serves this scenario's modules from its 4 MiB of flash. +Build those modules first, since the firmware takes each one's built package. +Set `WIFI_NETWORK` and `WIFI_PASSWORD` to the Wi-Fi network the board joins. +Then plug the board in over USB, where espflash finds it. +From this directory, build the firmware and load it with: + +```bash +MISE_ENV=firmware-esp32c6 mise run firmware-flash +``` + +To check what the board will serve without one, serve the same modules from this host and fetch +each, as a page would: + +```bash +MISE_ENV=firmware-esp32c6 mise run firmware-host-test +``` + +The board takes an address over DHCP and serves the page at port 8080 of it. npm packages the +modules use that are too big for its flash, such as onnxruntime-web, are not built in: the board +redirects a request for one to the same path under `https://cdn.jsdelivr.net/npm`, which the browser has to be +able to reach. diff --git a/verification/local/output/facility-security-scenario/mise.firmware-esp32c6.toml b/verification/local/output/facility-security-scenario/mise.firmware-esp32c6.toml new file mode 100644 index 00000000..b5697969 --- /dev/null +++ b/verification/local/output/facility-security-scenario/mise.firmware-esp32c6.toml @@ -0,0 +1,41 @@ +[tasks.firmware-build] +description = "Build the ESP32-C6 firmware serving facility-security-scenario" +dir = "../../../.." +run = """ +MODULES_PATHS="$PWD/data/model-modules/model-face1, $PWD/data/model-modules/model-har-motion1, $PWD/generated/python-ws" +MODULES_PATHS="$MODULES_PATHS, $PWD/libs/python-edge-toolkit, $PWD/libs/python-org" +MODULES_PATHS="$MODULES_PATHS, $PWD/services/ws-modules/face-detection, $PWD/services/ws-modules/har1" +MODULES_PATHS="$MODULES_PATHS, $PWD/services/ws-modules/pyface1, $PWD/services/ws-server/static" +MODULES_PATHS="$MODULES_PATHS, $PWD/services/ws-wasm-agent" +export MODULES_PATHS +mise -E rust run build-firmware-esp32c6 +""" + +[tasks.firmware-build.env] +MODULES_ELSEWHERE = "onnxruntime-web, pyodide, stats-gl" +MODULES_ELSEWHERE_URL = "https://cdn.jsdelivr.net/npm" +MODULES_ROOT = "@edge-toolkit/et-ws-server-static" + +[tasks.firmware-flash] +depends = ["firmware-build"] +description = "Load the ESP32-C6 firmware onto the board, and start it" +dir = "../../../.." +run = "espflash flash --monitor --log-format defmt target/riscv32imac-unknown-none-elf/release/et-ws-server-firmware" + +[tasks.firmware-host-test] +description = "Serve facility-security-scenario's modules from this host as the board would, and fetch each" +dir = "../../../.." +run = """ +MODULES_PATHS="$PWD/data/model-modules/model-face1, $PWD/data/model-modules/model-har-motion1, $PWD/generated/python-ws" +MODULES_PATHS="$MODULES_PATHS, $PWD/libs/python-edge-toolkit, $PWD/libs/python-org" +MODULES_PATHS="$MODULES_PATHS, $PWD/services/ws-modules/face-detection, $PWD/services/ws-modules/har1" +MODULES_PATHS="$MODULES_PATHS, $PWD/services/ws-modules/pyface1, $PWD/services/ws-server/static" +MODULES_PATHS="$MODULES_PATHS, $PWD/services/ws-wasm-agent" +export MODULES_PATHS +mise run cargo-test-pkg et-int-ws-server-firmware +""" + +[tasks.firmware-host-test.env] +MODULES_ELSEWHERE = "onnxruntime-web, pyodide, stats-gl" +MODULES_ELSEWHERE_URL = "https://cdn.jsdelivr.net/npm" +MODULES_ROOT = "@edge-toolkit/et-ws-server-static" diff --git a/verification/local/output/facility-security-scenario/mise.firmware-pico2.toml b/verification/local/output/facility-security-scenario/mise.firmware-pico2.toml index 66724077..e775faed 100644 --- a/verification/local/output/facility-security-scenario/mise.firmware-pico2.toml +++ b/verification/local/output/facility-security-scenario/mise.firmware-pico2.toml @@ -18,9 +18,9 @@ MODULES_ROOT = "@edge-toolkit/et-ws-server-static" [tasks.firmware-flash] depends = ["firmware-build"] -description = "Load the firmware onto a Raspberry Pi Pico 2 W, and start it" +description = "Load the Raspberry Pi Pico 2 W firmware onto the board, and start it" dir = "../../../.." -run = "picotool load -x -t elf target/thumbv8m.main-none-eabihf/release/et-ws-server-pico2w" +run = "picotool load -x -t elf target/thumbv8m.main-none-eabihf/release/et-ws-server-firmware" [tasks.firmware-host-test] description = "Serve facility-security-scenario's modules from this host as the board would, and fetch each" @@ -32,7 +32,7 @@ MODULES_PATHS="$MODULES_PATHS, $PWD/services/ws-modules/face-detection, $PWD/ser MODULES_PATHS="$MODULES_PATHS, $PWD/services/ws-modules/pyface1, $PWD/services/ws-server/static" MODULES_PATHS="$MODULES_PATHS, $PWD/services/ws-wasm-agent" export MODULES_PATHS -mise run cargo-test-pkg et-int-ws-server-pico2w +mise run cargo-test-pkg et-int-ws-server-firmware """ [tasks.firmware-host-test.env]