diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index f105dcfc1..27556de9e 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -6,7 +6,7 @@ This is an embedded controller (EC) services workspace — a collection of `no_s ## Build, Test, and Lint -Toolchain: Rust 1.88 (`rust-toolchain.toml`), edition 2024. Targets: `x86_64-unknown-linux-gnu` (std/testing) and `thumbv8m.main-none-eabihf` (ARM Cortex-M33). +Toolchain: Rust 1.90 (`rust-toolchain.toml`), edition 2024. Targets: `x86_64-unknown-linux-gnu` (std/testing) and `thumbv8m.main-none-eabihf` (ARM Cortex-M33). ```shell # Format @@ -196,3 +196,13 @@ Basic development tools (git, cargo, editors) should not be listed. AI agents **must** verify their own identity (agent name and model version) before composing the `Assisted-by` trailer — do not assume or hard-code a model name from a previous session. AI agents **MUST NOT** add `Signed-off-by` tags. Only humans can certify the Developer Certificate of Origin. + +## Rust PR Review Instructions +CI overview: +* CI will build the project and run `cargo test` and `cargo clippy`. +* Feature combinations are checked with `cargo hack`. +* Do not comment on compile errors, compiler warnings, or clippy warnings. + +Pay special attention to... +* code that uses async selection APIs such as `select`, `selectN`, `select_array`, `select_slice`, or is marked with a drop safety comment. These functions drop the futures that don't finish. Check that values are not lost when this happens. +* code that could possibly panic or is marked with a panic safety comment. diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index bdf764815..84f273f84 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -31,6 +31,9 @@ concurrency: group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} cancel-in-progress: true name: check +env: + # Crates that require std and won't build on embedded-targets + STD_CRATES: "fw-update-interface-mocks" jobs: fmt: @@ -93,6 +96,8 @@ jobs: # Get early warning of new lints which are regularly introduced in beta channels. toolchain: [stable, beta] target: [x86_64-unknown-linux-gnu, thumbv8m.main-none-eabihf] + env: + COMMON_HACK_ARGS: "--feature-powerset --mutually-exclusive-features=log,defmt,defmt-timestamp-uptime" steps: - uses: actions/checkout@v4 with: @@ -112,7 +117,11 @@ jobs: # intentionally no target specifier; see https://github.com/jonhoo/rust-ci-conf/pull/4 # --feature-powerset runs for every combination of features - name: cargo hack - run: cargo hack --feature-powerset --mutually-exclusive-features=log,defmt,defmt-timestamp-uptime clippy --locked --target ${{ matrix.target }} + if: ${{ matrix.target == 'x86_64-unknown-linux-gnu' }} + run: cargo hack $COMMON_HACK_ARGS clippy --locked --target ${{ matrix.target }} + - name: cargo hack + if: ${{ matrix.target != 'x86_64-unknown-linux-gnu' }} + run: cargo hack $COMMON_HACK_ARGS clippy --exclude $STD_CRATES --locked --target ${{ matrix.target }} deny: # cargo-deny checks licenses, advisories, sources, and bans for @@ -148,6 +157,9 @@ jobs: submodules: true - name: Install stable uses: dtolnay/rust-toolchain@stable + with: + # For grcov + components: llvm-tools - name: Download Cargo.lock files if: ${{ inputs.download-lockfiles }} uses: actions/download-artifact@v4 @@ -155,10 +167,25 @@ jobs: name: updated-lock-files - name: cargo test run: cargo test --locked + env: + RUSTFLAGS: '-C instrument-coverage' + CARGO_INCREMENTAL: '0' # After ensuring tests pass, finally ensure the test code itself contains no clippy warnings - name: cargo clippy run: | cargo clippy --locked --tests + # Generate and upload test coverage report + - name: Install grcov + uses: taiki-e/install-action@grcov + - name: Generate test coverage report + run: | + grcov . --binary-path ./target/debug/deps -s . --branch --ignore-not-existing --ignore "/home/runner/*" -t html -o ./target/debug/coverage + grcov . --binary-path ./target/debug/deps -s . --branch --ignore-not-existing --ignore "/home/runner/*" -t markdown >> $GITHUB_STEP_SUMMARY + - name: Upload test coverage report + uses: actions/upload-artifact@v4 + with: + name: test-coverage-report + path: ./target/debug/coverage msrv: # check that we can build using the minimal rust version that is specified by this crate @@ -185,9 +212,15 @@ jobs: with: name: updated-lock-files - name: cargo +${{ matrix.msrv }} check + if: ${{ matrix.target == 'x86_64-unknown-linux-gnu' }} run: | cargo check -F log --locked --target ${{ matrix.target }} cargo check -F defmt --locked --target ${{ matrix.target }} + - name: cargo +${{ matrix.msrv }} check + if: ${{ matrix.target != 'x86_64-unknown-linux-gnu' }} + run: | + cargo check -F log --locked --workspace --exclude $STD_CRATES --target ${{ matrix.target }} + cargo check -F defmt --locked --workspace --exclude $STD_CRATES --target ${{ matrix.target }} check-arm-examples: runs-on: ubuntu-latest @@ -239,6 +272,31 @@ jobs: run: | cargo clippy --locked + check-pico-de-gallo-examples: + runs-on: ubuntu-latest + # we use a matrix here just because env can't be used in job names + # https://docs.github.com/en/actions/learn-github-actions/contexts#context-availability + strategy: + fail-fast: false + matrix: + example_directory: ["examples/pico-de-gallo"] + name: ubuntu / check-examples + steps: + - uses: actions/checkout@v4 + with: + submodules: true + - name: Install stable + uses: dtolnay/rust-toolchain@stable + - name: Download Cargo.lock files + if: ${{ inputs.download-lockfiles }} + uses: actions/download-artifact@v4 + with: + name: updated-lock-files + - name: cargo clippy + working-directory: ${{ matrix.example_directory }} + run: | + cargo clippy --locked + machete: # cargo-machete checks for unused dependencies in Cargo.toml files runs-on: ubuntu-latest diff --git a/.vscode/settings.json b/.vscode/settings.json index b8686b2cb..90b7f0524 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -14,6 +14,7 @@ "Cargo.toml", "examples/rt633/Cargo.toml", "examples/rt685s-evk/Cargo.toml", - "examples/std/Cargo.toml" + "examples/std/Cargo.toml", + "examples/pico-de-gallo/Cargo.toml" ] -} +} \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 3e240778c..c5a132516 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -35,6 +35,56 @@ dependencies = [ "as-slice", ] +[[package]] +name = "anstream" +version = "0.6.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" + +[[package]] +name = "anstyle-parse" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + [[package]] name = "anyhow" version = "1.0.99" @@ -110,6 +160,7 @@ dependencies = [ name = "battery-service" version = "0.1.0" dependencies = [ + "battery-service-interface", "defmt 0.3.100", "embassy-futures", "embassy-sync", @@ -117,6 +168,28 @@ dependencies = [ "embedded-batteries-async", "embedded-services", "log", + "odp-service-common", + "power-policy-interface", +] + +[[package]] +name = "battery-service-interface" +version = "0.1.0" +dependencies = [ + "defmt 0.3.100", + "embedded-batteries-async", + "log", +] + +[[package]] +name = "battery-service-relay" +version = "0.1.0" +dependencies = [ + "battery-service-interface", + "defmt 0.3.100", + "embedded-services", + "log", + "num_enum", ] [[package]] @@ -218,18 +291,6 @@ version = "2.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394" -[[package]] -name = "bitvec" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" -dependencies = [ - "funty", - "radium", - "tap", - "wyz", -] - [[package]] name = "block-device-driver" version = "0.2.0" @@ -270,16 +331,28 @@ checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" name = "cfu-service" version = "0.1.0" dependencies = [ + "critical-section", "defmt 0.3.100", "embassy-futures", "embassy-sync", "embassy-time", "embedded-cfu-protocol", "embedded-services", + "env_logger", + "fw-update-interface", + "fw-update-interface-mocks", "heapless 0.9.2", "log", + "static_cell", + "tokio", ] +[[package]] +name = "colorchoice" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" + [[package]] name = "const-init" version = "1.0.0" @@ -331,9 +404,9 @@ dependencies = [ [[package]] name = "crc" -version = "3.3.0" +version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9710d3b3739c2e349eb44fe848ad0b7c8cb1e42bd87ee49371df2f7acaf3e675" +checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d" dependencies = [ "crc-catalog", ] @@ -362,6 +435,7 @@ version = "0.1.0" dependencies = [ "bbq2", "critical-section", + "debug-service-messages", "defmt 0.3.100", "embassy-sync", "embedded-services", @@ -369,6 +443,15 @@ dependencies = [ "rtt-target", ] +[[package]] +name = "debug-service-messages" +version = "0.1.0" +dependencies = [ + "defmt 0.3.100", + "embedded-services", + "num_enum", +] + [[package]] name = "defmt" version = "0.3.100" @@ -421,6 +504,12 @@ dependencies = [ "embedded-io-async 0.6.1", ] +[[package]] +name = "diff" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" + [[package]] name = "document-features" version = "0.2.11" @@ -570,12 +659,12 @@ dependencies = [ [[package]] name = "embassy-time-queue-utils" -version = "0.3.2" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "168297bf80aaf114b3c9ad589bf38b01b3009b9af7f97cd18086c5bbf96f5693" +checksum = "80e2ee86063bd028a420a5fb5898c18c87a8898026da1d4c852af2c443d0a454" dependencies = [ "embassy-executor-timer-queue", - "heapless 0.9.2", + "heapless 0.8.0", ] [[package]] @@ -741,28 +830,18 @@ name = "embedded-services" version = "0.1.0" dependencies = [ "bitfield 0.17.0", - "bitvec", "cortex-m", - "cortex-m-rt", "critical-section", "defmt 0.3.100", "embassy-futures", "embassy-sync", - "embassy-time", - "embassy-time-driver", - "embedded-batteries-async", - "embedded-cfu-protocol", - "embedded-usb-pd", - "heapless 0.9.2", "log", "mctp-rs", - "num_enum", + "paste", "portable-atomic", - "rstest", "serde", "static_cell", "tokio", - "uuid", ] [[package]] @@ -792,6 +871,29 @@ dependencies = [ "embedded-hal-async", ] +[[package]] +name = "env_filter" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bf3c259d255ca70051b30e2e95b5446cdb8949ac4cd22c0d7fd634d89f568e2" +dependencies = [ + "log", + "regex", +] + +[[package]] +name = "env_logger" +version = "0.11.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f" +dependencies = [ + "anstream", + "anstyle", + "env_filter", + "jiff", + "log", +] + [[package]] name = "equivalent" version = "1.0.2" @@ -824,6 +926,7 @@ dependencies = [ "embedded-services", "log", "mctp-rs", + "odp-service-common", ] [[package]] @@ -839,16 +942,21 @@ dependencies = [ ] [[package]] -name = "funty" -version = "2.0.0" +name = "futures-core" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" [[package]] -name = "futures-core" +name = "futures-macro" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] [[package]] name = "futures-sink" @@ -856,6 +964,51 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-timer" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-core", + "futures-macro", + "futures-task", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "fw-update-interface" +version = "0.1.0" +dependencies = [ + "defmt 0.3.100", + "embedded-services", + "log", + "tokio", +] + +[[package]] +name = "fw-update-interface-mocks" +version = "0.1.0" +dependencies = [ + "embedded-services", + "fw-update-interface", + "tokio", +] + [[package]] name = "generator" version = "0.8.7" @@ -903,9 +1056,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.15.5" +version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" [[package]] name = "heapless" @@ -967,9 +1120,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.11.0" +version = "2.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2481980430f9f78649238835720ddccc57e52df14ffce1c6f37391d61b563e9" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" dependencies = [ "equivalent", "hashbrown", @@ -986,6 +1139,12 @@ dependencies = [ "libc", ] +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + [[package]] name = "itertools" version = "0.10.5" @@ -1013,6 +1172,30 @@ dependencies = [ "either", ] +[[package]] +name = "jiff" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c867c356cc096b33f4981825ab281ecba3db0acefe60329f044c1789d94c6543" +dependencies = [ + "jiff-static", + "log", + "portable-atomic", + "portable-atomic-util", + "serde_core", +] + +[[package]] +name = "jiff-static" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7946b4325269738f270bb55b3c19ab5c5040525f83fd625259422a9d25d9be5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + [[package]] name = "keyberon" version = "0.2.0" @@ -1117,15 +1300,19 @@ dependencies = [ [[package]] name = "mctp-rs" version = "0.1.0" -source = "git+https://github.com/dymk/mctp-rs#3d941ba5205ca7781bf37e3dc7c5dfdc99a082d6" dependencies = [ "bit-register", + "crc", "defmt 0.3.100", "embedded-batteries", "espi-device", "num_enum", + "pretty_assertions", + "rstest", "smbus-pec", "thiserror", + "tokio", + "uuid", ] [[package]] @@ -1264,12 +1451,26 @@ dependencies = [ "memchr", ] +[[package]] +name = "odp-service-common" +version = "0.1.0" +dependencies = [ + "embedded-services", + "static_cell", +] + [[package]] name = "once_cell" version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + [[package]] name = "partition-manager" version = "0.1.0" @@ -1337,6 +1538,12 @@ version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + [[package]] name = "platform-service" version = "0.1.0" @@ -1357,6 +1564,15 @@ version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" +[[package]] +name = "portable-atomic-util" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a9db96d7fa8782dd8c15ce32ffe8680bbd1e978a43bf51a34d39483540495f5" +dependencies = [ + "portable-atomic", +] + [[package]] name = "power-button-service" version = "0.1.0" @@ -1368,17 +1584,55 @@ dependencies = [ "log", ] +[[package]] +name = "power-policy-interface" +version = "0.1.0" +dependencies = [ + "bitfield 0.17.0", + "critical-section", + "defmt 0.3.100", + "embassy-sync", + "embedded-batteries-async", + "embedded-services", + "log", + "num_enum", +] + [[package]] name = "power-policy-service" version = "0.1.0" dependencies = [ + "critical-section", "defmt 0.3.100", "embassy-futures", "embassy-sync", "embassy-time", + "embedded-batteries-async", "embedded-services", + "env_logger", "heapless 0.9.2", "log", + "power-policy-interface", + "tokio", +] + +[[package]] +name = "pretty_assertions" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ae130e2f271fbc2ac3a40fb1d07180839cdbbe443c7a27e1e3c13c5cac0116d" +dependencies = [ + "diff", + "yansi", +] + +[[package]] +name = "proc-macro-crate" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +dependencies = [ + "toml_edit 0.25.11+spec-1.1.0", ] [[package]] @@ -1445,12 +1699,6 @@ dependencies = [ "proc-macro2", ] -[[package]] -name = "radium" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" - [[package]] name = "rand_core" version = "0.9.5" @@ -1498,6 +1746,8 @@ version = "0.26.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f5a3193c063baaa2a95a33f03035c8a72b83d97a54916055ba22d35ed3839d49" dependencies = [ + "futures-timer", + "futures-util", "rstest_macros", ] @@ -1509,6 +1759,7 @@ checksum = "9c845311f0ff7951c5506121a9ad75aec44d083c31583b2ea5a30bcb0b0abba0" dependencies = [ "cfg-if", "glob", + "proc-macro-crate", "proc-macro2", "quote", "regex", @@ -1550,7 +1801,7 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" dependencies = [ - "semver 1.0.27", + "semver 1.0.28", ] [[package]] @@ -1576,9 +1827,9 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.27" +version = "1.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" [[package]] name = "semver-parser" @@ -1588,18 +1839,28 @@ checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" [[package]] name = "serde" -version = "1.0.219" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.219" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", @@ -1711,12 +1972,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "tap" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" - [[package]] name = "thermal-service" version = "0.1.0" @@ -1730,6 +1985,28 @@ dependencies = [ "embedded-services", "heapless 0.9.2", "log", + "odp-service-common", + "thermal-service-interface", +] + +[[package]] +name = "thermal-service-interface" +version = "0.1.0" +dependencies = [ + "defmt 0.3.100", + "embassy-time", + "embedded-fans-async", + "embedded-sensors-hal-async", +] + +[[package]] +name = "thermal-service-relay" +version = "0.1.0" +dependencies = [ + "defmt 0.3.100", + "embedded-services", + "num_enum", + "thermal-service-interface", "uuid", ] @@ -1762,6 +2039,49 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "time-alarm-service" +version = "0.1.0" +dependencies = [ + "critical-section", + "defmt 0.3.100", + "embassy-futures", + "embassy-sync", + "embassy-time", + "embedded-mcu-hal", + "embedded-services", + "log", + "odp-service-common", + "time-alarm-service", + "time-alarm-service-interface", + "tokio", + "zerocopy", +] + +[[package]] +name = "time-alarm-service-interface" +version = "0.1.0" +dependencies = [ + "bitfield 0.17.0", + "defmt 0.3.100", + "embedded-mcu-hal", + "log", + "num_enum", + "zerocopy", +] + +[[package]] +name = "time-alarm-service-relay" +version = "0.1.0" +dependencies = [ + "defmt 0.3.100", + "embedded-mcu-hal", + "embedded-services", + "log", + "num_enum", + "time-alarm-service-interface", +] + [[package]] name = "tokio" version = "1.47.1" @@ -1797,8 +2117,8 @@ dependencies = [ "indexmap", "serde", "serde_spanned", - "toml_datetime", - "toml_edit", + "toml_datetime 0.6.11", + "toml_edit 0.22.27", ] [[package]] @@ -1810,6 +2130,15 @@ dependencies = [ "serde", ] +[[package]] +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +dependencies = [ + "serde_core", +] + [[package]] name = "toml_edit" version = "0.22.27" @@ -1819,14 +2148,35 @@ dependencies = [ "indexmap", "serde", "serde_spanned", - "toml_datetime", - "winnow", + "toml_datetime 0.6.11", + "winnow 0.7.13", +] + +[[package]] +name = "toml_edit" +version = "0.25.11+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b59c4d22ed448339746c59b905d24568fcbb3ab65a500494f7b8c3e97739f2b" +dependencies = [ + "indexmap", + "toml_datetime 1.1.1+spec-1.1.0", + "toml_parser", + "winnow 1.0.2", +] + +[[package]] +name = "toml_parser" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" +dependencies = [ + "winnow 1.0.2", ] [[package]] name = "tps6699x" version = "0.1.0" -source = "git+https://github.com/OpenDevicePartnership/tps6699x#ba1b3b17ebf048fc007eb2107a4d2ab8cb545adf" +source = "git+https://github.com/OpenDevicePartnership/tps6699x?branch=v0.2.0#abe5568183bfe5fb2ea81806dded6cb60f3f9b58" dependencies = [ "bincode", "bitfield 0.19.2", @@ -1904,27 +2254,39 @@ dependencies = [ "tracing-log", ] +[[package]] +name = "type-c-interface" +version = "0.1.0" +dependencies = [ + "bitfield 0.17.0", + "defmt 0.3.100", + "embedded-services", + "embedded-usb-pd", + "heapless 0.9.2", + "log", + "power-policy-interface", +] + [[package]] name = "type-c-service" version = "0.1.0" dependencies = [ "bitfield 0.17.0", "bitflags 2.9.4", - "critical-section", "defmt 0.3.100", "embassy-futures", "embassy-sync", "embassy-time", - "embassy-time-driver", - "embedded-cfu-protocol", "embedded-hal-async", "embedded-services", "embedded-usb-pd", + "fw-update-interface", "heapless 0.9.2", "log", - "static_cell", + "power-policy-interface", "tokio", "tps6699x", + "type-c-interface", ] [[package]] @@ -1933,6 +2295,18 @@ version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" +[[package]] +name = "uart-service" +version = "0.1.0" +dependencies = [ + "defmt 0.3.100", + "embassy-sync", + "embedded-io-async 0.7.0", + "embedded-services", + "log", + "mctp-rs", +] + [[package]] name = "ufmt-write" version = "0.1.0" @@ -1961,6 +2335,12 @@ dependencies = [ "portable-atomic", ] +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + [[package]] name = "uuid" version = "1.17.0" @@ -2021,7 +2401,7 @@ dependencies = [ "windows-collections", "windows-core", "windows-future", - "windows-link", + "windows-link 0.1.3", "windows-numerics", ] @@ -2042,7 +2422,7 @@ checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" dependencies = [ "windows-implement", "windows-interface", - "windows-link", + "windows-link 0.1.3", "windows-result", "windows-strings", ] @@ -2054,7 +2434,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" dependencies = [ "windows-core", - "windows-link", + "windows-link 0.1.3", "windows-threading", ] @@ -2086,6 +2466,12 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + [[package]] name = "windows-numerics" version = "0.2.0" @@ -2093,7 +2479,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" dependencies = [ "windows-core", - "windows-link", + "windows-link 0.1.3", ] [[package]] @@ -2102,7 +2488,7 @@ version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" dependencies = [ - "windows-link", + "windows-link 0.1.3", ] [[package]] @@ -2111,7 +2497,7 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" dependencies = [ - "windows-link", + "windows-link 0.1.3", ] [[package]] @@ -2132,6 +2518,15 @@ dependencies = [ "windows-targets", ] +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link 0.2.1", +] + [[package]] name = "windows-targets" version = "0.52.6" @@ -2154,7 +2549,7 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" dependencies = [ - "windows-link", + "windows-link 0.1.3", ] [[package]] @@ -2215,14 +2610,20 @@ dependencies = [ ] [[package]] -name = "wyz" -version = "0.5.1" +name = "winnow" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +checksum = "2ee1708bef14716a11bae175f579062d4554d95be2c6829f518df847b7b3fdd0" dependencies = [ - "tap", + "memchr", ] +[[package]] +name = "yansi" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" + [[package]] name = "zerocopy" version = "0.8.26" diff --git a/Cargo.toml b/Cargo.toml index 0d445152f..2228fbbb0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,10 +2,15 @@ resolver = "2" members = [ "battery-service", + "battery-service-interface", + "battery-service-relay", "thermal-service", + "thermal-service-interface", + "thermal-service-relay", "cfu-service", "embedded-service", "espi-service", + "uart-service", "hid-service", "partition-manager/generation", "partition-manager/macros", @@ -13,9 +18,19 @@ members = [ "platform-service", "power-button-service", "power-policy-service", + "time-alarm-service", + "time-alarm-service-interface", + "time-alarm-service-relay", "type-c-service", "debug-service", + "debug-service-messages", "keyboard-service", + "power-policy-interface", + "odp-service-common", + "type-c-interface", + "fw-update-interface", + "fw-update-interface-mocks", + "mctp-rs", ] exclude = ["examples/*"] @@ -44,8 +59,12 @@ unreachable = "deny" unwrap_used = "deny" [workspace.dependencies] + +odp-service-common = { path = "./odp-service-common" } aligned = "0.4" anyhow = "1.0" +battery-service-interface = { path = "./battery-service-interface" } +battery-service-relay = { path = "./battery-service-relay" } bitfield = "0.17.0" bitflags = "2.8.0" bitvec = { version = "1.0.1", default-features = false } @@ -54,6 +73,13 @@ cortex-m = "0.7.6" cortex-m-rt = "0.7.5" critical-section = "1.1" defmt = "0.3" +document-features = "0.2.7" +debug-service-messages = { path = "./debug-service-messages" } +embassy-executor = "0.10.0" +cfu-service = { path = "./cfu-service" } +embedded-hal-nb = "1.0" +embedded-io-async = "0.7.0" +embedded-mcu-hal = "0.2.0" embassy-futures = "0.1.2" embassy-imxrt = { git = "https://github.com/OpenDevicePartnership/embassy-imxrt" } embassy-sync = "0.8" @@ -66,9 +92,15 @@ embedded-hal-async = "1.0" embedded-services = { path = "./embedded-service" } embedded-storage-async = "0.4.1" embedded-usb-pd = { git = "https://github.com/OpenDevicePartnership/embedded-usb-pd", default-features = false } -mctp-rs = { git = "https://github.com/dymk/mctp-rs" } +fw-update-interface = { path = "./fw-update-interface" } +fw-update-interface-mocks = { path = "./fw-update-interface-mocks" } +mctp-rs = { path = "./mctp-rs" } num_enum = { version = "0.7.5", default-features = false } portable-atomic = { version = "1.11", default-features = false } +power-policy-interface = { path = "./power-policy-interface" } +paste = "1.0.15" +power-policy-service = { path = "./power-policy-service" } +fixed = "1.23.1" heapless = "0.9.2" log = "0.4" proc-macro2 = "1.0" @@ -77,7 +109,13 @@ rstest = { version = "0.26.1", default-features = false } serde = { version = "1.0.*", default-features = false } static_cell = "2.1.0" toml = { version = "0.8", default-features = false } +thermal-service-interface = { path = "./thermal-service-interface" } +thermal-service-relay = { path = "./thermal-service-relay" } +time-alarm-service-interface = { path = "./time-alarm-service-interface" } +time-alarm-service-relay = { path = "./time-alarm-service-relay" } +type-c-interface = { path = "./type-c-interface" } syn = "2.0" -tps6699x = { git = "https://github.com/OpenDevicePartnership/tps6699x" } +tps6699x = { git = "https://github.com/OpenDevicePartnership/tps6699x", branch = "v0.2.0" } tokio = { version = "1.42.0" } uuid = { version = "=1.17.0", default-features = false } +zerocopy = "0.8" diff --git a/battery-service-interface/Cargo.toml b/battery-service-interface/Cargo.toml new file mode 100644 index 000000000..e9c9538f5 --- /dev/null +++ b/battery-service-interface/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "battery-service-interface" +version.workspace = true +edition.workspace = true +license.workspace = true +repository.workspace = true + +[package.metadata.cargo-machete] +ignored = ["log"] + +[dependencies] +defmt = { workspace = true, optional = true } +log = { workspace = true, optional = true } +embedded-batteries-async.workspace = true + +[lints] +workspace = true + +[features] +defmt = ["dep:defmt", "embedded-batteries-async/defmt"] +log = ["dep:log"] diff --git a/battery-service-interface/src/lib.rs b/battery-service-interface/src/lib.rs new file mode 100644 index 000000000..f93f5caab --- /dev/null +++ b/battery-service-interface/src/lib.rs @@ -0,0 +1,202 @@ +#![no_std] + +pub use embedded_batteries_async::acpi::{ + BatteryState, BatterySwapCapability, BatteryTechnology, Bct, BctReturnResult, Bma, Bmc, BmcControlFlags, Bmd, + BmdCapabilityFlags, BmdStatusFlags, Bms, Bpc, Bps, Bpt, BstReturn, Btm, BtmReturnResult, Btp, PowerSource, + PowerSourceState, PowerThresholdSupport, PowerUnit, PsrReturn, StaReturn, +}; + +/// Standard Battery Service Model Number String Size +pub const STD_BIX_MODEL_SIZE: usize = 8; +/// Standard Battery Service Serial Number String Size +pub const STD_BIX_SERIAL_SIZE: usize = 8; +/// Standard Battery Service Battery Type String Size +pub const STD_BIX_BATTERY_SIZE: usize = 8; +/// Standard Battery Service OEM Info String Size +pub const STD_BIX_OEM_SIZE: usize = 8; +/// Standard Power Policy Service Model Number String Size +pub const STD_PIF_MODEL_SIZE: usize = 8; +/// Standard Power Policy Serial Number String Size +pub const STD_PIF_SERIAL_SIZE: usize = 8; +/// Standard Power Policy Service OEM Info String Size +pub const STD_PIF_OEM_SIZE: usize = 8; + +#[derive(PartialEq, Clone, Copy, Default)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct BixFixedStrings { + /// Revision of the BIX structure. Current revision is 1. + pub revision: u32, + /// Unit used for capacity and rate values. + pub power_unit: PowerUnit, + /// Design capacity of the battery (in mWh or mAh). + pub design_capacity: u32, + /// Last full charge capacity (in mWh or mAh). + pub last_full_charge_capacity: u32, + /// Battery technology type. + pub battery_technology: BatteryTechnology, + /// Design voltage (in mV). + pub design_voltage: u32, + /// Warning capacity threshold (in mWh or mAh). + pub design_cap_of_warning: u32, + /// Low capacity threshold (in mWh or mAh). + pub design_cap_of_low: u32, + /// Number of charge/discharge cycles. + pub cycle_count: u32, + /// Measurement accuracy in thousandths of a percent (e.g., 80000 = 80.000%). + pub measurement_accuracy: u32, + /// Maximum supported sampling time (in ms). + pub max_sampling_time: u32, + /// Minimum supported sampling time (in ms). + pub min_sampling_time: u32, + /// Maximum supported averaging interval (in ms). + pub max_averaging_interval: u32, + /// Minimum supported averaging interval (in ms). + pub min_averaging_interval: u32, + /// Capacity granularity between low and warning (in mWh or mAh). + pub battery_capacity_granularity_1: u32, + /// Capacity granularity between warning and full (in mWh or mAh). + pub battery_capacity_granularity_2: u32, + /// OEM-specific model number (ASCIIZ). + pub model_number: [u8; STD_BIX_MODEL_SIZE], + /// OEM-specific serial number (ASCIIZ). + pub serial_number: [u8; STD_BIX_SERIAL_SIZE], + /// OEM-specific battery type (ASCIIZ). + pub battery_type: [u8; STD_BIX_BATTERY_SIZE], + /// OEM-specific information (ASCIIZ). + pub oem_info: [u8; STD_BIX_OEM_SIZE], + /// Battery swapping capability. + pub battery_swapping_capability: BatterySwapCapability, +} + +#[derive(PartialEq, Clone, Copy)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct PifFixedStrings { + /// Bitfield describing the state and characteristics of the power source. + pub power_source_state: PowerSourceState, + /// Maximum rated output power in milliwatts (mW). + /// + /// 0xFFFFFFFF indicates the value is unavailable. + pub max_output_power: u32, + /// Maximum rated input power in milliwatts (mW). + /// + /// 0xFFFFFFFF indicates the value is unavailable. + pub max_input_power: u32, + /// OEM-specific model number (ASCIIZ). Empty string if not supported. + pub model_number: [u8; STD_PIF_MODEL_SIZE], + /// OEM-specific serial number (ASCIIZ). Empty string if not supported. + pub serial_number: [u8; STD_PIF_SERIAL_SIZE], + /// OEM-specific information (ASCIIZ). Empty string if not supported. + pub oem_info: [u8; STD_PIF_OEM_SIZE], +} + +/// Fuel gauge ID +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct DeviceId(pub u8); + +pub trait BatteryService { + /// Queries the estimated time remaining until the battery reaches the specified charge level. Corresponds to ACPI's _BCT method + fn battery_charge_time( + &self, + battery_id: DeviceId, + charge_level: Bct, + ) -> impl core::future::Future>; + + /// Returns static information about the battery. Corresponds to ACPI's _BIX method. + fn battery_info( + &self, + battery_id: DeviceId, + ) -> impl core::future::Future>; + + /// Sets the averaging interval of battery capacity measurement in milliseconds. Corresponds to ACPI's _BMA method. + fn set_battery_measurement_averaging_interval( + &self, + battery_id: DeviceId, + bma: Bma, + ) -> impl core::future::Future>; + + /// Battery maintenance control. Corresponds to ACPI's _BMC method. + fn battery_maintenance_control( + &self, + battery_id: DeviceId, + bmc: Bmc, + ) -> impl core::future::Future>; + + /// Retrieves battery maintenance data. Corresponds to ACPI's _BMD method. + fn battery_maintenance_data( + &self, + battery_id: DeviceId, + ) -> impl core::future::Future>; + + /// Sets the battery measurement sampling time in milliseconds. Corresponds to ACPI's _BMS method. + fn set_battery_measurement_sampling_time( + &self, + battery_id: DeviceId, + battery_measurement_sampling: Bms, + ) -> impl core::future::Future>; + + /// Queries the current power characteristics of the battery. Corresponds to ACPI's _BPC method. + fn battery_power_characteristics( + &self, + battery_id: DeviceId, + ) -> impl core::future::Future>; + + /// Queries the current state of the battery. Corresponds to ACPI's _BPS method. + fn battery_power_state( + &self, + battery_id: DeviceId, + ) -> impl core::future::Future>; + + /// Sets battery power threshold. Corresponds to ACPI's _BPT method. + fn set_battery_power_threshold( + &self, + battery_id: DeviceId, + power_threshold: Bpt, + ) -> impl core::future::Future>; + + /// Queries the battery's current estimated remaining capacity. Corresponds to ACPI's _BST method. + fn battery_status( + &self, + battery_id: DeviceId, + ) -> impl core::future::Future>; + + /// Queries the estimated time remaining until the battery is fully discharged at the current discharge rate. Corresponds to ACPI's _BTM method. + fn battery_time_to_empty( + &self, + battery_id: DeviceId, + battery_discharge_rate: Btm, + ) -> impl core::future::Future>; + + /// Sets a battery trip point. Corresponds to ACPI's _BTP method. + fn set_battery_trip_point( + &self, + battery_id: DeviceId, + btp: Btp, + ) -> impl core::future::Future>; + + /// Queries whether the battery is currently in use (i.e., providing power to the system). Corresponds to ACPI's _PSR method. + fn is_in_use(&self, battery_id: DeviceId) -> impl core::future::Future>; + + /// Queries information about the battery's power source. Corresponds to ACPI's _PIF method. + fn power_source_information( + &self, + power_source_id: DeviceId, + ) -> impl core::future::Future>; + + /// Queries the battery's status. Corresponds to ACPI's _STA method. + fn device_status( + &self, + battery_id: DeviceId, + ) -> impl core::future::Future>; +} + +#[derive(Copy, Clone, Debug, PartialEq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +/// Errors that can occur when interacting with the battery service. +pub enum BatteryError { + /// The specified battery ID does not correspond to any known battery. + UnknownDeviceId, + + /// An unknown error occurred while processing the request. + UnspecifiedFailure, +} diff --git a/battery-service-relay/Cargo.toml b/battery-service-relay/Cargo.toml new file mode 100644 index 000000000..7d53c8541 --- /dev/null +++ b/battery-service-relay/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "battery-service-relay" +version.workspace = true +edition.workspace = true +license.workspace = true +repository.workspace = true + +[package.metadata.cargo-machete] +ignored = ["log"] + +[dependencies] +defmt = { workspace = true, optional = true } +log = { workspace = true, optional = true } +embedded-services.workspace = true +num_enum.workspace = true + +battery-service-interface.workspace = true + +[features] +defmt = [ + "dep:defmt", + "embedded-services/defmt", + "battery-service-interface/defmt", +] +log = ["dep:log", "embedded-services/log", "battery-service-interface/log"] + +[lints] +workspace = true diff --git a/battery-service-relay/src/lib.rs b/battery-service-relay/src/lib.rs new file mode 100644 index 000000000..92988ae2f --- /dev/null +++ b/battery-service-relay/src/lib.rs @@ -0,0 +1,100 @@ +#![no_std] + +use battery_service_interface::*; +use embedded_services::trace; + +mod serialization; +pub use serialization::{AcpiBatteryError, AcpiBatteryRequest, AcpiBatteryResponse, AcpiBatteryResult}; + +/// Relays messages to and from a battery service implementation over MCTP. +pub struct BatteryServiceRelayHandler { + service: S, +} + +impl BatteryServiceRelayHandler { + /// Create a new relay handler that uses the provided battery service implementation to handle requests. + pub fn new(service: S) -> Self { + Self { service } + } +} + +impl embedded_services::relay::mctp::RelayServiceHandlerTypes + for BatteryServiceRelayHandler +{ + type RequestType = serialization::AcpiBatteryRequest; + type ResultType = serialization::AcpiBatteryResult; +} + +impl embedded_services::relay::mctp::RelayServiceHandler + for BatteryServiceRelayHandler +{ + async fn process_request(&self, request: Self::RequestType) -> Self::ResultType { + trace!("Battery service: ACPI cmd recvd"); + Ok(match request { + AcpiBatteryRequest::GetBix { battery_id } => AcpiBatteryResponse::GetBix { + bix: self.service.battery_info(DeviceId(battery_id)).await?, + }, + AcpiBatteryRequest::GetBst { battery_id } => AcpiBatteryResponse::GetBst { + bst: self.service.battery_status(DeviceId(battery_id)).await?, + }, + AcpiBatteryRequest::GetPsr { battery_id } => AcpiBatteryResponse::GetPsr { + psr: self.service.is_in_use(DeviceId(battery_id)).await?, + }, + AcpiBatteryRequest::GetPif { battery_id } => AcpiBatteryResponse::GetPif { + pif: self.service.power_source_information(DeviceId(battery_id)).await?, + }, + AcpiBatteryRequest::GetBps { battery_id } => AcpiBatteryResponse::GetBps { + bps: self.service.battery_power_state(DeviceId(battery_id)).await?, + }, + AcpiBatteryRequest::SetBtp { battery_id, btp } => { + self.service.set_battery_trip_point(DeviceId(battery_id), btp).await?; + AcpiBatteryResponse::SetBtp {} + } + AcpiBatteryRequest::SetBpt { battery_id, bpt } => { + self.service + .set_battery_power_threshold(DeviceId(battery_id), bpt) + .await?; + AcpiBatteryResponse::SetBpt {} + } + + AcpiBatteryRequest::GetBpc { battery_id } => AcpiBatteryResponse::GetBpc { + bpc: self.service.battery_power_characteristics(DeviceId(battery_id)).await?, + }, + AcpiBatteryRequest::SetBmc { battery_id, bmc } => { + self.service + .battery_maintenance_control(DeviceId(battery_id), bmc) + .await?; + AcpiBatteryResponse::SetBmc {} + } + AcpiBatteryRequest::GetBmd { battery_id } => AcpiBatteryResponse::GetBmd { + bmd: self.service.battery_maintenance_data(DeviceId(battery_id)).await?, + }, + AcpiBatteryRequest::GetBct { battery_id, bct } => AcpiBatteryResponse::GetBct { + bct_response: self.service.battery_charge_time(DeviceId(battery_id), bct).await?, + }, + AcpiBatteryRequest::GetBtm { battery_id, btm } => AcpiBatteryResponse::GetBtm { + btm_response: self.service.battery_time_to_empty(DeviceId(battery_id), btm).await?, + }, + + AcpiBatteryRequest::SetBms { battery_id, bms } => { + self.service + .set_battery_measurement_sampling_time(DeviceId(battery_id), bms) + .await?; + AcpiBatteryResponse::SetBms { + status: 0, // TODO once we have a working reference platform, we should consider dropping this field, since it's redundant with the error type on Result. + } + } + AcpiBatteryRequest::SetBma { battery_id, bma } => { + self.service + .set_battery_measurement_averaging_interval(DeviceId(battery_id), bma) + .await?; + AcpiBatteryResponse::SetBma { + status: 0, // TODO once we have a working reference platform, we should consider dropping this field, since it's redundant with the error type on Result. + } + } + AcpiBatteryRequest::GetSta { battery_id } => AcpiBatteryResponse::GetSta { + sta: self.service.device_status(DeviceId(battery_id)).await?, + }, + }) + } +} diff --git a/battery-service-relay/src/serialization.rs b/battery-service-relay/src/serialization.rs new file mode 100644 index 000000000..39f411a44 --- /dev/null +++ b/battery-service-relay/src/serialization.rs @@ -0,0 +1,611 @@ +use battery_service_interface::*; +use embedded_services::relay::{MessageSerializationError, SerializableMessage}; + +#[derive(num_enum::IntoPrimitive, num_enum::TryFromPrimitive, Copy, Clone, Debug, PartialEq)] +#[repr(u16)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +/// ACPI Battery Methods +enum BatteryCmd { + /// Battery Information eXtended + GetBix = 1, + /// Battery Status + GetBst = 2, + /// Power Source + GetPsr = 3, + /// Power source InFormation + GetPif = 4, + /// Battery Power State + GetBps = 5, + /// Battery Trip Point + SetBtp = 6, + /// Battery Power Threshold + SetBpt = 7, + /// Battery Power Characteristics + GetBpc = 8, + /// Battery Maintenance Control + SetBmc = 9, + /// Battery Maintenance Data + GetBmd = 10, + /// Battery Charge Time + GetBct = 11, + /// Battery Time + GetBtm = 12, + /// Battery Measurement Sampling Time + SetBms = 13, + /// Battery Measurement Averaging Interval + SetBma = 14, + /// Device Status + GetSta = 15, +} + +impl From<&AcpiBatteryRequest> for BatteryCmd { + fn from(request: &AcpiBatteryRequest) -> Self { + match request { + AcpiBatteryRequest::GetBix { .. } => BatteryCmd::GetBix, + AcpiBatteryRequest::GetBst { .. } => BatteryCmd::GetBst, + AcpiBatteryRequest::GetPsr { .. } => BatteryCmd::GetPsr, + AcpiBatteryRequest::GetPif { .. } => BatteryCmd::GetPif, + AcpiBatteryRequest::GetBps { .. } => BatteryCmd::GetBps, + AcpiBatteryRequest::SetBtp { .. } => BatteryCmd::SetBtp, + AcpiBatteryRequest::SetBpt { .. } => BatteryCmd::SetBpt, + AcpiBatteryRequest::GetBpc { .. } => BatteryCmd::GetBpc, + AcpiBatteryRequest::SetBmc { .. } => BatteryCmd::SetBmc, + AcpiBatteryRequest::GetBmd { .. } => BatteryCmd::GetBmd, + AcpiBatteryRequest::GetBct { .. } => BatteryCmd::GetBct, + AcpiBatteryRequest::GetBtm { .. } => BatteryCmd::GetBtm, + AcpiBatteryRequest::SetBms { .. } => BatteryCmd::SetBms, + AcpiBatteryRequest::SetBma { .. } => BatteryCmd::SetBma, + AcpiBatteryRequest::GetSta { .. } => BatteryCmd::GetSta, + } + } +} + +impl From<&AcpiBatteryResponse> for BatteryCmd { + fn from(response: &AcpiBatteryResponse) -> Self { + match response { + AcpiBatteryResponse::GetBix { .. } => BatteryCmd::GetBix, + AcpiBatteryResponse::GetBst { .. } => BatteryCmd::GetBst, + AcpiBatteryResponse::GetPsr { .. } => BatteryCmd::GetPsr, + AcpiBatteryResponse::GetPif { .. } => BatteryCmd::GetPif, + AcpiBatteryResponse::GetBps { .. } => BatteryCmd::GetBps, + AcpiBatteryResponse::SetBtp { .. } => BatteryCmd::SetBtp, + AcpiBatteryResponse::SetBpt { .. } => BatteryCmd::SetBpt, + AcpiBatteryResponse::GetBpc { .. } => BatteryCmd::GetBpc, + AcpiBatteryResponse::SetBmc { .. } => BatteryCmd::SetBmc, + AcpiBatteryResponse::GetBmd { .. } => BatteryCmd::GetBmd, + AcpiBatteryResponse::GetBct { .. } => BatteryCmd::GetBct, + AcpiBatteryResponse::GetBtm { .. } => BatteryCmd::GetBtm, + AcpiBatteryResponse::SetBms { .. } => BatteryCmd::SetBms, + AcpiBatteryResponse::SetBma { .. } => BatteryCmd::SetBma, + AcpiBatteryResponse::GetSta { .. } => BatteryCmd::GetSta, + } + } +} + +#[derive(PartialEq, Clone, Copy)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +/// ACPI battery device message responses as defined in ACPI spec version 6.4, section 10.2 +pub enum AcpiBatteryResponse { + /// Extended battery information. Analogous to the return value of the _BIX method. + GetBix { bix: BixFixedStrings }, + + /// Battery status. Analogous to the return value of the _BST method. + GetBst { bst: BstReturn }, + + /// Power source in use. Analogous to the return value of the _PSR method. + GetPsr { psr: PsrReturn }, + + /// Power source information. Analogous to the return value of the _PIF method. + GetPif { pif: PifFixedStrings }, + + /// Battery power state. Analogous to the return value of the _BPS method. + GetBps { bps: Bps }, + + /// Result of setting a battery trip point. Analogous to the _BTP method. Semantically equivalent to (). + SetBtp {}, + + /// Result of setting a battery power threshold. Analogous to the _BPT method. Semantically equivalent to (). + SetBpt {}, + + /// Battery power characteristics. Analogous to the return value of the _BPC method. + GetBpc { bpc: Bpc }, + + /// Result of performing a battery maintenance control operation. Analogous to the return value of the _BMC method. Semantically equivalent to (). + SetBmc {}, + + /// Battery maintenance data. Analogous to the return value of the _BMD method. + GetBmd { bmd: Bmd }, + + /// Battery charge time. Analogous to the return value of the _BCT method. + GetBct { bct_response: BctReturnResult }, + + /// Battery time to empty. Analogous to the return value of the _BTM method. + GetBtm { btm_response: BtmReturnResult }, + + /// Result of setting the battery measurement sampling time. Analogous to the _BMS method. + SetBms { status: u32 }, + + /// Result of setting the battery measurement averaging interval. Analogous to the _BMA method. + SetBma { status: u32 }, + + /// Battery device status. Analogous to the return value of the _STA method. + GetSta { sta: StaReturn }, +} + +impl SerializableMessage for AcpiBatteryResponse { + fn serialize(self, buffer: &mut [u8]) -> Result { + match self { + Self::GetBix { bix } => bix_to_bytes(bix, buffer), + Self::GetBst { bst } => Ok(safe_put_dword(buffer, 0, bst.battery_state.bits())? + + safe_put_dword(buffer, 4, bst.battery_present_rate)? + + safe_put_dword(buffer, 8, bst.battery_remaining_capacity)? + + safe_put_dword(buffer, 12, bst.battery_present_voltage)?), + Self::GetPsr { psr } => safe_put_dword(buffer, 0, psr.power_source.into()), + + Self::GetPif { pif } => pif_to_bytes(pif, buffer), + Self::GetBps { bps } => Ok(safe_put_dword(buffer, 0, bps.revision)? + + safe_put_dword(buffer, 4, bps.instantaneous_peak_power_level)? + + safe_put_dword(buffer, 8, bps.instantaneous_peak_power_period)? + + safe_put_dword(buffer, 12, bps.sustainable_peak_power_level)? + + safe_put_dword(buffer, 16, bps.sustainable_peak_power_period)?), + Self::SetBtp {} => Ok(0), + Self::SetBpt {} => Ok(0), + Self::GetBpc { bpc } => Ok(safe_put_dword(buffer, 0, bpc.revision)? + + safe_put_dword(buffer, 4, bpc.power_threshold_support.bits())? + + safe_put_dword(buffer, 8, bpc.max_instantaneous_peak_power_threshold)? + + safe_put_dword(buffer, 12, bpc.max_sustainable_peak_power_threshold)?), + Self::SetBmc {} => Ok(0), + Self::GetBmd { bmd } => Ok(safe_put_dword(buffer, 0, bmd.status_flags.bits())? + + safe_put_dword(buffer, 4, bmd.capability_flags.bits())? + + safe_put_dword(buffer, 8, bmd.recalibrate_count)? + + safe_put_dword(buffer, 12, bmd.quick_recalibrate_time)? + + safe_put_dword(buffer, 16, bmd.slow_recalibrate_time)?), + Self::GetBct { bct_response } => safe_put_dword(buffer, 0, bct_response.into()), + Self::GetBtm { btm_response } => safe_put_dword(buffer, 0, btm_response.into()), + Self::SetBms { status } => safe_put_dword(buffer, 0, status), + Self::SetBma { status } => safe_put_dword(buffer, 0, status), + Self::GetSta { sta } => safe_put_dword(buffer, 0, sta.bits()), + } + } + + fn deserialize(discriminant: u16, buffer: &[u8]) -> Result { + Ok( + match BatteryCmd::try_from(discriminant) + .map_err(|_| MessageSerializationError::UnknownMessageDiscriminant(discriminant))? + { + BatteryCmd::GetBix => Self::GetBix { + bix: bix_from_bytes(buffer)?, + }, + BatteryCmd::GetBst => { + let bst = BstReturn { + battery_state: BatteryState::from_bits(safe_get_dword(buffer, 0)?) + .ok_or(MessageSerializationError::InvalidPayload("Invalid BatteryState"))?, + battery_present_rate: safe_get_dword(buffer, 4)?, + battery_remaining_capacity: safe_get_dword(buffer, 8)?, + battery_present_voltage: safe_get_dword(buffer, 12)?, + }; + Self::GetBst { bst } + } + BatteryCmd::GetPsr => Self::GetPsr { + psr: PsrReturn { + power_source: safe_get_dword(buffer, 0)? + .try_into() + .map_err(|_| MessageSerializationError::InvalidPayload("Invalid PowerSource"))?, + }, + }, + BatteryCmd::GetPif => Self::GetPif { + pif: pif_from_bytes(buffer)?, + }, + BatteryCmd::GetBps => Self::GetBps { + bps: Bps { + revision: safe_get_dword(buffer, 0)?, + instantaneous_peak_power_level: safe_get_dword(buffer, 4)?, + instantaneous_peak_power_period: safe_get_dword(buffer, 8)?, + sustainable_peak_power_level: safe_get_dword(buffer, 12)?, + sustainable_peak_power_period: safe_get_dword(buffer, 16)?, + }, + }, + BatteryCmd::SetBtp => Self::SetBtp {}, + BatteryCmd::SetBpt => Self::SetBpt {}, + BatteryCmd::GetBpc => Self::GetBpc { + bpc: Bpc { + revision: safe_get_dword(buffer, 0)?, + power_threshold_support: PowerThresholdSupport::from_bits(safe_get_dword(buffer, 4)?) + .ok_or(MessageSerializationError::InvalidPayload("Invalid BpcThresholdSupport"))?, + max_instantaneous_peak_power_threshold: safe_get_dword(buffer, 8)?, + max_sustainable_peak_power_threshold: safe_get_dword(buffer, 12)?, + }, + }, + BatteryCmd::SetBmc => Self::SetBmc {}, + BatteryCmd::GetBmd => Self::GetBmd { + bmd: Bmd { + status_flags: BmdStatusFlags::from_bits(safe_get_dword(buffer, 0)?) + .ok_or(MessageSerializationError::InvalidPayload("Invalid BmdStatusFlags"))?, + capability_flags: BmdCapabilityFlags::from_bits(safe_get_dword(buffer, 4)?) + .ok_or(MessageSerializationError::InvalidPayload("Invalid BmdCapabilityFlags"))?, + recalibrate_count: safe_get_dword(buffer, 8)?, + quick_recalibrate_time: safe_get_dword(buffer, 12)?, + slow_recalibrate_time: safe_get_dword(buffer, 16)?, + }, + }, + BatteryCmd::GetBct => Self::GetBct { + bct_response: safe_get_dword(buffer, 0)?.into(), + }, + BatteryCmd::GetBtm => Self::GetBtm { + btm_response: safe_get_dword(buffer, 0)?.into(), + }, + BatteryCmd::SetBms => Self::SetBms { + status: safe_get_dword(buffer, 0)?, + }, + BatteryCmd::SetBma => Self::SetBma { + status: safe_get_dword(buffer, 0)?, + }, + BatteryCmd::GetSta => Self::GetSta { + sta: StaReturn::from_bits(safe_get_dword(buffer, 0)?) + .ok_or(MessageSerializationError::InvalidPayload("Invalid STA flags"))?, + }, + }, + ) + } + + fn discriminant(&self) -> u16 { + BatteryCmd::from(self).into() + } +} + +#[derive(PartialEq, Clone, Copy)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum AcpiBatteryRequest { + /// Queries extended battery information. Analogous to ACPI's _BIX method. + GetBix { battery_id: u8 }, + + /// Queries battery status. Analogous to ACPI's _BST method. + GetBst { battery_id: u8 }, + + /// Queries the current power source. Analogous to ACPI's _PSR method. + GetPsr { battery_id: u8 }, + + /// Queries information about the battery's power source. Analogous to ACPI's _PIF method. + GetPif { battery_id: u8 }, + + /// Queries information about the current power delivery capabilities of the battery. Analogous to ACPI's _BPS method. + GetBps { battery_id: u8 }, + + /// Sets a battery trip point. Analogous to ACPI's _BTP method. + SetBtp { battery_id: u8, btp: Btp }, + + /// Sets a battery power threshold. Analogous to ACPI's _BPT method. + SetBpt { battery_id: u8, bpt: Bpt }, + + /// Queries the current power characteristics of the battery. Analogous to ACPI's _BPC method. + GetBpc { battery_id: u8 }, + + /// Performs a battery maintenance control operation. Analogous to ACPI's _BMC method. + SetBmc { battery_id: u8, bmc: Bmc }, + + /// Queries battery maintenance data. Analogous to ACPI's _BMD method. + GetBmd { battery_id: u8 }, + + /// Queries the estimated time remaining to charge the battery to the specified level. Analogous to ACPI's _BCT method. + GetBct { battery_id: u8, bct: Bct }, + + /// Queries the estimated time remaining until the battery is discharged to the specified level. Analogous to ACPI's _BTM method. + GetBtm { battery_id: u8, btm: Btm }, + + /// Sets the sampling time of battery measurements in milliseconds. Analogous to ACPI's _BMS method. + SetBms { battery_id: u8, bms: Bms }, + + /// Sets the averaging interval of battery measurements in milliseconds. Analogous to ACPI's _BMA method. + SetBma { battery_id: u8, bma: Bma }, + + /// Queries the current status of the battery device. Analogous to ACPI's _STA method. + GetSta { battery_id: u8 }, +} + +impl SerializableMessage for AcpiBatteryRequest { + fn serialize(self, buffer: &mut [u8]) -> Result { + match self { + Self::GetBix { battery_id } => safe_put_u8(buffer, 0, battery_id), + Self::GetBst { battery_id } => safe_put_u8(buffer, 0, battery_id), + Self::GetPsr { battery_id } => safe_put_u8(buffer, 0, battery_id), + Self::GetPif { battery_id } => safe_put_u8(buffer, 0, battery_id), + Self::GetBps { battery_id } => safe_put_u8(buffer, 0, battery_id), + Self::SetBtp { battery_id, btp } => { + Ok(safe_put_u8(buffer, 0, battery_id)? + safe_put_dword(buffer, 1, btp.trip_point)?) + } + Self::SetBpt { battery_id, bpt } => Ok(safe_put_u8(buffer, 0, battery_id)? + + safe_put_dword(buffer, 1, bpt.revision)? + + safe_put_dword(buffer, 5, bpt.threshold_id as u32)? + + safe_put_dword(buffer, 9, bpt.threshold_value)?), + Self::GetBpc { battery_id } => safe_put_u8(buffer, 0, battery_id), + Self::SetBmc { battery_id, bmc } => { + Ok(safe_put_u8(buffer, 0, battery_id)? + + safe_put_dword(buffer, 1, bmc.maintenance_control_flags.bits())?) + } + Self::GetBmd { battery_id } => safe_put_u8(buffer, 0, battery_id), + Self::GetBct { battery_id, bct } => { + Ok(safe_put_u8(buffer, 0, battery_id)? + safe_put_dword(buffer, 1, bct.charge_level_percent)?) + } + Self::GetBtm { battery_id, btm } => { + Ok(safe_put_u8(buffer, 0, battery_id)? + safe_put_dword(buffer, 1, btm.discharge_rate)?) + } + Self::SetBms { battery_id, bms } => { + Ok(safe_put_u8(buffer, 0, battery_id)? + safe_put_dword(buffer, 1, bms.sampling_time_ms)?) + } + Self::SetBma { battery_id, bma } => { + Ok(safe_put_u8(buffer, 0, battery_id)? + safe_put_dword(buffer, 1, bma.averaging_interval_ms)?) + } + Self::GetSta { battery_id } => safe_put_u8(buffer, 0, battery_id), + } + } + + fn deserialize(discriminant: u16, buffer: &[u8]) -> Result { + Ok( + match BatteryCmd::try_from(discriminant) + .map_err(|_| MessageSerializationError::UnknownMessageDiscriminant(discriminant))? + { + BatteryCmd::GetBix => Self::GetBix { + battery_id: safe_get_u8(buffer, 0)?, + }, + BatteryCmd::GetBst => Self::GetBst { + battery_id: safe_get_u8(buffer, 0)?, + }, + BatteryCmd::GetPsr => Self::GetPsr { + battery_id: safe_get_u8(buffer, 0)?, + }, + BatteryCmd::GetPif => Self::GetPif { + battery_id: safe_get_u8(buffer, 0)?, + }, + BatteryCmd::GetBps => Self::GetBps { + battery_id: safe_get_u8(buffer, 0)?, + }, + BatteryCmd::SetBtp => Self::SetBtp { + battery_id: safe_get_u8(buffer, 0)?, + btp: Btp { + trip_point: safe_get_dword(buffer, 1)?, + }, + }, + BatteryCmd::SetBpt => Self::SetBpt { + battery_id: safe_get_u8(buffer, 0)?, + bpt: Bpt { + revision: safe_get_dword(buffer, 1)?, + threshold_id: safe_get_dword(buffer, 5)? + .try_into() + .map_err(|_| MessageSerializationError::InvalidPayload("Invalid ThresholdId"))?, + threshold_value: safe_get_dword(buffer, 9)?, + }, + }, + BatteryCmd::GetBpc => Self::GetBpc { + battery_id: safe_get_u8(buffer, 0)?, + }, + BatteryCmd::SetBmc => Self::SetBmc { + battery_id: safe_get_u8(buffer, 0)?, + bmc: Bmc { + maintenance_control_flags: BmcControlFlags::from_bits_retain(safe_get_dword(buffer, 1)?), + }, + }, + BatteryCmd::GetBmd => Self::GetBmd { + battery_id: safe_get_u8(buffer, 0)?, + }, + BatteryCmd::GetBct => Self::GetBct { + battery_id: safe_get_u8(buffer, 0)?, + bct: Bct { + charge_level_percent: safe_get_dword(buffer, 1)?, + }, + }, + BatteryCmd::GetBtm => Self::GetBtm { + battery_id: safe_get_u8(buffer, 0)?, + btm: Btm { + discharge_rate: safe_get_dword(buffer, 1)?, + }, + }, + BatteryCmd::SetBms => Self::SetBms { + battery_id: safe_get_u8(buffer, 0)?, + bms: Bms { + sampling_time_ms: safe_get_dword(buffer, 1)?, + }, + }, + BatteryCmd::SetBma => Self::SetBma { + battery_id: safe_get_u8(buffer, 0)?, + bma: Bma { + averaging_interval_ms: safe_get_dword(buffer, 1)?, + }, + }, + BatteryCmd::GetSta => Self::GetSta { + battery_id: safe_get_u8(buffer, 0)?, + }, + }, + ) + } + + fn discriminant(&self) -> u16 { + BatteryCmd::from(self).into() + } +} + +/// Serializable result type for battery operations. +pub type AcpiBatteryResult = Result; + +#[derive(num_enum::IntoPrimitive, num_enum::TryFromPrimitive, Copy, Clone, Debug, PartialEq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +#[repr(u16)] +/// Errors that can occur while processing ACPI battery requests. +pub enum AcpiBatteryError { + /// The provided battery ID does not correspond to any known battery device. + UnknownDeviceId = 1, + + /// An unspecified error occurred while processing the request. + UnspecifiedFailure = 2, +} + +impl SerializableMessage for AcpiBatteryError { + fn serialize(self, _buffer: &mut [u8]) -> Result { + match self { + AcpiBatteryError::UnknownDeviceId | AcpiBatteryError::UnspecifiedFailure => Ok(0), + } + } + + fn deserialize(discriminant: u16, _buffer: &[u8]) -> Result { + AcpiBatteryError::try_from(discriminant) + .map_err(|_| MessageSerializationError::UnknownMessageDiscriminant(discriminant)) + } + + fn discriminant(&self) -> u16 { + (*self).into() + } +} + +impl From for AcpiBatteryError { + fn from(error: BatteryError) -> Self { + match error { + BatteryError::UnknownDeviceId => AcpiBatteryError::UnknownDeviceId, + BatteryError::UnspecifiedFailure => AcpiBatteryError::UnspecifiedFailure, + } + } +} + +fn safe_get_u8(buffer: &[u8], index: usize) -> Result { + buffer + .get(index) + .copied() + .ok_or(MessageSerializationError::BufferTooSmall) +} + +fn safe_get_dword(buffer: &[u8], index: usize) -> Result { + let bytes = buffer + .get(index..index + 4) + .ok_or(MessageSerializationError::BufferTooSmall)? + .try_into() + .map_err(|_| MessageSerializationError::BufferTooSmall)?; + Ok(u32::from_le_bytes(bytes)) +} + +fn safe_get_bytes(buffer: &[u8], index: usize) -> Result<[u8; N], MessageSerializationError> { + buffer + .get(index..index + N) + .ok_or(MessageSerializationError::BufferTooSmall)? + .try_into() + .map_err(|_| MessageSerializationError::BufferTooSmall) +} + +fn safe_put_u8(buffer: &mut [u8], index: usize, val: u8) -> Result { + *buffer.get_mut(index).ok_or(MessageSerializationError::BufferTooSmall)? = val; + Ok(1) +} + +fn safe_put_dword(buffer: &mut [u8], index: usize, val: u32) -> Result { + buffer + .get_mut(index..index + 4) + .ok_or(MessageSerializationError::BufferTooSmall)? + .copy_from_slice(&val.to_le_bytes()); + Ok(4) +} + +fn safe_put_bytes(buffer: &mut [u8], index: usize, bytes: &[u8]) -> Result { + buffer + .get_mut(index..index + bytes.len()) + .ok_or(MessageSerializationError::BufferTooSmall)? + .copy_from_slice(bytes); + Ok(bytes.len()) +} + +const BIX_MODEL_NUM_START_IDX: usize = 64; +const BIX_MODEL_NUM_END_IDX: usize = BIX_MODEL_NUM_START_IDX + STD_BIX_MODEL_SIZE; +const BIX_SERIAL_NUM_START_IDX: usize = BIX_MODEL_NUM_END_IDX; +const BIX_SERIAL_NUM_END_IDX: usize = BIX_SERIAL_NUM_START_IDX + STD_BIX_SERIAL_SIZE; +const BIX_BATTERY_TYPE_START_IDX: usize = BIX_SERIAL_NUM_END_IDX; +const BIX_BATTERY_TYPE_END_IDX: usize = BIX_BATTERY_TYPE_START_IDX + STD_BIX_BATTERY_SIZE; +const BIX_OEM_INFO_START_IDX: usize = BIX_BATTERY_TYPE_END_IDX; +const BIX_OEM_INFO_END_IDX: usize = BIX_OEM_INFO_START_IDX + STD_BIX_OEM_SIZE; + +fn bix_to_bytes(bix: BixFixedStrings, dst_slice: &mut [u8]) -> Result { + if dst_slice.len() < BIX_OEM_INFO_END_IDX + core::mem::size_of::() { + return Err(MessageSerializationError::BufferTooSmall); + } + + Ok(safe_put_dword(dst_slice, 0, bix.revision)? + + safe_put_dword(dst_slice, 4, bix.power_unit.into())? + + safe_put_dword(dst_slice, 8, bix.design_capacity)? + + safe_put_dword(dst_slice, 12, bix.last_full_charge_capacity)? + + safe_put_dword(dst_slice, 16, bix.battery_technology.into())? + + safe_put_dword(dst_slice, 20, bix.design_voltage)? + + safe_put_dword(dst_slice, 24, bix.design_cap_of_warning)? + + safe_put_dword(dst_slice, 28, bix.design_cap_of_low)? + + safe_put_dword(dst_slice, 32, bix.cycle_count)? + + safe_put_dword(dst_slice, 36, bix.measurement_accuracy)? + + safe_put_dword(dst_slice, 40, bix.max_sampling_time)? + + safe_put_dword(dst_slice, 44, bix.min_sampling_time)? + + safe_put_dword(dst_slice, 48, bix.max_averaging_interval)? + + safe_put_dword(dst_slice, 52, bix.min_averaging_interval)? + + safe_put_dword(dst_slice, 56, bix.battery_capacity_granularity_1)? + + safe_put_dword(dst_slice, 60, bix.battery_capacity_granularity_2)? + + safe_put_bytes(dst_slice, BIX_MODEL_NUM_START_IDX, &bix.model_number)? + + safe_put_bytes(dst_slice, BIX_SERIAL_NUM_START_IDX, &bix.serial_number)? + + safe_put_bytes(dst_slice, BIX_BATTERY_TYPE_START_IDX, &bix.battery_type)? + + safe_put_bytes(dst_slice, BIX_OEM_INFO_START_IDX, &bix.oem_info)? + + safe_put_dword(dst_slice, BIX_OEM_INFO_END_IDX, bix.battery_swapping_capability.into())?) +} + +fn bix_from_bytes(src_slice: &[u8]) -> Result { + Ok(BixFixedStrings { + revision: safe_get_dword(src_slice, 0)?, + power_unit: safe_get_dword(src_slice, 4)? + .try_into() + .map_err(|_| MessageSerializationError::InvalidPayload("Invalid PowerUnit"))?, + design_capacity: safe_get_dword(src_slice, 8)?, + last_full_charge_capacity: safe_get_dword(src_slice, 12)?, + battery_technology: safe_get_dword(src_slice, 16)? + .try_into() + .map_err(|_| MessageSerializationError::InvalidPayload("Invalid BatteryTechnology"))?, + design_voltage: safe_get_dword(src_slice, 20)?, + design_cap_of_warning: safe_get_dword(src_slice, 24)?, + design_cap_of_low: safe_get_dword(src_slice, 28)?, + cycle_count: safe_get_dword(src_slice, 32)?, + measurement_accuracy: safe_get_dword(src_slice, 36)?, + max_sampling_time: safe_get_dword(src_slice, 40)?, + min_sampling_time: safe_get_dword(src_slice, 44)?, + max_averaging_interval: safe_get_dword(src_slice, 48)?, + min_averaging_interval: safe_get_dword(src_slice, 52)?, + battery_capacity_granularity_1: safe_get_dword(src_slice, 56)?, + battery_capacity_granularity_2: safe_get_dword(src_slice, 60)?, + model_number: safe_get_bytes::(src_slice, BIX_MODEL_NUM_START_IDX)?, + serial_number: safe_get_bytes::(src_slice, BIX_SERIAL_NUM_START_IDX)?, + battery_type: safe_get_bytes::(src_slice, BIX_BATTERY_TYPE_START_IDX)?, + oem_info: safe_get_bytes::(src_slice, BIX_OEM_INFO_START_IDX)?, + battery_swapping_capability: safe_get_dword(src_slice, BIX_OEM_INFO_END_IDX)? + .try_into() + .map_err(|_| MessageSerializationError::InvalidPayload("Invalid BatterySwappingCapability"))?, + }) +} + +const PIF_MODEL_NUM_START_IDX: usize = 12; +const PIF_MODEL_NUM_END_IDX: usize = PIF_MODEL_NUM_START_IDX + STD_PIF_MODEL_SIZE; +const PIF_SERIAL_NUM_START_IDX: usize = PIF_MODEL_NUM_END_IDX; +const PIF_SERIAL_NUM_END_IDX: usize = PIF_SERIAL_NUM_START_IDX + STD_PIF_SERIAL_SIZE; +const PIF_OEM_INFO_START_IDX: usize = PIF_SERIAL_NUM_END_IDX; +const PIF_OEM_INFO_END_IDX: usize = PIF_OEM_INFO_START_IDX + STD_PIF_OEM_SIZE; + +fn pif_to_bytes(pif: PifFixedStrings, dst_slice: &mut [u8]) -> Result { + if dst_slice.len() < PIF_OEM_INFO_END_IDX { + return Err(MessageSerializationError::BufferTooSmall); + } + + Ok(safe_put_dword(dst_slice, 0, pif.power_source_state.bits())? + + safe_put_dword(dst_slice, 4, pif.max_output_power)? + + safe_put_dword(dst_slice, 8, pif.max_input_power)? + + safe_put_bytes(dst_slice, PIF_MODEL_NUM_START_IDX, &pif.model_number)? + + safe_put_bytes(dst_slice, PIF_SERIAL_NUM_START_IDX, &pif.serial_number)? + + safe_put_bytes(dst_slice, PIF_OEM_INFO_START_IDX, &pif.oem_info)?) +} + +fn pif_from_bytes(src_slice: &[u8]) -> Result { + Ok(PifFixedStrings { + power_source_state: PowerSourceState::from_bits(safe_get_dword(src_slice, 0)?) + .ok_or(MessageSerializationError::InvalidPayload("Invalid PowerSourceState"))?, + max_output_power: safe_get_dword(src_slice, 4)?, + max_input_power: safe_get_dword(src_slice, 8)?, + model_number: safe_get_bytes::(src_slice, PIF_MODEL_NUM_START_IDX)?, + serial_number: safe_get_bytes::(src_slice, PIF_SERIAL_NUM_START_IDX)?, + oem_info: safe_get_bytes::(src_slice, PIF_OEM_INFO_START_IDX)?, + }) +} diff --git a/battery-service/Cargo.toml b/battery-service/Cargo.toml index a3dfed015..dc8dea259 100644 --- a/battery-service/Cargo.toml +++ b/battery-service/Cargo.toml @@ -15,25 +15,33 @@ workspace = true [dependencies] defmt = { workspace = true, optional = true } +battery-service-interface.workspace = true embassy-futures.workspace = true embassy-sync.workspace = true embassy-time.workspace = true embedded-batteries-async.workspace = true embedded-services.workspace = true log = { workspace = true, optional = true } +odp-service-common.workspace = true +power-policy-interface.workspace = true [features] default = [] defmt = [ "dep:defmt", + "battery-service-interface/defmt", "embedded-services/defmt", "embassy-time/defmt", "embassy-sync/defmt", "embedded-batteries-async/defmt", + "power-policy-interface/defmt", ] log = [ "dep:log", + "battery-service-interface/log", "embedded-services/log", "embassy-time/log", "embassy-sync/log", + "power-policy-interface/log", ] +mock = [] diff --git a/battery-service/src/acpi.rs b/battery-service/src/acpi.rs index c2d0dab02..52d0074de 100644 --- a/battery-service/src/acpi.rs +++ b/battery-service/src/acpi.rs @@ -1,90 +1,23 @@ #![allow(dead_code)] use core::ops::Deref; +use battery_service_interface::BatteryError; use embedded_batteries_async::acpi::{PowerSourceState, PowerUnit}; -use embedded_services::{ - debug, - ec_type::message::{ - STD_BIX_BATTERY_SIZE, STD_BIX_MODEL_SIZE, STD_BIX_OEM_SIZE, STD_BIX_SERIAL_SIZE, STD_PIF_MODEL_SIZE, - STD_PIF_OEM_SIZE, STD_PIF_SERIAL_SIZE, StdHostMsg, StdHostRequest, - }, - ec_type::protocols::mctp, - error, info, - power::policy::PowerCapability, - trace, +use embedded_services::{info, trace}; + +use battery_service_interface::{ + BctReturnResult, BixFixedStrings, Bmd, Bpc, Bps, BstReturn, BtmReturnResult, DeviceId, PifFixedStrings, PsrReturn, + STD_BIX_BATTERY_SIZE, STD_BIX_MODEL_SIZE, STD_BIX_OEM_SIZE, STD_BIX_SERIAL_SIZE, STD_PIF_MODEL_SIZE, + STD_PIF_OEM_SIZE, STD_PIF_SERIAL_SIZE, StaReturn, }; +use power_policy_interface::capability::PowerCapability; + use crate::{ context::PsuState, - device::{DeviceId, DynamicBatteryMsgs, StaticBatteryMsgs}, + device::{DynamicBatteryMsgs, StaticBatteryMsgs}, }; -#[cfg_attr(feature = "defmt", derive(defmt::Format))] -pub(crate) struct Payload<'a> { - pub command: AcpiCmd, - pub status: u8, - pub data: &'a [u8], -} - -#[derive(Debug)] -#[cfg_attr(feature = "defmt", derive(defmt::Format))] -pub(crate) enum PayloadError { - MalformedPayload, - BufTooSmall(usize), -} - -const ACPI_HEADER_SIZE: usize = 4; - -#[derive(Copy, Clone)] -#[cfg_attr(feature = "defmt", derive(defmt::Format))] -pub(crate) enum AcpiCmd { - GetBix = 1, - GetBst = 2, - GetPsr = 3, - GetPif = 4, - GetBps = 5, - SetBtp = 6, - SetBpt = 7, - GetBpc = 8, - SetBmc = 9, - GetBmd = 10, - GetBct = 11, - GetBtm = 12, - SetBms = 13, - SetBma = 14, - GetSta = 15, -} - -impl TryFrom for AcpiCmd { - type Error = PayloadError; - fn try_from(value: u8) -> Result { - match value { - 1 => Ok(AcpiCmd::GetBix), - 2 => Ok(AcpiCmd::GetBst), - 3 => Ok(AcpiCmd::GetPsr), - 4 => Ok(AcpiCmd::GetPif), - 5 => Ok(AcpiCmd::GetBps), - 6 => Ok(AcpiCmd::SetBtp), - 7 => Ok(AcpiCmd::SetBpt), - 8 => Ok(AcpiCmd::GetBpc), - 9 => Ok(AcpiCmd::SetBmc), - 10 => Ok(AcpiCmd::GetBmd), - 11 => Ok(AcpiCmd::GetBct), - 12 => Ok(AcpiCmd::GetBtm), - 13 => Ok(AcpiCmd::SetBms), - 14 => Ok(AcpiCmd::SetBma), - 15 => Ok(AcpiCmd::GetSta), - _ => Err(PayloadError::MalformedPayload), - } - } -} - -impl From for u8 { - fn from(value: AcpiCmd) -> Self { - value as u8 - } -} - pub(crate) fn compute_bst(cache: &DynamicBatteryMsgs) -> embedded_batteries_async::acpi::BstReturn { let charging = if cache.battery_status & (1 << 6) == 0 { embedded_batteries_async::acpi::BatteryState::CHARGING @@ -104,37 +37,34 @@ pub(crate) fn compute_bst(cache: &DynamicBatteryMsgs) -> embedded_batteries_asyn pub(crate) fn compute_bix<'a>( static_cache: &'a StaticBatteryMsgs, dynamic_cache: &'a DynamicBatteryMsgs, -) -> Result, ()> -{ - let mut bix_return = - mctp::BixFixedStrings:: { - revision: 1, - power_unit: if static_cache.battery_mode.capacity_mode() { - PowerUnit::MilliWatts - } else { - PowerUnit::MilliAmps - }, - design_capacity: static_cache.design_capacity_mwh, - last_full_charge_capacity: dynamic_cache.full_charge_capacity_mwh, - battery_technology: embedded_batteries_async::acpi::BatteryTechnology::Secondary, - design_voltage: static_cache.design_voltage_mv.into(), - design_cap_of_warning: static_cache.design_cap_warning, - design_cap_of_low: static_cache.design_cap_low, - cycle_count: dynamic_cache.cycle_count.into(), - measurement_accuracy: u32::from(100 - dynamic_cache.max_error_pct) * 1000u32, - max_sampling_time: static_cache.max_sample_time, - min_sampling_time: static_cache.min_sample_time, - max_averaging_interval: static_cache.max_averaging_interval, - min_averaging_interval: static_cache.min_averaging_interval, - battery_capacity_granularity_1: static_cache.cap_granularity_1, - battery_capacity_granularity_2: static_cache.cap_granularity_2, - model_number: [0u8; STD_BIX_MODEL_SIZE], - serial_number: [0u8; STD_BIX_SERIAL_SIZE], - battery_type: [0u8; STD_BIX_BATTERY_SIZE], - oem_info: [0u8; STD_BIX_OEM_SIZE], - battery_swapping_capability: embedded_batteries_async::acpi::BatterySwapCapability::NonSwappable, - }; - +) -> Result { + let mut bix_return = BixFixedStrings { + revision: 1, + power_unit: if static_cache.battery_mode.capacity_mode() { + PowerUnit::MilliWatts + } else { + PowerUnit::MilliAmps + }, + design_capacity: static_cache.design_capacity_mwh, + last_full_charge_capacity: dynamic_cache.full_charge_capacity_mwh, + battery_technology: embedded_batteries_async::acpi::BatteryTechnology::Secondary, + design_voltage: static_cache.design_voltage_mv.into(), + design_cap_of_warning: static_cache.design_cap_warning, + design_cap_of_low: static_cache.design_cap_low, + cycle_count: dynamic_cache.cycle_count.into(), + measurement_accuracy: u32::from(100 - dynamic_cache.max_error_pct) * 1000u32, + max_sampling_time: static_cache.max_sample_time, + min_sampling_time: static_cache.min_sample_time, + max_averaging_interval: static_cache.max_averaging_interval, + min_averaging_interval: static_cache.min_averaging_interval, + battery_capacity_granularity_1: static_cache.cap_granularity_1, + battery_capacity_granularity_2: static_cache.cap_granularity_2, + model_number: [0u8; STD_BIX_MODEL_SIZE], + serial_number: [0u8; STD_BIX_SERIAL_SIZE], + battery_type: [0u8; STD_BIX_BATTERY_SIZE], + oem_info: [0u8; STD_BIX_OEM_SIZE], + battery_swapping_capability: embedded_batteries_async::acpi::BatterySwapCapability::NonSwappable, + }; let model_number_len = core::cmp::min(STD_BIX_MODEL_SIZE - 1, static_cache.device_name.len() - 1); bix_return .model_number @@ -234,16 +164,14 @@ pub(crate) fn compute_psr(psu_state: &PsuState) -> embedded_batteries_async::acp } } -pub(crate) fn compute_pif( - psu_state: &PsuState, -) -> mctp::PifFixedStrings { +pub(crate) fn compute_pif(psu_state: &PsuState) -> PifFixedStrings { // TODO: Grab real values from power policy let capability = psu_state.power_capability.unwrap_or(PowerCapability { voltage_mv: 0, current_ma: 0, }); - mctp::PifFixedStrings { + PifFixedStrings { power_source_state: PowerSourceState::empty(), max_output_power: capability.max_power_mw(), max_input_power: capability.max_power_mw(), @@ -254,438 +182,173 @@ pub(crate) fn compute_pif( } impl crate::context::Context { - // TODO Move these to a trait - pub(super) async fn bix_handler(&self, request: &mut StdHostRequest) { + pub(super) async fn bix_handler(&self, device_id: DeviceId) -> Result { trace!("Battery service: got BIX command!"); - // Enough space for all string fields to have 7 bytes + 1 null terminator byte - match request.payload { - mctp::Odp::BatteryGetBixRequest { battery_id } => { - if let Some(fg) = self.get_fuel_gauge(DeviceId(battery_id)) { - let static_cache_guard = fg.get_static_battery_cache_guarded().await; - let dynamic_cache_guard = fg.get_dynamic_battery_cache_guarded().await; - request.payload = mctp::Odp::BatteryGetBixResponse { - bix: match compute_bix(static_cache_guard.deref(), dynamic_cache_guard.deref()) { - Ok(bix) => bix, - Err(()) => { - error!("Battery service: Failed to compute BIX"); - // Drop locks before next await point to eliminate possibility of deadlock - drop(static_cache_guard); - drop(dynamic_cache_guard); - - request.status = 1; - request.payload = mctp::Odp::ErrorResponse {}; - - super::comms_send( - crate::EndpointID::External(embedded_services::comms::External::Host), - request, - ) - .await - .unwrap(); - debug!("response sent to espi_service"); - return; - } - }, - }; - // Drop locks before next await point to eliminate possibility of deadlock - drop(static_cache_guard); - drop(dynamic_cache_guard); - - request.status = 0; - super::comms_send( - crate::EndpointID::External(embedded_services::comms::External::Host), - &StdHostMsg::Response(*request), - ) - .await - .unwrap(); - - debug!("response sent to espi_service"); - } else { - error!("Battery service: FG not found when trying to process ACPI cmd!"); - } - } - _ => error!("Battery service: command and body mismatch!"), - } + + let fg = self.get_fuel_gauge(device_id).ok_or(BatteryError::UnknownDeviceId)?; + + let static_cache_guard = fg.get_static_battery_cache_guarded().await; + let dynamic_cache_guard = fg.get_dynamic_battery_cache_guarded().await; + + compute_bix(static_cache_guard.deref(), dynamic_cache_guard.deref()) + .map_err(|_| BatteryError::UnspecifiedFailure) } - pub(super) async fn bst_handler(&self, request: &mut StdHostRequest) { + pub(super) async fn bst_handler(&self, device_id: DeviceId) -> Result { trace!("Battery service: got BST command!"); - match request.payload { - mctp::Odp::BatteryGetBstRequest { battery_id } => { - if let Some(fg) = self.get_fuel_gauge(DeviceId(battery_id)) { - request.payload = mctp::Odp::BatteryGetBstResponse { - bst: compute_bst(&fg.get_dynamic_battery_cache().await), - }; - request.status = 0; - } else { - error!("Battery service: FG not found when trying to process ACPI cmd!"); - request.status = 1; - request.payload = mctp::Odp::ErrorResponse {}; - } - } - _ => error!("Battery service: command and body mismatch!"), - } - super::comms_send( - crate::EndpointID::External(embedded_services::comms::External::Host), - &StdHostMsg::Response(*request), - ) - .await - .unwrap(); - - trace!("response sent to espi_service"); + + let fg = self.get_fuel_gauge(device_id).ok_or(BatteryError::UnknownDeviceId)?; + + Ok(compute_bst(&fg.get_dynamic_battery_cache().await)) } - pub(super) async fn psr_handler(&self, request: &mut StdHostRequest) { + pub(super) async fn psr_handler(&self, device_id: DeviceId) -> Result { trace!("Battery service: got PSR command!"); - match request.payload { - mctp::Odp::BatteryGetPsrRequest { battery_id } => { - if let Some(_fg) = self.get_fuel_gauge(DeviceId(battery_id)) { - request.payload = mctp::Odp::BatteryGetPsrResponse { - psr: compute_psr(&self.get_power_info().await), - }; - request.status = 0; - } else { - error!("Battery service: FG not found when trying to process ACPI cmd!"); - request.status = 1; - request.payload = mctp::Odp::ErrorResponse {}; - } - } - _ => error!("Battery service: command and body mismatch!"), - } - - super::comms_send( - crate::EndpointID::External(embedded_services::comms::External::Host), - &StdHostMsg::Response(*request), - ) - .await - .unwrap(); - trace!("response sent to espi_service"); + let _fg = self.get_fuel_gauge(device_id).ok_or(BatteryError::UnknownDeviceId)?; + + Ok(compute_psr(&self.get_power_info().await)) } - pub(super) async fn pif_handler(&self, request: &mut StdHostRequest) { + pub(super) async fn pif_handler(&self, device_id: DeviceId) -> Result { trace!("Battery service: got PIF command!"); - match request.payload { - mctp::Odp::BatteryGetPifRequest { battery_id } => { - if let Some(_fg) = self.get_fuel_gauge(DeviceId(battery_id)) { - request.payload = mctp::Odp::BatteryGetPifResponse { - pif: compute_pif(&self.get_power_info().await), - }; - request.status = 0; - } else { - error!("Battery service: FG not found when trying to process ACPI cmd!"); - request.status = 1; - request.payload = mctp::Odp::ErrorResponse {}; - } - } - _ => error!("Battery service: command and body mismatch!"), - } - - super::comms_send( - crate::EndpointID::External(embedded_services::comms::External::Host), - &StdHostMsg::Response(*request), - ) - .await - .unwrap(); - trace!("response sent to espi_service"); + let _fg = self.get_fuel_gauge(device_id).ok_or(BatteryError::UnknownDeviceId)?; + + Ok(compute_pif(&self.get_power_info().await)) } - pub(super) async fn bps_handler(&self, request: &mut StdHostRequest) { + pub(super) async fn bps_handler(&self, device_id: DeviceId) -> Result { trace!("Battery service: got BPS command!"); - match request.payload { - mctp::Odp::BatteryGetBpsRequest { battery_id } => { - if let Some(fg) = self.get_fuel_gauge(DeviceId(battery_id)) { - request.payload = mctp::Odp::BatteryGetBpsResponse { - bps: compute_bps(&fg.get_dynamic_battery_cache().await), - }; - request.status = 0; - } else { - error!("Battery service: FG not found when trying to process ACPI cmd!"); - request.status = 1; - request.payload = mctp::Odp::ErrorResponse {}; - } - } - _ => error!("Battery service: command and body mismatch!"), - } - - super::comms_send( - crate::EndpointID::External(embedded_services::comms::External::Host), - &StdHostMsg::Response(*request), - ) - .await - .unwrap(); + let fg = self.get_fuel_gauge(device_id).ok_or(BatteryError::UnknownDeviceId)?; + + Ok(compute_bps(&fg.get_dynamic_battery_cache().await)) } - pub(super) async fn btp_handler(&self, request: &mut StdHostRequest) { + pub(super) async fn btp_handler( + &self, + device_id: DeviceId, + btp: embedded_batteries_async::acpi::Btp, + ) -> Result<(), BatteryError> { trace!("Battery service: got BTP command!"); - match request.payload { - mctp::Odp::BatterySetBtpRequest { battery_id, btp } => { - if let Some(_fg) = self.get_fuel_gauge(DeviceId(battery_id)) { - // TODO: Save trip point - info!("Battery service: New BTP {}", btp.trip_point); - request.payload = mctp::Odp::BatterySetBtpResponse {}; - request.status = 0; - } else { - error!("Battery service: FG not found when trying to process ACPI cmd!"); - request.status = 1; - request.payload = mctp::Odp::ErrorResponse {}; - } - } - _ => error!("Battery service: command and body mismatch!"), - } - - super::comms_send( - crate::EndpointID::External(embedded_services::comms::External::Host), - &StdHostMsg::Response(*request), - ) - .await - .unwrap(); + let _fg = self.get_fuel_gauge(device_id).ok_or(BatteryError::UnknownDeviceId)?; + + // TODO: Save trip point + info!("Battery service: New BTP {}", btp.trip_point); + + Ok(()) } - pub(super) async fn bpt_handler(&self, request: &mut StdHostRequest) { + pub(super) async fn bpt_handler( + &self, + device_id: DeviceId, + bpt: embedded_batteries_async::acpi::Bpt, + ) -> Result<(), BatteryError> { trace!("Battery service: got BPT command!"); - match request.payload { - mctp::Odp::BatterySetBptRequest { battery_id, bpt } => { - if let Some(_fg) = self.get_fuel_gauge(DeviceId(battery_id)) { - info!( - "Battery service: Threshold ID: {:?}, Threshold value: {:?}", - bpt.threshold_id as u32, bpt.threshold_value - ); - request.payload = mctp::Odp::BatterySetBptResponse {}; - request.status = 0; - } else { - error!("Battery service: FG not found when trying to process ACPI cmd!"); - request.status = 1; - request.payload = mctp::Odp::ErrorResponse {}; - } - } - _ => error!("Battery service: command and body mismatch!"), - } - - super::comms_send( - crate::EndpointID::External(embedded_services::comms::External::Host), - &StdHostMsg::Response(*request), - ) - .await - .unwrap(); + let _fg = self.get_fuel_gauge(device_id).ok_or(BatteryError::UnknownDeviceId)?; + + info!( + "Battery service: Threshold ID: {:?}, Threshold value: {:?}", + bpt.threshold_id as u32, bpt.threshold_value + ); + + Ok(()) } - pub(super) async fn bpc_handler(&self, request: &mut StdHostRequest) { + pub(super) async fn bpc_handler(&self, device_id: DeviceId) -> Result { trace!("Battery service: got BPC command!"); - match request.payload { - mctp::Odp::BatteryGetBpcRequest { battery_id } => { - if let Some(fg) = self.get_fuel_gauge(DeviceId(battery_id)) { - // TODO: Save trip point - request.payload = mctp::Odp::BatteryGetBpcResponse { - bpc: compute_bpc(&fg.get_static_battery_cache().await), - }; - request.status = 0; - } else { - error!("Battery service: FG not found when trying to process ACPI cmd!"); - request.status = 1; - request.payload = mctp::Odp::ErrorResponse {}; - } - } - _ => error!("Battery service: command and body mismatch!"), - } - - super::comms_send( - crate::EndpointID::External(embedded_services::comms::External::Host), - &StdHostMsg::Response(*request), - ) - .await - .unwrap(); + // TODO: Save trip point + let fg = self.get_fuel_gauge(device_id).ok_or(BatteryError::UnknownDeviceId)?; + + Ok(compute_bpc(&fg.get_static_battery_cache().await)) } - pub(super) async fn bmc_handler(&self, request: &mut StdHostRequest) { + pub(super) async fn bmc_handler( + &self, + device_id: DeviceId, + bmc: embedded_batteries_async::acpi::Bmc, + ) -> Result<(), BatteryError> { trace!("Battery service: got BMC command!"); - match request.payload { - mctp::Odp::BatterySetBmcRequest { battery_id, bmc } => { - if let Some(_fg) = self.get_fuel_gauge(DeviceId(battery_id)) { - info!("Battery service: Bmc {}", bmc.maintenance_control_flags.bits()); - request.payload = mctp::Odp::BatterySetBmcResponse {}; - request.status = 0; - } else { - error!("Battery service: FG not found when trying to process ACPI cmd!"); - request.status = 1; - request.payload = mctp::Odp::ErrorResponse {}; - } - } - _ => error!("Battery service: command and body mismatch!"), - } - - super::comms_send( - crate::EndpointID::External(embedded_services::comms::External::Host), - &StdHostMsg::Response(*request), - ) - .await - .unwrap(); + let _fg = self.get_fuel_gauge(device_id).ok_or(BatteryError::UnknownDeviceId)?; + + info!("Battery service: Bmc {}", bmc.maintenance_control_flags.bits()); + + Ok(()) } - pub(super) async fn bmd_handler(&self, request: &mut StdHostRequest) { + pub(super) async fn bmd_handler(&self, device_id: DeviceId) -> Result { trace!("Battery service: got BMD command!"); - match request.payload { - mctp::Odp::BatteryGetBmdRequest { battery_id } => { - if let Some(fg) = self.get_fuel_gauge(DeviceId(battery_id)) { - let static_cache = fg.get_static_battery_cache().await; - let dynamic_cache = fg.get_dynamic_battery_cache().await; - request.payload = mctp::Odp::BatteryGetBmdResponse { - bmd: compute_bmd(&static_cache, &dynamic_cache), - }; - request.status = 0; - } else { - error!("Battery service: FG not found when trying to process ACPI cmd!"); - request.status = 1; - request.payload = mctp::Odp::ErrorResponse {}; - } - } - _ => error!("Battery service: command and body mismatch!"), - } - - super::comms_send( - crate::EndpointID::External(embedded_services::comms::External::Host), - &StdHostMsg::Response(*request), - ) - .await - .unwrap(); + let fg = self.get_fuel_gauge(device_id).ok_or(BatteryError::UnknownDeviceId)?; + + let static_cache = fg.get_static_battery_cache().await; + let dynamic_cache = fg.get_dynamic_battery_cache().await; + + Ok(compute_bmd(&static_cache, &dynamic_cache)) } - pub(super) async fn bct_handler(&self, request: &mut StdHostRequest) { + pub(super) async fn bct_handler( + &self, + device_id: DeviceId, + bct: embedded_batteries_async::acpi::Bct, + ) -> Result { trace!("Battery service: got BCT command!"); - match request.payload { - mctp::Odp::BatteryGetBctRequest { battery_id, bct } => { - if let Some(fg) = self.get_fuel_gauge(DeviceId(battery_id)) { - info!("Recvd BCT charge_level_percent: {}", bct.charge_level_percent); - request.payload = mctp::Odp::BatteryGetBctResponse { - bct_response: compute_bct(&bct, &fg.get_dynamic_battery_cache().await), - }; - request.status = 0; - } else { - error!("Battery service: FG not found when trying to process ACPI cmd!"); - request.status = 1; - request.payload = mctp::Odp::ErrorResponse {}; - } - } - _ => error!("Battery service: command and body mismatch!"), - } - - super::comms_send( - crate::EndpointID::External(embedded_services::comms::External::Host), - &StdHostMsg::Response(*request), - ) - .await - .unwrap(); + let fg = self.get_fuel_gauge(device_id).ok_or(BatteryError::UnknownDeviceId)?; + + info!("Recvd BCT charge_level_percent: {}", bct.charge_level_percent); + Ok(compute_bct(&bct, &fg.get_dynamic_battery_cache().await)) } - pub(super) async fn btm_handler(&self, request: &mut StdHostRequest) { + pub(super) async fn btm_handler( + &self, + device_id: DeviceId, + btm: embedded_batteries_async::acpi::Btm, + ) -> Result { trace!("Battery service: got BTM command!"); - match request.payload { - mctp::Odp::BatteryGetBtmRequest { battery_id, btm } => { - if let Some(fg) = self.get_fuel_gauge(DeviceId(battery_id)) { - info!("Recvd BTM discharge_rate: {}", btm.discharge_rate); - request.payload = mctp::Odp::BatteryGetBtmResponse { - btm_response: compute_btm(&btm, &fg.get_dynamic_battery_cache().await), - }; - request.status = 0; - } else { - error!("Battery service: FG not found when trying to process ACPI cmd!"); - request.status = 1; - request.payload = mctp::Odp::ErrorResponse {}; - } - } - _ => error!("Battery service: command and body mismatch!"), - } - - super::comms_send( - crate::EndpointID::External(embedded_services::comms::External::Host), - &StdHostMsg::Response(*request), - ) - .await - .unwrap(); + let fg = self.get_fuel_gauge(device_id).ok_or(BatteryError::UnknownDeviceId)?; + + info!("Recvd BTM discharge_rate: {}", btm.discharge_rate); + Ok(compute_btm(&btm, &fg.get_dynamic_battery_cache().await)) } - pub(super) async fn bms_handler(&self, request: &mut StdHostRequest) { + pub(super) async fn bms_handler( + &self, + device_id: DeviceId, + bms: embedded_batteries_async::acpi::Bms, + ) -> Result<(), BatteryError> { trace!("Battery service: got BMS command!"); - match request.payload { - mctp::Odp::BatterySetBmsRequest { battery_id, bms } => { - if let Some(_fg) = self.get_fuel_gauge(DeviceId(battery_id)) { - info!("Recvd BMS sampling_time: {}", bms.sampling_time_ms); - request.payload = mctp::Odp::BatterySetBmsResponse { status: 0 }; - request.status = 0; - } else { - error!("Battery service: FG not found when trying to process ACPI cmd!"); - request.status = 1; - request.payload = mctp::Odp::BatterySetBmsResponse { status: 1 }; - } - } - _ => error!("Battery service: command and body mismatch!"), - } - - super::comms_send( - crate::EndpointID::External(embedded_services::comms::External::Host), - &StdHostMsg::Response(*request), - ) - .await - .unwrap(); + let _fg = self.get_fuel_gauge(device_id).ok_or(BatteryError::UnknownDeviceId)?; + + info!("Recvd BMS sampling_time: {}", bms.sampling_time_ms); + Ok(()) } - pub(super) async fn bma_handler(&self, request: &mut StdHostRequest) { + pub(super) async fn bma_handler( + &self, + device_id: DeviceId, + bma: embedded_batteries_async::acpi::Bma, + ) -> Result<(), BatteryError> { trace!("Battery service: got BMA command!"); - match request.payload { - mctp::Odp::BatterySetBmaRequest { battery_id, bma } => { - if let Some(_fg) = self.get_fuel_gauge(DeviceId(battery_id)) { - info!("Recvd BMA averaging_interval_ms: {}", bma.averaging_interval_ms); - request.payload = mctp::Odp::BatterySetBmaResponse { status: 0 }; - request.status = 0; - } else { - error!("Battery service: FG not found when trying to process ACPI cmd!"); - request.status = 1; - request.payload = mctp::Odp::BatterySetBmaResponse { status: 1 }; - } - } - _ => error!("Battery service: command and body mismatch!"), - } - - super::comms_send( - crate::EndpointID::External(embedded_services::comms::External::Host), - &StdHostMsg::Response(*request), - ) - .await - .unwrap(); + let _fg = self.get_fuel_gauge(device_id).ok_or(BatteryError::UnknownDeviceId)?; + + info!("Recvd BMA averaging_interval_ms: {}", bma.averaging_interval_ms); + Ok(()) } - pub(super) async fn sta_handler(&self, request: &mut StdHostRequest) { + pub(super) async fn sta_handler(&self, device_id: DeviceId) -> Result { trace!("Battery service: got STA command!"); - match request.payload { - mctp::Odp::BatteryGetStaRequest { battery_id } => { - if let Some(_fg) = self.get_fuel_gauge(DeviceId(battery_id)) { - request.payload = mctp::Odp::BatteryGetStaResponse { sta: compute_sta() }; - request.status = 0; - } else { - error!("Battery service: FG not found when trying to process ACPI cmd!"); - request.status = 1; - request.payload = mctp::Odp::ErrorResponse {}; - } - } - _ => error!("Battery service: command and body mismatch!"), - } - - super::comms_send( - crate::EndpointID::External(embedded_services::comms::External::Host), - &StdHostMsg::Response(*request), - ) - .await - .unwrap(); + let _fg = self.get_fuel_gauge(device_id).ok_or(BatteryError::UnknownDeviceId)?; + + Ok(compute_sta()) } } diff --git a/battery-service/src/context.rs b/battery-service/src/context.rs index 48764f60a..39c5648ed 100644 --- a/battery-service/src/context.rs +++ b/battery-service/src/context.rs @@ -1,16 +1,12 @@ -use crate::device::{self, DeviceId}; -use crate::device::{Device, FuelGaugeError}; +use crate::device::{self, Device, FuelGaugeError}; +use battery_service_interface::DeviceId; use embassy_sync::channel::Channel; use embassy_sync::channel::TrySendError; use embassy_sync::mutex::Mutex; -use embassy_sync::signal::Signal; use embassy_time::{Duration, with_timeout}; use embedded_services::GlobalRawMutex; -use embedded_services::comms::MailboxDelegateError; -use embedded_services::ec_type::message::StdHostRequest; -use embedded_services::ec_type::protocols::acpi::BatteryCmd; -use embedded_services::power::policy::PowerCapability; use embedded_services::{IntrusiveList, debug, error, info, intrusive_list, trace, warn}; +use power_policy_interface::capability::PowerCapability; use core::ops::DerefMut; use core::sync::atomic::AtomicUsize; @@ -138,7 +134,6 @@ pub struct Context { battery_response: Channel, no_op_retry_count: AtomicUsize, config: Config, - acpi_request: Signal, power_info: Mutex, } @@ -162,8 +157,6 @@ impl Default for Config { } } -embedded_services::define_static_buffer!(acpi_buf, u8, [0u8; 133]); - impl Context { /// Create a new context instance. pub fn new() -> Self { @@ -182,7 +175,6 @@ impl Context { battery_response: Channel::new(), no_op_retry_count: AtomicUsize::new(0), config, - acpi_request: Signal::new(), power_info: Mutex::new(PsuState::new()), } } @@ -404,29 +396,6 @@ impl Context { } } - pub(super) async fn process_acpi_cmd(&self, acpi_msg: &mut StdHostRequest) { - match acpi_msg.command { - embedded_services::ec_type::message::OdpCommand::Battery(cmd) => match cmd { - BatteryCmd::GetBix => self.bix_handler(acpi_msg).await, - BatteryCmd::GetBst => self.bst_handler(acpi_msg).await, - BatteryCmd::GetPsr => self.psr_handler(acpi_msg).await, - BatteryCmd::GetPif => self.pif_handler(acpi_msg).await, - BatteryCmd::GetBps => self.bps_handler(acpi_msg).await, - BatteryCmd::SetBtp => self.btp_handler(acpi_msg).await, - BatteryCmd::SetBpt => self.bpt_handler(acpi_msg).await, - BatteryCmd::GetBpc => self.bpc_handler(acpi_msg).await, - BatteryCmd::SetBmc => self.bmc_handler(acpi_msg).await, - BatteryCmd::GetBmd => self.bmd_handler(acpi_msg).await, - BatteryCmd::GetBct => self.bct_handler(acpi_msg).await, - BatteryCmd::GetBtm => self.btm_handler(acpi_msg).await, - BatteryCmd::SetBms => self.bms_handler(acpi_msg).await, - BatteryCmd::SetBma => self.bma_handler(acpi_msg).await, - BatteryCmd::GetSta => self.sta_handler(acpi_msg).await, - }, - _ => error!("Battery service: host command not found!"), - } - } - pub(crate) fn get_fuel_gauge(&self, id: DeviceId) -> Option<&'static Device> { for device in &self.fuel_gauges { if let Some(data) = device.data::() { @@ -472,14 +441,6 @@ impl Context { self.battery_event.receive().await } - pub(super) fn send_acpi_cmd(&self, raw: StdHostRequest) { - self.acpi_request.signal(raw); - } - - pub(super) async fn wait_acpi_cmd(&self) -> StdHostRequest { - self.acpi_request.wait().await - } - pub async fn get_state(&self) -> State { *self.state.lock().await } @@ -515,9 +476,11 @@ impl Context { *self.power_info.lock().await } - pub(crate) fn set_power_info( + // TODO: bring this back after moving away from comms for power policy + // See https://github.com/OpenDevicePartnership/embedded-services/issues/742 + /*pub(crate) fn set_power_info( &self, - power_info: &embedded_services::power::policy::CommsData, + power_info: &power_policy_interface::service::event::CommsData, ) -> Result<(), MailboxDelegateError> { let mut guard = self .power_info @@ -527,13 +490,13 @@ impl Context { let psu_state = guard.deref_mut(); match power_info { - embedded_services::power::policy::CommsData::ConsumerDisconnected(_) => { + power_policy_interface::service::event::CommsData::ConsumerDisconnected(_) => { *psu_state = PsuState { psu_connected: false, power_capability: None, } } - embedded_services::power::policy::CommsData::ConsumerConnected(_device_id, power_capability) => { + power_policy_interface::service::event::CommsData::ConsumerConnected(_device_id, power_capability) => { *psu_state = PsuState { psu_connected: true, power_capability: Some(power_capability.capability), @@ -544,7 +507,7 @@ impl Context { trace!("Battery: PSU state: {:?}", psu_state); Ok(()) - } + }*/ } impl Default for Context { diff --git a/battery-service/src/device.rs b/battery-service/src/device.rs index 6cf314f6b..e4a3f28c0 100644 --- a/battery-service/src/device.rs +++ b/battery-service/src/device.rs @@ -9,6 +9,8 @@ use embedded_batteries_async::{ }; use embedded_services::{GlobalRawMutex, Node, NodeContainer, SyncCell}; +pub use battery_service_interface::DeviceId; + #[derive(Debug, Clone, Copy, PartialEq, Eq)] #[cfg_attr(feature = "defmt", derive(defmt::Format))] /// Device errors. @@ -155,11 +157,6 @@ pub struct DynamicBatteryMsgs { pub bmd_status: BmdStatusFlags, } -/// Fuel gauge ID -#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] -#[cfg_attr(feature = "defmt", derive(defmt::Format))] -pub struct DeviceId(pub u8); - /// Hardware agnostic device object to be registered with context. pub struct Device { node: embedded_services::Node, diff --git a/battery-service/src/lib.rs b/battery-service/src/lib.rs index c6554076c..8548dbb81 100644 --- a/battery-service/src/lib.rs +++ b/battery-service/src/lib.rs @@ -3,135 +3,300 @@ use core::{any::Any, convert::Infallible}; use context::BatteryEvent; -use embassy_futures::select::select; use embedded_services::{ comms::{self, EndpointID}, - ec_type::message::StdHostRequest, - trace, + error, info, trace, +}; + +use battery_service_interface::{ + BatteryError, Bct, BctReturnResult, BixFixedStrings, Bma, Bmc, Bmd, Bms, Bpc, Bps, Bpt, BstReturn, Btm, + BtmReturnResult, Btp, DeviceId, PifFixedStrings, PsrReturn, StaReturn, }; mod acpi; pub mod context; pub mod controller; pub mod device; -pub mod task; +#[cfg(feature = "mock")] +pub mod mock; pub mod wrapper; -/// Standard Battery Service. -pub struct Service { - pub endpoint: comms::Endpoint, - pub context: context::Context, +/// Parameters required to initialize the battery service. +pub struct InitParams<'hw, const N: usize> { + pub devices: [&'hw device::Device; N], + pub config: context::Config, } -impl Service { - /// Create a new battery service instance. - pub const fn new() -> Self { - Self::new_inner(context::Config::new()) - } - - /// Create a new battery service instance with context configuration. - pub fn new_with_ctx_config(config: context::Config) -> Self { - Self::new_inner(config) - } +/// The main service implementation. +struct ServiceInner { + endpoint: comms::Endpoint, + context: context::Context, +} - const fn new_inner(config: context::Config) -> Self { - Service { +impl ServiceInner { + fn new(config: context::Config) -> Self { + Self { endpoint: comms::Endpoint::uninit(comms::EndpointID::Internal(comms::Internal::Battery)), context: context::Context::new_with_config(config), } } /// Main battery service processing function. - pub async fn process_next(&self) { + async fn process_next(&self) { let event = self.wait_next().await; self.process_event(event).await } /// Wait for next event. - pub async fn wait_next(&self) -> Event { - match select(self.context.wait_event(), self.context.wait_acpi_cmd()).await { - embassy_futures::select::Either::First(event) => Event::StateMachine(event), - embassy_futures::select::Either::Second(acpi_msg) => Event::AcpiRequest(acpi_msg), - } + async fn wait_next(&self) -> BatteryEvent { + self.context.wait_event().await } /// Process battery service event. - pub async fn process_event(&self, event: Event) { - match event { - Event::StateMachine(event) => { - trace!("Battery service: state machine event recvd {:?}", event); - self.context.process(event).await - } - Event::AcpiRequest(mut acpi_msg) => { - trace!("Battery service: ACPI cmd recvd"); - self.context.process_acpi_cmd(&mut acpi_msg).await - } + async fn process_event(&self, event: BatteryEvent) { + trace!("Battery service: state machine event recvd {:?}", event); + self.context.process(event).await + } + + /// Register fuel gauge device with the battery service. + fn register_fuel_gauge( + &self, + device: &'static device::Device, + ) -> Result<(), embedded_services::intrusive_list::Error> { + self.context.register_fuel_gauge(device)?; + Ok(()) + } + + /// Use the battery service endpoint to send data to other subsystems and services. + async fn comms_send(&self, endpoint_id: EndpointID, data: &(impl Any + Send + Sync)) -> Result<(), Infallible> { + self.endpoint.send(endpoint_id, data).await + } + + /// Send the battery service state machine an event and await a response. + async fn execute_event(&self, event: BatteryEvent) -> context::BatteryResponse { + self.context.execute_event(event).await + } + + /// Wait for a response from the battery service. + async fn wait_for_battery_response(&self) -> context::BatteryResponse { + self.context.wait_response().await + } + + /// Asynchronously query the state from the state machine. + async fn get_state(&self) -> context::State { + self.context.get_state().await + } +} + +/// The memory resources required by the battery service. +#[derive(Default)] +pub struct Resources { + inner: Option>, +} + +/// A task runner for the battery service. Users of the service must run this object in an embassy task or similar async execution context. +pub struct Runner<'hw, const N: usize> { + service: &'hw ServiceInner, +} + +impl<'hw, const N: usize> odp_service_common::runnable_service::ServiceRunner<'hw> for Runner<'hw, N> { + /// Run the service. + async fn run(self) -> embedded_services::Never { + info!("Starting battery-service"); + loop { + self.service.process_next().await; } } } +/// Control handle for the battery service. Use this to interact with the battery service. #[derive(Clone, Copy)] -#[cfg_attr(feature = "defmt", derive(defmt::Format))] -pub enum Event { - StateMachine(BatteryEvent), - AcpiRequest(StdHostRequest), +pub struct Service<'hw, const N: usize> { + inner: &'hw ServiceInner, } -impl Default for Service { - fn default() -> Self { - Self::new() +impl<'hw, const N: usize> Service<'hw, N> { + /// Main battery service processing function. + pub async fn process_next(&self) { + self.inner.process_next().await } -} -impl comms::MailboxDelegate for Service { - fn receive(&self, message: &comms::Message) -> Result<(), comms::MailboxDelegateError> { - if let Some(event) = message.data.get::() { - self.context.send_event_no_wait(*event).map_err(|e| match e { - embassy_sync::channel::TrySendError::Full(_) => comms::MailboxDelegateError::BufferFull, - })? - } else if let Some(acpi_cmd) = message.data.get::() { - self.context.send_acpi_cmd(*acpi_cmd); - } else if let Some(power_policy_msg) = message.data.get::() { - self.context.set_power_info(&power_policy_msg.data)?; - } + /// Wait for next event. + pub async fn wait_next(&self) -> BatteryEvent { + self.inner.wait_next().await + } - Ok(()) + /// Process battery service event. + pub async fn process_event(&self, event: BatteryEvent) { + self.inner.process_event(event).await + } + + /// Use the battery service endpoint to send data to other subsystems and services. + pub async fn comms_send(&self, endpoint_id: EndpointID, data: &(impl Any + Send + Sync)) -> Result<(), Infallible> { + self.inner.comms_send(endpoint_id, data).await } -} -static SERVICE: Service = Service::new(); + /// Send the battery service state machine an event and await a response. + /// + /// This is an alternative method of interacting with the battery service (instead of using the comms service), + /// and is a useful fn if you want to send an event and await a response sequentially. + pub async fn execute_event(&self, event: BatteryEvent) -> context::BatteryResponse { + self.inner.execute_event(event).await + } -/// Register fuel gauge device with the battery service. -/// -/// Must be done before sending the battery service commands so that hardware device is visible -/// to the battery service. -pub fn register_fuel_gauge(device: &'static device::Device) -> Result<(), embedded_services::intrusive_list::Error> { - SERVICE.context.register_fuel_gauge(device)?; + /// Wait for a response from the battery service. + /// + /// Use this function after sending the battery service a message via the comms system. + pub async fn wait_for_battery_response(&self) -> context::BatteryResponse { + self.inner.wait_for_battery_response().await + } - Ok(()) + /// Asynchronously query the state from the state machine. + pub async fn get_state(&self) -> context::State { + self.inner.get_state().await + } } -/// Use the battery service endpoint to send data to other subsystems and services. -pub async fn comms_send(endpoint_id: EndpointID, data: &impl Any) -> Result<(), Infallible> { - SERVICE.endpoint.send(endpoint_id, data).await +impl<'hw, const N: usize> battery_service_interface::BatteryService for Service<'hw, N> { + async fn battery_charge_time( + &self, + battery_id: DeviceId, + charge_level: Bct, + ) -> Result { + self.inner.context.bct_handler(battery_id, charge_level).await + } + + async fn battery_info(&self, battery_id: DeviceId) -> Result { + self.inner.context.bix_handler(battery_id).await + } + + async fn set_battery_measurement_averaging_interval( + &self, + battery_id: DeviceId, + bma: Bma, + ) -> Result<(), BatteryError> { + self.inner.context.bma_handler(battery_id, bma).await + } + + async fn battery_maintenance_control(&self, battery_id: DeviceId, bmc: Bmc) -> Result<(), BatteryError> { + self.inner.context.bmc_handler(battery_id, bmc).await + } + + async fn battery_maintenance_data(&self, battery_id: DeviceId) -> Result { + self.inner.context.bmd_handler(battery_id).await + } + + async fn set_battery_measurement_sampling_time( + &self, + battery_id: DeviceId, + battery_measurement_sampling: Bms, + ) -> Result<(), BatteryError> { + self.inner + .context + .bms_handler(battery_id, battery_measurement_sampling) + .await + } + + async fn battery_power_characteristics(&self, battery_id: DeviceId) -> Result { + self.inner.context.bpc_handler(battery_id).await + } + + async fn battery_power_state(&self, battery_id: DeviceId) -> Result { + self.inner.context.bps_handler(battery_id).await + } + + async fn set_battery_power_threshold( + &self, + battery_id: DeviceId, + power_threshold: Bpt, + ) -> Result<(), BatteryError> { + self.inner.context.bpt_handler(battery_id, power_threshold).await + } + + async fn battery_status(&self, battery_id: DeviceId) -> Result { + self.inner.context.bst_handler(battery_id).await + } + + async fn battery_time_to_empty( + &self, + battery_id: DeviceId, + battery_discharge_rate: Btm, + ) -> Result { + self.inner.context.btm_handler(battery_id, battery_discharge_rate).await + } + + async fn set_battery_trip_point(&self, battery_id: DeviceId, btp: Btp) -> Result<(), BatteryError> { + self.inner.context.btp_handler(battery_id, btp).await + } + + async fn is_in_use(&self, battery_id: DeviceId) -> Result { + self.inner.context.psr_handler(battery_id).await + } + + async fn power_source_information(&self, power_source_id: DeviceId) -> Result { + self.inner.context.pif_handler(power_source_id).await + } + + async fn device_status(&self, battery_id: DeviceId) -> Result { + self.inner.context.sta_handler(battery_id).await + } } -/// Send the battery service state machine an event and await a response. -/// -/// This is an alternative method of interacting with the battery service (instead of using the comms service), -/// and is a useful fn if you want to send an event and await a response sequentially. -pub async fn execute_event(event: BatteryEvent) -> context::BatteryResponse { - SERVICE.context.execute_event(event).await +/// Errors that can occur during battery service initialization. +#[derive(Debug)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum InitError { + DeviceRegistrationFailed(crate::device::DeviceId), + CommsRegistrationFailed, } -/// Wait for a response from the battery service. -/// -/// Use this function after sending the battery service a message via the comms system. -pub async fn wait_for_battery_response() -> context::BatteryResponse { - SERVICE.context.wait_response().await +impl<'hw, const N: usize> odp_service_common::runnable_service::Service<'hw> for Service<'hw, N> +where + 'hw: 'static, // TODO relax this 'static requirement when we drop usages of IntrusiveList (including comms) +{ + type Runner = Runner<'hw, N>; + type ErrorType = InitError; + type InitParams = InitParams<'hw, N>; + type Resources = Resources; + + async fn new( + service_storage: &'hw mut Resources, + init_params: Self::InitParams, + ) -> Result<(Self, Runner<'hw, N>), InitError> { + let service = service_storage.inner.insert(ServiceInner::new(init_params.config)); + + for device in init_params.devices { + if service.register_fuel_gauge(device).is_err() { + error!("Failed to register battery device with DeviceId {:?}", device.id()); + return Err(InitError::DeviceRegistrationFailed(device.id())); + } + } + + if comms::register_endpoint(service, &service.endpoint).await.is_err() { + error!("Failed to register battery service endpoint"); + return Err(InitError::CommsRegistrationFailed); + } + + Ok((Self { inner: service }, Runner { service })) + } } -/// Asynchronously query the state from the state machine. -pub async fn get_state() -> context::State { - SERVICE.context.get_state().await +impl comms::MailboxDelegate for ServiceInner { + fn receive(&self, message: &comms::Message) -> Result<(), comms::MailboxDelegateError> { + if let Some(event) = message.data.get::() { + self.context.send_event_no_wait(*event).map_err(|e| match e { + embassy_sync::channel::TrySendError::Full(_) => comms::MailboxDelegateError::BufferFull, + })? + } + // TODO: Migrate away from using comms for power policy updates + // See https://github.com/OpenDevicePartnership/embedded-services/issues/742 + /*else if let Some(power_policy_msg) = message + .data + .get::() + { + self.context.set_power_info(&power_policy_msg.data)?; + }*/ + + Ok(()) + } } diff --git a/battery-service/src/mock.rs b/battery-service/src/mock.rs new file mode 100644 index 000000000..d852ea65f --- /dev/null +++ b/battery-service/src/mock.rs @@ -0,0 +1,387 @@ +use embassy_time::{Duration, Timer}; +use embedded_batteries_async::{ + acpi, charger, + smart_battery::{self, SmartBattery}, +}; +use embedded_services::{GlobalRawMutex, error, info}; + +// Convenience fns +pub async fn init_state_machine( + battery_service: &crate::Service<'_, N>, +) -> Result<(), crate::context::ContextError> { + battery_service + .execute_event(crate::context::BatteryEvent { + event: crate::context::BatteryEventInner::DoInit, + device_id: crate::device::DeviceId(0), + }) + .await + .inspect_err(|f| embedded_services::debug!("Fuel gauge init error: {:?}", f))?; + + battery_service + .execute_event(crate::context::BatteryEvent { + event: crate::context::BatteryEventInner::PollStaticData, + device_id: crate::device::DeviceId(0), + }) + .await + .inspect_err(|f| embedded_services::debug!("Fuel gauge static data error: {:?}", f))?; + + battery_service + .execute_event(crate::context::BatteryEvent { + event: crate::context::BatteryEventInner::PollDynamicData, + device_id: crate::device::DeviceId(0), + }) + .await + .inspect_err(|f| embedded_services::debug!("Fuel gauge dynamic data error: {:?}", f))?; + + Ok(()) +} + +pub async fn recover_state_machine(battery_service: &crate::Service<'_, N>) -> Result<(), ()> { + loop { + match battery_service + .execute_event(crate::context::BatteryEvent { + event: crate::context::BatteryEventInner::Timeout, + device_id: crate::device::DeviceId(0), + }) + .await + { + Ok(_) => { + embedded_services::info!("FG recovered!"); + return Ok(()); + } + Err(e) => match e { + crate::context::ContextError::StateError(e) => match e { + crate::context::StateMachineError::DeviceTimeout => { + embedded_services::trace!("Recovery failed, trying again after a backoff period"); + Timer::after(Duration::from_secs(10)).await; + } + crate::context::StateMachineError::NoOpRecoveryFailed => { + embedded_services::error!("Couldn't recover, reinit needed"); + return Err(()); + } + _ => embedded_services::debug!("Unexpected error"), + }, + _ => embedded_services::debug!("Unexpected error"), + }, + } + } +} + +pub type MockBattery<'a> = crate::wrapper::Wrapper<'a, MockBatteryDriver>; + +#[derive(Default)] +pub struct MockBatteryDriver { + capacity_mode_bit: embassy_sync::mutex::Mutex, +} + +impl MockBatteryDriver { + pub fn new() -> Self { + MockBatteryDriver { + capacity_mode_bit: embassy_sync::mutex::Mutex::new(false), + } + } + + async fn set_capacity_bit(&mut self, mwh: bool) -> Result<(), MockBatteryError> { + let battery_mode = self.battery_mode().await?; + SmartBattery::set_battery_mode(self, battery_mode.with_capacity_mode(mwh)).await?; + *self.capacity_mode_bit.get_mut() = mwh; + + Ok(()) + } +} + +#[derive(Clone, Copy, Debug)] +pub struct MockBatteryError; + +impl crate::controller::Controller for MockBatteryDriver { + type ControllerError = MockBatteryError; + + async fn initialize(&mut self) -> Result<(), Self::ControllerError> { + // Milliamps + let mwh = false; + self.set_capacity_bit(mwh) + .await + .inspect_err(|_| error!("FG: failed to initialize"))?; + + info!("FG: initialized"); + Ok(()) + } + + async fn ping(&mut self) -> Result<(), Self::ControllerError> { + if let Err(e) = self.charging_voltage().await { + error!("FG: failed to ping"); + Err(e) + } else { + info!("FG: ping success"); + Ok(()) + } + } + + async fn get_dynamic_data(&mut self) -> Result { + let new_msgs = crate::device::DynamicBatteryMsgs { + average_current_ma: self.average_current().await?, + battery_status: self.battery_status().await?.into(), + max_power_mw: 100, + battery_temp_dk: self.temperature().await?, + sus_power_mw: 42, + charging_current_ma: self.charging_current().await?, + charging_voltage_mv: self.charging_voltage().await?, + voltage_mv: self.voltage().await?, + current_ma: self.current().await?, + full_charge_capacity_mwh: match self.full_charge_capacity().await? { + smart_battery::CapacityModeValue::CentiWattUnsigned(_) => 0xDEADBEEF, + smart_battery::CapacityModeValue::MilliAmpUnsigned(capacity) => capacity.into(), + }, + remaining_capacity_mwh: match self.remaining_capacity().await? { + smart_battery::CapacityModeValue::CentiWattUnsigned(_) => 0xDEADBEEF, + smart_battery::CapacityModeValue::MilliAmpUnsigned(capacity) => capacity.into(), + }, + relative_soc_pct: self.relative_state_of_charge().await?.into(), + cycle_count: self.cycle_count().await?, + max_error_pct: self.max_error().await?.into(), + bmd_status: acpi::BmdStatusFlags::default(), + turbo_vload_mv: 0, + turbo_rhf_effective_mohm: 0, + }; + Ok(new_msgs) + } + + #[allow(clippy::indexing_slicing)] + async fn get_static_data(&mut self) -> Result { + let design_capacity: u32 = match self.design_capacity().await? { + smart_battery::CapacityModeValue::CentiWattUnsigned(design_capacity) => design_capacity.into(), + smart_battery::CapacityModeValue::MilliAmpUnsigned(design_capacity) => design_capacity.into(), + }; + + let mut new_msgs = crate::device::StaticBatteryMsgs { + manufacturer_name: Default::default(), + device_name: Default::default(), + device_chemistry: Default::default(), + design_capacity_mwh: match self.design_capacity().await? { + smart_battery::CapacityModeValue::CentiWattUnsigned(design_capacity) => design_capacity.into(), + smart_battery::CapacityModeValue::MilliAmpUnsigned(design_capacity) => design_capacity.into(), + }, + design_voltage_mv: self.design_voltage().await?, + device_chemistry_id: Default::default(), + serial_num: Default::default(), + battery_mode: self.battery_mode().await?, + design_cap_warning: design_capacity / 4, + design_cap_low: design_capacity / 10, + measurement_accuracy: self.max_error().await?.into(), + max_sample_time: Default::default(), + min_sample_time: Default::default(), + max_averaging_interval: Default::default(), + min_averaging_interval: Default::default(), + cap_granularity_1: Default::default(), + cap_granularity_2: Default::default(), + power_threshold_support: battery_service_interface::PowerThresholdSupport::empty(), + max_instant_pwr_threshold: Default::default(), + max_sus_pwr_threshold: Default::default(), + bmc_flags: battery_service_interface::BmcControlFlags::empty(), + bmd_capability: battery_service_interface::BmdCapabilityFlags::empty(), + bmd_recalibrate_count: Default::default(), + bmd_quick_recalibrate_time: Default::default(), + bmd_slow_recalibrate_time: Default::default(), + }; + let mut buf = [0u8; 21]; + + let buf_len = new_msgs.manufacturer_name.len(); + self.manufacturer_name(&mut buf[..buf_len]).await?; + new_msgs.manufacturer_name.copy_from_slice(&buf[..buf_len]); + + let buf_len = new_msgs.device_name.len(); + self.device_name(&mut buf[..buf_len]).await?; + new_msgs.device_name.copy_from_slice(&buf[..buf_len]); + + let buf_len = new_msgs.device_chemistry.len(); + self.device_chemistry(&mut buf[..buf_len]).await?; + new_msgs.device_chemistry.copy_from_slice(&buf[..buf_len]); + + let buf_len = new_msgs.device_chemistry_id.len(); + self.device_chemistry(&mut buf[..buf_len]).await?; + new_msgs.device_chemistry_id.copy_from_slice(&buf[..buf_len]); + + let serial = self.serial_number().await?; + let serial = serial.to_le_bytes(); + new_msgs.serial_num = [serial[0], serial[1], 0, 0]; + + Ok(new_msgs) + } + + async fn get_device_event(&mut self) -> crate::controller::ControllerEvent { + // TODO: Loop forever till we figure out what we want to do here + loop { + Timer::after_secs(1000000).await; + } + } + + fn set_timeout(&mut self, _duration: embassy_time::Duration) {} +} + +impl smart_battery::Error for MockBatteryError { + fn kind(&self) -> smart_battery::ErrorKind { + smart_battery::ErrorKind::Other + } +} + +impl smart_battery::ErrorType for MockBatteryDriver { + type Error = MockBatteryError; +} + +// Revisit: Have this generate realistic data dynamically (right now just static arbitrary values) +impl smart_battery::SmartBattery for MockBatteryDriver { + async fn absolute_state_of_charge(&mut self) -> Result { + Ok(77) + } + + async fn at_rate(&mut self) -> Result { + Ok(smart_battery::CapacityModeSignedValue::MilliAmpSigned(100)) + } + + async fn at_rate_ok(&mut self) -> Result { + Ok(true) + } + + async fn at_rate_time_to_empty(&mut self) -> Result { + Ok(2600) + } + + async fn at_rate_time_to_full(&mut self) -> Result { + Ok(1337) + } + + async fn average_current(&mut self) -> Result { + Ok(42) + } + + async fn average_time_to_empty(&mut self) -> Result { + Ok(100) + } + + async fn average_time_to_full(&mut self) -> Result { + Ok(120) + } + + async fn battery_mode(&mut self) -> Result { + Ok(smart_battery::BatteryModeFields::new()) + } + + async fn battery_status(&mut self) -> Result { + Ok(smart_battery::BatteryStatusFields::new()) + } + + async fn charging_current(&mut self) -> Result { + Ok(50) + } + + async fn charging_voltage(&mut self) -> Result { + Ok(4242) + } + + async fn current(&mut self) -> Result { + Ok(500) + } + + async fn cycle_count(&mut self) -> Result { + Ok(10000) + } + + async fn design_capacity(&mut self) -> Result { + Ok(smart_battery::CapacityModeValue::CentiWattUnsigned(0)) + } + + async fn design_voltage(&mut self) -> Result { + Ok(12000) + } + + #[allow(clippy::indexing_slicing)] + async fn device_chemistry(&mut self, chemistry: &mut [u8]) -> Result<(), Self::Error> { + let bytes = [b'L', b'i', b'P', b'o', 0]; + let bytes_to_copy = core::cmp::min(bytes.len(), chemistry.len()); + chemistry[..bytes_to_copy].copy_from_slice(&bytes[..bytes_to_copy]); + Ok(()) + } + + #[allow(clippy::indexing_slicing)] + async fn device_name(&mut self, name: &mut [u8]) -> Result<(), Self::Error> { + let bytes = [b'O', b'd', b'p', b'B', b'a', b't', b't', 0]; + let bytes_to_copy = core::cmp::min(bytes.len(), name.len()); + name[..bytes_to_copy].copy_from_slice(&bytes[..bytes_to_copy]); + Ok(()) + } + + async fn full_charge_capacity(&mut self) -> Result { + Ok(smart_battery::CapacityModeValue::CentiWattUnsigned(0)) + } + + async fn manufacture_date(&mut self) -> Result { + Ok(smart_battery::ManufactureDate::new()) + } + + #[allow(clippy::indexing_slicing)] + async fn manufacturer_name(&mut self, name: &mut [u8]) -> Result<(), Self::Error> { + let bytes = [b'B', b'a', b't', b'B', b'r', b'o', b's', 0]; + let bytes_to_copy = core::cmp::min(bytes.len(), name.len()); + name[..bytes_to_copy].copy_from_slice(&bytes[..bytes_to_copy]); + Ok(()) + } + + async fn max_error(&mut self) -> Result { + Ok(2) + } + + async fn relative_state_of_charge(&mut self) -> Result { + Ok(10) + } + + async fn remaining_capacity(&mut self) -> Result { + Ok(smart_battery::CapacityModeValue::CentiWattUnsigned(0)) + } + + async fn remaining_capacity_alarm(&mut self) -> Result { + Ok(smart_battery::CapacityModeValue::CentiWattUnsigned(0)) + } + + async fn remaining_time_alarm(&mut self) -> Result { + Ok(85) + } + + async fn run_time_to_empty(&mut self) -> Result { + Ok(110) + } + + async fn serial_number(&mut self) -> Result { + Ok(0x4544) + } + + async fn set_at_rate(&mut self, _rate: smart_battery::CapacityModeSignedValue) -> Result<(), Self::Error> { + Ok(()) + } + + async fn set_battery_mode(&mut self, _flags: smart_battery::BatteryModeFields) -> Result<(), Self::Error> { + Ok(()) + } + + async fn set_remaining_capacity_alarm( + &mut self, + _capacity: smart_battery::CapacityModeValue, + ) -> Result<(), Self::Error> { + Ok(()) + } + + async fn set_remaining_time_alarm(&mut self, _time: smart_battery::Minutes) -> Result<(), Self::Error> { + Ok(()) + } + + async fn specification_info(&mut self) -> Result { + Ok(smart_battery::SpecificationInfoFields::new()) + } + + async fn temperature(&mut self) -> Result { + Ok(2981) + } + + async fn voltage(&mut self) -> Result { + Ok(12600) + } +} diff --git a/battery-service/src/task.rs b/battery-service/src/task.rs deleted file mode 100644 index 9e63bc3b5..000000000 --- a/battery-service/src/task.rs +++ /dev/null @@ -1,17 +0,0 @@ -use embedded_services::{comms, error, info}; - -use crate::SERVICE; - -/// Battery service task. -pub async fn task() { - info!("Starting battery-service task"); - - if comms::register_endpoint(&SERVICE, &SERVICE.endpoint).await.is_err() { - error!("Failed to register battery service endpoint"); - return; - } - - loop { - SERVICE.process_next().await; - } -} diff --git a/cfu-service/Cargo.toml b/cfu-service/Cargo.toml index 811c82ea7..ae960ad12 100644 --- a/cfu-service/Cargo.toml +++ b/cfu-service/Cargo.toml @@ -17,9 +17,19 @@ embassy-sync.workspace = true embassy-time.workspace = true embedded-cfu-protocol.workspace = true embedded-services.workspace = true +fw-update-interface.workspace = true heapless.workspace = true log = { workspace = true, optional = true } +[dev-dependencies] +static_cell.workspace = true +critical-section = { workspace = true, features = ["std"] } +embassy-time = { workspace = true, features = ["std", "generic-queue-8"] } +tokio = { workspace = true, features = ["rt", "macros", "time"] } +env_logger = "0.11.8" +log = { workspace = true } +fw-update-interface-mocks = { workspace = true } + [features] default = [] defmt = [ @@ -28,6 +38,7 @@ defmt = [ "embassy-time/defmt", "embassy-sync/defmt", "embedded-cfu-protocol/defmt", + "fw-update-interface/defmt", ] log = [ "dep:log", @@ -35,4 +46,5 @@ log = [ "embassy-time/log", "embassy-sync/log", "embedded-cfu-protocol/log", + "fw-update-interface/log", ] diff --git a/cfu-service/src/basic/config.rs b/cfu-service/src/basic/config.rs new file mode 100644 index 000000000..793de5b1b --- /dev/null +++ b/cfu-service/src/basic/config.rs @@ -0,0 +1,47 @@ +//! Configuration structs + +use embassy_time::Duration; + +/// Base interval for checking for FW update timeouts and recovery attempts +pub const DEFAULT_FW_UPDATE_TICK_INTERVAL: Duration = Duration::from_secs(5); +/// Default number of ticks before we consider a firmware update to have timed out +/// 300 seconds at 5 seconds per tick +pub const DEFAULT_FW_UPDATE_TIMEOUT_TICKS: u32 = 60; + +/// Config values for FW update recovery +#[derive(Copy, Clone)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +#[non_exhaustive] +pub struct Recovery { + /// Interval between recovery ticks + pub tick_interval: Duration, + /// Timeout (in recovery ticks) before we assume the update has failed. + pub update_timeout_ticks: u32, +} + +impl Default for Recovery { + fn default() -> Self { + Self { + tick_interval: DEFAULT_FW_UPDATE_TICK_INTERVAL, + update_timeout_ticks: DEFAULT_FW_UPDATE_TIMEOUT_TICKS, + } + } +} + +/// Configuration for [`crate::basic::Updater`] +#[derive(Copy, Clone, Default)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +#[non_exhaustive] +pub struct Updater { + /// Recovery configuration for the updater + pub recovery: Recovery, +} + +/// Configuration for [`crate::basic::event_receiver::EventReceiver`] +#[derive(Copy, Clone, Default)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +#[non_exhaustive] +pub struct EventReceiver { + /// Recovery configuration for the event receiver + pub recovery: Recovery, +} diff --git a/cfu-service/src/basic/event_receiver.rs b/cfu-service/src/basic/event_receiver.rs new file mode 100644 index 000000000..940a15291 --- /dev/null +++ b/cfu-service/src/basic/event_receiver.rs @@ -0,0 +1,197 @@ +use embassy_futures::select::{Either, select}; +use embassy_time::{Instant, Timer}; +use embedded_services::{debug, error, sync::Lockable}; + +use crate::basic::{ + Output, + config::EventReceiver as Config, + state::{FwUpdateState, SharedState}, +}; + +/// CFU events +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum Event { + /// CFU request + Request(crate::component::RequestData), + /// Recovery tick + /// + /// Occurs when the FW update has timed out to abort the update and return hardware to its normal state + RecoveryTick, +} + +/// Struct to receive CFU events. +pub struct EventReceiver<'a, Shared: Lockable> { + /// Config + config: Config, + /// CFU device used for firmware updates + cfu_device: &'static crate::component::CfuDevice, + /// State shared with [`crate::basic::Updater`] + shared_state: &'a Shared, +} + +impl<'a, Shared: Lockable> EventReceiver<'a, Shared> { + /// Create a new CFU event receiver + pub fn new(cfu_device: &'static crate::component::CfuDevice, shared_state: &'a Shared, config: Config) -> Self { + Self { + cfu_device, + shared_state, + config, + } + } + + /// Wait for the next CFU event + pub async fn wait_next(&mut self) -> Event { + loop { + let (fw_update_state, next_recovery_tick) = { + let state = self.shared_state.lock().await; + (state.fw_update_state, state.next_recovery_tick) + }; + match fw_update_state { + FwUpdateState::Idle => { + // No FW update in progress, just wait for a command + return Event::Request(self.cfu_device.wait_request().await); + } + FwUpdateState::InProgress(ticks) => { + match select(self.cfu_device.wait_request(), Timer::at(next_recovery_tick)).await { + Either::First(command) => return Event::Request(command), + Either::Second(_) => { + debug!("CFU tick: {}", ticks); + + let mut shared_state = self.shared_state.lock().await; + shared_state.next_recovery_tick = Instant::now() + self.config.recovery.tick_interval; + + if ticks + 1 < self.config.recovery.update_timeout_ticks { + shared_state.fw_update_state = FwUpdateState::InProgress(ticks + 1); + continue; + } else { + error!( + "FW update timed out after {} ticks", + self.config.recovery.update_timeout_ticks + ); + shared_state.fw_update_state = FwUpdateState::Recovery; + return Event::RecoveryTick; + } + } + } + } + FwUpdateState::Recovery => { + // Recovery state, wait for the next attempt to recover the device + let next_recovery_tick = self.shared_state.lock().await.next_recovery_tick; + Timer::at(next_recovery_tick).await; + self.shared_state.lock().await.next_recovery_tick = + Instant::now() + self.config.recovery.tick_interval; + debug!("FW update ticker ticked"); + return Event::RecoveryTick; + } + } + } + } + + /// Finalize the processing of an output + // TODO: remove this when we refactor CFU + pub async fn finalize(&mut self, output: Output) { + if let Output::CfuResponse(response) = output { + self.cfu_device.send_response(response).await + } + } +} + +#[cfg(test)] +mod test { + use crate::{basic::config::Recovery, component::CfuDevice}; + + use super::*; + use embassy_sync::mutex::Mutex; + use embassy_time::{Duration, Instant, TimeoutError, with_timeout}; + use embedded_services::GlobalRawMutex; + use static_cell::StaticCell; + + /// Test that we get recovery ticks as expected + #[tokio::test] + async fn test_recovery_timeout() { + static CFU_DEVICE: StaticCell = StaticCell::new(); + + // Maximum timeout for the recovery entry, actual time should be 1000, but gives us some margin + const RECOVERY_ENTRY_MAX_TIMEOUT: Duration = Duration::from_millis(1100); + // Maximum timeout for an individual recovery tick, actual time should be 100, but gives us some margin + const RECOVERY_TICK_MAX_TIMEOUT: Duration = Duration::from_millis(110); + // Expected measured interval between recovery ticks, actual time should be 100, but undershoot slightly for some margin + const EXPECTED_RECOVERY_TICK_INTERVAL: Duration = Duration::from_millis(90); + + let shared_state: Mutex = Mutex::new(SharedState::default()); + let cfu_device = CFU_DEVICE.init(CfuDevice::new(0)); + let recovery_config = Recovery { + tick_interval: Duration::from_millis(100), + update_timeout_ticks: 10, + }; + + let mut event_receiver = EventReceiver::new( + cfu_device, + &shared_state, + Config { + recovery: recovery_config, + }, + ); + + // First test the recovery timer isn't active in the idle state + assert_eq!( + with_timeout(RECOVERY_ENTRY_MAX_TIMEOUT, event_receiver.wait_next()).await, + Err(TimeoutError), + ); + assert_eq!( + event_receiver.shared_state.lock().await.fw_update_state, + FwUpdateState::Idle + ); + + // Start the recovery ticker, normally the update struct handles this. + shared_state + .lock() + .await + .enter_in_progress(recovery_config.tick_interval); + + let start = Instant::now(); + assert_eq!( + with_timeout(RECOVERY_ENTRY_MAX_TIMEOUT, event_receiver.wait_next()).await, + Ok(Event::RecoveryTick), + ); + let duration = Instant::now() - start; + + // Check that we waited approximately the correct amount of time + assert!(duration.as_millis() >= 1000); + assert_eq!( + event_receiver.shared_state.lock().await.fw_update_state, + FwUpdateState::Recovery + ); + + // Check the first recovery tick after the state transition + let start = Instant::now(); + assert_eq!( + with_timeout(RECOVERY_TICK_MAX_TIMEOUT, event_receiver.wait_next()).await, + Ok(Event::RecoveryTick), + ); + let duration = Instant::now() - start; + + // Check that we waited approximately the correct amount of time + assert!(duration >= EXPECTED_RECOVERY_TICK_INTERVAL); + assert_eq!( + event_receiver.shared_state.lock().await.fw_update_state, + FwUpdateState::Recovery + ); + + // Check subsequent recovery ticks + let start = Instant::now(); + assert_eq!( + with_timeout(RECOVERY_TICK_MAX_TIMEOUT, event_receiver.wait_next()).await, + Ok(Event::RecoveryTick), + ); + let duration = Instant::now() - start; + + // Check that we waited approximately the correct amount of time + assert!(duration >= EXPECTED_RECOVERY_TICK_INTERVAL); + assert_eq!( + event_receiver.shared_state.lock().await.fw_update_state, + FwUpdateState::Recovery + ); + } +} diff --git a/cfu-service/src/basic/mod.rs b/cfu-service/src/basic/mod.rs new file mode 100644 index 000000000..301d703a8 --- /dev/null +++ b/cfu-service/src/basic/mod.rs @@ -0,0 +1,302 @@ +//! Basic CFU implementation over the basic [`FwUpdate`] trait. +use crate::{ + basic::{ + config::Updater as Config, + event_receiver::Event, + state::{FwUpdateState, SharedState}, + }, + component::{InternalResponseData, RequestData}, + customization::Customization, +}; +use embedded_cfu_protocol::protocol_definitions::*; +use embedded_services::{debug, error, sync::Lockable}; +use fw_update_interface::basic::FwUpdate; + +pub mod config; +pub mod event_receiver; +pub mod state; + +#[cfg(test)] +mod test; + +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum Output { + CfuResponse(InternalResponseData), + CfuRecovery, +} + +/// Basic CFU handler that bridges CFU protocol commands with the [`FwUpdate`] trait. +/// +/// This struct is generic over a firmware offer validator and processes CFU commands +/// by delegating firmware operations to any [`FwUpdate`] implementor passed to each method. +pub struct Updater<'a, Device: Lockable, Shared: Lockable, Cust: Customization> { + device: &'a Device, + component_id: ComponentId, + customization: Cust, + shared_state: &'a Shared, + config: Config, +} + +impl<'a, Device: Lockable, Shared: Lockable, Cust: Customization> + Updater<'a, Device, Shared, Cust> +{ + /// Create a new CfuBasic instance + pub fn new( + device: &'a Device, + shared_state: &'a Shared, + config: Config, + component_id: ComponentId, + customization: Cust, + ) -> Self { + Self { + device, + shared_state, + component_id, + customization, + config, + } + } + + /// Create a response with an invalid firmware version + fn create_invalid_fw_version_response(&self) -> InternalResponseData { + let dev_inf = FwVerComponentInfo::new(FwVersion::new(0xffffffff), self.component_id); + let comp_info: [FwVerComponentInfo; MAX_CMPT_COUNT] = [dev_inf; MAX_CMPT_COUNT]; + InternalResponseData::FwVersionResponse(GetFwVersionResponse { + header: GetFwVersionResponseHeader::new(1, GetFwVerRespHeaderByte3::NoSpecialFlags), + component_info: comp_info, + }) + } + + /// Create an offer rejection response + fn create_offer_rejection() -> InternalResponseData { + InternalResponseData::OfferResponse(FwUpdateOfferResponse::new_with_failure( + HostToken::Driver, + OfferRejectReason::InvalidComponent, + OfferStatus::Reject, + )) + } + + /// Returns a copy of the current update state + pub async fn update_state(&self) -> FwUpdateState { + self.shared_state.lock().await.fw_update_state + } + + /// Gives immutable access to the customization object + pub fn customization(&self) -> &Cust { + &self.customization + } + + /// Gives mutable access to the customization object + pub fn customization_mut(&mut self) -> &mut Cust { + &mut self.customization + } + + /// Process a CFU event + pub async fn process_event(&mut self, event: Event) -> Output { + match event { + Event::Request(request) => { + let response = self.process_cfu_command(&request).await; + Output::CfuResponse(response) + } + Event::RecoveryTick => { + // FW Update recovery tick, process recovery attempts + self.process_recovery_tick().await; + Output::CfuRecovery + } + } + } + + /// Process a GetFwVersion command + pub async fn process_get_fw_version(&mut self) -> InternalResponseData { + let result = self.device.lock().await.get_active_fw_version().await; + let version = match result { + Ok(v) => v, + Err(e) => { + error!("Failed to get active firmware version: {:?}", e); + return self.create_invalid_fw_version_response(); + } + }; + + let dev_inf = FwVerComponentInfo::new(FwVersion::new(version), self.component_id); + let comp_info: [FwVerComponentInfo; MAX_CMPT_COUNT] = [dev_inf; MAX_CMPT_COUNT]; + InternalResponseData::FwVersionResponse(GetFwVersionResponse { + header: GetFwVersionResponseHeader::new(1, GetFwVerRespHeaderByte3::NoSpecialFlags), + component_info: comp_info, + }) + } + + /// Process a GiveOffer command + pub async fn process_give_offer(&mut self, offer: &FwUpdateOffer) -> InternalResponseData { + if offer.component_info.component_id != self.component_id { + return Self::create_offer_rejection(); + } + + let result = self.device.lock().await.get_active_fw_version().await; + let version = match result { + Ok(v) => v, + Err(e) => { + error!("Failed to get active firmware version: {:?}", e); + return Self::create_offer_rejection(); + } + }; + + InternalResponseData::OfferResponse(self.customization.validate(FwVersion::new(version), offer)) + } + + /// Process an AbortUpdate command + pub async fn process_abort_update(&mut self) -> InternalResponseData { + let result = self.device.lock().await.abort_fw_update().await; + match result { + Ok(_) => { + debug!("FW update aborted successfully"); + self.shared_state.lock().await.enter_idle(); + } + Err(e) => { + error!("Failed to abort FW update: {:?}", e); + self.shared_state.lock().await.enter_recovery(); + } + } + + InternalResponseData::ComponentPrepared + } + + /// Process a GiveContent command + pub async fn process_give_content(&mut self, content: &FwUpdateContentCommand) -> InternalResponseData { + let data = if let Some(data) = content.data.get(0..content.header.data_length as usize) { + data + } else { + return InternalResponseData::ContentResponse(FwUpdateContentResponse::new( + content.header.sequence_num, + CfuUpdateContentResponseStatus::ErrorPrepare, + )); + }; + + debug!("Got content {:#?}", content); + if content.header.flags & FW_UPDATE_FLAG_FIRST_BLOCK != 0 { + debug!("Got first block"); + + let result = self.device.lock().await.start_fw_update().await; + match result { + Ok(_) => { + debug!("FW update started successfully"); + } + Err(e) => { + error!("Failed to start FW update: {:?}", e); + self.shared_state.lock().await.enter_recovery(); + return InternalResponseData::ContentResponse(FwUpdateContentResponse::new( + content.header.sequence_num, + CfuUpdateContentResponseStatus::ErrorPrepare, + )); + } + } + + self.shared_state + .lock() + .await + .enter_in_progress(self.config.recovery.tick_interval); + } + + let result = self + .device + .lock() + .await + .write_fw_contents(content.header.firmware_address as usize, data) + .await; + match result { + Ok(_) => { + debug!("Block written successfully"); + } + Err(e) => { + error!("Failed to write block: {:?}", e); + return InternalResponseData::ContentResponse(FwUpdateContentResponse::new( + content.header.sequence_num, + CfuUpdateContentResponseStatus::ErrorWrite, + )); + } + } + + if content.header.flags & FW_UPDATE_FLAG_LAST_BLOCK != 0 { + let result = self.device.lock().await.finalize_fw_update().await; + match result { + Ok(_) => { + debug!("FW update finalized successfully"); + self.shared_state.lock().await.enter_idle(); + } + Err(e) => { + error!("Failed to finalize FW update: {:?}", e); + self.shared_state.lock().await.enter_recovery(); + return InternalResponseData::ContentResponse(FwUpdateContentResponse::new( + content.header.sequence_num, + CfuUpdateContentResponseStatus::ErrorWrite, + )); + } + } + } + + InternalResponseData::ContentResponse(FwUpdateContentResponse::new( + content.header.sequence_num, + CfuUpdateContentResponseStatus::Success, + )) + } + + /// Process a CFU recovery tick. + pub async fn process_recovery_tick(&mut self) { + // Update timed out, attempt to abort + self.shared_state.lock().await.enter_recovery(); + let result = self.device.lock().await.abort_fw_update().await; + match result { + Ok(_) => { + debug!("FW update aborted successfully"); + self.shared_state.lock().await.enter_idle(); + } + Err(e) => { + error!("Failed to abort FW update: {:?}", e); + } + } + } + + /// Process a CFU command, dispatching to the appropriate handler + pub async fn process_cfu_command(&mut self, command: &RequestData) -> InternalResponseData { + let fw_update_state = self.shared_state.lock().await.fw_update_state; + if fw_update_state == FwUpdateState::Recovery { + debug!("FW update in recovery state, rejecting command"); + return InternalResponseData::ComponentBusy; + } + + match command { + RequestData::FwVersionRequest => { + debug!("Got FwVersionRequest"); + self.process_get_fw_version().await + } + RequestData::GiveOffer(offer) => { + debug!("Got GiveOffer"); + self.process_give_offer(offer).await + } + RequestData::GiveContent(content) => { + debug!("Got GiveContent"); + self.process_give_content(content).await + } + RequestData::AbortUpdate => { + debug!("Got AbortUpdate"); + self.process_abort_update().await + } + RequestData::FinalizeUpdate => { + debug!("Got FinalizeUpdate"); + InternalResponseData::ComponentPrepared + } + RequestData::PrepareComponentForUpdate => { + debug!("Got PrepareComponentForUpdate"); + InternalResponseData::ComponentPrepared + } + RequestData::GiveOfferExtended(_) => { + debug!("Got GiveExtendedOffer, rejecting"); + Self::create_offer_rejection() + } + RequestData::GiveOfferInformation(_) => { + debug!("Got GiveOfferInformation, rejecting"); + Self::create_offer_rejection() + } + } + } +} diff --git a/cfu-service/src/basic/state.rs b/cfu-service/src/basic/state.rs new file mode 100644 index 000000000..c83773614 --- /dev/null +++ b/cfu-service/src/basic/state.rs @@ -0,0 +1,56 @@ +use embassy_time::{Duration, Instant}; + +/// Current state of the firmware update process +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum FwUpdateState { + /// None in progress + #[default] + Idle, + /// Firmware update in progress. + /// Integer is number of recovery ticks that have occurred since the start of the update. + InProgress(u32), + /// Firmware update has failed and the device is in an unknown state + Recovery, +} + +/// State shared between [`crate::basic::event_receiver::EventReceiver`] and [`crate::basic::Updater`] +#[derive(Clone, Copy)] +pub struct SharedState { + /// Current update state + pub(super) fw_update_state: FwUpdateState, + /// Next recovery tick + pub(super) next_recovery_tick: Instant, +} + +impl SharedState { + pub fn new() -> Self { + Self { + fw_update_state: FwUpdateState::Idle, + next_recovery_tick: Instant::MAX, + } + } + + pub(super) fn enter_idle(&mut self) { + self.fw_update_state = FwUpdateState::Idle; + self.next_recovery_tick = Instant::MAX; + } + + pub(super) fn enter_in_progress(&mut self, next_recovery_tick: Duration) { + self.fw_update_state = FwUpdateState::InProgress(0); + self.next_recovery_tick = Instant::now() + next_recovery_tick; + } + + pub(super) fn enter_recovery(&mut self) { + self.fw_update_state = FwUpdateState::Recovery; + if self.next_recovery_tick == Instant::MAX { + self.next_recovery_tick = Instant::now(); + } + } +} + +impl Default for SharedState { + fn default() -> Self { + Self::new() + } +} diff --git a/cfu-service/src/basic/test.rs b/cfu-service/src/basic/test.rs new file mode 100644 index 000000000..c675de669 --- /dev/null +++ b/cfu-service/src/basic/test.rs @@ -0,0 +1,366 @@ +//! Tests for [`crate::basic::Updater`] +#![allow(clippy::unwrap_used)] +extern crate std; + +use crate::{ + basic::{ + Output, Updater, + event_receiver::Event, + state::{FwUpdateState, SharedState}, + }, + component::{InternalResponseData, RequestData}, +}; +use embassy_sync::{mutex::Mutex, once_lock::OnceLock}; +use embassy_time::{Duration, with_timeout}; +use embedded_cfu_protocol::protocol_definitions::{ + CfuUpdateContentResponseStatus, DEFAULT_DATA_LENGTH, FW_UPDATE_FLAG_FIRST_BLOCK, FW_UPDATE_FLAG_LAST_BLOCK, + FwUpdateContentCommand, FwUpdateContentHeader, FwUpdateContentResponse, FwUpdateOffer, FwUpdateOfferResponse, + FwVerComponentInfo, FwVersion, GetFwVerRespHeaderByte3, GetFwVersionResponse, GetFwVersionResponseHeader, + HostToken, MAX_CMPT_COUNT, +}; +use embedded_services::GlobalRawMutex; + +use crate::mocks::customization::{FnCall as CustomizationFnCall, Mock as MockCustomization}; +use fw_update_interface_mocks::basic::{FnCall as FwFnCall, Mock}; + +use std::vec; + +const PER_CALL_TIMEOUT: Duration = Duration::from_millis(1000); + +const CURRENT_FW_VERSION: u32 = 0x12345678; +const NEW_FW_VERSION: u32 = 0x89abcdef; + +const DEVICE0_COMPONENT_ID: u8 = 5; + +const DEFAULT_TIMEOUT: Duration = Duration::from_secs(15); + +type DeviceType = Mutex; +type SharedStateType = Mutex; +type UpdaterType<'a> = Updater<'a, DeviceType, SharedStateType, MockCustomization>; + +/// Test the basic flow of the updater. +/// +/// This will get the FW version, give an offer, then send a start, middle, and end content +pub struct TestBasicFlow; + +impl Test for TestBasicFlow { + async fn run<'a>(&mut self, device: &'a DeviceType, cfu_basic: &'a mut UpdaterType<'a>) { + { + // Get FW version + let output = with_timeout( + PER_CALL_TIMEOUT, + cfu_basic.process_event(Event::Request(RequestData::FwVersionRequest)), + ) + .await + .unwrap(); + + assert_eq!( + output, + Output::CfuResponse(InternalResponseData::FwVersionResponse(GetFwVersionResponse { + header: GetFwVersionResponseHeader::new(1, GetFwVerRespHeaderByte3::NoSpecialFlags), + component_info: [FwVerComponentInfo::new(FwVersion::new(CURRENT_FW_VERSION), DEVICE0_COMPONENT_ID); + MAX_CMPT_COUNT], + })) + ); + assert_eq!(cfu_basic.update_state().await, FwUpdateState::Idle); + assert_eq!(device.lock().await.fn_calls.len(), 1); + assert_eq!( + device.lock().await.fn_calls.pop_front().unwrap(), + FwFnCall::GetActiveFwVersion + ); + } + + { + // Give offer + let output = with_timeout( + PER_CALL_TIMEOUT, + cfu_basic.process_event(Event::Request(RequestData::GiveOffer(FwUpdateOffer::new( + HostToken::Driver, + DEVICE0_COMPONENT_ID, + FwVersion::new(NEW_FW_VERSION), + 0, + 0, + )))), + ) + .await + .unwrap(); + + assert_eq!( + output, + Output::CfuResponse(InternalResponseData::OfferResponse(FwUpdateOfferResponse::new_accept( + HostToken::Driver + ))) + ); + assert_eq!(cfu_basic.update_state().await, FwUpdateState::Idle); + assert_eq!(device.lock().await.fn_calls.len(), 1); + assert_eq!( + device.lock().await.fn_calls.pop_front().unwrap(), + FwFnCall::GetActiveFwVersion + ); + + assert_eq!(cfu_basic.customization().fn_calls.len(), 1); + assert_eq!( + cfu_basic.customization_mut().fn_calls.pop_front(), + Some(CustomizationFnCall::Validate( + FwVersion::new(CURRENT_FW_VERSION), + FwUpdateOffer::new( + HostToken::Driver, + DEVICE0_COMPONENT_ID, + FwVersion::new(NEW_FW_VERSION), + 0, + 0, + ) + )) + ); + } + + { + // Give first content block + let output = with_timeout( + PER_CALL_TIMEOUT, + cfu_basic.process_event(Event::Request(RequestData::GiveContent(FwUpdateContentCommand { + header: FwUpdateContentHeader { + flags: FW_UPDATE_FLAG_FIRST_BLOCK, + data_length: DEFAULT_DATA_LENGTH as u8, + sequence_num: 0, + firmware_address: 0x0, + }, + data: [1; DEFAULT_DATA_LENGTH], + }))), + ) + .await + .unwrap(); + + assert_eq!( + output, + Output::CfuResponse(InternalResponseData::ContentResponse(FwUpdateContentResponse::new( + 0, + CfuUpdateContentResponseStatus::Success + ))) + ); + assert_eq!(cfu_basic.update_state().await, FwUpdateState::InProgress(0)); + assert_eq!(device.lock().await.fn_calls.len(), 2); + assert_eq!( + device.lock().await.fn_calls.pop_front().unwrap(), + FwFnCall::StartFwUpdate + ); + assert_eq!( + device.lock().await.fn_calls.pop_front().unwrap(), + FwFnCall::WriteFwContents(0, vec![1; DEFAULT_DATA_LENGTH]) + ); + } + + { + // Give middle content block + let output = with_timeout( + PER_CALL_TIMEOUT, + cfu_basic.process_event(Event::Request(RequestData::GiveContent(FwUpdateContentCommand { + header: FwUpdateContentHeader { + flags: 0, + data_length: DEFAULT_DATA_LENGTH as u8, + sequence_num: 1, + firmware_address: 0x0, + }, + data: [2; DEFAULT_DATA_LENGTH], + }))), + ) + .await + .unwrap(); + + assert_eq!( + output, + Output::CfuResponse(InternalResponseData::ContentResponse(FwUpdateContentResponse::new( + 1, + CfuUpdateContentResponseStatus::Success + ))) + ); + assert_eq!(cfu_basic.update_state().await, FwUpdateState::InProgress(0)); + assert_eq!(device.lock().await.fn_calls.len(), 1); + assert_eq!( + device.lock().await.fn_calls.pop_front().unwrap(), + FwFnCall::WriteFwContents(0, vec![2; DEFAULT_DATA_LENGTH]) + ); + } + + { + // Give final content block + let output = with_timeout( + PER_CALL_TIMEOUT, + cfu_basic.process_event(Event::Request(RequestData::GiveContent(FwUpdateContentCommand { + header: FwUpdateContentHeader { + flags: FW_UPDATE_FLAG_LAST_BLOCK, + data_length: DEFAULT_DATA_LENGTH as u8, + sequence_num: 2, + firmware_address: 0x0, + }, + data: [3; DEFAULT_DATA_LENGTH], + }))), + ) + .await + .unwrap(); + + assert_eq!( + output, + Output::CfuResponse(InternalResponseData::ContentResponse(FwUpdateContentResponse::new( + 2, + CfuUpdateContentResponseStatus::Success + ))) + ); + assert_eq!(cfu_basic.update_state().await, FwUpdateState::Idle); + assert_eq!(device.lock().await.fn_calls.len(), 2); + assert_eq!( + device.lock().await.fn_calls.pop_front().unwrap(), + FwFnCall::WriteFwContents(0, vec![3; DEFAULT_DATA_LENGTH]) + ); + assert_eq!( + device.lock().await.fn_calls.pop_front().unwrap(), + FwFnCall::FinalizeFwUpdate + ); + } + } +} + +/// Test that the recovery flow works immediately after sending the first content block. +struct TestStartRecoveryFlow; + +impl Test for TestStartRecoveryFlow { + async fn run<'a>(&mut self, device: &'a DeviceType, cfu_basic: &'a mut UpdaterType<'a>) { + { + // Give offer + let output = with_timeout( + PER_CALL_TIMEOUT, + cfu_basic.process_event(Event::Request(RequestData::GiveOffer(FwUpdateOffer::new( + HostToken::Driver, + DEVICE0_COMPONENT_ID, + FwVersion::new(NEW_FW_VERSION), + 0, + 0, + )))), + ) + .await + .unwrap(); + + assert_eq!( + output, + Output::CfuResponse(InternalResponseData::OfferResponse(FwUpdateOfferResponse::new_accept( + HostToken::Driver + ))) + ); + assert_eq!(cfu_basic.update_state().await, FwUpdateState::Idle); + assert_eq!(device.lock().await.fn_calls.len(), 1); + assert_eq!( + device.lock().await.fn_calls.pop_front().unwrap(), + FwFnCall::GetActiveFwVersion + ); + + assert_eq!(cfu_basic.customization().fn_calls.len(), 1); + assert_eq!( + cfu_basic.customization_mut().fn_calls.pop_front(), + Some(CustomizationFnCall::Validate( + FwVersion::new(CURRENT_FW_VERSION), + FwUpdateOffer::new( + HostToken::Driver, + DEVICE0_COMPONENT_ID, + FwVersion::new(NEW_FW_VERSION), + 0, + 0, + ) + )) + ); + } + + { + // Give first content block + let output = with_timeout( + PER_CALL_TIMEOUT, + cfu_basic.process_event(Event::Request(RequestData::GiveContent(FwUpdateContentCommand { + header: FwUpdateContentHeader { + flags: FW_UPDATE_FLAG_FIRST_BLOCK, + data_length: DEFAULT_DATA_LENGTH as u8, + sequence_num: 0, + firmware_address: 0x0, + }, + data: [1; DEFAULT_DATA_LENGTH], + }))), + ) + .await + .unwrap(); + + assert_eq!( + output, + Output::CfuResponse(InternalResponseData::ContentResponse(FwUpdateContentResponse::new( + 0, + CfuUpdateContentResponseStatus::Success + ))) + ); + assert_eq!(cfu_basic.update_state().await, FwUpdateState::InProgress(0)); + assert_eq!(device.lock().await.fn_calls.len(), 2); + assert_eq!( + device.lock().await.fn_calls.pop_front().unwrap(), + FwFnCall::StartFwUpdate + ); + assert_eq!( + device.lock().await.fn_calls.pop_front().unwrap(), + FwFnCall::WriteFwContents(0, vec![1; DEFAULT_DATA_LENGTH]) + ); + } + + { + // Trigger recovery + let output = with_timeout(PER_CALL_TIMEOUT, cfu_basic.process_event(Event::RecoveryTick)) + .await + .unwrap(); + + assert_eq!(output, Output::CfuRecovery); + // Idle since we should have successfully recovered + assert_eq!(cfu_basic.update_state().await, FwUpdateState::Idle); + assert_eq!(device.lock().await.fn_calls.len(), 1); + assert_eq!( + device.lock().await.fn_calls.pop_front().unwrap(), + FwFnCall::AbortFwUpdate + ); + } + } +} + +#[tokio::test] +async fn run_test_basic_flow() { + run_test(DEFAULT_TIMEOUT, TestBasicFlow).await; +} + +#[tokio::test] +async fn run_test_start_recovery_flow() { + run_test(DEFAULT_TIMEOUT, TestStartRecoveryFlow).await; +} + +/// Trait for runnable tests. +/// +/// This exists because there are lifetime issues with being generic over FnOnce or FnMut. +/// Those can be resolved, but having a dedicated trait is simpler. +pub trait Test { + fn run<'a>(&mut self, device: &'a DeviceType, cfu_basic: &'a mut UpdaterType<'a>) -> impl Future; +} + +/// Test running function +async fn run_test(timeout: Duration, mut test: impl Test) { + // Tokio runs tests in parallel, but logging is global so we need to run tests sequentially to avoid interleaved logs. + static TEST_MUTEX: OnceLock> = OnceLock::new(); + let test_mutex = TEST_MUTEX.get_or_init(|| Mutex::new(())); + let _lock = test_mutex.lock().await; + + // Initialize logging, ignore the error if the logger was already initialized by another test. + let _ = env_logger::builder().filter_level(log::LevelFilter::Debug).try_init(); + embedded_services::init().await; + + let shared_state: Mutex = Mutex::new(SharedState::default()); + let device = Mutex::new(Mock::new("PSU0", CURRENT_FW_VERSION)); + let mut cfu_basic = Updater::new( + &device, + &shared_state, + Default::default(), + DEVICE0_COMPONENT_ID, + MockCustomization::new(FwVersion::new(NEW_FW_VERSION)), + ); + + with_timeout(timeout, test.run(&device, &mut cfu_basic)).await.unwrap(); +} diff --git a/cfu-service/src/buffer.rs b/cfu-service/src/buffer.rs index fbc7422c1..6af16559a 100644 --- a/cfu-service/src/buffer.rs +++ b/cfu-service/src/buffer.rs @@ -10,14 +10,9 @@ use embassy_sync::{ }; use embassy_time::{Duration, TimeoutError, with_timeout}; use embedded_cfu_protocol::protocol_definitions::*; -use embedded_services::{ - GlobalRawMutex, - cfu::{ - self, - component::{CfuDevice, InternalResponseData, RequestData}, - }, - error, intrusive_list, trace, -}; +use embedded_services::{GlobalRawMutex, error, intrusive_list, trace}; + +use crate::component::{CfuDevice, InternalResponseData, RequestData}; /// Internal state for [`Buffer`] #[derive(Copy, Clone, Default)] @@ -97,9 +92,11 @@ impl<'a> Buffer<'a> { } /// Process a fw version request - async fn process_get_fw_version(&self) -> InternalResponseData { - if let Ok(InternalResponseData::FwVersionResponse(mut response)) = - cfu::route_request(self.buffered_id, RequestData::FwVersionRequest).await + async fn process_get_fw_version(&self, cfu_client: &crate::CfuClient) -> InternalResponseData { + if let Ok(InternalResponseData::FwVersionResponse(mut response)) = cfu_client + .context + .route_request(self.buffered_id, RequestData::FwVersionRequest) + .await { // Update the component ID in the response to match our external ID response.component_info[0].component_id = self.cfu_device.component_id(); @@ -110,8 +107,12 @@ impl<'a> Buffer<'a> { } } - async fn process_abort_update(&self) -> InternalResponseData { - match cfu::route_request(self.buffered_id, RequestData::AbortUpdate).await { + async fn process_abort_update(&self, cfu_client: &crate::CfuClient) -> InternalResponseData { + match cfu_client + .context + .route_request(self.buffered_id, RequestData::AbortUpdate) + .await + { Ok(response) => response, Err(e) => { error!("Failed to abort update for device {}: {:?}", self.buffered_id, e); @@ -121,11 +122,13 @@ impl<'a> Buffer<'a> { } /// Process a give offer request - async fn process_give_offer(&self, offer: &FwUpdateOffer) -> InternalResponseData { + async fn process_give_offer(&self, offer: &FwUpdateOffer, cfu_client: &crate::CfuClient) -> InternalResponseData { let mut offer = *offer; offer.component_info.component_id = self.buffered_id; - if let Ok(response @ InternalResponseData::OfferResponse(_)) = - cfu::route_request(self.buffered_id, RequestData::GiveOffer(offer)).await + if let Ok(response @ InternalResponseData::OfferResponse(_)) = cfu_client + .context + .route_request(self.buffered_id, RequestData::GiveOffer(offer)) + .await { response } else { @@ -139,7 +142,12 @@ impl<'a> Buffer<'a> { } /// Process update content - async fn process_give_content(&self, state: &mut State, content: &FwUpdateContentCommand) -> InternalResponseData { + async fn process_give_content( + &self, + state: &mut State, + content: &FwUpdateContentCommand, + cfu_client: &crate::CfuClient, + ) -> InternalResponseData { // Clear out any pending response if this is a new FW update if content.header.flags & FW_UPDATE_FLAG_FIRST_BLOCK != 0 { state.pending_response = None; @@ -153,7 +161,11 @@ impl<'a> Buffer<'a> { trace!("Content successfully buffered"); } else { // Buffered component can accept new content, send it - if let Err(e) = cfu::send_device_request(self.buffered_id, RequestData::GiveContent(*content)).await { + if let Err(e) = cfu_client + .context + .send_device_request(self.buffered_id, RequestData::GiveContent(*content)) + .await + { error!( "Failed to send content to buffered component {:?}: {:?}", self.buffered_id, e @@ -163,7 +175,12 @@ impl<'a> Buffer<'a> { } // Wait for a response from the buffered component - match with_timeout(self.config.buffer_timeout, cfu::wait_device_response(self.buffered_id)).await { + match with_timeout( + self.config.buffer_timeout, + cfu_client.context.wait_device_response(self.buffered_id), + ) + .await + { Err(TimeoutError) => { // Component didn't respond in time state.component_busy = true; @@ -224,7 +241,7 @@ impl<'a> Buffer<'a> { } /// Wait for an event - pub async fn wait_event(&self) -> Event { + pub async fn wait_event(&self, cfu_client: &crate::CfuClient) -> Event { let is_busy = self.state.lock().await.component_busy; match select3( // Wait for a buffered content request @@ -232,7 +249,7 @@ impl<'a> Buffer<'a> { // Wait for a request from the host self.cfu_device.wait_request(), // Wait for response from the buffered component - cfu::wait_device_response(self.buffered_id), + cfu_client.context.wait_device_response(self.buffered_id), ) .await { @@ -257,14 +274,18 @@ impl<'a> Buffer<'a> { } /// Top-level event processing function - pub async fn process(&self, event: Event) -> Option { + pub async fn process(&self, event: Event, cfu_client: &crate::CfuClient) -> Option { let mut state = self.state.lock().await; match event { - Event::CfuRequest(request) => Some(self.process_request(&mut state, request).await), + Event::CfuRequest(request) => Some(self.process_request(&mut state, request, cfu_client).await), Event::BufferedContent(content) => { // Send the buffered content to the component // Don't need to wait for a response here, the response will be caught later by either [`wait_event`] or [`process_give_content`] - if let Err(e) = cfu::send_device_request(self.buffered_id, RequestData::GiveContent(content)).await { + if let Err(e) = cfu_client + .context + .send_device_request(self.buffered_id, RequestData::GiveContent(content)) + .await + { error!( "Failed to send content to buffered component {:?}: {:?}", self.buffered_id, e @@ -285,23 +306,28 @@ impl<'a> Buffer<'a> { } /// Process a CFU message and produce a response - async fn process_request(&self, state: &mut State, request: RequestData) -> InternalResponseData { + async fn process_request( + &self, + state: &mut State, + request: RequestData, + cfu_client: &crate::CfuClient, + ) -> InternalResponseData { match request { RequestData::FwVersionRequest => { trace!("Got FwVersionRequest"); - self.process_get_fw_version().await + self.process_get_fw_version(cfu_client).await } RequestData::GiveOffer(offer) => { trace!("Got GiveOffer"); - self.process_give_offer(&offer).await + self.process_give_offer(&offer, cfu_client).await } RequestData::GiveContent(content) => { trace!("Got GiveContent"); - self.process_give_content(state, &content).await + self.process_give_content(state, &content, cfu_client).await } RequestData::AbortUpdate => { trace!("Got AbortUpdate"); - self.process_abort_update().await + self.process_abort_update(cfu_client).await } RequestData::FinalizeUpdate => { trace!("Got FinalizeUpdate"); @@ -338,7 +364,7 @@ impl<'a> Buffer<'a> { } /// Register the buffer with all relevant services - pub async fn register(&'static self) -> Result<(), intrusive_list::Error> { - cfu::register_device(&self.cfu_device).await + pub fn register(&'static self, cfu_client: &crate::CfuClient) -> Result<(), intrusive_list::Error> { + cfu_client.context.register_device(&self.cfu_device) } } diff --git a/embedded-service/src/cfu/component.rs b/cfu-service/src/component.rs similarity index 97% rename from embedded-service/src/cfu/component.rs rename to cfu-service/src/component.rs index d89773b35..6cac8120f 100644 --- a/embedded-service/src/cfu/component.rs +++ b/cfu-service/src/component.rs @@ -9,9 +9,8 @@ use embedded_cfu_protocol::writer::{CfuWriterAsync, CfuWriterError}; use heapless::Vec; use super::CfuError; -use crate::GlobalRawMutex; -use crate::cfu::route_request; -use crate::intrusive_list; +use embedded_services::GlobalRawMutex; +use embedded_services::intrusive_list; /// Component internal update state #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -222,7 +221,7 @@ impl CfuComponentDefault { } } /// wait for a request and process it - pub async fn process_request(&self) -> Result<(), CfuError> { + pub async fn process_request(&self, cfu_client: &'static crate::CfuClient) -> Result<(), CfuError> { match self.device.wait_request().await { RequestData::FwVersionRequest => { let fwv = self.get_fw_version().await.map_err(CfuError::ProtocolError)?; @@ -247,8 +246,10 @@ impl CfuComponentDefault { // panic safety: adding 1 here is safe because MAX_CMPT_COUNT is 1 more than MAX_SUBCMPT_COUNT for (index, id) in arr.iter().enumerate() { //info!("Forwarding GetFwVersion command to sub-component: {}", id); - if let InternalResponseData::FwVersionResponse(fwv) = - route_request(*id, RequestData::FwVersionRequest).await? + if let InternalResponseData::FwVersionResponse(fwv) = cfu_client + .context + .route_request(*id, RequestData::FwVersionRequest) + .await? { comp_info[index + 1] = fwv .component_info diff --git a/cfu-service/src/customization.rs b/cfu-service/src/customization.rs new file mode 100644 index 000000000..84d42e874 --- /dev/null +++ b/cfu-service/src/customization.rs @@ -0,0 +1,8 @@ +//! Common CFU customization trait +use embedded_cfu_protocol::protocol_definitions::{FwUpdateOffer, FwUpdateOfferResponse, FwVersion}; + +/// Common CFU customization trait +pub trait Customization { + /// Determine if we are accepting the firmware update offer, returns a CFU offer response + fn validate(&mut self, current: FwVersion, offer: &FwUpdateOffer) -> FwUpdateOfferResponse; +} diff --git a/cfu-service/src/lib.rs b/cfu-service/src/lib.rs index d248e30a4..4feccfef2 100644 --- a/cfu-service/src/lib.rs +++ b/cfu-service/src/lib.rs @@ -1,21 +1,26 @@ #![no_std] +use embassy_sync::channel::Channel; use embedded_cfu_protocol::client::CfuReceiveContent; use embedded_cfu_protocol::components::CfuComponentTraits; use embedded_cfu_protocol::protocol_definitions::*; -use embedded_services::cfu::component::*; -use embedded_services::cfu::{CfuError, ContextToken}; -use embedded_services::{comms, error, info, trace}; +use embedded_services::{GlobalRawMutex, comms, error, info, intrusive_list, trace}; +pub mod basic; pub mod buffer; +pub mod component; +pub mod customization; pub mod host; mod responses; pub mod splitter; pub mod task; +#[cfg(test)] +pub mod mocks; + pub struct CfuClient { /// Cfu Client context - context: ContextToken, + context: ClientContext, /// Comms endpoint tp: comms::Endpoint, } @@ -38,21 +43,32 @@ impl CfuReceiveContent for CfuClient { impl CfuClient { /// Create a new Cfu Client - pub fn create() -> Option { - Some(Self { - context: ContextToken::create()?, + pub async fn new(service_storage: &'static embassy_sync::once_lock::OnceLock) -> &'static Self { + let service_storage = service_storage.get_or_init(|| Self { + context: ClientContext::new(), tp: comms::Endpoint::uninit(comms::EndpointID::Internal(comms::Internal::Nonvol)), - }) + }); + + service_storage.init().await; + + service_storage } + + async fn init(&'static self) { + if comms::register_endpoint(self, &self.tp).await.is_err() { + error!("Failed to register cfu endpoint"); + } + } + pub async fn process_request(&self) -> Result<(), CfuError> { let request = self.context.wait_request().await; //let device = self.context.get_device(request.id).await?; let comp = request.id; match request.data { - RequestData::FwVersionRequest => { + component::RequestData::FwVersionRequest => { info!("Received FwVersionRequest, comp {}", comp); - if let Ok(device) = self.context.get_device(comp).await { + if let Ok(device) = self.context.get_device(comp) { let resp = device .execute_device_request(request.data) .await @@ -61,7 +77,7 @@ impl CfuClient { // TODO replace with signal to component to get its own fw version //cfu::send_request(comp, RequestData::FwVersionRequest).await?; match resp { - InternalResponseData::FwVersionResponse(r) => { + component::InternalResponseData::FwVersionResponse(r) => { let ver = r.component_info[0].fw_version; info!("got fw version {:?} for comp {}", ver, comp); } @@ -75,15 +91,15 @@ impl CfuClient { } Err(CfuError::InvalidComponent) } - RequestData::GiveContent(_content_cmd) => Ok(()), - RequestData::GiveOffer(_offer_cmd) => Ok(()), - RequestData::PrepareComponentForUpdate => Ok(()), - RequestData::AbortUpdate => Ok(()), - RequestData::FinalizeUpdate => Ok(()), - RequestData::GiveOfferExtended(_) => { + component::RequestData::GiveContent(_content_cmd) => Ok(()), + component::RequestData::GiveOffer(_offer_cmd) => Ok(()), + component::RequestData::PrepareComponentForUpdate => Ok(()), + component::RequestData::AbortUpdate => Ok(()), + component::RequestData::FinalizeUpdate => Ok(()), + component::RequestData::GiveOfferExtended(_) => { // Don't currently support extended offers self.context - .send_response(InternalResponseData::OfferResponse( + .send_response(component::InternalResponseData::OfferResponse( FwUpdateOfferResponse::new_with_failure( HostToken::Driver, OfferRejectReason::InvalidComponent, @@ -93,10 +109,10 @@ impl CfuClient { .await; Ok(()) } - RequestData::GiveOfferInformation(_) => { + component::RequestData::GiveOfferInformation(_) => { // Don't currently support information offers self.context - .send_response(InternalResponseData::OfferResponse( + .send_response(component::InternalResponseData::OfferResponse( FwUpdateOfferResponse::new_with_failure( HostToken::Driver, OfferRejectReason::InvalidComponent, @@ -108,6 +124,155 @@ impl CfuClient { } } } + + pub fn register_device( + &self, + device: &'static impl component::CfuDeviceContainer, + ) -> Result<(), intrusive_list::Error> { + self.context.register_device(device) + } + + pub async fn route_request( + &self, + to: ComponentId, + request: component::RequestData, + ) -> Result { + self.context.route_request(to, request).await + } } impl comms::MailboxDelegate for CfuClient {} + +/// Error type +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum CfuError { + /// Image did not pass validation + BadImage, + /// Component either doesn't exist + InvalidComponent, + /// Component is busy + ComponentBusy, + /// Component encountered a protocol error during execution + ProtocolError(CfuProtocolError), +} + +/// Request to the power policy service +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct Request { + /// Component that sent this request + pub id: ComponentId, + /// Request data + pub data: component::RequestData, +} + +/// Cfu context +pub struct ClientContext { + /// Registered devices + devices: embedded_services::intrusive_list::IntrusiveList, + /// Request to components + request: Channel, + /// Response from components + response: Channel, +} + +impl Default for ClientContext { + fn default() -> Self { + Self::new() + } +} + +impl ClientContext { + pub fn new() -> Self { + Self { + devices: embedded_services::intrusive_list::IntrusiveList::new(), + request: Channel::new(), + response: Channel::new(), + } + } + + /// Register a device with the Cfu Client service + fn register_device( + &self, + device: &'static impl component::CfuDeviceContainer, + ) -> Result<(), intrusive_list::Error> { + let device = device.get_cfu_component_device(); + if self.get_device(device.component_id()).is_ok() { + return Err(intrusive_list::Error::NodeAlreadyInList); + } + + self.devices.push(device) + } + + /// Convenience function to send a request to the Cfu service + pub async fn send_request( + &self, + from: ComponentId, + request: component::RequestData, + ) -> Result { + self.request + .send(Request { + id: from, + data: request, + }) + .await; + Ok(self.response.receive().await) + } + + /// Convenience function to route a request to a specific component + pub async fn route_request( + &self, + to: ComponentId, + request: component::RequestData, + ) -> Result { + let device = self.get_device(to)?; + device + .execute_device_request(request) + .await + .map_err(CfuError::ProtocolError) + } + + /// Send a request to the specific CFU device, but don't wait for a response + pub async fn send_device_request(&self, to: ComponentId, request: component::RequestData) -> Result<(), CfuError> { + let device = self.get_device(to)?; + device.send_request(request).await; + Ok(()) + } + + /// Wait for a response from the specific CFU device + pub async fn wait_device_response(&self, to: ComponentId) -> Result { + let device = self.get_device(to)?; + Ok(device.wait_response().await) + } + + /// Wait for a cfu request + pub async fn wait_request(&self) -> Request { + self.request.receive().await + } + + /// Send a response to a cfu request + pub async fn send_response(&self, response: component::InternalResponseData) { + self.response.send(response).await + } + + /// Get a device by its ID + pub fn get_device(&self, id: ComponentId) -> Result<&'static component::CfuDevice, CfuError> { + for device in &self.devices { + if let Some(data) = device.data::() { + if data.component_id() == id { + return Ok(data); + } + } else { + error!("Non-device located in devices list"); + } + } + + Err(CfuError::InvalidComponent) + } + + /// Provides access to the device list + pub fn devices(&self) -> &intrusive_list::IntrusiveList { + &self.devices + } +} diff --git a/cfu-service/src/mocks/customization.rs b/cfu-service/src/mocks/customization.rs new file mode 100644 index 000000000..ee184c102 --- /dev/null +++ b/cfu-service/src/mocks/customization.rs @@ -0,0 +1,83 @@ +//! Code related to mock for [`cfu_service::customization::Customization`] +extern crate std; + +use std::collections::VecDeque; + +use crate::customization::Customization; +use embedded_cfu_protocol::protocol_definitions::{ + FwUpdateOffer, FwUpdateOfferResponse, FwVersion, HostToken, OfferRejectReason, OfferStatus, +}; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum FnCall { + Validate(FwVersion, FwUpdateOffer), +} + +/// Simple mock +pub struct Mock { + /// Queue to record function calls + pub fn_calls: VecDeque, + /// Acceptable version + acceptable_version: FwVersion, +} + +impl Mock { + /// Create a new mock + pub fn new(acceptable_version: FwVersion) -> Self { + Self { + fn_calls: VecDeque::new(), + acceptable_version, + } + } + + fn record_fn_call(&mut self, fn_call: FnCall) { + self.fn_calls.push_back(fn_call); + } +} + +impl Customization for Mock { + fn validate(&mut self, current: FwVersion, fw_update_offer: &FwUpdateOffer) -> FwUpdateOfferResponse { + self.record_fn_call(FnCall::Validate(current, *fw_update_offer)); + if fw_update_offer.firmware_version == self.acceptable_version { + FwUpdateOfferResponse::new_accept(HostToken::Driver) + } else { + FwUpdateOfferResponse::new_with_failure(HostToken::Driver, OfferRejectReason::OldFw, OfferStatus::Reject) + } + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_validate_accept() { + let acceptable_version = FwVersion::new(1); + let mut mock = Mock::new(acceptable_version); + let fw_update_offer = FwUpdateOffer::new(HostToken::Driver, 0, FwVersion::new(1), 0, 0); + let response = mock.validate(acceptable_version, &fw_update_offer); + assert_eq!(response, FwUpdateOfferResponse::new_accept(HostToken::Driver)); + assert_eq!(mock.fn_calls.len(), 1); + assert_eq!( + mock.fn_calls.pop_front(), + Some(FnCall::Validate(acceptable_version, fw_update_offer)) + ); + } + + #[test] + fn test_validate_reject() { + let acceptable_version = FwVersion::new(1); + let mut mock = Mock::new(acceptable_version); + let fw_update_offer = FwUpdateOffer::new(HostToken::Driver, 0, FwVersion::new(9), 0, 0); + let response = mock.validate(acceptable_version, &fw_update_offer); + assert_eq!( + response, + FwUpdateOfferResponse::new_with_failure(HostToken::Driver, OfferRejectReason::OldFw, OfferStatus::Reject) + ); + assert_eq!(mock.fn_calls.len(), 1); + assert_eq!( + mock.fn_calls.pop_front(), + Some(FnCall::Validate(acceptable_version, fw_update_offer)) + ); + } +} diff --git a/cfu-service/src/mocks/mod.rs b/cfu-service/src/mocks/mod.rs new file mode 100644 index 000000000..37510cd59 --- /dev/null +++ b/cfu-service/src/mocks/mod.rs @@ -0,0 +1,2 @@ +//! Mocks for [`cfu_service`] testing. +pub mod customization; diff --git a/cfu-service/src/responses.rs b/cfu-service/src/responses.rs index 961e96f06..53782c99b 100644 --- a/cfu-service/src/responses.rs +++ b/cfu-service/src/responses.rs @@ -3,7 +3,7 @@ use embedded_cfu_protocol::protocol_definitions::{ GetFwVerRespHeaderByte3, GetFwVersionResponse, GetFwVersionResponseHeader, MAX_CMPT_COUNT, }; -use embedded_services::cfu::component::InternalResponseData; +use crate::component::InternalResponseData; const INVALID_FW_VERSION: u32 = u32::MAX; diff --git a/cfu-service/src/splitter.rs b/cfu-service/src/splitter.rs index bbbed3d50..c0d02a546 100644 --- a/cfu-service/src/splitter.rs +++ b/cfu-service/src/splitter.rs @@ -3,15 +3,10 @@ use core::{future::Future, iter::zip}; +use crate::component; use embassy_futures::join::{join, join3, join4}; use embedded_cfu_protocol::protocol_definitions::*; -use embedded_services::{ - cfu::{ - self, - component::{CfuDevice, InternalResponseData, RequestData}, - }, - error, intrusive_list, trace, -}; +use embedded_services::{error, intrusive_list, trace}; /// Trait containing customization functionality for [`Splitter`] pub trait Customization { @@ -28,7 +23,7 @@ pub trait Customization { /// Splitter struct pub struct Splitter<'a, C: Customization> { /// CFU device - cfu_device: CfuDevice, + cfu_device: component::CfuDevice, /// Component ID for each individual device devices: &'a [ComponentId], /// Customization for the Splitter @@ -45,7 +40,7 @@ impl<'a, C: Customization> Splitter<'a, C> { None } else { Some(Self { - cfu_device: CfuDevice::new(component_id), + cfu_device: component::CfuDevice::new(component_id), devices, customization, }) @@ -53,15 +48,17 @@ impl<'a, C: Customization> Splitter<'a, C> { } /// Process a fw version request - async fn process_get_fw_version(&self) -> InternalResponseData { + async fn process_get_fw_version(&self, cfu_client: &crate::CfuClient) -> component::InternalResponseData { let mut versions = [GetFwVersionResponse { header: Default::default(), component_info: Default::default(), }; MAX_SUPPORTED_DEVICES]; let success = map_slice_join(self.devices, &mut versions, |device_id| async move { - if let Ok(InternalResponseData::FwVersionResponse(version_info)) = - cfu::route_request(*device_id, RequestData::FwVersionRequest).await + if let Ok(component::InternalResponseData::FwVersionResponse(version_info)) = cfu_client + .context + .route_request(*device_id, component::RequestData::FwVersionRequest) + .await { Some(version_info) } else { @@ -76,14 +73,18 @@ impl<'a, C: Customization> Splitter<'a, C> { // The overall component version comes first overall_version.component_info[0].component_id = self.cfu_device.component_id(); - InternalResponseData::FwVersionResponse(overall_version) + component::InternalResponseData::FwVersionResponse(overall_version) } else { crate::responses::create_invalid_fw_version_response(self.cfu_device.component_id()) } } /// Process a give offer request - async fn process_give_offer(&self, offer: &FwUpdateOffer) -> InternalResponseData { + async fn process_give_offer( + &self, + offer: &FwUpdateOffer, + cfu_client: &crate::CfuClient, + ) -> component::InternalResponseData { let mut offer_responses = [FwUpdateOfferResponse::default(); MAX_SUPPORTED_DEVICES]; let success = map_slice_join(self.devices, &mut offer_responses, |device_id| async move { @@ -91,8 +92,10 @@ impl<'a, C: Customization> Splitter<'a, C> { // Override with the correct component ID for the device offer.component_info.component_id = *device_id; - if let Ok(InternalResponseData::OfferResponse(response)) = - cfu::route_request(*device_id, RequestData::GiveOffer(offer)).await + if let Ok(component::InternalResponseData::OfferResponse(response)) = cfu_client + .context + .route_request(*device_id, component::RequestData::GiveOffer(offer)) + .await { Some(response) } else { @@ -103,19 +106,27 @@ impl<'a, C: Customization> Splitter<'a, C> { .await; if success && let Some(offer_responses_slice) = offer_responses.get(..self.devices.len()) { - InternalResponseData::OfferResponse(self.customization.resolve_offer_response(offer_responses_slice)) + component::InternalResponseData::OfferResponse( + self.customization.resolve_offer_response(offer_responses_slice), + ) } else { crate::responses::create_invalid_fw_version_response(self.cfu_device.component_id()) } } /// Process update content - async fn process_give_content(&self, content: &FwUpdateContentCommand) -> InternalResponseData { + async fn process_give_content( + &self, + content: &FwUpdateContentCommand, + cfu_client: &crate::CfuClient, + ) -> component::InternalResponseData { let mut content_responses = [FwUpdateContentResponse::default(); MAX_SUPPORTED_DEVICES]; let success = map_slice_join(self.devices, &mut content_responses, |device_id| async move { - if let Ok(InternalResponseData::ContentResponse(response)) = - cfu::route_request(*device_id, RequestData::GiveContent(*content)).await + if let Ok(component::InternalResponseData::ContentResponse(response)) = cfu_client + .context + .route_request(*device_id, component::RequestData::GiveContent(*content)) + .await { Some(response) } else { @@ -126,57 +137,63 @@ impl<'a, C: Customization> Splitter<'a, C> { .await; if success && let Some(content_responses_slice) = content_responses.get(..self.devices.len()) { - InternalResponseData::ContentResponse(self.customization.resolve_content_response(content_responses_slice)) + component::InternalResponseData::ContentResponse( + self.customization.resolve_content_response(content_responses_slice), + ) } else { crate::responses::create_content_rejection(content.header.sequence_num) } } /// Wait for a CFU message - pub async fn wait_request(&self) -> RequestData { + pub async fn wait_request(&self) -> component::RequestData { self.cfu_device.wait_request().await } /// Process a CFU message and produce a response - pub async fn process_request(&self, request: RequestData) -> InternalResponseData { + pub async fn process_request( + &self, + request: component::RequestData, + cfu_client: &crate::CfuClient, + ) -> component::InternalResponseData { match request { - RequestData::FwVersionRequest => { + component::RequestData::FwVersionRequest => { trace!("Got FwVersionRequest"); - self.process_get_fw_version().await + self.process_get_fw_version(cfu_client).await } - RequestData::GiveOffer(offer) => { + component::RequestData::GiveOffer(offer) => { trace!("Got GiveOffer"); - self.process_give_offer(&offer).await + self.process_give_offer(&offer, cfu_client).await } - RequestData::GiveContent(content) => { + component::RequestData::GiveContent(content) => { trace!("Got GiveContent"); - self.process_give_content(&content).await + self.process_give_content(&content, cfu_client).await } - RequestData::AbortUpdate => { + component::RequestData::AbortUpdate => { trace!("Got AbortUpdate"); - InternalResponseData::ComponentPrepared + component::InternalResponseData::ComponentPrepared } - RequestData::FinalizeUpdate => { + component::RequestData::FinalizeUpdate => { trace!("Got FinalizeUpdate"); - InternalResponseData::ComponentPrepared + component::InternalResponseData::ComponentPrepared } - RequestData::PrepareComponentForUpdate => { + component::RequestData::PrepareComponentForUpdate => { trace!("Got PrepareComponentForUpdate"); - InternalResponseData::ComponentPrepared + component::InternalResponseData::ComponentPrepared } - RequestData::GiveOfferExtended(_) => { + component::RequestData::GiveOfferExtended(_) => { trace!("Got GiveExtendedOffer"); // Extended offers are not currently supported - InternalResponseData::OfferResponse(FwUpdateOfferResponse::new_with_failure( + component::InternalResponseData::OfferResponse(FwUpdateOfferResponse::new_with_failure( HostToken::Driver, OfferRejectReason::InvalidComponent, OfferStatus::Reject, )) } - RequestData::GiveOfferInformation(_) => { + component::RequestData::GiveOfferInformation(_) => { trace!("Got GiveOfferInformation"); // Offer information is not currently supported - InternalResponseData::OfferResponse(FwUpdateOfferResponse::new_with_failure( + component::InternalResponseData::OfferResponse(FwUpdateOfferResponse::new_with_failure( HostToken::Driver, OfferRejectReason::InvalidComponent, OfferStatus::Reject, @@ -186,12 +203,12 @@ impl<'a, C: Customization> Splitter<'a, C> { } /// Send a response to the CFU message - pub async fn send_response(&self, response: InternalResponseData) { + pub async fn send_response(&self, response: component::InternalResponseData) { self.cfu_device.send_response(response).await; } - pub async fn register(&'static self) -> Result<(), intrusive_list::Error> { - cfu::register_device(&self.cfu_device).await + pub fn register(&'static self, cfu_client: &crate::CfuClient) -> Result<(), intrusive_list::Error> { + cfu_client.context.register_device(&self.cfu_device) } } diff --git a/cfu-service/src/task.rs b/cfu-service/src/task.rs index ef71d8cfa..f4dd11879 100644 --- a/cfu-service/src/task.rs +++ b/cfu-service/src/task.rs @@ -1,21 +1,12 @@ -use embassy_sync::once_lock::OnceLock; -use embedded_services::{comms, error, info}; +use embedded_services::{error, info}; use crate::CfuClient; -pub async fn task() { +pub async fn task(cfu_client: &'static CfuClient) { info!("Starting cfu client task"); - static CLIENT: OnceLock = OnceLock::new(); - #[allow(clippy::expect_used)] // panic safety: singleton panic on initialization - let cfuclient = CLIENT.get_or_init(|| CfuClient::create().expect("cfu client singleton already initialized")); - - if comms::register_endpoint(cfuclient, &cfuclient.tp).await.is_err() { - error!("Failed to register cfu endpoint"); - return; - } loop { - if let Err(e) = cfuclient.process_request().await { + if let Err(e) = cfu_client.process_request().await { error!("Error processing request: {:?}", e); } } diff --git a/debug-service-messages/Cargo.toml b/debug-service-messages/Cargo.toml new file mode 100644 index 000000000..5127a7579 --- /dev/null +++ b/debug-service-messages/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "debug-service-messages" +version.workspace = true +edition.workspace = true +license.workspace = true +repository.workspace = true + +[dependencies] +defmt = { workspace = true, optional = true } +embedded-services.workspace = true +num_enum.workspace = true + +[lints] +workspace = true + +[features] +defmt = ["dep:defmt"] diff --git a/debug-service-messages/src/lib.rs b/debug-service-messages/src/lib.rs new file mode 100644 index 000000000..a4dafc673 --- /dev/null +++ b/debug-service-messages/src/lib.rs @@ -0,0 +1,127 @@ +#![no_std] +use embedded_services::relay::{MessageSerializationError, SerializableMessage}; + +/// Standard Debug Service Log Buffer Size +pub const STD_DEBUG_BUF_SIZE: usize = 128; + +#[derive(num_enum::IntoPrimitive, num_enum::TryFromPrimitive, Copy, Clone, Debug, PartialEq)] +#[repr(u16)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +/// ODP Specific Debug Commands +enum DebugCmd { + /// Get buffer of debug messages, if available. + /// Can be used to poll debug messages. + GetMsgs = 1, +} + +impl From<&DebugRequest> for DebugCmd { + fn from(request: &DebugRequest) -> Self { + match request { + DebugRequest::DebugGetMsgsRequest => DebugCmd::GetMsgs, + } + } +} + +impl From<&DebugResponse> for DebugCmd { + fn from(response: &DebugResponse) -> Self { + match response { + DebugResponse::DebugGetMsgsResponse { .. } => DebugCmd::GetMsgs, + } + } +} + +#[derive(PartialEq, Clone, Copy)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum DebugRequest { + DebugGetMsgsRequest, +} + +impl SerializableMessage for DebugRequest { + fn serialize(self, _buffer: &mut [u8]) -> Result { + match self { + Self::DebugGetMsgsRequest => Ok(0), + } + } + + fn deserialize(discriminant: u16, _buffer: &[u8]) -> Result { + Ok( + match DebugCmd::try_from(discriminant) + .map_err(|_| MessageSerializationError::UnknownMessageDiscriminant(discriminant))? + { + DebugCmd::GetMsgs => Self::DebugGetMsgsRequest, + }, + ) + } + + fn discriminant(&self) -> u16 { + let cmd: DebugCmd = self.into(); + cmd.into() + } +} + +#[derive(PartialEq, Clone, Copy)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum DebugResponse { + DebugGetMsgsResponse { debug_buf: [u8; STD_DEBUG_BUF_SIZE] }, +} + +impl SerializableMessage for DebugResponse { + fn serialize(self, buffer: &mut [u8]) -> Result { + match self { + Self::DebugGetMsgsResponse { debug_buf } => { + buffer + .get_mut(..debug_buf.len()) + .ok_or(MessageSerializationError::BufferTooSmall)? + .copy_from_slice(&debug_buf); + Ok(debug_buf.len()) + } + } + } + + fn deserialize(discriminant: u16, buffer: &[u8]) -> Result { + Ok( + match DebugCmd::try_from(discriminant) + .map_err(|_| MessageSerializationError::UnknownMessageDiscriminant(discriminant))? + { + DebugCmd::GetMsgs => Self::DebugGetMsgsResponse { + debug_buf: buffer + .get(0..STD_DEBUG_BUF_SIZE) + .ok_or(MessageSerializationError::BufferTooSmall)? + .try_into() + .map_err(|_| MessageSerializationError::BufferTooSmall)?, + }, + }, + ) + } + + fn discriminant(&self) -> u16 { + DebugCmd::from(self).into() + } +} + +#[derive(num_enum::IntoPrimitive, num_enum::TryFromPrimitive, Copy, Clone, Debug, PartialEq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +#[repr(u16)] +pub enum DebugError { + UnspecifiedFailure = 1, +} + +impl SerializableMessage for DebugError { + fn serialize(self, _buffer: &mut [u8]) -> Result { + match self { + Self::UnspecifiedFailure => Ok(0), + } + } + + fn deserialize(_discriminant: u16, _buffer: &[u8]) -> Result { + Err(MessageSerializationError::Other( + "unimplemented - don't need to deserialize responses on the EC side", + )) + } + + fn discriminant(&self) -> u16 { + (*self).into() + } +} + +pub type DebugResult = Result; diff --git a/debug-service/Cargo.toml b/debug-service/Cargo.toml index 415d95599..5381d6267 100644 --- a/debug-service/Cargo.toml +++ b/debug-service/Cargo.toml @@ -12,10 +12,14 @@ ignored = ["log"] bbq2 = "0.4.2" critical-section.workspace = true defmt.workspace = true +debug-service-messages.workspace = true embassy-sync.workspace = true embedded-services.workspace = true log.workspace = true rtt-target = "0.6.1" +[features] +defmt = ["debug-service-messages/defmt"] + [lints] workspace = true diff --git a/debug-service/src/debug_service.rs b/debug-service/src/debug_service.rs index 134fed490..ef9762307 100644 --- a/debug-service/src/debug_service.rs +++ b/debug-service/src/debug_service.rs @@ -1,9 +1,8 @@ +use debug_service_messages::{DebugRequest, DebugResult}; use embassy_sync::{once_lock::OnceLock, signal::Signal}; use embedded_services::GlobalRawMutex; use embedded_services::buffer::{OwnedRef, SharedRef}; -use embedded_services::comms::{self, EndpointID, Internal}; -use embedded_services::ec_type::message::StdHostRequest; -use embedded_services::{debug, error}; +use embedded_services::debug; // Maximum number of bytes to request per defmt frame write grant. // This decouples the logger from any external protocol-specific size constants. @@ -15,75 +14,43 @@ use embedded_services::{debug, error}; // - No partial publication: a frame is either not yet committed or fully committed. // If a defmt log event were to exceed this size, it will be split across multiple // BBQueue frames (each ≤ 1024). The consumer always observes complete frames. -pub(crate) const DEFMT_MAX_BYTES: u16 = embedded_services::ec_type::message::STD_DEBUG_BUF_SIZE as u16; +pub(crate) const DEFMT_MAX_BYTES: u16 = debug_service_messages::STD_DEBUG_BUF_SIZE as u16; // Static buffer for ACPI-style messages carrying defmt frames embedded_services::define_static_buffer!(defmt_acpi_buf, u8, [0u8; DEFMT_MAX_BYTES as usize]); /// Debug service that bridges an internal endpoint to an external transport. -/// -/// Terminology: -/// - Transport: The external-facing `comms::Endpoint` used to reach the host/PC. -/// It is provided by the platform (eSPI, USB, RTT bridge, etc.) and passed to -/// [`Service::new`]. Its ID is commonly `EndpointID::External(External::Host)`, -/// but the service does not assume a specific value. -/// - Endpoint: The internal endpoint owned by this service and registered under -/// `EndpointID::Internal(Internal::Debug)`. Messages addressed to this ID are -/// dispatched to the service via [`comms::MailboxDelegate::receive`]. -/// -/// Direction: -/// - Device → Host: Producers (e.g., the defmt forwarding task) should send from -/// `EndpointID::Internal(Internal::Debug)` to the transport endpoint ID exposed -/// by [`Service::endpoint_id`] or [`host_endpoint_id`]. -/// - Host → Device: The platform transport should deliver host messages to -/// `EndpointID::Internal(Internal::Debug)`, which this service handles in -/// [`receive`](comms::MailboxDelegate::receive). +#[derive(Default)] pub struct Service { - // The service-owned internal endpoint (Internal::Debug) that is registered - // with the comms layer and used as the "device side" address. - endpoint: comms::Endpoint, - // The external transport endpoint through which host traffic flows. - // This is provided by the platform and may map to eSPI/USB/etc. - transport: comms::Endpoint, // Hack frame_available: core::sync::atomic::AtomicBool, } impl Service { - pub const fn new(endpoint: comms::Endpoint) -> Self { + pub const fn new() -> Self { Service { - endpoint: comms::Endpoint::uninit(EndpointID::Internal(Internal::Debug)), - transport: endpoint, frame_available: core::sync::atomic::AtomicBool::new(false), } } +} - /// Returns the `EndpointID` of the external transport used by this service. - /// - /// Other components should target this ID when sending messages to the host - /// via the debug service - pub fn endpoint_id(&self) -> comms::EndpointID { - self.transport.get_id() - } +impl embedded_services::relay::mctp::RelayServiceHandlerTypes for Service { + type RequestType = DebugRequest; + type ResultType = DebugResult; } -impl comms::MailboxDelegate for Service { - fn receive(&self, message: &comms::Message) -> Result<(), comms::MailboxDelegateError> { - if let Some(_request) = message.data.get::() { - // Host sent an ACPI/MCTP request (e.g. GetDebugBuffer). Treat this as the - // trigger to send the staged debug buffer back to the host. - embedded_services::trace!("Received host ACPI request for debug buffer from {:?}", message.from); - // We only use the signal as a wakeup; the defmt task ignores any payload here. - if self.frame_available.load(core::sync::atomic::Ordering::SeqCst) { - response_notify_signal().signal(()); - } else { - no_avail_notify_signal().signal(()); - } +impl embedded_services::relay::mctp::RelayServiceHandler for Service { + async fn process_request(&self, _request: Self::RequestType) -> Self::ResultType { + // Host sent an ACPI/MCTP request (e.g. GetDebugBuffer). Treat this as the + // trigger to send the staged debug buffer back to the host. + // We only use the signal as a wakeup; the defmt task ignores any payload here. + if self.frame_available.load(core::sync::atomic::Ordering::SeqCst) { + response_notify_signal().signal(()); } else { - error!("Received unknown message from host"); + no_avail_notify_signal().signal(()); } - Ok(()) + frame_ready_signal().wait().await } } @@ -96,6 +63,9 @@ static RESP_NOTIFY: OnceLock> = OnceLock::new(); // For no frame avail task static NO_AVAIL_NOTIFY: OnceLock> = OnceLock::new(); +// Frame to send to host +static FRAME_READY: OnceLock> = OnceLock::new(); + pub(crate) fn owned_buffer() -> OwnedRef<'static, u8> { defmt_acpi_buf::get_mut().expect("defmt staging buffer already initialized elsewhere") } @@ -117,18 +87,13 @@ pub(crate) fn no_avail_notify_signal() -> &'static Signal { NO_AVAIL_NOTIFY.get_or_init(Signal::new) } -/// Returns the endpoint ID of the transport used by the debug service. -pub async fn host_endpoint_id() -> EndpointID { - let svc = DEBUG_SERVICE.get().await; - svc.endpoint_id() +pub(crate) fn frame_ready_signal() -> &'static Signal { + FRAME_READY.get_or_init(Signal::new) } /// Initialize and register the global Debug service endpoint. /// -/// This creates (or reuses) a single [`Service`] instance backed by the -/// provided transport [`comms::Endpoint`], then registers its internal -/// endpoint so messages addressed to [`EndpointID::Internal(Internal::Debug)`] -/// are dispatched to the service's [`comms::MailboxDelegate`] implementation. +/// This creates (or reuses) a single [`Service`] instance. /// /// Behavior: /// - Idempotent: repeated or concurrent calls return the same global instance. @@ -138,18 +103,14 @@ pub async fn host_endpoint_id() -> EndpointID { /// /// # Example /// ```no_run -/// use embedded_services::comms; /// use debug_service::debug_service_entry; /// -/// async fn boot(ep: comms::Endpoint) { -/// debug_service_entry(ep).await; +/// async fn boot() { +/// debug_service_entry().await; /// } /// ``` -pub async fn debug_service_entry(endpoint: comms::Endpoint) { - let debug_service = DEBUG_SERVICE.get_or_init(|| Service::new(endpoint)); - comms::register_endpoint(debug_service, &debug_service.endpoint) - .await - .unwrap(); +pub async fn debug_service_entry() { + let _debug_service = DEBUG_SERVICE.get_or_init(Service::new); // Emit an initial defmt frame so the defmt_to_host_task can drain and verify the path. - debug!("debug service initialized and endpoint registered"); + debug!("debug service initialized"); } diff --git a/debug-service/src/lib.rs b/debug-service/src/lib.rs index 827847212..48db64975 100644 --- a/debug-service/src/lib.rs +++ b/debug-service/src/lib.rs @@ -3,8 +3,29 @@ #![allow(clippy::indexing_slicing)] #![allow(clippy::unwrap_used)] +// This module has a hard dependency on defmt, which doesn't work on desktop. +// This means that the entire workspace's tests won't compile if this module is enabled. +// +// On Linux, we sort-of get away with it - as far as I can tell, the linker on Linux is more aggressive +// with pruning unused code, so as long as there's no test that calls into anything that eventually calls +// into defmt, we at least compile on Linux. +// +// However, on Windows, it looks like the linker is erroring out because it can't find defmt-related symbols before +// it does the analysis to determine that those symbols aren't reachable anyway, so we have to disable this module +// entirely to be able to compile the workspace's tests at all on Windows. +// +// If we ever want to run tests for this module on Windows, we'll need some way to either break the dependency +// on defmt or provide dummy implementations of the defmt symbols for test builds on Windows. Until then, +// we need to gate everything on #[cfg(not(test))]. + +#[cfg(not(test))] mod debug_service; + +#[cfg(not(test))] mod defmt_ring_logger; + +#[cfg(not(test))] pub mod task; +#[cfg(not(test))] pub use debug_service::*; diff --git a/debug-service/src/task.rs b/debug-service/src/task.rs index 757c31bbe..67e7a8636 100644 --- a/debug-service/src/task.rs +++ b/debug-service/src/task.rs @@ -1,31 +1,24 @@ use core::borrow::{Borrow, BorrowMut}; -use embedded_services::{ - comms, - ec_type::message::{StdHostPayload, StdHostRequest}, -}; +use debug_service_messages::{DebugError, DebugResponse}; -use crate::{debug_service_entry, defmt_ring_logger::DEFMT_BUFFER, frame_available, shared_buffer}; +use crate::{debug_service_entry, defmt_ring_logger::DEFMT_BUFFER, frame_available, frame_ready_signal, shared_buffer}; #[derive(Copy, Clone, Debug, PartialEq, Eq)] pub enum Error { Buffer(embedded_services::buffer::Error), } -pub async fn debug_service(endpoint: comms::Endpoint) { - debug_service_entry(endpoint).await; +pub async fn debug_service() { + debug_service_entry().await; } pub async fn defmt_to_host_task() -> Result { embedded_services::info!("defmt to host task start"); - use crate::debug_service::{host_endpoint_id, response_notify_signal}; - use embedded_services::comms::{self, EndpointID, Internal}; - use embedded_services::ec_type::message::HostMsg; + use crate::debug_service::response_notify_signal; let framed_consumer = DEFMT_BUFFER.framed_consumer(); - let host_ep = host_endpoint_id().await; - // Acquire the staging buffer once; we own it for the task lifetime. let acpi_owned = crate::owned_buffer(); @@ -67,20 +60,14 @@ pub async fn defmt_to_host_task() -> Result { // Send the staged defmt bytes frame as an ACPI-style message. // Scope the message so the shared borrow is dropped before we clear the buffer. { - let msg = HostMsg::Response(StdHostRequest { - command: embedded_services::ec_type::message::OdpCommand::Debug( - embedded_services::ec_type::protocols::debug::DebugCmd::GetMsgs, - ), - status: 0, - payload: StdHostPayload::DebugGetMsgsResponse { - debug_buf: { - let access = shared_buffer().borrow().map_err(Error::Buffer)?; - let slice: &[u8] = access.borrow(); - slice.try_into().unwrap() - }, + let msg = DebugResponse::DebugGetMsgsResponse { + debug_buf: { + let access = shared_buffer().borrow().map_err(Error::Buffer)?; + let slice: &[u8] = access.borrow(); + slice.try_into().unwrap() }, - }); - let _ = comms::send(EndpointID::Internal(Internal::Debug), host_ep, &msg).await; + }; + frame_ready_signal().signal(Ok(msg)); embedded_services::trace!("sent {} defmt bytes to host", copy_len); } @@ -97,11 +84,7 @@ pub async fn no_avail_to_host_task() -> Result embedded_services::define_static_buffer!(no_avail_acpi_buf, u8, [0u8; 12]); embedded_services::info!("no avail to host task start"); - use crate::debug_service::{host_endpoint_id, no_avail_notify_signal}; - use embedded_services::comms::{self, EndpointID, Internal}; - use embedded_services::ec_type::message::HostMsg; - - let host_ep = host_endpoint_id().await; + use crate::debug_service::no_avail_notify_signal; let acpi_owned = no_avail_acpi_buf::get_mut().expect("defmt staging buffer already initialized elsewhere"); { @@ -111,17 +94,11 @@ pub async fn no_avail_to_host_task() -> Result buf[4..12].copy_from_slice(&0xDEADBEEFu64.to_be_bytes()); } - let msg = HostMsg::Response(StdHostRequest { - command: embedded_services::ec_type::message::OdpCommand::Debug( - embedded_services::ec_type::protocols::debug::DebugCmd::GetMsgs, - ), - status: 1, - payload: StdHostPayload::ErrorResponse {}, - }); + let msg: Result = Err(DebugError::UnspecifiedFailure); // Send DEADBEEF if host requests frame but non available loop { no_avail_notify_signal().wait().await; - let _ = comms::send(EndpointID::Internal(Internal::Debug), host_ep, &msg).await; + frame_ready_signal().signal(msg); } } diff --git a/deny.toml b/deny.toml index e4ce30f88..9bdc71bda 100644 --- a/deny.toml +++ b/deny.toml @@ -78,7 +78,7 @@ ignore = [ { id = "RUSTSEC-2024-0436", reason = "there are no suitable replacements for paste right now; paste has been archived as read-only. It only affects compile time concatenation in macros. We will allow it for now" }, { id = "RUSTSEC-2023-0089", reason = "this is a deprecation warning for a dependency of a dependency. https://github.com/jamesmunns/postcard/issues/223 tracks fixing the dependency; until that's resolved, we can accept the deprecated code as it has no known vulnerabilities." }, { id = "RUSTSEC-2025-0141", reason = "bincode is unmaintained, planning on migrating to an alternative." }, - { id = "RUSTSEC-2026-0110", reason = "bare-metal is unmaintained, no safe upgrade available, need upstream dependencies to migrate away from it." }, + { id = "RUSTSEC-2026-0110", reason = "bare-metal is deprecated and archived, which cortex-m has a dependency on. Need cortex-m to migrate away from it." }, ] # If this is true, then cargo deny will use the git executable to fetch advisory database. # If this is false, then it uses a built-in git library. diff --git a/docs/api-guidelines.md b/docs/api-guidelines.md new file mode 100644 index 000000000..1918abf68 --- /dev/null +++ b/docs/api-guidelines.md @@ -0,0 +1,285 @@ +# Service API Guidelines + +This document establishes some guidelines that APIs in this repo should try to conform to, and explains the rationale behind those guidelines to help guide decisionmaking when tradeoffs need to be made. + +These guidelines attempt to make our APIs easier to compose, test, and use. + +## Guidelines + +### No 'static references + +References with lifetime `'static` in API functions should be avoided, even if the lifetime will always be `'static` in production use cases. Instead, make your type generic over a lifetime with the expectation that that lifetime will be `'static` in production use cases. + +__Reason__: Testability. If something needs to take a reference to an object 'O' with lifetime `'static`, that means that O can never be destroyed. This can make it pretty difficult to test things that use that API. + +__Example__: +Instead of this: +```rust +trait Subscriber {} +struct Notifier { subscriber: &'static dyn Subscriber } +// ^^^^^^^^ + +impl Notifier { + fn new(subscriber: &'static dyn Subscriber) -> Self { + // ^^^^^^^^ + Self { subscriber } + } +} +``` + +Consider something like this: +```rust +trait Subscriber {} +struct Notifier<'sub> { subscriber: &'sub dyn Subscriber } +// ^^^^^^ ^^^^^ + +impl<'sub> Notifier<'sub> { + fn new(subscriber: &'sub dyn Subscriber) -> Self { + // ^^^^^ + Self { subscriber } + } +} +``` +In cases like this, if you know that there will only be one concrete type for your reference, consider being generic over the type rather than taking it as `dyn`. This is particularly common for HAL trait implementations. This allows the compiler to inline and simplify code, which can result in performance and code size improvements in some circumstances. + +Alternatively, if you can take an owned `Subscriber` rather than a reference, something like this is probably better: +```rust +trait Subscriber {} +struct Notifier { + sub: S, +} + +impl Notifier { + const fn new(sub: S) -> Self { + Self { sub } + } +} +``` + +### External memory allocation / no static memory allocation + +Memory allocation should always be the role of the caller of the API. If you need memory, have your caller pass it into your constructor. Do not have things like `static INSTANCE: OnceLock` in your service module. + +If you don't need dynamic dispatch over user-provided types, additionally consider being generic over those user-provided types rather than taking `dyn` arguments - this is only possible if you have external memory allocation. + +Note that while this applies to code in this repo, it does not necessarily apply to other ODP repos (e.g. HAL crates that know exactly how many instances of peripheral X are available on the platform). + +__Reason__: Most code in this repo is expected to run primarily in environments that don't have a heap. In heapless environments, your options are either to have your caller provide you memory or to allocate it as a static variable in your module. Allocating it as a static variable in your module has negative impacts on flexibility, testability, performance, and code size. +Flexibility - Memory allocation in your module rather than by your caller means that the size of your object must be known when the module is compiled rather than when you're instantiated. This prevents you from storing any owned caller-provided types in your object (since you can't know those types when your module is compiled). +Testability - if you have a private singleton instance, tests can't arbitrarily destroy and recreate that state. This makes it difficult to test multiple startup paths. +Performance - if you can't be generic over a type, the only way you can interact with user-provided types is by dyn references to trait impls. External memory allocation allows you to be generic over a type, which means you don't have to pay for dynamic dispatch and the compiler can potentially inline code / optimize the interaction between your code and the user-provided type's code. +Code size - The compiler has to generate a bunch of code to handle dynamic dispatch, even if there's only ever a single concrete type that implements the trait you want to be generic over, which is common with HAL traits. + +__Example__: +Note that in the below example, the `OnceLock` / external `Resources` is only necessary if you need to hand out references to the contents of the `OnceLock` / `FooInner`. That's elided in the example and assumed to be implemented in the `/* .. */` blocks for simplicity. +```rust +pub struct Foo { /* .. */ } + +static INSTANCE: OnceLock = OnceLock::new(); + +impl Foo { + async fn init(/* .. */) -> &'static Foo { + let instance = INSTANCE.get_or_init(|| Foo{ /* .. */ }).await; + + // Create another reference to some state in 'inner' - perhaps by passing it to something in /* .. */ + + instance + } +} +``` + +Consider something like this: +```rust +struct FooInner<'hw> { /* .. */ } + +#[derive(Default)] +pub struct Resources<'hw> { + inner: Option> +} + +pub struct Foo<'hw> { + inner: &'hw FooInner<'hw> +} + +impl<'hw> Foo<'hw> { + fn new(resources: &'hw mut Resources, /* .. */) -> Self { + let inner = resources.insert(FooInner::new(/* .. */)); + + // Create another reference to some state in `inner` here that outlasts this function - perhaps by returning + // a `Runner` that contains a reference to `inner` or passing a reference to `inner` to one of the elided + // arguments in /* .. */. See the 'Use runner objects for concurrency' section for a concrete example of this. + // If you don't have a requirement to do this, you don't need the indirection / external `Resources` object at all. + + Self{ inner } + } +} +``` + + +### Use runner objects for concurrency + +Don't declare embassy tasks in your module - instead, have the constructor for your type return a `(Self, Runner)` tuple. The `Runner` object should have a single method `run(self) -> !` that the entity that instantiated your object must execute. You should have only one `Runner` object returned. Use the `odp-service-common::runnable_service::Service` trait to enforce this pattern. + +__Reason__: Declarations of embassy tasks are functionally static memory allocations. They can't be generic and you have to declare at declaration time a maximum number of instances that can be run concurrently. They also commit you to running on embassy, which is not necessarily desirable in test contexts. Pushing responsibility for the allocation out of your module allows your types to be generic. +However, it also means that your caller needs to be able to declare a task that can run your runner, and if you have multiple things that each need different pieces of state and need to run concurrently, setting up those tasks can make your API unwieldy and brittle. +Returning a simple `Runner` object at the same time as your object makes it difficult to forget to execute the runner. +Allowing only a single `Runner` with only one method that takes no external arguments makes it difficult to misuse the runner. + +__Example__: +Instead of this: +```rust +///// Your type's definition ///// +struct MyRunnableTypeInner { /* .. */ } + +impl<'hw> MyRunnableTypeInner<'hw> { + /* .. */ +} + +#[derive(Default)] +pub struct Resources<'hw> { + inner: Option> +} + +pub struct MyRunnableType<'hw> { + inner: &'hw MyRunnableTypeInner<'hw> +} + +impl<'hw> MyRunnableType<'hw> { + pub fn new(resources: &mut Resources, /* .. */ ) -> Self { + let inner = resources.insert(RunnableTypeInner::new(/* .. */)); + /* .. */ + Self { inner } + } +} + +mod tasks { + pub async fn run_task_1(runnable: &MyRunnableType, foo: Foo) -> ! { /* .. */ } + pub async fn run_task_2(runnable: &MyRunnableType, bar: Bar) -> ! { /* .. */ } + pub async fn run_task_3(runnable: &MyRunnableType, baz: Baz) -> ! { /* .. */ } +} + +///// End-user code ///// + +fn main() { + let instance = MyRunnableType::new(/* .. */); + #[embassy_task] + fn runner_1(runnable: &'static MyRunnableType, foo: Foo) -> ! { + my_runnable_type::tasks::run_task_1(runnable, foo).await + } + #[embassy_task] + fn runner_2(runnable: &'static MyRunnableType, bar: Bar) -> ! { + my_runnable_type::tasks::run_task_2(runnable, bar).await + } + #[embassy_task] + fn runner_3(runnable: &'static MyRunnableType, baz: Baz) -> ! { + my_runnable_type::tasks::run_task_3(runnable, baz).await + } + + spawner.must_spawn(runner_1(&instance, Foo::new( /* .. */ ))); + spawner.must_spawn(runner_2(&instance, Bar::new( /* .. */ ))); + spawner.must_spawn(runner_3(&instance, Baz::new( /* .. */ ))); +} + +``` + +Consider something like this: +```rust +///// Your type's definition ///// +struct MyRunnableTypeInner { /* .. */ } + +impl<'hw> MyRunnableTypeInner<'hw> { + async fn task_1(&self, foo: Foo) -> ! { /* .. */ } + async fn task_2(&self, bar: Bar) -> ! { /* .. */ } + async fn task_3(&self, baz: Baz) -> ! { /* .. */ } +} + +#[derive(Default)] +pub struct Resources<'hw> { + inner: Option> +} + +pub struct MyRunnableType<'hw> { + inner: &'hw MyRunnableTypeInner<'hw> +} + +pub struct Runner<'hw> { + inner: &'hw MyRunnableTypeInner<'hw>, + foo: Foo, + bar: Bar, + baz: Baz +} + +impl<'hw> Runner<'hw> { + pub async fn run(self) -> ! { + loop { + embassy_sync::join::join3( + self.inner.task_1(self.foo), + self.inner.task_2(self.bar), + self.inner.task_3(self.baz) + ).await; + } + } +} + +impl<'hw> MyRunnableType<'hw> { + pub fn new(resources: &mut Resources, foo: Foo, bar: Bar, baz: Baz /* .. */ ) -> (Self, Runner) { + let inner = resources.insert(RunnableTypeInner::new( /* .. */ )); + (Self { inner }, Runner { inner, foo, bar, baz }) + } +} + +///// End-user code ///// + +fn main() { + let (instance, runner) = MyRunnableType::new(/* .. */); + #[embassy_task] + fn runner_fn(runner: Runner) { + runner.run().await + } + + spawner.must_spawn(runner_fn(runner)); +} +``` +Notice that most of the complexity has been moved into internal implementation details and the client doesn't have to think about it. Also notice that if you want to add a new 'green thread', or change what state is available to which 'green threads' you can do that entirely in private code in the `run()` method, without requiring changes to your client. + +Note that this can change the order in which your tasks are scheduled compared to having dedicated tasks for each worker function. If your code was making any assumptions about a specific task scheduling order among these worker functions, this will break those assumptions. Ensure that if you need to emit events from your service that need to be emitted in a specific order, you explicitly enforce that ordering in your code (e.g. via emitting all events from the same task or perhaps coordinating between tasks with channels or queues). + +### Use traits for public methods expected to be used at run time whenever possible + +In most cases, public APIs in this repo should be exposed in terms of traits rather than methods directly on the object, and objects that need to interact with other embedded-services objects should refer to them by trait rather than by name. This does not apply to public methods used to construct or initialize a service, because those generally need to know something about the concrete implementation type to properly initialize it. + +These traits should be defined in standalone 'interface' crates (e.g. `battery-service-interface`) alongside any support types needed for the interface (e.g. an Error enum). + +__Reason__: Improved testability and customizability. +Testability - if all our types interact with each other via traits rather than direct dependencies on the type, it makes it much easier to mock out individual components. +Customizability - if an OEM needs to insert a special behavior, they can substitute in a different implementation of that trait and continue using the rest of the embedded-services code without modification. + +__Example__: +Instead of +```rust +pub struct ExampleService { /* */ } +impl ExampleService { + fn foo(&mut self) -> Result<()> { /* .. */ } + fn bar(&mut self) -> Result<()> { /* .. */ } + fn baz(&mut self) -> Result<()> { /* .. */ } +} +``` + +Consider: +```rust +// In a standalone interface crate +pub trait ExampleService { + fn foo(&mut self) -> Result<()>; + fn bar(&mut self) -> Result<()>; + fn baz(&mut self) -> Result<()>; +} + +// In the reference implementation crate +pub struct OdpExampleService { /* .. */ } + +impl embedded_services::ExampleService for OdpExampleService { + fn foo(&mut self) -> Result<()> { /* .. */ } + fn bar(&mut self) -> Result<()> { /* .. */ } + fn baz(&mut self) -> Result<()> { /* .. */ } +} +``` diff --git a/docs/power-policy.md b/docs/power-policy.md index 2b99d8f9b..bccddbd6b 100644 --- a/docs/power-policy.md +++ b/docs/power-policy.md @@ -65,4 +65,78 @@ These messages are used to communicate through the comms serivce. The given device has stopped consuming. #### `ConsumerConnected(device ID, max power)` -The given device has started consuming at the specified power. \ No newline at end of file +The given device has started consuming at the specified power. + +## Charger State Machine + +The power policy service manages charger hardware through a dedicated state machine. The charger tracks two pieces of state: an `InternalState` representing the charger's power and initialization status, and an optional cached `ConsumerPowerCapability` representing the current power policy configuration. + +### Charger State + +The charger can be in one of the following states (`charger::InternalState`): + +* `Unpowered`: The charger hardware is not powered and cannot communicate. +* `Powered(Init)`: The charger is powered and uninitialized. +* `Powered(PsuAttached)`: The charger is powered, initialized, and a PSU is attached (ready to charge). +* `Powered(PsuDetached)`: The charger is powered, initialized, but no PSU is attached. + +```mermaid +stateDiagram-v2 + [*] --> Unpowered + + Unpowered --> Init : on_ready_success() + + state Powered { + Init --> PsuAttached : on_initialized(Attached) + Init --> PsuDetached : on_initialized(Detached) + PsuAttached --> PsuDetached : PsuStateChange(Detached) + PsuDetached --> PsuAttached : PsuStateChange(Attached) + } + + Init --> Unpowered : on_timeout() / on_ready_failure() + PsuAttached --> Unpowered : on_timeout() / on_ready_failure() + PsuDetached --> Unpowered : on_timeout() / on_ready_failure() +``` + +### Charger Events + +Events originate from the charger hardware driver and are broadcast to the power policy service via `charger::event::EventData`: + +#### `PsuStateChange(PsuState)` +Reports that the PSU attachment state has changed. Transitions between `Powered(PsuAttached)` and `Powered(PsuDetached)`. Invalid from `Unpowered` or `Powered(Init)`. Typically this is tied to a GPIO or some other signal that represents that the charger hardware has detected a PSU state change. + +### Charger State Transition Methods + +State transitions are driven by the driver (trait implementer) by calling methods on `charger::State` (accessed via `state_mut()`). The power policy service does not call these methods directly — it is the driver's responsibility to advance the state machine based on hardware observations: + +#### `on_ready_success()` +Transitions an `Unpowered` charger to `Powered(Init)`. No-op if already powered. Should be called by the driver after confirming the charger hardware is powered and can communicate. + +#### `on_ready_failure()` +Transitions a `Powered` charger to `Unpowered`. Capability is preserved for diagnostics. No-op if already unpowered. Should be called by the driver after `is_ready()` fails. + +#### `on_initialized(PsuState)` +Transitions `Powered(Init)` to either `Powered(PsuAttached)` or `Powered(PsuDetached)` based on the PSU state. Returns an error if not in `Powered(Init)`. Should be called by the driver after hardware initialization completes, like at the end of the `init_charger()` trait method call. + +#### `on_timeout()` +Unconditionally transitions to `Unpowered` and clears the cached capability. Should be called by the driver when a communication timeout with the charger hardware is detected. + +#### `on_psu_state_change(PsuState)` +Transitions between `Powered(PsuAttached)` and `Powered(PsuDetached)`. Returns an error if not in a valid powered state. Typically this is called when a GPIO or some other signal that represents that the charger hardware has detected a PSU state change. + +### Policy Integration + +The charger state machine integrates with the PSU consumer selection flow: + +* **Consumer connected**: When the policy selects a new power consumer, it calls `attach_handler(capability)` on each registered charger and caches the capability via `on_policy_attach()`. If a charger is `Unpowered` at this point, the policy calls `is_ready()` followed by `init_charger()` to bring it to an initialized state before attaching. +* **Consumer switched/disconnected**: When the policy disconnects a consumer (to switch or because no consumer is available), it calls `detach_handler()` on each powered charger and clears the cached capability via `on_policy_detach()`. + +### Charger Trait + +Device drivers implement the `charger::Charger` trait (which extends `embedded_batteries_async::charger::Charger`) to integrate with the power policy. The driver is responsible for driving the charger state machine — it must call the appropriate `State` transition methods (via `state_mut()`) based on hardware observations: + +* `init_charger()` — Initializes charger hardware. Returns the current `PsuState`. The driver should call `state_mut().on_initialized(psu_state)` after successful initialization. +* `attach_handler(capability)` — Called when the power policy attaches to a new, best consumer. Should program the hardware for the requested capability. +* `detach_handler()` — Called when the power policy detaches the current PSU consumer, either to switch consumers or because the PSU was disconnected. +* `is_ready()` — Verifies the charger is powered and can communicate. Has a default implementation that returns `Ok(())`. The driver should call `state_mut().on_ready_success()` or `state_mut().on_ready_failure()` to advance the state machine accordingly. +* `state()` / `state_mut()` — Access the charger's `State`. Used by the driver to call state transition methods. The `State` struct fields are private to the `power-policy-interface` crate; transitions must go through the provided methods (e.g. `on_ready_success()`, `on_initialized()`, `on_timeout()`). \ No newline at end of file diff --git a/embedded-service/Cargo.toml b/embedded-service/Cargo.toml index e1f8934b0..9774188c1 100644 --- a/embedded-service/Cargo.toml +++ b/embedded-service/Cargo.toml @@ -12,18 +12,12 @@ workspace = true [dependencies] bitfield.workspace = true -bitvec.workspace = true critical-section.workspace = true defmt = { workspace = true, optional = true } embassy-sync.workspace = true -embassy-time.workspace = true -embedded-batteries-async.workspace = true -embedded-cfu-protocol.workspace = true -embedded-usb-pd.workspace = true -heapless.workspace = true +embassy-futures.workspace = true log = { workspace = true, optional = true } -num_enum.workspace = true -uuid.workspace = true +paste.workspace = true [dependencies.mctp-rs] workspace = true @@ -36,34 +30,16 @@ features = ["serde_derive"] [target.'cfg(not(target_has_atomic = "ptr"))'.dependencies] portable-atomic.workspace = true -[target.'cfg(target_os = "none")'.dependencies] -cortex-m-rt.workspace = true +[target.'cfg(all(target_os = "none", target_arch = "arm"))'.dependencies] cortex-m.workspace = true [dev-dependencies] critical-section = { workspace = true, features = ["std"] } -embassy-futures.workspace = true embassy-sync = { workspace = true, features = ["std"] } -embassy-time = { workspace = true, features = ["std", "generic-queue-8"] } -embassy-time-driver = { workspace = true } -rstest.workspace = true static_cell.workspace = true tokio = { workspace = true, features = ["rt", "macros", "time"] } [features] default = [] -defmt = [ - "dep:defmt", - "embassy-sync/defmt", - "embassy-time/defmt", - "embedded-usb-pd/defmt", - "embedded-cfu-protocol/defmt", - "embedded-batteries-async/defmt", - "mctp-rs/defmt", -] -log = [ - "dep:log", - "embassy-sync/log", - "embassy-time/log", - "embedded-cfu-protocol/log", -] +defmt = ["dep:defmt", "embassy-sync/defmt", "mctp-rs/defmt"] +log = ["dep:log", "embassy-sync/log"] diff --git a/embedded-service/src/cfu/mod.rs b/embedded-service/src/cfu/mod.rs deleted file mode 100644 index 3c3f97ead..000000000 --- a/embedded-service/src/cfu/mod.rs +++ /dev/null @@ -1,169 +0,0 @@ -//! Cfu Service related data structures and messages -//pub mod action; -pub mod component; - -use core::sync::atomic::{AtomicBool, Ordering}; - -use crate::GlobalRawMutex; -use embassy_sync::channel::Channel; -use embassy_sync::once_lock::OnceLock; -use embedded_cfu_protocol::protocol_definitions::{CfuProtocolError, ComponentId}; - -use crate::cfu::component::{CfuDevice, CfuDeviceContainer, DEVICE_CHANNEL_SIZE, InternalResponseData, RequestData}; -use crate::{error, intrusive_list}; - -/// Error type -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -#[cfg_attr(feature = "defmt", derive(defmt::Format))] -pub enum CfuError { - /// Image did not pass validation - BadImage, - /// Component either doesn't exist - InvalidComponent, - /// Component is busy - ComponentBusy, - /// Component encountered a protocol error during execution - ProtocolError(CfuProtocolError), -} - -/// Request to the power policy service -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -#[cfg_attr(feature = "defmt", derive(defmt::Format))] -pub struct Request { - /// Component that sent this request - pub id: ComponentId, - /// Request data - pub data: RequestData, -} - -/// Cfu context -struct ClientContext { - /// Registered devices - devices: intrusive_list::IntrusiveList, - /// Request to components - request: Channel, - /// Response from components - response: Channel, -} - -impl ClientContext { - fn new() -> Self { - Self { - devices: intrusive_list::IntrusiveList::new(), - request: Channel::new(), - response: Channel::new(), - } - } -} - -static CONTEXT: OnceLock = OnceLock::new(); - -/// Init Cfu Client service -pub fn init() { - CONTEXT.get_or_init(ClientContext::new); -} - -/// Register a device with the Cfu Client service -pub async fn register_device(device: &'static impl CfuDeviceContainer) -> Result<(), intrusive_list::Error> { - let device = device.get_cfu_component_device(); - if get_device(device.component_id()).await.is_some() { - return Err(intrusive_list::Error::NodeAlreadyInList); - } - - CONTEXT.get().await.devices.push(device) -} - -/// Find a device by its ID -async fn get_device(id: ComponentId) -> Option<&'static CfuDevice> { - for device in &CONTEXT.get().await.devices { - if let Some(data) = device.data::() { - if data.component_id() == id { - return Some(data); - } - } else { - error!("Non-device located in devices list"); - } - } - - None -} - -/// Convenience function to send a request to the Cfu service -pub async fn send_request(from: ComponentId, request: RequestData) -> Result { - let context = CONTEXT.get().await; - context - .request - .send(Request { - id: from, - data: request, - }) - .await; - Ok(context.response.receive().await) -} - -/// Convenience function to route a request to a specific component -pub async fn route_request(to: ComponentId, request: RequestData) -> Result { - if let Some(device) = get_device(to).await { - device - .execute_device_request(request) - .await - .map_err(CfuError::ProtocolError) - } else { - Err(CfuError::InvalidComponent) - } -} - -/// Send a request to the specific CFU device, but don't wait for a response -pub async fn send_device_request(to: ComponentId, request: RequestData) -> Result<(), CfuError> { - if let Some(device) = get_device(to).await { - device.send_request(request).await; - Ok(()) - } else { - Err(CfuError::InvalidComponent) - } -} - -/// Wait for a response from the specific CFU device -pub async fn wait_device_response(to: ComponentId) -> Result { - if let Some(device) = get_device(to).await { - Ok(device.wait_response().await) - } else { - Err(CfuError::InvalidComponent) - } -} - -/// Singleton struct to give access to the cfu client context -pub struct ContextToken(()); - -impl ContextToken { - /// Create a new context token, returning None if this function has been called before - pub fn create() -> Option { - static INIT: AtomicBool = AtomicBool::new(false); - if INIT.load(Ordering::SeqCst) { - return None; - } - - INIT.store(true, Ordering::SeqCst); - Some(ContextToken(())) - } - - /// Wait for a cfu request - pub async fn wait_request(&self) -> Request { - CONTEXT.get().await.request.receive().await - } - - /// Send a response to a cfu request - pub async fn send_response(&self, response: InternalResponseData) { - CONTEXT.get().await.response.send(response).await - } - - /// Get a device by its ID - pub async fn get_device(&self, id: ComponentId) -> Result<&'static CfuDevice, CfuError> { - get_device(id).await.ok_or(CfuError::InvalidComponent) - } - - /// Provides access to the device list - pub async fn devices(&self) -> &intrusive_list::IntrusiveList { - &CONTEXT.get().await.devices - } -} diff --git a/embedded-service/src/comms.rs b/embedded-service/src/comms.rs index f0e8a99ec..dd0df20e8 100644 --- a/embedded-service/src/comms.rs +++ b/embedded-service/src/comms.rs @@ -53,9 +53,6 @@ pub enum Internal { /// Security service provider Security, - /// Time alarm service provider - TimeAlarm, - /// OEM defined receiver Oem(OemKey), } @@ -101,17 +98,17 @@ impl From for EndpointID { #[derive(Copy, Clone, Debug)] #[cfg_attr(feature = "defmt", derive(defmt::Format))] pub struct Data<'a> { - contents: &'a dyn Any, + contents: &'a (dyn Any + Send + Sync), } impl<'a> Data<'a> { /// Construct a Data portion of a Message from some data input - pub fn new(from: &'a impl Any) -> Self { + pub fn new(from: &'a (impl Any + Send + Sync)) -> Self { Self { contents: from } } /// Attempt to retrieve data as type T -- None if incorrect type - pub fn get(&self) -> Option<&T> { + pub fn get(&self) -> Option<&T> { self.contents.downcast_ref() } @@ -143,7 +140,7 @@ impl<'a> Data<'a> { /// if `data.is_a::() {}` /// else if `data.is_a::() {}` /// etc. - pub fn is_a(&self) -> bool { + pub fn is_a(&self) -> bool { self.type_id() == TypeId::of::() } } @@ -224,7 +221,7 @@ impl Endpoint { } /// Send a generic message to an endpoint - pub async fn send(&self, to: EndpointID, data: &impl Any) -> Result<(), Infallible> { + pub async fn send(&self, to: EndpointID, data: &(impl Any + Send + Sync)) -> Result<(), Infallible> { send(self.id, to, data).await } @@ -280,7 +277,6 @@ fn get_list(target: EndpointID) -> &'static OnceLock { static INTERNAL_LIST_NONVOL: OnceLock = OnceLock::new(); static INTERNAL_LIST_DEBUG: OnceLock = OnceLock::new(); static INTERNAL_LIST_SECURITY: OnceLock = OnceLock::new(); - static INTERNAL_LIST_TIME_ALARM: OnceLock = OnceLock::new(); static INTERNAL_LIST_OEM: OnceLock = OnceLock::new(); match int_endpoint { @@ -296,7 +292,6 @@ fn get_list(target: EndpointID) -> &'static OnceLock { Nonvol => &INTERNAL_LIST_NONVOL, Debug => &INTERNAL_LIST_DEBUG, Security => &INTERNAL_LIST_SECURITY, - TimeAlarm => &INTERNAL_LIST_TIME_ALARM, Oem(_key) => &INTERNAL_LIST_OEM, } } @@ -304,7 +299,7 @@ fn get_list(target: EndpointID) -> &'static OnceLock { } /// Send a generic message to an endpoint -pub async fn send(from: EndpointID, to: EndpointID, data: &impl Any) -> Result<(), Infallible> { +pub async fn send(from: EndpointID, to: EndpointID, data: &(impl Any + Send + Sync)) -> Result<(), Infallible> { route(Message { from, to, @@ -342,7 +337,6 @@ pub(crate) fn init() { get_list(Internal::Nonvol.into()).get_or_init(IntrusiveList::new); get_list(Internal::Debug.into()).get_or_init(IntrusiveList::new); get_list(Internal::Security.into()).get_or_init(IntrusiveList::new); - get_list(Internal::TimeAlarm.into()).get_or_init(IntrusiveList::new); get_list(Internal::Oem(0).into()).get_or_init(IntrusiveList::new); // initialize external subscriber lists diff --git a/embedded-service/src/ec_type/generator/ec-memory-generator.py b/embedded-service/src/ec_type/generator/ec-memory-generator.py deleted file mode 100644 index 000196b1f..000000000 --- a/embedded-service/src/ec_type/generator/ec-memory-generator.py +++ /dev/null @@ -1,116 +0,0 @@ -import sys,yaml - -# Function to convert YAML data to Rust structures -def yaml_to_rust(data): - rust_code = "//! EC Internal Data Structures\n\n" - rust_code += "#[allow(missing_docs)]\n" - rust_code += "pub const EC_MEMMAP_VERSION: Version = Version {major: 0, minor: 1, spin: 0, res0: 0};\n\n" - for key, value in data.items(): - rust_code += "#[allow(missing_docs)]\n" - rust_code += "#[repr(C, packed)]\n" - rust_code += "#[derive(Clone, Copy, Debug, Default)]\n" - rust_code += f"pub struct {key} {{\n" - for sub_key, sub_value in value.items(): - if isinstance(sub_value, dict) and 'type' in sub_value: - rust_code += f" pub {sub_key}: {sub_value['type']},\n" - else: - rust_code += f" pub {sub_key}: {sub_value},\n" - rust_code += "}\n\n" - - return rust_code - -# Function to convert YAML data to C structures -def yaml_to_c(data): - c_code = "#pragma once\n\n" - c_code += "#include \n\n" - c_code += "#pragma pack(push, 1)\n\n" - for key, value in data.items(): - c_code += "typedef struct {\n" - for sub_key, sub_value in value.items(): - if isinstance(sub_value, dict) and 'type' in sub_value: - c_code += f" {type_to_c_type(sub_value['type'])} {sub_key};\n" - else: - c_code += f" {sub_value} {sub_key};\n" - c_code += f"}} {key};\n\n" - - c_code += "#pragma pack(pop)\n\n" - - c_code += "const Version EC_MEMMAP_VERSION = {0x00, 0x01, 0x00, 0x00};\n" - return c_code - -def type_to_c_type(type_str): - if type_str == 'u32': - return 'uint32_t' - elif type_str == 'u16': - return 'uint16_t' - elif type_str == 'u8': - return 'uint8_t' - elif type_str == 'i32': - return 'int32_t' - elif type_str == 'i16': - return 'int16_t' - elif type_str == 'i8': - return 'int8_t' - else: - return type_str - -def open_file(file_path): - try: - with open(file_path, 'r') as file: - data = file.read() - return data - except FileNotFoundError: - print(f"File not found: {file_path}") - except Exception as e: - print(f"An error occurred: {e}") - -def check_for_32bit_alignment(data): - sizes = {'u32': 4, 'u16': 2, 'u8': 1, 'i32': 4, 'i16': 2, 'i8': 1} - for key, value in data.items(): - size = 0 - for sub_key, sub_value in value.items(): - if isinstance(sub_value, dict) and 'type' in sub_value: - size += sizes[sub_value['type']] - sizes[key] = size - - for key, size in sizes.items(): - if not is_primitive_type(key) and size % 4 != 0: - print(f"Warning: {key} is not 32-bit aligned. Size: {size} bytes") - -def is_primitive_type(type_str): - return type_str in ['u32', 'u16', 'u8', 'i32', 'i16', 'i8'] - -if __name__ == "__main__": - if len(sys.argv) != 2: - print("Usage: python yamltorust.py ") - sys.exit(1) - else: - file_path = sys.argv[1] - yaml_data = open_file(file_path) - - # Load the YAML data - data = yaml.safe_load(yaml_data) - - check_for_32bit_alignment(data) - - # Convert the YAML data to Rust structures and print the result - rust_code = yaml_to_rust(data) - - c_code = yaml_to_c(data) - - rust_output_filename = "structure.rs" - c_output_filename = "ecmemory.h" - - try: - with open(rust_output_filename, "w") as output_file: - output_file.write(rust_code) - print(f"Rust code has been written to {rust_output_filename}") - except Exception as e: - print(f"An error occurred while writing to {rust_output_filename}: {e}") - - try: - with open(c_output_filename, "w") as output_file: - output_file.write(c_code) - print(f"C code has been written to {c_output_filename}") - except Exception as e: - print(f"An error occurred while writing to {c_output_filename}: {e}") diff --git a/embedded-service/src/ec_type/generator/ec_memory_map.yaml b/embedded-service/src/ec_type/generator/ec_memory_map.yaml deleted file mode 100644 index 05677cc64..000000000 --- a/embedded-service/src/ec_type/generator/ec_memory_map.yaml +++ /dev/null @@ -1,186 +0,0 @@ -# EC Memory layout definition - -Version: - major: - type: u8 - minor: - type: u8 - spin: - type: u8 - res0: - type: u8 - -# Size 0x14 -Capabilities: - events: - type: u32 - fw_version: - type: Version - secure_state: - type: u8 - variants: - INSECURE: 0 - SECURE: 1 - boot_status: - type: u8 - variants: - SUCCESS: 0 - ERROR: 1 - fan_mask: - type: u8 - battery_mask: - type: u8 - temp_mask: - type: u16 - key_mask: - type: u16 - debug_mask: - type: u16 - res0: - type: u16 - -# Size 0x28 -TimeAlarm: - events: - type: u32 - capability: - type: u32 - year: - type: u16 - month: - type: u8 - day: - type: u8 - hour: - type: u8 - minute: - type: u8 - second: - type: u8 - valid: - type: u8 - daylight: - type: u8 - res1: - type: u8 - milli: - type: u16 - time_zone: - type: u16 - res2: - type: u16 - alarm_status: - type: u32 - ac_time_val: - type: u32 - dc_time_val: - type: u32 - - -# Size 0x64 -Battery: - events: - type: u32 - status: - type: u32 - last_full_charge: - type: u32 - cycle_count: - type: u32 - state: - type: u32 - present_rate: - type: u32 - remain_cap: - type: u32 - present_volt: - type: u32 - psr_state: - type: u32 - psr_max_out: - type: u32 - psr_max_in: - type: u32 - peak_level: - type: u32 - peak_power: - type: u32 - sus_level: - type: u32 - sus_power: - type: u32 - peak_thres: - type: u32 - sus_thres: - type: u32 - trip_thres: - type: u32 - bmc_data: - type: u32 - bmd_data: - type: u32 - bmd_flags: - type: u32 - bmd_count: - type: u32 - charge_time: - type: u32 - run_time: - type: u32 - sample_time: - type: u32 - -# Size 0x38 -Thermal: - events: - type: u32 - cool_mode: - type: u32 - dba_limit: - type: u32 - sonne_limit: - type: u32 - ma_limit: - type: u32 - fan1_on_temp: - type: u32 - fan1_ramp_temp: - type: u32 - fan1_max_temp: - type: u32 - fan1_crt_temp: - type: u32 - fan1_hot_temp: - type: u32 - fan1_max_rpm: - type: u32 - fan1_cur_rpm: - type: u32 - tmp1_val: - type: u32 - tmp1_timeout: - type: u32 - tmp1_low: - type: u32 - tmp1_high: - type: u32 - -Notifications: - service: - type: u16 - event: - type: u16 - -ECMemory: - ver: - type: Version - caps: - type: Capabilities - notif: - type: Notifications - alarm: - type: TimeAlarm - batt: - type: Battery - therm: - type: Thermal diff --git a/embedded-service/src/ec_type/message.rs b/embedded-service/src/ec_type/message.rs deleted file mode 100644 index 3aa6be584..000000000 --- a/embedded-service/src/ec_type/message.rs +++ /dev/null @@ -1,270 +0,0 @@ -//! EC Internal Messages - -use crate::ec_type::protocols::{acpi, debug, mctp::OdpCommandCode, mptf}; - -#[allow(missing_docs)] -#[derive(Clone, Copy, Debug)] -pub enum CapabilitiesMessage { - Events(u32), - FwVersion(super::structure::Version), - SecureState(u8), - BootStatus(u8), - FanMask(u8), - BatteryMask(u8), - TempMask(u16), - KeyMask(u16), - DebugMask(u16), -} - -#[allow(missing_docs)] -#[derive(Clone, Copy, Debug, PartialEq)] -pub enum TimeAlarmMessage { - Events(u32), - Capability(u32), - Year(u16), - Month(u8), - Day(u8), - Hour(u8), - Minute(u8), - Second(u8), - Valid(u8), - Daylight(u8), - Res1(u8), - Milli(u16), - TimeZone(u16), - Res2(u16), - AlarmStatus(u32), - AcTimeVal(u32), - DcTimeVal(u32), -} - -#[allow(missing_docs)] -#[derive(Clone, Copy, Debug, PartialEq)] -pub enum BatteryMessage { - Events(u32), - Status(u32), - LastFullCharge(u32), - CycleCount(u32), - State(u32), - PresentRate(u32), - RemainCap(u32), - PresentVolt(u32), - PsrState(u32), - PsrMaxOut(u32), - PsrMaxIn(u32), - PeakLevel(u32), - PeakPower(u32), - SusLevel(u32), - SusPower(u32), - PeakThres(u32), - SusThres(u32), - TripThres(u32), - BmcData(u32), - BmdData(u32), - BmdFlags(u32), - BmdCount(u32), - ChargeTime(u32), - RunTime(u32), - SampleTime(u32), -} - -/// ACPI Message, compatible with comms system -#[derive(Debug, Clone, Copy)] -#[cfg_attr(feature = "defmt", derive(defmt::Format))] -pub struct HostRequest { - /// Command - pub command: Command, - /// Status code - pub status: u8, - /// Data payload - pub payload: Payload, -} - -/// Notification type to be sent to Host -#[derive(Clone, Copy, Debug, PartialEq)] -#[cfg_attr(feature = "defmt", derive(defmt::Format))] -pub struct NotificationMsg { - /// Interrupt offset - pub offset: u8, -} - -#[allow(missing_docs)] -#[derive(Clone, Copy, Debug, PartialEq)] -pub enum ThermalMessage { - Events(u32), - CoolMode(u32), - DbaLimit(u32), - SonneLimit(u32), - MaLimit(u32), - Fan1OnTemp(u32), - Fan1RampTemp(u32), - Fan1MaxTemp(u32), - Fan1CrtTemp(u32), - Fan1HotTemp(u32), - Fan1MaxRpm(u32), - Fan1CurRpm(u32), - Tmp1Val(u32), - Tmp1Timeout(u32), - Tmp1Low(u32), - Tmp1High(u32), -} - -/// Message type that services can send to communicate with the Host. -#[derive(Debug, Clone, Copy)] -#[cfg_attr(feature = "defmt", derive(defmt::Format))] -pub enum HostMsg { - /// Notification without data. After receivng a notification, - /// typically the host will request some data from the EC - Notification(NotificationMsg), - /// Response to Host request. - Response(HostRequest), -} - -/// ODP specific command code that can come in from the host. -#[derive(Debug, Clone, Copy)] -#[cfg_attr(feature = "defmt", derive(defmt::Format))] -pub enum OdpCommand { - /// Battery commands - Battery(acpi::BatteryCmd), - /// Thermal commands - Thermal(mptf::ThermalCmd), - /// Debug commands - Debug(debug::DebugCmd), -} - -/// Standard Battery Service Model Number String Size -pub const STD_BIX_MODEL_SIZE: usize = 8; -/// Standard Battery Service Serial Number String Size -pub const STD_BIX_SERIAL_SIZE: usize = 8; -/// Standard Battery Service Battery Type String Size -pub const STD_BIX_BATTERY_SIZE: usize = 8; -/// Standard Battery Service OEM Info String Size -pub const STD_BIX_OEM_SIZE: usize = 8; -/// Standard Power Policy Service Model Number String Size -pub const STD_PIF_MODEL_SIZE: usize = 8; -/// Standard Power Policy Serial Number String Size -pub const STD_PIF_SERIAL_SIZE: usize = 8; -/// Standard Power Policy Service OEM Info String Size -pub const STD_PIF_OEM_SIZE: usize = 8; -/// Standard Debug Service Log Buffer Size -pub const STD_DEBUG_BUF_SIZE: usize = 128; - -/// Standard ODP Host Payload -pub type StdHostPayload = crate::ec_type::protocols::mctp::Odp< - STD_BIX_MODEL_SIZE, - STD_BIX_SERIAL_SIZE, - STD_BIX_BATTERY_SIZE, - STD_BIX_OEM_SIZE, - STD_PIF_MODEL_SIZE, - STD_PIF_SERIAL_SIZE, - STD_PIF_OEM_SIZE, - STD_DEBUG_BUF_SIZE, ->; - -/// Standard Host Request -pub type StdHostRequest = HostRequest; -/// Standard Host Message -pub type StdHostMsg = HostMsg; - -impl From for OdpCommand { - fn from(value: OdpCommandCode) -> Self { - match value { - OdpCommandCode::BatteryGetBixRequest | OdpCommandCode::BatteryGetBixResponse => { - OdpCommand::Battery(acpi::BatteryCmd::GetBix) - } - OdpCommandCode::BatteryGetBstRequest | OdpCommandCode::BatteryGetBstResponse => { - OdpCommand::Battery(acpi::BatteryCmd::GetBst) - } - OdpCommandCode::BatteryGetPsrRequest | OdpCommandCode::BatteryGetPsrResponse => { - OdpCommand::Battery(acpi::BatteryCmd::GetPsr) - } - OdpCommandCode::BatteryGetPifRequest | OdpCommandCode::BatteryGetPifResponse => { - OdpCommand::Battery(acpi::BatteryCmd::GetPif) - } - OdpCommandCode::BatteryGetBpsRequest | OdpCommandCode::BatteryGetBpsResponse => { - OdpCommand::Battery(acpi::BatteryCmd::GetBps) - } - OdpCommandCode::BatterySetBtpRequest | OdpCommandCode::BatterySetBtpResponse => { - OdpCommand::Battery(acpi::BatteryCmd::SetBtp) - } - OdpCommandCode::BatterySetBptRequest | OdpCommandCode::BatterySetBptResponse => { - OdpCommand::Battery(acpi::BatteryCmd::SetBpt) - } - OdpCommandCode::BatteryGetBpcRequest | OdpCommandCode::BatteryGetBpcResponse => { - OdpCommand::Battery(acpi::BatteryCmd::GetBpc) - } - OdpCommandCode::BatterySetBmcRequest | OdpCommandCode::BatterySetBmcResponse => { - OdpCommand::Battery(acpi::BatteryCmd::SetBmc) - } - OdpCommandCode::BatteryGetBmdRequest | OdpCommandCode::BatteryGetBmdResponse => { - OdpCommand::Battery(acpi::BatteryCmd::GetBmd) - } - OdpCommandCode::BatteryGetBctRequest | OdpCommandCode::BatteryGetBctResponse => { - OdpCommand::Battery(acpi::BatteryCmd::GetBct) - } - OdpCommandCode::BatteryGetBtmRequest | OdpCommandCode::BatteryGetBtmResponse => { - OdpCommand::Battery(acpi::BatteryCmd::GetBtm) - } - OdpCommandCode::BatterySetBmsRequest | OdpCommandCode::BatterySetBmsResponse => { - OdpCommand::Battery(acpi::BatteryCmd::SetBms) - } - OdpCommandCode::BatterySetBmaRequest | OdpCommandCode::BatterySetBmaResponse => { - OdpCommand::Battery(acpi::BatteryCmd::SetBma) - } - OdpCommandCode::BatteryGetStaRequest | OdpCommandCode::BatteryGetStaResponse => { - OdpCommand::Battery(acpi::BatteryCmd::GetSta) - } - OdpCommandCode::ThermalGetTmpRequest | OdpCommandCode::ThermalGetTmpResponse => { - OdpCommand::Thermal(mptf::ThermalCmd::GetTmp) - } - OdpCommandCode::ThermalSetThrsRequest | OdpCommandCode::ThermalSetThrsResponse => { - OdpCommand::Thermal(mptf::ThermalCmd::SetThrs) - } - OdpCommandCode::ThermalGetThrsRequest | OdpCommandCode::ThermalGetThrsResponse => { - OdpCommand::Thermal(mptf::ThermalCmd::GetThrs) - } - OdpCommandCode::ThermalSetScpRequest | OdpCommandCode::ThermalSetScpResponse => { - OdpCommand::Thermal(mptf::ThermalCmd::SetScp) - } - OdpCommandCode::ThermalGetVarRequest | OdpCommandCode::ThermalGetVarResponse => { - OdpCommand::Thermal(mptf::ThermalCmd::GetVar) - } - OdpCommandCode::ThermalSetVarRequest | OdpCommandCode::ThermalSetVarResponse => { - OdpCommand::Thermal(mptf::ThermalCmd::SetVar) - } - OdpCommandCode::DebugGetMsgsRequest | OdpCommandCode::DebugGetMsgsResponse => { - OdpCommand::Debug(debug::DebugCmd::GetMsgs) - } - } - } -} - -// TODO: Maybe map to Response instead? -impl From for OdpCommandCode { - fn from(value: OdpCommand) -> Self { - match value { - OdpCommand::Battery(acpi::BatteryCmd::GetBix) => OdpCommandCode::BatteryGetBixRequest, - OdpCommand::Battery(acpi::BatteryCmd::GetBst) => OdpCommandCode::BatteryGetBstRequest, - OdpCommand::Battery(acpi::BatteryCmd::GetPsr) => OdpCommandCode::BatteryGetPsrRequest, - OdpCommand::Battery(acpi::BatteryCmd::GetPif) => OdpCommandCode::BatteryGetPifRequest, - OdpCommand::Battery(acpi::BatteryCmd::GetBps) => OdpCommandCode::BatteryGetBpsRequest, - OdpCommand::Battery(acpi::BatteryCmd::SetBtp) => OdpCommandCode::BatterySetBtpRequest, - OdpCommand::Battery(acpi::BatteryCmd::SetBpt) => OdpCommandCode::BatterySetBptRequest, - OdpCommand::Battery(acpi::BatteryCmd::GetBpc) => OdpCommandCode::BatteryGetBpcRequest, - OdpCommand::Battery(acpi::BatteryCmd::SetBmc) => OdpCommandCode::BatterySetBmcRequest, - OdpCommand::Battery(acpi::BatteryCmd::GetBmd) => OdpCommandCode::BatteryGetBmdRequest, - OdpCommand::Battery(acpi::BatteryCmd::GetBct) => OdpCommandCode::BatteryGetBctRequest, - OdpCommand::Battery(acpi::BatteryCmd::GetBtm) => OdpCommandCode::BatteryGetBtmRequest, - OdpCommand::Battery(acpi::BatteryCmd::SetBms) => OdpCommandCode::BatterySetBmsRequest, - OdpCommand::Battery(acpi::BatteryCmd::SetBma) => OdpCommandCode::BatterySetBmaRequest, - OdpCommand::Battery(acpi::BatteryCmd::GetSta) => OdpCommandCode::BatteryGetStaRequest, - OdpCommand::Thermal(mptf::ThermalCmd::GetTmp) => OdpCommandCode::ThermalGetTmpRequest, - OdpCommand::Thermal(mptf::ThermalCmd::SetThrs) => OdpCommandCode::ThermalSetThrsRequest, - OdpCommand::Thermal(mptf::ThermalCmd::GetThrs) => OdpCommandCode::ThermalGetThrsRequest, - OdpCommand::Thermal(mptf::ThermalCmd::SetScp) => OdpCommandCode::ThermalSetScpRequest, - OdpCommand::Thermal(mptf::ThermalCmd::GetVar) => OdpCommandCode::ThermalGetVarRequest, - OdpCommand::Thermal(mptf::ThermalCmd::SetVar) => OdpCommandCode::ThermalSetVarRequest, - OdpCommand::Debug(debug::DebugCmd::GetMsgs) => OdpCommandCode::DebugGetMsgsRequest, - } - } -} diff --git a/embedded-service/src/ec_type/mod.rs b/embedded-service/src/ec_type/mod.rs deleted file mode 100644 index 970afd5a2..000000000 --- a/embedded-service/src/ec_type/mod.rs +++ /dev/null @@ -1,1207 +0,0 @@ -//! Standard EC types -use core::mem::offset_of; - -pub mod message; -pub mod protocols; -pub mod structure; - -/// Error type -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum Error { - /// The requested base + offset is invalid - InvalidLocation, -} - -/// Update battery section of memory map based on battery message -pub fn update_battery_section(msg: &message::BatteryMessage, memory_map: &mut structure::ECMemory) { - match msg { - message::BatteryMessage::Events(events) => memory_map.batt.events = *events, - message::BatteryMessage::Status(status) => memory_map.batt.status = *status, - message::BatteryMessage::LastFullCharge(last_full_charge) => { - memory_map.batt.last_full_charge = *last_full_charge - } - message::BatteryMessage::CycleCount(cycle_count) => memory_map.batt.cycle_count = *cycle_count, - message::BatteryMessage::State(state) => memory_map.batt.state = *state, - message::BatteryMessage::PresentRate(present_rate) => memory_map.batt.present_rate = *present_rate, - message::BatteryMessage::RemainCap(remain_cap) => memory_map.batt.remain_cap = *remain_cap, - message::BatteryMessage::PresentVolt(present_volt) => memory_map.batt.present_volt = *present_volt, - message::BatteryMessage::PsrState(psr_state) => memory_map.batt.psr_state = *psr_state, - message::BatteryMessage::PsrMaxOut(psr_max_out) => memory_map.batt.psr_max_out = *psr_max_out, - message::BatteryMessage::PsrMaxIn(psr_max_in) => memory_map.batt.psr_max_in = *psr_max_in, - message::BatteryMessage::PeakLevel(peak_level) => memory_map.batt.peak_level = *peak_level, - message::BatteryMessage::PeakPower(peak_power) => memory_map.batt.peak_power = *peak_power, - message::BatteryMessage::SusLevel(sus_level) => memory_map.batt.sus_level = *sus_level, - message::BatteryMessage::SusPower(sus_power) => memory_map.batt.sus_power = *sus_power, - message::BatteryMessage::PeakThres(peak_thres) => memory_map.batt.peak_thres = *peak_thres, - message::BatteryMessage::SusThres(sus_thres) => memory_map.batt.sus_thres = *sus_thres, - message::BatteryMessage::TripThres(trip_thres) => memory_map.batt.trip_thres = *trip_thres, - message::BatteryMessage::BmcData(bmc_data) => memory_map.batt.bmc_data = *bmc_data, - message::BatteryMessage::BmdData(bmd_data) => memory_map.batt.bmd_data = *bmd_data, - message::BatteryMessage::BmdFlags(bmd_flags) => memory_map.batt.bmd_flags = *bmd_flags, - message::BatteryMessage::BmdCount(bmd_count) => memory_map.batt.bmd_count = *bmd_count, - message::BatteryMessage::ChargeTime(charge_time) => memory_map.batt.charge_time = *charge_time, - message::BatteryMessage::RunTime(run_time) => memory_map.batt.run_time = *run_time, - message::BatteryMessage::SampleTime(sample_time) => memory_map.batt.sample_time = *sample_time, - } -} - -/// Update capabilities section of memory map based on battery message -pub fn update_capabilities_section(msg: &message::CapabilitiesMessage, memory_map: &mut structure::ECMemory) { - match msg { - message::CapabilitiesMessage::Events(events) => memory_map.caps.events = *events, - message::CapabilitiesMessage::FwVersion(fw_version) => memory_map.caps.fw_version = *fw_version, - message::CapabilitiesMessage::SecureState(secure_state) => memory_map.caps.secure_state = *secure_state, - message::CapabilitiesMessage::BootStatus(boot_status) => memory_map.caps.boot_status = *boot_status, - message::CapabilitiesMessage::FanMask(fan_mask) => memory_map.caps.fan_mask = *fan_mask, - message::CapabilitiesMessage::BatteryMask(battery_mask) => memory_map.caps.battery_mask = *battery_mask, - message::CapabilitiesMessage::TempMask(temp_mask) => memory_map.caps.temp_mask = *temp_mask, - message::CapabilitiesMessage::KeyMask(key_mask) => memory_map.caps.key_mask = *key_mask, - message::CapabilitiesMessage::DebugMask(debug_mask) => memory_map.caps.debug_mask = *debug_mask, - } -} - -/// Update thermal section of memory map based on battery message -pub fn update_thermal_section(msg: &message::ThermalMessage, memory_map: &mut structure::ECMemory) { - match msg { - message::ThermalMessage::Events(events) => memory_map.therm.events = *events, - message::ThermalMessage::CoolMode(cool_mode) => memory_map.therm.cool_mode = *cool_mode, - message::ThermalMessage::DbaLimit(dba_limit) => memory_map.therm.dba_limit = *dba_limit, - message::ThermalMessage::SonneLimit(sonne_limit) => memory_map.therm.sonne_limit = *sonne_limit, - message::ThermalMessage::MaLimit(ma_limit) => memory_map.therm.ma_limit = *ma_limit, - message::ThermalMessage::Fan1OnTemp(fan1_on_temp) => memory_map.therm.fan1_on_temp = *fan1_on_temp, - message::ThermalMessage::Fan1RampTemp(fan1_ramp_temp) => memory_map.therm.fan1_ramp_temp = *fan1_ramp_temp, - message::ThermalMessage::Fan1MaxTemp(fan1_max_temp) => memory_map.therm.fan1_max_temp = *fan1_max_temp, - message::ThermalMessage::Fan1CrtTemp(fan1_crt_temp) => memory_map.therm.fan1_crt_temp = *fan1_crt_temp, - message::ThermalMessage::Fan1HotTemp(fan1_hot_temp) => memory_map.therm.fan1_hot_temp = *fan1_hot_temp, - message::ThermalMessage::Fan1MaxRpm(fan1_max_rpm) => memory_map.therm.fan1_max_rpm = *fan1_max_rpm, - message::ThermalMessage::Fan1CurRpm(fan1_cur_rpm) => memory_map.therm.fan1_cur_rpm = *fan1_cur_rpm, - message::ThermalMessage::Tmp1Val(tmp1_val) => memory_map.therm.tmp1_val = *tmp1_val, - message::ThermalMessage::Tmp1Timeout(tmp1_timeout) => memory_map.therm.tmp1_timeout = *tmp1_timeout, - message::ThermalMessage::Tmp1Low(tmp1_low) => memory_map.therm.tmp1_low = *tmp1_low, - message::ThermalMessage::Tmp1High(tmp1_high) => memory_map.therm.tmp1_high = *tmp1_high, - } -} - -/// Update time alarm section of memory map based on battery message -pub fn update_time_alarm_section(msg: &message::TimeAlarmMessage, memory_map: &mut structure::ECMemory) { - match msg { - message::TimeAlarmMessage::Events(events) => memory_map.alarm.events = *events, - message::TimeAlarmMessage::Capability(capability) => memory_map.alarm.capability = *capability, - message::TimeAlarmMessage::Year(year) => memory_map.alarm.year = *year, - message::TimeAlarmMessage::Month(month) => memory_map.alarm.month = *month, - message::TimeAlarmMessage::Day(day) => memory_map.alarm.day = *day, - message::TimeAlarmMessage::Hour(hour) => memory_map.alarm.hour = *hour, - message::TimeAlarmMessage::Minute(minute) => memory_map.alarm.minute = *minute, - message::TimeAlarmMessage::Second(second) => memory_map.alarm.second = *second, - message::TimeAlarmMessage::Valid(valid) => memory_map.alarm.valid = *valid, - message::TimeAlarmMessage::Daylight(daylight) => memory_map.alarm.daylight = *daylight, - message::TimeAlarmMessage::Res1(res1) => memory_map.alarm.res1 = *res1, - message::TimeAlarmMessage::Milli(milli) => memory_map.alarm.milli = *milli, - message::TimeAlarmMessage::TimeZone(time_zone) => memory_map.alarm.time_zone = *time_zone, - message::TimeAlarmMessage::Res2(res2) => memory_map.alarm.res2 = *res2, - message::TimeAlarmMessage::AlarmStatus(alarm_status) => memory_map.alarm.alarm_status = *alarm_status, - message::TimeAlarmMessage::AcTimeVal(ac_time_val) => memory_map.alarm.ac_time_val = *ac_time_val, - message::TimeAlarmMessage::DcTimeVal(dc_time_val) => memory_map.alarm.dc_time_val = *dc_time_val, - } -} - -/// Helper macro to simplify the conversion of memory map to message -macro_rules! into_message { - ($offset:ident, $length:ident, $member:expr, $msg:expr) => { - let value = $member; - *$offset += size_of_val(&value); - *$length -= size_of_val(&value); - return Ok($msg(value)); - }; -} - -/// Convert from memory map offset and length to battery message -/// Modifies offset and length -pub fn mem_map_to_battery_msg( - memory_map: &structure::ECMemory, - offset: &mut usize, - length: &mut usize, -) -> Result { - let local_offset = *offset - offset_of!(structure::ECMemory, batt); - - if local_offset == offset_of!(structure::Battery, events) { - into_message!(offset, length, memory_map.batt.events, message::BatteryMessage::Events); - } else if local_offset == offset_of!(structure::Battery, status) { - into_message!(offset, length, memory_map.batt.status, message::BatteryMessage::Status); - } else if local_offset == offset_of!(structure::Battery, last_full_charge) { - into_message!( - offset, - length, - memory_map.batt.last_full_charge, - message::BatteryMessage::LastFullCharge - ); - } else if local_offset == offset_of!(structure::Battery, cycle_count) { - into_message!( - offset, - length, - memory_map.batt.cycle_count, - message::BatteryMessage::CycleCount - ); - } else if local_offset == offset_of!(structure::Battery, state) { - into_message!(offset, length, memory_map.batt.state, message::BatteryMessage::State); - } else if local_offset == offset_of!(structure::Battery, present_rate) { - into_message!( - offset, - length, - memory_map.batt.present_rate, - message::BatteryMessage::PresentRate - ); - } else if local_offset == offset_of!(structure::Battery, remain_cap) { - into_message!( - offset, - length, - memory_map.batt.remain_cap, - message::BatteryMessage::RemainCap - ); - } else if local_offset == offset_of!(structure::Battery, present_volt) { - into_message!( - offset, - length, - memory_map.batt.present_volt, - message::BatteryMessage::PresentVolt - ); - } else if local_offset == offset_of!(structure::Battery, psr_state) { - into_message!( - offset, - length, - memory_map.batt.psr_state, - message::BatteryMessage::PsrState - ); - } else if local_offset == offset_of!(structure::Battery, psr_max_out) { - into_message!( - offset, - length, - memory_map.batt.psr_max_out, - message::BatteryMessage::PsrMaxOut - ); - } else if local_offset == offset_of!(structure::Battery, psr_max_in) { - into_message!( - offset, - length, - memory_map.batt.psr_max_in, - message::BatteryMessage::PsrMaxIn - ); - } else if local_offset == offset_of!(structure::Battery, peak_level) { - into_message!( - offset, - length, - memory_map.batt.peak_level, - message::BatteryMessage::PeakLevel - ); - } else if local_offset == offset_of!(structure::Battery, peak_power) { - into_message!( - offset, - length, - memory_map.batt.peak_power, - message::BatteryMessage::PeakPower - ); - } else if local_offset == offset_of!(structure::Battery, sus_level) { - into_message!( - offset, - length, - memory_map.batt.sus_level, - message::BatteryMessage::SusLevel - ); - } else if local_offset == offset_of!(structure::Battery, sus_power) { - into_message!( - offset, - length, - memory_map.batt.sus_power, - message::BatteryMessage::SusPower - ); - } else if local_offset == offset_of!(structure::Battery, peak_thres) { - into_message!( - offset, - length, - memory_map.batt.peak_thres, - message::BatteryMessage::PeakThres - ); - } else if local_offset == offset_of!(structure::Battery, sus_thres) { - into_message!( - offset, - length, - memory_map.batt.sus_thres, - message::BatteryMessage::SusThres - ); - } else if local_offset == offset_of!(structure::Battery, trip_thres) { - into_message!( - offset, - length, - memory_map.batt.trip_thres, - message::BatteryMessage::TripThres - ); - } else if local_offset == offset_of!(structure::Battery, bmc_data) { - into_message!( - offset, - length, - memory_map.batt.bmc_data, - message::BatteryMessage::BmcData - ); - } else if local_offset == offset_of!(structure::Battery, bmd_data) { - into_message!( - offset, - length, - memory_map.batt.bmd_data, - message::BatteryMessage::BmdData - ); - } else if local_offset == offset_of!(structure::Battery, bmd_flags) { - into_message!( - offset, - length, - memory_map.batt.bmd_flags, - message::BatteryMessage::BmdFlags - ); - } else if local_offset == offset_of!(structure::Battery, bmd_count) { - into_message!( - offset, - length, - memory_map.batt.bmd_count, - message::BatteryMessage::BmdCount - ); - } else if local_offset == offset_of!(structure::Battery, charge_time) { - into_message!( - offset, - length, - memory_map.batt.charge_time, - message::BatteryMessage::ChargeTime - ); - } else if local_offset == offset_of!(structure::Battery, run_time) { - into_message!( - offset, - length, - memory_map.batt.run_time, - message::BatteryMessage::RunTime - ); - } else if local_offset == offset_of!(structure::Battery, sample_time) { - into_message!( - offset, - length, - memory_map.batt.sample_time, - message::BatteryMessage::SampleTime - ); - } else { - Err(Error::InvalidLocation) - } -} - -/// Convert from memory map offset and length to thermal message -/// Modifies offset and length -pub fn mem_map_to_thermal_msg( - memory_map: &structure::ECMemory, - offset: &mut usize, - length: &mut usize, -) -> Result { - let local_offset = *offset - offset_of!(structure::ECMemory, therm); - - if local_offset == offset_of!(structure::Thermal, events) { - into_message!(offset, length, memory_map.therm.events, message::ThermalMessage::Events); - } else if local_offset == offset_of!(structure::Thermal, cool_mode) { - into_message!( - offset, - length, - memory_map.therm.cool_mode, - message::ThermalMessage::CoolMode - ); - } else if local_offset == offset_of!(structure::Thermal, dba_limit) { - into_message!( - offset, - length, - memory_map.therm.dba_limit, - message::ThermalMessage::DbaLimit - ); - } else if local_offset == offset_of!(structure::Thermal, sonne_limit) { - into_message!( - offset, - length, - memory_map.therm.sonne_limit, - message::ThermalMessage::SonneLimit - ); - } else if local_offset == offset_of!(structure::Thermal, ma_limit) { - into_message!( - offset, - length, - memory_map.therm.ma_limit, - message::ThermalMessage::MaLimit - ); - } else if local_offset == offset_of!(structure::Thermal, fan1_on_temp) { - into_message!( - offset, - length, - memory_map.therm.fan1_on_temp, - message::ThermalMessage::Fan1OnTemp - ); - } else if local_offset == offset_of!(structure::Thermal, fan1_ramp_temp) { - into_message!( - offset, - length, - memory_map.therm.fan1_ramp_temp, - message::ThermalMessage::Fan1RampTemp - ); - } else if local_offset == offset_of!(structure::Thermal, fan1_max_temp) { - into_message!( - offset, - length, - memory_map.therm.fan1_max_temp, - message::ThermalMessage::Fan1MaxTemp - ); - } else if local_offset == offset_of!(structure::Thermal, fan1_crt_temp) { - into_message!( - offset, - length, - memory_map.therm.fan1_crt_temp, - message::ThermalMessage::Fan1CrtTemp - ); - } else if local_offset == offset_of!(structure::Thermal, fan1_hot_temp) { - into_message!( - offset, - length, - memory_map.therm.fan1_hot_temp, - message::ThermalMessage::Fan1HotTemp - ); - } else if local_offset == offset_of!(structure::Thermal, fan1_max_rpm) { - into_message!( - offset, - length, - memory_map.therm.fan1_max_rpm, - message::ThermalMessage::Fan1MaxRpm - ); - } else if local_offset == offset_of!(structure::Thermal, fan1_cur_rpm) { - into_message!( - offset, - length, - memory_map.therm.fan1_cur_rpm, - message::ThermalMessage::Fan1CurRpm - ); - } else if local_offset == offset_of!(structure::Thermal, tmp1_val) { - into_message!( - offset, - length, - memory_map.therm.tmp1_val, - message::ThermalMessage::Tmp1Val - ); - } else if local_offset == offset_of!(structure::Thermal, tmp1_timeout) { - into_message!( - offset, - length, - memory_map.therm.tmp1_timeout, - message::ThermalMessage::Tmp1Timeout - ); - } else if local_offset == offset_of!(structure::Thermal, tmp1_low) { - into_message!( - offset, - length, - memory_map.therm.tmp1_low, - message::ThermalMessage::Tmp1Low - ); - } else if local_offset == offset_of!(structure::Thermal, tmp1_high) { - into_message!( - offset, - length, - memory_map.therm.tmp1_high, - message::ThermalMessage::Tmp1High - ); - } else { - Err(Error::InvalidLocation) - } -} - -/// Convert from memory map offset and length to time alarm message -/// Modifies offset and length -pub fn mem_map_to_time_alarm_msg( - memory_map: &structure::ECMemory, - offset: &mut usize, - length: &mut usize, -) -> Result { - let local_offset = *offset - offset_of!(structure::ECMemory, alarm); - - if local_offset == offset_of!(structure::TimeAlarm, events) { - into_message!( - offset, - length, - memory_map.alarm.events, - message::TimeAlarmMessage::Events - ); - } else if local_offset == offset_of!(structure::TimeAlarm, capability) { - into_message!( - offset, - length, - memory_map.alarm.capability, - message::TimeAlarmMessage::Capability - ); - } else if local_offset == offset_of!(structure::TimeAlarm, year) { - into_message!(offset, length, memory_map.alarm.year, message::TimeAlarmMessage::Year); - } else if local_offset == offset_of!(structure::TimeAlarm, month) { - into_message!(offset, length, memory_map.alarm.month, message::TimeAlarmMessage::Month); - } else if local_offset == offset_of!(structure::TimeAlarm, day) { - into_message!(offset, length, memory_map.alarm.day, message::TimeAlarmMessage::Day); - } else if local_offset == offset_of!(structure::TimeAlarm, hour) { - into_message!(offset, length, memory_map.alarm.hour, message::TimeAlarmMessage::Hour); - } else if local_offset == offset_of!(structure::TimeAlarm, minute) { - into_message!( - offset, - length, - memory_map.alarm.minute, - message::TimeAlarmMessage::Minute - ); - } else if local_offset == offset_of!(structure::TimeAlarm, second) { - into_message!( - offset, - length, - memory_map.alarm.second, - message::TimeAlarmMessage::Second - ); - } else if local_offset == offset_of!(structure::TimeAlarm, valid) { - into_message!(offset, length, memory_map.alarm.valid, message::TimeAlarmMessage::Valid); - } else if local_offset == offset_of!(structure::TimeAlarm, daylight) { - into_message!( - offset, - length, - memory_map.alarm.daylight, - message::TimeAlarmMessage::Daylight - ); - } else if local_offset == offset_of!(structure::TimeAlarm, res1) { - into_message!(offset, length, memory_map.alarm.res1, message::TimeAlarmMessage::Res1); - } else if local_offset == offset_of!(structure::TimeAlarm, milli) { - into_message!(offset, length, memory_map.alarm.milli, message::TimeAlarmMessage::Milli); - } else if local_offset == offset_of!(structure::TimeAlarm, time_zone) { - into_message!( - offset, - length, - memory_map.alarm.time_zone, - message::TimeAlarmMessage::TimeZone - ); - } else if local_offset == offset_of!(structure::TimeAlarm, res2) { - into_message!(offset, length, memory_map.alarm.res2, message::TimeAlarmMessage::Res2); - } else if local_offset == offset_of!(structure::TimeAlarm, alarm_status) { - into_message!( - offset, - length, - memory_map.alarm.alarm_status, - message::TimeAlarmMessage::AlarmStatus - ); - } else if local_offset == offset_of!(structure::TimeAlarm, ac_time_val) { - into_message!( - offset, - length, - memory_map.alarm.ac_time_val, - message::TimeAlarmMessage::AcTimeVal - ); - } else if local_offset == offset_of!(structure::TimeAlarm, dc_time_val) { - into_message!( - offset, - length, - memory_map.alarm.dc_time_val, - message::TimeAlarmMessage::DcTimeVal - ); - } else { - Err(Error::InvalidLocation) - } -} - -#[cfg(test)] -#[allow(clippy::unwrap_used)] -mod tests { - use super::*; - - macro_rules! test_field { - ($memory_map:ident, $offset:ident, $length:ident, $field:expr, $func:ident, $msg:expr) => { - let field = $field; - let next_offset = $offset + size_of_val(&field); - let next_length = $length - size_of_val(&field); - let msg = $func(&$memory_map, &mut $offset, &mut $length).unwrap(); - assert_eq!(msg, $msg(field)); - assert_eq!($offset, next_offset); - assert_eq!($length, next_length); - }; - } - - #[test] - fn test_mem_map_to_battery_msg() { - use crate::ec_type::message::BatteryMessage; - use crate::ec_type::structure::{Battery, ECMemory}; - - let memory_map = ECMemory { - batt: Battery { - events: 1, - status: 2, - last_full_charge: 3, - cycle_count: 4, - state: 5, - present_rate: 6, - remain_cap: 7, - present_volt: 8, - psr_state: 9, - psr_max_out: 10, - psr_max_in: 11, - peak_level: 12, - peak_power: 13, - sus_level: 14, - sus_power: 15, - peak_thres: 16, - sus_thres: 17, - trip_thres: 18, - bmc_data: 19, - bmd_data: 20, - bmd_flags: 21, - bmd_count: 22, - charge_time: 23, - run_time: 24, - sample_time: 25, - }, - ..Default::default() - }; - - let mut offset = offset_of!(ECMemory, batt); - let mut length = size_of::(); - - test_field!( - memory_map, - offset, - length, - memory_map.batt.events, - mem_map_to_battery_msg, - BatteryMessage::Events - ); - test_field!( - memory_map, - offset, - length, - memory_map.batt.status, - mem_map_to_battery_msg, - BatteryMessage::Status - ); - test_field!( - memory_map, - offset, - length, - memory_map.batt.last_full_charge, - mem_map_to_battery_msg, - BatteryMessage::LastFullCharge - ); - test_field!( - memory_map, - offset, - length, - memory_map.batt.cycle_count, - mem_map_to_battery_msg, - BatteryMessage::CycleCount - ); - test_field!( - memory_map, - offset, - length, - memory_map.batt.state, - mem_map_to_battery_msg, - BatteryMessage::State - ); - test_field!( - memory_map, - offset, - length, - memory_map.batt.present_rate, - mem_map_to_battery_msg, - BatteryMessage::PresentRate - ); - test_field!( - memory_map, - offset, - length, - memory_map.batt.remain_cap, - mem_map_to_battery_msg, - BatteryMessage::RemainCap - ); - test_field!( - memory_map, - offset, - length, - memory_map.batt.present_volt, - mem_map_to_battery_msg, - BatteryMessage::PresentVolt - ); - test_field!( - memory_map, - offset, - length, - memory_map.batt.psr_state, - mem_map_to_battery_msg, - BatteryMessage::PsrState - ); - test_field!( - memory_map, - offset, - length, - memory_map.batt.psr_max_out, - mem_map_to_battery_msg, - BatteryMessage::PsrMaxOut - ); - test_field!( - memory_map, - offset, - length, - memory_map.batt.psr_max_in, - mem_map_to_battery_msg, - BatteryMessage::PsrMaxIn - ); - test_field!( - memory_map, - offset, - length, - memory_map.batt.peak_level, - mem_map_to_battery_msg, - BatteryMessage::PeakLevel - ); - test_field!( - memory_map, - offset, - length, - memory_map.batt.peak_power, - mem_map_to_battery_msg, - BatteryMessage::PeakPower - ); - test_field!( - memory_map, - offset, - length, - memory_map.batt.sus_level, - mem_map_to_battery_msg, - BatteryMessage::SusLevel - ); - test_field!( - memory_map, - offset, - length, - memory_map.batt.sus_power, - mem_map_to_battery_msg, - BatteryMessage::SusPower - ); - test_field!( - memory_map, - offset, - length, - memory_map.batt.peak_thres, - mem_map_to_battery_msg, - BatteryMessage::PeakThres - ); - test_field!( - memory_map, - offset, - length, - memory_map.batt.sus_thres, - mem_map_to_battery_msg, - BatteryMessage::SusThres - ); - test_field!( - memory_map, - offset, - length, - memory_map.batt.trip_thres, - mem_map_to_battery_msg, - BatteryMessage::TripThres - ); - test_field!( - memory_map, - offset, - length, - memory_map.batt.bmc_data, - mem_map_to_battery_msg, - BatteryMessage::BmcData - ); - test_field!( - memory_map, - offset, - length, - memory_map.batt.bmd_data, - mem_map_to_battery_msg, - BatteryMessage::BmdData - ); - test_field!( - memory_map, - offset, - length, - memory_map.batt.bmd_flags, - mem_map_to_battery_msg, - BatteryMessage::BmdFlags - ); - test_field!( - memory_map, - offset, - length, - memory_map.batt.bmd_count, - mem_map_to_battery_msg, - BatteryMessage::BmdCount - ); - test_field!( - memory_map, - offset, - length, - memory_map.batt.charge_time, - mem_map_to_battery_msg, - BatteryMessage::ChargeTime - ); - test_field!( - memory_map, - offset, - length, - memory_map.batt.run_time, - mem_map_to_battery_msg, - BatteryMessage::RunTime - ); - test_field!( - memory_map, - offset, - length, - memory_map.batt.sample_time, - mem_map_to_battery_msg, - BatteryMessage::SampleTime - ); - - assert_eq!(length, 0); - } - - #[test] - fn test_mem_map_to_battery_msg_error() { - use crate::ec_type::structure::{Battery, ECMemory}; - - let memory_map = ECMemory { - batt: Battery { - events: 1, - status: 2, - last_full_charge: 3, - cycle_count: 4, - state: 5, - present_rate: 6, - remain_cap: 7, - present_volt: 8, - psr_state: 9, - psr_max_out: 10, - psr_max_in: 11, - peak_level: 12, - peak_power: 13, - sus_level: 14, - sus_power: 15, - peak_thres: 16, - sus_thres: 17, - trip_thres: 18, - bmc_data: 19, - bmd_data: 20, - bmd_flags: 21, - bmd_count: 22, - charge_time: 23, - run_time: 24, - sample_time: 25, - }, - ..Default::default() - }; - - let mut offset = offset_of!(ECMemory, batt) + 1; - let mut length = size_of::(); - - let res = mem_map_to_battery_msg(&memory_map, &mut offset, &mut length); - assert!(res.is_err() && res.unwrap_err() == Error::InvalidLocation); - } - - #[test] - fn test_mem_map_to_thermal_msg() { - use crate::ec_type::message::ThermalMessage; - use crate::ec_type::structure::{ECMemory, Thermal}; - - let memory_map = ECMemory { - therm: Thermal { - events: 1, - cool_mode: 2, - dba_limit: 3, - sonne_limit: 4, - ma_limit: 5, - fan1_on_temp: 6, - fan1_ramp_temp: 7, - fan1_max_temp: 8, - fan1_crt_temp: 9, - fan1_hot_temp: 10, - fan1_max_rpm: 11, - fan1_cur_rpm: 12, - tmp1_val: 13, - tmp1_timeout: 14, - tmp1_low: 15, - tmp1_high: 16, - }, - ..Default::default() - }; - - let mut offset = offset_of!(ECMemory, therm); - let mut length = size_of::(); - - test_field!( - memory_map, - offset, - length, - memory_map.therm.events, - mem_map_to_thermal_msg, - ThermalMessage::Events - ); - test_field!( - memory_map, - offset, - length, - memory_map.therm.cool_mode, - mem_map_to_thermal_msg, - ThermalMessage::CoolMode - ); - test_field!( - memory_map, - offset, - length, - memory_map.therm.dba_limit, - mem_map_to_thermal_msg, - ThermalMessage::DbaLimit - ); - test_field!( - memory_map, - offset, - length, - memory_map.therm.sonne_limit, - mem_map_to_thermal_msg, - ThermalMessage::SonneLimit - ); - test_field!( - memory_map, - offset, - length, - memory_map.therm.ma_limit, - mem_map_to_thermal_msg, - ThermalMessage::MaLimit - ); - test_field!( - memory_map, - offset, - length, - memory_map.therm.fan1_on_temp, - mem_map_to_thermal_msg, - ThermalMessage::Fan1OnTemp - ); - test_field!( - memory_map, - offset, - length, - memory_map.therm.fan1_ramp_temp, - mem_map_to_thermal_msg, - ThermalMessage::Fan1RampTemp - ); - test_field!( - memory_map, - offset, - length, - memory_map.therm.fan1_max_temp, - mem_map_to_thermal_msg, - ThermalMessage::Fan1MaxTemp - ); - test_field!( - memory_map, - offset, - length, - memory_map.therm.fan1_crt_temp, - mem_map_to_thermal_msg, - ThermalMessage::Fan1CrtTemp - ); - test_field!( - memory_map, - offset, - length, - memory_map.therm.fan1_hot_temp, - mem_map_to_thermal_msg, - ThermalMessage::Fan1HotTemp - ); - test_field!( - memory_map, - offset, - length, - memory_map.therm.fan1_max_rpm, - mem_map_to_thermal_msg, - ThermalMessage::Fan1MaxRpm - ); - test_field!( - memory_map, - offset, - length, - memory_map.therm.fan1_cur_rpm, - mem_map_to_thermal_msg, - ThermalMessage::Fan1CurRpm - ); - test_field!( - memory_map, - offset, - length, - memory_map.therm.tmp1_val, - mem_map_to_thermal_msg, - ThermalMessage::Tmp1Val - ); - test_field!( - memory_map, - offset, - length, - memory_map.therm.tmp1_timeout, - mem_map_to_thermal_msg, - ThermalMessage::Tmp1Timeout - ); - test_field!( - memory_map, - offset, - length, - memory_map.therm.tmp1_low, - mem_map_to_thermal_msg, - ThermalMessage::Tmp1Low - ); - test_field!( - memory_map, - offset, - length, - memory_map.therm.tmp1_high, - mem_map_to_thermal_msg, - ThermalMessage::Tmp1High - ); - - assert_eq!(length, 0); - } - - #[test] - fn test_mem_map_to_thermal_msg_error() { - use crate::ec_type::structure::{ECMemory, Thermal}; - - let memory_map = ECMemory { - therm: Thermal { - events: 1, - cool_mode: 2, - dba_limit: 3, - sonne_limit: 4, - ma_limit: 5, - fan1_on_temp: 6, - fan1_ramp_temp: 7, - fan1_max_temp: 8, - fan1_crt_temp: 9, - fan1_hot_temp: 10, - fan1_max_rpm: 11, - fan1_cur_rpm: 12, - tmp1_val: 13, - tmp1_timeout: 14, - tmp1_low: 15, - tmp1_high: 16, - }, - ..Default::default() - }; - - let mut offset = offset_of!(ECMemory, therm) + 1; - let mut length = size_of::(); - - let res = mem_map_to_thermal_msg(&memory_map, &mut offset, &mut length); - assert!(res.is_err() && res.unwrap_err() == Error::InvalidLocation); - } - - #[test] - fn test_mem_map_to_time_alarm_msg() { - use crate::ec_type::message::TimeAlarmMessage; - use crate::ec_type::structure::{ECMemory, TimeAlarm}; - - let memory_map = ECMemory { - alarm: TimeAlarm { - events: 1, - capability: 2, - year: 2025, - month: 3, - day: 12, - hour: 10, - minute: 30, - second: 45, - valid: 1, - daylight: 0, - res1: 0, - milli: 500, - time_zone: 1, - res2: 0, - alarm_status: 1, - ac_time_val: 100, - dc_time_val: 200, - }, - ..Default::default() - }; - - let mut offset = offset_of!(ECMemory, alarm); - let mut length = size_of::(); - - test_field!( - memory_map, - offset, - length, - memory_map.alarm.events, - mem_map_to_time_alarm_msg, - TimeAlarmMessage::Events - ); - test_field!( - memory_map, - offset, - length, - memory_map.alarm.capability, - mem_map_to_time_alarm_msg, - TimeAlarmMessage::Capability - ); - test_field!( - memory_map, - offset, - length, - memory_map.alarm.year, - mem_map_to_time_alarm_msg, - TimeAlarmMessage::Year - ); - test_field!( - memory_map, - offset, - length, - memory_map.alarm.month, - mem_map_to_time_alarm_msg, - TimeAlarmMessage::Month - ); - test_field!( - memory_map, - offset, - length, - memory_map.alarm.day, - mem_map_to_time_alarm_msg, - TimeAlarmMessage::Day - ); - test_field!( - memory_map, - offset, - length, - memory_map.alarm.hour, - mem_map_to_time_alarm_msg, - TimeAlarmMessage::Hour - ); - test_field!( - memory_map, - offset, - length, - memory_map.alarm.minute, - mem_map_to_time_alarm_msg, - TimeAlarmMessage::Minute - ); - test_field!( - memory_map, - offset, - length, - memory_map.alarm.second, - mem_map_to_time_alarm_msg, - TimeAlarmMessage::Second - ); - test_field!( - memory_map, - offset, - length, - memory_map.alarm.valid, - mem_map_to_time_alarm_msg, - TimeAlarmMessage::Valid - ); - test_field!( - memory_map, - offset, - length, - memory_map.alarm.daylight, - mem_map_to_time_alarm_msg, - TimeAlarmMessage::Daylight - ); - test_field!( - memory_map, - offset, - length, - memory_map.alarm.res1, - mem_map_to_time_alarm_msg, - TimeAlarmMessage::Res1 - ); - test_field!( - memory_map, - offset, - length, - memory_map.alarm.milli, - mem_map_to_time_alarm_msg, - TimeAlarmMessage::Milli - ); - test_field!( - memory_map, - offset, - length, - memory_map.alarm.time_zone, - mem_map_to_time_alarm_msg, - TimeAlarmMessage::TimeZone - ); - test_field!( - memory_map, - offset, - length, - memory_map.alarm.res2, - mem_map_to_time_alarm_msg, - TimeAlarmMessage::Res2 - ); - test_field!( - memory_map, - offset, - length, - memory_map.alarm.alarm_status, - mem_map_to_time_alarm_msg, - TimeAlarmMessage::AlarmStatus - ); - test_field!( - memory_map, - offset, - length, - memory_map.alarm.ac_time_val, - mem_map_to_time_alarm_msg, - TimeAlarmMessage::AcTimeVal - ); - test_field!( - memory_map, - offset, - length, - memory_map.alarm.dc_time_val, - mem_map_to_time_alarm_msg, - TimeAlarmMessage::DcTimeVal - ); - - assert_eq!(length, 0); - } - - #[test] - fn test_mem_map_to_time_alarm_msg_error() { - use crate::ec_type::structure::{ECMemory, TimeAlarm}; - - let memory_map = ECMemory { - alarm: TimeAlarm { - events: 1, - capability: 2, - year: 2025, - month: 3, - day: 12, - hour: 10, - minute: 30, - second: 45, - valid: 1, - daylight: 0, - res1: 0, - milli: 500, - time_zone: 1, - res2: 0, - alarm_status: 1, - ac_time_val: 100, - dc_time_val: 200, - }, - ..Default::default() - }; - - let mut offset = offset_of!(ECMemory, alarm) + 1; - let mut length = size_of::(); - - let res = mem_map_to_time_alarm_msg(&memory_map, &mut offset, &mut length); - assert!(res.is_err() && res.unwrap_err() == Error::InvalidLocation); - } -} diff --git a/embedded-service/src/ec_type/protocols/acpi.rs b/embedded-service/src/ec_type/protocols/acpi.rs deleted file mode 100644 index fc234e0f9..000000000 --- a/embedded-service/src/ec_type/protocols/acpi.rs +++ /dev/null @@ -1,35 +0,0 @@ -#[derive(Copy, Clone, Debug)] -#[cfg_attr(feature = "defmt", derive(defmt::Format))] -/// ACPI Battery Methods -pub enum BatteryCmd { - /// Battery Information eXtended - GetBix = 1, - /// Battery Status - GetBst = 2, - /// Power Source - GetPsr = 3, - /// Power source InFormation - GetPif = 4, - /// Battery Power State - GetBps = 5, - /// Battery Trip Point - SetBtp = 6, - /// Battery Power Threshold - SetBpt = 7, - /// Battery Power Characteristics - GetBpc = 8, - /// Battery Maintenance Control - SetBmc = 9, - /// Battery Maintenance Data - GetBmd = 10, - /// Battery Charge Time - GetBct = 11, - /// Battery Time - GetBtm = 12, - /// Battery Measurement Sampling Time - SetBms = 13, - /// Battery Measurement Averaging Interval - SetBma = 14, - /// Device Status - GetSta = 15, -} diff --git a/embedded-service/src/ec_type/protocols/debug.rs b/embedded-service/src/ec_type/protocols/debug.rs deleted file mode 100644 index cf299f6af..000000000 --- a/embedded-service/src/ec_type/protocols/debug.rs +++ /dev/null @@ -1,8 +0,0 @@ -#[derive(Copy, Clone, Debug)] -#[cfg_attr(feature = "defmt", derive(defmt::Format))] -/// ODP Specific Debug Commands -pub enum DebugCmd { - /// Get buffer of debug messages, if available. - /// Can be used to poll debug messages. - GetMsgs = 1, -} diff --git a/embedded-service/src/ec_type/protocols/mctp.rs b/embedded-service/src/ec_type/protocols/mctp.rs deleted file mode 100644 index 3f71f4cc8..000000000 --- a/embedded-service/src/ec_type/protocols/mctp.rs +++ /dev/null @@ -1,1367 +0,0 @@ -#![allow(missing_docs)] - -use core::ops::{Div, Mul}; - -use embedded_batteries_async::acpi::{ - BCT_RETURN_SIZE_BYTES, BMD_RETURN_SIZE_BYTES, BPC_RETURN_SIZE_BYTES, BPS_RETURN_SIZE_BYTES, BST_RETURN_SIZE_BYTES, - BTM_RETURN_SIZE_BYTES, BatteryState, BmdCapabilityFlags, BmdStatusFlags, PSR_RETURN_SIZE_BYTES, PowerSourceState, - PowerThresholdSupport, PsrReturn, STA_RETURN_SIZE_BYTES, -}; - -use mctp_rs::{ - MctpMedium, MctpMessageHeaderTrait, MctpMessageTrait, MctpPacketError, error::MctpPacketResult, - mctp_completion_code::MctpCompletionCode, -}; - -/// Append an MCTP header to the front of a message. -/// Returns the message and its new total with the appended header. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -#[cfg_attr(feature = "defmt", derive(defmt::Format))] -pub enum MctpError { - /// Header is not at least 9 bytes long. - InvalidHeaderSize, - /// Wrong destination address. - WrongDestinationAddr, - /// Invalid command code. - InvalidCommandCode, - /// Invalid byte count, encoded byte count does not match MCTP message length. - InvalidByteCount, - /// Invalid header version. Should be 1. - InvalidHeaderVersion, - /// Invalid destination endpoint - InvalidDestinationEndpoint, - /// Invalid source endpoint. - InvalidSourceEndpoint, - /// Multi message not supported. - InvalidFlags, -} - -/// Data type for MCTP message underlying data size. -pub type PayloadLen = usize; - -/// Max payload len, due to SMBUS block transaction limits. -pub const MAX_MCTP_BYTE_COUNT: usize = 69; -/// Payload len + bytes for destination target address, command code, and byte count. -pub const MAX_MCTP_PACKET_LEN: usize = MAX_MCTP_BYTE_COUNT + 3; - -fn round_up_to_nearest_mod_4(unrounded: usize) -> usize { - unrounded + (unrounded % 4) -} - -/// Decode a header from and MCTP message. -/// Returns the underlying data and its service endpoint ID and the underlying data size. -pub fn handle_mctp_header( - mctp_msg: &[u8], - data: &mut [u8], -) -> Result<(crate::comms::EndpointID, PayloadLen), MctpError> { - // assert we have at least 9 bytes, minimum - if mctp_msg.len() < 9 { - return Err(MctpError::InvalidHeaderSize); - } - - // EC is at address 2, if we have anything other than 2 reject it. - if mctp_msg[0] != 2 { - return Err(MctpError::WrongDestinationAddr); - } - - // MCTP command code is 0x0F. - if mctp_msg[1] != 0x0F { - return Err(MctpError::InvalidCommandCode); - } - - // Check the byte count is correctly formed and is not larger than the max in the spec. - if usize::from(mctp_msg[2]) > MAX_MCTP_BYTE_COUNT { - return Err(MctpError::InvalidByteCount); - } - // Some eSPI controllers behave oddly if packet sizes aren't multiples of 4, so the MCTP message is padded - // to multiples of 4. - // Byte size + header size (3) + padding to align size to multiple of 4 should equal length of message. - // Unfortunately since padding is variable, there is no way to validate byte count is truly correct. - // There is a chance that the number of valid bytes exceeds the byte count if mctp_msg.len() - // is not a multiple of 4 (and thus has padding bytes) - if ((usize::from(mctp_msg[2]) + 3) + 3).div(4).mul(4) != mctp_msg.len() { - return Err(MctpError::InvalidByteCount); - } - - // Only support header version 1. - if mctp_msg[4] != 1 { - return Err(MctpError::InvalidHeaderVersion); - } - - // Subsystems supported currently are battery (0x02), thermal (0x03), and debug (0x04). - let endpoint_id = match mctp_msg[5] { - 2 => crate::comms::EndpointID::Internal(crate::comms::Internal::Battery), - 3 => crate::comms::EndpointID::Internal(crate::comms::Internal::Thermal), - 4 => crate::comms::EndpointID::Internal(crate::comms::Internal::Debug), - _ => return Err(MctpError::InvalidDestinationEndpoint), - }; - - // Only source endpoint supported currently is host (1). - if mctp_msg[6] != 1 { - return Err(MctpError::InvalidSourceEndpoint); - } - - let som = mctp_msg[7] & (1 << 7) != 0; - let eom = mctp_msg[7] & (1 << 6) != 0; - let seq_num = (mctp_msg[7] & 0b0011_0000) >> 4; - let msg_tag = mctp_msg[7] & 0b0000_0111; - - // Verify flags - if !som || !eom || seq_num != 1 || msg_tag != 3 { - return Err(MctpError::InvalidFlags); - } - - let len = usize::from(mctp_msg[2]) - 5; - // Copy message contents without the padding to a multiple of 4 at the end. - data[..len].copy_from_slice(&mctp_msg[8..8 + len]); - - Ok((endpoint_id, len)) -} - -/// Append an MCTP header to the front of a message. -/// Returns the message and its new total with the appended header. -pub fn build_mctp_header( - data: &[u8], - data_len: usize, - src_endpoint: crate::comms::EndpointID, - start_of_msg: bool, - end_of_msg: bool, -) -> Result<([u8; MAX_MCTP_PACKET_LEN], usize), MctpError> { - let mut ret = [0u8; MAX_MCTP_PACKET_LEN]; - let padding = [0u8; 3]; - - // Host is at address 0. - ret[0] = 0; - - // MCTP command code is 0x0F. - ret[1] = 0x0F; - - // Size of the payload length + header size, without padding - ret[2] = (data_len + 5) as u8; - - // Source is EC (upper 7 bits = 0x01 | hardcoded LSB of 0x01) - ret[3] = 3; - - // Header version is 1 - ret[4] = 1; - - // Destination endpoint ID is Host (0x01) - ret[5] = 1; - - // Subsystems supported currently are battery (0x02), thermal (0x03), and debug (0x04). - match src_endpoint { - crate::comms::EndpointID::Internal(crate::comms::Internal::Battery) => ret[6] = 2, - crate::comms::EndpointID::Internal(crate::comms::Internal::Thermal) => ret[6] = 3, - crate::comms::EndpointID::Internal(crate::comms::Internal::Debug) => ret[6] = 4, - _ => return Err(MctpError::InvalidDestinationEndpoint), - } - - // Seq num 1 + Msg tag 3 - ret[7] = 0x13; - if start_of_msg { - ret[7] |= 1 << 7; - } - if end_of_msg { - ret[7] |= 1 << 6; - } - - // True packet size must be a multple of 4. Header is 8 bytes which is already a multiple of 4, - // so we don't need to include it here. - let data_len_padded = round_up_to_nearest_mod_4(data_len); - - ret[8..data_len + 8].copy_from_slice(&data[..data_len]); - - // Add padding to align to 4 bytes - ret[data_len + 8..data_len_padded + 8].copy_from_slice(&padding[..data_len_padded - data_len]); - - Ok((ret, data_len_padded + 8)) -} - -// 5 bits total -#[derive(num_enum::IntoPrimitive, num_enum::TryFromPrimitive, Debug, PartialEq, Clone, Copy)] -#[cfg_attr(feature = "defmt", derive(defmt::Format))] -#[repr(u8)] -pub enum OdpService { - Battery = 0x01, - Thermal = 0x02, - Debug = 0x03, -} - -// 10 bits total -// TODO: Fully define offsets for subsystem, temporarily it is every 32 entries -#[derive(num_enum::IntoPrimitive, num_enum::TryFromPrimitive, Debug, PartialEq, Clone, Copy)] -#[cfg_attr(feature = "defmt", derive(defmt::Format))] -#[repr(u16)] -pub enum OdpCommandCode { - BatteryGetBixRequest = 0x01, - BatteryGetBstRequest = 0x02, - BatteryGetPsrRequest = 0x03, - BatteryGetPifRequest = 0x04, - BatteryGetBpsRequest = 0x05, - BatterySetBtpRequest = 0x06, - BatterySetBptRequest = 0x07, - BatteryGetBpcRequest = 0x08, - BatterySetBmcRequest = 0x09, - BatteryGetBmdRequest = 0x0A, - BatteryGetBctRequest = 0x0B, - BatteryGetBtmRequest = 0x0C, - BatterySetBmsRequest = 0x0D, - BatterySetBmaRequest = 0x0E, - BatteryGetStaRequest = 0x0F, - BatteryGetBixResponse = 0x11, - BatteryGetBstResponse = 0x12, - BatteryGetPsrResponse = 0x13, - BatteryGetPifResponse = 0x14, - BatteryGetBpsResponse = 0x15, - BatterySetBtpResponse = 0x16, - BatterySetBptResponse = 0x17, - BatteryGetBpcResponse = 0x18, - BatterySetBmcResponse = 0x19, - BatteryGetBmdResponse = 0x1A, - BatteryGetBctResponse = 0x1B, - BatteryGetBtmResponse = 0x1C, - BatterySetBmsResponse = 0x1D, - BatterySetBmaResponse = 0x1E, - BatteryGetStaResponse = 0x1F, - ThermalGetTmpRequest = 0x20, - ThermalSetThrsRequest = 0x21, - ThermalGetThrsRequest = 0x22, - ThermalSetScpRequest = 0x23, - ThermalGetVarRequest = 0x24, - ThermalSetVarRequest = 0x25, - ThermalGetTmpResponse = 0x30, - ThermalSetThrsResponse = 0x31, - ThermalGetThrsResponse = 0x32, - ThermalSetScpResponse = 0x33, - ThermalGetVarResponse = 0x34, - ThermalSetVarResponse = 0x35, - DebugGetMsgsRequest = 0x40, - DebugGetMsgsResponse = 0x50, -} - -// 3 byte header -#[derive(Debug, PartialEq, Clone, Copy)] -#[cfg_attr(feature = "defmt", derive(defmt::Format))] -pub struct OdpHeader { - pub request_bit: bool, // [23:23] (1 bit) - pub datagram_bit: bool, // [22:22] (1 bit) - pub service: OdpService, // [18:21] (4 bits) - pub command_code: OdpCommandCode, // [8:17] (10 bits) - pub completion_code: MctpCompletionCode, // [0:7] (8 bits) -} - -#[derive(PartialEq, Clone, Copy)] -#[cfg_attr(feature = "defmt", derive(defmt::Format))] -pub struct BixFixedStrings< - const MODEL_SIZE: usize, - const SERIAL_SIZE: usize, - const BATTERY_SIZE: usize, - const OEM_SIZE: usize, -> { - /// Revision of the BIX structure. Current revision is 1. - pub revision: u32, - /// Unit used for capacity and rate values. - pub power_unit: embedded_batteries_async::acpi::PowerUnit, - /// Design capacity of the battery (in mWh or mAh). - pub design_capacity: u32, - /// Last full charge capacity (in mWh or mAh). - pub last_full_charge_capacity: u32, - /// Battery technology type. - pub battery_technology: embedded_batteries_async::acpi::BatteryTechnology, - /// Design voltage (in mV). - pub design_voltage: u32, - /// Warning capacity threshold (in mWh or mAh). - pub design_cap_of_warning: u32, - /// Low capacity threshold (in mWh or mAh). - pub design_cap_of_low: u32, - /// Number of charge/discharge cycles. - pub cycle_count: u32, - /// Measurement accuracy in thousandths of a percent (e.g., 80000 = 80.000%). - pub measurement_accuracy: u32, - /// Maximum supported sampling time (in ms). - pub max_sampling_time: u32, - /// Minimum supported sampling time (in ms). - pub min_sampling_time: u32, - /// Maximum supported averaging interval (in ms). - pub max_averaging_interval: u32, - /// Minimum supported averaging interval (in ms). - pub min_averaging_interval: u32, - /// Capacity granularity between low and warning (in mWh or mAh). - pub battery_capacity_granularity_1: u32, - /// Capacity granularity between warning and full (in mWh or mAh). - pub battery_capacity_granularity_2: u32, - /// OEM-specific model number (ASCIIZ). - pub model_number: [u8; MODEL_SIZE], - /// OEM-specific serial number (ASCIIZ). - pub serial_number: [u8; SERIAL_SIZE], - /// OEM-specific battery type (ASCIIZ). - pub battery_type: [u8; BATTERY_SIZE], - /// OEM-specific information (ASCIIZ). - pub oem_info: [u8; OEM_SIZE], - /// Battery swapping capability. - pub battery_swapping_capability: embedded_batteries_async::acpi::BatterySwapCapability, -} - -#[derive(Clone, Copy, PartialEq, Eq)] -#[cfg_attr(feature = "defmt", derive(defmt::Format))] -/// Error type when serializing ODP return types with fixed size strings. -pub enum OdpSerializeErr { - /// Input slice is too small to encapsulate all the fields. - InputSliceTooSmall, -} - -impl - BixFixedStrings -{ - pub fn to_bytes(self, dst_slice: &mut [u8]) -> Result<(), OdpSerializeErr> { - const MODEL_NUM_START_IDX: usize = 64; - let model_num_end_idx: usize = MODEL_NUM_START_IDX + MODEL_SIZE; - let serial_num_start_idx = model_num_end_idx; - let serial_num_end_idx = serial_num_start_idx + SERIAL_SIZE; - let battery_type_start_idx = serial_num_end_idx; - let battery_type_end_idx = battery_type_start_idx + BATTERY_SIZE; - let oem_info_start_idx = battery_type_end_idx; - let oem_info_end_idx = oem_info_start_idx + OEM_SIZE; - - if dst_slice.len() < oem_info_end_idx { - return Err(OdpSerializeErr::InputSliceTooSmall); - } - - dst_slice[..4].copy_from_slice(&u32::to_le_bytes(self.revision)); - dst_slice[4..8].copy_from_slice(&u32::to_le_bytes(self.power_unit.into())); - dst_slice[8..12].copy_from_slice(&u32::to_le_bytes(self.design_capacity)); - dst_slice[12..16].copy_from_slice(&u32::to_le_bytes(self.last_full_charge_capacity)); - dst_slice[16..20].copy_from_slice(&u32::to_le_bytes(self.battery_technology.into())); - dst_slice[20..24].copy_from_slice(&u32::to_le_bytes(self.design_voltage)); - dst_slice[24..28].copy_from_slice(&u32::to_le_bytes(self.design_cap_of_warning)); - dst_slice[28..32].copy_from_slice(&u32::to_le_bytes(self.design_cap_of_low)); - dst_slice[32..36].copy_from_slice(&u32::to_le_bytes(self.cycle_count)); - dst_slice[36..40].copy_from_slice(&u32::to_le_bytes(self.measurement_accuracy)); - dst_slice[40..44].copy_from_slice(&u32::to_le_bytes(self.max_sampling_time)); - dst_slice[44..48].copy_from_slice(&u32::to_le_bytes(self.min_sampling_time)); - dst_slice[48..52].copy_from_slice(&u32::to_le_bytes(self.max_averaging_interval)); - dst_slice[52..56].copy_from_slice(&u32::to_le_bytes(self.min_averaging_interval)); - dst_slice[56..60].copy_from_slice(&u32::to_le_bytes(self.battery_capacity_granularity_1)); - dst_slice[60..64].copy_from_slice(&u32::to_le_bytes(self.battery_capacity_granularity_2)); - dst_slice[MODEL_NUM_START_IDX..model_num_end_idx].copy_from_slice(&self.model_number); - dst_slice[serial_num_start_idx..serial_num_end_idx].copy_from_slice(&self.serial_number); - dst_slice[battery_type_start_idx..battery_type_end_idx].copy_from_slice(&self.battery_type); - dst_slice[oem_info_start_idx..oem_info_end_idx].copy_from_slice(&self.oem_info); - dst_slice[oem_info_end_idx..oem_info_end_idx + 4] - .copy_from_slice(&u32::to_le_bytes(self.battery_swapping_capability.into())); - Ok(()) - } -} - -#[derive(PartialEq, Clone, Copy)] -#[cfg_attr(feature = "defmt", derive(defmt::Format))] -pub struct PifFixedStrings { - /// Bitfield describing the state and characteristics of the power source. - pub power_source_state: embedded_batteries_async::acpi::PowerSourceState, - /// Maximum rated output power in milliwatts (mW). - /// - /// 0xFFFFFFFF indicates the value is unavailable. - pub max_output_power: u32, - /// Maximum rated input power in milliwatts (mW). - /// - /// 0xFFFFFFFF indicates the value is unavailable. - pub max_input_power: u32, - /// OEM-specific model number (ASCIIZ). Empty string if not supported. - pub model_number: [u8; MODEL_SIZE], - /// OEM-specific serial number (ASCIIZ). Empty string if not supported. - pub serial_number: [u8; SERIAL_SIZE], - /// OEM-specific information (ASCIIZ). Empty string if not supported. - pub oem_info: [u8; OEM_SIZE], -} - -impl - PifFixedStrings -{ - pub fn to_bytes(self, dst_slice: &mut [u8]) -> Result<(), OdpSerializeErr> { - const MODEL_NUM_START_IDX: usize = 12; - let model_num_end_idx: usize = MODEL_NUM_START_IDX + MODEL_SIZE; - let serial_num_start_idx = model_num_end_idx; - let serial_num_end_idx = serial_num_start_idx + SERIAL_SIZE; - let oem_info_start_idx = serial_num_end_idx; - let oem_info_end_idx = oem_info_start_idx + OEM_SIZE; - - if dst_slice.len() < oem_info_end_idx { - return Err(OdpSerializeErr::InputSliceTooSmall); - } - - dst_slice[..4].copy_from_slice(&u32::to_le_bytes(self.power_source_state.bits())); - dst_slice[4..8].copy_from_slice(&u32::to_le_bytes(self.max_output_power)); - dst_slice[8..12].copy_from_slice(&u32::to_le_bytes(self.max_input_power)); - dst_slice[MODEL_NUM_START_IDX..model_num_end_idx].copy_from_slice(&self.model_number); - dst_slice[serial_num_start_idx..serial_num_end_idx].copy_from_slice(&self.serial_number); - dst_slice[oem_info_start_idx..oem_info_end_idx].copy_from_slice(&self.oem_info); - Ok(()) - } -} - -/// Standard 32-bit DWORD -pub type Dword = u32; - -/// 16-bit variable length -pub type VarLen = u16; - -/// Instance ID -pub type InstanceId = u8; - -/// Time in milliseconds -pub type Milliseconds = Dword; - -/// MPTF expects temperatures in tenth Kelvins -pub type DeciKelvin = Dword; - -#[derive(PartialEq, Clone, Copy)] -#[cfg_attr(feature = "defmt", derive(defmt::Format))] -pub enum Odp< - const BIX_MODEL_SIZE: usize, - const BIX_SERIAL_SIZE: usize, - const BIX_BATTERY_SIZE: usize, - const BIX_OEM_SIZE: usize, - const PIF_MODEL_SIZE: usize, - const PIF_SERIAL_SIZE: usize, - const PIF_OEM_SIZE: usize, - const DEBUG_BUF_SIZE: usize, -> { - BatteryGetBixRequest { - battery_id: u8, - }, - BatteryGetBstRequest { - battery_id: u8, - }, - BatteryGetPsrRequest { - battery_id: u8, - }, - BatteryGetPifRequest { - battery_id: u8, - }, - BatteryGetBpsRequest { - battery_id: u8, - }, - BatterySetBtpRequest { - battery_id: u8, - btp: embedded_batteries_async::acpi::Btp, - }, - BatterySetBptRequest { - battery_id: u8, - bpt: embedded_batteries_async::acpi::Bpt, - }, - BatteryGetBpcRequest { - battery_id: u8, - }, - BatterySetBmcRequest { - battery_id: u8, - bmc: embedded_batteries_async::acpi::Bmc, - }, - BatteryGetBmdRequest { - battery_id: u8, - }, - BatteryGetBctRequest { - battery_id: u8, - bct: embedded_batteries_async::acpi::Bct, - }, - BatteryGetBtmRequest { - battery_id: u8, - btm: embedded_batteries_async::acpi::Btm, - }, - BatterySetBmsRequest { - battery_id: u8, - bms: embedded_batteries_async::acpi::Bms, - }, - BatterySetBmaRequest { - battery_id: u8, - bma: embedded_batteries_async::acpi::Bma, - }, - BatteryGetStaRequest { - battery_id: u8, - }, - BatteryGetBixResponse { - bix: BixFixedStrings, - }, - BatteryGetBstResponse { - bst: embedded_batteries_async::acpi::BstReturn, - }, - BatteryGetPsrResponse { - psr: embedded_batteries_async::acpi::PsrReturn, - }, - BatteryGetPifResponse { - pif: PifFixedStrings, - }, - BatteryGetBpsResponse { - bps: embedded_batteries_async::acpi::Bps, - }, - BatterySetBtpResponse {}, - BatterySetBptResponse {}, - BatteryGetBpcResponse { - bpc: embedded_batteries_async::acpi::Bpc, - }, - BatterySetBmcResponse {}, - BatteryGetBmdResponse { - bmd: embedded_batteries_async::acpi::Bmd, - }, - BatteryGetBctResponse { - bct_response: embedded_batteries_async::acpi::BctReturnResult, - }, - BatteryGetBtmResponse { - btm_response: embedded_batteries_async::acpi::BtmReturnResult, - }, - BatterySetBmsResponse { - status: Dword, - }, - BatterySetBmaResponse { - status: Dword, - }, - BatteryGetStaResponse { - sta: embedded_batteries_async::acpi::StaReturn, - }, - - ThermalGetTmpRequest { - instance_id: u8, - }, - ThermalSetThrsRequest { - instance_id: u8, - timeout: Milliseconds, - low: DeciKelvin, - high: DeciKelvin, - }, - ThermalGetThrsRequest { - instance_id: u8, - }, - ThermalSetScpRequest { - instance_id: u8, - policy_id: Dword, - acoustic_lim: Dword, - power_lim: Dword, - }, - ThermalGetVarRequest { - instance_id: u8, - len: VarLen, - var_uuid: uuid::Bytes, - }, - ThermalSetVarRequest { - instance_id: u8, - len: VarLen, - var_uuid: uuid::Bytes, - set_var: Dword, - }, - DebugGetMsgsRequest, - - ThermalGetTmpResponse { - temperature: DeciKelvin, - }, - ThermalSetThrsResponse { - status: Dword, - }, - ThermalGetThrsResponse { - status: Dword, - timeout: Milliseconds, - low: DeciKelvin, - high: DeciKelvin, - }, - ThermalSetScpResponse { - status: Dword, - }, - ThermalGetVarResponse { - status: Dword, - val: Dword, - }, - ThermalSetVarResponse { - status: Dword, - }, - DebugGetMsgsResponse { - debug_buf: [u8; DEBUG_BUF_SIZE], - }, - ErrorResponse {}, -} - -impl MctpMessageHeaderTrait for OdpHeader { - fn serialize(self, buffer: &mut [u8]) -> MctpPacketResult { - check_header_length(buffer)?; - let command_code: u16 = self.command_code as u16; - buffer[0] = (self.request_bit as u8) << 7 - | (self.datagram_bit as u8) << 6 - | ((self.service as u8) & 0b0000_1111) << 2 - | ((command_code >> 8) as u8 & 0b0000_0011); - buffer[1] = (command_code & 0x00FF) as u8; - buffer[2] = self.completion_code.into(); - Ok(3) - } - - fn deserialize(buffer: &[u8]) -> MctpPacketResult<(Self, &[u8]), M> { - check_header_length(buffer)?; - let request_bit = buffer[0] & 0b1000_0000 != 0; - let datagram_bit = buffer[0] & 0b0100_0000 != 0; - let service = (buffer[0] & 0b0011_1100) >> 2; - let command_code = ((buffer[0] & 0b0000_0011) as u16) << 8 | (buffer[1] as u16); - - let completion_code = buffer[2] - .try_into() - .map_err(|_| MctpPacketError::HeaderParseError("invalid completion code"))?; - let service = service - .try_into() - .map_err(|_| MctpPacketError::HeaderParseError("invalid odp service"))?; - let command_code = command_code - .try_into() - .map_err(|_| MctpPacketError::HeaderParseError("invalid odp command code"))?; - - Ok(( - OdpHeader { - request_bit, - datagram_bit, - service, - command_code, - completion_code, - }, - &buffer[3..], - )) - } -} - -impl< - const BIX_MODEL_SIZE: usize, - const BIX_SERIAL_SIZE: usize, - const BIX_BATTERY_SIZE: usize, - const BIX_OEM_SIZE: usize, - const PIF_MODEL_SIZE: usize, - const PIF_SERIAL_SIZE: usize, - const PIF_OEM_SIZE: usize, - const DEBUG_BUF_SIZE: usize, -> MctpMessageTrait<'_> - for Odp< - BIX_MODEL_SIZE, - BIX_SERIAL_SIZE, - BIX_BATTERY_SIZE, - BIX_OEM_SIZE, - PIF_MODEL_SIZE, - PIF_SERIAL_SIZE, - PIF_OEM_SIZE, - DEBUG_BUF_SIZE, - > -{ - const MESSAGE_TYPE: u8 = 0x7D; - type Header = OdpHeader; - - fn serialize(self, buffer: &mut [u8]) -> MctpPacketResult { - match self { - Self::BatteryGetBixRequest { battery_id } => write_to_buffer(buffer, [battery_id]), - Self::BatteryGetBstRequest { battery_id } => write_to_buffer(buffer, [battery_id]), - Self::BatteryGetPsrRequest { battery_id } => write_to_buffer(buffer, [battery_id]), - Self::BatteryGetPifRequest { battery_id } => write_to_buffer(buffer, [battery_id]), - Self::BatteryGetBpsRequest { battery_id } => write_to_buffer(buffer, [battery_id]), - Self::BatterySetBtpRequest { battery_id, btp } => { - buffer[0] = battery_id; - buffer[1..5].copy_from_slice(&u32::to_le_bytes(btp.trip_point)); - - Ok(5) - } - Self::BatterySetBptRequest { battery_id, bpt } => { - buffer[0] = battery_id; - buffer[1..5].copy_from_slice(&u32::to_le_bytes(bpt.revision)); - buffer[5..9].copy_from_slice(&u32::to_le_bytes(match bpt.threshold_id { - embedded_batteries_async::acpi::ThresholdId::ClearAll => 0, - embedded_batteries_async::acpi::ThresholdId::InstantaneousPeakPower => 1, - embedded_batteries_async::acpi::ThresholdId::SustainablePeakPower => 2, - })); - buffer[9..13].copy_from_slice(&u32::to_le_bytes(bpt.threshold_value)); - - Ok(13) - } - Self::BatteryGetBpcRequest { battery_id } => write_to_buffer(buffer, [battery_id]), - Self::BatterySetBmcRequest { battery_id, bmc } => { - buffer[0] = battery_id; - buffer[1..5].copy_from_slice(&u32::to_le_bytes(bmc.maintenance_control_flags.bits())); - - Ok(5) - } - Self::BatteryGetBmdRequest { battery_id } => write_to_buffer(buffer, [battery_id]), - Self::BatteryGetBctRequest { battery_id, bct } => { - buffer[0] = battery_id; - buffer[1..5].copy_from_slice(&u32::to_le_bytes(bct.charge_level_percent)); - - Ok(5) - } - Self::BatteryGetBtmRequest { battery_id, btm } => { - buffer[0] = battery_id; - buffer[1..5].copy_from_slice(&u32::to_le_bytes(btm.discharge_rate)); - - Ok(5) - } - Self::BatterySetBmsRequest { battery_id, bms } => { - buffer[0] = battery_id; - buffer[1..5].copy_from_slice(&u32::to_le_bytes(bms.sampling_time_ms)); - - Ok(5) - } - Self::BatterySetBmaRequest { battery_id, bma } => { - buffer[0] = battery_id; - buffer[1..5].copy_from_slice(&u32::to_le_bytes(bma.averaging_interval_ms)); - - Ok(5) - } - Self::BatteryGetStaRequest { battery_id } => write_to_buffer(buffer, [battery_id]), - Self::ThermalGetTmpRequest { instance_id } => write_to_buffer(buffer, [instance_id]), - Self::ThermalSetThrsRequest { - instance_id, - timeout, - low, - high, - } => { - buffer[0] = instance_id; - buffer[1..5].copy_from_slice(&u32::to_le_bytes(timeout)); - buffer[5..9].copy_from_slice(&u32::to_le_bytes(low)); - buffer[9..13].copy_from_slice(&u32::to_le_bytes(high)); - - Ok(13) - } - Self::ThermalGetThrsRequest { instance_id } => write_to_buffer(buffer, [instance_id]), - Self::ThermalSetScpRequest { - instance_id, - policy_id, - acoustic_lim, - power_lim, - } => { - buffer[0] = instance_id; - buffer[1..5].copy_from_slice(&u32::to_le_bytes(policy_id)); - buffer[5..9].copy_from_slice(&u32::to_le_bytes(acoustic_lim)); - buffer[9..13].copy_from_slice(&u32::to_le_bytes(power_lim)); - - Ok(13) - } - Self::ThermalGetVarRequest { - instance_id, - len, - var_uuid, - } => { - buffer[0] = instance_id; - buffer[1..3].copy_from_slice(&u16::to_le_bytes(len)); - buffer[3..19].copy_from_slice(&var_uuid); - - Ok(19) - } - Self::ThermalSetVarRequest { - instance_id, - len, - var_uuid, - set_var, - } => { - buffer[0] = instance_id; - buffer[1..3].copy_from_slice(&u16::to_le_bytes(len)); - buffer[3..19].copy_from_slice(&var_uuid); - buffer[19..23].copy_from_slice(&u32::to_le_bytes(set_var)); - - Ok(23) - } - Self::DebugGetMsgsRequest => Ok(0), - Self::BatteryGetBixResponse { bix } => bix - .to_bytes(buffer) - .map(|_| 100) - .map_err(|_| mctp_rs::MctpPacketError::HeaderParseError("Bix parse failed")), - Self::BatteryGetBstResponse { bst } => { - buffer[..4].copy_from_slice(&u32::to_le_bytes(bst.battery_state.bits())); - buffer[4..8].copy_from_slice(&u32::to_le_bytes(bst.battery_remaining_capacity)); - buffer[8..12].copy_from_slice(&u32::to_le_bytes(bst.battery_present_rate)); - buffer[12..16].copy_from_slice(&u32::to_le_bytes(bst.battery_present_voltage)); - - Ok(BST_RETURN_SIZE_BYTES) - } - Self::BatteryGetPsrResponse { psr } => { - buffer[..4].copy_from_slice(&u32::to_le_bytes(psr.power_source.into())); - - Ok(PSR_RETURN_SIZE_BYTES) - } - Self::BatteryGetPifResponse { pif } => pif - .to_bytes(buffer) - .map(|_| 36) - .map_err(|_| mctp_rs::MctpPacketError::HeaderParseError("Pif parse failed")), - Self::BatteryGetBpsResponse { bps } => { - buffer[..4].copy_from_slice(&u32::to_le_bytes(bps.revision)); - buffer[4..8].copy_from_slice(&u32::to_le_bytes(bps.instantaneous_peak_power_level)); - buffer[8..12].copy_from_slice(&u32::to_le_bytes(bps.instantaneous_peak_power_period)); - buffer[12..16].copy_from_slice(&u32::to_le_bytes(bps.sustainable_peak_power_level)); - buffer[16..20].copy_from_slice(&u32::to_le_bytes(bps.sustainable_peak_power_period)); - - Ok(BPS_RETURN_SIZE_BYTES) - } - Self::BatterySetBtpResponse {} => Ok(0), - Self::BatterySetBptResponse {} => Ok(0), - Self::BatteryGetBpcResponse { bpc } => { - buffer[..4].copy_from_slice(&u32::to_le_bytes(bpc.revision)); - buffer[4..8].copy_from_slice(&u32::to_le_bytes(bpc.power_threshold_support.bits())); - buffer[8..12].copy_from_slice(&u32::to_le_bytes(bpc.max_instantaneous_peak_power_threshold)); - buffer[12..16].copy_from_slice(&u32::to_le_bytes(bpc.max_sustainable_peak_power_threshold)); - - Ok(BPC_RETURN_SIZE_BYTES) - } - Self::BatterySetBmcResponse {} => Ok(0), - Self::BatteryGetBmdResponse { bmd } => { - buffer[..4].copy_from_slice(&u32::to_le_bytes(bmd.status_flags.bits())); - buffer[4..8].copy_from_slice(&u32::to_le_bytes(bmd.capability_flags.bits())); - buffer[8..12].copy_from_slice(&u32::to_le_bytes(bmd.recalibrate_count)); - buffer[12..16].copy_from_slice(&u32::to_le_bytes(bmd.quick_recalibrate_time)); - buffer[16..20].copy_from_slice(&u32::to_le_bytes(bmd.slow_recalibrate_time)); - - Ok(BMD_RETURN_SIZE_BYTES) - } - Self::BatteryGetBctResponse { bct_response } => { - buffer[..4].copy_from_slice(&u32::to_le_bytes(bct_response.into())); - - Ok(BCT_RETURN_SIZE_BYTES) - } - Self::BatteryGetBtmResponse { btm_response } => { - buffer[..4].copy_from_slice(&u32::to_le_bytes(btm_response.into())); - - Ok(BTM_RETURN_SIZE_BYTES) - } - Self::BatterySetBmsResponse { status } => { - buffer[..4].copy_from_slice(&u32::to_le_bytes(status)); - - Ok(4) - } - Self::BatterySetBmaResponse { status } => { - buffer[..4].copy_from_slice(&u32::to_le_bytes(status)); - - Ok(4) - } - Self::BatteryGetStaResponse { sta } => { - buffer[..4].copy_from_slice(&u32::to_le_bytes(sta.bits())); - - Ok(STA_RETURN_SIZE_BYTES) - } - Self::ThermalGetTmpResponse { temperature } => { - buffer[..4].copy_from_slice(&u32::to_le_bytes(temperature)); - - Ok(4) - } - Self::ThermalSetThrsResponse { status } => { - buffer[..4].copy_from_slice(&u32::to_le_bytes(status)); - - Ok(4) - } - Self::ThermalGetThrsResponse { - status, - timeout, - low, - high, - } => { - buffer[..4].copy_from_slice(&u32::to_le_bytes(status)); - buffer[4..8].copy_from_slice(&u32::to_le_bytes(timeout)); - buffer[8..12].copy_from_slice(&u32::to_le_bytes(low)); - buffer[12..16].copy_from_slice(&u32::to_le_bytes(high)); - - Ok(16) - } - Self::ThermalSetScpResponse { status } => { - buffer[..4].copy_from_slice(&u32::to_le_bytes(status)); - - Ok(4) - } - Self::ThermalGetVarResponse { status, val } => { - buffer[..4].copy_from_slice(&u32::to_le_bytes(status)); - buffer[4..8].copy_from_slice(&u32::to_le_bytes(val)); - - Ok(8) - } - Self::ThermalSetVarResponse { status } => { - buffer[..4].copy_from_slice(&u32::to_le_bytes(status)); - - Ok(4) - } - Self::DebugGetMsgsResponse { debug_buf } => { - buffer[..debug_buf.len()].copy_from_slice(&debug_buf); - Ok(debug_buf.len()) - } - Self::ErrorResponse {} => Ok(0), - } - } - - fn deserialize(header: &Self::Header, buffer: &'_ [u8]) -> MctpPacketResult { - Ok(match header.command_code { - OdpCommandCode::BatteryGetBixRequest => Self::BatteryGetBixRequest { - battery_id: safe_get_u8(buffer, 0)?, - }, - OdpCommandCode::BatteryGetBstRequest => Self::BatteryGetBstRequest { - battery_id: safe_get_u8(buffer, 0)?, - }, - OdpCommandCode::BatteryGetPsrRequest => Self::BatteryGetPsrRequest { - battery_id: safe_get_u8(buffer, 0)?, - }, - OdpCommandCode::BatteryGetPifRequest => Self::BatteryGetPifRequest { - battery_id: safe_get_u8(buffer, 0)?, - }, - OdpCommandCode::BatteryGetBpsRequest => Self::BatteryGetBpsRequest { - battery_id: safe_get_u8(buffer, 0)?, - }, - OdpCommandCode::BatterySetBtpRequest => Self::BatterySetBtpRequest { - battery_id: safe_get_u8(buffer, 0)?, - btp: embedded_batteries_async::acpi::Btp { - trip_point: safe_get_dword(buffer, 1)?, - }, - }, - OdpCommandCode::BatterySetBptRequest => Self::BatterySetBptRequest { - battery_id: safe_get_u8(buffer, 0)?, - bpt: embedded_batteries_async::acpi::Bpt { - revision: safe_get_dword(buffer, 1)?, - threshold_id: match safe_get_dword(buffer, 5)? { - 0 => embedded_batteries_async::acpi::ThresholdId::ClearAll, - 1 => embedded_batteries_async::acpi::ThresholdId::InstantaneousPeakPower, - 2 => embedded_batteries_async::acpi::ThresholdId::SustainablePeakPower, - _ => { - return Err(MctpPacketError::HeaderParseError("Unsupported threshold id")); - } - }, - threshold_value: safe_get_dword(buffer, 9)?, - }, - }, - OdpCommandCode::BatteryGetBpcRequest => Self::BatteryGetBpcRequest { - battery_id: safe_get_u8(buffer, 0)?, - }, - OdpCommandCode::BatterySetBmcRequest => Self::BatterySetBmcRequest { - battery_id: safe_get_u8(buffer, 0)?, - bmc: embedded_batteries_async::acpi::Bmc { - maintenance_control_flags: embedded_batteries_async::acpi::BmcControlFlags::from_bits_retain( - safe_get_dword(buffer, 1)?, - ), - }, - }, - OdpCommandCode::BatteryGetBmdRequest => Self::BatteryGetBmdRequest { - battery_id: safe_get_u8(buffer, 0)?, - }, - OdpCommandCode::BatteryGetBctRequest => Self::BatteryGetBctRequest { - battery_id: safe_get_u8(buffer, 0)?, - bct: embedded_batteries_async::acpi::Bct { - charge_level_percent: safe_get_dword(buffer, 1)?, - }, - }, - OdpCommandCode::BatteryGetBtmRequest => Self::BatteryGetBtmRequest { - battery_id: safe_get_u8(buffer, 0)?, - btm: embedded_batteries_async::acpi::Btm { - discharge_rate: safe_get_dword(buffer, 1)?, - }, - }, - OdpCommandCode::BatterySetBmsRequest => Self::BatterySetBmsRequest { - battery_id: safe_get_u8(buffer, 0)?, - bms: embedded_batteries_async::acpi::Bms { - sampling_time_ms: safe_get_dword(buffer, 1)?, - }, - }, - OdpCommandCode::BatterySetBmaRequest => Self::BatterySetBmaRequest { - battery_id: safe_get_u8(buffer, 0)?, - bma: embedded_batteries_async::acpi::Bma { - averaging_interval_ms: safe_get_dword(buffer, 1)?, - }, - }, - OdpCommandCode::BatteryGetStaRequest => Self::BatteryGetStaRequest { - battery_id: safe_get_u8(buffer, 0)?, - }, - OdpCommandCode::ThermalGetTmpRequest => Self::ThermalGetTmpRequest { - instance_id: safe_get_u8(buffer, 0)?, - }, - OdpCommandCode::ThermalSetThrsRequest => Self::ThermalSetThrsRequest { - instance_id: safe_get_u8(buffer, 0)?, - timeout: safe_get_dword(buffer, 1)?, - low: safe_get_dword(buffer, 5)?, - high: safe_get_dword(buffer, 9)?, - }, - OdpCommandCode::ThermalGetThrsRequest => Self::ThermalGetThrsRequest { - instance_id: safe_get_u8(buffer, 0)?, - }, - OdpCommandCode::ThermalSetScpRequest => Self::ThermalSetScpRequest { - instance_id: safe_get_u8(buffer, 0)?, - policy_id: safe_get_dword(buffer, 1)?, - acoustic_lim: safe_get_dword(buffer, 5)?, - power_lim: safe_get_dword(buffer, 9)?, - }, - OdpCommandCode::ThermalGetVarRequest => Self::ThermalGetVarRequest { - instance_id: safe_get_u8(buffer, 0)?, - len: safe_get_u16(buffer, 1)?, - var_uuid: safe_get_uuid(buffer, 3)?, - }, - OdpCommandCode::ThermalSetVarRequest => Self::ThermalSetVarRequest { - instance_id: safe_get_u8(buffer, 0)?, - len: safe_get_u16(buffer, 1)?, - var_uuid: safe_get_uuid(buffer, 3)?, - set_var: safe_get_dword(buffer, 19)?, - }, - OdpCommandCode::DebugGetMsgsRequest => Self::DebugGetMsgsRequest, - OdpCommandCode::BatteryGetBixResponse => Self::BatteryGetBixResponse { - bix: BixFixedStrings { - revision: safe_get_dword(buffer, 0)?, - power_unit: match safe_get_dword(buffer, 4)? { - 0 => embedded_batteries_async::acpi::PowerUnit::MilliWatts, - 1 => embedded_batteries_async::acpi::PowerUnit::MilliAmps, - _ => { - return Err(MctpPacketError::HeaderParseError("BIX deserialize failed")); - } - }, - design_capacity: safe_get_dword(buffer, 8)?, - last_full_charge_capacity: safe_get_dword(buffer, 12)?, - battery_technology: match safe_get_dword(buffer, 16)? { - 0 => embedded_batteries_async::acpi::BatteryTechnology::Primary, - 1 => embedded_batteries_async::acpi::BatteryTechnology::Secondary, - _ => { - return Err(MctpPacketError::HeaderParseError("BIX deserialize failed")); - } - }, - design_voltage: safe_get_dword(buffer, 20)?, - design_cap_of_warning: safe_get_dword(buffer, 24)?, - design_cap_of_low: safe_get_dword(buffer, 28)?, - cycle_count: safe_get_dword(buffer, 32)?, - measurement_accuracy: safe_get_dword(buffer, 36)?, - max_sampling_time: safe_get_dword(buffer, 40)?, - min_sampling_time: safe_get_dword(buffer, 44)?, - max_averaging_interval: safe_get_dword(buffer, 48)?, - min_averaging_interval: safe_get_dword(buffer, 52)?, - battery_capacity_granularity_1: safe_get_dword(buffer, 56)?, - battery_capacity_granularity_2: safe_get_dword(buffer, 60)?, - model_number: buffer[64..72] - .try_into() - .map_err(|_| MctpPacketError::HeaderParseError("BIX deserialize failed"))?, - serial_number: buffer[72..80] - .try_into() - .map_err(|_| MctpPacketError::HeaderParseError("BIX deserialize failed"))?, - battery_type: buffer[80..88] - .try_into() - .map_err(|_| MctpPacketError::HeaderParseError("BIX deserialize failed"))?, - oem_info: buffer[88..96] - .try_into() - .map_err(|_| MctpPacketError::HeaderParseError("BIX deserialize failed"))?, - battery_swapping_capability: match safe_get_dword(buffer, 100)? { - 0 => embedded_batteries_async::acpi::BatterySwapCapability::NonSwappable, - 1 => embedded_batteries_async::acpi::BatterySwapCapability::ColdSwappable, - 2 => embedded_batteries_async::acpi::BatterySwapCapability::HotSwappable, - _ => { - return Err(MctpPacketError::HeaderParseError("BIX deserialize failed")); - } - }, - }, - }, - OdpCommandCode::BatteryGetBstResponse => Self::BatteryGetBstResponse { - bst: embedded_batteries_async::acpi::BstReturn { - battery_state: BatteryState::from_bits_retain(safe_get_dword(buffer, 0)?), - battery_remaining_capacity: safe_get_dword(buffer, 4)?, - battery_present_rate: safe_get_dword(buffer, 8)?, - battery_present_voltage: safe_get_dword(buffer, 12)?, - }, - }, - OdpCommandCode::BatteryGetPsrResponse => Self::BatteryGetPsrResponse { - psr: PsrReturn { - power_source: match safe_get_dword(buffer, 0)? { - 0 => embedded_batteries_async::acpi::PowerSource::Offline, - 1 => embedded_batteries_async::acpi::PowerSource::Online, - _ => { - return Err(MctpPacketError::HeaderParseError("PSR deserialize failed")); - } - }, - }, - }, - OdpCommandCode::BatteryGetPifResponse => Self::BatteryGetPifResponse { - pif: PifFixedStrings { - power_source_state: PowerSourceState::from_bits_retain(safe_get_dword(buffer, 0)?), - max_output_power: safe_get_dword(buffer, 4)?, - max_input_power: safe_get_dword(buffer, 8)?, - model_number: buffer[12..20] - .try_into() - .map_err(|_| MctpPacketError::HeaderParseError("Pif deserialize failed"))?, - serial_number: buffer[20..28] - .try_into() - .map_err(|_| MctpPacketError::HeaderParseError("Pif deserialize failed"))?, - oem_info: buffer[28..36] - .try_into() - .map_err(|_| MctpPacketError::HeaderParseError("Pif deserialize failed"))?, - }, - }, - OdpCommandCode::BatteryGetBpsResponse => Self::BatteryGetBpsResponse { - bps: embedded_batteries_async::acpi::Bps { - revision: safe_get_dword(buffer, 0)?, - instantaneous_peak_power_level: safe_get_dword(buffer, 4)?, - instantaneous_peak_power_period: safe_get_dword(buffer, 8)?, - sustainable_peak_power_level: safe_get_dword(buffer, 12)?, - sustainable_peak_power_period: safe_get_dword(buffer, 16)?, - }, - }, - OdpCommandCode::BatterySetBtpResponse => Self::BatterySetBtpResponse {}, - OdpCommandCode::BatterySetBptResponse => Self::BatterySetBptResponse {}, - OdpCommandCode::BatteryGetBpcResponse => Self::BatteryGetBpcResponse { - bpc: embedded_batteries_async::acpi::Bpc { - revision: safe_get_dword(buffer, 0)?, - power_threshold_support: PowerThresholdSupport::from_bits_retain(safe_get_dword(buffer, 4)?), - max_instantaneous_peak_power_threshold: safe_get_dword(buffer, 8)?, - max_sustainable_peak_power_threshold: safe_get_dword(buffer, 12)?, - }, - }, - OdpCommandCode::BatterySetBmcResponse => Self::BatterySetBmcResponse {}, - OdpCommandCode::BatteryGetBmdResponse => Self::BatteryGetBmdResponse { - bmd: embedded_batteries_async::acpi::Bmd { - status_flags: BmdStatusFlags::from_bits_retain(safe_get_dword(buffer, 0)?), - capability_flags: BmdCapabilityFlags::from_bits_retain(safe_get_dword(buffer, 4)?), - recalibrate_count: safe_get_dword(buffer, 8)?, - quick_recalibrate_time: safe_get_dword(buffer, 12)?, - slow_recalibrate_time: safe_get_dword(buffer, 16)?, - }, - }, - OdpCommandCode::BatteryGetBctResponse => Self::BatteryGetBctResponse { - bct_response: embedded_batteries_async::acpi::BctReturnResult::from(safe_get_dword(buffer, 0)?), - }, - OdpCommandCode::BatteryGetBtmResponse => Self::BatteryGetBtmResponse { - btm_response: embedded_batteries_async::acpi::BtmReturnResult::from(safe_get_dword(buffer, 0)?), - }, - OdpCommandCode::BatterySetBmsResponse => Self::BatterySetBmsResponse { - status: safe_get_dword(buffer, 0)?, - }, - OdpCommandCode::BatterySetBmaResponse => Self::BatterySetBmaResponse { - status: safe_get_dword(buffer, 0)?, - }, - OdpCommandCode::BatteryGetStaResponse => Self::BatteryGetStaResponse { - sta: embedded_batteries_async::acpi::StaReturn::from_bits_retain(safe_get_dword(buffer, 0)?), - }, - OdpCommandCode::ThermalGetTmpResponse => Self::ThermalGetTmpResponse { - temperature: safe_get_dword(buffer, 0)?, - }, - OdpCommandCode::ThermalSetThrsResponse => Self::ThermalSetThrsResponse { - status: safe_get_dword(buffer, 0)?, - }, - OdpCommandCode::ThermalGetThrsResponse => Self::ThermalGetThrsResponse { - status: safe_get_dword(buffer, 0)?, - timeout: safe_get_dword(buffer, 4)?, - low: safe_get_dword(buffer, 8)?, - high: safe_get_dword(buffer, 12)?, - }, - OdpCommandCode::ThermalSetScpResponse => Self::ThermalSetScpResponse { - status: safe_get_dword(buffer, 0)?, - }, - OdpCommandCode::ThermalGetVarResponse => Self::ThermalGetVarResponse { - status: safe_get_dword(buffer, 0)?, - val: safe_get_dword(buffer, 4)?, - }, - OdpCommandCode::ThermalSetVarResponse => Self::ThermalSetVarResponse { - status: safe_get_dword(buffer, 0)?, - }, - OdpCommandCode::DebugGetMsgsResponse => Self::DebugGetMsgsResponse { - debug_buf: buffer[..DEBUG_BUF_SIZE] - .try_into() - .map_err(|_| MctpPacketError::HeaderParseError("MCTP buf not large enough"))?, - }, - }) - } -} - -fn safe_get_u8(buffer: &[u8], index: usize) -> MctpPacketResult { - if buffer.len() < index + 1 { - return Err(MctpPacketError::HeaderParseError("buffer too small for odp message")); - } - Ok(buffer[index]) -} - -fn safe_get_u16(buffer: &[u8], index: usize) -> MctpPacketResult { - if buffer.len() < index + 2 { - return Err(MctpPacketError::HeaderParseError("buffer too small for odp message")); - } - // Safe from panics as length is verified above. - Ok(u16::from_le_bytes(buffer[index..index + 2].try_into().unwrap())) -} - -fn safe_get_dword(buffer: &[u8], index: usize) -> MctpPacketResult { - if buffer.len() < index + 4 { - return Err(MctpPacketError::HeaderParseError("buffer too small for odp message")); - } - // Safe from panics as length is verified above. - Ok(u32::from_le_bytes(buffer[index..index + 4].try_into().unwrap())) -} - -fn safe_get_uuid(buffer: &[u8], index: usize) -> MctpPacketResult { - if buffer.len() < index + 16 { - return Err(MctpPacketError::HeaderParseError("buffer too small for odp message")); - } - // Safe from panics as length is verified above. - Ok(buffer[index..index + 16].try_into().unwrap()) -} - -fn write_to_buffer(buffer: &mut [u8], data: [u8; N]) -> MctpPacketResult { - if buffer.len() < N { - return Err(MctpPacketError::SerializeError("buffer too small for odp message")); - } - buffer[..N].copy_from_slice(&data); - Ok(N) -} - -fn check_header_length(buffer: &[u8]) -> MctpPacketResult<(), M> { - if buffer.len() < 3 { - return Err(MctpPacketError::HeaderParseError("buffer too small for odp header")); - } - - Ok(()) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[allow(dead_code)] - mod test_util { - use mctp_rs::{MctpMedium, MctpMediumFrame, MctpPacketError, error::MctpPacketResult}; - - #[derive(Debug, PartialEq, Eq, Copy, Clone)] - pub struct TestMedium { - header: &'static [u8], - trailer: &'static [u8], - mtu: usize, - } - impl TestMedium { - pub fn new() -> Self { - Self { - header: &[], - trailer: &[], - mtu: 32, - } - } - pub fn with_headers(mut self, header: &'static [u8], trailer: &'static [u8]) -> Self { - self.header = header; - self.trailer = trailer; - self - } - pub fn with_mtu(mut self, mtu: usize) -> Self { - self.mtu = mtu; - self - } - } - - #[derive(Debug, PartialEq, Eq, Copy, Clone)] - pub struct TestMediumFrame(usize); - - impl MctpMedium for TestMedium { - type Frame = TestMediumFrame; - type Error = &'static str; - type ReplyContext = (); - - fn deserialize<'buf>(&self, packet: &'buf [u8]) -> MctpPacketResult<(Self::Frame, &'buf [u8]), Self> { - let packet_len = packet.len(); - - // check that header / trailer is present and correct - if packet.len() < self.header.len() + self.trailer.len() { - return Err(MctpPacketError::MediumError("packet too short")); - } - if packet[0..self.header.len()] != *self.header { - return Err(MctpPacketError::MediumError("header mismatch")); - } - if packet[packet_len - self.trailer.len()..packet_len] != *self.trailer { - return Err(MctpPacketError::MediumError("trailer mismatch")); - } - - let packet = &packet[self.header.len()..packet_len - self.trailer.len()]; - Ok((TestMediumFrame(packet_len), packet)) - } - fn max_message_body_size(&self) -> usize { - self.mtu - } - fn serialize<'buf, F>( - &self, - _: Self::ReplyContext, - buffer: &'buf mut [u8], - message_writer: F, - ) -> MctpPacketResult<&'buf [u8], Self> - where - F: for<'a> FnOnce(&'a mut [u8]) -> MctpPacketResult, - { - let header_len = self.header.len(); - let trailer_len = self.trailer.len(); - - // Ensure buffer can fit at least headers and trailers - if buffer.len() < header_len + trailer_len { - return Err(MctpPacketError::MediumError("Buffer too small for headers")); - } - - // Calculate available space for message (respecting MTU) - let max_packet_size = self.mtu.min(buffer.len()); - if max_packet_size < header_len + trailer_len { - return Err(MctpPacketError::MediumError("MTU too small for headers")); - } - let max_message_size = max_packet_size - header_len - trailer_len; - - buffer[0..header_len].copy_from_slice(self.header); - let size = message_writer(&mut buffer[header_len..header_len + max_message_size])?; - let len = header_len + size; - buffer[len..len + trailer_len].copy_from_slice(self.trailer); - Ok(&buffer[..len + trailer_len]) - } - } - - impl MctpMediumFrame for TestMediumFrame { - fn packet_size(&self) -> usize { - self.0 - } - fn reply_context(&self) -> ::ReplyContext {} - } - } - - use test_util::TestMedium; - - #[rstest::rstest] - #[case(OdpHeader { - request_bit: true, - datagram_bit: false, - service: OdpService::Battery, - command_code: OdpCommandCode::BatteryGetBixRequest, - completion_code: MctpCompletionCode::Success - })] - #[case( - OdpHeader { - request_bit: false, - datagram_bit: true, - service: OdpService::Debug, - command_code: OdpCommandCode::BatteryGetBixRequest, - completion_code: MctpCompletionCode::ErrorUnsupportedCmd - })] - #[case( - OdpHeader { - request_bit: true, - datagram_bit: true, - service: OdpService::Battery, - command_code: OdpCommandCode::BatteryGetBixRequest, - completion_code: MctpCompletionCode::CommandSpecific(0x80) - })] - #[case( - OdpHeader { - request_bit: false, - datagram_bit: false, - service: OdpService::Debug, - command_code: OdpCommandCode::BatteryGetBixRequest, - completion_code: MctpCompletionCode::Success - })] - fn odp_header_roundtrip_happy_path(#[case] header: OdpHeader) { - let mut buf = [0u8; 3]; - let size = header.serialize::(&mut buf).unwrap(); - assert_eq!(size, 3); - - let (parsed, rest) = OdpHeader::deserialize::(&buf).unwrap(); - assert_eq!(parsed, header); - assert_eq!(rest.len(), 0); - } - - #[test] - #[allow(clippy::panic)] - fn odp_header_error_on_short_buffer() { - let header = OdpHeader { - request_bit: false, - datagram_bit: false, - service: OdpService::Battery, - command_code: OdpCommandCode::BatteryGetBixRequest, - completion_code: MctpCompletionCode::Success, - }; - - // Serialize works with correct buffer - let mut buf_ok = [0u8; 3]; - header.serialize::(&mut buf_ok).unwrap(); - - // Deserialize should fail on too-small buffer - let err = OdpHeader::deserialize::(&buf_ok[..2]).unwrap_err(); - match err { - MctpPacketError::HeaderParseError(msg) => { - assert_eq!(msg, "buffer too small for odp header") - } - other => panic!("unexpected error: {:?}", other), - } - } -} diff --git a/embedded-service/src/ec_type/protocols/mod.rs b/embedded-service/src/ec_type/protocols/mod.rs deleted file mode 100644 index aafb1e632..000000000 --- a/embedded-service/src/ec_type/protocols/mod.rs +++ /dev/null @@ -1,15 +0,0 @@ -//! Primitive serde and helper fns for protocols used by the EC. - -/// ACPI (Advanced Configuration and Power Interface). -pub mod acpi; - -/// ODP Specific Debug Protocol. -pub mod debug; - -/// MCTP (Management Component Transport Protocol). -#[allow(clippy::indexing_slicing)] //panic safety: no external client, not being deployed -#[allow(clippy::unwrap_used)] //panic safety: no external client, not being deployed -pub mod mctp; - -/// MTPF (Modern Thermal and Power Framework). -pub mod mptf; diff --git a/embedded-service/src/ec_type/protocols/mptf.rs b/embedded-service/src/ec_type/protocols/mptf.rs deleted file mode 100644 index 533311850..000000000 --- a/embedded-service/src/ec_type/protocols/mptf.rs +++ /dev/null @@ -1,17 +0,0 @@ -/// Standard MPTF requests expected by the thermal subsystem -#[derive(Debug, Clone, Copy)] -#[cfg_attr(feature = "defmt", derive(defmt::Format))] -pub enum ThermalCmd { - /// EC_THM_GET_TMP = 0x1 - GetTmp = 1, - /// EC_THM_SET_THRS = 0x2 - SetThrs = 2, - /// EC_THM_GET_THRS = 0x3 - GetThrs = 3, - /// EC_THM_SET_SCP = 0x4 - SetScp = 4, - /// EC_THM_GET_VAR = 0x5 - GetVar = 5, - /// EC_THM_SET_VAR = 0x6 - SetVar = 6, -} diff --git a/embedded-service/src/ec_type/structure.rs b/embedded-service/src/ec_type/structure.rs deleted file mode 100644 index 05b3c2e6f..000000000 --- a/embedded-service/src/ec_type/structure.rs +++ /dev/null @@ -1,131 +0,0 @@ -//! EC Internal Data Structures - -#[allow(missing_docs)] -pub const EC_MEMMAP_VERSION: Version = Version { - major: 0, - minor: 1, - spin: 0, - res0: 0, -}; - -#[allow(missing_docs)] -#[repr(C, packed)] -#[derive(Clone, Copy, Debug, Default)] -pub struct Version { - pub major: u8, - pub minor: u8, - pub spin: u8, - pub res0: u8, -} - -#[allow(missing_docs)] -#[repr(C, packed)] -#[derive(Clone, Copy, Debug, Default)] -pub struct Capabilities { - pub events: u32, - pub fw_version: Version, - pub secure_state: u8, - pub boot_status: u8, - pub fan_mask: u8, - pub battery_mask: u8, - pub temp_mask: u16, - pub key_mask: u16, - pub debug_mask: u16, - pub res0: u16, -} - -#[allow(missing_docs)] -#[repr(C, packed)] -#[derive(Clone, Copy, Debug, Default)] -pub struct TimeAlarm { - pub events: u32, - pub capability: u32, - pub year: u16, - pub month: u8, - pub day: u8, - pub hour: u8, - pub minute: u8, - pub second: u8, - pub valid: u8, - pub daylight: u8, - pub res1: u8, - pub milli: u16, - pub time_zone: u16, - pub res2: u16, - pub alarm_status: u32, - pub ac_time_val: u32, - pub dc_time_val: u32, -} - -#[allow(missing_docs)] -#[repr(C, packed)] -#[derive(Clone, Copy, Debug, Default)] -pub struct Battery { - pub events: u32, - pub status: u32, - pub last_full_charge: u32, - pub cycle_count: u32, - pub state: u32, - pub present_rate: u32, - pub remain_cap: u32, - pub present_volt: u32, - pub psr_state: u32, - pub psr_max_out: u32, - pub psr_max_in: u32, - pub peak_level: u32, - pub peak_power: u32, - pub sus_level: u32, - pub sus_power: u32, - pub peak_thres: u32, - pub sus_thres: u32, - pub trip_thres: u32, - pub bmc_data: u32, - pub bmd_data: u32, - pub bmd_flags: u32, - pub bmd_count: u32, - pub charge_time: u32, - pub run_time: u32, - pub sample_time: u32, -} - -#[allow(missing_docs)] -#[repr(C, packed)] -#[derive(Clone, Copy, Debug, Default)] -pub struct Thermal { - pub events: u32, - pub cool_mode: u32, - pub dba_limit: u32, - pub sonne_limit: u32, - pub ma_limit: u32, - pub fan1_on_temp: u32, - pub fan1_ramp_temp: u32, - pub fan1_max_temp: u32, - pub fan1_crt_temp: u32, - pub fan1_hot_temp: u32, - pub fan1_max_rpm: u32, - pub fan1_cur_rpm: u32, - pub tmp1_val: u32, - pub tmp1_timeout: u32, - pub tmp1_low: u32, - pub tmp1_high: u32, -} - -#[allow(missing_docs)] -#[repr(C, packed)] -#[derive(Clone, Copy, Debug, Default)] -pub struct Notifications { - pub service: u16, - pub event: u16, -} - -#[allow(missing_docs)] -#[repr(C, packed)] -#[derive(Clone, Copy, Debug, Default)] -pub struct ECMemory { - pub ver: Version, - pub caps: Capabilities, - pub notif: Notifications, - pub alarm: TimeAlarm, - pub batt: Battery, - pub therm: Thermal, -} diff --git a/embedded-service/src/event.rs b/embedded-service/src/event.rs new file mode 100644 index 000000000..a24736761 --- /dev/null +++ b/embedded-service/src/event.rs @@ -0,0 +1,145 @@ +//! Common traits for event senders and receivers +use core::{future::ready, marker::PhantomData}; + +use crate::error; + +use embassy_sync::{ + blocking_mutex::raw::RawMutex, + channel::{DynamicReceiver, DynamicSender, Receiver as ChannelReceiver, Sender as ChannelSender}, + pubsub::{DynImmediatePublisher, DynSubscriber, WaitResult}, +}; + +/// Common event sender trait +pub trait Sender { + /// Attempt to send an event + /// + /// Return none if the event cannot currently be sent + fn try_send(&mut self, event: E) -> Option<()>; + /// Send an event + fn send(&mut self, event: E) -> impl Future; +} + +/// Common event receiver trait +pub trait Receiver { + /// Attempt to receive an event + /// + /// Return none if there are no pending events + fn try_next(&mut self) -> Option; + /// Receive an event + fn wait_next(&mut self) -> impl Future; +} + +impl Sender for DynamicSender<'_, E> { + fn try_send(&mut self, event: E) -> Option<()> { + DynamicSender::try_send(self, event).ok() + } + + fn send(&mut self, event: E) -> impl Future { + DynamicSender::send(self, event) + } +} + +impl Receiver for DynamicReceiver<'_, E> { + fn try_next(&mut self) -> Option { + self.try_receive().ok() + } + + fn wait_next(&mut self) -> impl Future { + self.receive() + } +} + +impl Sender for DynImmediatePublisher<'_, E> { + fn try_send(&mut self, event: E) -> Option<()> { + self.try_publish(event).ok() + } + + fn send(&mut self, event: E) -> impl Future { + self.publish_immediate(event); + ready(()) + } +} + +impl Receiver for DynSubscriber<'_, E> { + fn try_next(&mut self) -> Option { + match self.try_next_message() { + Some(WaitResult::Message(e)) => Some(e), + Some(WaitResult::Lagged(e)) => { + error!("Subscriber lagged, skipping {} events", e); + None + } + _ => None, + } + } + + async fn wait_next(&mut self) -> E { + loop { + match self.next_message().await { + WaitResult::Message(e) => return e, + WaitResult::Lagged(e) => { + error!("Subscriber lagged, skipping {} events", e); + continue; + } + } + } + } +} + +impl Sender for ChannelSender<'_, M, E, N> { + fn try_send(&mut self, event: E) -> Option<()> { + ChannelSender::try_send(self, event).ok() + } + + fn send(&mut self, event: E) -> impl Future { + ChannelSender::send(self, event) + } +} + +impl Receiver for ChannelReceiver<'_, M, E, N> { + fn try_next(&mut self) -> Option { + ChannelReceiver::try_receive(self).ok() + } + + fn wait_next(&mut self) -> impl Future { + ChannelReceiver::receive(self) + } +} + +/// A sender that discards all events sent to it. +pub struct NoopSender; + +impl Sender for NoopSender { + fn try_send(&mut self, _event: E) -> Option<()> { + Some(()) + } + + async fn send(&mut self, _event: E) {} +} + +/// Applies a function on events before passing them to the wrapped sender +pub struct MapSender, F: FnMut(I) -> O> { + sender: S, + map_fn: F, + _phantom: PhantomData<(I, O)>, +} + +impl, F: FnMut(I) -> O> MapSender { + /// Create a new self + pub fn new(sender: S, map_fn: F) -> Self { + Self { + sender, + map_fn, + _phantom: PhantomData, + } + } +} + +impl, F: FnMut(I) -> O> Sender for MapSender { + fn try_send(&mut self, event: I) -> Option<()> { + self.sender.try_send((self.map_fn)(event)) + } + + fn send(&mut self, event: I) -> impl Future { + self.sender.send((self.map_fn)(event)) + } +} diff --git a/embedded-service/src/lib.rs b/embedded-service/src/lib.rs index 4ce50d61b..e62562e3a 100644 --- a/embedded-service/src/lib.rs +++ b/embedded-service/src/lib.rs @@ -14,17 +14,25 @@ pub mod thread_mode_cell; pub mod activity; pub mod broadcaster; pub mod buffer; -pub mod cfu; pub mod comms; -pub mod ec_type; +pub mod event; pub mod fmt; pub mod hid; pub mod init; pub mod ipc; pub mod keyboard; -pub mod power; +pub mod named; +pub mod relay; pub mod sync; -pub mod type_c; + +/// Hidden re-exports used by macros defined in this crate. +/// Not part of the public API — do not depend on these directly. +#[doc(hidden)] +pub mod _macro_internal { + pub use bitfield; + pub use mctp_rs; + pub use paste; +} /// Global Mutex type, ThreadModeRawMutex is used in a microcontroller context, whereas CriticalSectionRawMutex is used /// in a standard context for unit testing. @@ -75,8 +83,5 @@ pub type Never = core::convert::Infallible; pub async fn init() { comms::init(); activity::init(); - cfu::init(); keyboard::init(); - power::policy::init(); - type_c::controller::init(); } diff --git a/embedded-service/src/named.rs b/embedded-service/src/named.rs new file mode 100644 index 000000000..feebe6993 --- /dev/null +++ b/embedded-service/src/named.rs @@ -0,0 +1,7 @@ +//! Traits for things that have names. + +/// Trait for anything that has a name. +pub trait Named { + /// Return name + fn name(&self) -> &'static str; +} diff --git a/embedded-service/src/power/mod.rs b/embedded-service/src/power/mod.rs deleted file mode 100644 index 962fba0b9..000000000 --- a/embedded-service/src/power/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -//! Module for anything power related -#[allow(clippy::module_inception)] -pub mod policy; diff --git a/embedded-service/src/power/policy/action/device.rs b/embedded-service/src/power/policy/action/device.rs deleted file mode 100644 index 01d0d6c36..000000000 --- a/embedded-service/src/power/policy/action/device.rs +++ /dev/null @@ -1,172 +0,0 @@ -//! Device state machine actions -use super::*; -use crate::power::policy::{ConsumerPowerCapability, Error, ProviderPowerCapability, device, policy}; -use crate::{info, trace}; - -/// Device state machine control -pub struct Device<'a, S: Kind> { - device: &'a device::Device, - _state: core::marker::PhantomData, -} - -/// Enum to contain any state -pub enum AnyState<'a> { - /// Detached - Detached(Device<'a, Detached>), - /// Idle - Idle(Device<'a, Idle>), - /// Connected Consumer - ConnectedConsumer(Device<'a, ConnectedConsumer>), - /// Connected Provider - ConnectedProvider(Device<'a, ConnectedProvider>), -} - -impl AnyState<'_> { - /// Return the kind of the contained state - pub fn kind(&self) -> StateKind { - match self { - AnyState::Detached(_) => StateKind::Detached, - AnyState::Idle(_) => StateKind::Idle, - AnyState::ConnectedConsumer(_) => StateKind::ConnectedConsumer, - AnyState::ConnectedProvider(_) => StateKind::ConnectedProvider, - } - } -} - -impl<'a, S: Kind> Device<'a, S> { - /// Create a new state machine - pub(crate) fn new(device: &'a device::Device) -> Self { - Self { - device, - _state: core::marker::PhantomData, - } - } - - /// Detach the device - pub async fn detach(self) -> Result, Error> { - info!("Received detach from device {}", self.device.id().0); - self.device.set_state(device::State::Detached).await; - self.device.update_consumer_capability(None).await; - self.device.update_requested_provider_capability(None).await; - policy::send_request(self.device.id(), policy::RequestData::NotifyDetached) - .await? - .complete_or_err()?; - Ok(Device::new(self.device)) - } - - /// Disconnect this device - async fn disconnect_internal(&self) -> Result<(), Error> { - info!("Device {} disconnecting", self.device.id().0); - self.device.update_consumer_capability(None).await; - self.device.update_requested_provider_capability(None).await; - self.device.set_state(device::State::Idle).await; - policy::send_request(self.device.id(), policy::RequestData::NotifyDisconnect) - .await? - .complete_or_err() - } - - /// Notify the power policy service of an updated consumer power capability - async fn notify_consumer_power_capability_internal( - &self, - capability: Option, - ) -> Result<(), Error> { - info!( - "Device {} consume capability updated: {:#?}", - self.device.id().0, - capability - ); - self.device.update_consumer_capability(capability).await; - policy::send_request( - self.device.id(), - policy::RequestData::NotifyConsumerCapability(capability), - ) - .await? - .complete_or_err() - } - - /// Request the given power from the power policy service - async fn request_provider_power_capability_internal( - &self, - capability: ProviderPowerCapability, - ) -> Result<(), Error> { - if self.device.provider_capability().await == Some(capability) { - // Already operating at this capability, power policy is already aware, don't need to do anything - trace!("Device {} already requested: {:#?}", self.device.id().0, capability); - return Ok(()); - } - - info!("Request provide from device {}, {:#?}", self.device.id().0, capability); - self.device.update_requested_provider_capability(Some(capability)).await; - policy::send_request( - self.device.id(), - policy::RequestData::RequestProviderCapability(capability), - ) - .await? - .complete_or_err()?; - Ok(()) - } -} - -impl<'a> Device<'a, Detached> { - /// Attach the device - pub async fn attach(self) -> Result, Error> { - info!("Received attach from device {}", self.device.id().0); - self.device.set_state(device::State::Idle).await; - policy::send_request(self.device.id(), policy::RequestData::NotifyAttached) - .await? - .complete_or_err()?; - Ok(Device::new(self.device)) - } -} - -impl Device<'_, Idle> { - /// Notify the power policy service of an updated consumer power capability - pub async fn notify_consumer_power_capability( - &self, - capability: Option, - ) -> Result<(), Error> { - self.notify_consumer_power_capability_internal(capability).await - } - - /// Request the given power from the power policy service - pub async fn request_provider_power_capability(&self, capability: ProviderPowerCapability) -> Result<(), Error> { - self.request_provider_power_capability_internal(capability).await - } -} - -impl<'a> Device<'a, ConnectedConsumer> { - /// Disconnect this device - pub async fn disconnect(self) -> Result, Error> { - self.disconnect_internal().await?; - Ok(Device::new(self.device)) - } - - /// Notify the power policy service of an updated consumer power capability - pub async fn notify_consumer_power_capability( - &self, - capability: Option, - ) -> Result<(), Error> { - self.notify_consumer_power_capability_internal(capability).await - } -} - -impl<'a> Device<'a, ConnectedProvider> { - /// Disconnect this device - pub async fn disconnect(self) -> Result, Error> { - self.disconnect_internal().await?; - Ok(Device::new(self.device)) - } - - /// Request the given power from the power policy service - pub async fn request_provider_power_capability(&self, capability: ProviderPowerCapability) -> Result<(), Error> { - self.request_provider_power_capability_internal(capability).await - } - - /// Notify the power policy service of an updated consumer power capability - pub async fn notify_consumer_power_capability( - &self, - capability: Option, - ) -> Result<(), Error> { - self.notify_consumer_power_capability_internal(capability).await - } -} diff --git a/embedded-service/src/power/policy/action/mod.rs b/embedded-service/src/power/policy/action/mod.rs deleted file mode 100644 index 889dccf70..000000000 --- a/embedded-service/src/power/policy/action/mod.rs +++ /dev/null @@ -1,51 +0,0 @@ -//! Power policy actions -//! This modules contains wrapper structs that use type states to enforce the valid actions for each device state -use super::device::StateKind; - -pub mod device; -pub mod policy; - -trait Sealed {} - -/// Trait to provide the kind of a state type -#[allow(private_bounds)] -pub trait Kind: Sealed { - /// Return the kind of a state type - fn kind() -> StateKind; -} - -/// State type for a detached device -pub struct Detached; -impl Sealed for Detached {} -impl Kind for Detached { - fn kind() -> StateKind { - StateKind::Detached - } -} - -/// State type for an attached device -pub struct Idle; -impl Sealed for Idle {} -impl Kind for Idle { - fn kind() -> StateKind { - StateKind::Idle - } -} - -/// State type for a device that is providing power -pub struct ConnectedProvider; -impl Sealed for ConnectedProvider {} -impl Kind for ConnectedProvider { - fn kind() -> StateKind { - StateKind::ConnectedProvider - } -} - -/// State type for a device that is consuming power -pub struct ConnectedConsumer; -impl Sealed for ConnectedConsumer {} -impl Kind for ConnectedConsumer { - fn kind() -> StateKind { - StateKind::ConnectedConsumer - } -} diff --git a/embedded-service/src/power/policy/action/policy.rs b/embedded-service/src/power/policy/action/policy.rs deleted file mode 100644 index 0559421fc..000000000 --- a/embedded-service/src/power/policy/action/policy.rs +++ /dev/null @@ -1,238 +0,0 @@ -//! Policy state machine -use embassy_time::{Duration, TimeoutError, with_timeout}; - -use super::*; -use crate::power::policy::{ConsumerPowerCapability, Error, ProviderPowerCapability, device}; -use crate::{error, info}; - -/// Default timeout for device commands to prevent the policy from getting stuck -const DEFAULT_TIMEOUT: Duration = Duration::from_millis(5000); - -/// Policy state machine control -pub struct Policy<'a, S: Kind> { - device: &'a device::Device, - _state: core::marker::PhantomData, -} - -/// Enum to contain any state -pub enum AnyState<'a> { - /// Detached - Detached(Policy<'a, Detached>), - /// Idle - Idle(Policy<'a, Idle>), - /// Connected Consumer - ConnectedConsumer(Policy<'a, ConnectedConsumer>), - /// Connected Provider - ConnectedProvider(Policy<'a, ConnectedProvider>), -} - -impl AnyState<'_> { - /// Return the kind of the contained state - pub fn kind(&self) -> StateKind { - match self { - AnyState::Detached(_) => StateKind::Detached, - AnyState::Idle(_) => StateKind::Idle, - AnyState::ConnectedConsumer(_) => StateKind::ConnectedConsumer, - AnyState::ConnectedProvider(_) => StateKind::ConnectedProvider, - } - } -} - -impl<'a, S: Kind> Policy<'a, S> { - /// Create a new state machine - pub(crate) fn new(device: &'a device::Device) -> Self { - Self { - device, - _state: core::marker::PhantomData, - } - } - - /// Common disconnect function used by multiple states - async fn disconnect_internal_no_timeout(&self) -> Result<(), Error> { - info!("Device {} got disconnect request", self.device.id().0); - self.device - .execute_device_command(device::CommandData::Disconnect) - .await? - .complete_or_err()?; - self.device.set_state(device::State::Idle).await; - Ok(()) - } - - /// Common disconnect function used by multiple states - async fn disconnect_internal(&self) -> Result<(), Error> { - match with_timeout(DEFAULT_TIMEOUT, self.disconnect_internal_no_timeout()).await { - Ok(r) => r, - Err(TimeoutError) => Err(Error::Timeout), - } - } - - /// Common connect as provider function used by multiple states - async fn connect_as_provider_internal_no_timeout(&self, capability: ProviderPowerCapability) -> Result<(), Error> { - info!("Device {} connecting provider", self.device.id().0); - - self.device - .execute_device_command(device::CommandData::ConnectAsProvider(capability)) - .await? - .complete_or_err()?; - - self.device - .set_state(device::State::ConnectedProvider(capability)) - .await; - - Ok(()) - } - - /// Common connect provider function used by multiple states - async fn connect_provider_internal(&self, capability: ProviderPowerCapability) -> Result<(), Error> { - match with_timeout( - DEFAULT_TIMEOUT, - self.connect_as_provider_internal_no_timeout(capability), - ) - .await - { - Ok(r) => r, - Err(TimeoutError) => Err(Error::Timeout), - } - } -} - -// The policy can do nothing when no device is attached -impl Policy<'_, Detached> {} - -impl<'a> Policy<'a, Idle> { - /// Connect this device as a consumer - pub async fn connect_as_consumer_no_timeout( - self, - capability: ConsumerPowerCapability, - ) -> Result, Error> { - info!("Device {} connecting as consumer", self.device.id().0); - - self.device - .execute_device_command(device::CommandData::ConnectAsConsumer(capability)) - .await? - .complete_or_err()?; - - self.device - .set_state(device::State::ConnectedConsumer(capability)) - .await; - Ok(Policy::new(self.device)) - } - - /// Connect this device as a consumer - pub async fn connect_consumer( - self, - capability: ConsumerPowerCapability, - ) -> Result, Error> { - match with_timeout(DEFAULT_TIMEOUT, self.connect_as_consumer_no_timeout(capability)).await { - Ok(r) => r, - Err(TimeoutError) => Err(Error::Timeout), - } - } - - /// Connect this device as a provider - pub async fn connect_provider_no_timeout( - self, - capability: ProviderPowerCapability, - ) -> Result, Error> { - self.connect_as_provider_internal_no_timeout(capability) - .await - .map(|_| Policy::new(self.device)) - } - - /// Connect this device as a provider - pub async fn connect_provider( - self, - capability: ProviderPowerCapability, - ) -> Result, Error> { - self.connect_provider_internal(capability) - .await - .map(|_| Policy::new(self.device)) - } -} - -impl<'a> Policy<'a, ConnectedConsumer> { - /// Disconnect this device - pub async fn disconnect_no_timeout(self) -> Result, Error> { - self.disconnect_internal_no_timeout() - .await - .map(|_| Policy::new(self.device)) - } - - /// Disconnect this device - pub async fn disconnect(self) -> Result, Error> { - self.disconnect_internal().await.map(|_| Policy::new(self.device)) - } -} - -impl<'a> Policy<'a, ConnectedProvider> { - /// Disconnect this device - pub async fn disconnect_no_timeout(self) -> Result, Error> { - if let Err(e) = self.disconnect_internal_no_timeout().await { - error!("Error disconnecting device {}: {:?}", self.device.id().0, e); - return Err(e); - } - Ok(Policy::new(self.device)) - } - - /// Disconnect this device - pub async fn disconnect(self) -> Result, Error> { - match with_timeout(DEFAULT_TIMEOUT, self.disconnect_no_timeout()).await { - Ok(r) => r, - Err(TimeoutError) => Err(Error::Timeout), - } - } - - /// Connect this device as a consumer - pub async fn connect_as_consumer_no_timeout( - self, - capability: ConsumerPowerCapability, - ) -> Result, Error> { - info!("Device {} connecting as consumer", self.device.id().0); - - self.device - .execute_device_command(device::CommandData::ConnectAsConsumer(capability)) - .await? - .complete_or_err()?; - - self.device - .set_state(device::State::ConnectedConsumer(capability)) - .await; - Ok(Policy::new(self.device)) - } - - /// Connect this device as a consumer - pub async fn connect_consumer( - self, - capability: ConsumerPowerCapability, - ) -> Result, Error> { - match with_timeout(DEFAULT_TIMEOUT, self.connect_as_consumer_no_timeout(capability)).await { - Ok(r) => r, - Err(TimeoutError) => Err(Error::Timeout), - } - } - - /// Connect this device as a provider - pub async fn connect_provider_no_timeout( - &self, - capability: ProviderPowerCapability, - ) -> Result, Error> { - self.connect_as_provider_internal_no_timeout(capability) - .await - .map(|_| Policy::new(self.device)) - } - - /// Connect this device as a provider - pub async fn connect_provider( - &self, - capability: ProviderPowerCapability, - ) -> Result, Error> { - self.connect_provider_internal(capability) - .await - .map(|_| Policy::new(self.device)) - } - - /// Get the provider power capability of this device - pub async fn power_capability(&self) -> Option { - self.device.provider_capability().await - } -} diff --git a/embedded-service/src/power/policy/charger.rs b/embedded-service/src/power/policy/charger.rs deleted file mode 100644 index bca7fbd97..000000000 --- a/embedded-service/src/power/policy/charger.rs +++ /dev/null @@ -1,244 +0,0 @@ -//! Charger device struct and controller -use core::{future::Future, ops::DerefMut}; - -use embassy_sync::{channel::Channel, mutex::Mutex}; - -use crate::{ - GlobalRawMutex, intrusive_list, - power::{self, policy::ConsumerPowerCapability}, -}; - -/// Charger controller trait that device drivers may use to integrate with internal messaging system -pub trait ChargeController: embedded_batteries_async::charger::Charger { - /// Type of error returned by the bus - type ChargeControllerError; - - /// Returns with pending events - fn wait_event(&mut self) -> impl Future; - /// Initialize charger hardware, after this returns the charger should be ready to charge - fn init_charger(&mut self) -> impl Future>; - /// Returns if the charger hardware detects if a PSU is attached - fn is_psu_attached(&mut self) -> impl Future>; - /// Called after power policy attaches to a power port. - fn attach_handler( - &mut self, - capability: ConsumerPowerCapability, - ) -> impl Future>; - /// Called after power policy detaches from a power port, either to switch consumers, - /// or because PSU was disconnected. - fn detach_handler(&mut self) -> impl Future>; - /// Called when a charger CheckReady request (PolicyEvent::CheckReady) is sent to the power policy. - /// Upon successful return of this method, the charger is assumed to be powered and ready to communicate, - /// transitioning state from unpowered to powered. - /// - /// If the charger is powered, an Ok(()) does nothing. An Err(_) will put the charger into an - /// unpowered state, meaning another PolicyEvent::CheckReady must be sent to re-establish communications - /// with the charger. Upon successful return, the charger must be re-initialized by sending a - /// `PolicyEvent::InitRequest`. - fn is_ready(&mut self) -> impl Future> { - core::future::ready(Ok(())) - } -} - -/// Charger Device ID new type -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -#[cfg_attr(feature = "defmt", derive(defmt::Format))] -pub struct ChargerId(pub u8); - -/// PSU state as determined by charger device -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -#[cfg_attr(feature = "defmt", derive(defmt::Format))] -pub enum PsuState { - /// Charger detected PSU attached - Attached, - /// Charger detected PSU detached - Detached, -} - -impl From for PsuState { - fn from(value: bool) -> Self { - match value { - true => PsuState::Attached, - false => PsuState::Detached, - } - } -} - -/// Data for a device request -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -#[cfg_attr(feature = "defmt", derive(defmt::Format))] -pub enum ChargerEvent { - /// Charger finished initialization sequence - Initialized(PsuState), - /// PSU state changed - PsuStateChange(PsuState), - /// A timeout of some sort was detected - Timeout, - /// An error occured on the bus - BusError, -} - -/// Charger state errors -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -#[cfg_attr(feature = "defmt", derive(defmt::Format))] -pub enum ChargerError { - /// Charger received command in an invalid state - InvalidState(State), - /// Charger hardware timed out responding - Timeout, - /// Charger underlying bus error - BusError, -} - -impl From for power::policy::Error { - fn from(value: ChargerError) -> Self { - Self::Charger(value) - } -} - -/// Data for a device request -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -#[cfg_attr(feature = "defmt", derive(defmt::Format))] -pub enum PolicyEvent { - /// Request to initialize charger hardware - InitRequest, - /// New power policy detected - PolicyConfiguration(ConsumerPowerCapability), - /// Request to check if the charger hardware is ready to receive communications. - /// For example, if the charger is powered. - CheckReady, -} - -/// Data for a device request -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -#[cfg_attr(feature = "defmt", derive(defmt::Format))] -pub enum ChargerResponseData { - /// Command completed - Ack, - /// Charger Unpowered, but we are still Ok - UnpoweredAck, -} - -/// Response for charger requests from policy commands -pub type ChargerResponse = Result; - -/// Current state of the charger -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -#[cfg_attr(feature = "defmt", derive(defmt::Format))] -pub enum State { - /// Device is unpowered - Unpowered, - /// Device is powered - Powered(PoweredSubstate), -} - -/// Powered state substates -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -#[cfg_attr(feature = "defmt", derive(defmt::Format))] -pub enum PoweredSubstate { - /// Device is initializing - Init, - /// PSU is attached and device can charge if desired - PsuAttached, - /// PSU is detached - PsuDetached, -} - -/// Current state of the charger -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -#[cfg_attr(feature = "defmt", derive(defmt::Format))] -pub struct InternalState { - /// Charger device state - pub state: State, - /// Current charger capability - pub capability: Option, -} - -/// Channel size for device requests -pub const CHARGER_CHANNEL_SIZE: usize = 1; - -/// Device struct -pub struct Device { - /// Intrusive list node - node: intrusive_list::Node, - /// Device ID - id: ChargerId, - /// Current state of the device - state: Mutex, - /// Channel for requests to the device - commands: Channel, - /// Channel for responses from the device - response: Channel, -} - -impl Device { - /// Create a new device - pub fn new(id: ChargerId) -> Self { - Self { - node: intrusive_list::Node::uninit(), - id, - state: Mutex::new(InternalState { - state: State::Unpowered, - capability: None, - }), - commands: Channel::new(), - response: Channel::new(), - } - } - - /// Get the device ID - pub fn id(&self) -> ChargerId { - self.id - } - - /// Returns the current state of the device - pub async fn state(&self) -> InternalState { - *self.state.lock().await - } - - /// Set the state of the device - pub async fn set_state(&self, new_state: InternalState) { - let mut lock = self.state.lock().await; - let current_state = lock.deref_mut(); - *current_state = new_state; - } - - /// Wait for a command from policy - pub async fn wait_command(&self) -> PolicyEvent { - self.commands.receive().await - } - - /// Send a command to the charger - pub async fn send_command(&self, policy_event: PolicyEvent) { - self.commands.send(policy_event).await - } - - /// Send a response to the power policy - pub async fn send_response(&self, response: ChargerResponse) { - self.response.send(response).await - } - - /// Send a command and wait for a response from the charger - pub async fn execute_command(&self, policy_event: PolicyEvent) -> ChargerResponse { - self.send_command(policy_event).await; - self.response.receive().await - } -} - -impl intrusive_list::NodeContainer for Device { - fn get_node(&self) -> &crate::Node { - &self.node - } -} - -/// Trait for any container that holds a device -pub trait ChargerContainer { - /// Get the underlying device struct - fn get_charger(&self) -> &Device; -} - -impl ChargerContainer for Device { - fn get_charger(&self) -> &Device { - self - } -} diff --git a/embedded-service/src/power/policy/device.rs b/embedded-service/src/power/policy/device.rs deleted file mode 100644 index 23a6cc052..000000000 --- a/embedded-service/src/power/policy/device.rs +++ /dev/null @@ -1,286 +0,0 @@ -//! Device struct and methods -use core::ops::DerefMut; - -use embassy_sync::mutex::Mutex; - -use super::{DeviceId, Error, action}; -use crate::ipc::deferred; -use crate::power::policy::{ConsumerPowerCapability, ProviderPowerCapability}; -use crate::{GlobalRawMutex, intrusive_list}; - -/// Most basic device states -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -#[cfg_attr(feature = "defmt", derive(defmt::Format))] -pub enum StateKind { - /// No device attached - Detached, - /// Device is attached - Idle, - /// Device is actively providing power, USB PD source mode - ConnectedProvider, - /// Device is actively consuming power, USB PD sink mode - ConnectedConsumer, -} - -/// Current state of the power device -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -#[cfg_attr(feature = "defmt", derive(defmt::Format))] -pub enum State { - /// Device is attached, but is not currently providing or consuming power - Idle, - /// Device is attached and is currently providing power - ConnectedProvider(ProviderPowerCapability), - /// Device is attached and is currently consuming power - ConnectedConsumer(ConsumerPowerCapability), - /// No device attached - Detached, -} - -impl State { - /// Returns the correpsonding state kind - pub fn kind(&self) -> StateKind { - match self { - State::Idle => StateKind::Idle, - State::ConnectedProvider(_) => StateKind::ConnectedProvider, - State::ConnectedConsumer(_) => StateKind::ConnectedConsumer, - State::Detached => StateKind::Detached, - } - } -} - -/// Internal device state for power policy -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -#[cfg_attr(feature = "defmt", derive(defmt::Format))] -struct InternalState { - /// Current state of the device - pub state: State, - /// Current consumer capability - pub consumer_capability: Option, - /// Current requested provider capability - pub requested_provider_capability: Option, -} - -/// Data for a device request -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -#[cfg_attr(feature = "defmt", derive(defmt::Format))] -pub enum CommandData { - /// Start consuming on this device - ConnectAsConsumer(ConsumerPowerCapability), - /// Start providing power to port partner on this device - ConnectAsProvider(ProviderPowerCapability), - /// Stop providing or consuming on this device - Disconnect, -} - -/// Request from power policy service to a device -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -#[cfg_attr(feature = "defmt", derive(defmt::Format))] -pub struct Command { - /// Target device - pub id: DeviceId, - /// Request data - pub data: CommandData, -} - -/// Data for a device response -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -#[cfg_attr(feature = "defmt", derive(defmt::Format))] -pub enum ResponseData { - /// The request was successful - Complete, -} - -impl ResponseData { - /// Returns an InvalidResponse error if the response is not complete - pub fn complete_or_err(self) -> Result<(), Error> { - match self { - ResponseData::Complete => Ok(()), - } - } -} - -/// Wrapper type to make code cleaner -pub type InternalResponseData = Result; - -/// Response from a device to the power policy service -pub struct Response { - /// Target device - pub id: DeviceId, - /// Response data - pub data: ResponseData, -} - -/// Device struct -pub struct Device { - /// Intrusive list node - node: intrusive_list::Node, - /// Device ID - id: DeviceId, - /// Current state of the device - state: Mutex, - /// Command channel - command: deferred::Channel, -} - -impl Device { - /// Create a new device - pub fn new(id: DeviceId) -> Self { - Self { - node: intrusive_list::Node::uninit(), - id, - state: Mutex::new(InternalState { - state: State::Detached, - consumer_capability: None, - requested_provider_capability: None, - }), - command: deferred::Channel::new(), - } - } - - /// Get the device ID - pub fn id(&self) -> DeviceId { - self.id - } - - /// Returns the current state of the device - pub async fn state(&self) -> State { - self.state.lock().await.state - } - - /// Returns the current consumer capability of the device - pub async fn consumer_capability(&self) -> Option { - self.state.lock().await.consumer_capability - } - - /// Returns true if the device is currently consuming power - pub async fn is_consumer(&self) -> bool { - self.state().await.kind() == StateKind::ConnectedConsumer - } - - /// Returns current provider power capability - pub async fn provider_capability(&self) -> Option { - match self.state().await { - State::ConnectedProvider(capability) => Some(capability), - _ => None, - } - } - - /// Returns the current requested provider capability - pub async fn requested_provider_capability(&self) -> Option { - self.state.lock().await.requested_provider_capability - } - - /// Returns true if the device is currently providing power - pub async fn is_provider(&self) -> bool { - self.state().await.kind() == StateKind::ConnectedProvider - } - - /// Execute a command on the device - pub(super) async fn execute_device_command(&self, command: CommandData) -> Result { - self.command.execute(command).await - } - - /// Create a handler for the command channel - /// - /// DROP SAFETY: Direct call to deferred channel primitive - pub async fn receive(&self) -> deferred::Request<'_, GlobalRawMutex, CommandData, InternalResponseData> { - self.command.receive().await - } - - /// Internal function to set device state - pub(super) async fn set_state(&self, new_state: State) { - let mut lock = self.state.lock().await; - let state = lock.deref_mut(); - state.state = new_state; - } - - /// Internal function to set consumer capability - pub(super) async fn update_consumer_capability(&self, capability: Option) { - let mut lock = self.state.lock().await; - let state = lock.deref_mut(); - state.consumer_capability = capability; - } - - /// Internal function to set requested provider capability - pub(super) async fn update_requested_provider_capability(&self, capability: Option) { - let mut lock = self.state.lock().await; - let state = lock.deref_mut(); - state.requested_provider_capability = capability; - } - - /// Try to provide access to the device actions for the given state - pub async fn try_device_action(&self) -> Result, Error> { - let state = self.state().await.kind(); - if S::kind() != state { - return Err(Error::InvalidState(S::kind(), state)); - } - Ok(action::device::Device::new(self)) - } - - /// Provide access to the current device state - pub async fn device_action(&self) -> action::device::AnyState<'_> { - match self.state().await.kind() { - StateKind::Detached => action::device::AnyState::Detached(action::device::Device::new(self)), - StateKind::Idle => action::device::AnyState::Idle(action::device::Device::new(self)), - StateKind::ConnectedProvider => { - action::device::AnyState::ConnectedProvider(action::device::Device::new(self)) - } - StateKind::ConnectedConsumer => { - action::device::AnyState::ConnectedConsumer(action::device::Device::new(self)) - } - } - } - - /// Try to provide access to the policy actions for the given state - /// Implemented here for lifetime reasons - pub(super) async fn try_policy_action(&self) -> Result, Error> { - let state = self.state().await.kind(); - if S::kind() != state { - return Err(Error::InvalidState(S::kind(), state)); - } - Ok(action::policy::Policy::new(self)) - } - - /// Provide access to the current policy actions - /// Implemented here for lifetime reasons - pub(super) async fn policy_action(&self) -> action::policy::AnyState<'_> { - match self.state().await.kind() { - StateKind::Detached => action::policy::AnyState::Detached(action::policy::Policy::new(self)), - StateKind::Idle => action::policy::AnyState::Idle(action::policy::Policy::new(self)), - StateKind::ConnectedProvider => { - action::policy::AnyState::ConnectedProvider(action::policy::Policy::new(self)) - } - StateKind::ConnectedConsumer => { - action::policy::AnyState::ConnectedConsumer(action::policy::Policy::new(self)) - } - } - } - - /// Detach the device, this action is available in all states - pub async fn detach(&self) -> Result, Error> { - match self.device_action().await { - action::device::AnyState::Detached(state) => Ok(state), - action::device::AnyState::Idle(state) => state.detach().await, - action::device::AnyState::ConnectedProvider(state) => state.detach().await, - action::device::AnyState::ConnectedConsumer(state) => state.detach().await, - } - } -} - -impl intrusive_list::NodeContainer for Device { - fn get_node(&self) -> &crate::Node { - &self.node - } -} - -/// Trait for any container that holds a device -pub trait DeviceContainer { - /// Get the underlying device struct - fn get_power_policy_device(&self) -> &Device; -} - -impl DeviceContainer for Device { - fn get_power_policy_device(&self) -> &Device { - self - } -} diff --git a/embedded-service/src/power/policy/mod.rs b/embedded-service/src/power/policy/mod.rs deleted file mode 100644 index b925f860b..000000000 --- a/embedded-service/src/power/policy/mod.rs +++ /dev/null @@ -1,162 +0,0 @@ -//! Power policy related data structures and messages -pub mod action; -pub mod charger; -pub mod device; -pub mod flags; -pub mod policy; - -pub use policy::{init, register_device}; - -use crate::power::policy::charger::ChargerError; - -/// Error type -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -#[cfg_attr(feature = "defmt", derive(defmt::Format))] -pub enum Error { - /// The requested device does not exist - InvalidDevice, - /// The provide request was denied, contains maximum available power - CannotProvide(Option), - /// The consume request was denied, contains maximum available power - CannotConsume(Option), - /// The device is not in the correct state (expected, actual) - InvalidState(device::StateKind, device::StateKind), - /// Invalid response - InvalidResponse, - /// Busy, the device cannot respond to the request at this time - Busy, - /// Timeout - Timeout, - /// Bus error - Bus, - /// Charger specific error, underlying error should have more context - Charger(ChargerError), - /// Generic failure - Failed, -} - -/// Device ID new type -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -#[cfg_attr(feature = "defmt", derive(defmt::Format))] -pub struct DeviceId(pub u8); - -/// Amount of power that a device can provider or consume -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -#[cfg_attr(feature = "defmt", derive(defmt::Format))] -pub struct PowerCapability { - /// Available voltage in mV - pub voltage_mv: u16, - /// Max available current in mA - pub current_ma: u16, -} - -impl PowerCapability { - /// Calculate maximum power - pub fn max_power_mw(&self) -> u32 { - self.voltage_mv as u32 * self.current_ma as u32 / 1000 - } -} - -impl PartialOrd for PowerCapability { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } -} - -impl Ord for PowerCapability { - fn cmp(&self, other: &Self) -> core::cmp::Ordering { - self.max_power_mw().cmp(&other.max_power_mw()) - } -} - -/// Power capability with consumer flags -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -#[cfg_attr(feature = "defmt", derive(defmt::Format))] -pub struct ConsumerPowerCapability { - /// Power capability - pub capability: PowerCapability, - /// Consumer flags - pub flags: flags::Consumer, -} - -impl From for ConsumerPowerCapability { - fn from(capability: PowerCapability) -> Self { - Self { - capability, - flags: flags::Consumer::none(), - } - } -} - -/// Power capability with provider flags -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -#[cfg_attr(feature = "defmt", derive(defmt::Format))] -pub struct ProviderPowerCapability { - /// Power capability - pub capability: PowerCapability, - /// Provider flags - pub flags: flags::Provider, -} - -impl From for ProviderPowerCapability { - fn from(capability: PowerCapability) -> Self { - Self { - capability, - flags: flags::Provider::none(), - } - } -} - -/// Combined power capability with flags enum -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -#[cfg_attr(feature = "defmt", derive(defmt::Format))] -pub enum PowerCapabilityFlags { - /// Consumer flags - Consumer(ConsumerPowerCapability), - /// Provider flags - Provider(ProviderPowerCapability), -} - -/// Unconstrained state information -#[derive(Debug, Clone, Default, Copy, PartialEq, Eq)] -#[cfg_attr(feature = "defmt", derive(defmt::Format))] -pub struct UnconstrainedState { - /// Unconstrained state - pub unconstrained: bool, - /// Available unconstrained devices - pub available: usize, -} - -impl UnconstrainedState { - /// Create a new unconstrained state - pub fn new(unconstrained: bool, available: usize) -> Self { - Self { - unconstrained, - available, - } - } -} - -/// Data to send with the comms service -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -#[cfg_attr(feature = "defmt", derive(defmt::Format))] -pub enum CommsData { - /// Consumer disconnected - ConsumerDisconnected(DeviceId), - /// Consumer connected - ConsumerConnected(DeviceId, ConsumerPowerCapability), - /// Provider disconnected - ProviderDisconnected(DeviceId), - /// Provider connected - ProviderConnected(DeviceId, ProviderPowerCapability), - /// Unconstrained state changed - Unconstrained(UnconstrainedState), -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -#[cfg_attr(feature = "defmt", derive(defmt::Format))] -/// Message to send with the comms service -pub struct CommsMessage { - /// Message data - pub data: CommsData, -} diff --git a/embedded-service/src/power/policy/policy.rs b/embedded-service/src/power/policy/policy.rs deleted file mode 100644 index ecedcbce0..000000000 --- a/embedded-service/src/power/policy/policy.rs +++ /dev/null @@ -1,283 +0,0 @@ -//! Context for any power policy implementations -use core::sync::atomic::{AtomicBool, Ordering}; - -use crate::GlobalRawMutex; -use crate::broadcaster::immediate as broadcaster; -use crate::power::policy::{CommsMessage, ConsumerPowerCapability, ProviderPowerCapability}; -use embassy_sync::channel::Channel; - -use super::charger::ChargerResponse; -use super::device::{self}; -use super::{DeviceId, Error, action, charger}; -use crate::power::policy::charger::ChargerResponseData::Ack; -use crate::{error, intrusive_list}; - -/// Number of slots for policy requests -const POLICY_CHANNEL_SIZE: usize = 1; - -/// Data for a power policy request -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -#[cfg_attr(feature = "defmt", derive(defmt::Format))] -pub enum RequestData { - /// Notify that a device has attached - NotifyAttached, - /// Notify that available power for consumption has changed - NotifyConsumerCapability(Option), - /// Request the given amount of power to provider - RequestProviderCapability(ProviderPowerCapability), - /// Notify that a device cannot consume or provide power anymore - NotifyDisconnect, - /// Notify that a device has detached - NotifyDetached, -} - -/// Request to the power policy service -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -#[cfg_attr(feature = "defmt", derive(defmt::Format))] -pub struct Request { - /// Device that sent this request - pub id: DeviceId, - /// Request data - pub data: RequestData, -} - -/// Data for a power policy response -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -#[cfg_attr(feature = "defmt", derive(defmt::Format))] -pub enum ResponseData { - /// The request was completed successfully - Complete, -} - -impl ResponseData { - /// Returns an InvalidResponse error if the response is not complete - pub fn complete_or_err(self) -> Result<(), Error> { - match self { - ResponseData::Complete => Ok(()), - } - } -} - -/// Response from the power policy service -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -#[cfg_attr(feature = "defmt", derive(defmt::Format))] -pub struct Response { - /// Target device - pub id: DeviceId, - /// Response data - pub data: ResponseData, -} - -/// Wrapper type to make code cleaner -type InternalResponseData = Result; - -/// Power policy context -struct Context { - /// Registered devices - devices: intrusive_list::IntrusiveList, - /// Policy request - policy_request: Channel, - /// Policy response - policy_response: Channel, - /// Registered chargers - chargers: intrusive_list::IntrusiveList, - /// Message broadcaster - broadcaster: broadcaster::Immediate, -} - -impl Context { - const fn new() -> Self { - Self { - devices: intrusive_list::IntrusiveList::new(), - chargers: intrusive_list::IntrusiveList::new(), - policy_request: Channel::new(), - policy_response: Channel::new(), - broadcaster: broadcaster::Immediate::new(), - } - } -} - -static CONTEXT: Context = Context::new(); - -/// Init power policy service -pub fn init() {} - -/// Register a device with the power policy service -pub fn register_device(device: &'static impl device::DeviceContainer) -> Result<(), intrusive_list::Error> { - let device = device.get_power_policy_device(); - if get_device(device.id()).is_some() { - return Err(intrusive_list::Error::NodeAlreadyInList); - } - - CONTEXT.devices.push(device) -} - -/// Register a charger with the power policy service -pub fn register_charger(device: &'static impl charger::ChargerContainer) -> Result<(), intrusive_list::Error> { - let device = device.get_charger(); - if get_charger(device.id()).is_some() { - return Err(intrusive_list::Error::NodeAlreadyInList); - } - - CONTEXT.chargers.push(device) -} - -/// Find a device by its ID -fn get_device(id: DeviceId) -> Option<&'static device::Device> { - for device in &CONTEXT.devices { - if let Some(data) = device.data::() { - if data.id() == id { - return Some(data); - } - } else { - error!("Non-device located in devices list"); - } - } - - None -} - -/// Returns the total amount of power that is being supplied to external devices -pub async fn compute_total_provider_power_mw() -> u32 { - let mut total = 0; - for device in CONTEXT.devices.iter_only::() { - if let Some(capability) = device.provider_capability().await - && device.is_provider().await - { - total += capability.capability.max_power_mw(); - } - } - total -} - -/// Find a device by its ID -fn get_charger(id: charger::ChargerId) -> Option<&'static charger::Device> { - for charger in &CONTEXT.chargers { - if let Some(data) = charger.data::() { - if data.id() == id { - return Some(data); - } - } else { - error!("Non-device located in charger list"); - } - } - - None -} - -/// Convenience function to send a request to the power policy service -pub(super) async fn send_request(from: DeviceId, request: RequestData) -> Result { - CONTEXT - .policy_request - .send(Request { - id: from, - data: request, - }) - .await; - CONTEXT.policy_response.receive().await -} - -/// Initialize chargers in hardware -pub async fn init_chargers() -> ChargerResponse { - for charger in &CONTEXT.chargers { - if let Some(data) = charger.data::() { - data.execute_command(charger::PolicyEvent::InitRequest) - .await - .inspect_err(|e| error!("Charger {:?} failed InitRequest: {:?}", data.id(), e))?; - } - } - Ok(Ack) -} - -/// Check if charger hardware is ready for communications. -pub async fn check_chargers_ready() -> ChargerResponse { - for charger in &CONTEXT.chargers { - if let Some(data) = charger.data::() { - data.execute_command(charger::PolicyEvent::CheckReady) - .await - .inspect_err(|e| error!("Charger {:?} failed CheckReady: {:?}", data.id(), e))?; - } - } - Ok(Ack) -} - -/// Register a message receiver for power policy messages -pub fn register_message_receiver( - receiver: &'static broadcaster::Receiver<'_, CommsMessage>, -) -> intrusive_list::Result<()> { - CONTEXT.broadcaster.register_receiver(receiver) -} - -/// Singleton struct to give access to the power policy context -pub struct ContextToken(()); - -impl ContextToken { - /// Create a new context token, returning None if this function has been called before - pub fn create() -> Option { - static INIT: AtomicBool = AtomicBool::new(false); - if INIT.load(Ordering::SeqCst) { - return None; - } - - INIT.store(true, Ordering::SeqCst); - Some(ContextToken(())) - } - - /// Initialize Policy charger devices - pub async fn init() -> Result<(), Error> { - // Check if the chargers are powered and able to communicate - check_chargers_ready().await?; - // Initialize chargers - init_chargers().await?; - - Ok(()) - } - - /// Wait for a power policy request - pub async fn wait_request(&self) -> Request { - CONTEXT.policy_request.receive().await - } - - /// Send a response to a power policy request - pub async fn send_response(&self, response: Result) { - CONTEXT.policy_response.send(response).await - } - - /// Get a device by its ID - pub fn get_device(&self, id: DeviceId) -> Result<&'static device::Device, Error> { - get_device(id).ok_or(Error::InvalidDevice) - } - - /// Provides access to the device list - pub fn devices(&self) -> &intrusive_list::IntrusiveList { - &CONTEXT.devices - } - - /// Get a charger by its ID - pub fn get_charger(&self, id: charger::ChargerId) -> Result<&'static charger::Device, Error> { - get_charger(id).ok_or(Error::InvalidDevice) - } - - /// Provides access to the charger list - pub fn chargers(&self) -> &intrusive_list::IntrusiveList { - &CONTEXT.chargers - } - - /// Try to provide access to the actions available to the policy for the given state and device - pub async fn try_policy_action( - &self, - id: DeviceId, - ) -> Result, Error> { - self.get_device(id)?.try_policy_action().await - } - - /// Provide access to current policy actions - pub async fn policy_action(&self, id: DeviceId) -> Result, Error> { - Ok(self.get_device(id)?.policy_action().await) - } - - /// Broadcast a power policy message to all subscribers - pub async fn broadcast_message(&self, message: CommsMessage) { - CONTEXT.broadcaster.broadcast(message).await; - } -} diff --git a/embedded-service/src/relay/mod.rs b/embedded-service/src/relay/mod.rs new file mode 100644 index 000000000..d92cfb97f --- /dev/null +++ b/embedded-service/src/relay/mod.rs @@ -0,0 +1,503 @@ +//! Helper code for serialization/deserialization of arbitrary messages to/from the embedded controller via a relay service, e.g. the eSPI service. + +/// Error type for serializing/deserializing messages +#[derive(Debug)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum MessageSerializationError { + /// The message payload does not represent a valid message + InvalidPayload(&'static str), + + /// The message discriminant does not represent a known message type + UnknownMessageDiscriminant(u16), + + /// The provided buffer is too small to serialize the message + BufferTooSmall, + + /// Unspecified error + Other(&'static str), +} + +/// Trait for serializing and deserializing messages +pub trait SerializableMessage: Sized { + /// Serializes the message into the provided buffer. + /// On success, returns the number of bytes written + fn serialize(self, buffer: &mut [u8]) -> Result; + + /// Returns the discriminant needed to deserialize this type of message. + fn discriminant(&self) -> u16; + + /// Deserializes the message from the provided buffer. + fn deserialize(discriminant: u16, buffer: &[u8]) -> Result; +} + +// Prevent other types from implementing SerializableResult - they should instead use SerializableMessage on a Response type and an Error type +#[doc(hidden)] +mod private { + pub trait Sealed {} + + impl Sealed for Result {} +} + +/// Responses sent over MCTP are called "Results" and are of type Result where T and E both implement SerializableMessage +pub trait SerializableResult: private::Sealed + Sized { + /// The type of the result when the operation being responded to succeeded + type SuccessType: SerializableMessage; + + /// The type of the result when the operation being responded to failed + type ErrorType: SerializableMessage; + + /// Returns true if the result represents a successful operation, false otherwise + fn is_ok(&self) -> bool; + + /// Returns a unique discriminant that can be used to deserialize the specific type of result. + /// Discriminants can be reused for success and error messages. + fn discriminant(&self) -> u16; + + /// Writes the result into the provided buffer. + /// On success, returns the number of bytes written + fn serialize(self, buffer: &mut [u8]) -> Result; + + /// Attempts to deserialize the result from the provided buffer. + fn deserialize(is_error: bool, discriminant: u16, buffer: &[u8]) -> Result; +} + +impl SerializableResult for Result +where + T: SerializableMessage, + E: SerializableMessage, +{ + type SuccessType = T; + type ErrorType = E; + + fn is_ok(&self) -> bool { + Result::::is_ok(self) + } + + fn discriminant(&self) -> u16 { + match self { + Ok(success_value) => success_value.discriminant(), + Err(error_value) => error_value.discriminant(), + } + } + + fn serialize(self, buffer: &mut [u8]) -> Result { + match self { + Ok(success_value) => success_value.serialize(buffer), + Err(error_value) => error_value.serialize(buffer), + } + } + + fn deserialize(is_error: bool, discriminant: u16, buffer: &[u8]) -> Result { + if is_error { + Ok(Err(E::deserialize(discriminant, buffer)?)) + } else { + Ok(Ok(T::deserialize(discriminant, buffer)?)) + } + } +} + +pub mod mctp { + //! Contains helper functions for services that relay comms messages over MCTP + + /// Error type for MCTP relay operations + #[derive(Debug, Clone, Copy, PartialEq, Eq)] + #[cfg_attr(feature = "defmt", derive(defmt::Format))] + pub enum MctpError { + /// The endpoint ID does not correspond to a known service + UnknownEndpointId, + } + + /// Trait for types that are used by a relay service to relay messages from your service over the wire. + /// If you are implementing this trait, you should also implement RelayServiceHandler. + /// + pub trait RelayServiceHandlerTypes { + /// The request type that this service handler processes + type RequestType: super::SerializableMessage; + + /// The result type that this service handler processes + type ResultType: super::SerializableResult; + } + + /// Trait for a service that can be relayed over an external bus (e.g. battery service, thermal service, time-alarm service) + /// + pub trait RelayServiceHandler: RelayServiceHandlerTypes { + /// Process the provided request and yield a result. + fn process_request<'a>( + &'a self, + request: Self::RequestType, + ) -> impl core::future::Future + 'a; + } + + // Traits below this point are intended for consumption by relay services (e.g. the eSPI service), not individual services that want their messages relayed. + // In general, you should not implement these yourself; rather, you should leverage the `impl_odp_mctp_relay_handler` macro to do that for you. + + /// Contains additional methods that must be implemented on the relay header type. + /// Do not implement this yourself - rather, rely on the `impl_odp_mctp_relay_handler` macro to implement this. + #[doc(hidden)] + pub trait RelayHeader { + /// Return the ID of the service associated with the request + fn get_service_id(&self) -> ServiceIdType; + } + + /// Contains additional methods that must be implemented on the relay response type. + /// Do not implement this yourself - rather, rely on the `impl_odp_mctp_relay_handler` macro to implement this. + #[doc(hidden)] + pub trait RelayResponse { + /// Construct an MCTP header suitable for representing the result based on the provided service handler ID and result + fn create_header(&self, service_id: &ServiceIdType) -> HeaderType; + } + + /// Trait for aggregating collections of services that can be relayed over an external bus. + /// Do not implement this yourself - rather, rely on the `impl_odp_mctp_relay_handler` macro to implement this. + /// + pub trait RelayHandler { + /// The type that uniquely identifies individual services. Generally expected to be a C-style enum. + type ServiceIdType: Into + TryFrom + Copy; + + /// The header type used by request and result enums + type HeaderType: mctp_rs::MctpMessageHeaderTrait + RelayHeader; + + /// An enum over all possible request types + type RequestEnumType: for<'buf> mctp_rs::MctpMessageTrait<'buf, Header = Self::HeaderType>; + + /// An enum over all possible result types + type ResultEnumType: for<'buf> mctp_rs::MctpMessageTrait<'buf, Header = Self::HeaderType> + + RelayResponse; + + /// Process the provided request and yield a result. + fn process_request<'a>( + &'a self, + message: Self::RequestEnumType, + ) -> impl core::future::Future + 'a; + } + + /// This macro generates a relay type over a collection of message types, which can be used by a relay service to + /// receive messages over the wire and translate them into calls to a particular service on the EC. + /// + /// This is the recommended way to implement a relay handler - you should not implement the RelayHandler trait yourself. + /// + /// This macro will emit a type with the name you specify that is generic over a lifetime for the hardware (probably 'static in production code), + /// implements the `RelayHandler` trait, and has a single constructor method `new` that takes as arguments references to the service handler + /// types that you specify that have the 'hardware lifetime'. + /// + /// The macro takes the following inputs once: + /// relay_type_name: The name of the relay type to generate. This is arbitrary. The macro will emit a type with this name. + /// + /// Followed by a list of any number of service entries, which are specified by the following inputs: + /// service_name: A name to assign to generated identifiers associated with the service, e.g. "Battery". + /// This can be arbitrary. + /// service_id: A unique u8 that addresses that service on the EC. + /// service_handler_type: A type that implements the RelayServiceHandler trait, which will be used to process messages + /// for this service. + /// + /// Example usage: + /// + /// ```ignore + /// + /// impl_odp_mctp_relay_handler!( + /// MyRelayHandlerType; + /// Battery, 0x9, battery_service_relay::RelayHandler>; + /// TimeAlarm, 0xB, time_alarm_service_relay::RelayHandler>; + /// ); + /// + /// let relay_handler = MyRelayHandlerType::new(battery_service_instance, time_alarm_service_instance); + /// + /// // Then, pass relay_handler to your relay service (e.g. eSPI service), which should be generic over an `impl RelayHandler`. + /// + /// ``` + /// + #[macro_export] + macro_rules! impl_odp_mctp_relay_handler { + ( + $relay_type_name:ident; + $( + $service_name:ident, + $service_id:expr, + $service_handler_type:ty; + )+ + ) => { + $crate::_macro_internal::paste::paste! { + mod [< _odp_impl_ $relay_type_name:snake >] { + use $crate::_macro_internal::bitfield::bitfield; + use core::convert::Infallible; + use $crate::_macro_internal::mctp_rs::smbus_espi::SmbusEspiMedium; + use $crate::_macro_internal::mctp_rs::{MctpMedium, MctpMessageHeaderTrait, MctpMessageTrait, MctpPacketError, MctpPacketResult}; + use $crate::relay::{SerializableMessage, SerializableResult}; + use $crate::relay::mctp::RelayServiceHandler; + + #[derive(Debug, PartialEq, Eq, Clone, Copy)] + #[repr(u8)] + pub enum OdpService { + $( + $service_name = $service_id, + )+ + } + + impl From for u8 { + fn from(val: OdpService) -> u8 { + val as u8 + } + } + + impl TryFrom for OdpService { + type Error = u8; + fn try_from(value: u8) -> Result { + match value { + $( + $service_id => Ok(OdpService::$service_name), + )+ + other => Err(other), + } + } + } + + pub enum HostRequest { + $( + $service_name(<$service_handler_type as $crate::relay::mctp::RelayServiceHandlerTypes>::RequestType), + )+ + } + + impl MctpMessageTrait<'_> for HostRequest { + type Header = OdpHeader; + const MESSAGE_TYPE: u8 = 0x7D; // ODP message type + + fn serialize(self, buffer: &mut [u8]) -> MctpPacketResult { + match self { + $( + HostRequest::$service_name(request) => SerializableMessage::serialize(request, buffer) + .map_err(|_| MctpPacketError::SerializeError(concat!("Failed to serialize ", stringify!($service_name), " request"))), + )+ + } + } + + fn deserialize(header: &Self::Header, buffer: &'_ [u8]) -> MctpPacketResult { + Ok(match header.service { + $( + OdpService::$service_name => Self::$service_name( + <$service_handler_type as $crate::relay::mctp::RelayServiceHandlerTypes>::RequestType::deserialize(header.message_id, buffer) + .map_err(|_| MctpPacketError::CommandParseError(concat!("Could not parse ", stringify!($service_name), " request")))?, + ), + )+ + }) + } + } + + bitfield! { + /// Wire format for ODP MCTP headers. Not user-facing - use OdpHeader instead. + #[derive(Copy, Clone, PartialEq, Eq)] + struct OdpHeaderWireFormat(u32); + impl Debug; + impl new; + /// If true, represents a request; otherwise, represents a result + is_request, set_is_request: 25; + + /// The service ID that this message is related to + /// Note: Error checking is done when you access the field, not when you construct the OdpHeader. Take care when constructing a header. + u8, service_id, set_service_id: 23, 16; + + /// On results, indicates if the result message is an error. Unused on requests. + is_error, set_is_error: 15; + + /// The message type/discriminant + u16, message_id, set_message_id: 14, 0; + } + + #[derive(Copy, Clone, PartialEq, Eq)] + pub enum OdpMessageType { + Request, + Result { is_error: bool }, + } + + #[derive(Copy, Clone, PartialEq, Eq)] + pub struct OdpHeader { + pub message_type: OdpMessageType, + pub service: OdpService, + pub message_id: u16, + } + + impl From for OdpHeaderWireFormat { + fn from(src: OdpHeader) -> Self { + Self::new( + matches!(src.message_type, OdpMessageType::Request), + src.service.into(), + match src.message_type { + OdpMessageType::Request => false, // unused on requests + OdpMessageType::Result { is_error } => is_error, + }, + src.message_id, + ) + } + } + + impl TryFrom for OdpHeader { + type Error = MctpPacketError; + + fn try_from(src: OdpHeaderWireFormat) -> Result { + let service = OdpService::try_from(src.service_id()) + .map_err(|_| MctpPacketError::HeaderParseError("invalid odp service in odp header"))?; + + let message_type = if src.is_request() { + OdpMessageType::Request + } else { + OdpMessageType::Result { + is_error: src.is_error(), + } + }; + + Ok(OdpHeader { + message_type, + service, + message_id: src.message_id(), + }) + } + } + + impl MctpMessageHeaderTrait for OdpHeader { + fn serialize(self, buffer: &mut [u8]) -> MctpPacketResult { + let wire_format = OdpHeaderWireFormat::from(self); + let bytes = wire_format.0.to_be_bytes(); + buffer + .get_mut(0..bytes.len()) + .ok_or(MctpPacketError::SerializeError("buffer too small for odp header"))? + .copy_from_slice(&bytes); + + Ok(bytes.len()) + } + + fn deserialize(buffer: &[u8]) -> MctpPacketResult<(Self, &[u8]), M> { + let bytes = buffer + .get(0..core::mem::size_of::()) + .ok_or(MctpPacketError::HeaderParseError("buffer too small for odp header"))?; + let raw = u32::from_be_bytes( + bytes + .try_into() + .map_err(|_| MctpPacketError::HeaderParseError("buffer too small for odp header"))?, + ); + + let parsed_wire_format = OdpHeaderWireFormat(raw); + let header = OdpHeader::try_from(parsed_wire_format) + .map_err(|_| MctpPacketError::HeaderParseError("invalid odp header received"))?; + + Ok(( + header, + buffer + .get(core::mem::size_of::()..) + .ok_or(MctpPacketError::HeaderParseError("buffer too small for odp header"))?, + )) + } + } + + impl $crate::relay::mctp::RelayHeader for OdpHeader { + fn get_service_id(&self) -> OdpService { + self.service + } + } + + #[derive(Clone)] + pub enum HostResult { + $( + $service_name(<$service_handler_type as $crate::relay::mctp::RelayServiceHandlerTypes>::ResultType), + )+ + } + + impl $crate::relay::mctp::RelayResponse for HostResult { + fn create_header(&self, service_id: &OdpService) -> OdpHeader { + match (self) { + $( + (HostResult::$service_name(result)) => OdpHeader { + message_type: OdpMessageType::Result { is_error: !result.is_ok() }, + service: *service_id, + message_id: result.discriminant(), + }, + )+ + } + } + } + + impl MctpMessageTrait<'_> for HostResult { + const MESSAGE_TYPE: u8 = 0x7D; // ODP message type + type Header = OdpHeader; + + fn serialize(self, buffer: &mut [u8]) -> MctpPacketResult { + match self { + $( + HostResult::$service_name(result) => result + .serialize(buffer) + .map_err(|_| MctpPacketError::SerializeError(concat!("Failed to serialize ", stringify!($service_name), " result"))), + )+ + } + } + + fn deserialize(header: &Self::Header, buffer: &'_ [u8]) -> MctpPacketResult { + match header.service { + $( + OdpService::$service_name => { + match header.message_type { + OdpMessageType::Request => { + Err(MctpPacketError::CommandParseError(concat!("Received ", stringify!($service_name), " request when expecting result"))) + } + OdpMessageType::Result { is_error } => { + Ok(HostResult::$service_name(<$service_handler_type as $crate::relay::mctp::RelayServiceHandlerTypes>::ResultType::deserialize(is_error, header.message_id, buffer) + .map_err(|_| MctpPacketError::CommandParseError(concat!("Could not parse ", stringify!($service_name), " result")))?)) + } + } + }, + )+ + } + } + } + + + pub struct $relay_type_name { + $( + [<$service_name:snake _handler>]: $service_handler_type, + )+ + } + + impl $relay_type_name { + pub fn new( + $( + [<$service_name:snake _handler>]: $service_handler_type, + )+ + ) -> Self { + Self { + $( + [<$service_name:snake _handler>], + )+ + } + } + } + + impl $crate::relay::mctp::RelayHandler for $relay_type_name { + type ServiceIdType = OdpService; + type HeaderType = OdpHeader; + type RequestEnumType = HostRequest; + type ResultEnumType = HostResult; + + fn process_request<'a>( + &'a self, + message: HostRequest, + ) -> impl core::future::Future + 'a { + async move { + match message { + $( + HostRequest::$service_name(request) => { + let result = self.[<$service_name:snake _handler>].process_request(request).await; + HostResult::$service_name(result) + } + )+ + } + } + } + } + } // end mod __odp_impl + + // Allows this generated relay type to be publicly re-exported + pub use [< _odp_impl_ $relay_type_name:snake >]::$relay_type_name; + + } // end paste! + }; // end macro arm + } // end macro + + pub use impl_odp_mctp_relay_handler; +} diff --git a/embedded-service/src/type_c/comms.rs b/embedded-service/src/type_c/comms.rs deleted file mode 100644 index 9a998cd31..000000000 --- a/embedded-service/src/type_c/comms.rs +++ /dev/null @@ -1,33 +0,0 @@ -//! Comms service message definitions - -use embedded_usb_pd::GlobalPortId; - -/// Message generated when a debug acessory is connected or disconnected -#[derive(Copy, Clone, Debug)] -#[cfg_attr(feature = "defmt", derive(defmt::Format))] -pub struct DebugAccessoryMessage { - /// Port - pub port: GlobalPortId, - /// Connected - pub connected: bool, -} - -/// UCSI connector change message -#[derive(Copy, Clone, Debug)] -#[cfg_attr(feature = "defmt", derive(defmt::Format))] -pub struct UsciChangeIndicator { - /// Port - pub port: GlobalPortId, - /// Notify OPM - pub notify_opm: bool, -} - -/// Top-level comms message -#[derive(Copy, Clone, Debug)] -#[cfg_attr(feature = "defmt", derive(defmt::Format))] -pub enum CommsMessage { - /// Debug accessory message - DebugAccessory(DebugAccessoryMessage), - /// UCSI CCI message - UcsiCci(UsciChangeIndicator), -} diff --git a/embedded-service/src/type_c/controller.rs b/embedded-service/src/type_c/controller.rs deleted file mode 100644 index 9de50e2db..000000000 --- a/embedded-service/src/type_c/controller.rs +++ /dev/null @@ -1,1513 +0,0 @@ -//! PD controller related code -use core::future::Future; -use core::num::NonZeroU8; -use core::sync::atomic::{AtomicBool, Ordering}; - -use embassy_sync::signal::Signal; -use embassy_time::{Duration, with_timeout}; -use embedded_usb_pd::ucsi::{self, lpm}; -use embedded_usb_pd::{ - DataRole, Error, GlobalPortId, LocalPortId, PdError, PlugOrientation, PowerRole, - ado::Ado, - pdinfo::{AltMode, PowerPathStatus}, - type_c::ConnectionState, - vdm::structured::Svid, -}; -use heapless::Vec; - -use super::{ATTN_VDM_LEN, ControllerId, OTHER_VDM_LEN, external}; -use crate::ipc::deferred; -use crate::power::policy; -use crate::type_c::Cached; -use crate::type_c::comms::CommsMessage; -use crate::type_c::event::{PortEvent, PortPending}; -use crate::{GlobalRawMutex, IntrusiveNode, broadcaster::immediate as broadcaster, error, intrusive_list, trace}; - -/// maximum number of data objects in a VDM -pub const MAX_NUM_DATA_OBJECTS: usize = 7; // 7 VDOs of 4 bytes each - -/// Port status -#[derive(Copy, Clone, Debug)] -#[cfg_attr(feature = "defmt", derive(defmt::Format))] -pub struct PortStatus { - /// Current available source contract - pub available_source_contract: Option, - /// Current available sink contract - pub available_sink_contract: Option, - /// Current connection state - pub connection_state: Option, - /// Port partner supports dual-power roles - pub dual_power: bool, - /// plug orientation - pub plug_orientation: PlugOrientation, - /// power role - pub power_role: PowerRole, - /// data role - pub data_role: DataRole, - /// Active alt-modes - pub alt_mode: AltMode, - /// Power path status - pub power_path: PowerPathStatus, - /// EPR mode active - pub epr: bool, - /// Port partner is unconstrained - pub unconstrained_power: bool, -} - -impl PortStatus { - /// Create a new blank port status - /// Needed because default() is not const - pub const fn new() -> Self { - Self { - available_source_contract: None, - available_sink_contract: None, - connection_state: None, - dual_power: false, - plug_orientation: PlugOrientation::CC1, - power_role: PowerRole::Sink, - data_role: DataRole::Dfp, - alt_mode: AltMode::none(), - power_path: PowerPathStatus::none(), - epr: false, - unconstrained_power: false, - } - } - - /// Check if the port is connected - pub fn is_connected(&self) -> bool { - matches!( - self.connection_state, - Some(ConnectionState::Attached) - | Some(ConnectionState::DebugAccessory) - | Some(ConnectionState::AudioAccessory) - ) - } - - /// Check if a debug accessory is connected - pub fn is_debug_accessory(&self) -> bool { - matches!(self.connection_state, Some(ConnectionState::DebugAccessory)) - } -} - -impl Default for PortStatus { - fn default() -> Self { - Self::new() - } -} - -/// Other Vdm data -#[derive(Copy, Clone, Debug)] -#[cfg_attr(feature = "defmt", derive(defmt::Format))] -pub struct OtherVdm { - /// Other VDM data - pub data: [u8; OTHER_VDM_LEN], -} - -impl Default for OtherVdm { - fn default() -> Self { - Self { - data: [0; OTHER_VDM_LEN], - } - } -} - -impl From for [u8; OTHER_VDM_LEN] { - fn from(vdm: OtherVdm) -> Self { - vdm.data - } -} - -impl From<[u8; OTHER_VDM_LEN]> for OtherVdm { - fn from(data: [u8; OTHER_VDM_LEN]) -> Self { - Self { data } - } -} - -/// Attention Vdm data -#[derive(Copy, Clone, Debug)] -#[cfg_attr(feature = "defmt", derive(defmt::Format))] -pub struct AttnVdm { - /// Attention VDM data - pub data: [u8; ATTN_VDM_LEN], -} - -/// DisplayPort pin configuration -#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)] -#[cfg_attr(feature = "defmt", derive(defmt::Format))] -pub struct DpPinConfig { - /// 4L DP connection using USBC-USBC cable (Pin Assignment C) - pub pin_c: bool, - /// 2L USB + 2L DP connection using USBC-USBC cable (Pin Assignment D) - pub pin_d: bool, - /// 4L DP connection using USBC-DP cable (Pin Assignment E) - pub pin_e: bool, -} - -/// DisplayPort status data -#[derive(Copy, Clone, Debug)] -#[cfg_attr(feature = "defmt", derive(defmt::Format))] -pub struct DpStatus { - /// DP alt-mode entered - pub alt_mode_entered: bool, - /// Get DP DFP pin config - pub dfp_d_pin_cfg: DpPinConfig, -} - -/// DisplayPort configuration data -#[derive(Copy, Clone, Debug)] -#[cfg_attr(feature = "defmt", derive(defmt::Format))] -pub struct DpConfig { - /// DP alt-mode enabled - pub enable: bool, - /// Set DP DFP pin config - pub dfp_d_pin_cfg: DpPinConfig, -} - -impl Default for AttnVdm { - fn default() -> Self { - Self { - data: [0; ATTN_VDM_LEN], - } - } -} - -impl From for [u8; ATTN_VDM_LEN] { - fn from(vdm: AttnVdm) -> Self { - vdm.data - } -} - -impl From<[u8; ATTN_VDM_LEN]> for AttnVdm { - fn from(data: [u8; ATTN_VDM_LEN]) -> Self { - Self { data } - } -} - -/// Send VDM data -#[derive(Copy, Clone, Debug, PartialEq, Eq)] -#[cfg_attr(feature = "defmt", derive(defmt::Format))] -pub struct SendVdm { - /// initiating a VDM sequence - pub initiator: bool, - /// VDO count - pub vdo_count: u8, - /// VDO data - pub vdo_data: [u32; MAX_NUM_DATA_OBJECTS], -} - -impl SendVdm { - /// Create a new blank port status - pub const fn new() -> Self { - Self { - initiator: false, - vdo_count: 0, - vdo_data: [0; MAX_NUM_DATA_OBJECTS], - } - } -} - -impl Default for SendVdm { - fn default() -> Self { - Self::new() - } -} - -/// USB control configuration -#[cfg_attr(feature = "defmt", derive(defmt::Format))] -#[derive(Debug, Clone, Copy, PartialEq)] -pub struct UsbControlConfig { - /// Enable USB2 data path - pub usb2_enabled: bool, - /// Enable USB3 data path - pub usb3_enabled: bool, - /// Enable USB4 data path - pub usb4_enabled: bool, -} - -impl Default for UsbControlConfig { - fn default() -> Self { - Self { - usb2_enabled: true, - usb3_enabled: true, - usb4_enabled: true, - } - } -} - -/// Thunderbolt control configuration -#[cfg_attr(feature = "defmt", derive(defmt::Format))] -#[derive(Debug, Clone, Default, Copy, PartialEq)] -pub struct TbtConfig { - /// Enable Thunderbolt - pub tbt_enabled: bool, -} - -/// PD state-machine configuration -#[cfg_attr(feature = "defmt", derive(defmt::Format))] -#[derive(Debug, Clone, Default, Copy, PartialEq)] -pub struct PdStateMachineConfig { - /// Enable or disable the PD state-machine - pub enabled: bool, -} - -/// TypeC State Machine -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -#[cfg_attr(feature = "defmt", derive(defmt::Format))] -pub enum TypeCStateMachineState { - /// Sink state machine only - Sink, - /// Source state machine only - Source, - /// DRP state machine - Drp, - /// Disabled - Disabled, -} - -/// Response from the `Discover SVIDs REQ` message and the [`PortCommandData::GetDiscoveredSvids`] command. -// Could be changed to hold the heapless::Vec directly if they were Copy or if PortResponseData was not Copy -#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] -#[cfg_attr(feature = "defmt", derive(defmt::Format))] -pub struct DiscoveredSvids { - num_sop: usize, - sop: [Svid; Self::NUM_SVIDS], - - num_sop_prime: usize, - sop_prime: [Svid; Self::NUM_SVIDS], -} - -impl DiscoveredSvids { - /// The number of SVIDs that can be reported in a single [`PortResponseData::DiscoveredSvids`] response. - const NUM_SVIDS: usize = 8; - - /// Create a new response object from `sop` and `sop_prime`. - pub fn new(sop: Vec, sop_prime: Vec) -> Self { - let num_sop = sop.len(); - let num_sop_prime = sop_prime.len(); - - let mut sop_array = [Svid(0); _]; - for (svid, dest) in sop.into_iter().zip(sop_array.iter_mut()) { - *dest = svid; - } - - let mut sop_prime_array = [Svid(0); _]; - for (svid, dest) in sop_prime.into_iter().zip(sop_prime_array.iter_mut()) { - *dest = svid; - } - - Self { - num_sop, - sop: sop_array, - num_sop_prime, - sop_prime: sop_prime_array, - } - } - - /// Returns the number of SVIDs discovered on the SOP port partner. - pub fn number_sop_svids(&self) -> usize { - self.num_sop - } - - /// Returns an iterator over the SVIDs discovered on the SOP port partner. - pub fn svid_sop(&self) -> impl ExactSizeIterator { - self.sop.iter().copied().take(self.num_sop) - } - - /// Returns the number of SVIDs discovered on the SOP' cable plug. - pub fn number_sop_prime_svids(&self) -> usize { - self.num_sop_prime - } - - /// Returns an iterator over the SVIDs discovered on the SOP' cable plug. - pub fn svid_sop_prime(&self) -> impl ExactSizeIterator { - self.sop_prime.iter().copied().take(self.num_sop_prime) - } -} - -/// Port-specific command data -#[derive(Copy, Clone, Debug)] -#[cfg_attr(feature = "defmt", derive(defmt::Format))] -pub enum PortCommandData { - /// Get port status - PortStatus(Cached), - /// Get and clear events - ClearEvents, - /// Get retimer fw update state - RetimerFwUpdateGetState, - /// Set retimer fw update state - RetimerFwUpdateSetState, - /// Clear retimer fw update state - RetimerFwUpdateClearState, - /// Set retimer compliance - SetRetimerCompliance, - /// Reconfigure retimer - ReconfigureRetimer, - /// Get oldest unhandled PD alert - GetPdAlert, - /// Set the maximum sink voltage in mV for the given port - SetMaxSinkVoltage(Option), - /// Set unconstrained power - SetUnconstrainedPower(bool), - /// Clear the dead battery flag for the given port - ClearDeadBatteryFlag, - /// Get other VDM - GetOtherVdm, - /// Get attention VDM - GetAttnVdm, - /// Send VDM - SendVdm(SendVdm), - /// Set USB control configuration - SetUsbControl(UsbControlConfig), - /// Get DisplayPort status - GetDpStatus, - /// Set DisplayPort configuration - SetDpConfig(DpConfig), - /// Execute DisplayPort reset - ExecuteDrst, - /// Set Thunderbolt configuration - SetTbtConfig(TbtConfig), - /// Set PD state-machine configuration - SetPdStateMachineConfig(PdStateMachineConfig), - /// Set Type-C state-machine configuration - SetTypeCStateMachineConfig(TypeCStateMachineState), - /// Execute the UCSI command - ExecuteUcsiCommand(lpm::CommandData), - /// Execute electrical disconnect - ExecuteElectricalDisconnect { - /// The time, in seconds, after which the port should automatically reconnect. - /// - /// If [`None`], the port will not automatically reconnect. - reconnect_time_s: Option, - }, - /// Set the system power state - SetSystemPowerState(SystemPowerState), - /// Get the port's discovered SVIDs - GetDiscoveredSvids, - /// Trigger a hard reset on the given port. - HardReset, - /// Get the response to a Discover Identity command sent to the given port with SOP - GetDiscoverIdentitySop, - /// Get the response to a Discover Identity command sent to the given port with SOP' - GetDiscoverIdentitySopPrime, -} - -/// Port-specific commands -#[derive(Copy, Clone, Debug)] -#[cfg_attr(feature = "defmt", derive(defmt::Format))] -pub struct PortCommand { - /// Port ID - pub port: GlobalPortId, - /// Command data - pub data: PortCommandData, -} - -/// PD controller command-specific data -#[derive(Copy, Clone, Debug, PartialEq)] -#[cfg_attr(feature = "defmt", derive(defmt::Format))] -pub enum RetimerFwUpdateState { - /// Retimer FW Update Inactive - Inactive, - /// Revimer FW Update Active - Active, -} - -/// Port-specific response data -#[derive(Copy, Clone, Debug)] -#[cfg_attr(feature = "defmt", derive(defmt::Format))] -pub enum PortResponseData { - /// Command completed with no error - Complete, - /// Port status - PortStatus(PortStatus), - /// ClearEvents - ClearEvents(PortEvent), - /// Retimer Fw Update status - RtFwUpdateStatus(RetimerFwUpdateState), - /// PD alert - PdAlert(Option), - /// Get other VDM - OtherVdm(OtherVdm), - /// Get attention VDM - AttnVdm(AttnVdm), - /// Get DisplayPort status - DpStatus(DpStatus), - /// UCSI response - UcsiResponse(Result, PdError>), - /// Discovered SVIDs - DiscoveredSvids(DiscoveredSvids), - /// Discover Identity SOP response - DiscoverIdentitySop(embedded_usb_pd::vdm::structured::command::discover_identity::sop::ResponseVdos), - /// Discover Identity SOP' response - DiscoverIdentitySopPrime(embedded_usb_pd::vdm::structured::command::discover_identity::sop_prime::ResponseVdos), -} - -impl PortResponseData { - /// Helper function to convert to a result - pub fn complete_or_err(self) -> Result<(), PdError> { - match self { - PortResponseData::Complete => Ok(()), - _ => Err(PdError::InvalidResponse), - } - } -} - -/// Port-specific command response -pub type PortResponse = Result; - -/// System power state for Sx App Config register. -/// -/// Used to notify the PD controller of the current system power state, -/// which triggers Application Configuration updates (e.g., crossbar reconfiguration). -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -#[cfg_attr(feature = "defmt", derive(defmt::Format))] -pub enum SystemPowerState { - /// S0 - System fully running - S0, - /// S3 - Suspend to RAM - S3, - /// S4 - Hibernate - S4, - /// S5 - Soft off - S5, - /// S0ix - Modern standby / Connected standby - S0ix, -} - -/// PD controller command-specific data -#[derive(Copy, Clone, Debug)] -#[cfg_attr(feature = "defmt", derive(defmt::Format))] -pub enum InternalCommandData { - /// Reset the PD controller - Reset, - /// Get controller status - Status, - /// Sync controller state - SyncState, -} - -/// PD controller command -#[derive(Copy, Clone, Debug)] -#[cfg_attr(feature = "defmt", derive(defmt::Format))] -pub enum Command { - /// Controller specific command - Controller(InternalCommandData), - /// Port command - Port(PortCommand), - /// UCSI command passthrough - Lpm(lpm::GlobalCommand), -} - -/// Controller-specific response data -#[derive(Copy, Clone, Debug)] -#[cfg_attr(feature = "defmt", derive(defmt::Format))] -pub enum InternalResponseData<'a> { - /// Command complete - Complete, - /// Controller status - Status(ControllerStatus<'a>), -} - -/// Response for controller-specific commands -pub type InternalResponse<'a> = Result, PdError>; - -/// PD controller command response -#[derive(Copy, Clone, Debug)] -#[cfg_attr(feature = "defmt", derive(defmt::Format))] -pub enum Response<'a> { - /// Controller response - Controller(InternalResponse<'a>), - /// UCSI response passthrough - Ucsi(ucsi::GlobalResponse), - /// Port response - Port(PortResponse), -} - -/// Controller status -#[derive(Copy, Clone, Debug)] -#[cfg_attr(feature = "defmt", derive(defmt::Format))] -pub struct ControllerStatus<'a> { - /// Current controller mode - pub mode: &'a str, - /// True if we did not have to boot from a backup FW bank - pub valid_fw_bank: bool, - /// FW version 0 - pub fw_version0: u32, - /// FW version 1 - pub fw_version1: u32, -} - -/// PD controller -pub struct Device<'a> { - node: intrusive_list::Node, - id: ControllerId, - ports: &'a [GlobalPortId], - num_ports: usize, - command: deferred::Channel>, -} - -impl intrusive_list::NodeContainer for Device<'static> { - fn get_node(&self) -> &intrusive_list::Node { - &self.node - } -} - -impl<'a> Device<'a> { - /// Create a new PD controller struct - pub fn new(id: ControllerId, ports: &'a [GlobalPortId]) -> Self { - Self { - node: intrusive_list::Node::uninit(), - id, - ports, - num_ports: ports.len(), - command: deferred::Channel::new(), - } - } - - /// Get the controller ID - pub fn id(&self) -> ControllerId { - self.id - } - - /// Send a command to this controller - pub async fn execute_command(&self, command: Command) -> Response<'_> { - self.command.execute(command).await - } - - /// Check if this controller has the given port - pub fn has_port(&self, port: GlobalPortId) -> bool { - self.lookup_local_port(port).is_ok() - } - - /// Covert a local port ID to a global port ID - pub fn lookup_global_port(&self, port: LocalPortId) -> Result { - Ok(*self.ports.get(port.0 as usize).ok_or(PdError::InvalidParams)?) - } - - /// Convert a global port ID to a local port ID - pub fn lookup_local_port(&self, port: GlobalPortId) -> Result { - self.ports - .iter() - .position(|p| *p == port) - .map(|p| LocalPortId(p as u8)) - .ok_or(PdError::InvalidParams) - } - - /// Create a command handler for this controller - /// - /// DROP SAFETY: Direct call to deferred channel primitive - pub async fn receive(&self) -> deferred::Request<'_, GlobalRawMutex, Command, Response<'static>> { - self.command.receive().await - } - - /// Notify that there are pending events on one or more ports - pub fn notify_ports(&self, pending: PortPending) { - CONTEXT.notify_ports(pending); - } - - /// Number of ports on this controller - pub fn num_ports(&self) -> usize { - self.num_ports - } -} - -/// Trait for types that contain a controller struct -pub trait DeviceContainer { - /// Get the controller struct - fn get_pd_controller_device(&self) -> &Device<'_>; -} - -impl DeviceContainer for Device<'_> { - fn get_pd_controller_device(&self) -> &Device<'_> { - self - } -} - -/// PD controller trait that device drivers may use to integrate with internal messaging system -pub trait Controller { - /// Type of error returned by the bus - type BusError; - - /// Wait for a port event to occur - /// # Implementation guide - /// This function should be drop safe. - /// Any intermediate side effects must be undone if the returned [`Future`] is dropped before completing. - fn wait_port_event(&mut self) -> impl Future>>; - /// Returns and clears current events for the given port - /// # Implementation guide - /// This function should be drop safe. - /// Any intermediate side effects must be undone if the returned [`Future`] is dropped before completing. - fn clear_port_events( - &mut self, - port: LocalPortId, - ) -> impl Future>>; - /// Returns the port status - fn get_port_status(&mut self, port: LocalPortId) - -> impl Future>>; - - /// Reset the controller - fn reset_controller(&mut self) -> impl Future>>; - - /// Returns the retimer fw update state - fn get_rt_fw_update_status( - &mut self, - port: LocalPortId, - ) -> impl Future>>; - /// Set retimer fw update state - fn set_rt_fw_update_state(&mut self, port: LocalPortId) -> impl Future>>; - /// Clear retimer fw update state - fn clear_rt_fw_update_state( - &mut self, - port: LocalPortId, - ) -> impl Future>>; - /// Set retimer compliance - fn set_rt_compliance(&mut self, port: LocalPortId) -> impl Future>>; - - /// Reconfigure the retimer for the given port. - fn reconfigure_retimer(&mut self, port: LocalPortId) -> impl Future>>; - - /// Clear the dead battery flag for the given port. - fn clear_dead_battery_flag(&mut self, port: LocalPortId) - -> impl Future>>; - - /// Enable or disable sink path - fn enable_sink_path( - &mut self, - port: LocalPortId, - enable: bool, - ) -> impl Future>>; - /// Get current controller status - fn get_controller_status( - &mut self, - ) -> impl Future, Error>>; - /// Get current PD alert - fn get_pd_alert(&mut self, port: LocalPortId) -> impl Future, Error>>; - /// Set the maximum sink voltage for the given port - /// - /// This may trigger a renegotiation - fn set_max_sink_voltage( - &mut self, - port: LocalPortId, - voltage_mv: Option, - ) -> impl Future>>; - /// Set port unconstrained status - fn set_unconstrained_power( - &mut self, - port: LocalPortId, - unconstrained: bool, - ) -> impl Future>>; - - // TODO: remove all these once we migrate to a generic FW update trait - // https://github.com/OpenDevicePartnership/embedded-services/issues/242 - /// Get current FW version - fn get_active_fw_version(&mut self) -> impl Future>>; - /// Start a firmware update - fn start_fw_update(&mut self) -> impl Future>>; - /// Abort a firmware update - fn abort_fw_update(&mut self) -> impl Future>>; - /// Finalize a firmware update - fn finalize_fw_update(&mut self) -> impl Future>>; - /// Write firmware update contents - fn write_fw_contents( - &mut self, - offset: usize, - data: &[u8], - ) -> impl Future>>; - /// Get the Rx Other VDM data for the given port - fn get_other_vdm(&mut self, port: LocalPortId) -> impl Future>>; - /// Get the Rx Attention VDM data for the given port - fn get_attn_vdm(&mut self, port: LocalPortId) -> impl Future>>; - /// Send a VDM to the given port - fn send_vdm( - &mut self, - port: LocalPortId, - tx_vdm: SendVdm, - ) -> impl Future>>; - - /// Set USB control configuration for the given port - fn set_usb_control( - &mut self, - port: LocalPortId, - config: UsbControlConfig, - ) -> impl Future>>; - - /// Get DisplayPort status for the given port - fn get_dp_status(&mut self, port: LocalPortId) -> impl Future>>; - /// Set DisplayPort configuration for the given port - fn set_dp_config( - &mut self, - port: LocalPortId, - config: DpConfig, - ) -> impl Future>>; - /// Execute PD Data Reset for the given port - fn execute_drst(&mut self, port: LocalPortId) -> impl Future>>; - - /// Set Thunderbolt configuration for the given port - fn set_tbt_config( - &mut self, - port: LocalPortId, - config: TbtConfig, - ) -> impl Future>>; - - /// Set PD state-machine configuration for the given port - fn set_pd_state_machine_config( - &mut self, - port: LocalPortId, - config: PdStateMachineConfig, - ) -> impl Future>>; - - /// Set Type-C state-machine configuration for the given port - fn set_type_c_state_machine_config( - &mut self, - port: LocalPortId, - state: TypeCStateMachineState, - ) -> impl Future>>; - - /// Execute the given UCSI command - fn execute_ucsi_command( - &mut self, - command: lpm::LocalCommand, - ) -> impl Future, Error>>; - - /// Execute an electrical disconnect on the given port, if supported by the controller. - /// - /// If `reconnect_time_s` is provided, the controller should automatically reconnect the port after the specified time - /// has elapsed. If `reconnect_time_s` is [`None`], the port should remain disconnected until manually reconnected. - fn execute_electrical_disconnect( - &mut self, - port: LocalPortId, - reconnect_time_s: Option, - ) -> impl Future>>; - - /// Set the system power state on the given port. - /// - /// This notifies the PD controller of the current system power state, - /// which triggers Application Configuration updates (e.g., crossbar reconfiguration). - fn set_power_state( - &mut self, - port: LocalPortId, - state: SystemPowerState, - ) -> impl Future>>; - - /// Get the discovered SVIDs for the given port. - fn get_discovered_svids( - &mut self, - port: LocalPortId, - ) -> impl Future>>; - - /// Trigger a hard reset on the given port. - fn hard_reset(&mut self, port: LocalPortId) -> impl Future>>; - - /// Get the latest response from the Discover Identity command targeting SOP. - fn get_discover_identity_sop_response( - &mut self, - port: LocalPortId, - ) -> impl Future< - Output = Result< - embedded_usb_pd::vdm::structured::command::discover_identity::sop::ResponseVdos, - Error, - >, - >; - - /// Get the latest response from the Discover Identity command targeting SOP'. - fn get_discover_identity_sop_prime_response( - &mut self, - port: LocalPortId, - ) -> impl Future< - Output = Result< - embedded_usb_pd::vdm::structured::command::discover_identity::sop_prime::ResponseVdos, - Error, - >, - >; -} - -/// Internal context for managing PD controllers -struct Context { - controllers: intrusive_list::IntrusiveList, - port_events: Signal, - /// Channel for receiving commands to the type-C service - external_command: deferred::Channel>, - /// Event broadcaster - broadcaster: broadcaster::Immediate, -} - -impl Context { - const fn new() -> Self { - Self { - controllers: intrusive_list::IntrusiveList::new(), - port_events: Signal::new(), - external_command: deferred::Channel::new(), - broadcaster: broadcaster::Immediate::new(), - } - } - - /// Notify that there are pending events on one or more ports - /// Each bit corresponds to a global port ID - fn notify_ports(&self, pending: PortPending) { - let raw_pending: u32 = pending.into(); - trace!("Notify ports: {:#x}", raw_pending); - // Early exit if no events - if pending.is_none() { - return; - } - - self.port_events - .signal(if let Some(flags) = self.port_events.try_take() { - flags.union(pending) - } else { - pending - }); - } -} - -static CONTEXT: Context = Context::new(); - -/// Initialize the PD controller context -pub fn init() {} - -/// Register a PD controller -pub fn register_controller(controller: &'static impl DeviceContainer) -> Result<(), intrusive_list::Error> { - CONTEXT.controllers.push(controller.get_pd_controller_device()) -} - -pub(super) async fn lookup_controller(controller_id: ControllerId) -> Result<&'static Device<'static>, PdError> { - CONTEXT - .controllers - .into_iter() - .filter_map(|node| node.data::()) - .find(|controller| controller.id == controller_id) - .ok_or(PdError::InvalidController) -} - -/// Lookup the controller and local port ID for a given global port ID. -pub(super) async fn lookup_global_port( - port_id: GlobalPortId, -) -> Result<(&'static Device<'static>, LocalPortId), PdError> { - for controller in CONTEXT.controllers.iter_only::() { - if let Ok(local_port) = controller.lookup_local_port(port_id) { - return Ok((controller, local_port)); - } - } - - Err(PdError::InvalidPort) -} - -/// Get total number of ports on the system -pub(super) fn get_num_ports() -> usize { - CONTEXT - .controllers - .iter_only::() - .fold(0, |acc, controller| acc + controller.num_ports()) -} - -/// Register a message receiver for type-C messages -pub fn register_message_receiver( - receiver: &'static broadcaster::Receiver<'_, CommsMessage>, -) -> intrusive_list::Result<()> { - CONTEXT.broadcaster.register_receiver(receiver) -} - -/// Default command timeout -/// set to high value since this is intended to prevent an unresponsive device from blocking the service implementation -const DEFAULT_TIMEOUT: Duration = Duration::from_millis(5000); - -/// Type to provide access to the PD controller context for service implementations -pub struct ContextToken(()); - -impl ContextToken { - /// Create a new context token, returning None if this function has been called before - pub fn create() -> Option { - static INIT: AtomicBool = AtomicBool::new(false); - if INIT.load(Ordering::SeqCst) { - return None; - } - - INIT.store(true, Ordering::SeqCst); - Some(ContextToken(())) - } - - /// Send a command to the given controller with no timeout - pub async fn send_controller_command_no_timeout( - &self, - controller_id: ControllerId, - command: InternalCommandData, - ) -> Result, PdError> { - let node = CONTEXT - .controllers - .into_iter() - .find(|node| { - if let Some(controller) = node.data::() { - controller.id == controller_id - } else { - false - } - }) - .ok_or(PdError::InvalidController)?; - - match node - .data::() - .ok_or(PdError::InvalidController)? - .execute_command(Command::Controller(command)) - .await - { - Response::Controller(response) => response, - r => { - error!("Invalid response: expected controller, got {:?}", r); - Err(PdError::InvalidResponse) - } - } - } - - /// Send a command to the given controller with a timeout - pub async fn send_controller_command( - &self, - controller_id: ControllerId, - command: InternalCommandData, - ) -> Result, PdError> { - match with_timeout( - DEFAULT_TIMEOUT, - self.send_controller_command_no_timeout(controller_id, command), - ) - .await - { - Ok(response) => response, - Err(_) => Err(PdError::Timeout), - } - } - - /// Reset the given controller - pub async fn reset_controller(&self, controller_id: ControllerId) -> Result<(), PdError> { - self.send_controller_command(controller_id, InternalCommandData::Reset) - .await - .map(|_| ()) - } - - fn find_node_by_port(&self, port_id: GlobalPortId) -> Result<&IntrusiveNode, PdError> { - CONTEXT - .controllers - .into_iter() - .find(|node| { - if let Some(controller) = node.data::() { - controller.has_port(port_id) - } else { - false - } - }) - .ok_or(PdError::InvalidPort) - } - - /// Send a command to the given port - pub async fn send_port_command_ucsi_no_timeout( - &self, - port_id: GlobalPortId, - command: lpm::CommandData, - ) -> Result { - let node = self.find_node_by_port(port_id)?; - - match node - .data::() - .ok_or(PdError::InvalidController)? - .execute_command(Command::Lpm(lpm::Command::new(port_id, command))) - .await - { - Response::Ucsi(response) => Ok(response), - r => { - error!("Invalid response: expected LPM, got {:?}", r); - Err(PdError::InvalidResponse) - } - } - } - - /// Send a command to the given port with a timeout - pub async fn send_port_command_ucsi( - &self, - port_id: GlobalPortId, - command: lpm::CommandData, - ) -> Result { - match with_timeout( - DEFAULT_TIMEOUT, - self.send_port_command_ucsi_no_timeout(port_id, command), - ) - .await - { - Ok(response) => response, - Err(_) => Err(PdError::Timeout), - } - } - - /// Send a command to the given port with no timeout - pub async fn send_port_command_no_timeout( - &self, - port_id: GlobalPortId, - command: PortCommandData, - ) -> Result { - let node = self.find_node_by_port(port_id)?; - - match node - .data::() - .ok_or(PdError::InvalidController)? - .execute_command(Command::Port(PortCommand { - port: port_id, - data: command, - })) - .await - { - Response::Port(response) => response, - r => { - error!("Invalid response: expected port, got {:?}", r); - Err(PdError::InvalidResponse) - } - } - } - - /// Send a command to the given port with a timeout - pub async fn send_port_command( - &self, - port_id: GlobalPortId, - command: PortCommandData, - ) -> Result { - match with_timeout(DEFAULT_TIMEOUT, self.send_port_command_no_timeout(port_id, command)).await { - Ok(response) => response, - Err(_) => Err(PdError::Timeout), - } - } - - /// Get the current port events - pub async fn get_unhandled_events(&self) -> PortPending { - CONTEXT.port_events.wait().await - } - - /// Get the unhandled events for the given port - pub async fn get_port_event(&self, port: GlobalPortId) -> Result { - match self.send_port_command(port, PortCommandData::ClearEvents).await? { - PortResponseData::ClearEvents(event) => Ok(event), - r => { - error!("Invalid response: expected clear events, got {:?}", r); - Err(PdError::InvalidResponse) - } - } - } - - /// Get the current port status - pub async fn get_port_status(&self, port: GlobalPortId, cached: Cached) -> Result { - match self - .send_port_command(port, PortCommandData::PortStatus(cached)) - .await? - { - PortResponseData::PortStatus(status) => Ok(status), - r => { - error!("Invalid response: expected port status, got {:?}", r); - Err(PdError::InvalidResponse) - } - } - } - - /// Get the oldest unhandled PD alert for the given port - pub async fn get_pd_alert(&self, port: GlobalPortId) -> Result, PdError> { - match self.send_port_command(port, PortCommandData::GetPdAlert).await? { - PortResponseData::PdAlert(alert) => Ok(alert), - r => { - error!("Invalid response: expected PD alert, got {:?}", r); - Err(PdError::InvalidResponse) - } - } - } - - /// Get the retimer fw update status - pub async fn get_rt_fw_update_status(&self, port: GlobalPortId) -> Result { - match self - .send_port_command(port, PortCommandData::RetimerFwUpdateGetState) - .await? - { - PortResponseData::RtFwUpdateStatus(status) => Ok(status), - _ => Err(PdError::InvalidResponse), - } - } - - /// Set the retimer fw update state - pub async fn set_rt_fw_update_state(&self, port: GlobalPortId) -> Result<(), PdError> { - match self - .send_port_command(port, PortCommandData::RetimerFwUpdateSetState) - .await? - { - PortResponseData::Complete => Ok(()), - _ => Err(PdError::InvalidResponse), - } - } - - /// Clear the retimer fw update state - pub async fn clear_rt_fw_update_state(&self, port: GlobalPortId) -> Result<(), PdError> { - match self - .send_port_command(port, PortCommandData::RetimerFwUpdateClearState) - .await? - { - PortResponseData::Complete => Ok(()), - _ => Err(PdError::InvalidResponse), - } - } - - /// Set the retimer compliance - pub async fn set_rt_compliance(&self, port: GlobalPortId) -> Result<(), PdError> { - match self - .send_port_command(port, PortCommandData::SetRetimerCompliance) - .await? - { - PortResponseData::Complete => Ok(()), - _ => Err(PdError::InvalidResponse), - } - } - - /// Reconfigure the retimer for the given port. - pub async fn reconfigure_retimer(&self, port: GlobalPortId) -> Result<(), PdError> { - match self - .send_port_command(port, PortCommandData::ReconfigureRetimer) - .await? - { - PortResponseData::Complete => Ok(()), - _ => Err(PdError::InvalidResponse), - } - } - - /// Set the maximum sink voltage for the given port. - /// - /// See [`PortCommandData::SetMaxSinkVoltage`] for details on the `max_voltage_mv` parameter. - pub async fn set_max_sink_voltage(&self, port: GlobalPortId, max_voltage_mv: Option) -> Result<(), PdError> { - match self - .send_port_command(port, PortCommandData::SetMaxSinkVoltage(max_voltage_mv)) - .await? - { - PortResponseData::Complete => Ok(()), - _ => Err(PdError::InvalidResponse), - } - } - - /// Clear the dead battery flag for the given port. - pub async fn clear_dead_battery_flag(&self, port: GlobalPortId) -> Result<(), PdError> { - match self - .send_port_command(port, PortCommandData::ClearDeadBatteryFlag) - .await? - { - PortResponseData::Complete => Ok(()), - _ => Err(PdError::InvalidResponse), - } - } - - /// Get current controller status - pub async fn get_controller_status( - &self, - controller_id: ControllerId, - ) -> Result, PdError> { - match self - .send_controller_command(controller_id, InternalCommandData::Status) - .await? - { - InternalResponseData::Status(status) => Ok(status), - r => { - error!("Invalid response: expected controller status, got {:?}", r); - Err(PdError::InvalidResponse) - } - } - } - - /// Set unconstrained power for the given port - pub async fn set_unconstrained_power(&self, port: GlobalPortId, unconstrained: bool) -> Result<(), PdError> { - match self - .send_port_command(port, PortCommandData::SetUnconstrainedPower(unconstrained)) - .await? - { - PortResponseData::Complete => Ok(()), - _ => Err(PdError::InvalidResponse), - } - } - - /// Sync controller state - pub async fn sync_controller_state(&self, controller_id: ControllerId) -> Result<(), PdError> { - match self - .send_controller_command(controller_id, InternalCommandData::SyncState) - .await? - { - InternalResponseData::Complete => Ok(()), - r => { - error!("Invalid response: expected controller status, got {:?}", r); - Err(PdError::InvalidResponse) - } - } - } - - /// Wait for an external command - pub async fn wait_external_command( - &self, - ) -> deferred::Request<'_, GlobalRawMutex, external::Command, external::Response<'static>> { - CONTEXT.external_command.receive().await - } - - /// Notify that there are pending events on one or more ports - pub fn notify_ports(&self, pending: PortPending) { - CONTEXT.notify_ports(pending); - } - - /// Get the number of ports on the system - pub fn get_num_ports(&self) -> usize { - get_num_ports() - } - - /// Get the other vdm for the given port - pub async fn get_other_vdm(&self, port: GlobalPortId) -> Result { - match self.send_port_command(port, PortCommandData::GetOtherVdm).await? { - PortResponseData::OtherVdm(vdm) => Ok(vdm), - r => { - error!("Invalid response: expected other VDM, got {:?}", r); - Err(PdError::InvalidResponse) - } - } - } - - /// Get the attention vdm for the given port - pub async fn get_attn_vdm(&self, port: GlobalPortId) -> Result { - match self.send_port_command(port, PortCommandData::GetAttnVdm).await? { - PortResponseData::AttnVdm(vdm) => Ok(vdm), - r => { - error!("Invalid response: expected attention VDM, got {:?}", r); - Err(PdError::InvalidResponse) - } - } - } - - /// Send VDM to the given port - pub async fn send_vdm(&self, port: GlobalPortId, tx_vdm: SendVdm) -> Result<(), PdError> { - match self.send_port_command(port, PortCommandData::SendVdm(tx_vdm)).await? { - PortResponseData::Complete => Ok(()), - _ => Err(PdError::InvalidResponse), - } - } - - /// Set USB control configuration for the given port - pub async fn set_usb_control(&self, port: GlobalPortId, config: UsbControlConfig) -> Result<(), PdError> { - match self - .send_port_command(port, PortCommandData::SetUsbControl(config)) - .await? - { - PortResponseData::Complete => Ok(()), - _ => Err(PdError::InvalidResponse), - } - } - - /// Get DisplayPort status for the given port - pub async fn get_dp_status(&self, port: GlobalPortId) -> Result { - match self.send_port_command(port, PortCommandData::GetDpStatus).await? { - PortResponseData::DpStatus(status) => Ok(status), - r => { - error!("Invalid response: expected DP status, got {:?}", r); - Err(PdError::InvalidResponse) - } - } - } - - /// Set DisplayPort configuration for the given port - pub async fn set_dp_config(&self, port: GlobalPortId, config: DpConfig) -> Result<(), PdError> { - match self - .send_port_command(port, PortCommandData::SetDpConfig(config)) - .await? - { - PortResponseData::Complete => Ok(()), - _ => Err(PdError::InvalidResponse), - } - } - - /// Execute PD Data Reset for the given port - pub async fn execute_drst(&self, port: GlobalPortId) -> Result<(), PdError> { - match self.send_port_command(port, PortCommandData::ExecuteDrst).await? { - PortResponseData::Complete => Ok(()), - _ => Err(PdError::InvalidResponse), - } - } - - /// Set Thunderbolt configuration for the given port - pub async fn set_tbt_config(&self, port: GlobalPortId, config: TbtConfig) -> Result<(), PdError> { - match self - .send_port_command(port, PortCommandData::SetTbtConfig(config)) - .await? - { - PortResponseData::Complete => Ok(()), - _ => Err(PdError::InvalidResponse), - } - } - - /// Set PD state-machine configuration for the given port - pub async fn set_pd_state_machine_config( - &self, - port: GlobalPortId, - config: PdStateMachineConfig, - ) -> Result<(), PdError> { - match self - .send_port_command(port, PortCommandData::SetPdStateMachineConfig(config)) - .await? - { - PortResponseData::Complete => Ok(()), - _ => Err(PdError::InvalidResponse), - } - } - - /// Set Type-C state-machine configuration for the given port - pub async fn set_type_c_state_machine_config( - &self, - port: GlobalPortId, - state: TypeCStateMachineState, - ) -> Result<(), PdError> { - match self - .send_port_command(port, PortCommandData::SetTypeCStateMachineConfig(state)) - .await? - { - PortResponseData::Complete => Ok(()), - _ => Err(PdError::InvalidResponse), - } - } - - /// Execute the given UCSI command - pub async fn execute_ucsi_command( - &self, - command: lpm::GlobalCommand, - ) -> Result, PdError> { - match self - .send_port_command(command.port(), PortCommandData::ExecuteUcsiCommand(command.operation())) - .await? - { - PortResponseData::UcsiResponse(response) => response, - _ => Err(PdError::InvalidResponse), - } - } - - /// Execute an electrical disconnect on the given port. - pub async fn execute_electrical_disconnect( - &self, - port: GlobalPortId, - reconnect_time_s: Option, - ) -> Result<(), PdError> { - match self - .send_port_command(port, PortCommandData::ExecuteElectricalDisconnect { reconnect_time_s }) - .await? - { - PortResponseData::Complete => Ok(()), - _ => Err(PdError::InvalidResponse), - } - } - - /// Set the system power state on the given port. - /// - /// This notifies the PD controller of the current system power state, - /// which triggers Application Configuration updates (e.g., crossbar reconfiguration). - pub async fn set_power_state(&self, port: GlobalPortId, state: SystemPowerState) -> Result<(), PdError> { - match self - .send_port_command(port, PortCommandData::SetSystemPowerState(state)) - .await? - { - PortResponseData::Complete => Ok(()), - _ => Err(PdError::InvalidResponse), - } - } - - /// Get the discovered SVIDs for the given port. - pub async fn get_discovered_svids(&self, port: GlobalPortId) -> Result { - match self - .send_port_command(port, PortCommandData::GetDiscoveredSvids) - .await? - { - PortResponseData::DiscoveredSvids(svids) => Ok(svids), - r => { - error!("Invalid response: expected discovered SVIDs, got {:?}", r); - Err(PdError::InvalidResponse) - } - } - } - - /// Trigger a hard reset on the given port. - pub async fn hard_reset(&self, port: GlobalPortId) -> Result<(), PdError> { - match self.send_port_command(port, PortCommandData::HardReset).await? { - PortResponseData::Complete => Ok(()), - _ => Err(PdError::InvalidResponse), - } - } - - /// Get the latest response from the Discover Identity command targeting SOP. - pub async fn get_discover_identity_sop_response( - &self, - port: GlobalPortId, - ) -> Result { - match self - .send_port_command(port, PortCommandData::GetDiscoverIdentitySop) - .await? - { - PortResponseData::DiscoverIdentitySop(response) => Ok(response), - r => { - error!("Invalid response: expected Discover Identity SOP response, got {:?}", r); - Err(PdError::InvalidResponse) - } - } - } - - /// Get the latest response from the Discover Identity command targeting SOP'. - pub async fn get_discover_identity_sop_prime_response( - &self, - port: GlobalPortId, - ) -> Result { - match self - .send_port_command(port, PortCommandData::GetDiscoverIdentitySopPrime) - .await? - { - PortResponseData::DiscoverIdentitySopPrime(response) => Ok(response), - r => { - error!( - "Invalid response: expected Discover Identity SOP' response, got {:?}", - r - ); - Err(PdError::InvalidResponse) - } - } - } - - /// Broadcast a type-C message to all subscribers - pub async fn broadcast_message(&self, message: CommsMessage) { - CONTEXT.broadcaster.broadcast(message).await; - } -} - -/// Execute an external port command -pub(super) async fn execute_external_port_command( - command: external::Command, -) -> Result { - match CONTEXT.external_command.execute(command).await { - external::Response::Port(response) => response, - r => { - error!("Invalid response: expected external port, got {:?}", r); - Err(PdError::InvalidResponse) - } - } -} - -/// Execute an external controller command -pub(super) async fn execute_external_controller_command( - command: external::Command, -) -> Result, PdError> { - match CONTEXT.external_command.execute(command).await { - external::Response::Controller(response) => response, - r => { - error!("Invalid response: expected external controller, got {:?}", r); - Err(PdError::InvalidResponse) - } - } -} - -/// Execute an external UCSI command -pub(super) async fn execute_external_ucsi_command(command: ucsi::GlobalCommand) -> external::UcsiResponse { - match CONTEXT.external_command.execute(external::Command::Ucsi(command)).await { - external::Response::Ucsi(response) => response, - r => { - error!("Invalid response: expected external UCSI, got {:?}", r); - external::UcsiResponse { - // Always notify OPM of an error - notify_opm: true, - cci: ucsi::cci::GlobalCci::new_error(), - data: Err(PdError::InvalidResponse), - } - } - } -} diff --git a/embedded-service/src/type_c/external.rs b/embedded-service/src/type_c/external.rs deleted file mode 100644 index b86650d1f..000000000 --- a/embedded-service/src/type_c/external.rs +++ /dev/null @@ -1,579 +0,0 @@ -//! Message definitions for external type-C commands -use core::num::NonZeroU8; - -use embedded_usb_pd::{GlobalPortId, LocalPortId, PdError, ucsi}; - -use crate::type_c::{ - Cached, - controller::{ - DiscoveredSvids, PdStateMachineConfig, SystemPowerState, TbtConfig, TypeCStateMachineState, UsbControlConfig, - execute_external_ucsi_command, - }, -}; - -use super::{ - ControllerId, - controller::{ - ControllerStatus, DpConfig, DpStatus, PortStatus, RetimerFwUpdateState, SendVdm, - execute_external_controller_command, execute_external_port_command, lookup_controller, lookup_global_port, - }, -}; - -/// Data for controller-specific commands -#[derive(Copy, Clone, Debug)] -#[cfg_attr(feature = "defmt", derive(defmt::Format))] -pub enum ControllerCommandData { - /// Get controller status - ControllerStatus, - /// Sync controller state - SyncState, - /// Controller reset - Reset, -} - -/// Controller-specific commands -#[derive(Copy, Clone, Debug)] -#[cfg_attr(feature = "defmt", derive(defmt::Format))] -pub struct ControllerCommand { - /// Controller ID - pub id: ControllerId, - /// Command data - pub data: ControllerCommandData, -} - -/// Response data for controller-specific commands -#[derive(Copy, Clone, Debug)] -#[cfg_attr(feature = "defmt", derive(defmt::Format))] -pub enum ControllerResponseData<'a> { - /// Command complete - Complete, - /// Get controller status - ControllerStatus(ControllerStatus<'a>), -} - -/// Controller-specific command response -pub type ControllerResponse<'a> = Result, PdError>; - -/// Data for port-specific commands -#[derive(Copy, Clone, Debug)] -#[cfg_attr(feature = "defmt", derive(defmt::Format))] -pub enum PortCommandData { - /// Get port status. The `bool` argument indicates whether to use cached data or force a fetch of register values. - PortStatus(Cached), - /// Get retimer fw update status - RetimerFwUpdateGetState, - /// Set retimer fw update status - RetimerFwUpdateSetState, - /// Clear retimer fw update status - RetimerFwUpdateClearState, - /// Set retimer compliance - SetRetimerCompliance, - /// Reconfigure retimer - ReconfigureRetimer, - /// Set max sink voltage to a specific value. - SetMaxSinkVoltage { - /// The maximum voltage to set, in millivolts. - /// If [`None`], the port will be set to its default maximum voltage. - max_voltage_mv: Option, - }, - /// Clear the dead battery flag for the given port. - ClearDeadBatteryFlag, - /// Set USB control - SetUsbControl(UsbControlConfig), - /// Send VDM - SendVdm(SendVdm), - /// Get DisplayPort status - GetDpStatus, - /// Set DisplayPort configuration - SetDpConfig(DpConfig), - /// Execute DisplayPort reset - ExecuteDrst, - /// Set Thunderbolt configuration - SetTbtConfig(TbtConfig), - /// Set PD state-machine configuration - SetPdStateMachineConfig(PdStateMachineConfig), - /// Set Type-C state-machine configuration - SetTypeCStateMachineConfig(TypeCStateMachineState), - /// Execute electrical disconnect - ExecuteElectricalDisconnect { - /// The time, in seconds, after which the port should automatically reconnect. - /// - /// If [`None`], the port will not automatically reconnect. - reconnect_time_s: Option, - }, - /// Set the system power state - SetSystemPowerState(SystemPowerState), - /// Get the port's discovered SVIDs - GetDiscoveredSvids, - /// Trigger a hard reset on the given port. - HardReset, - /// Get the response to a Discover Identity command sent to the given port with SOP - GetDiscoverIdentitySop, - /// Get the response to a Discover Identity command sent to the given port with SOP' - GetDiscoverIdentitySopPrime, -} - -/// Port-specific commands -#[derive(Copy, Clone, Debug)] -#[cfg_attr(feature = "defmt", derive(defmt::Format))] -pub struct PortCommand { - /// Port ID - pub port: GlobalPortId, - /// Command data - pub data: PortCommandData, -} - -/// Response data for port-specific commands -#[derive(Copy, Clone, Debug)] -#[cfg_attr(feature = "defmt", derive(defmt::Format))] -pub enum PortResponseData { - /// Command completed with no error - Complete, - /// Get port status - PortStatus(PortStatus), - /// Get retimer fw update status - RetimerFwUpdateGetState(RetimerFwUpdateState), - /// Get DisplayPort status - GetDpStatus(DpStatus), - /// Get the port's discovered SVIDs - DiscoveredSvids(DiscoveredSvids), - /// Discover Identity response data for SOP - DiscoverIdentitySop(embedded_usb_pd::vdm::structured::command::discover_identity::sop::ResponseVdos), - /// Discover Identity response data for SOP' - DiscoverIdentitySopPrime(embedded_usb_pd::vdm::structured::command::discover_identity::sop_prime::ResponseVdos), -} - -/// Port-specific command response -pub type PortResponse = Result; - -/// External commands for type-C service -#[derive(Copy, Clone, Debug)] -#[cfg_attr(feature = "defmt", derive(defmt::Format))] -pub enum Command { - /// Port command - Port(PortCommand), - /// Controller command - Controller(ControllerCommand), - /// UCSI command - Ucsi(ucsi::GlobalCommand), -} - -/// UCSI command response -#[derive(Copy, Clone, Debug)] -#[cfg_attr(feature = "defmt", derive(defmt::Format))] -pub struct UcsiResponse { - /// Notify the OPM, the function call - pub notify_opm: bool, - /// Response CCI - pub cci: ucsi::cci::GlobalCci, - /// UCSI response data - pub data: Result, PdError>, -} - -/// Alias to help simplify conversion into a result -pub type UcsiResponseResult = Result; - -impl From for UcsiResponseResult { - fn from(value: UcsiResponse) -> Self { - match value.data { - Ok(data) => Ok(ucsi::GlobalResponse { cci: value.cci, data }), - Err(err) => Err(err), - } - } -} - -/// External command response for type-C service -#[derive(Copy, Clone, Debug)] -#[cfg_attr(feature = "defmt", derive(defmt::Format))] -pub enum Response<'a> { - /// Port command response - Port(PortResponse), - /// Controller command response - Controller(ControllerResponse<'a>), - /// UCSI command response - Ucsi(UcsiResponse), -} - -/// Get the status of the given port. -/// -/// Use the `cached` argument to specify whether to use cached data or force a fetch of register values. -pub async fn get_port_status(port: GlobalPortId, cached: Cached) -> Result { - match execute_external_port_command(Command::Port(PortCommand { - port, - data: PortCommandData::PortStatus(cached), - })) - .await? - { - PortResponseData::PortStatus(status) => Ok(status), - _ => Err(PdError::InvalidResponse), - } -} - -/// Get the status of the given port by its controller and local port ID. -/// -/// Use the `cached` argument to specify whether to use cached data or force a fetch of register values. -pub async fn get_controller_port_status( - controller: ControllerId, - port: LocalPortId, - cached: Cached, -) -> Result { - let global_port = controller_port_to_global_id(controller, port).await?; - get_port_status(global_port, cached).await -} - -/// Reset the given controller. -pub async fn reset_controller(controller_id: ControllerId) -> Result<(), PdError> { - match execute_external_controller_command(Command::Controller(ControllerCommand { - id: controller_id, - data: ControllerCommandData::Reset, - })) - .await? - { - ControllerResponseData::Complete => Ok(()), - _ => Err(PdError::InvalidResponse), - } -} - -/// Get the status of the given controller -#[allow(unreachable_patterns)] -pub async fn get_controller_status(id: ControllerId) -> Result, PdError> { - match execute_external_controller_command(Command::Controller(ControllerCommand { - id, - data: ControllerCommandData::ControllerStatus, - })) - .await? - { - ControllerResponseData::ControllerStatus(status) => Ok(status), - _ => Err(PdError::InvalidResponse), - } -} - -/// Get the number of ports on the given controller -pub async fn get_controller_num_ports(controller_id: ControllerId) -> Result { - Ok(lookup_controller(controller_id).await?.num_ports()) -} - -/// Convert a (controller ID, local port ID) to a global port ID -pub async fn controller_port_to_global_id( - controller_id: ControllerId, - port_id: LocalPortId, -) -> Result { - lookup_controller(controller_id).await?.lookup_global_port(port_id) -} - -/// Convert a global port ID to a (controller ID, local port ID) -pub async fn global_port_to_controller_port(global_port: GlobalPortId) -> Result<(ControllerId, LocalPortId), PdError> { - let (controller, local_port) = lookup_global_port(global_port).await?; - Ok((controller.id(), local_port)) -} - -/// Get the retimer fw update status of the given port -pub async fn port_get_rt_fw_update_status(port: GlobalPortId) -> Result { - match execute_external_port_command(Command::Port(PortCommand { - port, - data: PortCommandData::RetimerFwUpdateGetState, - })) - .await? - { - PortResponseData::RetimerFwUpdateGetState(status) => Ok(status), - _ => Err(PdError::InvalidResponse), - } -} - -/// Set the retimer fw update state of the given port -pub async fn port_set_rt_fw_update_state(port: GlobalPortId) -> Result<(), PdError> { - match execute_external_port_command(Command::Port(PortCommand { - port, - data: PortCommandData::RetimerFwUpdateSetState, - })) - .await? - { - PortResponseData::Complete => Ok(()), - _ => Err(PdError::InvalidResponse), - } -} - -/// Clear the retimer fw update state of the given port -pub async fn port_clear_rt_fw_update_state(port: GlobalPortId) -> Result<(), PdError> { - match execute_external_port_command(Command::Port(PortCommand { - port, - data: PortCommandData::RetimerFwUpdateClearState, - })) - .await? - { - PortResponseData::Complete => Ok(()), - _ => Err(PdError::InvalidResponse), - } -} - -/// Set the retimer comliance state of the given port -pub async fn port_set_rt_compliance(port: GlobalPortId) -> Result<(), PdError> { - match execute_external_port_command(Command::Port(PortCommand { - port, - data: PortCommandData::SetRetimerCompliance, - })) - .await? - { - PortResponseData::Complete => Ok(()), - _ => Err(PdError::InvalidResponse), - } -} - -/// Trigger a sync of the controller state -pub async fn sync_controller_state(id: ControllerId) -> Result<(), PdError> { - match execute_external_controller_command(Command::Controller(ControllerCommand { - id, - data: ControllerCommandData::SyncState, - })) - .await? - { - ControllerResponseData::Complete => Ok(()), - _ => Err(PdError::InvalidResponse), - } -} - -/// Set the system power state on the given port. -/// -/// This notifies the PD controller of the current system power state, -/// which triggers Application Configuration updates (e.g., crossbar reconfiguration). -pub async fn set_power_state(port: GlobalPortId, state: SystemPowerState) -> Result<(), PdError> { - match execute_external_port_command(Command::Port(PortCommand { - port, - data: PortCommandData::SetSystemPowerState(state), - })) - .await? - { - PortResponseData::Complete => Ok(()), - _ => Err(PdError::InvalidResponse), - } -} - -/// Get number of ports on the system -pub fn get_num_ports() -> usize { - super::controller::get_num_ports() -} - -/// Set the maximum voltage for the given port to a specific value. -/// -/// See [`PortCommandData::SetMaxSinkVoltage::max_voltage_mv`] for details on the `max_voltage_mv` parameter. -pub async fn set_max_sink_voltage(port: GlobalPortId, max_voltage_mv: Option) -> Result<(), PdError> { - match execute_external_port_command(Command::Port(PortCommand { - port, - data: PortCommandData::SetMaxSinkVoltage { max_voltage_mv }, - })) - .await? - { - PortResponseData::Complete => Ok(()), - _ => Err(PdError::InvalidResponse), - } -} - -/// Clear the dead battery flag for the given port. -pub async fn clear_dead_battery_flag(port: GlobalPortId) -> Result<(), PdError> { - match execute_external_port_command(Command::Port(PortCommand { - port, - data: PortCommandData::ClearDeadBatteryFlag, - })) - .await? - { - PortResponseData::Complete => Ok(()), - _ => Err(PdError::InvalidResponse), - } -} - -/// Reconfigure the retimer for the given port. -pub async fn reconfigure_retimer(port: GlobalPortId) -> Result<(), PdError> { - match execute_external_port_command(Command::Port(PortCommand { - port, - data: PortCommandData::ReconfigureRetimer, - })) - .await? - { - PortResponseData::Complete => Ok(()), - _ => Err(PdError::InvalidResponse), - } -} - -/// Execute a UCSI command -pub async fn execute_ucsi_command(command: ucsi::GlobalCommand) -> UcsiResponse { - execute_external_ucsi_command(command).await -} - -/// Execute an electrical disconnect on the given port. -/// -/// The `reconnect_time_s` parameter specifies the time, in seconds, after which the port should automatically reconnect. -/// If [`None`], the port will not automatically reconnect. -pub async fn execute_electrical_disconnect( - port: GlobalPortId, - reconnect_time_s: Option, -) -> Result<(), PdError> { - match execute_external_port_command(Command::Port(PortCommand { - port, - data: PortCommandData::ExecuteElectricalDisconnect { reconnect_time_s }, - })) - .await? - { - PortResponseData::Complete => Ok(()), - _ => Err(PdError::InvalidResponse), - } -} - -/// Send vdm to the given port -pub async fn send_vdm(port: GlobalPortId, tx_vdm: SendVdm) -> Result<(), PdError> { - match execute_external_port_command(Command::Port(PortCommand { - port, - data: PortCommandData::SendVdm(tx_vdm), - })) - .await? - { - PortResponseData::Complete => Ok(()), - _ => Err(PdError::InvalidResponse), - } -} - -/// Set USB control configuration -pub async fn set_usb_control(port: GlobalPortId, config: UsbControlConfig) -> Result<(), PdError> { - match execute_external_port_command(Command::Port(PortCommand { - port, - data: PortCommandData::SetUsbControl(config), - })) - .await? - { - PortResponseData::Complete => Ok(()), - _ => Err(PdError::InvalidResponse), - } -} - -/// Get DisplayPort status for the given port -pub async fn get_dp_status(port: GlobalPortId) -> Result { - match execute_external_port_command(Command::Port(PortCommand { - port, - data: PortCommandData::GetDpStatus, - })) - .await? - { - PortResponseData::GetDpStatus(status) => Ok(status), - _ => Err(PdError::InvalidResponse), - } -} - -/// Set DisplayPort configuration for the given port -pub async fn set_dp_config(port: GlobalPortId, config: DpConfig) -> Result<(), PdError> { - match execute_external_port_command(Command::Port(PortCommand { - port, - data: PortCommandData::SetDpConfig(config), - })) - .await? - { - PortResponseData::Complete => Ok(()), - _ => Err(PdError::InvalidResponse), - } -} - -/// Execute DisplayPort reset for the given port -pub async fn execute_drst(port: GlobalPortId) -> Result<(), PdError> { - match execute_external_port_command(Command::Port(PortCommand { - port, - data: PortCommandData::ExecuteDrst, - })) - .await? - { - PortResponseData::Complete => Ok(()), - _ => Err(PdError::InvalidResponse), - } -} - -/// Set Thunderbolt configuration for the given port -pub async fn set_tbt_config(port: GlobalPortId, config: TbtConfig) -> Result<(), PdError> { - match execute_external_port_command(Command::Port(PortCommand { - port, - data: PortCommandData::SetTbtConfig(config), - })) - .await? - { - PortResponseData::Complete => Ok(()), - _ => Err(PdError::InvalidResponse), - } -} - -/// Set PD state-machine configuration for the given port -pub async fn set_pd_state_machine_config(port: GlobalPortId, config: PdStateMachineConfig) -> Result<(), PdError> { - match execute_external_port_command(Command::Port(PortCommand { - port, - data: PortCommandData::SetPdStateMachineConfig(config), - })) - .await? - { - PortResponseData::Complete => Ok(()), - _ => Err(PdError::InvalidResponse), - } -} - -/// Set Type-C state-machine configuration for the given port -pub async fn set_type_c_state_machine_config(port: GlobalPortId, state: TypeCStateMachineState) -> Result<(), PdError> { - match execute_external_port_command(Command::Port(PortCommand { - port, - data: PortCommandData::SetTypeCStateMachineConfig(state), - })) - .await? - { - PortResponseData::Complete => Ok(()), - _ => Err(PdError::InvalidResponse), - } -} - -/// Get the port's discovered SVIDs -pub async fn get_discovered_svids(port: GlobalPortId) -> Result { - match execute_external_port_command(Command::Port(PortCommand { - port, - data: PortCommandData::GetDiscoveredSvids, - })) - .await? - { - PortResponseData::DiscoveredSvids(svids) => Ok(svids), - _ => Err(PdError::InvalidResponse), - } -} - -/// Trigger a hard reset on the given port -pub async fn hard_reset(port: GlobalPortId) -> Result<(), PdError> { - match execute_external_port_command(Command::Port(PortCommand { - port, - data: PortCommandData::HardReset, - })) - .await? - { - PortResponseData::Complete => Ok(()), - _ => Err(PdError::InvalidResponse), - } -} - -/// Get the response to a Discover Identity command sent to the given port with SOP. -pub async fn get_discover_identity_sop_response( - port: GlobalPortId, -) -> Result { - match execute_external_port_command(Command::Port(PortCommand { - port, - data: PortCommandData::GetDiscoverIdentitySop, - })) - .await? - { - PortResponseData::DiscoverIdentitySop(vdos) => Ok(vdos), - _ => Err(PdError::InvalidResponse), - } -} - -/// Get the response to a Discover Identity command sent to the given port with SOP'. -pub async fn get_discover_identity_sop_prime_response( - port: GlobalPortId, -) -> Result { - match execute_external_port_command(Command::Port(PortCommand { - port, - data: PortCommandData::GetDiscoverIdentitySopPrime, - })) - .await? - { - PortResponseData::DiscoverIdentitySopPrime(vdos) => Ok(vdos), - _ => Err(PdError::InvalidResponse), - } -} diff --git a/embedded-service/src/type_c/mod.rs b/embedded-service/src/type_c/mod.rs deleted file mode 100644 index 2943acddb..000000000 --- a/embedded-service/src/type_c/mod.rs +++ /dev/null @@ -1,71 +0,0 @@ -//! Type-C service -use embedded_usb_pd::pdo::{Common, Contract}; -use embedded_usb_pd::type_c; - -use crate::error; -use crate::power::policy; - -pub mod comms; -pub mod controller; -pub mod event; -pub mod external; - -/// Controller ID -#[derive(Copy, Clone, Debug, PartialEq, Eq)] -#[cfg_attr(feature = "defmt", derive(defmt::Format))] -pub struct ControllerId(pub u8); - -/// Length of the Other VDM data -pub const OTHER_VDM_LEN: usize = 29; -/// Length of the Attention VDM data -pub const ATTN_VDM_LEN: usize = 9; - -impl TryFrom for policy::PowerCapability { - type Error = (); - - fn try_from(contract: Contract) -> Result { - Ok(policy::PowerCapability { - voltage_mv: contract.pdo.max_voltage_mv(), - current_ma: contract.operating_current_ma().ok_or(())?, - }) - } -} - -impl From for policy::PowerCapability { - fn from(current: type_c::Current) -> Self { - policy::PowerCapability { - voltage_mv: 5000, - // Assume higher power for now - current_ma: current.to_ma(false), - } - } -} - -/// Type-C USB2 power capability 5V@500mA -pub const POWER_CAPABILITY_USB_DEFAULT_USB2: policy::PowerCapability = policy::PowerCapability { - voltage_mv: 5000, - current_ma: 500, -}; - -/// Type-C USB3 power capability 5V@900mA -pub const POWER_CAPABILITY_USB_DEFAULT_USB3: policy::PowerCapability = policy::PowerCapability { - voltage_mv: 5000, - current_ma: 900, -}; - -/// Type-C power capability 5V@1.5A -pub const POWER_CAPABILITY_5V_1A5: policy::PowerCapability = policy::PowerCapability { - voltage_mv: 5000, - current_ma: 1500, -}; - -/// Type-C power capability 5V@3A -pub const POWER_CAPABILITY_5V_3A0: policy::PowerCapability = policy::PowerCapability { - voltage_mv: 5000, - current_ma: 3000, -}; - -/// Newtype to help clarify arguments to port status commands -#[derive(Copy, Clone, Debug, PartialEq, Eq, Default)] -#[cfg_attr(feature = "defmt", derive(defmt::Format))] -pub struct Cached(pub bool); diff --git a/espi-service/Cargo.toml b/espi-service/Cargo.toml index 2d1f6b373..350adc4a8 100644 --- a/espi-service/Cargo.toml +++ b/espi-service/Cargo.toml @@ -8,7 +8,7 @@ rust-version.workspace = true license = "MIT" [package.metadata.cargo-machete] -ignored = ["log"] +ignored = ["log", "cortex-m", "cortex-m-rt"] [lints] workspace = true @@ -21,6 +21,7 @@ embassy-sync.workspace = true embassy-imxrt = { workspace = true, features = ["mimxrt633s"] } embassy-futures.workspace = true mctp-rs = { workspace = true, features = ["espi"] } +odp-service-common.workspace = true [target.'cfg(target_os = "none")'.dependencies] cortex-m-rt.workspace = true diff --git a/espi-service/src/espi_service.rs b/espi-service/src/espi_service.rs index 74d9c0e87..fdbdc1dec 100644 --- a/espi-service/src/espi_service.rs +++ b/espi-service/src/espi_service.rs @@ -1,16 +1,10 @@ -use core::mem::offset_of; use core::slice; -use core::borrow::BorrowMut; +use embassy_futures::select::select; use embassy_imxrt::espi; use embassy_sync::channel::Channel; use embassy_sync::mutex::Mutex; -use embassy_sync::once_lock::OnceLock; -use embedded_services::buffer::OwnedRef; -use embedded_services::comms::{self, EndpointID, External, Internal}; -use embedded_services::ec_type::message::{HostMsg, NotificationMsg, StdHostMsg, StdHostPayload, StdHostRequest}; -use embedded_services::ec_type::protocols::mctp; -use embedded_services::{GlobalRawMutex, debug, ec_type, error, info, trace}; +use embedded_services::{GlobalRawMutex, error, info, trace}; use mctp_rs::smbus_espi::SmbusEspiMedium; use mctp_rs::smbus_espi::SmbusEspiReplyContext; @@ -20,12 +14,15 @@ const HOST_TX_QUEUE_SIZE: usize = 5; // REVISIT: When adding support for other platforms, refactor this as they don't have a notion of port IDs const OOB_PORT_ID: usize = 1; -// Should be as large as the largest possible MCTP packet and it's metadata. +// Should be as large as the largest possible MCTP packet and its metadata. const ASSEMBLY_BUF_SIZE: usize = 256; -embedded_services::define_static_buffer!(assembly_buf, u8, [0u8; ASSEMBLY_BUF_SIZE]); - -type HostMsgInternal = (EndpointID, StdHostMsg); +#[derive(Clone)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +struct HostResultMessage { + pub handler_service_id: RelayHandler::ServiceIdType, + pub message: RelayHandler::ResultEnumType, +} #[derive(Debug, Clone, Copy)] #[cfg_attr(feature = "defmt", derive(defmt::Format))] @@ -34,154 +31,253 @@ pub enum Error { Buffer(embedded_services::buffer::Error), } -pub struct Service<'a> { - endpoint: comms::Endpoint, - ec_memory: Mutex, - host_tx_queue: Channel, - assembly_buf_owned_ref: OwnedRef<'a, u8>, +/// The memory required by the eSPI service to run +pub struct Resources<'hw, RelayHandler: embedded_services::relay::mctp::RelayHandler> { + inner: Option>, } -impl Service<'_> { - pub fn new(ec_memory: &'static mut ec_type::structure::ECMemory) -> Self { - Service { - endpoint: comms::Endpoint::uninit(EndpointID::External(External::Host)), - ec_memory: Mutex::new(ec_memory), - host_tx_queue: Channel::new(), - assembly_buf_owned_ref: assembly_buf::get_mut().unwrap(), - } +impl<'hw, RelayHandler: embedded_services::relay::mctp::RelayHandler> Default for Resources<'hw, RelayHandler> { + fn default() -> Self { + Self { inner: None } + } +} + +/// Service runner for the eSPI service. Users must call the run() method on the runner for the service to start processing events. +pub struct Runner<'hw, RelayHandler: embedded_services::relay::mctp::RelayHandler> { + inner: &'hw ServiceInner<'hw, RelayHandler>, +} + +impl<'hw, RelayHandler: embedded_services::relay::mctp::RelayHandler> + odp_service_common::runnable_service::ServiceRunner<'hw> for Runner<'hw, RelayHandler> +{ + /// Run the service event loop. + async fn run(self) -> embedded_services::Never { + self.inner.run().await + } +} + +pub struct Service<'hw, RelayHandler: embedded_services::relay::mctp::RelayHandler> { + _inner: &'hw ServiceInner<'hw, RelayHandler>, +} + +impl<'hw, RelayHandler: embedded_services::relay::mctp::RelayHandler> odp_service_common::runnable_service::Service<'hw> + for Service<'hw, RelayHandler> +{ + type Resources = Resources<'hw, RelayHandler>; + type Runner = Runner<'hw, RelayHandler>; + type ErrorType = core::convert::Infallible; + type InitParams = InitParams<'hw, RelayHandler>; + + async fn new( + resources: &'hw mut Self::Resources, + params: InitParams<'hw, RelayHandler>, + ) -> Result<(Self, Self::Runner), core::convert::Infallible> { + let inner = resources.inner.insert(ServiceInner::new(params).await); + Ok((Self { _inner: inner }, Runner { inner })) } +} - async fn route_to_service(&self, offset: usize, length: usize) -> Result<(), ec_type::Error> { - let mut offset = offset; - let mut length = length; +pub struct InitParams<'hw, RelayHandler: embedded_services::relay::mctp::RelayHandler> { + pub espi: espi::Espi<'hw>, + pub relay_handler: RelayHandler, +} - if offset + length > size_of::() { - return Err(ec_type::Error::InvalidLocation); +struct ServiceInner<'hw, RelayHandler: embedded_services::relay::mctp::RelayHandler> { + espi: Mutex>, + host_tx_queue: Channel, HOST_TX_QUEUE_SIZE>, + relay_handler: RelayHandler, +} + +impl<'hw, RelayHandler: embedded_services::relay::mctp::RelayHandler> ServiceInner<'hw, RelayHandler> { + async fn new(mut init_params: InitParams<'hw, RelayHandler>) -> Self { + init_params.espi.wait_for_plat_reset().await; + + Self { + espi: Mutex::new(init_params.espi), + host_tx_queue: Channel::new(), + relay_handler: init_params.relay_handler, } + } - while length > 0 { - if (offset >= offset_of!(ec_type::structure::ECMemory, ver) - && offset < offset_of!(ec_type::structure::ECMemory, ver) + size_of::()) - || (offset >= offset_of!(ec_type::structure::ECMemory, caps) - && offset - < offset_of!(ec_type::structure::ECMemory, caps) - + size_of::()) - { - // This is a read-only section. eSPI master should not write to it. - return Err(ec_type::Error::InvalidLocation); - } else if offset >= offset_of!(ec_type::structure::ECMemory, batt) - && offset < offset_of!(ec_type::structure::ECMemory, batt) + size_of::() - { - self.route_to_battery_service(&mut offset, &mut length).await?; - } else if offset >= offset_of!(ec_type::structure::ECMemory, therm) - && offset < offset_of!(ec_type::structure::ECMemory, therm) + size_of::() - { - self.route_to_thermal_service(&mut offset, &mut length).await?; - } else if offset >= offset_of!(ec_type::structure::ECMemory, alarm) - && offset < offset_of!(ec_type::structure::ECMemory, alarm) + size_of::() - { - self.route_to_time_alarm_service(&mut offset, &mut length).await?; + async fn run(&self) -> embedded_services::Never { + let mut espi = self.espi.lock().await; + loop { + let event = select(espi.wait_for_event(), self.host_tx_queue.receive()).await; + + match event { + embassy_futures::select::Either::First(controller_event) => { + self.process_controller_event(&mut espi, controller_event) + .await + .unwrap_or_else(|e| { + error!("Critical error processing eSPI controller event: {:?}", e); + }); + } + embassy_futures::select::Either::Second(host_msg) => { + self.process_response_to_host(&mut espi, host_msg).await + } } } - - Ok(()) } - async fn route_to_battery_service(&self, offset: &mut usize, length: &mut usize) -> Result<(), ec_type::Error> { - let msg = { - let memory_map = self - .ec_memory - .try_lock() - .expect("Messages handled one after another, should be infallible."); - ec_type::mem_map_to_battery_msg(&memory_map, offset, length)? - }; + // TODO The notification system was not actually used, so this is currently dead code. + // We need to implement some interface for triggering notifications from other subsystems, and it may do something like this: + // + // async fn process_notification_to_host(&self, espi: &mut espi::Espi<'_>, notification: &NotificationMsg) { + // espi.irq_push(notification.offset).await; + // info!("espi: Notification id {} sent to Host!", notification.offset); + // } - comms::send( - EndpointID::External(External::Host), - EndpointID::Internal(Internal::Battery), - &msg, - ) - .await - .unwrap(); + fn write_to_hw(&self, espi: &mut espi::Espi<'hw>, packet: &[u8]) -> Result<(), embassy_imxrt::espi::Error> { + // Send packet via your transport medium + // SAFETY: Safe as the access to espi is protected by a mut reference. + let dest_slice = unsafe { espi.oob_get_write_buffer(OOB_PORT_ID)? }; + dest_slice[..packet.len()].copy_from_slice(&packet[..packet.len()]); - Ok(()) + // Write response over OOB + espi.oob_write_data(OOB_PORT_ID, packet.len() as u8) } - async fn route_to_thermal_service(&self, offset: &mut usize, length: &mut usize) -> Result<(), ec_type::Error> { - let msg = { - let memory_map = self - .ec_memory - .try_lock() - .expect("Messages handled one after another, should be infallible."); - ec_type::mem_map_to_thermal_msg(&memory_map, offset, length)? - }; + async fn process_controller_event( + &self, + espi: &mut espi::Espi<'hw>, + event: Result, + ) -> Result<(), Error> { + match event { + Ok(espi::Event::PeripheralEvent(port_event)) => { + info!( + "eSPI PeripheralEvent Port: {}, direction: {}, address: {}, offset: {}, length: {}", + port_event.port, port_event.direction, port_event.offset, port_event.base_addr, port_event.length, + ); - comms::send( - EndpointID::External(External::Host), - EndpointID::Internal(Internal::Thermal), - &msg, - ) - .await - .unwrap(); + // We're not handling these - communication is all through OOB - Ok(()) - } + espi.complete_port(port_event.port); + } + Ok(espi::Event::OOBEvent(port_event)) => { + info!( + "eSPI OOBEvent Port: {}, direction: {}, address: {}, offset: {}, length: {}", + port_event.port, port_event.direction, port_event.offset, port_event.base_addr, port_event.length, + ); + + if port_event.direction { + let src_slice = + unsafe { slice::from_raw_parts(port_event.base_addr as *const u8, port_event.length) }; + + // TODO: This is a workaround because mctp_rs expects a PEC byte, so we hardcode a 0 at the end. + // We should add functionality to mctp_rs to disable PEC. + let mut with_pec = [0u8; 100]; + with_pec[..src_slice.len()].copy_from_slice(src_slice); + with_pec[src_slice.len()] = 0; + let with_pec = &with_pec[..=src_slice.len()]; + + #[cfg(feature = "defmt")] // Required because without defmt, there is no implementation of UpperHex for [u8] + embedded_services::debug!("OOB message: {:02X}", &src_slice[0..]); + + let mut assembly_buf = [0u8; ASSEMBLY_BUF_SIZE]; + let mut mctp_ctx = mctp_rs::MctpPacketContext::::new( + SmbusEspiMedium, + assembly_buf.as_mut_slice(), + ); - async fn route_to_time_alarm_service(&self, offset: &mut usize, length: &mut usize) -> Result<(), ec_type::Error> { - let msg = { - let memory_map = self - .ec_memory - .try_lock() - .expect("Messages handled one after another, should be infallible."); - ec_type::mem_map_to_time_alarm_msg(&memory_map, offset, length)? - }; + match mctp_ctx.deserialize_packet(with_pec) { + Ok(Some(message)) => { + trace!("MCTP packet successfully deserialized"); + match message.parse_as::() { + Ok((header, body)) => { + self.process_request_to_ec((header, body), espi, &port_event).await?; + } + Err(e) => { + error!("MCTP ODP type malformed: {:?}", e); + espi.complete_port(port_event.port); + return Err(Error::Serialize); + } + } + } + Ok(None) => { + // Partial message, waiting for more packets + error!("Partial msg, should not happen"); + espi.complete_port(OOB_PORT_ID); - comms::send( - EndpointID::External(External::Host), - EndpointID::Internal(Internal::TimeAlarm), - &msg, - ) - .await - .unwrap(); + return Err(Error::Serialize); + } + Err(_e) => { + // Handle protocol or medium error + error!("MCTP packet malformed"); + + error!("error code: {:?}", _e); + espi.complete_port(OOB_PORT_ID); + return Err(Error::Serialize); + } + } + } else { + espi.complete_port(port_event.port); + } + } + Ok(espi::Event::Port80) => { + info!("eSPI Port 80"); + } + Ok(espi::Event::WireChange(_)) => { + info!("eSPI WireChange"); + } + Err(e) => { + error!("eSPI Failed with error: {:?}", e); + } + } Ok(()) } - pub(crate) async fn wait_for_subsystem_msg(&self) -> HostMsgInternal { - self.host_tx_queue.receive().await - } + async fn process_request_to_ec( + &self, + (header, body): ( + >::Header, + RelayHandler::RequestEnumType, + ), + espi: &mut espi::Espi<'hw>, + port_event: &espi::PortEvent, + ) -> Result<(), Error> { + use embedded_services::relay::mctp::RelayHeader; + info!("Host Request received"); - pub(crate) async fn process_subsystem_msg(&self, espi: &mut espi::Espi<'static>, host_msg: HostMsgInternal) { - let (endpoint, host_msg) = host_msg; - match host_msg { - HostMsg::Notification(notification_msg) => self.process_notification_to_host(espi, ¬ification_msg).await, - HostMsg::Response(acpi_msg_comms) => self.process_response_to_host(espi, &acpi_msg_comms, endpoint).await, - } + espi.complete_port(port_event.port); + + let response = self.relay_handler.process_request(body).await; + self.host_tx_queue + .try_send(HostResultMessage { + handler_service_id: header.get_service_id(), + message: response, + }) + .map_err(|_| Error::Serialize)?; + + Ok(()) } - async fn process_notification_to_host(&self, espi: &mut espi::Espi<'_>, notification: &NotificationMsg) { - espi.irq_push(notification.offset).await; - info!("espi: Notification id {} sent to Host!", notification.offset); + async fn process_response_to_host(&self, espi: &mut espi::Espi<'hw>, response: HostResultMessage) { + match self.serialize_packet_from_subsystem(espi, response).await { + Ok(()) => { + trace!("Full packet successfully sent to host!") + } + Err(e) => { + // TODO we may want to consider sending a failure message to the debug service or something, but that'll require + // a 'facility of last resort' on the relay handler, so for now we just log the error + error!("Packet serialize error {:?}", e); + } + } } async fn serialize_packet_from_subsystem( &self, - espi: &mut espi::Espi<'static>, - response: &StdHostRequest, - endpoint: EndpointID, + espi: &mut espi::Espi<'hw>, + result: HostResultMessage, ) -> Result<(), Error> { - let mut assembly_buf_access = self.assembly_buf_owned_ref.borrow_mut().map_err(Error::Buffer)?; - let pkt_ctx_buf = assembly_buf_access.borrow_mut(); - let mut mctp_ctx = mctp_rs::MctpPacketContext::new(mctp_rs::smbus_espi::SmbusEspiMedium, pkt_ctx_buf); + use embedded_services::relay::mctp::RelayResponse; + let mut assembly_buf = [0u8; ASSEMBLY_BUF_SIZE]; + let mut mctp_ctx = + mctp_rs::MctpPacketContext::new(mctp_rs::smbus_espi::SmbusEspiMedium, assembly_buf.as_mut_slice()); let reply_context: mctp_rs::MctpReplyContext = mctp_rs::MctpReplyContext { source_endpoint_id: mctp_rs::EndpointId::Id(0x80), - destination_endpoint_id: match endpoint { - EndpointID::Internal(Internal::Battery) => mctp_rs::EndpointId::Id(8), - EndpointID::Internal(Internal::Thermal) => mctp_rs::EndpointId::Id(9), - EndpointID::Internal(Internal::Debug) => mctp_rs::EndpointId::Id(10), - _ => mctp_rs::EndpointId::Id(0x80), - }, + destination_endpoint_id: mctp_rs::EndpointId::Id(result.handler_service_id.into()), // TODO We're currently using this incorrectly - it should be the bus address of the host. Revisit once we have assigned a bus address to the host. packet_sequence_number: mctp_rs::MctpSequenceNumber::new(0), message_tag: mctp_rs::MctpMessageTag::try_from(3).map_err(|e| { error!("serialize_packet_from_subsystem: {:?}", e); @@ -193,21 +289,9 @@ impl Service<'_> { }, // Medium-specific context }; - let header = mctp::OdpHeader { - request_bit: false, - datagram_bit: false, - service: match endpoint { - EndpointID::Internal(Internal::Battery) => mctp::OdpService::Battery, - EndpointID::Internal(Internal::Thermal) => mctp::OdpService::Thermal, - EndpointID::Internal(Internal::Debug) => mctp::OdpService::Debug, - _ => mctp::OdpService::Debug, - }, - command_code: response.command.into(), - completion_code: Default::default(), - }; - + let header = result.message.create_header(&result.handler_service_id); let mut packet_state = mctp_ctx - .serialize_packet(reply_context, (header, response.payload)) + .serialize_packet(reply_context, (header, result.message)) .map_err(|e| { error!("serialize_packet_from_subsystem: {:?}", e); Error::Serialize @@ -220,7 +304,6 @@ impl Service<'_> { })?; // Last byte is PEC, ignore for now let packet = &packet[..packet.len() - 1]; - #[cfg(feature = "defmt")] trace!("Sending MCTP response: {:?}", packet); self.write_to_hw(espi, packet).map_err(|e| { @@ -230,240 +313,8 @@ impl Service<'_> { // Immediately service the packet with the ESPI HAL let event = espi.wait_for_event().await; - process_controller_event(espi, self, event).await?; - } - Ok(()) - } - - fn write_to_hw(&self, espi: &mut espi::Espi<'static>, packet: &[u8]) -> Result<(), embassy_imxrt::espi::Error> { - // Send packet via your transport medium - // SAFETY: Safe as the access to espi is protected by a mut reference. - let dest_slice = unsafe { espi.oob_get_write_buffer(OOB_PORT_ID)? }; - dest_slice[..packet.len()].copy_from_slice(&packet[..packet.len()]); - - // Write response over OOB - espi.oob_write_data(OOB_PORT_ID, packet.len() as u8) - } - - fn send_mctp_error_response(&self, endpoint: EndpointID, espi: &mut espi::Espi<'static>) { - // SAFETY: Unwrap is safe here as battery will always be supported. - // Data is ACPI payload [version, instance, reserved (error status), command] - let (final_packet, final_packet_size) = mctp::build_mctp_header(&[0, 0, 0, 1], 4, endpoint, true, true) - .expect("Unexpected error building MCTP header"); - - if let Err(e) = self.write_to_hw(espi, &final_packet[..final_packet_size]) { - error!("Critical error sending error response: {:?}", e); - } - } - - async fn process_response_to_host( - &self, - espi: &mut espi::Espi<'static>, - response: &StdHostRequest, - endpoint: EndpointID, - ) { - match self.serialize_packet_from_subsystem(espi, response, endpoint).await { - Err(e) => { - error!("Packet serialize error {:?}", e); - - self.send_mctp_error_response(endpoint, espi); - } - Ok(()) => { - trace!("Full packet successfully sent to host!") - } - } - } - - pub(crate) fn endpoint(&self) -> &comms::Endpoint { - &self.endpoint - } -} - -impl comms::MailboxDelegate for Service<'_> { - fn receive(&self, message: &comms::Message) -> Result<(), comms::MailboxDelegateError> { - if let Some(msg) = message.data.get::() { - let host_msg = (message.from, *msg); - debug!("Espi service: recvd acpi response"); - if self.host_tx_queue.try_send(host_msg).is_err() { - return Err(comms::MailboxDelegateError::BufferFull); - } - } else { - let mut memory_map = self - .ec_memory - .try_lock() - .expect("Messages handled one after another, should be infallible."); - if let Some(msg) = message.data.get::() { - ec_type::update_capabilities_section(msg, &mut memory_map); - } else if let Some(msg) = message.data.get::() { - ec_type::update_battery_section(msg, &mut memory_map); - } else if let Some(msg) = message.data.get::() { - ec_type::update_thermal_section(msg, &mut memory_map); - } else if let Some(msg) = message.data.get::() { - ec_type::update_time_alarm_section(msg, &mut memory_map); - } else { - return Err(comms::MailboxDelegateError::MessageNotFound); - } + self.process_controller_event(espi, event).await?; } - Ok(()) } } - -pub(crate) static ESPI_SERVICE: OnceLock = OnceLock::new(); - -pub(crate) async fn process_controller_event( - espi: &mut espi::Espi<'static>, - espi_service: &Service<'_>, - event: Result, -) -> Result<(), Error> { - match event { - Ok(espi::Event::PeripheralEvent(port_event)) => { - info!( - "eSPI PeripheralEvent Port: {}, direction: {}, address: {}, offset: {}, length: {}", - port_event.port, port_event.direction, port_event.offset, port_event.base_addr, port_event.length, - ); - - // If it is a peripheral channel write, then we need to notify the service - if port_event.direction { - let res = espi_service - .route_to_service(port_event.offset, port_event.length) - .await; - - if res.is_err() { - error!( - "eSPI master send invalid offset: {} length: {}", - port_event.offset, port_event.length - ); - } - } - - espi.complete_port(port_event.port); - } - Ok(espi::Event::OOBEvent(port_event)) => { - info!( - "eSPI OOBEvent Port: {}, direction: {}, address: {}, offset: {}, length: {}", - port_event.port, port_event.direction, port_event.offset, port_event.base_addr, port_event.length, - ); - - if port_event.direction { - let src_slice = unsafe { slice::from_raw_parts(port_event.base_addr as *const u8, port_event.length) }; - - // TODO: This is a workaround because mctp_rs expects a PEC byte, so we hardcode a 0 at the end. - // We should add functionality to mctp_rs to disable PEC. - let mut with_pec = [0u8; 100]; - with_pec[..src_slice.len()].copy_from_slice(src_slice); - with_pec[src_slice.len()] = 0; - let with_pec = &with_pec[..=src_slice.len()]; - - #[cfg(feature = "defmt")] - debug!("OOB message: {:02X}", &src_slice[0..]); - - let host_request: StdHostRequest; - let endpoint: EndpointID; - - { - let mut assembly_access = espi_service - .assembly_buf_owned_ref - .borrow_mut() - .map_err(Error::Buffer)?; - // let mut comms_access = espi_service.comms_buf_owned_ref.borrow_mut(); - let mut mctp_ctx = mctp_rs::MctpPacketContext::::new( - SmbusEspiMedium, - assembly_access.borrow_mut(), - ); - - match mctp_ctx.deserialize_packet(with_pec) { - Ok(Some(message)) => { - #[cfg(feature = "defmt")] - trace!("MCTP packet successfully deserialized"); - - match message.parse_as::() { - Ok((header, body)) => { - host_request = StdHostRequest { - command: header.command_code.into(), - status: header.completion_code.into(), - payload: body, - }; - endpoint = match header.service { - mctp::OdpService::Battery => { - EndpointID::Internal(embedded_services::comms::Internal::Battery) - } - mctp::OdpService::Thermal => { - EndpointID::Internal(embedded_services::comms::Internal::Thermal) - } - mctp::OdpService::Debug => { - EndpointID::Internal(embedded_services::comms::Internal::Debug) - } - }; - #[cfg(feature = "defmt")] - trace!( - "Host Request: Service {:?}, Command {:?}, Status {:?}", - endpoint, host_request.command, host_request.status, - ); - } - Err(_e) => { - #[cfg(feature = "defmt")] - error!("MCTP ODP type malformed"); - espi.complete_port(port_event.port); - - // REVISIT: An error here means that we couldn't decode the incoming message, - // thus we don't know what subsystem the message was meant for. For now, - // hardcode Debug but we might need a special endpoint for error. - espi_service.send_mctp_error_response( - EndpointID::Internal(embedded_services::comms::Internal::Debug), - espi, - ); - return Err(Error::Serialize); - } - } - } - Ok(None) => { - // Partial message, waiting for more packets - error!("Partial msg, should not happen"); - espi.complete_port(OOB_PORT_ID); - - // REVISIT: An error here means that we couldn't decode the incoming message, - // thus we don't know what subsystem the message was meant for. For now, - // hardcode Debug but we might need a special endpoint for error. - espi_service.send_mctp_error_response( - EndpointID::Internal(embedded_services::comms::Internal::Debug), - espi, - ); - return Err(Error::Serialize); - } - Err(_e) => { - // Handle protocol or medium error - error!("MCTP packet malformed"); - espi.complete_port(OOB_PORT_ID); - - // REVISIT: An error here means that we couldn't decode the incoming message, - // thus we don't know what subsystem the message was meant for. For now, - // hardcode Debug but we might need a special endpoint for error. - espi_service.send_mctp_error_response( - EndpointID::Internal(embedded_services::comms::Internal::Debug), - espi, - ); - return Err(Error::Serialize); - } - } - } - - espi.complete_port(port_event.port); - espi_service.endpoint.send(endpoint, &host_request).await.unwrap(); - info!("MCTP packet forwarded to service: {:?}", endpoint); - } else { - espi.complete_port(port_event.port); - } - } - Ok(espi::Event::Port80) => { - info!("eSPI Port 80"); - } - Ok(espi::Event::WireChange(_)) => { - info!("eSPI WireChange"); - } - Err(e) => { - error!("eSPI Failed with error: {:?}", e); - } - } - Ok(()) -} diff --git a/espi-service/src/lib.rs b/espi-service/src/lib.rs index 658cb1c0b..ce0c51fd6 100644 --- a/espi-service/src/lib.rs +++ b/espi-service/src/lib.rs @@ -4,7 +4,23 @@ #![allow(clippy::panic)] #![allow(clippy::unwrap_used)] +// This module has a hard dependency on embassy-imxrt, which doesn't work on desktop. +// This means that the entire workspace's tests won't compile if this module is enabled. +// +// On Linux, we sort-of get away with it - as far as I can tell, the linker on Linux is more aggressive +// with pruning unused code, so as long as there's no test that calls into anything that eventually calls +// into embassy-imxrt, we at least compile on Linux. +// +// However, on Windows, it looks like the linker is erroring out because it can't find embassy-imxrt-related +// symbols before it does the analysis to determine that those symbols aren't reachable anyway, so we have to +// disable this module entirely to be able to compile the workspace's tests at all on Windows. +// +// If we ever want to run tests for this module on Windows, we'll need some way to break the dependency +// on embassy-imxrt - probably by switching to some sort of trait-based interface with eSPI. Until then, +// we need to gate everything on #[cfg(not(test))]. + +#[cfg(not(test))] mod espi_service; -pub mod task; +#[cfg(not(test))] pub use espi_service::*; diff --git a/espi-service/src/task.rs b/espi-service/src/task.rs deleted file mode 100644 index ad1ee7fab..000000000 --- a/espi-service/src/task.rs +++ /dev/null @@ -1,48 +0,0 @@ -use embassy_futures::select::select; -use embassy_imxrt::espi; -use embedded_services::{comms, ec_type, info}; - -use crate::{ESPI_SERVICE, Service, process_controller_event}; - -pub async fn espi_service( - mut espi: espi::Espi<'static>, - memory_map_buffer: &'static mut [u8], -) -> Result { - info!("Reserved eSPI memory map buffer size: {}", memory_map_buffer.len()); - info!("eSPI MemoryMap size: {}", size_of::()); - - if size_of::() > memory_map_buffer.len() { - panic!("eSPI MemoryMap is too big for reserved memory buffer!!!"); - } - - memory_map_buffer.fill(0); - - let memory_map: &mut ec_type::structure::ECMemory = - unsafe { &mut *(memory_map_buffer.as_mut_ptr() as *mut ec_type::structure::ECMemory) }; - - espi.wait_for_plat_reset().await; - - info!("Initializing memory map"); - memory_map.ver.major = ec_type::structure::EC_MEMMAP_VERSION.major; - memory_map.ver.minor = ec_type::structure::EC_MEMMAP_VERSION.minor; - memory_map.ver.spin = ec_type::structure::EC_MEMMAP_VERSION.spin; - memory_map.ver.res0 = ec_type::structure::EC_MEMMAP_VERSION.res0; - - let espi_service = ESPI_SERVICE.get_or_init(|| Service::new(memory_map)); - comms::register_endpoint(espi_service, espi_service.endpoint()) - .await - .unwrap(); - - loop { - let event = select(espi.wait_for_event(), espi_service.wait_for_subsystem_msg()).await; - - match event { - embassy_futures::select::Either::First(controller_event) => { - process_controller_event(&mut espi, espi_service, controller_event).await? - } - embassy_futures::select::Either::Second(host_msg) => { - espi_service.process_subsystem_msg(&mut espi, host_msg).await - } - } - } -} diff --git a/examples/pico-de-gallo/Cargo.lock b/examples/pico-de-gallo/Cargo.lock new file mode 100644 index 000000000..205594855 --- /dev/null +++ b/examples/pico-de-gallo/Cargo.lock @@ -0,0 +1,1942 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "anstream" +version = "0.6.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" + +[[package]] +name = "anstyle-parse" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + +[[package]] +name = "anyhow" +version = "1.0.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" + +[[package]] +name = "arraydeque" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d902e3d592a523def97af8f317b08ce16b7ab854c1985a0c671e6f15cebc236" + +[[package]] +name = "askama" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f75363874b771be265f4ffe307ca705ef6f3baa19011c149da8674a87f1b75c4" +dependencies = [ + "askama_derive", + "itoa", + "percent-encoding", + "serde", + "serde_json", +] + +[[package]] +name = "askama_derive" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "129397200fe83088e8a68407a8e2b1f826cf0086b21ccdb866a722c8bcd3a94f" +dependencies = [ + "askama_parser", + "memchr", + "proc-macro2", + "quote", + "rustc-hash", + "syn", +] + +[[package]] +name = "askama_parser" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6ab5630b3d5eaf232620167977f95eb51f3432fc76852328774afbd242d4358" +dependencies = [ + "memchr", + "winnow 0.7.14", +] + +[[package]] +name = "atomic-polyfill" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cf2bce30dfe09ef0bfaef228b9d414faaf7e563035494d7fe092dba54b300f4" +dependencies = [ + "critical-section", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "bare-metal" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5deb64efa5bd81e31fcd1938615a6d98c82eafcbcd787162b6f63b91d6bac5b3" +dependencies = [ + "rustc_version 0.2.3", +] + +[[package]] +name = "battery-service" +version = "0.1.0" +dependencies = [ + "battery-service-interface", + "embassy-futures", + "embassy-sync", + "embassy-time", + "embedded-batteries-async", + "embedded-services", + "log", + "odp-service-common", + "power-policy-interface", +] + +[[package]] +name = "battery-service-interface" +version = "0.1.0" +dependencies = [ + "embedded-batteries-async", + "log", +] + +[[package]] +name = "bit-register" +version = "0.1.0" +source = "git+https://github.com/OpenDevicePartnership/odp-utilities#583015c08ad9855f310bdb25d5cf9abff77b5e08" +dependencies = [ + "num-traits", +] + +[[package]] +name = "bitfield" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46afbd2983a5d5a7bd740ccb198caf5b82f45c40c09c0eed36052d91cb92e719" + +[[package]] +name = "bitfield" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f798d2d157e547aa99aab0967df39edd0b70307312b6f8bd2848e6abe40896e0" + +[[package]] +name = "bitfield-struct" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8769c4854c5ada2852ddf6fd09d15cf43d4c2aaeccb4de6432f5402f08a6003b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "bitflags" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" + +[[package]] +name = "bitvec" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + +[[package]] +name = "bq40z50-rx" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55262eaa8d376e3db634b2cf851d2f255fbe86076abc3d4f8088248340836bb6" +dependencies = [ + "device-driver", + "embassy-time", + "embedded-batteries-async", + "embedded-hal 1.0.0", + "embedded-hal-async", + "smbus-pec", +] + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" + +[[package]] +name = "cc" +version = "1.2.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6354c81bbfd62d9cfa9cb3c773c2b7b2a3a482d569de977fd0e961f6e7c00583" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cobs" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa961b519f0b462e3a3b4a34b64d119eeaca1d59af726fe450bbba07a9fc0a1" +dependencies = [ + "thiserror", +] + +[[package]] +name = "colorchoice" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" + +[[package]] +name = "convert_case" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "cordyceps" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "688d7fbb8092b8de775ef2536f36c8c31f2bc4006ece2e8d8ad2d17d00ce0a2a" +dependencies = [ + "loom", + "tracing", +] + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cortex-m" +version = "0.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ec610d8f49840a5b376c69663b6369e71f4b34484b9b2eb29fb918d92516cb9" +dependencies = [ + "bare-metal", + "bitfield 0.13.2", + "embedded-hal 0.2.7", + "volatile-register", +] + +[[package]] +name = "critical-section" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" + +[[package]] +name = "dd-manifest-tree" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5793572036e0a6638977c7370c6afc423eac848ee8495f079b8fd3964de7b9f9" +dependencies = [ + "yaml-rust2", +] + +[[package]] +name = "device-driver" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af0e43acfcbb0bb3b7435cc1b1dbb33596cacfec1eb243336b74a398e0bd6cbf" +dependencies = [ + "device-driver-macros", + "embedded-io 0.6.1", + "embedded-io-async 0.6.1", +] + +[[package]] +name = "device-driver-generation" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3935aec9cf5bb2ab927f59ca69faecf976190390b0ce34c6023889e9041040c0" +dependencies = [ + "anyhow", + "askama", + "bitvec", + "convert_case", + "dd-manifest-tree", + "itertools", + "kdl", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "device-driver-macros" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fdc68ed515c4eddff2e95371185b4becba066085bf36d50f07f09782af98e17" +dependencies = [ + "device-driver-generation", + "proc-macro2", + "syn", +] + +[[package]] +name = "document-features" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" +dependencies = [ + "litrs", +] + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "embassy-executor-timer-queue" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fc328bf943af66b80b98755db9106bf7e7471b0cf47dc8559cd9a6be504cc9c" + +[[package]] +name = "embassy-futures" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc2d050bdc5c21e0862a89256ed8029ae6c290a93aecefc73084b3002cdebb01" + +[[package]] +name = "embassy-sync" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7bbd85cf5a5ae56bdf26f618364af642d1d0a4e245cdd75cd9aabda382f65a81" +dependencies = [ + "cfg-if", + "critical-section", + "embedded-io-async 0.7.0", + "futures-core", + "futures-sink", + "heapless 0.9.2", + "log", +] + +[[package]] +name = "embassy-time" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "592b0c143ec626e821d4d90da51a2bd91d559d6c442b7c74a47d368c9e23d97a" +dependencies = [ + "cfg-if", + "critical-section", + "document-features", + "embassy-time-driver", + "embassy-time-queue-utils", + "embedded-hal 0.2.7", + "embedded-hal 1.0.0", + "embedded-hal-async", + "futures-core", + "log", +] + +[[package]] +name = "embassy-time-driver" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ee71af1b3a0deaa53eaf2d39252f83504c853646e472400b763060389b9fcc9" +dependencies = [ + "document-features", +] + +[[package]] +name = "embassy-time-queue-utils" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80e2ee86063bd028a420a5fb5898c18c87a8898026da1d4c852af2c443d0a454" +dependencies = [ + "embassy-executor-timer-queue", + "heapless 0.8.0", +] + +[[package]] +name = "embedded-batteries" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40f975432b4e146342a1589c563cffab6b7a692024cb511bf87b6bfe78c84125" +dependencies = [ + "bitfield-struct", + "bitflags", + "embedded-hal 1.0.0", + "zerocopy", +] + +[[package]] +name = "embedded-batteries-async" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3bf0e4be67770cfc31f1cea8b73baf98c0baf2c57d6bd8c3a4c315acb1d8bd4" +dependencies = [ + "bitfield-struct", + "embedded-batteries", + "embedded-hal 1.0.0", +] + +[[package]] +name = "embedded-crc-macros" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f1c75747a43b086df1a87fb2a889590bc0725e0abf54bba6d0c4bf7bd9e762c" + +[[package]] +name = "embedded-hal" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35949884794ad573cf46071e41c9b60efb0cb311e3ca01f7af807af1debc66ff" +dependencies = [ + "nb 0.1.3", + "void", +] + +[[package]] +name = "embedded-hal" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "361a90feb7004eca4019fb28352a9465666b24f840f5c3cddf0ff13920590b89" + +[[package]] +name = "embedded-hal-async" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c4c685bbef7fe13c3c6dd4da26841ed3980ef33e841cddfa15ce8a8fb3f1884" +dependencies = [ + "embedded-hal 1.0.0", +] + +[[package]] +name = "embedded-io" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef1a6892d9eef45c8fa6b9e0086428a2cca8491aca8f787c534a3d6d0bcb3ced" + +[[package]] +name = "embedded-io" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d" + +[[package]] +name = "embedded-io" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9eb1aa714776b75c7e67e1da744b81a129b3ff919c8712b5e1b32252c1f07cc7" + +[[package]] +name = "embedded-io-async" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ff09972d4073aa8c299395be75161d582e7629cd663171d62af73c8d50dba3f" +dependencies = [ + "embedded-io 0.6.1", +] + +[[package]] +name = "embedded-io-async" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2564b9f813c544241430e147d8bc454815ef9ac998878d30cc3055449f7fd4c0" +dependencies = [ + "embedded-io 0.7.1", +] + +[[package]] +name = "embedded-services" +version = "0.1.0" +dependencies = [ + "bitfield 0.17.0", + "cortex-m", + "critical-section", + "embassy-futures", + "embassy-sync", + "log", + "mctp-rs", + "paste", + "portable-atomic", + "serde", +] + +[[package]] +name = "encode_unicode" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" + +[[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 = "env_filter" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bf3c259d255ca70051b30e2e95b5446cdb8949ac4cd22c0d7fd634d89f568e2" +dependencies = [ + "log", + "regex", +] + +[[package]] +name = "env_logger" +version = "0.11.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f" +dependencies = [ + "anstream", + "anstyle", + "env_filter", + "jiff", + "log", +] + +[[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 = "espi-device" +version = "0.1.0" +source = "git+https://github.com/OpenDevicePartnership/haf-ec-service#09eda26a729738adbd177231600acdb981690375" +dependencies = [ + "bit-register", + "bitflags", + "num-traits", + "num_enum", + "static_assertions", + "subenum", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8591b0bcc8a98a64310a2fae1bb3e9b8564dd10e381e6e28010fde8e8e8568db" + +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "generator" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52f04ae4152da20c76fe800fa48659201d5cf627c5149ca0b707b69d7eef6cf9" +dependencies = [ + "cc", + "cfg-if", + "libc", + "log", + "rustversion", + "windows-link", + "windows-result", +] + +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d60b12902ba28e2730cd37e95b8c9223af2808df9e902d4df49588d1470606" +dependencies = [ + "byteorder", +] + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "ahash", +] + +[[package]] +name = "hashlink" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af" +dependencies = [ + "hashbrown", +] + +[[package]] +name = "heapless" +version = "0.7.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdc6457c0eb62c71aac4bc17216026d8410337c4126773b9c5daba343f17964f" +dependencies = [ + "atomic-polyfill", + "hash32 0.2.1", + "rustc_version 0.4.1", + "serde", + "spin", + "stable_deref_trait", +] + +[[package]] +name = "heapless" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bfb9eb618601c89945a70e254898da93b13be0388091d42117462b265bb3fad" +dependencies = [ + "hash32 0.3.1", + "stable_deref_trait", +] + +[[package]] +name = "heapless" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af2455f757db2b292a9b1768c4b70186d443bcb3b316252d6b540aec1cd89ed" +dependencies = [ + "hash32 0.3.1", + "stable_deref_trait", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "io-kit-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "617ee6cf8e3f66f3b4ea67a4058564628cde41901316e19f559e14c7c72c5e7b" +dependencies = [ + "core-foundation-sys", + "mach2", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" + +[[package]] +name = "jiff" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67e8da4c49d6d9909fe03361f9b620f58898859f5c7aded68351e85e71ecf50" +dependencies = [ + "jiff-static", + "log", + "portable-atomic", + "portable-atomic-util", + "serde_core", +] + +[[package]] +name = "jiff-static" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0c84ee7f197eca9a86c6fd6cb771e55eb991632f15f2bc3ca6ec838929e6e78" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "kdl" +version = "6.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81a29e7b50079ff44549f68c0becb1c73d7f6de2a4ea952da77966daf3d4761e" +dependencies = [ + "miette", + "num", + "winnow 0.6.24", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.180" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" + +[[package]] +name = "linux-raw-sys" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + +[[package]] +name = "litrs" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "loom" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "419e0dc8046cb947daa77eb95ae174acfbddb7673b4151f56d1eed8e93fbfaca" +dependencies = [ + "cfg-if", + "generator", + "scoped-tls", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "mach2" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d640282b302c0bb0a2a8e0233ead9035e3bed871f0b7e81fe4a1ec829765db44" +dependencies = [ + "libc", +] + +[[package]] +name = "maitake-sync" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6816ab14147f80234c675b80ed6dc4f440d8a1cefc158e766067aedb84c0bcd5" +dependencies = [ + "cordyceps", + "loom", + "mycelium-bitfield", + "pin-project", + "portable-atomic", +] + +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + +[[package]] +name = "mctp-rs" +version = "0.1.0" +dependencies = [ + "bit-register", + "espi-device", + "num_enum", + "smbus-pec", + "thiserror", +] + +[[package]] +name = "memchr" +version = "2.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" + +[[package]] +name = "miette" +version = "7.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f98efec8807c63c752b5bd61f862c165c115b0a35685bdcfd9238c7aeb592b7" +dependencies = [ + "cfg-if", + "unicode-width", +] + +[[package]] +name = "mycelium-bitfield" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24e0cc5e2c585acbd15c5ce911dff71e1f4d5313f43345873311c4f5efd741cc" + +[[package]] +name = "nb" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "801d31da0513b6ec5214e9bf433a77966320625a37860f910be265be6e18d06f" +dependencies = [ + "nb 1.1.0", +] + +[[package]] +name = "nb" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d5439c4ad607c3c23abf66de8c8bf57ba8adcd1f129e699851a6e43935d339d" + +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "num" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23" +dependencies = [ + "num-bigint", + "num-complex", + "num-integer", + "num-iter", + "num-rational", + "num-traits", +] + +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-complex" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_enum" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1207a7e20ad57b847bbddc6776b968420d38292bbfe2089accff5e19e82454c" +dependencies = [ + "num_enum_derive", + "rustversion", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff32365de1b6743cb203b710788263c44a03de03802daf96092f2da4fe6ba4d7" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "nusb" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f861541f15de120eae5982923d073bfc0c1a65466561988c82d6e197734c19e" +dependencies = [ + "atomic-waker", + "core-foundation", + "core-foundation-sys", + "futures-core", + "io-kit-sys", + "libc", + "log", + "once_cell", + "rustix", + "slab", + "windows-sys 0.48.0", +] + +[[package]] +name = "odp-service-common" +version = "0.1.0" +dependencies = [ + "embedded-services", + "static_cell", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pico-de-gallo" +version = "0.1.0" +dependencies = [ + "battery-service", + "bq40z50-rx", + "critical-section", + "embassy-futures", + "embassy-time", + "embedded-batteries-async", + "embedded-services", + "env_logger", + "log", + "odp-service-common", + "pico-de-gallo-hal", + "static_cell", + "tokio", +] + +[[package]] +name = "pico-de-gallo-hal" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc9fc0623409958ece1636dec2e6f60b3c4843461de8a7bae08ac70b118d21f8" +dependencies = [ + "embedded-hal 1.0.0", + "embedded-hal-async", + "pico-de-gallo-lib", + "tokio", +] + +[[package]] +name = "pico-de-gallo-internal" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de34f7394e7208ca19cc20291cc9e25843190be1a4a8b3f1127bb29eacef02c5" +dependencies = [ + "postcard-rpc", + "postcard-schema", + "serde", +] + +[[package]] +name = "pico-de-gallo-lib" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9b50787d5272f2d58658deae15f13f362486e4eeeac4eaa9348d88f1487a27c" +dependencies = [ + "embedded-hal 1.0.0", + "nusb", + "pico-de-gallo-internal", + "postcard-rpc", + "tokio", +] + +[[package]] +name = "pin-project" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "portable-atomic" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f89776e4d69bb58bc6993e99ffa1d11f228b839984854c7daeb5d37f87cbe950" + +[[package]] +name = "portable-atomic-util" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507" +dependencies = [ + "portable-atomic", +] + +[[package]] +name = "postcard" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6764c3b5dd454e283a30e6dfe78e9b31096d9e32036b5d1eaac7a6119ccb9a24" +dependencies = [ + "cobs", + "embedded-io 0.4.0", + "embedded-io 0.6.1", + "heapless 0.7.17", + "serde", +] + +[[package]] +name = "postcard-derive" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0232bd009a197ceec9cc881ba46f727fcd8060a2d8d6a9dde7a69030a6fe2bb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "postcard-rpc" +version = "0.11.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7e1944dfb9859e440511700c442edce3eacd5862f90f5a9997d004bd3553f3b" +dependencies = [ + "heapless 0.8.0", + "maitake-sync", + "nusb", + "portable-atomic", + "postcard", + "postcard-schema", + "serde", + "ssmarshal", + "thiserror", + "tokio", + "tracing", + "trait-variant", +] + +[[package]] +name = "postcard-schema" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9475666d89f42231a0a57da32d5f6ca7f9b5cd4c335ea1fe8f3278215b7a21ff" +dependencies = [ + "postcard-derive", + "serde", +] + +[[package]] +name = "power-policy-interface" +version = "0.1.0" +dependencies = [ + "bitfield 0.17.0", + "embassy-sync", + "embedded-batteries-async", + "embedded-services", + "log", + "num_enum", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + +[[package]] +name = "regex" +version = "1.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" + +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + +[[package]] +name = "rustc_version" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a" +dependencies = [ + "semver 0.9.0", +] + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver 1.0.27", +] + +[[package]] +name = "rustix" +version = "0.38.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "scoped-tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "semver" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403" +dependencies = [ + "semver-parser", +] + +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" + +[[package]] +name = "semver-parser" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "slab" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "smbus-pec" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca0763a680cd5d72b28f7bfc8a054c117d8841380a6ad4f72f05bd2a34217d3e" +dependencies = [ + "embedded-crc-macros", +] + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api", +] + +[[package]] +name = "ssmarshal" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3e6ad23b128192ed337dfa4f1b8099ced0c2bf30d61e551b65fda5916dbb850" +dependencies = [ + "encode_unicode", + "serde", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "static_cell" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0530892bb4fa575ee0da4b86f86c667132a94b74bb72160f58ee5a4afec74c23" +dependencies = [ + "portable-atomic", +] + +[[package]] +name = "subenum" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec3d08fe7078c57309d5c3d938e50eba95ba1d33b9c3a101a8465fc6861a5416" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "syn" +version = "2.0.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "tokio" +version = "1.49.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" +dependencies = [ + "bytes", + "pin-project-lite", + "tokio-macros", +] + +[[package]] +name = "tokio-macros" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex-automata", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "trait-variant" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70977707304198400eb4835a78f6a9f928bf41bba420deb8fdb175cd965d77a7" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "unicode-ident" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" + +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + +[[package]] +name = "unicode-width" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "vcell" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77439c1b53d2303b20d9459b1ade71a83c716e3f9c34f3228c00e6f185d6c002" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "void" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d" + +[[package]] +name = "volatile-register" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de437e2a6208b014ab52972a27e59b33fa2920d3e00fe05026167a1c509d19cc" +dependencies = [ + "vcell", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "winnow" +version = "0.6.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8d71a593cc5c42ad7876e2c1fda56f314f3754c084128833e64f1345ff8a03a" +dependencies = [ + "memchr", +] + +[[package]] +name = "winnow" +version = "0.7.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" +dependencies = [ + "memchr", +] + +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + +[[package]] +name = "yaml-rust2" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a1a1c0bc9823338a3bdf8c61f994f23ac004c6fa32c08cd152984499b445e8d" +dependencies = [ + "arraydeque", + "encoding_rs", + "hashlink", +] + +[[package]] +name = "zerocopy" +version = "0.8.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dafd85c832c1b68bbb4ec0c72c7f6f4fc5179627d2bc7c26b30e4c0cc11e76cc" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cb7e4e8436d9db52fbd6625dbf2f45243ab84994a72882ec8227b99e72b439a" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zmij" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02aae0f83f69aafc94776e879363e9771d7ecbffe2c7fbb6c14c5e00dfe88439" diff --git a/examples/pico-de-gallo/Cargo.toml b/examples/pico-de-gallo/Cargo.toml new file mode 100644 index 000000000..033917d6f --- /dev/null +++ b/examples/pico-de-gallo/Cargo.toml @@ -0,0 +1,44 @@ + +[workspace] + +[package] +name = "pico-de-gallo" +version = "0.1.0" +edition = "2024" + + +[package.metadata.cargo-machete] +ignored = ["critical-section"] + +[workspace.lints.rust] +warnings = "deny" + +[lints] +workspace = true + +[dependencies] +tokio = { version = "1.48.0", features = ["rt-multi-thread", "macros"] } +embedded-services = { path = "../../embedded-service", features = ["log"] } +embassy-time = { version = "0.5.0", features = [ + "log", + "std", + "generic-queue-8", +] } +embassy-futures = "0.1.2" + +embedded-batteries-async = "0.3" +battery-service = { path = "../../battery-service", features = ["log"] } +odp-service-common = { path = "../../odp-service-common" } + +pico-de-gallo-hal = "0.1.0" + +env_logger = "0.11" +log = "0.4.14" +static_cell = "2" + +critical-section = { version = "1.1", features = ["std"] } +bq40z50-rx = { version = "0.8", features = ["r5", "embassy-timeout"] } + +# Needed otherwise cargo will pull from git +[patch."https://github.com/OpenDevicePartnership/embedded-services"] +embedded-services = { path = "../../embedded-service" } diff --git a/examples/pico-de-gallo/src/bin/battery.rs b/examples/pico-de-gallo/src/bin/battery.rs new file mode 100644 index 000000000..feaa0d0a7 --- /dev/null +++ b/examples/pico-de-gallo/src/bin/battery.rs @@ -0,0 +1,306 @@ +//! Pico-de-gallo battery example +//! +//! Runs the ODP battery service in a std environment, using the [pico-de-gallo](https://github.com/OpenDevicePartnership/pico-de-gallo) +//! as a sensor bridge to a [Texas Instruments BQ40Z50-R5 battery fuel gauge EVK](https://github.com/OpenDevicePartnership/bq40z50). +//! +//! The hardware setup should be as follows: +//! +//! ___________ ___________ ____________ +//! | | | pico- | <--SDA--> | BQ40Z50-R5 | +//! | Host PC | <-USB--> | de-gallo | <--SCL--> | Fuel Gauge | +//! |___________| |___________| <--GND--> |____________| +//! +//! The host PC running the battery-service should be connected via USB to the pico-de-gallo. The BQ40Z50-R5 EVK should be connected +//! to the pico-de-gallo's I2C lines (don't forget GND!). The BQ40Z50-R5 EVK should be connected to the appropriate power supply and +//! battery cells, as outlined in its datasheet. +//! +//! The example can be run simply by typing `cargo run --bin battery` + +use battery_service as bs; +use bq40z50_rx::{BQ40Z50Error, Bq40z50R5}; +use embedded_batteries_async::smart_battery::{BatteryModeFields, SmartBattery}; +use odp_service_common::runnable_service::{Service, ServiceRunner}; +use static_cell::StaticCell; + +/// Platform specific battery errors. +#[derive(Debug)] +enum BatteryError { + /// Generic failure + Failed, +} + +impl embedded_batteries_async::smart_battery::Error for BatteryError { + fn kind(&self) -> embedded_batteries_async::smart_battery::ErrorKind { + embedded_batteries_async::smart_battery::ErrorKind::Other + } +} + +impl From> for BatteryError { + fn from(_value: BQ40Z50Error) -> Self { + BatteryError::Failed + } +} + +/// Platform specific battery controller. +struct Battery { + pub driver: Bq40z50R5, +} + +embedded_batteries_async::impl_smart_battery_for_wrapper_type!(Battery, driver, BatteryError); + +impl bs::controller::Controller for Battery { + type ControllerError = BatteryError; + + async fn initialize(&mut self) -> Result<(), Self::ControllerError> { + self.driver + // Milliamps + .set_battery_mode(BatteryModeFields::with_capacity_mode(BatteryModeFields::new(), false)) + .await + .inspect_err(|_| embedded_services::error!("FG: failed to initialize"))?; + + embedded_services::info!("FG: initialized"); + Ok(()) + } + + async fn get_static_data(&mut self) -> Result { + let mut buf = [0u8; 21]; + let mut new_msgs = bs::device::StaticBatteryMsgs { + design_capacity_mwh: match self.design_capacity().await? { + embedded_batteries_async::smart_battery::CapacityModeValue::CentiWattUnsigned(_) => 0xDEADBEEF, + embedded_batteries_async::smart_battery::CapacityModeValue::MilliAmpUnsigned(design_capacity) => { + design_capacity.into() + } + }, + design_voltage_mv: self.design_voltage().await?, + ..Default::default() + }; + + let buf_len = new_msgs.device_chemistry.len(); + self.device_chemistry(&mut buf[..buf_len]).await?; + new_msgs.device_chemistry.copy_from_slice(&buf[..buf_len]); + + Ok(new_msgs) + } + + async fn get_dynamic_data(&mut self) -> Result { + let new_msgs = bs::device::DynamicBatteryMsgs { + average_current_ma: self.average_current().await?, + battery_status: self.battery_status().await?.into(), + max_power_mw: self + .driver + .device + .max_turbo_power() + .read_async() + .await? + .max_turbo_power() + .unsigned_abs() + .into(), + battery_temp_dk: self.temperature().await?, + sus_power_mw: self + .driver + .device + .sus_turbo_power() + .read_async() + .await? + .sus_turbo_power() + .unsigned_abs() + .into(), + charging_current_ma: self.charging_current().await?, + charging_voltage_mv: self.charging_voltage().await?, + voltage_mv: self.voltage().await?, + current_ma: self.current().await?, + full_charge_capacity_mwh: match self.full_charge_capacity().await? { + embedded_batteries_async::smart_battery::CapacityModeValue::CentiWattUnsigned(_) => 0xDEADBEEF, + embedded_batteries_async::smart_battery::CapacityModeValue::MilliAmpUnsigned(capacity) => { + capacity.into() + } + }, + remaining_capacity_mwh: match self.remaining_capacity().await? { + embedded_batteries_async::smart_battery::CapacityModeValue::CentiWattUnsigned(_) => 0xDEADBEEF, + embedded_batteries_async::smart_battery::CapacityModeValue::MilliAmpUnsigned(capacity) => { + capacity.into() + } + }, + relative_soc_pct: self.relative_state_of_charge().await?.into(), + cycle_count: self.cycle_count().await?, + max_error_pct: self.max_error().await?.into(), + bmd_status: embedded_batteries_async::acpi::BmdStatusFlags::default(), + turbo_vload_mv: 0, + turbo_rhf_effective_mohm: 0, + }; + Ok(new_msgs) + } + + async fn get_device_event(&mut self) -> bs::controller::ControllerEvent { + loop { + tokio::task::yield_now().await; + } + } + + async fn ping(&mut self) -> Result<(), Self::ControllerError> { + if let Err(e) = self.driver.voltage().await { + embedded_services::error!("FG: failed to ping"); + Err(e.into()) + } else { + embedded_services::info!("FG: ping success"); + Ok(()) + } + } + + fn set_timeout(&mut self, _duration: embassy_time::Duration) { + embassy_time::Duration::from_secs(60); + } +} + +async fn init_state_machine(battery_service: &bs::Service<'static, 1>) -> Result<(), bs::context::ContextError> { + battery_service + .execute_event(battery_service::context::BatteryEvent { + event: battery_service::context::BatteryEventInner::DoInit, + device_id: bs::device::DeviceId(0), + }) + .await + .inspect_err(|f| embedded_services::debug!("Fuel gauge init error: {:?}", f))?; + + battery_service + .execute_event(battery_service::context::BatteryEvent { + event: battery_service::context::BatteryEventInner::PollStaticData, + device_id: bs::device::DeviceId(0), + }) + .await + .inspect_err(|f| embedded_services::debug!("Fuel gauge static data error: {:?}", f))?; + + battery_service + .execute_event(battery_service::context::BatteryEvent { + event: battery_service::context::BatteryEventInner::PollDynamicData, + device_id: bs::device::DeviceId(0), + }) + .await + .inspect_err(|f| embedded_services::debug!("Fuel gauge dynamic data error: {:?}", f))?; + + Ok(()) +} + +async fn recover_state_machine(battery_service: &battery_service::Service<'static, 1>) -> Result<(), ()> { + loop { + match battery_service + .execute_event(battery_service::context::BatteryEvent { + event: battery_service::context::BatteryEventInner::Timeout, + device_id: bs::device::DeviceId(0), + }) + .await + { + Ok(_) => { + embedded_services::info!("FG recovered!"); + return Ok(()); + } + Err(e) => match e { + battery_service::context::ContextError::StateError(e) => match e { + battery_service::context::StateMachineError::DeviceTimeout => { + embedded_services::trace!("Recovery failed, trying again after a backoff period"); + tokio::time::sleep(std::time::Duration::from_secs(10)).await; + } + battery_service::context::StateMachineError::NoOpRecoveryFailed => { + embedded_services::error!("Couldn't recover, reinit needed"); + return Err(()); + } + _ => embedded_services::debug!("Unexpected error"), + }, + _ => embedded_services::debug!("Unexpected error"), + }, + } + } +} + +pub async fn run_app(battery_service: battery_service::Service<'static, 1>) { + // Initialize battery state machine. + let mut retries = 5; + while let Err(e) = init_state_machine(&battery_service).await { + retries -= 1; + if retries <= 0 { + embedded_services::error!("Failed to initialize Battery: {:?}", e); + return; + } + tokio::time::sleep(std::time::Duration::from_secs(1)).await; + } + + let mut failures: u32 = 0; + let mut count: usize = 1; + loop { + tokio::time::sleep(std::time::Duration::from_secs(1)).await; + if count.is_multiple_of(const { 60 * 60 * 60 }) + && let Err(e) = battery_service + .execute_event(battery_service::context::BatteryEvent { + event: battery_service::context::BatteryEventInner::PollStaticData, + device_id: bs::device::DeviceId(0), + }) + .await + { + failures += 1; + embedded_services::error!("Fuel gauge static data error: {:#?}", e); + } + if let Err(e) = battery_service + .execute_event(battery_service::context::BatteryEvent { + event: battery_service::context::BatteryEventInner::PollDynamicData, + device_id: bs::device::DeviceId(0), + }) + .await + { + failures += 1; + embedded_services::error!("Fuel gauge dynamic data error: {:#?}", e); + } + + if failures > 10 { + failures = 0; + count = 0; + embedded_services::error!("FG: Too many errors, timing out and starting recovery..."); + if recover_state_machine(&battery_service).await.is_err() { + embedded_services::error!("FG: Fatal error"); + return; + } + } + + count = count.wrapping_add(1); + } +} + +#[tokio::main] +async fn main() { + env_logger::builder().filter_level(log::LevelFilter::Info).init(); + embedded_services::info!("host: battery example started"); + + embedded_services::debug!("Initializing battery service"); + embedded_services::init().await; + + let p = pico_de_gallo_hal::Hal::new(); + + static BATTERY_DEVICE: StaticCell = StaticCell::new(); + let device = BATTERY_DEVICE.init(bs::device::Device::new(bs::device::DeviceId(0))); + + static BATTERY_WRAPPER: StaticCell> = StaticCell::new(); + let wrapper = BATTERY_WRAPPER.init(bs::wrapper::Wrapper::new( + device, + Battery { + driver: Bq40z50R5::new(p.i2c(), p.delay()), + }, + )); + + static BATTERY_SERVICE: StaticCell> = StaticCell::new(); + let (battery_service, runner) = bs::Service::new( + BATTERY_SERVICE.init(Default::default()), + bs::InitParams { + config: Default::default(), + devices: [device], + }, + ) + .await + .expect("failed to initialize battery service"); + + // Run battery service + let _ = embassy_futures::join::join3( + tokio::spawn(runner.run()), + tokio::spawn(wrapper.process()), + tokio::spawn(run_app(battery_service)), + ) + .await; + unreachable!(); +} diff --git a/examples/rt633/Cargo.lock b/examples/rt633/Cargo.lock index 48a8c9fab..62a233a4b 100644 --- a/examples/rt633/Cargo.lock +++ b/examples/rt633/Cargo.lock @@ -2,41 +2,6 @@ # It is not intended for manual editing. version = 4 -[[package]] -name = "aho-corasick" -version = "1.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" -dependencies = [ - "memchr", -] - -[[package]] -name = "aquamarine" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f50776554130342de4836ba542aa85a4ddb361690d7e8df13774d7284c3d5c2" -dependencies = [ - "include_dir", - "itertools 0.10.5", - "proc-macro-error2", - "proc-macro2", - "quote", - "syn 2.0.104", -] - -[[package]] -name = "autocfg" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" - -[[package]] -name = "az" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b7e4c2464d97fe331d41de9d5db0def0a96f4d823b8b32a2efd503578988973" - [[package]] name = "bare-metal" version = "0.2.5" @@ -46,43 +11,6 @@ dependencies = [ "rustc_version", ] -[[package]] -name = "battery-service" -version = "0.1.0" -dependencies = [ - "defmt 0.3.100", - "embassy-futures", - "embassy-sync 0.8.0", - "embassy-time", - "embedded-batteries-async", - "embedded-services", -] - -[[package]] -name = "bincode" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36eaf5d7b090263e8150820482d5d93cd964a81e4019913c972f4edcc6edb740" -dependencies = [ - "unty", -] - -[[package]] -name = "bit-register" -version = "0.1.0" -source = "git+https://github.com/OpenDevicePartnership/odp-utilities?rev=2f79d238#2f79d238149049d458199a9a9129b54be7893aee" -dependencies = [ - "num-traits", -] - -[[package]] -name = "bit-register" -version = "0.1.0" -source = "git+https://github.com/OpenDevicePartnership/odp-utilities#583015c08ad9855f310bdb25d5cf9abff77b5e08" -dependencies = [ - "num-traits", -] - [[package]] name = "bitfield" version = "0.13.2" @@ -95,129 +23,12 @@ version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c821a6e124197eb56d907ccc2188eab1038fb919c914f47976e64dd8dbc855d1" -[[package]] -name = "bitfield" -version = "0.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f798d2d157e547aa99aab0967df39edd0b70307312b6f8bd2848e6abe40896e0" - -[[package]] -name = "bitfield" -version = "0.19.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db1bcd90f88eabbf0cadbfb87a45bceeaebcd3b4bc9e43da379cd2ef0162590d" -dependencies = [ - "bitfield-macros", -] - -[[package]] -name = "bitfield-macros" -version = "0.19.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3787a07661997bfc05dd3431e379c0188573f78857080cf682e1393ab8e4d64c" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.104", -] - -[[package]] -name = "bitfield-struct" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2be5a46ba01b60005ae2c51a36a29cfe134bcacae2dd5cedcd4615fbaad1494b" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.104", -] - -[[package]] -name = "bitfield-struct" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8769c4854c5ada2852ddf6fd09d15cf43d4c2aaeccb4de6432f5402f08a6003b" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.104", -] - [[package]] name = "bitflags" version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" -[[package]] -name = "bitflags" -version = "2.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" - -[[package]] -name = "bitvec" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" -dependencies = [ - "funty", - "radium", - "tap", - "wyz", -] - -[[package]] -name = "bq40z50-rx" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09b6faf600295f12c3fb99b45266bc9140af5c344b08f2705bc06bfa0e8b549e" -dependencies = [ - "device-driver", - "embedded-batteries-async", - "embedded-hal 1.0.0", - "embedded-hal-async", - "smbus-pec", -] - -[[package]] -name = "bytemuck" -version = "1.23.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c76a5792e44e4abe34d3abf15636779261d45a7450612059293d1d2cfc63422" - -[[package]] -name = "byteorder" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" - -[[package]] -name = "cc" -version = "1.2.60" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43c5703da9466b66a946814e1adf53ea2c90f10063b86290cc9eb67ce3478a20" -dependencies = [ - "find-msvc-tools", - "shlex", -] - -[[package]] -name = "cfg-if" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" - -[[package]] -name = "cordyceps" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "688d7fbb8092b8de775ef2536f36c8c31f2bc4006ece2e8d8ad2d17d00ce0a2a" -dependencies = [ - "loom", - "tracing", -] - [[package]] name = "cortex-m" version = "0.7.7" @@ -226,78 +37,16 @@ checksum = "8ec610d8f49840a5b376c69663b6369e71f4b34484b9b2eb29fb918d92516cb9" dependencies = [ "bare-metal", "bitfield 0.13.2", - "critical-section", - "embedded-hal 0.2.7", + "embedded-hal", "volatile-register", ] -[[package]] -name = "cortex-m-rt" -version = "0.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "801d4dec46b34c299ccf6b036717ae0fce602faa4f4fe816d9013b9a7c9f5ba6" -dependencies = [ - "cortex-m-rt-macros", -] - -[[package]] -name = "cortex-m-rt-macros" -version = "0.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e37549a379a9e0e6e576fd208ee60394ccb8be963889eebba3ffe0980364f472" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.104", -] - [[package]] name = "critical-section" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" -[[package]] -name = "crunchy" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" - -[[package]] -name = "darling" -version = "0.20.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" -dependencies = [ - "darling_core", - "darling_macro", -] - -[[package]] -name = "darling_core" -version = "0.20.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" -dependencies = [ - "fnv", - "ident_case", - "proc-macro2", - "quote", - "strsim", - "syn 2.0.104", -] - -[[package]] -name = "darling_macro" -version = "0.20.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" -dependencies = [ - "darling_core", - "quote", - "syn 2.0.104", -] - [[package]] name = "defmt" version = "0.3.100" @@ -313,7 +62,7 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "548d977b6da32fa1d1fda2876453da1e7df63ad0304c8b3dae4dbe7b96f39b78" dependencies = [ - "bitflags 1.3.2", + "bitflags", "defmt-macros", ] @@ -327,7 +76,7 @@ dependencies = [ "proc-macro-error2", "proc-macro2", "quote", - "syn 2.0.104", + "syn", ] [[package]] @@ -350,1205 +99,181 @@ dependencies = [ ] [[package]] -name = "device-driver" -version = "1.0.6" +name = "embedded-hal" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1298272ea07037196af2fe8d1eb50792206f45476d79eefa435432b9323cf488" +checksum = "35949884794ad573cf46071e41c9b60efb0cb311e3ca01f7af807af1debc66ff" dependencies = [ - "embedded-io 0.6.1", - "embedded-io-async 0.6.1", + "nb 0.1.3", + "void", ] [[package]] -name = "document-features" -version = "0.2.11" +name = "mimxrt600-fcb" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95249b50c6c185bee49034bcb378a49dc2b5dff0be90ff6616d31d64febab05d" +checksum = "b1ebf867c0f22d440f0ad393c28d2007af23c08953b9111379dd711083ae19a9" dependencies = [ - "litrs", + "bitfield 0.15.0", ] [[package]] -name = "either" -version = "1.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" - -[[package]] -name = "embassy-embedded-hal" -version = "0.5.0" +name = "nb" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "554e3e840696f54b4c9afcf28a0f24da431c927f4151040020416e7393d6d0d8" +checksum = "801d31da0513b6ec5214e9bf433a77966320625a37860f910be265be6e18d06f" dependencies = [ - "embassy-futures", - "embassy-hal-internal 0.3.0", - "embassy-sync 0.7.2", - "embassy-time", - "embedded-hal 0.2.7", - "embedded-hal 1.0.0", - "embedded-hal-async", - "embedded-storage", - "embedded-storage-async", "nb 1.1.0", ] [[package]] -name = "embassy-embedded-hal" -version = "0.6.0" +name = "nb" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0641612053b2f34fc250bb63f6630ae75de46e02ade7f457268447081d709ce" -dependencies = [ - "embassy-futures", - "embassy-hal-internal 0.4.0", - "embassy-sync 0.8.0", - "embedded-hal 0.2.7", - "embedded-hal 1.0.0", - "embedded-hal-async", - "embedded-storage", - "embedded-storage-async", - "nb 1.1.0", -] +checksum = "8d5439c4ad607c3c23abf66de8c8bf57ba8adcd1f129e699851a6e43935d339d" [[package]] -name = "embassy-executor" -version = "0.10.0" +name = "panic-probe" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d0d3b15c9d7dc4fec1d8cb77112472fb008b3b28c51ad23838d83587a6d2f1e" +checksum = "4047d9235d1423d66cc97da7d07eddb54d4f154d6c13805c6d0793956f4f25b0" dependencies = [ - "cordyceps", "cortex-m", - "critical-section", - "defmt 1.0.1", - "document-features", - "embassy-executor-macros", - "embassy-executor-timer-queue", + "defmt 0.3.100", ] [[package]] -name = "embassy-executor-macros" -version = "0.8.0" +name = "proc-macro-error-attr2" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d11a246f53de5f97a387f40ac24726817cd0b6f833e7603baac784f29d6ff276" +checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" dependencies = [ - "darling", "proc-macro2", "quote", - "syn 2.0.104", ] [[package]] -name = "embassy-executor-timer-queue" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fc328bf943af66b80b98755db9106bf7e7471b0cf47dc8559cd9a6be504cc9c" - -[[package]] -name = "embassy-futures" -version = "0.1.2" +name = "proc-macro-error2" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc2d050bdc5c21e0862a89256ed8029ae6c290a93aecefc73084b3002cdebb01" +checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" +dependencies = [ + "proc-macro-error-attr2", + "proc-macro2", + "quote", + "syn", +] [[package]] -name = "embassy-hal-internal" -version = "0.3.0" +name = "proc-macro2" +version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95285007a91b619dc9f26ea8f55452aa6c60f7115a4edc05085cd2bd3127cd7a" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" dependencies = [ - "cortex-m", - "critical-section", - "defmt 1.0.1", - "num-traits", + "unicode-ident", ] [[package]] -name = "embassy-hal-internal" -version = "0.4.0" +name = "quote" +version = "1.0.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f10ce10a4dfdf6402d8e9bd63128986b96a736b1a0a6680547ed2ac55d55dba" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" dependencies = [ - "num-traits", + "proc-macro2", ] [[package]] -name = "embassy-imxrt" +name = "rt633-examples" version = "0.1.0" -source = "git+https://github.com/OpenDevicePartnership/embassy-imxrt#581f66e68ceaefb77e35f230dfb114322912dd6b" dependencies = [ - "cfg-if", - "cortex-m", - "cortex-m-rt", - "critical-section", - "defmt 1.0.1", - "document-features", - "embassy-embedded-hal 0.5.0", - "embassy-futures", - "embassy-hal-internal 0.3.0", - "embassy-sync 0.7.2", - "embassy-time", - "embassy-time-driver", - "embassy-time-queue-utils", - "embedded-hal 0.2.7", - "embedded-hal 1.0.0", - "embedded-hal-async", - "embedded-hal-nb", - "embedded-io 0.6.1", - "embedded-io-async 0.6.1", - "embedded-mcu-hal", - "embedded-storage", - "fixed", - "itertools 0.11.0", + "defmt 0.3.100", + "defmt-rtt", "mimxrt600-fcb", - "mimxrt633s-pac", - "mimxrt685s-pac", - "nb 1.1.0", - "paste", - "rand_core", - "storage_bus", + "panic-probe", ] [[package]] -name = "embassy-sync" -version = "0.7.2" +name = "rustc_version" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73974a3edbd0bd286759b3d483540f0ebef705919a5f56f4fc7709066f71689b" +checksum = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a" dependencies = [ - "cfg-if", - "critical-section", - "defmt 1.0.1", - "embedded-io-async 0.6.1", - "futures-core", - "futures-sink", - "heapless 0.8.0", + "semver", ] [[package]] -name = "embassy-sync" -version = "0.8.0" +name = "semver" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7bbd85cf5a5ae56bdf26f618364af642d1d0a4e245cdd75cd9aabda382f65a81" +checksum = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403" dependencies = [ - "cfg-if", - "critical-section", - "defmt 1.0.1", - "embedded-io-async 0.7.0", - "futures-core", - "futures-sink", - "heapless 0.9.2", + "semver-parser", ] [[package]] -name = "embassy-time" -version = "0.5.1" +name = "semver-parser" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "592b0c143ec626e821d4d90da51a2bd91d559d6c442b7c74a47d368c9e23d97a" -dependencies = [ - "cfg-if", - "critical-section", - "defmt 1.0.1", - "document-features", - "embassy-time-driver", - "embedded-hal 0.2.7", - "embedded-hal 1.0.0", - "embedded-hal-async", - "futures-core", -] +checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" [[package]] -name = "embassy-time-driver" -version = "0.2.2" +name = "syn" +version = "2.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ee71af1b3a0deaa53eaf2d39252f83504c853646e472400b763060389b9fcc9" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" dependencies = [ - "document-features", + "proc-macro2", + "quote", + "unicode-ident", ] [[package]] -name = "embassy-time-queue-utils" -version = "0.3.2" +name = "thiserror" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "168297bf80aaf114b3c9ad589bf38b01b3009b9af7f97cd18086c5bbf96f5693" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" dependencies = [ - "embassy-executor-timer-queue", - "heapless 0.9.2", + "thiserror-impl", ] [[package]] -name = "embedded-batteries" -version = "0.2.1" +name = "thiserror-impl" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31e14d288a59ef41f4e05468eae9b1c9fef6866977cea86d3f1a1ced295b6cab" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ - "bitfield-struct 0.10.1", - "bitflags 2.9.1", - "defmt 0.3.100", - "embedded-hal 1.0.0", - "zerocopy", + "proc-macro2", + "quote", + "syn", ] [[package]] -name = "embedded-batteries" -version = "0.3.4" +name = "unicode-ident" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40f975432b4e146342a1589c563cffab6b7a692024cb511bf87b6bfe78c84125" -dependencies = [ - "bitfield-struct 0.12.1", - "bitflags 2.9.1", - "defmt 0.3.100", - "embedded-hal 1.0.0", - "zerocopy", -] +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] -name = "embedded-batteries-async" -version = "0.3.4" +name = "vcell" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3bf0e4be67770cfc31f1cea8b73baf98c0baf2c57d6bd8c3a4c315acb1d8bd4" -dependencies = [ - "bitfield-struct 0.12.1", - "defmt 0.3.100", - "embedded-batteries 0.3.4", - "embedded-hal 1.0.0", -] - -[[package]] -name = "embedded-cfu-protocol" -version = "0.2.0" -source = "git+https://github.com/OpenDevicePartnership/embedded-cfu#a4cc8707842b878048447abbf2af4efa79fed368" -dependencies = [ - "defmt 0.3.100", - "embedded-io-async 0.6.1", -] +checksum = "77439c1b53d2303b20d9459b1ade71a83c716e3f9c34f3228c00e6f185d6c002" [[package]] -name = "embedded-crc-macros" -version = "1.0.0" +name = "void" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f1c75747a43b086df1a87fb2a889590bc0725e0abf54bba6d0c4bf7bd9e762c" +checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d" [[package]] -name = "embedded-hal" -version = "0.2.7" +name = "volatile-register" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35949884794ad573cf46071e41c9b60efb0cb311e3ca01f7af807af1debc66ff" +checksum = "de437e2a6208b014ab52972a27e59b33fa2920d3e00fe05026167a1c509d19cc" dependencies = [ - "nb 0.1.3", - "void", -] - -[[package]] -name = "embedded-hal" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "361a90feb7004eca4019fb28352a9465666b24f840f5c3cddf0ff13920590b89" - -[[package]] -name = "embedded-hal-async" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c4c685bbef7fe13c3c6dd4da26841ed3980ef33e841cddfa15ce8a8fb3f1884" -dependencies = [ - "embedded-hal 1.0.0", -] - -[[package]] -name = "embedded-hal-nb" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fba4268c14288c828995299e59b12babdbe170f6c6d73731af1b4648142e8605" -dependencies = [ - "embedded-hal 1.0.0", - "nb 1.1.0", -] - -[[package]] -name = "embedded-io" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d" - -[[package]] -name = "embedded-io" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9eb1aa714776b75c7e67e1da744b81a129b3ff919c8712b5e1b32252c1f07cc7" - -[[package]] -name = "embedded-io-async" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ff09972d4073aa8c299395be75161d582e7629cd663171d62af73c8d50dba3f" -dependencies = [ - "embedded-io 0.6.1", -] - -[[package]] -name = "embedded-io-async" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2564b9f813c544241430e147d8bc454815ef9ac998878d30cc3055449f7fd4c0" -dependencies = [ - "embedded-io 0.7.1", -] - -[[package]] -name = "embedded-mcu-hal" -version = "0.1.0" -source = "git+https://github.com/OpenDevicePartnership/embedded-mcu#146fda33807af6aeabcade513914da95c926fbc9" -dependencies = [ - "defmt 1.0.1", + "vcell", ] -[[package]] +[[patch.unused]] name = "embedded-services" version = "0.1.0" -dependencies = [ - "bitfield 0.17.0", - "bitvec", - "cortex-m", - "cortex-m-rt", - "critical-section", - "defmt 0.3.100", - "embassy-sync 0.8.0", - "embassy-time", - "embedded-batteries-async", - "embedded-cfu-protocol", - "embedded-usb-pd", - "heapless 0.9.2", - "mctp-rs", - "num_enum", - "portable-atomic", - "serde", - "uuid", -] - -[[package]] -name = "embedded-storage" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a21dea9854beb860f3062d10228ce9b976da520a73474aed3171ec276bc0c032" - -[[package]] -name = "embedded-storage-async" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1763775e2323b7d5f0aa6090657f5e21cfa02ede71f5dc40eead06d64dcd15cc" -dependencies = [ - "embedded-storage", -] - -[[package]] -name = "embedded-usb-pd" -version = "0.1.0" -source = "git+https://github.com/OpenDevicePartnership/embedded-usb-pd#21d0e228d21ddc6ccaeffc01d98ef9a5b87941ef" -dependencies = [ - "aquamarine", - "bincode", - "bitfield 0.19.1", - "defmt 0.3.100", - "embedded-hal-async", -] - -[[package]] -name = "espi-device" -version = "0.1.0" -source = "git+https://github.com/OpenDevicePartnership/haf-ec-service#8cdd61095471903b1b438dbb0eee142676cc3d74" -dependencies = [ - "bit-register 0.1.0 (git+https://github.com/OpenDevicePartnership/odp-utilities?rev=2f79d238)", - "bitflags 2.9.1", - "num-traits", - "num_enum", - "static_assertions", - "subenum", -] - -[[package]] -name = "espi-service" -version = "0.1.0" -dependencies = [ - "cortex-m", - "cortex-m-rt", - "defmt 0.3.100", - "embassy-futures", - "embassy-imxrt", - "embassy-sync 0.8.0", - "embedded-services", - "mctp-rs", -] - -[[package]] -name = "find-msvc-tools" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" - -[[package]] -name = "fixed" -version = "1.29.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "707070ccf8c4173548210893a0186e29c266901b71ed20cd9e2ca0193dfe95c3" -dependencies = [ - "az", - "bytemuck", - "half", - "typenum", -] - -[[package]] -name = "fnv" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" - -[[package]] -name = "funty" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" - -[[package]] -name = "futures-core" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" - -[[package]] -name = "futures-sink" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" - -[[package]] -name = "generator" -version = "0.8.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52f04ae4152da20c76fe800fa48659201d5cf627c5149ca0b707b69d7eef6cf9" -dependencies = [ - "cc", - "cfg-if", - "libc", - "log", - "rustversion", - "windows-link", - "windows-result", -] - -[[package]] -name = "half" -version = "2.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "459196ed295495a68f7d7fe1d84f6c4b7ff0e21fe3017b2f283c6fac3ad803c9" -dependencies = [ - "cfg-if", - "crunchy", -] - -[[package]] -name = "hash32" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47d60b12902ba28e2730cd37e95b8c9223af2808df9e902d4df49588d1470606" -dependencies = [ - "byteorder", -] - -[[package]] -name = "heapless" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bfb9eb618601c89945a70e254898da93b13be0388091d42117462b265bb3fad" -dependencies = [ - "hash32", - "stable_deref_trait", -] - -[[package]] -name = "heapless" -version = "0.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2af2455f757db2b292a9b1768c4b70186d443bcb3b316252d6b540aec1cd89ed" -dependencies = [ - "hash32", - "stable_deref_trait", -] - -[[package]] -name = "heck" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" - -[[package]] -name = "ident_case" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" - -[[package]] -name = "include_dir" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "923d117408f1e49d914f1a379a309cffe4f18c05cf4e3d12e613a15fc81bd0dd" -dependencies = [ - "include_dir_macros", -] - -[[package]] -name = "include_dir_macros" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cab85a7ed0bd5f0e76d93846e0147172bed2e2d3f859bcc33a8d9699cad1a75" -dependencies = [ - "proc-macro2", - "quote", -] - -[[package]] -name = "itertools" -version = "0.10.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" -dependencies = [ - "either", -] - -[[package]] -name = "itertools" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57" -dependencies = [ - "either", -] - -[[package]] -name = "lazy_static" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" - -[[package]] -name = "libc" -version = "0.2.185" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52ff2c0fe9bc6cb6b14a0592c2ff4fa9ceb83eea9db979b0487cd054946a2b8f" - -[[package]] -name = "litrs" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4ce301924b7887e9d637144fdade93f9dfff9b60981d4ac161db09720d39aa5" - -[[package]] -name = "log" -version = "0.4.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" - -[[package]] -name = "loom" -version = "0.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "419e0dc8046cb947daa77eb95ae174acfbddb7673b4151f56d1eed8e93fbfaca" -dependencies = [ - "cfg-if", - "generator", - "scoped-tls", - "tracing", - "tracing-subscriber", -] - -[[package]] -name = "matchers" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" -dependencies = [ - "regex-automata", -] - -[[package]] -name = "mctp-rs" -version = "0.1.0" -source = "git+https://github.com/dymk/mctp-rs#4456c65131366acd5e605c7d88a707881fa9e9f0" -dependencies = [ - "bit-register 0.1.0 (git+https://github.com/OpenDevicePartnership/odp-utilities)", - "defmt 0.3.100", - "embedded-batteries 0.2.1", - "espi-device", - "num_enum", - "smbus-pec", - "thiserror", -] - -[[package]] -name = "memchr" -version = "2.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" - -[[package]] -name = "mimxrt600-fcb" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1ebf867c0f22d440f0ad393c28d2007af23c08953b9111379dd711083ae19a9" -dependencies = [ - "bitfield 0.15.0", -] - -[[package]] -name = "mimxrt633s-pac" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843c1c63c367293e4fa270cc161b5bdfef55b4c6a0a18768f737241fb24be70c" -dependencies = [ - "cortex-m", - "cortex-m-rt", - "critical-section", - "defmt 0.3.100", - "vcell", -] - -[[package]] -name = "mimxrt685s-pac" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c0b80e5add9dc74500acbb1ca70248e237d242b77988631e41db40a225f3a40" -dependencies = [ - "cortex-m", - "cortex-m-rt", - "critical-section", - "defmt 0.3.100", - "vcell", -] - -[[package]] -name = "nb" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "801d31da0513b6ec5214e9bf433a77966320625a37860f910be265be6e18d06f" -dependencies = [ - "nb 1.1.0", -] - -[[package]] -name = "nb" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d5439c4ad607c3c23abf66de8c8bf57ba8adcd1f129e699851a6e43935d339d" - -[[package]] -name = "nu-ansi-term" -version = "0.50.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" -dependencies = [ - "windows-sys", -] - -[[package]] -name = "num-traits" -version = "0.2.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" -dependencies = [ - "autocfg", -] - -[[package]] -name = "num_enum" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26" -dependencies = [ - "num_enum_derive", - "rustversion", -] - -[[package]] -name = "num_enum_derive" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.104", -] - -[[package]] -name = "once_cell" -version = "1.21.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" - -[[package]] -name = "panic-probe" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4047d9235d1423d66cc97da7d07eddb54d4f154d6c13805c6d0793956f4f25b0" -dependencies = [ - "cortex-m", - "defmt 0.3.100", -] - -[[package]] -name = "paste" -version = "1.0.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" - -[[package]] -name = "pin-project-lite" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" - -[[package]] -name = "portable-atomic" -version = "1.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" - -[[package]] -name = "proc-macro-error-attr2" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" -dependencies = [ - "proc-macro2", - "quote", -] - -[[package]] -name = "proc-macro-error2" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" -dependencies = [ - "proc-macro-error-attr2", - "proc-macro2", - "quote", - "syn 2.0.104", -] - -[[package]] -name = "proc-macro2" -version = "1.0.95" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "quote" -version = "1.0.40" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "radium" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" - -[[package]] -name = "rand_core" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" - -[[package]] -name = "regex-automata" -version = "0.4.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" -dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax", -] - -[[package]] -name = "regex-syntax" -version = "0.8.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" - -[[package]] -name = "rt633-examples" -version = "0.1.0" -dependencies = [ - "battery-service", - "bq40z50-rx", - "cortex-m", - "cortex-m-rt", - "defmt 0.3.100", - "defmt-rtt", - "embassy-embedded-hal 0.6.0", - "embassy-executor", - "embassy-imxrt", - "embassy-sync 0.8.0", - "embassy-time", - "embedded-batteries-async", - "embedded-services", - "espi-service", - "mimxrt600-fcb", - "panic-probe", - "static_cell", -] - -[[package]] -name = "rustc_version" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a" -dependencies = [ - "semver", -] - -[[package]] -name = "rustversion" -version = "1.0.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" - -[[package]] -name = "scoped-tls" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" - -[[package]] -name = "semver" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403" -dependencies = [ - "semver-parser", -] - -[[package]] -name = "semver-parser" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" - -[[package]] -name = "serde" -version = "1.0.219" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_derive" -version = "1.0.219" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.104", -] - -[[package]] -name = "sharded-slab" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" -dependencies = [ - "lazy_static", -] - -[[package]] -name = "shlex" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" - -[[package]] -name = "smallvec" -version = "1.15.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" - -[[package]] -name = "smbus-pec" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca0763a680cd5d72b28f7bfc8a054c117d8841380a6ad4f72f05bd2a34217d3e" -dependencies = [ - "embedded-crc-macros", -] - -[[package]] -name = "stable_deref_trait" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" - -[[package]] -name = "static_assertions" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" - -[[package]] -name = "static_cell" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0530892bb4fa575ee0da4b86f86c667132a94b74bb72160f58ee5a4afec74c23" -dependencies = [ - "portable-atomic", -] - -[[package]] -name = "storage_bus" -version = "0.1.0" -source = "git+https://github.com/OpenDevicePartnership/embedded-mcu#146fda33807af6aeabcade513914da95c926fbc9" - -[[package]] -name = "strsim" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" - -[[package]] -name = "subenum" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f5d5dfb8556dd04017db5e318bbeac8ab2b0c67b76bf197bfb79e9b29f18ecf" -dependencies = [ - "heck", - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "syn" -version = "1.0.109" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "syn" -version = "2.0.104" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "tap" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" - -[[package]] -name = "thiserror" -version = "2.0.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3467d614147380f2e4e374161426ff399c91084acd2363eaf549172b3d5e60c0" -dependencies = [ - "thiserror-impl", -] - -[[package]] -name = "thiserror-impl" -version = "2.0.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c5e1be1c48b9172ee610da68fd9cd2770e7a4056cb3fc98710ee6906f0c7960" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.104", -] - -[[package]] -name = "thread_local" -version = "1.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "tracing" -version = "0.1.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" -dependencies = [ - "pin-project-lite", - "tracing-attributes", - "tracing-core", -] - -[[package]] -name = "tracing-attributes" -version = "0.1.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.104", -] - -[[package]] -name = "tracing-core" -version = "0.1.36" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" -dependencies = [ - "once_cell", - "valuable", -] - -[[package]] -name = "tracing-log" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" -dependencies = [ - "log", - "once_cell", - "tracing-core", -] - -[[package]] -name = "tracing-subscriber" -version = "0.3.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" -dependencies = [ - "matchers", - "nu-ansi-term", - "once_cell", - "regex-automata", - "sharded-slab", - "smallvec", - "thread_local", - "tracing", - "tracing-core", - "tracing-log", -] - -[[package]] -name = "typenum" -version = "1.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" - -[[package]] -name = "unicode-ident" -version = "1.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" - -[[package]] -name = "unty" -version = "0.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d49784317cd0d1ee7ec5c716dd598ec5b4483ea832a2dced265471cc0f690ae" - -[[package]] -name = "uuid" -version = "1.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3cf4199d1e5d15ddd86a694e4d0dffa9c323ce759fea589f00fef9d81cc1931d" - -[[package]] -name = "valuable" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" - -[[package]] -name = "vcell" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77439c1b53d2303b20d9459b1ade71a83c716e3f9c34f3228c00e6f185d6c002" - -[[package]] -name = "void" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d" - -[[package]] -name = "volatile-register" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de437e2a6208b014ab52972a27e59b33fa2920d3e00fe05026167a1c509d19cc" -dependencies = [ - "vcell", -] - -[[package]] -name = "windows-link" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" - -[[package]] -name = "windows-result" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows-sys" -version = "0.61.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" -dependencies = [ - "windows-link", -] - -[[package]] -name = "wyz" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" -dependencies = [ - "tap", -] - -[[package]] -name = "zerocopy" -version = "0.8.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1039dd0d3c310cf05de012d8a39ff557cb0d23087fd44cad61df08fc31907a2f" -dependencies = [ - "zerocopy-derive", -] - -[[package]] -name = "zerocopy-derive" -version = "0.8.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.104", -] diff --git a/examples/rt633/Cargo.toml b/examples/rt633/Cargo.toml index 80b1d6b29..479835c8b 100644 --- a/examples/rt633/Cargo.toml +++ b/examples/rt633/Cargo.toml @@ -10,48 +10,17 @@ license = "MIT" warnings = "deny" [package.metadata.cargo-machete] -ignored = ["cortex-m", "cortex-m-rt"] +ignored = ["defmt"] [lints] workspace = true [dependencies] -cortex-m = { version = "0.7.7", features = [ - "inline-asm", - "critical-section-single-core", -] } -cortex-m-rt = "0.7.3" defmt = "0.3.6" defmt-rtt = "0.4.0" panic-probe = { version = "0.3.1", features = ["print-defmt"] } -embassy-imxrt = { git = "https://github.com/OpenDevicePartnership/embassy-imxrt", features = [ - "defmt", - "time-driver-os-timer", - "time", - "mimxrt633s", - "unstable-pac", -] } - -embassy-sync = { version = "0.8", features = ["defmt"] } -embassy-executor = { version = "0.10.0", features = [ - "platform-cortex-m", - "executor-thread", - "executor-interrupt", - "defmt", -] } -embassy-time = { version = "0.5.1", features = [ - "defmt", - "defmt-timestamp-uptime", -] } mimxrt600-fcb = "0.2.0" -espi-service = { path = "../../espi-service", features = ["defmt"] } -embedded-services = { path = "../../embedded-service", features = ["defmt"] } -embedded-batteries-async = { version = "0.3", features = ["defmt"] } -battery-service = { path = "../../battery-service", features = ["defmt"] } -bq40z50-rx = { version = "0.8", features = ["r5"] } -static_cell = "2.1.0" -embassy-embedded-hal = { version = "0.6.0", default-features = false } # Needed otherwise cargo will pull from git [patch."https://github.com/OpenDevicePartnership/embedded-services"] diff --git a/examples/rt633/src/bin/espi.rs b/examples/rt633/src/bin/espi.rs deleted file mode 100644 index 743dc4db0..000000000 --- a/examples/rt633/src/bin/espi.rs +++ /dev/null @@ -1,177 +0,0 @@ -#![no_std] -#![no_main] - -extern crate rt633_examples; - -use core::slice::{self}; - -use defmt::info; -use embassy_executor::Spawner; -use embassy_imxrt::bind_interrupts; -use embassy_imxrt::espi::BaseOrAsz; -use embassy_imxrt::espi::{Base, Capabilities, Config, Direction, Espi, InterruptHandler, Len, Maxspd, PortConfig}; -use embassy_imxrt::peripherals::ESPI; -use {defmt_rtt as _, panic_probe as _}; - -// Mock battery service -mod battery_service { - use defmt::info; - use embassy_sync::blocking_mutex::raw::ThreadModeRawMutex; - use embassy_sync::once_lock::OnceLock; - use embassy_sync::signal::Signal; - use embedded_services::comms::{self, EndpointID, External, Internal}; - use embedded_services::ec_type; - - struct Service { - endpoint: comms::Endpoint, - - // This is can be an Embassy signal or channel or whatever Embassy async notification construct - signal: Signal, - } - - impl Service { - fn new() -> Self { - Service { - endpoint: comms::Endpoint::uninit(EndpointID::Internal(Internal::Battery)), - signal: Signal::new(), - } - } - } - - impl comms::MailboxDelegate for Service { - fn receive(&self, message: &comms::Message) -> Result<(), comms::MailboxDelegateError> { - let msg = message - .data - .get::() - .ok_or(comms::MailboxDelegateError::MessageNotFound)?; - - self.signal.signal(*msg); - - Ok(()) - } - } - - static BATTERY_SERVICE: OnceLock = OnceLock::new(); - - // Initialize battery service - pub async fn init() { - let battery_service = BATTERY_SERVICE.get_or_init(Service::new); - - comms::register_endpoint(battery_service, &battery_service.endpoint) - .await - .unwrap(); - } - - // Service to update the battery value in the memory map periodically - #[embassy_executor::task] - pub async fn battery_update_service() { - let battery_service = BATTERY_SERVICE.get().await; - - let mut battery_remain_cap = u32::MAX; - - loop { - battery_service - .endpoint - .send( - EndpointID::External(External::Host), - &ec_type::message::BatteryMessage::RemainCap(battery_remain_cap), - ) - .await - .unwrap(); - info!("Sending updated battery status to espi service"); - battery_remain_cap -= 1; - - embassy_time::Timer::after_secs(1).await; - } - } -} - -bind_interrupts!(struct Irqs { - ESPI => InterruptHandler; -}); - -// SAFETY: These are symbols defined by the linker and guaranteed to point to valid memory -unsafe extern "C" { - static __start_espi_data: u8; - static __end_espi_data: u8; -} - -#[embassy_executor::task] -async fn espi_service_task(espi: embassy_imxrt::espi::Espi<'static>, memory_map_buffer: &'static mut [u8]) -> ! { - let Err(e) = espi_service::task::espi_service(espi, memory_map_buffer).await; - panic!("espi_service_task error: {e:?}"); -} - -#[embassy_executor::main] -async fn main(spawner: Spawner) { - let p = embassy_imxrt::init(Default::default()); - - embedded_services::init().await; - - let espi = Espi::new( - p.ESPI, - p.PIO7_29, - p.PIO7_26, - p.PIO7_27, - p.PIO7_28, - p.PIO7_30, - p.PIO7_31, - p.PIO7_25, - p.PIO7_24, - Irqs, - Config { - caps: Capabilities { - max_speed: Maxspd::SmallThan20m, - alert_as_a_pin: true, - ..Default::default() - }, - ram_base: 0x2000_0000, - base0_addr: 0x2002_0000, - base1_addr: 0x2003_0000, - status_addr: Some(0x480), - status_base: Base::OffsetFrom0, - ports_config: [ - PortConfig::MailboxShared { - direction: Direction::BidirectionalUnenforced, - base_sel: BaseOrAsz::OffsetFrom0, - offset: 0, - length: Len::Len256, - }, - Default::default(), - Default::default(), - Default::default(), - Default::default(), - ], - ..Default::default() - }, - ); - - let memory_map_buffer = unsafe { - let start_espi_data = &__start_espi_data as *const u8 as *mut u8; - let end_espi_data = &__end_espi_data as *const u8 as *mut u8; - let espi_data_len = end_espi_data.offset_from(start_espi_data) as usize; - - slice::from_raw_parts_mut(start_espi_data, espi_data_len) - }; - - spawner.spawn(espi_service_task(espi, memory_map_buffer).unwrap()); - - battery_service::init().await; - - spawner.spawn(battery_service::battery_update_service().unwrap()); - - loop { - embassy_time::Timer::after_secs(10).await; - info!("The uptime is {} secs", embassy_time::Instant::now().as_secs()); - - let data = unsafe { - let start_espi_data = &__start_espi_data as *const u8 as *mut u8; - let end_espi_data = &__end_espi_data as *const u8 as *mut u8; - let espi_data_len = end_espi_data.offset_from(start_espi_data) as usize; - - slice::from_raw_parts_mut(start_espi_data, espi_data_len) - }; - - info!("Memory map contents: {:?}", data[..256]); - } -} diff --git a/examples/rt633/src/bin/espi_battery.rs b/examples/rt633/src/bin/espi_battery.rs deleted file mode 100644 index d3c3bc03f..000000000 --- a/examples/rt633/src/bin/espi_battery.rs +++ /dev/null @@ -1,374 +0,0 @@ -#![no_std] -#![no_main] - -extern crate rt633_examples; - -use battery_service::context::BatteryEvent; -use core::slice::{self}; -use embassy_imxrt::dma::NoDma; -use embassy_time::{Duration, Timer}; -use embedded_batteries_async::charger::MilliAmps; -use embedded_batteries_async::smart_battery::{ - BatteryModeFields, BatteryStatusFields, CapacityModeSignedValue, CapacityModeValue, Cycles, DeciKelvin, - ManufactureDate, MilliAmpsSigned, MilliVolts, Minutes, Percent, SmartBattery, SpecificationInfoFields, -}; -use embedded_services::{error, info}; - -use battery_service::controller::{Controller, ControllerEvent}; -use battery_service::device::{Device, DeviceId, DynamicBatteryMsgs, StaticBatteryMsgs}; -use battery_service::wrapper::Wrapper; -use bq40z50_rx::Bq40z50R5 as Bq40z50; -use embassy_embedded_hal::shared_bus::asynch::i2c::I2cDevice; -use embassy_executor::Spawner; -use embassy_imxrt::bind_interrupts; -use embassy_imxrt::espi::BaseOrAsz; -use embassy_imxrt::espi::{Base, Capabilities, Config, Direction, Espi, InterruptHandler, Len, Maxspd, PortConfig}; -use embassy_imxrt::i2c::Async; -use embassy_imxrt::i2c::master::I2cMaster; -use embassy_imxrt::peripherals::ESPI; -use embassy_sync::blocking_mutex::raw::NoopRawMutex; -use embassy_sync::mutex::Mutex; -use static_cell::StaticCell; -use {defmt_rtt as _, panic_probe as _}; - -bind_interrupts!(struct IrqsFg { - FLEXCOMM15 => embassy_imxrt::i2c::InterruptHandler; -}); - -static I2C_BUS_FG: StaticCell< - Mutex>, -> = StaticCell::new(); -static FG_DEVICE: StaticCell = StaticCell::new(); - -/// Wrapper struct for the fuel gauge driver -struct Bq40z50Controller { - driver: Bq40z50< - I2cDevice<'static, NoopRawMutex, embassy_imxrt::i2c::master::I2cMaster<'static, embassy_imxrt::i2c::Async>>, - embassy_time::Delay, - >, -} - -impl embedded_batteries_async::smart_battery::ErrorType for Bq40z50Controller { - type Error = >, embassy_time::Delay,> as embedded_batteries_async::smart_battery::ErrorType>::Error; -} - -impl embedded_batteries_async::smart_battery::SmartBattery for Bq40z50Controller { - async fn absolute_state_of_charge(&mut self) -> Result { - self.driver.absolute_state_of_charge().await - } - async fn at_rate(&mut self) -> Result { - self.driver.at_rate().await - } - async fn at_rate_ok(&mut self) -> Result { - self.driver.at_rate_ok().await - } - async fn at_rate_time_to_empty(&mut self) -> Result { - self.driver.at_rate_time_to_empty().await - } - async fn at_rate_time_to_full(&mut self) -> Result { - self.driver.at_rate_time_to_full().await - } - async fn average_current(&mut self) -> Result { - self.driver.average_current().await - } - async fn average_time_to_empty(&mut self) -> Result { - self.driver.average_time_to_empty().await - } - async fn average_time_to_full(&mut self) -> Result { - self.driver.average_time_to_full().await - } - async fn battery_mode(&mut self) -> Result { - self.driver.battery_mode().await - } - async fn battery_status(&mut self) -> Result { - self.driver.battery_status().await - } - async fn current(&mut self) -> Result { - self.driver.current().await - } - async fn cycle_count(&mut self) -> Result { - self.driver.cycle_count().await - } - async fn design_capacity(&mut self) -> Result { - self.driver.design_capacity().await - } - async fn design_voltage(&mut self) -> Result { - self.driver.design_voltage().await - } - async fn device_chemistry(&mut self, chemistry: &mut [u8]) -> Result<(), Self::Error> { - self.driver.device_chemistry(chemistry).await - } - async fn device_name(&mut self, name: &mut [u8]) -> Result<(), Self::Error> { - self.driver.device_name(name).await - } - async fn full_charge_capacity(&mut self) -> Result { - self.driver.full_charge_capacity().await - } - async fn manufacture_date(&mut self) -> Result { - self.driver.manufacture_date().await - } - async fn manufacturer_name(&mut self, name: &mut [u8]) -> Result<(), Self::Error> { - self.driver.manufacturer_name(name).await - } - async fn max_error(&mut self) -> Result { - self.driver.max_error().await - } - async fn relative_state_of_charge(&mut self) -> Result { - self.driver.relative_state_of_charge().await - } - async fn remaining_capacity(&mut self) -> Result { - self.driver.remaining_capacity().await - } - async fn remaining_capacity_alarm(&mut self) -> Result { - self.driver.remaining_capacity_alarm().await - } - async fn remaining_time_alarm(&mut self) -> Result { - self.driver.remaining_time_alarm().await - } - async fn run_time_to_empty(&mut self) -> Result { - self.driver.run_time_to_empty().await - } - async fn serial_number(&mut self) -> Result { - self.driver.serial_number().await - } - async fn set_at_rate(&mut self, rate: CapacityModeSignedValue) -> Result<(), Self::Error> { - self.driver.set_at_rate(rate).await - } - async fn set_battery_mode(&mut self, flags: BatteryModeFields) -> Result<(), Self::Error> { - self.driver.set_battery_mode(flags).await - } - async fn set_remaining_capacity_alarm(&mut self, capacity: CapacityModeValue) -> Result<(), Self::Error> { - self.driver.set_remaining_capacity_alarm(capacity).await - } - async fn set_remaining_time_alarm(&mut self, time: Minutes) -> Result<(), Self::Error> { - self.driver.set_remaining_time_alarm(time).await - } - async fn specification_info(&mut self) -> Result { - self.driver.specification_info().await - } - async fn temperature(&mut self) -> Result { - self.driver.temperature().await - } - async fn voltage(&mut self) -> Result { - self.driver.voltage().await - } - async fn charging_current(&mut self) -> Result { - self.driver.charging_current().await - } - async fn charging_voltage(&mut self) -> Result { - self.driver.charging_voltage().await - } -} - -impl Controller for Bq40z50Controller { - type ControllerError = >, embassy_time::Delay,> as embedded_batteries_async::smart_battery::ErrorType>::Error; - - async fn initialize(&mut self) -> Result<(), Self::ControllerError> { - info!("Fuel gauge inited!"); - Ok(()) - } - - async fn get_static_data(&mut self) -> Result { - info!("Sending static data"); - - Ok(StaticBatteryMsgs { ..Default::default() }) - } - - async fn get_dynamic_data(&mut self) -> Result { - info!("Sending dynamic data"); - info!("Voltage = {}", self.voltage().await?); - info!("Current = {}", self.current().await?); - info!("Cycle count = {}", self.cycle_count().await?); - - Ok(DynamicBatteryMsgs { ..Default::default() }) - } - - async fn get_device_event(&mut self) -> ControllerEvent { - loop { - Timer::after_secs(1000000).await; - } - } - - async fn ping(&mut self) -> Result<(), Self::ControllerError> { - info!("Ping!"); - Ok(()) - } - - fn get_timeout(&self) -> Duration { - unimplemented!() - } - - fn set_timeout(&mut self, _duration: Duration) { - unimplemented!() - } -} - -bind_interrupts!(struct Irqs { - ESPI => InterruptHandler; -}); - -// SAFETY: These are symbols defined by the linker and guaranteed to point to valid memory -unsafe extern "C" { - static __start_espi_data: u8; - static __end_espi_data: u8; -} - -#[embassy_executor::task] -async fn battery_publish_task(fg_device: &'static Device) { - loop { - Timer::after_secs(1).await; - // Get dynamic cache - let cache = fg_device.get_dynamic_battery_cache().await; - - // Send cache data to eSpi service - battery_service::comms_send( - embedded_services::comms::EndpointID::External(embedded_services::comms::External::Host), - &embedded_services::ec_type::message::BatteryMessage::CycleCount(cache.cycle_count.into()), - ) - .await - .unwrap(); - } -} - -#[embassy_executor::task] -async fn wrapper_task(wrapper: Wrapper<'static, Bq40z50Controller>) { - loop { - wrapper.process().await; - info!("Got new wrapper message"); - } -} - -#[embassy_executor::task] -async fn espi_service_task(espi: embassy_imxrt::espi::Espi<'static>, memory_map_buffer: &'static mut [u8]) -> ! { - let Err(e) = espi_service::task::espi_service(espi, memory_map_buffer).await; - panic!("espi_service_task error: {e:?}"); -} - -#[embassy_executor::task] -async fn battery_service_task() -> ! { - battery_service::task::task().await; - unreachable!() -} - -#[embassy_executor::main] -async fn main(spawner: Spawner) { - let p = embassy_imxrt::init(Default::default()); - - embedded_services::init().await; - - let espi = Espi::new( - p.ESPI, - p.PIO7_29, - p.PIO7_26, - p.PIO7_27, - p.PIO7_28, - p.PIO7_30, - p.PIO7_31, - p.PIO7_25, - p.PIO7_24, - Irqs, - Config { - caps: Capabilities { - max_speed: Maxspd::SmallThan20m, - alert_as_a_pin: true, - ..Default::default() - }, - ram_base: 0x2000_0000, - base0_addr: 0x2002_0000, - base1_addr: 0x2003_0000, - status_addr: Some(0x480), - status_base: Base::OffsetFrom0, - ports_config: [ - PortConfig::MailboxShared { - direction: Direction::BidirectionalUnenforced, - base_sel: BaseOrAsz::OffsetFrom0, - offset: 0, - length: Len::Len512, - }, - Default::default(), - Default::default(), - Default::default(), - Default::default(), - ], - ..Default::default() - }, - ); - - let memory_map_buffer = unsafe { - let start_espi_data = &__start_espi_data as *const u8 as *mut u8; - let end_espi_data = &__end_espi_data as *const u8 as *mut u8; - let espi_data_len = end_espi_data.offset_from(start_espi_data) as usize; - - slice::from_raw_parts_mut(start_espi_data, espi_data_len) - }; - - spawner.spawn(espi_service_task(espi, memory_map_buffer).unwrap()); - - let config = embassy_imxrt::i2c::master::Config { - speed: embassy_imxrt::i2c::master::Speed::Standard, - duty_cycle: embassy_imxrt::i2c::master::DutyCycle::new(50).unwrap(), - }; - - let i2c_fg = embassy_imxrt::i2c::master::I2cMaster::new_async( - p.FLEXCOMM15, - p.PIOFC15_SCL, - p.PIOFC15_SDA, - IrqsFg, - config, - unsafe { embassy_imxrt::Peri::new_unchecked(NoDma) }, - ) - .unwrap(); - - let i2c_bus_fg = I2C_BUS_FG.init(Mutex::new(i2c_fg)); - - let fg_bus = I2cDevice::new(i2c_bus_fg); - - let fg = FG_DEVICE.init(Device::new(DeviceId(0))); - - let wrap = Wrapper::new( - fg, - Bq40z50Controller { - driver: Bq40z50::new(fg_bus, embassy_time::Delay), - }, - ); - - spawner.spawn(wrapper_task(wrap).unwrap()); - spawner.spawn(battery_service_task().unwrap()); - - battery_service::register_fuel_gauge(fg).unwrap(); - - spawner.spawn(battery_publish_task(fg).unwrap()); - - if let Err(e) = battery_service::execute_event(BatteryEvent { - device_id: DeviceId(0), - event: battery_service::context::BatteryEventInner::DoInit, - }) - .await - { - error!("Error initializing fuel gauge, error: {:?}", e); - } - - loop { - embassy_time::Timer::after_secs(10).await; - info!("The uptime is {} secs", embassy_time::Instant::now().as_secs()); - - let data = unsafe { - let start_espi_data = &__start_espi_data as *const u8 as *mut u8; - let end_espi_data = &__end_espi_data as *const u8 as *mut u8; - let espi_data_len = end_espi_data.offset_from(start_espi_data) as usize; - - slice::from_raw_parts_mut(start_espi_data, espi_data_len) - }; - - info!("Memory map contents: {:?}", data[..64]); - - if let Err(e) = battery_service::execute_event(BatteryEvent { - device_id: DeviceId(0), - event: battery_service::context::BatteryEventInner::PollDynamicData, - }) - .await - { - error!("Error getting dynamic fuel gauge data, error: {:?}", e); - } - } -} diff --git a/examples/rt685s-evk/.cargo/config.toml b/examples/rt685s-evk/.cargo/config.toml index db42be81d..ef1631db6 100644 --- a/examples/rt685s-evk/.cargo/config.toml +++ b/examples/rt685s-evk/.cargo/config.toml @@ -2,16 +2,20 @@ runner = 'probe-rs run --chip MIMXRT685SFVKB' rustflags = [ - "-C", "linker=flip-link", - "-C", "link-arg=-Tlink.x", - "-C", "link-arg=-Tdefmt.x", + "-C", + "linker=flip-link", + "-C", + "link-arg=-Tlink.x", + "-C", + "link-arg=-Tdefmt.x", # This is needed if your flash or ram addresses are not aligned to 0x10000 in memory.x # See https://github.com/rust-embedded/cortex-m-quickstart/pull/95 - "-C", "link-arg=--nmagic", + "-C", + "link-arg=--nmagic", ] [build] target = "thumbv8m.main-none-eabihf" # Cortex-M33 [env] -DEFMT_LOG = "trace" +DEFMT_LOG = "info" diff --git a/examples/rt685s-evk/Cargo.lock b/examples/rt685s-evk/Cargo.lock index 6117376a2..39141ce73 100644 --- a/examples/rt685s-evk/Cargo.lock +++ b/examples/rt685s-evk/Cargo.lock @@ -22,7 +22,7 @@ dependencies = [ "proc-macro-error2", "proc-macro2", "quote", - "syn 2.0.106", + "syn", ] [[package]] @@ -33,9 +33,9 @@ checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "az" -version = "1.2.1" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b7e4c2464d97fe331d41de9d5db0def0a96f4d823b8b32a2efd503578988973" +checksum = "be5eb007b7cacc6c660343e96f650fedf4b5a77512399eb952ca6642cf8d13f7" [[package]] name = "bare-metal" @@ -65,14 +65,6 @@ dependencies = [ "virtue", ] -[[package]] -name = "bit-register" -version = "0.1.0" -source = "git+https://github.com/OpenDevicePartnership/odp-utilities?rev=2f79d238#2f79d238149049d458199a9a9129b54be7893aee" -dependencies = [ - "num-traits", -] - [[package]] name = "bit-register" version = "0.1.0" @@ -101,33 +93,22 @@ checksum = "f798d2d157e547aa99aab0967df39edd0b70307312b6f8bd2848e6abe40896e0" [[package]] name = "bitfield" -version = "0.19.2" +version = "0.19.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62a3a774b2fcac1b726922b921ebba5e9fe36ad37659c822cf8ff2c1e0819892" +checksum = "21ba6517c6b0f2bf08be60e187ab64b038438f22dd755614d8fe4d4098c46419" dependencies = [ "bitfield-macros", ] [[package]] name = "bitfield-macros" -version = "0.19.2" +version = "0.19.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52511b09931f7d5fe3a14f23adefbc23e5725b184013e96c8419febb61f14734" +checksum = "f48d6ace212fdf1b45fd6b566bb40808415344642b76c3224c07c8df9da81e97" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", -] - -[[package]] -name = "bitfield-struct" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2be5a46ba01b60005ae2c51a36a29cfe134bcacae2dd5cedcd4615fbaad1494b" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.106", + "syn", ] [[package]] @@ -138,7 +119,7 @@ checksum = "8769c4854c5ada2852ddf6fd09d15cf43d4c2aaeccb4de6432f5402f08a6003b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn", ] [[package]] @@ -149,27 +130,15 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.9.4" +version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394" - -[[package]] -name = "bitvec" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" -dependencies = [ - "funty", - "radium", - "tap", - "wyz", -] +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" [[package]] name = "bytemuck" -version = "1.23.2" +version = "1.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3995eaeebcdf32f91f980d360f78732ddc061097ab4e39991ae7a6ace9194677" +checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" [[package]] name = "byteorder" @@ -189,9 +158,23 @@ dependencies = [ [[package]] name = "cfg-if" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cfu-service" +version = "0.1.0" +dependencies = [ + "defmt 0.3.100", + "embassy-futures", + "embassy-sync", + "embassy-time", + "embedded-cfu-protocol", + "embedded-services", + "fw-update-interface", + "heapless 0.9.2", +] [[package]] name = "cordyceps" @@ -233,14 +216,14 @@ checksum = "e37549a379a9e0e6e576fd208ee60394ccb8be963889eebba3ffe0980364f472" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn", ] [[package]] name = "crc" -version = "3.3.0" +version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9710d3b3739c2e349eb44fe848ad0b7c8cb1e42bd87ee49371df2f7acaf3e675" +checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d" dependencies = [ "crc-catalog", ] @@ -284,7 +267,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.106", + "syn", ] [[package]] @@ -295,7 +278,7 @@ checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" dependencies = [ "darling_core", "quote", - "syn 2.0.106", + "syn", ] [[package]] @@ -327,7 +310,7 @@ dependencies = [ "proc-macro-error2", "proc-macro2", "quote", - "syn 2.0.106", + "syn", ] [[package]] @@ -362,9 +345,9 @@ dependencies = [ [[package]] name = "document-features" -version = "0.2.11" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95249b50c6c185bee49034bcb378a49dc2b5dff0be90ff6616d31d64febab05d" +checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" dependencies = [ "litrs", ] @@ -375,24 +358,6 @@ version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" -[[package]] -name = "embassy-embedded-hal" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "554e3e840696f54b4c9afcf28a0f24da431c927f4151040020416e7393d6d0d8" -dependencies = [ - "embassy-futures", - "embassy-hal-internal 0.3.0", - "embassy-sync 0.7.2", - "embassy-time", - "embedded-hal 0.2.7", - "embedded-hal 1.0.0", - "embedded-hal-async", - "embedded-storage", - "embedded-storage-async", - "nb 1.1.0", -] - [[package]] name = "embassy-embedded-hal" version = "0.6.0" @@ -402,7 +367,8 @@ dependencies = [ "defmt 1.0.1", "embassy-futures", "embassy-hal-internal 0.4.0", - "embassy-sync 0.8.0", + "embassy-sync", + "embassy-time", "embedded-hal 0.2.7", "embedded-hal 1.0.0", "embedded-hal-async", @@ -435,7 +401,7 @@ dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.106", + "syn", ] [[package]] @@ -474,7 +440,7 @@ dependencies = [ [[package]] name = "embassy-imxrt" version = "0.1.0" -source = "git+https://github.com/OpenDevicePartnership/embassy-imxrt#581f66e68ceaefb77e35f230dfb114322912dd6b" +source = "git+https://github.com/OpenDevicePartnership/embassy-imxrt#d95ffe560071e942d917b422bbdea7e73f6c4602" dependencies = [ "cfg-if", "cortex-m", @@ -482,10 +448,10 @@ dependencies = [ "critical-section", "defmt 1.0.1", "document-features", - "embassy-embedded-hal 0.5.0", + "embassy-embedded-hal", "embassy-futures", "embassy-hal-internal 0.3.0", - "embassy-sync 0.7.2", + "embassy-sync", "embassy-time", "embassy-time-driver", "embassy-time-queue-utils", @@ -508,21 +474,6 @@ dependencies = [ "storage_bus", ] -[[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", - "defmt 1.0.1", - "embedded-io-async 0.6.1", - "futures-core", - "futures-sink", - "heapless 0.8.0", -] - [[package]] name = "embassy-sync" version = "0.8.0" @@ -566,35 +517,22 @@ dependencies = [ [[package]] name = "embassy-time-queue-utils" -version = "0.3.2" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "168297bf80aaf114b3c9ad589bf38b01b3009b9af7f97cd18086c5bbf96f5693" +checksum = "7fe6ecec32eac4f98c5427904006b0fc61f77d350e1572530f744ef14650f7a1" dependencies = [ "embassy-executor-timer-queue", "heapless 0.9.2", ] -[[package]] -name = "embedded-batteries" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31e14d288a59ef41f4e05468eae9b1c9fef6866977cea86d3f1a1ced295b6cab" -dependencies = [ - "bitfield-struct 0.10.1", - "bitflags 2.9.4", - "defmt 0.3.100", - "embedded-hal 1.0.0", - "zerocopy", -] - [[package]] name = "embedded-batteries" version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40f975432b4e146342a1589c563cffab6b7a692024cb511bf87b6bfe78c84125" dependencies = [ - "bitfield-struct 0.12.1", - "bitflags 2.9.4", + "bitfield-struct", + "bitflags 2.11.1", "defmt 0.3.100", "embedded-hal 1.0.0", "zerocopy", @@ -606,16 +544,15 @@ version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a3bf0e4be67770cfc31f1cea8b73baf98c0baf2c57d6bd8c3a4c315acb1d8bd4" dependencies = [ - "bitfield-struct 0.12.1", - "defmt 0.3.100", - "embedded-batteries 0.3.4", + "bitfield-struct", + "embedded-batteries", "embedded-hal 1.0.0", ] [[package]] name = "embedded-cfu-protocol" version = "0.2.0" -source = "git+https://github.com/OpenDevicePartnership/embedded-cfu#a4cc8707842b878048447abbf2af4efa79fed368" +source = "git+https://github.com/OpenDevicePartnership/embedded-cfu#e0d776017cf34c902c9f2a2be0c75fe73a3a4dda" dependencies = [ "defmt 0.3.100", "embedded-io-async 0.6.1", @@ -697,10 +634,12 @@ dependencies = [ [[package]] name = "embedded-mcu-hal" -version = "0.1.0" -source = "git+https://github.com/OpenDevicePartnership/embedded-mcu#146fda33807af6aeabcade513914da95c926fbc9" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f02b992c2b871b7fc616e4539258d92ea8b085e2f09cc0ad2862aa4d0e185ad1" dependencies = [ "defmt 1.0.1", + "num_enum", ] [[package]] @@ -708,22 +647,15 @@ name = "embedded-services" version = "0.1.0" dependencies = [ "bitfield 0.17.0", - "bitvec", "cortex-m", - "cortex-m-rt", "critical-section", "defmt 0.3.100", - "embassy-sync 0.8.0", - "embassy-time", - "embedded-batteries-async", - "embedded-cfu-protocol", - "embedded-usb-pd", - "heapless 0.9.2", + "embassy-futures", + "embassy-sync", "mctp-rs", - "num_enum", + "paste", "portable-atomic", "serde", - "uuid", ] [[package]] @@ -748,7 +680,7 @@ source = "git+https://github.com/OpenDevicePartnership/embedded-usb-pd#21d0e228d dependencies = [ "aquamarine", "bincode", - "bitfield 0.19.2", + "bitfield 0.19.4", "defmt 0.3.100", "embedded-hal-async", ] @@ -756,10 +688,10 @@ dependencies = [ [[package]] name = "espi-device" version = "0.1.0" -source = "git+https://github.com/OpenDevicePartnership/haf-ec-service#8cdd61095471903b1b438dbb0eee142676cc3d74" +source = "git+https://github.com/OpenDevicePartnership/haf-ec-service#09eda26a729738adbd177231600acdb981690375" dependencies = [ - "bit-register 0.1.0 (git+https://github.com/OpenDevicePartnership/odp-utilities?rev=2f79d238)", - "bitflags 2.9.4", + "bit-register", + "bitflags 2.11.1", "num-traits", "num_enum", "static_assertions", @@ -774,9 +706,9 @@ checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" [[package]] name = "fixed" -version = "1.29.0" +version = "1.30.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "707070ccf8c4173548210893a0186e29c266901b71ed20cd9e2ca0193dfe95c3" +checksum = "c566da967934c6c7ee0458a9773de9b2a685bd2ce26a3b28ddfc740e640182f5" dependencies = [ "az", "bytemuck", @@ -790,23 +722,25 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" -[[package]] -name = "funty" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" - [[package]] name = "futures-core" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" [[package]] name = "futures-sink" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "fw-update-interface" +version = "0.1.0" +dependencies = [ + "defmt 0.3.100", + "embedded-services", +] [[package]] name = "generator" @@ -825,12 +759,13 @@ dependencies = [ [[package]] name = "half" -version = "2.6.0" +version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "459196ed295495a68f7d7fe1d84f6c4b7ff0e21fe3017b2f283c6fac3ad803c9" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" dependencies = [ "cfg-if", "crunchy", + "zerocopy", ] [[package]] @@ -864,9 +799,9 @@ dependencies = [ [[package]] name = "heck" -version = "0.4.1" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "ident_case" @@ -934,9 +869,9 @@ checksum = "52ff2c0fe9bc6cb6b14a0592c2ff4fa9ceb83eea9db979b0487cd054946a2b8f" [[package]] name = "litrs" -version = "0.4.2" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5e54036fe321fd421e10d732f155734c4e4afd610dd556d9a82833ab3ee0bed" +checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" [[package]] name = "log" @@ -969,11 +904,10 @@ dependencies = [ [[package]] name = "mctp-rs" version = "0.1.0" -source = "git+https://github.com/dymk/mctp-rs#4456c65131366acd5e605c7d88a707881fa9e9f0" dependencies = [ - "bit-register 0.1.0 (git+https://github.com/OpenDevicePartnership/odp-utilities)", + "bit-register", "defmt 0.3.100", - "embedded-batteries 0.2.1", + "embedded-batteries", "espi-device", "num_enum", "smbus-pec", @@ -982,9 +916,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.7.5" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" [[package]] name = "mimxrt600-fcb" @@ -1006,27 +940,27 @@ dependencies = [ [[package]] name = "mimxrt633s-pac" -version = "0.4.1" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843c1c63c367293e4fa270cc161b5bdfef55b4c6a0a18768f737241fb24be70c" +checksum = "a580cd0048e0e5b1eee17208b7f95ba05ec7ca0175789333b2fa7241798d3cdc" dependencies = [ "cortex-m", "cortex-m-rt", "critical-section", - "defmt 0.3.100", + "defmt 1.0.1", "vcell", ] [[package]] name = "mimxrt685s-pac" -version = "0.4.0" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c0b80e5add9dc74500acbb1ca70248e237d242b77988631e41db40a225f3a40" +checksum = "8c8860258951178c4f91e42581ae8f33241a0e9a3e89bf2721d932ef366db51b" dependencies = [ "cortex-m", "cortex-m-rt", "critical-section", - "defmt 0.3.100", + "defmt 1.0.1", "vcell", ] @@ -1081,14 +1015,22 @@ checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn", +] + +[[package]] +name = "odp-service-common" +version = "0.1.0" +dependencies = [ + "embedded-services", + "static_cell", ] [[package]] name = "once_cell" -version = "1.21.3" +version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" [[package]] name = "panic-probe" @@ -1120,16 +1062,16 @@ dependencies = [ "crc", "defmt 0.3.100", "embassy-imxrt", - "embassy-sync 0.8.0", + "embassy-sync", "embassy-time", "embedded-services", ] [[package]] name = "portable-atomic" -version = "1.11.1" +version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" [[package]] name = "power-button-service" @@ -1141,16 +1083,29 @@ dependencies = [ "embedded-hal-async", ] +[[package]] +name = "power-policy-interface" +version = "0.1.0" +dependencies = [ + "bitfield 0.17.0", + "defmt 0.3.100", + "embassy-sync", + "embedded-batteries-async", + "embedded-services", + "num_enum", +] + [[package]] name = "power-policy-service" version = "0.1.0" dependencies = [ "defmt 0.3.100", "embassy-futures", - "embassy-sync 0.8.0", + "embassy-sync", "embassy-time", "embedded-services", "heapless 0.9.2", + "power-policy-interface", ] [[package]] @@ -1172,38 +1127,32 @@ dependencies = [ "proc-macro-error-attr2", "proc-macro2", "quote", - "syn 2.0.106", + "syn", ] [[package]] name = "proc-macro2" -version = "1.0.101" +version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.40" +version = "1.0.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" dependencies = [ "proc-macro2", ] -[[package]] -name = "radium" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" - [[package]] name = "rand_core" -version = "0.6.4" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" [[package]] name = "regex-automata" @@ -1226,27 +1175,35 @@ checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" name = "rt685s-evk-example" version = "0.1.0" dependencies = [ + "cfu-service", "cortex-m", "cortex-m-rt", "crc", "defmt 0.3.100", "defmt-rtt", - "embassy-embedded-hal 0.6.0", + "embassy-embedded-hal", "embassy-executor", "embassy-futures", "embassy-imxrt", - "embassy-sync 0.8.0", + "embassy-sync", "embassy-time", "embedded-cfu-protocol", + "embedded-mcu-hal", "embedded-services", "embedded-usb-pd", "mimxrt600-fcb 0.1.0", + "odp-service-common", "panic-probe", "platform-service", "power-button-service", + "power-policy-interface", "power-policy-service", "static_cell", + "time-alarm-service", + "time-alarm-service-interface", + "time-alarm-service-relay", "tps6699x", + "type-c-interface", "type-c-service", ] @@ -1288,22 +1245,32 @@ checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" [[package]] name = "serde" -version = "1.0.219" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.219" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn", ] [[package]] @@ -1338,9 +1305,9 @@ dependencies = [ [[package]] name = "stable_deref_trait" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" [[package]] name = "static_assertions" @@ -1360,7 +1327,7 @@ dependencies = [ [[package]] name = "storage_bus" version = "0.1.0" -source = "git+https://github.com/OpenDevicePartnership/embedded-mcu#146fda33807af6aeabcade513914da95c926fbc9" +source = "git+https://github.com/OpenDevicePartnership/embedded-mcu#528045934782abc704531aa423169c75016e7727" [[package]] name = "strsim" @@ -1370,62 +1337,45 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "subenum" -version = "1.1.2" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f5d5dfb8556dd04017db5e318bbeac8ab2b0c67b76bf197bfb79e9b29f18ecf" +checksum = "ec3d08fe7078c57309d5c3d938e50eba95ba1d33b9c3a101a8465fc6861a5416" dependencies = [ "heck", "proc-macro2", "quote", - "syn 1.0.109", + "syn", ] [[package]] name = "syn" -version = "1.0.109" +version = "2.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] -[[package]] -name = "syn" -version = "2.0.106" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "tap" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" - [[package]] name = "thiserror" -version = "2.0.16" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3467d614147380f2e4e374161426ff399c91084acd2363eaf549172b3d5e60c0" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "2.0.16" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c5e1be1c48b9172ee610da68fd9cd2770e7a4056cb3fc98710ee6906f0c7960" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn", ] [[package]] @@ -1437,16 +1387,53 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "time-alarm-service" +version = "0.1.0" +dependencies = [ + "defmt 0.3.100", + "embassy-futures", + "embassy-sync", + "embassy-time", + "embedded-mcu-hal", + "embedded-services", + "odp-service-common", + "time-alarm-service-interface", + "zerocopy", +] + +[[package]] +name = "time-alarm-service-interface" +version = "0.1.0" +dependencies = [ + "bitfield 0.17.0", + "defmt 0.3.100", + "embedded-mcu-hal", + "num_enum", + "zerocopy", +] + +[[package]] +name = "time-alarm-service-relay" +version = "0.1.0" +dependencies = [ + "defmt 0.3.100", + "embedded-mcu-hal", + "embedded-services", + "num_enum", + "time-alarm-service-interface", +] + [[package]] name = "tps6699x" version = "0.1.0" -source = "git+https://github.com/OpenDevicePartnership/tps6699x#ba1b3b17ebf048fc007eb2107a4d2ab8cb545adf" +source = "git+https://github.com/OpenDevicePartnership/tps6699x?branch=v0.2.0#abe5568183bfe5fb2ea81806dded6cb60f3f9b58" dependencies = [ "bincode", - "bitfield 0.19.2", + "bitfield 0.19.4", "defmt 0.3.100", "device-driver", - "embassy-sync 0.8.0", + "embassy-sync", "embassy-time", "embedded-hal 1.0.0", "embedded-hal-async", @@ -1475,7 +1462,7 @@ checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn", ] [[package]] @@ -1517,36 +1504,49 @@ dependencies = [ "tracing-log", ] +[[package]] +name = "type-c-interface" +version = "0.1.0" +dependencies = [ + "bitfield 0.17.0", + "defmt 0.3.100", + "embedded-services", + "embedded-usb-pd", + "heapless 0.9.2", + "power-policy-interface", +] + [[package]] name = "type-c-service" version = "0.1.0" dependencies = [ "bitfield 0.17.0", - "bitflags 2.9.4", + "bitflags 2.11.1", "defmt 0.3.100", "embassy-futures", - "embassy-sync 0.8.0", + "embassy-sync", "embassy-time", - "embedded-cfu-protocol", "embedded-hal-async", "embedded-services", "embedded-usb-pd", + "fw-update-interface", "heapless 0.9.2", - "static_cell", + "power-policy-interface", "tps6699x", + "type-c-interface", ] [[package]] name = "typenum" -version = "1.18.0" +version = "1.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" +checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" [[package]] name = "unicode-ident" -version = "1.0.18" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] name = "unty" @@ -1554,12 +1554,6 @@ version = "0.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6d49784317cd0d1ee7ec5c716dd598ec5b4483ea832a2dced265471cc0f690ae" -[[package]] -name = "uuid" -version = "1.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3cf4199d1e5d15ddd86a694e4d0dffa9c323ce759fea589f00fef9d81cc1931d" - [[package]] name = "valuable" version = "0.1.1" @@ -1617,31 +1611,22 @@ dependencies = [ "windows-link", ] -[[package]] -name = "wyz" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" -dependencies = [ - "tap", -] - [[package]] name = "zerocopy" -version = "0.8.26" +version = "0.8.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1039dd0d3c310cf05de012d8a39ff557cb0d23087fd44cad61df08fc31907a2f" +checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.26" +version = "0.8.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181" +checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn", ] diff --git a/examples/rt685s-evk/Cargo.toml b/examples/rt685s-evk/Cargo.toml index 9206fc641..9ef0a4995 100644 --- a/examples/rt685s-evk/Cargo.toml +++ b/examples/rt685s-evk/Cargo.toml @@ -6,12 +6,12 @@ version = "0.1.0" edition = "2024" license = "MIT" -[workspace.lints.rust] -warnings = "deny" - [package.metadata.cargo-machete] ignored = ["cortex-m", "cortex-m-rt"] +[workspace.lints.rust] +warnings = "deny" + [lints] workspace = true @@ -50,13 +50,17 @@ mimxrt600-fcb = "0.1.0" embedded-cfu-protocol = { git = "https://github.com/OpenDevicePartnership/embedded-cfu" } embedded-services = { path = "../../embedded-service", features = ["defmt"] } +odp-service-common = { path = "../../odp-service-common" } power-button-service = { path = "../../power-button-service", features = [ "defmt", ] } +power-policy-interface = { path = "../../power-policy-interface", features = [ + "defmt", +] } power-policy-service = { path = "../../power-policy-service", features = [ "defmt", ] } -tps6699x = { git = "https://github.com/OpenDevicePartnership/tps6699x", features = [ +tps6699x = { git = "https://github.com/OpenDevicePartnership/tps6699x", branch = "v0.2.0", features = [ "defmt", "embassy", ] } @@ -64,6 +68,14 @@ embedded-usb-pd = { git = "https://github.com/OpenDevicePartnership/embedded-usb "defmt", ] } type-c-service = { path = "../../type-c-service", features = ["defmt"] } +type-c-interface = { path = "../../type-c-interface", features = ["defmt"] } +time-alarm-service = { path = "../../time-alarm-service", features = ["defmt"] } +time-alarm-service-interface = { path = "../../time-alarm-service-interface", features = [ + "defmt", +] } +time-alarm-service-relay = { path = "../../time-alarm-service-relay", features = [ + "defmt", +] } static_cell = "2.1.0" @@ -71,6 +83,9 @@ platform-service = { path = "../../platform-service", features = [ "defmt", "imxrt685", ] } +embedded-mcu-hal = "0.2.0" + +cfu-service = { path = "../../cfu-service", features = ["defmt"] } # Needed otherwise cargo will pull from git [patch."https://github.com/OpenDevicePartnership/embedded-services"] diff --git a/examples/rt685s-evk/src/bin/keyboard.rs b/examples/rt685s-evk/src/bin/keyboard.rs index b2482fb77..5363bb549 100644 --- a/examples/rt685s-evk/src/bin/keyboard.rs +++ b/examples/rt685s-evk/src/bin/keyboard.rs @@ -150,10 +150,10 @@ async fn main(spawner: Spawner) { info!("Service initialization complete..."); // create an activity service subscriber - spawner.spawn(activity_example::backlight::task().unwrap()); + spawner.spawn(activity_example::backlight::task().expect("Failed to create backlight task")); // create an activity service publisher - spawner.spawn(activity_example::publisher::keyboard_task().unwrap()); + spawner.spawn(activity_example::publisher::keyboard_task().expect("Failed to create keyboard task")); info!("Subsystem initialization complete..."); diff --git a/examples/rt685s-evk/src/bin/mock_espi_service.rs b/examples/rt685s-evk/src/bin/mock_espi_service.rs index a66b4f475..e52dfaa25 100644 --- a/examples/rt685s-evk/src/bin/mock_espi_service.rs +++ b/examples/rt685s-evk/src/bin/mock_espi_service.rs @@ -182,9 +182,9 @@ async fn main(spawner: Spawner) { info!("Service initialization complete..."); - spawner.spawn(espi_service::espi_service().unwrap()); + spawner.spawn(espi_service::espi_service().expect("Failed to create espi service task")); - spawner.spawn(battery_service::battery_service_task().unwrap()); + spawner.spawn(battery_service::battery_service_task().expect("Failed to create battery service task")); info!("Subsystem initialization complete..."); } diff --git a/examples/rt685s-evk/src/bin/power_button.rs b/examples/rt685s-evk/src/bin/power_button.rs index c0f883b0c..489b631df 100644 --- a/examples/rt685s-evk/src/bin/power_button.rs +++ b/examples/rt685s-evk/src/bin/power_button.rs @@ -129,8 +129,8 @@ async fn main(spawner: Spawner) { let config_b = ButtonConfig::default(); // Spawn the button tasks - spawner.spawn(button_task(button_a, config_a).unwrap()); - spawner.spawn(button_task(button_b, config_b).unwrap()); + spawner.spawn(button_task(button_a, config_a).expect("Failed to spawn button task")); + spawner.spawn(button_task(button_b, config_b).expect("Failed to spawn button task")); static RECEIVER: StaticCell = StaticCell::new(); let receiver = RECEIVER.init(receiver::Receiver::new()); diff --git a/examples/rt685s-evk/src/bin/reset.rs b/examples/rt685s-evk/src/bin/reset.rs index 45f7783c0..2ff3da3fb 100644 --- a/examples/rt685s-evk/src/bin/reset.rs +++ b/examples/rt685s-evk/src/bin/reset.rs @@ -50,7 +50,7 @@ async fn main(spawner: embassy_executor::Spawner) { // when immediately calling reset below blocker.register().expect("Infallible"); - spawner.spawn(reset_watcher(blocker).unwrap()); + spawner.spawn(reset_watcher(blocker).expect("Failed to spawn reset watcher task")); } // perform reset diff --git a/examples/rt685s-evk/src/bin/time_alarm.rs b/examples/rt685s-evk/src/bin/time_alarm.rs new file mode 100644 index 000000000..8673d9096 --- /dev/null +++ b/examples/rt685s-evk/src/bin/time_alarm.rs @@ -0,0 +1,80 @@ +#![no_std] +#![no_main] + +use embedded_mcu_hal::{ + nvram::Nvram, + time::{Datetime, DatetimeFields, Month}, +}; +use embedded_services::info; +use static_cell::StaticCell; +use time_alarm_service_interface::{ + AcpiDaylightSavingsTimeStatus, AcpiTimeZone, AcpiTimeZoneOffset, AcpiTimestamp, TimeAlarmService, +}; +use {defmt_rtt as _, panic_probe as _}; + +// Type aliases to make it easier to use the service and relay handler types without needing to write out all the generic parameters every time. +// This is especially helpful for the relay handler, which has a lot of generic parameters due to the traits it needs to implement. +// +type TimeAlarmServiceType = time_alarm_service::Service<'static>; +type TimeAlarmServiceRelayHandlerType = time_alarm_service_relay::TimeAlarmServiceRelayHandler; + +#[embassy_executor::main] +async fn main(spawner: embassy_executor::Spawner) { + let p = embassy_imxrt::init(Default::default()); + + static RTC: StaticCell = StaticCell::new(); + let rtc = RTC.init(embassy_imxrt::rtc::Rtc::new(p.RTC)); + let (dt_clock, rtc_nvram) = rtc.split(); + + let [tz, ac_expiration, ac_policy, dc_expiration, dc_policy, ..] = rtc_nvram.storage(); + + embedded_services::init().await; + info!("services initialized"); + + let time_service = odp_service_common::spawn_service!( + spawner, + TimeAlarmServiceType, + time_alarm_service::InitParams { + backing_clock: dt_clock, + tz_storage: tz, + ac_expiration_storage: ac_expiration, + ac_policy_storage: ac_policy, + dc_expiration_storage: dc_expiration, + dc_policy_storage: dc_policy + } + ) + .expect("Failed to spawn time alarm service"); + + use embedded_services::relay::mctp::impl_odp_mctp_relay_handler; + impl_odp_mctp_relay_handler!( + EspiRelayHandler; + TimeAlarm, 0x0B, crate::TimeAlarmServiceRelayHandlerType; + ); + + let _relay_handler = EspiRelayHandler::new(TimeAlarmServiceRelayHandlerType::new(time_service)); + + // Here, you'd normally pass _relay_handler to your relay service (e.g. eSPI service). + // In this example, we're not leveraging a relay service, so we'll just demonstrate some direct calls. + // + time_service + .set_real_time(AcpiTimestamp { + datetime: Datetime::new(DatetimeFields { + year: 2024, + month: Month::January, + day: 10, + hour: 12, + minute: 0, + second: 0, + nanosecond: 0, + }) + .unwrap(), + time_zone: AcpiTimeZone::MinutesFromUtc(AcpiTimeZoneOffset::new(60 * -8).unwrap()), + dst_status: AcpiDaylightSavingsTimeStatus::NotAdjusted, + }) + .unwrap(); + + loop { + embassy_time::Timer::after(embassy_time::Duration::from_secs(10)).await; + info!("Current time from service: {:?}", time_service.get_real_time().unwrap()); + } +} diff --git a/examples/rt685s-evk/src/bin/transport.rs b/examples/rt685s-evk/src/bin/transport.rs index c3c4834f1..abd725ba3 100644 --- a/examples/rt685s-evk/src/bin/transport.rs +++ b/examples/rt685s-evk/src/bin/transport.rs @@ -156,8 +156,8 @@ async fn main(spawner: Spawner) { info!("Service initialization complete..."); - spawner.spawn(simple_example::receiver().unwrap()); - spawner.spawn(simple_example::sender().unwrap()); + spawner.spawn(simple_example::receiver().expect("Failed to create receiver task")); + spawner.spawn(simple_example::sender().expect("Failed to create sender task")); info!("Subsystem initialization complete..."); diff --git a/examples/rt685s-evk/src/bin/type_c.rs b/examples/rt685s-evk/src/bin/type_c.rs index 1d074326f..9efa5a928 100644 --- a/examples/rt685s-evk/src/bin/type_c.rs +++ b/examples/rt685s-evk/src/bin/type_c.rs @@ -1,80 +1,147 @@ #![no_std] #![no_main] - -use ::tps6699x::{ADDR1, TPS66994_NUM_PORTS}; +use ::tps6699x::ADDR1; +use ::tps6699x::asynchronous::embassy::interrupt::InterruptReceiver; use embassy_embedded_hal::shared_bus::asynch::i2c::I2cDevice; use embassy_executor::Spawner; use embassy_imxrt::gpio::{Input, Inverter, Pull}; use embassy_imxrt::i2c::Async; use embassy_imxrt::i2c::master::{Config, I2cMaster}; use embassy_imxrt::{bind_interrupts, peripherals}; +use embassy_sync::channel::{DynamicReceiver, DynamicSender}; use embassy_sync::mutex::Mutex; +use embassy_sync::pubsub::{DynImmediatePublisher, DynSubscriber, PubSubChannel}; use embassy_time::{self as _, Delay}; -use embedded_cfu_protocol::protocol_definitions::{FwUpdateOffer, FwUpdateOfferResponse, FwVersion, HostToken}; use embedded_services::GlobalRawMutex; -use embedded_services::power::policy::DeviceId as PowerId; -use embedded_services::type_c::{self, Cached, ControllerId}; +use embedded_services::event::{MapSender, NoopSender}; use embedded_services::{error, info}; -use embedded_usb_pd::GlobalPortId; +use embedded_usb_pd::LocalPortId; +use power_policy_interface::psu; +use power_policy_service::psu::PsuEventReceivers; +use power_policy_service::service::registration::ArrayRegistration; use static_cell::StaticCell; use tps6699x::asynchronous::embassy as tps6699x; +use type_c_interface::port::event::PortEventBitfield; +use type_c_service::controller::Port; +use type_c_service::controller::event_receiver::{ + EventReceiver as PortEventReceiver, InterruptReceiver as _, PortEventSplitter, +}; +use type_c_service::controller::macros::PortComponents; +use type_c_service::controller::state::SharedState; +use type_c_service::define_controller_port_static_cell_channel; use type_c_service::driver::tps6699x::{self as tps6699x_drv}; -use type_c_service::wrapper::ControllerWrapper; -use type_c_service::wrapper::backing::{ReferencedStorage, Storage}; +use type_c_service::service::Service; +use type_c_service::service::registration::PortData; extern crate rt685s_evk_example; -const CONTROLLER0_ID: ControllerId = ControllerId(0); -const PORT0_ID: GlobalPortId = GlobalPortId(0); -const PORT1_ID: GlobalPortId = GlobalPortId(1); -const PORT0_PWR_ID: PowerId = PowerId(0); -const PORT1_PWR_ID: PowerId = PowerId(1); - bind_interrupts!(struct Irqs { FLEXCOMM2 => embassy_imxrt::i2c::InterruptHandler; }); -struct Validator; - -impl type_c_service::wrapper::FwOfferValidator for Validator { - fn validate(&self, _current: FwVersion, _offer: &FwUpdateOffer) -> FwUpdateOfferResponse { - // For this example, we always accept the offer - FwUpdateOfferResponse::new_accept(HostToken::Driver) - } -} +type SharedStateType = Mutex; +type PortType = Mutex< + GlobalRawMutex, + Port< + 'static, + Tps6699xMutex<'static>, + SharedStateType, + DynamicSender<'static, type_c_interface::service::event::PortEventData>, + DynamicSender<'static, power_policy_interface::psu::event::EventData>, + DynamicSender<'static, type_c_service::controller::event::Loopback>, + >, +>; +type ChargerType = power_policy_interface::charger::mock::ChargerType; type BusMaster<'a> = I2cMaster<'a, Async>; type BusDevice<'a> = I2cDevice<'a, GlobalRawMutex, BusMaster<'a>>; type Tps6699xMutex<'a> = Mutex>>; -type Wrapper<'a> = ControllerWrapper<'a, GlobalRawMutex, Tps6699xMutex<'a>, Validator>; type Controller<'a> = tps6699x::controller::Controller>; -type Interrupt<'a> = tps6699x::Interrupt<'a, GlobalRawMutex, BusDevice<'a>>; +type InterruptProcessor<'a> = tps6699x::interrupt::InterruptProcessor<'a, GlobalRawMutex, BusDevice<'a>>; + +type PowerPolicySenderType = MapSender< + power_policy_interface::service::event::Event<'static, PortType>, + power_policy_interface::service::event::EventData, + DynImmediatePublisher<'static, power_policy_interface::service::event::EventData>, + fn( + power_policy_interface::service::event::Event<'static, PortType>, + ) -> power_policy_interface::service::event::EventData, +>; + +type PowerPolicyReceiverType = DynSubscriber<'static, power_policy_interface::service::event::EventData>; + +type PowerPolicyServiceType = Mutex< + GlobalRawMutex, + power_policy_service::service::Service< + 'static, + ArrayRegistration<'static, PortType, 2, PowerPolicySenderType, 1, ChargerType, 0>, + >, +>; + +const PORT_COUNT: usize = 2; +type PortReceiverType = DynamicReceiver<'static, type_c_interface::service::event::PortEventData>; +type TypeCServiceEventReceiverType = type_c_service::service::event_receiver::ArrayEventReceiver< + 'static, + PORT_COUNT, + PortType, + PortReceiverType, + PowerPolicyReceiverType, +>; + +type TypeCServiceSenderType = NoopSender; +type TypeCRegistrationType = + type_c_service::service::registration::ArrayRegistration<'static, PortType, PORT_COUNT, TypeCServiceSenderType, 1>; +type TypeCServiceType = type_c_service::service::Service<'static, TypeCRegistrationType>; +type PortEventReceiverType = PortEventReceiver< + 'static, + SharedStateType, + DynamicReceiver<'static, PortEventBitfield>, + DynamicReceiver<'static, type_c_service::controller::event::Loopback>, +>; + +#[embassy_executor::task(pool_size = 2)] +async fn port_task(mut event_receiver: PortEventReceiverType, port: &'static PortType) { + port.lock().await.sync_state().await.unwrap(); -#[embassy_executor::task] -async fn pd_controller_task(controller: &'static Wrapper<'static>) { loop { - if let Err(e) = controller.process_next_event().await { - error!("Error processing controller event: {:?}", e); + let event = event_receiver.wait_event().await; + let output = port.lock().await.process_event(event).await; + if let Err(e) = output { + error!("Error processing event: {:?}", e); } } } #[embassy_executor::task] -async fn interrupt_task(mut int_in: Input<'static>, mut interrupt: Interrupt<'static>) { +async fn interrupt_task(mut int_in: Input<'static>, mut interrupt: InterruptProcessor<'static>) { tps6699x::task::interrupt_task(&mut int_in, &mut [&mut interrupt]).await; } #[embassy_executor::task] -async fn type_c_service_task() -> ! { - type_c_service::task(Default::default()).await; - unreachable!() +async fn interrupt_splitter_task( + mut interrupt_receiver: InterruptReceiver<'static, GlobalRawMutex, BusDevice<'static>>, + mut interrupt_splitter: PortEventSplitter<2, DynamicSender<'static, PortEventBitfield>>, +) -> ! { + loop { + let interrupts = interrupt_receiver.wait_interrupt().await; + interrupt_splitter.process_interrupts(interrupts).await; + } } #[embassy_executor::task] -async fn power_policy_service_task() { - power_policy_service::task::task(Default::default()) - .await - .expect("Failed to start power policy service task"); +async fn power_policy_task( + psu_events: PsuEventReceivers<'static, 2, PortType, DynamicReceiver<'static, psu::event::EventData>>, + power_policy: &'static PowerPolicyServiceType, +) { + power_policy_service::service::task::psu_task(psu_events, power_policy).await; +} + +#[embassy_executor::task] +async fn type_c_service_task( + service: &'static Mutex, + event_receiver: TypeCServiceEventReceiverType, +) { + type_c_service::task::task(service, event_receiver).await; } #[embassy_executor::main] @@ -84,14 +151,6 @@ async fn main(spawner: Spawner) { info!("Embedded service init"); embedded_services::init().await; - type_c::controller::init(); - - info!("Spawining power policy task"); - spawner.spawn(power_policy_service_task().unwrap()); - - info!("Spawining type-c service task"); - spawner.spawn(type_c_service_task().unwrap()); - let int_in = Input::new(p.PIO1_7, Pull::Up, Inverter::Disabled); static BUS: StaticCell>> = StaticCell::new(); let bus = BUS.init(Mutex::new( @@ -101,15 +160,15 @@ async fn main(spawner: Spawner) { let device = I2cDevice::new(bus); static CONTROLLER: StaticCell> = StaticCell::new(); - let controller = CONTROLLER.init(Controller::new_tps66994(device, ADDR1).unwrap()); - let (mut tps6699x, interrupt) = controller.make_parts(); + let controller = CONTROLLER.init(Controller::new_tps66994(device, Default::default(), ADDR1).unwrap()); + let (mut tps6699x, interrupt_processor, interrupt_receiver) = controller.make_parts(); info!("Resetting PD controller"); let mut delay = Delay; tps6699x.reset(&mut delay).await.unwrap(); info!("Spawining interrupt task"); - spawner.spawn(interrupt_task(int_in, interrupt).unwrap()); + spawner.spawn(interrupt_task(int_in, interrupt_processor).expect("Failed to spawn interrupt task")); // These aren't enabled by default tps6699x @@ -125,47 +184,104 @@ async fn main(spawner: Spawner) { .await .unwrap(); - static STORAGE: StaticCell> = StaticCell::new(); - let storage = STORAGE.init(Storage::new( - CONTROLLER0_ID, - 0, // CFU component ID - [(PORT0_ID, PORT0_PWR_ID), (PORT1_ID, PORT1_PWR_ID)], - )); - - static REFERENCED: StaticCell> = StaticCell::new(); - let referenced = REFERENCED.init( - storage - .create_referenced() - .expect("Failed to create referenced storage"), - ); - info!("Spawining PD controller task"); static CONTROLLER_MUTEX: StaticCell> = StaticCell::new(); let controller_mutex = CONTROLLER_MUTEX.init(Mutex::new(tps6699x_drv::tps66994( tps6699x, Default::default(), Default::default(), + "tps6699x_0", ))); - static WRAPPER: StaticCell = StaticCell::new(); - let wrapper = - WRAPPER.init(ControllerWrapper::try_new(controller_mutex, Default::default(), referenced, Validator).unwrap()); - - wrapper.register().await.unwrap(); - spawner.spawn(pd_controller_task(wrapper).unwrap()); + define_controller_port_static_cell_channel!(pub(self), port0, GlobalRawMutex, Tps6699xMutex<'static>); + let PortComponents { + port: port0, + power_policy_receiver: policy_receiver0, + event_receiver: event_receiver0, + interrupt_sender: port0_interrupt_sender, + type_c_receiver: type_c_receiver0, + } = port0::create("PD0", LocalPortId(0), Default::default(), controller_mutex); + + define_controller_port_static_cell_channel!(pub(self), port1, GlobalRawMutex, Tps6699xMutex<'static>); + let PortComponents { + port: port1, + power_policy_receiver: policy_receiver1, + event_receiver: event_receiver1, + interrupt_sender: port1_interrupt_sender, + type_c_receiver: type_c_receiver1, + } = port1::create("PD1", LocalPortId(1), Default::default(), controller_mutex); + + let port_event_splitter = PortEventSplitter::new([port0_interrupt_sender, port1_interrupt_sender]); + + // The service is the only receiver and we only use a DynImmediatePublisher, which doesn't take a publisher slot + static POWER_POLICY_CHANNEL: StaticCell< + PubSubChannel, + > = StaticCell::new(); + + let power_policy_channel = POWER_POLICY_CHANNEL.init(PubSubChannel::new()); + let power_policy_sender: PowerPolicySenderType = + MapSender::new(power_policy_channel.dyn_immediate_publisher(), |e| e.into()); + // Guaranteed to not panic since we initialized the channel above + let power_policy_subscriber = power_policy_channel.dyn_subscriber().unwrap(); + + // Create power policy service + let power_policy_registration = ArrayRegistration { + psus: [port0, port1], + chargers: [], + service_senders: [power_policy_sender], + }; + + static POWER_SERVICE: StaticCell = StaticCell::new(); + let power_service = POWER_SERVICE.init(Mutex::new(power_policy_service::service::Service::new( + power_policy_registration, + power_policy_service::service::config::Config::default(), + ))); - // Sync our internal state with the hardware - type_c::external::sync_controller_state(CONTROLLER0_ID).await.unwrap(); + static TYPE_C_SERVICE: StaticCell> = StaticCell::new(); + let type_c_service = TYPE_C_SERVICE.init(Mutex::new(Service::create( + Default::default(), + TypeCRegistrationType { + ports: [port0, port1], + service_senders: [NoopSender], + port_data: [ + PortData { + local_port: Some(LocalPortId(0)), + }, + PortData { + local_port: Some(LocalPortId(1)), + }, + ], + }, + ))); - embassy_time::Timer::after_secs(10).await; + info!("Spawining type-c service task"); + spawner.spawn( + type_c_service_task( + type_c_service, + TypeCServiceEventReceiverType::new( + [port0, port1], + [type_c_receiver0, type_c_receiver1], + power_policy_subscriber, + ), + ) + .expect("Failed to create type-c service task"), + ); - let status = type_c::external::get_controller_status(CONTROLLER0_ID).await.unwrap(); + info!("Spawining power policy task"); + spawner.spawn( + power_policy_task( + PsuEventReceivers::new([port0, port1], [policy_receiver0, policy_receiver1]), + power_service, + ) + .expect("Failed to create power policy task"), + ); - info!("Controller status: {:?}", status); + spawner.spawn(port_task(event_receiver0, port0).expect("Failed to create controller0 task")); - let status = type_c::external::get_port_status(PORT0_ID, Cached(true)).await.unwrap(); - info!("Port status: {:?}", status); + spawner.spawn(port_task(event_receiver1, port1).expect("Failed to create controller1 task")); - let status = type_c::external::get_port_status(PORT1_ID, Cached(true)).await.unwrap(); - info!("Port status: {:?}", status); + spawner.spawn( + interrupt_splitter_task(interrupt_receiver, port_event_splitter) + .expect("Failed to spawn interrupt splitter task"), + ); } diff --git a/examples/rt685s-evk/src/bin/type_c_cfu.rs b/examples/rt685s-evk/src/bin/type_c_cfu.rs index 60abcc754..4f484753a 100644 --- a/examples/rt685s-evk/src/bin/type_c_cfu.rs +++ b/examples/rt685s-evk/src/bin/type_c_cfu.rs @@ -1,30 +1,44 @@ #![no_std] #![no_main] -use ::tps6699x::{ADDR1, TPS66994_NUM_PORTS}; +use ::tps6699x::ADDR1; +use ::tps6699x::asynchronous::embassy::interrupt::InterruptReceiver; +use cfu_service::CfuClient; +use cfu_service::component::{CfuDevice, InternalResponseData, RequestData}; use embassy_embedded_hal::shared_bus::asynch::i2c::I2cDevice; use embassy_executor::Spawner; use embassy_imxrt::gpio::{Input, Inverter, Pull}; use embassy_imxrt::i2c::Async; use embassy_imxrt::i2c::master::{Config, I2cMaster}; use embassy_imxrt::{bind_interrupts, peripherals}; +use embassy_sync::channel::{DynamicReceiver, DynamicSender}; use embassy_sync::mutex::Mutex; +use embassy_sync::once_lock::OnceLock; +use embassy_sync::pubsub::{DynImmediatePublisher, DynSubscriber, PubSubChannel}; use embassy_time::Timer; use embassy_time::{self as _, Delay}; use embedded_cfu_protocol::protocol_definitions::*; use embedded_cfu_protocol::protocol_definitions::{FwUpdateOffer, FwUpdateOfferResponse, FwVersion}; -use embedded_services::cfu::component::InternalResponseData; -use embedded_services::cfu::component::RequestData; -use embedded_services::power::policy::DeviceId as PowerId; -use embedded_services::type_c::{self, ControllerId}; -use embedded_services::{GlobalRawMutex, cfu}; +use embedded_services::GlobalRawMutex; +use embedded_services::event::{MapSender, NoopSender}; use embedded_services::{error, info}; -use embedded_usb_pd::GlobalPortId; +use embedded_usb_pd::LocalPortId; +use power_policy_interface::psu; +use power_policy_service::psu::PsuEventReceivers; +use power_policy_service::service::registration::ArrayRegistration; use static_cell::StaticCell; use tps6699x::asynchronous::embassy as tps6699x; +use type_c_interface::port::event::PortEventBitfield; +use type_c_service::controller::Port; +use type_c_service::controller::event_receiver::{ + EventReceiver as PortEventReceiver, InterruptReceiver as _, PortEventSplitter, +}; +use type_c_service::controller::macros::PortComponents; +use type_c_service::controller::state::SharedState as PortSharedState; +use type_c_service::define_controller_port_static_cell_channel; use type_c_service::driver::tps6699x::{self as tps6699x_drv}; -use type_c_service::wrapper::ControllerWrapper; -use type_c_service::wrapper::backing::{ReferencedStorage, Storage}; +use type_c_service::service::Service; +use type_c_service::service::registration::PortData; extern crate rt685s_evk_example; @@ -32,52 +46,129 @@ bind_interrupts!(struct Irqs { FLEXCOMM2 => embassy_imxrt::i2c::InterruptHandler; }); -struct Validator; +struct CfuCustomization; -impl type_c_service::wrapper::FwOfferValidator for Validator { - fn validate(&self, _current: FwVersion, _offer: &FwUpdateOffer) -> FwUpdateOfferResponse { +impl cfu_service::customization::Customization for CfuCustomization { + fn validate(&mut self, _current: FwVersion, _offer: &FwUpdateOffer) -> FwUpdateOfferResponse { // For this example, we always accept the offer FwUpdateOfferResponse::new_accept(HostToken::Driver) } } +type PortSharedStateType = Mutex; +type PortType = Mutex< + GlobalRawMutex, + Port< + 'static, + Tps6699xMutex<'static>, + PortSharedStateType, + DynamicSender<'static, type_c_interface::service::event::PortEventData>, + DynamicSender<'static, power_policy_interface::psu::event::EventData>, + DynamicSender<'static, type_c_service::controller::event::Loopback>, + >, +>; +type ChargerType = power_policy_interface::charger::mock::ChargerType; + type BusMaster<'a> = I2cMaster<'a, Async>; type BusDevice<'a> = I2cDevice<'a, GlobalRawMutex, BusMaster<'a>>; type Tps6699xMutex<'a> = Mutex>>; -type Wrapper<'a> = ControllerWrapper<'a, GlobalRawMutex, Tps6699xMutex<'a>, Validator>; type Controller<'a> = tps6699x::controller::Controller>; -type Interrupt<'a> = tps6699x::Interrupt<'a, GlobalRawMutex, BusDevice<'a>>; +type InterruptProcessor<'a> = tps6699x::interrupt::InterruptProcessor<'a, GlobalRawMutex, BusDevice<'a>>; + +type PowerPolicySenderType = MapSender< + power_policy_interface::service::event::Event<'static, PortType>, + power_policy_interface::service::event::EventData, + DynImmediatePublisher<'static, power_policy_interface::service::event::EventData>, + fn( + power_policy_interface::service::event::Event<'static, PortType>, + ) -> power_policy_interface::service::event::EventData, +>; + +type PowerPolicyReceiverType = DynSubscriber<'static, power_policy_interface::service::event::EventData>; + +type PowerPolicyServiceType = Mutex< + GlobalRawMutex, + power_policy_service::service::Service< + 'static, + ArrayRegistration<'static, PortType, 2, PowerPolicySenderType, 1, ChargerType, 0>, + >, +>; + +const PORT_COUNT: usize = 2; +type PortReceiverType = DynamicReceiver<'static, type_c_interface::service::event::PortEventData>; +type TypeCServiceEventReceiverType = type_c_service::service::event_receiver::ArrayEventReceiver< + 'static, + PORT_COUNT, + PortType, + PortReceiverType, + PowerPolicyReceiverType, +>; + +type TypeCServiceSenderType = NoopSender; +type TypeCRegistrationType = + type_c_service::service::registration::ArrayRegistration<'static, PortType, PORT_COUNT, TypeCServiceSenderType, 1>; +type TypeCServiceType = type_c_service::service::Service<'static, TypeCRegistrationType>; +type PortEventReceiverType = PortEventReceiver< + 'static, + PortSharedStateType, + DynamicReceiver<'static, PortEventBitfield>, + DynamicReceiver<'static, type_c_service::controller::event::Loopback>, +>; + +type CfuUpdaterSharedStateType = Mutex; +type CfuUpdaterType<'a> = + cfu_service::basic::Updater<'a, Tps6699xMutex<'a>, CfuUpdaterSharedStateType, CfuCustomization>; -const CONTROLLER0_ID: ControllerId = ControllerId(0); const CONTROLLER0_CFU_ID: ComponentId = 0x12; -const PORT0_ID: GlobalPortId = GlobalPortId(0); -const PORT1_ID: GlobalPortId = GlobalPortId(1); -const PORT0_PWR_ID: PowerId = PowerId(0); -const PORT1_PWR_ID: PowerId = PowerId(1); -#[embassy_executor::task] -async fn pd_controller_task(controller: &'static Wrapper<'static>) { +#[embassy_executor::task(pool_size = 2)] +async fn port_task(mut event_receiver: PortEventReceiverType, port: &'static PortType) { + port.lock().await.sync_state().await.unwrap(); + loop { - if let Err(e) = controller.process_next_event().await { - error!("Error processing controller event: {:?}", e); + let event = event_receiver.wait_event().await; + let output = port.lock().await.process_event(event).await; + if let Err(e) = output { + error!("Error processing event: {:?}", e); } } } #[embassy_executor::task] -async fn interrupt_task(mut int_in: Input<'static>, mut interrupt: Interrupt<'static>) { +async fn cfu_updater_task( + mut event_receiver: cfu_service::basic::event_receiver::EventReceiver<'static, CfuUpdaterSharedStateType>, + mut updater: CfuUpdaterType<'static>, +) -> ! { + loop { + let event = event_receiver.wait_next().await; + let output = updater.process_event(event).await; + event_receiver.finalize(output).await; + } +} + +#[embassy_executor::task] +async fn interrupt_task(mut int_in: Input<'static>, mut interrupt: InterruptProcessor<'static>) { tps6699x::task::interrupt_task(&mut int_in, &mut [&mut interrupt]).await; } #[embassy_executor::task] -async fn fw_update_task() { +async fn interrupt_splitter_task( + mut interrupt_receiver: InterruptReceiver<'static, GlobalRawMutex, BusDevice<'static>>, + mut interrupt_splitter: PortEventSplitter<2, DynamicSender<'static, PortEventBitfield>>, +) -> ! { + loop { + let interrupts = interrupt_receiver.wait_interrupt().await; + interrupt_splitter.process_interrupts(interrupts).await; + } +} + +#[embassy_executor::task] +async fn fw_update_task(cfu_client: &'static CfuClient) { Timer::after_millis(1000).await; - let context = cfu::ContextToken::create().unwrap(); - let device = context.get_device(CONTROLLER0_CFU_ID).await.unwrap(); info!("Getting FW version"); - let response = device - .execute_device_request(RequestData::FwVersionRequest) + let response = cfu_client + .route_request(CONTROLLER0_CFU_ID, RequestData::FwVersionRequest) .await .unwrap(); let prev_version = match response { @@ -89,14 +180,17 @@ async fn fw_update_task() { info!("Got version: {:#x}", prev_version); info!("Giving offer"); - let offer = device - .execute_device_request(RequestData::GiveOffer(FwUpdateOffer::new( - HostToken::Driver, + let offer = cfu_client + .route_request( CONTROLLER0_CFU_ID, - FwVersion::new(0x211), - 0, - 0, - ))) + RequestData::GiveOffer(FwUpdateOffer::new( + HostToken::Driver, + CONTROLLER0_CFU_ID, + FwVersion::new(0x211), + 0, + 0, + )), + ) .await .unwrap(); info!("Got response: {:?}", offer); @@ -126,8 +220,8 @@ async fn fw_update_task() { }; info!("Sending chunk {} of {}", i + 1, num_chunks); - let response = device - .execute_device_request(RequestData::GiveContent(request)) + let response = cfu_client + .route_request(CONTROLLER0_CFU_ID, RequestData::GiveContent(request)) .await .unwrap(); info!("Got response: {:?}", response); @@ -135,8 +229,8 @@ async fn fw_update_task() { Timer::after_millis(2000).await; info!("Getting FW version"); - let response = device - .execute_device_request(RequestData::FwVersionRequest) + let response = cfu_client + .route_request(CONTROLLER0_CFU_ID, RequestData::FwVersionRequest) .await .unwrap(); let version = match response { @@ -150,16 +244,19 @@ async fn fw_update_task() { } #[embassy_executor::task] -async fn type_c_service_task() -> ! { - type_c_service::task(Default::default()).await; - unreachable!() +async fn power_policy_task( + psu_events: PsuEventReceivers<'static, 2, PortType, DynamicReceiver<'static, psu::event::EventData>>, + power_policy: &'static PowerPolicyServiceType, +) { + power_policy_service::service::task::psu_task(psu_events, power_policy).await; } #[embassy_executor::task] -async fn power_policy_service_task() { - power_policy_service::task::task(Default::default()) - .await - .expect("Failed to start power policy service task"); +async fn type_c_service_task( + service: &'static Mutex, + event_receiver: TypeCServiceEventReceiverType, +) { + type_c_service::task::task(service, event_receiver).await; } #[embassy_executor::main] @@ -169,14 +266,6 @@ async fn main(spawner: Spawner) { info!("Embedded service init"); embedded_services::init().await; - type_c::controller::init(); - - info!("Spawining power policy task"); - spawner.spawn(power_policy_service_task().unwrap()); - - info!("Spawining type-c service task"); - spawner.spawn(type_c_service_task().unwrap()); - let int_in = Input::new(p.PIO1_7, Pull::Up, Inverter::Disabled); static BUS: StaticCell>> = StaticCell::new(); let bus = BUS.init(Mutex::new( @@ -186,15 +275,15 @@ async fn main(spawner: Spawner) { let device = I2cDevice::new(bus); static CONTROLLER: StaticCell> = StaticCell::new(); - let controller = CONTROLLER.init(Controller::new_tps66994(device, ADDR1).unwrap()); - let (mut tps6699x, interrupt) = controller.make_parts(); + let controller = CONTROLLER.init(Controller::new_tps66994(device, Default::default(), ADDR1).unwrap()); + let (mut tps6699x, interrupt_processor, interrupt_receiver) = controller.make_parts(); info!("Resetting PD controller"); let mut delay = Delay; tps6699x.reset(&mut delay).await.unwrap(); info!("Spawining interrupt task"); - spawner.spawn(interrupt_task(int_in, interrupt).unwrap()); + spawner.spawn(interrupt_task(int_in, interrupt_processor).expect("Failed to spawn interrupt task")); // These aren't enabled by default tps6699x @@ -204,39 +293,137 @@ async fn main(spawner: Spawner) { mask.set_intel_vid_status_updated(true); mask.set_usb_status_updated(true); mask.set_power_path_switch_changed(true); + mask.set_sink_ready(true); *mask }) .await .unwrap(); - static STORAGE: StaticCell> = StaticCell::new(); - let storage = STORAGE.init(Storage::new( - CONTROLLER0_ID, - CONTROLLER0_CFU_ID, - [(PORT0_ID, PORT0_PWR_ID), (PORT1_ID, PORT1_PWR_ID)], - )); - - static REFERENCED: StaticCell> = StaticCell::new(); - let referenced = REFERENCED.init( - storage - .create_referenced() - .expect("Failed to create referenced storage"), - ); - info!("Spawining PD controller task"); static CONTROLLER_MUTEX: StaticCell> = StaticCell::new(); let controller_mutex = CONTROLLER_MUTEX.init(Mutex::new(tps6699x_drv::tps66994( tps6699x, Default::default(), Default::default(), + "tps6699x_0", ))); - static WRAPPER: StaticCell = StaticCell::new(); - let wrapper = - WRAPPER.init(ControllerWrapper::try_new(controller_mutex, Default::default(), referenced, Validator).unwrap()); + // Create controller CFU device and updater + static CFU_DEVICE: StaticCell = StaticCell::new(); + let cfu_device = CFU_DEVICE.init(CfuDevice::new(CONTROLLER0_CFU_ID)); + + static CFU_SHARED_STATE: StaticCell = StaticCell::new(); + let cfu_shared_state = CFU_SHARED_STATE.init(Mutex::new(cfu_service::basic::state::SharedState::new())); + + let cfu_event_receiver = + cfu_service::basic::event_receiver::EventReceiver::new(cfu_device, cfu_shared_state, Default::default()); + + let cfu_updater = cfu_service::basic::Updater::new( + controller_mutex, + cfu_shared_state, + Default::default(), + CONTROLLER0_CFU_ID, + CfuCustomization, + ); + + // Create CFU client + static CFU_CLIENT: OnceLock = OnceLock::new(); + let cfu_client = CfuClient::new(&CFU_CLIENT).await; + cfu_client.register_device(cfu_device).unwrap(); + + define_controller_port_static_cell_channel!(pub(self), port0, GlobalRawMutex, Tps6699xMutex<'static>); + let PortComponents { + port: port0, + power_policy_receiver: policy_receiver0, + event_receiver: event_receiver0, + interrupt_sender: port0_interrupt_sender, + type_c_receiver: type_c_receiver0, + } = port0::create("PD0", LocalPortId(0), Default::default(), controller_mutex); + + define_controller_port_static_cell_channel!(pub(self),port1, GlobalRawMutex, Tps6699xMutex<'static>); + let PortComponents { + port: port1, + power_policy_receiver: policy_receiver1, + event_receiver: event_receiver1, + interrupt_sender: port1_interrupt_sender, + type_c_receiver: type_c_receiver1, + } = port1::create("PD1", LocalPortId(1), Default::default(), controller_mutex); + + let port_event_splitter = PortEventSplitter::new([port0_interrupt_sender, port1_interrupt_sender]); + + // Create power policy service + // The service is the only receiver and we only use a DynImmediatePublisher, which doesn't take a publisher slot + static POWER_POLICY_CHANNEL: StaticCell< + PubSubChannel, + > = StaticCell::new(); + + let power_policy_channel = POWER_POLICY_CHANNEL.init(PubSubChannel::new()); + let power_policy_sender: PowerPolicySenderType = + MapSender::new(power_policy_channel.dyn_immediate_publisher(), |e| e.into()); + // Guaranteed to not panic since we initialized the channel above + let power_policy_subscriber = power_policy_channel.dyn_subscriber().unwrap(); + + let power_policy_registration = ArrayRegistration { + psus: [port0, port1], + chargers: [], + service_senders: [power_policy_sender], + }; + + static POWER_SERVICE: StaticCell = StaticCell::new(); + let power_service = POWER_SERVICE.init(Mutex::new(power_policy_service::service::Service::new( + power_policy_registration, + power_policy_service::service::config::Config::default(), + ))); + + static TYPE_C_SERVICE: StaticCell> = StaticCell::new(); + let type_c_service = TYPE_C_SERVICE.init(Mutex::new(Service::create( + Default::default(), + TypeCRegistrationType { + ports: [port0, port1], + port_data: [ + PortData { + local_port: Some(LocalPortId(0)), + }, + PortData { + local_port: Some(LocalPortId(1)), + }, + ], + service_senders: [NoopSender], + }, + ))); + + info!("Spawining type-c service task"); + spawner.spawn( + type_c_service_task( + type_c_service, + TypeCServiceEventReceiverType::new( + [port0, port1], + [type_c_receiver0, type_c_receiver1], + power_policy_subscriber, + ), + ) + .expect("Failed to spawn type-c service task"), + ); + + info!("Spawining power policy task"); + spawner.spawn( + power_policy_task( + PsuEventReceivers::new([port0, port1], [policy_receiver0, policy_receiver1]), + power_service, + ) + .expect("Failed to create power policy task"), + ); + + spawner.spawn(port_task(event_receiver0, port0).expect("Failed to create controller0 task")); + + spawner.spawn(port_task(event_receiver1, port1).expect("Failed to create controller1 task")); + + spawner.spawn( + interrupt_splitter_task(interrupt_receiver, port_event_splitter) + .expect("Failed to spawn interrupt splitter task"), + ); - wrapper.register().await.unwrap(); - spawner.spawn(pd_controller_task(wrapper).unwrap()); + spawner.spawn(cfu_updater_task(cfu_event_receiver, cfu_updater).expect("Failed to create CFU updater task")); - spawner.spawn(fw_update_task().unwrap()); + spawner.spawn(fw_update_task(cfu_client).expect("Failed to create fw update task")); } diff --git a/examples/std/Cargo.lock b/examples/std/Cargo.lock index 0bdc0a366..b48fb08c4 100644 --- a/examples/std/Cargo.lock +++ b/examples/std/Cargo.lock @@ -4,13 +4,63 @@ version = 4 [[package]] name = "aho-corasick" -version = "1.1.3" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" dependencies = [ "memchr", ] +[[package]] +name = "anstream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "anstyle-parse" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys", +] + [[package]] name = "aquamarine" version = "0.6.0" @@ -22,18 +72,7 @@ dependencies = [ "proc-macro-error2", "proc-macro2", "quote", - "syn 2.0.106", -] - -[[package]] -name = "atty" -version = "0.2.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" -dependencies = [ - "hermit-abi", - "libc", - "winapi", + "syn", ] [[package]] @@ -55,19 +94,30 @@ dependencies = [ name = "battery-service" version = "0.1.0" dependencies = [ + "battery-service-interface", "embassy-futures", "embassy-sync", "embassy-time", "embedded-batteries-async", "embedded-services", "log", + "odp-service-common", + "power-policy-interface", +] + +[[package]] +name = "battery-service-interface" +version = "0.1.0" +dependencies = [ + "embedded-batteries-async", + "log", ] [[package]] name = "bbq2" -version = "0.4.2" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25d3e5fd78b60ce3a60e21e9a92b1d5e741328264de8e609193e2bf7616747bd" +checksum = "3a71c275de2904ad7132c0b6ccfd5d6276c9c1b6618b049b90cbffee98a1796d" dependencies = [ "const-init", "critical-section", @@ -93,14 +143,6 @@ dependencies = [ "virtue", ] -[[package]] -name = "bit-register" -version = "0.1.0" -source = "git+https://github.com/OpenDevicePartnership/odp-utilities?rev=2f79d238#2f79d238149049d458199a9a9129b54be7893aee" -dependencies = [ - "num-traits", -] - [[package]] name = "bit-register" version = "0.1.0" @@ -123,22 +165,22 @@ checksum = "f798d2d157e547aa99aab0967df39edd0b70307312b6f8bd2848e6abe40896e0" [[package]] name = "bitfield" -version = "0.19.2" +version = "0.19.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62a3a774b2fcac1b726922b921ebba5e9fe36ad37659c822cf8ff2c1e0819892" +checksum = "21ba6517c6b0f2bf08be60e187ab64b038438f22dd755614d8fe4d4098c46419" dependencies = [ "bitfield-macros", ] [[package]] name = "bitfield-macros" -version = "0.19.2" +version = "0.19.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52511b09931f7d5fe3a14f23adefbc23e5725b184013e96c8419febb61f14734" +checksum = "f48d6ace212fdf1b45fd6b566bb40808415344642b76c3224c07c8df9da81e97" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn", ] [[package]] @@ -149,7 +191,7 @@ checksum = "8769c4854c5ada2852ddf6fd09d15cf43d4c2aaeccb4de6432f5402f08a6003b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn", ] [[package]] @@ -160,21 +202,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.9.4" +version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394" - -[[package]] -name = "bitvec" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" -dependencies = [ - "funty", - "radium", - "tap", - "wyz", -] +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" [[package]] name = "byteorder" @@ -184,9 +214,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "cc" -version = "1.2.35" +version = "1.2.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "590f9024a68a8c40351881787f1934dc11afd69090f5edb6831464694d836ea3" +checksum = "43c5703da9466b66a946814e1adf53ea2c90f10063b86290cc9eb67ce3478a20" dependencies = [ "find-msvc-tools", "shlex", @@ -194,9 +224,9 @@ dependencies = [ [[package]] name = "cfg-if" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "cfu-service" @@ -207,10 +237,17 @@ dependencies = [ "embassy-time", "embedded-cfu-protocol", "embedded-services", + "fw-update-interface", "heapless 0.9.2", "log", ] +[[package]] +name = "colorchoice" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + [[package]] name = "const-init" version = "1.0.0" @@ -239,26 +276,6 @@ dependencies = [ "volatile-register", ] -[[package]] -name = "cortex-m-rt" -version = "0.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "801d4dec46b34c299ccf6b036717ae0fce602faa4f4fe816d9013b9a7c9f5ba6" -dependencies = [ - "cortex-m-rt-macros", -] - -[[package]] -name = "cortex-m-rt-macros" -version = "0.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e37549a379a9e0e6e576fd208ee60394ccb8be963889eebba3ffe0980364f472" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.106", -] - [[package]] name = "critical-section" version = "1.2.0" @@ -286,7 +303,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.106", + "syn", ] [[package]] @@ -297,7 +314,7 @@ checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" dependencies = [ "darling_core", "quote", - "syn 2.0.106", + "syn", ] [[package]] @@ -306,6 +323,7 @@ version = "0.1.0" dependencies = [ "bbq2", "critical-section", + "debug-service-messages", "defmt 0.3.100", "embassy-sync", "embedded-services", @@ -313,6 +331,14 @@ dependencies = [ "rtt-target", ] +[[package]] +name = "debug-service-messages" +version = "0.1.0" +dependencies = [ + "embedded-services", + "num_enum", +] + [[package]] name = "defmt" version = "0.3.100" @@ -342,7 +368,7 @@ dependencies = [ "proc-macro-error2", "proc-macro2", "quote", - "syn 2.0.106", + "syn", ] [[package]] @@ -366,9 +392,9 @@ dependencies = [ [[package]] name = "document-features" -version = "0.2.11" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95249b50c6c185bee49034bcb378a49dc2b5dff0be90ff6616d31d64febab05d" +checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" dependencies = [ "litrs", ] @@ -402,7 +428,7 @@ dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.106", + "syn", ] [[package]] @@ -461,9 +487,9 @@ dependencies = [ [[package]] name = "embassy-time-queue-utils" -version = "0.3.2" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "168297bf80aaf114b3c9ad589bf38b01b3009b9af7f97cd18086c5bbf96f5693" +checksum = "7fe6ecec32eac4f98c5427904006b0fc61f77d350e1572530f744ef14650f7a1" dependencies = [ "embassy-executor-timer-queue", "heapless 0.9.2", @@ -476,7 +502,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40f975432b4e146342a1589c563cffab6b7a692024cb511bf87b6bfe78c84125" dependencies = [ "bitfield-struct", - "bitflags 2.9.4", + "bitflags 2.11.1", "embedded-hal 1.0.0", "zerocopy", ] @@ -495,7 +521,7 @@ dependencies = [ [[package]] name = "embedded-cfu-protocol" version = "0.2.0" -source = "git+https://github.com/OpenDevicePartnership/embedded-cfu#a4cc8707842b878048447abbf2af4efa79fed368" +source = "git+https://github.com/OpenDevicePartnership/embedded-cfu#e0d776017cf34c902c9f2a2be0c75fe73a3a4dda" dependencies = [ "embedded-io-async 0.6.1", "log", @@ -547,31 +573,6 @@ dependencies = [ "embedded-hal 1.0.0", ] -[[package]] -name = "embedded-hal-mock" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9a0f04f8886106faf281c47b6a0e4054a369baedaf63591fdb8da9761f3f379" -dependencies = [ - "embedded-hal 0.2.7", - "embedded-hal 1.0.0", - "embedded-hal-async", - "embedded-hal-nb", - "embedded-time", - "nb 1.1.0", - "void", -] - -[[package]] -name = "embedded-hal-nb" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fba4268c14288c828995299e59b12babdbe170f6c6d73731af1b4648142e8605" -dependencies = [ - "embedded-hal 1.0.0", - "nb 1.1.0", -] - [[package]] name = "embedded-io" version = "0.6.1" @@ -604,9 +605,12 @@ dependencies = [ [[package]] name = "embedded-sensors-hal" -version = "0.1.0" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "971cd616a2326c63f660375e485e2f4573575b0bd293d228d7817c2b07be3475" +checksum = "8c703756bee31e7aaf55d8fb6dcf7337cfc231cfb4a3ad34b9df509846fd9001" +dependencies = [ + "paste", +] [[package]] name = "embedded-sensors-hal-async" @@ -623,64 +627,58 @@ name = "embedded-services" version = "0.1.0" dependencies = [ "bitfield 0.17.0", - "bitvec", "cortex-m", - "cortex-m-rt", "critical-section", + "embassy-futures", "embassy-sync", - "embassy-time", - "embedded-batteries-async", - "embedded-cfu-protocol", - "embedded-usb-pd", - "heapless 0.9.2", "log", "mctp-rs", - "num_enum", + "paste", "portable-atomic", "serde", - "uuid", -] - -[[package]] -name = "embedded-time" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7a4b4d10ac48d08bfe3db7688c402baadb244721f30a77ce360bd24c3dffe58" -dependencies = [ - "num", ] [[package]] name = "embedded-usb-pd" version = "0.1.0" -source = "git+https://github.com/OpenDevicePartnership/embedded-usb-pd#21d0e228d21ddc6ccaeffc01d98ef9a5b87941ef" +source = "git+https://github.com/OpenDevicePartnership/embedded-usb-pd#0061a1e94a25c8db33ac1e8a0bb5a6b638fe2cd7" dependencies = [ "aquamarine", "bincode", - "bitfield 0.19.2", + "bitfield 0.19.4", "embedded-hal-async", ] [[package]] -name = "env_logger" -version = "0.9.3" +name = "env_filter" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a12e6657c4c97ebab115a42dcee77225f7f482cdd841cf7088c657a42e9e00e7" +checksum = "32e90c2accc4b07a8456ea0debdc2e7587bdd890680d71173a15d4ae604f6eef" dependencies = [ - "atty", - "humantime", "log", "regex", - "termcolor", +] + +[[package]] +name = "env_logger" +version = "0.11.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0621c04f2196ac3f488dd583365b9c09be011a4ab8b9f37248ffcc8f6198b56a" +dependencies = [ + "anstream", + "anstyle", + "env_filter", + "jiff", + "log", ] [[package]] name = "espi-device" version = "0.1.0" -source = "git+https://github.com/OpenDevicePartnership/haf-ec-service#8cdd61095471903b1b438dbb0eee142676cc3d74" +source = "git+https://github.com/OpenDevicePartnership/haf-ec-service#09eda26a729738adbd177231600acdb981690375" dependencies = [ - "bit-register 0.1.0 (git+https://github.com/OpenDevicePartnership/odp-utilities?rev=2f79d238)", - "bitflags 2.9.4", + "bit-register", + "bitflags 2.11.1", "num-traits", "num_enum", "static_assertions", @@ -689,9 +687,9 @@ dependencies = [ [[package]] name = "find-msvc-tools" -version = "0.1.0" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e178e4fba8a2726903f6ba98a6d221e76f9c12c650d5dc0e6afdc50677b49650" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" [[package]] name = "fnv" @@ -699,36 +697,39 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" -[[package]] -name = "funty" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" - [[package]] name = "futures-core" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" [[package]] name = "futures-sink" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "fw-update-interface" +version = "0.1.0" +dependencies = [ + "embedded-services", + "log", +] [[package]] name = "generator" -version = "0.8.7" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "605183a538e3e2a9c1038635cc5c2d194e2ee8fd0d1b66b8349fad7dbacce5a2" +checksum = "52f04ae4152da20c76fe800fa48659201d5cf627c5149ca0b707b69d7eef6cf9" dependencies = [ "cc", "cfg-if", "libc", "log", "rustversion", - "windows", + "windows-link", + "windows-result", ] [[package]] @@ -762,24 +763,9 @@ dependencies = [ [[package]] name = "heck" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" - -[[package]] -name = "hermit-abi" -version = "0.1.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" -dependencies = [ - "libc", -] - -[[package]] -name = "humantime" -version = "2.2.0" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b112acc8b3adf4b107a8ec20977da0273a8c386765a3ec0229bd500a1443f9f" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "ident_case" @@ -806,6 +792,12 @@ dependencies = [ "quote", ] +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + [[package]] name = "itertools" version = "0.10.5" @@ -824,6 +816,30 @@ dependencies = [ "either", ] +[[package]] +name = "jiff" +version = "0.2.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a3546dc96b6d42c5f24902af9e2538e82e39ad350b0c766eb3fbf2d8f3d8359" +dependencies = [ + "jiff-static", + "log", + "portable-atomic", + "portable-atomic-util", + "serde_core", +] + +[[package]] +name = "jiff-static" +version = "0.2.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a8c8b344124222efd714b73bb41f8b5120b27a7cc1c75593a6ff768d9d05aa4" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "lazy_static" version = "1.5.0" @@ -832,21 +848,21 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" -version = "0.2.175" +version = "0.2.185" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543" +checksum = "52ff2c0fe9bc6cb6b14a0592c2ff4fa9ceb83eea9db979b0487cd054946a2b8f" [[package]] name = "litrs" -version = "0.4.2" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5e54036fe321fd421e10d732f155734c4e4afd610dd556d9a82833ab3ee0bed" +checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" [[package]] name = "log" -version = "0.4.28" +version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" [[package]] name = "loom" @@ -888,9 +904,8 @@ dependencies = [ [[package]] name = "mctp-rs" version = "0.1.0" -source = "git+https://github.com/dymk/mctp-rs#4456c65131366acd5e605c7d88a707881fa9e9f0" dependencies = [ - "bit-register 0.1.0 (git+https://github.com/OpenDevicePartnership/odp-utilities)", + "bit-register", "espi-device", "num_enum", "smbus-pec", @@ -899,9 +914,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.7.5" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" [[package]] name = "mutex-traits" @@ -932,64 +947,11 @@ checksum = "8d5439c4ad607c3c23abf66de8c8bf57ba8adcd1f129e699851a6e43935d339d" [[package]] name = "nu-ansi-term" -version = "0.50.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4a28e057d01f97e61255210fcff094d74ed0466038633e95017f5beb68e4399" -dependencies = [ - "windows-sys 0.52.0", -] - -[[package]] -name = "num" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b7a8e9be5e039e2ff869df49155f1c06bd01ade2117ec783e56ab0932b67a8f" -dependencies = [ - "num-complex", - "num-integer", - "num-iter", - "num-rational", - "num-traits", -] - -[[package]] -name = "num-complex" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "747d632c0c558b87dbabbe6a82f3b4ae03720d0646ac5b7b4dae89394be5f2c5" -dependencies = [ - "num-traits", -] - -[[package]] -name = "num-integer" -version = "0.1.46" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" -dependencies = [ - "num-traits", -] - -[[package]] -name = "num-iter" -version = "0.1.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" -dependencies = [ - "autocfg", - "num-integer", - "num-traits", -] - -[[package]] -name = "num-rational" -version = "0.3.2" +version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12ac428b1cb17fce6f731001d307d351ec70a6d202fc2e60f7d4c5e42d8f4f07" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "autocfg", - "num-integer", - "num-traits", + "windows-sys", ] [[package]] @@ -1019,14 +981,28 @@ checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn", +] + +[[package]] +name = "odp-service-common" +version = "0.1.0" +dependencies = [ + "embedded-services", + "static_cell", ] [[package]] name = "once_cell" -version = "1.21.3" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" [[package]] name = "paste" @@ -1036,35 +1012,56 @@ checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" [[package]] name = "pin-project" -version = "1.1.10" +version = "1.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a" +checksum = "f1749c7ed4bcaf4c3d0a3efc28538844fb29bcdd7d2b67b2be7e20ba861ff517" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.1.10" +version = "1.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" +checksum = "d9b20ed30f105399776b9c883e68e536ef602a16ae6f596d2c473591d6ad64c6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn", ] [[package]] name = "pin-project-lite" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" [[package]] name = "portable-atomic" -version = "1.11.1" +version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" + +[[package]] +name = "portable-atomic-util" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a106d1259c23fac8e543272398ae0e3c0b8d33c88ed73d0cc71b0f1d902618" +dependencies = [ + "portable-atomic", +] + +[[package]] +name = "power-policy-interface" +version = "0.1.0" +dependencies = [ + "bitfield 0.17.0", + "embassy-sync", + "embedded-batteries-async", + "embedded-services", + "log", + "num_enum", +] [[package]] name = "power-policy-service" @@ -1076,6 +1073,7 @@ dependencies = [ "embedded-services", "heapless 0.9.2", "log", + "power-policy-interface", ] [[package]] @@ -1097,38 +1095,32 @@ dependencies = [ "proc-macro-error-attr2", "proc-macro2", "quote", - "syn 2.0.106", + "syn", ] [[package]] name = "proc-macro2" -version = "1.0.101" +version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.40" +version = "1.0.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" dependencies = [ "proc-macro2", ] -[[package]] -name = "radium" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" - [[package]] name = "regex" -version = "1.11.2" +version = "1.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23d7fd106d8c02486a8d64e778353d1cffe08ce79ac2e82f540c86d0facf6912" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" dependencies = [ "aho-corasick", "memchr", @@ -1138,9 +1130,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.10" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b9458fa0bfeeac22b5ca447c63aaf45f28439a709ccd244698632f9aa6394d6" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" dependencies = [ "aho-corasick", "memchr", @@ -1149,15 +1141,15 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.6" +version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "caf4aa5b0f434c91fe5c7f1ecb6a5ece2130b02ad2a590589dda5146df959001" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" [[package]] name = "rtt-target" -version = "0.6.1" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4235cd78091930e907d2a510adb0db1369e82668eafa338f109742fa0c83059d" +checksum = "e7afed1f4302eeba88c601636cf2c554c45e1cbb464bab44c6012bab0e71473c" dependencies = [ "critical-section", "portable-atomic", @@ -1202,22 +1194,32 @@ checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" [[package]] name = "serde" -version = "1.0.219" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.219" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn", ] [[package]] @@ -1252,9 +1254,9 @@ dependencies = [ [[package]] name = "stable_deref_trait" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" [[package]] name = "static_assertions" @@ -1279,23 +1281,24 @@ dependencies = [ "cfu-service", "critical-section", "debug-service", + "debug-service-messages", "defmt 0.3.100", "embassy-executor", "embassy-sync", "embassy-time", "embedded-batteries-async", "embedded-cfu-protocol", - "embedded-fans-async", - "embedded-hal-async", - "embedded-hal-mock", - "embedded-sensors-hal-async", "embedded-services", "embedded-usb-pd", "env_logger", "log", + "odp-service-common", + "power-policy-interface", "power-policy-service", "static_cell", "thermal-service", + "thermal-service-interface", + "type-c-interface", "type-c-service", ] @@ -1307,53 +1310,27 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "subenum" -version = "1.1.2" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f5d5dfb8556dd04017db5e318bbeac8ab2b0c67b76bf197bfb79e9b29f18ecf" +checksum = "ec3d08fe7078c57309d5c3d938e50eba95ba1d33b9c3a101a8465fc6861a5416" dependencies = [ "heck", "proc-macro2", "quote", - "syn 1.0.109", -] - -[[package]] -name = "syn" -version = "1.0.109" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", + "syn", ] [[package]] name = "syn" -version = "2.0.106" +version = "2.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] -[[package]] -name = "tap" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" - -[[package]] -name = "termcolor" -version = "1.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" -dependencies = [ - "winapi-util", -] - [[package]] name = "thermal-service" version = "0.1.0" @@ -1366,27 +1343,37 @@ dependencies = [ "embedded-services", "heapless 0.9.2", "log", - "uuid", + "odp-service-common", + "thermal-service-interface", +] + +[[package]] +name = "thermal-service-interface" +version = "0.1.0" +dependencies = [ + "embassy-time", + "embedded-fans-async", + "embedded-sensors-hal-async", ] [[package]] name = "thiserror" -version = "2.0.16" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3467d614147380f2e4e374161426ff399c91084acd2363eaf549172b3d5e60c0" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "2.0.16" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c5e1be1c48b9172ee610da68fd9cd2770e7a4056cb3fc98710ee6906f0c7960" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn", ] [[package]] @@ -1401,10 +1388,10 @@ dependencies = [ [[package]] name = "tps6699x" version = "0.1.0" -source = "git+https://github.com/OpenDevicePartnership/tps6699x#ba1b3b17ebf048fc007eb2107a4d2ab8cb545adf" +source = "git+https://github.com/OpenDevicePartnership/tps6699x?branch=v0.2.0#abe5568183bfe5fb2ea81806dded6cb60f3f9b58" dependencies = [ "bincode", - "bitfield 0.19.2", + "bitfield 0.19.4", "device-driver", "embassy-sync", "embassy-time", @@ -1419,9 +1406,9 @@ dependencies = [ [[package]] name = "tracing" -version = "0.1.41" +version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" dependencies = [ "pin-project-lite", "tracing-attributes", @@ -1430,20 +1417,20 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.30" +version = "0.1.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn", ] [[package]] name = "tracing-core" -version = "0.1.34" +version = "0.1.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" dependencies = [ "once_cell", "valuable", @@ -1462,9 +1449,9 @@ dependencies = [ [[package]] name = "tracing-subscriber" -version = "0.3.20" +version = "0.3.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2054a14f5307d601f88daf0553e1cbf472acc4f2c51afab632431cdcd72124d5" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" dependencies = [ "matchers", "nu-ansi-term", @@ -1478,23 +1465,36 @@ dependencies = [ "tracing-log", ] +[[package]] +name = "type-c-interface" +version = "0.1.0" +dependencies = [ + "bitfield 0.17.0", + "embedded-services", + "embedded-usb-pd", + "heapless 0.9.2", + "log", + "power-policy-interface", +] + [[package]] name = "type-c-service" version = "0.1.0" dependencies = [ "bitfield 0.17.0", - "bitflags 2.9.4", + "bitflags 2.11.1", "embassy-futures", "embassy-sync", "embassy-time", - "embedded-cfu-protocol", "embedded-hal-async", "embedded-services", "embedded-usb-pd", + "fw-update-interface", "heapless 0.9.2", "log", - "static_cell", + "power-policy-interface", "tps6699x", + "type-c-interface", ] [[package]] @@ -1505,9 +1505,9 @@ checksum = "e87a2ed6b42ec5e28cc3b94c09982969e9227600b2e3dcbc1db927a84c06bd69" [[package]] name = "unicode-ident" -version = "1.0.18" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] name = "unty" @@ -1516,10 +1516,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6d49784317cd0d1ee7ec5c716dd598ec5b4483ea832a2dced265471cc0f690ae" [[package]] -name = "uuid" -version = "1.17.0" +name = "utf8parse" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3cf4199d1e5d15ddd86a694e4d0dffa9c323ce759fea589f00fef9d81cc1931d" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "valuable" @@ -1554,320 +1554,46 @@ dependencies = [ "vcell", ] -[[package]] -name = "winapi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" -dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", -] - -[[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" - -[[package]] -name = "winapi-util" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0978bf7171b3d90bac376700cb56d606feb40f251a475a5d6634613564460b22" -dependencies = [ - "windows-sys 0.60.2", -] - -[[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" - -[[package]] -name = "windows" -version = "0.61.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" -dependencies = [ - "windows-collections", - "windows-core", - "windows-future", - "windows-link", - "windows-numerics", -] - -[[package]] -name = "windows-collections" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" -dependencies = [ - "windows-core", -] - -[[package]] -name = "windows-core" -version = "0.61.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" -dependencies = [ - "windows-implement", - "windows-interface", - "windows-link", - "windows-result", - "windows-strings", -] - -[[package]] -name = "windows-future" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" -dependencies = [ - "windows-core", - "windows-link", - "windows-threading", -] - -[[package]] -name = "windows-implement" -version = "0.60.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.106", -] - -[[package]] -name = "windows-interface" -version = "0.59.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.106", -] - [[package]] name = "windows-link" -version = "0.1.3" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" - -[[package]] -name = "windows-numerics" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" -dependencies = [ - "windows-core", - "windows-link", -] +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" [[package]] name = "windows-result" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows-strings" -version = "0.4.2" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" dependencies = [ "windows-link", ] [[package]] name = "windows-sys" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" -dependencies = [ - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-sys" -version = "0.60.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" -dependencies = [ - "windows-targets 0.53.3", -] - -[[package]] -name = "windows-targets" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" -dependencies = [ - "windows_aarch64_gnullvm 0.52.6", - "windows_aarch64_msvc 0.52.6", - "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm 0.52.6", - "windows_i686_msvc 0.52.6", - "windows_x86_64_gnu 0.52.6", - "windows_x86_64_gnullvm 0.52.6", - "windows_x86_64_msvc 0.52.6", -] - -[[package]] -name = "windows-targets" -version = "0.53.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5fe6031c4041849d7c496a8ded650796e7b6ecc19df1a431c1a363342e5dc91" -dependencies = [ - "windows-link", - "windows_aarch64_gnullvm 0.53.0", - "windows_aarch64_msvc 0.53.0", - "windows_i686_gnu 0.53.0", - "windows_i686_gnullvm 0.53.0", - "windows_i686_msvc 0.53.0", - "windows_x86_64_gnu 0.53.0", - "windows_x86_64_gnullvm 0.53.0", - "windows_x86_64_msvc 0.53.0", -] - -[[package]] -name = "windows-threading" -version = "0.1.0" +version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" dependencies = [ "windows-link", ] -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" - -[[package]] -name = "windows_i686_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" - -[[package]] -name = "windows_i686_gnu" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" - -[[package]] -name = "windows_i686_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" - -[[package]] -name = "windows_i686_gnullvm" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" - -[[package]] -name = "windows_i686_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" - -[[package]] -name = "windows_i686_msvc" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" - -[[package]] -name = "wyz" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" -dependencies = [ - "tap", -] - [[package]] name = "zerocopy" -version = "0.8.26" +version = "0.8.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1039dd0d3c310cf05de012d8a39ff557cb0d23087fd44cad61df08fc31907a2f" +checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.26" +version = "0.8.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181" +checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn", ] diff --git a/examples/std/Cargo.toml b/examples/std/Cargo.toml index b8b092ab2..d9cee8293 100644 --- a/examples/std/Cargo.toml +++ b/examples/std/Cargo.toml @@ -27,29 +27,33 @@ defmt = "0.3" embedded-usb-pd = { git = "https://github.com/OpenDevicePartnership/embedded-usb-pd" } embedded-services = { path = "../../embedded-service", features = ["log"] } +odp-service-common = { path = "../../odp-service-common" } power-policy-service = { path = "../../power-policy-service", features = [ "log", ] } +power-policy-interface = { path = "../../power-policy-interface", features = [ + "log", +] } cfu-service = { path = "../../cfu-service", features = ["log"] } embedded-cfu-protocol = { git = "https://github.com/OpenDevicePartnership/embedded-cfu" } -embedded-batteries-async = "0.3" -battery-service = { path = "../../battery-service", features = ["log"] } +battery-service = { path = "../../battery-service", features = ["log", "mock"] } type-c-service = { path = "../../type-c-service", features = ["log"] } +type-c-interface = { path = "../../type-c-interface", features = ["log"] } -embedded-sensors-hal-async = "0.3.0" -embedded-fans-async = "0.2.0" -thermal-service = { path = "../../thermal-service", features = ["log"] } +thermal-service = { path = "../../thermal-service", features = ["log", "mock"] } +thermal-service-interface = { path = "../../thermal-service-interface" } -env_logger = "0.9.0" +env_logger = "0.11.8" log = "0.4.14" static_cell = "2" -embedded-hal-async = "1.0.0" -embedded-hal-mock = { version = "0.11.1", features = ["embedded-hal-async"] } critical-section = { version = "1.1", features = ["std"] } debug-service = { path = "../../debug-service" } +debug-service-messages = { path = "../../debug-service-messages" } + +embedded-batteries-async = "0.3" [lib] name = "std_examples" @@ -59,18 +63,10 @@ path = "src/lib/lib.rs" name = "debug" path = "src/bin/debug.rs" -[[bin]] -name = "type-c-basic" -path = "src/bin/type_c/basic.rs" - [[bin]] name = "type-c-service" path = "src/bin/type_c/service.rs" -[[bin]] -name = "type-c-external" -path = "src/bin/type_c/external.rs" - [[bin]] name = "type-c-unconstrained" path = "src/bin/type_c/unconstrained.rs" diff --git a/examples/std/src/bin/battery.rs b/examples/std/src/bin/battery.rs index 295576aa5..957158134 100644 --- a/examples/std/src/bin/battery.rs +++ b/examples/std/src/bin/battery.rs @@ -1,506 +1,108 @@ -use std::convert::Infallible; +//! Standard battery example +//! +//! The example can be run simply by typing `cargo run --bin battery` -use battery_service::controller::{Controller, ControllerEvent}; -use battery_service::device::{Device, DeviceId, DynamicBatteryMsgs, StaticBatteryMsgs}; -use battery_service::wrapper::Wrapper; -use embassy_executor::Spawner; -use embassy_sync::once_lock::OnceLock; +use battery_service as bs; +use embassy_executor::{Executor, Spawner}; use embassy_time::{Duration, Timer}; -use embedded_batteries_async::charger::{MilliAmps, MilliVolts}; -use embedded_batteries_async::smart_battery::{ - self, BatteryModeFields, BatteryStatusFields, CapacityModeSignedValue, CapacityModeValue, Cycles, DeciKelvin, - ManufactureDate, MilliAmpsSigned, Minutes, Percent, SmartBattery, SpecificationInfoFields, -}; -use embedded_hal_mock::eh1::i2c::Mock; -use embedded_services::info; +use static_cell::StaticCell; -mod espi_service { - use battery_service::context::{BatteryEvent, BatteryEventInner}; - use battery_service::device::DeviceId; - use embassy_sync::once_lock::OnceLock; - use embassy_sync::signal::Signal; - use embassy_time::Timer; - use embedded_services::comms::{self, EndpointID, External}; - use embedded_services::ec_type::message::BatteryMessage; - use embedded_services::{GlobalRawMutex, error}; - use log::info; +use odp_service_common::runnable_service::spawn_service; - pub struct Service { - endpoint: comms::Endpoint, - _signal: Signal, - } - - impl Service { - pub fn new() -> Self { - Service { - endpoint: comms::Endpoint::uninit(EndpointID::External(External::Host)), - _signal: Signal::new(), - } - } - } +#[embassy_executor::task] +async fn embassy_main(spawner: Spawner) { + embedded_services::debug!("Initializing battery service"); + embedded_services::init().await; - impl comms::MailboxDelegate for Service { - fn receive(&self, message: &comms::Message) -> Result<(), comms::MailboxDelegateError> { - let msg = message - .data - .get::() - .ok_or(comms::MailboxDelegateError::MessageNotFound)?; + static BATTERY_DEVICE: StaticCell = StaticCell::new(); + let device = BATTERY_DEVICE.init(bs::device::Device::new(Default::default())); - match msg { - BatteryMessage::CycleCount(cycles) => { - info!("Bat cycles: {cycles}"); - Ok(()) - } - _ => Err(comms::MailboxDelegateError::InvalidData), - } + let battery_service = spawn_service!( + spawner, + battery_service::Service<'static, 1>, + battery_service::InitParams { + config: Default::default(), + devices: [device], } - } - - static ESPI_SERVICE: OnceLock = OnceLock::new(); - - pub async fn init() { - let espi_service = ESPI_SERVICE.get_or_init(Service::new); + ) + .expect("Failed to initialize battery service"); - comms::register_endpoint(espi_service, &espi_service.endpoint) - .await - .unwrap(); - } + static BATTERY_WRAPPER: StaticCell = StaticCell::new(); + let wrapper = BATTERY_WRAPPER.init(bs::wrapper::Wrapper::new( + device, + battery_service::mock::MockBatteryDriver::new(), + )); #[embassy_executor::task] - pub async fn task() { - let espi_service = ESPI_SERVICE.get().await; - - espi_service - .endpoint - .send( - EndpointID::Internal(comms::Internal::Battery), - &BatteryEvent { - device_id: DeviceId(0), - event: BatteryEventInner::DoInit, - }, - ) - .await - .unwrap(); - info!("Sent init request"); - match battery_service::wait_for_battery_response().await { - Ok(_) => { - info!("Init request succeeded!") - } - Err(e) => { - error!("Init request failed with {:?}", e); - } - } - Timer::after_secs(5).await; - - espi_service - .endpoint - .send( - EndpointID::Internal(comms::Internal::Battery), - &BatteryEvent { - device_id: DeviceId(0), - event: BatteryEventInner::PollStaticData, - }, - ) - .await - .unwrap(); - - loop { - espi_service - .endpoint - .send( - EndpointID::Internal(comms::Internal::Battery), - &BatteryEvent { - device_id: DeviceId(0), - event: BatteryEventInner::PollDynamicData, - }, - ) - .await - .unwrap(); - info!("Sent dynamic data request"); - match battery_service::wait_for_battery_response().await { - Ok(_) => { - info!("dynamic data request succeeded!") - } - Err(e) => { - error!("dynamic data request failed with {:?}", e); - } - } - Timer::after_secs(5).await; - } + async fn battery_wrapper_process(battery_wrapper: &'static battery_service::mock::MockBattery<'static>) { + battery_wrapper.process().await } -} -struct FuelGaugeController { - driver: MockFuelGaugeDriver, -} - -impl smart_battery::ErrorType for FuelGaugeController { - type Error = Infallible; -} - -impl SmartBattery for FuelGaugeController { - async fn absolute_state_of_charge(&mut self) -> Result { - self.driver.absolute_state_of_charge().await - } - async fn at_rate(&mut self) -> Result { - self.driver.at_rate().await - } - async fn at_rate_ok(&mut self) -> Result { - self.driver.at_rate_ok().await - } - async fn at_rate_time_to_empty(&mut self) -> Result { - self.driver.at_rate_time_to_empty().await - } - async fn at_rate_time_to_full(&mut self) -> Result { - self.driver.at_rate_time_to_full().await - } - async fn average_current(&mut self) -> Result { - self.driver.average_current().await - } - async fn average_time_to_empty(&mut self) -> Result { - self.driver.average_time_to_empty().await - } - async fn average_time_to_full(&mut self) -> Result { - self.driver.average_time_to_full().await - } - async fn battery_mode(&mut self) -> Result { - self.driver.battery_mode().await - } - async fn battery_status(&mut self) -> Result { - self.driver.battery_status().await - } - async fn charging_current(&mut self) -> Result { - self.driver.charging_current().await - } - async fn charging_voltage(&mut self) -> Result { - self.driver.charging_voltage().await - } - async fn current(&mut self) -> Result { - self.driver.current().await - } - async fn cycle_count(&mut self) -> Result { - self.driver.cycle_count().await - } - async fn design_capacity(&mut self) -> Result { - self.driver.design_capacity().await - } - async fn design_voltage(&mut self) -> Result { - self.driver.design_voltage().await - } - async fn device_chemistry(&mut self, chemistry: &mut [u8]) -> Result<(), Self::Error> { - self.driver.device_chemistry(chemistry).await - } - async fn device_name(&mut self, name: &mut [u8]) -> Result<(), Self::Error> { - self.driver.device_name(name).await - } - async fn full_charge_capacity(&mut self) -> Result { - self.driver.full_charge_capacity().await - } - async fn manufacture_date(&mut self) -> Result { - self.driver.manufacture_date().await - } - async fn manufacturer_name(&mut self, name: &mut [u8]) -> Result<(), Self::Error> { - self.driver.manufacturer_name(name).await - } - async fn max_error(&mut self) -> Result { - self.driver.max_error().await - } - async fn relative_state_of_charge(&mut self) -> Result { - self.driver.relative_state_of_charge().await - } - async fn remaining_capacity(&mut self) -> Result { - self.driver.remaining_capacity().await - } - async fn remaining_capacity_alarm(&mut self) -> Result { - self.driver.remaining_capacity_alarm().await - } - async fn remaining_time_alarm(&mut self) -> Result { - self.driver.remaining_time_alarm().await - } - async fn run_time_to_empty(&mut self) -> Result { - self.driver.run_time_to_empty().await - } - async fn serial_number(&mut self) -> Result { - self.driver.serial_number().await - } - async fn set_at_rate(&mut self, rate: CapacityModeSignedValue) -> Result<(), Self::Error> { - self.driver.set_at_rate(rate).await - } - async fn set_battery_mode(&mut self, flags: BatteryModeFields) -> Result<(), Self::Error> { - self.driver.set_battery_mode(flags).await - } - async fn set_remaining_capacity_alarm(&mut self, capacity: CapacityModeValue) -> Result<(), Self::Error> { - self.driver.set_remaining_capacity_alarm(capacity).await - } - async fn set_remaining_time_alarm(&mut self, time: Minutes) -> Result<(), Self::Error> { - self.driver.set_remaining_time_alarm(time).await - } - async fn specification_info(&mut self) -> Result { - self.driver.specification_info().await - } - async fn temperature(&mut self) -> Result { - self.driver.temperature().await - } - async fn voltage(&mut self) -> Result { - self.driver.voltage().await - } + spawner.spawn(battery_wrapper_process(wrapper).expect("Failed to create battery wrapper task")); + spawner.spawn(run_app(battery_service).expect("Failed to create run_app task")); } -impl Controller for FuelGaugeController { - type ControllerError = Infallible; - - async fn initialize(&mut self) -> Result<(), Self::ControllerError> { - info!("Fuel gauge inited!"); - Ok(()) - } - - async fn get_static_data(&mut self) -> Result { - info!("Sending static data"); - - Ok(StaticBatteryMsgs { ..Default::default() }) - } - - async fn get_dynamic_data(&mut self) -> Result { - info!("Sending dynamic data"); - Ok(DynamicBatteryMsgs { ..Default::default() }) - } - - async fn get_device_event(&mut self) -> ControllerEvent { - loop { - Timer::after_secs(1000000).await; +#[embassy_executor::task] +pub async fn run_app(battery_service: battery_service::Service<'static, 1>) { + // Initialize battery state machine. + let mut retries = 5; + while let Err(e) = bs::mock::init_state_machine(&battery_service).await { + retries -= 1; + if retries <= 0 { + embedded_services::error!("Failed to initialize Battery: {:?}", e); + return; } + Timer::after(Duration::from_secs(1)).await; } - async fn ping(&mut self) -> Result<(), Self::ControllerError> { - info!("Ping!"); - Ok(()) - } - - fn get_timeout(&self) -> Duration { - Duration::from_secs(5) - } - - fn set_timeout(&mut self, _duration: Duration) { - unimplemented!() - } -} - -struct MockFuelGaugeDriver { - _mock_bus: I2c, -} - -impl MockFuelGaugeDriver { - pub fn new(i2c: I2c) -> Self { - MockFuelGaugeDriver { _mock_bus: i2c } - } -} - -impl embedded_batteries_async::smart_battery::ErrorType - for MockFuelGaugeDriver -{ - type Error = Infallible; -} - -impl embedded_batteries_async::smart_battery::SmartBattery - for MockFuelGaugeDriver -{ - async fn remaining_capacity_alarm(&mut self) -> Result { - Ok(CapacityModeValue::MilliAmpUnsigned(0)) - } - - async fn set_remaining_capacity_alarm(&mut self, _capacity: CapacityModeValue) -> Result<(), Self::Error> { - Ok(()) - } - - async fn remaining_time_alarm(&mut self) -> Result { - Ok(0) - } - - async fn set_remaining_time_alarm(&mut self, _time: Minutes) -> Result<(), Self::Error> { - Ok(()) - } - - async fn battery_mode(&mut self) -> Result { - Ok(BatteryModeFields::new()) - } - - async fn set_battery_mode(&mut self, _flags: BatteryModeFields) -> Result<(), Self::Error> { - Ok(()) - } - - async fn at_rate(&mut self) -> Result { - Ok(CapacityModeSignedValue::MilliAmpSigned(0)) - } - - async fn set_at_rate(&mut self, _rate: CapacityModeSignedValue) -> Result<(), Self::Error> { - Ok(()) - } - - async fn at_rate_time_to_full(&mut self) -> Result { - Ok(0) - } - - async fn at_rate_time_to_empty(&mut self) -> Result { - Ok(0) - } - - async fn at_rate_ok(&mut self) -> Result { - Ok(true) - } - - async fn temperature(&mut self) -> Result { - Ok(0) - } - - async fn voltage(&mut self) -> Result { - Ok(0) - } - - async fn charging_voltage(&mut self) -> Result { - Ok(0) - } - - async fn current(&mut self) -> Result { - Ok(0) - } - - async fn charging_current(&mut self) -> Result { - Ok(0) - } - - async fn average_current( - &mut self, - ) -> Result { - Ok(0) - } - - async fn max_error(&mut self) -> Result { - Ok(0) - } - - async fn relative_state_of_charge( - &mut self, - ) -> Result { - Ok(0) - } - - async fn absolute_state_of_charge( - &mut self, - ) -> Result { - Ok(0) - } - - async fn remaining_capacity( - &mut self, - ) -> Result { - Ok(CapacityModeValue::MilliAmpUnsigned(0)) - } - - async fn full_charge_capacity( - &mut self, - ) -> Result { - Ok(CapacityModeValue::MilliAmpUnsigned(0)) - } - - async fn run_time_to_empty(&mut self) -> Result { - Ok(0) - } - - async fn average_time_to_empty(&mut self) -> Result { - Ok(0) - } - - async fn average_time_to_full(&mut self) -> Result { - Ok(0) - } - - async fn battery_status( - &mut self, - ) -> Result { - Ok(BatteryStatusFields::new()) - } - - async fn cycle_count(&mut self) -> Result { - Ok(33) - } - - async fn design_capacity( - &mut self, - ) -> Result { - Ok(CapacityModeValue::MilliAmpUnsigned(0)) - } - - async fn design_voltage(&mut self) -> Result { - Ok(0) - } - - async fn specification_info(&mut self) -> Result { - Ok(SpecificationInfoFields::new()) - } - - async fn manufacture_date( - &mut self, - ) -> Result { - Ok(ManufactureDate::new()) - } - - async fn serial_number(&mut self) -> Result { - Ok(0) - } - - async fn manufacturer_name(&mut self, _name: &mut [u8]) -> Result<(), Self::Error> { - Ok(()) - } - - async fn device_name(&mut self, _name: &mut [u8]) -> Result<(), Self::Error> { - Ok(()) - } + let mut failures: u32 = 0; + let mut count: usize = 1; + loop { + Timer::after(Duration::from_secs(1)).await; + if count.is_multiple_of(const { 60 * 60 * 60 }) + && let Err(e) = battery_service + .execute_event(battery_service::context::BatteryEvent { + event: battery_service::context::BatteryEventInner::PollStaticData, + device_id: bs::device::DeviceId(0), + }) + .await + { + failures += 1; + embedded_services::error!("Fuel gauge static data error: {:#?}", e); + } + if let Err(e) = battery_service + .execute_event(battery_service::context::BatteryEvent { + event: battery_service::context::BatteryEventInner::PollDynamicData, + device_id: bs::device::DeviceId(0), + }) + .await + { + failures += 1; + embedded_services::error!("Fuel gauge dynamic data error: {:#?}", e); + } - async fn device_chemistry(&mut self, _chemistry: &mut [u8]) -> Result<(), Self::Error> { - Ok(()) - } -} + if failures > 10 { + failures = 0; + count = 0; + embedded_services::error!("FG: Too many errors, timing out and starting recovery..."); + if bs::mock::recover_state_machine(&battery_service).await.is_err() { + embedded_services::error!("FG: Fatal error"); + return; + } + } -#[embassy_executor::task] -async fn wrapper_task(wrapper: Wrapper<'static, FuelGaugeController>) { - loop { - wrapper.process().await; - info!("Got new wrapper message"); + count = count.wrapping_add(1); } } -#[embassy_executor::task] -async fn battery_service_task() -> ! { - battery_service::task::task().await; - unreachable!() -} - -#[embassy_executor::main] -async fn main(spawner: Spawner) { - env_logger::builder().filter_level(log::LevelFilter::Trace).init(); - - let expectations = vec![]; - - static DEV: OnceLock = OnceLock::new(); - - let dev = DEV.get_or_init(|| Device::new(DeviceId(0))); - - let wrap = Wrapper::new( - dev, - FuelGaugeController { - driver: MockFuelGaugeDriver::new(Mock::new(&expectations)), - }, - ); - - embedded_services::init().await; - info!("services init'd"); - - espi_service::init().await; - info!("espi service init'd"); - - battery_service::register_fuel_gauge(dev).unwrap(); +fn main() { + env_logger::builder().filter_level(log::LevelFilter::Debug).init(); + embedded_services::info!("battery example started"); - spawner.spawn(espi_service::task().unwrap()); - spawner.spawn(wrapper_task(wrap).unwrap()); - spawner.spawn(battery_service_task().unwrap()); + static EXECUTOR: StaticCell = StaticCell::new(); + let executor = EXECUTOR.init(Executor::new()); + // Run battery service + executor.run(|spawner| { + spawner.spawn(embassy_main(spawner).expect("Failed to create embassy_main task")); + }); } diff --git a/examples/std/src/bin/buffer.rs b/examples/std/src/bin/buffer.rs index ff41cdb40..ae8ace7de 100644 --- a/examples/std/src/bin/buffer.rs +++ b/examples/std/src/bin/buffer.rs @@ -109,6 +109,6 @@ fn main() { static EXECUTOR: StaticCell = StaticCell::new(); let executor = EXECUTOR.init(Executor::new()); executor.run(|spawner| { - spawner.spawn(task().unwrap()); + spawner.spawn(task().expect("Failed to create task")); }); } diff --git a/examples/std/src/bin/cfu_buffer.rs b/examples/std/src/bin/cfu_buffer.rs index 0fa64b534..1769e3967 100644 --- a/examples/std/src/bin/cfu_buffer.rs +++ b/examples/std/src/bin/cfu_buffer.rs @@ -4,16 +4,13 @@ use embassy_time::{Duration, Timer}; use log::*; use static_cell::StaticCell; +use cfu_service::component::{InternalResponseData, RequestData}; use embedded_cfu_protocol::protocol_definitions::*; -use embedded_services::{ - GlobalRawMutex, - cfu::{self, component::InternalResponseData, route_request}, -}; +use embedded_services::GlobalRawMutex; +use cfu_service::CfuClient; use cfu_service::buffer; -use crate::cfu::component::RequestData; - /// Component ID for the CFU buffer const CFU_BUFFER_ID: ComponentId = 0x06; @@ -23,7 +20,7 @@ const CFU_COMPONENT0_ID: ComponentId = 0x20; mod mock { use std::sync::atomic::AtomicBool; - use embedded_services::cfu::component::{CfuDevice, CfuDeviceContainer, InternalResponseData}; + use cfu_service::component::{CfuDevice, CfuDeviceContainer, InternalResponseData}; use super::*; @@ -147,10 +144,10 @@ async fn device_task(device: &'static mock::Device) { } #[embassy_executor::task] -async fn buffer_task(buffer: &'static buffer::Buffer<'static>) { +async fn buffer_task(buffer: &'static buffer::Buffer<'static>, cfu_client: &'static CfuClient) { loop { - let request = buffer.wait_event().await; - if let Some(response) = buffer.process(request).await { + let request = buffer.wait_event(cfu_client).await; + if let Some(response) = buffer.process(request, cfu_client).await { buffer.send_response(response).await; } } @@ -160,6 +157,11 @@ async fn buffer_task(buffer: &'static buffer::Buffer<'static>) { async fn run(spawner: Spawner) { embedded_services::init().await; + static CFU_CLIENT: OnceLock = OnceLock::new(); + let cfu_client = CfuClient::new(&CFU_CLIENT).await; + + spawner.spawn(cfu_service_task(cfu_client).expect("Failed to create cfu service task")); + info!("Creating device 0"); static DEVICE0: OnceLock = OnceLock::new(); let device0 = DEVICE0.get_or_init(|| { @@ -172,8 +174,8 @@ async fn run(spawner: Spawner) { }, ) }); - cfu::register_device(device0).await.unwrap(); - spawner.spawn(device_task(device0).unwrap()); + cfu_client.register_device(device0).unwrap(); + spawner.spawn(device_task(device0).expect("Failed to create device task")); info!("Creating buffer"); static BUFFER: OnceLock> = OnceLock::new(); @@ -189,11 +191,12 @@ async fn run(spawner: Spawner) { buffer::Config::with_timeout(Duration::from_millis(75)), ) }); - buffer.register().await.unwrap(); - spawner.spawn(buffer_task(buffer).unwrap()); + buffer.register(cfu_client).unwrap(); + spawner.spawn(buffer_task(buffer, cfu_client).expect("Failed to create buffer task")); info!("Getting FW version"); - let response = route_request(CFU_BUFFER_ID, RequestData::FwVersionRequest) + let response = cfu_client + .route_request(CFU_BUFFER_ID, RequestData::FwVersionRequest) .await .unwrap(); let prev_version = match response { @@ -206,18 +209,19 @@ async fn run(spawner: Spawner) { info!("Got version: {prev_version:#x}"); info!("Giving offer"); - let offer = route_request( - CFU_BUFFER_ID, - RequestData::GiveOffer(FwUpdateOffer::new( - HostToken::Driver, + let offer = cfu_client + .route_request( CFU_BUFFER_ID, - FwVersion::new(0x211), - 0, - 0, - )), - ) - .await - .unwrap(); + RequestData::GiveOffer(FwUpdateOffer::new( + HostToken::Driver, + CFU_BUFFER_ID, + FwVersion::new(0x211), + 0, + 0, + )), + ) + .await + .unwrap(); info!("Got response: {offer:?}"); for i in 0..10 { @@ -235,7 +239,8 @@ async fn run(spawner: Spawner) { info!("Giving content"); let now = embassy_time::Instant::now(); - let response = route_request(CFU_BUFFER_ID, RequestData::GiveContent(request)) + let response = cfu_client + .route_request(CFU_BUFFER_ID, RequestData::GiveContent(request)) .await .unwrap(); info!("Got response in {:?} ms: {:?}", now.elapsed().as_millis(), response); @@ -246,8 +251,8 @@ async fn run(spawner: Spawner) { } #[embassy_executor::task] -async fn cfu_service_task() -> ! { - cfu_service::task::task().await; +async fn cfu_service_task(cfu_client: &'static CfuClient) -> ! { + cfu_service::task::task(cfu_client).await; unreachable!() } @@ -256,7 +261,6 @@ fn main() { static EXECUTOR: StaticCell = StaticCell::new(); let executor = EXECUTOR.init(Executor::new()); executor.run(|spawner| { - spawner.spawn(cfu_service_task().unwrap()); - spawner.spawn(run(spawner).unwrap()); + spawner.spawn(run(spawner).expect("Failed to create run task")); }); } diff --git a/examples/std/src/bin/cfu_client.rs b/examples/std/src/bin/cfu_client.rs index b9435b806..b57a6d961 100644 --- a/examples/std/src/bin/cfu_client.rs +++ b/examples/std/src/bin/cfu_client.rs @@ -7,24 +7,25 @@ use static_cell::StaticCell; use embedded_cfu_protocol::protocol_definitions::{ ComponentId, FwUpdateOffer, FwVersion, HostToken, MAX_SUBCMPT_COUNT, }; -use embedded_services::cfu; -use embedded_services::cfu::component::CfuComponentDefault; -use crate::cfu::component::RequestData; +use cfu_service::{ + CfuClient, + component::{CfuComponentDefault, RequestData}, +}; #[embassy_executor::task] -async fn device_task0(component: &'static CfuComponentDefault) { +async fn device_task0(component: &'static CfuComponentDefault, cfu_client: &'static CfuClient) { loop { - if let Err(e) = component.process_request().await { + if let Err(e) = component.process_request(cfu_client).await { error!("Error processing request: {e:?}"); } } } #[embassy_executor::task] -async fn device_task1(component: &'static CfuComponentDefault) { +async fn device_task1(component: &'static CfuComponentDefault, cfu_client: &'static CfuClient) { loop { - if let Err(e) = component.process_request().await { + if let Err(e) = component.process_request(cfu_client).await { error!("Error processing request: {e:?}"); } } @@ -34,20 +35,25 @@ async fn device_task1(component: &'static CfuComponentDefault) { async fn run(spawner: Spawner) { embedded_services::init().await; + static CFU_CLIENT: OnceLock = OnceLock::new(); + let cfu_client = CfuClient::new(&CFU_CLIENT).await; + + spawner.spawn(cfu_service_task(cfu_client).expect("Failed to create cfu service task")); + info!("Creating device 0"); static DEVICE0: OnceLock> = OnceLock::new(); let mut subs: [Option; MAX_SUBCMPT_COUNT] = [None; MAX_SUBCMPT_COUNT]; subs[0] = Some(2); let device0 = DEVICE0.get_or_init(|| CfuComponentDefault::new(1, true, subs, CfuWriterNop {})); - cfu::register_device(device0).await.unwrap(); - spawner.spawn(device_task0(device0).unwrap()); + cfu_client.register_device(device0).unwrap(); + spawner.spawn(device_task0(device0, cfu_client).expect("Failed to create device_task0")); info!("Creating device 1"); static DEVICE1: OnceLock> = OnceLock::new(); let device1 = DEVICE1.get_or_init(|| CfuComponentDefault::new(2, false, [None; MAX_SUBCMPT_COUNT], CfuWriterNop {})); - cfu::register_device(device1).await.unwrap(); - spawner.spawn(device_task1(device1).unwrap()); + cfu_client.register_device(device1).unwrap(); + spawner.spawn(device_task1(device1, cfu_client).expect("Failed to create device_task1")); let dummy_offer0 = FwUpdateOffer::new( HostToken::Driver, @@ -72,7 +78,7 @@ async fn run(spawner: Spawner) { 0, ); - match cfu::route_request(1, RequestData::GiveOffer(dummy_offer0)).await { + match cfu_client.route_request(1, RequestData::GiveOffer(dummy_offer0)).await { Ok(resp) => { info!("got okay response to device0 update {resp:?}"); } @@ -80,7 +86,7 @@ async fn run(spawner: Spawner) { error!("offer failed with error {e:?}"); } } - match cfu::route_request(2, RequestData::GiveOffer(dummy_offer1)).await { + match cfu_client.route_request(2, RequestData::GiveOffer(dummy_offer1)).await { Ok(resp) => { info!("got okay response to device1 update {resp:?}"); } @@ -91,8 +97,8 @@ async fn run(spawner: Spawner) { } #[embassy_executor::task] -async fn cfu_service_task() -> ! { - cfu_service::task::task().await; +async fn cfu_service_task(cfu_client: &'static CfuClient) -> ! { + cfu_service::task::task(cfu_client).await; unreachable!() } @@ -102,7 +108,6 @@ fn main() { static EXECUTOR: StaticCell = StaticCell::new(); let executor = EXECUTOR.init(Executor::new()); executor.run(|spawner| { - spawner.spawn(cfu_service_task().unwrap()); - spawner.spawn(run(spawner).unwrap()); + spawner.spawn(run(spawner).expect("Failed to create run task")); }); } diff --git a/examples/std/src/bin/cfu_splitter.rs b/examples/std/src/bin/cfu_splitter.rs index 6bc39185e..f77502ea0 100644 --- a/examples/std/src/bin/cfu_splitter.rs +++ b/examples/std/src/bin/cfu_splitter.rs @@ -3,12 +3,10 @@ use embassy_sync::once_lock::OnceLock; use log::*; use static_cell::StaticCell; +use cfu_service::component::{InternalResponseData, RequestData}; use embedded_cfu_protocol::protocol_definitions::*; -use embedded_services::cfu::{self, component::InternalResponseData, route_request}; -use cfu_service::splitter; - -use crate::cfu::component::RequestData; +use cfu_service::{CfuClient, splitter}; /// Component ID for the CFU Splitter const CFU_SPLITTER_ID: ComponentId = 0x06; @@ -19,7 +17,7 @@ const CFU_COMPONENT0_ID: ComponentId = 0x20; const CFU_COMPONENT1_ID: ComponentId = 0x21; mod mock { - use embedded_services::cfu::component::{CfuDevice, CfuDeviceContainer, InternalResponseData}; + use cfu_service::component::{CfuDevice, CfuDeviceContainer, InternalResponseData}; use super::*; @@ -166,10 +164,13 @@ async fn device_task(device: &'static mock::Device) { } #[embassy_executor::task] -async fn splitter_task(splitter: &'static splitter::Splitter<'static, mock::Customization>) { +async fn splitter_task( + splitter: &'static splitter::Splitter<'static, mock::Customization>, + cfu_client: &'static CfuClient, +) { loop { let request = splitter.wait_request().await; - let response = splitter.process_request(request).await; + let response = splitter.process_request(request, cfu_client).await; splitter.send_response(response).await; } } @@ -178,6 +179,11 @@ async fn splitter_task(splitter: &'static splitter::Splitter<'static, mock::Cust async fn run(spawner: Spawner) { embedded_services::init().await; + static CFU_CLIENT: OnceLock = OnceLock::new(); + let cfu_client = CfuClient::new(&CFU_CLIENT).await; + + spawner.spawn(cfu_service_task(cfu_client).expect("Failed to create cfu service task")); + info!("Creating device 0"); static DEVICE0: OnceLock = OnceLock::new(); let device0 = DEVICE0.get_or_init(|| { @@ -190,8 +196,8 @@ async fn run(spawner: Spawner) { }, ) }); - cfu::register_device(device0).await.unwrap(); - spawner.spawn(device_task(device0).unwrap()); + cfu_client.register_device(device0).unwrap(); + spawner.spawn(device_task(device0).expect("Failed to create device0 task")); info!("Creating device 1"); static DEVICE1: OnceLock = OnceLock::new(); @@ -205,19 +211,20 @@ async fn run(spawner: Spawner) { }, ) }); - cfu::register_device(device1).await.unwrap(); - spawner.spawn(device_task(device1).unwrap()); + cfu_client.register_device(device1).unwrap(); + spawner.spawn(device_task(device1).expect("Failed to create device1 task")); info!("Creating splitter"); static SPLITTER: OnceLock> = OnceLock::new(); static DEVICES: [ComponentId; 2] = [CFU_COMPONENT0_ID, CFU_COMPONENT1_ID]; let customization = mock::Customization {}; let splitter = SPLITTER.get_or_init(|| splitter::Splitter::new(CFU_SPLITTER_ID, &DEVICES, customization).unwrap()); - splitter.register().await.unwrap(); - spawner.spawn(splitter_task(splitter).unwrap()); + splitter.register(cfu_client).unwrap(); + spawner.spawn(splitter_task(splitter, cfu_client).expect("Failed to create splitter task")); info!("Getting FW version"); - let response = route_request(CFU_SPLITTER_ID, RequestData::FwVersionRequest) + let response = cfu_client + .route_request(CFU_SPLITTER_ID, RequestData::FwVersionRequest) .await .unwrap(); let prev_version = match response { @@ -230,18 +237,19 @@ async fn run(spawner: Spawner) { info!("Got version: {prev_version:#x}"); info!("Giving offer"); - let offer = route_request( - CFU_SPLITTER_ID, - RequestData::GiveOffer(FwUpdateOffer::new( - HostToken::Driver, + let offer = cfu_client + .route_request( CFU_SPLITTER_ID, - FwVersion::new(0x211), - 0, - 0, - )), - ) - .await - .unwrap(); + RequestData::GiveOffer(FwUpdateOffer::new( + HostToken::Driver, + CFU_SPLITTER_ID, + FwVersion::new(0x211), + 0, + 0, + )), + ) + .await + .unwrap(); info!("Got response: {offer:?}"); let header = FwUpdateContentHeader { @@ -256,15 +264,16 @@ async fn run(spawner: Spawner) { data: [0u8; DEFAULT_DATA_LENGTH], }; - let response = route_request(CFU_SPLITTER_ID, RequestData::GiveContent(request)) + let response = cfu_client + .route_request(CFU_SPLITTER_ID, RequestData::GiveContent(request)) .await .unwrap(); info!("Got response: {response:?}"); } #[embassy_executor::task] -async fn cfu_service_task() -> ! { - cfu_service::task::task().await; +async fn cfu_service_task(cfu_client: &'static CfuClient) -> ! { + cfu_service::task::task(cfu_client).await; unreachable!() } @@ -274,7 +283,6 @@ fn main() { static EXECUTOR: StaticCell = StaticCell::new(); let executor = EXECUTOR.init(Executor::new()); executor.run(|spawner| { - spawner.spawn(cfu_service_task().unwrap()); - spawner.spawn(run(spawner).unwrap()); + spawner.spawn(run(spawner).expect("Failed to create run task")); }); } diff --git a/examples/std/src/bin/debug.rs b/examples/std/src/bin/debug.rs index 9044fa053..af05f4732 100644 --- a/examples/std/src/bin/debug.rs +++ b/examples/std/src/bin/debug.rs @@ -29,7 +29,6 @@ mod _defmt_linker_stubs { } use embassy_executor::{Executor, Spawner}; -use embedded_services::comms::{Endpoint, EndpointID, External}; use embedded_services::info; use static_cell::StaticCell; @@ -40,13 +39,11 @@ defmt::timestamp!("{=u64}", { 0u64 }); // Mock eSPI transport service mod espi_service { use core::borrow::BorrowMut; + use debug_service_messages::{DebugRequest, DebugResponse, DebugResult, STD_DEBUG_BUF_SIZE}; use embassy_sync::{once_lock::OnceLock, signal::Signal}; use embedded_services::GlobalRawMutex; use embedded_services::buffer::OwnedRef; use embedded_services::comms::{self, EndpointID, External, Internal}; - use embedded_services::ec_type::message::{ - HostMsg, NotificationMsg, STD_DEBUG_BUF_SIZE, StdHostMsg, StdHostPayload, StdHostRequest, - }; use log::{info, trace}; // Max defmt payload we expect to shuttle in this mock @@ -55,6 +52,9 @@ mod espi_service { // Static request buffer used to build the "GetDebugBuffer" payload embedded_services::define_static_buffer!(debug_req_buf, u8, [0u8; 32]); + // TODO Notifications are not currently implemented. Remove this and replace it with the real notification struct when we do implement it. + pub struct NotificationMsg; + pub struct Service { endpoint: comms::Endpoint, notify: &'static Signal, @@ -77,30 +77,31 @@ mod espi_service { impl comms::MailboxDelegate for Service { fn receive(&self, message: &comms::Message) -> Result<(), comms::MailboxDelegateError> { - if let Some(host_msg) = message.data.get::() { - match host_msg { - HostMsg::Notification(n) => { - info!( - "mock eSPI got Host Notification: offset={} from {:?}", - n.offset, message.from - ); - // Defer to async host task via signal (receive is not async) - self.notify.signal(*n); - Ok(()) - } - HostMsg::Response(acpi) => { - // Stage the response bytes into the mock OOB buffer for the host - let mut access = self.resp_owned.borrow_mut().unwrap(); - let buf: &mut [u8] = core::borrow::BorrowMut::borrow_mut(&mut access); - if let StdHostPayload::DebugGetMsgsResponse { debug_buf } = acpi.payload { - let copy_len = core::cmp::min(debug_buf.len(), buf.len()); - buf[..copy_len].copy_from_slice(&debug_buf[..copy_len]); - trace!("mock eSPI staged {copy_len} response bytes for host"); - self.resp_len.signal(copy_len); - } - Ok(()) + if let Some(debug_result) = message.data.get::() { + // Stage the response bytes into the mock OOB buffer for the host + let mut access = self.resp_owned.borrow_mut().unwrap(); + let buf: &mut [u8] = core::borrow::BorrowMut::borrow_mut(&mut access); + + let debug_response = debug_result.map_err(|_| comms::MailboxDelegateError::Other)?; + match debug_response { + DebugResponse::DebugGetMsgsResponse { debug_buf } => { + let copy_len = core::cmp::min(debug_buf.len(), buf.len()); + buf[..copy_len].copy_from_slice(&debug_buf[..copy_len]); + trace!("mock eSPI staged {copy_len} response bytes for host"); + self.resp_len.signal(copy_len); } } + + Ok(()) + // TODO notification functionality is currently not implemented. Restore this when we implement it. + // } else if let Some(debug_notification) = message.data.get::() { + // info!( + // "mock eSPI got Host Notification: offset={} from {:?}", + // n.offset, message.from + // ); + // // Defer to async host task via signal (receive is not async) + // self.notify.signal(*n); + // Ok(()) } else { Err(comms::MailboxDelegateError::MessageNotFound) } @@ -140,11 +141,8 @@ mod espi_service { loop { // Wait for a device notification via the mock eSPI transport - let n: NotificationMsg = wait_host_notification().await; - info!( - "eSPI: got Host Notification (offset={}), sending OOB request/ACK to Debug", - n.offset - ); + let _n: NotificationMsg = wait_host_notification().await; + info!("eSPI: got Host Notification, sending OOB request/ACK to Debug",); // Build the ACPI/MCTP-style request payload for the Debug service let request = b"GetDebugBuffer"; @@ -159,13 +157,7 @@ mod espi_service { let _ = comms::send( EndpointID::External(External::Host), EndpointID::Internal(Internal::Debug), - &StdHostRequest { - command: embedded_services::ec_type::message::OdpCommand::Debug( - embedded_services::ec_type::protocols::debug::DebugCmd::GetMsgs, - ), - status: 0, - payload: StdHostPayload::DebugGetMsgsRequest, - }, + &DebugRequest::DebugGetMsgsRequest {}, ) .await; @@ -204,20 +196,20 @@ async fn init_task(spawner: Spawner) { info!("init espi service"); espi_service::init().await; // Spawn eSPI request task to drive the OOB request/response flow - spawner.spawn(espi_service::request_task().unwrap()); + spawner.spawn(espi_service::request_task().expect("Failed to create espi request task")); info!("spawn debug service"); - spawner.spawn(debug_service().unwrap()); + spawner.spawn(debug_service().expect("Failed to create debug service task")); info!("spawn defmt_to_host_task"); - spawner.spawn(defmt_to_host_task().unwrap()); + spawner.spawn(defmt_to_host_task().expect("Failed to create defmt_to_host task")); - spawner.spawn(defmt_frames_task().unwrap()); + spawner.spawn(defmt_frames_task().expect("Failed to create defmt_frames task")); } #[embassy_executor::task] async fn debug_service() -> ! { - debug_service::task::debug_service(Endpoint::uninit(EndpointID::External(External::Host))).await; + debug_service::task::debug_service().await; unreachable!() } @@ -235,6 +227,6 @@ fn main() { executor.run(|spawner| { // Spawn debug-service tasks and mock eSPI service - spawner.spawn(init_task(spawner).unwrap()); + spawner.spawn(init_task(spawner).expect("Failed to create init task")); }); } diff --git a/examples/std/src/bin/hid.rs b/examples/std/src/bin/hid.rs index 24a4015ca..bba0ca9e2 100644 --- a/examples/std/src/bin/hid.rs +++ b/examples/std/src/bin/hid.rs @@ -78,13 +78,13 @@ async fn run(spawner: Spawner) { let dev1 = DEVICE1.get_or_init(|| Device::new(DEV1_ID)); comms::register_endpoint(dev1, &dev1.tp).await.unwrap(); info!("Spawning host task"); - spawner.spawn(host().unwrap()); + spawner.spawn(host().expect("Failed to create host task")); } static EXECUTOR: StaticCell = StaticCell::new(); fn main() { env_logger::builder().filter_level(log::LevelFilter::Info).init(); let executor = EXECUTOR.init(Executor::new()); executor.run(|spawner| { - spawner.spawn(run(spawner).unwrap()); + spawner.spawn(run(spawner).expect("Failed to create run task")); }); } diff --git a/examples/std/src/bin/init.rs b/examples/std/src/bin/init.rs index 92843b287..97764e66d 100644 --- a/examples/std/src/bin/init.rs +++ b/examples/std/src/bin/init.rs @@ -25,7 +25,7 @@ fn main() { static EXECUTOR: StaticCell = StaticCell::new(); let executor = EXECUTOR.init(Executor::new()); executor.run(|spawner| { - spawner.spawn(registration_waiter().unwrap()); - spawner.spawn(registration_task().unwrap()); + spawner.spawn(registration_waiter().expect("Failed to create registration_waiter task")); + spawner.spawn(registration_task().expect("Failed to create registration task")); }); } diff --git a/examples/std/src/bin/keyboard.rs b/examples/std/src/bin/keyboard.rs index d72ba0346..14e221c13 100644 --- a/examples/std/src/bin/keyboard.rs +++ b/examples/std/src/bin/keyboard.rs @@ -139,8 +139,8 @@ async fn run(spawner: Spawner) { keyboard::enable_broadcast_host().await; - spawner.spawn(host().unwrap()); - spawner.spawn(device().unwrap()); + spawner.spawn(host().expect("Failed to create host task")); + spawner.spawn(device().expect("Failed to create device task")); } fn main() { @@ -149,6 +149,6 @@ fn main() { static EXECUTOR: StaticCell = StaticCell::new(); let executor = EXECUTOR.init(Executor::new()); executor.run(|spawner| { - spawner.spawn(run(spawner).unwrap()); + spawner.spawn(run(spawner).expect("Failed to create run task")); }); } diff --git a/examples/std/src/bin/power_policy.rs b/examples/std/src/bin/power_policy.rs index ad017cb14..763e106fd 100644 --- a/examples/std/src/bin/power_policy.rs +++ b/examples/std/src/bin/power_policy.rs @@ -1,13 +1,34 @@ use embassy_executor::{Executor, Spawner}; -use embassy_sync::{blocking_mutex::raw::NoopRawMutex, once_lock::OnceLock, pubsub::PubSubChannel}; -use embassy_time::{self as _, Timer}; -use embedded_services::{ - broadcaster::immediate as broadcaster, - power::policy::{self, ConsumerPowerCapability, PowerCapability, device, flags}, +use embassy_sync::{ + blocking_mutex::raw::NoopRawMutex, + channel::{self, Channel}, + mutex::Mutex, }; +use embassy_time::{self as _, Timer}; +use embedded_batteries_async::charger::{MilliAmps, MilliVolts}; +use embedded_services::{GlobalRawMutex, event::NoopSender, named::Named}; use log::*; +use power_policy_interface::{ + capability::{ConsumerFlags, ConsumerPowerCapability, PowerCapability, ProviderPowerCapability}, + charger, psu, +}; +use power_policy_interface::{ + charger::Charger, + psu::{Error, Psu}, +}; +use power_policy_service::{ + charger::ChargerEventReceivers, psu::PsuEventReceivers, service::registration::ArrayRegistration, +}; use static_cell::StaticCell; +type ServiceType = Mutex< + GlobalRawMutex, + power_policy_service::service::Service< + 'static, + ArrayRegistration<'static, DeviceType, 2, NoopSender, 1, ChargerType, 1>, + >, +>; + const LOW_POWER: PowerCapability = PowerCapability { voltage_mv: 5000, current_ma: 1500, @@ -18,227 +39,506 @@ const HIGH_POWER: PowerCapability = PowerCapability { current_ma: 3000, }; -struct ExampleDevice { - device: policy::device::Device, +const PER_CALL_DELAY_MS: u64 = 1000; + +struct ExampleDevice<'a> { + sender: channel::DynamicSender<'a, power_policy_interface::psu::event::EventData>, + state: psu::State, + name: &'static str, } -impl ExampleDevice { - fn new(id: policy::DeviceId) -> Self { +impl<'a> ExampleDevice<'a> { + fn new( + name: &'static str, + sender: channel::DynamicSender<'a, power_policy_interface::psu::event::EventData>, + ) -> Self { Self { - device: policy::device::Device::new(id), + name, + sender, + state: Default::default(), } } - async fn process_request(&self) -> Result<(), policy::Error> { - let request = self.device.receive().await; - match request.command { - device::CommandData::ConnectAsConsumer(capability) => { - info!( - "Device {} received connect consumer at {:#?}", - self.device.id().0, - capability - ); - } - device::CommandData::ConnectAsProvider(capability) => { - info!( - "Device {} received connect provider at {:#?}", - self.device.id().0, - capability - ); - } - device::CommandData::Disconnect => { - info!("Device {} received disconnect", self.device.id().0); - } - } + pub async fn simulate_attach(&mut self) { + self.state.attach().unwrap(); + self.sender + .send(power_policy_interface::psu::event::EventData::Attached) + .await; + } - request.respond(Ok(policy::device::ResponseData::Complete)); - Ok(()) + pub async fn simulate_update_consumer_power_capability(&mut self, capability: Option) { + self.state.update_consumer_power_capability(capability).unwrap(); + self.sender + .send(power_policy_interface::psu::event::EventData::UpdatedConsumerCapability(capability)) + .await; + } + + pub async fn simulate_detach(&mut self) { + self.state.detach(); + self.sender + .send(power_policy_interface::psu::event::EventData::Detached) + .await; + } + + pub async fn simulate_update_requested_provider_power_capability( + &mut self, + capability: Option, + ) { + self.state + .update_requested_provider_power_capability(capability) + .unwrap(); + self.sender + .send(power_policy_interface::psu::event::EventData::RequestedProviderCapability(capability)) + .await } } -impl policy::device::DeviceContainer for ExampleDevice { - fn get_power_policy_device(&self) -> &policy::device::Device { - &self.device +impl Psu for ExampleDevice<'_> { + async fn disconnect(&mut self) -> Result<(), Error> { + debug!("ExampleDevice disconnect"); + self.state.disconnect(false) + } + + async fn connect_provider(&mut self, capability: ProviderPowerCapability) -> Result<(), Error> { + debug!("ExampleDevice connect_provider with {capability:?}"); + self.state.connect_provider(capability) + } + + async fn connect_consumer(&mut self, capability: ConsumerPowerCapability) -> Result<(), Error> { + debug!("ExampleDevice connect_consumer with {capability:?}"); + self.state.connect_consumer(capability) + } + + fn state(&self) -> &psu::State { + &self.state + } + + fn state_mut(&mut self) -> &mut psu::State { + &mut self.state } } -#[embassy_executor::task] -async fn device_task0(device: &'static ExampleDevice) { - loop { - if let Err(e) = device.process_request().await { - error!("Error processing request: {e:?}"); - } +impl Named for ExampleDevice<'_> { + fn name(&self) -> &'static str { + self.name } } -#[embassy_executor::task] -async fn device_task1(device: &'static ExampleDevice) { - loop { - if let Err(e) = device.process_request().await { - error!("Error processing request: {e:?}"); +type DeviceType = Mutex>; + +struct ExampleCharger<'a> { + sender: channel::DynamicSender<'a, power_policy_interface::charger::event::EventData>, + state: charger::State, +} + +impl<'a> ExampleCharger<'a> { + fn new(sender: channel::DynamicSender<'a, power_policy_interface::charger::event::EventData>) -> Self { + Self { + sender, + state: charger::State::default(), } } + + fn assert_state(&self, internal_state: charger::InternalState, capability: Option) { + assert_eq!(*self.state.internal_state(), internal_state); + assert_eq!(*self.state.capability(), capability); + } + + pub async fn simulate_psu_state_change(&mut self, psu_state: charger::PsuState) { + self.sender.send(charger::EventData::PsuStateChange(psu_state)).await; + self.state_mut().on_psu_state_change(psu_state).unwrap(); + } + + pub fn simulate_timeout(&mut self) { + self.state_mut().on_timeout(); + } + + pub async fn simulate_check_ready(&mut self) { + self.is_ready().await.unwrap(); + } + + pub async fn simulate_init_request(&mut self) { + self.init_charger().await.unwrap(); + } } +impl<'a> embedded_batteries_async::charger::ErrorType for ExampleCharger<'a> { + type Error = core::convert::Infallible; +} + +impl<'a> embedded_batteries_async::charger::Charger for ExampleCharger<'a> { + async fn charging_current(&mut self, current: MilliAmps) -> Result { + Ok(current) + } + + async fn charging_voltage(&mut self, voltage: MilliVolts) -> Result { + Ok(voltage) + } +} + +impl<'a> charger::Charger for ExampleCharger<'a> { + type ChargerError = core::convert::Infallible; + + async fn init_charger(&mut self) -> Result { + info!("Charger init"); + self.state_mut().on_initialized(charger::PsuState::Detached).unwrap(); + Ok(charger::PsuState::Detached) + } + + fn attach_handler( + &mut self, + capability: ConsumerPowerCapability, + ) -> impl Future> { + info!("Charger attach: {:?}", capability); + self.state_mut().on_policy_attach(capability); + async { Ok(()) } + } + + fn detach_handler(&mut self) -> impl Future> { + info!("Charger detach"); + self.state_mut().on_policy_detach(); + async { Ok(()) } + } + + async fn is_ready(&mut self) -> Result<(), Self::ChargerError> { + info!("Charger check ready"); + self.state_mut().on_ready_success(); + Ok(()) + } + + fn state(&self) -> &charger::State { + &self.state + } + + fn state_mut(&mut self) -> &mut charger::State { + &mut self.state + } +} + +type ChargerType = Mutex>; + #[embassy_executor::task] async fn run(spawner: Spawner) { embedded_services::init().await; info!("Creating device 0"); - static DEVICE0: OnceLock = OnceLock::new(); - let device0_mock = DEVICE0.get_or_init(|| ExampleDevice::new(policy::DeviceId(0))); - policy::register_device(device0_mock).unwrap(); - spawner.spawn(device_task0(device0_mock).unwrap()); - let device0 = device0_mock.device.try_device_action().await.unwrap(); + static DEVICE0_EVENT_CHANNEL: StaticCell> = + StaticCell::new(); + let device0_event_channel = DEVICE0_EVENT_CHANNEL.init(Channel::new()); + static DEVICE0: StaticCell = StaticCell::new(); + let device0 = DEVICE0.init(Mutex::new(ExampleDevice::new( + "Device 0", + device0_event_channel.dyn_sender(), + ))); info!("Creating device 1"); - static DEVICE1: OnceLock = OnceLock::new(); - let device1_mock = DEVICE1.get_or_init(|| ExampleDevice::new(policy::DeviceId(1))); - policy::register_device(device1_mock).unwrap(); - spawner.spawn(device_task1(device1_mock).unwrap()); - let device1 = device1_mock.device.try_device_action().await.unwrap(); + static DEVICE1_EVENT_CHANNEL: StaticCell> = + StaticCell::new(); + let device1_event_channel = DEVICE1_EVENT_CHANNEL.init(Channel::new()); + static DEVICE1: StaticCell = StaticCell::new(); + let device1 = DEVICE1.init(Mutex::new(ExampleDevice::new( + "Device 1", + device1_event_channel.dyn_sender(), + ))); + + info!("Creating charger 0"); + static CHARGER0_EVENT_CHANNEL: StaticCell< + Channel, + > = StaticCell::new(); + let charger0_event_channel = CHARGER0_EVENT_CHANNEL.init(Channel::new()); + static CHARGER0: StaticCell = StaticCell::new(); + let charger0 = CHARGER0.init(Mutex::new(ExampleCharger::new(charger0_event_channel.dyn_sender()))); + + let registration = ArrayRegistration { + psus: [device0, device1], + service_senders: [NoopSender], + chargers: [charger0], + }; + + static SERVICE: StaticCell = StaticCell::new(); + let service = SERVICE.init(Mutex::new(power_policy_service::service::Service::new( + registration, + power_policy_service::service::config::Config::default(), + ))); + + spawner.spawn( + power_policy_task( + PsuEventReceivers::new( + [device0, device1], + [ + device0_event_channel.dyn_receiver(), + device1_event_channel.dyn_receiver(), + ], + ), + ChargerEventReceivers::new([charger0], [charger0_event_channel.dyn_receiver()]), + service, + ) + .expect("Failed to create power policy task"), + ); + + // Check ready charger 0, should transition to Powered(Init) + info!("Charger 0 check ready"); + { + let mut chrg0 = charger0.lock().await; + chrg0.simulate_check_ready().await; + chrg0.assert_state(charger::InternalState::Powered(charger::PoweredSubstate::Init), None); + } + + Timer::after_millis(PER_CALL_DELAY_MS).await; + + // Check ready charger 0, should transition to Powered(PsuDetached) + info!("Charger 0 init"); + { + let mut chrg0 = charger0.lock().await; + // For production code, use more robust error handling (eg. retries) instead of blowing up. + chrg0.simulate_init_request().await; + chrg0.assert_state( + charger::InternalState::Powered(charger::PoweredSubstate::PsuDetached), + None, + ); + } + Timer::after_millis(PER_CALL_DELAY_MS).await; // Plug in device 0, should become current consumer info!("Connecting device 0"); - let device0 = device0.attach().await.unwrap(); - device0 - .notify_consumer_power_capability(Some(ConsumerPowerCapability { + { + let mut dev0 = device0.lock().await; + dev0.simulate_attach().await; + dev0.simulate_update_consumer_power_capability(Some(ConsumerPowerCapability { capability: LOW_POWER, - flags: flags::Consumer::none().with_unconstrained_power(), + flags: ConsumerFlags::none().with_unconstrained_power(), })) - .await - .unwrap(); + .await; + } + Timer::after_millis(PER_CALL_DELAY_MS).await; + { + // Simulate PSU attach + let mut chrg0 = charger0.lock().await; + chrg0.simulate_psu_state_change(charger::PsuState::Attached).await; + chrg0.assert_state( + charger::InternalState::Powered(charger::PoweredSubstate::PsuAttached), + Some(ConsumerPowerCapability { + capability: LOW_POWER, + flags: ConsumerFlags::none().with_unconstrained_power(), + }), + ); + } // Plug in device 1, should become current consumer + // Charger should detach from device 0 and attach to device 1 with higher power info!("Connecting device 1"); - let device1 = device1.attach().await.unwrap(); - device1 - .notify_consumer_power_capability(Some(HIGH_POWER.into())) - .await - .unwrap(); + { + let mut dev1 = device1.lock().await; + dev1.simulate_attach().await; + dev1.simulate_update_consumer_power_capability(Some(HIGH_POWER.into())) + .await; + } + Timer::after_millis(PER_CALL_DELAY_MS).await; + + { + let chrg0 = charger0.lock().await; + chrg0.assert_state( + charger::InternalState::Powered(charger::PoweredSubstate::PsuAttached), + Some(HIGH_POWER.into()), + ); + } // Unplug device 0, device 1 should remain current consumer - info!("Unpluging device 0"); - let device0 = device0.detach().await.unwrap(); + info!("Unplugging device 0"); + { + let mut dev0 = device0.lock().await; + dev0.simulate_detach().await; + } + Timer::after_millis(PER_CALL_DELAY_MS).await; // Plug in device 0, device 1 should remain current consumer info!("Connecting device 0"); - let device0 = device0.attach().await.unwrap(); - device0 - .notify_consumer_power_capability(Some(LOW_POWER.into())) - .await - .unwrap(); + { + let mut dev0 = device0.lock().await; + dev0.simulate_attach().await; + dev0.simulate_update_consumer_power_capability(Some(LOW_POWER.into())) + .await; + } + Timer::after_millis(PER_CALL_DELAY_MS).await; // Unplug device 1, device 0 should become current consumer + // Charger should detach from device 1 and attach to device 0 with lower power info!("Unplugging device 1"); - let device1 = device1.detach().await.unwrap(); + { + let mut dev1 = device1.lock().await; + dev1.simulate_detach().await; + } + Timer::after_millis(PER_CALL_DELAY_MS).await; + + { + let chrg0 = charger0.lock().await; + chrg0.assert_state( + charger::InternalState::Powered(charger::PoweredSubstate::PsuAttached), + Some(LOW_POWER.into()), + ); + } // Replug device 1, device 1 becomes current consumer info!("Connecting device 1"); - let device1 = device1.attach().await.unwrap(); - device1 - .notify_consumer_power_capability(Some(HIGH_POWER.into())) - .await - .unwrap(); + { + let mut dev1 = device1.lock().await; + dev1.simulate_attach().await; + dev1.simulate_update_consumer_power_capability(Some(HIGH_POWER.into())) + .await; + } + Timer::after_millis(PER_CALL_DELAY_MS).await; - // Disconnect consumer device 0, device 1 should remain current consumer + // Detach consumer device 0, device 1 should remain current consumer // Device 0 should not be able to consume after device 1 is unplugged - info!("Disconnecting device 0"); - device0.notify_consumer_power_capability(None).await.unwrap(); - let device1 = device1.detach().await.unwrap(); - - // Switch to provider on device0 - info!("Device 0 requesting provider"); - device0 - .request_provider_power_capability(LOW_POWER.into()) - .await - .unwrap(); - Timer::after_millis(250).await; - info!( - "Total provider power: {} mW", - policy::policy::compute_total_provider_power_mw().await - ); + info!("Detach device 0"); + { + let mut dev0 = device0.lock().await; + dev0.simulate_update_consumer_power_capability(None).await; + } + Timer::after_millis(PER_CALL_DELAY_MS).await; + + // Charger should still have device 1 capability + { + let chrg0 = charger0.lock().await; + chrg0.assert_state( + charger::InternalState::Powered(charger::PoweredSubstate::PsuAttached), + Some(HIGH_POWER.into()), + ); + } + + // Detach device 1, no consumers available + // Charger should detach and clear capability + info!("Detaching device 1"); + { + let mut dev1 = device1.lock().await; + dev1.simulate_detach().await; + } + Timer::after_millis(PER_CALL_DELAY_MS).await; + + { + let chrg0 = charger0.lock().await; + chrg0.assert_state( + charger::InternalState::Powered(charger::PoweredSubstate::PsuAttached), + None, + ); + } + + // Simulate charger PSU detach, charger should transition to PsuDetached + info!("Simulating charger PSU detach"); + { + let mut chrg0 = charger0.lock().await; + chrg0.simulate_psu_state_change(charger::PsuState::Detached).await; + chrg0.assert_state( + charger::InternalState::Powered(charger::PoweredSubstate::PsuDetached), + None, + ); + } + Timer::after_millis(PER_CALL_DELAY_MS).await; + + // Simulate charger PSU reattach + info!("Simulating charger PSU reattach"); + { + let mut chrg0 = charger0.lock().await; + chrg0.simulate_psu_state_change(charger::PsuState::Attached).await; + chrg0.assert_state( + charger::InternalState::Powered(charger::PoweredSubstate::PsuAttached), + None, + ); + } + Timer::after_millis(PER_CALL_DELAY_MS).await; + + // Simulate charger timeout, should transition to Unpowered + info!("Simulating charger timeout"); + { + let mut chrg0 = charger0.lock().await; + chrg0.simulate_timeout(); + chrg0.assert_state(charger::InternalState::Unpowered, None); + } + Timer::after_millis(PER_CALL_DELAY_MS).await; + + // Recover charger: CheckReady -> Init -> PsuDetached + info!("Recovering charger: CheckReady"); + { + let mut chrg0 = charger0.lock().await; + chrg0.simulate_check_ready().await; + chrg0.assert_state(charger::InternalState::Powered(charger::PoweredSubstate::Init), None); + } + Timer::after_millis(PER_CALL_DELAY_MS).await; + + info!("Recovering charger: InitRequest"); + { + let mut chrg0 = charger0.lock().await; + chrg0.simulate_init_request().await; + chrg0.assert_state( + charger::InternalState::Powered(charger::PoweredSubstate::PsuDetached), + None, + ); + } + Timer::after_millis(PER_CALL_DELAY_MS).await; + + // Switch device 0 to provider + info!("Device 0 switch to provider"); + { + let mut dev0 = device0.lock().await; + dev0.simulate_update_requested_provider_power_capability(Some(HIGH_POWER.into())) + .await; + } + Timer::after_millis(PER_CALL_DELAY_MS).await; + // Attach device 1 and request provider info!("Device 1 attach and requesting provider"); - let device1 = device1.attach().await.unwrap(); - device1 - .request_provider_power_capability(LOW_POWER.into()) - .await - .unwrap(); - // Wait for the provider to be connected - Timer::after_millis(250).await; - info!( - "Total provider power: {} mW", - policy::policy::compute_total_provider_power_mw().await - ); + { + let mut dev1 = device1.lock().await; + dev1.simulate_attach().await; + dev1.simulate_update_requested_provider_power_capability(Some(LOW_POWER.into())) + .await; + } + Timer::after_millis(PER_CALL_DELAY_MS).await; - // Provider upgrade should fail because device 0 is already connected + // Provider upgrade should fail because device 0 is already connected at high power info!("Device 1 attempting provider upgrade"); - device1 - .request_provider_power_capability(HIGH_POWER.into()) - .await - .unwrap(); - // Wait for the upgrade flow to complete - Timer::after_millis(250).await; - info!( - "Total provider power: {} mW", - policy::policy::compute_total_provider_power_mw().await - ); + { + let mut dev1 = device1.lock().await; + dev1.simulate_update_requested_provider_power_capability(Some(HIGH_POWER.into())) + .await; + } + Timer::after_millis(PER_CALL_DELAY_MS).await; // Disconnect device 0 info!("Device 0 disconnecting"); - device0.detach().await.unwrap(); - // Wait for the detach flow to complete - Timer::after_millis(250).await; - info!( - "Total provider power: {} mW", - policy::policy::compute_total_provider_power_mw().await - ); + { + let mut dev0 = device0.lock().await; + dev0.simulate_detach().await; + } + Timer::after_millis(PER_CALL_DELAY_MS).await; // Provider upgrade should succeed now info!("Device 1 attempting provider upgrade"); - device1 - .request_provider_power_capability(HIGH_POWER.into()) - .await - .unwrap(); - // Wait for the upgrade flow to complete - Timer::after_millis(250).await; - info!( - "Total provider power: {} mW", - policy::policy::compute_total_provider_power_mw().await - ); -} - -#[embassy_executor::task] -async fn receiver_task() { - static CHANNEL: StaticCell> = StaticCell::new(); - let channel = CHANNEL.init(PubSubChannel::new()); - - let publisher = channel.dyn_immediate_publisher(); - let mut subscriber = channel.dyn_subscriber().unwrap(); - - static RECEIVER: StaticCell> = StaticCell::new(); - let receiver = RECEIVER.init(broadcaster::Receiver::new(publisher)); - - policy::policy::register_message_receiver(receiver).unwrap(); - - loop { - match subscriber.next_message().await { - embassy_sync::pubsub::WaitResult::Message(msg) => { - info!("Received message: {msg:?}"); - } - embassy_sync::pubsub::WaitResult::Lagged(count) => { - warn!("Lagged messages: {count}"); - } - } + { + let mut dev1 = device1.lock().await; + dev1.simulate_update_requested_provider_power_capability(Some(HIGH_POWER.into())) + .await; } + Timer::after_millis(PER_CALL_DELAY_MS).await; } #[embassy_executor::task] -async fn power_policy_task(config: power_policy_service::config::Config) { - power_policy_service::task::task(config) - .await - .expect("Failed to start power policy service task"); +async fn power_policy_task( + psu_events: PsuEventReceivers< + 'static, + 2, + DeviceType, + channel::DynamicReceiver<'static, power_policy_interface::psu::event::EventData>, + >, + charger_events: ChargerEventReceivers< + 'static, + 1, + ChargerType, + channel::DynamicReceiver<'static, power_policy_interface::charger::event::EventData>, + >, + power_policy: &'static ServiceType, +) { + power_policy_service::service::task::task(psu_events, charger_events, power_policy).await; } fn main() { @@ -246,9 +546,8 @@ fn main() { static EXECUTOR: StaticCell = StaticCell::new(); let executor = EXECUTOR.init(Executor::new()); + executor.run(|spawner| { - spawner.spawn(power_policy_task(power_policy_service::config::Config::default()).unwrap()); - spawner.spawn(run(spawner).unwrap()); - spawner.spawn(receiver_task().unwrap()); + spawner.spawn(run(spawner).expect("Failed to create run task")); }); } diff --git a/examples/std/src/bin/thermal.rs b/examples/std/src/bin/thermal.rs index 2d96770e0..d17faffeb 100644 --- a/examples/std/src/bin/thermal.rs +++ b/examples/std/src/bin/thermal.rs @@ -1,361 +1,85 @@ use embassy_executor::{Executor, Spawner}; -use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex; -use embassy_sync::mutex::Mutex; -use embassy_sync::once_lock::OnceLock; +use embassy_sync::channel::{Channel, Receiver as ChannelReceiver, Sender as ChannelSender}; use embassy_time::Timer; -use embedded_fans_async as fan; -use embedded_sensors_hal_async::sensor; -use embedded_sensors_hal_async::temperature::{DegreesCelsius, TemperatureSensor, TemperatureThresholdSet}; -use embedded_services::comms; -use log::{info, warn}; +use embedded_services::GlobalRawMutex; +use embedded_services::{info, warn}; use static_cell::StaticCell; -use std::sync::atomic::AtomicUsize; -use std::sync::atomic::Ordering; use thermal_service as ts; -use ts::mptf; +use thermal_service_interface::ThermalService; +use thermal_service_interface::fan::FanService; +use thermal_service_interface::sensor; +use thermal_service_interface::sensor::SensorService; + +// More readable type aliases for sensor, fan, and thermal services used in this example +type MockSensorService = ts::sensor::Service< + 'static, + ts::mock::sensor::MockSensor, + ChannelSender<'static, GlobalRawMutex, sensor::Event, 4>, + 16, +>; +type MockFanService = + ts::fan::Service<'static, ts::mock::fan::MockFan, MockSensorService, embedded_services::event::NoopSender, 16>; +type MockThermalService = ts::Service<'static, MockSensorService, MockFanService>; -// Mock host service -mod host { - use embassy_sync::{blocking_mutex::raw::CriticalSectionRawMutex, signal::Signal}; - use embedded_services::comms::{self, Endpoint, EndpointID, External, MailboxDelegate}; - use log::{info, warn}; - use thermal_service as ts; - use ts::mptf; - - pub struct Host { - pub tp: Endpoint, - pub alert: Signal, - } - - impl Host { - pub fn new() -> Self { - Self { - tp: Endpoint::uninit(EndpointID::External(External::Host)), - alert: Signal::new(), - } - } +#[embassy_executor::task] +async fn run(spawner: Spawner) { + embedded_services::init().await; - fn handle_response(&self, response: mptf::Response) { - match response.data { - mptf::ResponseData::GetTmp(tmp) => { - info!("Host received temperature: {} °C", ts::utils::dk_to_c(tmp)) - } - mptf::ResponseData::GetVar(_status, value) => { - info!("Host received fan RPM: {value}") - } - _ => info!("Received MPTF response: {response:?}"), - } + // Create a backing channel for sensor events to be sent on + static SENSOR_EVENT_CHANNEL: StaticCell> = StaticCell::new(); + let sensor_event_channel = SENSOR_EVENT_CHANNEL.init(Channel::new()); + + // Then create the list of senders for the sensor service to use + // Though we are only using one sender in this example, an abitrary number could be used + static SENSOR_SENDERS: StaticCell<[ChannelSender<'static, GlobalRawMutex, sensor::Event, 4>; 1]> = + StaticCell::new(); + let event_senders = SENSOR_SENDERS.init([sensor_event_channel.sender()]); + + // Spawn the sensor service which will begin running and generating events + let sensor_service = odp_service_common::spawn_service!( + spawner, + MockSensorService, + ts::sensor::InitParams { + driver: ts::mock::sensor::MockSensor::new(), + config: ts::mock::sensor::MockSensor::config(), + event_senders, } - } - - impl MailboxDelegate for Host { - fn receive(&self, message: &comms::Message) -> Result<(), comms::MailboxDelegateError> { - if let Some(&response) = message.data.get::() { - self.handle_response(response); - Ok(()) - } else if let Some(¬ification) = message.data.get::() { - warn!("Received notification: {notification:?}"); - self.alert.signal(()); - Ok(()) - } else { - Err(comms::MailboxDelegateError::MessageNotFound) - } + ) + .expect("Failed to spawn sensor service"); + + // Spawn the fan service which uses the above sensor service for automatic speed control + // In this example, we use an empty event sender list since the fan won't generate any events + let fan_service = odp_service_common::spawn_service!( + spawner, + MockFanService, + ts::fan::InitParams { + driver: ts::mock::fan::MockFan::new(), + config: ts::mock::fan::MockFan::config(), + sensor_service, + event_senders: &mut [], } - } -} - -// A mock struct shared by MockSensor and MockAlertPin to sync on raw samples and thresholds -struct MockBus { - samples: [f32; 35], - idx: AtomicUsize, - threshold_low: Mutex, - threshold_high: Mutex, -} - -impl MockBus { - fn new() -> Self { - Self { - samples: [ - 20.0, 25.0, 30.0, 35.0, 40.0, 45.0, 50.0, 55.0, 60.0, 65.0, 70.0, 75.0, 80.0, 85.0, 90.0, 95.0, 100.0, - 105.0, 100.0, 95.0, 90.0, 85.0, 80.0, 75.0, 70.0, 65.0, 60.0, 55.0, 50.0, 45.0, 40.0, 35.0, 30.0, 25.0, - 20.0, - ], - idx: AtomicUsize::new(0), - threshold_low: Mutex::new(0.0), - threshold_high: Mutex::new(0.0), - } - } - - // Return the current sample and move to next sample (wrapping around at end) - fn sample_and_next(&self) -> f32 { - self.samples[self - .idx - .fetch_update(Ordering::SeqCst, Ordering::SeqCst, |idx| { - Some((idx + 1) % self.samples.len()) - }) - .unwrap()] - } - - async fn set_threshold_low(&self, threshold: f32) { - *self.threshold_low.lock().await = threshold - } - - async fn set_threshold_high(&self, threshold: f32) { - *self.threshold_high.lock().await = threshold - } -} - -#[derive(Copy, Clone, Debug)] -struct MockSensorError; -impl sensor::Error for MockSensorError { - fn kind(&self) -> sensor::ErrorKind { - sensor::ErrorKind::Other - } -} - -// A mock temperature sensor -struct MockSensor { - bus: &'static MockBus, -} - -impl MockSensor { - fn new(bus: &'static MockBus) -> Self { - Self { bus } - } -} - -impl sensor::ErrorType for MockSensor { - type Error = MockSensorError; -} - -impl TemperatureSensor for MockSensor { - async fn temperature(&mut self) -> Result { - Ok(self.bus.sample_and_next()) - } -} - -impl TemperatureThresholdSet for MockSensor { - async fn set_temperature_threshold_low(&mut self, threshold: DegreesCelsius) -> Result<(), Self::Error> { - self.bus.set_threshold_low(threshold).await; - Ok(()) - } - - async fn set_temperature_threshold_high(&mut self, threshold: DegreesCelsius) -> Result<(), Self::Error> { - self.bus.set_threshold_high(threshold).await; - Ok(()) - } -} - -impl ts::sensor::CustomRequestHandler for MockSensor {} -impl ts::sensor::Controller for MockSensor {} - -#[derive(Copy, Clone, Debug)] -struct MockFanError; -impl fan::Error for MockFanError { - fn kind(&self) -> embedded_fans_async::ErrorKind { - fan::ErrorKind::Other - } -} - -// A mock fan -struct MockFan { - rpm: u16, -} - -impl MockFan { - fn new() -> Self { - Self { rpm: 0 } - } -} - -impl fan::ErrorType for MockFan { - type Error = MockFanError; -} - -impl fan::Fan for MockFan { - fn min_rpm(&self) -> u16 { - 1000 - } + ) + .expect("Failed to spawn fan service"); - fn max_rpm(&self) -> u16 { - 5000 - } - - fn min_start_rpm(&self) -> u16 { - 1000 - } - - async fn set_speed_rpm(&mut self, rpm: u16) -> Result { - self.rpm = rpm; - Ok(rpm) - } -} - -impl fan::RpmSense for MockFan { - async fn rpm(&mut self) -> Result { - Ok(self.rpm) - } -} - -impl ts::fan::CustomRequestHandler for MockFan {} -impl ts::fan::RampResponseHandler for MockFan {} -impl ts::fan::Controller for MockFan {} - -// Simulates host receiving requests from OSPM and forwarding to thermal service -#[embassy_executor::task] -async fn host() { - info!("Spawning host task"); - - static HOST: OnceLock = OnceLock::new(); - let host = HOST.get_or_init(host::Host::new); - info!("Registering host endpoint"); - comms::register_endpoint(host, &host.tp).await.unwrap(); - - let thermal_id = comms::EndpointID::Internal(comms::Internal::Thermal); - - // Set thresholds to 40 °C (3131 deciKelvin) - host.tp - .send(thermal_id, &mptf::Request::SetThrs(0, 0, 0, 3131)) - .await - .unwrap(); - Timer::after_millis(100).await; - - // Set Fan ON temp to 40 °C (3131 deciKelvin) - host.tp - .send( - thermal_id, - &mptf::Request::SetVar(0, 4, mptf::uuid_standard::FAN_ON_TEMP, 3131), - ) - .await - .unwrap(); - Timer::after_millis(100).await; - - // Set Fan RAMP temp to 50 °C (3231 deciKelvin) - host.tp - .send( - thermal_id, - &mptf::Request::SetVar(0, 4, mptf::uuid_standard::FAN_RAMP_TEMP, 3231), - ) - .await - .unwrap(); - Timer::after_millis(100).await; - - // Set Fan MAX temp to 80 °C (3531 deciKelvin) - host.tp - .send( - thermal_id, - &mptf::Request::SetVar(0, 4, mptf::uuid_standard::FAN_MAX_TEMP, 3531), - ) - .await - .unwrap(); - Timer::after_millis(100).await; - - // Wait to receive MPTF notification that threshold exceeded, then request temperature and RPM - loop { - host.alert.wait().await; - - info!("Host requesting temperature in response to threshold alert"); - host.tp.send(thermal_id, &mptf::Request::GetTmp(0)).await.unwrap(); - - // Need to wait briefly before send is fixed to propagate errors and we can handle retries - Timer::after_millis(100).await; - - info!("Host requesting fan RPM in response to threshold alert"); - host.tp - .send( - thermal_id, - &mptf::Request::GetVar(0, 4, mptf::uuid_standard::FAN_CURRENT_RPM), - ) - .await - .unwrap(); - } -} - -async fn init_sensor(spawner: Spawner) { - info!("Initializing mock bus"); - static BUS: OnceLock = OnceLock::new(); - let bus = BUS.get_or_init(MockBus::new); - - info!("Initializing mock sensor"); - let mock_sensor = MockSensor::new(bus); - static SENSOR: OnceLock> = OnceLock::new(); + // The thermal service accepts slices of associated sensors and fans, + // so we need static lifetime here since the thermal service handle is passed to task + static SENSORS: StaticCell<[MockSensorService; 1]> = StaticCell::new(); + let sensors = SENSORS.init([sensor_service]); - let profile = ts::sensor::Profile { - warn_high_threshold: 40.0, - prochot_threshold: 50.0, - crt_threshold: 80.0, - ..Default::default() - }; - let sensor = SENSOR.get_or_init(|| ts::sensor::Sensor::new(ts::sensor::DeviceId(0), mock_sensor, profile)); + static FANS: StaticCell<[MockFanService; 1]> = StaticCell::new(); + let fans = FANS.init([fan_service]); - ts::register_sensor(sensor.device()).await.unwrap(); - spawner.spawn(mock_sensor_task(sensor).unwrap()); -} - -async fn init_fan(spawner: Spawner) { - info!("Initializing mock fan"); - let mock_fan = MockFan::new(); - static FAN: OnceLock> = OnceLock::new(); - let fan = FAN.get_or_init(|| ts::fan::Fan::new(ts::fan::DeviceId(0), mock_fan, ts::fan::Profile::default())); - - ts::register_fan(fan.device()).await.unwrap(); - spawner.spawn(mock_fan_task(fan).unwrap()); -} - -async fn init_thermal(spawner: Spawner) { - info!("Initializing thermal service"); - ts::init().await.unwrap(); - - init_sensor(spawner).await; - init_fan(spawner).await; -} + // The thermal service handle mainly exists for host relaying, but this example does not make use of that + // + // However, we can still use the thermal service handle to access registered sensors and fans by id + static RESOURCES: StaticCell> = StaticCell::new(); + let resources = RESOURCES.init(ts::Resources::default()); + let thermal_service = ts::Service::init(resources, ts::InitParams { sensors, fans }); -#[embassy_executor::task] -async fn handle_alerts() { - loop { - match ts::wait_event().await { - ts::Event::ThresholdExceeded(ts::sensor::DeviceId(sensor_id), ts::sensor::ThresholdType::WarnHigh, _) => { - warn!("Sensor {sensor_id} exceeded WARN threshold"); - ts::send_service_msg(comms::EndpointID::External(comms::External::Host), &mptf::Notify::Warn) - .await - .unwrap() - } - ts::Event::ThresholdExceeded(ts::sensor::DeviceId(sensor_id), ts::sensor::ThresholdType::Prochot, _) => { - warn!("Sensor {sensor_id} exceeded PROCHOT threshold"); - ts::send_service_msg( - comms::EndpointID::External(comms::External::Host), - &mptf::Notify::ProcHot, - ) - .await - .unwrap() - } - ts::Event::ThresholdExceeded(ts::sensor::DeviceId(sensor_id), ts::sensor::ThresholdType::Critical, _) => { - warn!("Sensor {sensor_id} exceeded CRITICAL threshold"); - ts::send_service_msg( - comms::EndpointID::External(comms::External::Host), - &mptf::Notify::Critical, - ) - .await - .unwrap() - } - event => warn!("Event: {event:?}"), - } - } -} - -#[embassy_executor::task] -async fn handle_requests() -> ! { - ts::task::handle_requests().await; - unreachable!() -} - -#[embassy_executor::task] -async fn run(spawner: Spawner) { - embedded_services::init().await; - init_thermal(spawner).await; - spawner.spawn(host().unwrap()); - spawner.spawn(handle_alerts().unwrap()); - spawner.spawn(handle_requests().unwrap()); + spawner.spawn(monitor(thermal_service).expect("Failed to create monitor task")); + spawner.spawn( + sensor_event_listener(sensor_event_channel.receiver()).expect("Failed to create sensor event listener task"), + ); } fn main() { @@ -364,18 +88,31 @@ fn main() { static EXECUTOR: StaticCell = StaticCell::new(); let executor = EXECUTOR.init(Executor::new()); executor.run(|spawner| { - spawner.spawn(run(spawner).unwrap()); + spawner.spawn(run(spawner).expect("Failed to create run task")); }); } #[embassy_executor::task] -async fn mock_sensor_task(sensor: &'static ts::sensor::Sensor) -> ! { - ts::task::sensor_task(sensor).await; - unreachable!() +async fn sensor_event_listener(receiver: ChannelReceiver<'static, GlobalRawMutex, sensor::Event, 4>) { + loop { + let event = receiver.receive().await; + warn!("Sensor event: {:?}", event); + } } #[embassy_executor::task] -async fn mock_fan_task(fan: &'static ts::fan::Fan) -> ! { - ts::task::fan_task(fan).await; - unreachable!() +async fn monitor(service: MockThermalService) { + loop { + if let Some(sensor) = service.sensor(0) { + let temp = sensor.temperature().await; + info!("Mock sensor temp: {} C", temp); + } + + if let Some(fan) = service.fan(0) { + let rpm = fan.rpm().await; + info!("Mock fan RPM: {}", rpm); + } + + Timer::after_secs(1).await; + } } diff --git a/examples/std/src/bin/type_c/basic.rs b/examples/std/src/bin/type_c/basic.rs deleted file mode 100644 index 73dbb3e4b..000000000 --- a/examples/std/src/bin/type_c/basic.rs +++ /dev/null @@ -1,173 +0,0 @@ -use embassy_executor::{Executor, Spawner}; -use embassy_sync::once_lock::OnceLock; -use embassy_time::Timer; -use embedded_services::power; -use embedded_services::type_c::{Cached, ControllerId, controller}; -use embedded_usb_pd::ucsi::lpm; -use embedded_usb_pd::{GlobalPortId, PdError as Error}; -use log::*; -use static_cell::StaticCell; - -const CONTROLLER0_ID: ControllerId = ControllerId(0); -const PORT0_ID: GlobalPortId = GlobalPortId(0); -const PORT1_ID: GlobalPortId = GlobalPortId(1); -const POWER0_ID: power::policy::DeviceId = power::policy::DeviceId(0); - -mod test_controller { - use embedded_services::type_c::controller::{ControllerStatus, PortStatus}; - use embedded_usb_pd::ucsi; - - use super::*; - - pub struct Controller<'a> { - pub controller: controller::Device<'a>, - pub power_policy: power::policy::device::Device, - } - - impl controller::DeviceContainer for Controller<'_> { - fn get_pd_controller_device(&self) -> &controller::Device<'_> { - &self.controller - } - } - - impl power::policy::device::DeviceContainer for Controller<'_> { - fn get_power_policy_device(&self) -> &power::policy::device::Device { - &self.power_policy - } - } - - impl<'a> Controller<'a> { - pub fn new(id: ControllerId, power_id: power::policy::DeviceId, ports: &'a [GlobalPortId]) -> Self { - Self { - controller: controller::Device::new(id, ports), - power_policy: power::policy::device::Device::new(power_id), - } - } - - async fn process_controller_command( - &self, - command: controller::InternalCommandData, - ) -> Result, Error> { - match command { - controller::InternalCommandData::Reset => { - info!("Reset controller"); - Ok(controller::InternalResponseData::Complete) - } - controller::InternalCommandData::Status => { - info!("Get controller status"); - Ok(controller::InternalResponseData::Status(ControllerStatus { - mode: "Test", - valid_fw_bank: true, - fw_version0: 0xbadf00d, - fw_version1: 0xdeadbeef, - })) - } - controller::InternalCommandData::SyncState => { - info!("Sync controller state"); - Ok(controller::InternalResponseData::Complete) - } - } - } - - async fn process_ucsi_command(&self, command: &lpm::GlobalCommand) -> ucsi::GlobalResponse { - match command.operation() { - lpm::CommandData::ConnectorReset => { - info!("Reset for port {:#?}", command.port()); - ucsi::Response { - cci: ucsi::cci::Cci::new_cmd_complete(), - data: None, - } - } - rest => { - info!("UCSI command {:#?} for port {:#?}", rest, command.port()); - ucsi::Response { - cci: ucsi::cci::Cci::new_cmd_complete(), - data: None, - } - } - } - } - - async fn process_port_command( - &self, - command: controller::PortCommand, - ) -> Result { - Ok(match command.data { - controller::PortCommandData::PortStatus(Cached(true)) => { - info!("Port status for port {}", command.port.0); - controller::PortResponseData::PortStatus(PortStatus::new()) - } - _ => { - info!("Port command for port {}", command.port.0); - controller::PortResponseData::Complete - } - }) - } - - pub async fn process(&self) { - let request = self.controller.receive().await; - let response = match request.command { - controller::Command::Controller(command) => { - controller::Response::Controller(self.process_controller_command(command).await) - } - controller::Command::Lpm(command) => { - controller::Response::Ucsi(self.process_ucsi_command(&command).await) - } - controller::Command::Port(command) => { - controller::Response::Port(self.process_port_command(command).await) - } - }; - - request.respond(response); - } - } -} - -#[embassy_executor::task] -async fn controller_task() { - static CONTROLLER: OnceLock = OnceLock::new(); - - static PORTS: [GlobalPortId; 2] = [PORT0_ID, PORT1_ID]; - - let controller = CONTROLLER.get_or_init(|| test_controller::Controller::new(CONTROLLER0_ID, POWER0_ID, &PORTS)); - controller::register_controller(controller).unwrap(); - - loop { - controller.process().await; - } -} - -#[embassy_executor::task] -async fn task(spawner: Spawner) { - embedded_services::init().await; - - controller::init(); - - info!("Starting controller task"); - spawner.spawn(controller_task().unwrap()); - // Wait for controller to be registered - Timer::after_secs(1).await; - - let context = controller::ContextToken::create().unwrap(); - - context.reset_controller(CONTROLLER0_ID).await.unwrap(); - - let status = context.get_controller_status(CONTROLLER0_ID).await.unwrap(); - info!("Controller 0 status: {status:#?}"); - - let status = context.get_port_status(PORT0_ID, Cached(true)).await.unwrap(); - info!("Port 0 status: {status:#?}"); - - let status = context.get_port_status(PORT1_ID, Cached(true)).await.unwrap(); - info!("Port 1 status: {status:#?}"); -} - -fn main() { - env_logger::builder().filter_level(log::LevelFilter::Info).init(); - - static EXECUTOR: StaticCell = StaticCell::new(); - let executor = EXECUTOR.init(Executor::new()); - executor.run(|spawner| { - spawner.spawn(task(spawner).unwrap()); - }); -} diff --git a/examples/std/src/bin/type_c/external.rs b/examples/std/src/bin/type_c/external.rs deleted file mode 100644 index 13c24752f..000000000 --- a/examples/std/src/bin/type_c/external.rs +++ /dev/null @@ -1,116 +0,0 @@ -//! Low-level example of external messaging with a simple type-C service -use embassy_executor::{Executor, Spawner}; -use embassy_sync::mutex::Mutex; -use embassy_time::Timer; -use embedded_services::{ - GlobalRawMutex, power, - type_c::{Cached, ControllerId, external}, -}; -use embedded_usb_pd::GlobalPortId; -use log::*; -use static_cell::StaticCell; -use std_examples::type_c::mock_controller; -use type_c_service::wrapper::backing::Storage; - -const CONTROLLER0_ID: ControllerId = ControllerId(0); -const PORT0_ID: GlobalPortId = GlobalPortId(0); -const POWER0_ID: power::policy::DeviceId = power::policy::DeviceId(0); - -#[embassy_executor::task] -async fn controller_task() { - static STATE: StaticCell = StaticCell::new(); - let state = STATE.init(mock_controller::ControllerState::new()); - - static STORAGE: StaticCell> = StaticCell::new(); - let backing_storage = STORAGE.init(Storage::new( - CONTROLLER0_ID, - 0, // CFU component ID (unused) - [(PORT0_ID, POWER0_ID)], - )); - static REFERENCED: StaticCell> = - StaticCell::new(); - let referenced = REFERENCED.init( - backing_storage - .create_referenced() - .expect("Failed to create referenced storage"), - ); - - static CONTROLLER: StaticCell> = StaticCell::new(); - let controller = CONTROLLER.init(Mutex::new(mock_controller::Controller::new(state))); - - static WRAPPER: StaticCell = StaticCell::new(); - let wrapper = WRAPPER.init( - mock_controller::Wrapper::try_new( - controller, - Default::default(), - referenced, - crate::mock_controller::Validator, - ) - .expect("Failed to create wrapper"), - ); - - wrapper.register().await.unwrap(); - loop { - if let Err(e) = wrapper.process_next_event().await { - error!("Error processing wrapper: {e:#?}"); - } - } -} - -#[embassy_executor::task] -async fn task(_spawner: Spawner) { - info!("Starting main task"); - embedded_services::init().await; - - // Allow the controller to initialize and register itself - Timer::after_secs(1).await; - info!("Getting controller status"); - let controller_status = external::get_controller_status(ControllerId(0)).await.unwrap(); - info!("Controller status: {controller_status:?}"); - - info!("Getting port status"); - let port_status = external::get_port_status(GlobalPortId(0), Cached(true)).await.unwrap(); - info!("Port status: {port_status:?}"); - - info!("Getting retimer fw update status"); - let rt_fw_update_status = external::port_get_rt_fw_update_status(GlobalPortId(0)).await.unwrap(); - info!("Get retimer fw update status: {rt_fw_update_status:?}"); - - info!("Setting retimer fw update state"); - external::port_set_rt_fw_update_state(GlobalPortId(0)).await.unwrap(); - - info!("Clearing retimer fw update state"); - external::port_clear_rt_fw_update_state(GlobalPortId(0)).await.unwrap(); - - info!("Setting retimer compliance"); - external::port_set_rt_compliance(GlobalPortId(0)).await.unwrap(); - - info!("Setting max sink voltage"); - external::set_max_sink_voltage(GlobalPortId(0), Some(5000)) - .await - .unwrap(); - - info!("Clearing dead battery flag"); - external::clear_dead_battery_flag(GlobalPortId(0)).await.unwrap(); - - info!("Reconfiguring retimer"); - external::reconfigure_retimer(GlobalPortId(0)).await.unwrap(); -} - -#[embassy_executor::task] -async fn type_c_service_task() -> ! { - type_c_service::task(Default::default()).await; - unreachable!() -} - -fn main() { - env_logger::builder().filter_level(log::LevelFilter::Trace).init(); - - static EXECUTOR: StaticCell = StaticCell::new(); - let executor = EXECUTOR.init(Executor::new()); - executor.run(|spawner| { - spawner.spawn(type_c_service_task().unwrap()); - spawner.spawn(task(spawner).unwrap()); - spawner.spawn(controller_task().unwrap()); - }); -} diff --git a/examples/std/src/bin/type_c/service.rs b/examples/std/src/bin/type_c/service.rs index 08e190f1b..3b5bbbc26 100644 --- a/examples/std/src/bin/type_c/service.rs +++ b/examples/std/src/bin/type_c/service.rs @@ -1,110 +1,102 @@ use embassy_executor::{Executor, Spawner}; +use embassy_sync::channel::{DynamicReceiver, DynamicSender}; use embassy_sync::mutex::Mutex; -use embassy_sync::once_lock::OnceLock; +use embassy_sync::pubsub::{DynImmediatePublisher, DynSubscriber, PubSubChannel}; use embassy_time::Timer; -use embedded_services::power::{self}; -use embedded_services::type_c::{ControllerId, controller}; -use embedded_services::{GlobalRawMutex, comms}; -use embedded_usb_pd::GlobalPortId; +use embedded_services::GlobalRawMutex; +use embedded_services::event::{MapSender, NoopSender}; +use embedded_usb_pd::LocalPortId; use embedded_usb_pd::ado::Ado; use embedded_usb_pd::type_c::Current; use log::*; +use power_policy_interface::charger::mock::ChargerType; +use power_policy_interface::psu; +use power_policy_service::psu::PsuEventReceivers; +use power_policy_service::service::registration::ArrayRegistration; use static_cell::StaticCell; -use std_examples::type_c::mock_controller; -use type_c_service::wrapper::backing::{ReferencedStorage, Storage}; -use type_c_service::wrapper::message::*; +use std_examples::type_c::mock_controller::Port; +use std_examples::type_c::mock_controller::{self, InterruptReceiver}; +use type_c_interface::port::event::PortEventBitfield; +use type_c_interface::service::event::PortEventData as ServicePortEventData; +use type_c_service::controller::event_receiver::InterruptReceiver as _; +use type_c_service::controller::event_receiver::{EventReceiver as PortEventReceiver, PortEventSplitter}; +use type_c_service::controller::macros::PortComponents; +use type_c_service::controller::state::SharedState; +use type_c_service::define_controller_port_static_cell_channel; +use type_c_service::service::Service; +use type_c_service::service::config::Config; +use type_c_service::util::power_capability_from_current; -const CONTROLLER0_ID: ControllerId = ControllerId(0); -const PORT0_ID: GlobalPortId = GlobalPortId(0); -const POWER0_ID: power::policy::DeviceId = power::policy::DeviceId(0); const DELAY_MS: u64 = 1000; -mod debug { - use embedded_services::{ - comms::{self, Endpoint, EndpointID, Internal}, - info, - type_c::comms::DebugAccessoryMessage, - }; - - pub struct Listener { - pub tp: Endpoint, - } - - impl Listener { - pub fn new() -> Self { - Self { - tp: Endpoint::uninit(EndpointID::Internal(Internal::Usbc)), - } - } - } - - impl comms::MailboxDelegate for Listener { - fn receive(&self, message: &comms::Message) -> Result<(), comms::MailboxDelegateError> { - if let Some(message) = message.data.get::() { - if message.connected { - info!("Port{}: Debug accessory connected", message.port.0); - } else { - info!("Port{}: Debug accessory disconnected", message.port.0); - } - } - - Ok(()) - } - } -} +type ControllerType = Mutex>; +type PortType = Mutex>; + +type PowerPolicySenderType = MapSender< + power_policy_interface::service::event::Event<'static, PortType>, + power_policy_interface::service::event::EventData, + DynImmediatePublisher<'static, power_policy_interface::service::event::EventData>, + fn( + power_policy_interface::service::event::Event<'static, PortType>, + ) -> power_policy_interface::service::event::EventData, +>; + +type PowerPolicyReceiverType = DynSubscriber<'static, power_policy_interface::service::event::EventData>; + +type PowerPolicyServiceType = Mutex< + GlobalRawMutex, + power_policy_service::service::Service< + 'static, + ArrayRegistration<'static, PortType, 1, PowerPolicySenderType, 1, ChargerType, 0>, + >, +>; + +const PORT_COUNT: usize = 1; +type PortReceiverType = DynamicReceiver<'static, type_c_interface::service::event::PortEventData>; +type TypeCServiceEventReceiverType = type_c_service::service::event_receiver::ArrayEventReceiver< + 'static, + PORT_COUNT, + PortType, + PortReceiverType, + PowerPolicyReceiverType, +>; +type TypeCServiceSenderType = NoopSender; +type TypeCRegistrationType = + type_c_service::service::registration::ArrayRegistration<'static, PortType, PORT_COUNT, TypeCServiceSenderType, 1>; + +type ServiceType = type_c_service::service::Service<'static, TypeCRegistrationType>; +type SharedStateType = Mutex; +type PortEventReceiverType = PortEventReceiver< + 'static, + SharedStateType, + DynamicReceiver<'static, PortEventBitfield>, + DynamicReceiver<'static, type_c_service::controller::event::Loopback>, +>; #[embassy_executor::task] -async fn controller_task(state: &'static mock_controller::ControllerState) { - static STORAGE: StaticCell> = StaticCell::new(); - let storage = STORAGE.init(Storage::new( - CONTROLLER0_ID, - 0, // CFU component ID (unused) - [(PORT0_ID, POWER0_ID)], - )); - static REFERENCED: StaticCell> = StaticCell::new(); - let referenced = REFERENCED.init( - storage - .create_referenced() - .expect("Failed to create referenced storage"), - ); - - static CONTROLLER: StaticCell> = StaticCell::new(); - let controller = CONTROLLER.init(Mutex::new(mock_controller::Controller::new(state))); - - static WRAPPER: StaticCell = StaticCell::new(); - let wrapper = WRAPPER.init( - mock_controller::Wrapper::try_new( - controller, - Default::default(), - referenced, - crate::mock_controller::Validator, - ) - .expect("Failed to create wrapper"), - ); - - wrapper.register().await.unwrap(); - - controller.lock().await.custom_function(); - +async fn port_task(mut event_receiver: PortEventReceiverType, port: &'static PortType) { loop { - let event = wrapper.wait_next().await; - if let Err(e) = event { - error!("Error waiting for event: {e:?}"); - continue; - } - let output = wrapper.process_event(event.unwrap()).await; + let event = event_receiver.wait_event().await; + let output = port.lock().await.process_event(event).await; if let Err(e) = output { error!("Error processing event: {e:?}"); } let output = output.unwrap(); - if let Output::PdAlert(OutputPdAlert { port, ado }) = &output { - info!("Port{}: PD alert received: {:?}", port.0, ado); + if let Some(ServicePortEventData::Alert(ado)) = &output { + info!("PD alert received: {:?}", ado); } + } +} - if let Err(e) = wrapper.finalize(output).await { - error!("Error finalizing output: {e:?}"); - } +#[embassy_executor::task] +async fn interrupt_splitter_task( + mut interrupt_receiver: InterruptReceiver<'static>, + mut interrupt_splitter: PortEventSplitter<1, DynamicSender<'static, PortEventBitfield>>, +) -> ! { + loop { + let interrupts = interrupt_receiver.wait_interrupt().await; + interrupt_splitter.process_interrupts(interrupts).await; } } @@ -112,23 +104,85 @@ async fn controller_task(state: &'static mock_controller::ControllerState) { async fn task(spawner: Spawner) { embedded_services::init().await; - controller::init(); + static STATE: StaticCell = StaticCell::new(); + let state = STATE.init(mock_controller::ControllerState::new()); + + static CONTROLLER: StaticCell = StaticCell::new(); + let controller = CONTROLLER.init(Mutex::new(mock_controller::Controller::new(state, "Controller0"))); + + define_controller_port_static_cell_channel!(pub(self), port, GlobalRawMutex, Mutex>); + let PortComponents { + port, + power_policy_receiver, + event_receiver, + interrupt_sender: port_interrupt_sender, + type_c_receiver, + } = port::create("PD0", LocalPortId(0), Default::default(), controller); + + // Create type-c service + // The service is the only receiver and we only use a DynImmediatePublisher, which doesn't take a publisher slot + static POWER_POLICY_CHANNEL: StaticCell< + PubSubChannel, + > = StaticCell::new(); + + let power_policy_channel = POWER_POLICY_CHANNEL.init(PubSubChannel::new()); + let power_policy_sender: PowerPolicySenderType = + MapSender::new(power_policy_channel.dyn_immediate_publisher(), |e| e.into()); + // Guaranteed to not panic since we initialized the channel above + let power_policy_subscriber = power_policy_channel.dyn_subscriber().unwrap(); + + let power_policy_registration = ArrayRegistration { + psus: [port], + service_senders: [power_policy_sender], + chargers: [], + }; - // Register debug accessory listener - static LISTENER: OnceLock = OnceLock::new(); - let listener = LISTENER.get_or_init(debug::Listener::new); - comms::register_endpoint(listener, &listener.tp).await.unwrap(); + static POWER_SERVICE: StaticCell = StaticCell::new(); + let power_service = POWER_SERVICE.init(Mutex::new(power_policy_service::service::Service::new( + power_policy_registration, + power_policy_service::service::config::Config::default(), + ))); + + static TYPE_C_SERVICE: StaticCell> = StaticCell::new(); + let type_c_service = TYPE_C_SERVICE.init(Mutex::new(Service::create( + Config::default(), + type_c_service::service::registration::ArrayRegistration { + ports: [port], + service_senders: [NoopSender], + port_data: [type_c_service::service::registration::PortData { + local_port: Some(LocalPortId(0)), + }], + }, + ))); + + // Spin up power policy service + spawner.spawn( + power_policy_psu_task(PsuEventReceivers::new([port], [power_policy_receiver]), power_service) + .expect("Failed to create power policy task"), + ); + spawner.spawn( + type_c_service_task( + type_c_service, + TypeCServiceEventReceiverType::new([port], [type_c_receiver], power_policy_subscriber), + ) + .expect("Failed to create type-c service task"), + ); - static STATE: OnceLock = OnceLock::new(); - let state = STATE.get_or_init(mock_controller::ControllerState::new); + spawner.spawn(port_task(event_receiver, port).expect("Failed to create controller task")); - info!("Starting controller task"); - spawner.spawn(controller_task(state).unwrap()); - // Wait for controller to be registered - Timer::after_secs(1).await; + spawner.spawn( + interrupt_splitter_task( + state.create_interrupt_receiver(), + PortEventSplitter::new([port_interrupt_sender]), + ) + .expect("Failed to create interrupt splitter task"), + ); + Timer::after_millis(1000).await; info!("Simulating connection"); - state.connect_sink(Current::UsbDefault.into(), false).await; + state + .connect_sink(power_capability_from_current(Current::UsbDefault), false) + .await; Timer::after_millis(DELAY_MS).await; info!("Simulating PD alert"); @@ -149,16 +203,20 @@ async fn task(spawner: Spawner) { } #[embassy_executor::task] -async fn type_c_service_task() -> ! { - type_c_service::task(Default::default()).await; - unreachable!() +async fn power_policy_psu_task( + psu_events: PsuEventReceivers<'static, 1, PortType, DynamicReceiver<'static, psu::event::EventData>>, + power_policy: &'static PowerPolicyServiceType, +) { + power_policy_service::service::task::psu_task(psu_events, power_policy).await; } #[embassy_executor::task] -async fn power_policy_service_task() { - power_policy_service::task::task(Default::default()) - .await - .expect("Failed to start power policy service task"); +async fn type_c_service_task( + service: &'static Mutex, + event_receiver: TypeCServiceEventReceiverType, +) { + info!("Starting type-c task"); + type_c_service::task::task(service, event_receiver).await; } fn main() { @@ -166,9 +224,8 @@ fn main() { static EXECUTOR: StaticCell = StaticCell::new(); let executor = EXECUTOR.init(Executor::new()); + executor.run(|spawner| { - spawner.spawn(power_policy_service_task().unwrap()); - spawner.spawn(type_c_service_task().unwrap()); - spawner.spawn(task(spawner).unwrap()); + spawner.spawn(task(spawner).expect("Failed to create task")); }); } diff --git a/examples/std/src/bin/type_c/ucsi.rs b/examples/std/src/bin/type_c/ucsi.rs index 251d78797..3797ce498 100644 --- a/examples/std/src/bin/type_c/ucsi.rs +++ b/examples/std/src/bin/type_c/ucsi.rs @@ -1,79 +1,94 @@ +#![allow(unused_imports)] +use cfu_service::CfuClient; use embassy_executor::{Executor, Spawner}; +use embassy_sync::channel::{Channel, DynamicReceiver, DynamicSender}; use embassy_sync::mutex::Mutex; -use embedded_services::GlobalRawMutex; -use embedded_services::power::policy::{self, PowerCapability}; -use embedded_services::type_c::ControllerId; -use embedded_services::type_c::external::{UcsiResponseResult, execute_ucsi_command}; -use embedded_usb_pd::GlobalPortId; +use embassy_sync::once_lock::OnceLock; +use embassy_sync::pubsub::{DynImmediatePublisher, DynSubscriber, PubSubChannel}; +use embedded_services::IntrusiveList; +use embedded_services::event::{MapSender, NoopSender}; +use embedded_services::{GlobalRawMutex, event}; use embedded_usb_pd::ucsi::lpm::get_connector_capability::OperationModeFlags; use embedded_usb_pd::ucsi::ppm::ack_cc_ci::Ack; use embedded_usb_pd::ucsi::ppm::get_capability::ResponseData as UcsiCapabilities; use embedded_usb_pd::ucsi::ppm::set_notification_enable::NotificationEnable; use embedded_usb_pd::ucsi::{Command, lpm, ppm}; +use embedded_usb_pd::{GlobalPortId, LocalPortId}; use log::*; +use power_policy_interface::capability::PowerCapability; +use power_policy_interface::charger::mock::ChargerType; +use power_policy_interface::psu; +use power_policy_service::psu::PsuEventReceivers; +use power_policy_service::service::registration::ArrayRegistration; use static_cell::StaticCell; -use std_examples::type_c::mock_controller; +use std_examples::type_c::mock_controller::{self, InterruptReceiver, Port}; +use type_c_interface::controller::ControllerId; +use type_c_interface::port::event::PortEventBitfield; +use type_c_interface::service::event::{PortEvent as ServicePortEvent, PortEventData as ServicePortEventData}; +use type_c_service::controller::event::Event as PortEvent; +use type_c_service::controller::event_receiver::InterruptReceiver as _; +use type_c_service::controller::event_receiver::{EventReceiver as PortEventReceiver, PortEventSplitter}; +use type_c_service::controller::macros::PortComponents; +use type_c_service::controller::state::SharedState; +use type_c_service::define_controller_port_static_cell_channel; +use type_c_service::service::Service; use type_c_service::service::config::Config; -use type_c_service::wrapper::backing::{ReferencedStorage, Storage}; +use type_c_service::service::registration::PortData; -const CONTROLLER0_ID: ControllerId = ControllerId(0); -const CONTROLLER1_ID: ControllerId = ControllerId(1); -const PORT0_ID: GlobalPortId = GlobalPortId(0); -const POWER0_ID: policy::DeviceId = policy::DeviceId(0); -const PORT1_ID: GlobalPortId = GlobalPortId(1); -const POWER1_ID: policy::DeviceId = policy::DeviceId(1); -const CFU0_ID: u8 = 0x00; -const CFU1_ID: u8 = 0x01; +type ControllerType = Mutex>; +type PortType = Mutex>; -#[embassy_executor::task] -async fn opm_task(spawner: Spawner) { - static STORAGE0: StaticCell> = StaticCell::new(); - let storage0 = STORAGE0.init(Storage::new(CONTROLLER0_ID, CFU0_ID, [(PORT0_ID, POWER0_ID)])); - static REFERENCED0: StaticCell> = StaticCell::new(); - let referenced0 = REFERENCED0.init( - storage0 - .create_referenced() - .expect("Failed to create referenced storage"), - ); +type PowerPolicySenderType = MapSender< + power_policy_interface::service::event::Event<'static, PortType>, + power_policy_interface::service::event::EventData, + DynImmediatePublisher<'static, power_policy_interface::service::event::EventData>, + fn( + power_policy_interface::service::event::Event<'static, PortType>, + ) -> power_policy_interface::service::event::EventData, +>; - static STATE0: StaticCell = StaticCell::new(); - let state0 = STATE0.init(mock_controller::ControllerState::new()); - static CONTROLLER0: StaticCell> = StaticCell::new(); - let controller0 = CONTROLLER0.init(Mutex::new(mock_controller::Controller::new(state0))); - static WRAPPER0: StaticCell = StaticCell::new(); - let wrapper0 = WRAPPER0.init( - mock_controller::Wrapper::try_new(controller0, Default::default(), referenced0, mock_controller::Validator) - .expect("Failed to create wrapper"), - ); - spawner.spawn(wrapper_task(wrapper0).unwrap()); - - static STORAGE1: StaticCell> = StaticCell::new(); - let storage1 = STORAGE1.init(Storage::new(CONTROLLER1_ID, CFU1_ID, [(PORT1_ID, POWER1_ID)])); - static REFERENCED1: StaticCell> = StaticCell::new(); - let referenced1 = REFERENCED1.init( - storage1 - .create_referenced() - .expect("Failed to create referenced storage"), - ); +type PowerPolicyReceiverType = DynSubscriber<'static, power_policy_interface::service::event::EventData>; - static STATE1: StaticCell = StaticCell::new(); - let state1 = STATE1.init(mock_controller::ControllerState::new()); - static CONTROLLER1: StaticCell> = StaticCell::new(); - let controller1 = CONTROLLER1.init(Mutex::new(mock_controller::Controller::new(state1))); - static WRAPPER1: StaticCell = StaticCell::new(); - let wrapper1 = WRAPPER1.init( - mock_controller::Wrapper::try_new(controller1, Default::default(), referenced1, mock_controller::Validator) - .expect("Failed to create wrapper"), - ); - spawner.spawn(wrapper_task(wrapper1).unwrap()); +type PowerPolicyServiceType = Mutex< + GlobalRawMutex, + power_policy_service::service::Service< + 'static, + ArrayRegistration<'static, PortType, 2, PowerPolicySenderType, 1, ChargerType, 0>, + >, +>; + +const PORT_COUNT: usize = 2; +type TypeCServiceSenderType = NoopSender; +type PortReceiverType = DynamicReceiver<'static, type_c_interface::service::event::PortEventData>; +type TypeCServiceEventReceiverType = type_c_service::service::event_receiver::ArrayEventReceiver< + 'static, + PORT_COUNT, + PortType, + PortReceiverType, + PowerPolicyReceiverType, +>; +type TypeCRegistrationType = + type_c_service::service::registration::ArrayRegistration<'static, PortType, PORT_COUNT, TypeCServiceSenderType, 1>; +type TypeCServiceType = Service<'static, TypeCRegistrationType>; +type SharedStateType = Mutex; +type PortEventReceiverType = PortEventReceiver< + 'static, + SharedStateType, + DynamicReceiver<'static, PortEventBitfield>, + DynamicReceiver<'static, type_c_service::controller::event::Loopback>, +>; - const CAPABILITY: PowerCapability = PowerCapability { +#[embassy_executor::task] +async fn opm_task(_state: [&'static mock_controller::ControllerState; PORT_COUNT]) { + // TODO: migrate this logic to an integration test when we move away from 'static lifetimes. + /*const CAPABILITY: PowerCapability = PowerCapability { voltage_mv: 20000, current_ma: 5000, }; info!("Resetting PPM..."); - let response: UcsiResponseResult = execute_ucsi_command(Command::PpmCommand(ppm::Command::PpmReset)) + let response: UcsiResponseResult = context + .execute_ucsi_command_external(Command::PpmCommand(ppm::Command::PpmReset)) .await .into(); let response = response.unwrap(); @@ -87,13 +102,14 @@ async fn opm_task(spawner: Spawner) { let mut notifications = NotificationEnable::default(); notifications.set_cmd_complete(true); notifications.set_connect_change(true); - let response: UcsiResponseResult = execute_ucsi_command(Command::PpmCommand(ppm::Command::SetNotificationEnable( - ppm::set_notification_enable::Args { - notification_enable: notifications, - }, - ))) - .await - .into(); + let response: UcsiResponseResult = context + .execute_ucsi_command_external(Command::PpmCommand(ppm::Command::SetNotificationEnable( + ppm::set_notification_enable::Args { + notification_enable: notifications, + }, + ))) + .await + .into(); let response = response.unwrap(); if !response.cci.cmd_complete() || response.cci.error() { error!("Set Notification enable failed: {:?}", response.cci); @@ -102,8 +118,8 @@ async fn opm_task(spawner: Spawner) { } info!("Sending command complete ack..."); - let response: UcsiResponseResult = - execute_ucsi_command(Command::PpmCommand(ppm::Command::AckCcCi(ppm::ack_cc_ci::Args { + let response: UcsiResponseResult = context + .execute_ucsi_command_external(Command::PpmCommand(ppm::Command::AckCcCi(ppm::ack_cc_ci::Args { ack: *Ack::default().set_command_complete(true), }))) .await @@ -115,20 +131,22 @@ async fn opm_task(spawner: Spawner) { info!("Sending command complete ack successful"); } - info!("Connecting sinks on both ports"); - state0.connect_sink(CAPABILITY, false).await; - state1.connect_sink(CAPABILITY, false).await; + info!("Connecting sink on port 0"); + state[0].connect_sink(CAPABILITY, false).await; + info!("Connecting sink on port 1"); + state[1].connect_sink(CAPABILITY, false).await; // Ensure connect flow has time to complete embassy_time::Timer::after_millis(1000).await; info!("Port 0: Get connector status..."); - let response: UcsiResponseResult = execute_ucsi_command(Command::LpmCommand(lpm::GlobalCommand::new( - GlobalPortId(0), - lpm::CommandData::GetConnectorStatus, - ))) - .await - .into(); + let response: UcsiResponseResult = context + .execute_ucsi_command_external(Command::LpmCommand(lpm::GlobalCommand::new( + GlobalPortId(0), + lpm::CommandData::GetConnectorStatus, + ))) + .await + .into(); let response = response.unwrap(); if !response.cci.cmd_complete() || response.cci.error() { error!("Get connector status failed: {:?}", response.cci); @@ -140,8 +158,8 @@ async fn opm_task(spawner: Spawner) { } info!("Sending command complete ack..."); - let response: UcsiResponseResult = - execute_ucsi_command(Command::PpmCommand(ppm::Command::AckCcCi(ppm::ack_cc_ci::Args { + let response: UcsiResponseResult = context + .execute_ucsi_command_external(Command::PpmCommand(ppm::Command::AckCcCi(ppm::ack_cc_ci::Args { ack: *Ack::default().set_command_complete(true).set_connector_change(true), }))) .await @@ -157,12 +175,13 @@ async fn opm_task(spawner: Spawner) { } info!("Port 1: Get connector status..."); - let response: UcsiResponseResult = execute_ucsi_command(Command::LpmCommand(lpm::GlobalCommand::new( - GlobalPortId(1), - lpm::CommandData::GetConnectorStatus, - ))) - .await - .into(); + let response: UcsiResponseResult = context + .execute_ucsi_command_external(Command::LpmCommand(lpm::GlobalCommand::new( + GlobalPortId(1), + lpm::CommandData::GetConnectorStatus, + ))) + .await + .into(); let response = response.unwrap(); if !response.cci.cmd_complete() || response.cci.error() { error!("Get connector status failed: {:?}", response.cci); @@ -174,8 +193,8 @@ async fn opm_task(spawner: Spawner) { } info!("Sending command complete ack..."); - let response: UcsiResponseResult = - execute_ucsi_command(Command::PpmCommand(ppm::Command::AckCcCi(ppm::ack_cc_ci::Args { + let response: UcsiResponseResult = context + .execute_ucsi_command_external(Command::PpmCommand(ppm::Command::AckCcCi(ppm::ack_cc_ci::Args { ack: *Ack::default().set_command_complete(true).set_connector_change(true), }))) .await @@ -188,55 +207,45 @@ async fn opm_task(spawner: Spawner) { "Sending command complete ack successful, connector change: {:?}", response.cci.connector_change() ); - } + }*/ } #[embassy_executor::task(pool_size = 2)] -async fn wrapper_task(wrapper: &'static mock_controller::Wrapper<'static>) { - wrapper.register().await.unwrap(); - +async fn port_task(mut event_receiver: PortEventReceiverType, port: &'static PortType) { loop { - if let Err(e) = wrapper.process_next_event().await { - error!("Error processing wrapper: {e:#?}"); + let event = event_receiver.wait_event().await; + let output = port.lock().await.process_event(event).await; + if let Err(e) = output { + error!("Error processing event: {e:?}"); } } } +#[embassy_executor::task(pool_size = 2)] +async fn interrupt_splitter_task( + mut interrupt_receiver: InterruptReceiver<'static>, + mut interrupt_splitter: PortEventSplitter<1, DynamicSender<'static, PortEventBitfield>>, +) -> ! { + loop { + let interrupts = interrupt_receiver.wait_interrupt().await; + interrupt_splitter.process_interrupts(interrupts).await; + } +} + #[embassy_executor::task] -async fn type_c_service_task() -> ! { - type_c_service::task(Config { - ucsi_capabilities: UcsiCapabilities { - num_connectors: 2, - bcd_usb_pd_spec: 0x0300, - bcd_type_c_spec: 0x0200, - bcd_battery_charging_spec: 0x0120, - ..Default::default() - }, - ucsi_port_capabilities: Some( - *lpm::get_connector_capability::ResponseData::default() - .set_operation_mode( - *OperationModeFlags::default() - .set_drp(true) - .set_usb2(true) - .set_usb3(true), - ) - .set_consumer(true) - .set_provider(true) - .set_swap_to_dfp(true) - .set_swap_to_snk(true) - .set_swap_to_src(true), - ), - ..Default::default() - }) - .await; - unreachable!() +async fn power_policy_task( + psu_events: PsuEventReceivers<'static, 2, PortType, DynamicReceiver<'static, psu::event::EventData>>, + power_policy: &'static PowerPolicyServiceType, +) { + power_policy_service::service::task::psu_task(psu_events, power_policy).await; } #[embassy_executor::task] -async fn power_policy_service_task() { - power_policy_service::task::task(Default::default()) - .await - .expect("Failed to start power policy service task"); +async fn type_c_service_task( + service: &'static Mutex, + event_receiver: TypeCServiceEventReceiverType, +) { + type_c_service::task::task(service, event_receiver).await; } #[embassy_executor::task] @@ -245,9 +254,135 @@ async fn task(spawner: Spawner) { embedded_services::init().await; - spawner.spawn(power_policy_service_task().unwrap()); - spawner.spawn(type_c_service_task().unwrap()); - spawner.spawn(opm_task(spawner).unwrap()); + static STATE0: StaticCell = StaticCell::new(); + let state0 = STATE0.init(mock_controller::ControllerState::new()); + static CONTROLLER0: StaticCell = StaticCell::new(); + let controller0 = CONTROLLER0.init(Mutex::new(mock_controller::Controller::new(state0, "Controller0"))); + + define_controller_port_static_cell_channel!(pub(self), port0, GlobalRawMutex, Mutex>); + let PortComponents { + port: port0, + power_policy_receiver: policy_receiver0, + event_receiver: event_receiver0, + interrupt_sender: port0_interrupt_sender, + type_c_receiver: type_c_receiver0, + } = port0::create("PD0", LocalPortId(0), Default::default(), controller0); + + static STATE1: StaticCell = StaticCell::new(); + let state1 = STATE1.init(mock_controller::ControllerState::new()); + static CONTROLLER1: StaticCell = StaticCell::new(); + let controller1 = CONTROLLER1.init(Mutex::new(mock_controller::Controller::new(state1, "Controller1"))); + + define_controller_port_static_cell_channel!(pub(self), port1, GlobalRawMutex, Mutex>); + let PortComponents { + port: port1, + power_policy_receiver: policy_receiver1, + event_receiver: event_receiver1, + interrupt_sender: port1_interrupt_sender, + type_c_receiver: type_c_receiver1, + } = port1::create("PD1", LocalPortId(0), Default::default(), controller1); + + // Create power policy service + // The service is the only receiver and we only use a DynImmediatePublisher, which doesn't take a publisher slot + static POWER_POLICY_CHANNEL: StaticCell< + PubSubChannel, + > = StaticCell::new(); + + let power_policy_channel = POWER_POLICY_CHANNEL.init(PubSubChannel::new()); + let power_policy_sender: PowerPolicySenderType = + MapSender::new(power_policy_channel.dyn_immediate_publisher(), |e| e.into()); + // Guaranteed to not panic since we initialized the channel above + let power_policy_subscriber = power_policy_channel.dyn_subscriber().unwrap(); + + let power_policy_registration = ArrayRegistration { + psus: [port0, port1], + service_senders: [power_policy_sender], + chargers: [], + }; + + static POWER_SERVICE: StaticCell = StaticCell::new(); + let power_service = POWER_SERVICE.init(Mutex::new(power_policy_service::service::Service::new( + power_policy_registration, + power_policy_service::service::config::Config::default(), + ))); + + // Create type-c service + static TYPE_C_SERVICE: StaticCell> = StaticCell::new(); + let type_c_service = TYPE_C_SERVICE.init(Mutex::new(Service::create( + Config { + ucsi_capabilities: UcsiCapabilities { + num_connectors: 2, + bcd_usb_pd_spec: 0x0300, + bcd_type_c_spec: 0x0200, + bcd_battery_charging_spec: 0x0120, + ..Default::default() + }, + ucsi_port_capabilities: Some( + *lpm::get_connector_capability::ResponseData::default() + .set_operation_mode( + *OperationModeFlags::default() + .set_drp(true) + .set_usb2(true) + .set_usb3(true), + ) + .set_consumer(true) + .set_provider(true) + .set_swap_to_dfp(true) + .set_swap_to_snk(true) + .set_swap_to_src(true), + ), + ..Default::default() + }, + TypeCRegistrationType { + ports: [port0, port1], + service_senders: [NoopSender], + port_data: [ + PortData { + local_port: Some(LocalPortId(0)), + }, + PortData { + local_port: Some(LocalPortId(1)), + }, + ], + }, + ))); + + spawner.spawn( + power_policy_task( + PsuEventReceivers::new([port0, port1], [policy_receiver0, policy_receiver1]), + power_service, + ) + .expect("Failed to create power policy task"), + ); + + spawner.spawn( + type_c_service_task( + type_c_service, + TypeCServiceEventReceiverType::new( + [port0, port1], + [type_c_receiver0, type_c_receiver1], + power_policy_subscriber, + ), + ) + .expect("Failed to create type-c service task"), + ); + spawner.spawn(port_task(event_receiver0, port0).expect("Failed to create wrapper0 task")); + spawner.spawn( + interrupt_splitter_task( + state0.create_interrupt_receiver(), + PortEventSplitter::new([port0_interrupt_sender]), + ) + .expect("Failed to create interrupt splitter 0 task"), + ); + spawner.spawn(port_task(event_receiver1, port1).expect("Failed to create wrapper1 task")); + spawner.spawn( + interrupt_splitter_task( + state1.create_interrupt_receiver(), + PortEventSplitter::new([port1_interrupt_sender]), + ) + .expect("Failed to create interrupt splitter 1 task"), + ); + spawner.spawn(opm_task([state0, state1]).expect("Failed to create opm task")); } fn main() { @@ -255,7 +390,8 @@ fn main() { static EXECUTOR: StaticCell = StaticCell::new(); let executor = EXECUTOR.init(Executor::new()); + executor.run(|spawner| { - spawner.spawn(task(spawner).unwrap()); + spawner.spawn(task(spawner).expect("Failed to create task")); }); } diff --git a/examples/std/src/bin/type_c/unconstrained.rs b/examples/std/src/bin/type_c/unconstrained.rs index bced98129..039690745 100644 --- a/examples/std/src/bin/type_c/unconstrained.rs +++ b/examples/std/src/bin/type_c/unconstrained.rs @@ -1,126 +1,234 @@ use embassy_executor::{Executor, Spawner}; +use embassy_sync::channel::DynamicReceiver; +use embassy_sync::channel::DynamicSender; use embassy_sync::mutex::Mutex; +use embassy_sync::pubsub::{DynImmediatePublisher, DynSubscriber, PubSubChannel}; use embassy_time::Timer; use embedded_services::GlobalRawMutex; -use embedded_services::power::policy::PowerCapability; -use embedded_services::power::{self}; -use embedded_services::type_c::{ControllerId, controller}; -use embedded_usb_pd::GlobalPortId; +use embedded_services::event::MapSender; +use embedded_services::event::NoopSender; +use embedded_usb_pd::LocalPortId; use log::*; +use power_policy_interface::capability::PowerCapability; +use power_policy_interface::charger::mock::ChargerType; +use power_policy_interface::psu; +use power_policy_service::psu::PsuEventReceivers; +use power_policy_service::service::registration::ArrayRegistration; use static_cell::StaticCell; -use std_examples::type_c::mock_controller; -use type_c_service::wrapper::backing::{ReferencedStorage, Storage}; +use std_examples::type_c::mock_controller::Port; +use std_examples::type_c::mock_controller::{self, InterruptReceiver}; +use type_c_interface::port::event::PortEventBitfield; +use type_c_service::controller::event_receiver::InterruptReceiver as _; +use type_c_service::controller::event_receiver::{EventReceiver as PortEventReceiver, PortEventSplitter}; +use type_c_service::controller::macros::PortComponents; +use type_c_service::controller::state::SharedState; +use type_c_service::define_controller_port_static_cell_channel; +use type_c_service::service::Service; +use type_c_service::service::registration::PortData; -const CONTROLLER0_ID: ControllerId = ControllerId(0); -const PORT0_ID: GlobalPortId = GlobalPortId(0); -const POWER0_ID: power::policy::DeviceId = power::policy::DeviceId(0); -const CFU0_ID: u8 = 0x00; +const DELAY_MS: u64 = 1000; -const CONTROLLER1_ID: ControllerId = ControllerId(1); -const PORT1_ID: GlobalPortId = GlobalPortId(1); -const POWER1_ID: power::policy::DeviceId = power::policy::DeviceId(1); -const CFU1_ID: u8 = 0x01; +type ControllerType = Mutex>; +type PortType = Mutex>; -const CONTROLLER2_ID: ControllerId = ControllerId(2); -const PORT2_ID: GlobalPortId = GlobalPortId(2); -const POWER2_ID: power::policy::DeviceId = power::policy::DeviceId(2); -const CFU2_ID: u8 = 0x02; +type PowerPolicySenderType = MapSender< + power_policy_interface::service::event::Event<'static, PortType>, + power_policy_interface::service::event::EventData, + DynImmediatePublisher<'static, power_policy_interface::service::event::EventData>, + fn( + power_policy_interface::service::event::Event<'static, PortType>, + ) -> power_policy_interface::service::event::EventData, +>; -const DELAY_MS: u64 = 1000; +type PowerPolicyReceiverType = DynSubscriber<'static, power_policy_interface::service::event::EventData>; -#[embassy_executor::task(pool_size = 3)] -async fn controller_task(wrapper: &'static mock_controller::Wrapper<'static>) { - wrapper.register().await.unwrap(); +type PowerPolicyServiceType = Mutex< + GlobalRawMutex, + power_policy_service::service::Service< + 'static, + ArrayRegistration<'static, PortType, 3, PowerPolicySenderType, 1, ChargerType, 0>, + >, +>; +const PORT_COUNT: usize = 3; +type TypeCServiceSenderType = NoopSender; +type PortReceiverType = DynamicReceiver<'static, type_c_interface::service::event::PortEventData>; +type TypeCServiceEventReceiverType = type_c_service::service::event_receiver::ArrayEventReceiver< + 'static, + PORT_COUNT, + PortType, + PortReceiverType, + PowerPolicyReceiverType, +>; +type TypeCRegistrationType = + type_c_service::service::registration::ArrayRegistration<'static, PortType, PORT_COUNT, TypeCServiceSenderType, 1>; +type TypeCServiceType = Service<'static, TypeCRegistrationType>; +type SharedStateType = Mutex; +type PortEventReceiverType = PortEventReceiver< + 'static, + SharedStateType, + DynamicReceiver<'static, PortEventBitfield>, + DynamicReceiver<'static, type_c_service::controller::event::Loopback>, +>; + +#[embassy_executor::task(pool_size = 3)] +async fn port_task(mut event_receiver: PortEventReceiverType, port: &'static PortType) { loop { - if let Err(e) = wrapper.process_next_event().await { - error!("Error processing wrapper: {e:#?}"); + let event = event_receiver.wait_event().await; + let output = port.lock().await.process_event(event).await; + if let Err(e) = output { + error!("Error processing event: {e:?}"); } } } +#[embassy_executor::task(pool_size = 3)] +async fn interrupt_splitter_task( + mut interrupt_receiver: InterruptReceiver<'static>, + mut interrupt_splitter: PortEventSplitter<1, DynamicSender<'static, PortEventBitfield>>, +) -> ! { + loop { + let interrupts = interrupt_receiver.wait_interrupt().await; + interrupt_splitter.process_interrupts(interrupts).await; + } +} + #[embassy_executor::task] async fn task(spawner: Spawner) { embedded_services::init().await; - controller::init(); - - static STORAGE: StaticCell> = StaticCell::new(); - let storage = STORAGE.init(Storage::new(CONTROLLER0_ID, CFU0_ID, [(PORT0_ID, POWER0_ID)])); - static REFERENCED: StaticCell> = StaticCell::new(); - let referenced = REFERENCED.init( - storage - .create_referenced() - .expect("Failed to create referenced storage"), - ); - static STATE0: StaticCell = StaticCell::new(); let state0 = STATE0.init(mock_controller::ControllerState::new()); - static CONTROLLER0: StaticCell> = StaticCell::new(); - let controller0 = CONTROLLER0.init(Mutex::new(mock_controller::Controller::new(state0))); - static WRAPPER0: StaticCell = StaticCell::new(); - let wrapper0 = WRAPPER0.init( - mock_controller::Wrapper::try_new( - controller0, - Default::default(), - referenced, - crate::mock_controller::Validator, - ) - .expect("Failed to create wrapper"), - ); + static CONTROLLER0: StaticCell = StaticCell::new(); + let controller0 = CONTROLLER0.init(Mutex::new(mock_controller::Controller::new(state0, "Controller0"))); - static STORAGE1: StaticCell> = StaticCell::new(); - let storage1 = STORAGE1.init(Storage::new(CONTROLLER1_ID, CFU1_ID, [(PORT1_ID, POWER1_ID)])); - static REFERENCED1: StaticCell> = StaticCell::new(); - let referenced1 = REFERENCED1.init( - storage1 - .create_referenced() - .expect("Failed to create referenced storage"), - ); + define_controller_port_static_cell_channel!(pub(self), port0, GlobalRawMutex, Mutex>); + let PortComponents { + port: port0, + power_policy_receiver: policy_receiver0, + event_receiver: event_receiver0, + interrupt_sender: port0_interrupt_sender, + type_c_receiver: type_c_receiver0, + } = port0::create("PD0", LocalPortId(0), Default::default(), controller0); static STATE1: StaticCell = StaticCell::new(); let state1 = STATE1.init(mock_controller::ControllerState::new()); - static CONTROLLER1: StaticCell> = StaticCell::new(); - let controller1 = CONTROLLER1.init(Mutex::new(mock_controller::Controller::new(state1))); - static WRAPPER1: StaticCell = StaticCell::new(); - let wrapper1 = WRAPPER1.init( - mock_controller::Wrapper::try_new( - controller1, - Default::default(), - referenced1, - crate::mock_controller::Validator, - ) - .expect("Failed to create wrapper"), - ); + static CONTROLLER1: StaticCell = StaticCell::new(); + let controller1 = CONTROLLER1.init(Mutex::new(mock_controller::Controller::new(state1, "Controller1"))); - static STORAGE2: StaticCell> = StaticCell::new(); - let storage2 = STORAGE2.init(Storage::new(CONTROLLER2_ID, CFU2_ID, [(PORT2_ID, POWER2_ID)])); - static REFERENCED2: StaticCell> = StaticCell::new(); - let referenced2 = REFERENCED2.init( - storage2 - .create_referenced() - .expect("Failed to create referenced storage"), - ); + define_controller_port_static_cell_channel!(pub(self), port1, GlobalRawMutex, Mutex>); + let PortComponents { + port: port1, + power_policy_receiver: policy_receiver1, + event_receiver: event_receiver1, + interrupt_sender: port1_interrupt_sender, + type_c_receiver: type_c_receiver1, + } = port1::create("PD1", LocalPortId(0), Default::default(), controller1); static STATE2: StaticCell = StaticCell::new(); let state2 = STATE2.init(mock_controller::ControllerState::new()); - static CONTROLLER2: StaticCell> = StaticCell::new(); - let controller2 = CONTROLLER2.init(Mutex::new(mock_controller::Controller::new(state2))); - static WRAPPER2: StaticCell = StaticCell::new(); - let wrapper2 = WRAPPER2.init( - mock_controller::Wrapper::try_new( - controller2, - Default::default(), - referenced2, - crate::mock_controller::Validator, + static CONTROLLER2: StaticCell = StaticCell::new(); + let controller2 = CONTROLLER2.init(Mutex::new(mock_controller::Controller::new(state2, "Controller2"))); + + define_controller_port_static_cell_channel!(pub(self), port2, GlobalRawMutex, Mutex>); + let PortComponents { + port: port2, + power_policy_receiver: policy_receiver2, + event_receiver: event_receiver2, + interrupt_sender: port2_interrupt_sender, + type_c_receiver: type_c_receiver2, + } = port2::create("PD2", LocalPortId(0), Default::default(), controller2); + + // The service is the only receiver and we only use a DynImmediatePublisher, which doesn't take a publisher slot + static POWER_POLICY_CHANNEL: StaticCell< + PubSubChannel, + > = StaticCell::new(); + + let power_policy_channel = POWER_POLICY_CHANNEL.init(PubSubChannel::new()); + let power_policy_sender: PowerPolicySenderType = + MapSender::new(power_policy_channel.dyn_immediate_publisher(), |e| e.into()); + // Guaranteed to not panic since we initialized the channel above + let power_policy_subscriber = power_policy_channel.dyn_subscriber().unwrap(); + + let power_policy_registration = ArrayRegistration { + psus: [port0, port1, port2], + service_senders: [power_policy_sender], + chargers: [], + }; + + static POWER_SERVICE: StaticCell = StaticCell::new(); + let power_service = POWER_SERVICE.init(Mutex::new(power_policy_service::service::Service::new( + power_policy_registration, + power_policy_service::service::config::Config::default(), + ))); + + // Create type-c service + static TYPE_C_SERVICE: StaticCell> = StaticCell::new(); + let type_c_service = TYPE_C_SERVICE.init(Mutex::new(Service::create( + Default::default(), + TypeCRegistrationType { + ports: [port0, port1, port2], + service_senders: [NoopSender], + port_data: [ + PortData { + local_port: Some(LocalPortId(0)), + }, + PortData { + local_port: Some(LocalPortId(1)), + }, + PortData { + local_port: Some(LocalPortId(2)), + }, + ], + }, + ))); + + spawner.spawn( + power_policy_task( + PsuEventReceivers::new( + [port0, port1, port2], + [policy_receiver0, policy_receiver1, policy_receiver2], + ), + power_service, ) - .expect("Failed to create wrapper"), + .expect("Failed to create power policy task"), + ); + spawner.spawn( + type_c_service_task( + type_c_service, + TypeCServiceEventReceiverType::new( + [port0, port1, port2], + [type_c_receiver0, type_c_receiver1, type_c_receiver2], + power_policy_subscriber, + ), + ) + .expect("Failed to create type-c service task"), ); - info!("Starting controller tasks"); - spawner.spawn(controller_task(wrapper0).unwrap()); - spawner.spawn(controller_task(wrapper1).unwrap()); - spawner.spawn(controller_task(wrapper2).unwrap()); + spawner.spawn(port_task(event_receiver0, port0).expect("Failed to create controller0 task")); + spawner.spawn( + interrupt_splitter_task( + state0.create_interrupt_receiver(), + PortEventSplitter::new([port0_interrupt_sender]), + ) + .expect("Failed to create interrupt splitter 0 task"), + ); + spawner.spawn(port_task(event_receiver1, port1).expect("Failed to create controller1 task")); + spawner.spawn( + interrupt_splitter_task( + state1.create_interrupt_receiver(), + PortEventSplitter::new([port1_interrupt_sender]), + ) + .expect("Failed to create interrupt splitter 1 task"), + ); + spawner.spawn(port_task(event_receiver2, port2).expect("Failed to create controller2 task")); + spawner.spawn( + interrupt_splitter_task( + state2.create_interrupt_receiver(), + PortEventSplitter::new([port2_interrupt_sender]), + ) + .expect("Failed to create interrupt splitter 2 task"), + ); const CAPABILITY: PowerCapability = PowerCapability { voltage_mv: 20000, @@ -172,16 +280,20 @@ async fn task(spawner: Spawner) { } #[embassy_executor::task] -async fn type_c_service_task() -> ! { - type_c_service::task(Default::default()).await; - unreachable!() +async fn power_policy_task( + psu_events: PsuEventReceivers<'static, 3, PortType, DynamicReceiver<'static, psu::event::EventData>>, + power_policy: &'static PowerPolicyServiceType, +) { + power_policy_service::service::task::psu_task(psu_events, power_policy).await; } #[embassy_executor::task] -async fn power_policy_service_task() { - power_policy_service::task::task(Default::default()) - .await - .expect("Failed to start power policy service task"); +async fn type_c_service_task( + service: &'static Mutex, + event_receiver: TypeCServiceEventReceiverType, +) { + info!("Starting type-c task"); + type_c_service::task::task(service, event_receiver).await; } fn main() { @@ -190,8 +302,6 @@ fn main() { static EXECUTOR: StaticCell = StaticCell::new(); let executor = EXECUTOR.init(Executor::new()); executor.run(|spawner| { - spawner.spawn(power_policy_service_task().unwrap()); - spawner.spawn(type_c_service_task().unwrap()); - spawner.spawn(task(spawner).unwrap()); + spawner.spawn(task(spawner).expect("Failed to create task")); }); } diff --git a/examples/std/src/lib/type_c/mock_controller.rs b/examples/std/src/lib/type_c/mock_controller.rs index 2c6764897..10454d907 100644 --- a/examples/std/src/lib/type_c/mock_controller.rs +++ b/examples/std/src/lib/type_c/mock_controller.rs @@ -1,27 +1,31 @@ -use core::num::NonZeroU8; -use embassy_sync::{mutex::Mutex, signal::Signal}; -use embedded_cfu_protocol::protocol_definitions::{FwUpdateOfferResponse, HostToken}; -use embedded_services::{ - GlobalRawMutex, - power::policy::PowerCapability, - type_c::{ - controller::{ - AttnVdm, ControllerStatus, DiscoveredSvids, DpConfig, DpPinConfig, DpStatus, OtherVdm, - PdStateMachineConfig, PortStatus, RetimerFwUpdateState, SendVdm, SystemPowerState, TbtConfig, - TypeCStateMachineState, UsbControlConfig, - }, - event::PortEvent, - }, -}; -use embedded_usb_pd::{Error, ado::Ado}; +use std::num::NonZeroU8; + +use embassy_sync::{channel, mutex::Mutex, signal::Signal}; +use embedded_services::GlobalRawMutex; +use embedded_services::named::Named; +use embedded_usb_pd::ado::Ado; +use embedded_usb_pd::vdm::structured::command::discover_identity::{sop, sop_prime}; use embedded_usb_pd::{LocalPortId, PdError}; use embedded_usb_pd::{PowerRole, type_c::Current}; use embedded_usb_pd::{type_c::ConnectionState, ucsi::lpm}; -use log::{debug, info, trace}; -use std::cell::Cell; +use log::{debug, info}; + +use power_policy_interface::capability::PowerCapability; +use type_c_interface::control::dp::{DpConfig, DpPinConfig, DpStatus}; +use type_c_interface::control::pd::{PdStateMachineConfig, PortStatus}; +use type_c_interface::control::power::SystemPowerState; +use type_c_interface::control::retimer::RetimerFwUpdateState; +use type_c_interface::control::svid::DiscoveredSvids; +use type_c_interface::control::tbt::TbtConfig; +use type_c_interface::control::type_c::TypeCStateMachineState; +use type_c_interface::control::usb::UsbControlConfig; +use type_c_interface::control::vdm::{AttnVdm, OtherVdm, SendVdm}; +use type_c_interface::port::event::PortEventBitfield; +use type_c_service::controller::state::SharedState; +use type_c_service::util::power_capability_from_current; pub struct ControllerState { - events: Signal, + events: Signal, status: Mutex, pd_alert: Mutex>, } @@ -35,6 +39,10 @@ impl ControllerState { } } + pub fn create_interrupt_receiver(&self) -> InterruptReceiver<'_> { + InterruptReceiver { events: &self.events } + } + /// Simulate a connection pub async fn connect(&self, role: PowerRole, capability: PowerCapability, debug: bool, unconstrained: bool) { let mut status = PortStatus::new(); @@ -43,22 +51,24 @@ impl ControllerState { } else { ConnectionState::Attached }); + + let mut events = PortEventBitfield::none(); match role { PowerRole::Source => { status.available_source_contract = Some(capability); status.unconstrained_power = unconstrained; + events.status.set_new_power_contract_as_provider(true); } PowerRole::Sink => { status.available_sink_contract = Some(capability); status.unconstrained_power = unconstrained; + events.status.set_new_power_contract_as_consumer(true); + events.status.set_sink_ready(true); } } *self.status.lock().await = status; - let mut events = PortEvent::none(); events.status.set_plug_inserted_or_removed(true); - events.status.set_new_power_contract_as_consumer(true); - events.status.set_sink_ready(true); self.events.signal(events); } @@ -71,21 +81,22 @@ impl ControllerState { pub async fn disconnect(&self) { *self.status.lock().await = PortStatus::default(); - let mut events = PortEvent::none(); + let mut events = PortEventBitfield::none(); events.status.set_plug_inserted_or_removed(true); self.events.signal(events); } /// Simulate a debug accessory source connecting pub async fn connect_debug_accessory_source(&self, current: Current) { - self.connect(PowerRole::Source, current.into(), true, false).await; + self.connect(PowerRole::Source, power_capability_from_current(current), true, false) + .await; } /// Simulate a PD alert pub async fn send_pd_alert(&self, ado: Ado) { *self.pd_alert.lock().await = Some(ado); - let mut events = PortEvent::none(); + let mut events = PortEventBitfield::none(); events.notification.set_alert(true); self.events.signal(events); } @@ -99,89 +110,58 @@ impl Default for ControllerState { pub struct Controller<'a> { state: &'a ControllerState, - events: Cell, + name: &'static str, } impl<'a> Controller<'a> { - pub fn new(state: &'a ControllerState) -> Self { - Self { - state, - events: Cell::new(PortEvent::none()), - } + pub fn new(state: &'a ControllerState, name: &'static str) -> Self { + Self { state, name } } /// Function to demonstrate calling functions directly on the controller - pub fn custom_function(&self) { + pub fn custom_function(&mut self) { info!("Custom function called on controller"); } } -impl embedded_services::type_c::controller::Controller for Controller<'_> { - type BusError = (); - - async fn wait_port_event(&mut self) -> Result<(), Error> { - let events = self.state.events.wait().await; - trace!("Port event: {events:#?}"); - self.events.set(events); - Ok(()) - } - - async fn clear_port_events(&mut self, _port: LocalPortId) -> Result> { - let events = self.events.get(); - debug!("Clear port events: {events:#?}"); - self.events.set(PortEvent::none()); - Ok(events) - } - - async fn get_port_status(&mut self, _port: LocalPortId) -> Result> { - debug!("Get port status: {:#?}", *self.state.status.lock().await); - Ok(*self.state.status.lock().await) - } +pub struct InterruptReceiver<'a> { + events: &'a Signal, +} - async fn enable_sink_path(&mut self, _port: LocalPortId, enable: bool) -> Result<(), Error> { - debug!("Enable sink path: {enable}"); - Ok(()) +impl type_c_service::controller::event_receiver::InterruptReceiver for InterruptReceiver<'_> { + async fn wait_interrupt(&mut self) -> [PortEventBitfield; N] { + let events = self.events.wait().await; + let mut result = [PortEventBitfield::none(); N]; + result[0] = events; + result } +} - async fn get_controller_status(&mut self) -> Result, Error> { - debug!("Get controller status"); - Ok(ControllerStatus { - mode: "Test", - valid_fw_bank: true, - fw_version0: 0xbadf00d, - fw_version1: 0xdeadbeef, - }) +impl Named for Controller<'_> { + fn name(&self) -> &'static str { + self.name } +} - async fn reset_controller(&mut self) -> Result<(), Error> { +impl type_c_interface::controller::Controller for Controller<'_> { + async fn reset_controller(&mut self) -> Result<(), PdError> { debug!("Reset controller"); Ok(()) } +} - async fn get_rt_fw_update_status( - &mut self, - _port: LocalPortId, - ) -> Result> { - debug!("Get retimer fw update status"); - Ok(RetimerFwUpdateState::Inactive) - } - - async fn set_rt_fw_update_state(&mut self, _port: LocalPortId) -> Result<(), Error> { - debug!("Set retimer fw update state"); - Ok(()) - } - - async fn clear_rt_fw_update_state(&mut self, _port: LocalPortId) -> Result<(), Error> { - debug!("Clear retimer fw update state"); - Ok(()) +impl type_c_interface::controller::pd::Pd for Controller<'_> { + async fn get_port_status(&mut self, _port: LocalPortId) -> Result { + debug!("Get port status: {:#?}", *self.state.status.lock().await); + Ok(*self.state.status.lock().await) } - async fn set_rt_compliance(&mut self, _port: LocalPortId) -> Result<(), Error> { - debug!("Set retimer compliance"); + async fn enable_sink_path(&mut self, _port: LocalPortId, enable: bool) -> Result<(), PdError> { + debug!("Enable sink path: {enable}"); Ok(()) } - async fn get_pd_alert(&mut self, port: LocalPortId) -> Result, Error> { + async fn get_pd_alert(&mut self, port: LocalPortId) -> Result, PdError> { let pd_alert = self.state.pd_alert.lock().await; if let Some(ado) = *pd_alert { debug!("Port{}: Get PD alert: {ado:#?}", port.0); @@ -192,74 +172,32 @@ impl embedded_services::type_c::controller::Controller for Controller<'_> { } } - async fn set_unconstrained_power( - &mut self, - _port: LocalPortId, - unconstrained: bool, - ) -> Result<(), Error> { + async fn set_unconstrained_power(&mut self, _port: LocalPortId, unconstrained: bool) -> Result<(), PdError> { debug!("Set unconstrained power: {unconstrained}"); Ok(()) } - async fn get_active_fw_version(&mut self) -> Result> { - Ok(0) - } - - async fn start_fw_update(&mut self) -> Result<(), Error> { - Ok(()) - } - - async fn abort_fw_update(&mut self) -> Result<(), Error> { - Ok(()) - } - - async fn finalize_fw_update(&mut self) -> Result<(), Error> { - Ok(()) - } - - async fn write_fw_contents(&mut self, _offset: usize, _data: &[u8]) -> Result<(), Error> { - Ok(()) - } - - async fn set_max_sink_voltage( - &mut self, - port: LocalPortId, - voltage_mv: Option, - ) -> Result<(), Error> { - debug!("Set max sink voltage for port {}: {:?}", port.0, voltage_mv); - Ok(()) - } - - async fn reconfigure_retimer(&mut self, port: LocalPortId) -> Result<(), Error> { - debug!("reconfigure_retimer(port: {port:?})"); - Ok(()) - } - - async fn clear_dead_battery_flag(&mut self, port: LocalPortId) -> Result<(), Error> { + async fn clear_dead_battery_flag(&mut self, port: LocalPortId) -> Result<(), PdError> { debug!("clear_dead_battery_flag(port: {port:?})"); Ok(()) } - async fn get_other_vdm(&mut self, port: LocalPortId) -> Result> { + async fn get_other_vdm(&mut self, port: LocalPortId) -> Result { debug!("Get other VDM for port {port:?}"); Ok(OtherVdm::default()) } - async fn get_attn_vdm(&mut self, port: LocalPortId) -> Result> { + async fn get_attn_vdm(&mut self, port: LocalPortId) -> Result { debug!("Get attention VDM for port {port:?}"); Ok(AttnVdm::default()) } - async fn send_vdm(&mut self, port: LocalPortId, tx_vdm: SendVdm) -> Result<(), Error> { + async fn send_vdm(&mut self, port: LocalPortId, tx_vdm: SendVdm) -> Result<(), PdError> { debug!("Send VDM for port {port:?}: {tx_vdm:?}"); Ok(()) } - async fn set_usb_control( - &mut self, - port: LocalPortId, - config: UsbControlConfig, - ) -> Result<(), Error> { + async fn set_usb_control(&mut self, port: LocalPortId, config: UsbControlConfig) -> Result<(), PdError> { debug!( "set_usb_control(port: {port:?}, usb2: {}, usb3: {}, usb4: {})", config.usb2_enabled, config.usb3_enabled, config.usb4_enabled @@ -267,7 +205,7 @@ impl embedded_services::type_c::controller::Controller for Controller<'_> { Ok(()) } - async fn get_dp_status(&mut self, port: LocalPortId) -> Result> { + async fn get_dp_status(&mut self, port: LocalPortId) -> Result { debug!("Get DisplayPort status for port {port:?}"); Ok(DpStatus { alt_mode_entered: false, @@ -275,7 +213,7 @@ impl embedded_services::type_c::controller::Controller for Controller<'_> { }) } - async fn set_dp_config(&mut self, port: LocalPortId, config: DpConfig) -> Result<(), Error> { + async fn set_dp_config(&mut self, port: LocalPortId, config: DpConfig) -> Result<(), PdError> { debug!( "Set DisplayPort config for port {port:?}: enable={}, pin_cfg={:?}", config.enable, config.dfp_d_pin_cfg @@ -283,108 +221,135 @@ impl embedded_services::type_c::controller::Controller for Controller<'_> { Ok(()) } - async fn execute_drst(&mut self, port: LocalPortId) -> Result<(), Error> { + async fn execute_drst(&mut self, port: LocalPortId) -> Result<(), PdError> { debug!("Execute PD Data Reset for port {port:?}"); Ok(()) } - async fn set_tbt_config(&mut self, port: LocalPortId, config: TbtConfig) -> Result<(), Error> { + async fn set_tbt_config(&mut self, port: LocalPortId, config: TbtConfig) -> Result<(), PdError> { debug!("Set Thunderbolt config for port {port:?}: {config:?}"); Ok(()) } + async fn hard_reset(&mut self, port: LocalPortId) -> Result<(), PdError> { + debug!("Hard reset for port {port:?}"); + Ok(()) + } + + async fn get_discovered_svids(&mut self, port: LocalPortId) -> Result { + debug!("Get discovered SVIDs for port {port:?}"); + Ok(DiscoveredSvids::default()) + } + + async fn get_discover_identity_sop_response(&mut self, port: LocalPortId) -> Result { + debug!("Get Discover Identity SOP response for port {port:?}"); + Err(PdError::Failed) + } + + async fn get_discover_identity_sop_prime_response( + &mut self, + port: LocalPortId, + ) -> Result { + debug!("Get Discover Identity SOP' response for port {port:?}"); + Err(PdError::Failed) + } +} + +impl type_c_interface::controller::max_sink_voltage::MaxSinkVoltage for Controller<'_> { + async fn set_max_sink_voltage(&mut self, port: LocalPortId, voltage_mv: Option) -> Result<(), PdError> { + debug!("Set max sink voltage for port {}: {:?}", port.0, voltage_mv); + Ok(()) + } +} + +impl type_c_interface::controller::pd::StateMachine for Controller<'_> { async fn set_pd_state_machine_config( &mut self, port: LocalPortId, config: PdStateMachineConfig, - ) -> Result<(), Error> { + ) -> Result<(), PdError> { debug!("Set PD State Machine config for port {port:?}: {config:?}"); Ok(()) } +} +impl type_c_interface::controller::type_c::StateMachine for Controller<'_> { async fn set_type_c_state_machine_config( &mut self, port: LocalPortId, state: TypeCStateMachineState, - ) -> Result<(), Error> { + ) -> Result<(), PdError> { debug!("Set Type-C State Machine state for port {port:?}: {state:?}"); Ok(()) } +} - async fn execute_ucsi_command( - &mut self, - command: lpm::LocalCommand, - ) -> Result, Error> { +impl type_c_interface::ucsi::Lpm for Controller<'_> { + async fn execute_lpm_command(&mut self, command: lpm::LocalCommand) -> Result, PdError> { debug!("Execute UCSI command for port {:?}: {command:?}", command.port()); match command.operation() { lpm::CommandData::GetConnectorStatus => Ok(Some(lpm::ResponseData::GetConnectorStatus( lpm::get_connector_status::ResponseData::default(), ))), - _ => Err(PdError::UnrecognizedCommand.into()), + _ => Err(PdError::UnrecognizedCommand), } } +} +impl type_c_interface::controller::electrical_disconnect::ElectricalDisconnect for Controller<'_> { async fn execute_electrical_disconnect( &mut self, port: LocalPortId, reconnect_time_s: Option, - ) -> Result<(), Error> { + ) -> Result<(), PdError> { debug!("Execute electrical disconnect for port {port:?} with reconnect time {reconnect_time_s:?}"); Ok(()) } +} - async fn set_power_state( +impl type_c_interface::controller::power::SystemPowerStateStatus for Controller<'_> { + async fn set_system_power_state_status( &mut self, port: LocalPortId, state: SystemPowerState, - ) -> Result<(), Error> { - debug!("Set power state for port {port:?}: {state:?}"); + ) -> Result<(), PdError> { + debug!("Set system power state for port {port:?}: {state:?}"); Ok(()) } +} - async fn get_discovered_svids(&mut self, port: LocalPortId) -> Result> { - debug!("Get discovered SVIDs for port {port:?}"); - Ok(DiscoveredSvids::default()) +impl type_c_interface::controller::retimer::Retimer for Controller<'_> { + async fn get_rt_fw_update_status(&mut self, _port: LocalPortId) -> Result { + debug!("Get retimer fw update status"); + Ok(RetimerFwUpdateState::Inactive) } - async fn hard_reset(&mut self, port: LocalPortId) -> Result<(), Error> { - debug!("Hard reset for port {port:?}"); + async fn set_rt_fw_update_state(&mut self, _port: LocalPortId) -> Result<(), PdError> { + debug!("Set retimer fw update state"); Ok(()) } - async fn get_discover_identity_sop_response( - &mut self, - port: LocalPortId, - ) -> Result> - { - debug!("Get Discover Identity SOP response for port {port:?}"); - Err(Error::Pd(PdError::Failed)) + async fn clear_rt_fw_update_state(&mut self, _port: LocalPortId) -> Result<(), PdError> { + debug!("Clear retimer fw update state"); + Ok(()) } - async fn get_discover_identity_sop_prime_response( - &mut self, - port: LocalPortId, - ) -> Result< - embedded_usb_pd::vdm::structured::command::discover_identity::sop_prime::ResponseVdos, - Error, - > { - debug!("Get Discover Identity SOP' response for port {port:?}"); - Err(Error::Pd(PdError::Failed)) + async fn set_rt_compliance(&mut self, _port: LocalPortId) -> Result<(), PdError> { + debug!("Set retimer compliance"); + Ok(()) } -} -pub struct Validator; - -impl type_c_service::wrapper::FwOfferValidator for Validator { - fn validate( - &self, - _current: embedded_cfu_protocol::protocol_definitions::FwVersion, - _offer: &embedded_cfu_protocol::protocol_definitions::FwUpdateOffer, - ) -> embedded_cfu_protocol::protocol_definitions::FwUpdateOfferResponse { - // For this example, we always accept the new version - FwUpdateOfferResponse::new_accept(HostToken::Driver) + async fn reconfigure_retimer(&mut self, port: LocalPortId) -> Result<(), PdError> { + debug!("reconfigure_retimer(port: {port:?})"); + Ok(()) } } -pub type Wrapper<'a> = - type_c_service::wrapper::ControllerWrapper<'a, GlobalRawMutex, Mutex>, Validator>; +pub type Port<'a> = type_c_service::controller::Port< + 'a, + Mutex>, + Mutex, + channel::DynamicSender<'a, type_c_interface::service::event::PortEventData>, + channel::DynamicSender<'a, power_policy_interface::psu::event::EventData>, + channel::DynamicSender<'a, type_c_service::controller::event::Loopback>, +>; diff --git a/fw-update-interface-mocks/Cargo.toml b/fw-update-interface-mocks/Cargo.toml new file mode 100644 index 000000000..ddf28e036 --- /dev/null +++ b/fw-update-interface-mocks/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "fw-update-interface-mocks" +version.workspace = true +edition.workspace = true +license.workspace = true +repository.workspace = true + +[dependencies] +embedded-services = { workspace = true } +fw-update-interface = { workspace = true } + +[dev-dependencies] +tokio = { workspace = true, features = ["rt", "macros", "time"] } + +[lints] +workspace = true diff --git a/fw-update-interface-mocks/src/basic.rs b/fw-update-interface-mocks/src/basic.rs new file mode 100644 index 000000000..50d1a02ae --- /dev/null +++ b/fw-update-interface-mocks/src/basic.rs @@ -0,0 +1,151 @@ +//! Module for a mock that implements [`fw_update_interface::basic::FwUpdate`] +use std::collections::VecDeque; +use std::vec::Vec; + +use embedded_services::named::Named; +use fw_update_interface::basic::{Error, FwUpdate}; + +#[derive(Debug, Clone, PartialEq, Eq)] +#[allow(dead_code)] +pub enum FnCall { + GetActiveFwVersion, + StartFwUpdate, + AbortFwUpdate, + FinalizeFwUpdate, + WriteFwContents(usize, Vec), +} + +pub struct Mock { + /// Signal to record function calls + pub fn_calls: VecDeque, + /// The next error to return from the mock + next_error: Option, + /// Mock current FW version + current_fw_version: u32, + /// Human-readable name of the mock + name: &'static str, +} + +impl Mock { + pub fn new(name: &'static str, current_fw_version: u32) -> Self { + Self { + name, + fn_calls: VecDeque::new(), + next_error: None, + current_fw_version, + } + } + + fn record_fn_call(&mut self, fn_call: FnCall) { + self.fn_calls.push_back(fn_call); + } + + /// Set an error for the next function call + pub fn set_next_error(&mut self, error: Option) { + self.next_error = error; + } +} + +impl FwUpdate for Mock { + async fn get_active_fw_version(&mut self) -> Result { + self.record_fn_call(FnCall::GetActiveFwVersion); + if let Some(error) = self.next_error.take() { + return Err(error); + } + Ok(self.current_fw_version) + } + + async fn start_fw_update(&mut self) -> Result<(), Error> { + self.record_fn_call(FnCall::StartFwUpdate); + if let Some(error) = self.next_error.take() { + return Err(error); + } + Ok(()) + } + + async fn abort_fw_update(&mut self) -> Result<(), Error> { + self.record_fn_call(FnCall::AbortFwUpdate); + if let Some(error) = self.next_error.take() { + return Err(error); + } + Ok(()) + } + + async fn finalize_fw_update(&mut self) -> Result<(), Error> { + self.record_fn_call(FnCall::FinalizeFwUpdate); + if let Some(error) = self.next_error.take() { + return Err(error); + } + Ok(()) + } + + async fn write_fw_contents(&mut self, offset: usize, data: &[u8]) -> Result<(), Error> { + self.record_fn_call(FnCall::WriteFwContents(offset, Vec::from(data))); + + if let Some(error) = self.next_error.take() { + return Err(error); + } + Ok(()) + } +} + +impl Named for Mock { + fn name(&self) -> &'static str { + self.name + } +} + +#[cfg(test)] +mod test { + use super::*; + use std::vec; + + #[tokio::test] + async fn test_get_active_fw_version() { + let mut mock = super::Mock::new("test", 1); + let version = mock.get_active_fw_version().await; + assert_eq!(version, Ok(1)); + assert_eq!(mock.fn_calls.pop_front(), Some(FnCall::GetActiveFwVersion)); + } + + #[tokio::test] + async fn test_start_fw_update() { + let mut mock = super::Mock::new("test", 1); + let result = mock.start_fw_update().await; + assert_eq!(result, Ok(())); + assert_eq!(mock.fn_calls.pop_front(), Some(FnCall::StartFwUpdate)); + } + + #[tokio::test] + async fn test_abort_fw_update() { + let mut mock = super::Mock::new("test", 1); + let result = mock.abort_fw_update().await; + assert_eq!(result, Ok(())); + assert_eq!(mock.fn_calls.pop_front(), Some(FnCall::AbortFwUpdate)); + } + + #[tokio::test] + async fn test_finalize_fw_update() { + let mut mock = super::Mock::new("test", 1); + let result = mock.finalize_fw_update().await; + assert_eq!(result, Ok(())); + assert_eq!(mock.fn_calls.pop_front(), Some(FnCall::FinalizeFwUpdate)); + } + + #[tokio::test] + async fn test_write_fw_contents() { + let mut mock = super::Mock::new("test", 1); + let data = vec![1, 2, 3, 4]; + let result = mock.write_fw_contents(0, &data).await; + assert_eq!(result, Ok(())); + assert_eq!(mock.fn_calls.pop_front(), Some(FnCall::WriteFwContents(0, data))); + } + + #[tokio::test] + async fn test_set_next_error() { + let mut mock = super::Mock::new("test", 1); + mock.set_next_error(Some(Error::Failed)); + let result = mock.get_active_fw_version().await; + assert_eq!(result, Err(Error::Failed)); + } +} diff --git a/fw-update-interface-mocks/src/lib.rs b/fw-update-interface-mocks/src/lib.rs new file mode 100644 index 000000000..10fcf6524 --- /dev/null +++ b/fw-update-interface-mocks/src/lib.rs @@ -0,0 +1,2 @@ +//! Mocks for [`fw_update_interface`] testing. +pub mod basic; diff --git a/fw-update-interface/Cargo.toml b/fw-update-interface/Cargo.toml new file mode 100644 index 000000000..19625163c --- /dev/null +++ b/fw-update-interface/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "fw-update-interface" +version.workspace = true +edition.workspace = true +license.workspace = true +repository.workspace = true + +[dependencies] +log = { workspace = true, optional = true } +defmt = { workspace = true, optional = true } +embedded-services = { workspace = true } + +[dev-dependencies] +tokio = { workspace = true, features = ["rt", "macros", "time"] } + +[features] +default = [] +defmt = ["dep:defmt", "embedded-services/defmt"] +log = ["dep:log", "embedded-services/log"] + +[lints] +workspace = true + +[package.metadata.cargo-machete] +# Not directly needed currently, but present to match the log/defmt patterns of other crates +ignored = ["log"] diff --git a/fw-update-interface/src/basic.rs b/fw-update-interface/src/basic.rs new file mode 100644 index 000000000..cff70daa5 --- /dev/null +++ b/fw-update-interface/src/basic.rs @@ -0,0 +1,43 @@ +//! This module contains types for a very basic firmware update interface. + +use embedded_services::named::Named; + +/// Basic FW update error type +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +#[non_exhaustive] +pub enum Error { + /// The operation is not valid during a FW update + UpdateInProgress, + /// This operation is only valid when a FW update is in progress + NeedsActiveUpdate, + /// Invalid address + InvalidAddress(usize), + /// The firmware content is invalid + InvalidContent, + /// The requested operation timed out + Timeout, + /// The device is busy + Busy, + /// Bus error + Bus, + /// Unspecified failure + Failed, +} + +/// Basic FW update trait +/// +/// This is for devices that don't need to expose multiple banks and can support +/// a FW update done through a few operations. Write only. +pub trait FwUpdate: Named { + /// Get current FW version + fn get_active_fw_version(&mut self) -> impl Future>; + /// Start a firmware update + fn start_fw_update(&mut self) -> impl Future>; + /// Abort a firmware update + fn abort_fw_update(&mut self) -> impl Future>; + /// Finalize a firmware update + fn finalize_fw_update(&mut self) -> impl Future>; + /// Write firmware update contents + fn write_fw_contents(&mut self, offset: usize, data: &[u8]) -> impl Future>; +} diff --git a/fw-update-interface/src/lib.rs b/fw-update-interface/src/lib.rs new file mode 100644 index 000000000..94fa3e4ed --- /dev/null +++ b/fw-update-interface/src/lib.rs @@ -0,0 +1,3 @@ +#![no_std] + +pub mod basic; diff --git a/keyboard-service/src/lib.rs b/keyboard-service/src/lib.rs index 30d936a29..7f5f8612f 100644 --- a/keyboard-service/src/lib.rs +++ b/keyboard-service/src/lib.rs @@ -20,6 +20,7 @@ use embedded_services::hid; pub const HID_KB_ID: hid::DeviceId = hid::DeviceId(0); /// HID keyboard error. +#[derive(Debug)] pub enum KeyboardError { /// Rollover occurred Rollover, diff --git a/mctp-rs/Cargo.toml b/mctp-rs/Cargo.toml new file mode 100644 index 000000000..7d16b1c5c --- /dev/null +++ b/mctp-rs/Cargo.toml @@ -0,0 +1,31 @@ +[package] +name = "mctp-rs" +version = "0.1.0" +edition = "2024" + +[package.metadata.cargo-machete] +# Optional deps gated by features — cargo-machete sees them as unused at +# default features but they ARE consumed when the relevant feature is on. +ignored = ["embedded-batteries", "espi-device", "uuid", "crc"] + +[features] +default = [] +espi = ["dep:espi-device"] +defmt = ["dep:defmt", "embedded-batteries/defmt"] +serial = ["dep:crc"] + +[dependencies] +espi-device = { git = "https://github.com/OpenDevicePartnership/haf-ec-service", optional = true } +bit-register = { git = "https://github.com/OpenDevicePartnership/odp-utilities", package = "bit-register" } +crc = { version = "3.4", default-features = false, optional = true } +num_enum = { version = "0.7.4", default-features = false } +smbus-pec = "1.0.1" +thiserror = { version = "2.0.16", default-features = false } +uuid = { version = "=1.17.0", default-features = false, optional = true } +embedded-batteries = { version = "0.3", features = ["defmt"], optional = true } +defmt = { version = "0.3", optional = true } + +[dev-dependencies] +pretty_assertions = "1.4.1" +tokio = { version = "1.0", features = ["macros", "rt"] } +rstest = "0.26.1" diff --git a/mctp-rs/LICENSE.md b/mctp-rs/LICENSE.md new file mode 100644 index 000000000..d4fa98b78 --- /dev/null +++ b/mctp-rs/LICENSE.md @@ -0,0 +1,7 @@ +Copyright (c) 2025 Microsoft + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/mctp-rs/README.md b/mctp-rs/README.md new file mode 100644 index 000000000..1abf79165 --- /dev/null +++ b/mctp-rs/README.md @@ -0,0 +1,40 @@ +# mctp-rs + +A `no_std` Rust implementation of the Management Component Transport Protocol (MCTP) as defined in the [DMTF DSP0236 specification](https://www.dmtf.org/sites/default/files/standards/documents/DSP0236_1.3.3.pdf). + +## Overview + +MCTP is a communication protocol designed for platform management subsystems in computer systems. It facilitates communication between management controllers (like BMCs) and managed devices across various bus types. This library provides: + +- **Protocol Implementation**: Complete MCTP transport layer with packet assembly/disassembly +- **Medium Abstraction**: Support for different physical transport layers (SMBus/eSPI included) +- **No-std Compatible**: Suitable for embedded and resource-constrained environments + +## Features + +- `espi` - Enables eSPI device support via the `espi-device` crate + +## Documentation & Usage + +See the crate documentation for up-to-date usage and examples: [Rendered Docs](https://dymk.github.io/mctp-rs/) + +## Architecture + +The library is structured around: + +- **`MctpPacketContext`**: Main entry point for handling MCTP packets +- **`MctpMedium`**: Trait for implementing transport-specific packet handling +- **`MctpMessage`**: Represents a complete MCTP message with reply context +- **Control Commands**: Type-safe implementation of MCTP control protocol + + +## License + +MIT License - see [LICENSE.md](LICENSE.md) for details. + +## Contributing + +1. Ensure `cargo check` and `cargo test` pass +2. Test with all feature combinations using `cargo hack --feature-powerset check` +3. Maintain `no_std` compatibility +4. Follow the existing code patterns for protocol message handling diff --git a/mctp-rs/src/buffer_encoding.rs b/mctp-rs/src/buffer_encoding.rs new file mode 100644 index 000000000..96908e5a7 --- /dev/null +++ b/mctp-rs/src/buffer_encoding.rs @@ -0,0 +1,248 @@ +//! Stateless byte-level buffer-encoding transform for MCTP media. +//! +//! Most media (SMBus/eSPI) ship MCTP packets verbatim — wire bytes ARE +//! payload bytes. Some media (DSP0253 serial) need byte-stuffing: an +//! escape character expands certain payload bytes into 2-byte sequences +//! on the wire, and decode reverses that transform. +//! +//! [`BufferEncoding`] is the byte-stuffing layer ONLY. It is stateless: +//! [`write_byte`](BufferEncoding::write_byte) and +//! [`read_byte`](BufferEncoding::read_byte) are associated functions with +//! no `self` and no struct state. Higher-level framing concerns +//! (start/end delimiters, FCS / CRC) live on the medium type, not here. + +use core::marker::PhantomData; + +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum EncodeError { + /// `wire_buf` did not have room for the encoded bytes (1 for plain, + /// up to 2 for an escape sequence). The caller should advance no + /// cursors and treat the encode as failed. + BufferFull, +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum DecodeError { + /// `wire_buf` was empty or ended mid-escape-sequence. Indicates the + /// caller asked to decode past the end of valid wire data. + PrematureEnd, + /// An escape byte was followed by a byte not in the medium's + /// accept-list (strict-XOR rule per RFC1662 §4.2 / DSP0253 §6.4). + /// The caller should reject the entire frame. Reachable via + /// `SerialEncoding` when the byte following an escape (`0x7D`) is + /// neither `0x5E` nor `0x5D`. + InvalidEscape, +} + +/// Stateless byte-stuffing transform. Implementors define how a single +/// logical (payload) byte maps to one or more wire bytes (encode) and +/// how a wire-byte prefix maps back to a single payload byte (decode). +/// +/// All methods are associated functions — there is no `self` and no +/// struct state. Callers own the buffers and the read/write cursors. +pub trait BufferEncoding { + /// Encode one logical payload byte into `wire_buf` starting at + /// index 0. Returns the number of wire bytes written (1 for plain, + /// 2 for an escape sequence). The caller advances their write + /// cursor by the returned count. + fn write_byte(wire_buf: &mut [u8], byte: u8) -> Result; + + /// Decode the next logical payload byte from `wire_buf` starting at + /// index 0. Returns `(decoded_byte, wire_bytes_consumed)`. The + /// caller advances their read cursor by `wire_bytes_consumed`. + fn read_byte(wire_buf: &[u8]) -> Result<(u8, usize), DecodeError>; + + /// Wire-byte footprint of `decoded` under this encoding. Must equal + /// the sum of `write_byte(_, b)` lengths for each `b` in `decoded`. + /// NO default impl: every encoding declares its sizing rule + /// explicitly. + fn wire_size_of(decoded: &[u8]) -> usize; +} + +/// No-op encoding: wire bytes ARE payload bytes. Used by media that do +/// not byte-stuff (SMBus/eSPI, test fixtures). +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct PassthroughEncoding; + +impl BufferEncoding for PassthroughEncoding { + fn write_byte(wire_buf: &mut [u8], byte: u8) -> Result { + match wire_buf.first_mut() { + Some(slot) => { + *slot = byte; + Ok(1) + } + None => Err(EncodeError::BufferFull), + } + } + + fn read_byte(wire_buf: &[u8]) -> Result<(u8, usize), DecodeError> { + match wire_buf.first() { + Some(&byte) => Ok((byte, 1)), + None => Err(DecodeError::PrematureEnd), + } + } + + fn wire_size_of(decoded: &[u8]) -> usize { + decoded.len() + } +} + +/// Stateful cursor over a `&[u8]` wire buffer that reads decoded bytes +/// through `E: BufferEncoding`. Constructed by [`MctpMedium::deserialize`] +/// and handed to higher layers so they cannot bypass the encoding by +/// slicing the underlying buffer directly. +/// +/// [`MctpMedium::deserialize`]: crate::medium::MctpMedium::deserialize +pub struct EncodingDecoder<'buf, E: BufferEncoding> { + buf: &'buf [u8], + wire_pos: usize, + _phantom: PhantomData, +} + +impl<'buf, E: BufferEncoding> EncodingDecoder<'buf, E> { + /// Wrap a wire-byte buffer for stateful encoding-mediated reads. + pub fn new(buf: &'buf [u8]) -> Self { + Self { + buf, + wire_pos: 0, + _phantom: PhantomData, + } + } + + /// Read one decoded byte. Advances the wire cursor by the encoding's + /// per-byte wire footprint. Returns `DecodeError::PrematureEnd` when + /// the wire buffer is exhausted (or ends mid-escape) and + /// `DecodeError::InvalidEscape` for malformed escape sequences. + pub fn read(&mut self) -> Result { + let (byte, n) = E::read_byte(&self.buf[self.wire_pos..])?; + self.wire_pos += n; + Ok(byte) + } +} + +/// Stateful cursor over a `&mut [u8]` wire buffer that writes decoded +/// bytes through `E: BufferEncoding`. Constructed by +/// [`MctpMedium::serialize`] and handed to the caller's `message_writer` +/// closure so the closure cannot bypass the encoding. +/// +/// [`MctpMedium::serialize`]: crate::medium::MctpMedium::serialize +pub struct EncodingEncoder<'buf, E: BufferEncoding> { + buf: &'buf mut [u8], + wire_pos: usize, + _phantom: PhantomData, +} + +impl<'buf, E: BufferEncoding> EncodingEncoder<'buf, E> { + /// Wrap a wire-byte buffer for stateful encoding-mediated writes. + pub fn new(buf: &'buf mut [u8]) -> Self { + Self { + buf, + wire_pos: 0, + _phantom: PhantomData, + } + } + + /// Write one decoded byte. Advances the wire cursor by the encoding's + /// per-byte wire footprint. Returns `EncodeError::BufferFull` when + /// the underlying wire buffer cannot fit the encoded representation. + pub fn write(&mut self, byte: u8) -> Result<(), EncodeError> { + let n = E::write_byte(&mut self.buf[self.wire_pos..], byte)?; + self.wire_pos += n; + Ok(()) + } + + /// Write a contiguous slice of decoded bytes; aborts on the first + /// encode error. Equivalent to a `for &b in bytes { self.write(b)? }` + /// loop, but more concise at call sites that just splat a byte slice. + pub fn write_all(&mut self, bytes: &[u8]) -> Result<(), EncodeError> { + for &b in bytes { + self.write(b)?; + } + Ok(()) + } + + /// Wire bytes written so far (the size of the produced wire frame). + pub fn wire_position(&self) -> usize { + self.wire_pos + } + + /// Wire bytes remaining in the underlying buffer. + pub fn remaining_wire(&self) -> usize { + self.buf.len() - self.wire_pos + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn passthrough_write_byte_writes_one_byte() { + let mut buf = [0u8; 4]; + let n = PassthroughEncoding::write_byte(&mut buf, 0xAB).unwrap(); + assert_eq!(n, 1); + assert_eq!(buf, [0xAB, 0, 0, 0]); + } + + #[test] + fn passthrough_write_byte_full_buffer() { + let mut buf = []; + let err = PassthroughEncoding::write_byte(&mut buf, 0xAB).unwrap_err(); + assert_eq!(err, EncodeError::BufferFull); + } + + #[test] + fn passthrough_read_byte_reads_one_byte() { + let buf = [0xAB, 0xCD]; + let (b, n) = PassthroughEncoding::read_byte(&buf).unwrap(); + assert_eq!(b, 0xAB); + assert_eq!(n, 1); + } + + #[test] + fn passthrough_read_byte_premature_end() { + let buf = []; + let err = PassthroughEncoding::read_byte(&buf).unwrap_err(); + assert_eq!(err, DecodeError::PrematureEnd); + } + + #[test] + fn decoder_reads_all_bytes_via_passthrough() { + let buf = [0xAA, 0xBB, 0xCC, 0xDD]; + let mut decoder = EncodingDecoder::::new(&buf); + assert_eq!(decoder.read().unwrap(), 0xAA); + assert_eq!(decoder.read().unwrap(), 0xBB); + assert_eq!(decoder.read().unwrap(), 0xCC); + assert_eq!(decoder.read().unwrap(), 0xDD); + assert_eq!(decoder.read().unwrap_err(), DecodeError::PrematureEnd); + } + + #[test] + fn encoder_writes_all_bytes_via_passthrough() { + let mut buf = [0u8; 4]; + { + let mut encoder = EncodingEncoder::::new(&mut buf); + assert_eq!(encoder.wire_position(), 0); + assert_eq!(encoder.remaining_wire(), 4); + encoder.write(0x11).unwrap(); + encoder.write(0x22).unwrap(); + encoder.write(0x33).unwrap(); + encoder.write(0x44).unwrap(); + assert_eq!(encoder.wire_position(), 4); + assert_eq!(encoder.remaining_wire(), 0); + assert_eq!(encoder.write(0x55).unwrap_err(), EncodeError::BufferFull); + } + assert_eq!(buf, [0x11, 0x22, 0x33, 0x44]); + } + + #[test] + fn passthrough_wire_size_of_returns_input_len() { + assert_eq!(PassthroughEncoding::wire_size_of(&[]), 0); + assert_eq!(PassthroughEncoding::wire_size_of(&[0xAB]), 1); + let buf = [0u8; 64]; + assert_eq!(PassthroughEncoding::wire_size_of(&buf), 64); + } +} diff --git a/mctp-rs/src/deserialize.rs b/mctp-rs/src/deserialize.rs new file mode 100644 index 000000000..f1e0b3f59 --- /dev/null +++ b/mctp-rs/src/deserialize.rs @@ -0,0 +1,69 @@ +use crate::{ + MctpMessageBuffer, MctpPacketError, + buffer_encoding::{DecodeError, EncodingDecoder}, + error::MctpPacketResult, + mctp_transport_header::MctpTransportHeader, + medium::MctpMedium, +}; + +pub(crate) fn map_decode_err( + e: DecodeError, + on_premature: &'static str, + on_escape: &'static str, +) -> MctpPacketError { + match e { + DecodeError::PrematureEnd => MctpPacketError::HeaderParseError(on_premature), + DecodeError::InvalidEscape => MctpPacketError::HeaderParseError(on_escape), + } +} + +pub(crate) fn parse_transport_header( + decoder: &mut EncodingDecoder<'_, M::Encoding>, +) -> MctpPacketResult { + // Read 4 decoded bytes through the encoding-aware decoder. We do NOT + // pre-check `decoder.remaining_wire() < 4` because for stuffing + // encodings wire length is not decoded length; PrematureEnd from + // `read()` is the canonical "ran out of bytes while decoding the + // header" signal — it correctly handles BOTH the Passthrough case + // (wire < 4) AND the stuffing case (wire >= 4 but yields < 4 decoded + // bytes). + let mut header_bytes = [0u8; 4]; + for slot in header_bytes.iter_mut() { + *slot = decoder.read().map_err(|e| { + map_decode_err::( + e, + "Packet is too small, cannot parse transport header", + "Invalid encoding escape sequence in transport header", + ) + })?; + } + let transport_header_value = u32::from_be_bytes(header_bytes); + MctpTransportHeader::try_from(transport_header_value) + .map_err(|_| MctpPacketError::HeaderParseError("Invalid transport header")) +} + +pub(crate) fn parse_message_body( + packet: &[u8], +) -> MctpPacketResult<(MctpMessageBuffer<'_>, Option), M> { + // first four bytes are the message header, parse with MctpMessageHeader + // to figure out the type, then based on that, parse the type specific header + if packet.is_empty() { + return Err(MctpPacketError::HeaderParseError( + "packet too small to extract message type from header", + )); + } + + let integrity_check = packet[0] & 0b1000_0000; + let message_type = packet[0] & 0b0111_1111; + let packet = &packet[1..]; + + // TODO - compute message integrity check if header.integrity_check is set + Ok(( + MctpMessageBuffer { + integrity_check, + message_type, + rest: packet, + }, + None, + )) +} diff --git a/mctp-rs/src/endpoint_id.rs b/mctp-rs/src/endpoint_id.rs new file mode 100644 index 000000000..03c2f6e58 --- /dev/null +++ b/mctp-rs/src/endpoint_id.rs @@ -0,0 +1,45 @@ +use bit_register::{NumBytes, TryFromBits, TryIntoBits}; + +#[derive(Copy, Clone, PartialEq, Eq, Debug, Default)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum EndpointId { + /// 0x00 + #[default] + Null, + /// 0xFF + Broadcast, + /// 0x08 - 0x7F + Id(u8), +} + +impl TryFrom for EndpointId { + type Error = &'static str; + fn try_from(value: u8) -> Result { + Self::try_from_bits(value as u32) + } +} + +impl TryFromBits for EndpointId { + fn try_from_bits(bits: u32) -> Result { + match bits { + 0x00 => Ok(EndpointId::Null), + 0xFF => Ok(EndpointId::Broadcast), + 0x08..=0xFE => Ok(EndpointId::Id(bits as u8)), + _ => Err("Invalid endpoint ID"), + } + } +} + +impl TryIntoBits for EndpointId { + fn try_into_bits(self) -> Result { + match self { + EndpointId::Null => Ok(0x00), + EndpointId::Broadcast => Ok(0xFF), + EndpointId::Id(id) => Ok(id as u32), + } + } +} + +impl NumBytes for EndpointId { + const NUM_BYTES: usize = 1; +} diff --git a/mctp-rs/src/error.rs b/mctp-rs/src/error.rs new file mode 100644 index 000000000..c2dc147b3 --- /dev/null +++ b/mctp-rs/src/error.rs @@ -0,0 +1,41 @@ +use crate::{ + endpoint_id::EndpointId, mctp_completion_code::MctpCompletionCode, mctp_message_tag::MctpMessageTag, + mctp_sequence_number::MctpSequenceNumber, medium::MctpMedium, +}; + +#[derive(Debug, Copy, Clone, PartialEq, Eq, thiserror::Error)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum ProtocolError { + #[error("Expected start of message")] + ExpectedStartOfMessage, + #[error("Unexpected start of message")] + UnexpectedStartOfMessage, + #[error("Message tag mismatch")] + MessageTagMismatch(MctpMessageTag, MctpMessageTag), + #[error("Tag owner mismatch")] + TagOwnerMismatch(u8, u8), + #[error("Source endpoint id mismatch")] + SourceEndpointIdMismatch(EndpointId, EndpointId), + #[error("Unexpected packet sequence number")] + UnexpectedPacketSequenceNumber(MctpSequenceNumber, MctpSequenceNumber), + #[error("Received non-success completion code on request message")] + CompletionCodeOnRequestMessage(MctpCompletionCode), + #[error("Cannot send message while assembling")] + SendMessageWhileAssembling, + #[error("Cannot send message while receiving")] + SendingMessageWhileReceiving, +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq, thiserror::Error)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum MctpPacketError { + HeaderParseError(&'static str), + CommandParseError(&'static str), + SerializeError(&'static str), + UnsupportedMessageType(u8), + ProtocolError(#[from] ProtocolError), + MediumError(M::Error), +} + +// TODO - MctpPacketResult type alias +pub type MctpPacketResult = Result>; diff --git a/mctp-rs/src/lib.rs b/mctp-rs/src/lib.rs new file mode 100644 index 000000000..7fa1809b3 --- /dev/null +++ b/mctp-rs/src/lib.rs @@ -0,0 +1,731 @@ +#![no_std] +#![allow(dead_code)] +// extern crate std; +//! # mctp-rs +//! +//! A `no_std` Rust implementation of the [DMTF Management Component Transport Protocol (MCTP)](https://www.dmtf.org/sites/default/files/standards/documents/DSP0236_1.3.3.pdf) +//! transport. +//! +//! ## Receiving and parsing messages +//! +//! Use `MctpPacketContext` with your medium to assemble messages from packets. +//! +//! ```rust,no_run +//! # use mctp_rs::*; +//! # #[derive(Debug, Clone, Copy)] struct MyMedium { mtu: usize } +//! # #[derive(Debug, Clone, Copy)] struct MyMediumFrame { packet_size: usize } +//! # impl MctpMedium for MyMedium { type Frame=MyMediumFrame; type Error=&'static str; type ReplyContext=(); type Encoding=PassthroughEncoding; +//! # fn max_message_body_size(&self)->usize{self.mtu} +//! # fn deserialize<'b>(&self,p:&'b [u8])->MctpPacketResult<(Self::Frame,EncodingDecoder<'b,Self::Encoding>),Self>{Ok((MyMediumFrame{packet_size:p.len()},EncodingDecoder::new(p)))} +//! # fn serialize<'b,F>(&self,_:Self::ReplyContext,b:&'b mut [u8],w:F)->MctpPacketResult<&'b [u8],Self> where F: for<'a> FnOnce(&mut EncodingEncoder<'a,Self::Encoding>)->MctpPacketResult<(),Self>{let n={let mut e=EncodingEncoder::::new(b);w(&mut e)?;e.wire_position()};Ok(&b[..n])}} +//! # impl MctpMediumFrame for MyMediumFrame { fn packet_size(&self)->usize{self.packet_size} fn reply_context(&self)->(){()}} +//! let mut assembly_buffer = [0u8; 1024]; +//! let medium = MyMedium { mtu: 256 }; +//! let mut context = MctpPacketContext::new(medium, &mut assembly_buffer); +//! +//! // Typically obtained from your bus +//! let raw_packet_data: &[u8] = &[0x01, 0x02, 0x03, 0x83]; +//! +//! match context.deserialize_packet(raw_packet_data) { +//! Ok(Some(message)) => { +//! // We received a complete MCTP message +//! if let Ok((header, control)) = message.parse_as::() { +//! match control { +//! MctpControl::GetEndpointIdRequest => { +//! // Handle control request +//! let _instance = header.instance_id; +//! } +//! MctpControl::GetEndpointIdResponse(bytes3) => { +//! // Use response payload (3 bytes as per spec) +//! let _eid = bytes3[0]; +//! } +//! _ => {} +//! } +//! } +//! } +//! Ok(None) => { /* partial message; wait for more packets */ } +//! Err(e) => { +//! // handle protocol/medium error +//! let _ = e; +//! } +//! } +//! ``` +//! ## Sending messages +//! +//! Construct a header + body pair implementing `MctpMessageTrait` and serialize to one or +//! more packets using `serialize_packet`. +//! +//! ```rust,no_run +//! # use mctp_rs::*; +//! # #[derive(Debug, Clone, Copy)] struct MyMedium { mtu: usize } +//! # #[derive(Debug, Clone, Copy)] struct MyMediumFrame { packet_size: usize } +//! # impl MctpMedium for MyMedium { type Frame=MyMediumFrame; type Error=&'static str; type ReplyContext=(); type Encoding=PassthroughEncoding; fn max_message_body_size(&self)->usize{self.mtu} +//! # fn deserialize<'b>(&self,p:&'b [u8])->MctpPacketResult<(Self::Frame,EncodingDecoder<'b,Self::Encoding>),Self>{Ok((MyMediumFrame{packet_size:p.len()},EncodingDecoder::new(p)))} +//! # fn serialize<'b,F>(&self,_:Self::ReplyContext,b:&'b mut [u8],w:F)->MctpPacketResult<&'b [u8],Self> where F: for<'a> FnOnce(&mut EncodingEncoder<'a,Self::Encoding>)->MctpPacketResult<(),Self>{let n={let mut e=EncodingEncoder::::new(b);w(&mut e)?;e.wire_position()};Ok(&b[..n])}} +//! # impl MctpMediumFrame for MyMediumFrame { fn packet_size(&self)->usize{self.packet_size} fn reply_context(&self)->(){()}} +//! let mut buf = [0u8; 1024]; +//! let mut ctx = MctpPacketContext::new(MyMedium { mtu: 64 }, &mut buf); +//! +//! let reply = MctpReplyContext { +//! destination_endpoint_id: EndpointId::try_from(0x20).unwrap(), +//! source_endpoint_id: EndpointId::try_from(0x21).unwrap(), +//! packet_sequence_number: MctpSequenceNumber::new(0), +//! message_tag: MctpMessageTag::try_from(1).unwrap(), +//! medium_context: (), +//! }; +//! +//! let message = ( +//! VendorDefinedPciHeader(0x1234), +//! VendorDefinedPci(&[0xDE, 0xAD, 0xBE, 0xEF]), +//! ); +//! +//! let mut packets = ctx.serialize_packet(reply, message).unwrap(); +//! while let Some(packet_result) = packets.next() { +//! let packet_bytes = packet_result.unwrap(); +//! // send `packet_bytes` via your bus +//! let _ = packet_bytes; +//! } +//! ``` +//! +//! ## Implementing a custom medium +//! +//! The crate is transport-agnostic via the `MctpMedium` trait. Implement it for your bus +//! (e.g., SMBus, eSPI) and provide a frame type implementing `MctpMediumFrame`. +//! +//! ```rust,no_run +//! use mctp_rs::*; +//! +//! #[derive(Debug, Clone, Copy)] +//! struct MyMedium { +//! mtu: usize, +//! } +//! +//! #[derive(Debug, Clone, Copy)] +//! struct MyMediumFrame { +//! packet_size: usize, +//! } +//! +//! impl MctpMedium for MyMedium { +//! type Frame = MyMediumFrame; +//! type Error = &'static str; +//! type ReplyContext = (); +//! type Encoding = PassthroughEncoding; +//! +//! fn max_message_body_size(&self) -> usize { +//! self.mtu +//! } +//! +//! fn deserialize<'buf>( +//! &self, +//! packet: &'buf [u8], +//! ) -> MctpPacketResult<(Self::Frame, EncodingDecoder<'buf, Self::Encoding>), Self> { +//! // Strip/validate transport headers as needed for your bus and return MCTP payload slice +//! Ok(( +//! MyMediumFrame { +//! packet_size: packet.len(), +//! }, +//! EncodingDecoder::new(packet), +//! )) +//! } +//! +//! fn serialize<'buf, F>( +//! &self, +//! _reply_context: Self::ReplyContext, +//! buffer: &'buf mut [u8], +//! message_writer: F, +//! ) -> MctpPacketResult<&'buf [u8], Self> +//! where +//! F: for<'a> FnOnce( +//! &mut EncodingEncoder<'a, Self::Encoding>, +//! ) -> MctpPacketResult<(), Self>, +//! { +//! // Prepend transport headers as needed, then ask the writer to write MCTP payload +//! let written = { +//! let mut encoder = EncodingEncoder::::new(buffer); +//! message_writer(&mut encoder)?; +//! encoder.wire_position() +//! }; +//! Ok(&buffer[..written]) +//! } +//! } +//! +//! impl MctpMediumFrame for MyMediumFrame { +//! fn packet_size(&self) -> usize { +//! self.packet_size +//! } +//! fn reply_context(&self) -> ::ReplyContext { +//! () +//! } +//! } +//! ``` + +mod buffer_encoding; +mod deserialize; +mod endpoint_id; +pub mod error; +mod mctp_command_code; +pub mod mctp_completion_code; +mod mctp_message_tag; +mod mctp_packet_context; +mod mctp_sequence_number; +mod mctp_transport_header; +mod medium; +mod message_type; +mod serialize; +#[cfg(test)] +mod test_util; + +pub use buffer_encoding::{ + BufferEncoding, DecodeError, EncodeError, EncodingDecoder, EncodingEncoder, PassthroughEncoding, +}; +pub use endpoint_id::EndpointId; +pub use error::{MctpPacketError, MctpPacketResult}; +pub use mctp_message_tag::MctpMessageTag; +pub use mctp_packet_context::{MctpPacketContext, MctpReplyContext}; +pub use mctp_sequence_number::MctpSequenceNumber; +#[cfg(feature = "serial")] +pub use medium::serial::{CONST_MTU, EC_EID, MctpSerialMedium, MctpSerialMediumFrame, SP_EID, SerialEncoding}; +pub use medium::*; +pub use message_type::*; + +#[derive(Debug, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct MctpMessage<'buffer, M: MctpMedium> { + pub reply_context: MctpReplyContext, + pub message_buffer: MctpMessageBuffer<'buffer>, + pub message_integrity_check: Option, +} + +#[derive(Debug, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct MctpMessageBuffer<'buffer> { + integrity_check: u8, + message_type: u8, + rest: &'buffer [u8], +} + +impl<'buffer, M: MctpMedium> MctpMessage<'buffer, M> { + pub fn can_parse_as>(&self) -> bool { + self.message_buffer.message_type == P::MESSAGE_TYPE + } + pub fn parse_as>(&self) -> MctpPacketResult<(P::Header, P), M> { + if !self.can_parse_as::

() { + return Err(MctpPacketError::HeaderParseError("message type mismatch")); + } + let (header, rest) = P::Header::deserialize(self.message_buffer.rest)?; + let message = P::deserialize(&header, rest)?; + Ok((header, message)) + } +} + +#[cfg(test)] +mod tests { + use pretty_assertions::assert_eq; + + use super::*; + use crate::{ + error::ProtocolError, mctp_command_code::MctpControlCommandCode, mctp_packet_context::MctpPacketContext, + test_util::*, + }; + + struct Packet(&'static [u8]); + const GET_ENDPOINT_ID_PACKET_NO_EOM: Packet = Packet(&[ + // test medium frame (header + trailer): 0 bytes + // transport header: + 0b0000_0001, // mctp reserved, header version + 0b0000_1001, // destination endpoint id (9) + 0b0001_0110, // source endpoint id (22) + 0b1000_0011, // som, eom, seq (0), to, tag (3) + // message header: + 0b0000_0000, // integrity check (off) / message type (MessageType::MctpControl) + 0b0000_0000, // rq, d, rsvd, instance id + 0b0000_0010, // command code (2: get endpoint id) + 0b0000_0000, // completion code + // message body: + 0b0000_1111, // endpoint id (15) + 0b0000_0001, /* endpoint type (simple = 0b00) / endpoint id type (static eid supported = + * 0b01) */ + 0b1111_0000, // medium specific + ]); + + const EMPTY_PACKET_EOM: Packet = Packet(&[ + // transport header: + 0b0000_0001, // mctp reserved, header version + 0b0000_1001, // destination endpoint id (9) + 0b0001_0110, // source endpoint id (14) + 0b0101_0011, // som, eom, seq (1), to, tag (3) + ]); + + #[test] + fn split_over_two_packets() { + let mut buffer = [0; 1024]; + let mut context = MctpPacketContext::::new(TestMedium::new(), &mut buffer); + + assert_eq!( + context.deserialize_packet(GET_ENDPOINT_ID_PACKET_NO_EOM.0).unwrap(), + None + ); + + let message = context.deserialize_packet(EMPTY_PACKET_EOM.0).unwrap().unwrap(); + + assert_eq!(message.can_parse_as::(), true); + assert_eq!(message.message_integrity_check, None); + assert_eq!( + message.reply_context, + MctpReplyContext { + destination_endpoint_id: EndpointId::Id(9), + source_endpoint_id: EndpointId::Id(22), + packet_sequence_number: MctpSequenceNumber::new(1), + message_tag: MctpMessageTag::try_from(3).unwrap(), + medium_context: (), + } + ); + assert_eq!( + message.parse_as().unwrap(), + ( + MctpControlHeader { + command_code: MctpControlCommandCode::GetEndpointId, + ..Default::default() + }, + MctpControl::GetEndpointIdResponse([ + 0b0000_1111, // endpoint id (15) + 0b0000_0001, /* endpoint type (simple = 0b00) / endpoint id type (static eid + * supported = 0b01) */ + 0b1111_0000, // medium specific + ]), + ) + ); + } + + #[test] + fn lacking_start_of_message() { + let mut buffer = [0; 1024]; + let mut context = MctpPacketContext::::new(TestMedium::new(), &mut buffer); + + assert_eq!( + context.deserialize_packet(&[ + // transport header: + 0b0000_0000, // mctp reserved, header version + 0b0000_0000, // destination endpoint id + 0b0000_0000, // source endpoint id + 0b0000_0000, // som, eom, seq (0), to, tag + ]), + Err(MctpPacketError::ProtocolError(ProtocolError::ExpectedStartOfMessage,)) + ); + } + + #[test] + fn repeated_start_of_message() { + let mut buffer = [0; 1024]; + let mut context = MctpPacketContext::::new(TestMedium::new(), &mut buffer); + + context.deserialize_packet(GET_ENDPOINT_ID_PACKET_NO_EOM.0).unwrap(); + + assert_eq!( + context.deserialize_packet(&[ + // transport header: + 0b0000_0000, // mctp reserved, header version + 0b0000_0000, // destination endpoint id + 0b0000_0000, // source endpoint id + 0b1000_0000, // som, eom, seq (0), to, tag + ]), + Err(MctpPacketError::ProtocolError(ProtocolError::UnexpectedStartOfMessage,)) + ); + } + + #[test] + fn message_tag_mismatch() { + let mut buffer = [0; 1024]; + let mut context = MctpPacketContext::::new(TestMedium::new(), &mut buffer); + + // message tag = 0 + context.deserialize_packet(GET_ENDPOINT_ID_PACKET_NO_EOM.0).unwrap(); + + // message tag = 1 + assert_eq!( + context.deserialize_packet(&[ + // transport header: + 0b0000_0000, // mctp reserved, header version + 0b0000_0000, // destination endpoint id + 0b0000_0000, // source endpoint id + 0b0101_0010, // som, eom, seq (1), to, tag (2) + ]), + Err(MctpPacketError::ProtocolError(ProtocolError::MessageTagMismatch( + MctpMessageTag::try_from(3).unwrap(), + MctpMessageTag::try_from(2).unwrap(), + ),)) + ); + } + + #[test] + fn test_send_packet() { + let mut buffer = [0; 1024]; + let mut context = + MctpPacketContext::::new(TestMedium::new().with_headers(&[0xA, 0xB], &[0xC, 0xD]), &mut buffer); + + let reply_context = MctpReplyContext { + destination_endpoint_id: EndpointId::try_from(236).unwrap(), + source_endpoint_id: EndpointId::try_from(192).unwrap(), + packet_sequence_number: MctpSequenceNumber::new(1), + message_tag: MctpMessageTag::try_from(3).unwrap(), + medium_context: (), + }; + + let message = (VendorDefinedPciHeader(0x1234), VendorDefinedPci(&[0xA5, 0xB6])); + + let mut state = context.serialize_packet(reply_context, message).unwrap(); + + let packet = state.next().unwrap().unwrap(); + assert_eq!( + &[ + // test header - 2 bytes + 0xA, + 0xB, + // mctp transport header + 0b0000_0001, // mctp reserved, header version + 192, // destination endpoint id + 236, // source endpoint id + 0b1110_0011, // som (1), eom (1), seq (2), tag owner (0), message tag (3) + // mctp message header - 3 bytes + 0x7E, // integrity check (0), message type (vendor defined pci) + 0x12, // pci vendor id - low byte + 0x34, // pci vendor id - high byte + // mctp message body - 1 byte + 0xA5, + 0xB6, + // test trailer - 2 bytes + 0xC, + 0xD, + ], + packet + ); + } + + #[test] + fn test_send_packet_multi() { + const MTU_SIZE: usize = 14; + let mut buffer = [0; 1024]; + let mut context = MctpPacketContext::::new( + TestMedium::new() + .with_headers(&[0xA, 0xB], &[0xC, 0xD]) + // 4 bytes transport header + 4 bytes of data + .with_mtu(MTU_SIZE), + &mut buffer, + ); + + let reply_context = MctpReplyContext { + destination_endpoint_id: EndpointId::try_from(236).unwrap(), + source_endpoint_id: EndpointId::try_from(192).unwrap(), + packet_sequence_number: MctpSequenceNumber::new(1), + message_tag: MctpMessageTag::try_from(3).unwrap(), + medium_context: (), + }; + + // 10 byte to send over 3 packets + let data_to_send = [0xA5, 0xB6, 0xC7, 0xD8, 0xE9, 0xFA, 0x0B, 0x1C, 0x2D, 0x3E]; + let message = (VendorDefinedPciHeader(0x1234), VendorDefinedPci(&data_to_send)); + + let mut state = context.serialize_packet(reply_context, message).unwrap(); + + // First packet + let packet1 = state.next().unwrap().unwrap(); + let expected: [u8; MTU_SIZE] = [ + // test header - 2 bytes + 0xA, + 0xB, + // mctp transport header - 4 bytes + 0b0000_0001, // mctp reserved, header version + 192, // destination endpoint id + 236, // source endpoint id + 0b1010_0011, // som (1), eom (0), seq (2), tag owner (0), message tag (3) + // mctp message header - 3 bytes + 0x7E, // integrity check (0), message type (vendor defined pci) + 0x12, // pci vendor id - low byte + 0x34, // pci vendor id - high byte + // mctp message body data - 1 bytes + 0xA5, + 0xB6, + 0xC7, + // test trailer - 2 bytes + 0xC, + 0xD, + ]; + assert_eq!(packet1, &expected[..MTU_SIZE]); + + // Second packet (middle packet with 4 bytes of data) + let packet2 = state.next().unwrap().unwrap(); + let expected: [u8; MTU_SIZE] = [ + // test header - 2 bytes + 0xA, + 0xB, + // mctp transport header - 4 bytes + 0b0000_0001, // mctp reserved, header version + 192, // destination endpoint id + 236, // source endpoint id + 0b0011_0011, // som (0), eom (0), seq (3), tag owner (0), message tag (3) + // mctp body data - 4 bytes + 0xD8, + 0xE9, + 0xFA, + 0x0B, + 0x1C, + 0x2D, + // test trailer - 2 bytes + 0xC, + 0xD, + ]; + assert_eq!(packet2, &expected[..]); + + // Third packet (final packet with 2 bytes of data) + let packet3 = state.next().unwrap().unwrap(); + let expected: [u8; MTU_SIZE] = [ + // test header - 2 bytes + 0xA, + 0xB, + // mctp transport header - 4 bytes + 0b0000_0001, // mctp reserved, header version + 192, // destination endpoint id + 236, // source endpoint id + 0b0100_0011, // som (0), eom (1), seq (0), tag owner (0), message tag (3) + // mctp body data - 1 bytes + 0x3E, + // test trailer - 2 bytes + 0xC, + 0xD, + // remainder is not populated + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + ]; + assert_eq!(packet3, &expected[..9]); + + // Verify no more packets + let next = state.next(); + assert!(next.is_none(), "Expected exactly 3 packets: {next:x?}"); + } + + #[test] + fn test_buffer_overflow_protection() { + // Test that buffer overflow is properly prevented + let mut small_buffer = [0u8; 16]; // Very small buffer + let mut context = MctpPacketContext::::new(TestMedium::new(), &mut small_buffer); + + // Create a packet that would cause overflow without protection + let large_packet = [ + // transport header: + 0b0000_0001, // mctp reserved, header version + 0b0000_1001, // destination endpoint id (9) + 0b0001_0110, // source endpoint id (22) + 0b1000_0011, // som, eom, seq (0), to, tag (3) + // message header: + 0b0000_0000, // integrity check (off) / message type (mctp control message) + 0b0000_0000, // rq, d, rsvd, instance id + 0b0000_0010, // command code (2: get endpoint id) + 0b0000_0000, // completion code + // Large message body that would overflow small buffer + 0x01, + 0x02, + 0x03, + 0x04, + 0x05, + 0x06, + 0x07, + 0x08, + 0x09, + 0x0A, + 0x0B, + 0x0C, + 0x0D, + 0x0E, + 0x0F, + 0x10, + ]; + + // This should return an error instead of panicking + let result = context.deserialize_packet(&large_packet); + assert!(result.is_err()); + + if let Err(MctpPacketError::HeaderParseError(msg)) = result { + assert!(msg.contains("buffer overflow")); + } else { + panic!("Expected HeaderParseError with buffer overflow message"); + } + } + + #[test] + fn test_multi_packet_buffer_overflow() { + // Test buffer overflow with multiple packets + let mut small_buffer = [0u8; 20]; // Small buffer that can fit first packet but not second + let mut context = MctpPacketContext::::new(TestMedium::new(), &mut small_buffer); + + // First packet - fits in buffer + let first_packet = [ + // transport header: + 0b0000_0001, // mctp reserved, header version + 0b0000_1001, // destination endpoint id (9) + 0b0001_0110, // source endpoint id (22) + 0b1000_0011, // som (1), eom (0), seq (0), to, tag (3) + // message header: + 0b0000_0000, // integrity check (off) / message type (mctp control message) + 0b0000_0000, // rq, d, rsvd, instance id + 0b0000_0010, // command code (2: get endpoint id) + 0b0000_0000, // completion code + // Small message body + 0x01, + 0x02, + 0x03, + 0x04, + 0x05, + 0x06, + 0x07, + 0x08, + ]; + + // First packet should succeed + let result1 = context.deserialize_packet(&first_packet); + assert!(result1.is_ok()); + assert!(result1.unwrap().is_none()); // No complete message yet + + // Second packet - would cause overflow + let second_packet = [ + // transport header: + 0b0000_0001, // mctp reserved, header version + 0b0000_1001, // destination endpoint id (9) + 0b0001_0110, // source endpoint id (22) + 0b0101_0011, // som (0), eom (1), seq (1), to, tag (3) - correct sequence number + // Large continuation that would overflow + 0x09, + 0x0A, + 0x0B, + 0x0C, + 0x0D, + 0x0E, + 0x0F, + 0x10, + 0x11, + 0x12, + 0x13, + 0x14, + 0x15, + 0x16, + 0x17, + 0x18, + ]; + + // Second packet should fail with buffer overflow + let result2 = context.deserialize_packet(&second_packet); + assert!(result2.is_err()); + + if let Err(MctpPacketError::HeaderParseError(msg)) = result2 { + assert!(msg.contains("buffer overflow")); + } else { + panic!("Expected HeaderParseError with buffer overflow message"); + } + } + + #[test] + fn test_transport_header_underflow() { + // Test transport header parsing with insufficient bytes + let mut buffer = [0u8; 1024]; + let mut context = MctpPacketContext::::new(TestMedium::new(), &mut buffer); + + // Packet too short for transport header (only 3 bytes) + let short_packet = [0x01, 0x02, 0x03]; + + let result = context.deserialize_packet(&short_packet); + assert!(result.is_err()); + + if let Err(MctpPacketError::HeaderParseError(msg)) = result { + assert!(msg.contains("cannot parse transport header")); + } else { + panic!("Expected HeaderParseError for short transport header"); + } + } + + #[test] + fn test_message_header_underflow() { + // Test message body parsing with insufficient bytes for message header + let mut buffer = [0u8; 1024]; + let mut context = MctpPacketContext::::new(TestMedium::new(), &mut buffer); + + // Packet with transport header but no message header + let incomplete_packet = [ + // transport header only (4 bytes) + 0b0000_0001, // mctp reserved, header version + 0b0000_1001, // destination endpoint id (9) + 0b0001_0110, // source endpoint id (22) + 0b1110_0011, /* som (1), eom (1), seq (0), to, tag (3) + * No message header (need 4 more bytes) */ + ]; + + let result = context.deserialize_packet(&incomplete_packet); + assert!(result.is_err()); + + if let Err(MctpPacketError::HeaderParseError(msg)) = result { + assert!(msg.contains("packet too small"), "msg: {msg}"); + } else { + panic!("Expected HeaderParseError for short message header"); + } + } + + #[test] + fn test_serialize_buffer_underflow() { + // Test serialization with buffer too small for serializing the packet and having enough + // buffer for assembling packets + let mut tiny_buffer = [0u8; 4]; // Too small for 4-byte transport header + let mut context = MctpPacketContext::::new(TestMedium::new(), &mut tiny_buffer); + + let reply_context = MctpReplyContext { + destination_endpoint_id: EndpointId::try_from(236).unwrap(), + source_endpoint_id: EndpointId::try_from(192).unwrap(), + packet_sequence_number: MctpSequenceNumber::new(1), + message_tag: MctpMessageTag::try_from(3).unwrap(), + medium_context: (), + }; + + let message = (VendorDefinedPciHeader(0x1234), VendorDefinedPci(&[0xA5])); + let state_result = context.serialize_packet(reply_context, message); + assert!(state_result.is_ok(), "{state_result:?}"); + + let mut state = state_result.unwrap(); + let packet_result = state.next().unwrap(); + + // Should fail because buffer is too small for transport header + assert!(packet_result.is_err()); + if let Err(MctpPacketError::SerializeError(msg)) = packet_result { + assert!(msg.contains("assembly buffer too small")); + } else { + panic!("Expected SerializeError for small buffer"); + } + } + + #[test] + fn test_zero_size_assembly_buffer() { + // Test with zero-size assembly buffer + let mut empty_buffer = [0u8; 0]; + let mut context = MctpPacketContext::::new(TestMedium::new(), &mut empty_buffer); + + let packet = [ + 0b0000_0001, // mctp reserved, header version + 0b0000_1001, // destination endpoint id (9) + 0b0001_0110, // source endpoint id (22) + 0b1110_0011, // som (1), eom (1), seq (0), to, tag (3) + 0x7F, // message header - 3 bytes (vendor defined pci) + 0x12, // pci vendor id - low byte + 0x34, // pci vendor id - high byte + 0b0000_0010, + 0b0000_0000, + ]; + + let result = context.deserialize_packet(&packet); + assert!(result.is_err()); + + if let Err(MctpPacketError::HeaderParseError(msg)) = result { + assert!(msg.contains("buffer overflow")); + } else { + panic!("Expected buffer overflow error for zero-size buffer"); + } + } +} diff --git a/mctp-rs/src/mctp_command_code.rs b/mctp-rs/src/mctp_command_code.rs new file mode 100644 index 000000000..c312c99ea --- /dev/null +++ b/mctp-rs/src/mctp_command_code.rs @@ -0,0 +1,52 @@ +use bit_register::{NumBytes, TryFromBits, TryIntoBits}; + +#[repr(u8)] +#[derive(Copy, Clone, PartialEq, Eq, Debug, Default, num_enum::IntoPrimitive, num_enum::TryFromPrimitive)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum MctpControlCommandCode { + #[default] + Reserved = 0x00, + SetEndpointId = 0x01, + GetEndpointId = 0x02, + GetEndpointUuid = 0x03, + GetMctpVersionSupport = 0x04, + GetMessageTypeSupport = 0x05, + GetVendorDefinedMessageSupport = 0x06, + ResolveEndpointId = 0x07, + AllocateEndpointIds = 0x08, + RoutingInformationUpdate = 0x09, + GetRoutingTableEntries = 0x0A, + PrepareForEndpointDiscovery = 0x0B, + EndpointDiscovery = 0x0C, + DiscoveryNotify = 0x0D, + GetNetworkId = 0x0E, + QueryHop = 0x0F, + ResolveUuid = 0x10, + QueryRateLimit = 0x11, + RequestTxRateLimit = 0x12, + UpdateRateLimit = 0x13, + QuerySupportedInterfaces = 0x14, + // 0x15-0xEF are reserved for future use + // 0xF0-0xFF are transport specific commands +} + +impl TryFromBits for MctpControlCommandCode { + fn try_from_bits(bits: u32) -> Result { + if bits > 0xFF { + return Err("Out of range value for MCTP command code"); + } + (bits as u8) + .try_into() + .map_err(|_| "Invalid value for MCTP command code") + } +} + +impl TryIntoBits for MctpControlCommandCode { + fn try_into_bits(self) -> Result { + Ok(Into::::into(self) as u32) + } +} + +impl NumBytes for MctpControlCommandCode { + const NUM_BYTES: usize = 1; +} diff --git a/mctp-rs/src/mctp_completion_code.rs b/mctp-rs/src/mctp_completion_code.rs new file mode 100644 index 000000000..4645c9f66 --- /dev/null +++ b/mctp-rs/src/mctp_completion_code.rs @@ -0,0 +1,94 @@ +use bit_register::{NumBytes, TryFromBits, TryIntoBits}; + +#[derive(Copy, Clone, PartialEq, Eq, Debug, Default)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum MctpCompletionCode { + #[default] + Success, + Error, + ErrorInvalidData, + ErrorInvalidLength, + ErrorNotReady, + ErrorUnsupportedCmd, + CommandSpecific(u8), // 0x80-0xFF are command specific +} + +impl From for u8 { + fn from(value: MctpCompletionCode) -> Self { + match value { + MctpCompletionCode::Success => 0x00, + MctpCompletionCode::Error => 0x01, + MctpCompletionCode::ErrorInvalidData => 0x02, + MctpCompletionCode::ErrorInvalidLength => 0x03, + MctpCompletionCode::ErrorNotReady => 0x04, + MctpCompletionCode::ErrorUnsupportedCmd => 0x05, + MctpCompletionCode::CommandSpecific(code) => code, + } + } +} +impl TryFrom for MctpCompletionCode { + type Error = &'static str; + fn try_from(value: u8) -> Result { + Ok(match value { + 0x00 => MctpCompletionCode::Success, + 0x01 => MctpCompletionCode::Error, + 0x02 => MctpCompletionCode::ErrorInvalidData, + 0x03 => MctpCompletionCode::ErrorInvalidLength, + 0x04 => MctpCompletionCode::ErrorNotReady, + 0x05 => MctpCompletionCode::ErrorUnsupportedCmd, + 0x06..=0x7F => return Err("Invalid value for MCTP completion code - reserved range"), + 0x80..=0xFF => MctpCompletionCode::CommandSpecific(value), + }) + } +} + +impl TryFromBits for MctpCompletionCode { + fn try_from_bits(bits: u32) -> Result { + if bits > 0xFF { + return Err("Out of range value for MCTP completion code"); + } + (bits as u8).try_into() + } +} + +impl TryIntoBits for MctpCompletionCode { + fn try_into_bits(self) -> Result { + Ok(Into::::into(self) as u32) + } +} + +impl NumBytes for MctpCompletionCode { + const NUM_BYTES: usize = 1; +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_completion_code_reserved_range() { + // Test that reserved range 0x06-0x7F is properly rejected + for code in 0x06..=0x7F { + let result = MctpCompletionCode::try_from(code); + assert!(result.is_err(), "Code 0x{:02X} should be rejected", code); + if let Err(msg) = result { + assert!(msg.contains("reserved range")); + } + } + + // Test valid ranges still work + assert_eq!(MctpCompletionCode::try_from(0x00).unwrap(), MctpCompletionCode::Success); + assert_eq!( + MctpCompletionCode::try_from(0x05).unwrap(), + MctpCompletionCode::ErrorUnsupportedCmd + ); + assert_eq!( + MctpCompletionCode::try_from(0x80).unwrap(), + MctpCompletionCode::CommandSpecific(0x80) + ); + assert_eq!( + MctpCompletionCode::try_from(0xFF).unwrap(), + MctpCompletionCode::CommandSpecific(0xFF) + ); + } +} diff --git a/mctp-rs/src/mctp_message_tag.rs b/mctp-rs/src/mctp_message_tag.rs new file mode 100644 index 000000000..5795c263c --- /dev/null +++ b/mctp-rs/src/mctp_message_tag.rs @@ -0,0 +1,31 @@ +use bit_register::{NumBytes, TryFromBits, TryIntoBits}; + +#[derive(Debug, Default, PartialEq, Eq, Copy, Clone)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct MctpMessageTag(u8); + +impl TryFrom for MctpMessageTag { + type Error = &'static str; + fn try_from(value: u8) -> Result { + if value > 0b111 { + return Err("Invalid message tag"); + } + Ok(Self(value)) + } +} + +impl NumBytes for MctpMessageTag { + const NUM_BYTES: usize = 1; +} + +impl TryFromBits for MctpMessageTag { + fn try_from_bits(bits: u32) -> Result { + Self::try_from(bits as u8) + } +} + +impl TryIntoBits for MctpMessageTag { + fn try_into_bits(self) -> Result { + Ok(self.0 as u32) + } +} diff --git a/mctp-rs/src/mctp_packet_context.rs b/mctp-rs/src/mctp_packet_context.rs new file mode 100644 index 000000000..893b333c6 --- /dev/null +++ b/mctp-rs/src/mctp_packet_context.rs @@ -0,0 +1,198 @@ +use crate::{ + MctpMessage, MctpMessageHeaderTrait, MctpMessageTrait, MctpPacketError, + deserialize::{map_decode_err, parse_message_body, parse_transport_header}, + endpoint_id::EndpointId, + error::{MctpPacketResult, ProtocolError}, + mctp_message_tag::MctpMessageTag, + mctp_sequence_number::MctpSequenceNumber, + medium::{MctpMedium, MctpMediumFrame}, + serialize::SerializePacketState, +}; + +/// Represents the state needed to construct a repsonse to a request: +/// the MCTP transport source/destination, the sequence number to use for +/// the reply, and the medium-specific context that came with the request. +#[derive(Debug, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct MctpReplyContext { + pub destination_endpoint_id: EndpointId, + pub source_endpoint_id: EndpointId, + pub packet_sequence_number: MctpSequenceNumber, + pub message_tag: MctpMessageTag, + pub medium_context: M::ReplyContext, +} + +/// Context for serializing and deserializing an MCTP message, which may be split among multiple +/// packets. +pub struct MctpPacketContext<'buf, M: MctpMedium> { + assembly_state: AssemblyState, + medium: M, + packet_assembly_buffer: &'buf mut [u8], +} + +impl<'buf, M: MctpMedium> MctpPacketContext<'buf, M> { + pub fn new(medium: M, packet_assembly_buffer: &'buf mut [u8]) -> Self { + Self { + medium, + assembly_state: AssemblyState::Idle, + packet_assembly_buffer, + } + } + + pub fn deserialize_packet(&mut self, packet: &[u8]) -> MctpPacketResult>, M> { + let (medium_frame, mut decoder) = self.medium.deserialize(packet)?; + let transport_header = parse_transport_header::(&mut decoder)?; + + let mut state = match self.assembly_state { + AssemblyState::Idle => { + if transport_header.start_of_message == 0 { + return Err(MctpPacketError::ProtocolError(ProtocolError::ExpectedStartOfMessage)); + } + + AssemblingState { + message_tag: transport_header.message_tag, + tag_owner: transport_header.tag_owner, + source_endpoint_id: transport_header.source_endpoint_id, + packet_sequence_number: transport_header.packet_sequence_number, + packet_assembly_buffer_index: 0, + } + } + AssemblyState::Receiving(assembling_state) => { + if transport_header.start_of_message != 0 { + return Err(MctpPacketError::ProtocolError(ProtocolError::UnexpectedStartOfMessage)); + } + if assembling_state.message_tag != transport_header.message_tag { + return Err(MctpPacketError::ProtocolError(ProtocolError::MessageTagMismatch( + assembling_state.message_tag, + transport_header.message_tag, + ))); + } + if assembling_state.tag_owner != transport_header.tag_owner { + return Err(MctpPacketError::ProtocolError(ProtocolError::TagOwnerMismatch( + assembling_state.tag_owner, + transport_header.tag_owner, + ))); + } + if assembling_state.source_endpoint_id != transport_header.source_endpoint_id { + return Err(MctpPacketError::ProtocolError(ProtocolError::SourceEndpointIdMismatch( + assembling_state.source_endpoint_id, + transport_header.source_endpoint_id, + ))); + } + let expected_sequence_number = assembling_state.packet_sequence_number.next(); + if expected_sequence_number != transport_header.packet_sequence_number { + return Err(MctpPacketError::ProtocolError( + ProtocolError::UnexpectedPacketSequenceNumber( + expected_sequence_number, + transport_header.packet_sequence_number, + ), + )); + } + assembling_state + } + }; + + let buffer_idx = state.packet_assembly_buffer_index; + let packet_size = medium_frame.packet_size(); + if packet_size < 4 { + return Err(MctpPacketError::HeaderParseError( + "transport frame indicated packet length < 4", + )); + } + let packet_size = packet_size - 4; // to account for the transport header + // Check assembly buffer bounds (decoded bytes destination) + if buffer_idx + packet_size > self.packet_assembly_buffer.len() { + return Err(MctpPacketError::HeaderParseError( + "packet assembly buffer overflow - insufficient space", + )); + } + // Decode `packet_size` payload bytes from the (possibly stuffed) wire + // buffer into the assembly buffer one byte at a time via the + // medium-supplied decoder. We do NOT pre-check + // `decoder.remaining_wire() < packet_size` because for stuffing + // encodings wire length is not decoded length; PrematureEnd from + // `read()` is the canonical "ran out of bytes while decoding the + // body" signal. + for i in 0..packet_size { + self.packet_assembly_buffer[buffer_idx + i] = decoder.read().map_err(|e| { + map_decode_err::( + e, + "packet body too short to extract expected decoded bytes", + "Invalid encoding escape sequence in packet body", + ) + })?; + } + state.packet_assembly_buffer_index += packet_size; + + let message = if transport_header.end_of_message == 1 { + self.assembly_state = AssemblyState::Idle; + let (message_body, message_integrity_check) = + parse_message_body::(&self.packet_assembly_buffer[..state.packet_assembly_buffer_index])?; + Some(MctpMessage { + reply_context: MctpReplyContext { + destination_endpoint_id: transport_header.destination_endpoint_id, + source_endpoint_id: transport_header.source_endpoint_id, + packet_sequence_number: transport_header.packet_sequence_number, + message_tag: transport_header.message_tag, + medium_context: medium_frame.reply_context(), + }, + message_buffer: message_body, + message_integrity_check, + }) + } else { + self.assembly_state = AssemblyState::Receiving(state); + None + }; + + Ok(message) + } + + pub fn serialize_packet>( + &'buf mut self, + reply_context: MctpReplyContext, + message: (P::Header, P), + ) -> MctpPacketResult, M> { + match self.assembly_state { + AssemblyState::Idle => {} + _ => { + return Err(MctpPacketError::ProtocolError( + ProtocolError::SendMessageWhileAssembling, + )); + } + }; + + self.packet_assembly_buffer[0] = P::MESSAGE_TYPE; + let header_size = message.0.serialize(&mut self.packet_assembly_buffer[1..])?; + let body_size = message + .1 + .serialize(&mut self.packet_assembly_buffer[header_size + 1..])?; + + let (message, rest) = self.packet_assembly_buffer.split_at_mut(header_size + body_size + 1); + + Ok(SerializePacketState { + medium: &self.medium, + reply_context, + current_packet_num: 0, + serialized_message_header: false, + message_buffer: message, + assembly_buffer: rest, + }) + } +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +enum AssemblyState { + Idle, + Receiving(AssemblingState), +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +struct AssemblingState { + message_tag: MctpMessageTag, + tag_owner: u8, + source_endpoint_id: EndpointId, + packet_sequence_number: MctpSequenceNumber, + packet_assembly_buffer_index: usize, +} diff --git a/mctp-rs/src/mctp_sequence_number.rs b/mctp-rs/src/mctp_sequence_number.rs new file mode 100644 index 000000000..a79404b96 --- /dev/null +++ b/mctp-rs/src/mctp_sequence_number.rs @@ -0,0 +1,42 @@ +use bit_register::{NumBytes, TryFromBits, TryIntoBits}; + +#[derive(Default, Debug, Copy, Clone, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct MctpSequenceNumber(u8); + +impl MctpSequenceNumber { + const MAX: u8 = 4; + + pub fn new(value: u8) -> Self { + Self(value) + } + + pub fn inc(&mut self) -> Self { + *self = self.next(); + *self + } + + pub fn next(&self) -> Self { + Self((self.0 + 1) % Self::MAX) + } +} + +impl NumBytes for MctpSequenceNumber { + const NUM_BYTES: usize = 1; +} + +impl TryIntoBits for MctpSequenceNumber { + fn try_into_bits(self) -> Result { + Ok(self.0 as u32) + } +} + +impl TryFromBits for MctpSequenceNumber { + fn try_from_bits(bits: u32) -> Result { + if bits >= Self::MAX as u32 { + Err("sequence number out of range") + } else { + Ok(Self(bits as u8)) + } + } +} diff --git a/mctp-rs/src/mctp_transport_header.rs b/mctp-rs/src/mctp_transport_header.rs new file mode 100644 index 000000000..e9fe5915b --- /dev/null +++ b/mctp-rs/src/mctp_transport_header.rs @@ -0,0 +1,51 @@ +use bit_register::bit_register; + +use crate::{endpoint_id::EndpointId, mctp_message_tag::MctpMessageTag, mctp_sequence_number::MctpSequenceNumber}; + +bit_register! { + #[derive(Debug, Default, PartialEq, Eq, Copy, Clone)] + #[cfg_attr(feature = "defmt", derive(defmt::Format))] + pub struct MctpTransportHeader: little_endian u32 { + pub reserved: u8 => [28:31], + pub header_version: u8 => [24:27], + pub destination_endpoint_id: EndpointId => [16:23], + pub source_endpoint_id: EndpointId => [8:15], + pub start_of_message: u8 => [7], + pub end_of_message: u8 => [6], + pub packet_sequence_number: MctpSequenceNumber => [4:5], + pub tag_owner: u8 => [3], + pub message_tag: MctpMessageTag => [0:2], + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_mctp_transport_header_bit_register() { + let header = MctpTransportHeader::try_from(u32::from_be_bytes([ + 0b0000_0001, // reserved, header version (1) + 0b0000_1001, // destination endpoint id (9) + 0b0001_0010, // source endpoint id (18) + 0b0000_0101, /* start of message, end of message, packet sequence number (0), tag + * owner, message tag */ + ])) + .unwrap(); + + assert_eq!( + header, + MctpTransportHeader { + reserved: 0b0000, + header_version: 0b0001, + destination_endpoint_id: EndpointId::Id(9), + source_endpoint_id: EndpointId::Id(18), + start_of_message: 0b0000, + end_of_message: 0b0000, + packet_sequence_number: MctpSequenceNumber::new(0), + tag_owner: 0b0000, + message_tag: MctpMessageTag::try_from(0b101).unwrap(), + } + ); + } +} diff --git a/mctp-rs/src/medium/mod.rs b/mctp-rs/src/medium/mod.rs new file mode 100644 index 000000000..693deda46 --- /dev/null +++ b/mctp-rs/src/medium/mod.rs @@ -0,0 +1,63 @@ +use crate::{ + buffer_encoding::{BufferEncoding, EncodingDecoder, EncodingEncoder}, + error::MctpPacketResult, +}; + +pub mod smbus_espi; +mod util; + +#[cfg(feature = "serial")] +pub mod serial; + +pub trait MctpMedium: Sized { + /// the medium specific header and trailer for the packet + type Frame: MctpMediumFrame; + + /// the error type for deserialization of the medium specific header + type Error: core::fmt::Debug + Copy + Clone + PartialEq + Eq; + + // the type used for the data needed to send a reply to a request + type ReplyContext: core::fmt::Debug + Copy + Clone + PartialEq + Eq; + + /// the byte-stuffing transform used by this medium when (de)serializing + /// wire bytes. Stateless — see [`crate::buffer_encoding`]. Most media + /// use [`PassthroughEncoding`](crate::buffer_encoding::PassthroughEncoding) + /// (no transform); media that need byte-stuffing (e.g., DSP0253 serial) + /// supply their own impl. + type Encoding: BufferEncoding; + + /// the maximum transmission unit for the medium + fn max_message_body_size(&self) -> usize; + + /// Deserialize a packet into the medium-specific header (frame) and an + /// [`EncodingDecoder`] that wraps the inner stuffed-region bytes. + /// Higher layers (e.g., `parse_transport_header`, the payload copy + /// loop in `MctpPacketContext`) read decoded bytes through the + /// returned decoder and physically cannot bypass the medium's + /// encoding by slicing the underlying buffer directly. + fn deserialize<'buf>( + &self, + packet: &'buf [u8], + ) -> MctpPacketResult<(Self::Frame, EncodingDecoder<'buf, Self::Encoding>), Self>; + + /// Serialize a packet by allowing the caller's `message_writer` + /// closure to write decoded bytes into the medium's stuffed region + /// through an [`EncodingEncoder`]. The medium owns its outer framing + /// (e.g., SMBus header + PEC, DSP0253 start/end flags + FCS) and + /// inspects the encoder's + /// [`wire_position`](EncodingEncoder::wire_position) after the + /// closure returns to size headers/trailers and compute checksums. + fn serialize<'buf, F>( + &self, + reply_context: Self::ReplyContext, + buffer: &'buf mut [u8], + message_writer: F, + ) -> MctpPacketResult<&'buf [u8], Self> + where + F: for<'a> FnOnce(&mut EncodingEncoder<'a, Self::Encoding>) -> MctpPacketResult<(), Self>; +} + +pub trait MctpMediumFrame: Clone + Copy { + fn packet_size(&self) -> usize; + fn reply_context(&self) -> M::ReplyContext; +} diff --git a/mctp-rs/src/medium/serial.rs b/mctp-rs/src/medium/serial.rs new file mode 100644 index 000000000..9075ba1f3 --- /dev/null +++ b/mctp-rs/src/medium/serial.rs @@ -0,0 +1,716 @@ +//! DSP0253 byte-stuffed serial medium for MCTP. +//! +//! Two-layer split: +//! - [`SerialEncoding`]: stateless byte-stuffing (0x7E, 0x7D escape pair). +//! - [`MctpSerialMedium`]: framing (revision byte, byte_count, body, FCS-16, end-flag). +//! +//! Both layers are gated behind the `serial` cargo feature. + +use crate::{ + MctpPacketError, + buffer_encoding::{BufferEncoding, DecodeError, EncodeError, EncodingDecoder, EncodingEncoder}, + error::MctpPacketResult, + medium::{MctpMedium, MctpMediumFrame}, +}; + +/// DSP0253 byte-stuffing transform. Stateless ZST. +/// +/// Encode: `0x7E -> [0x7D, 0x5E]`, `0x7D -> [0x7D, 0x5D]`, any other +/// byte -> `[b]`. +/// Decode: `0x7D 0x5E -> 0x7E`, `0x7D 0x5D -> 0x7D`, `0x7D ` -> +/// `InvalidEscape`. +/// +/// Raw `0x7E` in the wire stream is NOT rejected here — that's a +/// framing concern owned by `MctpSerialMedium::deserialize`, which +/// checks the body region for stray flags. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct SerialEncoding; + +impl BufferEncoding for SerialEncoding { + fn write_byte(wire_buf: &mut [u8], byte: u8) -> Result { + match byte { + 0x7E => { + if wire_buf.len() < 2 { + return Err(EncodeError::BufferFull); + } + wire_buf[0] = 0x7D; + wire_buf[1] = 0x5E; + Ok(2) + } + 0x7D => { + if wire_buf.len() < 2 { + return Err(EncodeError::BufferFull); + } + wire_buf[0] = 0x7D; + wire_buf[1] = 0x5D; + Ok(2) + } + b => match wire_buf.first_mut() { + Some(slot) => { + *slot = b; + Ok(1) + } + None => Err(EncodeError::BufferFull), + }, + } + } + + fn read_byte(wire_buf: &[u8]) -> Result<(u8, usize), DecodeError> { + match wire_buf.first().copied() { + None => Err(DecodeError::PrematureEnd), + Some(0x7D) => match wire_buf.get(1).copied() { + None => Err(DecodeError::PrematureEnd), + Some(0x5E) => Ok((0x7E, 2)), + Some(0x5D) => Ok((0x7D, 2)), + Some(_) => Err(DecodeError::InvalidEscape), + }, + // Raw 0x7E falls through here as a 1-byte read; the framing + // layer (`MctpSerialMedium::deserialize`) rejects bare + // 0x7E inside the body region. + Some(b) => Ok((b, 1)), + } + } + + fn wire_size_of(decoded: &[u8]) -> usize { + decoded + .iter() + .map(|&b| if b == 0x7E || b == 0x7D { 2 } else { 1 }) + .sum() + } +} + +/// SP MCTP endpoint id per CONTEXT D-D-06. +pub const SP_EID: crate::endpoint_id::EndpointId = crate::endpoint_id::EndpointId::Id(0x08); +/// EC MCTP endpoint id per CONTEXT D-D-06. +pub const EC_EID: crate::endpoint_id::EndpointId = crate::endpoint_id::EndpointId::Id(0x0A); +/// Maximum DSP0253 packet body size (DECODED bytes, before stuffing). +pub const CONST_MTU: usize = 251; + +const SERIAL_REVISION: u8 = 0x01; +const END_FLAG: u8 = 0x7E; +/// Header bytes: revision + byte_count (decoded body byte count). +const HEADER_LEN: usize = 2; +/// Worst-case trailer wire bytes: 2 stuffed FCS bytes (each may +/// expand 1 -> 2) + 1 end-flag. +const MAX_TRAILER_WIRE: usize = 5; + +// CRC-16/X-25 per DSP0253 §8 (poly 0x1021, init 0xFFFF, refin/refout, +// xorout 0xFFFF). Algorithm catalog entry locked in CONTEXT D-D-02. +// FCS bytes on the wire are MSB-first per DSP0253 §5.2 (overrides +// RFC1662's LSB-first PPP convention). +const FCS_ALGO: crc::Crc = crc::Crc::::new(&crc::CRC_16_IBM_SDLC); + +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct MctpSerialMediumFrame { + pub revision: u8, + /// DECODED body byte count per DSP0253 §6.2 (NOT the wire byte + /// count). Cap = `CONST_MTU` = 251; max u8 = 255, fits comfortably. + pub byte_count: u8, + pub fcs: u16, +} + +impl MctpMediumFrame for MctpSerialMediumFrame { + fn packet_size(&self) -> usize { + // packet_size is the DECODED body byte count — the contract + // used by `MctpPacketContext::deserialize_packet`, which then + // subtracts 4 for the transport header. + self.byte_count as usize + } + + fn reply_context(&self) {} +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct MctpSerialMedium; + +impl MctpMedium for MctpSerialMedium { + type Frame = MctpSerialMediumFrame; + type Error = &'static str; + type ReplyContext = (); + type Encoding = SerialEncoding; + + fn max_message_body_size(&self) -> usize { + CONST_MTU + } + + fn deserialize<'buf>( + &self, + packet: &'buf [u8], + ) -> MctpPacketResult<(Self::Frame, EncodingDecoder<'buf, Self::Encoding>), Self> { + // Minimum frame: 2 header + 0 body + 2 FCS (unstuffed) + 1 end-flag = 5 bytes. + if packet.len() < HEADER_LEN + 3 { + return Err(MctpPacketError::MediumError("packet too short for serial frame")); + } + let revision = packet[0]; + if revision != SERIAL_REVISION { + return Err(MctpPacketError::MediumError("unsupported serial revision")); + } + let byte_count = packet[1]; + if (byte_count as usize) > CONST_MTU { + return Err(MctpPacketError::MediumError("byte_count exceeds MTU")); + } + + // Single forward walk: un-stuff body bytes (count must equal + // `byte_count`), un-stuff 2 FCS bytes, expect end-flag, compare + // CRC. + let body_wire_start = HEADER_LEN; + let mut decoded = [0u8; CONST_MTU]; + let mut decoded_len = 0usize; + let mut wire_pos = 0usize; // offset from body_wire_start + + while decoded_len < byte_count as usize { + let (b, n) = SerialEncoding::read_byte(&packet[body_wire_start + wire_pos..]).map_err(|e| match e { + DecodeError::PrematureEnd => MctpPacketError::MediumError("premature end in body"), + DecodeError::InvalidEscape => MctpPacketError::MediumError("invalid escape in body"), + })?; + if b == END_FLAG && n == 1 { + // Bare (unstuffed) 0x7E inside the body region is a + // protocol error (MEDIUM-05). A decoded 0x7E whose wire + // representation was the stuffed pair `0x7D 0x5E` + // (n==2) is a legitimate payload byte and is kept. + return Err(MctpPacketError::MediumError("unexpected 0x7E in body")); + } + decoded[decoded_len] = b; + decoded_len += 1; + wire_pos += n; + } + let body_wire_end = body_wire_start + wire_pos; + + // Un-stuff 2 FCS bytes (DSP0253 §7.1 stuffing applies to FCS). + let (fcs_msb, n_msb) = SerialEncoding::read_byte(&packet[body_wire_end..]) + .map_err(|_| MctpPacketError::MediumError("invalid escape in fcs"))?; + let (fcs_lsb, n_lsb) = SerialEncoding::read_byte(&packet[body_wire_end + n_msb..]) + .map_err(|_| MctpPacketError::MediumError("invalid escape in fcs"))?; + let trailer_pos = body_wire_end + n_msb + n_lsb; + + if trailer_pos >= packet.len() || packet[trailer_pos] != END_FLAG { + return Err(MctpPacketError::MediumError("missing end flag")); + } + if trailer_pos + 1 != packet.len() { + return Err(MctpPacketError::MediumError("trailing bytes after end flag")); + } + + // FCS-16/X-25 over un-stuffed (revision || byte_count || decoded body). + let mut digest = FCS_ALGO.digest(); + digest.update(&[revision, byte_count]); + digest.update(&decoded[..decoded_len]); + let computed_fcs = digest.finalize(); + // DSP0253 §5.2: MSB first on wire. + let wire_fcs = u16::from_be_bytes([fcs_msb, fcs_lsb]); + if wire_fcs != computed_fcs { + return Err(MctpPacketError::MediumError("fcs mismatch")); + } + + Ok(( + MctpSerialMediumFrame { + revision, + byte_count, + fcs: wire_fcs, + }, + EncodingDecoder::::new(&packet[body_wire_start..body_wire_end]), + )) + } + + fn serialize<'buf, F>( + &self, + _reply_context: Self::ReplyContext, + buffer: &'buf mut [u8], + message_writer: F, + ) -> MctpPacketResult<&'buf [u8], Self> + where + F: for<'a> FnOnce(&mut EncodingEncoder<'a, Self::Encoding>) -> MctpPacketResult<(), Self>, + { + if buffer.len() < HEADER_LEN + MAX_TRAILER_WIRE { + return Err(MctpPacketError::MediumError("buffer too small for serial frame")); + } + let buffer_len = buffer.len(); + + // Run closure over body region (reserve worst-case 5-byte + // trailer). The encoder stuffs body bytes via + // `SerialEncoding::write_byte` automatically. + let body_wire_len = { + let body_buf = &mut buffer[HEADER_LEN..buffer_len - MAX_TRAILER_WIRE]; + let mut encoder = EncodingEncoder::::new(body_buf); + message_writer(&mut encoder)?; + encoder.wire_position() + }; + + // Re-decode body to recover DECODED bytes + decoded count for + // `byte_count` and FCS. CONTEXT D-B-02 acknowledges the + // double-walk; ~250 bytes max, no_std, cheap. + let mut decoded = [0u8; CONST_MTU]; + let mut decoded_len = 0usize; + let mut wire_pos = 0usize; + while wire_pos < body_wire_len { + let (b, n) = SerialEncoding::read_byte(&buffer[HEADER_LEN + wire_pos..HEADER_LEN + body_wire_len]) + .map_err(|_| MctpPacketError::MediumError("internal: failed to re-decode body"))?; + if decoded_len >= CONST_MTU { + return Err(MctpPacketError::MediumError("body exceeds MTU")); + } + decoded[decoded_len] = b; + decoded_len += 1; + wire_pos += n; + } + // Should not fire — `EncodingEncoder::write` returns + // `BufferFull` long before decoded_len could exceed 251. + if decoded_len > u8::MAX as usize { + return Err(MctpPacketError::MediumError("body exceeds byte_count u8 cap")); + } + let byte_count = decoded_len as u8; + + // FCS-16/X-25 over un-stuffed (revision || byte_count || decoded body). + let mut digest = FCS_ALGO.digest(); + digest.update(&[SERIAL_REVISION, byte_count]); + digest.update(&decoded[..decoded_len]); + let fcs = digest.finalize(); + // DSP0253 §5.2: MSB first on wire. + let [fcs_msb, fcs_lsb] = fcs.to_be_bytes(); + + // Header: revision + byte_count emitted directly (NOT stuffed), + // matching `SmbusEspiMedium`'s header pattern. See PLAN + // note for the conformance caveat when byte_count + // happens to equal 0x7E or 0x7D — round-trips cleanly through + // this implementation's deserialize. + buffer[0] = SERIAL_REVISION; + buffer[1] = byte_count; + + // Stuff and write FCS bytes via SerialEncoding (DSP0253 §7.1 + + // CONTEXT D-B-02 — deserialize un-stuffs FCS, so serialize + // must stuff). + let fcs_start = HEADER_LEN + body_wire_len; + let n_msb = SerialEncoding::write_byte(&mut buffer[fcs_start..], fcs_msb) + .map_err(|_| MctpPacketError::MediumError("internal: failed to encode fcs"))?; + let n_lsb = SerialEncoding::write_byte(&mut buffer[fcs_start + n_msb..], fcs_lsb) + .map_err(|_| MctpPacketError::MediumError("internal: failed to encode fcs"))?; + let end_pos = fcs_start + n_msb + n_lsb; + + // End-flag is written directly (flags are NOT stuffed by + // definition). + buffer[end_pos] = END_FLAG; + + Ok(&buffer[..end_pos + 1]) + } +} + +#[cfg(test)] +mod encoding_tests { + use super::*; + use crate::buffer_encoding::EncodingDecoder; + + #[test] + fn write_byte_stuffs_7e() { + let mut buf = [0u8; 4]; + let n = SerialEncoding::write_byte(&mut buf, 0x7E).unwrap(); + assert_eq!(n, 2); + assert_eq!(&buf[..2], &[0x7D, 0x5E]); + } + + #[test] + fn write_byte_stuffs_7d() { + let mut buf = [0u8; 4]; + let n = SerialEncoding::write_byte(&mut buf, 0x7D).unwrap(); + assert_eq!(n, 2); + assert_eq!(&buf[..2], &[0x7D, 0x5D]); + } + + #[test] + fn write_byte_passthrough_plain() { + let mut buf = [0u8; 1]; + let n = SerialEncoding::write_byte(&mut buf, 0x41).unwrap(); + assert_eq!(n, 1); + assert_eq!(buf, [0x41]); + } + + #[test] + fn write_byte_full_buffer_plain() { + let mut buf = []; + assert_eq!( + SerialEncoding::write_byte(&mut buf, 0x41).unwrap_err(), + EncodeError::BufferFull + ); + } + + #[test] + fn write_byte_full_buffer_escape() { + let mut buf = [0u8; 1]; + assert_eq!( + SerialEncoding::write_byte(&mut buf, 0x7E).unwrap_err(), + EncodeError::BufferFull + ); + } + + #[test] + fn read_byte_unstuffs_7e() { + assert_eq!(SerialEncoding::read_byte(&[0x7D, 0x5E]).unwrap(), (0x7E, 2)); + } + + #[test] + fn read_byte_unstuffs_7d() { + assert_eq!(SerialEncoding::read_byte(&[0x7D, 0x5D]).unwrap(), (0x7D, 2)); + } + + #[test] + fn read_byte_passthrough_plain() { + assert_eq!(SerialEncoding::read_byte(&[0x41]).unwrap(), (0x41, 1)); + } + + #[test] + fn read_byte_raw_7e_passes_through() { + // Raw 0x7E is NOT rejected at the encoding layer — framing is + // the framing layer's concern. + assert_eq!(SerialEncoding::read_byte(&[0x7E]).unwrap(), (0x7E, 1)); + } + + #[test] + fn read_byte_premature_end_empty() { + assert_eq!(SerialEncoding::read_byte(&[]).unwrap_err(), DecodeError::PrematureEnd); + } + + #[test] + fn read_byte_premature_end_after_escape() { + assert_eq!( + SerialEncoding::read_byte(&[0x7D]).unwrap_err(), + DecodeError::PrematureEnd + ); + } + + #[test] + fn read_byte_invalid_escape() { + assert_eq!( + SerialEncoding::read_byte(&[0x7D, 0xAA]).unwrap_err(), + DecodeError::InvalidEscape + ); + } + + #[test] + fn wire_size_of_mixed() { + assert_eq!(SerialEncoding::wire_size_of(&[0x41, 0x7E, 0x42, 0x7D, 0x43]), 7); + } + + #[test] + fn wire_size_of_empty() { + assert_eq!(SerialEncoding::wire_size_of(&[]), 0); + } + + #[test] + fn roundtrip_all_byte_values() { + // 256-byte payload of every byte value, encoded into a 512-byte + // wire buffer (worst case is 2x expansion if every byte stuffs; + // actual expansion here is 256 + 2 = 258 wire bytes). + let mut decoded = [0u8; 256]; + for (i, slot) in decoded.iter_mut().enumerate() { + *slot = i as u8; + } + let mut wire = [0u8; 512]; + let mut wpos = 0usize; + for &b in &decoded { + wpos += SerialEncoding::write_byte(&mut wire[wpos..], b).unwrap(); + } + assert_eq!(wpos, SerialEncoding::wire_size_of(&decoded)); + let mut dec = EncodingDecoder::::new(&wire[..wpos]); + for &expected in &decoded { + assert_eq!(dec.read().unwrap(), expected); + } + assert_eq!(dec.read().unwrap_err(), DecodeError::PrematureEnd); + } +} + +#[cfg(test)] +mod fixtures { + //! Hand-authored DSP0253 serial frame fixtures (golden vectors). + //! + //! Layout per fixture (no leading flag — this implementation omits + //! the open `0x7E` per CONTEXT D-D-01; upstream UART layer supplies + //! it in Phase 27): + //! + //! `[REVISION=0x01, byte_count, ...stuffed body..., ...stuffed FCS-MSB..., ...stuffed + //! FCS-LSB..., 0x7E]` + //! + //! - Header bytes (REVISION, byte_count) are NOT stuffed (matches production serialize). + //! - Body bytes are stuffed per `SerialEncoding`. + //! - FCS-16/X-25 computed over un-stuffed `[REVISION, byte_count, ...decoded body...]`, emitted + //! MSB-first on wire (DSP0253 §5.2), each FCS byte then stuffed if equal to 0x7E or 0x7D. + //! - Trailing `0x7E` is the end-flag (not stuffed by definition). + + pub(crate) const FIXTURE_BASIC_RX: &[u8] = &[0x01, 0x04, 0xAA, 0xBB, 0xCC, 0xDD, 0x6D, 0xA1, 0x7E]; + + pub(crate) const FIXTURE_PAYLOAD_CONTAINS_7E: &[u8] = &[0x01, 0x03, 0xAA, 0x7D, 0x5E, 0xCC, 0xFB, 0xE7, 0x7E]; + + pub(crate) const FIXTURE_PAYLOAD_CONTAINS_7D: &[u8] = &[0x01, 0x03, 0xAA, 0x7D, 0x5D, 0xCC, 0xD1, 0x8F, 0x7E]; + + pub(crate) const FIXTURE_PAYLOAD_CONTAINS_BOTH: &[u8] = + &[0x01, 0x03, 0x7D, 0x5E, 0x7D, 0x5D, 0x42, 0x50, 0x97, 0x7E]; + + /// 251-byte body `(0..251)` decoded; wire = 258 bytes after stuffing + /// the lone 0x7D (idx 125) and 0x7E (idx 126) inside the body. + pub(crate) const FIXTURE_MAX_MTU_FRAME: &[u8] = &[ + 0x01, 0xFB, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, + 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F, 0x20, 0x21, + 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2A, 0x2B, 0x2C, 0x2D, 0x2E, 0x2F, 0x30, 0x31, 0x32, 0x33, + 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3A, 0x3B, 0x3C, 0x3D, 0x3E, 0x3F, 0x40, 0x41, 0x42, 0x43, 0x44, 0x45, + 0x46, 0x47, 0x48, 0x49, 0x4A, 0x4B, 0x4C, 0x4D, 0x4E, 0x4F, 0x50, 0x51, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57, + 0x58, 0x59, 0x5A, 0x5B, 0x5C, 0x5D, 0x5E, 0x5F, 0x60, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, + 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x6F, 0x70, 0x71, 0x72, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7A, 0x7B, + 0x7C, 0x7D, 0x5D, 0x7D, 0x5E, 0x7F, 0x80, 0x81, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, 0x8A, 0x8B, + 0x8C, 0x8D, 0x8E, 0x8F, 0x90, 0x91, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, 0x99, 0x9A, 0x9B, 0x9C, 0x9D, + 0x9E, 0x9F, 0xA0, 0xA1, 0xA2, 0xA3, 0xA4, 0xA5, 0xA6, 0xA7, 0xA8, 0xA9, 0xAA, 0xAB, 0xAC, 0xAD, 0xAE, 0xAF, + 0xB0, 0xB1, 0xB2, 0xB3, 0xB4, 0xB5, 0xB6, 0xB7, 0xB8, 0xB9, 0xBA, 0xBB, 0xBC, 0xBD, 0xBE, 0xBF, 0xC0, 0xC1, + 0xC2, 0xC3, 0xC4, 0xC5, 0xC6, 0xC7, 0xC8, 0xC9, 0xCA, 0xCB, 0xCC, 0xCD, 0xCE, 0xCF, 0xD0, 0xD1, 0xD2, 0xD3, + 0xD4, 0xD5, 0xD6, 0xD7, 0xD8, 0xD9, 0xDA, 0xDB, 0xDC, 0xDD, 0xDE, 0xDF, 0xE0, 0xE1, 0xE2, 0xE3, 0xE4, 0xE5, + 0xE6, 0xE7, 0xE8, 0xE9, 0xEA, 0xEB, 0xEC, 0xED, 0xEE, 0xEF, 0xF0, 0xF1, 0xF2, 0xF3, 0xF4, 0xF5, 0xF6, 0xF7, + 0xF8, 0xF9, 0xFA, 0xF6, 0x07, 0x7E, + ]; + + pub(crate) const FIXTURE_EMPTY_PAYLOAD: &[u8] = &[0x01, 0x00, 0x16, 0x9F, 0x7E]; + + pub(crate) const FIXTURE_FCS_VALID: &[u8] = &[0x01, 0x03, 0x10, 0x20, 0x30, 0x76, 0xDB, 0x7E]; + + /// Same body as FCS_VALID but FCS-MSB byte XOR 0xFF (0x76 -> 0x89). + pub(crate) const FIXTURE_FCS_INVALID: &[u8] = &[0x01, 0x03, 0x10, 0x20, 0x30, 0x89, 0xDB, 0x7E]; + + /// byte_count=2 claims 2 decoded bytes; first body wire byte is + /// `0x7D 0xAA` (escape followed by non-`{0x5E,0x5D}`) -> rejected + /// as "invalid escape in body" before reaching FCS. + pub(crate) const FIXTURE_INVALID_ESCAPE: &[u8] = &[0x01, 0x02, 0x7D, 0xAA, 0x00, 0x00, 0x7E]; + + /// byte_count=3, body wire region is `[0xAA, 0x7E, 0xCC]` — the + /// raw 0x7E inside the body region is rejected before FCS. + pub(crate) const FIXTURE_PREMATURE_END_FLAG: &[u8] = &[0x01, 0x03, 0xAA, 0x7E, 0xCC, 0x00, 0x00, 0x7E]; +} + +#[cfg(test)] +mod medium_tests { + use super::{fixtures::*, *}; + + fn drain_decoder(mut dec: EncodingDecoder<'_, SerialEncoding>) -> ([u8; CONST_MTU], usize) { + let mut out = [0u8; CONST_MTU]; + let mut n = 0; + while let Ok(b) = dec.read() { + out[n] = b; + n += 1; + } + (out, n) + } + + #[test] + fn decode_basic_rx_succeeds() { + let (frame, dec) = MctpSerialMedium.deserialize(FIXTURE_BASIC_RX).unwrap(); + assert_eq!(frame.revision, 0x01); + assert_eq!(frame.byte_count, 4); + assert_eq!(frame.fcs, 0x6DA1); + let (decoded, n) = drain_decoder(dec); + assert_eq!(&decoded[..n], &[0xAA, 0xBB, 0xCC, 0xDD]); + } + + #[test] + fn decode_payload_contains_7e() { + let (frame, dec) = MctpSerialMedium.deserialize(FIXTURE_PAYLOAD_CONTAINS_7E).unwrap(); + assert_eq!(frame.byte_count, 3); + let (decoded, n) = drain_decoder(dec); + assert_eq!(&decoded[..n], &[0xAA, 0x7E, 0xCC]); + } + + #[test] + fn decode_payload_contains_7d() { + let (frame, dec) = MctpSerialMedium.deserialize(FIXTURE_PAYLOAD_CONTAINS_7D).unwrap(); + assert_eq!(frame.byte_count, 3); + let (decoded, n) = drain_decoder(dec); + assert_eq!(&decoded[..n], &[0xAA, 0x7D, 0xCC]); + } + + #[test] + fn decode_payload_contains_both() { + let (frame, dec) = MctpSerialMedium.deserialize(FIXTURE_PAYLOAD_CONTAINS_BOTH).unwrap(); + assert_eq!(frame.byte_count, 3); + let (decoded, n) = drain_decoder(dec); + assert_eq!(&decoded[..n], &[0x7E, 0x7D, 0x42]); + } + + #[test] + fn decode_max_mtu_frame() { + let (frame, dec) = MctpSerialMedium.deserialize(FIXTURE_MAX_MTU_FRAME).unwrap(); + assert_eq!(frame.byte_count as usize, CONST_MTU); + let (decoded, n) = drain_decoder(dec); + assert_eq!(n, CONST_MTU); + for (i, &b) in decoded[..n].iter().enumerate() { + assert_eq!(b, i as u8, "mismatch at idx {i}"); + } + } + + #[test] + fn decode_empty_payload() { + let (frame, dec) = MctpSerialMedium.deserialize(FIXTURE_EMPTY_PAYLOAD).unwrap(); + assert_eq!(frame.byte_count, 0); + let (_, n) = drain_decoder(dec); + assert_eq!(n, 0); + } + + #[test] + fn decode_fcs_valid() { + assert!(MctpSerialMedium.deserialize(FIXTURE_FCS_VALID).is_ok()); + } + + #[test] + fn decode_fcs_invalid_rejects() { + match MctpSerialMedium.deserialize(FIXTURE_FCS_INVALID) { + Err(crate::MctpPacketError::MediumError("fcs mismatch")) => {} + other => panic!("expected MediumError(\"fcs mismatch\"), got {:?}", other.err()), + } + } + + #[test] + fn decode_invalid_escape_rejects() { + match MctpSerialMedium.deserialize(FIXTURE_INVALID_ESCAPE) { + Err(crate::MctpPacketError::MediumError("invalid escape in body")) => {} + other => panic!( + "expected MediumError(\"invalid escape in body\"), got {:?}", + other.err() + ), + } + } + + #[test] + fn decode_premature_end_flag_rejects() { + match MctpSerialMedium.deserialize(FIXTURE_PREMATURE_END_FLAG) { + Err(crate::MctpPacketError::MediumError("unexpected 0x7E in body")) => {} + other => panic!( + "expected MediumError(\"unexpected 0x7E in body\"), got {:?}", + other.err() + ), + } + } + + fn fixture_roundtrip(wire: &[u8]) { + let m = MctpSerialMedium; + let (_frame, dec) = m.deserialize(wire).unwrap(); + let (decoded, n) = drain_decoder(dec); + let mut out = [0u8; 1024]; + let serialized = m + .serialize((), &mut out, |e| { + e.write_all(&decoded[..n]) + .map_err(|_| MctpPacketError::MediumError("write failed")) + }) + .unwrap(); + assert_eq!(serialized, wire); + } + + #[test] + fn fixture_roundtrip_basic_rx() { + fixture_roundtrip(FIXTURE_BASIC_RX); + } + + #[test] + fn fixture_roundtrip_payload_contains_7e() { + fixture_roundtrip(FIXTURE_PAYLOAD_CONTAINS_7E); + } + + #[test] + fn fixture_roundtrip_payload_contains_7d() { + fixture_roundtrip(FIXTURE_PAYLOAD_CONTAINS_7D); + } + + #[test] + fn fixture_roundtrip_payload_contains_both() { + fixture_roundtrip(FIXTURE_PAYLOAD_CONTAINS_BOTH); + } + + #[test] + fn fixture_roundtrip_max_mtu_frame() { + fixture_roundtrip(FIXTURE_MAX_MTU_FRAME); + } + + #[test] + fn fixture_roundtrip_empty_payload() { + fixture_roundtrip(FIXTURE_EMPTY_PAYLOAD); + } + + #[test] + fn fixture_roundtrip_fcs_valid() { + fixture_roundtrip(FIXTURE_FCS_VALID); + } + + #[test] + fn public_api_smoke() { + let _: crate::MctpSerialMedium = crate::MctpSerialMedium; + let _: crate::SerialEncoding = crate::SerialEncoding; + assert_eq!(crate::CONST_MTU, 251); + assert_eq!(crate::SP_EID, crate::EndpointId::Id(0x08)); + assert_eq!(crate::EC_EID, crate::EndpointId::Id(0x0A)); + } + + #[test] + fn packetize_with_stuffing_respects_mtu() { + // 251-byte payload of all 0x7E. Each byte stuffs to 2 wire + // bytes (0x7D 0x5E), so encoded body footprint per packet is + // 2x decoded length. The packet body MTU is 251 wire bytes; + // each MCTP packet also carries a 4-byte transport header + // which itself is `wire_size_of`-measured. Expect the message + // to split across multiple packets and no body region to + // exceed CONST_MTU wire bytes. + use crate::{ + endpoint_id::EndpointId, mctp_message_tag::MctpMessageTag, mctp_packet_context::MctpReplyContext, + mctp_sequence_number::MctpSequenceNumber, serialize::SerializePacketState, + }; + + let payload = [0x7E_u8; 251]; + let mut assembly = [0u8; 1024]; + let medium = MctpSerialMedium; + let reply_context = MctpReplyContext:: { + destination_endpoint_id: EndpointId::Id(0x0A), + source_endpoint_id: EndpointId::Id(0x08), + packet_sequence_number: MctpSequenceNumber::new(0), + message_tag: MctpMessageTag::default(), + medium_context: (), + }; + let mut state = SerializePacketState { + medium: &medium, + reply_context, + current_packet_num: 0, + serialized_message_header: false, + message_buffer: &payload[..], + assembly_buffer: &mut assembly[..], + }; + + let mut total_decoded_body = 0usize; + let mut packet_count = 0usize; + loop { + // We cannot iterate `state.next()` more than once because + // `next` mutably borrows the assembly buffer for each + // returned slice. Take one packet, process it, then break. + let pkt = match state.next() { + Some(Ok(pkt)) => { + let mut tmp = [0u8; 1024]; + tmp[..pkt.len()].copy_from_slice(pkt); + (tmp, pkt.len()) + } + Some(Err(e)) => panic!("serialize error: {e:?}"), + None => break, + }; + packet_count += 1; + // Deserialize the packet to recover the wire body length + // and the decoded body byte count. + let (frame, dec) = medium.deserialize(&pkt.0[..pkt.1]).unwrap(); + // Decoded body byte count INCLUDES the 4 transport-header + // bytes — subtract to get the actual payload bytes. + assert!(frame.byte_count as usize >= 4); + let payload_decoded = frame.byte_count as usize - 4; + total_decoded_body += payload_decoded; + // Wire body region (between header and FCS) MUST be <= + // CONST_MTU under MEDIUM-08 chunk-sizing. + let _ = dec; // decoder discard + let wire_body_len = pkt.1 - 2 /* hdr */ - 1 /* end-flag */; + // Subtract the (possibly stuffed) FCS bytes — they are 2 + // FCS bytes but each may stuff to 2 wire bytes. Worst case + // 4 bytes; lower bound on body wire = wire_body_len - 4. + assert!( + wire_body_len <= CONST_MTU + 4, + "packet {packet_count} body exceeds MTU + worst-case FCS: {wire_body_len}" + ); + } + assert!(packet_count >= 2, "expected multi-packet split, got {packet_count}"); + assert_eq!(total_decoded_body, payload.len()); + } +} diff --git a/mctp-rs/src/medium/smbus_espi.rs b/mctp-rs/src/medium/smbus_espi.rs new file mode 100644 index 000000000..eeeb5979f --- /dev/null +++ b/mctp-rs/src/medium/smbus_espi.rs @@ -0,0 +1,848 @@ +use bit_register::{NumBytes, TryFromBits, TryIntoBits, bit_register}; + +use crate::{ + MctpPacketError, + buffer_encoding::{EncodingDecoder, EncodingEncoder, PassthroughEncoding}, + error::MctpPacketResult, + medium::{ + MctpMedium, MctpMediumFrame, + util::{One, Zero}, + }, +}; + +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct SmbusEspiMedium; + +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct SmbusEspiReplyContext { + pub destination_slave_address: u8, + pub source_slave_address: u8, +} + +impl MctpMedium for SmbusEspiMedium { + type Frame = SmbusEspiMediumFrame; + type Error = &'static str; + type ReplyContext = SmbusEspiReplyContext; + type Encoding = PassthroughEncoding; + + fn deserialize<'buf>( + &self, + packet: &'buf [u8], + ) -> MctpPacketResult<(Self::Frame, EncodingDecoder<'buf, Self::Encoding>), Self> { + // Check if packet has enough bytes for header + if packet.len() < 4 { + return Err(MctpPacketError::MediumError("Packet too short to parse smbus header")); + } + + let header_value = u32::from_be_bytes( + packet[0..4] + .try_into() + .map_err(|_| MctpPacketError::MediumError("Packet too short to parse smbus header"))?, + ); + // strip off the smbus header + let packet = &packet[4..]; + let header = SmbusEspiMediumHeader::try_from(header_value) + .map_err(|_| MctpPacketError::MediumError("Invalid smbus header"))?; + if header.byte_count as usize + 1 > packet.len() { + return Err(MctpPacketError::MediumError( + "Packet too short to parse smbus body and PEC", + )); + } + let pec = packet[header.byte_count as usize]; + // strip off the PEC byte; the inner stuffed region is the body bytes + let inner = &packet[..header.byte_count as usize]; + Ok((SmbusEspiMediumFrame { header, pec }, EncodingDecoder::new(inner))) + } + + fn serialize<'buf, F>( + &self, + reply_context: Self::ReplyContext, + buffer: &'buf mut [u8], + message_writer: F, + ) -> MctpPacketResult<&'buf [u8], Self> + where + F: for<'a> FnOnce(&mut EncodingEncoder<'a, Self::Encoding>) -> MctpPacketResult<(), Self>, + { + // Reserve space for header (4 bytes) and PEC (1 byte) + if buffer.len() < 5 { + return Err(MctpPacketError::MediumError("Buffer too small for smbus frame")); + } + let buffer_len = buffer.len(); + + // Write the body first via an encoder over the body region (reserve + // 4 leading header bytes and 1 trailing PEC byte). + let body_wire_len = { + let body_buf = &mut buffer[4..buffer_len - 1]; + let mut encoder = EncodingEncoder::::new(body_buf); + message_writer(&mut encoder)?; + encoder.wire_position() + }; + + // with the body has been written, construct the header. byte_count + // is the number of wire bytes that follow on the line per SMBus + // (PassthroughEncoding pairing means wire byte count == decoded + // byte count for SMBus today). + let header = SmbusEspiMediumHeader { + destination_slave_address: reply_context.source_slave_address, + source_slave_address: reply_context.destination_slave_address, + byte_count: body_wire_len as u8, + command_code: SmbusCommandCode::Mctp, + ..Default::default() + }; + let header_value = TryInto::::try_into(header).map_err(MctpPacketError::MediumError)?; + buffer[0..4].copy_from_slice(&header_value.to_be_bytes()); + + // with the header written, compute the PEC byte + let pec_value = smbus_pec::pec(&buffer[0..4 + body_wire_len]); + buffer[4 + body_wire_len] = pec_value; + + // add 4 for frame header, add 1 for PEC byte + Ok(&buffer[0..4 + body_wire_len + 1]) + } + + // TODO - this is a guess, need to find the actual value from spec + fn max_message_body_size(&self) -> usize { + 32 + } +} + +#[repr(u8)] +#[derive(Debug, Copy, Clone, PartialEq, Eq, num_enum::IntoPrimitive, num_enum::TryFromPrimitive, Default)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +enum SmbusCommandCode { + #[default] + Mctp = 0x0F, +} +impl TryFromBits for SmbusCommandCode { + fn try_from_bits(bits: u32) -> Result { + if bits > 0xFF { + Err("Command code out of range") + } else { + SmbusCommandCode::try_from(bits as u8).map_err(|_| "Invalid command code") + } + } +} +impl TryIntoBits for SmbusCommandCode { + fn try_into_bits(self) -> Result { + Ok(Into::::into(self) as u32) + } +} +impl NumBytes for SmbusCommandCode { + const NUM_BYTES: usize = 1; +} + +// SMBus header per documentation in eSPI spec: https://cdrdv2-public.intel.com/841685/841685_ESPI_IBS_TS_Rev_1_6.pdf +// See figure 46 on page 74. This struct corresponds to bytes 3..=6 of the sample OOB MCTP packet +// frame. +bit_register! { + #[derive(Copy, Clone, PartialEq, Eq, Default, Debug)] + #[cfg_attr(feature = "defmt", derive(defmt::Format))] + struct SmbusEspiMediumHeader: little_endian u32 { + pub destination_slave_address: u8 => [25:31], + pub _reserved1: Zero => [24], + pub command_code: SmbusCommandCode => [16:23], + pub byte_count: u8 => [8:15], + pub source_slave_address: u8 => [1:7], + pub _reserved2: One => [0], + } +} + +#[derive(Copy, Clone, PartialEq, Eq, Debug)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct SmbusEspiMediumFrame { + header: SmbusEspiMediumHeader, + pec: u8, +} + +impl SmbusEspiReplyContext { + fn new(frame: SmbusEspiMediumFrame) -> Self { + Self { + destination_slave_address: frame.header.destination_slave_address, + source_slave_address: frame.header.source_slave_address, + } + } +} + +impl MctpMediumFrame for SmbusEspiMediumFrame { + fn packet_size(&self) -> usize { + self.header.byte_count as usize + } + + fn reply_context(&self) -> SmbusEspiReplyContext { + SmbusEspiReplyContext::new(*self) + } +} + +#[cfg(test)] +mod tests { + extern crate std; + use std::vec::Vec; + + use super::*; + use crate::buffer_encoding::DecodeError; + + /// Test-only helper: drain an `EncodingDecoder` to a `Vec` for + /// content assertions. Stops at the first error (e.g., `PrematureEnd`). + fn drain_to_vec(decoder: &mut EncodingDecoder<'_, PassthroughEncoding>) -> Vec { + let mut out = Vec::new(); + while let Ok(b) = decoder.read() { + out.push(b); + } + out + } + + #[test] + fn test_deserialize_valid_packet() { + let medium = SmbusEspiMedium; + + // Create a valid SMBus packet with little-endian header + // destination_slave_address: 0x20, source_slave_address: 0x10, command: 0x0F, byte_count: 4 + let header = SmbusEspiMediumHeader { + destination_slave_address: 0x20, + source_slave_address: 0x10, + command_code: SmbusCommandCode::Mctp, + byte_count: 4, + ..Default::default() + }; + let header_value: u32 = header.try_into().unwrap(); + let header_bytes = header_value.to_be_bytes(); + + let payload = [0xAA, 0xBB, 0xCC, 0xDD]; // 4 bytes as specified by byte_count + let mut combined = [0u8; 8]; + combined[0..4].copy_from_slice(&header_bytes); + combined[4..8].copy_from_slice(&payload); + let pec = smbus_pec::pec(&combined); + + let mut packet = [0u8; 9]; + packet[0..4].copy_from_slice(&header_bytes); + packet[4..8].copy_from_slice(&payload); + packet[8] = pec; + + let result = medium.deserialize(&packet).unwrap(); + let (frame, mut decoder) = result; + let body = drain_to_vec(&mut decoder); + + assert_eq!(frame.header.destination_slave_address, 0x20); + assert_eq!(frame.header.source_slave_address, 0x10); + assert_eq!(frame.header.command_code, SmbusCommandCode::Mctp); + assert_eq!(frame.header.byte_count, 4); + assert_eq!(frame.pec, pec); + assert_eq!(body, payload); + } + + #[test] + fn test_deserialize_packet_too_short_header() { + let medium = SmbusEspiMedium; + let short_packet = [0x01, 0x02]; // Only 2 bytes, need at least 4 for header + + let err = medium.deserialize(&short_packet).err().unwrap(); + assert_eq!( + err, + MctpPacketError::MediumError("Packet too short to parse smbus header") + ); + } + + #[test] + fn test_deserialize_packet_too_short_body() { + let medium = SmbusEspiMedium; + + // Header indicates 10 bytes of data but we only provide 2 + let header_bytes = [ + 0x20, // destination_slave_address + 0x0F, // command_code (MCTP) + 0x0A, // byte_count: 10 bytes + 0x21, // source_slave_address + ]; + + let short_payload = [0xAA, 0xBB]; // Only 2 bytes, but header says 10 + + let mut packet = [0u8; 6]; + packet[0..4].copy_from_slice(&header_bytes); + packet[4..6].copy_from_slice(&short_payload); + + let err = medium.deserialize(&packet).err().unwrap(); + assert_eq!( + err, + MctpPacketError::MediumError("Packet too short to parse smbus body and PEC") + ); + } + + #[test] + fn test_deserialize_invalid_header() { + let medium = SmbusEspiMedium; + + // Create invalid header with command code that's not MCTP + let invalid_header_bytes = [ + 0x20, // destination_slave_address + 0xFF, // invalid command_code (not 0x0F) + 0x04, // byte_count + 0x20, // source_slave_address + ]; + + let payload = [0xAA, 0xBB, 0xCC, 0xDD]; + let pec = 0x00; // PEC doesn't matter for this test + + let mut packet = [0u8; 9]; + packet[0..4].copy_from_slice(&invalid_header_bytes); + packet[4..8].copy_from_slice(&payload); + packet[8] = pec; + + let err = medium.deserialize(&packet).err().unwrap(); + assert_eq!(err, MctpPacketError::MediumError("Invalid smbus header")); + } + + #[test] + fn test_deserialize_zero_byte_count() { + let medium = SmbusEspiMedium; + + let header_bytes = [ + 0x20, // destination_slave_address + 0x0F, // command_code (MCTP) + 0x00, // byte_count: 0 bytes + 0x21, // source_slave_address + ]; + + let pec = smbus_pec::pec(&header_bytes); + + let mut packet = [0u8; 5]; + packet[0..4].copy_from_slice(&header_bytes); + packet[4] = pec; + + let result = medium.deserialize(&packet).unwrap(); + let (frame, mut decoder) = result; + + assert_eq!(frame.header.byte_count, 0); + assert_eq!(frame.pec, pec); + assert_eq!(decoder.read().unwrap_err(), DecodeError::PrematureEnd); + } + + #[test] + fn test_serialize_valid_packet() { + let medium = SmbusEspiMedium; + let reply_context = SmbusEspiReplyContext { + destination_slave_address: 0x20, + source_slave_address: 0x10, + }; + + let mut buffer = [0u8; 64]; + let test_payload = [0xAA, 0xBB, 0xCC, 0xDD]; + + let result = medium + .serialize(reply_context, &mut buffer, |encoder| { + encoder + .write_all(&test_payload) + .map_err(|_| MctpPacketError::SerializeError("encode error")) + }) + .unwrap(); + + // Verify the serialized packet structure + // Header: 4 bytes + payload: 4 bytes + PEC: 1 byte = 9 bytes total + assert_eq!(result.len(), 9); + + // Parse the header to verify correctness + let header_value = u32::from_be_bytes([result[0], result[1], result[2], result[3]]); + let header = SmbusEspiMediumHeader::try_from(header_value).unwrap(); + + // Note: destination and source are swapped in reply + assert_eq!(header.destination_slave_address, 0x10); // reply_context.source + assert_eq!(header.source_slave_address, 0x20); // reply_context.destination + assert_eq!(header.command_code, SmbusCommandCode::Mctp); + assert_eq!(header.byte_count, 4); + + // Verify payload + assert_eq!(&result[4..8], &test_payload); + + // Verify PEC byte + let expected_pec = smbus_pec::pec(&result[0..8]); + assert_eq!(result[8], expected_pec); + } + + #[test] + fn test_serialize_buffer_too_small() { + let medium = SmbusEspiMedium; + let reply_context = SmbusEspiReplyContext { + destination_slave_address: 0x20, + source_slave_address: 0x10, + }; + + let mut small_buffer = [0u8; 4]; // Only 4 bytes, need at least 5 (header + PEC) + + let err = medium + .serialize(reply_context, &mut small_buffer, |_| Ok(())) + .err() + .unwrap(); + + assert_eq!(err, MctpPacketError::MediumError("Buffer too small for smbus frame")); + } + + #[test] + fn test_serialize_minimal_buffer() { + let medium = SmbusEspiMedium; + let reply_context = SmbusEspiReplyContext { + destination_slave_address: 0x20, + source_slave_address: 0x10, + }; + + let mut minimal_buffer = [0u8; 5]; // Exactly 5 bytes (4 header + 1 PEC) + + let result = medium + .serialize( + reply_context, + &mut minimal_buffer, + |_| Ok(()), // No payload data + ) + .unwrap(); + + assert_eq!(result.len(), 5); + + // Verify header + let header_value = u32::from_be_bytes([result[0], result[1], result[2], result[3]]); + let header = SmbusEspiMediumHeader::try_from(header_value).unwrap(); + assert_eq!(header.byte_count, 0); + + // Verify PEC + let expected_pec = smbus_pec::pec(&result[0..4]); + assert_eq!(result[4], expected_pec); + } + + #[test] + fn test_serialize_max_payload() { + let medium = SmbusEspiMedium; + let reply_context = SmbusEspiReplyContext { + destination_slave_address: 0x20, + source_slave_address: 0x10, + }; + + // Test with maximum payload size (255 bytes as byte_count is u8) + let max_payload = [0x55u8; 255]; + let mut buffer = [0u8; 260]; // 4 + 255 + 1 = header + max payload + PEC + + let result = medium + .serialize(reply_context, &mut buffer, |encoder| { + encoder + .write_all(&max_payload) + .map_err(|_| MctpPacketError::SerializeError("encode error")) + }) + .unwrap(); + + assert_eq!(result.len(), 260); // 4 + 255 + 1 + + // Verify header + let header_value = u32::from_be_bytes([result[0], result[1], result[2], result[3]]); + let header = SmbusEspiMediumHeader::try_from(header_value).unwrap(); + assert_eq!(header.byte_count, 255); + + // Verify payload + assert_eq!(&result[4..259], &max_payload[..]); + + // Verify PEC + let expected_pec = smbus_pec::pec(&result[0..259]); + assert_eq!(result[259], expected_pec); + } + + #[test] + fn test_serialize_message_writer_error() { + let medium = SmbusEspiMedium; + let reply_context = SmbusEspiReplyContext { + destination_slave_address: 0x20, + source_slave_address: 0x10, + }; + + let mut buffer = [0u8; 64]; + + let result = medium.serialize(reply_context, &mut buffer, |_| { + Err(MctpPacketError::MediumError("Test error")) + }); + + assert_eq!(result, Err(MctpPacketError::MediumError("Test error"))); + } + + #[test] + fn test_roundtrip_serialization_deserialization() { + let medium = SmbusEspiMedium; + let original_context = SmbusEspiReplyContext { + destination_slave_address: 0x42, + source_slave_address: 0x24, + }; + + let original_payload = [0x11, 0x22, 0x33, 0x44, 0x55]; + let mut buffer = [0u8; 64]; + + // Serialize + let serialized = medium + .serialize(original_context, &mut buffer, |encoder| { + encoder + .write_all(&original_payload) + .map_err(|_| MctpPacketError::SerializeError("encode error")) + }) + .unwrap(); + + // Deserialize + let (frame, mut decoder) = medium.deserialize(serialized).unwrap(); + let deserialized_payload = drain_to_vec(&mut decoder); + + // Verify roundtrip correctness + assert_eq!(deserialized_payload, original_payload); + assert_eq!(frame.header.destination_slave_address, 0x24); // swapped + assert_eq!(frame.header.source_slave_address, 0x42); // swapped + assert_eq!(frame.header.command_code, SmbusCommandCode::Mctp); + assert_eq!(frame.header.byte_count, original_payload.len() as u8); + + // Verify PEC is correct + let expected_pec = smbus_pec::pec(&serialized[0..serialized.len() - 1]); + assert_eq!(frame.pec, expected_pec); + } + + #[test] + fn test_frame_packet_size() { + let frame = SmbusEspiMediumFrame { + header: SmbusEspiMediumHeader { + byte_count: 42, + ..Default::default() + }, + pec: 0, + }; + + assert_eq!(frame.packet_size(), 42); + } + + #[test] + fn test_frame_reply_context() { + let frame = SmbusEspiMediumFrame { + header: SmbusEspiMediumHeader { + destination_slave_address: 0x30, + source_slave_address: 0x40, + ..Default::default() + }, + pec: 0, + }; + + let context = frame.reply_context(); + assert_eq!(context.destination_slave_address, 0x30); + assert_eq!(context.source_slave_address, 0x40); + } + + #[test] + fn test_smbus_command_code_conversion() { + // Test valid command code + assert_eq!(SmbusCommandCode::try_from_bits(0x0F).unwrap(), SmbusCommandCode::Mctp); + + // Test out of range (> 0xFF) + assert_eq!(SmbusCommandCode::try_from_bits(0x100), Err("Command code out of range")); + + // Test invalid command code + assert_eq!(SmbusCommandCode::try_from_bits(0x10), Err("Invalid command code")); + + // Test conversion to bits + assert_eq!(SmbusCommandCode::Mctp.try_into_bits().unwrap(), 0x0F); + } + + #[test] + fn test_header_bit_register_edge_cases() { + // Test all zeros - this should use default command code + let header = SmbusEspiMediumHeader::default(); + assert_eq!(header.destination_slave_address, 0); + assert_eq!(header.source_slave_address, 0); + assert_eq!(header.byte_count, 0); + assert_eq!(header.command_code, SmbusCommandCode::Mctp); // default + + // Test valid maximum values within bit ranges + let header = SmbusEspiMediumHeader { + destination_slave_address: 0x7F, // 7 bits max (bits 25-31) + source_slave_address: 0x3F, // 6 bits max (bits 1-7, bit 0 reserved) + byte_count: 0xFF, // 8 bits max (bits 8-15) + command_code: SmbusCommandCode::Mctp, + ..Default::default() + }; + + // Verify we can convert to u32 and back + let header_value: u32 = header.try_into().unwrap(); + let reconstructed = SmbusEspiMediumHeader::try_from(header_value).unwrap(); + assert_eq!(reconstructed, header); + } + + #[test] + fn test_pec_calculation_accuracy() { + let medium = SmbusEspiMedium; + let reply_context = SmbusEspiReplyContext { + destination_slave_address: 0x50, + source_slave_address: 0x30, + }; + + // Test with known data to verify PEC calculation + let test_data = [0x01, 0x02, 0x03]; + let mut buffer = [0u8; 32]; + + let result = medium + .serialize(reply_context, &mut buffer, |encoder| { + encoder + .write_all(&test_data) + .map_err(|_| MctpPacketError::SerializeError("encode error")) + }) + .unwrap(); + + // Manually calculate expected PEC and compare + let data_for_pec = &result[0..result.len() - 1]; + let expected_pec = smbus_pec::pec(data_for_pec); + let actual_pec = result[result.len() - 1]; + + assert_eq!(actual_pec, expected_pec); + } + + #[test] + fn test_serialize_with_empty_payload() { + let medium = SmbusEspiMedium; + let reply_context = SmbusEspiReplyContext { + destination_slave_address: 0x60, + source_slave_address: 0x70, + }; + + let mut buffer = [0u8; 16]; + + let result = medium + .serialize( + reply_context, + &mut buffer, + |_| Ok(()), // Empty payload + ) + .unwrap(); + + assert_eq!(result.len(), 5); // 4 bytes header + 1 byte PEC + + // Verify header + let header_value = u32::from_be_bytes([result[0], result[1], result[2], result[3]]); + let header = SmbusEspiMediumHeader::try_from(header_value).unwrap(); + assert_eq!(header.byte_count, 0); + assert_eq!(header.destination_slave_address, 0x70); // swapped + assert_eq!(header.source_slave_address, 0x60); // swapped + + // Verify PEC + let expected_pec = smbus_pec::pec(&result[0..4]); + assert_eq!(result[4], expected_pec); + } + + #[test] + fn test_max_message_body_size() { + let medium = SmbusEspiMedium; + assert_eq!(medium.max_message_body_size(), 32); + } + + #[test] + fn test_address_swapping_in_reply_context() { + // Test that addresses are properly swapped when creating reply context + let original_frame = SmbusEspiMediumFrame { + header: SmbusEspiMediumHeader { + destination_slave_address: 0x2A, // Valid 7-bit address + source_slave_address: 0x3B, // Valid 6-bit address + ..Default::default() + }, + pec: 0, + }; + + let reply_context = SmbusEspiReplyContext::new(original_frame); + assert_eq!(reply_context.destination_slave_address, 0x2A); + assert_eq!(reply_context.source_slave_address, 0x3B); + + // Now test that when we serialize with this context, addresses are swapped back + let medium = SmbusEspiMedium; + let mut buffer = [0u8; 16]; + + let result = medium.serialize(reply_context, &mut buffer, |_| Ok(())).unwrap(); + + let header_value = u32::from_be_bytes([result[0], result[1], result[2], result[3]]); + let response_header = SmbusEspiMediumHeader::try_from(header_value).unwrap(); + + // In the response, source becomes destination and vice versa + assert_eq!(response_header.destination_slave_address, 0x3B); + assert_eq!(response_header.source_slave_address, 0x2A); + } + + #[test] + fn test_deserialize_with_different_byte_counts() { + let medium = SmbusEspiMedium; + + for byte_count in [1, 16, 32, 64, 128, 255] { + let header_bytes = [ + 0x20, // destination_slave_address + 0x0F, // command_code (MCTP) + byte_count, // byte_count + 0x21, // source_slave_address + ]; + + let payload = [0x42u8; 255]; + let payload_slice = &payload[..byte_count as usize]; + + let mut combined = [0u8; 259]; // 4 header + 255 max payload + combined[0..4].copy_from_slice(&header_bytes); + combined[4..4 + byte_count as usize].copy_from_slice(payload_slice); + let pec = smbus_pec::pec(&combined[0..4 + byte_count as usize]); + + let mut packet = [0u8; 260]; // 4 + 255 + 1 + packet[0..4].copy_from_slice(&header_bytes); + packet[4..4 + byte_count as usize].copy_from_slice(payload_slice); + packet[4 + byte_count as usize] = pec; + + let packet_slice = &packet[0..4 + byte_count as usize + 1]; + let result = medium.deserialize(packet_slice).unwrap(); + let (frame, mut decoder) = result; + let body = drain_to_vec(&mut decoder); + + assert_eq!(frame.header.byte_count, byte_count); + assert_eq!(body.len(), byte_count as usize); + assert_eq!(frame.pec, pec); + } + } + + #[test] + fn test_smbus_buffer_overflow_protection() { + let medium = SmbusEspiMedium; + + // Test packet with byte_count that would cause overflow + let header_bytes = [ + 0x20, // destination_slave_address + 0x0F, // command_code (MCTP) + 0xFF, // byte_count: 255 bytes (maximum) + 0x21, // source_slave_address + ]; + + // Provide a packet that's too short for the claimed byte_count + let short_payload = [0xAA, 0xBB]; // Only 2 bytes, but header claims 255 + let mut packet = [0u8; 7]; // 4 header + 2 payload + 1 PEC = 7 total + packet[0..4].copy_from_slice(&header_bytes); + packet[4..6].copy_from_slice(&short_payload); + packet[6] = 0x00; // PEC (doesn't matter for this test) + + let err = medium.deserialize(&packet).err().unwrap(); + assert_eq!( + err, + MctpPacketError::MediumError("Packet too short to parse smbus body and PEC") + ); + } + + #[test] + fn test_smbus_serialize_buffer_underflow() { + let medium = SmbusEspiMedium; + let reply_context = SmbusEspiReplyContext { + destination_slave_address: 0x20, + source_slave_address: 0x10, + }; + + // Test with buffer smaller than minimum required (4 header + 1 PEC = 5 bytes) + let mut tiny_buffer = [0u8; 4]; // Only 4 bytes, need at least 5 + + let err = medium + .serialize(reply_context, &mut tiny_buffer, |_| { + Ok(()) // No payload + }) + .err() + .unwrap(); + + assert_eq!(err, MctpPacketError::MediumError("Buffer too small for smbus frame")); + } + + #[test] + fn test_smbus_header_bounds_checking() { + let medium = SmbusEspiMedium; + + // Test with packet shorter than header size (4 bytes) + for packet_size in 0..4 { + let short_packet = [0u8; 4]; + let err = medium.deserialize(&short_packet[..packet_size]).err().unwrap(); + assert_eq!( + err, + MctpPacketError::MediumError("Packet too short to parse smbus header") + ); + } + } + + #[test] + fn test_smbus_pec_bounds_checking() { + let medium = SmbusEspiMedium; + + // Test with packet that has header but claims more data than available for PEC + let header_bytes = [ + 0x20, // destination_slave_address + 0x0F, // command_code (MCTP) + 0x05, // byte_count: 5 bytes + 0x21, // source_slave_address + ]; + + // Provide exactly enough bytes for the data but no PEC byte + let payload = [0xAA, 0xBB, 0xCC, 0xDD, 0xEE]; // 5 bytes as claimed + let mut packet = [0u8; 9]; // 4 header + 5 payload = 9 total (missing PEC) + packet[0..4].copy_from_slice(&header_bytes); + packet[4..9].copy_from_slice(&payload); + + let err = medium.deserialize(&packet).err().unwrap(); + assert_eq!( + err, + MctpPacketError::MediumError("Packet too short to parse smbus body and PEC") + ); + } + + #[test] + fn test_smbus_zero_byte_count_edge_case() { + let medium = SmbusEspiMedium; + + // Test with zero byte count but packet shorter than header + PEC + let header_bytes = [ + 0x20, // destination_slave_address + 0x0F, // command_code (MCTP) + 0x00, // byte_count: 0 bytes + 0x21, // source_slave_address + ]; + + // Test with packet missing PEC byte + let mut short_packet = [0u8; 4]; // Only header, no PEC + short_packet.copy_from_slice(&header_bytes); + + let err = medium.deserialize(&short_packet).err().unwrap(); + assert_eq!( + err, + MctpPacketError::MediumError("Packet too short to parse smbus body and PEC") + ); + } + + #[test] + fn test_smbus_maximum_payload_boundary() { + let medium = SmbusEspiMedium; + + // Test serialization at the boundary of maximum payload (255 bytes) + let reply_context = SmbusEspiReplyContext { + destination_slave_address: 0x20, + source_slave_address: 0x10, + }; + + let max_payload = [0x55u8; 255]; + let mut buffer = [0u8; 260]; // 4 + 255 + 1 = exactly enough + + let result = medium.serialize(reply_context, &mut buffer, |encoder| { + encoder + .write_all(&max_payload) + .map_err(|_| MctpPacketError::SerializeError("encode error")) + }); + + assert!(result.is_ok()); + let serialized = result.unwrap(); + assert_eq!(serialized.len(), 260); // Should use exactly all available space + + // Test with buffer one byte too small for maximum payload. + // The encoder will hit BufferFull when trying to write the + // 255th payload byte (only 254 fit after header reservation), + // so this serialize call now returns an error rather than + // silently truncating. + let mut small_buffer = [0u8; 259]; // One byte short for max payload + let result_small = medium.serialize(reply_context, &mut small_buffer, |encoder| { + encoder + .write_all(&max_payload) + .map_err(|_| MctpPacketError::SerializeError("encode error")) + }); + + assert_eq!( + result_small.err().unwrap(), + MctpPacketError::SerializeError("encode error") + ); + } +} diff --git a/mctp-rs/src/medium/util.rs b/mctp-rs/src/medium/util.rs new file mode 100644 index 000000000..c97089e7a --- /dev/null +++ b/mctp-rs/src/medium/util.rs @@ -0,0 +1,37 @@ +use bit_register::{NumBytes, TryFromBits, TryIntoBits}; + +#[derive(Copy, Clone, PartialEq, Eq, Default, Debug)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct Zero; + +impl TryFromBits for Zero { + fn try_from_bits(bits: u32) -> Result { + if bits != 0 { Err("Bits must be 0") } else { Ok(Zero) } + } +} +impl TryIntoBits for Zero { + fn try_into_bits(self) -> Result { + Ok(0) + } +} +impl NumBytes for Zero { + const NUM_BYTES: usize = 4; +} + +#[derive(Copy, Clone, PartialEq, Eq, Default, Debug)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct One; + +impl TryFromBits for One { + fn try_from_bits(bits: u32) -> Result { + if bits != 1 { Err("Value must be 1") } else { Ok(One) } + } +} +impl TryIntoBits for One { + fn try_into_bits(self) -> Result { + Ok(1) + } +} +impl NumBytes for One { + const NUM_BYTES: usize = 4; +} diff --git a/mctp-rs/src/message_type/mctp_control.rs b/mctp-rs/src/message_type/mctp_control.rs new file mode 100644 index 000000000..c85b94de0 --- /dev/null +++ b/mctp-rs/src/message_type/mctp_control.rs @@ -0,0 +1,227 @@ +use crate::{ + MctpMedium, MctpMessageHeaderTrait, MctpMessageTrait, + MctpPacketError::{self, HeaderParseError}, + error::{MctpPacketResult, ProtocolError}, + mctp_command_code::MctpControlCommandCode, + mctp_completion_code::MctpCompletionCode, +}; + +#[derive(Debug, Default, PartialEq, Eq, Clone)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct MctpControlHeader { + pub request_bit: bool, // bit 7 + pub datagram_bit: bool, // bit 6 + pub instance_id: u8, // bits 4-0 + pub command_code: MctpControlCommandCode, + pub completion_code: MctpCompletionCode, +} + +#[derive(Debug, PartialEq, Eq, Clone)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum MctpControl { + SetEndpointIdRequest([u8; 2]), + SetEndpointIdResponse([u8; 3]), + GetEndpointIdRequest, + GetEndpointIdResponse([u8; 3]), +} + +impl MctpMessageHeaderTrait for MctpControlHeader { + fn serialize(self, buffer: &mut [u8]) -> MctpPacketResult { + if buffer.len() < 3 { + return Err(crate::MctpPacketError::SerializeError( + "buffer too small for mctp control header", + )); + } + + check_request_and_completion_code(self.request_bit, self.completion_code)?; + + buffer[0] = (self.request_bit as u8) << 7 | (self.datagram_bit as u8) << 6 | (self.instance_id & 0b0001_1111); + buffer[1] = self.command_code as u8; + buffer[2] = self.completion_code.into(); + Ok(3) + } + + fn deserialize(buffer: &[u8]) -> MctpPacketResult<(Self, &[u8]), M> { + if buffer.len() < 3 { + return Err(HeaderParseError("buffer too small for mctp control header")); + } + + let request_bit = buffer[0] & 0b1000_0000 != 0; + let datagram_bit = buffer[0] & 0b0100_0000 != 0; + let instance_id = buffer[0] & 0b0001_1111; + let command_code = + MctpControlCommandCode::try_from(buffer[1]).map_err(|_| HeaderParseError("invalid mctp command code"))?; + let completion_code = + MctpCompletionCode::try_from(buffer[2]).map_err(|_| HeaderParseError("invalid mctp completion code"))?; + + check_request_and_completion_code(request_bit, completion_code)?; + + Ok(( + MctpControlHeader { + request_bit, + datagram_bit, + instance_id, + command_code, + completion_code, + }, + &buffer[3..], + )) + } +} + +fn check_request_and_completion_code( + request_bit: bool, + completion_code: MctpCompletionCode, +) -> MctpPacketResult<(), M> { + if request_bit && completion_code != MctpCompletionCode::Success { + return Err(MctpPacketError::ProtocolError( + ProtocolError::CompletionCodeOnRequestMessage(completion_code), + )); + } + Ok(()) +} + +impl<'buf> MctpMessageTrait<'buf> for MctpControl { + type Header = MctpControlHeader; + const MESSAGE_TYPE: u8 = 0x00; + + fn serialize(self, buffer: &mut [u8]) -> MctpPacketResult { + match self { + Self::SetEndpointIdRequest(data) => copy_and_check_len(buffer, data), + Self::SetEndpointIdResponse(data) => copy_and_check_len(buffer, data), + Self::GetEndpointIdRequest => copy_and_check_len(buffer, []), + Self::GetEndpointIdResponse(data) => copy_and_check_len(buffer, data), + } + } + + fn deserialize(header: &Self::Header, buffer: &'buf [u8]) -> MctpPacketResult { + let message = match (header.request_bit, header.command_code) { + (true, MctpControlCommandCode::SetEndpointId) => Self::SetEndpointIdRequest(try_into_array(buffer)?), + (true, MctpControlCommandCode::GetEndpointId) => Self::GetEndpointIdRequest, + (false, MctpControlCommandCode::SetEndpointId) => Self::SetEndpointIdResponse(try_into_array(buffer)?), + (false, MctpControlCommandCode::GetEndpointId) => Self::GetEndpointIdResponse(try_into_array(buffer)?), + _ => { + return Err(HeaderParseError("invalid mctp control command code")); + } + }; + Ok(message) + } +} + +fn copy_and_check_len(buffer: &mut [u8], data: [u8; N]) -> MctpPacketResult { + if buffer.len() < N { + return Err(crate::MctpPacketError::SerializeError( + "buffer too small for mctp control message", + )); + } + buffer[..N].copy_from_slice(&data); + Ok(N) +} + +fn try_into_array(buffer: &[u8]) -> MctpPacketResult<[u8; N], M> { + if buffer.len() < N { + return Err(HeaderParseError("buffer too small for mctp control message")); + } + Ok(buffer[..N].try_into().unwrap()) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{error::ProtocolError, test_util::TestMedium}; + + #[test] + fn header_serialize_deserialize_happy_path() { + let header = MctpControlHeader { + request_bit: true, + datagram_bit: false, + instance_id: 0b1_1111, + command_code: MctpControlCommandCode::GetEndpointId, + completion_code: MctpCompletionCode::Success, + }; + + let mut buf = [0u8; 3]; + let size = header.clone().serialize::(&mut buf).unwrap(); + assert_eq!(size, 3); + assert_eq!( + buf, + [ + 0b1000_0000 | 0b0001_1111, // rq=1, d=0, instance id=0x1F + MctpControlCommandCode::GetEndpointId as u8, + u8::from(MctpCompletionCode::Success), + ] + ); + + let (parsed, rest) = MctpControlHeader::deserialize::(&buf).unwrap(); + assert_eq!(parsed, header); + assert_eq!(rest.len(), 0); + } + + #[test] + fn header_serialize_error_on_completion_code_in_request() { + let header = MctpControlHeader { + request_bit: true, + datagram_bit: false, + instance_id: 0, + command_code: MctpControlCommandCode::SetEndpointId, + completion_code: MctpCompletionCode::Error, + }; + + let mut buf = [0u8; 3]; + let err = header.serialize::(&mut buf).unwrap_err(); + match err { + MctpPacketError::ProtocolError(ProtocolError::CompletionCodeOnRequestMessage(code)) => { + assert_eq!(code, MctpCompletionCode::Error) + } + other => panic!("unexpected error: {:?}", other), + } + } + + #[rstest::rstest] + #[case(MctpControlCommandCode::SetEndpointId, false, MctpControl::SetEndpointIdResponse([0xAA, 0xBB, 0xCC]), &[0xAA, 0xBB, 0xCC])] + #[case(MctpControlCommandCode::SetEndpointId, true, MctpControl::SetEndpointIdRequest([0xAA, 0xBB]), &[0xAA, 0xBB])] + #[case(MctpControlCommandCode::GetEndpointId, false, MctpControl::GetEndpointIdResponse([0xAA, 0xBB, 0xCC]), &[0xAA, 0xBB, 0xCC])] + #[case(MctpControlCommandCode::GetEndpointId, true, MctpControl::GetEndpointIdRequest, &[])] + fn message_serialize_deserialize_happy_path( + #[case] command_code: MctpControlCommandCode, + #[case] request_bit: bool, + #[case] message: MctpControl, + #[case] expected: &[u8], + ) { + let mut buf = [0u8; 1024]; + let size = message.clone().serialize::(&mut buf).unwrap(); + assert_eq!(size, expected.len()); + assert_eq!(&buf[..size], expected); + + let header = MctpControlHeader { + request_bit, + datagram_bit: false, + instance_id: 0, + command_code, + completion_code: MctpCompletionCode::Success, + }; + + let parsed = MctpControl::deserialize::(&header, &buf).unwrap(); + assert_eq!(parsed, message); + } + + #[test] + fn message_deserialize_error_on_invalid_command_for_header() { + // request message with unsupported command code should error + let header = MctpControlHeader { + request_bit: true, + datagram_bit: false, + instance_id: 0, + command_code: MctpControlCommandCode::Reserved, + completion_code: MctpCompletionCode::Success, + }; + + let err = MctpControl::deserialize::(&header, &[]).unwrap_err(); + match err { + MctpPacketError::HeaderParseError(msg) => { + assert_eq!(msg, "invalid mctp control command code") + } + other => panic!("unexpected error: {:?}", other), + } + } +} diff --git a/mctp-rs/src/message_type/mod.rs b/mctp-rs/src/message_type/mod.rs new file mode 100644 index 000000000..bb356dc14 --- /dev/null +++ b/mctp-rs/src/message_type/mod.rs @@ -0,0 +1,22 @@ +mod mctp_control; +mod vendor_defined_pci; + +pub use mctp_control::*; +pub use vendor_defined_pci::*; + +use crate::{MctpMedium, error::MctpPacketResult}; + +pub trait MctpMessageHeaderTrait: Sized { + fn serialize(self, buffer: &mut [u8]) -> MctpPacketResult; + + fn deserialize(buffer: &[u8]) -> MctpPacketResult<(Self, &[u8]), M>; +} + +pub trait MctpMessageTrait<'buf>: Sized { + const MESSAGE_TYPE: u8; + type Header: MctpMessageHeaderTrait; + + fn serialize(self, buffer: &mut [u8]) -> MctpPacketResult; + + fn deserialize(header: &Self::Header, buffer: &'buf [u8]) -> MctpPacketResult; +} diff --git a/mctp-rs/src/message_type/vendor_defined_pci.rs b/mctp-rs/src/message_type/vendor_defined_pci.rs new file mode 100644 index 000000000..fc591088c --- /dev/null +++ b/mctp-rs/src/message_type/vendor_defined_pci.rs @@ -0,0 +1,48 @@ +use super::*; + +pub struct VendorDefinedPci<'buf>(pub &'buf [u8]); +pub struct VendorDefinedPciHeader(pub u16); +const HEADER_LEN: usize = size_of::(); + +impl MctpMessageHeaderTrait for VendorDefinedPciHeader { + fn serialize(self, buffer: &mut [u8]) -> MctpPacketResult { + if buffer.len() < HEADER_LEN { + return Err(crate::MctpPacketError::SerializeError( + "buffer too small for vendor defined pci header", + )); + } + buffer[..HEADER_LEN].copy_from_slice(&self.0.to_be_bytes()); + Ok(HEADER_LEN) + } + + fn deserialize(buffer: &[u8]) -> MctpPacketResult<(Self, &[u8]), M> { + if buffer.len() < HEADER_LEN { + return Err(crate::MctpPacketError::HeaderParseError( + "buffer too small for vendor defined pci header", + )); + } + let header = VendorDefinedPciHeader(u16::from_be_bytes(buffer[..HEADER_LEN].try_into().unwrap())); + Ok((header, &buffer[HEADER_LEN..])) + } +} + +impl<'buf> MctpMessageTrait<'buf> for VendorDefinedPci<'buf> { + type Header = VendorDefinedPciHeader; + const MESSAGE_TYPE: u8 = 0x7E; + + fn serialize(self, buffer: &mut [u8]) -> MctpPacketResult { + let self_len = self.0.len(); + if buffer.len() < self_len { + return Err(crate::MctpPacketError::SerializeError( + "buffer too small for vendor defined pci message", + )); + } + buffer[..self_len].copy_from_slice(self.0); + Ok(self_len) + } + + fn deserialize(_: &Self::Header, buffer: &'buf [u8]) -> MctpPacketResult, M> { + let message = VendorDefinedPci(buffer); + Ok(message) + } +} diff --git a/mctp-rs/src/serialize.rs b/mctp-rs/src/serialize.rs new file mode 100644 index 000000000..69fac7773 --- /dev/null +++ b/mctp-rs/src/serialize.rs @@ -0,0 +1,139 @@ +use crate::{ + MctpPacketError, + buffer_encoding::{BufferEncoding, EncodeError, EncodingEncoder}, + error::MctpPacketResult, + mctp_packet_context::MctpReplyContext, + mctp_transport_header::MctpTransportHeader, + medium::MctpMedium, +}; + +#[derive(Debug, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct SerializePacketState<'buf, M: MctpMedium> { + pub(crate) medium: &'buf M, + pub(crate) reply_context: MctpReplyContext, + pub(crate) current_packet_num: u8, + pub(crate) serialized_message_header: bool, + pub(crate) message_buffer: &'buf [u8], + pub(crate) assembly_buffer: &'buf mut [u8], +} + +pub const TRANSPORT_HEADER_SIZE: usize = 4; + +impl<'buf, M: MctpMedium> SerializePacketState<'buf, M> { + pub fn next(&mut self) -> Option> { + if self.message_buffer.is_empty() { + return None; + } + + let packet = self.medium.serialize( + self.reply_context.medium_context, + self.assembly_buffer, + |encoder: &mut EncodingEncoder<'_, M::Encoding>| { + let max_wire = self.medium.max_message_body_size().min(encoder.remaining_wire()); + + // Build the transport header first (with end_of_message + // tentatively 0) so we can measure its wire footprint + // under the medium's encoding before chunking the body. + let start_of_message = if self.current_packet_num == 0 { 1 } else { 0 }; + let packet_sequence_number = self.reply_context.packet_sequence_number.inc(); + let mut transport_header_value: u32 = MctpTransportHeader { + reserved: 0, + header_version: 1, + start_of_message, + end_of_message: 0, + packet_sequence_number, + tag_owner: 0, + message_tag: self.reply_context.message_tag, + source_endpoint_id: self.reply_context.destination_endpoint_id, + destination_endpoint_id: self.reply_context.source_endpoint_id, + } + .try_into() + .map_err(MctpPacketError::SerializeError)?; + let mut header_bytes = transport_header_value.to_be_bytes(); + let header_wire_cost = M::Encoding::wire_size_of(&header_bytes); + if header_wire_cost > max_wire { + return Err(MctpPacketError::SerializeError( + "assembly buffer too small for mctp transport header", + )); + } + + // Walk decoded body bytes one at a time, accumulating + // their per-byte wire footprint via + // `M::Encoding::wire_size_of`. Stop when adding the + // next byte would exceed the wire budget. Correct for + // both passthrough and stuffing encodings (both shipped + // encodings are byte-additive — `wire_size_of(a ++ b) + // == wire_size_of(a) + wire_size_of(b)`). + let body_wire_budget = max_wire - header_wire_cost; + let mut consumed_wire = 0usize; + let mut message_size = 0usize; + for &b in self.message_buffer.iter() { + let cost = M::Encoding::wire_size_of(&[b]); + if consumed_wire + cost > body_wire_budget { + break; + } + consumed_wire += cost; + message_size += 1; + } + + // if there is no room for any of the body, and the body is not empty, + // then return an error, otherwise we infinate loop sending packets with headers and + // no body, making it impossible to ever assemble a message + if message_size == 0 && !self.message_buffer.is_empty() { + return Err(MctpPacketError::SerializeError( + "assembly buffer too small for non-empty message body", + )); + } + + let body = &self.message_buffer[..message_size]; + self.message_buffer = &self.message_buffer[message_size..]; + + // Now that we know whether this is the final chunk, + // rebuild the transport header if `end_of_message` + // flips to 1. Re-measure the wire cost — none of the + // EOM-bit bytes hit 0x7E or 0x7D under either shipped + // encoding in practice, but do not assume. + let end_of_message = if self.message_buffer.is_empty() { 1 } else { 0 }; + if end_of_message == 1 { + transport_header_value = MctpTransportHeader { + reserved: 0, + header_version: 1, + start_of_message, + end_of_message, + packet_sequence_number, + tag_owner: 0, + message_tag: self.reply_context.message_tag, + source_endpoint_id: self.reply_context.destination_endpoint_id, + destination_endpoint_id: self.reply_context.source_endpoint_id, + } + .try_into() + .map_err(MctpPacketError::SerializeError)?; + header_bytes = transport_header_value.to_be_bytes(); + let rebuilt_header_wire_cost = M::Encoding::wire_size_of(&header_bytes); + if rebuilt_header_wire_cost + consumed_wire > max_wire { + return Err(MctpPacketError::SerializeError( + "assembly buffer too small after EOM bit set", + )); + } + } + + // write the transport header and message body via the + // medium-supplied encoder. + let map_encode_err = |e: EncodeError| match e { + EncodeError::BufferFull => MctpPacketError::SerializeError("encoding: buffer full"), + }; + encoder.write_all(&header_bytes).map_err(map_encode_err)?; + encoder.write_all(body).map_err(map_encode_err)?; + Ok(()) + }, + ); + + // Increment packet number for next call + if packet.is_ok() { + self.current_packet_num += 1; + } + + Some(packet) + } +} diff --git a/mctp-rs/src/test_util.rs b/mctp-rs/src/test_util.rs new file mode 100644 index 000000000..e328a0c94 --- /dev/null +++ b/mctp-rs/src/test_util.rs @@ -0,0 +1,109 @@ +use crate::{ + MctpPacketError, + buffer_encoding::{EncodingDecoder, EncodingEncoder, PassthroughEncoding}, + error::MctpPacketResult, + medium::{MctpMedium, MctpMediumFrame}, +}; + +#[derive(Debug, PartialEq, Eq, Copy, Clone)] +pub struct TestMedium { + header: &'static [u8], + trailer: &'static [u8], + mtu: usize, +} +impl TestMedium { + pub fn new() -> Self { + Self { + header: &[], + trailer: &[], + mtu: 32, + } + } + pub fn with_headers(mut self, header: &'static [u8], trailer: &'static [u8]) -> Self { + self.header = header; + self.trailer = trailer; + self + } + pub fn with_mtu(mut self, mtu: usize) -> Self { + self.mtu = mtu; + self + } +} + +#[derive(Debug, PartialEq, Eq, Copy, Clone)] +pub struct TestMediumFrame(usize); + +impl MctpMedium for TestMedium { + type Frame = TestMediumFrame; + type Error = &'static str; + type ReplyContext = (); + type Encoding = PassthroughEncoding; + + fn deserialize<'buf>( + &self, + packet: &'buf [u8], + ) -> MctpPacketResult<(Self::Frame, EncodingDecoder<'buf, Self::Encoding>), Self> { + let packet_len = packet.len(); + + // check that header / trailer is present and correct + if packet.len() < self.header.len() + self.trailer.len() { + return Err(MctpPacketError::MediumError("packet too short")); + } + if packet[0..self.header.len()] != *self.header { + return Err(MctpPacketError::MediumError("header mismatch")); + } + if packet[packet_len - self.trailer.len()..packet_len] != *self.trailer { + return Err(MctpPacketError::MediumError("trailer mismatch")); + } + + let inner = &packet[self.header.len()..packet_len - self.trailer.len()]; + Ok((TestMediumFrame(packet_len), EncodingDecoder::new(inner))) + } + fn max_message_body_size(&self) -> usize { + self.mtu + } + fn serialize<'buf, F>( + &self, + _: Self::ReplyContext, + buffer: &'buf mut [u8], + message_writer: F, + ) -> MctpPacketResult<&'buf [u8], Self> + where + F: for<'a> FnOnce(&mut EncodingEncoder<'a, Self::Encoding>) -> MctpPacketResult<(), Self>, + { + let header_len = self.header.len(); + let trailer_len = self.trailer.len(); + + // Ensure buffer can fit at least headers and trailers + if buffer.len() < header_len + trailer_len { + return Err(MctpPacketError::MediumError("Buffer too small for headers")); + } + + // Calculate available space for message (respecting MTU) + let max_packet_size = self.mtu.min(buffer.len()); + if max_packet_size < header_len + trailer_len { + return Err(MctpPacketError::MediumError("MTU too small for headers")); + } + let max_message_size = max_packet_size - header_len - trailer_len; + + buffer[0..header_len].copy_from_slice(self.header); + + let body_wire_len = { + let body_buf = &mut buffer[header_len..header_len + max_message_size]; + let mut encoder = EncodingEncoder::::new(body_buf); + message_writer(&mut encoder)?; + encoder.wire_position() + }; + + let len = header_len + body_wire_len; + buffer[len..len + trailer_len].copy_from_slice(self.trailer); + Ok(&buffer[..len + trailer_len]) + } +} + +impl MctpMediumFrame for TestMediumFrame { + fn packet_size(&self) -> usize { + self.0 + } + fn reply_context(&self) -> ::ReplyContext {} +} diff --git a/odp-service-common/Cargo.toml b/odp-service-common/Cargo.toml new file mode 100644 index 000000000..ef6b5a46d --- /dev/null +++ b/odp-service-common/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "odp-service-common" +version.workspace = true +edition.workspace = true +license.workspace = true +repository.workspace = true + +[dependencies] +embedded-services.workspace = true +static_cell.workspace = true + +[lints] +workspace = true diff --git a/odp-service-common/src/lib.rs b/odp-service-common/src/lib.rs new file mode 100644 index 000000000..6ae7498ca --- /dev/null +++ b/odp-service-common/src/lib.rs @@ -0,0 +1,4 @@ +//! This crate contains code that is common to multiple ODP service implementations. +#![no_std] + +pub mod runnable_service; diff --git a/odp-service-common/src/runnable_service.rs b/odp-service-common/src/runnable_service.rs new file mode 100644 index 000000000..149b2aed6 --- /dev/null +++ b/odp-service-common/src/runnable_service.rs @@ -0,0 +1,83 @@ +//! This module contains helper traits and functions for services that run on the EC. + +/// A trait for a service that requires the caller to launch a long-running task on its behalf to operate. +pub trait Service<'hw>: Sized { + /// A type that can be used to run the service. This is returned by the new() function and the user is + /// expected to call its run() method in an embassy task (or similar parallel execution context on other + /// async runtimes). + type Runner: ServiceRunner<'hw>; + + /// Any memory resources that your service needs. This is typically an opaque type that is only used by the service + /// and is not interacted with by users of the service. Must be default-constructible for spawn_service!() to work. + type Resources: Default; + + /// The error type that your `new` function can return on failure. + type ErrorType; + + /// Any initialization parameters that your service needs to run. + type InitParams; + + /// Initializes an instance of the service using the provided storage and returns a control handle for the service and + /// a runner that can be used to run the service. + fn new( + storage: &'hw mut Self::Resources, + params: Self::InitParams, + ) -> impl core::future::Future>; +} + +/// A trait for a run handle used to execute a service's event loop. This is returned by Service::new() +/// and the user is expected to call its run() method in an embassy task (or similar parallel execution context +/// on other async runtimes). +pub trait ServiceRunner<'hw> { + /// Run the service event loop. This future never completes. + fn run(self) -> impl core::future::Future + 'hw; +} + +/// Initializes a service, creates an embassy task to run it, and spawns that task. +/// +/// This macro handles the boilerplate of: +/// 1. Creating a `static` [`StaticCell`](static_cell::StaticCell) to hold the service +/// 2. Calling the service's `new()` method +/// 3. Defining an embassy_executor::task to run the service +/// 4. Spawning the task on the provided executor +/// +/// Returns a Result<&Service, Error> where Error is the error type of $service_ty::new(). +/// +/// Arguments +/// +/// - spawner: An embassy_executor::Spawner. +/// - service_ty: The service type that implements Service that you want to create and run. +/// - init_arg: The init argument type to pass to `Service::new()` +/// +/// Example: +/// +/// ```ignore +/// let time_service = odp_service_common::runnable_service::spawn_service!( +/// spawner, +/// time_alarm_service::Service<'static>, +/// time_alarm_service::ServiceInitParams { dt_clock, tz, ac_expiration, ac_policy, dc_expiration, dc_policy } +/// ).expect("failed to initialize time_alarm service"); +/// ``` +#[macro_export] +macro_rules! spawn_service { + ($spawner:expr, $service_ty:ty, $init_arg:expr) => {{ + use $crate::runnable_service::{Service, ServiceRunner}; + static SERVICE_RESOURCES: static_cell::StaticCell<(<$service_ty as Service>::Resources)> = + static_cell::StaticCell::new(); + let service_resources = SERVICE_RESOURCES.init(<<$service_ty as Service>::Resources as Default>::default()); + + #[embassy_executor::task] + async fn service_task_fn(runner: <$service_ty as $crate::runnable_service::Service<'static>>::Runner) { + runner.run().await; + } + + <$service_ty>::new(service_resources, $init_arg) + .await + .map(|(control_handle, runner)| { + $spawner.spawn(service_task_fn(runner).expect("Failed to spawn service task")); + control_handle + }) + }}; +} + +pub use spawn_service; diff --git a/partition-manager/partition-manager/Cargo.toml b/partition-manager/partition-manager/Cargo.toml index 1b3bce14e..043d42677 100644 --- a/partition-manager/partition-manager/Cargo.toml +++ b/partition-manager/partition-manager/Cargo.toml @@ -9,6 +9,9 @@ categories = ["embedded", "hardware-support", "no-std::no-alloc", "no-std"] readme = "README.md" +[package.metadata.cargo-machete] +ignored = ["critical-section"] + [lints] workspace = true diff --git a/power-policy-interface/Cargo.toml b/power-policy-interface/Cargo.toml new file mode 100644 index 000000000..e65a83371 --- /dev/null +++ b/power-policy-interface/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "power-policy-interface" +version.workspace = true +edition.workspace = true +license.workspace = true +repository.workspace = true + +[package.metadata.cargo-machete] +ignored = ["log"] + +[lints] +workspace = true + +[dependencies] +defmt = { workspace = true, optional = true } +embassy-sync.workspace = true +embedded-services.workspace = true +num_enum.workspace = true +bitfield.workspace = true +log = { workspace = true, optional = true } +embedded-batteries-async.workspace = true + +[features] +default = [] +defmt = ["dep:defmt", "embedded-services/defmt", "embassy-sync/defmt"] +log = ["dep:log", "embedded-services/log", "embassy-sync/log"] + +[dev-dependencies] +critical-section = { workspace = true, features = ["std"] } diff --git a/embedded-service/src/power/policy/flags.rs b/power-policy-interface/src/capability.rs similarity index 66% rename from embedded-service/src/power/policy/flags.rs rename to power-policy-interface/src/capability.rs index b9fbcc4d7..d53698b16 100644 --- a/embedded-service/src/power/policy/flags.rs +++ b/power-policy-interface/src/capability.rs @@ -1,8 +1,84 @@ -//! Consumer and provider flags, these are used to signal additional information about a consumer/provider request - +//! Power capability definitions and related flags use bitfield::bitfield; use num_enum::{IntoPrimitive, TryFromPrimitive}; +/// Amount of power that a device can provider or consume +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct PowerCapability { + /// Available voltage in mV + pub voltage_mv: u16, + /// Max available current in mA + pub current_ma: u16, +} + +impl PowerCapability { + /// Calculate maximum power + pub fn max_power_mw(&self) -> u32 { + self.voltage_mv as u32 * self.current_ma as u32 / 1000 + } +} + +impl PartialOrd for PowerCapability { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for PowerCapability { + fn cmp(&self, other: &Self) -> core::cmp::Ordering { + self.max_power_mw().cmp(&other.max_power_mw()) + } +} + +/// Power capability with consumer flags +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct ConsumerPowerCapability { + /// Power capability + pub capability: PowerCapability, + /// Consumer flags + pub flags: ConsumerFlags, +} + +impl From for ConsumerPowerCapability { + fn from(capability: PowerCapability) -> Self { + Self { + capability, + flags: ConsumerFlags::none(), + } + } +} + +/// Power capability with provider flags +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct ProviderPowerCapability { + /// Power capability + pub capability: PowerCapability, + /// Provider flags + pub flags: ProviderFlags, +} + +impl From for ProviderPowerCapability { + fn from(capability: PowerCapability) -> Self { + Self { + capability, + flags: ProviderFlags::none(), + } + } +} + +/// Combined power capability with flags enum +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum PowerCapabilityFlags { + /// Consumer flags + Consumer(ConsumerPowerCapability), + /// Provider flags + Provider(ProviderPowerCapability), +} + /// PSU type #[derive(Copy, Clone, Debug, PartialEq, Eq, IntoPrimitive, TryFromPrimitive)] #[num_enum(error_type(name = InvalidPsuType, constructor = InvalidPsuType))] @@ -37,7 +113,7 @@ bitfield! { /// Raw consumer flags bit field #[derive(Copy, Clone, PartialEq, Eq)] #[cfg_attr(feature = "defmt", derive(defmt::Format))] - struct ConsumerRaw(u32); + struct ConsumerFlagsRaw(u32); impl Debug; /// Unconstrained power, indicates that we are drawing power from something like an outlet and not a limited source like a battery pub bool, unconstrained_power, set_unconstrained_power: 0; @@ -48,12 +124,12 @@ bitfield! { /// Type safe wrapper for consumer flags #[derive(Copy, Clone, Debug, PartialEq, Eq)] #[cfg_attr(feature = "defmt", derive(defmt::Format))] -pub struct Consumer(ConsumerRaw); +pub struct ConsumerFlags(ConsumerFlagsRaw); -impl Consumer { +impl ConsumerFlags { /// Create a new consumer with no flags set pub const fn none() -> Self { - Self(ConsumerRaw(0)) + Self(ConsumerFlagsRaw(0)) } /// Builder method to set the unconstrained power flag @@ -102,9 +178,9 @@ bitfield! { /// Type safe wrapper for provider flags #[derive(Copy, Clone, Debug, PartialEq, Eq)] #[cfg_attr(feature = "defmt", derive(defmt::Format))] -pub struct Provider(ProviderRaw); +pub struct ProviderFlags(ProviderRaw); -impl Provider { +impl ProviderFlags { /// Create a new provider with no flags set pub const fn none() -> Self { Self(ProviderRaw(0)) @@ -158,24 +234,24 @@ mod tests { } #[test] - fn test_consumer_unconstrained() { - let mut consumer = Consumer::none().with_unconstrained_power(); + fn test_consumer_flags_unconstrained() { + let mut consumer = ConsumerFlags::none().with_unconstrained_power(); assert_eq!(consumer.0.0, 0x1); consumer.set_unconstrained_power(false); assert_eq!(consumer.0.0, 0x0); } #[test] - fn test_consumer_psu_type() { - let mut consumer = Consumer::none().with_psu_type(PsuType::TypeC); + fn test_consumer_flags_psu_type() { + let mut consumer = ConsumerFlags::none().with_psu_type(PsuType::TypeC); assert_eq!(consumer.0.0, 0x100); consumer.set_psu_type(PsuType::Unknown); assert_eq!(consumer.0.0, 0x0); } #[test] - fn test_provider_psu_type() { - let mut provider = Provider::none().with_psu_type(PsuType::TypeC); + fn test_provider_flags_psu_type() { + let mut provider = ProviderFlags::none().with_psu_type(PsuType::TypeC); assert_eq!(provider.0.0, 0x100); provider.set_psu_type(PsuType::Unknown); assert_eq!(provider.0.0, 0x0); diff --git a/power-policy-interface/src/charger/event.rs b/power-policy-interface/src/charger/event.rs new file mode 100644 index 000000000..c59de5f29 --- /dev/null +++ b/power-policy-interface/src/charger/event.rs @@ -0,0 +1,44 @@ +//! Events originating from a charger device + +use embedded_services::sync::Lockable; + +/// PSU state as determined by charger device +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum PsuState { + /// Charger detected PSU attached + Attached, + /// Charger detected PSU detached + Detached, +} + +impl From for PsuState { + fn from(value: bool) -> Self { + match value { + true => PsuState::Attached, + false => PsuState::Detached, + } + } +} + +/// Data for a charger event +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +#[non_exhaustive] +pub enum EventData { + /// PSU state changed + PsuStateChange(PsuState), +} + +/// Event broadcast from a charger. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct Event<'a, D: Lockable> +where + D::Inner: crate::charger::Charger, +{ + /// Device that sent this request + pub charger: &'a D, + /// Event data + pub event: EventData, +} diff --git a/power-policy-interface/src/charger/mock.rs b/power-policy-interface/src/charger/mock.rs new file mode 100644 index 000000000..4e0e8ae1c --- /dev/null +++ b/power-policy-interface/src/charger/mock.rs @@ -0,0 +1,62 @@ +use embassy_sync::mutex::Mutex; +use embedded_batteries_async::charger::{MilliAmps, MilliVolts}; +use embedded_services::{GlobalRawMutex, debug}; + +pub type ChargerType = Mutex; + +pub struct NoopCharger(super::State); + +impl NoopCharger { + pub fn new() -> Self { + Self(super::State::default()) + } +} +impl Default for NoopCharger { + fn default() -> Self { + Self::new() + } +} + +impl super::Charger for NoopCharger { + type ChargerError = core::convert::Infallible; + + async fn init_charger(&mut self) -> Result { + debug!("Charger initialized"); + Ok(super::PsuState::Attached) + } + + async fn attach_handler( + &mut self, + capability: crate::capability::ConsumerPowerCapability, + ) -> Result<(), Self::ChargerError> { + debug!("Charger recvd capability {:?}", capability); + Ok(()) + } + + async fn detach_handler(&mut self) -> Result<(), Self::ChargerError> { + debug!("Charger recvd detach"); + Ok(()) + } + + fn state(&self) -> &super::State { + &self.0 + } + + fn state_mut(&mut self) -> &mut super::State { + &mut self.0 + } +} + +impl embedded_batteries_async::charger::Charger for NoopCharger { + async fn charging_current(&mut self, current: MilliAmps) -> Result { + Ok(current) + } + + async fn charging_voltage(&mut self, voltage: MilliVolts) -> Result { + Ok(voltage) + } +} + +impl embedded_batteries_async::charger::ErrorType for NoopCharger { + type Error = core::convert::Infallible; +} diff --git a/power-policy-interface/src/charger/mod.rs b/power-policy-interface/src/charger/mod.rs new file mode 100644 index 000000000..40af43661 --- /dev/null +++ b/power-policy-interface/src/charger/mod.rs @@ -0,0 +1,206 @@ +//! Charger events, state machine, and trait + +use crate::capability::ConsumerPowerCapability; +use core::{convert::Infallible, future::Future}; + +pub mod event; +/// Mock software representation of a charger +pub mod mock; +#[cfg(test)] +mod tests; + +pub use event::{Event, EventData, PsuState}; + +/// Charger Device ID new type +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct ChargerId(pub u8); + +/// Charger state errors +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +#[non_exhaustive] +pub enum ChargerError { + /// Charger received command in an invalid state + InvalidState(InternalState), + /// Charger hardware timed out responding + Timeout, + /// Charger underlying bus error + BusError, + /// Charger received an unknown event + UnknownEvent, +} + +impl From for crate::psu::Error { + fn from(value: ChargerError) -> Self { + Self::Charger(value) + } +} + +impl From for ChargerError { + fn from(_value: Infallible) -> Self { + Self::BusError + } +} + +/// Current state of the charger +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum InternalState { + /// Device is unpowered + Unpowered, + /// Device is powered + Powered(PoweredSubstate), +} + +/// Powered state substates +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum PoweredSubstate { + /// Device is initializing + Init, + /// PSU is attached and device can charge if desired + PsuAttached, + /// PSU is detached + PsuDetached, +} + +/// Current state of the charger +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct State { + /// Charger device state + state: InternalState, + /// Current charger capability + capability: Option, +} + +impl Default for State { + fn default() -> Self { + Self { + state: InternalState::Unpowered, + capability: None, + } + } +} + +impl State { + /// Returns a reference to the current internal charger state. + pub fn internal_state(&self) -> &InternalState { + &self.state + } + + /// Returns a reference to the current cached power capability, if any. + pub fn capability(&self) -> &Option { + &self.capability + } + + /// Handle charger initialization completing. Transitions from `Powered(Init)` to + /// `Powered(PsuAttached)` or `Powered(PsuDetached)` based on PSU state. + /// + /// Returns `Err` if not in `Powered(Init)`. + pub fn on_initialized(&mut self, psu_state: PsuState) -> Result<(), ChargerError> { + match self.state { + InternalState::Powered(PoweredSubstate::Init) => { + self.state = match psu_state { + PsuState::Attached => InternalState::Powered(PoweredSubstate::PsuAttached), + PsuState::Detached => InternalState::Powered(PoweredSubstate::PsuDetached), + }; + Ok(()) + } + other => Err(ChargerError::InvalidState(other)), + } + } + + /// Handle a PSU state change event. Transitions between `Powered(PsuAttached)` and + /// `Powered(PsuDetached)`. + /// + /// Returns `Err` if not in `Powered(PsuAttached)` or `Powered(PsuDetached)`. + pub fn on_psu_state_change(&mut self, psu_state: PsuState) -> Result<(), ChargerError> { + match self.state { + InternalState::Powered(PoweredSubstate::PsuAttached) => { + if psu_state == PsuState::Detached { + self.state = InternalState::Powered(PoweredSubstate::PsuDetached); + } + Ok(()) + } + InternalState::Powered(PoweredSubstate::PsuDetached) => { + if psu_state == PsuState::Attached { + self.state = InternalState::Powered(PoweredSubstate::PsuAttached); + } + Ok(()) + } + other => Err(ChargerError::InvalidState(other)), + } + } + + /// Handle a communication timeout. Transitions to `Unpowered` and clears the cached capability. + pub fn on_timeout(&mut self) { + self.state = InternalState::Unpowered; + self.capability = None; + } + + /// Transition after a successful check-ready response. + /// + /// If currently unpowered, moves to `Powered(Init)` and clears capability. + /// If already powered, this is a no-op. + pub fn on_ready_success(&mut self) { + if self.state == InternalState::Unpowered { + self.state = InternalState::Powered(PoweredSubstate::Init); + self.capability = None; + } + } + + /// Transition after a failed check-ready response. + /// + /// If currently powered, moves to `Unpowered`. Capability is preserved for diagnostics. + /// If already unpowered, this is a no-op. + pub fn on_ready_failure(&mut self) { + if matches!(self.state, InternalState::Powered(_)) { + self.state = InternalState::Unpowered; + } + } + + /// Cache a new capability from a policy configuration attach. + /// Does not change the charger state. + pub fn on_policy_attach(&mut self, capability: ConsumerPowerCapability) { + self.capability = Some(capability); + } + + /// Clear the cached capability after a policy configuration detach. + /// Does not change the charger state. + pub fn on_policy_detach(&mut self) { + self.capability = None; + } + + /// Returns `true` if the charger is in the `Unpowered` state. + pub fn is_unpowered(&self) -> bool { + self.state == InternalState::Unpowered + } +} + +/// Charger controller trait that devices must implement to use the power policy service. +pub trait Charger: embedded_batteries_async::charger::Charger { + /// Type of error returned by the bus + type ChargerError: Into + embedded_batteries_async::charger::Error; + + /// Initialize charger hardware, after this returns the charger should be ready to charge + fn init_charger(&mut self) -> impl Future>; + /// Called after power policy attaches to a power port. + fn attach_handler( + &mut self, + capability: ConsumerPowerCapability, + ) -> impl Future>; + /// Called after power policy detaches from a power port, either to switch consumers, + /// or because PSU was disconnected. + fn detach_handler(&mut self) -> impl Future>; + /// Upon successful return of this method, the charger is assumed to be powered and ready to communicate, + /// transitioning state from unpowered to powered. + fn is_ready(&mut self) -> impl Future> { + core::future::ready(Ok(())) + } + /// Return an immutable reference to the current charger state + fn state(&self) -> &State; + /// Return a mutable reference to the current charger state + fn state_mut(&mut self) -> &mut State; +} diff --git a/power-policy-interface/src/charger/tests.rs b/power-policy-interface/src/charger/tests.rs new file mode 100644 index 000000000..518c5ea75 --- /dev/null +++ b/power-policy-interface/src/charger/tests.rs @@ -0,0 +1,285 @@ +use super::*; +use crate::capability::{ConsumerFlags, PowerCapability}; + +fn cap(voltage_mv: u16, current_ma: u16) -> ConsumerPowerCapability { + ConsumerPowerCapability { + capability: PowerCapability { voltage_mv, current_ma }, + flags: ConsumerFlags::none(), + } +} + +fn state_init() -> State { + State { + state: InternalState::Powered(PoweredSubstate::Init), + capability: None, + } +} + +fn state_psu_attached() -> State { + State { + state: InternalState::Powered(PoweredSubstate::PsuAttached), + capability: None, + } +} + +fn state_psu_detached() -> State { + State { + state: InternalState::Powered(PoweredSubstate::PsuDetached), + capability: None, + } +} + +fn state_unpowered() -> State { + State::default() +} + +// on_initialized + +#[test] +fn on_initialized_from_init_attached() { + let mut s = state_init(); + assert!(s.on_initialized(PsuState::Attached).is_ok()); + assert_eq!(s.state, InternalState::Powered(PoweredSubstate::PsuAttached)); +} + +#[test] +fn on_initialized_from_init_detached() { + let mut s = state_init(); + assert!(s.on_initialized(PsuState::Detached).is_ok()); + assert_eq!(s.state, InternalState::Powered(PoweredSubstate::PsuDetached)); +} + +#[test] +fn on_initialized_from_psu_attached_fails() { + let mut s = state_psu_attached(); + assert_eq!( + s.on_initialized(PsuState::Attached), + Err(ChargerError::InvalidState(InternalState::Powered( + PoweredSubstate::PsuAttached + ))) + ); +} + +#[test] +fn on_initialized_from_unpowered_fails() { + let mut s = state_unpowered(); + assert_eq!( + s.on_initialized(PsuState::Attached), + Err(ChargerError::InvalidState(InternalState::Unpowered)) + ); +} + +// on_psu_state_change + +#[test] +fn psu_state_change_attached_to_detached() { + let mut s = state_psu_attached(); + assert!(s.on_psu_state_change(PsuState::Detached).is_ok()); + assert_eq!(s.state, InternalState::Powered(PoweredSubstate::PsuDetached)); +} + +#[test] +fn psu_state_change_detached_to_attached() { + let mut s = state_psu_detached(); + assert!(s.on_psu_state_change(PsuState::Attached).is_ok()); + assert_eq!(s.state, InternalState::Powered(PoweredSubstate::PsuAttached)); +} + +#[test] +fn psu_state_change_same_state_is_noop() { + let mut s = state_psu_attached(); + assert!(s.on_psu_state_change(PsuState::Attached).is_ok()); + assert_eq!(s.state, InternalState::Powered(PoweredSubstate::PsuAttached)); +} + +#[test] +fn psu_state_change_from_init_fails() { + let mut s = state_init(); + assert_eq!( + s.on_psu_state_change(PsuState::Attached), + Err(ChargerError::InvalidState(InternalState::Powered( + PoweredSubstate::Init + ))) + ); +} + +#[test] +fn psu_state_change_from_unpowered_fails() { + let mut s = state_unpowered(); + assert_eq!( + s.on_psu_state_change(PsuState::Attached), + Err(ChargerError::InvalidState(InternalState::Unpowered)) + ); +} + +// on_timeout + +#[test] +fn timeout_from_psu_attached() { + let mut s = state_psu_attached(); + s.capability = Some(cap(5000, 3000)); + s.on_timeout(); + assert_eq!(s.state, InternalState::Unpowered); + assert!(s.capability.is_none()); +} + +#[test] +fn timeout_from_psu_detached() { + let mut s = state_psu_detached(); + s.on_timeout(); + assert_eq!(s.state, InternalState::Unpowered); +} + +#[test] +fn timeout_from_init() { + let mut s = state_init(); + s.on_timeout(); + assert_eq!(s.state, InternalState::Unpowered); +} + +#[test] +fn timeout_from_unpowered() { + let mut s = state_unpowered(); + s.on_timeout(); + assert_eq!(s.state, InternalState::Unpowered); +} + +// on_ready_success + +#[test] +fn ready_success_from_unpowered() { + let mut s = state_unpowered(); + s.on_ready_success(); + assert_eq!(s.state, InternalState::Powered(PoweredSubstate::Init)); + assert!(s.capability.is_none()); +} + +#[test] +fn ready_success_from_powered_is_noop() { + let mut s = state_psu_attached(); + s.capability = Some(cap(5000, 3000)); + s.on_ready_success(); + assert_eq!(s.state, InternalState::Powered(PoweredSubstate::PsuAttached)); + assert!(s.capability.is_some()); +} + +// on_ready_failure + +#[test] +fn ready_failure_from_powered() { + let mut s = state_psu_attached(); + s.capability = Some(cap(5000, 3000)); + s.on_ready_failure(); + assert_eq!(s.state, InternalState::Unpowered); + assert!(s.capability.is_some()); // preserved for diagnostics +} + +#[test] +fn ready_failure_from_unpowered_is_noop() { + let mut s = state_unpowered(); + s.on_ready_failure(); + assert_eq!(s.state, InternalState::Unpowered); +} + +// on_policy_attach + +#[test] +fn policy_attach_from_psu_attached() { + let mut s = state_psu_attached(); + let c = cap(5000, 3000); + s.on_policy_attach(c); + assert_eq!(s.capability, Some(c)); + assert_eq!(s.state, InternalState::Powered(PoweredSubstate::PsuAttached)); +} + +#[test] +fn policy_attach_from_psu_detached() { + let mut s = state_psu_detached(); + let c = cap(9000, 2000); + s.on_policy_attach(c); + assert_eq!(s.capability, Some(c)); + assert_eq!(s.state, InternalState::Powered(PoweredSubstate::PsuDetached)); +} + +#[test] +fn policy_attach_from_init() { + let mut s = state_init(); + let c = cap(5000, 3000); + s.on_policy_attach(c); + assert_eq!(s.capability, Some(c)); + assert_eq!(s.state, InternalState::Powered(PoweredSubstate::Init)); +} + +#[test] +fn policy_attach_from_unpowered() { + let mut s = state_unpowered(); + let c = cap(5000, 3000); + s.on_policy_attach(c); + assert_eq!(s.capability, Some(c)); + assert_eq!(s.state, InternalState::Unpowered); +} + +// on_policy_detach + +#[test] +fn policy_detach_from_psu_attached() { + let mut s = state_psu_attached(); + s.capability = Some(cap(5000, 3000)); + s.on_policy_detach(); + assert!(s.capability.is_none()); + assert_eq!(s.state, InternalState::Powered(PoweredSubstate::PsuAttached)); +} + +#[test] +fn policy_detach_from_init() { + let mut s = state_init(); + s.capability = Some(cap(5000, 3000)); + s.on_policy_detach(); + assert!(s.capability.is_none()); + assert_eq!(s.state, InternalState::Powered(PoweredSubstate::Init)); +} + +#[test] +fn policy_detach_from_unpowered() { + let mut s = state_unpowered(); + s.capability = Some(cap(5000, 3000)); + s.on_policy_detach(); + assert!(s.capability.is_none()); + assert_eq!(s.state, InternalState::Unpowered); +} + +// Full transition sequence + +#[test] +fn full_lifecycle_unpowered_to_charging_and_back() { + let mut s = state_unpowered(); + + // Check ready → powered init + s.on_ready_success(); + assert_eq!(s.state, InternalState::Powered(PoweredSubstate::Init)); + + // Initialized with PSU attached + assert!(s.on_initialized(PsuState::Attached).is_ok()); + assert_eq!(s.state, InternalState::Powered(PoweredSubstate::PsuAttached)); + + // Policy attach + let c = cap(5000, 3000); + s.on_policy_attach(c); + assert_eq!(s.capability, Some(c)); + + // PSU detach + assert!(s.on_psu_state_change(PsuState::Detached).is_ok()); + assert_eq!(s.state, InternalState::Powered(PoweredSubstate::PsuDetached)); + + // Policy detach + s.on_policy_detach(); + assert!(s.capability.is_none()); + + // PSU reattach + assert!(s.on_psu_state_change(PsuState::Attached).is_ok()); + assert_eq!(s.state, InternalState::Powered(PoweredSubstate::PsuAttached)); + + // Timeout → unpowered + s.on_timeout(); + assert_eq!(s.state, InternalState::Unpowered); +} diff --git a/power-policy-interface/src/lib.rs b/power-policy-interface/src/lib.rs new file mode 100644 index 000000000..41be38249 --- /dev/null +++ b/power-policy-interface/src/lib.rs @@ -0,0 +1,6 @@ +#![no_std] + +pub mod capability; +pub mod charger; +pub mod psu; +pub mod service; diff --git a/power-policy-interface/src/psu/event.rs b/power-policy-interface/src/psu/event.rs new file mode 100644 index 000000000..aff48ef3d --- /dev/null +++ b/power-policy-interface/src/psu/event.rs @@ -0,0 +1,36 @@ +//! Messages originating from a PSU +use embedded_services::sync::Lockable; + +use crate::{ + capability::{ConsumerPowerCapability, ProviderPowerCapability}, + psu, +}; + +/// Data for an event broadcast from a PSU. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum EventData { + /// Notify that a device has attached + Attached, + /// Notify that available power for consumption has changed + UpdatedConsumerCapability(Option), + /// Request the given amount of power to provider + RequestedProviderCapability(Option), + /// Notify that a device cannot consume or provide power anymore + Disconnected, + /// Notify that a device has detached + Detached, +} + +/// Event broadcast from a PSU. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct Event<'a, D: Lockable> +where + D::Inner: psu::Psu, +{ + /// Device that sent this request + pub psu: &'a D, + /// Event data + pub event: EventData, +} diff --git a/power-policy-interface/src/psu/mod.rs b/power-policy-interface/src/psu/mod.rs new file mode 100644 index 000000000..987f70174 --- /dev/null +++ b/power-policy-interface/src/psu/mod.rs @@ -0,0 +1,254 @@ +//! Device struct and methods +use embedded_services::named::Named; + +use crate::capability::{ConsumerPowerCapability, PowerCapability, ProviderPowerCapability}; + +pub mod event; + +/// Error type +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum Error { + /// The requested device does not exist + InvalidDevice, + /// The provide request was denied, contains maximum available power + CannotProvide(Option), + /// The consume request was denied, contains maximum available power + CannotConsume(Option), + /// The device is not in the correct state (expected, actual) + InvalidState(&'static [StateKind], StateKind), + /// Invalid response + InvalidResponse, + /// Busy, the device cannot respond to the request at this time + Busy, + /// Timeout + Timeout, + /// Bus error + Bus, + /// Charger specific error, underlying error should have more context + Charger(crate::charger::ChargerError), + /// Generic failure + Failed, +} + +/// Most basic device states +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum StateKind { + /// No device attached + Detached, + /// Device is attached + Idle, + /// Device is actively providing power, USB PD source mode + ConnectedProvider, + /// Device is actively consuming power, USB PD sink mode + ConnectedConsumer, +} + +/// Current state of the power device +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum PsuState { + /// Device is attached, but is not currently providing or consuming power + Idle, + /// Device is attached and is currently providing power + ConnectedProvider(ProviderPowerCapability), + /// Device is attached and is currently consuming power + ConnectedConsumer(ConsumerPowerCapability), + /// No device attached + Detached, +} + +impl PsuState { + /// Returns the corresponding state kind + pub fn kind(&self) -> StateKind { + match self { + PsuState::Idle => StateKind::Idle, + PsuState::ConnectedProvider(_) => StateKind::ConnectedProvider, + PsuState::ConnectedConsumer(_) => StateKind::ConnectedConsumer, + PsuState::Detached => StateKind::Detached, + } + } +} + +/// Per-device state for power policy implementation +/// +/// This struct implements the state machine outlined in the docs directory. +/// The various state transition functions always succeed in the sense that +/// the desired state is always entered, but some still return a result. +/// This is because a the device that is driving this state machine is the +/// ultimate source of truth and the recovery procedure would ultimately +/// end up catching up to this state anyway. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct State { + /// Current state of the device + pub psu_state: PsuState, + /// Current consumer capability + pub consumer_capability: Option, + /// Current requested provider capability + pub requested_provider_capability: Option, +} + +impl Default for State { + fn default() -> Self { + Self { + psu_state: PsuState::Detached, + consumer_capability: None, + requested_provider_capability: None, + } + } +} + +impl State { + /// Attach the device + pub fn attach(&mut self) -> Result<(), Error> { + let result = if self.psu_state == PsuState::Detached { + Ok(()) + } else { + Err(Error::InvalidState(&[StateKind::Detached], self.psu_state.kind())) + }; + self.psu_state = PsuState::Idle; + result + } + + /// Detach the device + /// + /// Detach is always a valid transition + pub fn detach(&mut self) { + self.psu_state = PsuState::Detached; + self.consumer_capability = None; + self.requested_provider_capability = None; + } + + /// Disconnect this device + pub fn disconnect(&mut self, clear_caps: bool) -> Result<(), Error> { + let result = if matches!( + self.psu_state, + PsuState::ConnectedConsumer(_) | PsuState::ConnectedProvider(_) | PsuState::Idle + ) { + Ok(()) + } else { + Err(Error::InvalidState( + &[ + StateKind::ConnectedConsumer, + StateKind::ConnectedProvider, + StateKind::Idle, + ], + self.psu_state.kind(), + )) + }; + self.psu_state = PsuState::Idle; + if clear_caps { + self.consumer_capability = None; + self.requested_provider_capability = None; + } + result + } + + /// Update the available consumer capability + pub fn update_consumer_power_capability( + &mut self, + capability: Option, + ) -> Result<(), Error> { + let result = match self.psu_state { + PsuState::Idle | PsuState::ConnectedConsumer(_) | PsuState::ConnectedProvider(_) => Ok(()), + _ => Err(Error::InvalidState( + &[ + StateKind::Idle, + StateKind::ConnectedConsumer, + StateKind::ConnectedProvider, + ], + self.psu_state.kind(), + )), + }; + self.consumer_capability = capability; + result + } + + /// Update the requested provider capability + pub fn update_requested_provider_power_capability( + &mut self, + capability: Option, + ) -> Result<(), Error> { + if self.requested_provider_capability == capability { + // Already operating at this capability, power policy is already aware, don't need to do anything + return Ok(()); + } + + let result = match self.psu_state { + PsuState::Idle | PsuState::ConnectedConsumer(_) | PsuState::ConnectedProvider(_) => Ok(()), + _ => Err(Error::InvalidState( + &[ + StateKind::Idle, + StateKind::ConnectedProvider, + StateKind::ConnectedConsumer, + ], + self.psu_state.kind(), + )), + }; + + self.requested_provider_capability = capability; + result + } + + /// Check if a request to connect as a consumer from the policy is valid given the current state + /// Returns () or the error with information about why the request is invalid + pub fn can_connect_consumer(&self) -> Result<(), Error> { + match self.psu_state { + PsuState::Idle | PsuState::ConnectedConsumer(_) => Ok(()), + _ => Err(Error::InvalidState( + &[StateKind::Idle, StateKind::ConnectedConsumer], + self.psu_state.kind(), + )), + } + } + + /// Handle a request to connect as a consumer from the policy + pub fn connect_consumer(&mut self, capability: ConsumerPowerCapability) -> Result<(), Error> { + self.can_connect_consumer()?; + self.psu_state = PsuState::ConnectedConsumer(capability); + Ok(()) + } + + /// Check if a request to connect as a provider from the policy is valid given the current state + /// Returns () or the error with information about why the request is invalid + pub fn can_connect_provider(&self) -> Result<(), Error> { + match self.psu_state { + PsuState::Idle | PsuState::ConnectedProvider(_) => Ok(()), + _ => Err(Error::InvalidState( + &[StateKind::Idle, StateKind::ConnectedProvider], + self.psu_state.kind(), + )), + } + } + + /// Handle a request to connect as a provider from the policy + pub fn connect_provider(&mut self, capability: ProviderPowerCapability) -> Result<(), Error> { + self.can_connect_provider()?; + self.psu_state = PsuState::ConnectedProvider(capability); + Ok(()) + } + + /// Returns the current provider capability if the PSU is connected as a provider + pub fn connected_provider_capability(&self) -> Option { + match self.psu_state { + PsuState::ConnectedProvider(capability) => Some(capability), + _ => None, + } + } +} + +/// Trait for PSU devices +pub trait Psu: Named { + /// Disconnect power from this device + fn disconnect(&mut self) -> impl Future>; + /// Connect this device to provide power to an external connection + fn connect_provider(&mut self, capability: ProviderPowerCapability) -> impl Future>; + /// Connect this device to consume power from an external connection + fn connect_consumer(&mut self, capability: ConsumerPowerCapability) -> impl Future>; + /// Return an immutable reference to the current PSU state + fn state(&self) -> &State; + /// Return a mutable reference to the current PSU state + fn state_mut(&mut self) -> &mut State; +} diff --git a/power-policy-interface/src/service/event.rs b/power-policy-interface/src/service/event.rs new file mode 100644 index 000000000..41d7b319d --- /dev/null +++ b/power-policy-interface/src/service/event.rs @@ -0,0 +1,78 @@ +use embedded_services::sync::Lockable; + +use crate::{ + capability::{ConsumerPowerCapability, ProviderPowerCapability}, + psu::Psu, + service::UnconstrainedState, +}; + +/// Event data broadcast from the service. +/// +/// This enum doesn't contain a reference to the device and is suitable +/// for receivers that don't need to know which device triggered the event +/// and allows for receivers that don't need to be generic over the device type. +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum EventData { + /// Consumer disconnected + ConsumerDisconnected, + /// Consumer connected + ConsumerConnected(ConsumerPowerCapability), + /// Provider disconnected + ProviderDisconnected, + /// Provider connected + ProviderConnected(ProviderPowerCapability), + /// Unconstrained state changed + Unconstrained(UnconstrainedState), +} + +impl<'device, PSU: Lockable> From> for EventData +where + PSU::Inner: Psu, +{ + fn from(value: Event<'device, PSU>) -> Self { + match value { + Event::ConsumerDisconnected(_) => EventData::ConsumerDisconnected, + Event::ConsumerConnected(_, capability) => EventData::ConsumerConnected(capability), + Event::ProviderDisconnected(_) => EventData::ProviderDisconnected, + Event::ProviderConnected(_, capability) => EventData::ProviderConnected(capability), + Event::Unconstrained(unconstrained) => EventData::Unconstrained(unconstrained), + } + } +} + +/// Events broadcast from the service. +#[derive(Debug, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum Event<'device, PSU: Lockable> +where + PSU::Inner: Psu, +{ + /// Consumer disconnected + ConsumerDisconnected(&'device PSU), + /// Consumer connected + ConsumerConnected(&'device PSU, ConsumerPowerCapability), + /// Provider disconnected + ProviderDisconnected(&'device PSU), + /// Provider connected + ProviderConnected(&'device PSU, ProviderPowerCapability), + /// Unconstrained state changed + Unconstrained(UnconstrainedState), +} + +impl<'device, PSU> Clone for Event<'device, PSU> +where + PSU: Lockable, + PSU::Inner: Psu, +{ + fn clone(&self) -> Self { + *self + } +} + +impl<'device, PSU> Copy for Event<'device, PSU> +where + PSU: Lockable, + PSU::Inner: Psu, +{ +} diff --git a/power-policy-interface/src/service/mod.rs b/power-policy-interface/src/service/mod.rs new file mode 100644 index 000000000..a87dfe0b9 --- /dev/null +++ b/power-policy-interface/src/service/mod.rs @@ -0,0 +1,21 @@ +pub mod event; + +/// Unconstrained state information +#[derive(Debug, Clone, Default, Copy, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct UnconstrainedState { + /// Unconstrained state + pub unconstrained: bool, + /// Available unconstrained devices + pub available: usize, +} + +impl UnconstrainedState { + /// Create a new unconstrained state + pub fn new(unconstrained: bool, available: usize) -> Self { + Self { + unconstrained, + available, + } + } +} diff --git a/power-policy-service/Cargo.toml b/power-policy-service/Cargo.toml index def102017..1be2dd6a3 100644 --- a/power-policy-service/Cargo.toml +++ b/power-policy-service/Cargo.toml @@ -7,6 +7,9 @@ repository = "https://github.com/OpenDevicePartnership/embedded-services" rust-version.workspace = true license = "MIT" +[package.metadata.cargo-machete] +ignored = ["critical-section"] + [lints] workspace = true @@ -18,21 +21,32 @@ embassy-time.workspace = true embedded-services.workspace = true log = { workspace = true, optional = true } heapless.workspace = true +power-policy-interface.workspace = true + +[dev-dependencies] +critical-section = { workspace = true, features = ["std"] } +embassy-time = { workspace = true, features = ["std", "generic-queue-8"] } +tokio = { workspace = true, features = ["rt", "macros", "time"] } +env_logger = "0.11.8" +log = { workspace = true } +embedded-batteries-async = { workspace = true } +# TODO: figure out why enabling the log feature here causes running tests at the workspace level to fail to compile +# Uncomment this line to enable log output in tests +# power-policy-service = { workspace = true, features = ["log"] } [features] default = [] defmt = [ "dep:defmt", "embedded-services/defmt", + "power-policy-interface/defmt", "embassy-time/defmt", "embassy-sync/defmt", ] log = [ "dep:log", "embedded-services/log", + "power-policy-interface/log", "embassy-time/log", "embassy-sync/log", ] - -[package.metadata.cargo-machete] -ignored = ["log"] diff --git a/power-policy-service/src/charger.rs b/power-policy-service/src/charger.rs index f8e12e17e..98ddcca1d 100644 --- a/power-policy-service/src/charger.rs +++ b/power-policy-service/src/charger.rs @@ -1,270 +1,43 @@ -use embassy_sync::mutex::Mutex; -use embedded_services::GlobalRawMutex; +use core::pin::pin; -use embassy_futures::select::select; -use embedded_services::{ - debug, error, info, - power::policy::charger::{ - self, ChargeController, ChargerEvent, ChargerResponse, InternalState, PolicyEvent, PoweredSubstate, State, - }, - trace, warn, -}; +use embassy_futures::select::select_slice; +use embedded_services::event::Receiver; +use embedded_services::sync::Lockable; +use power_policy_interface::charger::Charger; +use power_policy_interface::charger::event::{Event, EventData}; -pub struct Wrapper<'a, C: ChargeController> +/// Struct used to contain charger event receivers and manage mapping from a receiver to its corresponding device. +pub struct ChargerEventReceivers<'a, const N: usize, CHARGER: Lockable, R: Receiver> where - charger::ChargerError: From<::ChargeControllerError>, + CHARGER::Inner: Charger, { - charger_policy_state: &'a charger::Device, - controller: Mutex, + pub charger_devices: [&'a CHARGER; N], + pub receivers: [R; N], } -impl<'a, C: ChargeController> Wrapper<'a, C> +impl<'a, const N: usize, CHARGER: Lockable, R: Receiver> ChargerEventReceivers<'a, N, CHARGER, R> where - charger::ChargerError: From<::ChargeControllerError>, + CHARGER::Inner: Charger, { - pub fn new(charger_policy_state: &'a charger::Device, controller: C) -> Self { + /// Create a new instance + pub fn new(charger_devices: [&'a CHARGER; N], receivers: [R; N]) -> Self { Self { - charger_policy_state, - controller: Mutex::new(controller), + charger_devices, + receivers, } } - pub async fn get_state(&self) -> charger::InternalState { - self.charger_policy_state.state().await - } - - pub async fn set_state(&self, new_state: charger::InternalState) { - self.charger_policy_state.set_state(new_state).await - } - - async fn wait_policy_command(&self) -> PolicyEvent { - self.charger_policy_state.wait_command().await - } - - #[allow(clippy::single_match)] - async fn process_controller_event(&self, controller: &mut C, event: ChargerEvent) { - let state = self.get_state().await; - match state.state { - State::Powered(powered_substate) => match powered_substate { - PoweredSubstate::Init => match event { - ChargerEvent::Initialized(psu_state) => { - self.set_state(InternalState { - state: match psu_state { - charger::PsuState::Attached => State::Powered(PoweredSubstate::PsuAttached), - charger::PsuState::Detached => State::Powered(PoweredSubstate::PsuDetached), - }, - capability: state.capability, - }) - .await; - - // If we have a cached capability, it means a power contract was sent to the charger before we finished initializing - // Go ahead and apply that capability to the charger hardware. - if let Some(power_capability) = state.capability { - debug!("Charger just finished initializing and a new power policy configuration was detected while initializing. - Executing attach sequence with cached capability"); - if let Err(e) = controller.attach_handler(power_capability).await { - let err = charger::ChargerError::from(e); - error!("Error executing charger power port attach sequence: {:?}", err) - } - } - } - // If we are initializing, we don't want to update the state - _ => (), - }, - PoweredSubstate::PsuAttached => match event { - ChargerEvent::PsuStateChange(charger::PsuState::Detached) => { - self.set_state(InternalState { - state: State::Powered(PoweredSubstate::PsuDetached), - capability: state.capability, - }) - .await - } - ChargerEvent::Timeout => { - self.set_state(InternalState { - state: State::Powered(PoweredSubstate::Init), - capability: None, - }) - .await - } - _ => (), - }, - PoweredSubstate::PsuDetached => match event { - ChargerEvent::PsuStateChange(charger::PsuState::Attached) => { - self.set_state(InternalState { - state: State::Powered(PoweredSubstate::PsuAttached), - capability: state.capability, - }) - .await - } - ChargerEvent::Timeout => { - self.set_state(InternalState { - state: State::Powered(PoweredSubstate::Init), - capability: None, - }) - .await - } - _ => (), - }, - }, - State::Unpowered => warn!( - "Charger is unpowered but ChargeController event received event: {:?}", - event - ), - } - } - - async fn process_policy_command(&self, controller: &mut C, event: PolicyEvent) { - let state = self.get_state().await; - let res: ChargerResponse = match event { - PolicyEvent::InitRequest => { - if state.state == State::Unpowered { - error!("Charger received request to initialize but it's unpowered!"); - Err(charger::ChargerError::InvalidState(State::Unpowered)) - } else { - if state.state == State::Powered(PoweredSubstate::Init) { - info!("Charger received request to initialize."); - } else { - warn!("Charger received request to initialize but it's already initialized! Reinitializing..."); - } - - if let Err(err) = controller.init_charger().await { - error!("Charger failed initialzation sequence."); - Err(err.into()) - } else { - Ok(charger::ChargerResponseData::Ack) - } - } - } - PolicyEvent::PolicyConfiguration(power_capability) => match state.state { - State::Unpowered => { - // Power policy sends this event when a new type-c plug event comes in - // For the scenario where the charger is unpowered, we don't want to block the power policy - // from completing it's connect_consumer() call, as there might be cases where we don't want - // chargers to be powered or the charger can't be powered. - error!( - "Charger detected new power policy configuration but it's unpowered! - Caching capability so when we finish initializing, we start off with this capability set." - ); - self.set_state(InternalState { - state: state.state, - capability: Some(power_capability), - }) - .await; - Ok(charger::ChargerResponseData::UnpoweredAck) - } - State::Powered(substate) => match substate { - PoweredSubstate::Init => { - warn!( - "Charger detected new power policy configuration but charger is still initializing. - Caching capability so when we finish initializing, we start off with this capability set." - ); - self.set_state(InternalState { - state: state.state, - capability: Some(power_capability), - }) - .await; - Err(charger::ChargerError::InvalidState(State::Powered( - PoweredSubstate::Init, - ))) - } - PoweredSubstate::PsuAttached | PoweredSubstate::PsuDetached => { - if power_capability.capability.current_ma == 0 { - // Policy detected a detach - debug!("Charger detected new power policy configuration. Executing detach sequence"); - if let Err(err) = controller - .detach_handler() - .await - .inspect_err(|_| error!("Error executing charger power port detach sequence!")) - { - Err(err.into()) - } else { - // Update power capability but do not change controller state. - // That is handled by process_controller_event(). - // This way capability is cached even if the - // hardware charger device lags on changing its PSU state. - self.set_state(InternalState { - state: state.state, - capability: None, - }) - .await; - Ok(charger::ChargerResponseData::Ack) - } - } else { - // Policy detected an attach - debug!("Charger detected new power policy configuration. Executing attach sequence"); - if let Err(err) = controller - .attach_handler(power_capability) - .await - .inspect_err(|_| error!("Error executing charger power port attach sequence!")) - { - Err(err.into()) - } else { - // Update power capability but do not change controller state. - // That is handled by process_controller_event(). - // This way capability is cached even if the - // hardware charger device lags on changing its PSU state. - self.set_state(InternalState { - state: state.state, - capability: Some(power_capability), - }) - .await; - Ok(charger::ChargerResponseData::Ack) - } - } - } - }, - }, - PolicyEvent::CheckReady => { - debug!("Charger received check ready request."); - let ret = controller.is_ready().await; - match state.state { - State::Powered(_) => { - if let Err(e) = ret { - self.set_state(InternalState { - state: State::Unpowered, - // Cache capability for logging/debug - capability: state.capability, - }) - .await; - Err(e.into()) - } else { - Ok(charger::ChargerResponseData::Ack) - } - } - State::Unpowered => { - if let Err(e) = ret { - Err(e.into()) - } else { - self.set_state(InternalState { - state: State::Powered(PoweredSubstate::Init), - capability: None, - }) - .await; - Ok(charger::ChargerResponseData::Ack) - } - } - } + /// Get the next pending charger event + pub async fn wait_event(&mut self) -> Event<'a, CHARGER> { + let ((event, charger), _) = { + let mut futures = heapless::Vec::<_, N>::new(); + for (receiver, psu) in self.receivers.iter_mut().zip(self.charger_devices.iter()) { + // Push will never fail since the number of receivers is the same as the capacity of the vector + let _ = futures.push(async move { (receiver.wait_next().await, psu) }); } + select_slice(pin!(&mut futures)).await }; - // Send response - self.charger_policy_state.send_response(res).await; - } - - pub async fn process(&self) { - let mut controller = self.controller.lock().await; - loop { - let res = select(controller.wait_event(), self.wait_policy_command()).await; - match res { - embassy_futures::select::Either::First(event) => { - trace!("New charger device event."); - self.process_controller_event(&mut controller, event).await; - } - embassy_futures::select::Either::Second(event) => { - trace!("New charger policy command."); - self.process_policy_command(&mut controller, event).await; - } - }; - } + Event { charger, event } } } diff --git a/power-policy-service/src/consumer.rs b/power-policy-service/src/consumer.rs deleted file mode 100644 index d03101bd6..000000000 --- a/power-policy-service/src/consumer.rs +++ /dev/null @@ -1,306 +0,0 @@ -use core::cmp::Ordering; -use embedded_services::debug; -use embedded_services::power::policy::charger::Device as ChargerDevice; -use embedded_services::power::policy::charger::PolicyEvent; -use embedded_services::power::policy::policy::check_chargers_ready; -use embedded_services::power::policy::policy::init_chargers; - -use super::*; - -/// State of the current consumer -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -#[cfg_attr(feature = "defmt", derive(defmt::Format))] -pub struct AvailableConsumer { - /// The ID of the currently connected consumer - pub device_id: DeviceId, - /// The power capability of the currently connected consumer - pub consumer_power_capability: ConsumerPowerCapability, -} - -/// Compare two consumer capabilities to determine which one is better -/// -/// This is not part of the `Ord` implementation for `ConsumerPowerCapability`, because it's specific to this implementation. -/// *_is_current indicate if the device with that capability is the currently connected consumer. This is used to make the -/// implementation stick so as to avoid switching between otherwise equivalent consumers. -fn cmp_consumer_capability( - a: &ConsumerPowerCapability, - a_is_current: bool, - b: &ConsumerPowerCapability, - b_is_current: bool, -) -> Ordering { - (a.capability, a_is_current).cmp(&(b.capability, b_is_current)) -} - -impl PowerPolicy { - /// Iterate over all devices to determine what is best power port provides the highest power - async fn find_best_consumer(&self, state: &InternalState) -> Result, Error> { - let mut best_consumer = None; - let current_consumer_id = state.current_consumer_state.map(|f| f.device_id); - - for node in self.context.devices() { - let device = node.data::().ok_or(Error::InvalidDevice)?; - - let consumer_capability = device.consumer_capability().await; - // Don't consider consumers below minimum threshold - if consumer_capability - .zip(self.config.min_consumer_threshold_mw) - .is_some_and(|(cap, min)| cap.capability.max_power_mw() < min) - { - info!( - "Device{}: Not considering consumer, power capability is too low", - device.id().0, - ); - continue; - } - - // Update the best available consumer - best_consumer = match (best_consumer, consumer_capability) { - // Nothing available - (None, None) => None, - // No existing consumer - (None, Some(power_capability)) => Some(AvailableConsumer { - device_id: device.id(), - consumer_power_capability: power_capability, - }), - // Existing consumer, no new consumer - (Some(_), None) => best_consumer, - // Existing consumer, new available consumer - (Some(best), Some(available)) => { - if cmp_consumer_capability( - &available, - Some(device.id()) == current_consumer_id, - &best.consumer_power_capability, - Some(best.device_id) == current_consumer_id, - ) == core::cmp::Ordering::Greater - { - Some(AvailableConsumer { - device_id: device.id(), - consumer_power_capability: available, - }) - } else { - best_consumer - } - } - }; - } - - Ok(best_consumer) - } - - /// Update unconstrained state and broadcast notifications if needed - async fn update_unconstrained_state(&self, state: &mut InternalState) -> Result<(), Error> { - // Count how many available unconstrained devices we have - let mut unconstrained_new = UnconstrainedState::default(); - for node in self.context.devices() { - let device = node.data::().ok_or(Error::InvalidDevice)?; - if let Some(capability) = device.consumer_capability().await - && capability.flags.unconstrained_power() - { - unconstrained_new.available += 1; - } - } - - // The overall unconstrained state is true if an unconstrained consumer is currently connected - unconstrained_new.unconstrained = state - .current_consumer_state - .is_some_and(|current| current.consumer_power_capability.flags.unconstrained_power()); - - if unconstrained_new != state.unconstrained { - info!("Unconstrained state changed: {:?}", unconstrained_new); - state.unconstrained = unconstrained_new; - self.comms_notify(CommsMessage { - data: CommsData::Unconstrained(state.unconstrained), - }) - .await; - } - Ok(()) - } - - /// Common logic to execute after a consumer is connected - async fn post_consumer_connected( - &self, - state: &mut InternalState, - connected_consumer: AvailableConsumer, - ) -> Result<(), Error> { - state.current_consumer_state = Some(connected_consumer); - // todo: review the delay time - embassy_time::Timer::after_millis(800).await; - - // If no chargers are registered, they won't receive the new power capability. - for node in self.context.chargers() { - let device = node.data::().ok_or(Error::InvalidDevice)?; - // Chargers should be powered at this point, but in case they are not... - if let embedded_services::power::policy::charger::ChargerResponseData::UnpoweredAck = device - .execute_command(PolicyEvent::PolicyConfiguration( - connected_consumer.consumer_power_capability, - )) - .await? - { - // Force charger CheckReady and InitRequest to get it into an initialized state. - // This condition can get hit if we did not have a previous consumer and the charger is unpowered. - info!("Charger is unpowered, forcing charger CheckReady and Init sequence"); - check_chargers_ready().await?; - init_chargers().await?; - device - .execute_command(PolicyEvent::PolicyConfiguration( - connected_consumer.consumer_power_capability, - )) - .await?; - } - } - self.comms_notify(CommsMessage { - data: CommsData::ConsumerConnected( - connected_consumer.device_id, - connected_consumer.consumer_power_capability, - ), - }) - .await; - - Ok(()) - } - - /// Disconnect all chargers - pub(super) async fn disconnect_chargers(&self) -> Result<(), Error> { - for node in self.context.chargers() { - let device = node.data::().ok_or(Error::InvalidDevice)?; - if let embedded_services::power::policy::charger::ChargerResponseData::UnpoweredAck = device - .execute_command(PolicyEvent::PolicyConfiguration(ConsumerPowerCapability { - capability: PowerCapability { - voltage_mv: 0, - current_ma: 0, - }, - flags: flags::Consumer::none(), - })) - .await? - { - debug!("Charger is unpowered, continuing disconnect_chargers()..."); - } - } - - Ok(()) - } - - /// Connect to a new consumer - async fn connect_new_consumer( - &self, - state: &mut InternalState, - new_consumer: AvailableConsumer, - ) -> Result<(), Error> { - // Handle our current consumer - if let Some(current_consumer) = state.current_consumer_state { - if new_consumer.device_id == current_consumer.device_id - && new_consumer.consumer_power_capability == current_consumer.consumer_power_capability - { - // If the consumer is the same device, capability, and is still available, we don't need to do anything - info!("Best consumer is the same, not switching"); - return Ok(()); - } - - state.current_consumer_state = None; - // Disconnect the current consumer if needed - if let Ok(consumer) = self - .context - .try_policy_action::(current_consumer.device_id) - .await - { - info!( - "Device {}, disconnecting current consumer", - current_consumer.device_id.0 - ); - // disconnect current consumer and set idle - consumer.disconnect().await?; - } - - // If no chargers are registered, they won't receive the new power capability. - // Also, if chargers return UnpoweredAck, that means the charger isn't powered. - // Further down this fn the power rails are enabled and thus the charger will get power, - // so just continue execution. - self.disconnect_chargers().await?; - - self.comms_notify(CommsMessage { - data: CommsData::ConsumerDisconnected(current_consumer.device_id), - }) - .await; - - // Don't update the unconstrained here because this is a transitional state - } - - info!("Device {}, connecting new consumer", new_consumer.device_id.0); - if let Ok(idle) = self - .context - .try_policy_action::(new_consumer.device_id) - .await - { - idle.connect_consumer(new_consumer.consumer_power_capability).await?; - self.post_consumer_connected(state, new_consumer).await?; - } else if let Ok(provider) = self - .context - .try_policy_action::(new_consumer.device_id) - .await - { - provider - .connect_consumer(new_consumer.consumer_power_capability) - .await?; - state.current_consumer_state = Some(new_consumer); - self.post_consumer_connected(state, new_consumer).await?; - } else { - error!("Error obtaining device in idle state"); - } - - Ok(()) - } - - /// Determines and connects the best external power - pub(super) async fn update_current_consumer(&self) -> Result<(), Error> { - let mut guard = self.state.lock().await; - let state = guard.deref_mut(); - info!( - "Selecting power port, current power: {:#?}", - state.current_consumer_state - ); - - let best_consumer = self.find_best_consumer(state).await?; - info!("Best consumer: {:#?}", best_consumer); - if let Some(best_consumer) = best_consumer { - self.connect_new_consumer(state, best_consumer).await?; - } else { - // Notify disconnect if recently detached consumer was previously attached. - if let Some(consumer_state) = state.current_consumer_state { - self.disconnect_chargers().await?; - self.comms_notify(CommsMessage { - data: CommsData::ConsumerDisconnected(consumer_state.device_id), - }) - .await; - } - // No new consumer available - state.current_consumer_state = None; - } - - self.update_unconstrained_state(state).await - } -} - -#[cfg(test)] -mod tests { - use super::*; - - const P0: PowerCapability = PowerCapability { - voltage_mv: 5000, - current_ma: 1000, - }; - const P1: PowerCapability = PowerCapability { - voltage_mv: 5000, - current_ma: 1500, - }; - - /// Tests the [`cmp_consumer_capability`] without any flags set. - #[test] - fn test_cmp_consumer_capability_no_flags() { - let p0 = P0.into(); - let p1 = P1.into(); - - assert_eq!(cmp_consumer_capability(&p0, false, &p1, false), Ordering::Less); - assert_eq!(cmp_consumer_capability(&p1, false, &p1, false), Ordering::Equal); - assert_eq!(cmp_consumer_capability(&p1, false, &p0, false), Ordering::Greater); - } -} diff --git a/power-policy-service/src/lib.rs b/power-policy-service/src/lib.rs index 85c598ad7..1142968c0 100644 --- a/power-policy-service/src/lib.rs +++ b/power-policy-service/src/lib.rs @@ -1,192 +1,4 @@ #![no_std] -use core::ops::DerefMut; -use embassy_sync::mutex::Mutex; -use embedded_services::GlobalRawMutex; -use embedded_services::power::policy::device::Device; -use embedded_services::power::policy::{action, policy, *}; -use embedded_services::{comms, error, info}; - -pub mod config; -pub mod consumer; -pub mod provider; -pub mod task; - -pub use config::Config; - -use crate::provider::PowerState; pub mod charger; - -const MAX_CONNECTED_PROVIDERS: usize = 4; - -#[derive(Clone, Default)] -struct InternalState { - /// Current consumer state, if any - current_consumer_state: Option, - /// Current provider global state - current_provider_state: provider::State, - /// System unconstrained power - unconstrained: UnconstrainedState, - /// Connected providers - connected_providers: heapless::index_set::FnvIndexSet, -} - -/// Power policy state -pub struct PowerPolicy { - /// Power policy context - context: policy::ContextToken, - /// State - state: Mutex, - /// Comms endpoint - tp: comms::Endpoint, - /// Config - config: config::Config, -} - -impl PowerPolicy { - /// Create a new power policy - pub fn create(config: config::Config) -> Option { - Some(Self { - context: policy::ContextToken::create()?, - state: Mutex::new(InternalState::default()), - tp: comms::Endpoint::uninit(comms::EndpointID::Internal(comms::Internal::Power)), - config, - }) - } - - async fn process_notify_attach(&self) -> Result<(), Error> { - self.context.send_response(Ok(policy::ResponseData::Complete)).await; - Ok(()) - } - - async fn process_notify_detach(&self, device: &device::Device) -> Result<(), Error> { - self.context.send_response(Ok(policy::ResponseData::Complete)).await; - self.remove_connected_provider(device.id()).await; - self.update_current_consumer().await?; - Ok(()) - } - - async fn process_notify_consumer_power_capability(&self) -> Result<(), Error> { - self.context.send_response(Ok(policy::ResponseData::Complete)).await; - self.update_current_consumer().await?; - Ok(()) - } - - async fn process_request_provider_power_capabilities(&self, device: DeviceId) -> Result<(), Error> { - self.context.send_response(Ok(policy::ResponseData::Complete)).await; - self.connect_provider(device).await; - Ok(()) - } - - async fn process_notify_disconnect(&self, device: &device::Device) -> Result<(), Error> { - self.context.send_response(Ok(policy::ResponseData::Complete)).await; - - self.remove_connected_provider(device.id()).await; - let current_consumer = self - .state - .lock() - .await - .current_consumer_state - .take_if(|d| d.device_id == device.id()); - if let Some(consumer) = current_consumer { - // Current PSU is disconnected, disconnect chargers, and attempt to select next PSU - info!("Device{}: Connected consumer disconnected", consumer.device_id.0); - self.disconnect_chargers().await?; - - self.comms_notify(CommsMessage { - data: CommsData::ConsumerDisconnected(consumer.device_id), - }) - .await; - - self.update_current_consumer().await?; - } - - Ok(()) - } - - /// Send a notification with the comms service - async fn comms_notify(&self, message: CommsMessage) { - self.context.broadcast_message(message).await; - let _ = self - .tp - .send(comms::EndpointID::Internal(comms::Internal::Battery), &message) - .await; - } - - /// Common logic for when a provider is disconnected - /// - /// Returns true if the device was operating as a provider - async fn remove_connected_provider(&self, device_id: DeviceId) -> bool { - if self.state.lock().await.connected_providers.remove(&device_id) { - // Determine total requested power draw - let mut total_power_mw = 0; - for device in self.context.devices().iter_only::() { - total_power_mw += device - .provider_capability() - .await - .map_or(0, |cap| cap.capability.max_power_mw()); - } - - self.state.lock().await.current_provider_state.state = - if total_power_mw > self.config.limited_power_threshold_mw { - PowerState::Limited - } else { - PowerState::Unlimited - }; - - self.comms_notify(CommsMessage { - data: CommsData::ProviderDisconnected(device_id), - }) - .await; - true - } else { - false - } - } - - async fn wait_request(&self) -> policy::Request { - self.context.wait_request().await - } - - async fn process_request(&self, request: policy::Request) -> Result<(), Error> { - let device = self.context.get_device(request.id)?; - - match request.data { - policy::RequestData::NotifyAttached => { - info!("Received notify attached from device {}", device.id().0); - self.process_notify_attach().await - } - policy::RequestData::NotifyDetached => { - info!("Received notify detached from device {}", device.id().0); - self.process_notify_detach(device).await - } - policy::RequestData::NotifyConsumerCapability(capability) => { - info!( - "Device{}: Received notify consumer capability: {:#?}", - device.id().0, - capability, - ); - self.process_notify_consumer_power_capability().await - } - policy::RequestData::RequestProviderCapability(capability) => { - info!( - "Device{}: Received request provider capability: {:#?}", - device.id().0, - capability, - ); - self.process_request_provider_power_capabilities(device.id()).await - } - policy::RequestData::NotifyDisconnect => { - info!("Received notify disconnect from device {}", device.id().0); - self.process_notify_disconnect(device).await - } - } - } - - /// Top-level event loop function - pub async fn process(&self) -> Result<(), Error> { - let request = self.wait_request().await; - self.process_request(request).await - } -} - -impl comms::MailboxDelegate for PowerPolicy {} +pub mod psu; +pub mod service; diff --git a/power-policy-service/src/provider.rs b/power-policy-service/src/provider.rs deleted file mode 100644 index 2b31c1633..000000000 --- a/power-policy-service/src/provider.rs +++ /dev/null @@ -1,136 +0,0 @@ -//! This file implements logic to determine how much power to provide to each connected device. -//! When total provided power is below [limited_power_threshold_mw](super::Config::limited_power_threshold_mw) -//! the system is in unlimited power state. In this mode up to [provider_unlimited](super::Config::provider_unlimited) -//! is provided to each device. Above this threshold, the system is in limited power state. -//! In this mode [provider_limited](super::Config::provider_limited) is provided to each device -use embedded_services::{debug, trace}; - -use super::*; - -/// Current system provider power state -#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] -#[cfg_attr(feature = "defmt", derive(defmt::Format))] -pub enum PowerState { - /// System is capable of providing high power - #[default] - Unlimited, - /// System can only provide limited power - Limited, -} - -/// Power policy provider global state -#[derive(Clone, Copy, Default)] -pub(super) struct State { - /// Current power state - pub state: PowerState, -} - -impl PowerPolicy { - /// Attempt to connect the requester as a provider - pub(super) async fn connect_provider(&self, requester_id: DeviceId) { - trace!("Device{}: Attempting to connect as provider", requester_id.0); - let requester = match self.context.get_device(requester_id) { - Ok(device) => device, - Err(_) => { - error!("Device{}: Invalid device", requester_id.0); - return; - } - }; - let requested_power_capability = match requester.requested_provider_capability().await { - Some(cap) => cap, - // Requester is no longer requesting power - _ => { - info!("Device{}: No-longer requesting power", requester.id().0); - return; - } - }; - let mut state = self.state.lock().await; - let mut total_power_mw = 0; - - // Determine total requested power draw - for device in self.context.devices().iter_only::() { - let target_provider_cap = if device.id() == requester_id { - // Use the requester's requested power capability - // this handles both new connections and upgrade requests - Some(requested_power_capability) - } else { - // Use the device's current working provider capability - device.provider_capability().await - }; - total_power_mw += target_provider_cap.map_or(0, |cap| cap.capability.max_power_mw()); - - if total_power_mw > self.config.limited_power_threshold_mw { - state.current_provider_state.state = PowerState::Limited; - } else { - state.current_provider_state.state = PowerState::Unlimited; - } - } - - debug!("New power state: {:?}", state.current_provider_state.state); - - let target_power = match state.current_provider_state.state { - PowerState::Limited => ProviderPowerCapability { - capability: self.config.provider_limited, - flags: requested_power_capability.flags, - }, - PowerState::Unlimited => { - if requested_power_capability.capability.max_power_mw() < self.config.provider_unlimited.max_power_mw() - { - // Don't auto upgrade to a higher contract - requested_power_capability - } else { - ProviderPowerCapability { - capability: self.config.provider_unlimited, - flags: requested_power_capability.flags, - } - } - } - }; - - let connected = if let Ok(action) = self.context.try_policy_action::(requester.id()).await { - if let Err(e) = action.connect_provider(target_power).await { - error!("Device{}: Failed to connect as provider, {:#?}", requester.id().0, e); - } else { - self.post_provider_connected(&mut state, requester.id(), target_power) - .await; - } - Ok(()) - } else if let Ok(action) = self - .context - .try_policy_action::(requester.id()) - .await - { - if let Err(e) = action.connect_provider(target_power).await { - error!("Device{}: Failed to connect as provider, {:#?}", requester.id().0, e); - } else { - self.post_provider_connected(&mut state, requester.id(), target_power) - .await; - } - Ok(()) - } else { - Err(Error::InvalidState( - device::StateKind::Idle, - requester.state().await.kind(), - )) - }; - - // Don't need to do anything special, the device is responsible for attempting to reconnect - if let Err(e) = connected { - error!("Device{}: Failed to connect as provider, {:#?}", requester.id().0, e); - } - } - - /// Common logic for after a provider has successfully connected - async fn post_provider_connected( - &self, - state: &mut InternalState, - provider_id: DeviceId, - target_power: ProviderPowerCapability, - ) { - let _ = state.connected_providers.insert(provider_id); - self.comms_notify(CommsMessage { - data: CommsData::ProviderConnected(provider_id, target_power), - }) - .await; - } -} diff --git a/power-policy-service/src/psu.rs b/power-policy-service/src/psu.rs new file mode 100644 index 000000000..99b72cf61 --- /dev/null +++ b/power-policy-service/src/psu.rs @@ -0,0 +1,40 @@ +use core::pin::pin; + +use embassy_futures::select::select_slice; +use embedded_services::event::Receiver; +use embedded_services::sync::Lockable; +use power_policy_interface::psu::Psu; +use power_policy_interface::psu::event::{Event, EventData}; + +/// Struct used to contain PSU event receivers and manage mapping from a receiver to its corresponding device. +pub struct PsuEventReceivers<'a, const N: usize, PSU: Lockable, R: Receiver> +where + PSU::Inner: Psu, +{ + pub psu_devices: [&'a PSU; N], + pub receivers: [R; N], +} + +impl<'a, const N: usize, PSU: Lockable, R: Receiver> PsuEventReceivers<'a, N, PSU, R> +where + PSU::Inner: Psu, +{ + /// Create a new instance + pub fn new(psu_devices: [&'a PSU; N], receivers: [R; N]) -> Self { + Self { psu_devices, receivers } + } + + /// Get the next pending PSU event + pub async fn wait_event(&mut self) -> Event<'a, PSU> { + let ((event, psu), _) = { + let mut futures = heapless::Vec::<_, N>::new(); + for (receiver, psu) in self.receivers.iter_mut().zip(self.psu_devices.iter()) { + // Push will never fail since the number of receivers is the same as the capacity of the vector + let _ = futures.push(async move { (receiver.wait_next().await, psu) }); + } + select_slice(pin!(&mut futures)).await + }; + + Event { psu, event } + } +} diff --git a/power-policy-service/src/config.rs b/power-policy-service/src/service/config.rs similarity index 95% rename from power-policy-service/src/config.rs rename to power-policy-service/src/service/config.rs index 4ee3fce96..854739e6d 100644 --- a/power-policy-service/src/config.rs +++ b/power-policy-service/src/service/config.rs @@ -1,6 +1,6 @@ //! Configuration types for the power policy service -use embedded_services::power::policy::PowerCapability; +use power_policy_interface::capability::PowerCapability; #[derive(Clone, Copy)] pub struct Config { diff --git a/power-policy-service/src/service/consumer.rs b/power-policy-service/src/service/consumer.rs new file mode 100644 index 000000000..77fffd30f --- /dev/null +++ b/power-policy-service/src/service/consumer.rs @@ -0,0 +1,287 @@ +use core::cmp::Ordering; +use embedded_services::error; +use embedded_services::named::Named; + +use super::*; + +use power_policy_interface::psu; +use power_policy_interface::service::event::Event as ServiceEvent; +use power_policy_interface::{capability::ConsumerPowerCapability, psu::PsuState}; + +/// State of the current consumer +#[derive(Debug, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct AvailableConsumer<'device, Psu: Lockable> { + /// Device reference + pub psu: &'device Psu, + /// The power capability of the currently connected consumer + pub consumer_power_capability: ConsumerPowerCapability, +} + +impl<'device, Psu: Lockable> Clone for AvailableConsumer<'device, Psu> { + fn clone(&self) -> Self { + *self + } +} + +impl<'device, Psu: Lockable> Copy for AvailableConsumer<'device, Psu> {} + +/// Compare two consumer capabilities to determine which one is better +/// +/// This is not part of the `Ord` implementation for `ConsumerPowerCapability`, because it's specific to this implementation. +/// *_is_current indicate if the device with that capability is the currently connected consumer. This is used to make the +/// implementation stick so as to avoid switching between otherwise equivalent consumers. +fn cmp_consumer_capability( + a: &ConsumerPowerCapability, + a_is_current: bool, + b: &ConsumerPowerCapability, + b_is_current: bool, +) -> Ordering { + (a.capability, a_is_current).cmp(&(b.capability, b_is_current)) +} + +impl<'device, Reg: Registration<'device>> Service<'device, Reg> { + /// Iterate over all devices to determine what is best power port provides the highest power + async fn find_best_consumer(&self) -> Result>, Error> { + let mut best_consumer = None; + let current_consumer = self.state.current_consumer_state.as_ref().map(|f| f.psu); + + for psu in self.registration.psus() { + let locked_psu = psu.lock().await; + let consumer_capability = locked_psu.state().consumer_capability; + // Don't consider consumers below minimum threshold + if consumer_capability + .zip(self.config.min_consumer_threshold_mw) + .is_some_and(|(cap, min)| cap.capability.max_power_mw() < min) + { + info!( + "({}): Not considering consumer, power capability is too low", + locked_psu.name(), + ); + continue; + } + + // Update the best available consumer + best_consumer = match (best_consumer, consumer_capability) { + // Nothing available + (None, None) => None, + // No existing consumer + (None, Some(power_capability)) => Some(AvailableConsumer { + psu: *psu, + consumer_power_capability: power_capability, + }), + // Existing consumer, no new consumer + (Some(_), None) => best_consumer, + // Existing consumer, new available consumer + (Some(best), Some(available)) => { + if cmp_consumer_capability( + &available, + current_consumer.is_some_and(|current_consumer| ptr::eq(current_consumer, *psu)), + &best.consumer_power_capability, + current_consumer.is_some_and(|current_consumer| ptr::eq(current_consumer, best.psu)), + ) == core::cmp::Ordering::Greater + { + Some(AvailableConsumer { + psu, + consumer_power_capability: available, + }) + } else { + best_consumer + } + } + }; + } + + Ok(best_consumer) + } + + /// Update unconstrained state and broadcast notifications if needed + async fn update_unconstrained_state(&mut self) -> Result<(), Error> { + // Count how many available unconstrained devices we have + let mut unconstrained_new = UnconstrainedState::default(); + for psu in self.registration.psus() { + if let Some(capability) = psu.lock().await.state().consumer_capability + && capability.flags.unconstrained_power() + { + unconstrained_new.available += 1; + } + } + + // The overall unconstrained state is true if an unconstrained consumer is currently connected + unconstrained_new.unconstrained = self + .state + .current_consumer_state + .as_ref() + .is_some_and(|current| current.consumer_power_capability.flags.unconstrained_power()); + + if unconstrained_new != self.state.unconstrained { + info!("Unconstrained state changed: {:?}", unconstrained_new); + self.state.unconstrained = unconstrained_new; + self.broadcast_event(ServiceEvent::Unconstrained(self.state.unconstrained)) + .await; + } + Ok(()) + } + + /// Common logic to execute after a consumer is connected + async fn post_consumer_connected( + &mut self, + connected_consumer: AvailableConsumer<'device, Reg::Psu>, + ) -> Result<(), Error> { + self.state.current_consumer_state = Some(connected_consumer); + // todo: review the delay time + embassy_time::Timer::after_millis(800).await; + + // If no chargers are registered, they won't receive the new power capability. + for node in self.registration.chargers() { + let mut locked_charger = node.lock().await; + // Chargers should be powered at this point, but in case they are not... + if locked_charger.state().is_unpowered() { + // Force charger CheckReady and InitRequest to get it into an initialized state. + // This condition can get hit if we did not have a previous consumer and the charger is unpowered. + info!("Charger is unpowered, forcing charger CheckReady and Init sequence"); + + locked_charger.is_ready().await.map_err(|e| Error::Charger(e.into()))?; + locked_charger + .init_charger() + .await + .map_err(|e| Error::Charger(e.into()))?; + } + + // Attach and update state to new capability + locked_charger + .attach_handler(connected_consumer.consumer_power_capability) + .await + .map_err(|e| Error::Charger(e.into()))?; + } + self.broadcast_event(ServiceEvent::ConsumerConnected( + connected_consumer.psu, + connected_consumer.consumer_power_capability, + )) + .await; + + Ok(()) + } + + /// Disconnect all chargers, skipping over unpowered chargers + pub(super) async fn disconnect_chargers(&self) -> Result<(), Error> { + for charger in self.registration.chargers() { + let mut locked_charger = charger.lock().await; + if !locked_charger.state().is_unpowered() { + locked_charger + .detach_handler() + .await + .map_err(|e| Error::Charger(e.into()))?; + } + } + + Ok(()) + } + + /// Connect to a new consumer + async fn connect_new_consumer(&mut self, new_consumer: AvailableConsumer<'device, Reg::Psu>) -> Result<(), Error> { + // Handle our current consumer + if let Some(current_consumer) = self.state.current_consumer_state { + if ptr::eq(current_consumer.psu, new_consumer.psu) + && new_consumer.consumer_power_capability == current_consumer.consumer_power_capability + { + // If the consumer is the same device, capability, and is still available, we don't need to do anything + info!("Best consumer is the same, not switching"); + return Ok(()); + } + + self.state.current_consumer_state = None; + let mut current_psu = current_consumer.psu.lock().await; + + if matches!(current_psu.state().psu_state, PsuState::ConnectedConsumer(_)) { + // Disconnect the current consumer if needed + info!("({}): Disconnecting current consumer", current_psu.name()); + current_psu.disconnect().await?; + } + + // If no chargers are registered, they won't receive the new power capability. + // Also, if chargers return UnpoweredAck, that means the charger isn't powered. + // Further down this fn the power rails are enabled and thus the charger will get power, + // so just continue execution. + self.disconnect_chargers().await?; + + self.broadcast_event(ServiceEvent::ConsumerDisconnected(current_consumer.psu)) + .await; + + // Don't update the unconstrained here because this is a transitional state + } + + let mut psu = new_consumer.psu.lock().await; + info!("({}): Connecting new consumer", psu.name()); + + if let e @ Err(_) = psu.state().can_connect_consumer() { + error!( + "({}): Not ready to connect consumer, state: {:#?}", + psu.name(), + psu.state().psu_state + ); + e + } else { + psu.connect_consumer(new_consumer.consumer_power_capability).await?; + self.post_consumer_connected(new_consumer).await + } + } + + /// Determines and connects the best external power + pub(super) async fn update_current_consumer(&mut self) -> Result<(), Error> { + let current_consumer_name = if let Some(current_consumer) = self.state.current_consumer_state { + current_consumer.psu.lock().await.name() + } else { + "None" + }; + info!("Selecting power port, current power: {:#?}", current_consumer_name); + + let best_consumer = self.find_best_consumer().await?; + let best_consumer_name = if let Some(best_consumer) = best_consumer { + best_consumer.psu.lock().await.name() + } else { + "None" + }; + info!("Best consumer: {:#?}", best_consumer_name); + if let Some(best_consumer) = best_consumer { + self.connect_new_consumer(best_consumer).await?; + } else { + // Notify disconnect if recently detached consumer was previously attached. + if let Some(current_consumer) = self.state.current_consumer_state { + self.disconnect_chargers().await?; + self.broadcast_event(ServiceEvent::ConsumerDisconnected(current_consumer.psu)) + .await; + } + // No new consumer available + self.state.current_consumer_state = None; + } + + self.update_unconstrained_state().await + } +} + +#[cfg(test)] +mod tests { + use super::*; + use power_policy_interface::capability::PowerCapability; + + const P0: PowerCapability = PowerCapability { + voltage_mv: 5000, + current_ma: 1000, + }; + const P1: PowerCapability = PowerCapability { + voltage_mv: 5000, + current_ma: 1500, + }; + + /// Tests the [`cmp_consumer_capability`] without any flags set. + #[test] + fn test_cmp_consumer_capability_no_flags() { + let p0 = P0.into(); + let p1 = P1.into(); + + assert_eq!(cmp_consumer_capability(&p0, false, &p1, false), Ordering::Less); + assert_eq!(cmp_consumer_capability(&p1, false, &p1, false), Ordering::Equal); + assert_eq!(cmp_consumer_capability(&p1, false, &p0, false), Ordering::Greater); + } +} diff --git a/power-policy-service/src/service/mod.rs b/power-policy-service/src/service/mod.rs new file mode 100644 index 000000000..00fb327fa --- /dev/null +++ b/power-policy-service/src/service/mod.rs @@ -0,0 +1,193 @@ +//! Power policy related data structures and messages +use core::ptr; + +pub mod config; +pub mod consumer; +pub mod provider; +pub mod registration; +pub mod task; + +use embedded_services::named::Named; +use embedded_services::{event::Sender, info, sync::Lockable, trace}; + +use power_policy_interface::charger::{Charger, PsuState}; +use power_policy_interface::{ + capability::{ConsumerPowerCapability, ProviderPowerCapability}, + charger::{Event as ChargerEvent, EventData as ChargerEventData}, + psu::{ + Error, Psu, + event::{Event as PsuEvent, EventData as PsuEventData}, + }, + service::{UnconstrainedState, event::Event as ServiceEvent}, +}; + +use crate::service::registration::Registration; + +const MAX_CONNECTED_PROVIDERS: usize = 4; + +#[derive(Clone)] +struct InternalState<'device, PSU: Lockable> +where + PSU::Inner: Psu, +{ + /// Current consumer state, if any + current_consumer_state: Option>, + /// Current provider global state + current_provider_state: provider::State, + /// System unconstrained power + unconstrained: UnconstrainedState, + /// Connected providers + connected_providers: heapless::index_set::FnvIndexSet, +} + +impl Default for InternalState<'_, PSU> +where + PSU::Inner: Psu, +{ + fn default() -> Self { + Self { + current_consumer_state: None, + current_provider_state: provider::State::default(), + unconstrained: UnconstrainedState::default(), + connected_providers: heapless::index_set::FnvIndexSet::new(), + } + } +} + +/// Power policy service +pub struct Service<'device, Reg: Registration<'device>> { + /// Service registration + registration: Reg, + /// State + state: InternalState<'device, Reg::Psu>, + /// Config + config: config::Config, +} + +impl<'device, Reg: Registration<'device>> Service<'device, Reg> { + /// Create a new power policy + pub fn new(registration: Reg, config: config::Config) -> Self { + Self { + registration, + state: InternalState::default(), + config, + } + } + + /// Returns the total amount of power that is being supplied to external devices + pub async fn compute_total_provider_power_mw(&self) -> u32 { + let mut total = 0; + + for psu in self.registration.psus() { + let psu = psu.lock().await; + total += psu + .state() + .connected_provider_capability() + .map(|cap| cap.capability.max_power_mw()) + .unwrap_or(0); + } + + total + } + + async fn process_notify_attach(&self, device: &'device Reg::Psu) { + info!("({}): Received notify attached", device.lock().await.name()); + } + + async fn process_notify_detach(&mut self, device: &'device Reg::Psu) -> Result<(), Error> { + info!("({}): Received notify detached", device.lock().await.name()); + self.post_provider_removed(device).await; + self.update_current_consumer().await?; + Ok(()) + } + + async fn process_notify_consumer_power_capability( + &mut self, + device: &'device Reg::Psu, + capability: Option, + ) -> Result<(), Error> { + info!( + "({}): Received notify consumer capability: {:#?}", + device.lock().await.name(), + capability, + ); + + self.update_current_consumer().await + } + + async fn process_request_provider_power_capabilities( + &mut self, + requester: &'device Reg::Psu, + capability: Option, + ) -> Result<(), Error> { + info!( + "({}): Received request provider capability: {:#?}", + requester.lock().await.name(), + capability, + ); + + self.connect_provider(requester).await + } + + async fn process_notify_disconnect(&mut self, device: &'device Reg::Psu) -> Result<(), Error> { + info!("({}): Received notify disconnect", device.lock().await.name()); + self.post_provider_removed(device).await; + self.update_current_consumer().await?; + Ok(()) + } + + /// Send an event to all registered listeners + async fn broadcast_event(&mut self, event: ServiceEvent<'device, Reg::Psu>) { + for sender in self.registration.event_senders() { + sender.send(event).await; + } + } + + pub async fn process_psu_event(&mut self, event: PsuEvent<'device, Reg::Psu>) -> Result<(), Error> { + let device = event.psu; + match event.event { + PsuEventData::Attached => { + self.process_notify_attach(device).await; + Ok(()) + } + PsuEventData::Detached => self.process_notify_detach(device).await, + PsuEventData::UpdatedConsumerCapability(capability) => { + self.process_notify_consumer_power_capability(device, capability).await + } + PsuEventData::RequestedProviderCapability(capability) => { + self.process_request_provider_power_capabilities(device, capability) + .await + } + PsuEventData::Disconnected => self.process_notify_disconnect(device).await, + } + } + + async fn process_psu_state_change( + &mut self, + charger: &'device Reg::Charger, + psu_state: PsuState, + ) -> Result<(), Error> { + // Currently a no-op, but functionality might be added in the future. + let locked_charger = charger.lock().await; + trace!( + "Charger PSU state change to {:?} event recvd in charger state {:?}", + psu_state, + locked_charger.state() + ); + Ok(()) + } + + pub async fn process_charger_event(&mut self, event: ChargerEvent<'device, Reg::Charger>) -> Result<(), Error> { + let charger = event.charger; + + match event.event { + ChargerEventData::PsuStateChange(psu_state) => self.process_psu_state_change(charger, psu_state).await?, + _ => { + return Err(Error::Charger( + power_policy_interface::charger::ChargerError::UnknownEvent, + )); + } + }; + Ok(()) + } +} diff --git a/power-policy-service/src/service/provider.rs b/power-policy-service/src/service/provider.rs new file mode 100644 index 000000000..e41c15d6a --- /dev/null +++ b/power-policy-service/src/service/provider.rs @@ -0,0 +1,146 @@ +//! This file implements logic to determine how much power to provide to each connected device. +//! When total provided power is below [limited_power_threshold_mw](super::config::Config::limited_power_threshold_mw) +//! the system is in unlimited power state. In this mode up to [provider_unlimited](super::config::Config::provider_unlimited) +//! is provided to each device. Above this threshold, the system is in limited power state. +//! In this mode [provider_limited](super::config::Config::provider_limited) is provided to each device +use core::ptr; + +use embedded_services::debug; +use embedded_services::error; +use embedded_services::named::Named; + +use super::*; + +/// Current system provider power state +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum PowerState { + /// System is capable of providing high power + #[default] + Unlimited, + /// System can only provide limited power + Limited, +} + +/// Power policy provider global state +#[derive(Clone, Copy, Default)] +pub(super) struct State { + /// Current power state + state: PowerState, +} + +impl<'device, Reg: Registration<'device>> Service<'device, Reg> { + /// Attempt to connect the requester as a provider + pub(super) async fn connect_provider(&mut self, requester: &'device Reg::Psu) -> Result<(), Error> { + let requested_power_capability = { + let requester = requester.lock().await; + debug!("({}): Attempting to connect as provider", requester.name()); + match requester.state().requested_provider_capability { + Some(cap) => cap, + // Requester is no longer requesting power + _ => { + info!("({}): No-longer requesting power", requester.name()); + return Ok(()); + } + } + }; + + // Determine total requested power draw + let mut total_power_mw = 0; + for psu in self.registration.psus() { + let target_provider_cap = if ptr::eq(*psu, requester) { + // Use the requester's requested power capability + // this handles both new connections and upgrade requests + Some(requested_power_capability) + } else { + // Use the device's current working provider capability + psu.lock().await.state().connected_provider_capability() + }; + total_power_mw += target_provider_cap.map_or(0, |cap| cap.capability.max_power_mw()); + } + + if total_power_mw > self.config.limited_power_threshold_mw { + self.state.current_provider_state.state = PowerState::Limited; + } else { + self.state.current_provider_state.state = PowerState::Unlimited; + } + + debug!("New power state: {:?}", self.state.current_provider_state.state); + + let target_power = match self.state.current_provider_state.state { + PowerState::Limited => ProviderPowerCapability { + capability: self.config.provider_limited, + flags: requested_power_capability.flags, + }, + PowerState::Unlimited => { + if requested_power_capability.capability.max_power_mw() < self.config.provider_unlimited.max_power_mw() + { + // Don't auto upgrade to a higher contract + requested_power_capability + } else { + ProviderPowerCapability { + capability: self.config.provider_unlimited, + flags: requested_power_capability.flags, + } + } + } + }; + + let mut locked_requester = requester.lock().await; + if let e @ Err(_) = locked_requester.state().can_connect_provider() { + error!( + "({}): Cannot provide, device is in state {:#?}", + locked_requester.name(), + locked_requester.state().psu_state + ); + e + } else { + locked_requester.connect_provider(target_power).await?; + self.post_provider_connected(requester, target_power).await; + Ok(()) + } + } + + /// Common logic for after a provider has successfully connected + async fn post_provider_connected(&mut self, requester: &'device Reg::Psu, target_power: ProviderPowerCapability) { + if self + .state + .connected_providers + .insert(requester as *const Reg::Psu as usize) + .is_err() + { + error!("Tracked providers set is full"); + } + self.broadcast_event(ServiceEvent::ProviderConnected(requester, target_power)) + .await; + } + + /// Common logic for when a provider is removed + /// + /// Returns true if the device was operating as a provider + pub(super) async fn post_provider_removed(&mut self, psu: &'device Reg::Psu) -> bool { + if self + .state + .connected_providers + .remove(&(psu as *const Reg::Psu as usize)) + { + // Determine total requested power draw + let mut total_power_mw = 0; + for psu in self.registration.psus() { + let target_provider_cap = psu.lock().await.state().connected_provider_capability(); + total_power_mw += target_provider_cap.map_or(0, |cap| cap.capability.max_power_mw()); + } + + if total_power_mw > self.config.limited_power_threshold_mw { + self.state.current_provider_state.state = PowerState::Limited; + } else { + self.state.current_provider_state.state = PowerState::Unlimited; + } + + self.broadcast_event(ServiceEvent::ProviderDisconnected(psu)).await; + true + } else { + false + } + } +} diff --git a/power-policy-service/src/service/registration.rs b/power-policy-service/src/service/registration.rs new file mode 100644 index 000000000..722c832c7 --- /dev/null +++ b/power-policy-service/src/service/registration.rs @@ -0,0 +1,64 @@ +//! Code related to registration with the power policy service. + +use embedded_services::{event::Sender, sync::Lockable}; +use power_policy_interface::{charger, psu, service::event::Event as ServiceEvent}; + +/// Registration trait that abstracts over various registration details. +pub trait Registration<'device> { + type Psu: Lockable + 'device; + type ServiceSender: Sender>; + type Charger: Lockable + 'device; + + /// Returns a slice to access PSU devices + fn psus(&self) -> &[&'device Self::Psu]; + /// Returns a slice to access power policy event senders + fn event_senders(&mut self) -> &mut [Self::ServiceSender]; + /// Returns a slice to access charger devices + fn chargers(&self) -> &[&'device Self::Charger]; +} + +/// A registration implementation based around arrays +pub struct ArrayRegistration< + 'device, + Psu: Lockable + 'device, + const PSU_COUNT: usize, + ServiceSender: Sender>, + const SERVICE_SENDER_COUNT: usize, + Charger: Lockable + 'device, + const CHARGER_COUNT: usize, +> { + /// Array of registered PSUs + pub psus: [&'device Psu; PSU_COUNT], + /// Array of registered chargers + pub chargers: [&'device Charger; CHARGER_COUNT], + /// Array of power policy service event senders + pub service_senders: [ServiceSender; SERVICE_SENDER_COUNT], +} + +impl< + 'device, + Psu: Lockable + 'device, + const PSU_COUNT: usize, + ServiceSender: Sender>, + const SERVICE_SENDER_COUNT: usize, + Charger: Lockable + 'device, + const CHARGER_COUNT: usize, +> Registration<'device> + for ArrayRegistration<'device, Psu, PSU_COUNT, ServiceSender, SERVICE_SENDER_COUNT, Charger, CHARGER_COUNT> +{ + type Psu = Psu; + type ServiceSender = ServiceSender; + type Charger = Charger; + + fn psus(&self) -> &[&'device Self::Psu] { + &self.psus + } + + fn event_senders(&mut self) -> &mut [Self::ServiceSender] { + &mut self.service_senders + } + + fn chargers(&self) -> &[&'device Self::Charger] { + &self.chargers + } +} diff --git a/power-policy-service/src/service/task.rs b/power-policy-service/src/service/task.rs new file mode 100644 index 000000000..a3de389e6 --- /dev/null +++ b/power-policy-service/src/service/task.rs @@ -0,0 +1,82 @@ +use embedded_services::{error, info, sync::Lockable}; + +use embedded_services::event::Receiver; +use power_policy_interface::charger; +use power_policy_interface::psu::event::EventData; + +use crate::service::registration::Registration; + +use super::Service; + +/// Runs the power policy PSU task. +pub async fn psu_task< + 'device, + const PSU_COUNT: usize, + S: Lockable>, + Reg: Registration<'device>, + PsuReceiver: Receiver, +>( + mut psu_events: crate::psu::PsuEventReceivers<'device, PSU_COUNT, Reg::Psu, PsuReceiver>, + policy: &'device S, +) -> ! { + info!("Starting power policy PSU task"); + loop { + let event = psu_events.wait_event().await; + + if let Err(e) = policy.lock().await.process_psu_event(event).await { + error!("Error processing request: {:?}", e); + } + } +} + +/// Runs the power policy charger task. +pub async fn charger_task< + 'device, + const CHARGER_COUNT: usize, + S: Lockable>, + Reg: Registration<'device>, + ChargerReceiver: Receiver, +>( + mut charger_events: crate::charger::ChargerEventReceivers<'device, CHARGER_COUNT, Reg::Charger, ChargerReceiver>, + policy: &'device S, +) -> ! { + info!("Starting power policy charger task"); + loop { + let event = charger_events.wait_event().await; + + if let Err(e) = policy.lock().await.process_charger_event(event).await { + error!("Error processing request: {:?}", e); + } + } +} + +/// Runs the power policy unified task. +pub async fn task< + 'device, + const PSU_COUNT: usize, + const CHARGER_COUNT: usize, + S: Lockable>, + Reg: Registration<'device>, + PsuReceiver: Receiver, + ChargerReceiver: Receiver, +>( + mut psu_events: crate::psu::PsuEventReceivers<'device, PSU_COUNT, Reg::Psu, PsuReceiver>, + mut charger_events: crate::charger::ChargerEventReceivers<'device, CHARGER_COUNT, Reg::Charger, ChargerReceiver>, + policy: &'device S, +) -> ! { + info!("Starting power policy task"); + loop { + match embassy_futures::select::select(psu_events.wait_event(), charger_events.wait_event()).await { + embassy_futures::select::Either::First(psu_event) => { + if let Err(e) = policy.lock().await.process_psu_event(psu_event).await { + error!("Error processing PSU request: {:?}", e); + } + } + embassy_futures::select::Either::Second(charger_event) => { + if let Err(e) = policy.lock().await.process_charger_event(charger_event).await { + error!("Error processing charger request: {:?}", e); + } + } + } + } +} diff --git a/power-policy-service/src/task.rs b/power-policy-service/src/task.rs deleted file mode 100644 index 916b2909f..000000000 --- a/power-policy-service/src/task.rs +++ /dev/null @@ -1,35 +0,0 @@ -use embassy_sync::once_lock::OnceLock; -use embedded_services::{comms, error, info}; - -use crate::{PowerPolicy, config}; - -#[derive(Debug, Clone, Copy)] -#[cfg_attr(feature = "defmt", derive(defmt::Format))] -pub enum InitError { - /// Power policy singleton has already been initialized - AlreadyInitialized, - /// Comms registration failed - RegistrationFailed, -} - -pub async fn task(config: config::Config) -> Result { - info!("Starting power policy task"); - static POLICY: OnceLock = OnceLock::new(); - let policy = if let Some(policy) = PowerPolicy::create(config) { - POLICY.get_or_init(|| policy) - } else { - error!("Power policy service already initialized"); - return Err(InitError::AlreadyInitialized); - }; - - if comms::register_endpoint(policy, &policy.tp).await.is_err() { - error!("Failed to register power policy endpoint"); - return Err(InitError::RegistrationFailed); - } - - loop { - if let Err(e) = policy.process().await { - error!("Error processing request: {:?}", e); - } - } -} diff --git a/power-policy-service/tests/common/mock.rs b/power-policy-service/tests/common/mock.rs new file mode 100644 index 000000000..0b97d0f0c --- /dev/null +++ b/power-policy-service/tests/common/mock.rs @@ -0,0 +1,202 @@ +#![allow(clippy::unwrap_used)] +#![allow(dead_code)] +use embassy_sync::{channel, mutex::Mutex, signal::Signal}; +use embedded_batteries_async::charger::{MilliAmps, MilliVolts}; +use embedded_services::{GlobalRawMutex, event::Sender, info, named::Named}; +use power_policy_interface::{ + capability::{ConsumerPowerCapability, PowerCapability, ProviderFlags, ProviderPowerCapability}, + charger, + psu::{Error, Psu, State, event::EventData}, +}; + +#[derive(Debug, Clone, PartialEq, Eq)] +#[allow(dead_code)] +pub enum FnCall { + ConnectConsumer(ConsumerPowerCapability), + ConnectProvider(ProviderPowerCapability), + Disconnect, + Reset, +} + +pub struct Mock<'a, S: Sender> { + sender: S, + fn_call: &'a Signal, + // Internal state + pub state: State, + name: &'static str, +} + +impl<'a, S: Sender> Mock<'a, S> { + pub fn new(name: &'static str, sender: S, fn_call: &'a Signal) -> Self { + Self { + name, + sender, + fn_call, + state: Default::default(), + } + } + + fn record_fn_call(&mut self, fn_call: FnCall) { + let num_fn_calls = self + .fn_call + .try_take() + .map(|(num_fn_calls, _)| num_fn_calls) + .unwrap_or(0); + self.fn_call.signal((num_fn_calls + 1, fn_call)); + } + + pub async fn simulate_consumer_connection(&mut self, capability: ConsumerPowerCapability) { + self.state.attach().unwrap(); + self.sender.send(EventData::Attached).await; + self.state.update_consumer_power_capability(Some(capability)).unwrap(); + self.sender + .send(EventData::UpdatedConsumerCapability(Some(capability))) + .await; + } + + pub async fn simulate_detach(&mut self) { + self.state.detach(); + self.sender.send(EventData::Detached).await; + } + + pub async fn simulate_provider_connection(&mut self, capability: PowerCapability) { + self.state.attach().unwrap(); + self.sender.send(EventData::Attached).await; + + let capability = Some(ProviderPowerCapability { + capability, + flags: ProviderFlags::none(), + }); + self.state + .update_requested_provider_power_capability(capability) + .unwrap(); + self.sender + .send(EventData::RequestedProviderCapability(capability)) + .await; + } + + pub async fn simulate_disconnect(&mut self) { + self.state.disconnect(true).unwrap(); + self.sender.send(EventData::Disconnected).await; + } + + pub async fn simulate_update_requested_provider_power_capability( + &mut self, + capability: Option, + ) { + self.state + .update_requested_provider_power_capability(capability) + .unwrap(); + self.sender + .send(power_policy_interface::psu::event::EventData::RequestedProviderCapability(capability)) + .await + } +} + +impl<'a, S: Sender> Psu for Mock<'a, S> { + async fn connect_consumer(&mut self, capability: ConsumerPowerCapability) -> Result<(), Error> { + info!("Connect consumer {:#?}", capability); + self.record_fn_call(FnCall::ConnectConsumer(capability)); + self.state.connect_consumer(capability) + } + + async fn connect_provider(&mut self, capability: ProviderPowerCapability) -> Result<(), Error> { + info!("Connect provider: {:#?}", capability); + self.record_fn_call(FnCall::ConnectProvider(capability)); + self.state.connect_provider(capability) + } + + async fn disconnect(&mut self) -> Result<(), Error> { + info!("Disconnect"); + self.record_fn_call(FnCall::Disconnect); + self.state.disconnect(false) + } + + fn state(&self) -> &State { + &self.state + } + + fn state_mut(&mut self) -> &mut State { + &mut self.state + } +} + +impl<'a, S: Sender> Named for Mock<'a, S> { + fn name(&self) -> &'static str { + self.name + } +} + +pub struct ExampleCharger<'a> { + sender: channel::DynamicSender<'a, power_policy_interface::charger::event::EventData>, + state: charger::State, +} + +impl<'a> ExampleCharger<'a> { + pub fn new(sender: channel::DynamicSender<'a, power_policy_interface::charger::event::EventData>) -> Self { + Self { + sender, + state: charger::State::default(), + } + } + + pub fn assert_state(&self, internal_state: charger::InternalState, capability: Option) { + assert_eq!(*self.state.internal_state(), internal_state); + assert_eq!(*self.state.capability(), capability); + } + + pub async fn simulate_psu_state_change(&self, psu_state: charger::PsuState) { + self.sender.send(charger::EventData::PsuStateChange(psu_state)).await; + } +} + +impl<'a> embedded_batteries_async::charger::ErrorType for ExampleCharger<'a> { + type Error = core::convert::Infallible; +} + +impl<'a> embedded_batteries_async::charger::Charger for ExampleCharger<'a> { + async fn charging_current(&mut self, current: MilliAmps) -> Result { + Ok(current) + } + + async fn charging_voltage(&mut self, voltage: MilliVolts) -> Result { + Ok(voltage) + } +} + +impl<'a> charger::Charger for ExampleCharger<'a> { + type ChargerError = core::convert::Infallible; + + async fn init_charger(&mut self) -> Result { + info!("Charger init"); + Ok(charger::PsuState::Detached) + } + + fn attach_handler( + &mut self, + capability: ConsumerPowerCapability, + ) -> impl Future> { + info!("Charger attach: {:?}", capability); + async { Ok(()) } + } + + fn detach_handler(&mut self) -> impl Future> { + info!("Charger detach"); + async { Ok(()) } + } + + async fn is_ready(&mut self) -> Result<(), Self::ChargerError> { + info!("Charger check ready"); + Ok(()) + } + + fn state(&self) -> &charger::State { + &self.state + } + + fn state_mut(&mut self) -> &mut charger::State { + &mut self.state + } +} + +pub type ChargerType<'a> = Mutex>; diff --git a/power-policy-service/tests/common/mod.rs b/power-policy-service/tests/common/mod.rs new file mode 100644 index 000000000..aab4d1a0c --- /dev/null +++ b/power-policy-service/tests/common/mod.rs @@ -0,0 +1,218 @@ +#![allow(clippy::unwrap_used)] +#![allow(dead_code)] +#![allow(clippy::panic)] +use std::mem::ManuallyDrop; + +use embassy_futures::{ + join::join, + select::{Either, select}, +}; +use embassy_sync::{ + channel::{Channel, DynamicReceiver, DynamicSender}, + mutex::Mutex, + once_lock::OnceLock, + signal::Signal, +}; +use embassy_time::{Duration, with_timeout}; +use embedded_services::GlobalRawMutex; +use power_policy_interface::psu::event::EventData; +use power_policy_interface::{ + capability::{ConsumerPowerCapability, PowerCapability, ProviderPowerCapability}, + service::{UnconstrainedState, event::Event as ServiceEvent}, +}; +use power_policy_service::service::{Service, config::Config}; +use power_policy_service::{psu::PsuEventReceivers, service::registration::ArrayRegistration}; + +pub mod mock; + +use mock::Mock; + +use crate::common::mock::{ChargerType, FnCall}; + +pub const MINIMAL_POWER: PowerCapability = PowerCapability { + voltage_mv: 5000, + current_ma: 500, +}; + +pub const LOW_POWER: PowerCapability = PowerCapability { + voltage_mv: 5000, + current_ma: 1500, +}; + +#[allow(dead_code)] +pub const HIGH_POWER: PowerCapability = PowerCapability { + voltage_mv: 5000, + current_ma: 3000, +}; + +pub const DEFAULT_TIMEOUT: Duration = Duration::from_secs(15); + +const EVENT_CHANNEL_SIZE: usize = 4; + +pub type DeviceType<'a> = Mutex>>; +pub type ServiceType<'device, 'sender> = Service< + 'device, + ArrayRegistration< + 'device, + DeviceType<'device>, + 2, + DynamicSender<'sender, ServiceEvent<'device, DeviceType<'device>>>, + 1, + ChargerType<'device>, + 0, + >, +>; + +pub type ServiceMutex<'device, 'sender> = Mutex>; + +async fn power_policy_task<'device, 'sender, const N: usize>( + completion_signal: &'device Signal, + power_policy: &ServiceMutex<'device, 'sender>, + mut event_receivers: PsuEventReceivers<'device, N, DeviceType<'device>, DynamicReceiver<'device, EventData>>, +) { + while let Either::First(result) = select(event_receivers.wait_event(), completion_signal.wait()).await { + power_policy.lock().await.process_psu_event(result).await.unwrap(); + } +} + +/// Trait for runnable tests. +/// +/// This exists because there are lifetime issues with being generic over FnOnce or FnMut. +/// Those can be resolved, but having a dedicated trait is simpler. +pub trait Test { + fn run<'a>( + &mut self, + service: &'a ServiceMutex<'a, 'a>, + service_receiver: DynamicReceiver<'a, ServiceEvent<'a, DeviceType<'a>>>, + device0: &'a DeviceType<'a>, + device0_signal: &'a Signal, + device1: &'a DeviceType<'a>, + device1_signal: &'a Signal, + ) -> impl Future; +} + +pub async fn run_test(timeout: Duration, mut test: impl Test, config: Config) { + // Tokio runs tests in parallel, but logging is global so we need to run tests sequentially to avoid interleaved logs. + static TEST_MUTEX: OnceLock> = OnceLock::new(); + let test_mutex = TEST_MUTEX.get_or_init(|| Mutex::new(())); + let _lock = test_mutex.lock().await; + + // Initialize logging, ignore the error if the logger was already initialized by another test. + let _ = env_logger::builder().filter_level(log::LevelFilter::Info).try_init(); + embedded_services::init().await; + + let device0_signal = Signal::new(); + let device0_event_channel: Channel = Channel::new(); + let device0_sender = device0_event_channel.dyn_sender(); + let device0_receiver = device0_event_channel.dyn_receiver(); + let device0 = Mutex::new(Mock::new("PSU0", device0_sender, &device0_signal)); + + let device1_signal = Signal::new(); + let device1_event_channel: Channel = Channel::new(); + let device1_sender = device1_event_channel.dyn_sender(); + let device1_receiver = device1_event_channel.dyn_receiver(); + let device1 = Mutex::new(Mock::new("PSU1", device1_sender, &device1_signal)); + + let completion_signal = Signal::new(); + + // For simplicity, Test::run is only generic over a single lifetime. But this causes issues with the drop checker because + // the device lifetime doesn't outlive the channel lifetime from its perspective. Use ManuallyDrop to work around this. + let service_event_channel: ManuallyDrop< + Channel>, EVENT_CHANNEL_SIZE>, + > = ManuallyDrop::new(Channel::new()); + let service_receiver = service_event_channel.dyn_receiver(); + + let power_policy_registration = ArrayRegistration { + psus: [&device0, &device1], + service_senders: [service_event_channel.dyn_sender()], + chargers: [], + }; + + let power_policy = Mutex::new(power_policy_service::service::Service::new( + power_policy_registration, + config, + )); + + with_timeout( + timeout, + join( + power_policy_task( + &completion_signal, + &power_policy, + PsuEventReceivers::new([&device0, &device1], [device0_receiver, device1_receiver]), + ), + async { + test.run( + &power_policy, + service_receiver, + &device0, + &device0_signal, + &device1, + &device1_signal, + ) + .await; + completion_signal.signal(()); + }, + ), + ) + .await + .unwrap(); +} + +pub async fn assert_consumer_disconnected<'a>( + receiver: DynamicReceiver<'a, ServiceEvent<'a, DeviceType<'a>>>, + expected_device: &DeviceType<'a>, +) { + let ServiceEvent::ConsumerDisconnected(device) = receiver.receive().await else { + panic!("Expected ConsumerDisconnected event"); + }; + assert_eq!(device as *const _, expected_device as *const _); +} + +pub async fn assert_consumer_connected<'a>( + receiver: DynamicReceiver<'a, ServiceEvent<'a, DeviceType<'a>>>, + expected_device: &DeviceType<'a>, + expected_capability: ConsumerPowerCapability, +) { + let ServiceEvent::ConsumerConnected(device, capability) = receiver.receive().await else { + panic!("Expected ConsumerConnected event"); + }; + assert_eq!(device as *const _, expected_device as *const _); + assert_eq!(capability, expected_capability); +} + +pub async fn assert_provider_disconnected<'a>( + receiver: DynamicReceiver<'a, ServiceEvent<'a, DeviceType<'a>>>, + expected_device: &DeviceType<'a>, +) { + let ServiceEvent::ProviderDisconnected(device) = receiver.receive().await else { + panic!("Expected ProviderDisconnected event"); + }; + assert_eq!(device as *const _, expected_device as *const _); +} + +pub async fn assert_provider_connected<'a>( + receiver: DynamicReceiver<'a, ServiceEvent<'a, DeviceType<'a>>>, + expected_device: &DeviceType<'a>, + expected_capability: ProviderPowerCapability, +) { + let ServiceEvent::ProviderConnected(device, capability) = receiver.receive().await else { + panic!("Expected ProviderConnected event"); + }; + assert_eq!(device as *const _, expected_device as *const _); + assert_eq!(capability, expected_capability); +} + +pub async fn assert_unconstrained<'a>( + receiver: DynamicReceiver<'a, ServiceEvent<'a, DeviceType<'a>>>, + expected_state: UnconstrainedState, +) { + let ServiceEvent::Unconstrained(state) = receiver.receive().await else { + panic!("Expected Unconstrained event"); + }; + assert_eq!(state, expected_state); +} + +pub fn assert_no_event<'a>(receiver: DynamicReceiver<'a, ServiceEvent<'a, DeviceType<'a>>>) { + assert!(receiver.try_receive().is_err()); +} diff --git a/power-policy-service/tests/consumer.rs b/power-policy-service/tests/consumer.rs new file mode 100644 index 000000000..4626349ff --- /dev/null +++ b/power-policy-service/tests/consumer.rs @@ -0,0 +1,704 @@ +#![allow(clippy::unwrap_used)] +use embassy_sync::channel::DynamicReceiver; +use embassy_sync::signal::Signal; +use embassy_time::{Duration, TimeoutError, with_timeout}; +use embedded_services::GlobalRawMutex; +use embedded_services::info; +use power_policy_interface::capability::ProviderFlags; +use power_policy_interface::capability::ProviderPowerCapability; +use power_policy_interface::capability::{ConsumerFlags, ConsumerPowerCapability}; + +mod common; + +use common::{LOW_POWER, ServiceMutex}; +use power_policy_interface::service::event::Event as ServiceEvent; +use power_policy_service::service::config::Config; + +use crate::common::DeviceType; +use crate::common::MINIMAL_POWER; +use crate::common::Test; +use crate::common::assert_no_event; +use crate::common::assert_provider_connected; +use crate::common::assert_provider_disconnected; +use crate::common::{ + DEFAULT_TIMEOUT, HIGH_POWER, assert_consumer_connected, assert_consumer_disconnected, mock::FnCall, run_test, +}; + +const PER_CALL_TIMEOUT: Duration = Duration::from_millis(1000); + +const MIN_CONSUMER_THRESHOLD_MW: u32 = 7500; + +/// Test the basic consumer flow with a single device. +struct TestSingle; + +impl Test for TestSingle { + async fn run<'a>( + &mut self, + service: &ServiceMutex<'a, 'a>, + service_receiver: DynamicReceiver<'a, ServiceEvent<'a, DeviceType<'a>>>, + device0: &DeviceType<'a>, + device0_signal: &Signal, + _device1: &DeviceType<'a>, + _device1_signal: &Signal, + ) { + info!("Running test_single"); + // Test initial connection + { + device0 + .lock() + .await + .simulate_consumer_connection(LOW_POWER.into()) + .await; + + assert_eq!( + with_timeout(PER_CALL_TIMEOUT, device0_signal.wait()).await.unwrap(), + ( + 1, + FnCall::ConnectConsumer(ConsumerPowerCapability { + capability: LOW_POWER, + flags: ConsumerFlags::none(), + }) + ) + ); + device0_signal.reset(); + + assert_consumer_connected( + service_receiver, + device0, + ConsumerPowerCapability { + capability: LOW_POWER, + flags: ConsumerFlags::none(), + }, + ) + .await; + + // Ensure consumer change doesn't affect provider power computation + assert_eq!(service.lock().await.compute_total_provider_power_mw().await, 0); + } + // Test detach + { + device0.lock().await.simulate_detach().await; + + // Power policy shouldn't call any functions on detach so we'll timeout + assert_eq!( + with_timeout(PER_CALL_TIMEOUT, device0_signal.wait()).await, + Err(TimeoutError) + ); + device0_signal.reset(); + + assert_consumer_disconnected(service_receiver, device0).await; + // Ensure consumer change doesn't affect provider power computation + assert_eq!(service.lock().await.compute_total_provider_power_mw().await, 0); + } + + assert_no_event(service_receiver); + } +} + +/// Test swapping to a higher powered device. +struct TestSwapHigher; + +impl Test for TestSwapHigher { + async fn run<'a>( + &mut self, + service: &ServiceMutex<'a, 'a>, + service_receiver: DynamicReceiver<'a, ServiceEvent<'a, DeviceType<'a>>>, + device0: &DeviceType<'a>, + device0_signal: &Signal, + device1: &DeviceType<'a>, + device1_signal: &Signal, + ) { + info!("Running test_swap_higher"); + // Device0 connection at low power + { + device0 + .lock() + .await + .simulate_consumer_connection(LOW_POWER.into()) + .await; + + assert_eq!( + with_timeout(PER_CALL_TIMEOUT, device0_signal.wait()).await.unwrap(), + ( + 1, + FnCall::ConnectConsumer(ConsumerPowerCapability { + capability: LOW_POWER, + flags: ConsumerFlags::none(), + }) + ) + ); + device0_signal.reset(); + + assert_consumer_connected( + service_receiver, + device0, + ConsumerPowerCapability { + capability: LOW_POWER, + flags: ConsumerFlags::none(), + }, + ) + .await; + + // Ensure consumer change doesn't affect provider power computation + assert_eq!(service.lock().await.compute_total_provider_power_mw().await, 0); + } + // Device1 connection at high power + { + device1 + .lock() + .await + .simulate_consumer_connection(HIGH_POWER.into()) + .await; + + assert_eq!( + with_timeout(PER_CALL_TIMEOUT, device0_signal.wait()).await.unwrap(), + (1, FnCall::Disconnect) + ); + device0_signal.reset(); + + assert_eq!( + with_timeout(PER_CALL_TIMEOUT, device1_signal.wait()).await.unwrap(), + ( + 1, + FnCall::ConnectConsumer(ConsumerPowerCapability { + capability: HIGH_POWER, + flags: ConsumerFlags::none(), + }) + ) + ); + device1_signal.reset(); + + // Should receive a disconnect event from device0 first + assert_consumer_disconnected(service_receiver, device0).await; + + assert_consumer_connected( + service_receiver, + device1, + ConsumerPowerCapability { + capability: HIGH_POWER, + flags: ConsumerFlags::none(), + }, + ) + .await; + + // Ensure consumer change doesn't affect provider power computation + assert_eq!(service.lock().await.compute_total_provider_power_mw().await, 0); + } + // Test detach device1, should reconnect device0 + { + device1.lock().await.simulate_detach().await; + + // Power policy shouldn't call any functions on detach so we'll timeout + assert_eq!( + with_timeout(PER_CALL_TIMEOUT, device1_signal.wait()).await, + Err(TimeoutError) + ); + + assert_eq!( + with_timeout(PER_CALL_TIMEOUT, device0_signal.wait()).await.unwrap(), + ( + 1, + FnCall::ConnectConsumer(ConsumerPowerCapability { + capability: LOW_POWER, + flags: ConsumerFlags::none(), + }) + ) + ); + device0_signal.reset(); + + // Should receive a disconnect event from device1 first + assert_consumer_disconnected(service_receiver, device1).await; + + assert_consumer_connected( + service_receiver, + device0, + ConsumerPowerCapability { + capability: LOW_POWER, + flags: ConsumerFlags::none(), + }, + ) + .await; + + // Ensure consumer change doesn't affect provider power computation + assert_eq!(service.lock().await.compute_total_provider_power_mw().await, 0); + } + + assert_no_event(service_receiver); + } +} + +/// Test a disconnect initiated by the current consumer. +struct TestDisconnect; + +impl Test for TestDisconnect { + async fn run<'a>( + &mut self, + service: &ServiceMutex<'a, 'a>, + service_receiver: DynamicReceiver<'a, ServiceEvent<'a, DeviceType<'a>>>, + device0: &DeviceType<'a>, + device0_signal: &Signal, + device1: &DeviceType<'a>, + device1_signal: &Signal, + ) { + info!("Running test_disconnect"); + // Device0 connection at low power + { + device0 + .lock() + .await + .simulate_consumer_connection(LOW_POWER.into()) + .await; + + assert_eq!( + with_timeout(PER_CALL_TIMEOUT, device0_signal.wait()).await.unwrap(), + ( + 1, + FnCall::ConnectConsumer(ConsumerPowerCapability { + capability: LOW_POWER, + flags: ConsumerFlags::none(), + }) + ) + ); + device0_signal.reset(); + + assert_consumer_connected( + service_receiver, + device0, + ConsumerPowerCapability { + capability: LOW_POWER, + flags: ConsumerFlags::none(), + }, + ) + .await; + + // Ensure consumer change doesn't affect provider power computation + assert_eq!(service.lock().await.compute_total_provider_power_mw().await, 0); + } + // Device1 connection at high power + { + device1 + .lock() + .await + .simulate_consumer_connection(HIGH_POWER.into()) + .await; + + assert_eq!( + with_timeout(PER_CALL_TIMEOUT, device0_signal.wait()).await.unwrap(), + (1, FnCall::Disconnect) + ); + device0_signal.reset(); + + assert_eq!( + with_timeout(PER_CALL_TIMEOUT, device1_signal.wait()).await.unwrap(), + ( + 1, + FnCall::ConnectConsumer(ConsumerPowerCapability { + capability: HIGH_POWER, + flags: ConsumerFlags::none(), + }) + ) + ); + device1_signal.reset(); + + // Should receive a disconnect event from device0 first + assert_consumer_disconnected(service_receiver, device0).await; + + assert_consumer_connected( + service_receiver, + device1, + ConsumerPowerCapability { + capability: HIGH_POWER, + flags: ConsumerFlags::none(), + }, + ) + .await; + + // Ensure consumer change doesn't affect provider power computation + assert_eq!(service.lock().await.compute_total_provider_power_mw().await, 0); + } + + // Test disconnect device1, should reconnect device0 + { + device1.lock().await.simulate_disconnect().await; + + // Power policy shouldn't call any functions on disconnect so we'll timeout + assert_eq!( + with_timeout(PER_CALL_TIMEOUT, device1_signal.wait()).await, + Err(TimeoutError) + ); + + assert_eq!( + with_timeout(PER_CALL_TIMEOUT, device0_signal.wait()).await.unwrap(), + ( + 1, + FnCall::ConnectConsumer(ConsumerPowerCapability { + capability: LOW_POWER, + flags: ConsumerFlags::none(), + }) + ) + ); + device0_signal.reset(); + + // Consume the disconnect event generated by `simulate_disconnect` + assert_consumer_disconnected(service_receiver, device1).await; + + assert_consumer_connected( + service_receiver, + device0, + ConsumerPowerCapability { + capability: LOW_POWER, + flags: ConsumerFlags::none(), + }, + ) + .await; + + // Ensure consumer change doesn't affect provider power computation + assert_eq!(service.lock().await.compute_total_provider_power_mw().await, 0); + } + + assert_no_event(service_receiver); + } +} + +/// Test a disconnect initiated by a consumer other than the current consumer. +struct TestDisconnectOtherConsumer; + +impl Test for TestDisconnectOtherConsumer { + async fn run<'a>( + &mut self, + service: &ServiceMutex<'a, 'a>, + service_receiver: DynamicReceiver<'a, ServiceEvent<'a, DeviceType<'a>>>, + device0: &DeviceType<'a>, + device0_signal: &Signal, + device1: &DeviceType<'a>, + device1_signal: &Signal, + ) { + info!("Running test_disconnect_other_consumer"); + // Device0 connection at high power + { + device0 + .lock() + .await + .simulate_consumer_connection(HIGH_POWER.into()) + .await; + + assert_eq!( + with_timeout(PER_CALL_TIMEOUT, device0_signal.wait()).await.unwrap(), + ( + 1, + FnCall::ConnectConsumer(ConsumerPowerCapability { + capability: HIGH_POWER, + flags: ConsumerFlags::none(), + }) + ) + ); + device0_signal.reset(); + + assert_consumer_connected( + service_receiver, + device0, + ConsumerPowerCapability { + capability: HIGH_POWER, + flags: ConsumerFlags::none(), + }, + ) + .await; + + // Ensure consumer change doesn't affect provider power computation + assert_eq!(service.lock().await.compute_total_provider_power_mw().await, 0); + } + // Device1 connection at low power + { + device1 + .lock() + .await + .simulate_consumer_connection(LOW_POWER.into()) + .await; + + assert_eq!( + with_timeout(PER_CALL_TIMEOUT, device1_signal.wait()).await, + Err(TimeoutError) + ); + device1_signal.reset(); + + // Ensure consumer change doesn't affect provider power computation + assert_eq!(service.lock().await.compute_total_provider_power_mw().await, 0); + } + + // Test disconnect device1, should have no effect on device0 + { + device1.lock().await.simulate_disconnect().await; + + // Power policy shouldn't call any functions on disconnect so we'll timeout + assert_eq!( + with_timeout(PER_CALL_TIMEOUT, device0_signal.wait()).await, + Err(TimeoutError) + ); + assert_eq!( + with_timeout(PER_CALL_TIMEOUT, device1_signal.wait()).await, + Err(TimeoutError) + ); + + // Ensure consumer change doesn't affect provider power computation + assert_eq!(service.lock().await.compute_total_provider_power_mw().await, 0); + } + + assert_no_event(service_receiver); + } +} + +/// Test a disconnect initiated by a provider other than the current consumer. +struct TestDisconnectOtherProvider; + +impl Test for TestDisconnectOtherProvider { + async fn run<'a>( + &mut self, + service: &ServiceMutex<'a, 'a>, + service_receiver: DynamicReceiver<'a, ServiceEvent<'a, DeviceType<'a>>>, + device0: &DeviceType<'a>, + device0_signal: &Signal, + device1: &DeviceType<'a>, + device1_signal: &Signal, + ) { + info!("Running test_disconnect_other_provider"); + // Device0 connection at high power + { + device0 + .lock() + .await + .simulate_consumer_connection(HIGH_POWER.into()) + .await; + + assert_eq!( + with_timeout(PER_CALL_TIMEOUT, device0_signal.wait()).await.unwrap(), + ( + 1, + FnCall::ConnectConsumer(ConsumerPowerCapability { + capability: HIGH_POWER, + flags: ConsumerFlags::none(), + }) + ) + ); + device0_signal.reset(); + + assert_consumer_connected( + service_receiver, + device0, + ConsumerPowerCapability { + capability: HIGH_POWER, + flags: ConsumerFlags::none(), + }, + ) + .await; + + // Ensure consumer change doesn't affect provider power computation + assert_eq!(service.lock().await.compute_total_provider_power_mw().await, 0); + } + // Device1 connect as provider + { + device1.lock().await.simulate_provider_connection(LOW_POWER).await; + + assert_eq!( + with_timeout(PER_CALL_TIMEOUT, device1_signal.wait()).await.unwrap(), + ( + 1, + FnCall::ConnectProvider(ProviderPowerCapability { + capability: LOW_POWER, + flags: ProviderFlags::none(), + }) + ) + ); + device1_signal.reset(); + + assert_provider_connected( + service_receiver, + device1, + ProviderPowerCapability { + capability: LOW_POWER, + flags: ProviderFlags::none(), + }, + ) + .await; + assert_eq!(service.lock().await.compute_total_provider_power_mw().await, 7500); + } + + // Test disconnect device1, should have no effect on device0 + { + device1.lock().await.simulate_disconnect().await; + + // Power policy shouldn't call any functions on disconnect so we'll timeout + assert_eq!( + with_timeout(PER_CALL_TIMEOUT, device0_signal.wait()).await, + Err(TimeoutError) + ); + assert_eq!( + with_timeout(PER_CALL_TIMEOUT, device1_signal.wait()).await, + Err(TimeoutError) + ); + + assert_provider_disconnected(service_receiver, device1).await; + assert_eq!(service.lock().await.compute_total_provider_power_mw().await, 0); + } + + assert_no_event(service_receiver); + } +} + +/// Test minimum consumer power logic. +/// +/// Config for this test uses [`MIN_CONSUMER_THRESHOLD_MW`]. +struct TestMinConsumerPower; + +impl Test for TestMinConsumerPower { + async fn run<'a>( + &mut self, + service: &ServiceMutex<'a, 'a>, + service_receiver: DynamicReceiver<'a, ServiceEvent<'a, DeviceType<'a>>>, + device0: &DeviceType<'a>, + device0_signal: &Signal, + _device1: &DeviceType<'a>, + _device1_signal: &Signal, + ) { + info!("Running test_min_consumer_power"); + // Connect with power below the minimum threshold. + { + device0 + .lock() + .await + .simulate_consumer_connection(MINIMAL_POWER.into()) + .await; + + // Power policy shouldn't connect, so this call should timeout. + assert_eq!( + with_timeout(PER_CALL_TIMEOUT, device0_signal.wait()).await, + Err(TimeoutError) + ); + device0_signal.reset(); + + // Ensure consumer change doesn't affect provider power computation + assert_eq!(service.lock().await.compute_total_provider_power_mw().await, 0); + } + + // Service shouldn't broadcast any events in this case. + assert_no_event(service_receiver); + } +} + +/// Test that we won't swap if the capabilities are the same +struct TestNoSwap; + +impl Test for TestNoSwap { + async fn run<'a>( + &mut self, + service: &ServiceMutex<'a, 'a>, + service_receiver: DynamicReceiver<'a, ServiceEvent<'a, DeviceType<'a>>>, + device0: &DeviceType<'a>, + device0_signal: &Signal, + device1: &DeviceType<'a>, + device1_signal: &Signal, + ) { + info!("Running test_no_swap"); + // Device0 connection at low power + { + device0 + .lock() + .await + .simulate_consumer_connection(LOW_POWER.into()) + .await; + + assert_eq!( + with_timeout(PER_CALL_TIMEOUT, device0_signal.wait()).await.unwrap(), + ( + 1, + FnCall::ConnectConsumer(ConsumerPowerCapability { + capability: LOW_POWER, + flags: ConsumerFlags::none(), + }) + ) + ); + device0_signal.reset(); + + assert_consumer_connected( + service_receiver, + device0, + ConsumerPowerCapability { + capability: LOW_POWER, + flags: ConsumerFlags::none(), + }, + ) + .await; + + // Ensure consumer change doesn't affect provider power computation + assert_eq!(service.lock().await.compute_total_provider_power_mw().await, 0); + } + // Device1 connection at low power, should not cause a swap since capabilities are the same + { + device1 + .lock() + .await + .simulate_consumer_connection(LOW_POWER.into()) + .await; + + // These should timeout since we shouldn't swap + assert_eq!( + with_timeout(PER_CALL_TIMEOUT, device0_signal.wait()).await, + Err(TimeoutError) + ); + device0_signal.reset(); + + assert_eq!( + with_timeout(PER_CALL_TIMEOUT, device1_signal.wait()).await, + Err(TimeoutError) + ); + device1_signal.reset(); + + // Shouldn't affect provider power computation + assert_eq!(service.lock().await.compute_total_provider_power_mw().await, 0); + } + + // Service shouldn't broadcast any events in this case since we shouldn't swap. + assert_no_event(service_receiver); + } +} +#[tokio::test] +async fn run_test_swap_higher() { + run_test(DEFAULT_TIMEOUT, TestSwapHigher, Default::default()).await; +} + +#[tokio::test] +async fn run_test_single() { + run_test(DEFAULT_TIMEOUT, TestSingle, Default::default()).await; +} + +#[tokio::test] +async fn run_test_disconnect() { + run_test(DEFAULT_TIMEOUT, TestDisconnect, Default::default()).await; +} + +#[tokio::test] +async fn run_test_disconnect_other_consumer() { + run_test(DEFAULT_TIMEOUT, TestDisconnectOtherConsumer, Default::default()).await; +} + +#[tokio::test] +async fn run_test_disconnect_other_provider() { + run_test(DEFAULT_TIMEOUT, TestDisconnectOtherProvider, Default::default()).await; +} + +#[tokio::test] +async fn run_test_min_consumer_power() { + run_test( + DEFAULT_TIMEOUT, + TestMinConsumerPower, + Config { + min_consumer_threshold_mw: Some(MIN_CONSUMER_THRESHOLD_MW), + ..Default::default() + }, + ) + .await; +} + +#[tokio::test] +async fn run_test_no_swap() { + run_test(DEFAULT_TIMEOUT, TestNoSwap, Default::default()).await; +} diff --git a/power-policy-service/tests/provider.rs b/power-policy-service/tests/provider.rs new file mode 100644 index 000000000..2a4a7f2b7 --- /dev/null +++ b/power-policy-service/tests/provider.rs @@ -0,0 +1,311 @@ +#![allow(clippy::unwrap_used)] +use embassy_sync::channel::DynamicReceiver; +use embassy_sync::signal::Signal; +use embassy_time::{Duration, TimeoutError, with_timeout}; +use embedded_services::GlobalRawMutex; +use embedded_services::info; +use power_policy_interface::capability::ProviderFlags; +use power_policy_interface::capability::ProviderPowerCapability; + +mod common; + +use common::{LOW_POWER, ServiceMutex}; +use power_policy_interface::service::event::Event as ServiceEvent; + +use crate::common::DeviceType; +use crate::common::HIGH_POWER; +use crate::common::Test; +use crate::common::assert_no_event; +use crate::common::{DEFAULT_TIMEOUT, assert_provider_connected, assert_provider_disconnected, mock::FnCall, run_test}; + +const PER_CALL_TIMEOUT: Duration = Duration::from_millis(1000); + +/// Test the basic provider flow with a single device. +struct TestSingle; + +impl Test for TestSingle { + async fn run<'a>( + &mut self, + _service: &ServiceMutex<'a, 'a>, + service_receiver: DynamicReceiver<'a, ServiceEvent<'a, DeviceType<'a>>>, + device0: &DeviceType<'a>, + device0_signal: &Signal, + _device1: &DeviceType<'a>, + _device1_signal: &Signal, + ) { + info!("Running test_single"); + // Test initial connection + { + device0.lock().await.simulate_provider_connection(LOW_POWER).await; + + assert_eq!( + with_timeout(PER_CALL_TIMEOUT, device0_signal.wait()).await.unwrap(), + ( + 1, + FnCall::ConnectProvider(ProviderPowerCapability { + capability: LOW_POWER, + flags: ProviderFlags::none(), + }) + ) + ); + device0_signal.reset(); + + assert_provider_connected( + service_receiver, + device0, + ProviderPowerCapability { + capability: LOW_POWER, + flags: ProviderFlags::none(), + }, + ) + .await; + } + // Test detach + { + device0.lock().await.simulate_detach().await; + + // Power policy shouldn't call any functions on detach so we'll timeout + assert_eq!( + with_timeout(PER_CALL_TIMEOUT, device0_signal.wait()).await, + Err(TimeoutError) + ); + device0_signal.reset(); + + assert_provider_disconnected(service_receiver, device0).await; + } + + assert_no_event(service_receiver); + } +} + +/// Test provider flow involving multiple devices and upgrading a provider's power capability. +struct TestUpgrade; + +impl Test for TestUpgrade { + async fn run<'a>( + &mut self, + service: &ServiceMutex<'a, 'a>, + service_receiver: DynamicReceiver<'a, ServiceEvent<'a, DeviceType<'a>>>, + device0: &DeviceType<'a>, + device0_signal: &Signal, + device1: &DeviceType<'a>, + device1_signal: &Signal, + ) { + info!("Running test_upgrade"); + { + // Connect device0 at high power, default service config should allow this + device0.lock().await.simulate_provider_connection(HIGH_POWER).await; + + assert_eq!( + with_timeout(PER_CALL_TIMEOUT, device0_signal.wait()).await.unwrap(), + ( + 1, + FnCall::ConnectProvider(ProviderPowerCapability { + capability: HIGH_POWER, + flags: ProviderFlags::none(), + }) + ) + ); + device0_signal.reset(); + + assert_provider_connected( + service_receiver, + device0, + ProviderPowerCapability { + capability: HIGH_POWER, + flags: ProviderFlags::none(), + }, + ) + .await; + + assert_eq!(service.lock().await.compute_total_provider_power_mw().await, 15000); + } + + { + // Connect device1 at low power, default service config should allow this + device1.lock().await.simulate_provider_connection(LOW_POWER).await; + + assert_eq!( + with_timeout(PER_CALL_TIMEOUT, device1_signal.wait()).await.unwrap(), + ( + 1, + FnCall::ConnectProvider(ProviderPowerCapability { + capability: LOW_POWER, + flags: ProviderFlags::none(), + }) + ) + ); + device1_signal.reset(); + + assert_provider_connected( + service_receiver, + device1, + ProviderPowerCapability { + capability: LOW_POWER, + flags: ProviderFlags::none(), + }, + ) + .await; + + assert_eq!(service.lock().await.compute_total_provider_power_mw().await, 22500); + } + + { + // Attempt to upgrade device1 to high power, power policy should reject this since device0 is already connected at high power + // Power policy will instead allow us to connect at low power + device1 + .lock() + .await + .simulate_update_requested_provider_power_capability(Some(HIGH_POWER.into())) + .await; + + assert_eq!( + with_timeout(PER_CALL_TIMEOUT, device1_signal.wait()).await.unwrap(), + ( + 1, + FnCall::ConnectProvider(ProviderPowerCapability { + capability: LOW_POWER, + flags: ProviderFlags::none(), + }) + ) + ); + device1_signal.reset(); + + assert_provider_connected( + service_receiver, + device1, + ProviderPowerCapability { + capability: LOW_POWER, + flags: ProviderFlags::none(), + }, + ) + .await; + + assert_eq!(service.lock().await.compute_total_provider_power_mw().await, 22500); + } + + { + // Detach device0, this should allow us to upgrade device1 to high power + device0.lock().await.simulate_detach().await; + + // Power policy shouldn't call any functions on detach so we'll timeout + assert_eq!( + with_timeout(PER_CALL_TIMEOUT, device0_signal.wait()).await, + Err(TimeoutError) + ); + device0_signal.reset(); + + assert_provider_disconnected(service_receiver, device0).await; + assert_eq!(service.lock().await.compute_total_provider_power_mw().await, 7500); + } + + { + // Attempt to upgrade device1 to high power should now succeed + device1 + .lock() + .await + .simulate_update_requested_provider_power_capability(Some(HIGH_POWER.into())) + .await; + + assert_eq!( + with_timeout(PER_CALL_TIMEOUT, device1_signal.wait()).await.unwrap(), + ( + 1, + FnCall::ConnectProvider(ProviderPowerCapability { + capability: HIGH_POWER, + flags: ProviderFlags::none(), + }) + ) + ); + device1_signal.reset(); + + assert_provider_connected( + service_receiver, + device1, + ProviderPowerCapability { + capability: HIGH_POWER, + flags: ProviderFlags::none(), + }, + ) + .await; + assert_eq!(service.lock().await.compute_total_provider_power_mw().await, 15000); + } + + assert_no_event(service_receiver); + } +} + +/// Test the provider disconnect flow +struct TestDisconnect; + +impl Test for TestDisconnect { + async fn run<'a>( + &mut self, + service: &ServiceMutex<'a, 'a>, + service_receiver: DynamicReceiver<'a, ServiceEvent<'a, DeviceType<'a>>>, + device0: &DeviceType<'a>, + device0_signal: &Signal, + _device1: &DeviceType<'a>, + _device1_signal: &Signal, + ) { + info!("Running test_disconnect"); + // Test initial connection + { + device0.lock().await.simulate_provider_connection(LOW_POWER).await; + + assert_eq!( + with_timeout(PER_CALL_TIMEOUT, device0_signal.wait()).await.unwrap(), + ( + 1, + FnCall::ConnectProvider(ProviderPowerCapability { + capability: LOW_POWER, + flags: ProviderFlags::none(), + }) + ) + ); + device0_signal.reset(); + + assert_provider_connected( + service_receiver, + device0, + ProviderPowerCapability { + capability: LOW_POWER, + flags: ProviderFlags::none(), + }, + ) + .await; + assert_eq!(service.lock().await.compute_total_provider_power_mw().await, 7500); + } + // Test disconnect + { + device0.lock().await.simulate_disconnect().await; + + // Power policy shouldn't call any functions on disconnect so we'll timeout + assert_eq!( + with_timeout(PER_CALL_TIMEOUT, device0_signal.wait()).await, + Err(TimeoutError) + ); + device0_signal.reset(); + + assert_provider_disconnected(service_receiver, device0).await; + assert_eq!(service.lock().await.compute_total_provider_power_mw().await, 0); + } + + assert_no_event(service_receiver); + } +} + +#[tokio::test] +async fn run_test_single() { + run_test(DEFAULT_TIMEOUT, TestSingle, Default::default()).await; +} + +#[tokio::test] +async fn run_test_upgrade() { + run_test(DEFAULT_TIMEOUT, TestUpgrade, Default::default()).await; +} + +#[tokio::test] +async fn run_test_disconnect() { + run_test(DEFAULT_TIMEOUT, TestDisconnect, Default::default()).await; +} diff --git a/power-policy-service/tests/unconstrained.rs b/power-policy-service/tests/unconstrained.rs new file mode 100644 index 000000000..673eadfc6 --- /dev/null +++ b/power-policy-service/tests/unconstrained.rs @@ -0,0 +1,177 @@ +#![allow(clippy::unwrap_used)] +use embassy_sync::channel::DynamicReceiver; +use embassy_sync::signal::Signal; +use embassy_time::TimeoutError; +use embassy_time::{Duration, with_timeout}; +use embedded_services::GlobalRawMutex; +use embedded_services::info; +use power_policy_interface::capability::{ConsumerFlags, ConsumerPowerCapability}; + +mod common; + +use common::LOW_POWER; +use power_policy_interface::service::UnconstrainedState; +use power_policy_interface::service::event::Event as ServiceEvent; + +use crate::common::HIGH_POWER; +use crate::common::{ + DEFAULT_TIMEOUT, assert_consumer_connected, assert_consumer_disconnected, assert_no_event, assert_unconstrained, + mock::FnCall, run_test, +}; +use crate::common::{DeviceType, ServiceMutex, Test}; + +const PER_CALL_TIMEOUT: Duration = Duration::from_millis(1000); + +/// Test unconstrained consumer flow with multiple devices. +struct TestUnconstrained; + +impl Test for TestUnconstrained { + async fn run<'a>( + &mut self, + _service: &ServiceMutex<'a, 'a>, + service_receiver: DynamicReceiver<'a, ServiceEvent<'a, DeviceType<'a>>>, + device0: &DeviceType<'a>, + device0_signal: &Signal, + device1: &DeviceType<'a>, + device1_signal: &Signal, + ) { + info!("Running test_unconstrained"); + { + // Connect device0, without unconstrained, + device0 + .lock() + .await + .simulate_consumer_connection(LOW_POWER.into()) + .await; + + assert_eq!( + with_timeout(PER_CALL_TIMEOUT, device0_signal.wait()).await.unwrap(), + ( + 1, + FnCall::ConnectConsumer(ConsumerPowerCapability { + capability: LOW_POWER, + flags: ConsumerFlags::none(), + }) + ) + ); + device0_signal.reset(); + + assert_consumer_connected( + service_receiver, + device0, + ConsumerPowerCapability { + capability: LOW_POWER, + flags: ConsumerFlags::none(), + }, + ) + .await; + + // Should not have any unconstrained events + assert!(service_receiver.try_receive().is_err()); + } + + { + // Connect device1 with unconstrained at HIGH_POWER to force power policy to select this consumer. + device1 + .lock() + .await + .simulate_consumer_connection(ConsumerPowerCapability { + capability: HIGH_POWER, + flags: ConsumerFlags::none().with_unconstrained_power(), + }) + .await; + + assert_eq!( + with_timeout(PER_CALL_TIMEOUT, device0_signal.wait()).await.unwrap(), + (1, FnCall::Disconnect) + ); + device0_signal.reset(); + + assert_eq!( + with_timeout(PER_CALL_TIMEOUT, device1_signal.wait()).await.unwrap(), + ( + 1, + FnCall::ConnectConsumer(ConsumerPowerCapability { + capability: HIGH_POWER, + flags: ConsumerFlags::none().with_unconstrained_power(), + }) + ) + ); + device1_signal.reset(); + + // Should receive a disconnect event from device0 first + assert_consumer_disconnected(service_receiver, device0).await; + + assert_consumer_connected( + service_receiver, + device1, + ConsumerPowerCapability { + capability: HIGH_POWER, + flags: ConsumerFlags::none().with_unconstrained_power(), + }, + ) + .await; + + assert_unconstrained( + service_receiver, + UnconstrainedState { + unconstrained: true, + available: 1, + }, + ) + .await; + } + + { + // Test detach device1, unconstrained state should change + device1.lock().await.simulate_detach().await; + + // Power policy shouldn't call any functions on detach so we'll timeout + assert_eq!( + with_timeout(PER_CALL_TIMEOUT, device1_signal.wait()).await, + Err(TimeoutError) + ); + + assert_eq!( + with_timeout(PER_CALL_TIMEOUT, device0_signal.wait()).await.unwrap(), + ( + 1, + FnCall::ConnectConsumer(ConsumerPowerCapability { + capability: LOW_POWER, + flags: ConsumerFlags::none(), + }) + ) + ); + device0_signal.reset(); + + // Should receive a disconnect event from device1 first + assert_consumer_disconnected(service_receiver, device1).await; + + assert_consumer_connected( + service_receiver, + device0, + ConsumerPowerCapability { + capability: LOW_POWER, + flags: ConsumerFlags::none(), + }, + ) + .await; + + assert_unconstrained( + service_receiver, + UnconstrainedState { + unconstrained: false, + available: 0, + }, + ) + .await; + } + + assert_no_event(service_receiver); + } +} + +#[tokio::test] +async fn run_test_unconstrained() { + run_test(DEFAULT_TIMEOUT, TestUnconstrained, Default::default()).await; +} diff --git a/supply-chain/audits.toml b/supply-chain/audits.toml index a13008070..8fa3b8413 100644 --- a/supply-chain/audits.toml +++ b/supply-chain/audits.toml @@ -22,6 +22,11 @@ who = "Robert Zieba " criteria = "safe-to-run" version = "0.6.21" +[[audits.anstyle]] +who = "matteotullo " +criteria = "safe-to-deploy" +delta = "1.0.10 -> 1.0.13" + [[audits.anstyle-parse]] who = "Robert Zieba " criteria = "safe-to-run" @@ -151,6 +156,21 @@ who = "Robert Zieba " criteria = "safe-to-run" version = "1.0.3" +[[audits.colorchoice]] +who = "matteotullo " +criteria = "safe-to-deploy" +delta = "1.0.3 -> 1.0.4" + +[[audits.colorchoice]] +who = "matteotullo " +criteria = "safe-to-deploy" +delta = "1.0.3 -> 1.0.4" + +[[audits.colorchoice]] +who = "matteotullo " +criteria = "safe-to-deploy" +delta = "1.0.3 -> 1.0.4" + [[audits.const-init]] who = "Jerry Xie " criteria = "safe-to-deploy" @@ -204,6 +224,16 @@ criteria = "safe-to-deploy" version = "1.0.7" notes = "no_std device driver toolkit. Unsafe limited to ops.rs bitfield load/store using get_unchecked with documented invariants; fuzz-tested against bitvec. No build script, no proc macros, no filesystem/network/process access. Assisted-by: copilot-chat:claude-opus-4.6 cargo-vet" +[[audits.device-driver]] +who = "Felipe Balbi " +criteria = "safe-to-deploy" +version = "1.0.9" + +[[audits.device-driver]] +who = "Felipe Balbi " +criteria = "safe-to-run" +version = "1.0.9" + [[audits.device-driver]] who = "Adam Sasine " criteria = "safe-to-deploy" @@ -229,10 +259,19 @@ version = "0.5.0" notes = "No unsafe, no build script, no proc macros. no_std shared bus/flash partition utilities for embedded-hal traits. Assisted-by: copilot-cli:claude-opus-4.6 cargo-vet" [[audits.embassy-embedded-hal]] -who = "Jerry Xie " +who = "Billy Price " criteria = "safe-to-deploy" delta = "0.5.0 -> 0.6.0" -notes = "No unsafe code, no build script, no powerful imports. Added Clone for I2C devices. Updated embassy dependencies (embassy-sync 0.7→0.8, embassy-hal-internal 0.3→0.4, embassy-time 0.5→0.5.1). All changes safe." + +[[audits.embassy-executor]] +who = "Billy Price " +criteria = "safe-to-deploy" +delta = "0.9.1 -> 0.10.0" + +[[audits.embassy-executor-macros]] +who = "Billy Price " +criteria = "safe-to-deploy" +delta = "0.7.0 -> 0.8.0" [[audits.embassy-futures]] who = "Jerry Xie " @@ -246,6 +285,11 @@ criteria = "safe-to-deploy" version = "0.3.0" notes = "no_std HAL internals. Unsafe in atomic ring buffer (sound SPSC), peripheral singletons, cortex-m interrupt priority. Build script emits cfg flags only. Assisted-by: copilot-cli:claude-opus-4.6 cargo-vet" +[[audits.embassy-hal-internal]] +who = "Billy Price " +criteria = "safe-to-deploy" +delta = "0.3.0 -> 0.4.0" + [[audits.embassy-hal-internal]] who = "Jerry Xie " criteria = "safe-to-deploy" @@ -258,6 +302,11 @@ criteria = "safe-to-deploy" version = "0.8.0" notes = "no_std async sync primitives. Substantial unsafe for UnsafeCell-based interiors and Send/Sync impls -- all reviewed and sound, guarded by RawMutex/critical_section. Build script only reads TARGET env var. No proc macros, no powerful imports. Assisted-by: copilot-cli:claude-opus-4.6 cargo-vet" +[[audits.embassy-time]] +who = "Billy Price " +criteria = "safe-to-deploy" +delta = "0.5.0 -> 0.5.1" + [[audits.embassy-time]] who = "Jerry Xie " criteria = "safe-to-deploy" @@ -270,12 +319,23 @@ criteria = "safe-to-deploy" version = "0.2.1" notes = "no_std driver trait for embassy-time. Minimal unsafe for extern Rust FFI calls (sound via links key). Empty build.rs. Assisted-by: copilot-cli:claude-opus-4.6 cargo-vet" +[[audits.embassy-time-driver]] +who = "Billy Price " +criteria = "safe-to-deploy" +delta = "0.2.1 -> 0.2.2" + [[audits.embassy-time-driver]] who = "Jerry Xie " criteria = "safe-to-deploy" delta = "0.2.1 -> 0.2.2" notes = "Rust 2024 edition update with 375kHz tick rate feature. Empty build.rs, no unsafe code, no powerful imports." +[[audits.embassy-time-driver]] +who = "Jerry Xie " +criteria = "safe-to-deploy" +delta = "0.5.0 -> 0.6.0" +notes = "No unsafe code, no build script, no powerful imports. Added Clone for I2C devices. Updated embassy dependencies (embassy-sync 0.7→0.8, embassy-hal-internal 0.3→0.4, embassy-time 0.5→0.5.1). All changes safe." + [[audits.embassy-time-queue-utils]] who = "Felipe Balbi " criteria = "safe-to-deploy" @@ -529,6 +589,11 @@ who = "Matteo Tullo " criteria = "safe-to-deploy" version = "0.4.1" +[[audits.ident_case]] +who = "matteotullo " +criteria = "safe-to-deploy" +version = "1.0.1" + [[audits.include_dir]] who = "Robert Zieba " criteria = "safe-to-deploy" @@ -577,17 +642,32 @@ who = "Robert Zieba " criteria = "safe-to-run" version = "0.2.18" +[[audits.jiff]] +who = "Robert Zieba " +criteria = "safe-to-run" +version = "0.2.20" + [[audits.jiff-static]] who = "Robert Zieba " criteria = "safe-to-run" version = "0.2.18" +[[audits.jiff-static]] +who = "Robert Zieba " +criteria = "safe-to-run" +version = "0.2.20" + [[audits.kdl]] who = "Jerry Xie " criteria = "safe-to-deploy" version = "6.3.4" notes = "Pure KDL document language parser/formatter. No unsafe code, no build script, no proc macros, no filesystem/network/process access. Assisted-by: copilot-cli:claude-opus-4.6 cargo-vet" +[[audits.libc]] +who = "Robert Zieba " +criteria = "safe-to-run" +version = "0.2.18" + [[audits.libc]] who = "Robert Zieba " criteria = "safe-to-run" @@ -727,15 +807,16 @@ delta = "0.7.5 -> 0.7.6" notes = "Version bump with test infrastructure updates. No unsafe code, no build script, no powerful imports. Purely additive test changes." [[audits.num_enum_derive]] -who = "Matteo Tullo " +who = "Billy Price " criteria = "safe-to-deploy" version = "0.7.4" +notes = "Looks like mostly improvements to error messaging" [[audits.num_enum_derive]] who = "Billy Price " criteria = "safe-to-deploy" delta = "0.7.4 -> 0.7.5" -notes = "Looks like mostly improvements to error messaging" +notes = "Looks like this is just uptaking a new version of num_enum_derive" [[audits.num_enum_derive]] who = "Jerry Xie " @@ -786,6 +867,11 @@ who = "Robert Zieba " criteria = "safe-to-run" version = "0.2.4" +[[audits.portable-atomic-util]] +who = "Robert Zieba " +criteria = "safe-to-run" +version = "0.2.5" + [[audits.proc-macro-error]] who = "Jerry Xie " criteria = "safe-to-deploy" diff --git a/supply-chain/config.toml b/supply-chain/config.toml index d2b556ca3..659f35ae6 100644 --- a/supply-chain/config.toml +++ b/supply-chain/config.toml @@ -22,6 +22,66 @@ audit-as-crates-io = false [policy.keyberon] audit-as-crates-io = false +[[exemptions.crc]] +version = "3.4.0" +criteria = "safe-to-deploy" + +[[exemptions.diff]] +version = "0.1.13" +criteria = "safe-to-run" + +[[exemptions.futures-task]] +version = "0.3.32" +criteria = "safe-to-run" + +[[exemptions.futures-timer]] +version = "3.0.3" +criteria = "safe-to-run" + [[exemptions.generator]] version = "0.8.5" criteria = "safe-to-deploy" + +[[exemptions.hashbrown]] +version = "0.17.0" +criteria = "safe-to-deploy" + +[[exemptions.indexmap]] +version = "2.14.0" +criteria = "safe-to-deploy" + +[[exemptions.pretty_assertions]] +version = "1.4.1" +criteria = "safe-to-run" + +[[exemptions.proc-macro-crate]] +version = "3.5.0" +criteria = "safe-to-run" + +[[exemptions.rustc-demangle]] +version = "0.1.26" +criteria = "safe-to-run" + +[[exemptions.semver]] +version = "1.0.28" +criteria = "safe-to-run" + +[[exemptions.toml_datetime]] +version = "1.1.1+spec-1.1.0" +criteria = "safe-to-run" + +[[exemptions.toml_edit]] +version = "0.25.11+spec-1.1.0" +criteria = "safe-to-run" + +[[exemptions.toml_parser]] +version = "1.1.2+spec-1.1.0" +criteria = "safe-to-run" + +[[exemptions.winnow]] +version = "1.0.2" +criteria = "safe-to-run" + +[[exemptions.yansi]] +version = "1.0.1" +criteria = "safe-to-run" diff --git a/supply-chain/imports.lock b/supply-chain/imports.lock index 9d90c2088..0fc50148b 100644 --- a/supply-chain/imports.lock +++ b/supply-chain/imports.lock @@ -103,13 +103,6 @@ user-id = 3618 user-login = "dtolnay" user-name = "David Tolnay" -[[publisher.thiserror]] -version = "1.0.69" -when = "2024-11-10" -user-id = 3618 -user-login = "dtolnay" -user-name = "David Tolnay" - [[publisher.thiserror]] version = "2.0.16" when = "2025-08-20" @@ -117,13 +110,6 @@ user-id = 3618 user-login = "dtolnay" user-name = "David Tolnay" -[[publisher.thiserror-impl]] -version = "1.0.69" -when = "2024-11-10" -user-id = 3618 -user-login = "dtolnay" -user-name = "David Tolnay" - [[publisher.thiserror-impl]] version = "2.0.16" when = "2025-08-20" @@ -180,6 +166,13 @@ user-id = 64539 user-login = "kennykerr" user-name = "Kenny Kerr" +[[publisher.windows-link]] +version = "0.2.1" +when = "2025-10-06" +user-id = 64539 +user-login = "kennykerr" +user-name = "Kenny Kerr" + [[publisher.windows-numerics]] version = "0.2.0" when = "2025-03-18" @@ -285,18 +278,109 @@ user-id = 64539 user-login = "kennykerr" user-name = "Kenny Kerr" +[[audits.OpenDevicePartnership.audits.autocfg]] +who = "Jerry Xie " +criteria = "safe-to-deploy" +delta = "1.4.0 -> 1.5.0" +notes = "No unsafe, no build.rs, no network access; delta adds edition-aware rustc probing and best-effort probe-file cleanup only. Assisted-by: copilot-cli:GPT-5.3-Codex cargo-vet" +aggregated-from = "https://raw.githubusercontent.com/OpenDevicePartnership/embedded-services/refs/heads/main/supply-chain/audits.toml" + +[[audits.OpenDevicePartnership.audits.az]] +who = "Jerry Xie " +criteria = "safe-to-deploy" +version = "1.2.1" +notes = "No unsafe code. no_std library with only safe numeric cast traits. Build script probes for track_caller via rustc in OUT_DIR only. No network, no ambient capabilities. Assisted-by: copilot-cli:claude-opus-4.6 cargo-vet" +aggregated-from = "https://raw.githubusercontent.com/OpenDevicePartnership/embedded-services/refs/heads/main/supply-chain/audits.toml" + [[audits.OpenDevicePartnership.audits.bare-metal]] who = "Felipe Balbi " criteria = "safe-to-deploy" version = "0.2.5" aggregated-from = "https://raw.githubusercontent.com/OpenDevicePartnership/mcxa-pac/refs/heads/main/supply-chain/audits.toml" +[[audits.OpenDevicePartnership.audits.bbq2]] +who = "Jerry Xie " +criteria = "safe-to-deploy" +version = "0.4.2" +notes = "no_std SPSC bip-buffer queue. Non-trivial unsafe for lock-free coordination and pointer arithmetic, all reviewed and sound. No build script, no proc macros, no I/O. Has Miri CI. Assisted-by: copilot-chat:claude-opus-4.6 cargo-vet" +aggregated-from = "https://raw.githubusercontent.com/OpenDevicePartnership/embedded-services/refs/heads/main/supply-chain/audits.toml" + +[[audits.OpenDevicePartnership.audits.bincode]] +who = "Jerry Xie " +criteria = "safe-to-deploy" +version = "2.0.1" +notes = "no_std binary serialization library. ~15 unsafe blocks for u8 type-specialization guarded by unty::type_equal and MaybeUninit patterns. No build script, no proc macros. std imports only for Encode/Decode trait impls. Assisted-by: copilot-cli:claude-opus-4.6 cargo-vet" +aggregated-from = "https://raw.githubusercontent.com/OpenDevicePartnership/embedded-services/refs/heads/main/supply-chain/audits.toml" + +[[audits.OpenDevicePartnership.audits.bincode_derive]] +who = "Jerry Xie " +criteria = "safe-to-deploy" +version = "2.0.1" +notes = "Proc-macro derive for bincode Encode/Decode. No unsafe, no build script, no I/O. Assisted-by: copilot-cli:claude-opus-4.6 cargo-vet" +aggregated-from = "https://raw.githubusercontent.com/OpenDevicePartnership/embedded-services/refs/heads/main/supply-chain/audits.toml" + [[audits.OpenDevicePartnership.audits.bitfield]] who = "Felipe Balbi " criteria = "safe-to-deploy" version = "0.13.2" aggregated-from = "https://raw.githubusercontent.com/OpenDevicePartnership/mcxa-pac/refs/heads/main/supply-chain/audits.toml" +[[audits.OpenDevicePartnership.audits.bitfield]] +who = "Jerry Xie " +criteria = "safe-to-deploy" +delta = "0.13.2 -> 0.15.0" +notes = "Delta audit: BitRange/Bit traits split into read-only and mutable variants (BitRangeMut/BitMut); added mask constant generation; clippy fixes; MSRV bump. No unsafe, no build script, no proc macros, no powerful imports. Assisted-by: copilot-cli:claude-opus-4.6 cargo-vet" +aggregated-from = "https://raw.githubusercontent.com/OpenDevicePartnership/embedded-services/refs/heads/main/supply-chain/audits.toml" + +[[audits.OpenDevicePartnership.audits.bitfield]] +who = "Jerry Xie " +criteria = "safe-to-deploy" +delta = "0.15.0 -> 0.17.0" +notes = "Delta: adds bitwise op derives, constructor derives, arbitrary visibility. Pure declarative macros. No unsafe, no build script. Assisted-by: copilot-cli:claude-opus-4.6 cargo-vet" +aggregated-from = "https://raw.githubusercontent.com/OpenDevicePartnership/embedded-services/refs/heads/main/supply-chain/audits.toml" + +[[audits.OpenDevicePartnership.audits.bitfield]] +who = "Jerry Xie " +criteria = "safe-to-deploy" +delta = "0.15.0 -> 0.19.2" +notes = "Delta: refactored to proc macros in bitfield-macros, added BitAnd/BitOr/BitXor, signed types, bool arrays. No unsafe. Assisted-by: copilot-cli:claude-opus-4.6 cargo-vet" +aggregated-from = "https://raw.githubusercontent.com/OpenDevicePartnership/embedded-services/refs/heads/main/supply-chain/audits.toml" + +[[audits.OpenDevicePartnership.audits.bitfield-macros]] +who = "Jerry Xie " +criteria = "safe-to-deploy" +version = "0.19.2" +notes = "Proc-macro generating bitfield getters/setters/masks. No unsafe, no build script, no powerful imports. Assisted-by: copilot-cli:claude-opus-4.6 cargo-vet" +aggregated-from = "https://raw.githubusercontent.com/OpenDevicePartnership/embedded-services/refs/heads/main/supply-chain/audits.toml" + +[[audits.OpenDevicePartnership.audits.bitfield-struct]] +who = "Jerry Xie " +criteria = "safe-to-deploy" +version = "0.10.1" +notes = "Proc-macro crate generating safe bitfield structs. No unsafe, no build script. Standard proc-macro deps only. Assisted-by: copilot-cli:claude-opus-4.6 cargo-vet" +aggregated-from = "https://raw.githubusercontent.com/OpenDevicePartnership/embedded-services/refs/heads/main/supply-chain/audits.toml" + +[[audits.OpenDevicePartnership.audits.bytemuck]] +who = "Jerry Xie " +criteria = "safe-to-deploy" +delta = "1.22.0 -> 1.23.2" +notes = "Delta 1.22.0->1.23.2: new ZeroableInOption impls for function pointer types (sound, uses guaranteed niche optimization), core::error::Error impls behind feature flag, safe derive helper module. No new unsafe blocks, no build script, no I/O. Assisted-by: copilot-cli:claude-opus-4.6 cargo-vet" +aggregated-from = "https://raw.githubusercontent.com/OpenDevicePartnership/embedded-services/refs/heads/main/supply-chain/audits.toml" + +[[audits.OpenDevicePartnership.audits.cfg-if]] +who = "Jerry Xie " +criteria = "safe-to-deploy" +delta = "1.0.0 -> 1.0.3" +notes = "Delta 1.0.0->1.0.3: formatting/readability refactor of macro identifiers, removed compiler_builtins dep, updated CI. No unsafe, no build script, no imports. Pure macro_rules crate. Assisted-by: copilot-cli:claude-opus-4.6 cargo-vet" +aggregated-from = "https://raw.githubusercontent.com/OpenDevicePartnership/embedded-services/refs/heads/main/supply-chain/audits.toml" + +[[audits.OpenDevicePartnership.audits.cordyceps]] +who = "Jerry Xie " +criteria = "safe-to-deploy" +version = "0.3.4" +notes = "Intrusive data structures crate (no_std). ~115 unsafe blocks, all necessary for intrusive linked list/queue/stack ops. Correct patterns: addr_of_mut, proper atomic orderings, Vyukov MPSC algorithm. No build script, no proc macros, no powerful imports. Assisted-by: copilot-cli:claude-opus-4.6 cargo-vet" +aggregated-from = "https://raw.githubusercontent.com/OpenDevicePartnership/embedded-services/refs/heads/main/supply-chain/audits.toml" + [[audits.OpenDevicePartnership.audits.cortex-m]] who = "Felipe Balbi " criteria = "safe-to-deploy" @@ -315,12 +399,40 @@ criteria = "safe-to-deploy" version = "0.7.5" aggregated-from = "https://raw.githubusercontent.com/OpenDevicePartnership/mcxa-pac/refs/heads/main/supply-chain/audits.toml" +[[audits.OpenDevicePartnership.audits.crc]] +who = "Jerry Xie " +criteria = "safe-to-deploy" +version = "3.3.0" +notes = "No unsafe (forbid(unsafe_code)), no build script, no I/O, no_std pure CRC computation. Assisted-by: copilot-chat:claude-opus-4.6 cargo-vet" +aggregated-from = "https://raw.githubusercontent.com/OpenDevicePartnership/embedded-services/refs/heads/main/supply-chain/audits.toml" + +[[audits.OpenDevicePartnership.audits.crc-catalog]] +who = "Jerry Xie " +criteria = "safe-to-deploy" +version = "2.4.0" +notes = "Pure no_std data-only crate. No unsafe, no build script, no dependencies, no I/O. Contains only const CRC algorithm parameter structs. Assisted-by: copilot-cli:claude-opus-4.6 cargo-vet" +aggregated-from = "https://raw.githubusercontent.com/OpenDevicePartnership/embedded-services/refs/heads/main/supply-chain/audits.toml" + [[audits.OpenDevicePartnership.audits.critical-section]] who = "Felipe Balbi " criteria = "safe-to-deploy" version = "1.2.0" aggregated-from = "https://raw.githubusercontent.com/OpenDevicePartnership/mcxa-pac/refs/heads/main/supply-chain/audits.toml" +[[audits.OpenDevicePartnership.audits.crunchy]] +who = "Jerry Xie " +criteria = "safe-to-deploy" +delta = "0.2.3 -> 0.2.4" +notes = "Tiny diff to use newer core/std features via build.rs env var for path separator; no safety impact. Assisted-by: copilot-cli:GPT-5.3-Codex cargo-vet" +aggregated-from = "https://raw.githubusercontent.com/OpenDevicePartnership/embedded-services/refs/heads/main/supply-chain/audits.toml" + +[[audits.OpenDevicePartnership.audits.defmt]] +who = "Jerry Xie " +criteria = "safe-to-deploy" +version = "0.3.100" +notes = "Compatibility shim: no_std crate that re-exports defmt 1.x items for 0.3 API compatibility. No unsafe code, no build script, no powerful imports, no logic - pure pub-use re-exports. Assisted-by: copilot-cli:claude-opus-4.6 cargo-vet" +aggregated-from = "https://raw.githubusercontent.com/OpenDevicePartnership/embedded-services/refs/heads/main/supply-chain/audits.toml" + [[audits.OpenDevicePartnership.audits.defmt]] who = "Felipe Balbi " criteria = "safe-to-deploy" @@ -339,18 +451,53 @@ criteria = "safe-to-deploy" version = "1.0.0" aggregated-from = "https://raw.githubusercontent.com/OpenDevicePartnership/mcxa-pac/refs/heads/main/supply-chain/audits.toml" +[[audits.OpenDevicePartnership.audits.embassy-embedded-hal]] +who = "Jerry Xie " +criteria = "safe-to-deploy" +version = "0.5.0" +notes = "No unsafe, no build script, no proc macros. no_std shared bus/flash partition utilities for embedded-hal traits. Assisted-by: copilot-cli:claude-opus-4.6 cargo-vet" +aggregated-from = "https://raw.githubusercontent.com/OpenDevicePartnership/embedded-services/refs/heads/main/supply-chain/audits.toml" + [[audits.OpenDevicePartnership.audits.embassy-executor-timer-queue]] who = "Felipe Balbi " criteria = "safe-to-deploy" version = "0.1.0" aggregated-from = "https://raw.githubusercontent.com/OpenDevicePartnership/embassy-imxrt/refs/heads/main/supply-chain/audits.toml" +[[audits.OpenDevicePartnership.audits.embassy-futures]] +who = "Jerry Xie " +criteria = "safe-to-deploy" +version = "0.1.2" +notes = "no_std future combinators. All unsafe is pin-projection and no-op RawWaker - reviewed and sound. Assisted-by: copilot-cli:claude-opus-4.6 cargo-vet" +aggregated-from = "https://raw.githubusercontent.com/OpenDevicePartnership/embedded-services/refs/heads/main/supply-chain/audits.toml" + +[[audits.OpenDevicePartnership.audits.embassy-hal-internal]] +who = "Jerry Xie " +criteria = "safe-to-deploy" +version = "0.3.0" +notes = "no_std HAL internals. Unsafe in atomic ring buffer (sound SPSC), peripheral singletons, cortex-m interrupt priority. Build script emits cfg flags only. Assisted-by: copilot-cli:claude-opus-4.6 cargo-vet" +aggregated-from = "https://raw.githubusercontent.com/OpenDevicePartnership/embedded-services/refs/heads/main/supply-chain/audits.toml" + +[[audits.OpenDevicePartnership.audits.embassy-sync]] +who = "Jerry Xie " +criteria = "safe-to-deploy" +version = "0.8.0" +notes = "no_std async sync primitives. Substantial unsafe for UnsafeCell-based interiors and Send/Sync impls -- all reviewed and sound, guarded by RawMutex/critical_section. Build script only reads TARGET env var. No proc macros, no powerful imports. Assisted-by: copilot-cli:claude-opus-4.6 cargo-vet" +aggregated-from = "https://raw.githubusercontent.com/OpenDevicePartnership/embedded-services/refs/heads/main/supply-chain/audits.toml" + [[audits.OpenDevicePartnership.audits.embassy-time]] who = "Felipe Balbi " criteria = "safe-to-deploy" version = "0.5.0" aggregated-from = "https://raw.githubusercontent.com/OpenDevicePartnership/tps6699x/refs/heads/main/supply-chain/audits.toml" +[[audits.OpenDevicePartnership.audits.embassy-time-driver]] +who = "Jerry Xie " +criteria = "safe-to-deploy" +version = "0.2.1" +notes = "no_std driver trait for embassy-time. Minimal unsafe for extern Rust FFI calls (sound via links key). Empty build.rs. Assisted-by: copilot-cli:claude-opus-4.6 cargo-vet" +aggregated-from = "https://raw.githubusercontent.com/OpenDevicePartnership/embedded-services/refs/heads/main/supply-chain/audits.toml" + [[audits.OpenDevicePartnership.audits.embassy-time-queue-utils]] who = "Felipe Balbi " criteria = "safe-to-deploy" @@ -363,12 +510,159 @@ criteria = "safe-to-deploy" version = "0.2.7" aggregated-from = "https://raw.githubusercontent.com/OpenDevicePartnership/mcxa-pac/refs/heads/main/supply-chain/audits.toml" +[[audits.OpenDevicePartnership.audits.embedded-hal]] +who = "Jerry Xie " +criteria = "safe-to-deploy" +delta = "0.2.7 -> 1.0.0" +notes = "Pure no_std trait crate. Complete API redesign for 1.0: removed nb-based traits, CAN module, all unsafe code. Only defines traits/enums/types for digital, I2C, SPI, PWM, delay. No build script, no proc macros, no powerful imports. Assisted-by: copilot-cli:claude-opus-4.6 cargo-vet" +aggregated-from = "https://raw.githubusercontent.com/OpenDevicePartnership/embedded-services/refs/heads/main/supply-chain/audits.toml" + +[[audits.OpenDevicePartnership.audits.embedded-hal-async]] +who = "Jerry Xie " +criteria = "safe-to-deploy" +version = "1.0.0" +notes = "no_std async HAL trait definitions. No unsafe in library. Build script only runs rustc --version. Assisted-by: copilot-cli:claude-opus-4.6 cargo-vet" +aggregated-from = "https://raw.githubusercontent.com/OpenDevicePartnership/embedded-services/refs/heads/main/supply-chain/audits.toml" + +[[audits.OpenDevicePartnership.audits.embedded-hal-nb]] +who = "Jerry Xie " +criteria = "safe-to-deploy" +version = "1.0.0" +notes = "no_std trait-only crate. No unsafe, no build script, no proc macros, no powerful imports. Assisted-by: copilot-cli:claude-opus-4.6 cargo-vet" +aggregated-from = "https://raw.githubusercontent.com/OpenDevicePartnership/embedded-services/refs/heads/main/supply-chain/audits.toml" + +[[audits.OpenDevicePartnership.audits.embedded-io]] +who = "Jerry Xie " +criteria = "safe-to-deploy" +delta = "0.6.1 -> 0.7.1" +notes = "Add core::error::Error trait bound (MSRV 1.81). defmt 0.3->1.0. Implement ReadReady/WriteReady for slices and VecDeque. Add seek_relative(). Fix method forwardings. Trusted publisher (Dirbaio from Embedded WG)." +aggregated-from = "https://raw.githubusercontent.com/OpenDevicePartnership/embassy-imxrt/refs/heads/main/supply-chain/audits.toml" + +[[audits.OpenDevicePartnership.audits.embedded-io-async]] +who = "Jerry Xie " +criteria = "safe-to-deploy" +version = "0.6.1" +notes = "No unsafe. Build script only detects nightly via rustc --version. Pure async trait definitions for embedded I/O. Assisted-by: copilot-cli:claude-opus-4.6 cargo-vet" +aggregated-from = "https://raw.githubusercontent.com/OpenDevicePartnership/embedded-services/refs/heads/main/supply-chain/audits.toml" + +[[audits.OpenDevicePartnership.audits.embedded-io-async]] +who = "Jerry Xie " +criteria = "safe-to-deploy" +delta = "0.6.1 -> 0.7.0" +notes = "Delta 0.6.1->0.7.0: No unsafe. Build script removed (AFIT now stable). flush() made required, BufRead requires Read, new VecDeque impls. Assisted-by: copilot-cli:claude-opus-4.6 cargo-vet" +aggregated-from = "https://raw.githubusercontent.com/OpenDevicePartnership/embedded-services/refs/heads/main/supply-chain/audits.toml" + +[[audits.OpenDevicePartnership.audits.embedded-storage]] +who = "Jerry Xie " +criteria = "safe-to-deploy" +version = "0.3.1" +notes = "Pure no_std storage abstraction traits. deny(unsafe_code), no build script, no dependencies, no powerful imports. Assisted-by: copilot-cli:claude-opus-4.6 cargo-vet" +aggregated-from = "https://raw.githubusercontent.com/OpenDevicePartnership/embedded-services/refs/heads/main/supply-chain/audits.toml" + +[[audits.OpenDevicePartnership.audits.embedded-storage-async]] +who = "Jerry Xie " +criteria = "safe-to-deploy" +version = "0.4.1" +notes = "Pure no_std async trait definitions for NOR flash storage. No unsafe code, no build script, no powerful imports. Only dependency is embedded-storage. Assisted-by: copilot-cli:claude-opus-4.6 cargo-vet" +aggregated-from = "https://raw.githubusercontent.com/OpenDevicePartnership/embedded-services/refs/heads/main/supply-chain/audits.toml" + +[[audits.OpenDevicePartnership.audits.fixed]] +who = "Jerry Xie " +criteria = "safe-to-deploy" +version = "1.29.0" +notes = "no_std fixed-point number library. Unsafe limited to: bytemuck Pod/Zeroable impls on repr(transparent) types, NonZero::new_unchecked after proven-nonzero guards, unreachable_unchecked in exhaustive remainder logic. Build script probes compiler features in OUT_DIR. Assisted-by: copilot-cli:claude-opus-4.6 cargo-vet" +aggregated-from = "https://raw.githubusercontent.com/OpenDevicePartnership/embedded-services/refs/heads/main/supply-chain/audits.toml" + +[[audits.OpenDevicePartnership.audits.hash32]] +who = "Jerry Xie " +criteria = "safe-to-deploy" +version = "0.3.1" +notes = "no_std 32-bit hashing (FNV, MurmurHash3). ~10 unsafe blocks in murmur3.rs for MaybeUninit buffer handling - all sound. Assisted-by: copilot-cli:claude-opus-4.6 cargo-vet" +aggregated-from = "https://raw.githubusercontent.com/OpenDevicePartnership/embedded-services/refs/heads/main/supply-chain/audits.toml" + +[[audits.OpenDevicePartnership.audits.heapless]] +who = "Jerry Xie " +criteria = "safe-to-deploy" +version = "0.8.0" +notes = "no_std fixed-capacity data structures. Extensive unsafe for MaybeUninit buffer management, lock-free queues (Vyukov MPMC, SPSC), and Treiber stack memory pools with ABA prevention. Patterns mirror std or published algorithms. Build script probes for atomic/LLSC support. Assisted-by: copilot-cli:claude-opus-4.6 cargo-vet" +aggregated-from = "https://raw.githubusercontent.com/OpenDevicePartnership/embedded-services/refs/heads/main/supply-chain/audits.toml" + +[[audits.OpenDevicePartnership.audits.heapless]] +who = "Jerry Xie " +criteria = "safe-to-deploy" +version = "0.9.2" +notes = "no_std fixed-capacity data structures. Extensive unsafe for MaybeUninit buffers, lock-free queues (Vyukov MPMC, SPSC), Treiber stack pools with ABA prevention (CAS tagged pointers + ARM LLSC). All Send/Sync bounds verified correct. Build script probes for ARM LLSC. Assisted-by: copilot-cli:claude-opus-4.6 cargo-vet" +aggregated-from = "https://raw.githubusercontent.com/OpenDevicePartnership/embedded-services/refs/heads/main/supply-chain/audits.toml" + +[[audits.OpenDevicePartnership.audits.io-uring]] +who = "Jerry Xie " +criteria = "safe-to-run" +delta = "0.5.13 -> 0.7.10" +notes = "Delta audit. Linux io_uring bindings. +15 hand-written unsafe (new from_fd, buffer/file registration APIs). SeqCst fence fix improves atomics correctness. Probe refactored to stack allocation. Build script adds cfg checks only. All unsafe for expected syscall/mmap/fd operations. Assisted-by: copilot-cli:claude-opus-4.6 cargo-vet" +aggregated-from = "https://raw.githubusercontent.com/OpenDevicePartnership/embedded-services/refs/heads/main/supply-chain/audits.toml" + +[[audits.OpenDevicePartnership.audits.litrs]] +who = "Jerry Xie " +criteria = "safe-to-deploy" +delta = "0.4.1 -> 0.4.2" +notes = "Delta 0.4.1->0.4.2: Bug fixes for non-ASCII byte string escapes, removes CR LF normalization to align with spec, fixes error span for out-of-range Unicode escapes. No unsafe code, no build script, no powerful imports. Assisted-by: copilot-cli:claude-opus-4.6 cargo-vet" +aggregated-from = "https://raw.githubusercontent.com/OpenDevicePartnership/embedded-services/refs/heads/main/supply-chain/audits.toml" + +[[audits.OpenDevicePartnership.audits.maitake-sync]] +who = "Jerry Xie " +criteria = "safe-to-deploy" +version = "0.2.2" +notes = "No-std async sync primitives. Extensive unsafe for Send/Sync impls, UnsafeCell access under locks/atomics, intrusive linked list nodes, spinlocks -- all follow standard patterns. Uses unreachable_unchecked! macro (panics in debug). No build script, no proc macros. Loom-tested. Assisted-by: copilot-cli:claude-opus-4.6 cargo-vet" +aggregated-from = "https://raw.githubusercontent.com/OpenDevicePartnership/embedded-services/refs/heads/main/supply-chain/audits.toml" + +[[audits.OpenDevicePartnership.audits.mimxrt600-fcb]] +who = "Jerry Xie " +criteria = "safe-to-deploy" +version = "0.2.1" +notes = "Pure no_std data-definition crate for MIMXRT600 flash config blocks. No unsafe, no build script. Assisted-by: copilot-cli:claude-opus-4.6 cargo-vet" +aggregated-from = "https://raw.githubusercontent.com/OpenDevicePartnership/embedded-services/refs/heads/main/supply-chain/audits.toml" + [[audits.OpenDevicePartnership.audits.mimxrt600-fcb]] who = "Jerry Xie " criteria = "safe-to-deploy" delta = "0.2.1 -> 0.2.2" aggregated-from = "https://raw.githubusercontent.com/OpenDevicePartnership/embassy-imxrt/refs/heads/main/supply-chain/audits.toml" +[[audits.OpenDevicePartnership.audits.mio]] +who = "Jerry Xie " +criteria = "safe-to-run" +delta = "1.0.1 -> 1.0.4" +notes = "Delta 1.0.1->1.0.4: I/O safety trait impls, AIX poll(2) support, windows-sys 0.59 pointer fixes. Unsafe Send/Sync for CompletionPort/Inner sound. Assisted-by: copilot-cli:claude-opus-4.6 cargo-vet" +aggregated-from = "https://raw.githubusercontent.com/OpenDevicePartnership/embedded-services/refs/heads/main/supply-chain/audits.toml" + +[[audits.OpenDevicePartnership.audits.mycelium-bitfield]] +who = "Jerry Xie " +criteria = "safe-to-deploy" +version = "0.1.5" +notes = "Pure safe no_std bitfield macro crate. No unsafe code, no build script, no proc macros, no dependencies, no powerful imports. Only core:: types used. Assisted-by: copilot-chat:claude-opus-4.6 cargo-vet" +aggregated-from = "https://raw.githubusercontent.com/OpenDevicePartnership/embedded-services/refs/heads/main/supply-chain/audits.toml" + +[[audits.OpenDevicePartnership.audits.once_cell]] +who = "Jerry Xie " +criteria = "safe-to-deploy" +version = "1.20.1" +notes = "Single-assignment cells and lazy values. All unsafe reviewed: UnsafeCell access, Send/Sync impls, atomic waiter queue, strict provenance polyfill - all sound with correct bounds. No build script, no proc macros, no powerful imports beyond std::thread/atomic. Assisted-by: copilot-chat:claude-opus-4.6 cargo-vet" +aggregated-from = "https://raw.githubusercontent.com/OpenDevicePartnership/embedded-services/refs/heads/main/supply-chain/audits.toml" + +[[audits.OpenDevicePartnership.audits.pin-project]] +who = "Jerry Xie " +criteria = "safe-to-deploy" +version = "1.1.10" +notes = "no_std pin-projection helper. Re-exports proc macros from pin-project-internal. Minimal unsafe in __private module (drop guards, UnsafeUnpin forwarding) -- all sound with SAFETY comments. No build script, no powerful imports. Assisted-by: copilot-cli:claude-opus-4.6 cargo-vet" +aggregated-from = "https://raw.githubusercontent.com/OpenDevicePartnership/embedded-services/refs/heads/main/supply-chain/audits.toml" + +[[audits.OpenDevicePartnership.audits.pin-project-internal]] +who = "Jerry Xie " +criteria = "safe-to-deploy" +version = "1.1.10" +notes = "Proc-macro for pin projection. forbid(unsafe_code) in macro itself. Generated unsafe is sound pin projection (Pin::new_unchecked, get_unchecked_mut) with compile-time safety enforced via trait tricks. No build script, no I/O. Deps: proc-macro2, quote, syn only. Assisted-by: copilot-chat:claude-opus-4.6 cargo-vet" +aggregated-from = "https://raw.githubusercontent.com/OpenDevicePartnership/embedded-services/refs/heads/main/supply-chain/audits.toml" + [[audits.OpenDevicePartnership.audits.proc-macro-error-attr2]] who = "Felipe Balbi " criteria = "safe-to-deploy" @@ -399,18 +693,122 @@ criteria = "safe-to-deploy" version = "0.7.0" aggregated-from = "https://raw.githubusercontent.com/OpenDevicePartnership/mcxa-pac/refs/heads/main/supply-chain/audits.toml" +[[audits.OpenDevicePartnership.audits.serde_spanned]] +who = "Jerry Xie " +criteria = "safe-to-deploy" +delta = "0.6.8 -> 0.6.9" +notes = "Trivial delta: metadata, lint config, and doc formatting only. No functional code changes, no unsafe, no build script, no I/O. Assisted-by: copilot-cli:claude-opus-4.6 cargo-vet" +aggregated-from = "https://raw.githubusercontent.com/OpenDevicePartnership/embedded-services/refs/heads/main/supply-chain/audits.toml" + +[[audits.OpenDevicePartnership.audits.slab]] +who = "Jerry Xie " +criteria = "safe-to-run" +delta = "0.4.8 -> 0.4.11" +notes = "Delta 0.4.8->0.4.11: new get_disjoint_mut uses unsafe (MaybeUninit + raw ptrs) with sound bounds/overlap checks. build.rs removed. No powerful imports. Assisted-by: copilot-cli:claude-opus-4.6 cargo-vet" +aggregated-from = "https://raw.githubusercontent.com/OpenDevicePartnership/embedded-services/refs/heads/main/supply-chain/audits.toml" + +[[audits.OpenDevicePartnership.audits.thread_local]] +who = "Jerry Xie " +criteria = "safe-to-deploy" +delta = "1.1.4 -> 1.1.9" +notes = "No build script, no FS/net/process capability expansion; unsafe refactor to lock-free insertion and nightly TLS path appears sound on review. Assisted-by: copilot-cli:GPT-5.3-Codex cargo-vet" +aggregated-from = "https://raw.githubusercontent.com/OpenDevicePartnership/embedded-services/refs/heads/main/supply-chain/audits.toml" + +[[audits.OpenDevicePartnership.audits.tokio]] +who = "Jerry Xie " +criteria = "safe-to-run" +delta = "1.45.0 -> 1.47.1" +notes = "Delta audit. New SetOnce sync primitive, OwnedNotified, spawn location tracking (tokio_unstable), experimental io_uring behind cfg gate, block_in_place hardening. All new unsafe follows existing patterns with safety comments. No build script, no proc macros. Assisted-by: copilot-cli:claude-opus-4.6 cargo-vet" +aggregated-from = "https://raw.githubusercontent.com/OpenDevicePartnership/embedded-services/refs/heads/main/supply-chain/audits.toml" + +[[audits.OpenDevicePartnership.audits.toml]] +who = "Jerry Xie " +criteria = "safe-to-deploy" +delta = "0.8.22 -> 0.8.23" +notes = "Delta: adds TupleVariant/StructVariant serialization support. All new code is thin wrappers delegating to toml_edit. No unsafe (forbid(unsafe_code)), no build script, no powerful imports. Assisted-by: copilot-cli:claude-opus-4.6 cargo-vet" +aggregated-from = "https://raw.githubusercontent.com/OpenDevicePartnership/embedded-services/refs/heads/main/supply-chain/audits.toml" + +[[audits.OpenDevicePartnership.audits.toml_datetime]] +who = "Jerry Xie " +criteria = "safe-to-deploy" +delta = "0.6.9 -> 0.6.11" +notes = "Delta 0.6.9->0.6.11: parser refactored from char-by-char to lexer-based tokenizer with improved error messages; no unsafe (forbid(unsafe_code)), no build script, no powerful imports. Assisted-by: copilot-cli:claude-opus-4.6 cargo-vet" +aggregated-from = "https://raw.githubusercontent.com/OpenDevicePartnership/embedded-services/refs/heads/main/supply-chain/audits.toml" + +[[audits.OpenDevicePartnership.audits.toml_edit]] +who = "Jerry Xie " +criteria = "safe-to-deploy" +delta = "0.22.26 -> 0.22.27" +notes = "Delta: no changes to unsafe code (all pre-existing from_utf8_unchecked on ASCII-validated buffers). Visibility reductions on parser internals, serializer refactoring, new consuming accessors. No build script, no proc macros, no powerful imports. Assisted-by: copilot-cli:claude-opus-4.6 cargo-vet" +aggregated-from = "https://raw.githubusercontent.com/OpenDevicePartnership/embedded-services/refs/heads/main/supply-chain/audits.toml" + +[[audits.OpenDevicePartnership.audits.typenum]] +who = "Jerry Xie " +criteria = "safe-to-deploy" +version = "1.18.0" +notes = "Pure no_std type-level numbers crate. forbid(unsafe_code) -- zero unsafe anywhere. Build script only writes generated test code to OUT_DIR. No proc macros, no FFI, no network/filesystem/process access in library. Assisted-by: copilot-cli:claude-opus-4.6 cargo-vet" +aggregated-from = "https://raw.githubusercontent.com/OpenDevicePartnership/embedded-services/refs/heads/main/supply-chain/audits.toml" + +[[audits.OpenDevicePartnership.audits.unty]] +who = "Jerry Xie " +criteria = "safe-to-deploy" +version = "0.0.4" +notes = "Tiny no_std crate (1 file, ~120 LOC, zero deps). Two unsafe blocks: transmute_copy guarded by TypeId check in unty(), and a dtolnay-pattern transmute in non_static_type_id(). Both documented; no build script, no powerful imports. Assisted-by: copilot-cli:claude-opus-4.6 cargo-vet" +aggregated-from = "https://raw.githubusercontent.com/OpenDevicePartnership/embedded-services/refs/heads/main/supply-chain/audits.toml" + +[[audits.OpenDevicePartnership.audits.valuable]] +who = "Jerry Xie " +criteria = "safe-to-deploy" +version = "0.1.1" +notes = "No unsafe code; build.rs only sets target atomic cfg via env; no fs/net/process capability use observed; behavior matches value-inspection purpose. Assisted-by: copilot-cli:GPT-5.3-Codex cargo-vet" +aggregated-from = "https://raw.githubusercontent.com/OpenDevicePartnership/embedded-services/refs/heads/main/supply-chain/audits.toml" + [[audits.OpenDevicePartnership.audits.vcell]] who = "Felipe Balbi " criteria = "safe-to-deploy" version = "0.1.3" aggregated-from = "https://raw.githubusercontent.com/OpenDevicePartnership/mcxa-pac/refs/heads/main/supply-chain/audits.toml" +[[audits.OpenDevicePartnership.audits.version_check]] +who = "Jerry Xie " +criteria = "safe-to-deploy" +delta = "0.9.4 -> 0.9.5" +notes = "Delta 0.9.4->0.9.5: documentation-only changes (added feature detection guidance, doc cross-references) and Cargo.toml normalization. No code changes, no unsafe, no new imports. Assisted-by: copilot-cli:claude-opus-4.6 cargo-vet" +aggregated-from = "https://raw.githubusercontent.com/OpenDevicePartnership/embedded-services/refs/heads/main/supply-chain/audits.toml" + +[[audits.OpenDevicePartnership.audits.virtue]] +who = "Jerry Xie " +criteria = "safe-to-deploy" +version = "0.0.18" +notes = "Proc-macro derive helper library. No unsafe code, no build script. Uses std::fs/std::env only in opt-in export_to_file() debug helper scoped to target/ dir. Assisted-by: copilot-cli:claude-opus-4.6 cargo-vet" +aggregated-from = "https://raw.githubusercontent.com/OpenDevicePartnership/embedded-services/refs/heads/main/supply-chain/audits.toml" + [[audits.OpenDevicePartnership.audits.volatile-register]] who = "Felipe Balbi " criteria = "safe-to-deploy" version = "0.2.2" aggregated-from = "https://raw.githubusercontent.com/OpenDevicePartnership/mcxa-pac/refs/heads/main/supply-chain/audits.toml" +[[audits.OpenDevicePartnership.audits.wasi]] +who = "Jerry Xie " +criteria = "safe-to-run" +version = "0.11.1+wasi-snapshot-preview1" +notes = "Auto-generated WASI snapshot-preview1 bindings from Bytecode Alliance. no_std, no build script, zero runtime deps. Unsafe limited to FFI wrappers for WASI host calls and unreachable_unchecked in exhaustive enum match arms. Assisted-by: copilot-chat:claude-opus-4.6 cargo-vet" +aggregated-from = "https://raw.githubusercontent.com/OpenDevicePartnership/embedded-services/refs/heads/main/supply-chain/audits.toml" + +[[audits.OpenDevicePartnership.audits.windows-sys]] +who = "Felipe Balbi " +criteria = "safe-to-run" +version = "0.61.2" +aggregated-from = "https://raw.githubusercontent.com/OpenDevicePartnership/embedded-mcu/refs/heads/main/supply-chain/audits.toml" + +[[audits.OpenDevicePartnership.audits.winnow]] +who = "Jerry Xie " +criteria = "safe-to-deploy" +delta = "0.7.10 -> 0.7.13" +notes = "Delta adds Accumulate impls (Cow str, String, VecDeque), fixes macro PartialEq/PartialOrd, optimizes str::next_token, adds tests, improves docs. No unsafe changes, no build script, no new powerful imports. Assisted-by: copilot-cli:claude-opus-4.6 cargo-vet" +aggregated-from = "https://raw.githubusercontent.com/OpenDevicePartnership/embedded-services/refs/heads/main/supply-chain/audits.toml" + [[audits.bytecode-alliance.audits.adler2]] who = "Alex Crichton " criteria = "safe-to-deploy" @@ -480,6 +878,11 @@ who = "Pat Hickey " criteria = "safe-to-deploy" delta = "0.3.28 -> 0.3.31" +[[audits.bytecode-alliance.audits.futures-macro]] +who = "Joel Dice " +criteria = "safe-to-deploy" +version = "0.3.31" + [[audits.bytecode-alliance.audits.futures-sink]] who = "Pat Hickey " criteria = "safe-to-deploy" @@ -525,11 +928,6 @@ Lots of new iterators and shuffling some things around. Some new unsafe code but it's well-documented and well-tested. Nothing suspicious. """ -[[audits.bytecode-alliance.audits.itoa]] -who = "Dan Gohman " -criteria = "safe-to-deploy" -delta = "1.0.11 -> 1.0.14" - [[audits.bytecode-alliance.audits.log]] who = "Alex Crichton " criteria = "safe-to-deploy" @@ -583,21 +981,10 @@ criteria = "safe-to-deploy" version = "0.46.0" notes = "one use of unsafe to call windows specific api to get console handle." -[[audits.bytecode-alliance.audits.percent-encoding]] -who = "Alex Crichton " -criteria = "safe-to-deploy" -version = "2.2.0" -notes = """ -This crate is a single-file crate that does what it says on the tin. There are -a few `unsafe` blocks related to utf-8 validation which are locally verifiable -as correct and otherwise this crate is good to go. -""" - -[[audits.bytecode-alliance.audits.semver]] +[[audits.bytecode-alliance.audits.pin-utils]] who = "Pat Hickey " criteria = "safe-to-deploy" -version = "1.0.17" -notes = "plenty of unsafe pointer and vec tricks, but in well-structured and commented code that appears to be correct" +version = "0.1.0" [[audits.bytecode-alliance.audits.sharded-slab]] who = "Pat Hickey " @@ -637,6 +1024,45 @@ who = "Pat Hickey " criteria = "safe-to-deploy" version = "0.3.17" +[[audits.google.audits.anstyle]] +who = "Yu-An Wang " +criteria = "safe-to-run" +version = "1.0.4" +aggregated-from = "https://chromium.googlesource.com/chromiumos/third_party/rust_crates/+/refs/heads/main/cargo-vet/audits.toml?format=TEXT" + +[[audits.google.audits.anstyle]] +who = "Lukasz Anforowicz " +criteria = "safe-to-run" +delta = "1.0.4 -> 1.0.6" +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.anstyle]] +who = "danakj " +criteria = "safe-to-run" +delta = "1.0.6 -> 1.0.7" +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.anstyle]] +who = "Lukasz Anforowicz " +criteria = "safe-to-run" +delta = "1.0.7 -> 1.0.8" +notes = "Only Cargo.toml changes in the 1.0.7 => 1.0.8 delta." +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.anstyle]] +who = "Dustin J. Mitchell " +criteria = "safe-to-run" +delta = "1.0.8 -> 1.0.9" +notes = "No changes" +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + +[[audits.google.audits.anstyle]] +who = "Lukasz Anforowicz " +criteria = "safe-to-run" +delta = "1.0.9 -> 1.0.10" +notes = "Minor changes related to `write_str`." +aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" + [[audits.google.audits.autocfg]] who = "Manish Goregaokar " criteria = "safe-to-deploy" @@ -769,6 +1195,25 @@ delta = "1.0.1 -> 1.0.2" notes = "No changes to any .rs files or Rust code." aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" +[[audits.google.audits.futures-util]] +who = "George Burgess IV " +criteria = "safe-to-run" +version = "0.3.28" +notes = """ +There's a custom xorshift-based `random::shuffle` implementation in +src/async_await/random.rs. This is `doc(hidden)` and seems to exist just so +that `futures-macro::select` can be unbiased. Sicne xorshift is explicitly not +intended to be a cryptographically secure algorithm, it is not considered +crypto. +""" +aggregated-from = "https://chromium.googlesource.com/chromiumos/third_party/rust_crates/+/refs/heads/main/cargo-vet/audits.toml?format=TEXT" + +[[audits.google.audits.futures-util]] +who = "George Burgess IV " +criteria = "safe-to-run" +delta = "0.3.28 -> 0.3.31" +aggregated-from = "https://chromium.googlesource.com/chromiumos/third_party/rust_crates/+/refs/heads/main/cargo-vet/audits.toml?format=TEXT" + [[audits.google.audits.glob]] who = "George Burgess IV " criteria = "safe-to-deploy" @@ -1086,248 +1531,6 @@ delta = "0.4.0 -> 0.4.1" notes = "No unsafe, net or fs." aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" -[[audits.google.audits.semver]] -who = "Daniel Cheng " -criteria = "safe-to-run" -delta = "1.0.25 -> 1.0.26" -notes = "Only minor documentation updates." -aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" - -[[audits.google.audits.serde]] -who = "Lukasz Anforowicz " -criteria = "safe-to-deploy" -version = "1.0.197" -notes = """ -Grepped for `-i cipher`, `-i crypto`, `'\bfs\b'`, `'\bnet\b'`, `'\bunsafe\b'`. - -There were some hits for `net`, but they were related to serialization and -not actually opening any connections or anything like that. - -There were 2 hits of `unsafe` when grepping: -* In `fn as_str` in `impl Buf` -* In `fn serialize` in `impl Serialize for net::Ipv4Addr` - -Unsafe review comments can be found in https://crrev.com/c/5350573/2 (this -review also covered `serde_json_lenient`). - -Version 1.0.130 of the crate has been added to Chromium in -https://crrev.com/c/3265545. The CL description contains a link to a -(Google-internal, sorry) document with a mini security review. -""" -aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" - -[[audits.google.audits.serde]] -who = "Dustin J. Mitchell " -criteria = "safe-to-deploy" -delta = "1.0.197 -> 1.0.198" -aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" - -[[audits.google.audits.serde]] -who = "danakj " -criteria = "safe-to-deploy" -delta = "1.0.198 -> 1.0.201" -aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" - -[[audits.google.audits.serde]] -who = "Dustin J. Mitchell " -criteria = "safe-to-deploy" -delta = "1.0.201 -> 1.0.202" -notes = "Trivial changes" -aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" - -[[audits.google.audits.serde]] -who = "Lukasz Anforowicz " -criteria = "safe-to-deploy" -delta = "1.0.202 -> 1.0.203" -notes = "s/doc_cfg/docsrs/ + tuple_impls/tuple_impl_body-related changes" -aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" - -[[audits.google.audits.serde]] -who = "Adrian Taylor " -criteria = "safe-to-deploy" -delta = "1.0.203 -> 1.0.204" -aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" - -[[audits.google.audits.serde]] -who = "Lukasz Anforowicz " -criteria = "safe-to-deploy" -delta = "1.0.204 -> 1.0.207" -notes = "The small change in `src/private/ser.rs` should have no impact on `ub-risk-2`." -aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" - -[[audits.google.audits.serde]] -who = "Lukasz Anforowicz " -criteria = "safe-to-deploy" -delta = "1.0.207 -> 1.0.209" -notes = """ -The delta carries fairly small changes in `src/private/de.rs` and -`src/private/ser.rs` (see https://crrev.com/c/5812194/2..5). AFAICT the -delta has no impact on the `unsafe`, `from_utf8_unchecked`-related parts -of the crate (in `src/de/format.rs` and `src/ser/impls.rs`). -""" -aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" - -[[audits.google.audits.serde]] -who = "Adrian Taylor " -criteria = "safe-to-deploy" -delta = "1.0.209 -> 1.0.210" -notes = "Almost no new code - just feature rearrangement" -aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" - -[[audits.google.audits.serde]] -who = "Liza Burakova " -criteria = "safe-to-deploy" -delta = "1.0.210 -> 1.0.213" -aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" - -[[audits.google.audits.serde]] -who = "Dustin J. Mitchell " -criteria = "safe-to-deploy" -delta = "1.0.213 -> 1.0.214" -notes = "No unsafe, no crypto" -aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" - -[[audits.google.audits.serde]] -who = "Adrian Taylor " -criteria = "safe-to-deploy" -delta = "1.0.214 -> 1.0.215" -aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" - -[[audits.google.audits.serde]] -who = "Lukasz Anforowicz " -criteria = "safe-to-deploy" -delta = "1.0.215 -> 1.0.216" -notes = "The delta makes minor changes in `build.rs` - switching to the `?` syntax sugar." -aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" - -[[audits.google.audits.serde]] -who = "Dustin J. Mitchell " -criteria = "safe-to-deploy" -delta = "1.0.216 -> 1.0.217" -notes = "Minimal changes, nothing unsafe" -aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" - -[[audits.google.audits.serde]] -who = "Daniel Cheng " -criteria = "safe-to-deploy" -delta = "1.0.217 -> 1.0.218" -notes = "No changes outside comments and documentation." -aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" - -[[audits.google.audits.serde]] -who = "Lukasz Anforowicz " -criteria = "safe-to-deploy" -delta = "1.0.218 -> 1.0.219" -notes = "Just allowing `clippy::elidable_lifetime_names`." -aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" - -[[audits.google.audits.serde_derive]] -who = "Lukasz Anforowicz " -criteria = "safe-to-deploy" -version = "1.0.197" -notes = 'Grepped for "unsafe", "crypt", "cipher", "fs", "net" - there were no hits' -aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" - -[[audits.google.audits.serde_derive]] -who = "danakj " -criteria = "safe-to-deploy" -delta = "1.0.197 -> 1.0.201" -aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" - -[[audits.google.audits.serde_derive]] -who = "Dustin J. Mitchell " -criteria = "safe-to-deploy" -delta = "1.0.201 -> 1.0.202" -aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" - -[[audits.google.audits.serde_derive]] -who = "Lukasz Anforowicz " -criteria = "safe-to-deploy" -delta = "1.0.202 -> 1.0.203" -notes = 'Grepped for "unsafe", "crypt", "cipher", "fs", "net" - there were no hits' -aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" - -[[audits.google.audits.serde_derive]] -who = "Adrian Taylor " -criteria = "safe-to-deploy" -delta = "1.0.203 -> 1.0.204" -aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" - -[[audits.google.audits.serde_derive]] -who = "Lukasz Anforowicz " -criteria = "safe-to-deploy" -delta = "1.0.204 -> 1.0.207" -notes = 'Grepped for \"unsafe\", \"crypt\", \"cipher\", \"fs\", \"net\" - there were no hits' -aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" - -[[audits.google.audits.serde_derive]] -who = "Lukasz Anforowicz " -criteria = "safe-to-deploy" -delta = "1.0.207 -> 1.0.209" -notes = ''' -There are no code changes in this delta - see https://crrev.com/c/5812194/2..5 - -I've neverthless also grepped for `-i cipher`, `-i crypto`, `\bfs\b`, -`\bnet\b`, and `\bunsafe\b`. There were no hits. -''' -aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" - -[[audits.google.audits.serde_derive]] -who = "Adrian Taylor " -criteria = "safe-to-deploy" -delta = "1.0.209 -> 1.0.210" -notes = "Almost no new code - just feature rearrangement" -aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" - -[[audits.google.audits.serde_derive]] -who = "Liza Burakova " -criteria = "safe-to-deploy" -delta = "1.0.210 -> 1.0.213" -notes = "Grepped for 'unsafe', 'crypt', 'cipher', 'fs', 'net' - there were no hits" -aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" - -[[audits.google.audits.serde_derive]] -who = "Dustin J. Mitchell " -criteria = "safe-to-deploy" -delta = "1.0.213 -> 1.0.214" -notes = "No changes to unsafe, no crypto" -aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" - -[[audits.google.audits.serde_derive]] -who = "Adrian Taylor " -criteria = "safe-to-deploy" -delta = "1.0.214 -> 1.0.215" -notes = "Minor changes should not impact UB risk" -aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" - -[[audits.google.audits.serde_derive]] -who = "Lukasz Anforowicz " -criteria = "safe-to-deploy" -delta = "1.0.215 -> 1.0.216" -notes = "The delta adds `#[automatically_derived]` in a few places. Still no `unsafe`." -aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" - -[[audits.google.audits.serde_derive]] -who = "Dustin J. Mitchell " -criteria = "safe-to-deploy" -delta = "1.0.216 -> 1.0.217" -notes = "No changes" -aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" - -[[audits.google.audits.serde_derive]] -who = "Daniel Cheng " -criteria = "safe-to-deploy" -delta = "1.0.217 -> 1.0.218" -notes = "No changes outside comments and documentation." -aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" - -[[audits.google.audits.serde_derive]] -who = "Lukasz Anforowicz " -criteria = "safe-to-deploy" -delta = "1.0.218 -> 1.0.219" -notes = "Minor changes (clippy tweaks, using `mem::take` instead of `mem::replace`)." -aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" - [[audits.google.audits.slab]] who = "Android Legacy" criteria = "safe-to-run" @@ -1391,6 +1594,12 @@ delta = "1.0.16 -> 1.0.18" notes = "Only minor comment and documentation updates." aggregated-from = "https://chromium.googlesource.com/chromium/src/+/main/third_party/rust/chromium_crates_io/supply-chain/audits.toml?format=TEXT" +[[audits.google.audits.utf8parse]] +who = "Ying Hsu " +criteria = "safe-to-run" +version = "0.2.1" +aggregated-from = "https://chromium.googlesource.com/chromiumos/third_party/rust_crates/+/refs/heads/main/cargo-vet/audits.toml?format=TEXT" + [[audits.google.audits.version_check]] who = "George Burgess IV " criteria = "safe-to-deploy" @@ -1403,33 +1612,6 @@ criteria = "safe-to-deploy" version = "1.0.2" aggregated-from = "https://chromium.googlesource.com/chromiumos/third_party/rust_crates/+/refs/heads/main/cargo-vet/audits.toml?format=TEXT" -[[audits.mozilla.wildcard-audits.encoding_rs]] -who = "Henri Sivonen " -criteria = "safe-to-deploy" -user-id = 4484 -start = "2019-02-26" -end = "2025-10-23" -notes = "I, Henri Sivonen, wrote encoding_rs for Gecko and have reviewed contributions by others. There are two caveats to the certification: 1) The crate does things that are documented to be UB but that do not appear to actually be UB due to integer types differing from the general rule; https://github.com/hsivonen/encoding_rs/issues/79 . 2) It would be prudent to re-review the code that reinterprets buffers of integers as SIMD vectors; see https://github.com/hsivonen/encoding_rs/issues/87 ." -aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" - -[[audits.mozilla.wildcard-audits.unicode-segmentation]] -who = "Manish Goregaokar " -criteria = "safe-to-deploy" -user-id = 1139 -start = "2019-05-15" -end = "2026-02-01" -notes = "All code written or reviewed by Manish" -aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" - -[[audits.mozilla.wildcard-audits.unicode-width]] -who = "Manish Goregaokar " -criteria = "safe-to-deploy" -user-id = 1139 -start = "2019-12-05" -end = "2026-02-01" -notes = "All code written or reviewed by Manish" -aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" - [[audits.mozilla.audits.adler2]] who = "Erich Gubler " criteria = "safe-to-deploy" @@ -1442,67 +1624,6 @@ criteria = "safe-to-deploy" version = "0.5.1" aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" -[[audits.mozilla.audits.askama]] -who = "Ben Dean-Kawamura " -criteria = "safe-to-deploy" -version = "0.13.1" -notes = """ -Template crate. This is only used to generate the Rust/JS code for UniFFI. - -We used to use askama, then we switched to rinja which was a fork. Now rinja and -askama have merged again. - -The differences from askama 0.12, are pretty straightforward and don't seem risky to me. There's -some unsafe code and macros, but nothing that complicated. -""" -aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" - -[[audits.mozilla.audits.askama]] -who = "Jan-Erik Rediger " -criteria = "safe-to-deploy" -delta = "0.13.1 -> 0.14.0" -aggregated-from = "https://raw.githubusercontent.com/mozilla/glean/main/supply-chain/audits.toml" - -[[audits.mozilla.audits.askama_derive]] -who = "Ben Dean-Kawamura " -criteria = "safe-to-deploy" -version = "0.13.1" -notes = """ -Template crate. This is only used to generate the Rust/JS code for UniFFI. - -We used to use askama, then we switched to rinja which was a fork. Now rinja and -askama have merged again. - -I did a quick scan of the current code and couldn't find any issues. -""" -aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" - -[[audits.mozilla.audits.askama_derive]] -who = "Jan-Erik Rediger " -criteria = "safe-to-deploy" -delta = "0.13.1 -> 0.14.0" -aggregated-from = "https://raw.githubusercontent.com/mozilla/glean/main/supply-chain/audits.toml" - -[[audits.mozilla.audits.askama_parser]] -who = "Ben Dean-Kawamura " -criteria = "safe-to-deploy" -version = "0.13.0" -notes = """ -Template crate. This is only used to generate the Rust/JS code for UniFFI. - -We used to use askama, then we switched to rinja which was a fork. Now rinja and -askama have merged again. - -I did a quick scan of the current code and couldn't find any issues. -""" -aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" - -[[audits.mozilla.audits.askama_parser]] -who = "Jan-Erik Rediger " -criteria = "safe-to-deploy" -delta = "0.13.0 -> 0.14.0" -aggregated-from = "https://raw.githubusercontent.com/mozilla/glean/main/supply-chain/audits.toml" - [[audits.mozilla.audits.bitflags]] who = "Alex Franchuk " criteria = "safe-to-deploy" @@ -1609,25 +1730,6 @@ criteria = "safe-to-deploy" delta = "1.8.3 -> 2.5.0" aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" -[[audits.mozilla.audits.hashbrown]] -who = "Mike Hommey " -criteria = "safe-to-deploy" -version = "0.12.3" -notes = "This version is used in rust's libstd, so effectively we're already trusting it" -aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" - -[[audits.mozilla.audits.hashbrown]] -who = "Erich Gubler " -criteria = "safe-to-deploy" -delta = "0.15.2 -> 0.15.5" -aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" - -[[audits.mozilla.audits.indexmap]] -who = "Erich Gubler " -criteria = "safe-to-deploy" -delta = "2.8.0 -> 2.11.4" -aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" - [[audits.mozilla.audits.litrs]] who = "Erich Gubler " criteria = "safe-to-deploy" @@ -1640,27 +1742,6 @@ criteria = "safe-to-deploy" delta = "0.8.8 -> 1.0.1" aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" -[[audits.mozilla.audits.num]] -who = "Josh Stone " -criteria = "safe-to-deploy" -version = "0.4.0" -notes = "All code written or reviewed by Josh Stone." -aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" - -[[audits.mozilla.audits.num-bigint]] -who = "Josh Stone " -criteria = "safe-to-deploy" -version = "0.4.3" -notes = "All code written or reviewed by Josh Stone." -aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" - -[[audits.mozilla.audits.num-complex]] -who = "Josh Stone " -criteria = "safe-to-deploy" -version = "0.4.2" -notes = "All code written or reviewed by Josh Stone." -aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" - [[audits.mozilla.audits.once_cell]] who = "Erich Gubler " criteria = "safe-to-deploy" @@ -1686,24 +1767,6 @@ criteria = "safe-to-deploy" delta = "1.21.1 -> 1.21.3" aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" -[[audits.mozilla.audits.percent-encoding]] -who = "Valentin Gosu " -criteria = "safe-to-deploy" -delta = "2.2.0 -> 2.3.0" -aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" - -[[audits.mozilla.audits.percent-encoding]] -who = "Valentin Gosu " -criteria = "safe-to-deploy" -delta = "2.3.0 -> 2.3.1" -aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" - -[[audits.mozilla.audits.percent-encoding]] -who = "edgul " -criteria = "safe-to-deploy" -delta = "2.3.1 -> 2.3.2" -aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" - [[audits.mozilla.audits.pin-project-lite]] who = "Mike Hommey " criteria = "safe-to-deploy" @@ -1720,38 +1783,16 @@ Only functional change is to work around a bug in the negative_impls feature """ aggregated-from = "https://raw.githubusercontent.com/mozilla/cargo-vet/main/supply-chain/audits.toml" -[[audits.mozilla.audits.radium]] -who = "Nika Layzell " -criteria = "safe-to-deploy" -version = "0.5.3" -notes = """ -I am no longer the primary maintainer of `radium`, however I have audited the -code to ensure it is still correct. The implementation contains no `unsafe` -logic, and will not abstract away `Sync` trait bounds. - -The core logic is very simple, and acts as an abstraction trait for `Cell` -and `AtomicT`. -""" -aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" - -[[audits.mozilla.audits.rustc-hash]] -who = "Bobby Holley " -criteria = "safe-to-deploy" -version = "1.1.0" -notes = "Straightforward crate with no unsafe code, does what it says on the tin." -aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" - -[[audits.mozilla.audits.rustc-hash]] -who = "Ben Dean-Kawamura " +[[audits.mozilla.audits.serde_core]] +who = "Erich Gubler " criteria = "safe-to-deploy" -delta = "1.1.0 -> 2.1.1" -notes = "Simple hashing crate, no unsafe code." +delta = "1.0.226 -> 1.0.227" aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" -[[audits.mozilla.audits.semver]] +[[audits.mozilla.audits.serde_core]] who = "Jan-Erik Rediger " criteria = "safe-to-deploy" -delta = "1.0.17 -> 1.0.25" +delta = "1.0.227 -> 1.0.228" aggregated-from = "https://raw.githubusercontent.com/mozilla/glean/main/supply-chain/audits.toml" [[audits.mozilla.audits.sharded-slab]] @@ -1831,6 +1872,12 @@ criteria = "safe-to-deploy" delta = "0.3.19 -> 0.3.20" aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" +[[audits.mozilla.audits.utf8parse]] +who = "Nika Layzell " +criteria = "safe-to-deploy" +delta = "0.2.1 -> 0.2.2" +aggregated-from = "https://raw.githubusercontent.com/mozilla/cargo-vet/main/supply-chain/audits.toml" + [[audits.mozilla.audits.yaml-rust2]] who = "Lars Eggert " criteria = "safe-to-deploy" diff --git a/thermal-service-interface/Cargo.toml b/thermal-service-interface/Cargo.toml new file mode 100644 index 000000000..f3ff7885a --- /dev/null +++ b/thermal-service-interface/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "thermal-service-interface" +version.workspace = true +edition.workspace = true +license.workspace = true +repository.workspace = true + +[lints] +workspace = true + +[dependencies] +defmt = { workspace = true, optional = true } +embassy-time.workspace = true +embedded-fans-async = "0.2.0" +embedded-sensors-hal-async = "0.3.0" + +[features] +defmt = [ + "dep:defmt", + "embassy-time/defmt", + "embedded-fans-async/defmt", + "embedded-sensors-hal-async/defmt", +] diff --git a/thermal-service-interface/src/fan.rs b/thermal-service-interface/src/fan.rs new file mode 100644 index 000000000..86a23a263 --- /dev/null +++ b/thermal-service-interface/src/fan.rs @@ -0,0 +1,133 @@ +use core::future::Future; +use embassy_time::Duration; +use embedded_fans_async::{Fan, RpmSense}; +use embedded_sensors_hal_async::temperature::DegreesCelsius; + +/// Ensures all necessary traits are implemented for the underlying fan driver. +pub trait Driver: Fan + RpmSense {} + +/// Fan error. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +#[non_exhaustive] +pub enum Error { + /// Fan encountered a hardware failure. + Hardware, +} + +/// Fan event. +#[derive(Debug, PartialEq, Clone, Copy)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +#[non_exhaustive] +pub enum Event { + /// Fan encountered a failure. + Failure(Error), +} + +/// Fan on (running) state. +#[derive(Debug, Clone, Copy, PartialEq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum OnState { + /// Fan is on and running at its minimum speed. + Min, + /// Fan is ramping up or down along a curve in response to a temperature change. + Ramping, + /// Fan is running at its maximum speed. + Max, +} + +/// Fan state. +#[derive(Debug, Clone, Copy, PartialEq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum State { + /// Fan is off. + Off, + /// Fan is on in the specified [`OnState`]. + On(OnState), +} + +/// Fan service interface trait. +pub trait FanService { + /// Enable automatic fan control. + /// + /// This allows the fan to automatically change [`State`] based on periodic readings from an associated temperature sensor. + fn enable_auto_control(&self) -> impl Future>; + /// Returns the most recently sampled RPM measurement. + fn rpm(&self) -> impl Future; + /// Returns the minimum RPM supported by the fan. + fn min_rpm(&self) -> impl Future; + /// Returns the maximum RPM supported by the fan. + fn max_rpm(&self) -> impl Future; + /// Returns the average RPM over a sampling period. + fn rpm_average(&self) -> impl Future; + /// Immediately samples the fan for an RPM measurement and returns the result. + fn rpm_immediate(&self) -> impl Future>; + /// Sets the fan to run at the specified RPM (and disables automatic control). + fn set_rpm(&self, rpm: u16) -> impl Future>; + /// Sets the fan to run at the specified duty cycle percentage (and disables automatic control). + fn set_duty_percent(&self, duty: u8) -> impl Future>; + /// Stops the fan (and disables automatic control). + fn stop(&self) -> impl Future>; + /// Set the rate at which RPM measurements are sampled. + fn set_rpm_sampling_period(&self, period: Duration) -> impl Future; + /// Set the rate at which the fan will update its RPM in response to a temperature change when in automatic control mode. + fn set_rpm_update_period(&self, period: Duration) -> impl Future; + /// Returns the temperature at which the fan will change to the specified [`OnState`] when in automatic control mode. + fn state_temp(&self, state: OnState) -> impl Future; + /// Sets the temperature at which the fan will change to the specified [`OnState`] when in automatic control mode. + fn set_state_temp(&self, state: OnState, temp: DegreesCelsius) -> impl Future; +} + +impl FanService for &T { + fn enable_auto_control(&self) -> impl Future> { + T::enable_auto_control(self) + } + + fn rpm(&self) -> impl Future { + T::rpm(self) + } + + fn min_rpm(&self) -> impl Future { + T::min_rpm(self) + } + + fn max_rpm(&self) -> impl Future { + T::max_rpm(self) + } + + fn rpm_average(&self) -> impl Future { + T::rpm_average(self) + } + + fn rpm_immediate(&self) -> impl Future> { + T::rpm_immediate(self) + } + + fn set_rpm(&self, rpm: u16) -> impl Future> { + T::set_rpm(self, rpm) + } + + fn set_duty_percent(&self, duty: u8) -> impl Future> { + T::set_duty_percent(self, duty) + } + + fn stop(&self) -> impl Future> { + T::stop(self) + } + + fn set_rpm_sampling_period(&self, period: Duration) -> impl Future { + T::set_rpm_sampling_period(self, period) + } + + fn set_rpm_update_period(&self, period: Duration) -> impl Future { + T::set_rpm_update_period(self, period) + } + + fn state_temp(&self, state: OnState) -> impl Future { + T::state_temp(self, state) + } + + fn set_state_temp(&self, state: OnState, temp: DegreesCelsius) -> impl Future { + T::set_state_temp(self, state, temp) + } +} diff --git a/thermal-service-interface/src/lib.rs b/thermal-service-interface/src/lib.rs new file mode 100644 index 000000000..b1e56a100 --- /dev/null +++ b/thermal-service-interface/src/lib.rs @@ -0,0 +1,17 @@ +#![no_std] + +pub mod fan; +pub mod sensor; + +/// Thermal service interface trait. +pub trait ThermalService { + /// Associated type for registered sensor services. + type Sensor: sensor::SensorService; + /// Associated type for registered fan services. + type Fan: fan::FanService; + + /// Retrieve a handle to the sensor service with the specified instance ID, if it exists. + fn sensor(&self, id: u8) -> Option; + /// Retrieve a handle to the fan service with the specified instance ID, if it exists. + fn fan(&self, id: u8) -> Option; +} diff --git a/thermal-service-interface/src/sensor.rs b/thermal-service-interface/src/sensor.rs new file mode 100644 index 000000000..70769e1bb --- /dev/null +++ b/thermal-service-interface/src/sensor.rs @@ -0,0 +1,98 @@ +use core::future::Future; +use embassy_time::Duration; +use embedded_sensors_hal_async::temperature::{DegreesCelsius, TemperatureSensor}; + +/// Ensures all necessary traits are implemented for the underlying sensor driver. +pub trait Driver: TemperatureSensor {} + +/// Sensor error. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +#[non_exhaustive] +pub enum Error { + /// Sensor encountered a hardware failure. + Hardware, + /// Retry attempts to communicate with sensor exhausted. + RetryExhausted, +} + +/// Sensor event. +#[derive(Debug, PartialEq, Clone, Copy)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +#[non_exhaustive] +pub enum Event { + /// A sensor threshold was exceeded. + ThresholdExceeded(Threshold), + /// A sensor threshold which was previously exceeded is now cleared. + ThresholdCleared(Threshold), + /// Sensor encountered a failure. + Failure(Error), +} + +/// Sensor threshold types. +#[derive(Debug, PartialEq, Clone, Copy)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum Threshold { + /// The temperature threshold below which a warning event is generated. + WarnLow, + /// The temperature threshold above which a warning event is generated. + WarnHigh, + /// The temperature threshold above which a prochot event is generated. + Prochot, + /// The temperature threshold above which a critical event is generated. + Critical, +} + +/// Sensor service interface trait +pub trait SensorService { + /// Returns the most recently sampled temperature measurement in degrees Celsius. + fn temperature(&self) -> impl Future; + /// Returns the average temperature over a sampling period in degrees Celsius. + fn temperature_average(&self) -> impl Future; + /// Immediately samples the sensor for a temperature measurement and returns the result in degrees Celsius. + fn temperature_immediate(&self) -> impl Future>; + /// Sets the temperature for which a sensor event will be generated when the threshold is exceeded, in degrees Celsius. + fn set_threshold(&self, threshold: Threshold, value: DegreesCelsius) -> impl Future; + /// Returns the temperature threshold value for the specified threshold type in degrees Celsius. + fn threshold(&self, threshold: Threshold) -> impl Future; + /// Sets the rate at which temperature measurements are sampled. + fn set_sample_period(&self, period: Duration) -> impl Future; + /// Enable periodic temperature sampling. + fn enable_sampling(&self) -> impl Future; + /// Disable periodic temperature sampling. + fn disable_sampling(&self) -> impl Future; +} + +impl SensorService for &T { + async fn temperature(&self) -> DegreesCelsius { + T::temperature(self).await + } + + async fn temperature_average(&self) -> DegreesCelsius { + T::temperature_average(self).await + } + + async fn temperature_immediate(&self) -> Result { + T::temperature_immediate(self).await + } + + async fn set_threshold(&self, threshold: Threshold, value: DegreesCelsius) { + T::set_threshold(self, threshold, value).await + } + + async fn threshold(&self, threshold: Threshold) -> DegreesCelsius { + T::threshold(self, threshold).await + } + + async fn set_sample_period(&self, period: Duration) { + T::set_sample_period(self, period).await + } + + async fn enable_sampling(&self) { + T::enable_sampling(self).await + } + + async fn disable_sampling(&self) { + T::disable_sampling(self).await + } +} diff --git a/thermal-service-relay/Cargo.toml b/thermal-service-relay/Cargo.toml new file mode 100644 index 000000000..43f7c1dbd --- /dev/null +++ b/thermal-service-relay/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "thermal-service-relay" +version.workspace = true +edition.workspace = true +license.workspace = true +repository.workspace = true + +[dependencies] +defmt = { workspace = true, optional = true } +embedded-services.workspace = true +thermal-service-interface.workspace = true +num_enum.workspace = true +uuid.workspace = true + +[lints] +workspace = true + +[features] +defmt = ["dep:defmt"] diff --git a/thermal-service-relay/src/lib.rs b/thermal-service-relay/src/lib.rs new file mode 100644 index 000000000..fceae864d --- /dev/null +++ b/thermal-service-relay/src/lib.rs @@ -0,0 +1,223 @@ +#![no_std] + +mod serialization; + +pub use serialization::{ThermalError, ThermalRequest, ThermalResponse, ThermalResult}; +use thermal_service_interface::ThermalService; +use thermal_service_interface::fan::{self, FanService}; +use thermal_service_interface::sensor::{self, SensorService}; + +/// DeciKelvin temperature representation. +/// +/// This exists because the host to EC interface expects DeciKelvin, +/// though internally we still use Celsius for ease of use. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct DeciKelvin(pub u32); + +impl DeciKelvin { + /// Convert from degrees Celsius to DeciKelvin. + pub const fn from_celsius(c: f32) -> Self { + Self(((c + 273.15) * 10.0) as u32) + } + + /// Convert from DeciKelvin to degrees Celsius. + pub const fn to_celsius(self) -> f32 { + (self.0 as f32 / 10.0) - 273.15 + } +} + +/// MPTF Standard UUIDs which the thermal service understands. +pub mod uuid_standard { + /// The critical temperature threshold of a sensor. + pub const CRT_TEMP: uuid::Bytes = uuid::uuid!("218246e7-baf6-45f1-aa13-07e4845256b8").to_bytes_le(); + /// The prochot temperature threshold of a sensor. + pub const PROC_HOT_TEMP: uuid::Bytes = uuid::uuid!("22dc52d2-fd0b-47ab-95b8-26552f9831a5").to_bytes_le(); + /// The temperature threshold at which a fan should turn on and begin running at its minimum RPM. + pub const FAN_MIN_TEMP: uuid::Bytes = uuid::uuid!("ba17b567-c368-48d5-bc6f-a312a41583c1").to_bytes_le(); + /// The temperature threshold at which a fan should start ramping up. + pub const FAN_RAMP_TEMP: uuid::Bytes = uuid::uuid!("3a62688c-d95b-4d2d-bacc-90d7a5816bcd").to_bytes_le(); + /// The temperature threshold at which a fan should be at max speed. + pub const FAN_MAX_TEMP: uuid::Bytes = uuid::uuid!("dcb758b1-f0fd-4ec7-b2c0-ef1e2a547b76").to_bytes_le(); + /// The minimum RPM a fan is capable of running at reliably. + pub const FAN_MIN_RPM: uuid::Bytes = uuid::uuid!("db261c77-934b-45e2-9742-256c62badb7a").to_bytes_le(); + /// The maximum RPM a fan is capable of running at reliably. + pub const FAN_MAX_RPM: uuid::Bytes = uuid::uuid!("5cf839df-8be7-42b9-9ac5-3403ca2c8a6a").to_bytes_le(); + /// The current RPM of a fan. + pub const FAN_CURRENT_RPM: uuid::Bytes = uuid::uuid!("adf95492-0776-4ffc-84f3-b6c8b5269683").to_bytes_le(); +} + +/// Thermal service relay handler which wraps a thermal service instance. +pub struct ThermalServiceRelayHandler { + service: T, +} + +impl ThermalServiceRelayHandler { + /// Create a new thermal service relay handler. + pub fn new(service: T) -> Self { + Self { service } + } + + async fn sensor_get_tmp(&self, instance_id: u8) -> ThermalResult { + let sensor = self.service.sensor(instance_id).ok_or(ThermalError::InvalidParameter)?; + let temp = sensor.temperature().await; + Ok(ThermalResponse::ThermalGetTmpResponse { + temperature: DeciKelvin::from_celsius(temp), + }) + } + + async fn sensor_set_warn_thrs( + &self, + instance_id: u8, + _timeout: u32, + low: DeciKelvin, + high: DeciKelvin, + ) -> ThermalResult { + let sensor = self.service.sensor(instance_id).ok_or(ThermalError::InvalidParameter)?; + sensor.set_threshold(sensor::Threshold::WarnLow, low.to_celsius()).await; + sensor + .set_threshold(sensor::Threshold::WarnHigh, high.to_celsius()) + .await; + Ok(ThermalResponse::ThermalSetThrsResponse) + } + + async fn get_var_handler(&self, instance_id: u8, var_uuid: uuid::Bytes) -> ThermalResult { + match var_uuid { + uuid_standard::CRT_TEMP => self.sensor_get_thrs(instance_id, sensor::Threshold::Critical).await, + uuid_standard::PROC_HOT_TEMP => self.sensor_get_thrs(instance_id, sensor::Threshold::Prochot).await, + uuid_standard::FAN_MIN_TEMP => self.fan_get_state_temp(instance_id, fan::OnState::Min).await, + uuid_standard::FAN_RAMP_TEMP => self.fan_get_state_temp(instance_id, fan::OnState::Ramping).await, + uuid_standard::FAN_MAX_TEMP => self.fan_get_state_temp(instance_id, fan::OnState::Max).await, + uuid_standard::FAN_MIN_RPM => self.fan_get_min_rpm(instance_id).await, + uuid_standard::FAN_MAX_RPM => self.fan_get_max_rpm(instance_id).await, + uuid_standard::FAN_CURRENT_RPM => self.fan_get_rpm(instance_id).await, + _ => Err(ThermalError::InvalidParameter), + } + } + + async fn set_var_handler(&self, instance_id: u8, var_uuid: uuid::Bytes, set_var: u32) -> ThermalResult { + match var_uuid { + uuid_standard::CRT_TEMP => { + self.sensor_set_thrs(instance_id, sensor::Threshold::Critical, set_var) + .await + } + uuid_standard::PROC_HOT_TEMP => { + self.sensor_set_thrs(instance_id, sensor::Threshold::Prochot, set_var) + .await + } + uuid_standard::FAN_MIN_TEMP => { + self.fan_set_state_temp(instance_id, fan::OnState::Min, DeciKelvin(set_var)) + .await + } + uuid_standard::FAN_RAMP_TEMP => { + self.fan_set_state_temp(instance_id, fan::OnState::Ramping, DeciKelvin(set_var)) + .await + } + uuid_standard::FAN_MAX_TEMP => { + self.fan_set_state_temp(instance_id, fan::OnState::Max, DeciKelvin(set_var)) + .await + } + uuid_standard::FAN_CURRENT_RPM => { + let rpm = u16::try_from(set_var).map_err(|_| ThermalError::InvalidParameter)?; + self.fan_set_rpm(instance_id, rpm).await + } + _ => Err(ThermalError::InvalidParameter), + } + } + + async fn fan_get_state_temp(&self, instance_id: u8, state: fan::OnState) -> ThermalResult { + let fan = self.service.fan(instance_id).ok_or(ThermalError::InvalidParameter)?; + let temp = fan.state_temp(state).await; + Ok(ThermalResponse::ThermalGetVarResponse { + val: DeciKelvin::from_celsius(temp).0, + }) + } + + async fn fan_get_rpm(&self, instance_id: u8) -> ThermalResult { + let fan = self.service.fan(instance_id).ok_or(ThermalError::InvalidParameter)?; + let rpm = fan.rpm().await; + Ok(ThermalResponse::ThermalGetVarResponse { val: rpm.into() }) + } + + async fn fan_get_min_rpm(&self, instance_id: u8) -> ThermalResult { + let fan = self.service.fan(instance_id).ok_or(ThermalError::InvalidParameter)?; + let rpm = fan.min_rpm().await; + Ok(ThermalResponse::ThermalGetVarResponse { val: rpm.into() }) + } + + async fn fan_get_max_rpm(&self, instance_id: u8) -> ThermalResult { + let fan = self.service.fan(instance_id).ok_or(ThermalError::InvalidParameter)?; + let rpm = fan.max_rpm().await; + Ok(ThermalResponse::ThermalGetVarResponse { val: rpm.into() }) + } + + async fn sensor_set_thrs(&self, instance_id: u8, threshold: sensor::Threshold, threshold_dk: u32) -> ThermalResult { + let sensor = self.service.sensor(instance_id).ok_or(ThermalError::InvalidParameter)?; + sensor + .set_threshold(threshold, DeciKelvin(threshold_dk).to_celsius()) + .await; + Ok(ThermalResponse::ThermalSetVarResponse) + } + + async fn sensor_get_thrs(&self, instance_id: u8, threshold: sensor::Threshold) -> ThermalResult { + let sensor = self.service.sensor(instance_id).ok_or(ThermalError::InvalidParameter)?; + let temp = sensor.threshold(threshold).await; + Ok(ThermalResponse::ThermalGetVarResponse { + val: DeciKelvin::from_celsius(temp).0, + }) + } + + async fn sensor_get_warn_thrs(&self, instance_id: u8) -> ThermalResult { + let sensor = self.service.sensor(instance_id).ok_or(ThermalError::InvalidParameter)?; + let low = sensor.threshold(sensor::Threshold::WarnLow).await; + let high = sensor.threshold(sensor::Threshold::WarnHigh).await; + Ok(ThermalResponse::ThermalGetThrsResponse { + timeout: 0, + low: DeciKelvin::from_celsius(low), + high: DeciKelvin::from_celsius(high), + }) + } + + async fn fan_set_state_temp(&self, instance_id: u8, state: fan::OnState, temp: DeciKelvin) -> ThermalResult { + let fan = self.service.fan(instance_id).ok_or(ThermalError::InvalidParameter)?; + fan.set_state_temp(state, temp.to_celsius()).await; + Ok(ThermalResponse::ThermalSetVarResponse) + } + + async fn fan_set_rpm(&self, instance_id: u8, rpm: u16) -> ThermalResult { + let fan = self.service.fan(instance_id).ok_or(ThermalError::InvalidParameter)?; + fan.set_rpm(rpm).await.map_err(|_| ThermalError::HardwareError)?; + Ok(ThermalResponse::ThermalSetVarResponse) + } +} + +impl embedded_services::relay::mctp::RelayServiceHandlerTypes for ThermalServiceRelayHandler { + type RequestType = ThermalRequest; + type ResultType = ThermalResult; +} + +impl embedded_services::relay::mctp::RelayServiceHandler for ThermalServiceRelayHandler { + async fn process_request(&self, request: Self::RequestType) -> Self::ResultType { + match request { + ThermalRequest::ThermalGetTmpRequest { instance_id } => self.sensor_get_tmp(instance_id).await, + ThermalRequest::ThermalSetThrsRequest { + instance_id, + timeout, + low, + high, + } => self.sensor_set_warn_thrs(instance_id, timeout, low, high).await, + ThermalRequest::ThermalGetThrsRequest { instance_id } => self.sensor_get_warn_thrs(instance_id).await, + // Revisit: Don't currently have a good strategy for handling this request + ThermalRequest::ThermalSetScpRequest { .. } => Err(ThermalError::InvalidParameter), + ThermalRequest::ThermalGetVarRequest { + instance_id, var_uuid, .. + } => self.get_var_handler(instance_id, var_uuid).await, + ThermalRequest::ThermalSetVarRequest { + instance_id, + var_uuid, + set_var, + .. + } => self.set_var_handler(instance_id, var_uuid, set_var).await, + } + } +} diff --git a/thermal-service-relay/src/serialization.rs b/thermal-service-relay/src/serialization.rs new file mode 100644 index 000000000..6b1a7a63d --- /dev/null +++ b/thermal-service-relay/src/serialization.rs @@ -0,0 +1,316 @@ +use crate::DeciKelvin; +use embedded_services::relay::{MessageSerializationError, SerializableMessage}; + +// Standard MPTF requests expected by the thermal subsystem +#[derive(num_enum::IntoPrimitive, num_enum::TryFromPrimitive, Copy, Clone, Debug, PartialEq)] +#[repr(u16)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +enum ThermalCmd { + /// EC_THM_GET_TMP = 0x1 + GetTmp = 1, + /// EC_THM_SET_THRS = 0x2 + SetThrs = 2, + /// EC_THM_GET_THRS = 0x3 + GetThrs = 3, + /// EC_THM_SET_SCP = 0x4 + SetScp = 4, + /// EC_THM_GET_VAR = 0x5 + GetVar = 5, + /// EC_THM_SET_VAR = 0x6 + SetVar = 6, +} + +impl From<&ThermalRequest> for ThermalCmd { + fn from(request: &ThermalRequest) -> Self { + match request { + ThermalRequest::ThermalGetTmpRequest { .. } => ThermalCmd::GetTmp, + ThermalRequest::ThermalSetThrsRequest { .. } => ThermalCmd::SetThrs, + ThermalRequest::ThermalGetThrsRequest { .. } => ThermalCmd::GetThrs, + ThermalRequest::ThermalSetScpRequest { .. } => ThermalCmd::SetScp, + ThermalRequest::ThermalGetVarRequest { .. } => ThermalCmd::GetVar, + ThermalRequest::ThermalSetVarRequest { .. } => ThermalCmd::SetVar, + } + } +} + +impl From<&ThermalResponse> for ThermalCmd { + fn from(response: &ThermalResponse) -> Self { + match response { + ThermalResponse::ThermalGetTmpResponse { .. } => ThermalCmd::GetTmp, + ThermalResponse::ThermalSetThrsResponse => ThermalCmd::SetThrs, + ThermalResponse::ThermalGetThrsResponse { .. } => ThermalCmd::GetThrs, + ThermalResponse::ThermalSetScpResponse => ThermalCmd::SetScp, + ThermalResponse::ThermalGetVarResponse { .. } => ThermalCmd::GetVar, + ThermalResponse::ThermalSetVarResponse => ThermalCmd::SetVar, + } + } +} + +#[derive(PartialEq, Clone, Copy)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum ThermalRequest { + ThermalGetTmpRequest { + instance_id: u8, + }, + ThermalSetThrsRequest { + instance_id: u8, + timeout: u32, + low: DeciKelvin, + high: DeciKelvin, + }, + ThermalGetThrsRequest { + instance_id: u8, + }, + ThermalSetScpRequest { + instance_id: u8, + policy_id: u32, + acoustic_lim: u32, + power_lim: u32, + }, + ThermalGetVarRequest { + instance_id: u8, + len: u16, + var_uuid: uuid::Bytes, + }, + ThermalSetVarRequest { + instance_id: u8, + len: u16, + var_uuid: uuid::Bytes, + set_var: u32, + }, +} + +impl SerializableMessage for ThermalRequest { + fn serialize(self, buffer: &mut [u8]) -> Result { + match self { + Self::ThermalGetTmpRequest { instance_id } => safe_put_u8(buffer, 0, instance_id), + Self::ThermalSetThrsRequest { + instance_id, + timeout, + low, + high, + } => Ok(safe_put_u8(buffer, 0, instance_id)? + + safe_put_dword(buffer, 1, timeout)? + + safe_put_dword(buffer, 5, low.0)? + + safe_put_dword(buffer, 9, high.0)?), + Self::ThermalGetThrsRequest { instance_id } => safe_put_u8(buffer, 0, instance_id), + Self::ThermalSetScpRequest { + instance_id, + policy_id, + acoustic_lim, + power_lim, + } => Ok(safe_put_u8(buffer, 0, instance_id)? + + safe_put_dword(buffer, 1, policy_id)? + + safe_put_dword(buffer, 5, acoustic_lim)? + + safe_put_dword(buffer, 9, power_lim)?), + Self::ThermalGetVarRequest { + instance_id, + len, + var_uuid, + } => Ok(safe_put_u8(buffer, 0, instance_id)? + + safe_put_u16(buffer, 1, len)? + + safe_put_uuid(buffer, 3, var_uuid)?), + Self::ThermalSetVarRequest { + instance_id, + len, + var_uuid, + set_var, + } => Ok(safe_put_u8(buffer, 0, instance_id)? + + safe_put_u16(buffer, 1, len)? + + safe_put_uuid(buffer, 3, var_uuid)? + + safe_put_dword(buffer, 19, set_var)?), + } + } + + fn deserialize(discriminant: u16, buffer: &[u8]) -> Result { + Ok( + match ThermalCmd::try_from(discriminant) + .map_err(|_| MessageSerializationError::UnknownMessageDiscriminant(discriminant))? + { + ThermalCmd::GetTmp => Self::ThermalGetTmpRequest { + instance_id: safe_get_u8(buffer, 0)?, + }, + ThermalCmd::SetThrs => Self::ThermalSetThrsRequest { + instance_id: safe_get_u8(buffer, 0)?, + timeout: safe_get_dword(buffer, 1)?, + low: DeciKelvin(safe_get_dword(buffer, 5)?), + high: DeciKelvin(safe_get_dword(buffer, 9)?), + }, + ThermalCmd::GetThrs => Self::ThermalGetThrsRequest { + instance_id: safe_get_u8(buffer, 0)?, + }, + ThermalCmd::SetScp => Self::ThermalSetScpRequest { + instance_id: safe_get_u8(buffer, 0)?, + policy_id: safe_get_dword(buffer, 1)?, + acoustic_lim: safe_get_dword(buffer, 5)?, + power_lim: safe_get_dword(buffer, 9)?, + }, + ThermalCmd::GetVar => Self::ThermalGetVarRequest { + instance_id: safe_get_u8(buffer, 0)?, + len: safe_get_u16(buffer, 1)?, + var_uuid: safe_get_uuid(buffer, 3)?, + }, + ThermalCmd::SetVar => Self::ThermalSetVarRequest { + instance_id: safe_get_u8(buffer, 0)?, + len: safe_get_u16(buffer, 1)?, + var_uuid: safe_get_uuid(buffer, 3)?, + set_var: safe_get_dword(buffer, 19)?, + }, + }, + ) + } + + fn discriminant(&self) -> u16 { + let cmd: ThermalCmd = self.into(); + cmd.into() + } +} + +#[derive(PartialEq, Clone, Copy, Debug)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum ThermalResponse { + ThermalGetTmpResponse { + temperature: DeciKelvin, + }, + ThermalSetThrsResponse, + ThermalGetThrsResponse { + timeout: u32, + low: DeciKelvin, + high: DeciKelvin, + }, + ThermalSetScpResponse, + ThermalGetVarResponse { + val: u32, + }, + ThermalSetVarResponse, +} + +impl SerializableMessage for ThermalResponse { + fn serialize(self, buffer: &mut [u8]) -> Result { + match self { + Self::ThermalGetTmpResponse { temperature } => safe_put_dword(buffer, 0, temperature.0), + Self::ThermalGetThrsResponse { timeout, low, high } => Ok(safe_put_dword(buffer, 0, timeout)? + + safe_put_dword(buffer, 4, low.0)? + + safe_put_dword(buffer, 8, high.0)?), + Self::ThermalGetVarResponse { val } => safe_put_dword(buffer, 0, val), + Self::ThermalSetVarResponse | Self::ThermalSetScpResponse | Self::ThermalSetThrsResponse => Ok(0), + } + } + + fn deserialize(discriminant: u16, buffer: &[u8]) -> Result { + Ok( + match ThermalCmd::try_from(discriminant) + .map_err(|_| MessageSerializationError::UnknownMessageDiscriminant(discriminant))? + { + ThermalCmd::GetTmp => Self::ThermalGetTmpResponse { + temperature: DeciKelvin(safe_get_dword(buffer, 0)?), + }, + ThermalCmd::SetThrs => Self::ThermalSetThrsResponse, + ThermalCmd::GetThrs => Self::ThermalGetThrsResponse { + timeout: safe_get_dword(buffer, 0)?, + low: DeciKelvin(safe_get_dword(buffer, 4)?), + high: DeciKelvin(safe_get_dword(buffer, 8)?), + }, + ThermalCmd::SetScp => Self::ThermalSetScpResponse, + ThermalCmd::GetVar => Self::ThermalGetVarResponse { + val: safe_get_dword(buffer, 0)?, + }, + ThermalCmd::SetVar => Self::ThermalSetVarResponse, + }, + ) + } + + fn discriminant(&self) -> u16 { + ThermalCmd::from(self).into() + } +} + +#[derive(num_enum::IntoPrimitive, num_enum::TryFromPrimitive, Copy, Clone, Debug, PartialEq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +#[repr(u16)] +pub enum ThermalError { + InvalidParameter = 1, + UnsupportedRevision = 2, + HardwareError = 3, +} + +impl SerializableMessage for ThermalError { + fn serialize(self, _buffer: &mut [u8]) -> Result { + match self { + Self::UnsupportedRevision | Self::InvalidParameter | Self::HardwareError => Ok(0), + } + } + + fn deserialize(discriminant: u16, _buffer: &[u8]) -> Result { + ThermalError::try_from(discriminant) + .map_err(|_| MessageSerializationError::UnknownMessageDiscriminant(discriminant)) + } + + fn discriminant(&self) -> u16 { + (*self).into() + } +} + +pub type ThermalResult = Result; + +fn safe_get_u8(buffer: &[u8], index: usize) -> Result { + buffer + .get(index) + .copied() + .ok_or(MessageSerializationError::BufferTooSmall) +} + +fn safe_get_u16(buffer: &[u8], index: usize) -> Result { + let bytes = buffer + .get(index..index + 2) + .ok_or(MessageSerializationError::BufferTooSmall)? + .try_into() + .map_err(|_| MessageSerializationError::BufferTooSmall)?; + Ok(u16::from_le_bytes(bytes)) +} + +fn safe_get_dword(buffer: &[u8], index: usize) -> Result { + let bytes = buffer + .get(index..index + 4) + .ok_or(MessageSerializationError::BufferTooSmall)? + .try_into() + .map_err(|_| MessageSerializationError::BufferTooSmall)?; + Ok(u32::from_le_bytes(bytes)) +} + +fn safe_get_uuid(buffer: &[u8], index: usize) -> Result { + buffer + .get(index..index + 16) + .ok_or(MessageSerializationError::BufferTooSmall)? + .try_into() + .map_err(|_| MessageSerializationError::BufferTooSmall) +} + +fn safe_put_u8(buffer: &mut [u8], index: usize, val: u8) -> Result { + *buffer.get_mut(index).ok_or(MessageSerializationError::BufferTooSmall)? = val; + Ok(1) +} + +fn safe_put_u16(buffer: &mut [u8], index: usize, val: u16) -> Result { + buffer + .get_mut(index..index + 2) + .ok_or(MessageSerializationError::BufferTooSmall)? + .copy_from_slice(&val.to_le_bytes()); + Ok(2) +} + +fn safe_put_dword(buffer: &mut [u8], index: usize, val: u32) -> Result { + buffer + .get_mut(index..index + 4) + .ok_or(MessageSerializationError::BufferTooSmall)? + .copy_from_slice(&val.to_le_bytes()); + Ok(4) +} + +fn safe_put_uuid(buffer: &mut [u8], index: usize, uuid: uuid::Bytes) -> Result { + buffer + .get_mut(index..index + 16) + .ok_or(MessageSerializationError::BufferTooSmall)? + .copy_from_slice(&uuid); + Ok(16) +} diff --git a/thermal-service/Cargo.toml b/thermal-service/Cargo.toml index 6ad5abb6e..01db4314f 100644 --- a/thermal-service/Cargo.toml +++ b/thermal-service/Cargo.toml @@ -18,7 +18,8 @@ embassy-sync.workspace = true embassy-time.workspace = true embedded-services.workspace = true heapless.workspace = true -uuid.workspace = true +odp-service-common.workspace = true +thermal-service-interface.workspace = true embedded-fans-async = "0.2.0" embedded-sensors-hal-async = "0.3.0" @@ -31,6 +32,7 @@ defmt = [ "embassy-sync/defmt", "embedded-fans-async/defmt", "embedded-sensors-hal-async/defmt", + "thermal-service-interface/defmt", ] log = [ "dep:log", @@ -38,6 +40,7 @@ log = [ "embassy-time/log", "embassy-sync/log", ] +mock = [] [lints] -workspace = true \ No newline at end of file +workspace = true diff --git a/thermal-service/src/context.rs b/thermal-service/src/context.rs deleted file mode 100644 index 93e171356..000000000 --- a/thermal-service/src/context.rs +++ /dev/null @@ -1,134 +0,0 @@ -//! Thermal service context -use crate::mptf; -use crate::{Error, Event, fan, sensor}; -use embassy_sync::channel::Channel; -use embedded_services::GlobalRawMutex; -use embedded_services::buffer::OwnedRef; -use embedded_services::ec_type::message::StdHostRequest; -use embedded_services::{error, intrusive_list}; - -embedded_services::define_static_buffer!(mctp_buf, u8, [0u8; 69]); - -pub(crate) struct Context<'a> { - // Registered temperature sensors - sensors: intrusive_list::IntrusiveList, - // Registered fans - fans: intrusive_list::IntrusiveList, - // MPTF Request Queue - mptf: Channel, - // Raw MCTP Payload Queue - mctp: Channel, - // MCTP message buffer - mctp_buf: OwnedRef<'a, u8>, - // Event queue - events: Channel, -} - -impl<'a> Context<'a> { - pub(crate) fn new() -> Self { - Self { - sensors: intrusive_list::IntrusiveList::new(), - fans: intrusive_list::IntrusiveList::new(), - mptf: Channel::new(), - mctp: Channel::new(), - mctp_buf: mctp_buf::get_mut().unwrap(), - events: Channel::new(), - } - } - - pub(crate) fn register_sensor(&self, sensor: &'static sensor::Device) -> Result<(), intrusive_list::Error> { - if self.get_sensor(sensor.id()).is_some() { - return Err(intrusive_list::Error::NodeAlreadyInList); - } - - self.sensors.push(sensor) - } - - pub(crate) fn sensors(&self) -> &intrusive_list::IntrusiveList { - &self.sensors - } - - pub(crate) fn get_sensor(&self, id: sensor::DeviceId) -> Option<&'static sensor::Device> { - for sensor in &self.sensors { - if let Some(data) = sensor.data::() { - if data.id() == id { - return Some(data); - } - } else { - error!("Non-device located in sensors list"); - } - } - - None - } - - pub(crate) async fn execute_sensor_request( - &self, - id: sensor::DeviceId, - request: sensor::Request, - ) -> sensor::Response { - let sensor = self.get_sensor(id).ok_or(sensor::Error::InvalidRequest)?; - sensor.execute_request(request).await - } - - pub(crate) fn register_fan(&self, fan: &'static fan::Device) -> Result<(), intrusive_list::Error> { - if self.get_fan(fan.id()).is_some() { - return Err(intrusive_list::Error::NodeAlreadyInList); - } - - self.fans.push(fan) - } - - pub(crate) fn fans(&self) -> &intrusive_list::IntrusiveList { - &self.fans - } - - pub(crate) fn get_fan(&self, id: fan::DeviceId) -> Option<&'static fan::Device> { - for fan in &self.fans { - if let Some(data) = fan.data::() { - if data.id() == id { - return Some(data); - } - } else { - error!("Non-device located in fan list"); - } - } - - None - } - - pub(crate) async fn execute_fan_request(&self, id: fan::DeviceId, request: fan::Request) -> fan::Response { - let fan = self.get_fan(id).ok_or(fan::Error::InvalidRequest)?; - fan.execute_request(request).await - } - - pub(crate) fn send_mptf_request(&self, msg: mptf::Request) -> Result<(), Error> { - self.mptf.try_send(msg).map_err(|_| Error)?; - Ok(()) - } - - pub(crate) async fn wait_mptf_request(&self) -> mptf::Request { - self.mptf.receive().await - } - - pub(crate) fn send_mctp_payload(&self, msg: StdHostRequest) -> Result<(), Error> { - self.mctp.try_send(msg).map_err(|_| Error)?; - Ok(()) - } - - pub(crate) async fn wait_mctp_payload(&self) -> StdHostRequest { - self.mctp.receive().await - } - - pub(crate) fn get_mctp_buf(&self) -> &OwnedRef<'a, u8> { - &self.mctp_buf - } - - pub(crate) async fn send_event(&self, event: Event) { - self.events.send(event).await - } - - pub(crate) async fn wait_event(&self) -> Event { - self.events.receive().await - } -} diff --git a/thermal-service/src/fan.rs b/thermal-service/src/fan.rs index 524d3d386..3e0b24715 100644 --- a/thermal-service/src/fan.rs +++ b/thermal-service/src/fan.rs @@ -1,528 +1,405 @@ -//! Fan Device use crate::utils::SampleBuf; -use crate::{Event, send_event}; +use core::marker::PhantomData; use embassy_sync::mutex::Mutex; use embassy_sync::signal::Signal; -use embassy_time::Timer; -use embedded_fans_async::{self as fan_traits, Error as HardwareError}; +use embassy_time::{Duration, Timer}; +use embedded_fans_async::Error as _; use embedded_sensors_hal_async::temperature::DegreesCelsius; -use embedded_services::GlobalRawMutex; -use embedded_services::ipc::deferred as ipc; -use embedded_services::{Node, intrusive_list}; -use embedded_services::{error, trace}; - -/// Convenience type for Fan response result -pub type Response = Result; - -/// Allows OEM to implement custom requests -/// -/// The default response is to return an error on unrecognized requests -pub trait CustomRequestHandler { - fn handle_custom_request(&self, _request: Request) -> impl core::future::Future { - async { Err(Error::InvalidRequest) } +use embedded_services::event::Sender; +use embedded_services::{GlobalRawMutex, error, trace}; +use thermal_service_interface::{fan, sensor}; + +/// Fan service configuration parameters. +#[derive(Clone, Copy, Debug)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct Config { + /// Rate at which to sample the fan RPM. + pub sample_period: Duration, + /// Rate at which to update the fan state based on temperature readings when auto control is enabled. + pub update_period: Duration, + /// Whether automatic fan control based on temperature is enabled. + pub auto_control: bool, + /// Hysteresis value to prevent rapid toggling between fan states when temperature is around a state transition point. + pub hysteresis: DegreesCelsius, + /// Temperature at which the fan will turn on and begin running at its minimum RPM. + pub min_temp: DegreesCelsius, + /// Temperature at which the fan will follow a speed curve between its minimum and maximum RPM. + pub ramp_temp: DegreesCelsius, + /// Temperature at which the fan will run at its maximum RPM. + pub max_temp: DegreesCelsius, +} + +impl Default for Config { + fn default() -> Self { + Self { + sample_period: Duration::from_secs(1), + update_period: Duration::from_secs(1), + auto_control: true, + hysteresis: 2.0, + min_temp: 25.0, + ramp_temp: 35.0, + max_temp: 45.0, + } } } -/// Allows OEMs to override the default linear response ramp response of fan -pub trait RampResponseHandler: fan_traits::Fan + fan_traits::RpmSense { - fn handle_ramp_response( - &mut self, - profile: &Profile, - temp: DegreesCelsius, - ) -> impl core::future::Future> { - let fan_ramp_temp = profile.ramp_temp; - let fan_max_temp = profile.max_temp; - let min_rpm = self.min_start_rpm(); - let max_rpm = self.max_rpm(); +struct ServiceInner { + driver: Mutex, + state: Mutex, + en_signal: Signal, + config: Mutex, + samples: Mutex>, +} - // Provide a linear fan response between its min and max RPM relative to temperature between ramp start and max temp - let rpm = if temp <= fan_ramp_temp { - min_rpm - } else if temp >= fan_max_temp { - max_rpm - } else { - let ratio = (temp - fan_ramp_temp) / (fan_max_temp - fan_ramp_temp); - let range = (max_rpm - min_rpm) as f32; - min_rpm + (ratio * range) as u16 - }; +impl ServiceInner { + fn new(driver: T, config: Config) -> Self { + Self { + driver: Mutex::new(driver), + state: Mutex::new(fan::State::Off), + en_signal: Signal::new(), + config: Mutex::new(config), + samples: Mutex::new(SampleBuf::create()), + } + } + + async fn handle_sampling(&self) { + loop { + match self.driver.lock().await.rpm().await { + Ok(rpm) => self.samples.lock().await.push(rpm), + Err(e) => error!("Fan error sampling fan rpm: {:?}", e.kind()), + } - async move { - self.set_speed_rpm(rpm).await?; - Ok(()) + let period = self.config.lock().await.sample_period; + Timer::after(period).await; } } -} -/// Ensures all necessary traits are implemented for the controlling driver -pub trait Controller: RampResponseHandler + CustomRequestHandler {} + async fn change_state(&self, to: fan::State) -> Result<(), fan::Error> { + let mut driver = self.driver.lock().await; + match to { + fan::State::Off => { + driver.stop().await.map_err(|_| fan::Error::Hardware)?; + } + fan::State::On(fan::OnState::Min) => { + driver.start().await.map_err(|_| fan::Error::Hardware)?; + } + fan::State::On(fan::OnState::Ramping) => { + // Ramp state will continuously update RPM according to its ramp response function + } + fan::State::On(fan::OnState::Max) => { + let max_rpm = driver.max_rpm(); + let _ = driver.set_speed_rpm(max_rpm).await.map_err(|_| fan::Error::Hardware)?; + } + } + drop(driver); -/// Fan error type -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -#[cfg_attr(feature = "defmt", derive(defmt::Format))] -pub enum Error { - /// Invalid request - InvalidRequest, - /// Device encountered a hardware failure - Hardware, -} + let mut state = self.state.lock().await; + trace!("Fan transitioned to {:?} state from {:?} state", to, *state); + *state = to; -/// Fan request -#[derive(Debug, Clone, Copy, PartialEq)] -#[cfg_attr(feature = "defmt", derive(defmt::Format))] -pub enum Request { - /// Most recent RPM measurement - GetRpm, - /// Average RPM measurement - GetAvgRpm, - /// Get Min RPM - GetMinRpm, - /// Get Max RPM - GetMaxRpm, - /// Set RPM manually and disable temperature-based control - SetRpm(u16), - /// Set duty cycle manually (in percent) and disable temperature-based control - SetDuty(u8), - /// Stop the fan and disable temperature-based control - Stop, - /// Enable temperature-based control - EnableAutoControl, - /// Set RPM sampling period (in ms) - SetSamplingPeriod(u64), - /// Set speed update period - SetSpeedUpdatePeriod(u64), - /// Get temperature which fan will turn on to minimum RPM (in degrees Celsius) - GetOnTemp, - /// Get temperature which fan will begin ramping (in degrees Celsius) - GetRampTemp, - /// Get temperature which fan will reach its max RPM (in degrees Celsius) - GetMaxTemp, - /// Set temperature which fan will turn on to minimum RPM (in degrees Celsius) - SetOnTemp(DegreesCelsius), - /// Set temperature which fan will begin ramping (in degrees Celsius) - SetRampTemp(DegreesCelsius), - /// Set temperature which fan will reach its max RPM (in degrees Celsius) - SetMaxTemp(DegreesCelsius), - /// Set hysteresis value between fan on and fan off (in degrees Celsius) - SetHysteresis(DegreesCelsius), - /// Get the profile associated with this fan - GetProfile, - /// Set the profile associated with this fan - SetProfile(Profile), - /// Custom-implemented command - Custom(u8, &'static [u8]), + Ok(()) + } } -/// Fan response -#[derive(Debug, Clone, Copy, PartialEq)] -#[cfg_attr(feature = "defmt", derive(defmt::Format))] -pub enum ResponseData { - /// Response for any request that is successful but does not require data - Success, - /// RPM - Rpm(u16), - /// Temperature - Temp(DegreesCelsius), - /// Profile - Profile(Profile), - /// Custom-implemented response - Custom(&'static [u8]), +/// Fan service control handle. +pub struct Service<'hw, T: fan::Driver, S: sensor::SensorService, E: Sender, const SAMPLE_BUF_LEN: usize> { + inner: &'hw ServiceInner, + _phantom: PhantomData<(S, E)>, } -#[derive(Debug, Clone, Copy)] -#[cfg_attr(feature = "defmt", derive(defmt::Format))] -enum FanState { - Off, - On, - Ramping, - Max, +// Note: We can't derive these traits because the compiler thinks our generics then need to be Copy + Clone, +// but we only hold a reference and don't actually need to be that strict +impl, const SAMPLE_BUF_LEN: usize> Clone + for Service<'_, T, S, E, SAMPLE_BUF_LEN> +{ + fn clone(&self) -> Self { + *self + } } -/// Fan device ID new type -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -#[cfg_attr(feature = "defmt", derive(defmt::Format))] -pub struct DeviceId(pub u8); - -/// Fan device struct -pub struct Device { - // Intrusive list node allowing Device to be contained in a list - node: Node, - // Device ID - id: DeviceId, - // Channel for IPC requests and responses - ipc: ipc::Channel, - // Signal for auto-control enable - auto_control_enable: Signal, +impl, const SAMPLE_BUF_LEN: usize> Copy + for Service<'_, T, S, E, SAMPLE_BUF_LEN> +{ } -impl Device { - /// Create a new fan device - pub fn new(id: DeviceId) -> Self { - Self { - node: Node::uninit(), - id, - ipc: ipc::Channel::new(), - auto_control_enable: Signal::new(), - } +impl<'hw, T: fan::Driver, S: sensor::SensorService, E: Sender, const SAMPLE_BUF_LEN: usize> fan::FanService + for Service<'hw, T, S, E, SAMPLE_BUF_LEN> +{ + async fn enable_auto_control(&self) -> Result<(), fan::Error> { + self.inner.change_state(fan::State::Off).await?; + self.inner.config.lock().await.auto_control = true; + self.inner.en_signal.signal(()); + Ok(()) } - /// Get the device ID - pub fn id(&self) -> DeviceId { - self.id + async fn rpm(&self) -> u16 { + self.inner.samples.lock().await.recent() } - /// Execute request and wait for response - pub async fn execute_request(&self, request: Request) -> Response { - self.ipc.execute(request).await + async fn min_rpm(&self) -> u16 { + self.inner.driver.lock().await.min_rpm() } -} -impl intrusive_list::NodeContainer for Device { - fn get_node(&self) -> &Node { - &self.node + async fn max_rpm(&self) -> u16 { + self.inner.driver.lock().await.max_rpm() } -} -/// Fan profile -#[derive(Debug, Clone, Copy, PartialEq)] -#[cfg_attr(feature = "defmt", derive(defmt::Format))] -pub struct Profile { - /// Profile ID - pub id: usize, - /// ID of sensor this fan will query for auto control - pub sensor_id: crate::sensor::DeviceId, - /// Period (in ms) fan will sample its RPM - pub sample_period: u64, - /// Period (in ms) fan will update its state during auto control - pub update_period: u64, - /// Whether fan is under automatic temperature-based control or not - pub auto_control: bool, - /// Hysteresis value (in degrees Celsius) preventing fan from rapidly switching between states - pub hysteresis: DegreesCelsius, - /// Temperature (in degrees Celsius) at which fan will turn on - pub on_temp: DegreesCelsius, - /// Temperature (in degrees Celsius) at which fan will begin its ramp response - pub ramp_temp: DegreesCelsius, - /// Temperature (in degrees Celsius) at which fan will run at its max speed - pub max_temp: DegreesCelsius, -} + async fn rpm_average(&self) -> u16 { + self.inner.samples.lock().await.average() + } -impl Default for Profile { - fn default() -> Self { - Self { - id: 0, - sensor_id: crate::sensor::DeviceId(0), - sample_period: 1000, - update_period: 1000, - auto_control: true, - hysteresis: 2.0, - on_temp: 39.0, - ramp_temp: 40.0, - max_temp: 44.0, - } + async fn rpm_immediate(&self) -> Result { + self.inner + .driver + .lock() + .await + .rpm() + .await + .map_err(|_| fan::Error::Hardware) } -} -/// Fan struct containing device for comms and driver -pub struct Fan { - // Underlying device - device: Device, - // Underlying controller - controller: Mutex, - // Fan profile - profile: Mutex, - // RPM samples - samples: Mutex>, - // State - state: Mutex, -} + async fn set_rpm(&self, rpm: u16) -> Result<(), fan::Error> { + self.inner + .driver + .lock() + .await + .set_speed_rpm(rpm) + .await + .map_err(|_| fan::Error::Hardware)?; + self.inner.config.lock().await.auto_control = false; + Ok(()) + } -impl Fan { - /// New fan - /// - /// Sample buffer length MUST be a power of two - pub fn new(id: DeviceId, controller: T, profile: Profile) -> Self { - Self { - device: Device::new(id), - controller: Mutex::new(controller), - profile: Mutex::new(profile), - samples: Mutex::new(SampleBuf::create()), - state: Mutex::new(FanState::Off), - } + async fn set_duty_percent(&self, duty: u8) -> Result<(), fan::Error> { + self.inner + .driver + .lock() + .await + .set_speed_percent(duty) + .await + .map_err(|_| fan::Error::Hardware)?; + self.inner.config.lock().await.auto_control = false; + Ok(()) } - /// Retrieve a reference to underlying device for registration with services - pub fn device(&self) -> &Device { - &self.device + async fn stop(&self) -> Result<(), fan::Error> { + self.inner + .driver + .lock() + .await + .stop() + .await + .map_err(|_| fan::Error::Hardware)?; + self.inner.config.lock().await.auto_control = false; + Ok(()) } - /// Retrieve a Mutex wrapping the underlying controller - /// - /// Should only be used to update OEM specific state - pub fn controller(&self) -> &Mutex { - &self.controller + async fn set_rpm_sampling_period(&self, period: Duration) { + self.inner.config.lock().await.sample_period = period; } - /// Wait for fan to receive a request - pub async fn wait_request(&self) -> ipc::Request<'_, GlobalRawMutex, Request, Response> { - self.device.ipc.receive().await + async fn set_rpm_update_period(&self, period: Duration) { + self.inner.config.lock().await.update_period = period; } - /// Process fan request - pub async fn process_request(&self, request: Request) -> Response { - match request { - Request::GetRpm => { - let rpm = self.samples.lock().await.recent(); - Ok(ResponseData::Rpm(rpm)) - } - Request::GetAvgRpm => { - let rpm = self.samples.lock().await.average(); - Ok(ResponseData::Rpm(rpm)) - } - Request::SetRpm(rpm) => { - self.controller - .lock() - .await - .set_speed_rpm(rpm) - .await - .map_err(|_| Error::Hardware)?; - self.profile.lock().await.auto_control = false; - Ok(ResponseData::Success) - } - Request::SetDuty(percent) => { - self.controller - .lock() - .await - .set_speed_percent(percent) - .await - .map_err(|_| Error::Hardware)?; - self.profile.lock().await.auto_control = false; - Ok(ResponseData::Success) - } - Request::Stop => { - self.change_state(FanState::Off).await?; - self.profile.lock().await.auto_control = false; - Ok(ResponseData::Success) - } - Request::GetMinRpm => { - let min_rpm = self.controller.lock().await.min_rpm(); - Ok(ResponseData::Rpm(min_rpm)) - } - Request::GetMaxRpm => { - let max_rpm = self.controller.lock().await.max_rpm(); - Ok(ResponseData::Rpm(max_rpm)) - } - Request::SetSamplingPeriod(period) => { - self.profile.lock().await.sample_period = period; - Ok(ResponseData::Success) - } - Request::EnableAutoControl => { - // Make sure we actually transition to a known state - // Next iteration of handle auto control would then put it in actual correct state - self.change_state(FanState::Off).await?; - self.profile.lock().await.auto_control = true; - self.device.auto_control_enable.signal(()); - Ok(ResponseData::Success) - } - Request::SetSpeedUpdatePeriod(period) => { - self.profile.lock().await.update_period = period; - Ok(ResponseData::Success) - } - Request::GetOnTemp => { - let temp = self.profile.lock().await.on_temp; - Ok(ResponseData::Temp(temp)) - } - Request::GetRampTemp => { - let temp = self.profile.lock().await.ramp_temp; - Ok(ResponseData::Temp(temp)) - } - Request::GetMaxTemp => { - let temp = self.profile.lock().await.max_temp; - Ok(ResponseData::Temp(temp)) - } - Request::SetOnTemp(temp) => { - self.profile.lock().await.on_temp = temp; - Ok(ResponseData::Success) - } - Request::SetRampTemp(temp) => { - self.profile.lock().await.ramp_temp = temp; - Ok(ResponseData::Success) - } - Request::SetMaxTemp(temp) => { - self.profile.lock().await.max_temp = temp; - Ok(ResponseData::Success) - } - Request::SetHysteresis(temp) => { - self.profile.lock().await.hysteresis = temp; - Ok(ResponseData::Success) - } - Request::GetProfile => { - let profile = *self.profile.lock().await; - Ok(ResponseData::Profile(profile)) - } - Request::SetProfile(profile) => { - *self.profile.lock().await = profile; - Ok(ResponseData::Success) - } - Request::Custom(_, _) => self.controller.lock().await.handle_custom_request(request).await, + async fn state_temp(&self, on_state: fan::OnState) -> DegreesCelsius { + let config = self.inner.config.lock().await; + match on_state { + fan::OnState::Min => config.min_temp, + fan::OnState::Ramping => config.ramp_temp, + fan::OnState::Max => config.max_temp, } } - /// Wait for fan to receive a request, process it, and send a response - pub async fn wait_and_process(&self) { - let request = self.wait_request().await; - let response = self.process_request(request.command).await; - request.respond(response); + async fn set_state_temp(&self, on_state: fan::OnState, temp: DegreesCelsius) { + let mut config = self.inner.config.lock().await; + match on_state { + fan::OnState::Min => config.min_temp = temp, + fan::OnState::Ramping => config.ramp_temp = temp, + fan::OnState::Max => config.max_temp = temp, + } } +} - /// Waits for a IPC request, then processes it - pub async fn handle_rx(&self) { - loop { - self.wait_and_process().await; - } +/// Parameters required to initialize a fan service. +pub struct InitParams<'hw, T: fan::Driver, S: sensor::SensorService, E: Sender> { + /// The underlying fan driver this service will control. + pub driver: T, + /// Initial configuration for the fan service. + pub config: Config, + /// The sensor service this fan will use to get temperature readings. + pub sensor_service: S, + /// Event senders for fan events. + pub event_senders: &'hw mut [E], +} + +/// The memory resources required by the fan. +pub struct Resources { + inner: Option>, +} + +// Note: We can't derive Default unless we trait bound T by Default, +// but we don't want that restriction since the default is just the None case +impl Default for Resources { + fn default() -> Self { + Self { inner: None } } +} - /// Periodically samples RPM from physical fan and caches it - pub async fn handle_sampling(&self) { - loop { - match self.controller.lock().await.rpm().await { - Ok(rpm) => self.samples.lock().await.push(rpm), - Err(e) => error!("Fan {} error sampling fan rpm: {:?}", self.device.id.0, e.kind()), - } +/// A task runner for a fan. Users must run this in an embassy task or similar async execution context. +pub struct Runner<'hw, T: fan::Driver, S: sensor::SensorService, E: Sender, const SAMPLE_BUF_LEN: usize> { + service: &'hw ServiceInner, + sensor: S, + event_senders: &'hw mut [E], +} - let period = self.profile.lock().await.sample_period; - Timer::after_millis(period).await; +impl<'hw, T: fan::Driver, S: sensor::SensorService, E: Sender, const SAMPLE_BUF_LEN: usize> + Runner<'hw, T, S, E, SAMPLE_BUF_LEN> +{ + async fn broadcast_event(&mut self, event: fan::Event) { + for sender in self.event_senders.iter_mut() { + sender.send(event).await; } } - pub async fn handle_auto_control(&self) { - loop { - if self.profile.lock().await.auto_control { - let temp = match crate::execute_sensor_request( - self.profile.lock().await.sensor_id, - crate::sensor::Request::GetTemp, - ) - .await - { - Ok(crate::sensor::ResponseData::Temp(temp)) => temp, - _ => { - error!( - "Fan {} failed to get temperature, disabling auto control and setting speed to max", - self.device.id.0 - ); - - self.profile.lock().await.auto_control = false; - if self.controller.lock().await.set_speed_max().await.is_err() { - error!("Fan {} failed to set speed to max!", self.device.id.0); - } - - send_event(Event::FanFailure(self.device.id, Error::Hardware)).await; - continue; - } - }; + async fn ramp_response(&self, temp: DegreesCelsius) -> Result<(), fan::Error> { + let config = *self.service.config.lock().await; - if let Err(e) = self.handle_fan_state(temp).await { - send_event(Event::FanFailure(self.device.id, e)).await; - error!("Fan {} error handling fan state transition: {:?}", self.device.id.0, e); - } + let mut driver = self.service.driver.lock().await; + let min_rpm = driver.min_start_rpm(); + let max_rpm = driver.max_rpm(); - let sleep_duration = self.profile.lock().await.update_period; - Timer::after_millis(sleep_duration).await; + // Provide a linear fan response between its min and max RPM relative to temperature between ramp start and max temp + let rpm = if temp <= config.ramp_temp { + min_rpm + } else if temp >= config.max_temp { + max_rpm + } else { + let ratio = (temp - config.ramp_temp) / (config.max_temp - config.ramp_temp); + let range = (max_rpm - min_rpm) as f32; + min_rpm + (ratio * range) as u16 + }; - // Sleep until auto control is re-enabled - } else { - self.device.auto_control_enable.wait().await; - } - } + driver + .set_speed_rpm(rpm) + .await + .map(|_| ()) + .map_err(|_| fan::Error::Hardware) } - async fn handle_fan_off_state(&self, temp: DegreesCelsius) -> Result<(), Error> { - let profile = self.profile.lock().await; + async fn handle_fan_off_state(&self, temp: DegreesCelsius) -> Result<(), fan::Error> { + let config = *self.service.config.lock().await; - if temp >= profile.on_temp { - self.change_state(FanState::On).await?; + if temp >= config.min_temp { + self.service.change_state(fan::State::On(fan::OnState::Min)).await?; } Ok(()) } - async fn handle_fan_on_state(&self, temp: DegreesCelsius) -> Result<(), Error> { - let profile = self.profile.lock().await; + async fn handle_fan_on_state(&self, temp: DegreesCelsius) -> Result<(), fan::Error> { + let config = *self.service.config.lock().await; - if temp < (profile.on_temp - profile.hysteresis) { - self.change_state(FanState::Off).await?; - } else if temp >= profile.ramp_temp { - self.change_state(FanState::Ramping).await?; + if temp < (config.min_temp - config.hysteresis) { + self.service.change_state(fan::State::Off).await?; + } else if temp >= config.ramp_temp { + self.service.change_state(fan::State::On(fan::OnState::Ramping)).await?; } Ok(()) } - async fn handle_fan_ramping_state(&self, temp: DegreesCelsius) -> Result<(), Error> { - let profile = self.profile.lock().await; + async fn handle_fan_ramping_state(&self, temp: DegreesCelsius) -> Result<(), fan::Error> { + let config = *self.service.config.lock().await; - if temp < (profile.ramp_temp - profile.hysteresis) { - self.change_state(FanState::On).await?; - } else if temp >= profile.max_temp { - self.change_state(FanState::Max).await?; + if temp < (config.ramp_temp - config.hysteresis) { + self.service.change_state(fan::State::On(fan::OnState::Min)).await?; + } else if temp >= config.max_temp { + self.service.change_state(fan::State::On(fan::OnState::Max)).await?; } else { - self.controller - .lock() - .await - .handle_ramp_response(&profile, temp) - .await - .map_err(|_| Error::Hardware)?; + self.ramp_response(temp).await?; } Ok(()) } - async fn handle_fan_max_state(&self, temp: DegreesCelsius) -> Result<(), Error> { - let profile = self.profile.lock().await; + async fn handle_fan_max_state(&self, temp: DegreesCelsius) -> Result<(), fan::Error> { + let config = *self.service.config.lock().await; - if temp < (profile.max_temp - profile.hysteresis) { - self.change_state(FanState::Ramping).await?; + if temp < (config.max_temp - config.hysteresis) { + self.service.change_state(fan::State::On(fan::OnState::Ramping)).await?; } Ok(()) } - async fn change_state(&self, to: FanState) -> Result<(), Error> { - let mut controller = self.controller.lock().await; - match to { - FanState::Off => { - controller.stop().await.map_err(|_| Error::Hardware)?; - } - FanState::On => { - controller.start().await.map_err(|_| Error::Hardware)?; - } - FanState::Ramping => { - // Ramp state will continuously update RPM according to its ramp response function - } - FanState::Max => { - let max_rpm = controller.max_rpm(); - let _ = controller.set_speed_rpm(max_rpm).await.map_err(|_| Error::Hardware)?; - } + async fn handle_fan_state(&self, temp: DegreesCelsius) -> Result<(), fan::Error> { + let state = *self.service.state.lock().await; + match state { + fan::State::Off => self.handle_fan_off_state(temp).await, + fan::State::On(fan::OnState::Min) => self.handle_fan_on_state(temp).await, + fan::State::On(fan::OnState::Ramping) => self.handle_fan_ramping_state(temp).await, + fan::State::On(fan::OnState::Max) => self.handle_fan_max_state(temp).await, } - drop(controller); + } - let state = *self.state.lock().await; - trace!( - "Fan {} transitioned to {:?} state from {:?} state", - self.device.id.0, to, state - ); - *self.state.lock().await = to; + async fn handle_auto_control(&mut self) { + loop { + if self.service.config.lock().await.auto_control { + let temp = self.sensor.temperature().await; + if let Err(e) = self.handle_fan_state(temp).await { + error!("Error handling fan state transition, disabling auto control: {:?}", e); + self.service.config.lock().await.auto_control = false; + self.broadcast_event(fan::Event::Failure(e)).await; + } - Ok(()) + let sleep_duration = self.service.config.lock().await.update_period; + Timer::after(sleep_duration).await; + + // Sleep until auto control is re-enabled + } else { + self.service.en_signal.wait().await; + } + } } +} - async fn handle_fan_state(&self, temp: DegreesCelsius) -> Result<(), Error> { - // Must copy state here, if attempt to dereference in match, mutex is still held in match arms - let state = *self.state.lock().await; - match state { - FanState::Off => self.handle_fan_off_state(temp).await, - FanState::On => self.handle_fan_on_state(temp).await, - FanState::Ramping => self.handle_fan_ramping_state(temp).await, - FanState::Max => self.handle_fan_max_state(temp).await, +impl<'hw, T: fan::Driver, S: sensor::SensorService + 'hw, E: Sender + 'hw, const SAMPLE_BUF_LEN: usize> + odp_service_common::runnable_service::ServiceRunner<'hw> for Runner<'hw, T, S, E, SAMPLE_BUF_LEN> +{ + async fn run(mut self) -> embedded_services::Never { + let service = self.service; + loop { + let _ = embassy_futures::join::join(service.handle_sampling(), self.handle_auto_control()).await; } } } + +impl<'hw, T: fan::Driver, S: sensor::SensorService + 'hw, E: Sender + 'hw, const SAMPLE_BUF_LEN: usize> + odp_service_common::runnable_service::Service<'hw> for Service<'hw, T, S, E, SAMPLE_BUF_LEN> +{ + type Runner = Runner<'hw, T, S, E, SAMPLE_BUF_LEN>; + type Resources = Resources; + type ErrorType = fan::Error; + type InitParams = InitParams<'hw, T, S, E>; + + async fn new( + service_storage: &'hw mut Self::Resources, + init_params: Self::InitParams, + ) -> Result<(Self, Self::Runner), Self::ErrorType> { + let service = service_storage + .inner + .insert(ServiceInner::new(init_params.driver, init_params.config)); + Ok(( + Self { + inner: service, + _phantom: PhantomData, + }, + Runner { + service, + sensor: init_params.sensor_service, + event_senders: init_params.event_senders, + }, + )) + } +} diff --git a/thermal-service/src/lib.rs b/thermal-service/src/lib.rs index b43c7d7db..dbcc178a4 100644 --- a/thermal-service/src/lib.rs +++ b/thermal-service/src/lib.rs @@ -1,162 +1,74 @@ //! Thermal service #![no_std] -#![allow(clippy::todo)] -#![allow(clippy::unwrap_used)] -use embassy_sync::once_lock::OnceLock; -use embedded_sensors_hal_async::temperature::DegreesCelsius; -use embedded_services::buffer::OwnedRef; -use embedded_services::ec_type::message::StdHostRequest; -use embedded_services::{comms, error, info, intrusive_list}; +use thermal_service_interface::{fan::FanService, sensor::SensorService}; -mod context; pub mod fan; -pub mod mptf; +#[cfg(feature = "mock")] +pub mod mock; pub mod sensor; -pub mod task; -pub mod utils; +mod utils; -/// Thermal error -#[derive(Debug)] -#[cfg_attr(feature = "defmt", derive(defmt::Format))] -pub struct Error; - -/// Thermal event -#[derive(Debug, Clone, Copy, PartialEq)] -#[cfg_attr(feature = "defmt", derive(defmt::Format))] -pub enum Event { - /// Sensor sampled temperature exceeding a threshold - ThresholdExceeded(sensor::DeviceId, sensor::ThresholdType, DegreesCelsius), - /// Sensor is no longer exceeding a threshold - ThresholdCleared(sensor::DeviceId, sensor::ThresholdType), - /// Sensor encountered hardware failure - SensorFailure(sensor::DeviceId, sensor::Error), - /// Fan encountered hardware failure - FanFailure(fan::DeviceId, fan::Error), +struct ServiceInner<'hw, S: SensorService, F: FanService> { + sensors: &'hw [S], + fans: &'hw [F], } -struct Service<'a> { - context: context::Context<'a>, - endpoint: comms::Endpoint, +/// Thermal service handle. +/// +/// This maintains a list of registered temperature sensors and fans, which can be accessed by instance ID. +/// +/// To allow for a collection of sensors and fans of different underlying driver types, +/// type erasure will need to be handled by the user, likely via enum dispatch, +/// since async traits are not currently dyn compatible. +#[derive(Clone, Copy)] +pub struct Service<'hw, S: SensorService, F: FanService> { + inner: &'hw ServiceInner<'hw, S, F>, } -impl<'a> Service<'a> { - fn new() -> Self { - Self { - context: context::Context::new(), - endpoint: comms::Endpoint::uninit(comms::EndpointID::Internal(comms::Internal::Thermal)), - } - } +/// Parameters required to initialize the thermal service. +pub struct InitParams<'hw, S: SensorService, F: FanService> { + /// Registered temperature sensors. + pub sensors: &'hw [S], + /// Registered fans. + pub fans: &'hw [F], } -impl<'a> comms::MailboxDelegate for Service<'a> { - fn receive(&self, message: &comms::Message) -> Result<(), comms::MailboxDelegateError> { - // Queue for later processing - if let Some(msg) = message.data.get::() { - self.context - .send_mctp_payload(*msg) - .map_err(|_| comms::MailboxDelegateError::BufferFull) - } else if let Some(&msg) = message.data.get::() { - self.context - .send_mptf_request(msg) - .map_err(|_| comms::MailboxDelegateError::BufferFull) - } else { - Err(comms::MailboxDelegateError::InvalidData) - } - } +/// The memory resources required by the thermal service. +pub struct Resources<'hw, S: SensorService, F: FanService> { + inner: Option>, } -// Just one instance of the service should be running -static SERVICE: OnceLock = OnceLock::new(); - -/// This must be called to initialize the Thermal service -pub async fn init() -> Result<(), Error> { - info!("Starting thermal service task"); - let service = SERVICE.get_or_init(Service::new); - - if comms::register_endpoint(service, &service.endpoint).await.is_err() { - error!("Failed to register thermal service endpoint"); - Err(Error) - } else { - Ok(()) +// Note: We can't derive Default because the compiler requires S: Default + F: Default bounds, +// but we don't need that since the default is just the None case +impl Default for Resources<'_, S, F> { + fn default() -> Self { + Self { inner: None } } } -// TODO: Don't like the code duplication from all these wrappers, consider better approach - -/// Used to send messages to other services from the Thermal service, -/// such as notifying the Host of thresholds crossed or the Power service if CRT TEMP is reached. -pub async fn send_service_msg(to: comms::EndpointID, data: &impl embedded_services::Any) -> Result<(), Error> { - // TODO: When this gets updated to return error, handle retrying send on failure - SERVICE.get().await.endpoint.send(to, data).await.map_err(|_| Error)?; - Ok(()) -} - -/// Send a MPTF request -pub async fn queue_mptf_request(msg: mptf::Request) -> Result<(), Error> { - SERVICE.get().await.context.send_mptf_request(msg) -} - -/// Wait for a MPTF request -pub async fn wait_mptf_request() -> mptf::Request { - SERVICE.get().await.context.wait_mptf_request().await -} - -/// Wait for a MCTP payload -pub async fn wait_mctp_payload() -> StdHostRequest { - SERVICE.get().await.context.wait_mctp_payload().await -} - -pub fn get_mctp_buf<'a>() -> &'a OwnedRef<'a, u8> { - SERVICE.try_get().unwrap().context.get_mctp_buf() -} - -/// Send a thermal event -pub async fn send_event(event: Event) { - SERVICE.get().await.context.send_event(event).await -} - -/// Wait for a thermal event -pub async fn wait_event() -> Event { - SERVICE.get().await.context.wait_event().await -} - -/// Register a sensor with the thermal service -pub async fn register_sensor(sensor: &'static sensor::Device) -> Result<(), intrusive_list::Error> { - SERVICE.get().await.context.register_sensor(sensor) -} - -/// Provides access to the sensors list -pub async fn sensors() -> &'static intrusive_list::IntrusiveList { - SERVICE.get().await.context.sensors() -} - -/// Find a sensor by its ID -pub async fn get_sensor(id: sensor::DeviceId) -> Option<&'static sensor::Device> { - SERVICE.get().await.context.get_sensor(id) -} - -/// Send a request to a sensor through the thermal service instead of directly. -pub async fn execute_sensor_request(id: sensor::DeviceId, request: sensor::Request) -> sensor::Response { - SERVICE.get().await.context.execute_sensor_request(id, request).await -} - -/// Register a fan with the thermal service -pub async fn register_fan(fan: &'static fan::Device) -> Result<(), intrusive_list::Error> { - SERVICE.get().await.context.register_fan(fan) +impl<'hw, S: SensorService, F: FanService> Service<'hw, S, F> { + /// Initializes the thermal service with the provided sensors and fans. + pub fn init(resources: &'hw mut Resources<'hw, S, F>, init_params: InitParams<'hw, S, F>) -> Self { + let inner = resources.inner.insert(ServiceInner { + sensors: init_params.sensors, + fans: init_params.fans, + }); + Self { inner } + } } -/// Provides access to the fans list -pub async fn fans() -> &'static intrusive_list::IntrusiveList { - SERVICE.get().await.context.fans() -} +impl<'hw, S: SensorService + Copy, F: FanService + Copy> thermal_service_interface::ThermalService + for Service<'hw, S, F> +{ + type Sensor = S; + type Fan = F; -/// Find a fan by its ID -pub async fn get_fan(id: fan::DeviceId) -> Option<&'static fan::Device> { - SERVICE.get().await.context.get_fan(id) -} + fn sensor(&self, id: u8) -> Option { + self.inner.sensors.get(id as usize).copied() + } -/// Send a request to a fan through the thermal service instead of directly. -pub async fn execute_fan_request(id: fan::DeviceId, request: fan::Request) -> fan::Response { - SERVICE.get().await.context.execute_fan_request(id, request).await + fn fan(&self, id: u8) -> Option { + self.inner.fans.get(id as usize).copied() + } } diff --git a/thermal-service/src/mock/fan.rs b/thermal-service/src/mock/fan.rs new file mode 100644 index 000000000..c86256dd9 --- /dev/null +++ b/thermal-service/src/mock/fan.rs @@ -0,0 +1,68 @@ +use crate::fan::Config; +use embedded_fans_async::{Error, ErrorKind, ErrorType, Fan, RpmSense}; +use thermal_service_interface::fan as fan_interface; + +/// `MockFan` error. +#[derive(Clone, Copy, Debug)] +pub struct MockFanError; +impl Error for MockFanError { + fn kind(&self) -> ErrorKind { + ErrorKind::Other + } +} + +/// Mock fan. +#[derive(Clone, Copy, Debug, Default)] +pub struct MockFan { + rpm: u16, +} + +impl MockFan { + /// Create a new `MockFan`. + pub fn new() -> Self { + Self::default() + } + + /// Returns a suitable `Config` for a mock fan service. + pub fn config() -> Config { + Config { + min_temp: super::MIN_TEMP + super::TEMP_RANGE / 4.0, + ramp_temp: super::MIN_TEMP + super::TEMP_RANGE / 2.0, + max_temp: super::MAX_TEMP - super::TEMP_RANGE / 4.0, + ..Default::default() + } + } +} + +impl ErrorType for MockFan { + type Error = MockFanError; +} + +impl Fan for MockFan { + fn min_rpm(&self) -> u16 { + 0 + } + + fn max_rpm(&self) -> u16 { + 6000 + } + + fn min_start_rpm(&self) -> u16 { + 1000 + } + + async fn set_speed_rpm(&mut self, rpm: u16) -> Result { + self.rpm = rpm; + Ok(rpm) + } +} + +impl RpmSense for MockFan { + async fn rpm(&mut self) -> Result { + // The mock fan is simple, it just remembers the last RPM it was set to and reports that + // as its current RPM. + Ok(self.rpm) + } +} + +impl fan_interface::Driver for MockFan {} diff --git a/thermal-service/src/mock/mod.rs b/thermal-service/src/mock/mod.rs new file mode 100644 index 000000000..f93912d19 --- /dev/null +++ b/thermal-service/src/mock/mod.rs @@ -0,0 +1,7 @@ +pub mod fan; +pub mod sensor; + +// Represents the temperature ranges the mock thermal service will move through +pub(crate) const MIN_TEMP: f32 = 20.0; +pub(crate) const MAX_TEMP: f32 = 40.0; +pub(crate) const TEMP_RANGE: f32 = MAX_TEMP - MIN_TEMP; diff --git a/thermal-service/src/mock/sensor.rs b/thermal-service/src/mock/sensor.rs new file mode 100644 index 000000000..6c587cfb1 --- /dev/null +++ b/thermal-service/src/mock/sensor.rs @@ -0,0 +1,80 @@ +use crate::sensor::Config; +use embedded_sensors_hal_async::sensor as sensor_traits; +use embedded_sensors_hal_async::temperature::{DegreesCelsius, TemperatureSensor, TemperatureThresholdSet}; +use thermal_service_interface::sensor; + +/// `MockSensor` error. +#[derive(Clone, Copy, Debug)] +pub struct MockSensorError; +impl sensor_traits::Error for MockSensorError { + fn kind(&self) -> sensor_traits::ErrorKind { + sensor_traits::ErrorKind::Other + } +} + +impl sensor_traits::ErrorType for MockSensor { + type Error = MockSensorError; +} + +/// Mock sensor. +#[derive(Clone, Copy, Debug, Default)] +pub struct MockSensor { + temp: DegreesCelsius, + falling: bool, +} + +impl MockSensor { + /// Create a new `MockSensor`. + pub fn new() -> Self { + Self { + temp: super::MIN_TEMP, + falling: false, + } + } + + /// Returns a suitable `Config` for a mock sensor service. + pub fn config() -> Config { + Config { + warn_high_threshold: super::MIN_TEMP + super::TEMP_RANGE / 4.0, + prochot_threshold: super::MIN_TEMP + super::TEMP_RANGE / 2.0, + critical_threshold: super::MAX_TEMP - super::TEMP_RANGE / 4.0, + ..Default::default() + } + } +} + +impl TemperatureSensor for MockSensor { + async fn temperature(&mut self) -> Result { + let t = self.temp; + + // Creates a sawtooth pattern + if self.falling { + self.temp -= 1.0; + if self.temp <= super::MIN_TEMP { + self.temp = super::MIN_TEMP; + self.falling = false; + } + } else { + self.temp += 1.0; + if self.temp >= super::MAX_TEMP { + self.temp = super::MAX_TEMP; + self.falling = true; + } + } + + Ok(t) + } +} + +// Setting a threshold for `MockSensor` doesn't make sense so immediately return error +impl TemperatureThresholdSet for MockSensor { + async fn set_temperature_threshold_low(&mut self, _threshold: DegreesCelsius) -> Result<(), Self::Error> { + Err(MockSensorError) + } + + async fn set_temperature_threshold_high(&mut self, _threshold: DegreesCelsius) -> Result<(), Self::Error> { + Err(MockSensorError) + } +} + +impl sensor::Driver for MockSensor {} diff --git a/thermal-service/src/mptf.rs b/thermal-service/src/mptf.rs deleted file mode 100644 index ea1195003..000000000 --- a/thermal-service/src/mptf.rs +++ /dev/null @@ -1,566 +0,0 @@ -//! Definitions for standard MPTF messages the generic Thermal service can expect -//! -//! Transport services such as eSPI and SSH would need to ensure messages are sent to the Thermal service in this format. -//! -//! This interface is subject to change as the eSPI OOB service is developed -use crate::{self as ts, fan, sensor, utils}; -use embedded_services::ec_type::message::{StdHostPayload, StdHostRequest}; -use embedded_services::{ec_type::protocols::mctp, error}; - -/// MPTF Standard UUIDs which the thermal service understands -pub mod uuid_standard { - pub const CRT_TEMP: uuid::Bytes = uuid::uuid!("218246e7-baf6-45f1-aa13-07e4845256b8").to_bytes_le(); - pub const PROC_HOT_TEMP: uuid::Bytes = uuid::uuid!("22dc52d2-fd0b-47ab-95b8-26552f9831a5").to_bytes_le(); - pub const PROFILE_TYPE: uuid::Bytes = uuid::uuid!("23b4a025-cdfd-4af9-a411-37a24c574615").to_bytes_le(); - pub const FAN_ON_TEMP: uuid::Bytes = uuid::uuid!("ba17b567-c368-48d5-bc6f-a312a41583c1").to_bytes_le(); - pub const FAN_RAMP_TEMP: uuid::Bytes = uuid::uuid!("3a62688c-d95b-4d2d-bacc-90d7a5816bcd").to_bytes_le(); - pub const FAN_MAX_TEMP: uuid::Bytes = uuid::uuid!("dcb758b1-f0fd-4ec7-b2c0-ef1e2a547b76").to_bytes_le(); - pub const FAN_MIN_RPM: uuid::Bytes = uuid::uuid!("db261c77-934b-45e2-9742-256c62badb7a").to_bytes_le(); - pub const FAN_MAX_RPM: uuid::Bytes = uuid::uuid!("5cf839df-8be7-42b9-9ac5-3403ca2c8a6a").to_bytes_le(); - pub const FAN_CURRENT_RPM: uuid::Bytes = uuid::uuid!("adf95492-0776-4ffc-84f3-b6c8b5269683").to_bytes_le(); -} - -/// Standard 32-bit DWORD -pub type Dword = u32; - -/// 16-bit variable length -pub type VarLen = u16; - -/// Instance ID -pub type InstanceId = u8; - -/// Time in milliseconds -pub type Milliseconds = Dword; - -/// MPTF expects temperatures in tenth Kelvins -pub type DeciKelvin = Dword; - -/// MPTF Response -#[derive(Debug, Clone, Copy)] -#[cfg_attr(feature = "defmt", derive(defmt::Format))] -pub struct Response { - // Status code (not necessarily related to Status code in response) - // This is used because some commands can fail but don't contain Status output as part of MPTF spec - pub status: Status, - // Response data - pub data: ResponseData, -} - -impl Response { - fn new(status: Status, data: ResponseData) -> Self { - Self { status, data } - } -} - -/// MPTF Status -#[derive(Debug, Clone, Copy)] -#[cfg_attr(feature = "defmt", derive(defmt::Format))] -pub enum Status { - /// Success - Success, - /// Invalid parameter was used - InvalidParameter, - /// Revision is not supported - UnsupportedRevision, - /// A hardware error occurred - HardwareError, -} - -impl From for u32 { - fn from(status: Status) -> Self { - match status { - Status::Success => 0, - Status::InvalidParameter => 1, - Status::UnsupportedRevision => 2, - Status::HardwareError => 3, - } - } -} - -impl From for u8 { - fn from(status: Status) -> Self { - u32::from(status) as u8 - } -} - -/// Standard MPTF requests expected by the thermal subsystem -#[derive(Debug, Clone, Copy)] -#[cfg_attr(feature = "defmt", derive(defmt::Format))] -pub enum Request { - /// EC_THM_GET_TMP = 0x1 - GetTmp(InstanceId), - /// EC_THM_SET_THRS = 0x2 - SetThrs(InstanceId, Milliseconds, DeciKelvin, DeciKelvin), - /// EC_THM_GET_THRS = 0x3 - GetThrs(InstanceId), - /// EC_THM_SET_SCP = 0x4 - SetScp(InstanceId, Dword, Dword, Dword), - /// EC_THM_GET_VAR = 0x5 - GetVar(InstanceId, VarLen, uuid::Bytes), - /// EC_THM_SET_VAR = 0x6 - SetVar(InstanceId, VarLen, uuid::Bytes, Dword), -} - -impl From for u8 { - fn from(request: Request) -> Self { - match request { - Request::GetTmp(_) => 1, - Request::SetThrs(_, _, _, _) => 2, - Request::GetThrs(_) => 3, - Request::SetScp(_, _, _, _) => 4, - Request::GetVar(_, _, _) => 5, - Request::SetVar(_, _, _, _) => 6, - } - } -} - -/// Data returned by thermal subsystem in response to MPTF requests -#[derive(Debug, Clone, Copy)] -#[cfg_attr(feature = "defmt", derive(defmt::Format))] -pub enum ResponseData { - /// EC_THM_GET_TMP = 0x1 - GetTmp(DeciKelvin), - /// EC_THM_SET_THRS = 0x2 - SetThrs(Status), - /// EC_THM_GET_THRS = 0x3 - GetThrs(Status, Milliseconds, DeciKelvin, DeciKelvin), - /// EC_THM_SET_SCP = 0x4 - SetScp(Status), - /// EC_THM_GET_VAR = 0x5 - GetVar(Status, Dword), - /// EC_THM_SET_VAR = 0x6 - SetVar(Status), -} - -impl From for u8 { - fn from(response: ResponseData) -> Self { - match response { - ResponseData::GetTmp(_) => 1, - ResponseData::SetThrs(_) => 2, - ResponseData::GetThrs(_, _, _, _) => 3, - ResponseData::SetScp(_) => 4, - ResponseData::GetVar(_, _) => 5, - ResponseData::SetVar(_) => 6, - } - } -} - -/// Notifications to Host -#[derive(Debug, Clone, Copy)] -#[cfg_attr(feature = "defmt", derive(defmt::Format))] -pub enum Notify { - /// Warn threshold was exceeded - Warn, - /// Prochot threshold was exceeded - ProcHot, - /// Critical threshold was exceeded - Critical, -} - -async fn sensor_get_tmp(request: &mut StdHostRequest) { - match request.payload { - mctp::Odp::ThermalGetTmpRequest { instance_id } => { - match ts::execute_sensor_request(sensor::DeviceId(instance_id), sensor::Request::GetTemp).await { - Ok(ts::sensor::ResponseData::Temp(temp)) => { - request.payload = StdHostPayload::ThermalGetTmpResponse { - temperature: utils::c_to_dk(temp), - }; - request.status = 0; - } - _ => { - request.payload = StdHostPayload::ErrorResponse {}; - request.status = 1; - } - } - } - _ => error!("Thermal Service: Host message header and payload mismatch"), - } -} - -async fn get_var_handler(request: &mut StdHostRequest) { - match request.payload { - mctp::Odp::ThermalGetVarRequest { - instance_id, - len: _, - var_uuid, - } => match var_uuid { - uuid_standard::CRT_TEMP => { - let Response { status: _, data } = sensor_get_thrs(instance_id, sensor::ThresholdType::Critical).await; - if let ResponseData::GetVar(Status::Success, val) = data { - request.status = Status::Success.into(); - request.payload = mctp::Odp::ThermalGetVarResponse { - status: Status::Success.into(), - val, - } - } else if let ResponseData::GetVar(error, val) = data { - request.payload = mctp::Odp::ThermalGetVarResponse { - status: error.into(), - val, - } - } - } - uuid_standard::PROC_HOT_TEMP => { - let Response { status: _, data } = sensor_get_thrs(instance_id, sensor::ThresholdType::Prochot).await; - if let ResponseData::GetVar(Status::Success, val) = data { - request.status = Status::Success.into(); - request.payload = mctp::Odp::ThermalGetVarResponse { - status: Status::Success.into(), - val, - } - } else if let ResponseData::GetVar(error, val) = data { - request.payload = mctp::Odp::ThermalGetVarResponse { - status: error.into(), - val, - } - } - } - // TODO: Add a SetProfileId request type? But for sensor or fan? - uuid_standard::PROFILE_TYPE => { - todo!() - } - uuid_standard::FAN_ON_TEMP => { - let Response { status: _, data } = fan_get_temp(instance_id, fan::Request::GetOnTemp).await; - if let ResponseData::GetVar(Status::Success, val) = data { - request.status = Status::Success.into(); - request.payload = mctp::Odp::ThermalGetVarResponse { - status: Status::Success.into(), - val, - } - } else if let ResponseData::GetVar(error, val) = data { - request.payload = mctp::Odp::ThermalGetVarResponse { - status: error.into(), - val, - } - } - } - uuid_standard::FAN_RAMP_TEMP => { - let Response { status: _, data } = fan_get_temp(instance_id, fan::Request::GetRampTemp).await; - if let ResponseData::GetVar(Status::Success, val) = data { - request.status = Status::Success.into(); - request.payload = mctp::Odp::ThermalGetVarResponse { - status: Status::Success.into(), - val, - } - } else if let ResponseData::GetVar(error, val) = data { - request.payload = mctp::Odp::ThermalGetVarResponse { - status: error.into(), - val, - } - } - } - uuid_standard::FAN_MAX_TEMP => { - let Response { status: _, data } = fan_get_temp(instance_id, fan::Request::GetMaxTemp).await; - if let ResponseData::GetVar(Status::Success, val) = data { - request.status = Status::Success.into(); - request.payload = mctp::Odp::ThermalGetVarResponse { - status: Status::Success.into(), - val, - } - } else if let ResponseData::GetVar(error, val) = data { - request.payload = mctp::Odp::ThermalGetVarResponse { - status: error.into(), - val, - } - } - } - uuid_standard::FAN_MIN_RPM => { - let Response { status: _, data } = fan_get_rpm(instance_id, fan::Request::GetMinRpm).await; - if let ResponseData::GetVar(Status::Success, val) = data { - request.status = Status::Success.into(); - request.payload = mctp::Odp::ThermalGetVarResponse { - status: Status::Success.into(), - val, - } - } else if let ResponseData::GetVar(error, val) = data { - request.payload = mctp::Odp::ThermalGetVarResponse { - status: error.into(), - val, - } - } - } - uuid_standard::FAN_MAX_RPM => { - let Response { status: _, data } = fan_get_rpm(instance_id, fan::Request::GetMaxRpm).await; - if let ResponseData::GetVar(Status::Success, val) = data { - request.status = Status::Success.into(); - request.payload = mctp::Odp::ThermalGetVarResponse { - status: Status::Success.into(), - val, - } - } else if let ResponseData::GetVar(error, val) = data { - request.status = error.into(); - request.payload = mctp::Odp::ThermalGetVarResponse { - status: error.into(), - val, - } - } - } - uuid_standard::FAN_CURRENT_RPM => { - let Response { status: _, data } = fan_get_rpm(instance_id, fan::Request::GetRpm).await; - if let ResponseData::GetVar(Status::Success, val) = data { - request.payload = mctp::Odp::ThermalGetVarResponse { - status: Status::Success.into(), - val, - } - } else if let ResponseData::GetVar(error, val) = data { - request.status = error.into(); - request.payload = mctp::Odp::ThermalGetVarResponse { - status: error.into(), - val, - } - } - } - // TODO: Allow OEM to handle these? - uuid => { - error!("Received GetVar for unrecognized UUID: {:?}", uuid); - request.status = Status::InvalidParameter.into(); - request.payload = mctp::Odp::ThermalGetVarResponse { - status: Status::InvalidParameter.into(), - val: 0, - } - } - }, - _ => error!("Thermal Service: Host message header and payload mismatch"), - } -} - -async fn set_var_handler(request: &mut StdHostRequest) { - match request.payload { - mctp::Odp::ThermalSetVarRequest { - instance_id, - len: _, - var_uuid, - set_var, - } => match var_uuid { - uuid_standard::CRT_TEMP => { - let Response { status: _, data } = - sensor_set_thrs(instance_id, sensor::ThresholdType::Critical, set_var).await; - if let ResponseData::SetVar(Status::Success) = data { - request.status = Status::Success.into(); - request.payload = mctp::Odp::ThermalSetVarResponse { - status: Status::Success.into(), - } - } else if let ResponseData::SetVar(error) = data { - request.payload = mctp::Odp::ThermalSetVarResponse { status: error.into() } - } - } - uuid_standard::PROC_HOT_TEMP => { - let Response { status: _, data } = - sensor_set_thrs(instance_id, sensor::ThresholdType::Prochot, set_var).await; - if let ResponseData::SetVar(Status::Success) = data { - request.status = Status::Success.into(); - request.payload = mctp::Odp::ThermalSetVarResponse { - status: Status::Success.into(), - } - } else if let ResponseData::SetVar(error) = data { - request.payload = mctp::Odp::ThermalSetVarResponse { status: error.into() } - } - } - // TODO: Add a SetProfileId request type? But for sensor or fan? - uuid_standard::PROFILE_TYPE => { - todo!() - } - uuid_standard::FAN_ON_TEMP => { - let Response { status: _, data } = - fan_set_var(instance_id, fan::Request::SetOnTemp(utils::dk_to_c(set_var))).await; - if let ResponseData::SetVar(Status::Success) = data { - request.status = Status::Success.into(); - request.payload = mctp::Odp::ThermalSetVarResponse { - status: Status::Success.into(), - } - } else if let ResponseData::SetVar(error) = data { - request.payload = mctp::Odp::ThermalSetVarResponse { status: error.into() } - } - } - uuid_standard::FAN_RAMP_TEMP => { - let Response { status: _, data } = - fan_set_var(instance_id, fan::Request::SetRampTemp(utils::dk_to_c(set_var))).await; - if let ResponseData::SetVar(Status::Success) = data { - request.status = Status::Success.into(); - request.payload = mctp::Odp::ThermalSetVarResponse { - status: Status::Success.into(), - } - } else if let ResponseData::SetVar(error) = data { - request.payload = mctp::Odp::ThermalSetVarResponse { status: error.into() } - } - } - uuid_standard::FAN_MAX_TEMP => { - let Response { status: _, data } = - fan_set_var(instance_id, fan::Request::SetMaxTemp(utils::dk_to_c(set_var))).await; - if let ResponseData::SetVar(Status::Success) = data { - request.status = Status::Success.into(); - request.payload = mctp::Odp::ThermalSetVarResponse { - status: Status::Success.into(), - } - } else if let ResponseData::SetVar(error) = data { - request.payload = mctp::Odp::ThermalSetVarResponse { status: error.into() } - } - } - // TODO: What does it mean to set the min/max RPM? Aren't these hardware defined? - uuid_standard::FAN_MIN_RPM => { - todo!() - } - // TODO: What does it mean to set the min/max RPM? Aren't these hardware defined? - uuid_standard::FAN_MAX_RPM => { - todo!() - } - uuid_standard::FAN_CURRENT_RPM => { - let Response { status: _, data } = fan_set_var(instance_id, fan::Request::SetRpm(set_var as u16)).await; - if let ResponseData::SetVar(Status::Success) = data { - request.payload = mctp::Odp::ThermalSetVarResponse { - status: Status::Success.into(), - } - } else if let ResponseData::SetVar(error) = data { - request.status = error.into(); - request.payload = mctp::Odp::ThermalSetVarResponse { status: error.into() } - } - } - // TODO: Allow OEM to handle these? - uuid => { - error!("Received SetVar for unrecognized UUID: {:?}", uuid); - request.status = Status::InvalidParameter.into(); - request.payload = mctp::Odp::ThermalSetVarResponse { - status: Status::InvalidParameter.into(), - } - } - }, - _ => error!("Thermal Service: Host message header and payload mismatch"), - } -} - -async fn sensor_get_warn_thrs(request: &mut StdHostRequest) { - match request.payload { - mctp::Odp::ThermalGetThrsRequest { instance_id } => { - let low = ts::execute_sensor_request( - sensor::DeviceId(instance_id), - sensor::Request::GetThreshold(sensor::ThresholdType::WarnLow), - ) - .await; - let high = ts::execute_sensor_request( - sensor::DeviceId(instance_id), - sensor::Request::GetThreshold(sensor::ThresholdType::WarnHigh), - ) - .await; - - match (low, high) { - (Ok(sensor::ResponseData::Threshold(low)), Ok(sensor::ResponseData::Threshold(high))) => { - request.payload = StdHostPayload::ThermalGetThrsResponse { - status: 0, - timeout: 0, - low: utils::c_to_dk(low), - high: utils::c_to_dk(high), - }; - request.status = 0; - } - _ => { - request.payload = StdHostPayload::ThermalGetThrsResponse { - status: 1, - timeout: 0, - low: 0, - high: 0, - }; - request.status = 1; - } - } - } - _ => error!("Thermal Service: Host message header and payload mismatch"), - } -} - -async fn sensor_set_warn_thrs(request: &mut StdHostRequest) { - match request.payload { - mctp::Odp::ThermalSetThrsRequest { - instance_id, - timeout: _, - low, - high, - } => { - let low_res = ts::execute_sensor_request( - sensor::DeviceId(instance_id), - sensor::Request::SetThreshold(sensor::ThresholdType::WarnLow, utils::dk_to_c(low)), - ) - .await; - let high_res = ts::execute_sensor_request( - sensor::DeviceId(instance_id), - sensor::Request::SetThreshold(sensor::ThresholdType::WarnHigh, utils::dk_to_c(high)), - ) - .await; - - if low_res.is_ok() && high_res.is_ok() { - request.payload = mctp::Odp::ThermalSetThrsResponse { status: 0 }; - request.status = 0; - } else { - request.payload = mctp::Odp::ThermalSetThrsResponse { status: 1 }; - request.status = 1; - } - } - _ => error!("Thermal Service: Host message header and payload mismatch"), - } -} - -async fn sensor_get_thrs(instance: u8, threshold_type: sensor::ThresholdType) -> Response { - match ts::execute_sensor_request( - sensor::DeviceId(instance), - sensor::Request::GetThreshold(threshold_type), - ) - .await - { - Ok(sensor::ResponseData::Temp(temp)) => Response::new( - Status::Success, - ResponseData::GetVar(Status::Success, utils::c_to_dk(temp)), - ), - _ => Response::new(Status::Success, ResponseData::GetVar(Status::HardwareError, 0)), - } -} - -async fn fan_get_temp(instance: u8, fan_request: fan::Request) -> Response { - match ts::execute_fan_request(fan::DeviceId(instance), fan_request).await { - Ok(fan::ResponseData::Temp(temp)) => Response::new( - Status::Success, - ResponseData::GetVar(Status::Success, utils::c_to_dk(temp)), - ), - _ => Response::new(Status::Success, ResponseData::GetVar(Status::HardwareError, 0)), - } -} - -async fn fan_get_rpm(instance: u8, fan_request: fan::Request) -> Response { - match ts::execute_fan_request(fan::DeviceId(instance), fan_request).await { - Ok(fan::ResponseData::Rpm(rpm)) => { - Response::new(Status::Success, ResponseData::GetVar(Status::Success, rpm as u32)) - } - _ => Response::new(Status::Success, ResponseData::GetVar(Status::HardwareError, 0)), - } -} - -async fn sensor_set_thrs(instance: u8, threshold_type: sensor::ThresholdType, threshold_dk: Dword) -> Response { - match ts::execute_sensor_request( - sensor::DeviceId(instance), - sensor::Request::SetThreshold(threshold_type, utils::dk_to_c(threshold_dk)), - ) - .await - { - Ok(sensor::ResponseData::Success) => Response::new(Status::Success, ResponseData::SetVar(Status::Success)), - _ => Response::new(Status::Success, ResponseData::SetVar(Status::HardwareError)), - } -} - -async fn fan_set_var(instance: u8, fan_request: fan::Request) -> Response { - match ts::execute_fan_request(fan::DeviceId(instance), fan_request).await { - Ok(fan::ResponseData::Success) => Response::new(Status::Success, ResponseData::SetVar(Status::Success)), - _ => Response::new(Status::Success, ResponseData::SetVar(Status::HardwareError)), - } -} - -pub(crate) async fn process_request(request: &mut StdHostRequest) { - match request.command { - embedded_services::ec_type::message::OdpCommand::Thermal(thermal_msg) => match thermal_msg { - embedded_services::ec_type::protocols::mptf::ThermalCmd::GetTmp => sensor_get_tmp(request).await, - embedded_services::ec_type::protocols::mptf::ThermalCmd::SetThrs => sensor_set_warn_thrs(request).await, - embedded_services::ec_type::protocols::mptf::ThermalCmd::GetThrs => sensor_get_warn_thrs(request).await, - // TODO: How do we handle this genericly? - embedded_services::ec_type::protocols::mptf::ThermalCmd::SetScp => todo!(), - embedded_services::ec_type::protocols::mptf::ThermalCmd::GetVar => get_var_handler(request).await, - embedded_services::ec_type::protocols::mptf::ThermalCmd::SetVar => set_var_handler(request).await, - }, - _ => error!("Thermal Service: Recvd other subsystem host message"), - } -} diff --git a/thermal-service/src/sensor.rs b/thermal-service/src/sensor.rs index 58da7e38a..9c50e0717 100644 --- a/thermal-service/src/sensor.rs +++ b/thermal-service/src/sensor.rs @@ -1,32 +1,14 @@ -//! Sensor Device use crate::utils::SampleBuf; -use crate::{Event, send_event}; -use embassy_sync::mutex::Mutex; -use embassy_sync::signal::Signal; -use embassy_time::Timer; -use embedded_sensors_hal_async::temperature::{DegreesCelsius, TemperatureSensor, TemperatureThresholdSet}; -use embedded_services::GlobalRawMutex; -use embedded_services::error; -use embedded_services::ipc::deferred as ipc; -use embedded_services::{Node, intrusive_list}; - -// Timeout period (in ms) for physical bus access -const BUS_TIMEOUT: u64 = 200; - -/// Convenience type for Sensor response result -pub type Response = Result; - -/// Allows OEM to implement custom requests -/// -/// The default response is to return an error on unrecognized requests -pub trait CustomRequestHandler { - fn handle_custom_request(&self, _request: Request) -> impl core::future::Future { - async { Err(Error::InvalidRequest) } - } -} +use core::marker::PhantomData; +use embassy_sync::{mutex::Mutex, signal::Signal}; +use embassy_time::{Duration, Timer, with_timeout}; +use embedded_sensors_hal_async::temperature::DegreesCelsius; +use embedded_services::event::Sender; +use embedded_services::{GlobalRawMutex, error}; +use thermal_service_interface::sensor; -/// Ensures all necessary traits are implemented for the controlling driver -pub trait Controller: TemperatureSensor + TemperatureThresholdSet + CustomRequestHandler {} +// Timeout period for physical bus access +const BUS_TIMEOUT: Duration = Duration::from_millis(200); /* Helper macro for calling a bus function with automatic retry after timeout or failure. * @@ -38,14 +20,14 @@ macro_rules! with_retry { $self:expr, $bus_method:expr ) => {{ - let mut retry_attempts = $self.profile.lock().await.retry_attempts; + let mut retry_attempts = $self.config.lock().await.retry_attempts; loop { if retry_attempts == 0 { - break Err(Error::Hardware); + break Err(sensor::Error::RetryExhausted); } - match embassy_time::with_timeout(embassy_time::Duration::from_millis(BUS_TIMEOUT), $bus_method).await { + match with_timeout(BUS_TIMEOUT, $bus_method).await { Ok(Ok(val)) => break Ok(val), _ => { retry_attempts -= 1; @@ -55,424 +37,303 @@ macro_rules! with_retry { }}; } -/// Sensor threshold type -#[derive(Debug, Clone, Copy, PartialEq)] -#[cfg_attr(feature = "defmt", derive(defmt::Format))] -pub enum ThresholdType { - /// Threshold below which host is notified - WarnLow, - /// Threshold above which host is notified - WarnHigh, - /// Threshold above which PROCHOT is asserted - Prochot, - /// Threshold above which critical temperature is reached and system should be shutdown - /// Some systems may tie sensor alert pin directly to reset controller, in which case - /// SetHardAlert should be used. - Critical, -} - -/// Sensor error type -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -#[cfg_attr(feature = "defmt", derive(defmt::Format))] -pub enum Error { - /// Invalid request - InvalidRequest, - /// Device encountered a hardware failure - Hardware, -} - -/// Sensor request -#[derive(Debug, Clone, Copy, PartialEq)] -#[cfg_attr(feature = "defmt", derive(defmt::Format))] -pub enum Request { - /// Most recent cached temperature measurement - GetTemp, - /// Average temperature measurement (over BUFFER_SIZE * SAMPLING_PERIOD) - GetAvgTemp, - /// Instructs sensor to immediately sample temperature (not cached) - GetTmpNow, - /// Low threshold below which sensor will set the alert pin active (in degrees Celsius) - SetHardAlertLow(DegreesCelsius), - /// High threshold above which sensor will set the alert pin active (in degrees Celsius) - SetHardAlertHigh(DegreesCelsius), - /// Get a threshold - GetThreshold(ThresholdType), - /// Set a threshold - SetThreshold(ThresholdType, DegreesCelsius), - /// Threshold in which sensor begins fast sampling - SetFastSamplingThreshold(DegreesCelsius), - /// Set temperature sampling period (in ms) - SetSamplingPeriod(u64), - /// Set fast temperature sampling period (in ms) - SetFastSamplingPeriod(u64), - /// An offset that is applied to all physical temperature samples (in degrees Celsius) - SetOffset(DegreesCelsius), - /// Enable sensor sampling - EnableSampling, - /// Disable sensor sampling - DisableSampling, - /// Set the max number of times communication with physical sensor will be attempted until error is reported - SetRetryAttempts(u8), - /// Get the thermal profile associated with this sensor - GetProfile, - /// Set the thermal profile associated with this sensor - SetProfile(Profile), - /// Custom-implemented command - Custom(u8, &'static [u8]), -} - -/// Sensor response -#[derive(Debug, Clone, Copy, PartialEq)] -#[cfg_attr(feature = "defmt", derive(defmt::Format))] -pub enum ResponseData { - /// Response for any request that is successful but does not require data - Success, - /// Temperature (in degrees Celsius) - Temp(DegreesCelsius), - /// Threshold (in degrees Celsius) - Threshold(DegreesCelsius), - /// Profile - Profile(Profile), - /// Custom-implemented response - Custom(&'static [u8]), -} - -/// Sensor device ID new type -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -#[cfg_attr(feature = "defmt", derive(defmt::Format))] -pub struct DeviceId(pub u8); - -/// Sensor device struct -pub struct Device { - /// Intrusive list node allowing Device to be contained in a list - node: Node, - /// Device ID - id: DeviceId, - /// Channel for IPC requests and responses - ipc: ipc::Channel, - /// Signal for enable - enable: Signal, -} - -impl Device { - /// Create a new sensor device - pub fn new(id: DeviceId) -> Self { - Self { - node: Node::uninit(), - id, - ipc: ipc::Channel::new(), - enable: Signal::new(), - } - } - - /// Get the device ID - pub fn id(&self) -> DeviceId { - self.id - } - - /// Execute request and wait for response - pub async fn execute_request(&self, request: Request) -> Response { - self.ipc.execute(request).await - } -} - -impl intrusive_list::NodeContainer for Device { - fn get_node(&self) -> &Node { - &self.node - } -} - -/// Sensor profile -#[derive(Debug, Clone, Copy, PartialEq)] +/// Sensor service configuration parameters. +#[derive(Clone, Copy, Debug)] #[cfg_attr(feature = "defmt", derive(defmt::Format))] -pub struct Profile { - /// Profile ID - pub id: usize, - /// Period (in ms) sensor will sample its temperature - pub sample_period: u64, - /// Period (in ms) sensor will sample its temperature when in fast sampling state - pub fast_sample_period: u64, - /// Whether or not automatic background sampling is enabled or not +pub struct Config { + /// Rate at which to sample the sensor when operating in normal conditions. + pub sample_period: Duration, + /// Rate at which to sample the sensor when operating in fast conditions. + pub fast_sample_period: Duration, + /// Whether periodic sampling is enabled. pub sampling_enabled: bool, - /// Hysteresis value (in degrees Celsius) preventing sensor from rapidly reporting threshold events + /// Hysteresis value to prevent rapid generation of threshold events when temperature is near a threshold. pub hysteresis: DegreesCelsius, - /// Threshold (in degrees Celsius) at which sensor will trigger a WARN LOW event + /// Temperature threshold below which a warning event will be generated. pub warn_low_threshold: DegreesCelsius, - /// Threshold (in degrees Celsius) at which sensor will trigger a WARN HIGH event + /// Temperature threshold above which a warning event will be generated. pub warn_high_threshold: DegreesCelsius, - /// Threshold (in degrees Celsius) at which sensor will trigger a PROCHOT event + /// Temperature threshold above which a prochot event will be generated. pub prochot_threshold: DegreesCelsius, - /// Threshold (in degrees Celsius) at which sensor will trigger a CRITICAL event - pub crt_threshold: DegreesCelsius, - /// Threshold (in degrees Celsius) at which sensor will enter the fast sampling state + /// Temperature threshold above which a critical event will be generated. + pub critical_threshold: DegreesCelsius, + /// Temperature threshold above which fast sampling is enabled. pub fast_sampling_threshold: DegreesCelsius, - /// Offset (in degrees Celsius) to be added to sampled temperature + /// Offset to be applied to the temperature readings. pub offset: DegreesCelsius, - /// Number of attempts sensor will make to communicate with the physical device over the bus + /// Number of retry attempts for bus operations. pub retry_attempts: u8, } -impl Default for Profile { +impl Default for Config { fn default() -> Self { Self { - id: 0, - sample_period: 1000, - fast_sample_period: 200, + sample_period: Duration::from_secs(1), + fast_sample_period: Duration::from_millis(200), sampling_enabled: true, + hysteresis: 2.0, warn_low_threshold: DegreesCelsius::MIN, warn_high_threshold: DegreesCelsius::MAX, prochot_threshold: DegreesCelsius::MAX, - crt_threshold: DegreesCelsius::MAX, + critical_threshold: DegreesCelsius::MAX, fast_sampling_threshold: DegreesCelsius::MAX, offset: 0.0, retry_attempts: 5, - hysteresis: 2.0, } } } -// Additional Sensor state -#[derive(Debug, Clone, Copy, PartialEq, Default)] -#[cfg_attr(feature = "defmt", derive(defmt::Format))] -struct State { - is_warn_low: bool, - is_warn_high: bool, - is_prochot: bool, - is_critical: bool, -} - -/// Wrapper binding a communication device, hardware driver, and additional state. -pub struct Sensor { - /// Sensor communication device - device: Device, - /// Sensor controller - controller: Mutex, - /// Sensor profile - profile: Mutex, - /// Sensor state - state: Mutex, - /// Cached temperature samples +struct ServiceInner { + driver: Mutex, + en_signal: Signal, + config: Mutex, samples: Mutex>, } -impl Sensor { - /// New sensor - /// - /// Sample buffer length MUST be a power of two - pub fn new(id: DeviceId, controller: T, profile: Profile) -> Self { +impl ServiceInner { + fn new(driver: T, config: Config) -> Self { Self { - device: Device::new(id), - controller: Mutex::new(controller), - profile: Mutex::new(profile), - state: Mutex::new(State::default()), + driver: Mutex::new(driver), + en_signal: Signal::new(), + config: Mutex::new(config), samples: Mutex::new(SampleBuf::create()), } } +} + +/// Sensor service control handle. +pub struct Service<'hw, T: sensor::Driver, E: Sender, const SAMPLE_BUF_LEN: usize> { + inner: &'hw ServiceInner, + _phantom: PhantomData, +} - /// Retrieve a reference to underlying device for registration with services - pub fn device(&self) -> &Device { - &self.device +// Note: We can't derive these traits because the compiler thinks our generics then need to be Copy + Clone, +// but we only hold a reference and don't actually need to be that strict +impl, const SAMPLE_BUF_LEN: usize> Clone + for Service<'_, T, E, SAMPLE_BUF_LEN> +{ + fn clone(&self) -> Self { + *self } +} + +impl, const SAMPLE_BUF_LEN: usize> Copy + for Service<'_, T, E, SAMPLE_BUF_LEN> +{ +} - /// Retrieve a Mutex wrapping the underlying controller - /// - /// Should only be used to update OEM specific state - pub fn controller(&self) -> &Mutex { - &self.controller +impl<'hw, T: sensor::Driver, E: Sender, const SAMPLE_BUF_LEN: usize> sensor::SensorService + for Service<'hw, T, E, SAMPLE_BUF_LEN> +{ + async fn temperature(&self) -> DegreesCelsius { + self.inner.samples.lock().await.recent() } - /// Wait for sensor to receive a request - pub async fn wait_request(&self) -> ipc::Request<'_, GlobalRawMutex, Request, Response> { - self.device.ipc.receive().await + async fn temperature_average(&self) -> DegreesCelsius { + self.inner.samples.lock().await.average() } - /// Process sensor request - pub async fn process_request(&self, request: Request) -> Response { - match request { - Request::GetTemp => { - let temp = self.samples.lock().await.recent(); - Ok(ResponseData::Temp(temp)) - } - Request::GetAvgTemp => { - let temp = self.samples.lock().await.average(); - Ok(ResponseData::Temp(temp)) - } - Request::GetTmpNow => { - let temp = with_retry!(self, self.controller.lock().await.temperature())?; - Ok(ResponseData::Temp(temp)) - } - Request::SetHardAlertLow(low) => { - with_retry!(self, self.controller.lock().await.set_temperature_threshold_low(low))?; - Ok(ResponseData::Success) - } - Request::SetHardAlertHigh(high) => { - with_retry!(self, self.controller.lock().await.set_temperature_threshold_high(high))?; - Ok(ResponseData::Success) - } - Request::GetThreshold(ThresholdType::WarnLow) => { - let threshold = self.profile.lock().await.warn_low_threshold; - Ok(ResponseData::Threshold(threshold)) - } - Request::GetThreshold(ThresholdType::WarnHigh) => { - let threshold = self.profile.lock().await.warn_high_threshold; - Ok(ResponseData::Threshold(threshold)) - } - Request::GetThreshold(ThresholdType::Prochot) => { - let threshold = self.profile.lock().await.prochot_threshold; - Ok(ResponseData::Threshold(threshold)) - } - Request::GetThreshold(ThresholdType::Critical) => { - let threshold = self.profile.lock().await.crt_threshold; - Ok(ResponseData::Threshold(threshold)) - } - Request::SetThreshold(ThresholdType::WarnLow, threshold) => { - self.profile.lock().await.warn_low_threshold = threshold; - Ok(ResponseData::Success) - } - Request::SetThreshold(ThresholdType::WarnHigh, threshold) => { - self.profile.lock().await.warn_high_threshold = threshold; - Ok(ResponseData::Success) - } - Request::SetThreshold(ThresholdType::Prochot, threshold) => { - self.profile.lock().await.prochot_threshold = threshold; - Ok(ResponseData::Success) - } - Request::SetThreshold(ThresholdType::Critical, threshold) => { - self.profile.lock().await.crt_threshold = threshold; - Ok(ResponseData::Success) - } - Request::SetFastSamplingThreshold(threshold) => { - self.profile.lock().await.fast_sampling_threshold = threshold; - Ok(ResponseData::Success) - } - Request::SetSamplingPeriod(period) => { - self.profile.lock().await.sample_period = period; - Ok(ResponseData::Success) - } - Request::SetFastSamplingPeriod(period) => { - self.profile.lock().await.fast_sample_period = period; - Ok(ResponseData::Success) - } - Request::SetOffset(offset) => { - self.profile.lock().await.offset = offset; - Ok(ResponseData::Success) - } - Request::EnableSampling => { - self.profile.lock().await.sampling_enabled = true; - self.device.enable.signal(()); - Ok(ResponseData::Success) - } - Request::DisableSampling => { - self.profile.lock().await.sampling_enabled = false; - Ok(ResponseData::Success) - } - Request::SetRetryAttempts(limit) => { - self.profile.lock().await.retry_attempts = limit; - Ok(ResponseData::Success) - } - Request::GetProfile => { - let profile = *self.profile.lock().await; - Ok(ResponseData::Profile(profile)) - } - Request::SetProfile(profile) => { - *self.profile.lock().await = profile; - Ok(ResponseData::Success) - } - Request::Custom(_, _) => self.controller.lock().await.handle_custom_request(request).await, - } + async fn temperature_immediate(&self) -> Result { + with_retry!(self.inner, self.inner.driver.lock().await.temperature()) } - // Wait for sensor to receive a request, process it, and send a response - pub async fn wait_and_process(&self) { - let request = self.wait_request().await; - let response = self.process_request(request.command).await; - request.respond(response); + async fn set_threshold(&self, threshold: sensor::Threshold, value: DegreesCelsius) { + let mut config = self.inner.config.lock().await; + match threshold { + sensor::Threshold::WarnLow => config.warn_low_threshold = value, + sensor::Threshold::WarnHigh => config.warn_high_threshold = value, + sensor::Threshold::Prochot => config.prochot_threshold = value, + sensor::Threshold::Critical => config.critical_threshold = value, + } } - /// Waits for a request then processes it and sends a response - pub async fn handle_rx(&self) { - loop { - self.wait_and_process().await; + async fn threshold(&self, threshold: sensor::Threshold) -> DegreesCelsius { + let config = self.inner.config.lock().await; + match threshold { + sensor::Threshold::WarnLow => config.warn_low_threshold, + sensor::Threshold::WarnHigh => config.warn_high_threshold, + sensor::Threshold::Prochot => config.prochot_threshold, + sensor::Threshold::Critical => config.critical_threshold, } } - async fn check_thresholds(&self, temp: DegreesCelsius) { - let profile = self.profile.lock().await; - let mut state = self.state.lock().await; + async fn set_sample_period(&self, period: Duration) { + self.inner.config.lock().await.sample_period = period; + } + + async fn enable_sampling(&self) { + self.inner.config.lock().await.sampling_enabled = true; + self.inner.en_signal.signal(()); + } + + async fn disable_sampling(&self) { + self.inner.config.lock().await.sampling_enabled = false; + } +} + +/// Parameters required to initialize a sensor service. +pub struct InitParams<'hw, T: sensor::Driver, E: Sender> { + /// The underlying sensor driver this service will control. + pub driver: T, + /// Initial configuration for the sensor service. + pub config: Config, + /// Event senders for sensor events. + pub event_senders: &'hw mut [E], +} + +/// The memory resources required by the sensor. +pub struct Resources { + inner: Option>, +} + +// Note: We can't derive Default unless we trait bound T by Default, +// but we don't want that restriction since the default is just the None case +impl Default for Resources { + fn default() -> Self { + Self { inner: None } + } +} + +// Additional sensor runner state +#[derive(Debug, Clone, Copy, PartialEq, Default)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +struct State { + is_warn_low: bool, + is_warn_high: bool, + is_prochot: bool, + is_critical: bool, +} + +/// A task runner for a sensor. Users must run this in an embassy task or similar async execution context. +pub struct Runner<'hw, T: sensor::Driver, E: Sender, const SAMPLE_BUF_LEN: usize> { + service: &'hw ServiceInner, + event_senders: &'hw mut [E], + state: State, +} - if temp >= profile.warn_high_threshold && !state.is_warn_high { - send_event(Event::ThresholdExceeded(self.device.id, ThresholdType::WarnHigh, temp)).await; - state.is_warn_high = true; - } else if temp < (profile.warn_high_threshold - profile.hysteresis) && state.is_warn_high { - send_event(Event::ThresholdCleared(self.device.id, ThresholdType::WarnHigh)).await; - state.is_warn_high = false; +impl<'hw, T: sensor::Driver, E: Sender, const SAMPLE_BUF_LEN: usize> Runner<'hw, T, E, SAMPLE_BUF_LEN> { + async fn broadcast_event(&mut self, event: sensor::Event) { + for sender in self.event_senders.iter_mut() { + sender.send(event).await; } + } - if temp <= profile.warn_low_threshold && !state.is_warn_low { - send_event(Event::ThresholdExceeded(self.device.id, ThresholdType::WarnLow, temp)).await; - state.is_warn_low = true; - } else if temp > (profile.warn_low_threshold + profile.hysteresis) && state.is_warn_low { - send_event(Event::ThresholdCleared(self.device.id, ThresholdType::WarnLow)).await; - state.is_warn_low = false; + async fn check_thresholds(&mut self, temp: DegreesCelsius) { + let config = *self.service.config.lock().await; + + if temp >= config.warn_high_threshold && !self.state.is_warn_high { + self.state.is_warn_high = true; + self.broadcast_event(sensor::Event::ThresholdExceeded(sensor::Threshold::WarnHigh)) + .await; + } else if temp < (config.warn_high_threshold - config.hysteresis) && self.state.is_warn_high { + self.state.is_warn_high = false; + self.broadcast_event(sensor::Event::ThresholdCleared(sensor::Threshold::WarnHigh)) + .await; } - if temp >= profile.prochot_threshold && !state.is_prochot { - send_event(Event::ThresholdExceeded(self.device.id, ThresholdType::Prochot, temp)).await; - state.is_prochot = true; - } else if temp < (profile.prochot_threshold - profile.hysteresis) && state.is_prochot { - send_event(Event::ThresholdCleared(self.device.id, ThresholdType::Prochot)).await; - state.is_prochot = false; + if temp <= config.warn_low_threshold && !self.state.is_warn_low { + self.state.is_warn_low = true; + self.broadcast_event(sensor::Event::ThresholdExceeded(sensor::Threshold::WarnLow)) + .await; + } else if temp > (config.warn_low_threshold + config.hysteresis) && self.state.is_warn_low { + self.state.is_warn_low = false; + self.broadcast_event(sensor::Event::ThresholdCleared(sensor::Threshold::WarnLow)) + .await; } - if temp >= profile.crt_threshold && !state.is_critical { - send_event(Event::ThresholdExceeded(self.device.id, ThresholdType::Critical, temp)).await; - state.is_critical = true; - } else if temp < (profile.crt_threshold - profile.hysteresis) && state.is_critical { - send_event(Event::ThresholdCleared(self.device.id, ThresholdType::Critical)).await; - state.is_critical = false; + if temp >= config.prochot_threshold && !self.state.is_prochot { + self.state.is_prochot = true; + self.broadcast_event(sensor::Event::ThresholdExceeded(sensor::Threshold::Prochot)) + .await; + } else if temp < (config.prochot_threshold - config.hysteresis) && self.state.is_prochot { + self.state.is_prochot = false; + self.broadcast_event(sensor::Event::ThresholdCleared(sensor::Threshold::Prochot)) + .await; + } + + if temp >= config.critical_threshold && !self.state.is_critical { + self.state.is_critical = true; + self.broadcast_event(sensor::Event::ThresholdExceeded(sensor::Threshold::Critical)) + .await; + } else if temp < (config.critical_threshold - config.hysteresis) && self.state.is_critical { + self.state.is_critical = false; + self.broadcast_event(sensor::Event::ThresholdCleared(sensor::Threshold::Critical)) + .await; } } +} - /// Periodically samples temperature from physical sensor and caches it - pub async fn handle_sampling(&self) { +impl<'hw, T: sensor::Driver, E: Sender, const SAMPLE_BUF_LEN: usize> + odp_service_common::runnable_service::ServiceRunner<'hw> for Runner<'hw, T, E, SAMPLE_BUF_LEN> +{ + async fn run(mut self) -> embedded_services::Never { loop { + let config = *self.service.config.lock().await; + // Only sample temperature if enabled - if self.profile.lock().await.sampling_enabled { - let temp = match with_retry!(self, self.controller.lock().await.temperature()) { + if config.sampling_enabled { + let temp = match with_retry!(self.service, self.service.driver.lock().await.temperature()) { Ok(temp) => temp, - _ => { - self.profile.lock().await.sampling_enabled = false; - send_event(Event::SensorFailure(self.device.id, Error::Hardware)).await; - error!("Error sampling sensor {}, disabling sampling", self.device.id.0); + Err(e) => { + self.service.config.lock().await.sampling_enabled = false; + self.broadcast_event(sensor::Event::Failure(e)).await; + error!("Error sampling sensor, disabling sampling"); continue; } }; // Add offset to measured temperature - let temp = temp + self.profile.lock().await.offset; + let temp = temp + config.offset; // Cache in buffer for quick retrieval from other services - self.samples.lock().await.push(temp); + self.service.samples.lock().await.push(temp); // Check thresholds self.check_thresholds(temp).await; // Adjust sampling rate based on how hot we are getting - let profile = self.profile.lock().await; - let sleep_duration = if temp >= profile.fast_sampling_threshold { - profile.fast_sample_period + let sleep_duration = if temp >= config.fast_sampling_threshold { + config.fast_sample_period } else { - profile.sample_period + config.sample_period }; - drop(profile); // Sleep in-between sampling periods - Timer::after_millis(sleep_duration).await; + Timer::after(sleep_duration).await; // Otherwise sleep and wait to be re-enabled } else { - self.device.enable.wait().await; + self.service.en_signal.wait().await; } } } } + +impl<'hw, T: sensor::Driver, E: Sender + 'hw, const SAMPLE_BUF_LEN: usize> + odp_service_common::runnable_service::Service<'hw> for Service<'hw, T, E, SAMPLE_BUF_LEN> +{ + type Runner = Runner<'hw, T, E, SAMPLE_BUF_LEN>; + type Resources = Resources; + type ErrorType = sensor::Error; + type InitParams = InitParams<'hw, T, E>; + + async fn new( + service_storage: &'hw mut Self::Resources, + init_params: Self::InitParams, + ) -> Result<(Self, Self::Runner), Self::ErrorType> { + let service = service_storage + .inner + .insert(ServiceInner::new(init_params.driver, init_params.config)); + Ok(( + Self { + inner: service, + _phantom: PhantomData, + }, + Runner { + service, + event_senders: init_params.event_senders, + state: State::default(), + }, + )) + } +} diff --git a/thermal-service/src/task.rs b/thermal-service/src/task.rs deleted file mode 100644 index 82d214276..000000000 --- a/thermal-service/src/task.rs +++ /dev/null @@ -1,31 +0,0 @@ -use embedded_services::{comms, error}; - -use crate::{self as ts, mptf::process_request}; - -pub async fn handle_requests() { - loop { - let mut request = ts::wait_mctp_payload().await; - process_request(&mut request).await; - let send_result = ts::send_service_msg( - comms::EndpointID::External(comms::External::Host), - &embedded_services::ec_type::message::HostMsg::Response(request), - ) - .await; - - if send_result.is_err() { - error!("Failed to send response to MPTF request!"); - } - } -} - -pub async fn fan_task( - fan: &'static crate::fan::Fan, -) { - let _ = embassy_futures::join::join3(fan.handle_rx(), fan.handle_sampling(), fan.handle_auto_control()).await; -} - -pub async fn sensor_task( - sensor: &'static crate::sensor::Sensor, -) { - let _ = embassy_futures::join::join(sensor.handle_rx(), sensor.handle_sampling()).await; -} diff --git a/thermal-service/src/utils.rs b/thermal-service/src/utils.rs index 4497fc982..f8598c130 100644 --- a/thermal-service/src/utils.rs +++ b/thermal-service/src/utils.rs @@ -1,5 +1,4 @@ -//! Helpful utilities for the thermal service -use crate::mptf; +//! Helpful utilities for the thermal service. use heapless::Deque; /// Buffer for storing samples @@ -30,23 +29,20 @@ impl SampleBuf { } impl SampleBuf { + /// Returns the average of the samples in the buffer, or 0.0 if the buffer is empty. pub fn average(&self) -> f32 { - self.deque.iter().copied().sum::() / (self.deque.len() as f32) + let len = self.deque.len(); + if len == 0 { + return 0.0; + } + self.deque.iter().copied().sum::() / len as f32 } } impl SampleBuf { + /// Returns the average of the samples in the buffer, or 0 if the buffer is empty. pub fn average(&self) -> u16 { - self.deque.iter().copied().sum::() / (self.deque.len() as u16) + let sum: u32 = self.deque.iter().copied().map(u32::from).sum(); + sum.checked_div(self.deque.len() as u32).unwrap_or(0) as u16 } } - -/// Convert deciKelvin to degrees Celsius -pub const fn dk_to_c(dk: mptf::DeciKelvin) -> f32 { - (dk as f32 / 10.0) - 273.15 -} - -/// Convert degrees Celsius to deciKelvin -pub const fn c_to_dk(c: f32) -> mptf::DeciKelvin { - ((c + 273.15) * 10.0) as mptf::DeciKelvin -} diff --git a/time-alarm-service-interface/Cargo.toml b/time-alarm-service-interface/Cargo.toml new file mode 100644 index 000000000..13a09e048 --- /dev/null +++ b/time-alarm-service-interface/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "time-alarm-service-interface" +version.workspace = true +edition.workspace = true +license.workspace = true +repository.workspace = true + +[dependencies] +bitfield.workspace = true +defmt = { workspace = true, optional = true } +log = { workspace = true, optional = true } +embedded-mcu-hal.workspace = true +num_enum.workspace = true +zerocopy = { workspace = true, features = ["derive"] } + +[features] +defmt = ["dep:defmt", "embedded-mcu-hal/defmt"] +log = ["dep:log"] + +[lints] +workspace = true + +[package.metadata.cargo-machete] +ignored = ["log"] diff --git a/time-alarm-service-interface/src/acpi_timestamp.rs b/time-alarm-service-interface/src/acpi_timestamp.rs new file mode 100644 index 000000000..d554bc4c8 --- /dev/null +++ b/time-alarm-service-interface/src/acpi_timestamp.rs @@ -0,0 +1,188 @@ +use embedded_mcu_hal::time::{Datetime, DatetimeClockError, DatetimeFields, Month}; +use zerocopy::{FromBytes, I16, Immutable, IntoBytes, KnownLayout, LE, U16, Unaligned}; + +// Timestamp structure as specified in the ACPI spec. Must be exactly this layout. +#[repr(C, packed)] +#[derive(FromBytes, IntoBytes, Immutable, KnownLayout, Unaligned, Copy, Clone, Debug)] +struct RawAcpiTimestamp { + // Year: 1900 - 9999 + year: U16, + + // Month: 1 - 12 + month: u8, + + // Day: 1 - 31 + day: u8, + + // Hour: 0 - 23 + hour: u8, + + // Minute: 0 - 59 + minute: u8, + + // Second: 0 - 59. Leap seconds are not supported. + second: u8, + + // For _GRT, 0 = time is not valid (request failed), 1 = time is valid. For _SRT, this is padding and should be 0. + valid_or_padding: u8, + + // Milliseconds: 0-999. Leap seconds are not supported. + milliseconds: U16, + + // Time zone: -1440 to 1440 in minutes from UTC, or 2047 if unspecified + time_zone: I16, + + // 1 = daylight savings time in effect, 0 = standard time + daylight: u8, + + // Reserved, must be 0 + _padding: [u8; 3], +} + +impl From<&AcpiTimestamp> for RawAcpiTimestamp { + fn from(ts: &AcpiTimestamp) -> Self { + Self { + year: ts.datetime.year().into(), + month: ts.datetime.month().into(), + day: ts.datetime.day(), + hour: ts.datetime.hour(), + minute: ts.datetime.minute(), + second: ts.datetime.second(), + valid_or_padding: 1, // valid + milliseconds: ((ts.datetime.nanoseconds() / 1_000_000) as u16).into(), + time_zone: i16::from(ts.time_zone).into(), + daylight: ts.dst_status.into(), + _padding: [0; 3], + } + } +} + +// ------------------------------------------------- + +/// The current daylight savings time status of the timer. +#[derive(Clone, Copy, Debug, PartialEq, num_enum::IntoPrimitive, num_enum::TryFromPrimitive)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +#[repr(u8)] +pub enum AcpiDaylightSavingsTimeStatus { + /// Daylight savings time is not observed in this timezone. + NotObserved = 0, + + /// Daylight savings time is observed in this timezone, but the current time has not been adjusted for it. + NotAdjusted = 1, + + // Note: in the spec, this is a pair of flags where bit 0 = observed, bit 1 = adjusted. 2 (adjusted but not observed) is nonsensical, so we omit it. + // + /// Daylight savings time is observed in this timezone, and the current time has been adjusted for it. + Adjusted = 3, +} + +// ------------------------------------------------- + +/// The time offset from UTC of the system's time zone, expressed in minutes +#[derive(Clone, Copy, Debug, PartialEq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct AcpiTimeZoneOffset { + minutes_from_utc: i16, // minutes from UTC +} + +impl AcpiTimeZoneOffset { + /// Constructs a time zone offset with the given number of minutes from UTC. Valid values are -1440 to 1440 (inclusive). + pub fn new(minutes_from_utc: i16) -> Result { + if !(-1440..=1440).contains(&minutes_from_utc) { + Err(DatetimeClockError::UnsupportedDatetime) + } else { + Ok(Self { minutes_from_utc }) + } + } + + /// The number of minutes that the time zone is offset from UTC. + pub fn minutes_from_utc(&self) -> i16 { + self.minutes_from_utc + } +} + +/// The time zone of the system, either unknown or specified as a number of minutes from UTC. +#[derive(Copy, Clone, Debug, PartialEq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum AcpiTimeZone { + /// The time zone is not specified and no relation to UTC can be inferred. + Unknown, + + /// The time zone is this many minutes from UTC. + MinutesFromUtc(AcpiTimeZoneOffset), +} + +impl TryFrom for AcpiTimeZone { + type Error = DatetimeClockError; + + fn try_from(value: i16) -> Result { + if value == 2047 { + Ok(Self::Unknown) + } else { + Ok(Self::MinutesFromUtc(AcpiTimeZoneOffset::new(value)?)) + } + } +} + +impl From for i16 { + fn from(val: AcpiTimeZone) -> Self { + match val { + AcpiTimeZone::Unknown => 2047, + AcpiTimeZone::MinutesFromUtc(offset) => offset.minutes_from_utc(), + } + } +} + +// ------------------------------------------------- + +/// A timestamp as specified in the ACPI spec, including time zone and daylight savings time status. +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct AcpiTimestamp { + pub datetime: Datetime, + pub time_zone: AcpiTimeZone, + pub dst_status: AcpiDaylightSavingsTimeStatus, +} + +impl AcpiTimestamp { + /// Converts this timestamp into the raw byte format specified in the ACPI spec version 6.4, section 9.18.3 (_GRT), with the Valid bit set. + pub fn as_bytes(&self) -> [u8; core::mem::size_of::()] /* 16 */ { + // Size is guaranteed to be correct by zerocopy, but zerocopy returns as a slice rather than an array, + // and we need to return an owned array, so we need to convert. + // This operation is infallible due to the size guarantee. + #[allow(clippy::expect_used)] + RawAcpiTimestamp::from(self) + .as_bytes() + .try_into() + .expect("Size is guaranteed to be the size of RawAcpiTimestamp") + } + + /// Attempt to parse an ACPI timestamp from a byte slice. Returns an error if the slice does not represent a valid ACPI timestamp as specified in ACPI spec version 6.4, section 9.18.4 (_SRT). + pub fn try_from_bytes(bytes: &[u8]) -> Result { + let raw = RawAcpiTimestamp::ref_from_bytes( + bytes + .get(..core::mem::size_of::()) + .ok_or(DatetimeClockError::Unknown)?, + ) + .map_err(|_| DatetimeClockError::Unknown)?; + + Ok(Self { + datetime: Datetime::new(DatetimeFields { + year: raw.year.get(), + month: Month::try_from(raw.month).map_err(|_| DatetimeClockError::Unknown)?, + day: raw.day, + hour: raw.hour, + minute: raw.minute, + second: raw.second, + nanosecond: (raw.milliseconds.get() as u32) * 1_000_000, + }) + .map_err(|_| DatetimeClockError::Unknown)?, + time_zone: raw + .time_zone + .get() + .try_into() + .map_err(|_| DatetimeClockError::Unknown)?, + dst_status: raw.daylight.try_into().map_err(|_| DatetimeClockError::Unknown)?, + }) + } +} diff --git a/time-alarm-service-interface/src/lib.rs b/time-alarm-service-interface/src/lib.rs new file mode 100644 index 000000000..71a6f621d --- /dev/null +++ b/time-alarm-service-interface/src/lib.rs @@ -0,0 +1,133 @@ +#![no_std] + +mod acpi_timestamp; +pub use acpi_timestamp::{AcpiDaylightSavingsTimeStatus, AcpiTimeZone, AcpiTimeZoneOffset, AcpiTimestamp}; + +use bitfield::bitfield; +use embedded_mcu_hal::time::DatetimeClockError; + +/// The number of seconds before a timer will expire. +#[derive(Clone, Copy, Debug, PartialEq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct AlarmTimerSeconds(pub u32); +impl AlarmTimerSeconds { + /// The alarm is not running and will never expire. + pub const DISABLED: Self = Self(u32::MAX); +} + +impl Default for AlarmTimerSeconds { + fn default() -> Self { + Self::DISABLED + } +} + +// ------------------------------------------------- + +/// If a timer is on the wrong power source when it expires, the number of seconds after switching to the correct +/// power source that must elapse on the correct power source before the timer actually triggers a wake event. +#[derive(Clone, Copy, Debug, PartialEq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct AlarmExpiredWakePolicy(pub u32); +impl AlarmExpiredWakePolicy { + /// The timer will trigger a wake event immediately upon switching to the correct power source. + pub const INSTANTLY: Self = Self(0); + + /// The timer will never trigger a wake event if it expires on the wrong power source, even if it later switches to the correct power source. + pub const NEVER: Self = Self(u32::MAX); +} + +impl Default for AlarmExpiredWakePolicy { + fn default() -> Self { + Self::NEVER + } +} + +// ------------------------------------------------- + +/// ACPI timer ID as defined in the ACPI spec. +#[derive(Clone, Copy, Debug, PartialEq, num_enum::TryFromPrimitive, num_enum::IntoPrimitive)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +#[repr(u32)] +pub enum AcpiTimerId { + /// The timer that is active when the system is on external power. + AcPower = 0, + + /// The timer that is active when the system is on battery power. + DcPower = 1, +} + +impl AcpiTimerId { + /// Gets the timer ID for the other power source. + pub fn get_other_timer_id(&self) -> Self { + match self { + AcpiTimerId::AcPower => AcpiTimerId::DcPower, + AcpiTimerId::DcPower => AcpiTimerId::AcPower, + } + } +} + +bitfield!( + /// Describes the current status of a timer, including whether it has expired and whether it triggered a wake event. + #[derive(Copy, Clone, Default, PartialEq, Eq)] + #[cfg_attr(feature = "defmt", derive(defmt::Format))] + pub struct TimerStatus(u32); + impl Debug; + bool; + pub timer_expired, set_timer_expired: 0; + pub timer_triggered_wake, set_timer_triggered_wake: 1; +); + +// ------------------------------------------------- + +bitfield!( + /// Describes the capabilities of a time-alarm device. Details on semantics of individual fields are available in the ACPI spec, version 6.4, section 9.18.2 + #[derive(Copy, Clone, Default, PartialEq, Eq)] + #[cfg_attr(feature = "defmt", derive(defmt::Format))] + pub struct TimeAlarmDeviceCapabilities(u32); + impl Debug; + bool; + pub ac_wake_implemented, set_ac_wake_implemented: 0; + pub dc_wake_implemented, set_dc_wake_implemented: 1; + pub realtime_implemented, set_realtime_implemented: 2; + pub realtime_accuracy_in_milliseconds, set_realtime_accuracy_in_milliseconds: 3; + pub get_wake_status_supported, set_get_wake_status_supported: 4; + pub ac_s4_wake_supported, set_ac_s4_wake_supported: 5; + pub ac_s5_wake_supported, set_ac_s5_wake_supported: 6; + pub dc_s4_wake_supported, set_dc_s4_wake_supported: 7; + pub dc_s5_wake_supported, set_dc_s5_wake_supported: 8; +); + +/// The interface for a time-alarm service, which implements the ACPI Time and Alarm device specification. +/// See the ACPI spec version 6.4, section 9.18, for more details on the expected behavior of each method. +pub trait TimeAlarmService { + /// Query the capabilities of the time-alarm device. Analogous to ACPI TAD's _GCP method. + fn get_capabilities(&self) -> TimeAlarmDeviceCapabilities; + + /// Query the current time. Analogous to ACPI TAD's _GRT method. + fn get_real_time(&self) -> Result; + + /// Change the current time. Analogous to ACPI TAD's _SRT method. + fn set_real_time(&self, timestamp: AcpiTimestamp) -> Result<(), DatetimeClockError>; + + /// Query the current wake status. Analogous to ACPI TAD's _GWS method. + fn get_wake_status(&self, timer_id: AcpiTimerId) -> TimerStatus; + + /// Clear the current wake status. Analogous to ACPI TAD's _CWS method. + fn clear_wake_status(&self, timer_id: AcpiTimerId); + + /// Configures behavior when the timer expires while the system is on the other power source. Analogous to ACPI TAD's _STP method. + fn set_expired_timer_policy( + &self, + timer_id: AcpiTimerId, + policy: AlarmExpiredWakePolicy, + ) -> Result<(), DatetimeClockError>; + + /// Query current behavior when the timer expires while the system is on the other power source. Analogous to ACPI TAD's _TIP method. + fn get_expired_timer_policy(&self, timer_id: AcpiTimerId) -> AlarmExpiredWakePolicy; + + /// Change the expiry time for the given timer. Analogous to ACPI TAD's _STV method. + fn set_timer_value(&self, timer_id: AcpiTimerId, timer_value: AlarmTimerSeconds) -> Result<(), DatetimeClockError>; + + /// Query the expiry time for the given timer. Analogous to ACPI TAD's _TIV method. + fn get_timer_value(&self, timer_id: AcpiTimerId) -> Result; +} diff --git a/time-alarm-service-relay/Cargo.toml b/time-alarm-service-relay/Cargo.toml new file mode 100644 index 000000000..e098b1904 --- /dev/null +++ b/time-alarm-service-relay/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "time-alarm-service-relay" +version.workspace = true +edition.workspace = true +license.workspace = true +repository.workspace = true + +[package.metadata.cargo-machete] +ignored = ["log"] + +[dependencies] +defmt = { workspace = true, optional = true } +log = { workspace = true, optional = true } +embedded-services.workspace = true +embedded-mcu-hal.workspace = true +num_enum.workspace = true +time-alarm-service-interface.workspace = true + +[features] +defmt = [ + "dep:defmt", + "embedded-mcu-hal/defmt", + "time-alarm-service-interface/defmt", +] +log = ["dep:log", "embedded-services/log"] + +[lints] +workspace = true diff --git a/time-alarm-service-relay/src/lib.rs b/time-alarm-service-relay/src/lib.rs new file mode 100644 index 000000000..ed444a394 --- /dev/null +++ b/time-alarm-service-relay/src/lib.rs @@ -0,0 +1,59 @@ +#![no_std] + +use time_alarm_service_interface::TimeAlarmService; + +mod serialization; +pub use serialization::{AcpiTimeAlarmRequest, AcpiTimeAlarmResponse, AcpiTimeAlarmResult}; + +/// A relay handler that converts MCTP messages into function calls against the time-alarm service. +pub struct TimeAlarmServiceRelayHandler { + service: T, +} + +impl TimeAlarmServiceRelayHandler { + /// Construct a new relay handler that transmits requests to the given time-alarm service. + pub fn new(service: T) -> Self { + Self { service } + } +} + +impl embedded_services::relay::mctp::RelayServiceHandlerTypes for TimeAlarmServiceRelayHandler { + type RequestType = AcpiTimeAlarmRequest; + type ResultType = AcpiTimeAlarmResult; +} + +impl embedded_services::relay::mctp::RelayServiceHandler for TimeAlarmServiceRelayHandler { + async fn process_request(&self, request: Self::RequestType) -> Self::ResultType { + match request { + AcpiTimeAlarmRequest::GetCapabilities => { + Ok(AcpiTimeAlarmResponse::Capabilities(self.service.get_capabilities())) + } + AcpiTimeAlarmRequest::GetRealTime => Ok(AcpiTimeAlarmResponse::RealTime(self.service.get_real_time()?)), + AcpiTimeAlarmRequest::SetRealTime(timestamp) => { + self.service.set_real_time(timestamp)?; + Ok(AcpiTimeAlarmResponse::OkNoData) + } + AcpiTimeAlarmRequest::GetWakeStatus(timer_id) => Ok(AcpiTimeAlarmResponse::TimerStatus( + self.service.get_wake_status(timer_id), + )), + AcpiTimeAlarmRequest::ClearWakeStatus(timer_id) => { + self.service.clear_wake_status(timer_id); + Ok(AcpiTimeAlarmResponse::OkNoData) + } + AcpiTimeAlarmRequest::SetExpiredTimerPolicy(timer_id, timer_policy) => { + self.service.set_expired_timer_policy(timer_id, timer_policy)?; + Ok(AcpiTimeAlarmResponse::OkNoData) + } + AcpiTimeAlarmRequest::GetExpiredTimerPolicy(timer_id) => Ok(AcpiTimeAlarmResponse::WakePolicy( + self.service.get_expired_timer_policy(timer_id), + )), + AcpiTimeAlarmRequest::SetTimerValue(timer_id, timer_value) => { + self.service.set_timer_value(timer_id, timer_value)?; + Ok(AcpiTimeAlarmResponse::OkNoData) + } + AcpiTimeAlarmRequest::GetTimerValue(timer_id) => Ok(AcpiTimeAlarmResponse::TimerSeconds( + self.service.get_timer_value(timer_id)?, + )), + } + } +} diff --git a/time-alarm-service-relay/src/serialization.rs b/time-alarm-service-relay/src/serialization.rs new file mode 100644 index 000000000..7eb3d7abd --- /dev/null +++ b/time-alarm-service-relay/src/serialization.rs @@ -0,0 +1,315 @@ +use core::array::TryFromSliceError; +use embedded_services::relay::{MessageSerializationError, SerializableMessage}; +use time_alarm_service_interface::{ + AcpiDaylightSavingsTimeStatus, AcpiTimerId, AcpiTimestamp, AlarmExpiredWakePolicy, AlarmTimerSeconds, + TimeAlarmDeviceCapabilities, TimerStatus, +}; + +/// Message types for the ACPI Time and Alarm device service. +/// These are directly analogous to the ACPI Time and Alarm device methods. +/// See ACPI Specification 6.4, Section 9.18 "Time and Alarm Device" for additional details on semantics. +#[rustfmt::skip] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum AcpiTimeAlarmRequest { + GetCapabilities, // _GCP + GetRealTime, // _GRT + SetRealTime(AcpiTimestamp), // _SRT + GetWakeStatus(AcpiTimerId), // _GWS + ClearWakeStatus(AcpiTimerId), // _CWS + SetTimerValue(AcpiTimerId, AlarmTimerSeconds), // _STV + GetTimerValue(AcpiTimerId), // _TIV + SetExpiredTimerPolicy(AcpiTimerId, AlarmExpiredWakePolicy), // _STP + GetExpiredTimerPolicy(AcpiTimerId), // _TIP +} + +#[derive(Clone, Copy, Debug, PartialEq, num_enum::IntoPrimitive, num_enum::TryFromPrimitive)] +#[repr(u16)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +enum AcpiTimeAlarmRequestDiscriminant { + GetCapabilities = 1, + GetRealTime = 2, + SetRealTime = 3, + GetWakeStatus = 4, + ClearWakeStatus = 5, + SetTimerValue = 6, + GetTimerValue = 7, + SetExpiredTimerPolicy = 8, + GetExpiredTimerPolicy = 9, +} + +impl SerializableMessage for AcpiTimeAlarmRequest { + fn serialize(self, buffer: &mut [u8]) -> Result { + match self { + Self::GetCapabilities => Ok(0), + Self::GetRealTime => Ok(0), + Self::SetRealTime(timestamp) => { + let serialized = timestamp.as_bytes(); + buffer + .split_at_mut_checked(serialized.len()) + .ok_or(MessageSerializationError::BufferTooSmall)? + .0 + .copy_from_slice(&serialized); + Ok(serialized.len()) + } + Self::GetWakeStatus(timer_id) + | Self::ClearWakeStatus(timer_id) + | Self::GetTimerValue(timer_id) + | Self::GetExpiredTimerPolicy(timer_id) => safe_put_u32(buffer, 0, timer_id.into()), + + Self::SetTimerValue(timer_id, alarm_timer_seconds) => { + safe_put_u32(buffer, 0, timer_id.into())?; + safe_put_u32(buffer, 4, alarm_timer_seconds.0)?; + Ok(8) + } + Self::SetExpiredTimerPolicy(timer_id, alarm_expired_wake_policy) => { + safe_put_u32(buffer, 0, timer_id.into())?; + safe_put_u32(buffer, 4, alarm_expired_wake_policy.0)?; + Ok(8) + } + } + } + + fn discriminant(&self) -> u16 { + match self { + AcpiTimeAlarmRequest::GetCapabilities => AcpiTimeAlarmRequestDiscriminant::GetCapabilities.into(), + AcpiTimeAlarmRequest::GetRealTime => AcpiTimeAlarmRequestDiscriminant::GetRealTime.into(), + AcpiTimeAlarmRequest::SetRealTime(_) => AcpiTimeAlarmRequestDiscriminant::SetRealTime.into(), + AcpiTimeAlarmRequest::GetWakeStatus(_) => AcpiTimeAlarmRequestDiscriminant::GetWakeStatus.into(), + AcpiTimeAlarmRequest::ClearWakeStatus(_) => AcpiTimeAlarmRequestDiscriminant::ClearWakeStatus.into(), + AcpiTimeAlarmRequest::SetTimerValue(_, _) => AcpiTimeAlarmRequestDiscriminant::SetTimerValue.into(), + AcpiTimeAlarmRequest::GetTimerValue(_) => AcpiTimeAlarmRequestDiscriminant::GetTimerValue.into(), + AcpiTimeAlarmRequest::SetExpiredTimerPolicy(_, _) => { + AcpiTimeAlarmRequestDiscriminant::SetExpiredTimerPolicy.into() + } + AcpiTimeAlarmRequest::GetExpiredTimerPolicy(_) => { + AcpiTimeAlarmRequestDiscriminant::GetExpiredTimerPolicy.into() + } + } + } + + fn deserialize(discriminant: u16, buffer: &[u8]) -> Result { + let discriminant = AcpiTimeAlarmRequestDiscriminant::try_from(discriminant) + .map_err(|_| MessageSerializationError::UnknownMessageDiscriminant(discriminant))?; + match discriminant { + AcpiTimeAlarmRequestDiscriminant::GetCapabilities => Ok(AcpiTimeAlarmRequest::GetCapabilities), + AcpiTimeAlarmRequestDiscriminant::GetRealTime => Ok(AcpiTimeAlarmRequest::GetRealTime), + AcpiTimeAlarmRequestDiscriminant::SetRealTime => Ok(AcpiTimeAlarmRequest::SetRealTime( + AcpiTimestamp::try_from_bytes(buffer) + .map_err(|_| MessageSerializationError::InvalidPayload("Could not deserialize timestamp"))?, + )), + _ => { + let (timer_id, buffer) = buffer + .split_at_checked(4) + .ok_or(MessageSerializationError::BufferTooSmall)?; + let timer_id = AcpiTimerId::try_from(u32::from_le_bytes( + timer_id + .try_into() + .map_err(|_| MessageSerializationError::BufferTooSmall)?, + )) + .map_err(|_| MessageSerializationError::InvalidPayload("Could not deserialize timer ID"))?; + + match discriminant { + AcpiTimeAlarmRequestDiscriminant::GetWakeStatus => { + Ok(AcpiTimeAlarmRequest::GetWakeStatus(timer_id)) + } + AcpiTimeAlarmRequestDiscriminant::ClearWakeStatus => { + Ok(AcpiTimeAlarmRequest::ClearWakeStatus(timer_id)) + } + AcpiTimeAlarmRequestDiscriminant::SetTimerValue => Ok(AcpiTimeAlarmRequest::SetTimerValue( + timer_id, + AlarmTimerSeconds(u32::from_le_bytes( + buffer + .try_into() + .map_err(|_| MessageSerializationError::BufferTooSmall)?, + )), + )), + AcpiTimeAlarmRequestDiscriminant::GetTimerValue => { + Ok(AcpiTimeAlarmRequest::GetTimerValue(timer_id)) + } + AcpiTimeAlarmRequestDiscriminant::SetExpiredTimerPolicy => { + Ok(AcpiTimeAlarmRequest::SetExpiredTimerPolicy( + timer_id, + AlarmExpiredWakePolicy(u32::from_le_bytes( + buffer + .try_into() + .map_err(|_| MessageSerializationError::BufferTooSmall)?, + )), + )) + } + AcpiTimeAlarmRequestDiscriminant::GetExpiredTimerPolicy => { + Ok(AcpiTimeAlarmRequest::GetExpiredTimerPolicy(timer_id)) + } + _ => Err(MessageSerializationError::UnknownMessageDiscriminant( + discriminant.into(), + )), + } + } + } + } +} + +// ------------------------------------------------- + +/// Response types for the ACPI Time and Alarm device service. +#[derive(Copy, Clone, Debug, PartialEq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum AcpiTimeAlarmResponse { + /// Response to a _GCP request, containing the capabilities of the time-alarm device. + Capabilities(TimeAlarmDeviceCapabilities), + + /// Response to a _GRT request, containing the current real time according to the time-alarm device. + RealTime(AcpiTimestamp), + + /// Response to a _GWS request, containing the current status of the specified timer. + TimerStatus(TimerStatus), + + /// Response to a _TIP request, containing the current wake policy for the specified timer. + WakePolicy(AlarmExpiredWakePolicy), + + /// Response to a _TIV request, containing the current timer value for the specified timer. + TimerSeconds(AlarmTimerSeconds), + + /// Operation succeeded, but there's no data to return - response to methods that just return a boolean - _SRT, _CWS, _STP, _STV + OkNoData, +} + +#[derive(Copy, Clone, Debug, PartialEq, num_enum::IntoPrimitive, num_enum::TryFromPrimitive)] +#[repr(u16)] +enum AcpiTimeAlarmResponseDiscriminant { + Capabilities = 1, + RealTime = 2, + TimerStatus = 3, + WakePolicy = 4, + TimerSeconds = 5, + OkNoData = 6, +} + +impl SerializableMessage for AcpiTimeAlarmResponse { + fn serialize(self, buffer: &mut [u8]) -> Result { + match self { + Self::Capabilities(capabilities) => safe_put_u32(buffer, 0, capabilities.0), + Self::RealTime(timestamp) => { + let result = timestamp.as_bytes(); + buffer + .split_at_mut_checked(result.len()) + .ok_or(MessageSerializationError::BufferTooSmall)? + .0 + .copy_from_slice(&result); + Ok(result.len()) + } + Self::TimerStatus(timer_status) => safe_put_u32(buffer, 0, timer_status.0), + Self::WakePolicy(wake_policy) => safe_put_u32(buffer, 0, wake_policy.0), + Self::TimerSeconds(timer_seconds) => safe_put_u32(buffer, 0, timer_seconds.0), + Self::OkNoData => Ok(0), + } + } + + fn discriminant(&self) -> u16 { + match self { + Self::Capabilities(_) => AcpiTimeAlarmResponseDiscriminant::Capabilities.into(), + Self::RealTime(_) => AcpiTimeAlarmResponseDiscriminant::RealTime.into(), + Self::TimerStatus(_) => AcpiTimeAlarmResponseDiscriminant::TimerStatus.into(), + Self::WakePolicy(_) => AcpiTimeAlarmResponseDiscriminant::WakePolicy.into(), + Self::TimerSeconds(_) => AcpiTimeAlarmResponseDiscriminant::TimerSeconds.into(), + Self::OkNoData => AcpiTimeAlarmResponseDiscriminant::OkNoData.into(), + } + } + + fn deserialize(discriminant: u16, buffer: &[u8]) -> Result { + let discriminant = AcpiTimeAlarmResponseDiscriminant::try_from(discriminant) + .map_err(|_| MessageSerializationError::UnknownMessageDiscriminant(discriminant))?; + match discriminant { + AcpiTimeAlarmResponseDiscriminant::Capabilities => Ok(Self::Capabilities(TimeAlarmDeviceCapabilities( + safe_get_u32(buffer, 0)?, + ))), + AcpiTimeAlarmResponseDiscriminant::RealTime => { + Ok(Self::RealTime(AcpiTimestamp::try_from_bytes(buffer).map_err(|_| { + MessageSerializationError::InvalidPayload("invalid timestamp") + })?)) + } + AcpiTimeAlarmResponseDiscriminant::TimerStatus => { + Ok(Self::TimerStatus(TimerStatus(safe_get_u32(buffer, 0)?))) + } + AcpiTimeAlarmResponseDiscriminant::WakePolicy => { + Ok(Self::WakePolicy(AlarmExpiredWakePolicy(safe_get_u32(buffer, 0)?))) + } + AcpiTimeAlarmResponseDiscriminant::TimerSeconds => { + Ok(Self::TimerSeconds(AlarmTimerSeconds(safe_get_u32(buffer, 0)?))) + } + AcpiTimeAlarmResponseDiscriminant::OkNoData => Ok(Self::OkNoData), + } + } +} + +#[derive(Clone, Copy, Debug, PartialEq, num_enum::IntoPrimitive, num_enum::TryFromPrimitive)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +#[repr(u16)] +pub enum AcpiTimeAlarmError { + UnspecifiedFailure = 1, +} + +impl SerializableMessage for AcpiTimeAlarmError { + fn serialize(self, _buffer: &mut [u8]) -> Result { + match self { + Self::UnspecifiedFailure => Ok(0), + } + } + + fn discriminant(&self) -> u16 { + (*self).into() + } + + fn deserialize(discriminant: u16, _buffer: &[u8]) -> Result { + let discriminant = AcpiTimeAlarmError::try_from(discriminant) + .map_err(|_| MessageSerializationError::UnknownMessageDiscriminant(discriminant))?; + + match discriminant { + AcpiTimeAlarmError::UnspecifiedFailure => Ok(AcpiTimeAlarmError::UnspecifiedFailure), + } + } +} + +impl From for AcpiTimeAlarmError { + fn from(_error: embedded_mcu_hal::time::DatetimeError) -> Self { + AcpiTimeAlarmError::UnspecifiedFailure + } +} + +impl From> for AcpiTimeAlarmError { + fn from(_error: num_enum::TryFromPrimitiveError) -> Self { + AcpiTimeAlarmError::UnspecifiedFailure + } +} + +impl From for AcpiTimeAlarmError { + fn from(_error: TryFromSliceError) -> Self { + AcpiTimeAlarmError::UnspecifiedFailure + } +} + +impl From for AcpiTimeAlarmError { + fn from(_error: embedded_mcu_hal::time::DatetimeClockError) -> Self { + AcpiTimeAlarmError::UnspecifiedFailure + } +} + +pub type AcpiTimeAlarmResult = Result; + +fn safe_put_u32(buffer: &mut [u8], index: usize, val: u32) -> Result { + let val = val.to_le_bytes(); + buffer + .get_mut(index..index + val.len()) + .ok_or(MessageSerializationError::BufferTooSmall)? + .copy_from_slice(&val); + Ok(val.len()) +} + +fn safe_get_u32(buffer: &[u8], index: usize) -> Result { + let bytes = buffer + .get(index..index + 4) + .ok_or(MessageSerializationError::BufferTooSmall)? + .try_into() + .map_err(|_| MessageSerializationError::BufferTooSmall)?; + Ok(u32::from_le_bytes(bytes)) +} diff --git a/time-alarm-service/Cargo.toml b/time-alarm-service/Cargo.toml new file mode 100644 index 000000000..be4d2970a --- /dev/null +++ b/time-alarm-service/Cargo.toml @@ -0,0 +1,43 @@ +[package] +name = "time-alarm-service" +description = "Time and Alarm service implementation" +version.workspace = true +edition.workspace = true +license.workspace = true +repository.workspace = true + +[package.metadata.cargo-machete] +ignored = ["log", "defmt", "critical-section"] + +[dependencies] +defmt = { workspace = true, optional = true } +log = { workspace = true, optional = true } +embassy-futures.workspace = true +embassy-sync.workspace = true +embassy-time.workspace = true +embedded-mcu-hal.workspace = true +embedded-services.workspace = true +odp-service-common.workspace = true +time-alarm-service-interface.workspace = true +zerocopy.workspace = true + +[features] +defmt = [ + "dep:defmt", + "embedded-services/defmt", + "embassy-time/defmt", + "embassy-sync/defmt", + "time-alarm-service-interface/defmt", +] + +log = ["dep:log", "embedded-services/log", "embassy-time/log"] +mock = [] + +[lints] +workspace = true + +[dev-dependencies] +time-alarm-service = { path = ".", features = ["mock"] } +tokio = { workspace = true, features = ["rt", "macros", "time"] } +critical-section = { version = "1.1", features = ["std"] } +embassy-time = { workspace = true, features = ["std", "generic-queue-8"] } diff --git a/time-alarm-service/src/lib.rs b/time-alarm-service/src/lib.rs new file mode 100644 index 000000000..7fbbc51a9 --- /dev/null +++ b/time-alarm-service/src/lib.rs @@ -0,0 +1,394 @@ +#![cfg_attr(not(test), no_std)] + +use core::cell::RefCell; +use embassy_sync::blocking_mutex::Mutex; +use embassy_sync::signal::Signal; +use embedded_mcu_hal::nvram::NvramStorage; +use embedded_mcu_hal::time::{Datetime, DatetimeClock, DatetimeClockError}; +use embedded_services::GlobalRawMutex; +use embedded_services::{info, warn}; +use time_alarm_service_interface::*; + +mod timer; +use timer::Timer; +#[cfg(feature = "mock")] +pub mod mock; + +// ------------------------------------------------- + +mod time_zone_data { + use crate::AcpiDaylightSavingsTimeStatus; + use crate::AcpiTimeZone; + use crate::NvramStorage; + + pub struct TimeZoneData<'hw> { + // Storage used to back the timezone and DST settings. + storage: &'hw mut dyn NvramStorage<'hw, u32>, + } + + #[repr(C)] + #[derive(zerocopy::FromBytes, zerocopy::IntoBytes, zerocopy::Immutable, Copy, Clone, Debug)] + struct RawTimeZoneData { + tz: i16, + dst: u8, + _padding: u8, + } + + impl<'hw> TimeZoneData<'hw> { + pub fn new(storage: &'hw mut dyn NvramStorage<'hw, u32>) -> Self { + Self { storage } + } + + /// Writes the given time zone and daylight savings time status to NVRAM. + /// + pub fn set_data(&mut self, tz: AcpiTimeZone, dst: AcpiDaylightSavingsTimeStatus) { + let representation = RawTimeZoneData { + tz: tz.into(), + dst: dst.into(), + _padding: 0, + }; + + self.storage.write(zerocopy::transmute!(representation)); + } + + /// Retrieves the current time zone / daylight savings time. + /// If the stored data is invalid, implying that the NVRAM has never been initialized, defaults to + /// (AcpiTimeZone::Unknown, AcpiDaylightSavingsTimeStatus::NotObserved). + /// + pub fn get_data(&self) -> (AcpiTimeZone, AcpiDaylightSavingsTimeStatus) { + let representation: RawTimeZoneData = zerocopy::transmute!(self.storage.read()); + + let time_zone = AcpiTimeZone::try_from(representation.tz).unwrap_or(AcpiTimeZone::Unknown); + let dst_status = AcpiDaylightSavingsTimeStatus::try_from(representation.dst) + .unwrap_or(AcpiDaylightSavingsTimeStatus::NotObserved); + (time_zone, dst_status) + } + } +} +use time_zone_data::TimeZoneData; + +// ------------------------------------------------- + +struct ClockState<'hw> { + datetime_clock: &'hw mut dyn DatetimeClock, + tz_data: TimeZoneData<'hw>, +} + +// ------------------------------------------------- + +struct Timers<'hw> { + ac_timer: Timer<'hw>, + dc_timer: Timer<'hw>, +} + +impl<'hw> Timers<'hw> { + fn get_timer(&self, timer: AcpiTimerId) -> &Timer<'hw> { + match timer { + AcpiTimerId::AcPower => &self.ac_timer, + AcpiTimerId::DcPower => &self.dc_timer, + } + } + + fn new( + ac_expiration_storage: &'hw mut dyn NvramStorage<'hw, u32>, + ac_policy_storage: &'hw mut dyn NvramStorage<'hw, u32>, + dc_expiration_storage: &'hw mut dyn NvramStorage<'hw, u32>, + dc_policy_storage: &'hw mut dyn NvramStorage<'hw, u32>, + ) -> Self { + Self { + ac_timer: Timer::new(ac_expiration_storage, ac_policy_storage), + dc_timer: Timer::new(dc_expiration_storage, dc_policy_storage), + } + } +} + +// ------------------------------------------------- + +/// Parameters required to initialize the time/alarm service. +pub struct InitParams<'hw> { + pub backing_clock: &'hw mut dyn DatetimeClock, + pub tz_storage: &'hw mut dyn NvramStorage<'hw, u32>, + pub ac_expiration_storage: &'hw mut dyn NvramStorage<'hw, u32>, + pub ac_policy_storage: &'hw mut dyn NvramStorage<'hw, u32>, + pub dc_expiration_storage: &'hw mut dyn NvramStorage<'hw, u32>, + pub dc_policy_storage: &'hw mut dyn NvramStorage<'hw, u32>, +} + +/// The main service implementation. Users will interact with this via the Service struct, which is a thin wrapper around this that allows +/// the client to provide storage for the service. +struct ServiceInner<'hw> { + clock_state: Mutex>>, + + // TODO [POWER_SOURCE] signal this whenever the power source changes + power_source_signal: Signal, + + timers: Timers<'hw>, + + capabilities: TimeAlarmDeviceCapabilities, +} + +impl<'hw> ServiceInner<'hw> { + fn new(init_params: InitParams<'hw>) -> Self { + Self { + clock_state: Mutex::new(RefCell::new(ClockState { + datetime_clock: init_params.backing_clock, + tz_data: TimeZoneData::new(init_params.tz_storage), + })), + power_source_signal: Signal::new(), + timers: Timers::new( + init_params.ac_expiration_storage, + init_params.ac_policy_storage, + init_params.dc_expiration_storage, + init_params.dc_policy_storage, + ), + capabilities: { + // TODO [CONFIG] We could consider making some of these user-configurable, e.g. if we want to support devices that don't have a battery + let mut caps = TimeAlarmDeviceCapabilities(0); + caps.set_ac_wake_implemented(true); + caps.set_dc_wake_implemented(true); + caps.set_realtime_implemented(true); + caps.set_realtime_accuracy_in_milliseconds(false); + caps.set_get_wake_status_supported(true); + caps.set_ac_s4_wake_supported(true); + caps.set_ac_s5_wake_supported(true); + caps.set_dc_s4_wake_supported(true); + caps.set_dc_s5_wake_supported(true); + caps + }, + } + } + + /// Query clock capabilities. Analogous to ACPI TAD's _GRT method. + fn get_capabilities(&self) -> TimeAlarmDeviceCapabilities { + self.capabilities + } + + /// Query the current time. Analogous to ACPI TAD's _GRT method. + fn get_real_time(&self) -> Result { + self.clock_state.lock(|clock_state| { + let clock_state = clock_state.borrow(); + let datetime = clock_state.datetime_clock.now()?; + let (time_zone, dst_status) = clock_state.tz_data.get_data(); + Ok(AcpiTimestamp { + datetime, + time_zone, + dst_status, + }) + }) + } + + /// Change the current time. Analogous to ACPI TAD's _SRT method. + fn set_real_time(&self, timestamp: AcpiTimestamp) -> Result<(), DatetimeClockError> { + self.clock_state.lock(|clock_state| { + let mut clock_state = clock_state.borrow_mut(); + clock_state.datetime_clock.set(timestamp.datetime)?; + clock_state.tz_data.set_data(timestamp.time_zone, timestamp.dst_status); + Ok(()) + }) + } + + /// Query the current wake status. Analogous to ACPI TAD's _GWS method. + fn get_wake_status(&self, timer_id: AcpiTimerId) -> TimerStatus { + self.timers.get_timer(timer_id).get_wake_status() + } + + /// Clear the current wake status. Analogous to ACPI TAD's _CWS method. + fn clear_wake_status(&self, timer_id: AcpiTimerId) { + self.timers.get_timer(timer_id).clear_wake_status(); + } + + /// Configures behavior when the timer expires while the system is on the other power source. Analogous to ACPI TAD's _STP method. + fn set_expired_timer_policy( + &self, + timer_id: AcpiTimerId, + policy: AlarmExpiredWakePolicy, + ) -> Result<(), DatetimeClockError> { + self.timers + .get_timer(timer_id) + .set_timer_wake_policy(&self.clock_state, policy)?; + Ok(()) + } + + /// Query current behavior when the timer expires while the system is on the other power source. Analogous to ACPI TAD's _TIP method. + fn get_expired_timer_policy(&self, timer_id: AcpiTimerId) -> AlarmExpiredWakePolicy { + self.timers.get_timer(timer_id).get_timer_wake_policy() + } + + /// Change the expiry time for the given timer. Analogous to ACPI TAD's _STV method. + fn set_timer_value(&self, timer_id: AcpiTimerId, timer_value: AlarmTimerSeconds) -> Result<(), DatetimeClockError> { + let new_expiration_time = match timer_value { + AlarmTimerSeconds::DISABLED => None, + AlarmTimerSeconds(secs) => { + let current_time = self + .clock_state + .lock(|clock_state| clock_state.borrow().datetime_clock.now())?; + + Some(Datetime::from_unix_timestamp( + current_time.unix_timestamp() + u64::from(secs), + )) + } + }; + + self.timers + .get_timer(timer_id) + .set_expiration_time(&self.clock_state, new_expiration_time)?; + Ok(()) + } + + /// Query the expiry time for the given timer. Analogous to ACPI TAD's _TIV method. + fn get_timer_value(&self, timer_id: AcpiTimerId) -> Result { + let expiration_time = self.timers.get_timer(timer_id).get_expiration_time(); + match expiration_time { + Some(expiration_time) => { + let current_time = self + .clock_state + .lock(|clock_state| clock_state.borrow().datetime_clock.now())?; + + Ok(AlarmTimerSeconds( + expiration_time + .unix_timestamp() + .saturating_sub(current_time.unix_timestamp()) as u32, + )) + } + None => Ok(AlarmTimerSeconds::DISABLED), + } + } + + async fn handle_power_source_updates(&'hw self) -> ! { + loop { + let new_power_source = self.power_source_signal.wait().await; + info!("[Time/Alarm] Power source changed to {:?}", new_power_source); + + self.timers + .get_timer(new_power_source.get_other_timer_id()) + .set_active(&self.clock_state, false); + self.timers + .get_timer(new_power_source) + .set_active(&self.clock_state, true); + } + } + + async fn handle_timer(&'hw self, timer_id: AcpiTimerId) -> ! { + let timer = self.timers.get_timer(timer_id); + loop { + timer.wait_until_wake(&self.clock_state).await; + self.timers + .get_timer(timer_id.get_other_timer_id()) + .set_timer_wake_policy(&self.clock_state, AlarmExpiredWakePolicy::NEVER) + .unwrap_or_else(|e| { + warn!( + "[Time/Alarm] Failed to update wake policy on timer expiry - this should never happen: {:?}", + e + ); + }); + + warn!( + "[Time/Alarm] Timer {:?} expired and would trigger a wake now, but the power service is not yet implemented so will currently do nothing", + timer_id + ); + // TODO [COMMS] We can't currently trigger a wake because the power service isn't implemented yet - when it is, we need to notify it here + } + } +} + +/// The memory resources required by the time/alarm service. +#[derive(Default)] +pub struct Resources<'hw> { + inner: Option>, +} + +/// A task runner for the time/alarm service. Users of the service must run this object in an embassy task or similar async execution context. +pub struct Runner<'hw> { + service: &'hw ServiceInner<'hw>, +} + +impl<'hw> odp_service_common::runnable_service::ServiceRunner<'hw> for Runner<'hw> { + /// Run the service. + async fn run(self) -> embedded_services::Never { + loop { + embassy_futures::select::select3( + self.service.handle_power_source_updates(), + self.service.handle_timer(AcpiTimerId::AcPower), + self.service.handle_timer(AcpiTimerId::DcPower), + ) + .await; + } + } +} + +/// Control handle for the time-alarm service. Use this to manipulate the time on the service. +#[derive(Clone, Copy)] +pub struct Service<'hw> { + inner: &'hw ServiceInner<'hw>, +} + +impl<'hw> TimeAlarmService for Service<'hw> { + fn get_capabilities(&self) -> TimeAlarmDeviceCapabilities { + self.inner.get_capabilities() + } + + /// Query the current time. Analogous to ACPI TAD's _GRT method. + fn get_real_time(&self) -> Result { + self.inner.get_real_time() + } + + /// Change the current time. Analogous to ACPI TAD's _SRT method. + fn set_real_time(&self, timestamp: AcpiTimestamp) -> Result<(), DatetimeClockError> { + self.inner.set_real_time(timestamp) + } + + /// Query the current wake status. Analogous to ACPI TAD's _GWS method. + fn get_wake_status(&self, timer_id: AcpiTimerId) -> TimerStatus { + self.inner.get_wake_status(timer_id) + } + + /// Clear the current wake status. Analogous to ACPI TAD's _CWS method. + fn clear_wake_status(&self, timer_id: AcpiTimerId) { + self.inner.clear_wake_status(timer_id); + } + + /// Configures behavior when the timer expires while the system is on the other power source. Analogous to ACPI TAD's _STP method. + fn set_expired_timer_policy( + &self, + timer_id: AcpiTimerId, + policy: AlarmExpiredWakePolicy, + ) -> Result<(), DatetimeClockError> { + self.inner.set_expired_timer_policy(timer_id, policy) + } + + /// Query current behavior when the timer expires while the system is on the other power source. Analogous to ACPI TAD's _TIP method. + fn get_expired_timer_policy(&self, timer_id: AcpiTimerId) -> AlarmExpiredWakePolicy { + self.inner.get_expired_timer_policy(timer_id) + } + + /// Change the expiry time for the given timer. Analogous to ACPI TAD's _STV method. + fn set_timer_value(&self, timer_id: AcpiTimerId, timer_value: AlarmTimerSeconds) -> Result<(), DatetimeClockError> { + self.inner.set_timer_value(timer_id, timer_value) + } + + /// Query the expiry time for the given timer. Analogous to ACPI TAD's _TIV method. + fn get_timer_value(&self, timer_id: AcpiTimerId) -> Result { + self.inner.get_timer_value(timer_id) + } +} + +impl<'hw> odp_service_common::runnable_service::Service<'hw> for Service<'hw> { + type Runner = Runner<'hw>; + type ErrorType = DatetimeClockError; + type InitParams = InitParams<'hw>; + type Resources = Resources<'hw>; + + async fn new( + service_storage: &'hw mut Resources<'hw>, + init_params: Self::InitParams, + ) -> Result<(Self, Runner<'hw>), DatetimeClockError> { + let service = service_storage.inner.insert(ServiceInner::new(init_params)); + + // TODO [POWER_SOURCE] we need to subscribe to messages that tell us if we're on AC or DC power so we can decide which alarms to trigger, but those notifications are not yet implemented - revisit when they are. + // TODO [POWER_SOURCE] if it's possible to learn which power source is active at init time, we should set that one active rather than defaulting to the AC timer. + service.timers.ac_timer.start(&service.clock_state, true)?; + service.timers.dc_timer.start(&service.clock_state, false)?; + + Ok((Self { inner: service }, Runner { service })) + } +} diff --git a/time-alarm-service/src/mock.rs b/time-alarm-service/src/mock.rs new file mode 100644 index 000000000..3bb2bd02d --- /dev/null +++ b/time-alarm-service/src/mock.rs @@ -0,0 +1,120 @@ +#![allow(dead_code)] // We have some functionality in these mocks that isn't used yet but will be in future tests. + +use embedded_mcu_hal::nvram::NvramStorage; +use embedded_mcu_hal::time::{Datetime, DatetimeClock, DatetimeClockError}; + +// Used for `cargo test` runs in an std environment +#[cfg(test)] +fn now_seconds() -> u64 { + // Panic safety: Only used in tests so panicking is acceptable here + #[allow(clippy::expect_used)] + std::time::SystemTime::now() + .duration_since(std::time::SystemTime::UNIX_EPOCH) + .expect("System clock was adjusted during test") + .as_secs() +} + +// Allows us to use this mock in no_std contexts +// Note: `now` will always reflect time as starting from the beginning +// of UNIX time (1970), and not current wall-clock time. This is sufficient for its use case +// since it provides a consistent baseline and allows time to advance, but is something to be aware of. +#[cfg(not(test))] +fn now_seconds() -> u64 { + embassy_time::Instant::now().as_secs() +} + +pub enum MockDatetimeClock { + Running { seconds_offset: i64 }, + Paused { frozen_time: Datetime }, +} + +impl MockDatetimeClock { + /// New `MockDatetimeClock` in which time is advancing. + pub fn new_running() -> Self { + Self::Running { seconds_offset: 0 } + } + + /// New `MockDatetimeClock` in which time is paused. + pub fn new_paused() -> Self { + Self::Paused { + frozen_time: Datetime::from_unix_timestamp(now_seconds()), + } + } + + /// Stop time from advancing. + pub fn pause(&mut self) { + if let Self::Running { .. } = self { + *self = MockDatetimeClock::Paused { + // Panic safety: Mocks aren't used in production code, so panicking is acceptable here + #[allow(clippy::unwrap_used)] + frozen_time: self.now().unwrap(), + }; + } + } + + /// Resume time advancing. + pub fn resume(&mut self) { + if let Self::Paused { frozen_time } = self { + let target_secs = frozen_time.unix_timestamp() as i64; + *self = MockDatetimeClock::Running { + seconds_offset: target_secs - now_seconds() as i64, + }; + } + } +} + +impl DatetimeClock for MockDatetimeClock { + fn now(&self) -> Result { + match self { + MockDatetimeClock::Paused { frozen_time } => Ok(*frozen_time), + MockDatetimeClock::Running { seconds_offset } => { + let adjusted_seconds = now_seconds() as i64 + seconds_offset; + Ok(Datetime::from_unix_timestamp(adjusted_seconds as u64)) + } + } + } + + fn set(&mut self, datetime: Datetime) -> Result<(), DatetimeClockError> { + match self { + MockDatetimeClock::Paused { .. } => { + *self = MockDatetimeClock::Paused { frozen_time: datetime }; + Ok(()) + } + MockDatetimeClock::Running { .. } => { + let target_secs = datetime.unix_timestamp() as i64; + *self = MockDatetimeClock::Running { + seconds_offset: target_secs - now_seconds() as i64, + }; + Ok(()) + } + } + } + + fn resolution_hz(&self) -> u32 { + 1 + } +} + +pub struct MockNvramStorage<'a> { + value: u32, + _phantom: core::marker::PhantomData<&'a ()>, +} + +impl<'a> MockNvramStorage<'a> { + pub fn new(initial_value: u32) -> Self { + Self { + value: initial_value, + _phantom: core::marker::PhantomData, + } + } +} + +impl<'a> NvramStorage<'a, u32> for MockNvramStorage<'a> { + fn read(&self) -> u32 { + self.value + } + + fn write(&mut self, value: u32) { + self.value = value; + } +} diff --git a/time-alarm-service/src/timer.rs b/time-alarm-service/src/timer.rs new file mode 100644 index 000000000..27034e5e5 --- /dev/null +++ b/time-alarm-service/src/timer.rs @@ -0,0 +1,393 @@ +use crate::{AlarmExpiredWakePolicy, ClockState, TimerStatus}; +use core::cell::RefCell; +use embassy_futures::select::{Either, select}; +use embassy_sync::{blocking_mutex::Mutex, signal::Signal}; +use embedded_mcu_hal::nvram::NvramStorage; +use embedded_mcu_hal::time::{Datetime, DatetimeClockError}; +use embedded_services::{GlobalRawMutex, error}; + +/// Represents where in the timer lifecycle the current timer is +#[derive(Copy, Clone, Debug, PartialEq)] +enum WakeState { + /// Timer is not active + Clear, + /// Timer is active and programmed with the original expiration time + Armed, + /// Timer is active but expired when on the wrong power source + /// Includes the time at which we started running down the policy delay and the number of seconds that had already elapsed on the policy delay when we started waiting + ExpiredWaitingForPolicyDelay(Datetime, u32), + /// Timer is active and waiting for power source to be consistent with the timer type. + /// Includes the number of seconds that we've spent in the ExpiredWaitingForPolicyDelay state for so far. + ExpiredWaitingForPowerSource(u32), + /// Expired while the policy was set to NEVER, so the timer is effectively dead until reprogrammed + ExpiredOrphaned, +} + +mod persistent_storage { + use crate::NvramStorage; + use crate::{AlarmExpiredWakePolicy, Datetime}; + + pub struct PersistentStorage<'hw> { + /// When the timer is programmed to expire, or None if the timer is not set + /// This can't be part of the wake_state because we need to be able to report its value for _CWS even when the timer has expired and + /// we're handling the power source policy. + expiration_time_storage: &'hw mut dyn NvramStorage<'hw, u32>, + + // Persistent storage for the AlarmExpiredWakePolicy + wake_policy_storage: &'hw mut dyn NvramStorage<'hw, u32>, + } + + impl<'hw> PersistentStorage<'hw> { + pub fn new( + expiration_time_storage: &'hw mut dyn NvramStorage<'hw, u32>, + wake_policy_storage: &'hw mut dyn NvramStorage<'hw, u32>, + ) -> Self { + Self { + expiration_time_storage, + wake_policy_storage, + } + } + + const NO_EXPIRATION_TIME: u32 = u32::MAX; + + pub fn get_timer_wake_policy(&self) -> AlarmExpiredWakePolicy { + AlarmExpiredWakePolicy(self.wake_policy_storage.read()) + } + + pub fn set_timer_wake_policy(&mut self, wake_policy: AlarmExpiredWakePolicy) { + self.wake_policy_storage.write(wake_policy.0); + } + + pub fn get_expiration_time(&self) -> Option { + match self.expiration_time_storage.read() { + Self::NO_EXPIRATION_TIME => None, + secs => Some(Datetime::from_unix_timestamp(secs.into())), + } + } + + pub fn set_expiration_time(&mut self, expiration_time: Option) { + match expiration_time { + Some(dt) => { + // This won't overflow until 2106, which is acceptable for our use case. + self.expiration_time_storage.write(dt.unix_timestamp() as u32); + } + None => { + self.expiration_time_storage.write(Self::NO_EXPIRATION_TIME); + } + } + } + } +} +use persistent_storage::PersistentStorage; + +struct TimerState<'hw> { + persistent_storage: PersistentStorage<'hw>, + + wake_state: WakeState, + + timer_status: TimerStatus, + + // Whether or not this timer is currently active (i.e. the system is on the power source this timer manages) + // Even if it's not active, it still counts down if it's programmed - it just won't trigger a wake event if it expires while inactive. + is_active: bool, +} + +pub(crate) struct Timer<'hw> { + timer_state: Mutex>>, + + timer_signal: Signal>, +} + +impl<'hw> Timer<'hw> { + pub fn new( + expiration_time_storage: &'hw mut dyn NvramStorage<'hw, u32>, + wake_policy_storage: &'hw mut dyn NvramStorage<'hw, u32>, + ) -> Self { + Self { + timer_state: Mutex::new(RefCell::new(TimerState { + persistent_storage: PersistentStorage::new(expiration_time_storage, wake_policy_storage), + wake_state: WakeState::Clear, + timer_status: Default::default(), + is_active: false, + })), + timer_signal: Signal::new(), + } + } + + pub fn start( + &self, + clock_state: &Mutex>>, + active: bool, + ) -> Result<(), DatetimeClockError> { + self.set_timer_wake_policy( + clock_state, + self.timer_state + .lock(|timer_state| timer_state.borrow().persistent_storage.get_timer_wake_policy()), + )?; + + self.set_expiration_time( + clock_state, + self.timer_state + .lock(|timer_state| timer_state.borrow().persistent_storage.get_expiration_time()), + )?; + + self.set_active(clock_state, active); + + Ok(()) + } + + pub fn get_wake_status(&self) -> TimerStatus { + self.timer_state.lock(|timer_state| { + let timer_state = timer_state.borrow(); + timer_state.timer_status + }) + } + + pub fn clear_wake_status(&self) { + self.timer_state.lock(|timer_state| { + let mut timer_state = timer_state.borrow_mut(); + timer_state.timer_status = Default::default(); + }); + } + + pub fn get_timer_wake_policy(&self) -> AlarmExpiredWakePolicy { + self.timer_state + .lock(|timer_state| timer_state.borrow().persistent_storage.get_timer_wake_policy()) + } + + pub fn set_timer_wake_policy( + &self, + clock_state: &Mutex>>, + wake_policy: AlarmExpiredWakePolicy, + ) -> Result<(), DatetimeClockError> { + self.timer_state.lock(|timer_state| { + let mut timer_state = timer_state.borrow_mut(); + if let WakeState::ExpiredWaitingForPolicyDelay(_, _) = timer_state.wake_state { + timer_state.wake_state = WakeState::ExpiredWaitingForPolicyDelay(Self::now(clock_state)?, 0); + self.timer_signal.signal(Some(wake_policy.0)); + } + + timer_state.persistent_storage.set_timer_wake_policy(wake_policy); + + Ok(()) + }) + } + + pub fn set_expiration_time( + &self, + clock_state: &Mutex>>, + expiration_time: Option, + ) -> Result<(), DatetimeClockError> { + self.timer_state.lock(|timer_state| { + let mut timer_state = timer_state.borrow_mut(); + + // Per ACPI 6.4 section 9.18.1: "The status of wake timers can be reset by setting the wake alarm". + timer_state.timer_status = Default::default(); + + match expiration_time { + Some(dt) => { + // Note: If the expiration time was in the past, this will immediately trigger the timer to expire. + self.timer_signal.signal(Some( + dt.unix_timestamp() + .saturating_sub(Self::now(clock_state)?.unix_timestamp()) as u32, // The ACPI spec doesn't provide a facility to program a timer more than u32::MAX seconds in the future, so this cast is safe + )); + + timer_state.persistent_storage.set_expiration_time(expiration_time); + timer_state.wake_state = WakeState::Armed; + } + None => self.clear_expiration_time(&mut timer_state), + } + + Ok(()) + }) + } + + pub fn get_expiration_time(&self) -> Option { + self.timer_state + .lock(|timer_state| timer_state.borrow().persistent_storage.get_expiration_time()) + } + + pub fn set_active(&self, clock_state: &Mutex>>, is_active: bool) { + self.timer_state.lock(|timer_state| { + let mut timer_state = timer_state.borrow_mut(); + + let was_active = timer_state.is_active; + timer_state.is_active = is_active; + + if was_active == is_active { + return; // No change + } + + if !was_active { + if let WakeState::ExpiredWaitingForPowerSource(seconds_already_elapsed) = timer_state.wake_state { + match Self::now(clock_state) { + Ok(now) => { + timer_state.wake_state = + WakeState::ExpiredWaitingForPolicyDelay(now, seconds_already_elapsed); + self.timer_signal.signal(Some( + timer_state + .persistent_storage + .get_timer_wake_policy() + .0 + .saturating_sub(seconds_already_elapsed), + )); + } + Err(_) => { + // This should never happen, because it means the clock is not working after we've successfully initialized (which + // requires the clock to be working). + // If it does, though, we don't have a way to communicate failure to the host PC at this point, so we'll just + // forego the power source policy and wake the device immediately. + error!( + "[Time/Alarm] Failed to get current datetime when transitioning timer to active state" + ); + timer_state.wake_state = WakeState::Armed; + self.timer_signal.signal(Some(0)); + } + } + } + } else if let WakeState::ExpiredWaitingForPolicyDelay(wait_start_time, seconds_elapsed_before_wait) = + timer_state.wake_state + { + let total_seconds_elapsed_on_policy_delay = match Self::now(clock_state) { + Ok(now) => { + seconds_elapsed_before_wait + + (now + .unix_timestamp() + .saturating_sub(wait_start_time.unix_timestamp()) + as u32) // The ACPI spec expresses timeouts in terms of u32s - it's impossible to schedule a timer u32::MAX seconds in the future + } + Err(_) => { + // This should never happen, because it means the clock is not working after we've successfully initialized (which + // requires the clock to be working). + // If it does, though, we don't have a way to communicate failure to the host PC at this point, so we'll just + // pretend that the entire policy delay has elapsed. This will trigger an immediate wake when the power source becomes active again. + error!( + "[Time/Alarm] Failed to get current datetime when transitioning expired timer waiting for policy delay to inactive state" + ); + u32::MAX + } + }; + + timer_state.wake_state = WakeState::ExpiredWaitingForPowerSource(total_seconds_elapsed_on_policy_delay); + self.timer_signal.signal(None); + } + }); + } + + pub(crate) async fn wait_until_wake(&self, clock_state: &Mutex>>) { + loop { + let mut wait_duration: Option = self.timer_signal.wait().await; + 'waiting_for_timer: loop { + match wait_duration { + Some(seconds) => { + match select( + embassy_time::Timer::after_secs(seconds.into()), + self.timer_signal.wait(), + ) + .await + { + Either::First(()) => { + if self.process_expired_timer(clock_state) { + return; + } + } + Either::Second(new_wait_duration) => { + wait_duration = new_wait_duration; + } + } + } + None => { + // Wait until a new timer command comes in + break 'waiting_for_timer; + } + } + } + } + } + + /// Handles state changes for when the timer expires (figuring out what to do based on the current power source, etc). + /// Returns true if the timer's expiry indicates that a wake event should be signaled to the host. + fn process_expired_timer(&self, clock_state: &Mutex>>) -> bool { + self.timer_state.lock(|timer_state| { + let mut timer_state = timer_state.borrow_mut(); + + match timer_state.wake_state { + // Clear: timer was disarmed right as we were waking - nothing to do. + // ExpiredOrphaned: shouldn't happen, but if we're in this state the timer should be dead, so nothing to do. + // ExpiredWaitingForPowerSource: shouldn't happen, but if we're in this state the timer is still waiting for power source so nothing to do. + WakeState::Clear | WakeState::ExpiredOrphaned | WakeState::ExpiredWaitingForPowerSource(_) => { + return false; + } + + WakeState::Armed | WakeState::ExpiredWaitingForPolicyDelay(_, _) => { + let expiration_time = match timer_state.persistent_storage.get_expiration_time() { + Some(expiration_time) => expiration_time, + None => { + error!( + "[Time/Alarm] Timer expired when no expiration time was set - this should never happen" + ); + return false; + } + }; + + match Self::now(clock_state) { + Ok(now) => { + if now.unix_timestamp() < expiration_time.unix_timestamp() { + // Time hasn't actually passed the mark yet - this can happen if we were reprogrammed with a different time right as the old timer was expiring. Reset the timer. + timer_state.wake_state = WakeState::Armed; + self.timer_signal.signal(Some( + expiration_time.unix_timestamp().saturating_sub(now.unix_timestamp()) as u32, + )); + return false; + } + } + Err(_) => { + // This should never happen, because it means the clock is not working after we've successfully initialized (which + // requires the clock to be working). + // If it does, though, we don't have a way to communicate failure to the host PC at this point, so we'll just + // wake the device immediately on the assumption that the alarm has actually expired. This gets it wrong in the case + // where the timer is reprogrammed immediately as it expires, but that's an extremely rare case and we can't do better + // than that if our clock is broken. + error!("[Time/Alarm] Failed to get current datetime when processing expired timer"); + } + } + + timer_state.timer_status.set_timer_expired(true); + if timer_state.is_active { + timer_state.timer_status.set_timer_triggered_wake(true); + timer_state + .persistent_storage + .set_timer_wake_policy(AlarmExpiredWakePolicy::NEVER); + self.clear_expiration_time(&mut timer_state); + return true; + } else { + if timer_state.persistent_storage.get_timer_wake_policy() == AlarmExpiredWakePolicy::NEVER { + timer_state.wake_state = WakeState::ExpiredOrphaned; + return false; + } + + if let WakeState::ExpiredWaitingForPolicyDelay(_, _) = timer_state.wake_state { + timer_state.wake_state = WakeState::ExpiredWaitingForPowerSource( + timer_state.persistent_storage.get_timer_wake_policy().0, + ); + } else { + timer_state.wake_state = WakeState::ExpiredWaitingForPowerSource(0); + } + } + } + } + + false + }) + } + + fn clear_expiration_time(&self, timer_state: &mut TimerState) { + timer_state.persistent_storage.set_expiration_time(None); + timer_state.wake_state = WakeState::Clear; + self.timer_signal.signal(None); + } + + fn now(clock_state: &Mutex>>) -> Result { + clock_state.lock(|clock_state| clock_state.borrow().datetime_clock.now()) + } +} diff --git a/time-alarm-service/tests/tad_test.rs b/time-alarm-service/tests/tad_test.rs new file mode 100644 index 000000000..a9d1bed35 --- /dev/null +++ b/time-alarm-service/tests/tad_test.rs @@ -0,0 +1,110 @@ +// Panicking is how tests communicate failure, so we need to allow it here. +#![allow(clippy::unwrap_used)] +#![allow(clippy::expect_used)] + +#[cfg(test)] +mod test { + use embassy_time::Timer; + use embedded_mcu_hal::time::{Datetime, DatetimeClock}; + use odp_service_common::runnable_service::{Service, ServiceRunner}; + + use time_alarm_service_interface::{AcpiDaylightSavingsTimeStatus, AcpiTimeZone, AcpiTimestamp, TimeAlarmService}; + + use time_alarm_service::mock::*; + + #[tokio::test] + async fn test_get_time() { + let mut tz_storage = MockNvramStorage::new(0); + let mut ac_exp_storage = MockNvramStorage::new(0); + let mut ac_pol_storage = MockNvramStorage::new(0); + let mut dc_exp_storage = MockNvramStorage::new(0); + let mut dc_pol_storage = MockNvramStorage::new(0); + + let mut clock = MockDatetimeClock::new_running(); + let mut storage = Default::default(); + + let (service, runner) = time_alarm_service::Service::new( + &mut storage, + time_alarm_service::InitParams { + backing_clock: &mut clock, + tz_storage: &mut tz_storage, + ac_expiration_storage: &mut ac_exp_storage, + ac_policy_storage: &mut ac_pol_storage, + dc_expiration_storage: &mut dc_exp_storage, + dc_policy_storage: &mut dc_pol_storage, + }, + ) + .await + .unwrap(); + + // We need to have the service have non-static lifetime for our test use cases so we can have + // multiple test cases. This means we can't spawn tasks that require 'static lifetime. + // + // Instead, we'll use select! to run the worker task in the local scope, which lets us take + // borrows from the stack and not require 'static. The worker task is expected to + // return !, so we should go until the test arm completes and then shut down. + // + tokio::select! { + _ = runner.run() => unreachable!("time alarm service task finished unexpectedly"), + _ = async { + let delay_secs = 2; + let begin = service.get_real_time().unwrap(); + println!("Current time from service: {begin:?}"); + Timer::after(embassy_time::Duration::from_millis(delay_secs * 1000)).await; + let end = service.get_real_time().unwrap(); + println!("Current time from service after delay: {end:?}"); + assert!(end.datetime.unix_timestamp() - begin.datetime.unix_timestamp() <= delay_secs + 1); + assert!(end.datetime.unix_timestamp() - begin.datetime.unix_timestamp() >= delay_secs - 1); + } => {} + } + } + + #[tokio::test] + async fn test_set_time() { + let mut tz_storage = MockNvramStorage::new(0); + let mut ac_exp_storage = MockNvramStorage::new(0); + let mut ac_pol_storage = MockNvramStorage::new(0); + let mut dc_exp_storage = MockNvramStorage::new(0); + let mut dc_pol_storage = MockNvramStorage::new(0); + + let mut clock = MockDatetimeClock::new_paused(); + const TEST_UNIX_TIME: u64 = 1_234_567_890; + clock.set(Datetime::from_unix_timestamp(TEST_UNIX_TIME)).unwrap(); + + let mut storage = Default::default(); + + let (service, runner) = time_alarm_service::Service::new( + &mut storage, + time_alarm_service::InitParams { + backing_clock: &mut clock, + tz_storage: &mut tz_storage, + ac_expiration_storage: &mut ac_exp_storage, + ac_policy_storage: &mut ac_pol_storage, + dc_expiration_storage: &mut dc_exp_storage, + dc_policy_storage: &mut dc_pol_storage, + }, + ) + .await + .unwrap(); + + tokio::select! { + _ = runner.run() => unreachable!("time alarm service task finished unexpectedly"), + _ = async { + // Clock is paused, so time shouldn't advance unless we set it. + let begin = service.get_real_time().unwrap(); + assert_eq!(begin.datetime.unix_timestamp(), TEST_UNIX_TIME); + + let target_timestamp = AcpiTimestamp { + datetime: Datetime::from_unix_timestamp(TEST_UNIX_TIME), + time_zone: AcpiTimeZone::Unknown, + dst_status: AcpiDaylightSavingsTimeStatus::Adjusted, + }; + service.set_real_time(target_timestamp).unwrap(); + + let actual_timestamp = service.get_real_time().unwrap(); + assert_eq!(actual_timestamp, target_timestamp); + + } => {} + } + } +} diff --git a/type-c-interface/Cargo.toml b/type-c-interface/Cargo.toml new file mode 100644 index 000000000..f8565d547 --- /dev/null +++ b/type-c-interface/Cargo.toml @@ -0,0 +1,31 @@ +[package] +name = "type-c-interface" +version.workspace = true +edition.workspace = true +license.workspace = true +repository.workspace = true + +[package.metadata.cargo-machete] +ignored = ["log"] + +[dependencies] +bitfield.workspace = true +log = { workspace = true, optional = true } +defmt = { workspace = true, optional = true } +embedded-services.workspace = true +embedded-usb-pd.workspace = true +power-policy-interface.workspace = true +heapless.workspace = true + +[lints] +workspace = true + +[features] +default = [] +defmt = [ + "dep:defmt", + "embedded-services/defmt", + "embedded-usb-pd/defmt", + "power-policy-interface/defmt", +] +log = ["dep:log", "embedded-services/log", "power-policy-interface/log"] diff --git a/type-c-interface/src/control/dp.rs b/type-c-interface/src/control/dp.rs new file mode 100644 index 000000000..21c616521 --- /dev/null +++ b/type-c-interface/src/control/dp.rs @@ -0,0 +1,32 @@ +//! DP-related control types +/// DisplayPort pin configuration +#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct DpPinConfig { + /// 4L DP connection using USBC-USBC cable (Pin Assignment C) + pub pin_c: bool, + /// 2L USB + 2L DP connection using USBC-USBC cable (Pin Assignment D) + pub pin_d: bool, + /// 4L DP connection using USBC-DP cable (Pin Assignment E) + pub pin_e: bool, +} + +/// DisplayPort status data +#[derive(Copy, Clone, Debug)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct DpStatus { + /// DP alt-mode entered + pub alt_mode_entered: bool, + /// Get DP DFP pin config + pub dfp_d_pin_cfg: DpPinConfig, +} + +/// DisplayPort configuration data +#[derive(Copy, Clone, Debug)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct DpConfig { + /// DP alt-mode enabled + pub enable: bool, + /// Set DP DFP pin config + pub dfp_d_pin_cfg: DpPinConfig, +} diff --git a/type-c-interface/src/control/mod.rs b/type-c-interface/src/control/mod.rs new file mode 100644 index 000000000..426d42af1 --- /dev/null +++ b/type-c-interface/src/control/mod.rs @@ -0,0 +1,10 @@ +//! Shared types for controlling a PD port +pub mod dp; +pub mod pd; +pub mod power; +pub mod retimer; +pub mod svid; +pub mod tbt; +pub mod type_c; +pub mod usb; +pub mod vdm; diff --git a/type-c-interface/src/control/pd.rs b/type-c-interface/src/control/pd.rs new file mode 100644 index 000000000..a4b9c8a2a --- /dev/null +++ b/type-c-interface/src/control/pd.rs @@ -0,0 +1,84 @@ +//! Control types for core PD functionality + +use embedded_usb_pd::{ + DataRole, PlugOrientation, PowerRole, + pdinfo::{AltMode, PowerPathStatus}, + type_c::ConnectionState, +}; + +/// Port status +#[derive(Copy, Clone, Debug)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct PortStatus { + /// Current available source contract + pub available_source_contract: Option, + /// Current available sink contract + pub available_sink_contract: Option, + /// Current connection state + pub connection_state: Option, + /// Port partner supports dual-power roles + pub dual_power: bool, + /// plug orientation + pub plug_orientation: PlugOrientation, + /// power role + pub power_role: PowerRole, + /// data role + pub data_role: DataRole, + /// Active alt-modes + pub alt_mode: AltMode, + /// Power path status + pub power_path: PowerPathStatus, + /// EPR mode active + pub epr: bool, + /// Port partner is unconstrained + pub unconstrained_power: bool, +} + +impl PortStatus { + /// Create a new blank port status + /// Needed because default() is not const + pub const fn new() -> Self { + Self { + available_source_contract: None, + available_sink_contract: None, + connection_state: None, + dual_power: false, + plug_orientation: PlugOrientation::CC1, + power_role: PowerRole::Sink, + data_role: DataRole::Dfp, + alt_mode: AltMode::none(), + power_path: PowerPathStatus::none(), + epr: false, + unconstrained_power: false, + } + } + + /// Check if the port is connected + pub fn is_connected(&self) -> bool { + matches!( + self.connection_state, + Some(ConnectionState::Attached) + | Some(ConnectionState::DebugAccessory) + | Some(ConnectionState::AudioAccessory) + ) + } + + /// Check if a debug accessory is connected + pub fn is_debug_accessory(&self) -> bool { + matches!(self.connection_state, Some(ConnectionState::DebugAccessory)) + } +} + +impl Default for PortStatus { + fn default() -> Self { + Self::new() + } +} + +/// PD state-machine configuration +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +#[derive(Debug, Clone, Default, Copy, PartialEq)] +pub struct PdStateMachineConfig { + /// Enable or disable the PD state-machine + pub enabled: bool, +} diff --git a/type-c-interface/src/control/power.rs b/type-c-interface/src/control/power.rs new file mode 100644 index 000000000..e4388e95c --- /dev/null +++ b/type-c-interface/src/control/power.rs @@ -0,0 +1,20 @@ +//! General power related control types + +/// System power state +/// +/// Used to notify the PD controller of the current system power state, +/// which triggers Application Configuration updates (e.g., crossbar reconfiguration). +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum SystemPowerState { + /// S0 - System fully running + S0, + /// S3 - Suspend to RAM + S3, + /// S4 - Hibernate + S4, + /// S5 - Soft off + S5, + /// S0ix - Modern standby / Connected standby + S0ix, +} diff --git a/type-c-interface/src/control/retimer.rs b/type-c-interface/src/control/retimer.rs new file mode 100644 index 000000000..e3460638d --- /dev/null +++ b/type-c-interface/src/control/retimer.rs @@ -0,0 +1,11 @@ +//! Retimer related control types + +/// Retimer update state +#[derive(Copy, Clone, Debug, PartialEq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum RetimerFwUpdateState { + /// Retimer FW Update Inactive + Inactive, + /// Retimer FW Update Active + Active, +} diff --git a/type-c-interface/src/control/svid.rs b/type-c-interface/src/control/svid.rs new file mode 100644 index 000000000..259b2f533 --- /dev/null +++ b/type-c-interface/src/control/svid.rs @@ -0,0 +1,62 @@ +use embedded_usb_pd::vdm::structured::Svid; +use heapless::Vec; + +/// Response from the `Discover SVIDs REQ` message and the PortCommandData::GetDiscoveredSvids command. +// Could be changed to hold the heapless::Vec directly if they were Copy or if PortResponseData was not Copy +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct DiscoveredSvids { + num_sop: usize, + sop: [Svid; Self::NUM_SVIDS], + + num_sop_prime: usize, + sop_prime: [Svid; Self::NUM_SVIDS], +} + +impl DiscoveredSvids { + /// The number of SVIDs that can be reported in a single DiscoveredSvids response. + pub const NUM_SVIDS: usize = 8; + + /// Create a new response object from `sop` and `sop_prime`. + pub fn new(sop: Vec, sop_prime: Vec) -> Self { + let num_sop = sop.len(); + let num_sop_prime = sop_prime.len(); + + let mut sop_array = [Svid(0); _]; + for (svid, dest) in sop.into_iter().zip(sop_array.iter_mut()) { + *dest = svid; + } + + let mut sop_prime_array = [Svid(0); _]; + for (svid, dest) in sop_prime.into_iter().zip(sop_prime_array.iter_mut()) { + *dest = svid; + } + + Self { + num_sop, + sop: sop_array, + num_sop_prime, + sop_prime: sop_prime_array, + } + } + + /// Returns the number of SVIDs discovered on the SOP port partner. + pub fn number_sop_svids(&self) -> usize { + self.num_sop + } + + /// Returns an iterator over the SVIDs discovered on the SOP port partner. + pub fn svid_sop(&self) -> impl ExactSizeIterator { + self.sop.iter().copied().take(self.num_sop) + } + + /// Returns the number of SVIDs discovered on the SOP' cable plug. + pub fn number_sop_prime_svids(&self) -> usize { + self.num_sop_prime + } + + /// Returns an iterator over the SVIDs discovered on the SOP' cable plug. + pub fn svid_sop_prime(&self) -> impl ExactSizeIterator { + self.sop_prime.iter().copied().take(self.num_sop_prime) + } +} diff --git a/type-c-interface/src/control/tbt.rs b/type-c-interface/src/control/tbt.rs new file mode 100644 index 000000000..192ed6c23 --- /dev/null +++ b/type-c-interface/src/control/tbt.rs @@ -0,0 +1,9 @@ +//! Thunderbolt-related control types + +/// Thunderbolt control configuration +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +#[derive(Debug, Clone, Default, Copy, PartialEq)] +pub struct TbtConfig { + /// Enable Thunderbolt + pub tbt_enabled: bool, +} diff --git a/type-c-interface/src/control/type_c.rs b/type-c-interface/src/control/type_c.rs new file mode 100644 index 000000000..0e13c7b86 --- /dev/null +++ b/type-c-interface/src/control/type_c.rs @@ -0,0 +1,15 @@ +//! Type-C related control types + +/// TypeC State Machine +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum TypeCStateMachineState { + /// Sink state machine only + Sink, + /// Source state machine only + Source, + /// DRP state machine + Drp, + /// Disabled + Disabled, +} diff --git a/type-c-interface/src/control/usb.rs b/type-c-interface/src/control/usb.rs new file mode 100644 index 000000000..237eb947d --- /dev/null +++ b/type-c-interface/src/control/usb.rs @@ -0,0 +1,23 @@ +//! USB related control types + +/// USB control configuration +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct UsbControlConfig { + /// Enable USB2 data path + pub usb2_enabled: bool, + /// Enable USB3 data path + pub usb3_enabled: bool, + /// Enable USB4 data path + pub usb4_enabled: bool, +} + +impl Default for UsbControlConfig { + fn default() -> Self { + Self { + usb2_enabled: true, + usb3_enabled: true, + usb4_enabled: true, + } + } +} diff --git a/type-c-interface/src/control/vdm.rs b/type-c-interface/src/control/vdm.rs new file mode 100644 index 000000000..1bd2ebe3b --- /dev/null +++ b/type-c-interface/src/control/vdm.rs @@ -0,0 +1,93 @@ +//! VDM-related control types + +/// Length of the Other VDM data +pub const OTHER_VDM_LEN: usize = 29; +/// Length of the Attention VDM data +pub const ATTN_VDM_LEN: usize = 9; +/// maximum number of data objects in a VDM +pub const MAX_NUM_DATA_OBJECTS: usize = 7; // 7 VDOs of 4 bytes each + +/// Other Vdm data +#[derive(Copy, Clone, Debug)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct OtherVdm { + /// Other VDM data + pub data: [u8; OTHER_VDM_LEN], +} + +impl Default for OtherVdm { + fn default() -> Self { + Self { + data: [0; OTHER_VDM_LEN], + } + } +} + +impl From for [u8; OTHER_VDM_LEN] { + fn from(vdm: OtherVdm) -> Self { + vdm.data + } +} + +impl From<[u8; OTHER_VDM_LEN]> for OtherVdm { + fn from(data: [u8; OTHER_VDM_LEN]) -> Self { + Self { data } + } +} + +/// Attention Vdm data +#[derive(Copy, Clone, Debug)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct AttnVdm { + /// Attention VDM data + pub data: [u8; ATTN_VDM_LEN], +} + +impl Default for AttnVdm { + fn default() -> Self { + Self { + data: [0; ATTN_VDM_LEN], + } + } +} + +impl From for [u8; ATTN_VDM_LEN] { + fn from(vdm: AttnVdm) -> Self { + vdm.data + } +} + +impl From<[u8; ATTN_VDM_LEN]> for AttnVdm { + fn from(data: [u8; ATTN_VDM_LEN]) -> Self { + Self { data } + } +} + +/// Send VDM data +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct SendVdm { + /// initiating a VDM sequence + pub initiator: bool, + /// VDO count + pub vdo_count: u8, + /// VDO data + pub vdo_data: [u32; MAX_NUM_DATA_OBJECTS], +} + +impl SendVdm { + /// Create a new blank VDM + pub const fn new() -> Self { + Self { + initiator: false, + vdo_count: 0, + vdo_data: [0; MAX_NUM_DATA_OBJECTS], + } + } +} + +impl Default for SendVdm { + fn default() -> Self { + Self::new() + } +} diff --git a/type-c-interface/src/controller/electrical_disconnect.rs b/type-c-interface/src/controller/electrical_disconnect.rs new file mode 100644 index 000000000..f4d54df74 --- /dev/null +++ b/type-c-interface/src/controller/electrical_disconnect.rs @@ -0,0 +1,17 @@ +use core::num::NonZeroU8; + +use embedded_usb_pd::{LocalPortId, PdError}; + +use crate::controller::pd::Pd; + +pub trait ElectricalDisconnect: Pd { + /// Execute an electrical disconnect on the given port, if supported by the controller. + /// + /// If `reconnect_time_s` is provided, the controller should automatically reconnect the port after the specified time + /// has elapsed. If `reconnect_time_s` is [`None`], the port should remain disconnected until manually reconnected. + fn execute_electrical_disconnect( + &mut self, + port: LocalPortId, + reconnect_time_s: Option, + ) -> impl Future>; +} diff --git a/type-c-interface/src/controller/max_sink_voltage.rs b/type-c-interface/src/controller/max_sink_voltage.rs new file mode 100644 index 000000000..706282331 --- /dev/null +++ b/type-c-interface/src/controller/max_sink_voltage.rs @@ -0,0 +1,15 @@ +use embedded_usb_pd::{LocalPortId, PdError}; + +use crate::controller::pd::Pd; + +/// Functionality related to setting the maximum sink voltage for a port. +pub trait MaxSinkVoltage: Pd { + /// Set the maximum sink voltage for the given port + /// + /// This may trigger a renegotiation + fn set_max_sink_voltage( + &mut self, + port: LocalPortId, + voltage_mv: Option, + ) -> impl Future>; +} diff --git a/type-c-interface/src/controller/mod.rs b/type-c-interface/src/controller/mod.rs new file mode 100644 index 000000000..9268ba28f --- /dev/null +++ b/type-c-interface/src/controller/mod.rs @@ -0,0 +1,22 @@ +//! Module for PD controller related code + +use embedded_services::named::Named; +use embedded_usb_pd::PdError; + +pub mod electrical_disconnect; +pub mod max_sink_voltage; +pub mod pd; +pub mod power; +pub mod retimer; +pub mod type_c; + +/// Controller ID +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct ControllerId(pub u8); + +/// PD controller trait +pub trait Controller: Named { + /// Reset the controller + fn reset_controller(&mut self) -> impl Future>; +} diff --git a/type-c-interface/src/controller/pd.rs b/type-c-interface/src/controller/pd.rs new file mode 100644 index 000000000..287fef713 --- /dev/null +++ b/type-c-interface/src/controller/pd.rs @@ -0,0 +1,85 @@ +use embedded_services::named::Named; +use embedded_usb_pd::vdm::structured::command::discover_identity::{sop, sop_prime}; +use embedded_usb_pd::{LocalPortId, PdError, ado::Ado}; + +use crate::control::{ + dp::{DpConfig, DpStatus}, + pd::{PdStateMachineConfig, PortStatus}, + svid::DiscoveredSvids, + tbt::TbtConfig, + usb::UsbControlConfig, + vdm::{AttnVdm, OtherVdm, SendVdm}, +}; + +/// Trait for basic functionality from the PD spec. +pub trait Pd: Named { + /// Returns the port status + fn get_port_status(&mut self, port: LocalPortId) -> impl Future>; + + /// Clear the dead battery flag for the given port. + fn clear_dead_battery_flag(&mut self, port: LocalPortId) -> impl Future>; + + /// Enable or disable sink path + fn enable_sink_path(&mut self, port: LocalPortId, enable: bool) -> impl Future>; + + /// Get current PD alert + fn get_pd_alert(&mut self, port: LocalPortId) -> impl Future, PdError>>; + + /// Set port unconstrained status + fn set_unconstrained_power( + &mut self, + port: LocalPortId, + unconstrained: bool, + ) -> impl Future>; + + /// Get the Rx Other VDM data for the given port + fn get_other_vdm(&mut self, port: LocalPortId) -> impl Future>; + /// Get the Rx Attention VDM data for the given port + fn get_attn_vdm(&mut self, port: LocalPortId) -> impl Future>; + /// Send a VDM to the given port + fn send_vdm(&mut self, port: LocalPortId, tx_vdm: SendVdm) -> impl Future>; + /// Execute PD Data Reset for the given port + fn execute_drst(&mut self, port: LocalPortId) -> impl Future>; + /// Execute a Hard Reset on the given port. + fn hard_reset(&mut self, port: LocalPortId) -> impl Future>; + + /// Get DisplayPort status for the given port + fn get_dp_status(&mut self, port: LocalPortId) -> impl Future>; + /// Set DisplayPort configuration for the given port + fn set_dp_config(&mut self, port: LocalPortId, config: DpConfig) -> impl Future>; + + /// Set Thunderbolt configuration for the given port + fn set_tbt_config(&mut self, port: LocalPortId, config: TbtConfig) -> impl Future>; + + /// Set USB control configuration for the given port + fn set_usb_control( + &mut self, + port: LocalPortId, + config: UsbControlConfig, + ) -> impl Future>; + + /// Get the given port's discovered SVIDs + fn get_discovered_svids(&mut self, port: LocalPortId) -> impl Future>; + + /// Get the latest response from the Discover Identity command targeting SOP. + fn get_discover_identity_sop_response( + &mut self, + port: LocalPortId, + ) -> impl Future>; + + /// Get the latest response from the Discover Identity command targeting SOP'. + fn get_discover_identity_sop_prime_response( + &mut self, + port: LocalPortId, + ) -> impl Future>; +} + +/// PD state machine related controller functionality +pub trait StateMachine: Pd { + /// Set PD state-machine configuration for the given port + fn set_pd_state_machine_config( + &mut self, + port: LocalPortId, + config: PdStateMachineConfig, + ) -> impl Future>; +} diff --git a/type-c-interface/src/controller/power.rs b/type-c-interface/src/controller/power.rs new file mode 100644 index 000000000..c485258d6 --- /dev/null +++ b/type-c-interface/src/controller/power.rs @@ -0,0 +1,15 @@ +use embedded_services::named::Named; +use embedded_usb_pd::{LocalPortId, PdError}; + +/// System power state related controller functionality +pub trait SystemPowerStateStatus: Named { + /// Set the system power state on the given port. + /// + /// This notifies the PD controller of the current system power state, + /// which triggers Application Configuration updates (e.g., crossbar reconfiguration). + fn set_system_power_state_status( + &mut self, + port: LocalPortId, + state: crate::control::power::SystemPowerState, + ) -> impl Future>; +} diff --git a/type-c-interface/src/controller/retimer.rs b/type-c-interface/src/controller/retimer.rs new file mode 100644 index 000000000..e676141db --- /dev/null +++ b/type-c-interface/src/controller/retimer.rs @@ -0,0 +1,21 @@ +use embedded_services::named::Named; +use embedded_usb_pd::{LocalPortId, PdError}; + +use crate::control::retimer::RetimerFwUpdateState; + +/// Retimer-related functionality +pub trait Retimer: Named { + /// Returns the retimer fw update state + fn get_rt_fw_update_status( + &mut self, + port: LocalPortId, + ) -> impl Future>; + /// Set retimer fw update state + fn set_rt_fw_update_state(&mut self, port: LocalPortId) -> impl Future>; + /// Clear retimer fw update state + fn clear_rt_fw_update_state(&mut self, port: LocalPortId) -> impl Future>; + /// Set retimer compliance + fn set_rt_compliance(&mut self, port: LocalPortId) -> impl Future>; + /// Reconfigure the retimer for the given port. + fn reconfigure_retimer(&mut self, port: LocalPortId) -> impl Future>; +} diff --git a/type-c-interface/src/controller/type_c.rs b/type-c-interface/src/controller/type_c.rs new file mode 100644 index 000000000..fd7271c55 --- /dev/null +++ b/type-c-interface/src/controller/type_c.rs @@ -0,0 +1,13 @@ +use embedded_usb_pd::{LocalPortId, PdError}; + +use crate::{control::type_c::TypeCStateMachineState, controller::pd::Pd}; + +/// Type-C state machine related controller functionality +pub trait StateMachine: Pd { + /// Set Type-C state-machine configuration for the given port + fn set_type_c_state_machine_config( + &mut self, + port: LocalPortId, + state: TypeCStateMachineState, + ) -> impl Future>; +} diff --git a/type-c-interface/src/lib.rs b/type-c-interface/src/lib.rs new file mode 100644 index 000000000..aaa86cafa --- /dev/null +++ b/type-c-interface/src/lib.rs @@ -0,0 +1,7 @@ +//! Interface for type-C service. +#![no_std] +pub mod control; +pub mod controller; +pub mod port; +pub mod service; +pub mod ucsi; diff --git a/type-c-interface/src/port/electrical_disconnect.rs b/type-c-interface/src/port/electrical_disconnect.rs new file mode 100644 index 000000000..d0b62d437 --- /dev/null +++ b/type-c-interface/src/port/electrical_disconnect.rs @@ -0,0 +1,16 @@ +use core::num::NonZeroU8; + +use embedded_usb_pd::PdError; + +use crate::port::pd::Pd; + +pub trait ElectricalDisconnect: Pd { + /// Execute an electrical disconnect on this port, if supported by the controller. + /// + /// If `reconnect_time_s` is provided, the controller should automatically reconnect the port after the specified time + /// has elapsed. If `reconnect_time_s` is [`None`], the port should remain disconnected until manually reconnected. + fn execute_electrical_disconnect( + &mut self, + reconnect_time_s: Option, + ) -> impl Future>; +} diff --git a/embedded-service/src/type_c/event.rs b/type-c-interface/src/port/event.rs similarity index 61% rename from embedded-service/src/type_c/event.rs rename to type-c-interface/src/port/event.rs index df7dbd4bc..d46c76b1f 100644 --- a/embedded-service/src/type_c/event.rs +++ b/type-c-interface/src/port/event.rs @@ -1,18 +1,19 @@ //! This module provides TCPM event types and bitfields. //! Hardware typically uses bitfields to store pending events/interrupts so we provide generic versions of these. -//! [`PortStatusChanged`] contains events related to the overall port state (plug state, power contract, etc). -//! Processing these events typically requires acessing similar registers so they are grouped together. -//! [`PortNotification`] contains events that are typically more message-like (PD alerts, VDMs, etc) and can be processed independently. -//! Consequently [`PortNotification`] implements iterator traits to allow for processing these events as a stream. -use super::error; +//! [`PortStatusEventBitfield`] contains events related to the overall port state (plug state, power contract, etc). +//! Processing these events typically requires accessing similar registers so they are grouped together. +//! [`PortNotificationEventBitfield`] contains events that are typically more message-like (PD alerts, VDMs, etc) and can be processed independently. +//! Consequently [`PortNotificationEventBitfield`] implements iterator traits to allow for processing these events as a stream. use bitfield::bitfield; -use bitvec::BitArr; + +use crate::control::vdm::AttnVdm; +use crate::control::vdm::OtherVdm; bitfield! { /// Raw bitfield of possible port status events #[derive(Copy, Clone, PartialEq, Eq, Default)] #[cfg_attr(feature = "defmt", derive(defmt::Format))] - struct PortStatusChangedRaw(u16); + struct PortStatusEventBitfieldRaw(u16); impl Debug; /// Plug inserted or removed pub u8, plug_inserted_or_removed, set_plug_inserted_or_removed: 0, 0; @@ -34,31 +35,23 @@ bitfield! { pub u8, pd_hard_reset, set_pd_hard_reset: 8, 8; } -/// Port pending errors -#[derive(Clone, Copy, Debug, PartialEq)] -#[cfg_attr(feature = "defmt", derive(defmt::Format))] -pub enum PortPendingError { - /// Invalid port - InvalidPort(usize), -} - /// Port status change events /// This is a type-safe wrapper around the raw bitfield /// These events are related to the overall port state and typically need to be considered together. #[derive(Copy, Clone, Debug, PartialEq, Eq, Default)] #[cfg_attr(feature = "defmt", derive(defmt::Format))] -pub struct PortStatusChanged(PortStatusChangedRaw); +pub struct PortStatusEventBitfield(PortStatusEventBitfieldRaw); -impl PortStatusChanged { +impl PortStatusEventBitfield { /// Create a new PortEventKind with no pending events pub const fn none() -> Self { - Self(PortStatusChangedRaw(0)) + Self(PortStatusEventBitfieldRaw(0)) } /// Returns the union of self and other - pub fn union(self, other: PortStatusChanged) -> PortStatusChanged { + pub fn union(self, other: PortStatusEventBitfield) -> PortStatusEventBitfield { // This spacing is what rustfmt wants - PortStatusChanged(PortStatusChangedRaw(self.0.0 | other.0.0)) + PortStatusEventBitfield(PortStatusEventBitfieldRaw(self.0.0 | other.0.0)) } /// Returns true if a plug was inserted or removed @@ -156,7 +149,7 @@ bitfield! { /// Raw bitfield of possible port notification events #[derive(Copy, Clone, PartialEq, Eq)] #[cfg_attr(feature = "defmt", derive(defmt::Format))] - struct PortNotificationRaw(u16); + struct PortNotificationEventBitfieldRaw(u16); impl Debug; /// PD alert pub u8, alert, set_alert: 0, 0; @@ -181,18 +174,18 @@ bitfield! { /// These events are unrelated to the overall port state and each other. #[derive(Copy, Clone, Debug, PartialEq, Eq)] #[cfg_attr(feature = "defmt", derive(defmt::Format))] -pub struct PortNotification(PortNotificationRaw); +pub struct PortNotificationEventBitfield(PortNotificationEventBitfieldRaw); -impl PortNotification { +impl PortNotificationEventBitfield { /// Create a new PortNotification with no pending events pub const fn none() -> Self { - Self(PortNotificationRaw(0)) + Self(PortNotificationEventBitfieldRaw(0)) } /// Returns the union of self and other - pub fn union(self, other: PortNotification) -> PortNotification { + pub fn union(self, other: PortNotificationEventBitfield) -> PortNotificationEventBitfield { // This spacing is what rustfmt wants - PortNotification(PortNotificationRaw(self.0.0 | other.0.0)) + PortNotificationEventBitfield(PortNotificationEventBitfieldRaw(self.0.0 | other.0.0)) } /// Returns true if an alert was received @@ -290,10 +283,26 @@ pub enum VdmNotification { OtherReceived, } -/// Individual port notifications +/// VDM event data +#[derive(Copy, Clone, Debug)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum VdmData { + /// Entered custom mode + Entered(OtherVdm), + /// Exited custom mode + Exited(OtherVdm), + /// Received a vendor-defined other message + ReceivedOther(OtherVdm), + /// Received a vendor-defined attention message + ReceivedAttn(AttnVdm), +} + +/// Enum to contain all port event variants #[derive(Copy, Clone, Debug, PartialEq, Eq)] #[cfg_attr(feature = "defmt", derive(defmt::Format))] -pub enum PortNotificationSingle { +pub enum PortEvent { + /// Port status change events + StatusChanged(PortStatusEventBitfield), /// PD alert Alert, /// VDM @@ -306,34 +315,34 @@ pub enum PortNotificationSingle { DpStatusUpdate, } -impl Iterator for PortNotification { - type Item = PortNotificationSingle; +impl Iterator for PortNotificationEventBitfield { + type Item = PortEvent; fn next(&mut self) -> Option { if self.alert() { self.set_alert(false); - Some(PortNotificationSingle::Alert) + Some(PortEvent::Alert) } else if self.custom_mode_entered() { self.set_custom_mode_entered(false); - Some(PortNotificationSingle::Vdm(VdmNotification::Entered)) + Some(PortEvent::Vdm(VdmNotification::Entered)) } else if self.custom_mode_exited() { self.set_custom_mode_exited(false); - Some(PortNotificationSingle::Vdm(VdmNotification::Exited)) + Some(PortEvent::Vdm(VdmNotification::Exited)) } else if self.custom_mode_attention_received() { self.set_custom_mode_attention_received(false); - Some(PortNotificationSingle::Vdm(VdmNotification::AttentionReceived)) + Some(PortEvent::Vdm(VdmNotification::AttentionReceived)) } else if self.custom_mode_other_vdm_received() { self.set_custom_mode_other_vdm_received(false); - Some(PortNotificationSingle::Vdm(VdmNotification::OtherReceived)) + Some(PortEvent::Vdm(VdmNotification::OtherReceived)) } else if self.discover_mode_completed() { self.set_discover_mode_completed(false); - Some(PortNotificationSingle::DiscoverModeCompleted) + Some(PortEvent::DiscoverModeCompleted) } else if self.usb_mux_error_recovery() { self.set_usb_mux_error_recovery(false); - Some(PortNotificationSingle::UsbMuxErrorRecovery) + Some(PortEvent::UsbMuxErrorRecovery) } else if self.dp_status_update() { self.set_dp_status_update(false); - Some(PortNotificationSingle::DpStatusUpdate) + Some(PortEvent::DpStatusUpdate) } else { None } @@ -343,304 +352,146 @@ impl Iterator for PortNotification { /// Overall port event type #[derive(Copy, Clone, Debug, PartialEq, Eq)] #[cfg_attr(feature = "defmt", derive(defmt::Format))] -pub struct PortEvent { +pub struct PortEventBitfield { /// Port status change events - pub status: PortStatusChanged, + pub status: PortStatusEventBitfield, /// Port notification events - pub notification: PortNotification, + pub notification: PortNotificationEventBitfield, } -impl PortEvent { +impl PortEventBitfield { /// Creates a new PortEvent with no pending events pub const fn none() -> Self { Self { - status: PortStatusChanged::none(), - notification: PortNotification::none(), + status: PortStatusEventBitfield::none(), + notification: PortNotificationEventBitfield::none(), } } /// Returns the union of self and other - pub fn union(self, other: PortEvent) -> PortEvent { - PortEvent { + pub fn union(self, other: PortEventBitfield) -> PortEventBitfield { + PortEventBitfield { status: self.status.union(other.status), notification: self.notification.union(other.notification), } } } -impl Default for PortEvent { +impl Default for PortEventBitfield { fn default() -> Self { Self::none() } } -impl From for PortEvent { - fn from(status: PortStatusChanged) -> Self { +impl From for PortEventBitfield { + fn from(status: PortStatusEventBitfield) -> Self { Self { status, - notification: PortNotification::none(), + notification: PortNotificationEventBitfield::none(), } } } -impl From for PortEvent { - fn from(notification: PortNotification) -> Self { +impl From for PortEventBitfield { + fn from(notification: PortNotificationEventBitfield) -> Self { Self { - status: PortStatusChanged::none(), + status: PortStatusEventBitfield::none(), notification, } } } -/// Bit vector type to store pending port events -type PortPendingVec = BitArr!(for 32, in u32); - -/// Pending port events -/// -/// This type works using usize to allow use with both global and local port IDs. -#[derive(Copy, Clone, Debug, PartialEq, Eq)] -#[repr(transparent)] -pub struct PortPending(PortPendingVec); - -impl PortPending { - /// Creates a new PortPending with no pending ports - pub const fn none() -> Self { - Self(PortPendingVec::ZERO) - } - - /// Returns true if there are no pending ports - pub fn is_none(&self) -> bool { - self.0 == PortPendingVec::ZERO - } - - /// Marks the given port as pending - pub fn pend_port(&mut self, port: usize) -> Result<(), PortPendingError> { - if port >= self.0.len() { - return Err(PortPendingError::InvalidPort(port)); - } - self.0.set(port, true); - - Ok(()) - } - - /// Marks the indexes given by the iterator as pending - pub fn pend_ports>(&mut self, iter: I) { - for port in iter { - if self.pend_port(port).is_err() { - error!("Error pending port {}", port); - } - } - } - - /// Clears the pending status of the given port - pub fn clear_port(&mut self, port: usize) -> Result<(), PortPendingError> { - if port >= self.0.len() { - return Err(PortPendingError::InvalidPort(port)); - } - - self.0.set(port, false); - Ok(()) - } - - /// Returns true if the given port is pending - pub fn is_pending(&self, port: usize) -> Result { - Ok(*self.0.get(port).ok_or(PortPendingError::InvalidPort(port))?) - } - - /// Returns a combination of the current pending ports and other - pub fn union(&self, other: PortPending) -> PortPending { - PortPending(self.0 | other.0) - } - - /// Returns the number of bits in Self - #[allow(clippy::len_without_is_empty)] - pub fn len(&self) -> usize { - self.0.len() - } -} - -impl From for u32 { - fn from(flags: PortPending) -> Self { - flags.0.data[0] - } -} - -impl Default for PortPending { - fn default() -> Self { - Self::none() - } -} - -impl FromIterator for PortPending { - fn from_iter>(iter: T) -> Self { - let mut flags = PortPending::none(); - flags.pend_ports(iter); - flags - } -} - -/// An iterator over the pending port event flags -#[derive(Copy, Clone)] -pub struct PortPendingIter { - /// The flags being iterated over - flags: PortPending, - /// The current index in the flags - index: usize, -} - -impl Iterator for PortPendingIter { - type Item = usize; - - fn next(&mut self) -> Option { - while self.index < self.flags.len() { - let port_index = self.index; - self.index += 1; - if self.flags.is_pending(port_index).unwrap_or(false) { - if self.flags.clear_port(port_index).is_ok() { - return Some(port_index); - } else { - continue; - } - } - } - None - } -} - -impl IntoIterator for PortPending { - type Item = usize; - type IntoIter = PortPendingIter; - - fn into_iter(self) -> PortPendingIter { - PortPendingIter { flags: self, index: 0 } - } -} - #[cfg(test)] #[allow(clippy::unwrap_used)] mod tests { use super::*; - #[test] - fn test_port_event_flags_iter() { - let mut pending = PortPending::none(); - - pending.pend_port(0).unwrap(); - pending.pend_port(1).unwrap(); - pending.pend_port(2).unwrap(); - pending.pend_port(10).unwrap(); - pending.pend_port(23).unwrap(); - pending.pend_port(31).unwrap(); - - let result = pending.pend_port(32); - let expected = Err(PortPendingError::InvalidPort(32)); - assert_eq!(expected, result); - - let mut iter = pending.into_iter(); - assert_eq!(iter.next(), Some(0)); - assert_eq!(iter.next(), Some(1)); - assert_eq!(iter.next(), Some(2)); - assert_eq!(iter.next(), Some(10)); - assert_eq!(iter.next(), Some(23)); - assert_eq!(iter.next(), Some(31)); - assert_eq!(iter.next(), None); - } - #[test] fn test_port_notification_iter_all() { - let mut notification = PortNotification::none(); + let mut notification = PortNotificationEventBitfield::none(); notification.set_alert(true); notification.set_custom_mode_entered(true); - assert_eq!(notification.next(), Some(PortNotificationSingle::Alert)); - assert_eq!( - notification.next(), - Some(PortNotificationSingle::Vdm(VdmNotification::Entered)) - ); + assert_eq!(notification.next(), Some(PortEvent::Alert)); + assert_eq!(notification.next(), Some(PortEvent::Vdm(VdmNotification::Entered))); assert_eq!(notification.next(), None); } #[test] fn test_port_notification_iter_alert() { - let mut notification = PortNotification::none(); + let mut notification = PortNotificationEventBitfield::none(); notification.set_alert(true); - assert_eq!(notification.next(), Some(PortNotificationSingle::Alert)); + assert_eq!(notification.next(), Some(PortEvent::Alert)); assert_eq!(notification.next(), None); } #[test] fn test_port_notification_iter_custom_mode_entered() { - let mut notification = PortNotification::none(); + let mut notification = PortNotificationEventBitfield::none(); notification.set_custom_mode_entered(true); - assert_eq!( - notification.next(), - Some(PortNotificationSingle::Vdm(VdmNotification::Entered)) - ); + assert_eq!(notification.next(), Some(PortEvent::Vdm(VdmNotification::Entered))); assert_eq!(notification.next(), None); } #[test] fn test_port_notification_iter_custom_mode_exited() { - let mut notification = PortNotification::none(); + let mut notification = PortNotificationEventBitfield::none(); notification.set_custom_mode_exited(true); - assert_eq!( - notification.next(), - Some(PortNotificationSingle::Vdm(VdmNotification::Exited)) - ); + assert_eq!(notification.next(), Some(PortEvent::Vdm(VdmNotification::Exited))); assert_eq!(notification.next(), None); } #[test] fn test_port_notification_iter_custom_mode_attention_received() { - let mut notification = PortNotification::none(); + let mut notification = PortNotificationEventBitfield::none(); notification.set_custom_mode_attention_received(true); assert_eq!( notification.next(), - Some(PortNotificationSingle::Vdm(VdmNotification::AttentionReceived)) + Some(PortEvent::Vdm(VdmNotification::AttentionReceived)) ); assert_eq!(notification.next(), None); } #[test] fn test_port_notification_iter_custom_mode_other_vdm_received() { - let mut notification = PortNotification::none(); + let mut notification = PortNotificationEventBitfield::none(); notification.set_custom_mode_other_vdm_received(true); assert_eq!( notification.next(), - Some(PortNotificationSingle::Vdm(VdmNotification::OtherReceived)) + Some(PortEvent::Vdm(VdmNotification::OtherReceived)) ); assert_eq!(notification.next(), None); } #[test] fn test_port_notification_iter_discover_mode_completed() { - let mut notification = PortNotification::none(); + let mut notification = PortNotificationEventBitfield::none(); notification.set_discover_mode_completed(true); - assert_eq!(notification.next(), Some(PortNotificationSingle::DiscoverModeCompleted)); + assert_eq!(notification.next(), Some(PortEvent::DiscoverModeCompleted)); assert_eq!(notification.next(), None); } #[test] fn test_port_notification_iter_usb_mux_error_recovery() { - let mut notification = PortNotification::none(); + let mut notification = PortNotificationEventBitfield::none(); notification.set_usb_mux_error_recovery(true); - assert_eq!(notification.next(), Some(PortNotificationSingle::UsbMuxErrorRecovery)); + assert_eq!(notification.next(), Some(PortEvent::UsbMuxErrorRecovery)); assert_eq!(notification.next(), None); } #[test] fn test_port_notification_iter_dp_status_update() { - let mut notification = PortNotification::none(); + let mut notification = PortNotificationEventBitfield::none(); notification.set_dp_status_update(true); - assert_eq!(notification.next(), Some(PortNotificationSingle::DpStatusUpdate)); + assert_eq!(notification.next(), Some(PortEvent::DpStatusUpdate)); assert_eq!(notification.next(), None); } } diff --git a/type-c-interface/src/port/max_sink_voltage.rs b/type-c-interface/src/port/max_sink_voltage.rs new file mode 100644 index 000000000..a37c8a116 --- /dev/null +++ b/type-c-interface/src/port/max_sink_voltage.rs @@ -0,0 +1,11 @@ +use embedded_usb_pd::PdError; + +use crate::port::pd::Pd; + +/// Functionality related to setting the maximum sink voltage for a port. +pub trait MaxSinkVoltage: Pd { + /// Set the maximum sink voltage for this port + /// + /// This may trigger a renegotiation + fn set_max_sink_voltage(&mut self, voltage_mv: Option) -> impl Future>; +} diff --git a/type-c-interface/src/port/mod.rs b/type-c-interface/src/port/mod.rs new file mode 100644 index 000000000..7920f6db1 --- /dev/null +++ b/type-c-interface/src/port/mod.rs @@ -0,0 +1,8 @@ +//! Type-C port related code +pub mod electrical_disconnect; +pub mod event; +pub mod max_sink_voltage; +pub mod pd; +pub mod power; +pub mod retimer; +pub mod type_c; diff --git a/type-c-interface/src/port/pd.rs b/type-c-interface/src/port/pd.rs new file mode 100644 index 000000000..3cf613a00 --- /dev/null +++ b/type-c-interface/src/port/pd.rs @@ -0,0 +1,72 @@ +use embedded_services::named::Named; +use embedded_usb_pd::vdm::structured::command::discover_identity::{sop, sop_prime}; +use embedded_usb_pd::{PdError, ado::Ado}; + +use crate::control::{ + dp::{DpConfig, DpStatus}, + pd::{PdStateMachineConfig, PortStatus}, + svid::DiscoveredSvids, + tbt::TbtConfig, + usb::UsbControlConfig, + vdm::{AttnVdm, OtherVdm, SendVdm}, +}; + +/// Trait for basic functionality from the PD spec. +pub trait Pd: Named { + /// Returns the port status + fn get_port_status(&mut self) -> impl Future>; + + /// Clear the dead battery flag for this port. + fn clear_dead_battery_flag(&mut self) -> impl Future>; + + /// Enable or disable sink path + fn enable_sink_path(&mut self, enable: bool) -> impl Future>; + + /// Get current PD alert + fn get_pd_alert(&mut self) -> impl Future, PdError>>; + + /// Set port unconstrained status + fn set_unconstrained_power(&mut self, unconstrained: bool) -> impl Future>; + + /// Get the Rx Other VDM data for this port + fn get_other_vdm(&mut self) -> impl Future>; + /// Get the Rx Attention VDM data for this port + fn get_attn_vdm(&mut self) -> impl Future>; + /// Send a VDM to this port + fn send_vdm(&mut self, tx_vdm: SendVdm) -> impl Future>; + /// Execute PD Data Reset for this port + fn execute_drst(&mut self) -> impl Future>; + /// Execute a Hard Reset on this port. + fn hard_reset(&mut self) -> impl Future>; + + /// Get DisplayPort status for this port + fn get_dp_status(&mut self) -> impl Future>; + /// Set DisplayPort configuration for this port + fn set_dp_config(&mut self, config: DpConfig) -> impl Future>; + + /// Set Thunderbolt configuration for this port + fn set_tbt_config(&mut self, config: TbtConfig) -> impl Future>; + + /// Set USB control configuration for this port + fn set_usb_control(&mut self, config: UsbControlConfig) -> impl Future>; + + /// Get this port's discovered SVIDs + fn get_discovered_svids(&mut self) -> impl Future>; + + /// Get the latest response from the Discover Identity command targeting SOP. + fn get_discover_identity_sop_response(&mut self) -> impl Future>; + + /// Get the latest response from the Discover Identity command targeting SOP'. + fn get_discover_identity_sop_prime_response( + &mut self, + ) -> impl Future>; +} + +/// PD state machine related controller functionality +pub trait StateMachine: Pd { + /// Set PD state-machine configuration for this port + fn set_pd_state_machine_config( + &mut self, + config: PdStateMachineConfig, + ) -> impl Future>; +} diff --git a/type-c-interface/src/port/power.rs b/type-c-interface/src/port/power.rs new file mode 100644 index 000000000..afdb3b103 --- /dev/null +++ b/type-c-interface/src/port/power.rs @@ -0,0 +1,14 @@ +use embedded_services::named::Named; +use embedded_usb_pd::PdError; + +/// System power state related controller functionality +pub trait SystemPowerStateStatus: Named { + /// Set the system power state on this port. + /// + /// This notifies the PD controller of the current system power state, + /// which triggers Application Configuration updates (e.g., crossbar reconfiguration). + fn set_system_power_state_status( + &mut self, + state: crate::control::power::SystemPowerState, + ) -> impl Future>; +} diff --git a/type-c-interface/src/port/retimer.rs b/type-c-interface/src/port/retimer.rs new file mode 100644 index 000000000..c79831c92 --- /dev/null +++ b/type-c-interface/src/port/retimer.rs @@ -0,0 +1,18 @@ +use embedded_services::named::Named; +use embedded_usb_pd::PdError; + +use crate::control::retimer::RetimerFwUpdateState; + +/// Retimer-related functionality +pub trait Retimer: Named { + /// Returns the retimer fw update state + fn get_rt_fw_update_status(&mut self) -> impl Future>; + /// Set retimer fw update state + fn set_rt_fw_update_state(&mut self) -> impl Future>; + /// Clear retimer fw update state + fn clear_rt_fw_update_state(&mut self) -> impl Future>; + /// Set retimer compliance + fn set_rt_compliance(&mut self) -> impl Future>; + /// Reconfigure the retimer for this port. + fn reconfigure_retimer(&mut self) -> impl Future>; +} diff --git a/type-c-interface/src/port/type_c.rs b/type-c-interface/src/port/type_c.rs new file mode 100644 index 000000000..5d24fb528 --- /dev/null +++ b/type-c-interface/src/port/type_c.rs @@ -0,0 +1,12 @@ +use embedded_usb_pd::PdError; + +use crate::{control::type_c::TypeCStateMachineState, port::pd::Pd}; + +/// Type-C state machine related controller functionality +pub trait StateMachine: Pd { + /// Set Type-C state-machine configuration for this port + fn set_type_c_state_machine_config( + &mut self, + state: TypeCStateMachineState, + ) -> impl Future>; +} diff --git a/type-c-interface/src/service/event.rs b/type-c-interface/src/service/event.rs new file mode 100644 index 000000000..e20f6bf26 --- /dev/null +++ b/type-c-interface/src/service/event.rs @@ -0,0 +1,93 @@ +//! Comms service message definitions + +use embedded_services::sync::Lockable; +use embedded_usb_pd::{GlobalPortId, ado::Ado}; + +use crate::{ + control::{dp::DpStatus, pd::PortStatus}, + port::{ + event::{PortStatusEventBitfield, VdmData}, + pd::Pd, + }, +}; + +/// Struct containing data for a [`PortEventData::StatusChanged`] event +#[derive(Copy, Clone, Debug)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct StatusChangedData { + /// Status changed event + pub status_event: PortStatusEventBitfield, + /// Previous port status + pub previous_status: PortStatus, + /// Current port status + pub current_status: PortStatus, +} + +/// Enum to contain all port event variants +#[derive(Copy, Clone, Debug)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum PortEventData { + /// Port status change events + StatusChanged(StatusChangedData), + /// PD alert + Alert(Ado), + /// VDM + Vdm(VdmData), + /// Discover mode completed + DiscoverModeCompleted, + /// USB mux error recovery + UsbMuxErrorRecovery, + /// DP status update + DpStatusUpdate(DpStatus), +} + +/// Struct containing a complete port event +#[derive(Copy, Clone, Debug)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct PortEvent<'port, Port: Lockable> { + pub port: &'port Port, + pub event: PortEventData, +} + +/// Message generated when a debug accessory is connected or disconnected +#[derive(Copy, Clone, Debug)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct DebugAccessoryData { + /// Connected + pub connected: bool, +} + +/// UCSI connector change message +#[derive(Copy, Clone, Debug)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct UsciChangeIndicatorData { + /// Port + pub port: GlobalPortId, + /// Notify OPM + pub notify_opm: bool, +} + +/// Top-level comms message +#[derive(Copy, Clone, Debug)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum EventData { + DebugAccessory(DebugAccessoryData), + UsciChangeIndicator(UsciChangeIndicatorData), +} + +/// Top-level comms message +#[derive(Copy, Debug)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct Event<'port, Port: Lockable> { + pub port: &'port Port, + pub event: EventData, +} + +impl<'port, Port: Lockable> Clone for Event<'port, Port> { + fn clone(&self) -> Self { + Self { + port: self.port, + event: self.event, + } + } +} diff --git a/type-c-interface/src/service/mod.rs b/type-c-interface/src/service/mod.rs new file mode 100644 index 000000000..53f112654 --- /dev/null +++ b/type-c-interface/src/service/mod.rs @@ -0,0 +1 @@ +pub mod event; diff --git a/type-c-interface/src/ucsi.rs b/type-c-interface/src/ucsi.rs new file mode 100644 index 000000000..536d5d692 --- /dev/null +++ b/type-c-interface/src/ucsi.rs @@ -0,0 +1,11 @@ +use embedded_services::named::Named; +use embedded_usb_pd::{PdError, ucsi::lpm}; + +/// UCSI LPM command execution trait +pub trait Lpm: Named { + /// Execute the given LPM command + fn execute_lpm_command( + &mut self, + command: lpm::LocalCommand, + ) -> impl Future, PdError>>; +} diff --git a/type-c-service/Cargo.toml b/type-c-service/Cargo.toml index a1a89affd..0582d28aa 100644 --- a/type-c-service/Cargo.toml +++ b/type-c-service/Cargo.toml @@ -17,23 +17,22 @@ workspace = true bitfield.workspace = true bitflags = { workspace = true } defmt = { workspace = true, optional = true } -embedded-cfu-protocol.workspace = true embassy-futures.workspace = true embassy-sync.workspace = true embassy-time.workspace = true embedded-hal-async.workspace = true embedded-services.workspace = true embedded-usb-pd.workspace = true +fw-update-interface.workspace = true heapless.workspace = true log = { workspace = true, optional = true } -static_cell = { workspace = true } tps6699x = { workspace = true, features = ["embassy"] } +power-policy-interface.workspace = true +type-c-interface.workspace = true [dev-dependencies] embassy-time = { workspace = true, features = ["std", "generic-queue-8"] } embassy-sync = { workspace = true, features = ["std"] } -critical-section = { workspace = true, features = ["std"] } -embassy-time-driver = { workspace = true } embassy-futures.workspace = true tokio = { workspace = true, features = ["rt", "macros", "time"] } @@ -46,6 +45,9 @@ defmt = [ "embassy-sync/defmt", "tps6699x/defmt", "embedded-usb-pd/defmt", + "power-policy-interface/defmt", + "type-c-interface/defmt", + "fw-update-interface/defmt", ] log = [ "dep:log", @@ -53,4 +55,7 @@ log = [ "embassy-time/log", "embassy-sync/log", "tps6699x/log", + "power-policy-interface/log", + "type-c-interface/log", + "fw-update-interface/log", ] diff --git a/type-c-service/src/wrapper/config.rs b/type-c-service/src/controller/config.rs similarity index 100% rename from type-c-service/src/wrapper/config.rs rename to type-c-service/src/controller/config.rs diff --git a/type-c-service/src/controller/electrical_disconnect.rs b/type-c-service/src/controller/electrical_disconnect.rs new file mode 100644 index 000000000..8ef5499b8 --- /dev/null +++ b/type-c-service/src/controller/electrical_disconnect.rs @@ -0,0 +1,28 @@ +//! Electrical disconnect port trait implementation +use core::num::NonZeroU8; + +use embedded_services::{event::Sender, sync::Lockable}; +use embedded_usb_pd::PdError; +use type_c_interface::controller::electrical_disconnect::ElectricalDisconnect; + +use super::*; +use crate::controller::state::SharedState; + +impl< + 'device, + C: Lockable, + Shared: Lockable, + TypeCSender: Sender, + PowerSender: Sender, + LoopbackSender: Sender, +> type_c_interface::port::electrical_disconnect::ElectricalDisconnect + for Port<'device, C, Shared, TypeCSender, PowerSender, LoopbackSender> +{ + async fn execute_electrical_disconnect(&mut self, reconnect_time_s: Option) -> Result<(), PdError> { + self.controller + .lock() + .await + .execute_electrical_disconnect(self.port, reconnect_time_s) + .await + } +} diff --git a/type-c-service/src/controller/event.rs b/type-c-service/src/controller/event.rs new file mode 100644 index 000000000..40c91bc38 --- /dev/null +++ b/type-c-service/src/controller/event.rs @@ -0,0 +1,22 @@ +//! Port event types + +use type_c_interface::port::event::PortEventBitfield; + +/// Top-level port event type +#[derive(Copy, Clone, Debug)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +#[non_exhaustive] +pub enum Event { + /// Port event + PortEvent(type_c_interface::port::event::PortEvent), +} + +/// Loopback event to allow `sync_state` and similar functions +/// to generate events that can be processed by the same code as real events. +#[derive(Copy, Clone, Debug)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +#[non_exhaustive] +pub enum Loopback { + /// Port event + PortEvent(PortEventBitfield), +} diff --git a/type-c-service/src/controller/event_receiver.rs b/type-c-service/src/controller/event_receiver.rs new file mode 100644 index 000000000..8a17f93a0 --- /dev/null +++ b/type-c-service/src/controller/event_receiver.rs @@ -0,0 +1,140 @@ +//! This module contains event receiver types for the controller wrapper. +use core::array; +use core::future::pending; +use embassy_futures::select::{Either, select}; +use embassy_time::Timer; +use embedded_services::event::{Receiver, Sender}; +use embedded_services::sync::Lockable; + +use crate::PortEventStreamer; +use crate::controller::event::{Event, Loopback}; +use crate::controller::state::SharedState; +use type_c_interface::port::event::{PortEvent, PortEventBitfield, PortStatusEventBitfield}; + +/// Trait used for receiving interrupt from the controller. +pub trait InterruptReceiver { + /// Wait for the next interrupt event. + fn wait_interrupt(&mut self) -> impl Future; +} + +/// Struct to send received interrupts to their corresponding port receivers +pub struct PortEventSplitter> { + /// Senders to forward port events to their corresponding port receivers + sender: [S; N], +} + +impl> PortEventSplitter { + /// Create a new instance + pub fn new(sender: [S; N]) -> Self { + Self { sender } + } + + /// Wait for the next interrupt event and forward it to the corresponding port receiver. + pub async fn process_interrupts(&mut self, interrupts: [PortEventBitfield; N]) { + for (interrupt, sender) in interrupts.into_iter().zip(self.sender.iter_mut()) { + if interrupt != PortEventBitfield::none() { + sender.send(interrupt).await; + } + } + } +} + +/// Struct to receive and stream port events from the controller. +pub struct PortEventReceiver, LoopbackReceiver: Receiver> { + /// Receiver for the controller's interrupt events + receiver: R, + /// Port event streaming state + streaming_state: Option>>, + /// Loopback receiver for software-generated events + loopback_receiver: LoopbackReceiver, +} + +impl, LoopbackReceiver: Receiver> PortEventReceiver { + /// Create a new instance + pub fn new(receiver: R, loopback_receiver: LoopbackReceiver) -> Self { + Self { + receiver, + streaming_state: None, + loopback_receiver, + } + } + + /// Wait for the next port event + pub async fn wait_next(&mut self) -> type_c_interface::port::event::PortEvent { + loop { + let streaming_state = if let Some(streaming_state) = &mut self.streaming_state { + // Yield to ensure we don't monopolize the executor + embassy_futures::yield_now().await; + streaming_state + } else { + let (Either::First(Loopback::PortEvent(events)) | Either::Second(events)) = + select(self.loopback_receiver.wait_next(), self.receiver.wait_next()).await; + self.streaming_state + .insert(PortEventStreamer::new([events].into_iter())) + }; + + if let Some((_, event)) = streaming_state.next() { + return event; + } else { + self.streaming_state = None; + } + } + } +} + +/// Struct used for containing controller event receivers. +pub struct EventReceiver< + 'a, + State: Lockable, + InterruptReceiver: Receiver, + LoopbackReceiver: Receiver, +> { + /// Port event receiver + port_event_receiver: PortEventReceiver, + /// Shared state + shared_state: &'a State, +} + +impl< + 'a, + State: Lockable, + InterruptReceiver: Receiver, + LoopbackReceiver: Receiver, +> EventReceiver<'a, State, InterruptReceiver, LoopbackReceiver> +{ + /// Create a new instance + pub fn new( + shared_state: &'a State, + port_event_receiver: InterruptReceiver, + loopback_receiver: LoopbackReceiver, + ) -> Self { + Self { + shared_state, + port_event_receiver: PortEventReceiver::new(port_event_receiver, loopback_receiver), + } + } + + /// Wait for the next port event from any port. + /// + /// Returns the local port ID and the event bitfield. + pub async fn wait_event(&mut self) -> Event { + let timeout = self.shared_state.lock().await.sink_ready_timeout; + match select(self.port_event_receiver.wait_next(), async move { + if let Some(timeout) = timeout { + Timer::at(timeout).await; + } else { + pending::<()>().await; + } + }) + .await + { + Either::First(event) => Event::PortEvent(event), + Either::Second(_) => { + let mut status_event = PortStatusEventBitfield::none(); + status_event.set_sink_ready(true); + self.shared_state.lock().await.sink_ready_timeout = None; + Event::PortEvent(PortEvent::StatusChanged(status_event)) + } + } + } +} diff --git a/type-c-service/src/controller/macros.rs b/type-c-service/src/controller/macros.rs new file mode 100644 index 000000000..0fe30d216 --- /dev/null +++ b/type-c-service/src/controller/macros.rs @@ -0,0 +1,192 @@ +use embedded_services::{ + event::{Receiver, Sender}, + sync::Lockable, +}; +use type_c_interface::port::event::PortEventBitfield; + +use crate::controller::{event_receiver::EventReceiver, state}; + +pub const DEFAULT_POWER_POLICY_CHANNEL_SIZE: usize = 2; +pub const DEFAULT_TYPE_C_CHANNEL_SIZE: usize = 2; +pub const DEFAULT_LOOPBACK_CHANNEL_SIZE: usize = 1; +pub const DEFAULT_INTERRUPT_CHANNEL_SIZE: usize = 4; + +/// Components returned from port creation +pub struct PortComponents< + 'a, + Port, + SharedState: Lockable, + TypeCReceiver: Receiver, + PowerPolicyReceveiver: Receiver, + LoopbackReceiver: Receiver, + InterruptReceiver: Receiver, + InterruptSender: Sender, +> { + /// Port instance + pub port: &'a Port, + /// Type-C service event receiver + pub type_c_receiver: TypeCReceiver, + /// Power policy event receiver + pub power_policy_receiver: PowerPolicyReceveiver, + /// Port event receiver + pub event_receiver: EventReceiver<'a, SharedState, InterruptReceiver, LoopbackReceiver>, + /// Interrupt sender + pub interrupt_sender: InterruptSender, +} + +/// Creates a module containing all state for a controller port, based on static cells and channels. +#[macro_export] +macro_rules! define_controller_port_static_cell_channel { + ($vis:vis, $name:ident, $mutex:ty, $controller:ty) => { + $vis mod $name { + use super::*; + + // We prefix all aliases with 'Inner' to avoid potential name conflicts with user code when this macro is invoked + // Unfortunately, super::$ty is not valid syntax in a macro, so we have to pull in everything with super::*. + /// Type alias for the power policy sender + pub type InnerPowerPolicySenderType = + ::embassy_sync::channel::DynamicSender<'static, ::power_policy_interface::psu::event::EventData>; + /// Type alias for the power policy receiver + pub type InnerPowerPolicyReceiverType = + ::embassy_sync::channel::DynamicReceiver<'static, ::power_policy_interface::psu::event::EventData>; + + /// Type alias for the type-c service event sender + pub type InnerTypeCSenderType = ::embassy_sync::channel::DynamicSender<'static, ::type_c_interface::service::event::PortEventData>; + /// Type alias for the type-c service event receiver + pub type InnerTypeCReceiverType = ::embassy_sync::channel::DynamicReceiver<'static, ::type_c_interface::service::event::PortEventData>; + + /// Type alias for the loopback sender + pub type InnerLoopbackSenderType = + ::embassy_sync::channel::DynamicSender<'static, $crate::controller::event::Loopback>; + /// Type alias for the loopback receiver + pub type InnerLoopbackReceiverType = + ::embassy_sync::channel::DynamicReceiver<'static, $crate::controller::event::Loopback>; + + /// Type alias for the interrupt sender + pub type InnerInterruptReceiverType = + ::embassy_sync::channel::DynamicReceiver<'static, ::type_c_interface::port::event::PortEventBitfield>; + /// Type alias for the interrupt receiver + pub type InnerInterruptSenderType = + ::embassy_sync::channel::DynamicSender<'static, ::type_c_interface::port::event::PortEventBitfield>; + + /// Type alias for the shared state mutex + pub type InnerSharedStateType = + ::embassy_sync::mutex::Mutex<$mutex, $crate::controller::state::SharedState>; + /// Type alias for the port + pub type InnerPortType = ::embassy_sync::mutex::Mutex< + $mutex, + $crate::controller::Port< + 'static, + // Controller type + $controller, + // Shared state type + InnerSharedStateType, + // Type-C service event sender type + InnerTypeCSenderType, + // Power policy event sender type + InnerPowerPolicySenderType, + // Loopback event sender type + InnerLoopbackSenderType, + >, + >; + + /// Channel to send events to the type-c service + static TYPE_C_CHANNEL: ::static_cell::StaticCell< + ::embassy_sync::channel::Channel< + $mutex, + ::type_c_interface::service::event::PortEventData, + { $crate::controller::macros::DEFAULT_TYPE_C_CHANNEL_SIZE }, + >, + > = ::static_cell::StaticCell::new(); + /// Channel to send events to the power policy service + static POWER_POLICY_CHANNEL: ::static_cell::StaticCell< + ::embassy_sync::channel::Channel< + $mutex, + ::power_policy_interface::psu::event::EventData, + { $crate::controller::macros::DEFAULT_POWER_POLICY_CHANNEL_SIZE }, + >, + > = ::static_cell::StaticCell::new(); + /// Loopback channel + static LOOPBACK_CHANNEL: ::static_cell::StaticCell< + ::embassy_sync::channel::Channel< + $mutex, + $crate::controller::event::Loopback, + { $crate::controller::macros::DEFAULT_LOOPBACK_CHANNEL_SIZE }, + >, + > = ::static_cell::StaticCell::new(); + /// Interrupt channel + static INTERRUPT_CHANNEL: ::static_cell::StaticCell< + ::embassy_sync::channel::Channel< + $mutex, + ::type_c_interface::port::event::PortEventBitfield, + { $crate::controller::macros::DEFAULT_INTERRUPT_CHANNEL_SIZE }, + >, + > = ::static_cell::StaticCell::new(); + /// State shared between the port and event receiver + static SHARED_STATE: ::static_cell::StaticCell< + ::embassy_sync::mutex::Mutex<$mutex, $crate::controller::state::SharedState>, + > = ::static_cell::StaticCell::new(); + /// Port instance + static PORT: ::static_cell::StaticCell = ::static_cell::StaticCell::new(); + + pub fn create( + name: &'static str, + port: ::embedded_usb_pd::LocalPortId, + config: $crate::controller::config::Config, + controller: &'static $controller, + ) -> $crate::controller::macros::PortComponents< + 'static, + InnerPortType, + InnerSharedStateType, + InnerTypeCReceiverType, + InnerPowerPolicyReceiverType, + InnerLoopbackReceiverType, + InnerInterruptReceiverType, + InnerInterruptSenderType, + > { + let shared_state = SHARED_STATE.init(::embassy_sync::mutex::Mutex::new( + $crate::controller::state::SharedState::new(), + )); + + let power_policy_channel = POWER_POLICY_CHANNEL.init(::embassy_sync::channel::Channel::new()); + let power_policy_sender = power_policy_channel.dyn_sender(); + let power_policy_receiver = power_policy_channel.dyn_receiver(); + + let type_c_channel = TYPE_C_CHANNEL.init(::embassy_sync::channel::Channel::new()); + let type_c_sender = type_c_channel.dyn_sender(); + let type_c_receiver = type_c_channel.dyn_receiver(); + + let loopback_channel = LOOPBACK_CHANNEL.init(::embassy_sync::channel::Channel::new()); + let loopback_sender = loopback_channel.dyn_sender(); + let loopback_receiver = loopback_channel.dyn_receiver(); + + let interrupt_channel = INTERRUPT_CHANNEL.init(::embassy_sync::channel::Channel::new()); + let interrupt_sender = interrupt_channel.dyn_sender(); + let interrupt_receiver = interrupt_channel.dyn_receiver(); + + let port = PORT.init(::embassy_sync::mutex::Mutex::new($crate::controller::Port::new( + name, + config, + port, + controller, + shared_state, + type_c_sender, + power_policy_sender, + loopback_sender, + ))); + let event_receiver = $crate::controller::event_receiver::EventReceiver::new( + shared_state, + interrupt_receiver, + loopback_receiver, + ); + $crate::controller::macros::PortComponents { + port, + type_c_receiver, + power_policy_receiver, + event_receiver, + interrupt_sender, + } + } + } + }; +} diff --git a/type-c-service/src/controller/max_sink_voltage.rs b/type-c-service/src/controller/max_sink_voltage.rs new file mode 100644 index 000000000..78bd4059b --- /dev/null +++ b/type-c-service/src/controller/max_sink_voltage.rs @@ -0,0 +1,26 @@ +//! Max sink voltage port trait implementation +use embedded_services::{event::Sender, sync::Lockable}; +use embedded_usb_pd::PdError; +use type_c_interface::controller::max_sink_voltage::MaxSinkVoltage; + +use super::*; +use crate::controller::state::SharedState; + +impl< + 'device, + C: Lockable, + Shared: Lockable, + TypeCSender: Sender, + PowerSender: Sender, + LoopbackSender: Sender, +> type_c_interface::port::max_sink_voltage::MaxSinkVoltage + for Port<'device, C, Shared, TypeCSender, PowerSender, LoopbackSender> +{ + async fn set_max_sink_voltage(&mut self, voltage_mv: Option) -> Result<(), PdError> { + self.controller + .lock() + .await + .set_max_sink_voltage(self.port, voltage_mv) + .await + } +} diff --git a/type-c-service/src/controller/mod.rs b/type-c-service/src/controller/mod.rs new file mode 100644 index 000000000..e81cff9bf --- /dev/null +++ b/type-c-service/src/controller/mod.rs @@ -0,0 +1,229 @@ +//! Struct that manages per-port state, interfacing with a controller object that exposes multiple ports. +use embedded_services::{debug, error, event::Sender, info, named::Named, sync::Lockable}; +use embedded_usb_pd::{LocalPortId, PdError}; +use power_policy_interface::psu::PsuState; +use type_c_interface::control::pd::PortStatus; +use type_c_interface::controller::pd::Pd; +use type_c_interface::port::event::PortEventBitfield; +use type_c_interface::port::{event::PortEvent as InterfacePortEvent, event::PortStatusEventBitfield}; +use type_c_interface::service::event::{PortEventData as ServicePortEventData, StatusChangedData}; + +use crate::controller::event::{Event, Loopback}; +use crate::controller::state::SharedState; + +pub mod config; +pub mod electrical_disconnect; +pub mod event; +pub mod event_receiver; +pub mod macros; +pub mod max_sink_voltage; +mod pd; +mod power; +pub mod retimer; +pub mod state; +pub mod type_c; +pub mod ucsi; + +pub struct Port< + 'device, + C: Lockable, + Shared: Lockable, + TypeCSender: Sender, + PowerSender: Sender, + LoopbackSender: Sender, +> { + /// Local port + port: LocalPortId, + /// Controller + controller: &'device C, + /// Per-port PSU state + psu_state: power_policy_interface::psu::State, + /// Name for this port + name: &'static str, + /// Cached port status + status: PortStatus, + /// Sender for type-c service events + type_c_sender: TypeCSender, + /// Sender for power policy events + power_policy_sender: PowerSender, + /// Configuration + config: config::Config, + /// Shared state + shared_state: &'device Shared, + /// Loopback sender + loopback_sender: LoopbackSender, +} + +impl< + 'device, + C: Lockable, + Shared: Lockable, + TypeCSender: Sender, + PowerSender: Sender, + LoopbackSender: Sender, +> Port<'device, C, Shared, TypeCSender, PowerSender, LoopbackSender> +{ + /// Create new Port instance + // TODO: refactor arguments into a registration struct + #[allow(clippy::too_many_arguments)] + pub fn new( + name: &'static str, + config: config::Config, + port: LocalPortId, + controller: &'device C, + shared_state: &'device Shared, + type_c_sender: TypeCSender, + power_policy_sender: PowerSender, + loopback_sender: LoopbackSender, + ) -> Self { + Self { + name, + controller, + port, + status: PortStatus::default(), + psu_state: power_policy_interface::psu::State::default(), + power_policy_sender, + config, + shared_state, + loopback_sender, + type_c_sender, + } + } + + /// Top-level processing function + pub async fn process_event(&mut self, event: Event) -> Result, PdError> { + match event { + Event::PortEvent(port_event) => self.process_port_event(port_event).await, + } + } + + /// Process a port notification + async fn process_port_event(&mut self, event: InterfacePortEvent) -> Result, PdError> { + match event { + InterfacePortEvent::StatusChanged(status_event) => { + self.process_port_status_changed(status_event).await.map(Some) + } + InterfacePortEvent::Alert => self.process_pd_alert().await, + InterfacePortEvent::Vdm(vdm_event) => self.process_vdm_event(vdm_event).await.map(Some), + InterfacePortEvent::DpStatusUpdate => self.process_dp_status_update().await.map(Some), + rest => { + // Nothing currently implemented for these + debug!("({}): Notification: {:#?}", self.name, rest); + Ok(None) + } + } + } + + /// Process port status changed events + async fn process_port_status_changed( + &mut self, + status_event: PortStatusEventBitfield, + ) -> Result { + let new_status = self.controller.lock().await.get_port_status(self.port).await?; + debug!("({}) status: {:#?}", self.name, new_status); + debug!("({}) status events: {:#?}", self.name, status_event); + + if status_event.plug_inserted_or_removed() { + self.process_plug_event(&new_status).await?; + } + + // Only notify power policy of a contract after Sink Ready event (always after explicit or implicit contract) + if status_event.sink_ready() { + self.process_new_consumer_contract(&new_status).await?; + } + + if status_event.new_power_contract_as_provider() { + self.process_new_provider_contract(&new_status).await?; + } + + self.check_sink_ready_timeout( + &new_status, + status_event.new_power_contract_as_consumer(), + status_event.sink_ready(), + ) + .await?; + + let event = ServicePortEventData::StatusChanged(StatusChangedData { + status_event, + previous_status: self.status, + current_status: new_status, + }); + self.status = new_status; + self.type_c_sender.send(event).await; + Ok(event) + } + + /// Handle a plug event + async fn process_plug_event(&mut self, new_status: &PortStatus) -> Result<(), PdError> { + info!("Plug event"); + if new_status.is_connected() { + info!("Plug inserted"); + if self.psu_state.psu_state != PsuState::Detached { + info!("Device not in detached state, recovering"); + self.psu_state.detach(); + } + + if let Err(e) = self.psu_state.attach() { + // This should never happen because we should have detached above + error!("Failed to attach PSU: {:?}", e); + return Err(PdError::Failed); + } + + self.power_policy_sender + .send(power_policy_interface::psu::event::EventData::Attached) + .await; + } else { + info!("Plug removed"); + self.psu_state.detach(); + self.power_policy_sender + .send(power_policy_interface::psu::event::EventData::Detached) + .await; + } + + Ok(()) + } + + /// Get the cached port status, returns None if the port is invalid + pub fn get_cached_port_status(&self) -> PortStatus { + self.status + } + + /// Synchronize the state between the controller and the internal state + pub async fn sync_state(&mut self) -> Result<(), PdError> { + let status = self.controller.lock().await.get_port_status(self.port).await?; + + let mut event = PortEventBitfield::none(); + let previous_status = self.status; + + if previous_status.is_connected() != status.is_connected() { + event.status.set_plug_inserted_or_removed(true); + } + + if previous_status.available_sink_contract != status.available_sink_contract { + event.status.set_new_power_contract_as_consumer(true); + } + + if previous_status.available_source_contract != status.available_source_contract { + event.status.set_new_power_contract_as_provider(true); + } + + if event != PortEventBitfield::none() { + self.loopback_sender.send(Loopback::PortEvent(event)).await; + } + Ok(()) + } +} + +impl< + 'device, + C: Lockable, + Shared: Lockable, + TypeCSender: Sender, + PowerSender: Sender, + LoopbackSender: Sender, +> Named for Port<'device, C, Shared, TypeCSender, PowerSender, LoopbackSender> +{ + fn name(&self) -> &'static str { + self.name + } +} diff --git a/type-c-service/src/controller/pd.rs b/type-c-service/src/controller/pd.rs new file mode 100644 index 000000000..f3b862f57 --- /dev/null +++ b/type-c-service/src/controller/pd.rs @@ -0,0 +1,177 @@ +//! PD functionality unrelated to power contracts and general port status +use embedded_services::{event::Sender, sync::Lockable}; +use embedded_usb_pd::PdError; +use embedded_usb_pd::ado::Ado; +use embedded_usb_pd::vdm::structured::command::discover_identity::{sop, sop_prime}; +use type_c_interface::control::{ + dp::{DpConfig, DpStatus}, + pd::{PdStateMachineConfig, PortStatus}, + svid::DiscoveredSvids, + tbt::TbtConfig, + usb::UsbControlConfig, + vdm::{AttnVdm, OtherVdm, SendVdm}, +}; +use type_c_interface::controller::pd::StateMachine; +use type_c_interface::port::event::{VdmData, VdmNotification}; +use type_c_interface::service::event::PortEventData as ServicePortEventData; + +use super::*; +use crate::controller::state::SharedState; + +impl< + 'device, + C: Lockable, + Shared: Lockable, + TypeCSender: Sender, + PowerSender: Sender, + LoopbackSender: Sender, +> Port<'device, C, Shared, TypeCSender, PowerSender, LoopbackSender> +{ + /// Process a VDM event by retrieving the relevant VDM data from the `controller` for the appropriate `port`. + pub(super) async fn process_vdm_event(&mut self, event: VdmNotification) -> Result { + debug!("({}): Processing VDM event: {:?}", self.name, event); + let vdm_data = { + let mut controller = self.controller.lock().await; + match event { + VdmNotification::Entered => VdmData::Entered(controller.get_other_vdm(self.port).await?), + VdmNotification::Exited => VdmData::Exited(controller.get_other_vdm(self.port).await?), + VdmNotification::OtherReceived => VdmData::ReceivedOther(controller.get_other_vdm(self.port).await?), + VdmNotification::AttentionReceived => VdmData::ReceivedAttn(controller.get_attn_vdm(self.port).await?), + } + }; + + let event = ServicePortEventData::Vdm(vdm_data); + self.type_c_sender.send(event).await; + Ok(event) + } + + /// Process a DisplayPort status update by retrieving the current DP status from the `controller` for the appropriate `port`. + pub(super) async fn process_dp_status_update(&mut self) -> Result { + debug!("({}): Processing DP status update event", self.name); + let status = self.controller.lock().await.get_dp_status(self.port).await?; + let event = ServicePortEventData::DpStatusUpdate(status); + self.type_c_sender.send(event).await; + Ok(event) + } + + pub(super) async fn process_pd_alert(&mut self) -> Result, PdError> { + let ado = self.controller.lock().await.get_pd_alert(self.port).await?; + debug!("({}): PD alert: {:#?}", self.name, ado); + if let Some(ado) = ado { + let event = ServicePortEventData::Alert(ado); + self.type_c_sender.send(event).await; + Ok(Some(event)) + } else { + // For some reason we didn't read an alert, nothing to do + Ok(None) + } + } +} + +impl< + 'device, + C: Lockable, + Shared: Lockable, + TypeCSender: Sender, + PowerSender: Sender, + LoopbackSender: Sender, +> type_c_interface::port::pd::Pd for Port<'device, C, Shared, TypeCSender, PowerSender, LoopbackSender> +{ + async fn get_port_status(&mut self) -> Result { + self.controller.lock().await.get_port_status(self.port).await + } + + async fn clear_dead_battery_flag(&mut self) -> Result<(), PdError> { + self.controller.lock().await.clear_dead_battery_flag(self.port).await + } + + async fn enable_sink_path(&mut self, enable: bool) -> Result<(), PdError> { + self.controller.lock().await.enable_sink_path(self.port, enable).await + } + + async fn get_pd_alert(&mut self) -> Result, PdError> { + self.controller.lock().await.get_pd_alert(self.port).await + } + + async fn set_unconstrained_power(&mut self, unconstrained: bool) -> Result<(), PdError> { + self.controller + .lock() + .await + .set_unconstrained_power(self.port, unconstrained) + .await + } + + async fn get_other_vdm(&mut self) -> Result { + self.controller.lock().await.get_other_vdm(self.port).await + } + + async fn get_attn_vdm(&mut self) -> Result { + self.controller.lock().await.get_attn_vdm(self.port).await + } + + async fn send_vdm(&mut self, tx_vdm: SendVdm) -> Result<(), PdError> { + self.controller.lock().await.send_vdm(self.port, tx_vdm).await + } + + async fn execute_drst(&mut self) -> Result<(), PdError> { + self.controller.lock().await.execute_drst(self.port).await + } + + async fn get_dp_status(&mut self) -> Result { + self.controller.lock().await.get_dp_status(self.port).await + } + + async fn set_dp_config(&mut self, config: DpConfig) -> Result<(), PdError> { + self.controller.lock().await.set_dp_config(self.port, config).await + } + + async fn set_tbt_config(&mut self, config: TbtConfig) -> Result<(), PdError> { + self.controller.lock().await.set_tbt_config(self.port, config).await + } + + async fn set_usb_control(&mut self, config: UsbControlConfig) -> Result<(), PdError> { + self.controller.lock().await.set_usb_control(self.port, config).await + } + + async fn hard_reset(&mut self) -> Result<(), PdError> { + self.controller.lock().await.hard_reset(self.port).await + } + + async fn get_discovered_svids(&mut self) -> Result { + self.controller.lock().await.get_discovered_svids(self.port).await + } + + async fn get_discover_identity_sop_response(&mut self) -> Result { + self.controller + .lock() + .await + .get_discover_identity_sop_response(self.port) + .await + } + + async fn get_discover_identity_sop_prime_response(&mut self) -> Result { + self.controller + .lock() + .await + .get_discover_identity_sop_prime_response(self.port) + .await + } +} + +impl< + 'device, + C: Lockable, + Shared: Lockable, + TypeCSender: Sender, + PowerSender: Sender, + LoopbackSender: Sender, +> type_c_interface::port::pd::StateMachine for Port<'device, C, Shared, TypeCSender, PowerSender, LoopbackSender> +{ + async fn set_pd_state_machine_config(&mut self, config: PdStateMachineConfig) -> Result<(), PdError> { + self.controller + .lock() + .await + .set_pd_state_machine_config(self.port, config) + .await + } +} diff --git a/type-c-service/src/controller/power.rs b/type-c-service/src/controller/power.rs new file mode 100644 index 000000000..8d48d3817 --- /dev/null +++ b/type-c-service/src/controller/power.rs @@ -0,0 +1,190 @@ +//! Module for power policy related functionality +use embassy_time::{Duration, Instant}; +use embedded_services::{debug, error, event::Sender, info, sync::Lockable}; +use embedded_usb_pd::{ + PdError, + constants::{T_PS_TRANSITION_EPR_MS, T_PS_TRANSITION_SPR_MS}, +}; +use power_policy_interface::{ + capability::{ConsumerPowerCapability, ProviderPowerCapability, PsuType}, + psu::{Error as PsuError, Psu, State}, +}; +use type_c_interface::controller::power::SystemPowerStateStatus; + +use crate::{controller::config::UnconstrainedSink, util::power_policy_error_from_pd_error}; + +use super::*; + +impl< + 'device, + C: Lockable, + Shared: Lockable, + TypeCSender: Sender, + PowerSender: Sender, + LoopbackSender: Sender, +> Port<'device, C, Shared, TypeCSender, PowerSender, LoopbackSender> +{ + /// Handle a new contract as consumer + pub(super) async fn process_new_consumer_contract(&mut self, new_status: &PortStatus) -> Result<(), PdError> { + info!("Process new consumer contract"); + let available_sink_contract = new_status.available_sink_contract.map(|c| { + let mut c: ConsumerPowerCapability = c.into(); + let unconstrained = match self.config.unconstrained_sink { + UnconstrainedSink::Auto => new_status.unconstrained_power, + UnconstrainedSink::PowerThresholdMilliwatts(threshold) => c.capability.max_power_mw() >= threshold, + UnconstrainedSink::Never => false, + }; + c.flags.set_unconstrained_power(unconstrained); + c.flags.set_psu_type(PsuType::TypeC); + c + }); + + if let Err(e) = self.psu_state.update_consumer_power_capability(available_sink_contract) { + error!("Failed to update consumer power capability: {:?}", e); + return Err(PdError::Failed); + } + self.power_policy_sender + .send(power_policy_interface::psu::event::EventData::UpdatedConsumerCapability(available_sink_contract)) + .await; + Ok(()) + } + + /// Handle a new contract as provider + pub(super) async fn process_new_provider_contract(&mut self, new_status: &PortStatus) -> Result<(), PdError> { + info!("Process New provider contract"); + let capability = new_status.available_source_contract.map(|caps| { + let mut caps = ProviderPowerCapability::from(caps); + caps.flags.set_psu_type(PsuType::TypeC); + caps + }); + if let Err(e) = self.psu_state.update_requested_provider_power_capability(capability) { + error!("Failed to update requested provider power capability: {:?}", e); + return Err(PdError::Failed); + } + self.power_policy_sender + .send(power_policy_interface::psu::event::EventData::RequestedProviderCapability(capability)) + .await; + Ok(()) + } + + /// Check the sink ready timeout + /// + /// After accepting a sink contract (new contract as consumer), the PD spec guarantees that the + /// source will be available to provide power after `tPSTransition`. This allows us to handle transitions + /// even for controllers that might not always broadcast sink ready events. + pub(super) async fn check_sink_ready_timeout( + &mut self, + new_status: &PortStatus, + new_contract: bool, + sink_ready: bool, + ) -> Result<(), PdError> { + let contract_changed = self.status.available_sink_contract != new_status.available_sink_contract; + let mut shared_state = self.shared_state.lock().await; + let timeout = &mut shared_state.sink_ready_timeout; + + // Don't start the timeout if the sink has signaled it's ready or if the contract didn't change. + // The latter ensures that soft resets won't continually reset the ready timeout + debug!( + "({}): Check sink ready: new_contract={:?}, sink_ready={:?}, contract_changed={:?}, deadline={:?}", + self.name, new_contract, sink_ready, contract_changed, timeout, + ); + if new_contract && !sink_ready && contract_changed { + // Start the timeout + // Double the spec maximum transition time to provide a safety margin for hardware/controller delays or out-of-spec controllers. + let timeout_ms = if new_status.epr { + T_PS_TRANSITION_EPR_MS + } else { + T_PS_TRANSITION_SPR_MS + } + .maximum + .0 * 2; + + debug!("({}): Sink ready timeout started for {}ms", self.name, timeout_ms); + *timeout = Some(Instant::now() + Duration::from_millis(timeout_ms as u64)); + } else if timeout.is_some() + && (!new_status.is_connected() || new_status.available_sink_contract.is_none() || sink_ready) + { + debug!("({}): Sink ready timeout cleared", self.name); + *timeout = None; + } + Ok(()) + } +} + +impl< + 'device, + C: Lockable, + Shared: Lockable, + TypeCSender: Sender, + PowerSender: Sender, + LoopbackSender: Sender, +> Psu for Port<'device, C, Shared, TypeCSender, PowerSender, LoopbackSender> +{ + async fn disconnect(&mut self) -> Result<(), PsuError> { + self.controller + .lock() + .await + .enable_sink_path(self.port, false) + .await + .map_err(|e| { + error!("({}): Error disabling sink path", self.name); + power_policy_error_from_pd_error(e) + })?; + self.psu_state.disconnect(false) + } + + async fn connect_provider(&mut self, capability: ProviderPowerCapability) -> Result<(), PsuError> { + info!("({}): Connect as provider: {:#?}", self.name, capability); + // TODO: Implement controller over provider enablement + self.psu_state.connect_provider(capability).inspect_err(|e| { + error!("({}): Failed to transition to provider state: {:#?}", self.name, e); + }) + } + + async fn connect_consumer(&mut self, capability: ConsumerPowerCapability) -> Result<(), PsuError> { + info!( + "({}): Connect as consumer: {:?}, enable input switch", + self.name, capability + ); + self.controller + .lock() + .await + .enable_sink_path(self.port, true) + .await + .map_err(|e| { + error!("({}): Error enabling sink path", self.name); + power_policy_error_from_pd_error(e) + })?; + self.psu_state.connect_consumer(capability) + } + + fn state(&self) -> &State { + &self.psu_state + } + + fn state_mut(&mut self) -> &mut State { + &mut self.psu_state + } +} + +impl< + 'device, + C: Lockable, + Shared: Lockable, + TypeCSender: Sender, + PowerSender: Sender, + LoopbackSender: Sender, +> type_c_interface::port::power::SystemPowerStateStatus + for Port<'device, C, Shared, TypeCSender, PowerSender, LoopbackSender> +{ + async fn set_system_power_state_status( + &mut self, + state: type_c_interface::control::power::SystemPowerState, + ) -> Result<(), PdError> { + self.controller + .lock() + .await + .set_system_power_state_status(self.port, state) + .await + } +} diff --git a/type-c-service/src/controller/retimer.rs b/type-c-service/src/controller/retimer.rs new file mode 100644 index 000000000..f7710169b --- /dev/null +++ b/type-c-service/src/controller/retimer.rs @@ -0,0 +1,38 @@ +//! Retimer port trait implementation +use embedded_services::{event::Sender, sync::Lockable}; +use embedded_usb_pd::PdError; +use type_c_interface::control::retimer::RetimerFwUpdateState; +use type_c_interface::controller::retimer::Retimer; + +use super::*; +use crate::controller::state::SharedState; + +impl< + 'device, + C: Lockable, + Shared: Lockable, + TypeCSender: Sender, + PowerSender: Sender, + LoopbackSender: Sender, +> type_c_interface::port::retimer::Retimer for Port<'device, C, Shared, TypeCSender, PowerSender, LoopbackSender> +{ + async fn get_rt_fw_update_status(&mut self) -> Result { + self.controller.lock().await.get_rt_fw_update_status(self.port).await + } + + async fn set_rt_fw_update_state(&mut self) -> Result<(), PdError> { + self.controller.lock().await.set_rt_fw_update_state(self.port).await + } + + async fn clear_rt_fw_update_state(&mut self) -> Result<(), PdError> { + self.controller.lock().await.clear_rt_fw_update_state(self.port).await + } + + async fn set_rt_compliance(&mut self) -> Result<(), PdError> { + self.controller.lock().await.set_rt_compliance(self.port).await + } + + async fn reconfigure_retimer(&mut self) -> Result<(), PdError> { + self.controller.lock().await.reconfigure_retimer(self.port).await + } +} diff --git a/type-c-service/src/controller/state.rs b/type-c-service/src/controller/state.rs new file mode 100644 index 000000000..c19fd3a6f --- /dev/null +++ b/type-c-service/src/controller/state.rs @@ -0,0 +1,23 @@ +use embassy_time::Instant; + +/// State shared between the port and event receiver +#[derive(Copy, Clone)] +pub struct SharedState { + /// Sink ready timeout + pub(crate) sink_ready_timeout: Option, +} + +impl SharedState { + /// Create a new instance with default values + pub fn new() -> Self { + Self { + sink_ready_timeout: None, + } + } +} + +impl Default for SharedState { + fn default() -> Self { + Self::new() + } +} diff --git a/type-c-service/src/controller/type_c.rs b/type-c-service/src/controller/type_c.rs new file mode 100644 index 000000000..30bd84c89 --- /dev/null +++ b/type-c-service/src/controller/type_c.rs @@ -0,0 +1,26 @@ +//! Type-C state machine port trait implementation +use embedded_services::{event::Sender, sync::Lockable}; +use embedded_usb_pd::PdError; +use type_c_interface::control::type_c::TypeCStateMachineState; +use type_c_interface::controller::type_c::StateMachine; + +use super::*; +use crate::controller::state::SharedState; + +impl< + 'device, + C: Lockable, + Shared: Lockable, + TypeCSender: Sender, + PowerSender: Sender, + LoopbackSender: Sender, +> type_c_interface::port::type_c::StateMachine for Port<'device, C, Shared, TypeCSender, PowerSender, LoopbackSender> +{ + async fn set_type_c_state_machine_config(&mut self, state: TypeCStateMachineState) -> Result<(), PdError> { + self.controller + .lock() + .await + .set_type_c_state_machine_config(self.port, state) + .await + } +} diff --git a/type-c-service/src/controller/ucsi.rs b/type-c-service/src/controller/ucsi.rs new file mode 100644 index 000000000..3dfdbf6dd --- /dev/null +++ b/type-c-service/src/controller/ucsi.rs @@ -0,0 +1,21 @@ +//! UCSI LPM port trait implementation +use embedded_services::{event::Sender, sync::Lockable}; +use embedded_usb_pd::{PdError, ucsi::lpm}; +use type_c_interface::ucsi::Lpm as UcsiLpm; + +use super::*; +use crate::controller::state::SharedState; + +impl< + 'device, + C: Lockable, + Shared: Lockable, + TypeCSender: Sender, + PowerSender: Sender, + LoopbackSender: Sender, +> type_c_interface::ucsi::Lpm for Port<'device, C, Shared, TypeCSender, PowerSender, LoopbackSender> +{ + async fn execute_lpm_command(&mut self, command: lpm::LocalCommand) -> Result, PdError> { + self.controller.lock().await.execute_lpm_command(command).await + } +} diff --git a/type-c-service/src/driver/tps6699x.rs b/type-c-service/src/driver/tps6699x.rs index 8f1830eac..caa019094 100644 --- a/type-c-service/src/driver/tps6699x.rs +++ b/type-c-service/src/driver/tps6699x.rs @@ -1,32 +1,24 @@ -use ::tps6699x::registers::field_sets::IntEventBus1; use ::tps6699x::registers::{PdCcPullUp, PpExtVbusSw, PpIntVbusSw}; use ::tps6699x::{PORT0, PORT1, TPS66993_NUM_PORTS, TPS66994_NUM_PORTS}; use bitfield::bitfield; use bitflags::bitflags; -use core::array::from_fn; -use core::future::Future; use core::iter::zip; use core::num::NonZeroU8; use embassy_sync::blocking_mutex::raw::RawMutex; use embassy_time::Delay; use embedded_hal_async::i2c::I2c; -use embedded_services::power::policy::PowerCapability; -use embedded_services::type_c::ATTN_VDM_LEN; -use embedded_services::type_c::controller::{ - self, AttnVdm, Controller, ControllerStatus, DiscoveredSvids, DpPinConfig, OtherVdm, PortStatus, SendVdm, - TbtConfig, TypeCStateMachineState, UsbControlConfig, -}; -use embedded_services::type_c::event::PortEvent; -use embedded_services::{debug, error, trace, type_c, warn}; +use embedded_services::named::Named; +use embedded_services::{debug, error, trace, warn}; use embedded_usb_pd::ado::Ado; use embedded_usb_pd::pdinfo::PowerPathStatus; use embedded_usb_pd::pdo::{Common, Contract, Rdo, sink, source}; use embedded_usb_pd::type_c::Current as TypecCurrent; use embedded_usb_pd::ucsi::lpm; use embedded_usb_pd::{DataRole, Error, LocalPortId, PdError, PlugOrientation, PowerRole}; +use fw_update_interface::basic::{Error as BasicFwUpdateError, FwUpdate as BasicFwUpdate}; use heapless::Vec; use tps6699x::MAX_SUPPORTED_PORTS; -use tps6699x::asynchronous::embassy as tps6699x_drv; +use tps6699x::asynchronous::embassy::{self as tps6699x_drv, interrupt}; use tps6699x::asynchronous::fw_update::UpdateTarget; use tps6699x::asynchronous::fw_update::{ BorrowedUpdater, BorrowedUpdaterInProgress, disable_all_interrupts, enable_port0_interrupts, @@ -36,7 +28,25 @@ use tps6699x::command::{ vdms::{INITIATOR_WAIT_TIME_MS, MAX_NUM_DATA_OBJECTS, Version}, }; use tps6699x::fw_update::UpdateConfig as FwUpdateConfig; +use tps6699x::registers::field_sets::IntEventBus1; use tps6699x::registers::port_config::TypeCStateMachine; +use type_c_interface::control::dp::{DpConfig, DpPinConfig, DpStatus}; +use type_c_interface::control::pd::{PdStateMachineConfig, PortStatus}; +use type_c_interface::control::power::SystemPowerState; +use type_c_interface::control::retimer::RetimerFwUpdateState; +use type_c_interface::control::svid::DiscoveredSvids; +use type_c_interface::control::tbt::TbtConfig; +use type_c_interface::control::type_c::TypeCStateMachineState; +use type_c_interface::control::usb::UsbControlConfig; +use type_c_interface::control::vdm::{ATTN_VDM_LEN, AttnVdm, OtherVdm, SendVdm}; +use type_c_interface::controller::Controller; +use type_c_interface::controller::pd::Pd; +use type_c_interface::controller::retimer::Retimer; +use type_c_interface::port::event::PortEventBitfield; + +use crate::util::{ + basic_fw_update_error_from_pd_error, power_capability_from_current, power_capability_try_from_contract, +}; type Updater<'a, M, B> = BorrowedUpdaterInProgress>; @@ -48,7 +58,7 @@ struct FwUpdateState<'a, M: RawMutex, B: I2c> { /// /// This value is never read, only used to keep the interrupt guard alive #[allow(dead_code)] - guards: [Option>; MAX_SUPPORTED_PORTS], + guards: [Option>; MAX_SUPPORTED_PORTS], } /// The method used to control USB capabilities. @@ -80,12 +90,12 @@ pub struct Config { } pub struct Tps6699x<'a, M: RawMutex, B: I2c> { - port_events: heapless::Vec, tps6699x: tps6699x_drv::Tps6699x<'a, M, B>, update_state: Option>, /// Firmware update configuration fw_update_config: FwUpdateConfig, config: Config, + name: &'static str, } impl<'a, M: RawMutex, B: I2c> Tps6699x<'a, M, B> { @@ -97,20 +107,43 @@ impl<'a, M: RawMutex, B: I2c> Tps6699x<'a, M, B> { num_ports: usize, fw_update_config: FwUpdateConfig, config: Config, + name: &'static str, ) -> Option { if num_ports == 0 || num_ports > MAX_SUPPORTED_PORTS { None } else { Some(Self { // num_ports validated by branch - port_events: heapless::Vec::from_iter((0..num_ports).map(|_| PortEvent::none())), tps6699x, update_state: None, fw_update_config, config, + name, }) } } + + fn log_error(&self, e: Error) -> PdError { + match e { + Error::Bus(_) => { + error!("({}): Bus error", self.name()); + PdError::Failed + } + Error::Pd(pd_error) => { + error!("({}): PD error: {:#?}", self.name(), pd_error); + pd_error + } + } + } + + /// Returns a busy error if a FW update is currently in progress + fn guard_no_fw_update_active(&self) -> Result<(), PdError> { + if self.update_state.is_some() { + Err(PdError::Busy) + } else { + Ok(()) + } + } } bitfield! { @@ -190,139 +223,160 @@ bitfield! { pub u16, reserved1, set_reserved1: 31, 16; } -impl Controller for Tps6699x<'_, M, B> { - type BusError = B::Error; - - /// Controller reset - async fn reset_controller(&mut self) -> Result<(), Error> { - let mut delay = Delay; - self.tps6699x.reset(&mut delay).await?; - - Ok(()) +impl Named for Tps6699x<'_, M, B> { + fn name(&self) -> &'static str { + self.name } +} - /// Wait for an event on any port - async fn wait_port_event(&mut self) -> Result<(), Error> { - let interrupts = self - .tps6699x - .wait_interrupt_any(false, from_fn(|_| IntEventBus1::all())) - .await; - - for (interrupt, event) in zip(interrupts.iter(), self.port_events.iter_mut()) { - if *interrupt == IntEventBus1::new_zero() { - continue; - } - - { - if interrupt.plug_event() { - debug!("Event: Plug event"); - event.status.set_plug_inserted_or_removed(true); - } - if interrupt.source_caps_received() { - debug!("Event: Source Caps received"); - event.status.set_source_caps_received(true); - } - - if interrupt.sink_ready() { - debug!("Event: Sink ready"); - event.status.set_sink_ready(true); - } - - if interrupt.new_consumer_contract() { - debug!("Event: New contract as consumer, PD controller act as Sink"); - // Port is consumer and power negotiation is complete - event.status.set_new_power_contract_as_consumer(true); - } - - if interrupt.new_provider_contract() { - debug!("Event: New contract as provider, PD controller act as source"); - // Port is provider and power negotiation is complete - event.status.set_new_power_contract_as_provider(true); - } - - if interrupt.power_swap_completed() { - debug!("Event: power swap completed"); - event.status.set_power_swap_completed(true); - } - - if interrupt.data_swap_completed() { - debug!("Event: data swap completed"); - event.status.set_data_swap_completed(true); - } - - if interrupt.am_entered() { - debug!("Event: alt mode entered"); - event.status.set_alt_mode_entered(true); - } - - if interrupt.hard_reset() { - debug!("Event: hard reset"); - event.status.set_pd_hard_reset(true); - } - - if interrupt.crossbar_error() { - debug!("Event: crossbar error"); - event.notification.set_usb_mux_error_recovery(true); - } +impl<'a, M: RawMutex, B: I2c> BasicFwUpdate for Tps6699x<'a, M, B> { + async fn get_active_fw_version(&mut self) -> Result { + let customer_use = CustomerUse( + self.tps6699x + .get_customer_use() + .await + .map_err(|e| basic_fw_update_error_from_pd_error(self.log_error(e)))?, + ); + Ok(customer_use.custom_fw_version()) + } - if interrupt.usvid_mode_entered() { - debug!("Event: user svid mode entered"); - event.notification.set_custom_mode_entered(true); - } + async fn start_fw_update(&mut self) -> Result<(), BasicFwUpdateError> { + let mut delay = Delay; + let mut updater: BorrowedUpdater> = + BorrowedUpdater::with_config(self.fw_update_config.clone()); - if interrupt.usvid_mode_exited() { - debug!("Event: usvid mode exited"); - event.notification.set_custom_mode_exited(true); - } + // Abandon any previous in-progress update + if let Some(update) = self.update_state.take() { + warn!("Abandoning in-progress update"); + update + .updater + .abort_fw_update(&mut [&mut self.tps6699x], &mut delay) + .await; + } - if interrupt.usvid_attention_vdm_received() { - debug!("Event: user svid attention vdm received"); - event.notification.set_custom_mode_attention_received(true); - } + let mut guards = [const { None }; MAX_SUPPORTED_PORTS]; + // Disable all interrupts on both ports, use guards[1] to ensure that this set of guards is dropped last + disable_all_interrupts::>(&mut [&mut self.tps6699x], &mut guards[1..]) + .await + .map_err(|e| basic_fw_update_error_from_pd_error(self.log_error(e)))?; + let in_progress = updater + .start_fw_update(&mut [&mut self.tps6699x], &mut delay) + .await + .map_err(|e| basic_fw_update_error_from_pd_error(self.log_error(e)))?; - if interrupt.usvid_other_vdm_received() { - debug!("Event: user svid other vdm received"); - event.notification.set_custom_mode_other_vdm_received(true); - } + // Re-enable interrupts on port 0 only + if let Err(e) = + enable_port0_interrupts::>(&mut [&mut self.tps6699x], &mut guards[0..1]) + .await + .map_err(|e| basic_fw_update_error_from_pd_error(self.log_error(e))) + { + error!("Failed to enable port 0 interrupts, aborting update: {:#?}", e); + in_progress.abort_fw_update(&mut [&mut self.tps6699x], &mut delay).await; + return Err(e); + } - if interrupt.discover_mode_completed() { - debug!("Event: discover mode completed"); - event.notification.set_discover_mode_completed(true); - } + self.update_state = Some(FwUpdateState { + updater: in_progress, + guards, + }); + Ok(()) + } - if interrupt.dp_sid_status_updated() { - debug!("Event: dp sid status updated"); - event.notification.set_dp_status_update(true); - } + /// Aborts the firmware update in progress + /// + /// This can reset the controller + async fn abort_fw_update(&mut self) -> Result<(), BasicFwUpdateError> { + // Check if we're still in firmware update mode + if self + .tps6699x + .get_mode() + .await + .map_err(|e| basic_fw_update_error_from_pd_error(self.log_error(e)))? + == tps6699x::Mode::F211 + { + let mut delay = Delay; - if interrupt.alert_message_received() { - debug!("Event: alert message received"); - event.notification.set_alert(true); - } + if let Some(update) = self.update_state.take() { + // Attempt to abort the firmware update by consuming our update object + update + .updater + .abort_fw_update(&mut [&mut self.tps6699x], &mut delay) + .await; + Ok(()) + } else { + // Bypass our update object since we've gotten into a state where we don't have one + self.tps6699x + .fw_update_mode_exit(&mut delay) + .await + .map_err(|e| basic_fw_update_error_from_pd_error(self.log_error(e))) } + } else { + // Not in FW update mode, don't need to do anything + Ok(()) } - Ok(()) } - /// Returns and clears current events for the given port + /// Finalize the firmware update /// - /// Drop safety: All state changes happen after await point - async fn clear_port_events(&mut self, port: LocalPortId) -> Result> { - Ok(core::mem::replace( - self.port_events.get_mut(port.0 as usize).ok_or(PdError::InvalidPort)?, - PortEvent::none(), - )) + /// This will reset the controller + async fn finalize_fw_update(&mut self) -> Result<(), BasicFwUpdateError> { + if let Some(update) = self.update_state.take() { + let mut delay = Delay; + update + .updater + .complete_fw_update(&mut [&mut self.tps6699x], &mut delay) + .await + .map_err(|e| basic_fw_update_error_from_pd_error(self.log_error(e))) + } else { + Err(BasicFwUpdateError::NeedsActiveUpdate) + } + } + + async fn write_fw_contents(&mut self, _offset: usize, data: &[u8]) -> Result<(), BasicFwUpdateError> { + if let Some(update) = &mut self.update_state { + let mut delay = Delay; + update + .updater + .write_bytes(&mut [&mut self.tps6699x], &mut delay, data) + .await + .map_err(|e| basic_fw_update_error_from_pd_error(self.log_error(e)))?; + Ok(()) + } else { + Err(BasicFwUpdateError::NeedsActiveUpdate) + } } +} + +impl Controller for Tps6699x<'_, M, B> { + /// Controller reset + async fn reset_controller(&mut self) -> Result<(), PdError> { + self.guard_no_fw_update_active()?; + let mut delay = Delay; + self.tps6699x.reset(&mut delay).await.map_err(|e| self.log_error(e))?; + Ok(()) + } +} + +impl Pd for Tps6699x<'_, M, B> { /// Returns the current status of the port - async fn get_port_status(&mut self, port: LocalPortId) -> Result> { - let status = self.tps6699x.get_port_status(port).await?; + async fn get_port_status(&mut self, port: LocalPortId) -> Result { + self.guard_no_fw_update_active()?; + let status = self + .tps6699x + .get_port_status(port) + .await + .map_err(|e| self.log_error(e))?; debug!("Port{} status: {:#?}", port.0, status); - let pd_status = self.tps6699x.get_pd_status(port).await?; + let pd_status = self.tps6699x.get_pd_status(port).await.map_err(|e| self.log_error(e))?; debug!("Port{} PD status: {:#?}", port.0, pd_status); - let port_control = self.tps6699x.get_port_control(port).await?; + let port_control = self + .tps6699x + .get_port_control(port) + .await + .map_err(|e| self.log_error(e))?; debug!("Port{} control: {:#?}", port.0, port_control); let mut port_status = PortStatus::default(); @@ -335,52 +389,65 @@ impl Controller for Tps6699x<'_, M, B> { if port_status.is_connected() { // Determine current contract if any - let pdo_raw = self.tps6699x.get_active_pdo_contract(port).await?.active_pdo(); + let pdo_raw = self + .tps6699x + .get_active_pdo_contract(port) + .await + .map_err(|e| self.log_error(e))? + .active_pdo(); trace!("Raw PDO: {:#X}", pdo_raw); - let rdo_raw = self.tps6699x.get_active_rdo_contract(port).await?.active_rdo(); + let rdo_raw = self + .tps6699x + .get_active_rdo_contract(port) + .await + .map_err(|e| self.log_error(e))? + .active_rdo(); trace!("Raw RDO: {:#X}", rdo_raw); if pdo_raw != 0 && rdo_raw != 0 { // Got a valid explicit contract if pd_status.is_source() { - let pdo = source::Pdo::try_from(pdo_raw).map_err(|_| Error::from(PdError::InvalidParams))?; - let rdo = Rdo::for_pdo(rdo_raw, pdo).ok_or(Error::Pd(PdError::InvalidParams))?; + let pdo = source::Pdo::try_from(pdo_raw)?; + let rdo = Rdo::for_pdo(rdo_raw, pdo).ok_or(PdError::InvalidParams)?; debug!("PDO: {:#?}", pdo); debug!("RDO: {:#?}", rdo); - port_status.available_source_contract = Contract::from_source(pdo, rdo).try_into().ok(); + port_status.available_source_contract = + power_capability_try_from_contract(Contract::from_source(pdo, rdo)); port_status.dual_power = pdo.dual_role_power(); } else { // active_rdo_contract doesn't contain the full picture let mut source_pdos: [source::Pdo; 1] = [source::Pdo::default()]; // Read 5V fixed supply source PDO, guaranteed to be present as the first SPR PDO - let (num_sprs, _) = self - .tps6699x - .lock_inner() - .await - .get_rx_src_caps(port, &mut source_pdos[..], &mut []) - .await?; + let (num_sprs, _) = { + self.tps6699x + .lock_inner() + .await + .get_rx_src_caps(port, &mut source_pdos[..], &mut []) + .await + } + .map_err(|e| self.log_error(e.into()))?; if num_sprs == 0 { // USB PD spec requires at least one source PDO be present, something is really wrong error!("Port{} no source PDOs found", port.0); - return Err(PdError::InvalidParams.into()); + return Err(PdError::InvalidParams); } - let pdo = sink::Pdo::try_from(pdo_raw).map_err(|_| Error::from(PdError::InvalidParams))?; - let rdo = Rdo::for_pdo(rdo_raw, pdo).ok_or(Error::Pd(PdError::InvalidParams))?; + let pdo = sink::Pdo::try_from(pdo_raw)?; + let rdo = Rdo::for_pdo(rdo_raw, pdo).ok_or(PdError::InvalidParams)?; debug!("PDO: {:#?}", pdo); debug!("RDO: {:#?}", rdo); - port_status.available_sink_contract = Contract::from_sink(pdo, rdo).try_into().ok(); + port_status.available_sink_contract = + power_capability_try_from_contract(Contract::from_sink(pdo, rdo)); port_status.dual_power = source_pdos[0].dual_role_power(); port_status.unconstrained_power = source_pdos[0].unconstrained_power(); } } else if status.port_role() { // port_role is true for source // Implicit source contract - let current = TypecCurrent::try_from(port_control.typec_current()).map_err(Error::Pd)?; + let current = TypecCurrent::try_from(port_control.typec_current())?; debug!("Port{} type-C source current: {:#?}", port.0, current); - let new_contract = Some(PowerCapability::from(current)); - port_status.available_source_contract = new_contract; + port_status.available_source_contract = Some(power_capability_from_current(current)); } else { // Implicit sink contract let pull = pd_status.cc_pull_up(); @@ -389,9 +456,9 @@ impl Controller for Tps6699x<'_, M, B> { debug!("Port{} no pull up", port.0); None } else { - let current = TypecCurrent::try_from(pd_status.cc_pull_up()).map_err(Error::Pd)?; + let current = TypecCurrent::try_from(pd_status.cc_pull_up())?; debug!("Port{} type-C sink current: {:#?}", port.0, current); - Some(PowerCapability::from(current)) + Some(power_capability_from_current(current)) }; port_status.available_sink_contract = new_contract; } @@ -413,12 +480,20 @@ impl Controller for Tps6699x<'_, M, B> { }; // Update alt-mode status - let alt_mode = self.tps6699x.get_alt_mode_status(port).await?; + let alt_mode = self + .tps6699x + .get_alt_mode_status(port) + .await + .map_err(|e| self.log_error(e))?; debug!("Port{} alt mode: {:#?}", port.0, alt_mode); port_status.alt_mode = alt_mode; // Update power path status - let power_path = self.tps6699x.get_power_path_status(port).await?; + let power_path = self + .tps6699x + .get_power_path_status(port) + .await + .map_err(|e| self.log_error(e))?; trace!("Port{} power source: {:#?}", port.0, power_path); port_status.power_path = match port { PORT0 => PowerPathStatus::new( @@ -437,201 +512,64 @@ impl Controller for Tps6699x<'_, M, B> { Ok(port_status) } - async fn get_rt_fw_update_status( - &mut self, - port: LocalPortId, - ) -> Result> { - match self.tps6699x.get_rt_fw_update_status(port).await { - Ok(true) => Ok(type_c::controller::RetimerFwUpdateState::Active), - Ok(false) => Ok(type_c::controller::RetimerFwUpdateState::Inactive), - Err(e) => Err(e), - } - } - - async fn set_rt_fw_update_state(&mut self, port: LocalPortId) -> Result<(), Error> { - self.tps6699x.set_rt_fw_update_state(port).await - } - - fn clear_rt_fw_update_state( - &mut self, - port: LocalPortId, - ) -> impl Future>> { - self.tps6699x.clear_rt_fw_update_state(port) - } - - async fn set_rt_compliance(&mut self, port: LocalPortId) -> Result<(), Error> { - self.tps6699x.set_rt_compliance(port).await - } - - async fn reconfigure_retimer(&mut self, port: LocalPortId) -> Result<(), Error> { - let input = { - let mut input = tps6699x::command::muxr::Input(0); - input.set_en_retry_on_target_addr_tbt(true); - input - }; - - match self.tps6699x.execute_muxr(port, input).await? { - ReturnValue::Success => Ok(()), - r => { - debug!("Error executing MuxR on port {}: {:#?}", port.0, r); - Err(Error::Pd(PdError::InvalidResponse)) - } - } - } - - async fn clear_dead_battery_flag(&mut self, port: LocalPortId) -> Result<(), Error> { - match self.tps6699x.execute_dbfg(port).await? { + async fn clear_dead_battery_flag(&mut self, port: LocalPortId) -> Result<(), PdError> { + self.guard_no_fw_update_active()?; + match self.tps6699x.execute_dbfg(port).await.map_err(|e| self.log_error(e))? { ReturnValue::Success => Ok(()), r => { - debug!("Error executing DBfg on port {}: {:#?}", port.0, r); - Err(Error::Pd(PdError::InvalidResponse)) + error!("Error executing DBfg on port {}: {:#?}", port.0, r); + Err(PdError::InvalidResponse) } } } - async fn enable_sink_path(&mut self, port: LocalPortId, enable: bool) -> Result<(), Error> { + async fn enable_sink_path(&mut self, port: LocalPortId, enable: bool) -> Result<(), PdError> { + self.guard_no_fw_update_active()?; debug!("Port{} enable sink path: {}", port.0, enable); - self.tps6699x.enable_sink_path(port, enable).await - } - - async fn get_pd_alert(&mut self, port: LocalPortId) -> Result, Error> { - self.tps6699x.get_rx_ado(port).await.map_err(Error::from) - } - - async fn get_controller_status(&mut self) -> Result, Error> { - let boot_flags = self.tps6699x.get_boot_flags().await?; - let customer_use = CustomerUse(self.tps6699x.get_customer_use().await?); - - Ok(ControllerStatus { - mode: self.tps6699x.get_mode().await?.into(), - valid_fw_bank: (boot_flags.active_bank() == 0 && boot_flags.bank0_valid() != 0) - || (boot_flags.active_bank() == 1 && boot_flags.bank1_valid() != 0), - fw_version0: customer_use.ti_fw_version(), - fw_version1: customer_use.custom_fw_version(), - }) + self.tps6699x + .enable_sink_path(port, enable) + .await + .map_err(|e| self.log_error(e)) } - fn set_unconstrained_power( - &mut self, - port: LocalPortId, - unconstrained: bool, - ) -> impl Future>> { - self.tps6699x.set_unconstrained_power(port, unconstrained) + async fn get_pd_alert(&mut self, port: LocalPortId) -> Result, PdError> { + self.guard_no_fw_update_active()?; + self.tps6699x + .get_rx_ado(port) + .await + .map_err(|e| self.log_error(e.into())) } - async fn get_active_fw_version(&mut self) -> Result> { - let customer_use = CustomerUse(self.tps6699x.get_customer_use().await?); - Ok(customer_use.custom_fw_version()) + async fn set_unconstrained_power(&mut self, port: LocalPortId, unconstrained: bool) -> Result<(), PdError> { + self.guard_no_fw_update_active()?; + self.tps6699x + .set_unconstrained_power(port, unconstrained) + .await + .map_err(|e| self.log_error(e)) } - async fn start_fw_update(&mut self) -> Result<(), Error> { - let mut delay = Delay; - let mut updater: BorrowedUpdater> = - BorrowedUpdater::with_config(self.fw_update_config.clone()); - - // Abandon any previous in-progress update - if let Some(update) = self.update_state.take() { - warn!("Abandoning in-progress update"); - update - .updater - .abort_fw_update(&mut [&mut self.tps6699x], &mut delay) - .await; - } - - let mut guards = [const { None }; MAX_SUPPORTED_PORTS]; - // Disable all interrupts on both ports, use guards[1] to ensure that this set of guards is dropped last - disable_all_interrupts::>(&mut [&mut self.tps6699x], &mut guards[1..]).await?; - let in_progress = updater.start_fw_update(&mut [&mut self.tps6699x], &mut delay).await?; - // Re-enable interrupts on port 0 only - enable_port0_interrupts::>(&mut [&mut self.tps6699x], &mut guards[0..1]) - .await?; - self.update_state = Some(FwUpdateState { - updater: in_progress, - guards, - }); - Ok(()) - } - - /// Aborts the firmware update in progress - /// - /// This can reset the controller - async fn abort_fw_update(&mut self) -> Result<(), Error> { - // Check if we're still in firmware update mode - if self.tps6699x.get_mode().await? == tps6699x::Mode::F211 { - let mut delay = Delay; - - if let Some(update) = self.update_state.take() { - // Attempt to abort the firmware update by consuming our update object - update - .updater - .abort_fw_update(&mut [&mut self.tps6699x], &mut delay) - .await; - Ok(()) - } else { - // Bypass our update object since we've gotten into a state where we don't have one - self.tps6699x.fw_update_mode_exit(&mut delay).await - } - } else { - // Not in FW update mode, don't need to do anything - Ok(()) - } - } - - /// Finalize the firmware update - /// - /// This will reset the controller - async fn finalize_fw_update(&mut self) -> Result<(), Error> { - if let Some(update) = self.update_state.take() { - let mut delay = Delay; - update - .updater - .complete_fw_update(&mut [&mut self.tps6699x], &mut delay) - .await - } else { - Err(PdError::InvalidMode.into()) - } - } - - async fn write_fw_contents(&mut self, _offset: usize, data: &[u8]) -> Result<(), Error> { - if let Some(update) = &mut self.update_state { - let mut delay = Delay; - update - .updater - .write_bytes(&mut [&mut self.tps6699x], &mut delay, data) - .await?; - Ok(()) - } else { - Err(PdError::InvalidMode.into()) - } - } - - fn set_max_sink_voltage( - &mut self, - port: LocalPortId, - voltage_mv: Option, - ) -> impl Future>> { - self.tps6699x.set_autonegotiate_sink_max_voltage(port, voltage_mv) - } - - async fn get_other_vdm(&mut self, port: LocalPortId) -> Result> { + async fn get_other_vdm(&mut self, port: LocalPortId) -> Result { + self.guard_no_fw_update_active()?; match self.tps6699x.get_rx_other_vdm(port).await { Ok(vdm) => Ok((*vdm.as_bytes()).into()), - Err(e) => Err(e), + Err(e) => Err(self.log_error(e)), } } - async fn get_attn_vdm(&mut self, port: LocalPortId) -> Result> { + async fn get_attn_vdm(&mut self, port: LocalPortId) -> Result { + self.guard_no_fw_update_active()?; match self.tps6699x.get_rx_attn_vdm(port).await { Ok(vdm) => { let buf: [u8; ATTN_VDM_LEN] = vdm.into(); let attn_vdm: AttnVdm = buf.into(); Ok(attn_vdm) } - Err(e) => Err(e), + Err(e) => Err(self.log_error(e)), } } - async fn send_vdm(&mut self, port: LocalPortId, tx_vdm: SendVdm) -> Result<(), Error> { + async fn send_vdm(&mut self, port: LocalPortId, tx_vdm: SendVdm) -> Result<(), PdError> { + self.guard_no_fw_update_active()?; let input = { let mut input = tps6699x::command::vdms::Input::default(); input.set_num_vdo(tx_vdm.vdo_count); @@ -651,21 +589,23 @@ impl Controller for Tps6699x<'_, M, B> { input }; - match self.tps6699x.send_vdms(port, input).await? { + match self + .tps6699x + .send_vdms(port, input) + .await + .map_err(|e| self.log_error(e))? + { ReturnValue::Success => Ok(()), r => { - debug!("Error executing VDMs on port {}: {:#?}", port.0, r); - Err(Error::Pd(PdError::InvalidResponse)) + error!("Error executing VDMs on port {}: {:#?}", port.0, r); + Err(PdError::InvalidResponse) } } } /// Set USB control configuration for the given port - async fn set_usb_control( - &mut self, - port: LocalPortId, - config: UsbControlConfig, - ) -> Result<(), Error> { + async fn set_usb_control(&mut self, port: LocalPortId, config: UsbControlConfig) -> Result<(), PdError> { + self.guard_no_fw_update_active()?; match self.config.usb_control_method { UsbControlMethod::TxIdentity => { let tx_identity_value = @@ -678,7 +618,8 @@ impl Controller for Tps6699x<'_, M, B> { identity.set_dfp1_vdo(dfp_vdo.0); identity.clone() }) - .await?; + .await + .map_err(|e| self.log_error(e))?; } UsbControlMethod::DpConfig => { use tps6699x::registers::DpUsbDataPath; @@ -693,7 +634,8 @@ impl Controller for Tps6699x<'_, M, B> { dp_config.set_usb_data_path(usb_data_path); *dp_config }) - .await?; + .await + .map_err(|e| self.log_error(e))?; } UsbControlMethod::TbtConfig => { use tps6699x::registers::TbtUsbDataPath; @@ -703,22 +645,26 @@ impl Controller for Tps6699x<'_, M, B> { TbtUsbDataPath::NotRequired }; - self.tps6699x - .lock_inner() - .await - .modify_tbt_config(port, |tbt_config| { - tbt_config.set_usb_data_path(usb_data_path); - *tbt_config - }) - .await?; + { + self.tps6699x + .lock_inner() + .await + .modify_tbt_config(port, |tbt_config| { + tbt_config.set_usb_data_path(usb_data_path); + *tbt_config + }) + .await + } + .map_err(|e| self.log_error(e))?; } } Ok(()) } - async fn get_dp_status(&mut self, port: LocalPortId) -> Result> { - let dp_status = self.tps6699x.get_dp_status(port).await?; + async fn get_dp_status(&mut self, port: LocalPortId) -> Result { + self.guard_no_fw_update_active()?; + let dp_status = self.tps6699x.get_dp_status(port).await.map_err(|e| self.log_error(e))?; debug!("Port{} DP status: {:#?}", port.0, dp_status); let alt_mode_entered = dp_status.dp_mode_active() != 0; @@ -727,20 +673,17 @@ impl Controller for Tps6699x<'_, M, B> { let cfg_raw: PdDpPinConfig = dp_config.config_pin().into(); let pin_config: DpPinConfig = cfg_raw.into(); - Ok(controller::DpStatus { + Ok(DpStatus { alt_mode_entered, dfp_d_pin_cfg: pin_config, }) } - async fn set_dp_config( - &mut self, - port: LocalPortId, - config: controller::DpConfig, - ) -> Result<(), Error> { + async fn set_dp_config(&mut self, port: LocalPortId, config: DpConfig) -> Result<(), PdError> { + self.guard_no_fw_update_active()?; debug!("Port{} setting DP config: {:#?}", port.0, config); - let mut dp_config_reg = self.tps6699x.get_dp_config(port).await?; + let mut dp_config_reg = self.tps6699x.get_dp_config(port).await.map_err(|e| self.log_error(e))?; debug!("Current DP config: {:#?}", dp_config_reg); @@ -748,53 +691,194 @@ impl Controller for Tps6699x<'_, M, B> { let cfg_raw: PdDpPinConfig = config.dfp_d_pin_cfg.into(); dp_config_reg.set_dfpd_pin_assignment(cfg_raw.bits()); - self.tps6699x.set_dp_config(port, dp_config_reg).await?; + self.tps6699x + .set_dp_config(port, dp_config_reg) + .await + .map_err(|e| self.log_error(e))?; Ok(()) } - async fn execute_drst(&mut self, port: LocalPortId) -> Result<(), Error> { - match self.tps6699x.execute_drst(port).await? { + async fn execute_drst(&mut self, port: LocalPortId) -> Result<(), PdError> { + self.guard_no_fw_update_active()?; + match self.tps6699x.execute_drst(port).await.map_err(|e| self.log_error(e))? { ReturnValue::Success => Ok(()), r => { error!("Error executing DRST on port {}: {:#?}", port.0, r); - Err(Error::Pd(PdError::InvalidResponse)) + Err(PdError::InvalidResponse) } } } - async fn set_tbt_config(&mut self, port: LocalPortId, config: TbtConfig) -> Result<(), Error> { + async fn set_tbt_config(&mut self, port: LocalPortId, config: TbtConfig) -> Result<(), PdError> { + self.guard_no_fw_update_active()?; debug!("Port{} setting TBT config: {:#?}", port.0, config); - let mut config_reg = self.tps6699x.lock_inner().await.get_tbt_config(port).await?; + let mut config_reg = + { self.tps6699x.lock_inner().await.get_tbt_config(port).await }.map_err(|e| self.log_error(e))?; config_reg.set_tbt_vid_en(config.tbt_enabled); config_reg.set_tbt_mode_en(config.tbt_enabled); - self.tps6699x.lock_inner().await.set_tbt_config(port, config_reg).await + { self.tps6699x.lock_inner().await.set_tbt_config(port, config_reg).await }.map_err(|e| self.log_error(e)) + } + + async fn hard_reset(&mut self, port: LocalPortId) -> Result<(), PdError> { + self.guard_no_fw_update_active()?; + match self.tps6699x.execute_hrst(port).await.map_err(|e| self.log_error(e))? { + ReturnValue::Success => Ok(()), + r => { + error!("Error executing hard reset on port {}: {:#?}", port.0, r); + Err(PdError::InvalidResponse) + } + } + } + + async fn get_discovered_svids(&mut self, port: LocalPortId) -> Result { + self.guard_no_fw_update_active()?; + let svids = self + .tps6699x + .get_discovered_svids(port) + .await + .map_err(|e| self.log_error(e))?; + debug!("{:?} discovered SVIDs: {:?}", port, svids); + let mut sop = Vec::new(); + for svid in svids.svid_sop().take(sop.capacity()) { + let _ = sop.push(svid); + } + + let mut sop_prime = Vec::new(); + for svid in svids.svid_sop_prime().take(sop_prime.capacity()) { + let _ = sop_prime.push(svid); + } + + Ok(DiscoveredSvids::new(sop, sop_prime)) } + async fn get_discover_identity_sop_response( + &mut self, + port: LocalPortId, + ) -> Result { + self.guard_no_fw_update_active()?; + let data = self + .tps6699x + .get_received_sop_identity_data(port) + .await + .map_err(|e| self.log_error(e))?; + match data.try_into() { + Ok(vdos) => Ok(vdos), + Err(e) => { + error!("Error deserializing Received SOP Identity Data: {:?}", e); + Err(PdError::Serialize) + } + } + } + + async fn get_discover_identity_sop_prime_response( + &mut self, + port: LocalPortId, + ) -> Result { + self.guard_no_fw_update_active()?; + let data = self + .tps6699x + .get_received_sop_prime_identity_data(port) + .await + .map_err(|e| self.log_error(e))?; + match data.try_into() { + Ok(vdos) => Ok(vdos), + Err(e) => { + error!("Error deserializing Received SOP Prime Identity Data: {:?}", e); + Err(PdError::Serialize) + } + } + } +} + +impl Retimer for Tps6699x<'_, M, B> { + async fn get_rt_fw_update_status(&mut self, port: LocalPortId) -> Result { + self.guard_no_fw_update_active()?; + match self.tps6699x.get_rt_fw_update_status(port).await { + Ok(true) => Ok(RetimerFwUpdateState::Active), + Ok(false) => Ok(RetimerFwUpdateState::Inactive), + Err(e) => Err(self.log_error(e)), + } + } + + async fn set_rt_fw_update_state(&mut self, port: LocalPortId) -> Result<(), PdError> { + self.guard_no_fw_update_active()?; + self.tps6699x + .set_rt_fw_update_state(port) + .await + .map_err(|e| self.log_error(e)) + } + + async fn clear_rt_fw_update_state(&mut self, port: LocalPortId) -> Result<(), PdError> { + self.guard_no_fw_update_active()?; + self.tps6699x + .clear_rt_fw_update_state(port) + .await + .map_err(|e| self.log_error(e)) + } + + async fn set_rt_compliance(&mut self, port: LocalPortId) -> Result<(), PdError> { + self.guard_no_fw_update_active()?; + self.tps6699x + .set_rt_compliance(port) + .await + .map_err(|e| self.log_error(e)) + } + + async fn reconfigure_retimer(&mut self, port: LocalPortId) -> Result<(), PdError> { + self.guard_no_fw_update_active()?; + let input = { + let mut input = tps6699x::command::muxr::Input(0); + input.set_en_retry_on_target_addr_tbt(true); + input + }; + + match self + .tps6699x + .execute_muxr(port, input) + .await + .map_err(|e| self.log_error(e))? + { + ReturnValue::Success => Ok(()), + r => { + error!("Error executing MuxR on port {}: {:#?}", port.0, r); + Err(PdError::InvalidResponse) + } + } + } +} + +impl type_c_interface::controller::pd::StateMachine for Tps6699x<'_, M, B> { async fn set_pd_state_machine_config( &mut self, port: LocalPortId, - config: controller::PdStateMachineConfig, - ) -> Result<(), Error> { + config: PdStateMachineConfig, + ) -> Result<(), PdError> { + self.guard_no_fw_update_active()?; debug!("Port{} setting PD state machine config: {:#?}", port.0, config); - let mut config_reg = self.tps6699x.lock_inner().await.get_port_config(port).await?; + let mut config_reg = + { self.tps6699x.lock_inner().await.get_port_config(port).await }.map_err(|e| self.log_error(e))?; config_reg.set_disable_pd(!config.enabled); - self.tps6699x.lock_inner().await.set_port_config(port, config_reg).await + { self.tps6699x.lock_inner().await.set_port_config(port, config_reg).await }.map_err(|e| self.log_error(e)) } +} +impl type_c_interface::controller::type_c::StateMachine for Tps6699x<'_, M, B> { async fn set_type_c_state_machine_config( &mut self, port: LocalPortId, - state: controller::TypeCStateMachineState, - ) -> Result<(), Error> { + state: TypeCStateMachineState, + ) -> Result<(), PdError> { + self.guard_no_fw_update_active()?; debug!("Port{} setting Type-C state machine state: {:#?}", port.0, state); - let mut config_reg = self.tps6699x.lock_inner().await.get_port_config(port).await?; + let mut config_reg = + { self.tps6699x.lock_inner().await.get_port_config(port).await }.map_err(|e| self.log_error(e))?; let typec_state = match state { TypeCStateMachineState::Sink => TypeCStateMachine::Sink, TypeCStateMachineState::Source => TypeCStateMachine::Source, @@ -803,106 +887,76 @@ impl Controller for Tps6699x<'_, M, B> { }; config_reg.set_typec_state_machine(typec_state); - self.tps6699x.lock_inner().await.set_port_config(port, config_reg).await + { self.tps6699x.lock_inner().await.set_port_config(port, config_reg).await }.map_err(|e| self.log_error(e)) } +} - async fn execute_ucsi_command( - &mut self, - command: lpm::LocalCommand, - ) -> Result, Error> { - self.tps6699x.execute_ucsi_command(&command).await +impl type_c_interface::ucsi::Lpm for Tps6699x<'_, M, B> { + async fn execute_lpm_command(&mut self, command: lpm::LocalCommand) -> Result, PdError> { + self.guard_no_fw_update_active()?; + self.tps6699x + .execute_ucsi_command(&command) + .await + .map_err(|e| self.log_error(e)) } +} +impl type_c_interface::controller::electrical_disconnect::ElectricalDisconnect + for Tps6699x<'_, M, B> +{ async fn execute_electrical_disconnect( &mut self, port: LocalPortId, reconnect_time_s: Option, - ) -> Result<(), Error> { + ) -> Result<(), PdError> { + self.guard_no_fw_update_active()?; let reconnect_time_s = reconnect_time_s.map(|t| t.get()); - match self.tps6699x.execute_disc(port, reconnect_time_s).await? { + match self + .tps6699x + .execute_disc(port, reconnect_time_s) + .await + .map_err(|e| self.log_error(e))? + { ReturnValue::Success => Ok(()), r => { - debug!("Error executing DISC on port {}: {:#?}", port.0, r); - Err(Error::Pd(PdError::InvalidResponse)) + error!("Error executing DISC on port {}: {:#?}", port.0, r); + Err(PdError::InvalidResponse) } } } +} - async fn set_power_state( +impl type_c_interface::controller::power::SystemPowerStateStatus for Tps6699x<'_, M, B> { + async fn set_system_power_state_status( &mut self, port: LocalPortId, - state: controller::SystemPowerState, - ) -> Result<(), Error> { + state: SystemPowerState, + ) -> Result<(), PdError> { + self.guard_no_fw_update_active()?; use tps6699x::registers::SystemPowerState as DriverSystemPowerState; let driver_state = match state { - controller::SystemPowerState::S0 => DriverSystemPowerState::S0, - controller::SystemPowerState::S3 => DriverSystemPowerState::S3, - controller::SystemPowerState::S4 => DriverSystemPowerState::S4, - controller::SystemPowerState::S5 => DriverSystemPowerState::S5, - controller::SystemPowerState::S0ix => DriverSystemPowerState::S0Ix, + SystemPowerState::S0 => DriverSystemPowerState::S0, + SystemPowerState::S3 => DriverSystemPowerState::S3, + SystemPowerState::S4 => DriverSystemPowerState::S4, + SystemPowerState::S5 => DriverSystemPowerState::S5, + SystemPowerState::S0ix => DriverSystemPowerState::S0Ix, }; - self.tps6699x.set_sx_app_config(port, driver_state).await - } - - async fn get_discovered_svids(&mut self, port: LocalPortId) -> Result> { - let svids = self.tps6699x.get_discovered_svids(port).await?; - debug!("{:?} discovered SVIDs: {:?}", port, svids); - let mut sop = Vec::new(); - for svid in svids.svid_sop().take(sop.capacity()) { - let _ = sop.push(svid); - } - - let mut sop_prime = Vec::new(); - for svid in svids.svid_sop_prime().take(sop_prime.capacity()) { - let _ = sop_prime.push(svid); - } - - let svids = DiscoveredSvids::new(sop, sop_prime); - Ok(svids) - } - - async fn hard_reset(&mut self, port: LocalPortId) -> Result<(), Error> { - match self.tps6699x.execute_hrst(port).await? { - ReturnValue::Success => Ok(()), - r => { - debug!("Error executing hard reset on port {}: {:#?}", port.0, r); - Err(Error::Pd(PdError::InvalidResponse)) - } - } - } - - async fn get_discover_identity_sop_response( - &mut self, - port: LocalPortId, - ) -> Result> - { - let data = self.tps6699x.get_received_sop_identity_data(port).await?; - match data.try_into() { - Ok(vdos) => Ok(vdos), - Err(e) => { - debug!("Error deserializing Received SOP Identity Data: {:?}", e); - Err(Error::Pd(PdError::Serialize)) - } - } + self.tps6699x + .set_sx_app_config(port, driver_state) + .await + .map_err(|e| self.log_error(e)) } +} - async fn get_discover_identity_sop_prime_response( - &mut self, - port: LocalPortId, - ) -> Result< - embedded_usb_pd::vdm::structured::command::discover_identity::sop_prime::ResponseVdos, - Error, - > { - let data = self.tps6699x.get_received_sop_prime_identity_data(port).await?; - match data.try_into() { - Ok(vdos) => Ok(vdos), - Err(e) => { - debug!("Error deserializing Received SOP Prime Identity Data: {:?}", e); - Err(Error::Pd(PdError::Serialize)) - } - } +impl type_c_interface::controller::max_sink_voltage::MaxSinkVoltage for Tps6699x<'_, M, B> { + async fn set_max_sink_voltage(&mut self, port: LocalPortId, voltage_mv: Option) -> Result<(), PdError> { + self.guard_no_fw_update_active()?; + self.tps6699x + .set_autonegotiate_sink_max_voltage(port, voltage_mv) + .await + .map_err(|e| self.log_error(e)) } } @@ -923,6 +977,7 @@ pub fn tps66994<'a, M: RawMutex, BUS: I2c>( controller: tps6699x_drv::Tps6699x<'a, M, BUS>, fw_update_config: FwUpdateConfig, config: Config, + name: &'static str, ) -> Tps6699x<'a, M, BUS> { const _: () = assert!( TPS66994_NUM_PORTS > 0 && TPS66994_NUM_PORTS <= MAX_SUPPORTED_PORTS, @@ -931,7 +986,7 @@ pub fn tps66994<'a, M: RawMutex, BUS: I2c>( // Panic safety: statically checked above #[allow(clippy::unwrap_used)] - Tps6699x::try_new(controller, TPS66994_NUM_PORTS, fw_update_config, config).unwrap() + Tps6699x::try_new(controller, TPS66994_NUM_PORTS, fw_update_config, config, name).unwrap() } /// Create a TPS66993 object mutex @@ -939,6 +994,7 @@ pub fn tps66993<'a, M: RawMutex, BUS: I2c>( controller: tps6699x_drv::Tps6699x<'a, M, BUS>, fw_update_config: FwUpdateConfig, config: Config, + name: &'static str, ) -> Tps6699x<'a, M, BUS> { const _: () = assert!( TPS66993_NUM_PORTS > 0 && TPS66993_NUM_PORTS <= MAX_SUPPORTED_PORTS, @@ -947,7 +1003,7 @@ pub fn tps66993<'a, M: RawMutex, BUS: I2c>( // Panic safety: statically checked above #[allow(clippy::unwrap_used)] - Tps6699x::try_new(controller, TPS66993_NUM_PORTS, fw_update_config, config).unwrap() + Tps6699x::try_new(controller, TPS66993_NUM_PORTS, fw_update_config, config, name).unwrap() } bitfield! { @@ -961,3 +1017,104 @@ bitfield! { /// TI FW version pub u32, ti_fw_version, set_ti_fw_version: 63, 32; } + +impl<'a, M: RawMutex, BUS: I2c> crate::controller::event_receiver::InterruptReceiver + for interrupt::InterruptReceiver<'a, M, BUS> +{ + async fn wait_interrupt(&mut self) -> [PortEventBitfield; MAX_SUPPORTED_PORTS] { + let interrupts = self.wait_any(false).await; + let mut port_events = [PortEventBitfield::none(); MAX_SUPPORTED_PORTS]; + for (interrupt, event) in zip(interrupts.iter(), port_events.iter_mut()) { + if *interrupt == IntEventBus1::new_zero() { + continue; + } + + if interrupt.plug_event() { + debug!("Event: Plug event"); + event.status.set_plug_inserted_or_removed(true); + } + if interrupt.source_caps_received() { + debug!("Event: Source Caps received"); + event.status.set_source_caps_received(true); + } + + if interrupt.sink_ready() { + debug!("Event: Sink ready"); + event.status.set_sink_ready(true); + } + + if interrupt.new_consumer_contract() { + debug!("Event: New contract as consumer, PD controller act as Sink"); + // Port is consumer and power negotiation is complete + event.status.set_new_power_contract_as_consumer(true); + } + + if interrupt.new_provider_contract() { + debug!("Event: New contract as provider, PD controller act as source"); + // Port is provider and power negotiation is complete + event.status.set_new_power_contract_as_provider(true); + } + + if interrupt.power_swap_completed() { + debug!("Event: power swap completed"); + event.status.set_power_swap_completed(true); + } + + if interrupt.data_swap_completed() { + debug!("Event: data swap completed"); + event.status.set_data_swap_completed(true); + } + + if interrupt.am_entered() { + debug!("Event: alt mode entered"); + event.status.set_alt_mode_entered(true); + } + + if interrupt.hard_reset() { + debug!("Event: hard reset"); + event.status.set_pd_hard_reset(true); + } + + if interrupt.crossbar_error() { + debug!("Event: crossbar error"); + event.notification.set_usb_mux_error_recovery(true); + } + + if interrupt.usvid_mode_entered() { + debug!("Event: user svid mode entered"); + event.notification.set_custom_mode_entered(true); + } + + if interrupt.usvid_mode_exited() { + debug!("Event: usvid mode exited"); + event.notification.set_custom_mode_exited(true); + } + + if interrupt.usvid_attention_vdm_received() { + debug!("Event: user svid attention vdm received"); + event.notification.set_custom_mode_attention_received(true); + } + + if interrupt.usvid_other_vdm_received() { + debug!("Event: user svid other vdm received"); + event.notification.set_custom_mode_other_vdm_received(true); + } + + if interrupt.discover_mode_completed() { + debug!("Event: discover mode completed"); + event.notification.set_discover_mode_completed(true); + } + + if interrupt.dp_sid_status_updated() { + debug!("Event: dp sid status updated"); + event.notification.set_dp_status_update(true); + } + + if interrupt.alert_message_received() { + debug!("Event: alert message received"); + event.notification.set_alert(true); + } + } + port_events + } +} diff --git a/type-c-service/src/lib.rs b/type-c-service/src/lib.rs index a53d22e3c..2ee1d6488 100644 --- a/type-c-service/src/lib.rs +++ b/type-c-service/src/lib.rs @@ -1,120 +1,67 @@ #![no_std] +pub mod controller; pub mod driver; pub mod service; pub mod task; -pub mod wrapper; +pub mod util; -use core::future::Future; +use core::iter::Enumerate; -use embedded_services::type_c::event::{ - PortEvent, PortNotification, PortNotificationSingle, PortPendingIter, PortStatusChanged, +use type_c_interface::port::event::{ + PortEvent, PortEventBitfield, PortNotificationEventBitfield, PortStatusEventBitfield, }; -pub use task::task; - -/// Enum to contain all port event variants -#[derive(Copy, Clone, Debug, PartialEq, Eq)] -#[cfg_attr(feature = "defmt", derive(defmt::Format))] -pub enum PortEventVariant { - /// Port status change events - StatusChanged(PortStatusChanged), - /// Port notification events - Notification(PortNotificationSingle), -} /// Struct to convert port events into a stream of events -#[derive(Clone, Copy)] -pub struct PortEventStreamer { - /// Current port index being processed - port_index: Option, - /// Iterator over pending ports - pending_iter: PortPendingIter, +#[derive(Clone)] +pub struct PortEventStreamer> { + /// Iterator over pending event bitfields + port_iter: Enumerate, /// Notification to be streamed - pending_notifications: Option, + pending_notifications: Option<(usize, PortNotificationEventBitfield)>, } -impl PortEventStreamer { +impl> PortEventStreamer { /// Create new PortEventStreamer - /// - /// Returns none if there are no pending ports to process. - pub fn new(pending_iter: PortPendingIter) -> Self { + pub fn new(port_iter: Iter) -> Self { Self { - port_index: None, - pending_iter, + port_iter: port_iter.enumerate(), pending_notifications: None, } } } -impl PortEventStreamer { - /// Get the next port event, calls the closure if it needs to get pending events for the current port. - pub async fn next>, F: FnMut(usize) -> Fut>( - &mut self, - mut f: F, - ) -> Result, E> { - loop { - let port_index = if let Some(index) = self.port_index { - index - } else if let Some(next_port) = self.pending_iter.next() { - // First time this function is called, get our starting port index - self.port_index = Some(next_port); - next_port - } else { - // No pending ports to process - return Ok(None); - }; - - let mut advance_port = false; - let mut ret = None; +impl> Iterator for PortEventStreamer { + type Item = (usize, PortEvent); - if let Some(mut pending) = self.pending_notifications { - if let Some(port_event) = pending.next() { - // Return a single notification - self.pending_notifications = Some(pending); - ret = Some((port_index, PortEventVariant::Notification(port_event))); - } else { - // Done with pending notifications, continue to the next port - advance_port = true; - self.pending_notifications = None; - } - } else { - // Haven't read port events yet - let event = f(port_index).await?; + fn next(&mut self) -> Option { + loop { + // Handle any pending notifications first + if let Some((port_index, pending)) = &mut self.pending_notifications + && let Some(port_event) = pending.next() + { + // Return a single notification + return Some((*port_index, port_event)); + } - if event.notification != PortNotification::none() { - // Have pending notifications to stream as events, store those for the next loop/call to this function - self.pending_notifications = Some(event.notification); + // No pending notifications, fetch the next port event + if let Some((port_index, event_bitfield)) = self.port_iter.next() { + // Pending notifications for this port if there are any + if event_bitfield.notification != PortNotificationEventBitfield::none() { + self.pending_notifications = Some((port_index, event_bitfield.notification)); } else { - // No pending notifications, we can advance to the next port - advance_port = true; self.pending_notifications = None; } - if event.status != PortStatusChanged::none() { - // Return the port status changed event first if there is one - ret = Some((port_index, PortEventVariant::StatusChanged(event.status))); - } - } - - if advance_port { - if let Some(next_port) = self.pending_iter.next() { - // Move to the next port - self.port_index = Some(next_port); - } else if ret.is_none() { - // Don't have any more ports to process - // And we didn't have any events to return, we're done - return Ok(None); - } else { - // This is the last port, but we have an event to return - // We'll have to return none on the next call, achieve this by setting port_index to None - // The next call will call next() on the pending port iterator which will return None - self.port_index = None; + // Return a status changed event if there is one + if event_bitfield.status != PortStatusEventBitfield::none() { + return Some((port_index, PortEvent::StatusChanged(event_bitfield.status))); } + } else { + // No more ports to process, we're done + return None; } - // Return the event if we have one, otherwise loop to get the next event - if ret.is_some() { - return Ok(ret); - } + //Otherwise loop, to handle any remaining notifications } } } @@ -122,15 +69,11 @@ impl PortEventStreamer { #[cfg(test)] #[allow(clippy::unwrap_used)] mod tests { - use core::sync::atomic::AtomicBool; - - use embedded_services::type_c::event::PortPending; - use super::*; - /// Utitily function to create a PortStatusChanged event - fn status_changed(plug_event: bool, power_contract: bool, sink_ready: bool) -> PortStatusChanged { - let mut status_changed = PortStatusChanged::none(); + /// Utility function to create a PortStatusChanged event + fn status_changed(plug_event: bool, power_contract: bool, sink_ready: bool) -> PortStatusEventBitfield { + let mut status_changed = PortStatusEventBitfield::none(); status_changed.set_plug_inserted_or_removed(plug_event); status_changed.set_new_power_contract_as_consumer(power_contract); status_changed.set_sink_ready(sink_ready); @@ -138,225 +81,118 @@ mod tests { } /// Utility function to create a PortNotification event - fn notification(alert: bool, discover_mode_completed: bool) -> PortNotification { - let mut notification = PortNotification::none(); + fn notification(alert: bool, discover_mode_completed: bool) -> PortNotificationEventBitfield { + let mut notification = PortNotificationEventBitfield::none(); notification.set_alert(alert); notification.set_discover_mode_completed(discover_mode_completed); notification } /// Test iterating over port status changed events - #[tokio::test] - async fn test_port_status_changed() { - let mut pending_ports = PortPending::none(); - pending_ports.pend_port(0).unwrap(); - pending_ports.pend_port(2).unwrap(); - pending_ports.pend_port(3).unwrap(); - - let mut streamer = PortEventStreamer::new(pending_ports.into_iter()); + #[test] + fn test_port_status_changed() { + let events = [ + status_changed(true, true, true).into(), + status_changed(true, false, true).into(), + status_changed(false, false, true).into(), + ]; + let mut streamer = PortEventStreamer::new(events.iter().copied()); - let event = streamer - .next::<(), _, _>(async |_| Ok(status_changed(true, true, true).into())) - .await; assert_eq!( - event, - Ok(Some(( - 0, - PortEventVariant::StatusChanged(status_changed(true, true, true)) - ))) + streamer.next(), + Some((0, PortEvent::StatusChanged(status_changed(true, true, true)))) ); - - let event = streamer - .next::<(), _, _>(async |_| Ok(status_changed(true, false, true).into())) - .await; assert_eq!( - event, - Ok(Some(( - 2, - PortEventVariant::StatusChanged(status_changed(true, false, true)) - ))) + streamer.next(), + Some((1, PortEvent::StatusChanged(status_changed(true, false, true)))) ); - - let event = streamer - .next::<(), _, _>(async |_| Ok(status_changed(false, false, true).into())) - .await; assert_eq!( - event, - Ok(Some(( - 3, - PortEventVariant::StatusChanged(status_changed(false, false, true)) - ))) + streamer.next(), + Some((2, PortEvent::StatusChanged(status_changed(false, false, true)))) ); - - let event = streamer - .next::<(), _, _>(async |_| Ok(status_changed(false, false, true).into())) - .await; - assert_eq!(event, Ok(None)); + assert_eq!(streamer.next(), None); } /// Test iterating over port notifications - #[tokio::test] - async fn test_port_notification() { - let mut pending_ports = PortPending::none(); - pending_ports.pend_port(0).unwrap(); - - let mut streamer = PortEventStreamer::new(pending_ports.into_iter()); - let event = streamer - .next::<(), _, _>(async |_| Ok(notification(true, true).into())) - .await; - assert_eq!( - event, - Ok(Some((0, PortEventVariant::Notification(PortNotificationSingle::Alert)))) - ); - - let event = streamer - .next::<(), _, _>(async |_| Ok(notification(true, true).into())) - .await; - assert_eq!( - event, - Ok(Some(( - 0, - PortEventVariant::Notification(PortNotificationSingle::DiscoverModeCompleted) - ))) - ); - - let event = streamer - .next::<(), _, _>(async |_| Ok(notification(true, true).into())) - .await; - assert_eq!(event, Ok(None)); + #[test] + fn test_port_notification() { + let events = [notification(true, true).into()]; + let mut streamer = PortEventStreamer::new(events.iter().copied()); + + assert_eq!(streamer.next(), Some((0, PortEvent::Alert))); + assert_eq!(streamer.next(), Some((0, PortEvent::DiscoverModeCompleted))); + assert_eq!(streamer.next(), None); } - /// Test the the final port with no pending notifications - #[tokio::test] - async fn test_last_notifications() { - let mut pending_ports = PortPending::none(); - pending_ports.pend_port(0).unwrap(); - - let mut streamer = PortEventStreamer::new(pending_ports.into_iter()); - - // Test p0 events + /// Test the final port with no pending notifications + #[test] + fn test_last_notifications() { let p0_event = status_changed(true, true, true).into(); - let event = streamer.next::<(), _, _>(async |_| Ok(p0_event)).await; + let events = [p0_event]; + let mut streamer = PortEventStreamer::new(events.iter().copied()); + assert_eq!( - event, - Ok(Some(( - 0, - PortEventVariant::StatusChanged(status_changed(true, true, true)) - ))) + streamer.next(), + Some((0, PortEvent::StatusChanged(status_changed(true, true, true)))) ); - - let event = streamer.next::<(), _, _>(async |_| Ok(p0_event)).await; - assert_eq!(event, Ok(None)); + assert_eq!(streamer.next(), None); } /// Test iterating over both status and notification events - #[tokio::test] - async fn test_port_event() { - let mut pending_ports = PortPending::none(); - pending_ports.pend_port(0).unwrap(); - pending_ports.pend_port(6).unwrap(); - - let mut streamer = PortEventStreamer::new(pending_ports.into_iter()); - - // Test p0 events - let p0_event = PortEvent { + #[test] + fn test_port_event() { + let p0_event = PortEventBitfield { status: status_changed(true, true, true), notification: notification(true, false), }; - - let event = streamer.next::<(), _, _>(async |_| Ok(p0_event)).await; - assert_eq!( - event, - Ok(Some(( - 0, - PortEventVariant::StatusChanged(status_changed(true, true, true)) - ))) - ); - - let event = streamer.next::<(), _, _>(async |_| Ok(p0_event)).await; - assert_eq!( - event, - Ok(Some((0, PortEventVariant::Notification(PortNotificationSingle::Alert)))) - ); - - // Test p6 events - let p6_event = PortEvent { + let p1_event = PortEventBitfield { status: status_changed(false, true, false), notification: notification(false, true), }; + let events = [p0_event, p1_event]; + let mut streamer = PortEventStreamer::new(events.iter().copied()); - let event = streamer.next::<(), _, _>(async |_| Ok(p6_event)).await; assert_eq!( - event, - Ok(Some(( - 6, - PortEventVariant::StatusChanged(status_changed(false, true, false)) - ))) + streamer.next(), + Some((0, PortEvent::StatusChanged(status_changed(true, true, true)))) ); - - let event = streamer.next::<(), _, _>(async |_| Ok(p6_event)).await; + assert_eq!(streamer.next(), Some((0, PortEvent::Alert))); assert_eq!( - event, - Ok(Some(( - 6, - PortEventVariant::Notification(PortNotificationSingle::DiscoverModeCompleted) - ))) + streamer.next(), + Some((1, PortEvent::StatusChanged(status_changed(false, true, false)))) ); - - let event = streamer.next::<(), _, _>(async |_| Ok(p6_event)).await; - assert_eq!(event, Ok(None)); + assert_eq!(streamer.next(), Some((1, PortEvent::DiscoverModeCompleted))); + assert_eq!(streamer.next(), None); } /// Test no pending ports - #[tokio::test] - async fn test_no_pending_ports() { - let pending_ports = PortPending::none(); - let mut streamer = PortEventStreamer::new(pending_ports.into_iter()); - let event = streamer - .next::<(), _, _>(async |_| Ok(status_changed(true, true, true).into())) - .await; - assert_eq!(event, Ok(None)); + #[test] + fn test_no_pending_ports() { + let events: [PortEventBitfield; 0] = []; + let mut streamer = PortEventStreamer::new(events.iter().copied()); + + assert_eq!(streamer.next(), None); } /// Test a port with a pending event with no actual event - #[tokio::test] - async fn test_empty_event() { - let mut pending_ports = PortPending::none(); - pending_ports.pend_port(0).unwrap(); + #[test] + fn test_empty_event() { + let events = [PortEventBitfield::none()]; + let mut streamer = PortEventStreamer::new(events.iter().copied()); - let mut streamer = PortEventStreamer::new(pending_ports.into_iter()); - let event = streamer.next::<(), _, _>(async |_| Ok(PortEvent::none())).await; - assert_eq!(event, Ok(None)); + assert_eq!(streamer.next(), None); } /// Test advancing to the next port when there are no events - #[tokio::test] - async fn test_skip_no_pending() { - let mut pending_ports = PortPending::none(); - pending_ports.pend_port(0).unwrap(); - pending_ports.pend_port(1).unwrap(); + #[test] + fn test_skip_no_pending() { + let events = [PortEventBitfield::none(), status_changed(true, true, true).into()]; + let mut streamer = PortEventStreamer::new(events.iter().copied()); - let mut streamer = PortEventStreamer::new(pending_ports.into_iter()); - let event = streamer - .next::<(), _, _>(async |_| { - static HAVE_EVENTS: AtomicBool = AtomicBool::new(false); - let have_events = HAVE_EVENTS.load(core::sync::atomic::Ordering::Relaxed); - let event = Ok(status_changed(have_events, have_events, have_events).into()); - HAVE_EVENTS.store(true, core::sync::atomic::Ordering::Relaxed); - event - }) - .await; assert_eq!( - event, - Ok(Some(( - 1, - PortEventVariant::StatusChanged(status_changed(true, true, true)) - ))) + streamer.next(), + Some((1, PortEvent::StatusChanged(status_changed(true, true, true)))) ); - - let event = streamer - .next::<(), _, _>(async |_| Ok(status_changed(false, false, false).into())) - .await; - assert_eq!(event, Ok(None)); + assert_eq!(streamer.next(), None); } } diff --git a/type-c-service/src/service/controller.rs b/type-c-service/src/service/controller.rs deleted file mode 100644 index de1c22732..000000000 --- a/type-c-service/src/service/controller.rs +++ /dev/null @@ -1,60 +0,0 @@ -use embedded_services::{ - debug, error, - type_c::{ - ControllerId, - external::{self, ControllerCommandData}, - }, -}; - -use super::*; - -impl<'a> Service<'a> { - /// Process external controller status command - pub(super) async fn process_external_controller_status( - &self, - controller: ControllerId, - ) -> external::Response<'static> { - let status = self.context.get_controller_status(controller).await; - if let Err(e) = status { - error!("Error getting controller status: {:#?}", e); - } - external::Response::Controller(status.map(external::ControllerResponseData::ControllerStatus)) - } - - /// Process external controller sync state command - pub(super) async fn process_external_controller_sync_state( - &self, - controller: ControllerId, - ) -> external::Response<'static> { - let status = self.context.sync_controller_state(controller).await; - if let Err(e) = status { - error!("Error getting controller sync state: {:#?}", e); - } - external::Response::Controller(status.map(|_| external::ControllerResponseData::Complete)) - } - - /// Process external controller reset command - pub(super) async fn process_external_controller_reset( - &self, - controller: ControllerId, - ) -> external::Response<'static> { - let status = self.context.reset_controller(controller).await; - if let Err(e) = status { - error!("Error resetting controller: {:#?}", e); - } - external::Response::Controller(status.map(|_| external::ControllerResponseData::Complete)) - } - - /// Process external controller commands - pub(super) async fn process_external_controller_command( - &self, - command: &external::ControllerCommand, - ) -> external::Response<'static> { - debug!("Processing external controller command: {:#?}", command); - match command.data { - ControllerCommandData::ControllerStatus => self.process_external_controller_status(command.id).await, - ControllerCommandData::SyncState => self.process_external_controller_sync_state(command.id).await, - ControllerCommandData::Reset => self.process_external_controller_reset(command.id).await, - } - } -} diff --git a/type-c-service/src/service/event_receiver.rs b/type-c-service/src/service/event_receiver.rs new file mode 100644 index 000000000..b048fc9a6 --- /dev/null +++ b/type-c-service/src/service/event_receiver.rs @@ -0,0 +1,100 @@ +use core::pin::pin; + +use crate::service::Event; +use embassy_futures::select::{Either, select, select_slice}; +use embedded_services::{event::Receiver, sync::Lockable}; +use power_policy_interface::service::event::EventData as PowerPolicyEventData; +use type_c_interface::{port::pd::Pd, service::event::PortEvent}; + +struct PowerPolicySubscriber> { + receiver: PowerReceiver, +} + +impl> PowerPolicySubscriber { + /// Wait for a power policy event + async fn wait_next(&mut self) -> PowerPolicyEventData { + self.receiver.wait_next().await + } +} + +pub struct ArrayPortReceivers< + 'port, + const N: usize, + Port: Lockable, + PortReceiver: Receiver, +> { + ports: [&'port Port; N], + port_receivers: [PortReceiver; N], +} + +impl< + 'port, + const N: usize, + Port: Lockable, + PortReceiver: Receiver, +> ArrayPortReceivers<'port, N, Port, PortReceiver> +{ + /// Get the next pending PSU event + pub async fn wait_next(&mut self) -> Event<'port, Port> { + let ((event, port), _) = { + let mut futures = heapless::Vec::<_, N>::new(); + for (receiver, psu) in self.port_receivers.iter_mut().zip(self.ports.iter()) { + // Push will never fail since the number of receivers is the same as the capacity of the vector + let _ = futures.push(async move { (receiver.wait_next().await, psu) }); + } + select_slice(pin!(&mut futures)).await + }; + + Event::PortEvent(PortEvent { port: *port, event }) + } +} + +/// Struct used to contain port event receivers and manage mapping from a receiver to its corresponding device. +pub struct ArrayEventReceiver< + 'a, + const N: usize, + Port: Lockable, + PortReceiver: Receiver, + PowerReceiver: Receiver, +> { + /// Power policy event subscriber + power_policy_event_subscriber: PowerPolicySubscriber, + /// Port event receivers and corresponding ports + port_receivers: ArrayPortReceivers<'a, N, Port, PortReceiver>, +} + +impl< + 'port, + const N: usize, + Port: Lockable, + PortReceiver: Receiver, + PowerReceiver: Receiver, +> ArrayEventReceiver<'port, N, Port, PortReceiver, PowerReceiver> +{ + /// Create a new instance + pub fn new( + ports: [&'port Port; N], + port_receivers: [PortReceiver; N], + power_policy_event_receiver: PowerReceiver, + ) -> Self { + Self { + port_receivers: ArrayPortReceivers { ports, port_receivers }, + power_policy_event_subscriber: PowerPolicySubscriber { + receiver: power_policy_event_receiver, + }, + } + } + + /// Wait for the next event, whether it's a port event or a power policy event + pub async fn wait_next(&mut self) -> Event<'port, Port> { + match select( + self.port_receivers.wait_next(), + self.power_policy_event_subscriber.wait_next(), + ) + .await + { + Either::First(event) => event, + Either::Second(event) => Event::PowerPolicy(event), + } + } +} diff --git a/type-c-service/src/service/mod.rs b/type-c-service/src/service/mod.rs index 753fc5ba6..699a4c276 100644 --- a/type-c-service/src/service/mod.rs +++ b/type-c-service/src/service/mod.rs @@ -1,230 +1,153 @@ -use embassy_futures::select::{Either3, select3}; -use embassy_sync::{ - mutex::Mutex, - pubsub::{DynImmediatePublisher, DynSubscriber}, -}; -use embedded_services::{ - GlobalRawMutex, debug, error, info, intrusive_list, - ipc::deferred, - trace, - type_c::{ - self, comms, - controller::PortStatus, - event::{PortNotificationSingle, PortStatusChanged}, - external, - }, -}; -use embedded_services::{power::policy as power_policy, type_c::Cached}; +use core::marker::PhantomData; +use core::ptr; + +use embedded_services::event::Sender as _; +use embedded_services::named::Named as _; +use embedded_services::sync::Lockable; +use embedded_services::{debug, error, info, trace}; use embedded_usb_pd::GlobalPortId; use embedded_usb_pd::PdError as Error; +use power_policy_interface::service::event::EventData as PowerPolicyEventData; +use type_c_interface::control::pd::PortStatus; +use type_c_interface::port::pd::Pd; +use type_c_interface::service::event::{DebugAccessoryData, EventData, PortEvent, PortEventData}; + +use type_c_interface::port::event::PortStatusEventBitfield; +use type_c_interface::service::event::Event as ServiceEvent; -use crate::{PortEventStreamer, PortEventVariant}; +use crate::service::registration::Registration; pub mod config; -mod controller; -pub mod pd; -mod port; +pub mod event_receiver; mod power; +pub mod registration; mod ucsi; -pub mod vdm; - -const MAX_SUPPORTED_PORTS: usize = 4; - -/// Maximum number of power policy events to buffer -/// Arbitrary number, but power policy events in general shouldn't be too frequent -pub const MAX_POWER_POLICY_EVENTS: usize = 4; -/// Type-C service state -#[derive(Default)] -struct State { - /// Current port status - port_status: [PortStatus; MAX_SUPPORTED_PORTS], - /// Next port to check, this is used to round-robin through ports - port_event_streaming_state: Option, +/// Type-C service +/// +/// Constructing a Service is the first step in using the Type-C service. +/// Arguments should be an initialized context +pub struct Service<'port, Reg: Registration<'port>> { /// UCSI state ucsi: ucsi::State, -} - -/// Type-C service -pub struct Service<'a> { - /// Type-C context token - context: type_c::controller::ContextToken, - /// Current state - state: Mutex, /// Config config: config::Config, - /// Power policy event receiver - /// - /// This is the corresponding publisher to [`Self::power_policy_event_subscriber`], power policy events - /// will be buffered in the channel until they are brought into the event loop with the subscriber. - power_policy_event_publisher: embedded_services::broadcaster::immediate::Receiver<'a, power_policy::CommsMessage>, - /// Power policy event subscriber - /// - /// This is the corresponding subscriber to [`Self::power_policy_event_publisher`], needs to be a mutex because getting a message - /// from the channel requires mutable access. - power_policy_event_subscriber: Mutex>, -} - -/// Power policy events -// This is present instead of just using [`power_policy::CommsMessage`] to allow for -// supporting variants like `ConsumerConnected(GlobalPortId, ConsumerPowerCapability)` -// But there's currently not a way to do look-ups between power policy device IDs and GlobalPortIds -pub enum PowerPolicyEvent { - /// Unconstrained state changed - Unconstrained(power_policy::UnconstrainedState), - /// Consumer disconnected - ConsumerDisconnected, - /// Consumer connected - ConsumerConnected, + /// Service registration + registration: Reg, + _phantom: PhantomData<&'port ()>, } /// Type-C service events -pub enum Event<'a> { +#[derive(Clone)] +pub enum Event<'port, Port: Lockable> { /// Port event - PortStatusChanged(GlobalPortId, PortStatusChanged, PortStatus), - /// A controller notified of an event that occurred. - PortNotification(GlobalPortId, PortNotificationSingle), - /// External command - ExternalCommand(deferred::Request<'a, GlobalRawMutex, external::Command, external::Response<'static>>), + PortEvent(PortEvent<'port, Port>), /// Power policy event - PowerPolicy(PowerPolicyEvent), + PowerPolicy(PowerPolicyEventData), } -impl<'a> Service<'a> { +impl<'port, Reg: Registration<'port>> Service<'port, Reg> { /// Create a new service the given configuration - pub fn create( - config: config::Config, - power_policy_publisher: DynImmediatePublisher<'a, power_policy::CommsMessage>, - power_policy_subscriber: DynSubscriber<'a, power_policy::CommsMessage>, - ) -> Option { - Some(Self { - context: type_c::controller::ContextToken::create()?, - state: Mutex::new(State::default()), + pub fn create(config: config::Config, registration: Reg) -> Self { + Self { + ucsi: ucsi::State::default(), config, - power_policy_event_publisher: power_policy_publisher.into(), - power_policy_event_subscriber: Mutex::new(power_policy_subscriber), - }) + registration, + _phantom: PhantomData, + } } - /// Get the cached port status - pub async fn get_cached_port_status(&self, port_id: GlobalPortId) -> Result { - let state = self.state.lock().await; - Ok(*state.port_status.get(port_id.0 as usize).ok_or(Error::InvalidPort)?) + fn get_port_index(&self, port: &'port Reg::Port) -> Result { + self.registration + .ports() + .iter() + .position(|p| ptr::eq(*p, port)) + .ok_or(Error::InvalidPort) } - /// Set the cached port status - async fn set_cached_port_status(&self, port_id: GlobalPortId, status: PortStatus) -> Result<(), Error> { - let mut state = self.state.lock().await; - *state - .port_status - .get_mut(port_id.0 as usize) - .ok_or(Error::InvalidPort)? = status; - Ok(()) + /// Look up the port for a given global port ID + fn lookup_port(&self, port_id: GlobalPortId) -> Result<&'port Reg::Port, Error> { + self.registration + .ports() + .get(port_id.0 as usize) + .ok_or(Error::InvalidPort) + .copied() + } + + /// Send an event to all registered listeners + async fn broadcast_event(&mut self, event: ServiceEvent<'port, Reg::Port>) { + for sender in self.registration.event_senders() { + sender.send(event.clone()).await; + } } /// Process events for a specific port - async fn process_port_event( - &self, - port_id: GlobalPortId, - event: PortStatusChanged, - status: PortStatus, + async fn process_port_status_event( + &mut self, + port: &'port Reg::Port, + event: PortStatusEventBitfield, + new_status: PortStatus, + old_status: PortStatus, ) -> Result<(), Error> { - let old_status = self.get_cached_port_status(port_id).await?; + let port_name = { port.lock().await.name() }; - debug!("Port{}: Event: {:#?}", port_id.0, event); - debug!("Port{} Previous status: {:#?}", port_id.0, old_status); - debug!("Port{} Status: {:#?}", port_id.0, status); + debug!("({}): Event: {:#?}", port_name, event); + debug!("({}) Previous status: {:#?}", port_name, old_status); + debug!("({}) Status: {:#?}", port_name, new_status); - let connection_changed = status.is_connected() != old_status.is_connected(); - if connection_changed && (status.is_debug_accessory() || old_status.is_debug_accessory()) { + let connection_changed = new_status.is_connected() != old_status.is_connected(); + if connection_changed && (new_status.is_debug_accessory() || old_status.is_debug_accessory()) { // Notify that a debug connection has connected/disconnected - if status.is_connected() { - debug!("Port{}: Debug accessory connected", port_id.0); + if new_status.is_connected() { + debug!("({}): Debug accessory connected", port_name); } else { - debug!("Port{}: Debug accessory disconnected", port_id.0); + debug!("({}): Debug accessory disconnected", port_name); } - self.context - .broadcast_message(comms::CommsMessage::DebugAccessory(comms::DebugAccessoryMessage { - port: port_id, - connected: status.is_connected(), - })) - .await; + self.broadcast_event(ServiceEvent { + port, + event: EventData::DebugAccessory(DebugAccessoryData { + connected: new_status.is_connected(), + }), + }) + .await; } - self.set_cached_port_status(port_id, status).await?; - self.handle_ucsi_port_event(port_id, event, &status).await; + self.handle_ucsi_port_event(port, GlobalPortId(self.get_port_index(port)? as u8), event, &new_status) + .await; Ok(()) } - /// Process external commands - async fn process_external_command(&self, command: &external::Command) -> external::Response<'static> { - match command { - external::Command::Controller(command) => self.process_external_controller_command(command).await, - external::Command::Port(command) => self.process_external_port_command(command).await, - external::Command::Ucsi(command) => external::Response::Ucsi(self.process_ucsi_command(command).await), - } - } - - /// Wait for the next event - pub async fn wait_next(&self) -> Result, Error> { - loop { - match select3( - self.wait_port_flags(), - self.context.wait_external_command(), - self.wait_power_policy_event(), - ) - .await - { - Either3::First(mut stream) => { - if let Some((port_id, event)) = stream - .next(|port_id| self.context.get_port_event(GlobalPortId(port_id as u8))) - .await? - { - let port_id = GlobalPortId(port_id as u8); - self.state.lock().await.port_event_streaming_state = Some(stream); - match event { - PortEventVariant::StatusChanged(status_event) => { - // Return a port status changed event - let status = self.context.get_port_status(port_id, Cached(true)).await?; - return Ok(Event::PortStatusChanged(port_id, status_event, status)); - } - PortEventVariant::Notification(notification) => { - // Other notifications - trace!("Port notification: {:?}", notification); - return Ok(Event::PortNotification(port_id, notification)); - } - } - } else { - self.state.lock().await.port_event_streaming_state = None; - } - } - Either3::Second(request) => { - return Ok(Event::ExternalCommand(request)); - } - Either3::Third(event) => return Ok(event), + async fn process_port_event(&mut self, event: &PortEvent<'port, Reg::Port>) -> Result<(), Error> { + match &event.event { + PortEventData::StatusChanged(status_event) => { + self.process_port_status_event( + event.port, + status_event.status_event, + status_event.current_status, + status_event.previous_status, + ) + .await + } + unhandled => { + // Currently just log notifications, but may want to do more in the future + debug!( + "({}): Received notification event: {:#?}", + event.port.lock().await.name(), + unhandled + ); + Ok(()) } } } /// Process the given event - pub async fn process_event(&self, event: Event<'_>) -> Result<(), Error> { + pub async fn process_event(&mut self, event: Event<'port, Reg::Port>) -> Result<(), Error> { match event { - Event::PortStatusChanged(port, event_kind, status) => { - trace!("Port{}: Processing port status changed", port.0); - self.process_port_event(port, event_kind, status).await - } - Event::PortNotification(port, notification) => { - // Other port notifications - info!("Port{}: Got port notification: {:?}", port.0, notification); - Ok(()) - } - Event::ExternalCommand(request) => { - trace!("Processing external command"); - let response = self.process_external_command(&request.command).await; - request.respond(response); - Ok(()) + Event::PortEvent(event) => { + trace!("({}): Processing port event", event.port.lock().await.name()); + self.process_port_event(&event).await } Event::PowerPolicy(event) => { trace!("Processing power policy event"); @@ -232,15 +155,4 @@ impl<'a> Service<'a> { } } } - - /// Combined processing function - pub async fn process_next_event(&self) -> Result<(), Error> { - let event = self.wait_next().await?; - self.process_event(event).await - } - - /// Register the Type-C service with the power policy service - pub fn register_comms(&'static self) -> Result<(), intrusive_list::Error> { - power_policy::policy::register_message_receiver(&self.power_policy_event_publisher) - } } diff --git a/type-c-service/src/service/pd.rs b/type-c-service/src/service/pd.rs deleted file mode 100644 index 21934fa76..000000000 --- a/type-c-service/src/service/pd.rs +++ /dev/null @@ -1,14 +0,0 @@ -//! Power Delivery (PD) related functionality. - -use embedded_usb_pd::{GlobalPortId, PdError, ado::Ado}; - -use super::Service; - -impl Service<'_> { - /// Get the oldest unhandled PD alert for the given port. - /// - /// Returns [`None`] if no alerts are pending. - pub async fn get_pd_alert(&self, port: GlobalPortId) -> Result, PdError> { - self.context.get_pd_alert(port).await - } -} diff --git a/type-c-service/src/service/port.rs b/type-c-service/src/service/port.rs deleted file mode 100644 index 199d95b9f..000000000 --- a/type-c-service/src/service/port.rs +++ /dev/null @@ -1,341 +0,0 @@ -use core::num::NonZeroU8; - -use embedded_services::{ - debug, error, - type_c::{ - controller::{DpConfig, PdStateMachineConfig, TbtConfig, TypeCStateMachineState, UsbControlConfig}, - external, - }, -}; -use embedded_usb_pd::GlobalPortId; - -use super::*; -use crate::PortEventStreamer; - -use embedded_services::type_c::controller::SendVdm; - -impl<'a> Service<'a> { - /// Wait for port flags - pub(super) async fn wait_port_flags(&self) -> PortEventStreamer { - if let Some(ref streamer) = self.state.lock().await.port_event_streaming_state { - // If we have an existing iterator, return it - // Yield first to prevent starving other tasks - embassy_futures::yield_now().await; - *streamer - } else { - // Wait for the next port event and create a streamer - PortEventStreamer::new(self.context.get_unhandled_events().await.into_iter()) - } - } - - /// Process external port commands - pub(super) async fn process_external_port_command( - &self, - command: &external::PortCommand, - ) -> external::Response<'static> { - debug!("Processing external port command: {:#?}", command); - match command.data { - external::PortCommandData::PortStatus(cached) => { - self.process_external_port_status(command.port, cached).await - } - external::PortCommandData::RetimerFwUpdateGetState => { - self.process_get_rt_fw_update_status(command.port).await - } - external::PortCommandData::RetimerFwUpdateSetState => { - self.process_set_rt_fw_update_state(command.port).await - } - external::PortCommandData::RetimerFwUpdateClearState => { - self.process_clear_rt_fw_update_state(command.port).await - } - external::PortCommandData::SetRetimerCompliance => self.process_set_rt_compliance(command.port).await, - external::PortCommandData::ReconfigureRetimer => self.process_reconfigure_retimer(command.port).await, - external::PortCommandData::SetMaxSinkVoltage { max_voltage_mv } => { - self.process_set_max_sink_voltage(command.port, max_voltage_mv).await - } - external::PortCommandData::ClearDeadBatteryFlag => self.process_clear_dead_battery_flag(command.port).await, - external::PortCommandData::SendVdm(tx_vdm) => self.process_send_vdm(command.port, tx_vdm).await, - external::PortCommandData::SetUsbControl(config) => { - self.process_set_usb_control(command.port, config).await - } - external::PortCommandData::GetDpStatus => self.process_get_dp_status(command.port).await, - external::PortCommandData::SetDpConfig(config) => self.process_set_dp_config(command.port, config).await, - external::PortCommandData::ExecuteDrst => self.process_execute_drst(command.port).await, - external::PortCommandData::SetTbtConfig(config) => self.process_set_tbt_config(command.port, config).await, - external::PortCommandData::SetPdStateMachineConfig(config) => { - self.process_set_pd_state_machine_config(command.port, config).await - } - external::PortCommandData::SetTypeCStateMachineConfig(state) => { - self.process_set_type_c_state_machine_config(command.port, state).await - } - external::PortCommandData::ExecuteElectricalDisconnect { reconnect_time_s } => { - self.process_execute_electrical_disconnect(command.port, reconnect_time_s) - .await - } - external::PortCommandData::SetSystemPowerState(state) => { - self.process_set_power_state(command.port, state).await - } - external::PortCommandData::GetDiscoveredSvids => self.process_get_discovered_svids(command.port).await, - external::PortCommandData::HardReset => self.process_hard_reset(command.port).await, - external::PortCommandData::GetDiscoverIdentitySop => { - self.process_get_discover_identity_sop_response(command.port).await - } - external::PortCommandData::GetDiscoverIdentitySopPrime => { - self.process_get_discover_identity_sop_prime_response(command.port) - .await - } - } - } - - /// Process external port status command - pub(super) async fn process_external_port_status( - &self, - port_id: GlobalPortId, - cached: Cached, - ) -> external::Response<'static> { - let status = self.context.get_port_status(port_id, cached).await; - if let Err(e) = status { - error!("Error getting port status: {:#?}", e); - } - external::Response::Port(status.map(external::PortResponseData::PortStatus)) - } - - /// Process get retimer fw update status commands - pub(super) async fn process_get_rt_fw_update_status(&self, port_id: GlobalPortId) -> external::Response<'static> { - let status = self.context.get_rt_fw_update_status(port_id).await; - if let Err(e) = status { - error!("Error getting retimer fw update status: {:#?}", e); - } - - external::Response::Port(status.map(external::PortResponseData::RetimerFwUpdateGetState)) - } - - /// Process set retimer fw update state commands - pub(super) async fn process_set_rt_fw_update_state(&self, port_id: GlobalPortId) -> external::Response<'static> { - let status = self.context.set_rt_fw_update_state(port_id).await; - if let Err(e) = status { - error!("Error setting retimer fw update state: {:#?}", e); - } - - external::Response::Port(status.map(|_| external::PortResponseData::Complete)) - } - - /// Process clear retimer fw update state commands - pub(super) async fn process_clear_rt_fw_update_state(&self, port_id: GlobalPortId) -> external::Response<'static> { - let status = self.context.clear_rt_fw_update_state(port_id).await; - if let Err(e) = status { - error!("Error clear retimer fw update state: {:#?}", e); - } - - external::Response::Port(status.map(|_| external::PortResponseData::Complete)) - } - - /// Process set retimer compliance - pub(super) async fn process_set_rt_compliance(&self, port_id: GlobalPortId) -> external::Response<'static> { - let status = self.context.set_rt_compliance(port_id).await; - if let Err(e) = status { - error!("Error set retimer compliance: {:#?}", e); - } - - external::Response::Port(status.map(|_| external::PortResponseData::Complete)) - } - - async fn process_reconfigure_retimer(&self, port_id: GlobalPortId) -> external::Response<'static> { - let status = self.context.reconfigure_retimer(port_id).await; - if let Err(e) = status { - error!("Error reconfiguring retimer: {:#?}", e); - } - - external::Response::Port(status.map(|_| external::PortResponseData::Complete)) - } - - async fn process_set_max_sink_voltage( - &self, - port_id: GlobalPortId, - max_voltage_mv: Option, - ) -> external::Response<'static> { - let status = self.context.set_max_sink_voltage(port_id, max_voltage_mv).await; - if let Err(e) = status { - error!("Error setting max voltage: {:#?}", e); - } - - external::Response::Port(status.map(|_| external::PortResponseData::Complete)) - } - - async fn process_clear_dead_battery_flag(&self, port_id: GlobalPortId) -> external::Response<'static> { - let status = self.context.clear_dead_battery_flag(port_id).await; - if let Err(e) = status { - error!("Error clearing dead battery flag: {:#?}", e); - } - - external::Response::Port(status.map(|_| external::PortResponseData::Complete)) - } - - /// Process send vdm commands - async fn process_send_vdm(&self, port_id: GlobalPortId, tx_vdm: SendVdm) -> external::Response<'static> { - let status = self.context.send_vdm(port_id, tx_vdm).await; - if let Err(e) = status { - error!("Error sending VDM data: {:#?}", e); - } - - external::Response::Port(status.map(|_| external::PortResponseData::Complete)) - } - - /// Process set USB control commands - async fn process_set_usb_control( - &self, - port_id: GlobalPortId, - config: UsbControlConfig, - ) -> external::Response<'static> { - let status = self.context.set_usb_control(port_id, config).await; - if let Err(e) = status { - error!("Error setting USB control: {:#?}", e); - } - - external::Response::Port(status.map(|_| external::PortResponseData::Complete)) - } - - /// Process get DisplayPort status commands - async fn process_get_dp_status(&self, port_id: GlobalPortId) -> external::Response<'static> { - let status = self.context.get_dp_status(port_id).await; - if let Err(e) = status { - error!("Error getting DP status: {:#?}", e); - } - - external::Response::Port(status.map(external::PortResponseData::GetDpStatus)) - } - - /// Process set DisplayPort config commands - async fn process_set_dp_config(&self, port_id: GlobalPortId, config: DpConfig) -> external::Response<'static> { - let status = self.context.set_dp_config(port_id, config).await; - if let Err(e) = status { - error!("Error setting DP config: {:#?}", e); - } - - external::Response::Port(status.map(|_| external::PortResponseData::Complete)) - } - - /// Process execute PD data reset commands - async fn process_execute_drst(&self, port_id: GlobalPortId) -> external::Response<'static> { - let status = self.context.execute_drst(port_id).await; - if let Err(e) = status { - error!("Error executing PD data reset: {:#?}", e); - } - - external::Response::Port(status.map(|_| external::PortResponseData::Complete)) - } - - /// Process set Thunderbolt configuration command - async fn process_set_tbt_config(&self, port_id: GlobalPortId, config: TbtConfig) -> external::Response<'static> { - let status = self.context.set_tbt_config(port_id, config).await; - if let Err(e) = status { - error!("Error setting TBT config: {:#?}", e); - } - - external::Response::Port(status.map(|_| external::PortResponseData::Complete)) - } - - /// Process set PD state-machine configuration command - async fn process_set_pd_state_machine_config( - &self, - port_id: GlobalPortId, - config: PdStateMachineConfig, - ) -> external::Response<'static> { - let status = self.context.set_pd_state_machine_config(port_id, config).await; - if let Err(e) = status { - error!("Error setting PD state-machine config: {:#?}", e); - } - - external::Response::Port(status.map(|_| external::PortResponseData::Complete)) - } - - /// Process set Type-C state-machine configuration command - async fn process_set_type_c_state_machine_config( - &self, - port_id: GlobalPortId, - state: TypeCStateMachineState, - ) -> external::Response<'static> { - let status = self.context.set_type_c_state_machine_config(port_id, state).await; - if let Err(e) = status { - error!("Error setting Type-C state-machine config: {:#?}", e); - } - - external::Response::Port(status.map(|_| external::PortResponseData::Complete)) - } - - /// Process [`external::PortCommandData::ExecuteElectricalDisconnect`] command - /// - /// The `reconnect_time_s` parameter specifies the time, in seconds, after which the port should automatically reconnect. - /// If [`None`], the port will not automatically reconnect. - async fn process_execute_electrical_disconnect( - &self, - port_id: GlobalPortId, - reconnect_time_s: Option, - ) -> external::Response<'static> { - let status = self - .context - .execute_electrical_disconnect(port_id, reconnect_time_s) - .await; - if let Err(e) = status { - error!("Error executing electrical disconnect: {:#?}", e); - } - - external::Response::Port(status.map(|_| external::PortResponseData::Complete)) - } - - /// Process [`external::PortCommandData::SetSystemPowerState`] command - async fn process_set_power_state( - &self, - port_id: GlobalPortId, - state: embedded_services::type_c::controller::SystemPowerState, - ) -> external::Response<'static> { - let status = self.context.set_power_state(port_id, state).await; - if let Err(e) = status { - error!("Error setting power state: {:#?}", e); - } - - external::Response::Port(status.map(|_| external::PortResponseData::Complete)) - } - - /// Process [`external::PortCommandData::GetDiscoveredSvids`] command - async fn process_get_discovered_svids(&self, port_id: GlobalPortId) -> external::Response<'static> { - let status = self.context.get_discovered_svids(port_id).await; - if let Err(e) = status { - error!("Error getting discovered SVIDs: {:#?}", e); - } - - external::Response::Port(status.map(external::PortResponseData::DiscoveredSvids)) - } - - /// Process [`external::PortCommandData::HardReset`] command - async fn process_hard_reset(&self, port_id: GlobalPortId) -> external::Response<'static> { - let status = self.context.hard_reset(port_id).await; - if let Err(e) = status { - error!("Error executing hard reset: {:#?}", e); - } - - external::Response::Port(status.map(|_| external::PortResponseData::Complete)) - } - - /// Process [`external::PortCommandData::GetDiscoverIdentitySop`] command - async fn process_get_discover_identity_sop_response(&self, port_id: GlobalPortId) -> external::Response<'static> { - let status = self.context.get_discover_identity_sop_response(port_id).await; - if let Err(e) = status { - error!("Error getting Discover Identity SOP response: {:#?}", e); - } - - external::Response::Port(status.map(external::PortResponseData::DiscoverIdentitySop)) - } - - /// Process [`external::PortCommandData::GetDiscoverIdentitySopPrime`] command - async fn process_get_discover_identity_sop_prime_response( - &self, - port_id: GlobalPortId, - ) -> external::Response<'static> { - let status = self.context.get_discover_identity_sop_prime_response(port_id).await; - if let Err(e) = status { - error!("Error getting Discover Identity SOP' response: {:#?}", e); - } - - external::Response::Port(status.map(external::PortResponseData::DiscoverIdentitySopPrime)) - } -} diff --git a/type-c-service/src/service/power.rs b/type-c-service/src/service/power.rs index c6bb628d2..5e443713c 100644 --- a/type-c-service/src/service/power.rs +++ b/type-c-service/src/service/power.rs @@ -1,53 +1,27 @@ -use embassy_sync::pubsub::WaitResult; -use embedded_services::power::policy as power_policy; +use core::ptr; -use super::*; +use embedded_services::sync::Lockable as _; +use power_policy_interface::service as power_policy; +use power_policy_interface::service::event::EventData as PowerPolicyEventData; +use type_c_interface::port::pd::Pd as _; -impl<'a> Service<'a> { - /// Wait for a power policy event - pub(super) async fn wait_power_policy_event(&self) -> Event<'_> { - loop { - match self.power_policy_event_subscriber.lock().await.next_message().await { - WaitResult::Lagged(lagged) => { - // Missed some messages, all we can do is log an error - error!("Power policy {} event(s) lagged", lagged); - } - WaitResult::Message(message) => match message.data { - power_policy::CommsData::Unconstrained(state) => { - return Event::PowerPolicy(PowerPolicyEvent::Unconstrained(state)); - } - power_policy::CommsData::ConsumerDisconnected(_) => { - return Event::PowerPolicy(PowerPolicyEvent::ConsumerDisconnected); - } - power_policy::CommsData::ConsumerConnected(_, _) => { - return Event::PowerPolicy(PowerPolicyEvent::ConsumerConnected); - } - _ => { - // No other events currently implemented - } - }, - } - } - } +use super::*; +impl<'a, Reg: Registration<'a>> Service<'a, Reg> { /// Set the unconstrained state for all ports - pub(super) async fn set_unconstrained_all(&self, unconstrained: bool) -> Result<(), Error> { - for port_index in 0..self.context.get_num_ports() { - self.context - .set_unconstrained_power(GlobalPortId(port_index as u8), unconstrained) - .await?; + pub(super) async fn set_unconstrained_all(&mut self, unconstrained: bool) -> Result<(), Error> { + for port in self.registration.ports() { + port.lock().await.set_unconstrained_power(unconstrained).await?; } Ok(()) } /// Processed unconstrained state change pub(super) async fn process_unconstrained_state_change( - &self, + &mut self, unconstrained_state: &power_policy::UnconstrainedState, ) -> Result<(), Error> { if unconstrained_state.unconstrained { - let state = self.state.lock().await; - if unconstrained_state.available > 1 { // There are multiple available unconstrained consumers, set all ports to unconstrained // TODO: determine if we need to consider if we need to consider @@ -57,24 +31,27 @@ impl<'a> Service<'a> { self.set_unconstrained_all(true).await?; } else { // Only one unconstrained device is present, see if that's one of our ports - let num_ports = self.context.get_num_ports(); - let unconstrained_port = state - .port_status - .iter() - .take(num_ports) - .position(|status| status.available_sink_contract.is_some() && status.unconstrained_power); + let mut unconstrained_port = None; + for port in self.registration.ports().iter() { + let status = port.lock().await.get_port_status().await?; + if status.available_sink_contract.is_some() && status.unconstrained_power { + unconstrained_port = Some(*port); + break; + } + } - if let Some(unconstrained_index) = unconstrained_port { + if let Some(unconstrained_port) = unconstrained_port { // One of our ports is the unconstrained consumer // If it switches to sourcing then the system will no longer be unconstrained // So set that port to constrained and unconstrain all others info!( - "Setting port{} to constrained, all others unconstrained", - unconstrained_index + "Setting port ({}) to constrained, all others unconstrained", + unconstrained_port.lock().await.name() ); - for port_index in 0..num_ports { - self.context - .set_unconstrained_power(GlobalPortId(port_index as u8), port_index != unconstrained_index) + for port in self.registration.ports().iter() { + port.lock() + .await + .set_unconstrained_power(!ptr::eq(*port, unconstrained_port)) .await?; } } else { @@ -94,23 +71,22 @@ impl<'a> Service<'a> { } /// Process power policy events - pub(super) async fn process_power_policy_event(&self, message: &PowerPolicyEvent) -> Result<(), Error> { + pub(super) async fn process_power_policy_event(&mut self, message: &PowerPolicyEventData) -> Result<(), Error> { match message { - PowerPolicyEvent::Unconstrained(state) => self.process_unconstrained_state_change(state).await, - PowerPolicyEvent::ConsumerDisconnected => { - let mut state = self.state.lock().await; - state.ucsi.psu_connected = false; + PowerPolicyEventData::Unconstrained(state) => self.process_unconstrained_state_change(state).await, + PowerPolicyEventData::ConsumerDisconnected => { + self.ucsi.psu_connected = false; // Notify OPM because this can affect battery charging capability status - self.pend_ucsi_connected_ports(&mut state).await; + self.pend_ucsi_connected_ports().await; Ok(()) } - PowerPolicyEvent::ConsumerConnected => { - let mut state = self.state.lock().await; - state.ucsi.psu_connected = true; + PowerPolicyEventData::ConsumerConnected(_) => { + self.ucsi.psu_connected = true; // Notify OPM because this can affect battery charging capability status - self.pend_ucsi_connected_ports(&mut state).await; + self.pend_ucsi_connected_ports().await; Ok(()) } + _ => Ok(()), // Other events don't require any action from the service } } } diff --git a/type-c-service/src/service/registration.rs b/type-c-service/src/service/registration.rs new file mode 100644 index 000000000..815b151cb --- /dev/null +++ b/type-c-service/src/service/registration.rs @@ -0,0 +1,67 @@ +//! Code related to registration with the type-C service + +use embedded_services::{event::Sender, sync::Lockable}; +use embedded_usb_pd::{GlobalPortId, LocalPortId}; +use type_c_interface::port::pd::Pd; +use type_c_interface::service::event::Event as ServiceEvent; +use type_c_interface::ucsi::Lpm as UcsiLpm; + +/// Registration trait that abstracts over various registration details. +pub trait Registration<'port> { + type Port: Lockable + 'port; + type ServiceSender: Sender>; + + /// Returns a slice to access ports + fn ports(&self) -> &[&'port Self::Port]; + /// Returns a slice to access type-c event senders + fn event_senders(&mut self) -> &mut [Self::ServiceSender]; + /// Returns the ucsi local port ID for a given global port + fn ucsi_local_port_id(&self, global_port: GlobalPortId) -> Option; +} + +pub struct PortData { + /// local port ID + pub local_port: Option, +} + +/// A registration implementation based around arrays +pub struct ArrayRegistration< + 'port, + Port: Lockable + 'port, + const PORT_COUNT: usize, + ServiceSender: Sender>, + const SERVICE_SENDER_COUNT: usize, +> { + /// Array of registered ports + pub ports: [&'port Port; PORT_COUNT], + /// Array of local port data + pub port_data: [PortData; PORT_COUNT], + /// Array of service event senders + pub service_senders: [ServiceSender; SERVICE_SENDER_COUNT], +} + +impl< + 'port, + Port: Lockable + 'port, + const PORT_COUNT: usize, + ServiceSender: Sender>, + const SERVICE_SENDER_COUNT: usize, +> Registration<'port> for ArrayRegistration<'port, Port, PORT_COUNT, ServiceSender, SERVICE_SENDER_COUNT> +{ + type Port = Port; + type ServiceSender = ServiceSender; + + fn event_senders(&mut self) -> &mut [Self::ServiceSender] { + &mut self.service_senders + } + + fn ports(&self) -> &[&'port Self::Port] { + &self.ports + } + + fn ucsi_local_port_id(&self, global_port: GlobalPortId) -> Option { + self.port_data + .get(global_port.0 as usize) + .and_then(|data| data.local_port) + } +} diff --git a/type-c-service/src/service/ucsi.rs b/type-c-service/src/service/ucsi.rs index 033fab414..7f9d25876 100644 --- a/type-c-service/src/service/ucsi.rs +++ b/type-c-service/src/service/ucsi.rs @@ -1,3 +1,4 @@ +use embedded_services::sync::Lockable; use embedded_services::warn; use embedded_usb_pd::ucsi::cci::{Cci, GlobalCci}; use embedded_usb_pd::ucsi::lpm::get_connector_status::{BatteryChargingCapabilityStatus, ConnectorStatusChange}; @@ -7,9 +8,25 @@ use embedded_usb_pd::ucsi::ppm::state_machine::{ }; use embedded_usb_pd::ucsi::{GlobalCommand, ResponseData, lpm, ppm}; use embedded_usb_pd::{PdError, PowerRole}; +use type_c_interface::service::event::{Event, UsciChangeIndicatorData}; +use type_c_interface::ucsi::Lpm as _; use super::*; +const MAX_SUPPORTED_PORTS: usize = 4; + +/// UCSI command response +#[derive(Copy, Clone, Debug)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct UcsiResponse { + /// Notify the OPM, the function call + pub notify_opm: bool, + /// Response CCI + pub cci: GlobalCci, + /// UCSI response data + pub data: Result, PdError>, +} + /// UCSI state #[derive(Default)] pub(super) struct State { @@ -28,37 +45,33 @@ pub(super) struct State { pub(super) psu_connected: bool, } -impl<'a> Service<'a> { +impl<'port, Reg: Registration<'port>> Service<'port, Reg> { /// PPM reset implementation - fn process_ppm_reset(&self, state: &mut State) { + fn process_ppm_reset(&mut self) { debug!("Resetting PPM"); - state.notifications_enabled = NotificationEnable::default(); - state.pending_ports.clear(); - state.valid_battery_charging_capability.clear(); + self.ucsi.notifications_enabled = NotificationEnable::default(); + self.ucsi.pending_ports.clear(); + self.ucsi.valid_battery_charging_capability.clear(); } /// Set notification enable implementation - fn process_set_notification_enable(&self, state: &mut State, enable: NotificationEnable) { + fn process_set_notification_enable(&mut self, enable: NotificationEnable) { debug!("Set Notification Enable: {:?}", enable); - state.notifications_enabled = enable; + self.ucsi.notifications_enabled = enable; } /// PPM get capabilities implementation fn process_get_capabilities(&self) -> ppm::ResponseData { debug!("Get PPM capabilities: {:?}", self.config.ucsi_capabilities); let mut capabilities = self.config.ucsi_capabilities; - capabilities.num_connectors = external::get_num_ports() as u8; + capabilities.num_connectors = self.registration.ports().len() as u8; ppm::ResponseData::GetCapability(capabilities) } - fn process_ppm_command( - &self, - state: &mut State, - command: &ucsi::ppm::Command, - ) -> Result, PdError> { + fn process_ppm_command(&mut self, command: &ucsi::ppm::Command) -> Result, PdError> { match command { ppm::Command::SetNotificationEnable(enable) => { - self.process_set_notification_enable(state, enable.notification_enable); + self.process_set_notification_enable(enable.notification_enable); Ok(None) } ppm::Command::GetCapability => Ok(Some(self.process_get_capabilities())), @@ -69,12 +82,11 @@ impl<'a> Service<'a> { /// Determine the battery charging capability status for the given port fn determine_battery_charging_capability_status( &self, - state: &mut State, port_id: GlobalPortId, port_status: &PortStatus, ) -> Option { if port_status.power_role == PowerRole::Sink { - if state.valid_battery_charging_capability.contains(&port_id) && !state.psu_connected { + if self.ucsi.valid_battery_charging_capability.contains(&port_id) && !self.ucsi.psu_connected { // Only run this logic when no PSU is attached to prevent excessive notifications // when new type-C PSUs are attached let power_mw = port_status @@ -94,22 +106,28 @@ impl<'a> Service<'a> { } async fn process_lpm_command( - &self, - state: &mut super::State, + &mut self, command: &ucsi::lpm::GlobalCommand, ) -> Result, PdError> { debug!("Processing LPM command: {:?}", command); + let mut port = self.lookup_port(command.port())?.lock().await; + let local_port_id = self + .registration + .ucsi_local_port_id(command.port()) + .ok_or(PdError::InvalidPort)?; + let local_command = ucsi::lpm::LocalCommand::new(local_port_id, command.operation()); + match command.operation() { lpm::CommandData::GetConnectorCapability => { // Override the capabilities if present in the config if let Some(capabilities) = &self.config.ucsi_port_capabilities { Ok(Some(lpm::ResponseData::GetConnectorCapability(*capabilities))) } else { - self.context.execute_ucsi_command(*command).await + port.execute_lpm_command(local_command).await } } lpm::CommandData::GetConnectorStatus => { - let mut response = self.context.execute_ucsi_command(*command).await; + let mut response = port.execute_lpm_command(local_command).await; if let Ok(Some(lpm::ResponseData::GetConnectorStatus(lpm::get_connector_status::ResponseData { status_change: ref mut states_change, status: @@ -120,22 +138,21 @@ impl<'a> Service<'a> { .. }))) = response { - let raw_port = command.port().0 as usize; - let port_status = state.port_status.get(raw_port).ok_or(PdError::InvalidPort)?; + let port_status = port.get_port_status().await?; *battery_charging_status = - self.determine_battery_charging_capability_status(&mut state.ucsi, command.port(), port_status); + self.determine_battery_charging_capability_status(command.port(), &port_status); states_change.set_battery_charging_status_change(battery_charging_status.is_some()); } response } - _ => self.context.execute_ucsi_command(*command).await, + _ => port.execute_lpm_command(local_command).await, } } - /// Upate the CCI connector change field based on the current pending port - fn set_cci_connector_change(&self, state: &mut State, cci: &mut GlobalCci) { - if let Some(current_port) = state.pending_ports.front() { + /// Update the CCI connector change field based on the current pending port + fn set_cci_connector_change(&self, cci: &mut GlobalCci) { + if let Some(current_port) = self.ucsi.pending_ports.front() { // UCSI connector numbers are 1-based cci.set_connector_change(GlobalPortId(current_port.0 + 1)); } else { @@ -145,33 +162,41 @@ impl<'a> Service<'a> { } /// Acknowledge the current connector change and move to the next if present - async fn ack_connector_change(&self, state: &mut State, cci: &mut GlobalCci) { + async fn ack_connector_change(&mut self, cci: &mut GlobalCci) { // Pop the just acknowledged port and move to the next if present - if let Some(_current_port) = state.pending_ports.pop_front() { - if let Some(next_port) = state.pending_ports.front() { - debug!("ACK_CCI processed, next pending port: {:?}", next_port); - self.context - .broadcast_message(comms::CommsMessage::UcsiCci(comms::UsciChangeIndicator { - port: *next_port, - // False here because the OPM gets notified by the CCI, don't need a separate notification - notify_opm: false, - })) - .await; - } else { - debug!("ACK_CCI processed, no more pending ports"); - } - } else { + let Some(_current_port) = self.ucsi.pending_ports.pop_front() else { warn!("Received ACK_CCI with no pending connector changes"); - } + return; + }; - self.set_cci_connector_change(state, cci); + let Some(next_port) = self.ucsi.pending_ports.front() else { + debug!("ACK_CCI processed, no more pending ports"); + return; + }; + + debug!("ACK_CCI processed, next pending port: {:?}", next_port); + let Ok(port) = self.lookup_port(*next_port) else { + error!("Invalid port ID in pending ports: {:?}", next_port); + return; + }; + + self.broadcast_event(Event { + port, + event: EventData::UsciChangeIndicator(UsciChangeIndicatorData { + port: *next_port, + // False here because the OPM gets notified by the CCI, don't need a separate notification + notify_opm: false, + }), + }) + .await; + + self.set_cci_connector_change(cci); } - /// Process an external UCSI command - pub(super) async fn process_ucsi_command(&self, command: &GlobalCommand) -> external::UcsiResponse { - let state = &mut self.state.lock().await; + /// Process a UCSI command + pub async fn process_ucsi_command(&mut self, command: &GlobalCommand) -> UcsiResponse { let mut next_input = Some(PpmInput::Command(command)); - let mut response: external::UcsiResponse = external::UcsiResponse { + let mut response = UcsiResponse { notify_opm: false, cci: Cci::default(), data: Ok(None), @@ -182,10 +207,10 @@ impl<'a> Service<'a> { // Using a loop allows all logic to be centralized loop { let output = if let Some(next_input) = next_input.take() { - state.ucsi.ppm_state_machine.consume(next_input) + self.ucsi.ppm_state_machine.consume(next_input) } else { error!("Unexpected end of state machine processing"); - return external::UcsiResponse { + return UcsiResponse { notify_opm: true, cci: Cci::new_error(), data: Err(PdError::InvalidMode), @@ -196,7 +221,7 @@ impl<'a> Service<'a> { Ok(output) => output, Err(e @ InvalidTransition { .. }) => { error!("PPM state machine transition failed: {:#?}", e); - return external::UcsiResponse { + return UcsiResponse { notify_opm: true, cci: Cci::new_error(), data: Err(PdError::Failed), @@ -212,12 +237,12 @@ impl<'a> Service<'a> { match command { ucsi::GlobalCommand::PpmCommand(ppm_command) => { response.data = self - .process_ppm_command(&mut state.ucsi, ppm_command) + .process_ppm_command(ppm_command) .map(|inner| inner.map(ResponseData::Ppm)); } ucsi::GlobalCommand::LpmCommand(lpm_command) => { response.data = self - .process_lpm_command(state, lpm_command) + .process_lpm_command(lpm_command) .await .map(|inner| inner.map(ResponseData::Lpm)); } @@ -226,20 +251,20 @@ impl<'a> Service<'a> { // Don't return yet, need to inform state machine that command is complete } PpmOutput::OpmNotifyCommandComplete => { - response.notify_opm = state.ucsi.notifications_enabled.cmd_complete(); + response.notify_opm = self.ucsi.notifications_enabled.cmd_complete(); response.cci.set_cmd_complete(true); response.cci.set_error(response.data.is_err()); - self.set_cci_connector_change(&mut state.ucsi, &mut response.cci); + self.set_cci_connector_change(&mut response.cci); return response; } PpmOutput::AckComplete(ack) => { - response.notify_opm = state.ucsi.notifications_enabled.cmd_complete(); + response.notify_opm = self.ucsi.notifications_enabled.cmd_complete(); if ack.command_complete() { response.cci.set_ack_command(true); } if ack.connector_change() { - self.ack_connector_change(&mut state.ucsi, &mut response.cci).await; + self.ack_connector_change(&mut response.cci).await; } return response; @@ -247,18 +272,18 @@ impl<'a> Service<'a> { PpmOutput::ResetComplete => { // Resets don't follow the normal command execution flow // So do any reset processing here - self.process_ppm_reset(&mut state.ucsi); + self.process_ppm_reset(); // Don't notify OPM because it'll poll response.notify_opm = false; response.cci = Cci::new_reset_complete(); - self.set_cci_connector_change(&mut state.ucsi, &mut response.cci); + self.set_cci_connector_change(&mut response.cci); return response; } PpmOutput::OpmNotifyBusy => { // Notify if notifications are enabled in general - response.notify_opm = !state.ucsi.notifications_enabled.is_empty(); + response.notify_opm = !self.ucsi.notifications_enabled.is_empty(); response.cci.set_busy(true); - self.set_cci_connector_change(&mut state.ucsi, &mut response.cci); + self.set_cci_connector_change(&mut response.cci); return response; } }, @@ -267,7 +292,7 @@ impl<'a> Service<'a> { response.notify_opm = false; response.cci = Cci::default(); response.data = Ok(None); - self.set_cci_connector_change(&mut state.ucsi, &mut response.cci); + self.set_cci_connector_change(&mut response.cci); return response; } } @@ -276,12 +301,12 @@ impl<'a> Service<'a> { /// Handle PD port events, update UCSI state, and generate corresponding UCSI notifications pub(super) async fn handle_ucsi_port_event( - &self, + &mut self, + port: &'port Reg::Port, port_id: GlobalPortId, - port_event: PortStatusChanged, + port_event: PortStatusEventBitfield, port_status: &PortStatus, ) { - let state = &mut self.state.lock().await.ucsi; let mut ucsi_event = ConnectorStatusChange::default(); ucsi_event.set_connect_change(port_event.plug_inserted_or_removed()); @@ -303,36 +328,51 @@ impl<'a> Service<'a> { ucsi_event.set_battery_charging_status_change(true); // Power negotiation completed, battery charging capability status is now valid - if state.valid_battery_charging_capability.insert(port_id).is_err() { - error!("Valid battery charging capability overflow for port {:?}", port_id); + if self.ucsi.valid_battery_charging_capability.insert(port_id).is_err() { + error!( + "({}): Valid battery charging capability overflow", + port.lock().await.name() + ); } } if !port_status.is_connected() { // Reset battery charging capability status when disconnected - let _ = state.valid_battery_charging_capability.remove(&port_id); + let _ = self.ucsi.valid_battery_charging_capability.remove(&port_id); } - if ucsi_event.filter_enabled(state.notifications_enabled).is_empty() { + if ucsi_event.filter_enabled(self.ucsi.notifications_enabled).is_empty() { trace!("{:?}: event received, but no UCSI notifications enabled", port_id); return; } - self.pend_ucsi_port(state, port_id).await; + self.pend_ucsi_port(port, port_id).await; } /// Pend UCSI events for all connected ports - pub(super) async fn pend_ucsi_connected_ports(&self, state: &mut super::State) { - for (port_id, port_status) in state.port_status.iter().enumerate() { - if port_status.is_connected() { - self.pend_ucsi_port(&mut state.ucsi, GlobalPortId(port_id as u8)).await; + pub(super) async fn pend_ucsi_connected_ports(&mut self) { + // Panic Safety: i is limited by the length of port_status + #[allow(clippy::indexing_slicing)] + for i in 0..self.registration.ports().len() { + let port_id = GlobalPortId(i as u8); + let Some(port) = self.registration.ports().get(i) else { + error!("Invalid port ID: {}", i); + continue; + }; + + if let Ok(port_status) = port.lock().await.get_port_status().await { + if port_status.is_connected() { + self.pend_ucsi_port(port, port_id).await; + } + } else { + error!("({}): Failed to get status for port", port.lock().await.name()); } } } /// Pend a UCSI event for the given port - async fn pend_ucsi_port(&self, state: &mut State, port_id: GlobalPortId) { - if state.pending_ports.iter().any(|pending| *pending == port_id) { + async fn pend_ucsi_port(&mut self, port: &'port Reg::Port, port_id: GlobalPortId) { + if self.ucsi.pending_ports.iter().any(|pending| *pending == port_id) { // Already have a pending event for this port, don't need to process it twice return; } @@ -340,14 +380,16 @@ impl<'a> Service<'a> { // Only notifiy the OPM if we don't have any pending events // Once the OPM starts processing events, the next pending port will be sent as part // of the CCI response to the ACK_CC_CI command. See [`Self::set_cci_connector_change`] - let notify_opm = state.pending_ports.is_empty(); - if state.pending_ports.push_back(port_id).is_ok() { - self.context - .broadcast_message(comms::CommsMessage::UcsiCci(comms::UsciChangeIndicator { + let notify_opm = self.ucsi.pending_ports.is_empty(); + if self.ucsi.pending_ports.push_back(port_id).is_ok() { + self.broadcast_event(Event { + port, + event: EventData::UsciChangeIndicator(UsciChangeIndicatorData { port: port_id, notify_opm, - })) - .await; + }), + }) + .await; } else { // This shouldn't happen because we have a single slot per port // Would likely indicate that an invalid port ID got in somehow diff --git a/type-c-service/src/service/vdm.rs b/type-c-service/src/service/vdm.rs deleted file mode 100644 index eda35679f..000000000 --- a/type-c-service/src/service/vdm.rs +++ /dev/null @@ -1,18 +0,0 @@ -//! VDM (Vendor Defined Messages) related functionality. - -use embedded_services::type_c::controller::{AttnVdm, OtherVdm}; -use embedded_usb_pd::{GlobalPortId, PdError}; - -use super::Service; - -impl Service<'_> { - /// Get the other vdm for the given port - pub async fn get_other_vdm(&self, port_id: GlobalPortId) -> Result { - self.context.get_other_vdm(port_id).await - } - - /// Get the attention vdm for the given port - pub async fn get_attn_vdm(&self, port_id: GlobalPortId) -> Result { - self.context.get_attn_vdm(port_id).await - } -} diff --git a/type-c-service/src/task.rs b/type-c-service/src/task.rs index 0d965e8e9..8ad364bd9 100644 --- a/type-c-service/src/task.rs +++ b/type-c-service/src/task.rs @@ -1,51 +1,25 @@ -use core::future::Future; -use embassy_sync::pubsub::PubSubChannel; -use embedded_services::{GlobalRawMutex, error, info, power}; -use static_cell::StaticCell; - -use crate::service::config::Config; -use crate::service::{MAX_POWER_POLICY_EVENTS, Service}; - -/// Task to run the Type-C service, takes a closure to customize the event loop -pub async fn task_closure<'a, Fut: Future, F: Fn(&'a Service) -> Fut>(config: Config, f: F) { +use embedded_services::{error, event::Receiver, info, sync::Lockable}; +use power_policy_interface::service::event::EventData as PowerPolicyEventData; +use type_c_interface::port::pd::Pd; + +use crate::service::{Service, event_receiver::ArrayEventReceiver, registration::Registration}; + +/// Task to run the Type-C service, running the default event loop +pub async fn task< + const N: usize, + Port: Lockable, + PortReceiver: Receiver, + PowerReceiver: Receiver, +>( + service: &'static impl Lockable>>, + mut event_receiver: ArrayEventReceiver<'static, N, Port, PortReceiver, PowerReceiver>, +) { info!("Starting type-c task"); - // The service is the only receiver and we only use a DynImmediatePublisher, which doesn't take a publisher slot - static POWER_POLICY_CHANNEL: StaticCell< - PubSubChannel, - > = StaticCell::new(); - - let power_policy_channel = POWER_POLICY_CHANNEL.init(PubSubChannel::new()); - let power_policy_publisher = power_policy_channel.dyn_immediate_publisher(); - let Ok(power_policy_subscriber) = power_policy_channel.dyn_subscriber() else { - error!("Failed to create power policy subscriber"); - return; - }; - - let service = Service::create(config, power_policy_publisher, power_policy_subscriber); - let Some(service) = service else { - error!("Type-C service already initialized"); - return; - }; - - static SERVICE: StaticCell = StaticCell::new(); - let service = SERVICE.init(service); - - if service.register_comms().is_err() { - error!("Failed to register type-c service endpoint"); - return; - } - loop { - f(service).await; - } -} - -pub async fn task(config: Config) { - task_closure(config, |service: &Service| async { - if let Err(e) = service.process_next_event().await { + let event = event_receiver.wait_next().await; + if let Err(e) = service.lock().await.process_event(event).await { error!("Type-C service processing error: {:#?}", e); } - }) - .await; + } } diff --git a/type-c-service/src/util.rs b/type-c-service/src/util.rs new file mode 100644 index 000000000..c9203d9d7 --- /dev/null +++ b/type-c-service/src/util.rs @@ -0,0 +1,84 @@ +//! Type-C service utility functions and constants. +use embedded_usb_pd::pdo::{Common, Contract}; +use embedded_usb_pd::type_c; +use embedded_usb_pd::{Error as PdBusError, PdError}; +use fw_update_interface::basic::Error as BasicFwError; +use power_policy_interface::psu::Error as PowerPolicyError; + +pub fn power_capability_try_from_contract( + contract: Contract, +) -> Option { + Some(power_policy_interface::capability::PowerCapability { + voltage_mv: contract.pdo.max_voltage_mv(), + current_ma: contract.operating_current_ma()?, + }) +} + +pub fn power_capability_from_current(current: type_c::Current) -> power_policy_interface::capability::PowerCapability { + power_policy_interface::capability::PowerCapability { + voltage_mv: 5000, + // Assume higher power for now + current_ma: current.to_ma(false), + } +} + +/// Type-C USB2 power capability 5V@500mA +pub const POWER_CAPABILITY_USB_DEFAULT_USB2: power_policy_interface::capability::PowerCapability = + power_policy_interface::capability::PowerCapability { + voltage_mv: 5000, + current_ma: 500, + }; + +/// Type-C USB3 power capability 5V@900mA +pub const POWER_CAPABILITY_USB_DEFAULT_USB3: power_policy_interface::capability::PowerCapability = + power_policy_interface::capability::PowerCapability { + voltage_mv: 5000, + current_ma: 900, + }; + +/// Type-C power capability 5V@1.5A +pub const POWER_CAPABILITY_5V_1A5: power_policy_interface::capability::PowerCapability = + power_policy_interface::capability::PowerCapability { + voltage_mv: 5000, + current_ma: 1500, + }; + +/// Type-C power capability 5V@3A +pub const POWER_CAPABILITY_5V_3A0: power_policy_interface::capability::PowerCapability = + power_policy_interface::capability::PowerCapability { + voltage_mv: 5000, + current_ma: 3000, + }; + +/// Converts a PD error into a basic FW update error +pub fn basic_fw_update_error_from_pd_error(pd_error: PdError) -> BasicFwError { + match pd_error { + PdError::Busy => BasicFwError::Busy, + _ => BasicFwError::Failed, + } +} + +/// Converts a PD error into a basic FW update error +pub fn basic_fw_update_error_from_pd_bus_error(pd_error: PdBusError) -> BasicFwError { + match pd_error { + PdBusError::Pd(pd_error) => basic_fw_update_error_from_pd_error(pd_error), + PdBusError::Bus(_) => BasicFwError::Bus, + } +} + +/// Converts a PD error into a power policy error +pub fn power_policy_error_from_pd_error(pd_error: PdError) -> PowerPolicyError { + match pd_error { + PdError::Busy => PowerPolicyError::Busy, + PdError::Timeout => PowerPolicyError::Timeout, + _ => PowerPolicyError::Failed, + } +} + +/// Converts a PD bus error into a power policy error +pub fn power_policy_error_from_pd_bus_error(pd_error: PdBusError) -> PowerPolicyError { + match pd_error { + PdBusError::Pd(pd_error) => power_policy_error_from_pd_error(pd_error), + PdBusError::Bus(_) => PowerPolicyError::Bus, + } +} diff --git a/type-c-service/src/wrapper/backing.rs b/type-c-service/src/wrapper/backing.rs deleted file mode 100644 index baf9cf5a2..000000000 --- a/type-c-service/src/wrapper/backing.rs +++ /dev/null @@ -1,251 +0,0 @@ -//! Various types of state and objects required for [`crate::wrapper::ControllerWrapper`]. -//! -//! The wrapper needs per-port state which ultimately needs to come from something like an array. -//! We need to erase the generic `N` parameter from that storage so as not to monomorphize the entire -//! wrapper over it. This module provides the necessary types and traits to do so. Things required by -//! the wrapper can be split into two categories: objects used for service registration (which must be immutable), -//! and mutable state. These are represented by the [`Registration`] and [`DynPortState`] respectively. The later -//! is a trait intended to be used as a trait object to erase the generic port count. -//! -//! [`Storage`] is the base storage type and is generic over the number of ports. However, there are additional -//! objects that need to reference the storage. To avoid a self-referential -//! struct, [`ReferencedStorage`] contains these. This struct is still generic over the number of ports. -//! -//! Lastly, [`Backing`] contains references to the registration and type-erased state and is what is passed -//! to the wrapper. -//! -//! Example usage: -//! ``` -//! use embassy_sync::blocking_mutex::raw::NoopRawMutex; -//! use static_cell::StaticCell; -//! use embedded_services::type_c::ControllerId; -//! use embedded_services::power; -//! use embedded_usb_pd::GlobalPortId; -//! use type_c_service::wrapper::backing::{Storage, ReferencedStorage}; -//! -//! -//! const NUM_PORTS: usize = 2; -//! -//! fn init() { -//! static STORAGE: StaticCell> = StaticCell::new(); -//! let storage = STORAGE.init(Storage::new( -//! ControllerId(0), -//! 0x0, -//! [(GlobalPortId(0), power::policy::DeviceId(0)), (GlobalPortId(1), power::policy::DeviceId(1))], -//! )); -//! static REFERENCED: StaticCell> = StaticCell::new(); -//! let referenced = REFERENCED.init(storage.create_referenced().unwrap()); -//! let _backing = referenced.create_backing().unwrap(); -//! } -//! ``` -use core::cell::{RefCell, RefMut}; - -use embassy_sync::{ - blocking_mutex::raw::RawMutex, - pubsub::{DynImmediatePublisher, DynSubscriber, PubSubChannel}, -}; -use embassy_time::Instant; -use embedded_cfu_protocol::protocol_definitions::ComponentId; -use embedded_services::{ - power, - type_c::{ - ControllerId, - controller::PortStatus, - event::{PortEvent, PortStatusChanged}, - }, -}; -use embedded_usb_pd::{GlobalPortId, ado::Ado}; - -use crate::{PortEventStreamer, wrapper::cfu}; - -/// Per-port state -pub struct PortState<'a> { - /// Cached port status - pub(crate) status: PortStatus, - /// Software status event - pub(crate) sw_status_event: PortStatusChanged, - /// Sink ready deadline instant - pub(crate) sink_ready_deadline: Option, - /// Pending events for the type-C service - pub(crate) pending_events: PortEvent, - /// PD alert channel for this port - // There's no direct immediate equivalent of a channel. PubSubChannel has immediate publisher behavior - // so we use that, but this requires us to keep separate publisher and subscriber objects. - pub(crate) pd_alerts: (DynImmediatePublisher<'a, Ado>, DynSubscriber<'a, Ado>), -} - -/// Internal per-controller state -#[derive(Copy, Clone)] -pub struct ControllerState { - /// If we're currently doing a firmware update - pub(crate) fw_update_state: cfu::FwUpdateState, - /// State used to keep track of where we are as we turn the event bitfields into a stream of events - pub(crate) port_event_streaming_state: Option, -} - -impl Default for ControllerState { - fn default() -> Self { - Self { - fw_update_state: cfu::FwUpdateState::Idle, - port_event_streaming_state: None, - } - } -} - -/// Internal state containing all per-port and per-controller state -struct InternalState<'a, const N: usize> { - controller_state: ControllerState, - port_states: [PortState<'a>; N], -} - -impl<'a, const N: usize> InternalState<'a, N> { - fn try_new(storage: &'a Storage) -> Option { - let port_states = storage.pd_alerts.each_ref().map(|pd_alert| { - Some(PortState { - status: PortStatus::new(), - sw_status_event: PortStatusChanged::none(), - sink_ready_deadline: None, - pending_events: PortEvent::none(), - pd_alerts: (pd_alert.dyn_immediate_publisher(), pd_alert.dyn_subscriber().ok()?), - }) - }); - - if port_states.iter().any(|s| s.is_none()) { - return None; - } - - Some(Self { - controller_state: ControllerState::default(), - // Panic safety: All array elements checked above - #[allow(clippy::unwrap_used)] - port_states: port_states.map(|s| s.unwrap()), - }) - } -} - -impl<'a, const N: usize> DynPortState<'a> for InternalState<'a, N> { - fn num_ports(&self) -> usize { - self.port_states.len() - } - - fn port_states(&self) -> &[PortState<'a>] { - &self.port_states - } - - fn port_states_mut(&mut self) -> &mut [PortState<'a>] { - &mut self.port_states - } - - fn controller_state(&self) -> &ControllerState { - &self.controller_state - } - - fn controller_state_mut(&mut self) -> &mut ControllerState { - &mut self.controller_state - } -} - -/// Trait to erase the generic port count argument -pub trait DynPortState<'a> { - fn num_ports(&self) -> usize; - - fn port_states(&self) -> &[PortState<'a>]; - fn port_states_mut(&mut self) -> &mut [PortState<'a>]; - - fn controller_state(&self) -> &ControllerState; - fn controller_state_mut(&mut self) -> &mut ControllerState; -} - -/// Service registration objects -pub struct Registration<'a> { - pub pd_controller: &'a embedded_services::type_c::controller::Device<'a>, - pub cfu_device: &'a embedded_services::cfu::component::CfuDevice, - pub power_devices: &'a [embedded_services::power::policy::device::Device], -} - -impl<'a> Registration<'a> { - pub fn num_ports(&self) -> usize { - self.power_devices.len() - } -} - -/// PD alerts should be fairly uncommon, four seems like a reasonable number to start with. -const MAX_BUFFERED_PD_ALERTS: usize = 4; - -/// Base storage -pub struct Storage { - // Registration-related - controller_id: ControllerId, - pd_ports: [GlobalPortId; N], - cfu_device: embedded_services::cfu::component::CfuDevice, - power_devices: [embedded_services::power::policy::device::Device; N], - - // State-related - pd_alerts: [PubSubChannel; N], -} - -impl Storage { - pub fn new( - controller_id: ControllerId, - cfu_id: ComponentId, - ports: [(GlobalPortId, power::policy::DeviceId); N], - ) -> Self { - Self { - controller_id, - pd_ports: ports.map(|(port, _)| port), - cfu_device: embedded_services::cfu::component::CfuDevice::new(cfu_id), - power_devices: ports.map(|(_, device)| embedded_services::power::policy::device::Device::new(device)), - pd_alerts: [const { PubSubChannel::new() }; N], - } - } - - /// Create referenced storage from this storage - pub fn create_referenced(&self) -> Option> { - ReferencedStorage::try_from_storage(self) - } -} - -/// Contains any values that need to reference [`Storage`] -/// -/// To simplify usage, we use interior mutability through a ref cell to avoid having to declare the state -/// completely separately. -pub struct ReferencedStorage<'a, const N: usize, M: RawMutex> { - storage: &'a Storage, - state: RefCell>, - pd_controller: embedded_services::type_c::controller::Device<'a>, -} - -impl<'a, const N: usize, M: RawMutex> ReferencedStorage<'a, N, M> { - /// Create a new referenced storage from the given storage and controller ID - fn try_from_storage(storage: &'a Storage) -> Option { - Some(Self { - storage, - state: RefCell::new(InternalState::try_new(storage)?), - pd_controller: embedded_services::type_c::controller::Device::new( - storage.controller_id, - storage.pd_ports.as_slice(), - ), - }) - } - - /// Creates the backing, returns `None` if a backing has already been created - pub fn create_backing<'b>(&'b self) -> Option> - where - 'b: 'a, - { - self.state.try_borrow_mut().ok().map(|state| Backing { - registration: Registration { - pd_controller: &self.pd_controller, - cfu_device: &self.storage.cfu_device, - power_devices: &self.storage.power_devices, - }, - state, - }) - } -} - -/// Wrapper around registration and type-erased state -pub struct Backing<'a> { - pub(crate) registration: Registration<'a>, - pub(crate) state: RefMut<'a, dyn DynPortState<'a>>, -} diff --git a/type-c-service/src/wrapper/cfu.rs b/type-c-service/src/wrapper/cfu.rs deleted file mode 100644 index d1b7f7246..000000000 --- a/type-c-service/src/wrapper/cfu.rs +++ /dev/null @@ -1,387 +0,0 @@ -//! CFU message bridge -//! TODO: remove this once we have a more generic FW update implementation -use embassy_futures::select::{Either, select}; -use embedded_cfu_protocol::protocol_definitions::*; -use embedded_services::cfu::component::{InternalResponseData, RequestData}; -use embedded_services::power; -use embedded_services::type_c::controller::Controller; -use embedded_services::{debug, error}; - -use super::message::EventCfu; -use super::*; - -/// Current state of the firmware update process -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum FwUpdateState { - /// None in progress - Idle, - /// Firmware update in progress - /// Contains number of ticks [`super::DEFAULT_FW_UPDATE_TICK_INTERVAL_MS`] that have passed - InProgress(u8), - /// Firmware update has failed and the device is in an unknown state - Recovery, -} - -impl FwUpdateState { - /// Check if the firmware update is in progress - pub fn in_progress(&self) -> bool { - matches!(self, FwUpdateState::InProgress(_) | FwUpdateState::Recovery) - } -} - -impl<'device, M: RawMutex, C: Lockable, V: FwOfferValidator> ControllerWrapper<'device, M, C, V> -where - ::Inner: Controller, -{ - /// Create a new invalid FW version response - fn create_invalid_fw_version_response(&self) -> InternalResponseData { - let dev_inf = FwVerComponentInfo::new(FwVersion::new(0xffffffff), self.registration.cfu_device.component_id()); - let comp_info: [FwVerComponentInfo; MAX_CMPT_COUNT] = [dev_inf; MAX_CMPT_COUNT]; - InternalResponseData::FwVersionResponse(GetFwVersionResponse { - header: GetFwVersionResponseHeader::new(1, GetFwVerRespHeaderByte3::NoSpecialFlags), - component_info: comp_info, - }) - } - - /// Process a GetFwVersion command - async fn process_get_fw_version(&self, target: &mut C::Inner) -> InternalResponseData { - let version = match target.get_active_fw_version().await { - Ok(v) => v, - Err(Error::Pd(e)) => { - error!("Failed to get active firmware version: {:?}", e); - return self.create_invalid_fw_version_response(); - } - Err(Error::Bus(_)) => { - error!("Failed to get active firmware version, bus error"); - return self.create_invalid_fw_version_response(); - } - }; - - let dev_inf = FwVerComponentInfo::new(FwVersion::new(version), self.registration.cfu_device.component_id()); - let comp_info: [FwVerComponentInfo; MAX_CMPT_COUNT] = [dev_inf; MAX_CMPT_COUNT]; - InternalResponseData::FwVersionResponse(GetFwVersionResponse { - header: GetFwVersionResponseHeader::new(1, GetFwVerRespHeaderByte3::NoSpecialFlags), - component_info: comp_info, - }) - } - - /// Create an offer rejection response - fn create_offer_rejection() -> InternalResponseData { - InternalResponseData::OfferResponse(FwUpdateOfferResponse::new_with_failure( - HostToken::Driver, - OfferRejectReason::InvalidComponent, - OfferStatus::Reject, - )) - } - - /// Process a GiveOffer command - async fn process_give_offer(&self, target: &mut C::Inner, offer: &FwUpdateOffer) -> InternalResponseData { - if offer.component_info.component_id != self.registration.cfu_device.component_id() { - return Self::create_offer_rejection(); - } - - let version = match target.get_active_fw_version().await { - Ok(v) => v, - Err(Error::Pd(e)) => { - error!("Failed to get active firmware version: {:?}", e); - return Self::create_offer_rejection(); - } - Err(Error::Bus(_)) => { - error!("Failed to get active firmware version, bus error"); - return Self::create_offer_rejection(); - } - }; - - InternalResponseData::OfferResponse(self.fw_version_validator.validate(FwVersion::new(version), offer)) - } - - async fn process_abort_update( - &self, - controller: &mut C::Inner, - state: &mut dyn DynPortState<'_>, - ) -> InternalResponseData { - // abort the update process - match controller.abort_fw_update().await { - Ok(_) => { - debug!("FW update aborted successfully"); - state.controller_state_mut().fw_update_state = FwUpdateState::Idle; - } - Err(Error::Pd(e)) => { - error!("Failed to abort FW update: {:?}", e); - state.controller_state_mut().fw_update_state = FwUpdateState::Recovery; - } - Err(Error::Bus(_)) => { - error!("Failed to abort FW update, bus error"); - state.controller_state_mut().fw_update_state = FwUpdateState::Recovery; - } - } - - InternalResponseData::ComponentPrepared - } - - /// Process a GiveContent command - async fn process_give_content( - &self, - controller: &mut C::Inner, - state: &mut dyn DynPortState<'_>, - content: &FwUpdateContentCommand, - ) -> InternalResponseData { - let data = if let Some(data) = content.data.get(0..content.header.data_length as usize) { - data - } else { - return InternalResponseData::ContentResponse(FwUpdateContentResponse::new( - content.header.sequence_num, - CfuUpdateContentResponseStatus::ErrorPrepare, - )); - }; - - debug!("Got content {:#?}", content); - if content.header.flags & FW_UPDATE_FLAG_FIRST_BLOCK != 0 { - debug!("Got first block"); - - // Detach from the power policy so it doesn't attempt to do anything while we are updating - let controller_id = self.registration.pd_controller.id(); - let mut detached_all = true; - for power in self.registration.power_devices { - info!("Controller{}: checking power device", controller_id.0); - if power.state().await != power::policy::device::State::Detached { - info!("Controller{}: Detaching power device", controller_id.0); - if let Err(e) = power.detach().await { - error!("Controller{}: Failed to detach power device: {:?}", controller_id.0, e); - - // Sync to bring the controller to a known state with all services - match self.sync_state_internal(controller, state).await { - Ok(_) => debug!( - "Controller{}: Synced state after detaching power device", - controller_id.0 - ), - Err(Error::Pd(e)) => error!( - "Controller{}: Failed to sync state after detaching power device: {:?}", - controller_id.0, e - ), - Err(Error::Bus(_)) => error!( - "Controller{}: Failed to sync state after detaching power device, bus error", - controller_id.0 - ), - } - - detached_all = false; - break; - } - } - } - - if !detached_all { - error!( - "Controller{}: Failed to detach all power devices, rejecting offer", - controller_id.0 - ); - return InternalResponseData::ContentResponse(FwUpdateContentResponse::new( - content.header.sequence_num, - CfuUpdateContentResponseStatus::ErrorPrepare, - )); - } - - // Need to start the update - self.fw_update_ticker.lock().await.reset(); - match controller.start_fw_update().await { - Ok(_) => { - debug!("FW update started successfully"); - } - Err(Error::Pd(e)) => { - error!("Failed to start FW update: {:?}", e); - state.controller_state_mut().fw_update_state = FwUpdateState::Recovery; - return InternalResponseData::ContentResponse(FwUpdateContentResponse::new( - content.header.sequence_num, - CfuUpdateContentResponseStatus::ErrorPrepare, - )); - } - Err(Error::Bus(_)) => { - error!("Failed to start FW update, bus error"); - state.controller_state_mut().fw_update_state = FwUpdateState::Recovery; - return InternalResponseData::ContentResponse(FwUpdateContentResponse::new( - content.header.sequence_num, - CfuUpdateContentResponseStatus::ErrorPrepare, - )); - } - } - - state.controller_state_mut().fw_update_state = FwUpdateState::InProgress(0); - } - - match controller - .write_fw_contents(content.header.firmware_address as usize, data) - .await - { - Ok(_) => { - debug!("Block written successfully"); - } - Err(Error::Pd(e)) => { - error!("Failed to write block: {:?}", e); - return InternalResponseData::ContentResponse(FwUpdateContentResponse::new( - content.header.sequence_num, - CfuUpdateContentResponseStatus::ErrorWrite, - )); - } - Err(Error::Bus(_)) => { - error!("Failed to write block, bus error"); - return InternalResponseData::ContentResponse(FwUpdateContentResponse::new( - content.header.sequence_num, - CfuUpdateContentResponseStatus::ErrorWrite, - )); - } - } - - if content.header.flags & FW_UPDATE_FLAG_LAST_BLOCK != 0 { - match controller.finalize_fw_update().await { - Ok(_) => { - debug!("FW update finalized successfully"); - state.controller_state_mut().fw_update_state = FwUpdateState::Idle; - } - Err(Error::Pd(e)) => { - error!("Failed to finalize FW update: {:?}", e); - state.controller_state_mut().fw_update_state = FwUpdateState::Recovery; - return Self::create_offer_rejection(); - } - Err(Error::Bus(_)) => { - error!("Failed to finalize FW update, bus error"); - state.controller_state_mut().fw_update_state = FwUpdateState::Recovery; - return Self::create_offer_rejection(); - } - } - } - - InternalResponseData::ContentResponse(FwUpdateContentResponse::new( - content.header.sequence_num, - CfuUpdateContentResponseStatus::Success, - )) - } - - /// Process a CFU tick - pub async fn process_cfu_tick(&self, controller: &mut C::Inner, state: &mut dyn DynPortState<'_>) { - match state.controller_state_mut().fw_update_state { - FwUpdateState::Idle => { - // No FW update in progress, nothing to do - return; - } - FwUpdateState::InProgress(ticks) => { - if ticks + 1 < DEFAULT_FW_UPDATE_TIMEOUT_TICKS { - trace!("CFU tick: {}", ticks); - state.controller_state_mut().fw_update_state = FwUpdateState::InProgress(ticks + 1); - return; - } else { - error!("FW update timed out after {} ticks", ticks); - } - } - FwUpdateState::Recovery => { - // Continue recovery process - } - }; - - // Update timed out, attempt to exit the FW update - state.controller_state_mut().fw_update_state = FwUpdateState::Recovery; - match controller.abort_fw_update().await { - Ok(_) => { - debug!("FW update aborted successfully"); - } - Err(Error::Pd(e)) => { - error!("Failed to abort FW update: {:?}", e); - return; - } - Err(Error::Bus(_)) => { - error!("Failed to abort FW update, bus error"); - return; - } - } - - state.controller_state_mut().fw_update_state = FwUpdateState::Idle; - } - - /// Process a CFU command - pub async fn process_cfu_command( - &self, - controller: &mut C::Inner, - state: &mut dyn DynPortState<'_>, - command: &RequestData, - ) -> InternalResponseData { - if state.controller_state().fw_update_state == FwUpdateState::Recovery { - debug!("FW update in recovery state, rejecting command"); - return InternalResponseData::ComponentBusy; - } - - match command { - RequestData::FwVersionRequest => { - debug!("Got FwVersionRequest"); - self.process_get_fw_version(controller).await - } - RequestData::GiveOffer(offer) => { - debug!("Got GiveOffer"); - self.process_give_offer(controller, offer).await - } - RequestData::GiveContent(content) => { - debug!("Got GiveContent"); - self.process_give_content(controller, state, content).await - } - RequestData::AbortUpdate => { - debug!("Got AbortUpdate"); - self.process_abort_update(controller, state).await - } - RequestData::FinalizeUpdate => { - debug!("Got FinalizeUpdate"); - InternalResponseData::ComponentPrepared - } - RequestData::PrepareComponentForUpdate => { - debug!("Got PrepareComponentForUpdate"); - InternalResponseData::ComponentPrepared - } - RequestData::GiveOfferExtended(_) => { - debug!("Got GiveExtendedOffer, rejecting"); - Self::create_offer_rejection() - } - RequestData::GiveOfferInformation(_) => { - debug!("Got GiveOfferInformation, rejecting"); - Self::create_offer_rejection() - } - } - } - - /// Sends a CFU response to the command - pub async fn send_cfu_response(&self, response: InternalResponseData) { - self.registration.cfu_device.send_response(response).await; - } - - /// Wait for a CFU command - /// - /// Returns None if the FW update ticker has ticked - /// DROP SAFETY: No state that needs to be restored - pub async fn wait_cfu_command(&self) -> EventCfu { - // Only lock long enough to grab our state - let fw_update_state = self.state.lock().await.controller_state().fw_update_state; - match fw_update_state { - FwUpdateState::Idle => { - // No FW update in progress, just wait for a command - EventCfu::Request(self.registration.cfu_device.wait_request().await) - } - FwUpdateState::InProgress(_) => { - match select( - self.registration.cfu_device.wait_request(), - self.fw_update_ticker.lock().await.next(), - ) - .await - { - Either::First(command) => EventCfu::Request(command), - Either::Second(_) => { - debug!("FW update ticker ticked"); - EventCfu::RecoveryTick - } - } - } - FwUpdateState::Recovery => { - // Recovery state, wait for the next attempt to recover the device - self.fw_update_ticker.lock().await.next().await; - debug!("FW update ticker ticked"); - EventCfu::RecoveryTick - } - } - } -} diff --git a/type-c-service/src/wrapper/dp.rs b/type-c-service/src/wrapper/dp.rs deleted file mode 100644 index 8bae8560f..000000000 --- a/type-c-service/src/wrapper/dp.rs +++ /dev/null @@ -1,22 +0,0 @@ -use super::{ControllerWrapper, FwOfferValidator}; -use crate::wrapper::message::OutputDpStatusChanged; -use embassy_sync::blocking_mutex::raw::RawMutex; -use embedded_services::{sync::Lockable, trace, type_c::controller::Controller}; -use embedded_usb_pd::{Error, LocalPortId}; - -impl<'device, M: RawMutex, C: Lockable, V: FwOfferValidator> ControllerWrapper<'device, M, C, V> -where - ::Inner: Controller, -{ - /// Process a DisplayPort status update by retrieving the current DP status from the `controller` for the appropriate `port`. - pub(super) async fn process_dp_status_update( - &self, - controller: &mut C::Inner, - port: LocalPortId, - ) -> Result::BusError>> { - trace!("Processing DP status update event on port {}", port.0); - - let status = controller.get_dp_status(port).await?; - Ok(OutputDpStatusChanged { port, status }) - } -} diff --git a/type-c-service/src/wrapper/message.rs b/type-c-service/src/wrapper/message.rs deleted file mode 100644 index f8416b3ae..000000000 --- a/type-c-service/src/wrapper/message.rs +++ /dev/null @@ -1,170 +0,0 @@ -//! [`crate::wrapper::ControllerWrapper`] message types -use embedded_services::{ - GlobalRawMutex, - ipc::deferred, - power::policy, - type_c::{ - controller::{self, DpStatus, PortStatus}, - event::{PortNotificationSingle, PortStatusChanged}, - }, -}; -use embedded_usb_pd::{LocalPortId, ado::Ado}; - -/// Port status changed event data -#[derive(Copy, Clone, Debug)] -#[cfg_attr(feature = "defmt", derive(defmt::Format))] -pub struct EventPortStatusChanged { - /// Port ID - pub port: LocalPortId, - /// Status changed event - pub status_event: PortStatusChanged, -} - -/// Port notification event data -#[derive(Copy, Clone, Debug)] -#[cfg_attr(feature = "defmt", derive(defmt::Format))] -pub struct EventPortNotification { - /// Port ID - pub port: LocalPortId, - /// Notification event - pub notification: PortNotificationSingle, -} - -/// Power policy command event data -pub struct EventPowerPolicyCommand<'a> { - /// Port ID - pub port: LocalPortId, - /// Power policy request - pub request: - deferred::Request<'a, GlobalRawMutex, policy::device::CommandData, policy::device::InternalResponseData>, -} - -/// CFU events -#[derive(Copy, Clone, Debug, PartialEq, Eq)] -#[cfg_attr(feature = "defmt", derive(defmt::Format))] -pub enum EventCfu { - /// CFU request - Request(embedded_services::cfu::component::RequestData), - /// Recovery tick - /// - /// Occurs when the FW update has timed out to abort the update and return hardware to its normal state - RecoveryTick, -} - -/// Wrapper events -pub enum Event<'a> { - /// Port status changed - PortStatusChanged(EventPortStatusChanged), - /// Port notification - PortNotification(EventPortNotification), - /// Power policy command received - PowerPolicyCommand(EventPowerPolicyCommand<'a>), - /// Command from TCPM - ControllerCommand(deferred::Request<'a, GlobalRawMutex, controller::Command, controller::Response<'static>>), - /// Cfu event - CfuEvent(EventCfu), -} - -/// Port status changed output data -#[derive(Copy, Clone, Debug)] -#[cfg_attr(feature = "defmt", derive(defmt::Format))] -pub struct OutputPortStatusChanged { - /// Port ID - pub port: LocalPortId, - /// Status changed event - pub status_event: PortStatusChanged, - /// Port status - pub status: PortStatus, -} - -/// PD alert output data -#[derive(Copy, Clone, PartialEq, Eq, Debug)] -#[cfg_attr(feature = "defmt", derive(defmt::Format))] -pub struct OutputPdAlert { - /// Port ID - pub port: LocalPortId, - /// ADO data - pub ado: Ado, -} - -/// Power policy command output data -pub struct OutputPowerPolicyCommand<'a> { - /// Port ID - pub port: LocalPortId, - /// Power policy request - pub request: - deferred::Request<'a, GlobalRawMutex, policy::device::CommandData, policy::device::InternalResponseData>, - /// Response - pub response: policy::device::InternalResponseData, -} - -/// Controller command output data -pub struct OutputControllerCommand<'a> { - /// Controller request - pub request: deferred::Request<'a, GlobalRawMutex, controller::Command, controller::Response<'static>>, - /// Response - pub response: controller::Response<'static>, -} - -pub mod vdm { - //! Events and output for vendor-defined messaging. - use super::LocalPortId; - use embedded_services::type_c::controller::{AttnVdm, OtherVdm}; - - /// The kind of output from processing a vendor-defined message. - #[derive(Copy, Clone, Debug)] - #[cfg_attr(feature = "defmt", derive(defmt::Format))] - pub enum OutputKind { - /// Entered custom mode - Entered(OtherVdm), - /// Exited custom mode - Exited(OtherVdm), - /// Received a vendor-defined other message - ReceivedOther(OtherVdm), - /// Received a vendor-defined attention message - ReceivedAttn(AttnVdm), - } - - /// Output from processing a vendor-defined message. - #[derive(Copy, Clone, Debug)] - #[cfg_attr(feature = "defmt", derive(defmt::Format))] - pub struct Output { - /// The port that the VDM message is associated with. - pub port: LocalPortId, - - /// The kind of VDM output. - pub kind: OutputKind, - } -} - -/// DP status changed output data -#[derive(Copy, Clone, Debug)] -#[cfg_attr(feature = "defmt", derive(defmt::Format))] -pub struct OutputDpStatusChanged { - /// Port ID - pub port: LocalPortId, - /// Port status - pub status: DpStatus, -} - -/// [`crate::wrapper::ControllerWrapper`] output -pub enum Output<'a> { - /// No-op when nothing specific is needed - Nop, - /// Port status changed - PortStatusChanged(OutputPortStatusChanged), - /// PD alert - PdAlert(OutputPdAlert), - /// Vendor-defined messaging. - Vdm(vdm::Output), - /// Power policy command received - PowerPolicyCommand(OutputPowerPolicyCommand<'a>), - /// TPCM command response - ControllerCommand(OutputControllerCommand<'a>), - /// CFU recovery tick - CfuRecovery, - /// CFU response - CfuResponse(embedded_services::cfu::component::InternalResponseData), - /// Dp status update - DpStatusUpdate(OutputDpStatusChanged), -} diff --git a/type-c-service/src/wrapper/mod.rs b/type-c-service/src/wrapper/mod.rs deleted file mode 100644 index e0f71fd16..000000000 --- a/type-c-service/src/wrapper/mod.rs +++ /dev/null @@ -1,693 +0,0 @@ -//! This module contains the [`ControllerWrapper`] struct. This struct serves as a bridge between various service messages -//! and the actual controller functions provided by [`embedded_services::type_c::controller::Controller`]. -//! # Supported service messaging -//! This struct current currently supports messages from the following services: -//! * Type-C: [`embedded_services::type_c::controller::Command`] -//! * Power policy: [`embedded_services::power::policy::device::Command`] -//! * CFU: [`embedded_services::cfu::Request`] -//! # Event loop -//! This struct follows a standard wait/process/finalize event loop. -//! -//! [`ControllerWrapper::wait_next`] returns [`message::Event`] and does not perform any actions on the controller -//! aside from reading pending events. -//! -//! [`ControllerWrapper::process_event`] reads any additional data relevant to the event and returns [`message::Output`]. -//! e.g. port status for a port status changed event, VDM data for a VDM event -//! -//! [`ControllerWrapper::process_event`] consumes [`message::Output`] and responds to any deferred requests, performs -//! any caching/buffering of data, and notifies the type-C service implementation of the event if needed. -use core::array::from_fn; -use core::cell::RefMut; -use core::future::pending; -use core::ops::DerefMut; - -use embassy_futures::select::{Either, Either5, select, select_array, select5}; -use embassy_sync::blocking_mutex::raw::RawMutex; -use embassy_sync::mutex::Mutex; -use embassy_sync::signal::Signal; -use embassy_time::Instant; -use embedded_cfu_protocol::protocol_definitions::{FwUpdateOffer, FwUpdateOfferResponse, FwVersion}; -use embedded_services::GlobalRawMutex; -use embedded_services::power::policy::device::StateKind; -use embedded_services::power::policy::{self, action}; -use embedded_services::sync::Lockable; -use embedded_services::type_c::controller::{self, Controller, PortStatus}; -use embedded_services::type_c::event::{PortEvent, PortNotificationSingle, PortPending, PortStatusChanged}; -use embedded_services::{debug, error, info, trace, warn}; -use embedded_usb_pd::ado::Ado; -use embedded_usb_pd::{Error, LocalPortId, PdError, PowerRole}; - -use crate::wrapper::backing::DynPortState; -use crate::wrapper::message::*; -use crate::{PortEventStreamer, PortEventVariant}; - -pub mod backing; -mod cfu; -pub mod config; -mod dp; -pub mod message; -mod pd; -mod power; -mod vdm; - -/// Base interval for checking for FW update timeouts and recovery attempts -pub const DEFAULT_FW_UPDATE_TICK_INTERVAL_MS: u64 = 5000; -/// Default number of ticks before we consider a firmware update to have timed out -/// 300 seconds at 5 seconds per tick -pub const DEFAULT_FW_UPDATE_TIMEOUT_TICKS: u8 = 60; - -/// Trait for validating firmware versions before applying an update -// TODO: remove this once we have a better framework for OEM customization -// See https://github.com/OpenDevicePartnership/embedded-services/issues/326 -pub trait FwOfferValidator { - /// Determine if we are accepting the firmware update offer, returns a CFU offer response - fn validate(&self, current: FwVersion, offer: &FwUpdateOffer) -> FwUpdateOfferResponse; -} - -/// Maximum number of supported ports -pub const MAX_SUPPORTED_PORTS: usize = 2; - -/// Common functionality implemented on top of [`embedded_services::type_c::controller::Controller`] -pub struct ControllerWrapper<'device, M: RawMutex, C: Lockable, V: FwOfferValidator> -where - ::Inner: Controller, -{ - controller: &'device C, - /// Trait object for validating firmware versions - fw_version_validator: V, - /// FW update ticker used to check for timeouts and recovery attempts - fw_update_ticker: Mutex, - /// Registration information for services - registration: backing::Registration<'device>, - /// State - state: Mutex>>, - /// SW port status event signal - sw_status_event: Signal, - /// General config - config: config::Config, -} - -impl<'device, M: RawMutex, C: Lockable, V: FwOfferValidator> ControllerWrapper<'device, M, C, V> -where - ::Inner: Controller, -{ - /// Create a new controller wrapper, returns `None` if the backing storage is already in use - pub fn try_new( - controller: &'device C, - config: config::Config, - storage: &'device backing::ReferencedStorage<'device, N, M>, - fw_version_validator: V, - ) -> Option { - const { - assert!(N > 0 && N <= MAX_SUPPORTED_PORTS, "Invalid number of ports"); - }; - - let backing = storage.create_backing()?; - Some(Self { - controller, - config, - fw_version_validator, - fw_update_ticker: Mutex::new(embassy_time::Ticker::every(embassy_time::Duration::from_millis( - DEFAULT_FW_UPDATE_TICK_INTERVAL_MS, - ))), - registration: backing.registration, - state: Mutex::new(backing.state), - sw_status_event: Signal::new(), - }) - } - - /// Get the power policy devices for this controller. - pub fn power_policy_devices(&self) -> &[policy::device::Device] { - self.registration.power_devices - } - - /// Get the cached port status, returns None if the port is invalid - pub async fn get_cached_port_status(&self, local_port: LocalPortId) -> Option { - self.state - .lock() - .await - .port_states() - .get(local_port.0 as usize) - .map(|s| s.status) - } - - /// Synchronize the state between the controller and the internal state - pub async fn sync_state(&self) -> Result<(), Error<::BusError>> { - let mut controller = self.controller.lock().await; - let mut state = self.state.lock().await; - self.sync_state_internal(&mut controller, state.deref_mut().deref_mut()) - .await - } - - /// Synchronize the state between the controller and the internal state - async fn sync_state_internal( - &self, - controller: &mut C::Inner, - state: &mut dyn DynPortState<'_>, - ) -> Result<(), Error<::BusError>> { - // Sync the controller state with the PD controller - for (i, port_state) in state.port_states_mut().iter_mut().enumerate() { - let mut status_changed = port_state.sw_status_event; - let local_port = LocalPortId(i as u8); - let status = controller.get_port_status(local_port).await?; - trace!("Port{} status: {:#?}", i, status); - - let previous_status = port_state.status; - - if previous_status.is_connected() != status.is_connected() { - status_changed.set_plug_inserted_or_removed(true); - } - - if previous_status.available_sink_contract != status.available_sink_contract { - status_changed.set_new_power_contract_as_consumer(true); - } - - if previous_status.available_source_contract != status.available_source_contract { - status_changed.set_new_power_contract_as_provider(true); - } - - port_state.sw_status_event = status_changed; - if port_state.sw_status_event != PortStatusChanged::none() { - // Have a status changed event, notify - trace!("Port{} status changed: {:#?}", i, status); - self.sw_status_event.signal(()); - } - } - Ok(()) - } - - /// Handle a plug event - async fn process_plug_event( - &self, - _controller: &mut C::Inner, - power: &policy::device::Device, - port: LocalPortId, - status: &PortStatus, - ) -> Result<(), Error<::BusError>> { - if port.0 as usize >= self.registration.num_ports() { - error!("Invalid port {}", port.0); - return PdError::InvalidPort.into(); - } - - info!("Plug event"); - if status.is_connected() { - info!("Plug inserted"); - - // Recover if we're not in the correct state - if power.state().await.kind() != StateKind::Detached { - warn!("Power device not in detached state, recovering"); - if let Err(e) = power.detach().await { - error!("Error detaching power device: {:?}", e); - return PdError::Failed.into(); - } - } - - if let Ok(state) = power.try_device_action::().await { - if let Err(e) = state.attach().await { - error!("Error attaching power device: {:?}", e); - return PdError::Failed.into(); - } - } else { - // This should never happen - error!("Power device not in detached state"); - return PdError::InvalidMode.into(); - } - } else { - info!("Plug removed"); - if let Err(e) = power.detach().await { - error!("Error detaching power device: {:?}", e); - return PdError::Failed.into(); - }; - } - - Ok(()) - } - - /// Process port status changed events - async fn process_port_status_changed<'b>( - &self, - controller: &mut C::Inner, - state: &mut dyn DynPortState<'_>, - local_port_id: LocalPortId, - status_event: PortStatusChanged, - ) -> Result, Error<::BusError>> { - let global_port_id = self - .registration - .pd_controller - .lookup_global_port(local_port_id) - .map_err(Error::Pd)?; - - let previous_status = state - .port_states() - .get(local_port_id.0 as usize) - .ok_or(Error::Pd(PdError::InvalidPort))? - .status; - let status = controller.get_port_status(local_port_id).await?; - trace!("Port{} status: {:#?}", global_port_id.0, status); - - let power = self - .get_power_device(local_port_id) - .ok_or(Error::Pd(PdError::InvalidPort))?; - trace!("Port{} status events: {:#?}", global_port_id.0, status_event); - - if status_event.pd_hard_reset() { - info!("Port{}: PD hard reset", global_port_id.0); - - if let Ok(connected_consumer) = power.try_device_action::().await { - info!("Port{}: Disabling sink path due to PD hard reset", global_port_id.0); - // Vbus drops to 0V during a hard reset, stop drawing power - match controller.enable_sink_path(local_port_id, false).await { - Err(Error::Pd(err)) => error!( - "Port{}: Error disabling sink path after PD hard reset, {:#?}", - global_port_id.0, err - ), - Err(Error::Bus(_)) => error!( - "Port{}: Error disabling sink path after PD hard reset, Bus error", - global_port_id.0 - ), - _ => {} - } - if let Err(e) = connected_consumer.disconnect().await { - error!( - "Port{}: Error disconnecting from ConnectedConsumer after PD hard reset: {:#?}", - global_port_id.0, e - ); - } - } else if let Ok(connected_provider) = power.try_device_action::().await { - info!("Port{}: Disconnecting provider after hard reset", global_port_id.0); - if let Err(e) = connected_provider.disconnect().await { - error!( - "Port{}: Error disconnecting from ConnectedProvider after PD hard reset: {:#?}", - global_port_id.0, e - ); - } - } - } - - if status_event.plug_inserted_or_removed() { - self.process_plug_event(controller, power, local_port_id, &status) - .await?; - } - - // Only notify power policy of a contract after Sink Ready event (always after explicit or implicit contract) - if status_event.sink_ready() { - self.process_new_consumer_contract(power, &status).await?; - } - - if status.is_connected() && status.available_source_contract != previous_status.available_source_contract { - self.process_new_provider_contract(power, &status).await?; - } - - self.check_sink_ready_timeout( - state, - &status, - local_port_id, - status_event.new_power_contract_as_consumer(), - status_event.sink_ready(), - )?; - - Ok(Output::PortStatusChanged(OutputPortStatusChanged { - port: local_port_id, - status_event, - status, - })) - } - - /// Finalize a port status change output - fn finalize_port_status_change( - &self, - state: &mut dyn DynPortState<'_>, - local_port: LocalPortId, - status_event: PortStatusChanged, - status: PortStatus, - ) -> Result<(), Error<::BusError>> { - let port_index = local_port.0 as usize; - let global_port_id = self - .registration - .pd_controller - .lookup_global_port(local_port) - .map_err(Error::Pd)?; - - let port_state = state - .port_states_mut() - .get_mut(port_index) - .ok_or(Error::Pd(PdError::InvalidPort))?; - let mut events = port_state.pending_events; - events.status = events.status.union(status_event); - port_state.pending_events = events; - port_state.status = status; - - if events != PortEvent::none() { - let mut pending = PortPending::none(); - pending - .pend_port(global_port_id.0 as usize) - .map_err(|_| Error::Pd(PdError::InvalidPort))?; - self.registration.pd_controller.notify_ports(pending); - trace!("P{}: Notified service for events: {:#?}", global_port_id.0, events); - } - - Ok(()) - } - - /// Finalize a PD alert output - fn finalize_pd_alert( - &self, - state: &mut dyn DynPortState<'_>, - local_port: LocalPortId, - alert: Ado, - ) -> Result<(), Error<::BusError>> { - let port_index = local_port.0 as usize; - let global_port_id = self - .registration - .pd_controller - .lookup_global_port(local_port) - .map_err(Error::Pd)?; - - let port_state = state - .port_states_mut() - .get_mut(port_index) - .ok_or(Error::Pd(PdError::InvalidPort))?; - // Buffer the alert - port_state.pd_alerts.0.publish_immediate(alert); - - // Pend the alert - port_state.pending_events.notification.set_alert(true); - - // Pend this port - let mut pending = PortPending::none(); - pending - .pend_port(global_port_id.0 as usize) - .map_err(|_| Error::Pd(PdError::InvalidPort))?; - self.registration.pd_controller.notify_ports(pending); - Ok(()) - } - - /// Wait for a pending port event - /// - /// DROP SAFETY: No state that needs to be restored - async fn wait_port_pending( - &self, - controller: &mut C::Inner, - ) -> Result::BusError>> { - if self.state.lock().await.controller_state().fw_update_state.in_progress() { - // Don't process events while firmware update is in progress - debug!("Firmware update in progress, ignoring port events"); - return pending().await; - } - - let streaming_state = self.state.lock().await.controller_state().port_event_streaming_state; - if let Some(streamer) = streaming_state { - // If we're converting the bitfields into an event stream yield first to prevent starving other tasks - embassy_futures::yield_now().await; - Ok(streamer) - } else { - // Otherwise, wait for the next port event - // DROP SAFETY: Safe as long as `wait_port_event` is drop safe - match select(controller.wait_port_event(), async { - self.sw_status_event.wait().await; - Ok::<_, Error<::BusError>>(()) - }) - .await - { - Either::First(r) => r?, - Either::Second(_) => (), - }; - let pending: PortPending = FromIterator::from_iter(0..self.registration.num_ports()); - Ok(PortEventStreamer::new(pending.into_iter())) - } - } - - /// Wait for the next event - pub async fn wait_next(&self) -> Result, Error<::BusError>> { - // This loop is to ensure that if we finish streaming events we go back to waiting for the next port event - loop { - let event = { - let mut controller = self.controller.lock().await; - // DROP SAFETY: Select over drop safe functions - select5( - self.wait_port_pending(&mut controller), - self.wait_power_command(), - self.registration.pd_controller.receive(), - self.wait_cfu_command(), - self.wait_sink_ready_timeout(), - ) - .await - }; - match event { - Either5::First(stream) => { - let mut stream = stream?; - if let Some((port_index, event)) = stream - .next::::BusError>, _, _>(async |port_index| { - // Combine the event read from the controller with any software generated events - // Acquire the locks first to centralize the awaits here - let mut controller = self.controller.lock().await; - let mut state = self.state.lock().await; - let port_state = state - .port_states_mut() - .get_mut(port_index) - .ok_or(Error::Pd(PdError::InvalidPort))?; - let hw_event = controller.clear_port_events(LocalPortId(port_index as u8)).await?; - - // No more awaits, modify state here for drop safety - let sw_event = - core::mem::replace(&mut port_state.sw_status_event, PortStatusChanged::none()); - Ok(hw_event.union(sw_event.into())) - }) - .await? - { - let port_id = LocalPortId(port_index as u8); - self.state - .lock() - .await - .controller_state_mut() - .port_event_streaming_state = Some(stream); - match event { - PortEventVariant::StatusChanged(status_event) => { - return Ok(Event::PortStatusChanged(EventPortStatusChanged { - port: port_id, - status_event, - })); - } - PortEventVariant::Notification(notification) => { - return Ok(Event::PortNotification(EventPortNotification { - port: port_id, - notification, - })); - } - } - } else { - self.state - .lock() - .await - .controller_state_mut() - .port_event_streaming_state = None; - } - } - Either5::Second((port, request)) => { - return Ok(Event::PowerPolicyCommand(EventPowerPolicyCommand { port, request })); - } - Either5::Third(request) => return Ok(Event::ControllerCommand(request)), - Either5::Fourth(event) => return Ok(Event::CfuEvent(event)), - Either5::Fifth(port) => { - // Sink ready timeout event - debug!("Port{0}: Sink ready timeout", port.0); - self.state - .lock() - .await - .port_states_mut() - .get_mut(port.0 as usize) - .ok_or(Error::Pd(PdError::InvalidPort))? - .sink_ready_deadline = None; - let mut status_event = PortStatusChanged::none(); - status_event.set_sink_ready(true); - return Ok(Event::PortStatusChanged(EventPortStatusChanged { port, status_event })); - } - } - } - } - - /// Process a port notification - async fn process_port_notification<'b>( - &self, - controller: &mut C::Inner, - port: LocalPortId, - notification: PortNotificationSingle, - ) -> Result, Error<::BusError>> { - match notification { - PortNotificationSingle::Alert => { - let ado = controller.get_pd_alert(port).await?; - trace!("Port{}: PD alert: {:#?}", port.0, ado); - if let Some(ado) = ado { - Ok(Output::PdAlert(OutputPdAlert { port, ado })) - } else { - // For some reason we didn't read an alert, nothing to do - Ok(Output::Nop) - } - } - PortNotificationSingle::Vdm(event) => { - self.process_vdm_event(controller, port, event).await.map(Output::Vdm) - } - PortNotificationSingle::DpStatusUpdate => self - .process_dp_status_update(controller, port) - .await - .map(Output::DpStatusUpdate), - rest => { - // Nothing currently implemented for these - trace!("Port{}: Notification: {:#?}", port.0, rest); - Ok(Output::Nop) - } - } - } - - /// Top-level processing function - /// Only call this fn from one place in a loop. Otherwise a deadlock could occur. - pub async fn process_event<'b>( - &self, - event: Event<'b>, - ) -> Result, Error<::BusError>> { - let mut controller = self.controller.lock().await; - let mut state = self.state.lock().await; - match event { - Event::PortStatusChanged(EventPortStatusChanged { port, status_event }) => { - self.process_port_status_changed(&mut controller, state.deref_mut().deref_mut(), port, status_event) - .await - } - Event::PowerPolicyCommand(EventPowerPolicyCommand { port, request }) => { - let response = self - .process_power_command(&mut controller, state.deref_mut().deref_mut(), port, &request.command) - .await; - Ok(Output::PowerPolicyCommand(OutputPowerPolicyCommand { - port, - request, - response, - })) - } - Event::ControllerCommand(request) => { - let response = self - .process_pd_command(&mut controller, state.deref_mut().deref_mut(), &request.command) - .await; - Ok(Output::ControllerCommand(OutputControllerCommand { request, response })) - } - Event::CfuEvent(event) => match event { - EventCfu::Request(request) => { - let response = self - .process_cfu_command(&mut controller, state.deref_mut().deref_mut(), &request) - .await; - Ok(Output::CfuResponse(response)) - } - EventCfu::RecoveryTick => { - // FW Update tick, process timeouts and recovery attempts - self.process_cfu_tick(&mut controller, state.deref_mut().deref_mut()) - .await; - Ok(Output::CfuRecovery) - } - }, - Event::PortNotification(EventPortNotification { port, notification }) => { - self.process_port_notification(&mut controller, port, notification) - .await - } - } - } - - /// Event loop finalize - pub async fn finalize<'b>(&self, output: Output<'b>) -> Result<(), Error<::BusError>> { - let mut state = self.state.lock().await; - - match output { - Output::Nop => Ok(()), - Output::PortStatusChanged(OutputPortStatusChanged { - port, - status_event, - status, - }) => self.finalize_port_status_change(state.deref_mut().deref_mut(), port, status_event, status), - Output::PdAlert(OutputPdAlert { port, ado }) => { - self.finalize_pd_alert(state.deref_mut().deref_mut(), port, ado) - } - Output::Vdm(vdm) => self.finalize_vdm(state.deref_mut().deref_mut(), vdm).map_err(Error::Pd), - Output::PowerPolicyCommand(OutputPowerPolicyCommand { request, response, .. }) => { - request.respond(response); - Ok(()) - } - Output::ControllerCommand(OutputControllerCommand { request, response }) => { - request.respond(response); - Ok(()) - } - Output::CfuRecovery => { - // Nothing to do here - Ok(()) - } - Output::CfuResponse(response) => { - self.send_cfu_response(response).await; - Ok(()) - } - Output::DpStatusUpdate(_) => { - // Nothing to do here - Ok(()) - } - } - } - - /// Combined processing and finialization function - pub async fn process_and_finalize_event<'b>( - &self, - event: Event<'b>, - ) -> Result<(), Error<::BusError>> { - let output = self.process_event(event).await?; - self.finalize(output).await - } - - /// Combined processing function - pub async fn process_next_event(&self) -> Result<(), Error<::BusError>> { - let event = self.wait_next().await?; - self.process_and_finalize_event(event).await - } - - /// Register all devices with their respective services - pub async fn register(&'static self) -> Result<(), Error<::BusError>> { - for device in self.registration.power_devices { - policy::register_device(device).map_err(|_| { - error!( - "Controller{}: Failed to register power device {}", - self.registration.pd_controller.id().0, - device.id().0 - ); - Error::Pd(PdError::Failed) - })?; - } - - controller::register_controller(self.registration.pd_controller).map_err(|_| { - error!( - "Controller{}: Failed to register PD controller", - self.registration.pd_controller.id().0 - ); - Error::Pd(PdError::Failed) - })?; - - //TODO: Remove when we have a more general framework in place - embedded_services::cfu::register_device(self.registration.cfu_device) - .await - .map_err(|_| { - error!( - "Controller{}: Failed to register CFU device", - self.registration.pd_controller.id().0 - ); - Error::Pd(PdError::Failed) - })?; - Ok(()) - } -} - -impl<'device, M: RawMutex, C: Lockable, V: FwOfferValidator> Lockable for ControllerWrapper<'device, M, C, V> -where - ::Inner: Controller, -{ - type Inner = C::Inner; - - fn try_lock(&self) -> Option> { - self.controller.try_lock() - } - - fn lock(&self) -> impl Future> { - self.controller.lock() - } -} diff --git a/type-c-service/src/wrapper/pd.rs b/type-c-service/src/wrapper/pd.rs deleted file mode 100644 index a9b233c98..000000000 --- a/type-c-service/src/wrapper/pd.rs +++ /dev/null @@ -1,517 +0,0 @@ -use embassy_futures::yield_now; -use embassy_sync::pubsub::WaitResult; -use embassy_time::{Duration, Timer}; -use embedded_services::debug; -use embedded_services::type_c::Cached; -use embedded_services::type_c::controller::{InternalResponseData, Response}; -use embedded_usb_pd::constants::{T_PS_TRANSITION_EPR_MS, T_PS_TRANSITION_SPR_MS}; -use embedded_usb_pd::ucsi::{self, lpm}; - -use super::*; - -impl<'device, M: RawMutex, C: Lockable, V: FwOfferValidator> ControllerWrapper<'device, M, C, V> -where - ::Inner: Controller, -{ - async fn process_get_pd_alert( - &self, - state: &mut dyn DynPortState<'_>, - local_port: LocalPortId, - ) -> Result, PdError> { - loop { - match state - .port_states_mut() - .get_mut(local_port.0 as usize) - .ok_or(PdError::InvalidPort)? - .pd_alerts - .1 - .try_next_message() - { - Some(WaitResult::Message(alert)) => return Ok(Some(alert)), - None => return Ok(None), - Some(WaitResult::Lagged(count)) => { - warn!("Port{}: Lagged PD alert channel: {}", local_port.0, count); - // Yield to avoid starving other tasks since we're in a loop and try_next_message isn't async - yield_now().await; - } - } - } - } - - /// Check the sink ready timeout - /// - /// After accepting a sink contract (new contract as consumer), the PD spec guarantees that the - /// source will be available to provide power after `tPSTransition`. This allows us to handle transitions - /// even for controllers that might not always broadcast sink ready events. - pub(super) fn check_sink_ready_timeout( - &self, - state: &mut dyn DynPortState<'_>, - status: &PortStatus, - port: LocalPortId, - new_contract: bool, - sink_ready: bool, - ) -> Result<(), PdError> { - if port.0 as usize >= state.num_ports() { - return Err(PdError::InvalidPort); - } - - let port_state = state - .port_states_mut() - .get_mut(port.0 as usize) - .ok_or(PdError::InvalidPort)?; - - let contract_changed = port_state.status.available_sink_contract != status.available_sink_contract; - let deadline = &mut port_state.sink_ready_deadline; - - // Don't start the timeout if the sink has signaled it's ready or if the contract didn't change. - // The latter ensures that soft resets won't continually reset the ready timeout - debug!( - "Port{}: Check sink ready: new_contract={:?}, sink_ready={:?}, contract_changed={:?}, deadline={:?}", - port.0, new_contract, sink_ready, contract_changed, deadline, - ); - if new_contract && !sink_ready && contract_changed { - // Start the timeout - // Double the spec maximum transition time to provide a safety margin for hardware/controller delays or out-of-spec controllers. - let timeout_ms = if status.epr { - T_PS_TRANSITION_EPR_MS - } else { - T_PS_TRANSITION_SPR_MS - } - .maximum - .0 * 2; - - debug!("Port{}: Sink ready timeout started for {}ms", port.0, timeout_ms); - *deadline = Some(Instant::now() + Duration::from_millis(timeout_ms as u64)); - } else if deadline.is_some() - && (!status.is_connected() || status.available_sink_contract.is_none() || sink_ready) - { - debug!("Port{}: Sink ready timeout cleared", port.0); - *deadline = None; - } - Ok(()) - } - - /// Wait for a sink ready timeout and return the port that has timed out. - /// - /// DROP SAFETY: No state to restore - pub(super) async fn wait_sink_ready_timeout(&self) -> LocalPortId { - let futures: [_; MAX_SUPPORTED_PORTS] = from_fn(|i| async move { - let deadline = self - .state - .lock() - .await - .port_states() - .get(i) - .and_then(|s| s.sink_ready_deadline); - - if let Some(deadline) = deadline { - Timer::at(deadline).await; - debug!("Port{}: Sink ready timeout reached", i); - if let Some(state) = self.state.lock().await.port_states_mut().get_mut(i) { - state.sink_ready_deadline = None; - } else { - error!("Invalid state array index {}", i); - } - } else { - pending::<()>().await; - } - }); - - // DROP SAFETY: Select over drop safe futures - let (_, port_index) = select_array(futures).await; - LocalPortId(port_index as u8) - } - - /// Set the maximum sink voltage for a port - pub async fn set_max_sink_voltage(&self, local_port: LocalPortId, voltage_mv: Option) -> Result<(), PdError> { - let mut controller = self.controller.lock().await; - let _ = self - .process_set_max_sink_voltage(&mut controller, local_port, voltage_mv) - .await?; - Ok(()) - } - - /// Process a request to set the maximum sink voltage for a port - async fn process_set_max_sink_voltage( - &self, - controller: &mut C::Inner, - local_port: LocalPortId, - voltage_mv: Option, - ) -> Result { - let power_device = self.get_power_device(local_port).ok_or(PdError::InvalidPort)?; - - let state = power_device.state().await; - debug!("Port{}: Current state: {:#?}", local_port.0, state); - if let Ok(connected_consumer) = power_device.try_device_action::().await { - debug!("Port{}: Set max sink voltage, connected consumer found", local_port.0); - if voltage_mv.is_some() - && voltage_mv - < power_device - .consumer_capability() - .await - .map(|c| c.capability.voltage_mv) - { - // New max voltage is lower than current consumer capability which will trigger a renegociation - // So disconnect first - debug!( - "Port{}: Disconnecting consumer before setting max sink voltage", - local_port.0 - ); - let _ = connected_consumer.disconnect().await; - } - } - - match controller.set_max_sink_voltage(local_port, voltage_mv).await { - Ok(()) => Ok(controller::PortResponseData::Complete), - Err(e) => match e { - Error::Bus(_) => Err(PdError::Failed), - Error::Pd(e) => Err(e), - }, - } - } - - async fn process_get_port_status( - &self, - controller: &mut C::Inner, - state: &mut dyn DynPortState<'_>, - local_port: LocalPortId, - cached: Cached, - ) -> Result { - if cached.0 { - Ok(controller::PortResponseData::PortStatus( - state - .port_states() - .get(local_port.0 as usize) - .ok_or(PdError::InvalidPort)? - .status, - )) - } else { - match controller.get_port_status(local_port).await { - Ok(status) => Ok(controller::PortResponseData::PortStatus(status)), - Err(e) => match e { - Error::Bus(_) => Err(PdError::Failed), - Error::Pd(e) => Err(e), - }, - } - } - } - - /// Handle a port command - async fn process_port_command( - &self, - controller: &mut C::Inner, - state: &mut dyn DynPortState<'_>, - command: &controller::PortCommand, - ) -> Response<'static> { - if state.controller_state().fw_update_state.in_progress() { - debug!("FW update in progress, ignoring port command"); - return controller::Response::Port(Err(PdError::Busy)); - } - - let local_port = if let Ok(port) = self.registration.pd_controller.lookup_local_port(command.port) { - port - } else { - debug!("Invalid port: {:?}", command.port); - return controller::Response::Port(Err(PdError::InvalidPort)); - }; - - controller::Response::Port(match command.data { - controller::PortCommandData::PortStatus(cached) => { - self.process_get_port_status(controller, state, local_port, cached) - .await - } - controller::PortCommandData::ClearEvents => { - let port_index = local_port.0 as usize; - let state = if let Some(state) = state.port_states_mut().get_mut(port_index) { - state - } else { - return controller::Response::Port(Err(PdError::InvalidPort)); - }; - - let event = core::mem::take(&mut state.pending_events); - Ok(controller::PortResponseData::ClearEvents(event)) - } - controller::PortCommandData::RetimerFwUpdateGetState => { - match controller.get_rt_fw_update_status(local_port).await { - Ok(status) => Ok(controller::PortResponseData::RtFwUpdateStatus(status)), - Err(e) => match e { - Error::Bus(_) => Err(PdError::Failed), - Error::Pd(e) => Err(e), - }, - } - } - controller::PortCommandData::RetimerFwUpdateSetState => { - match controller.set_rt_fw_update_state(local_port).await { - Ok(()) => Ok(controller::PortResponseData::Complete), - Err(e) => match e { - Error::Bus(_) => Err(PdError::Failed), - Error::Pd(e) => Err(e), - }, - } - } - controller::PortCommandData::RetimerFwUpdateClearState => { - match controller.clear_rt_fw_update_state(local_port).await { - Ok(()) => Ok(controller::PortResponseData::Complete), - Err(e) => match e { - Error::Bus(_) => Err(PdError::Failed), - Error::Pd(e) => Err(e), - }, - } - } - controller::PortCommandData::SetRetimerCompliance => match controller.set_rt_compliance(local_port).await { - Ok(()) => Ok(controller::PortResponseData::Complete), - Err(e) => match e { - Error::Bus(_) => Err(PdError::Failed), - Error::Pd(e) => Err(e), - }, - }, - controller::PortCommandData::ReconfigureRetimer => match controller.reconfigure_retimer(local_port).await { - Ok(()) => Ok(controller::PortResponseData::Complete), - Err(e) => match e { - Error::Bus(_) => Err(PdError::Failed), - Error::Pd(e) => Err(e), - }, - }, - controller::PortCommandData::GetPdAlert => match self.process_get_pd_alert(state, local_port).await { - Ok(alert) => Ok(controller::PortResponseData::PdAlert(alert)), - Err(e) => Err(e), - }, - controller::PortCommandData::SetMaxSinkVoltage(voltage_mv) => { - match self.registration.pd_controller.lookup_local_port(command.port) { - Ok(local_port) => { - self.process_set_max_sink_voltage(controller, local_port, voltage_mv) - .await - } - Err(e) => Err(e), - } - } - controller::PortCommandData::SetUnconstrainedPower(unconstrained) => { - match controller.set_unconstrained_power(local_port, unconstrained).await { - Ok(()) => Ok(controller::PortResponseData::Complete), - Err(e) => match e { - Error::Bus(_) => Err(PdError::Failed), - Error::Pd(e) => Err(e), - }, - } - } - controller::PortCommandData::ClearDeadBatteryFlag => { - match controller.clear_dead_battery_flag(local_port).await { - Ok(()) => Ok(controller::PortResponseData::Complete), - Err(e) => match e { - Error::Bus(_) => Err(PdError::Failed), - Error::Pd(e) => Err(e), - }, - } - } - controller::PortCommandData::GetOtherVdm => match controller.get_other_vdm(local_port).await { - Ok(vdm) => { - debug!("Port{}: Other VDM: {:?}", local_port.0, vdm); - Ok(controller::PortResponseData::OtherVdm(vdm)) - } - Err(e) => match e { - Error::Bus(_) => Err(PdError::Failed), - Error::Pd(e) => Err(e), - }, - }, - controller::PortCommandData::GetAttnVdm => match controller.get_attn_vdm(local_port).await { - Ok(vdm) => { - debug!("Port{}: Attention VDM: {:?}", local_port.0, vdm); - Ok(controller::PortResponseData::AttnVdm(vdm)) - } - Err(e) => match e { - Error::Bus(_) => Err(PdError::Failed), - Error::Pd(e) => Err(e), - }, - }, - controller::PortCommandData::SendVdm(tx_vdm) => match controller.send_vdm(local_port, tx_vdm).await { - Ok(()) => Ok(controller::PortResponseData::Complete), - Err(e) => match e { - Error::Bus(_) => Err(PdError::Failed), - Error::Pd(e) => Err(e), - }, - }, - controller::PortCommandData::SetUsbControl(config) => { - match controller.set_usb_control(local_port, config).await { - Ok(()) => Ok(controller::PortResponseData::Complete), - Err(e) => match e { - Error::Bus(_) => Err(PdError::Failed), - Error::Pd(e) => Err(e), - }, - } - } - controller::PortCommandData::GetDpStatus => match controller.get_dp_status(local_port).await { - Ok(status) => { - debug!("Port{}: DP Status: {:?}", local_port.0, status); - Ok(controller::PortResponseData::DpStatus(status)) - } - Err(e) => match e { - Error::Bus(_) => Err(PdError::Failed), - Error::Pd(e) => Err(e), - }, - }, - controller::PortCommandData::SetDpConfig(config) => { - match controller.set_dp_config(local_port, config).await { - Ok(()) => Ok(controller::PortResponseData::Complete), - Err(e) => match e { - Error::Bus(_) => Err(PdError::Failed), - Error::Pd(e) => Err(e), - }, - } - } - controller::PortCommandData::ExecuteDrst => match controller.execute_drst(local_port).await { - Ok(()) => Ok(controller::PortResponseData::Complete), - Err(e) => match e { - Error::Bus(_) => Err(PdError::Failed), - Error::Pd(e) => Err(e), - }, - }, - controller::PortCommandData::SetTbtConfig(config) => { - match controller.set_tbt_config(local_port, config).await { - Ok(()) => Ok(controller::PortResponseData::Complete), - Err(e) => match e { - Error::Bus(_) => Err(PdError::Failed), - Error::Pd(e) => Err(e), - }, - } - } - controller::PortCommandData::SetPdStateMachineConfig(config) => { - match controller.set_pd_state_machine_config(local_port, config).await { - Ok(()) => Ok(controller::PortResponseData::Complete), - Err(e) => match e { - Error::Bus(_) => Err(PdError::Failed), - Error::Pd(e) => Err(e), - }, - } - } - controller::PortCommandData::SetTypeCStateMachineConfig(state) => { - match controller.set_type_c_state_machine_config(local_port, state).await { - Ok(()) => Ok(controller::PortResponseData::Complete), - Err(e) => match e { - Error::Bus(_) => Err(PdError::Failed), - Error::Pd(e) => Err(e), - }, - } - } - controller::PortCommandData::ExecuteUcsiCommand(command_data) => { - Ok(controller::PortResponseData::UcsiResponse( - controller - .execute_ucsi_command(lpm::Command::new(local_port, command_data)) - .await - .map_err(|e| match e { - Error::Bus(_) => PdError::Failed, - Error::Pd(e) => e, - }), - )) - } - controller::PortCommandData::ExecuteElectricalDisconnect { reconnect_time_s } => { - match controller - .execute_electrical_disconnect(local_port, reconnect_time_s) - .await - { - Ok(()) => Ok(controller::PortResponseData::Complete), - Err(e) => match e { - Error::Bus(_) => Err(PdError::Failed), - Error::Pd(e) => Err(e), - }, - } - } - controller::PortCommandData::SetSystemPowerState(power_state) => { - match controller.set_power_state(local_port, power_state).await { - Ok(()) => Ok(controller::PortResponseData::Complete), - Err(e) => match e { - Error::Bus(_) => Err(PdError::Failed), - Error::Pd(e) => Err(e), - }, - } - } - controller::PortCommandData::GetDiscoveredSvids => { - match controller.get_discovered_svids(local_port).await { - Ok(svids) => Ok(controller::PortResponseData::DiscoveredSvids(svids)), - Err(e) => match e { - Error::Bus(_) => Err(PdError::Failed), - Error::Pd(e) => Err(e), - }, - } - } - controller::PortCommandData::HardReset => match controller.hard_reset(local_port).await { - Ok(()) => Ok(controller::PortResponseData::Complete), - Err(e) => match e { - Error::Bus(_) => Err(PdError::Failed), - Error::Pd(e) => Err(e), - }, - }, - controller::PortCommandData::GetDiscoverIdentitySop => { - match controller.get_discover_identity_sop_response(local_port).await { - Ok(vdos) => Ok(controller::PortResponseData::DiscoverIdentitySop(vdos)), - Err(e) => match e { - Error::Bus(_) => Err(PdError::Failed), - Error::Pd(e) => Err(e), - }, - } - } - controller::PortCommandData::GetDiscoverIdentitySopPrime => { - match controller.get_discover_identity_sop_prime_response(local_port).await { - Ok(vdos) => Ok(controller::PortResponseData::DiscoverIdentitySopPrime(vdos)), - Err(e) => match e { - Error::Bus(_) => Err(PdError::Failed), - Error::Pd(e) => Err(e), - }, - } - } - }) - } - - async fn process_controller_command( - &self, - controller: &mut C::Inner, - state: &mut dyn DynPortState<'_>, - command: &controller::InternalCommandData, - ) -> Response<'static> { - if state.controller_state().fw_update_state.in_progress() { - debug!("FW update in progress, ignoring controller command"); - return controller::Response::Controller(Err(PdError::Busy)); - } - - match command { - controller::InternalCommandData::Status => { - let status = controller.get_controller_status().await; - controller::Response::Controller(status.map(InternalResponseData::Status).map_err(|_| PdError::Failed)) - } - controller::InternalCommandData::SyncState => { - let result = self.sync_state_internal(controller, state).await; - controller::Response::Controller( - result - .map(|_| InternalResponseData::Complete) - .map_err(|_| PdError::Failed), - ) - } - controller::InternalCommandData::Reset => { - let result = controller.reset_controller().await; - controller::Response::Controller( - result - .map(|_| InternalResponseData::Complete) - .map_err(|_| PdError::Failed), - ) - } - } - } - - /// Handle a PD controller command - pub(super) async fn process_pd_command( - &self, - controller: &mut C::Inner, - state: &mut dyn DynPortState<'_>, - command: &controller::Command, - ) -> Response<'static> { - match command { - controller::Command::Port(command) => self.process_port_command(controller, state, command).await, - controller::Command::Controller(command) => { - self.process_controller_command(controller, state, command).await - } - controller::Command::Lpm(_) => controller::Response::Ucsi(ucsi::Response { - cci: ucsi::cci::Cci::new_error(), - data: None, - }), - } - } -} diff --git a/type-c-service/src/wrapper/power.rs b/type-c-service/src/wrapper/power.rs deleted file mode 100644 index 49be74568..000000000 --- a/type-c-service/src/wrapper/power.rs +++ /dev/null @@ -1,286 +0,0 @@ -//! Module contain power-policy related message handling -use core::future; - -use embedded_services::{ - debug, - ipc::deferred, - power::policy::{ - ConsumerPowerCapability, ProviderPowerCapability, - device::{CommandData, InternalResponseData}, - flags::PsuType, - }, -}; - -use crate::wrapper::config::UnconstrainedSink; - -use super::*; - -impl<'device, M: RawMutex, C: Lockable, V: FwOfferValidator> ControllerWrapper<'device, M, C, V> -where - ::Inner: Controller, -{ - /// Return the power device for the given port - pub fn get_power_device(&self, port: LocalPortId) -> Option<&policy::device::Device> { - self.registration.power_devices.get(port.0 as usize) - } - - /// Handle a new contract as consumer - pub(super) async fn process_new_consumer_contract( - &self, - power: &policy::device::Device, - status: &PortStatus, - ) -> Result<(), Error<::BusError>> { - info!("Process new consumer contract"); - - let current_state = power.state().await.kind(); - info!("current power state: {:?}", current_state); - - if status.power_role == PowerRole::Source { - info!("Port is source, not notifying of consumer contract"); - return Ok(()); - } - - let available_sink_contract = status.available_sink_contract.map(|c| { - let mut c: ConsumerPowerCapability = c.into(); - let unconstrained = match self.config.unconstrained_sink { - UnconstrainedSink::Auto => status.unconstrained_power, - UnconstrainedSink::PowerThresholdMilliwatts(threshold) => c.capability.max_power_mw() >= threshold, - UnconstrainedSink::Never => false, - }; - c.flags.set_unconstrained_power(unconstrained); - c.flags.set_psu_type(PsuType::TypeC); - c - }); - - // Recover if we're not in the correct state - if status.is_connected() - && let action::device::AnyState::Detached(state) = power.device_action().await - { - warn!("Power device is detached, attempting to attach"); - if let Err(e) = state.attach().await { - error!("Error attaching power device: {:?}", e); - return PdError::Failed.into(); - } - } - - if let Ok(state) = power.try_device_action::().await { - if let Err(e) = state.notify_consumer_power_capability(available_sink_contract).await { - error!("Error setting power contract: {:?}", e); - return PdError::Failed.into(); - } - } else if let Ok(state) = power.try_device_action::().await { - // Staying a consumer, but we have updated capabilities - if let Err(e) = state.notify_consumer_power_capability(available_sink_contract).await { - error!("Error setting power contract: {:?}", e); - return PdError::Failed.into(); - } - } else if let Ok(state) = power.try_device_action::().await { - // Transition from provider to consumer. - // This handles role swaps from source to sink. - let Ok(state) = state.disconnect().await else { - error!("Error disconnecting as provider"); - return PdError::Failed.into(); - }; - - if let Err(e) = state.notify_consumer_power_capability(available_sink_contract).await { - error!("Error setting power contract: {:?}", e); - return PdError::Failed.into(); - } - } else { - error!("Invalid mode"); - return PdError::InvalidMode.into(); - } - - Ok(()) - } - - /// Handle a new contract as provider - pub(super) async fn process_new_provider_contract( - &self, - power: &policy::device::Device, - status: &PortStatus, - ) -> Result<(), Error<::BusError>> { - info!("Process New provider contract"); - - let current_state = power.state().await.kind(); - info!("current power state: {:?}", current_state); - - let contract = status.available_source_contract.map(|c| { - let mut c: ProviderPowerCapability = c.into(); - c.flags.set_psu_type(PsuType::TypeC); - c - }); - - if status.power_role == PowerRole::Sink { - info!("Port is sink, not notifying of provider contract"); - return Ok(()); - } - - if let action::device::AnyState::ConnectedConsumer(state) = power.device_action().await { - info!("ConnectedConsumer"); - if let Err(e) = state.detach().await { - info!("Error detaching power device: {:?}", e); - return PdError::Failed.into(); - } - } - - // Recover if we're not in the correct state - if status.is_connected() - && let action::device::AnyState::Detached(state) = power.device_action().await - { - warn!("Power device is detached, attempting to attach"); - if let Err(e) = state.attach().await { - error!("Error attaching power device: {:?}", e); - return PdError::Failed.into(); - } - } - - if let Ok(state) = power.try_device_action::().await { - if let Some(contract) = contract - && let Err(e) = state.request_provider_power_capability(contract).await - { - error!("Error setting power contract: {:?}", e); - return PdError::Failed.into(); - } - } else if let Ok(state) = power.try_device_action::().await { - if let Some(contract) = contract { - // Staying a provider, but we have updated capabilities - if let Err(e) = state.request_provider_power_capability(contract).await { - error!("Error setting power contract: {:?}", e); - return PdError::Failed.into(); - } - } else { - // No longer need to source, so disconnect - if let Err(e) = state.disconnect().await { - error!("Error disconnecting as provider: {:?}", e); - return PdError::Failed.into(); - } - } - } else if let Ok(state) = power.try_device_action::().await { - // Transition from consumer to provider. - // This handles role swaps from sink to source. - let Ok(state) = state.disconnect().await else { - error!("Error disconnecting as consumer"); - return PdError::Failed.into(); - }; - - // If contract is none, we're no longer requesting power on this port - if let Some(contract) = contract - && let Err(e) = state.request_provider_power_capability(contract).await - { - error!("Error setting power contract: {:?}", e); - return PdError::Failed.into(); - } - } else { - error!("Invalid mode"); - return PdError::InvalidMode.into(); - } - - Ok(()) - } - - /// Handle a disconnect command - async fn process_disconnect( - &self, - port: LocalPortId, - controller: &mut C::Inner, - power: &policy::device::Device, - ) -> Result<(), Error<::BusError>> { - let state = power.state().await.kind(); - if state == StateKind::ConnectedConsumer { - info!("Port{}: Disconnect from ConnectedConsumer", port.0); - if controller.enable_sink_path(port, false).await.is_err() { - error!("Error disabling sink path"); - return PdError::Failed.into(); - } - } - - Ok(()) - } - - /// Handle a connect as provider command - fn process_connect_as_provider( - &self, - port: LocalPortId, - capability: ProviderPowerCapability, - _controller: &mut C::Inner, - ) -> Result<(), Error<::BusError>> { - info!("Port{}: Connect as provider: {:#?}", port.0, capability); - // TODO: double check explicit contract handling - Ok(()) - } - - /// Wait for a power command - /// - /// Returns (local port ID, deferred request) - /// DROP SAFETY: Call to a select over drop safe futures - pub(super) async fn wait_power_command( - &self, - ) -> ( - LocalPortId, - deferred::Request<'_, GlobalRawMutex, CommandData, InternalResponseData>, - ) { - let futures: [_; MAX_SUPPORTED_PORTS] = from_fn(|i| async move { - if let Some(device) = self.registration.power_devices.get(i) { - device.receive().await - } else { - future::pending().await - } - }); - // DROP SAFETY: Select over drop safe futures - let (request, local_id) = select_array(futures).await; - trace!("Power command: device{} {:#?}", local_id, request.command); - (LocalPortId(local_id as u8), request) - } - - /// Process a power command - /// Returns no error because this is a top-level function - pub(super) async fn process_power_command( - &self, - controller: &mut C::Inner, - state: &mut dyn DynPortState<'_>, - port: LocalPortId, - command: &CommandData, - ) -> InternalResponseData { - trace!("Processing power command: device{} {:#?}", port.0, command); - if state.controller_state().fw_update_state.in_progress() { - debug!("Port{}: Firmware update in progress", port.0); - return Err(policy::Error::Busy); - } - - let power = match self.get_power_device(port) { - Some(power) => power, - None => { - error!("Port{}: Error getting power device for port", port.0); - return Err(policy::Error::InvalidDevice); - } - }; - - match command { - policy::device::CommandData::ConnectAsConsumer(capability) => { - info!( - "Port{}: Connect as consumer: {:?}, enable input switch", - port.0, capability - ); - if controller.enable_sink_path(port, true).await.is_err() { - error!("Error enabling sink path"); - return Err(policy::Error::Failed); - } - } - policy::device::CommandData::ConnectAsProvider(capability) => { - if self.process_connect_as_provider(port, *capability, controller).is_err() { - error!("Error processing connect provider"); - return Err(policy::Error::Failed); - } - } - policy::device::CommandData::Disconnect => { - if self.process_disconnect(port, controller, power).await.is_err() { - error!("Error processing disconnect"); - return Err(policy::Error::Failed); - } - } - } - - Ok(policy::device::ResponseData::Complete) - } -} diff --git a/type-c-service/src/wrapper/vdm.rs b/type-c-service/src/wrapper/vdm.rs deleted file mode 100644 index 0eb01fa5b..000000000 --- a/type-c-service/src/wrapper/vdm.rs +++ /dev/null @@ -1,64 +0,0 @@ -use embassy_sync::blocking_mutex::raw::RawMutex; -use embedded_services::{ - sync::Lockable, - trace, - type_c::{ - controller::Controller, - event::{PortPending, VdmNotification}, - }, -}; -use embedded_usb_pd::{Error, LocalPortId, PdError}; - -use crate::wrapper::{DynPortState, message::vdm::OutputKind}; - -use super::{ControllerWrapper, FwOfferValidator, message::vdm::Output}; - -impl<'device, M: RawMutex, C: Lockable, V: FwOfferValidator> ControllerWrapper<'device, M, C, V> -where - ::Inner: Controller, -{ - /// Process a VDM event by retrieving the relevant VDM data from the `controller` for the appropriate `port`. - pub(super) async fn process_vdm_event( - &self, - controller: &mut C::Inner, - port: LocalPortId, - event: VdmNotification, - ) -> Result::BusError>> { - trace!("Processing VDM event: {:?} on port {}", event, port.0); - let kind = match event { - VdmNotification::Entered => OutputKind::Entered(controller.get_other_vdm(port).await?), - VdmNotification::Exited => OutputKind::Exited(controller.get_other_vdm(port).await?), - VdmNotification::OtherReceived => OutputKind::ReceivedOther(controller.get_other_vdm(port).await?), - VdmNotification::AttentionReceived => OutputKind::ReceivedAttn(controller.get_attn_vdm(port).await?), - }; - - Ok(Output { port, kind }) - } - - /// Finalize a VDM output by notifying the service. - pub(super) fn finalize_vdm(&self, state: &mut dyn DynPortState<'_>, output: Output) -> Result<(), PdError> { - trace!("Finalizing VDM output: {:?}", output); - let Output { port, kind } = output; - let global_port_id = self.registration.pd_controller.lookup_global_port(port)?; - let port_index = port.0 as usize; - let notification = &mut state - .port_states_mut() - .get_mut(port_index) - .ok_or(PdError::InvalidPort)? - .pending_events - .notification; - match kind { - OutputKind::Entered(_) => notification.set_custom_mode_entered(true), - OutputKind::Exited(_) => notification.set_custom_mode_exited(true), - OutputKind::ReceivedOther(_) => notification.set_custom_mode_other_vdm_received(true), - OutputKind::ReceivedAttn(_) => notification.set_custom_mode_attention_received(true), - } - - let mut pending = PortPending::none(); - pending - .pend_port(global_port_id.0 as usize) - .map_err(|_| PdError::InvalidPort)?; - self.registration.pd_controller.notify_ports(pending); - Ok(()) - } -} diff --git a/uart-service/Cargo.toml b/uart-service/Cargo.toml new file mode 100644 index 000000000..beb373559 --- /dev/null +++ b/uart-service/Cargo.toml @@ -0,0 +1,33 @@ +[package] +name = "uart-service" +version = "0.1.0" +edition = "2024" +description = "UART embedded service implementation" +repository = "https://github.com/OpenDevicePartnership/embedded-services" +rust-version = "1.88" +license = "MIT" + +[package.metadata.cargo-machete] +ignored = ["log"] + +[lints] +workspace = true + +[dependencies] +embedded-services.workspace = true +defmt = { workspace = true, optional = true } +log = { workspace = true, optional = true } +embassy-sync.workspace = true +mctp-rs = { workspace = true } +embedded-io-async.workspace = true + +[features] +default = [] +defmt = [ + "dep:defmt", + "embedded-services/defmt", + "embassy-sync/defmt", + "mctp-rs/defmt", +] + +log = ["dep:log", "embedded-services/log"] diff --git a/uart-service/src/lib.rs b/uart-service/src/lib.rs new file mode 100644 index 000000000..9f31f9866 --- /dev/null +++ b/uart-service/src/lib.rs @@ -0,0 +1,138 @@ +//! uart-service +//! +//! To keep things consistent with eSPI service, this also uses the `SmbusEspiMedium` (though not +//! strictly necessary, this helps minimize code changes on the host side when swicthing between +//! eSPI or UART). +//! +//! Revisit: Will also need to consider how to handle notifications (likely need to have user +//! provide GPIO pin we can use). +#![no_std] + +pub mod task; + +use embassy_sync::channel::Channel; +use embedded_io_async::Read as UartRead; +use embedded_io_async::Write as UartWrite; +use embedded_services::GlobalRawMutex; +use embedded_services::relay::mctp::{RelayHandler, RelayHeader, RelayResponse}; +use embedded_services::trace; +use mctp_rs::smbus_espi::SmbusEspiMedium; +use mctp_rs::smbus_espi::SmbusEspiReplyContext; + +// Should be as large as the largest possible MCTP packet and its metadata. +const BUF_SIZE: usize = 256; +const HOST_TX_QUEUE_SIZE: usize = 5; +const SMBUS_HEADER_SIZE: usize = 4; +const SMBUS_LEN_IDX: usize = 2; + +#[derive(Clone)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub(crate) struct HostResultMessage { + pub handler_service_id: R::ServiceIdType, + pub message: R::ResultEnumType, +} + +#[derive(Debug, Clone, Copy)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum Error { + /// Comms error. + Comms, + /// UART error. + Uart, + /// MCTP serialization error. + Mctp(mctp_rs::MctpPacketError), + /// Other serialization error. + Serialize(&'static str), + /// Index/slice error. + IndexSlice, + /// Buffer error. + Buffer(embedded_services::buffer::Error), +} + +pub struct Service { + host_tx_queue: Channel, HOST_TX_QUEUE_SIZE>, + relay_handler: R, +} + +impl Service { + pub fn new(relay_handler: R) -> Result { + Ok(Self { + host_tx_queue: Channel::new(), + relay_handler, + }) + } + + async fn process_response(&self, uart: &mut T, response: HostResultMessage) -> Result<(), Error> { + let mut assembly_buf = [0u8; BUF_SIZE]; + let mut mctp_ctx = mctp_rs::MctpPacketContext::new(SmbusEspiMedium, &mut assembly_buf); + + let reply_context: mctp_rs::MctpReplyContext = mctp_rs::MctpReplyContext { + source_endpoint_id: mctp_rs::EndpointId::Id(0x80), + destination_endpoint_id: mctp_rs::EndpointId::Id(response.handler_service_id.into()), + packet_sequence_number: mctp_rs::MctpSequenceNumber::new(0), + message_tag: mctp_rs::MctpMessageTag::try_from(3).map_err(Error::Serialize)?, + medium_context: SmbusEspiReplyContext { + destination_slave_address: 1, + source_slave_address: 0, + }, // Medium-specific context + }; + + let header = response.message.create_header(&response.handler_service_id); + let mut packet_state = mctp_ctx + .serialize_packet(reply_context, (header, response.message)) + .map_err(Error::Mctp)?; + + while let Some(packet_result) = packet_state.next() { + let packet = packet_result.map_err(Error::Mctp)?; + // Last byte is PEC, ignore for now + let packet = packet.get(..packet.len() - 1).ok_or(Error::IndexSlice)?; + + // Then actually send the response packet (which includes 4-byte SMBUS header containing payload size) + uart.write_all(packet).await.map_err(|_| Error::Uart)?; + } + + Ok(()) + } + + async fn wait_for_request(&self, uart: &mut T) -> Result<(), Error> { + let mut assembly_buf = [0u8; BUF_SIZE]; + let mut mctp_ctx = mctp_rs::MctpPacketContext::::new(SmbusEspiMedium, &mut assembly_buf); + + // First wait for SMBUS header, which tells us how big the incoming packet is + let mut buf = [0; BUF_SIZE]; + uart.read_exact(buf.get_mut(..SMBUS_HEADER_SIZE).ok_or(Error::IndexSlice)?) + .await + .map_err(|_| Error::Uart)?; + + // Then wait until we've received the full payload + let len = *buf.get(SMBUS_LEN_IDX).ok_or(Error::IndexSlice)? as usize; + uart.read_exact( + buf.get_mut(SMBUS_HEADER_SIZE..SMBUS_HEADER_SIZE + len) + .ok_or(Error::IndexSlice)?, + ) + .await + .map_err(|_| Error::Uart)?; + + let message = mctp_ctx + .deserialize_packet(&buf) + .map_err(Error::Mctp)? + .ok_or(Error::Serialize("Partial message not supported"))?; + + let (header, body) = message.parse_as::().map_err(Error::Mctp)?; + trace!("Received host request"); + + let response = self.relay_handler.process_request(body).await; + self.host_tx_queue + .try_send(HostResultMessage { + handler_service_id: header.get_service_id(), + message: response, + }) + .map_err(|_| Error::Comms)?; + + Ok(()) + } + + async fn wait_for_response(&self) -> HostResultMessage { + self.host_tx_queue.receive().await + } +} diff --git a/uart-service/src/task.rs b/uart-service/src/task.rs new file mode 100644 index 000000000..c5f7d59e4 --- /dev/null +++ b/uart-service/src/task.rs @@ -0,0 +1,26 @@ +use crate::{Error, Service}; +use embedded_io_async::Read as UartRead; +use embedded_io_async::Write as UartWrite; +use embedded_services::error; +use embedded_services::relay::mctp::RelayHandler; + +pub async fn uart_service( + uart_service: &Service, + mut uart: T, +) -> Result { + // Note: eSPI service uses `select!` to seemingly allow asyncrhonous `responses` from services, + // but there are concerns around async cancellation here at least for UART service. + // + // Thus this assumes services will only send messages in response to requests from the host, + // so we handle this in order. + loop { + if let Err(e) = uart_service.wait_for_request(&mut uart).await { + error!("uart-service request error: {:?}", e); + } else { + let host_msg = uart_service.wait_for_response().await; + if let Err(e) = uart_service.process_response(&mut uart, host_msg).await { + error!("uart-service response error: {:?}", e) + } + } + } +}