Repository: OpenDevicePartnership/embedded-services
Pull request #285: Create simple RPC channel
Author: login: RobertZ2011
Labels: name: cargo vet
description: PRs pending auditor review
Created: 2025-05-06T16:52:40Z | Merged: 2025-05-16T15:04:03Z

Our existing RPC message channels are implemented from scratch for each device type. These implementations generally look like:
```rust
struct Device {
    command: Channel<_, Message, 1>,
    response: Channel<_, Response, 1>,
}

impl Device {
    pub async fn execute_command(&self, command: Command) -> Response {
        self.command.send(command).await;
        self.response.recv().await
    }
}
```

This implementation is susceptible to a timing bug with multiple tasks. Imagine two tasks: A and B. Task A calls `execute_command(Command::A)` while task B calls `execute_command(Command:B)`. Imagine the following timing:

* Task A puts its command in the `command` channel, task B blocks as the channel is now full
* The RPC is executed and the response in put in the `response` channel
* Task B is unblocked and puts its command in the channel, but can immediately return because there's already a response ready
* Task B reads the response from A's command and task A will end up with the response to B's command

This PR provides a common implementation which includes locking to prevent the above from happening. This implementation also provides wrapper structs that more clearly define the separation between the invoker of an RPC and who actually handles the RPC execution. Once this PR goes in, I'll submit a follow-up PR for the power policy and type-C code and let the owners of other service implements know to migrate over to this struct.


Diff:
diff --git a/embedded-service/Cargo.lock b/embedded-service/Cargo.lock
new file mode 100644
index 000000000..e0786c30c
--- /dev/null
+++ b/embedded-service/Cargo.lock
@@ -0,0 +1,795 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 4
+
+[[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 = "autocfg"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
+
+[[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"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5deb64efa5bd81e31fcd1938615a6d98c82eafcbcd787162b6f63b91d6bac5b3"
+dependencies = [
+ "rustc_version 0.2.3",
+]
+
+[[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"
+version = "0.18.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4c7e6caee68becd795bfd65f1a026e4d00d8f0c2bc9be5eb568e1015f9ce3c34"
+dependencies = [
+ "bitfield-macros",
+]
+
+[[package]]
+name = "bitfield-macros"
+version = "0.18.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "331afbb18ce7b644c0b428726d369c5dd37ca0b815d72a459fcc2896c3c8ad32"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[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.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd"
+
+[[package]]
+name = "bytemuck"
+version = "1.22.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6b1fc10dbac614ebc03540c9dbd60e83887fda27794998c6528f1782047d540"
+
+[[package]]
+name = "byteorder"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
+
+[[package]]
+name = "cfg-if"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
+
+[[package]]
+name = "chrono"
+version = "0.4.40"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1a7964611d71df112cb1730f2ee67324fcf4d0fc6606acbbe9bfe06df124637c"
+dependencies = [
+ "num-traits",
+]
+
+[[package]]
+name = "cobs"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "67ba02a97a2bd10f4b59b25c7973101c79642302776489e030cd13cdab09ed15"
+
+[[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 = "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",
+]
+
+[[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.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "43da5946c66ffcc7745f48db692ffbb10a83bfe0afd96235c5c2a4fb23994929"
+
+[[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",
+]
+
+[[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",
+]
+
+[[package]]
+name = "defmt"
+version = "0.3.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "86f6162c53f659f65d00619fe31f14556a6e9f8752ccc4a41bd177ffcf3d6130"
+dependencies = [
+ "bitflags 1.3.2",
+ "defmt-macros",
+]
+
+[[package]]
+name = "defmt-macros"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9d135dd939bad62d7490b0002602d35b358dce5fd9233a709d3c1ef467d4bde6"
+dependencies = [
+ "defmt-parser",
+ "proc-macro-error2",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "defmt-parser"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3983b127f13995e68c1e29071e5d115cd96f215ccb5e6812e3728cd6f92653b3"
+dependencies = [
+ "thiserror",
+]
+
+[[package]]
+name = "document-features"
+version = "0.2.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "95249b50c6c185bee49034bcb378a49dc2b5dff0be90ff6616d31d64febab05d"
+dependencies = [
+ "litrs",
+]
+
+[[package]]
+name = "embassy-executor"
+version = "0.7.0"
+source = "git+https://github.com/embassy-rs/embassy#a44abaf7e4562fa5393087fd845bf0d02141325b"
+dependencies = [
+ "critical-section",
+ "defmt",
+ "document-features",
+ "embassy-executor-macros",
+ "log",
+]
+
+[[package]]
+name = "embassy-executor-macros"
+version = "0.6.2"
+source = "git+https://github.com/embassy-rs/embassy#a44abaf7e4562fa5393087fd845bf0d02141325b"
+dependencies = [
+ "darling",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "embassy-futures"
+version = "0.1.1"
+source = "git+https://github.com/embassy-rs/embassy#a44abaf7e4562fa5393087fd845bf0d02141325b"
+dependencies = [
+ "defmt",
+ "log",
+]
+
+[[package]]
+name = "embassy-sync"
+version = "0.6.2"
+source = "git+https://github.com/embassy-rs/embassy#a44abaf7e4562fa5393087fd845bf0d02141325b"
+dependencies = [
+ "cfg-if",
+ "critical-section",
+ "defmt",
+ "embedded-io-async",
+ "futures-sink",
+ "futures-util",
+ "heapless 0.8.0",
+ "log",
+]
+
+[[package]]
+name = "embassy-time"
+version = "0.4.0"
+source = "git+https://github.com/embassy-rs/embassy#a44abaf7e4562fa5393087fd845bf0d02141325b"
+dependencies = [
+ "cfg-if",
+ "critical-section",
+ "defmt",
+ "document-features",
+ "embassy-time-driver",
+ "embedded-hal 0.2.7",
+ "embedded-hal 1.0.0",
+ "embedded-hal-async",
+ "futures-util",
+ "log",
+]
+
+[[package]]
+name = "embassy-time-driver"
+version = "0.2.0"
+source = "git+https://github.com/embassy-rs/embassy#a44abaf7e4562fa5393087fd845bf0d02141325b"
+dependencies = [
+ "document-features",
+]
+
+[[package]]
+name = "embedded-cfu-protocol"
+version = "0.1.0"
+source = "git+https://github.com/OpenDevicePartnership/embedded-cfu#08fc9dd47b4102ac9e2fe9741a7691554fdc5ee1"
+dependencies = [
+ "defmt",
+ "embedded-io-async",
+ "log",
+]
+
+[[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-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-async"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3ff09972d4073aa8c299395be75161d582e7629cd663171d62af73c8d50dba3f"
+dependencies = [
+ "embedded-io",
+]
+
+[[package]]
+name = "embedded-services"
+version = "0.1.0"
+dependencies = [
+ "bitfield 0.17.0",
+ "bitflags 2.9.0",
+ "cfg-if",
+ "chrono",
+ "cortex-m",
+ "cortex-m-rt",
+ "critical-section",
+ "defmt",
+ "document-features",
+ "embassy-executor",
+ "embassy-futures",
+ "embassy-sync",
+ "embassy-time",
+ "embassy-time-driver",
+ "embedded-cfu-protocol",
+ "embedded-hal 0.2.7",
+ "embedded-hal 1.0.0",
+ "embedded-hal-async",
+ "embedded-hal-nb",
+ "embedded-io",
+ "embedded-io-async",
+ "embedded-storage",
+ "embedded-storage-async",
+ "embedded-usb-pd",
+ "fixed",
+ "heapless 0.8.0",
+ "log",
+ "nb 1.1.0",
+ "postcard",
+ "rand_core",
+ "serde",
+]
+
+[[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#d91252d11f60cb6dabb123472ce23b7a745488d8"
+dependencies = [
+ "bitfield 0.18.1",
+ "defmt",
+ "embedded-hal-async",
+]
+
+[[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 = "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 = "futures-task"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988"
+
+[[package]]
+name = "futures-util"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
+dependencies = [
+ "futures-core",
+ "futures-task",
+ "pin-project-lite",
+ "pin-utils",
+]
+
+[[package]]
+name = "half"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7db2ff139bba50379da6aa0766b52fdcb62cb5b263009b09ed58ba604e14bbd1"
+dependencies = [
+ "cfg-if",
+ "crunchy",
+]
+
+[[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 = "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 = "ident_case"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
+
+[[package]]
+name = "litrs"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b4ce301924b7887e9d637144fdade93f9dfff9b60981d4ac161db09720d39aa5"
+
+[[package]]
+name = "lock_api"
+version = "0.4.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17"
+dependencies = [
+ "autocfg",
+ "scopeguard",
+]
+
+[[package]]
+name = "log"
+version = "0.4.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94"
+
+[[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 = "num-traits"
+version = "0.2.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "pin-project-lite"
+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 = "postcard"
+version = "1.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "170a2601f67cc9dba8edd8c4870b15f71a6a2dc196daec8c83f72b59dff628a8"
+dependencies = [
+ "cobs",
+ "heapless 0.7.17",
+ "serde",
+]
+
+[[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",
+]
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.94"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84"
+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 = "rand_core"
+version = "0.6.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
+
+[[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.26",
+]
+
+[[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.26"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0"
+
+[[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",
+]
+
+[[package]]
+name = "spin"
+version = "0.9.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
+dependencies = [
+ "lock_api",
+]
+
+[[package]]
+name = "stable_deref_trait"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
+
+[[package]]
+name = "strsim"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
+
+[[package]]
+name = "syn"
+version = "2.0.100"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "thiserror"
+version = "2.0.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708"
+dependencies = [
+ "thiserror-impl",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "2.0.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[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 = "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",
+]
diff --git a/embedded-service/src/ipc/deferred.rs b/embedded-service/src/ipc/deferred.rs
new file mode 100644
index 000000000..4351d321e
--- /dev/null
+++ b/embedded-service/src/ipc/deferred.rs
@@ -0,0 +1,99 @@
+//! Definitions for deferred execution of commands
+use core::sync::atomic::{AtomicUsize, Ordering};
+
+use crate::debug;
+use embassy_sync::{blocking_mutex::raw::RawMutex, mutex::Mutex, signal::Signal};
+
+/// A unique identifier for a particular command invocation
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+#[cfg_attr(feature = "defmt", derive(defmt::Format))]
+struct RequestId(usize);
+
+/// A simple channel for executing deferred commands.
+///
+/// This implementation provides synchronization for command invocations
+/// and ensures that responses are sent back to the correct sender
+/// using a unique invocation ID.
+pub struct Channel<M: RawMutex, C, R> {
+    /// Signal for sending commands
+    command: Signal<M, (C, RequestId)>,
+    /// Signal for receiving responses
+    response: Signal<M, (R, RequestId)>,
+    /// Mutex for synchronizing access to command invocation
+    request_lock: Mutex<M, ()>,
+    /// Unique ID for the next invocation
+    next_request_id: AtomicUsize,
+}
+
+impl<M: RawMutex, C, R> Channel<M, C, R> {
+    /// Create a new channel
+    pub const fn new() -> Self {
+        Self {
+            command: Signal::new(),
+            response: Signal::new(),
+            request_lock: Mutex::new(()),
+            next_request_id: AtomicUsize::new(0),
+        }
+    }
+
+    /// Get the next request ID
+    fn get_next_request_id(&self) -> RequestId {
+        let id = self.next_request_id.fetch_add(1, Ordering::SeqCst);
+        RequestId(id)
+    }
+
+    /// Send a command and return the response
+    /// This locks to ensure that commands are executed atomically
+    pub async fn execute(&self, command: C) -> R {
+        let _guard = self.request_lock.lock().await;
+        let request_id = self.get_next_request_id();
+        self.command.signal((command, request_id));
+        loop {
+            // Wait until we receive a response for out particular request
+            let (response, id) = self.response.wait().await;
+            if id == request_id {
+                return response;
+            } else {
+                // Not an error because this is the expected behavior in certain cases,
+                // particularly if the sender times out before the response is received.
+                debug!("Received response for different invocation: {}", id.0);
+            }
+        }
+    }
+
+    /// Wait for an invocation
+    pub async fn receive(&self) -> Request<'_, M, C, R> {
+        let (command, request_id) = self.command.wait().await;
+        Request {
+            channel: self,
+            request_id,
+            command,
+        }
+    }
+}
+
+impl<M: RawMutex, C, R> Default for Channel<M, C, R> {
+    /// Default implementation
+    fn default() -> Self {
+        Self::new()
+    }
+}
+
+/// A specific request
+pub struct Request<'a, M: RawMutex, C, R> {
+    /// The channel this invocation came from
+    channel: &'a Channel<M, C, R>,
+    /// Request ID
+    request_id: RequestId,
+    /// Command to execute
+    pub command: C,
+}
+
+impl<M: RawMutex, C, R> Request<'_, M, C, R> {
+    /// Send a response to the command, consuming the command in the process.
+    ///
+    /// Consuming the command ensures each command may only be responded to once.
+    pub fn respond(self, response: R) {
+        self.channel.response.signal((response, self.request_id));
+    }
+}
diff --git a/embedded-service/src/ipc/mod.rs b/embedded-service/src/ipc/mod.rs
new file mode 100644
index 000000000..3f6d20ce9
--- /dev/null
+++ b/embedded-service/src/ipc/mod.rs
@@ -0,0 +1,2 @@
+//! IPC related definitions
+pub mod deferred;
diff --git a/embedded-service/src/lib.rs b/embedded-service/src/lib.rs
index 85f31bd2b..0444f1be2 100644
--- a/embedded-service/src/lib.rs
+++ b/embedded-service/src/lib.rs
@@ -15,6 +15,7 @@ pub mod ec_type;
 pub mod fmt;
 pub mod hid;
 pub mod init;
+pub mod ipc;
 pub mod keyboard;
 pub mod power;
 pub mod type_c;


Reviews:
user: login: JamesHuard
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-06T18:28:11Z
user: login: JamesHuard
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-06T18:30:00Z
user: login: JamesHuard
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-06T18:31:06Z
user: login: JamesHuard
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-06T18:32:44Z
user: login: JamesHuard
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-06T18:33:55Z
user: login: JamesHuard
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-06T18:35:43Z
user: login: asasine
state: DISMISSED
author_association: CONTRIBUTOR
submitted_at: 2025-05-06T18:39:20Z
user: login: RobertZ2011
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-06T19:07:05Z
user: login: RobertZ2011
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-06T19:41:42Z
user: login: RobertZ2011
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-06T19:41:52Z
user: login: RobertZ2011
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-06T19:44:14Z
user: login: RobertZ2011
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-06T19:45:37Z
user: login: RobertZ2011
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-06T19:45:54Z
user: login: RobertZ2011
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-06T19:47:47Z
user: login: RobertZ2011
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-06T19:49:07Z
user: login: asasine
state: DISMISSED
author_association: CONTRIBUTOR
submitted_at: 2025-05-07T13:12:00Z
user: login: JamesHuard
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-07T15:12:07Z
user: login: JamesHuard
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-07T15:16:28Z
user: login: JamesHuard
state: DISMISSED
author_association: CONTRIBUTOR
submitted_at: 2025-05-07T15:16:39Z
user: login: felipebalbi
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-09T14:41:31Z
user: login: RobertZ2011
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-09T17:36:12Z
user: login: RobertZ2011
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-09T17:39:14Z
user: login: asasine
body: Looks good to me.

Bikeshedding: I don't like the invoker/invocation name; it's too wordy imo and requires too much new mental chewing to grasp since it doesn't resemble existing synchronization primitives. What about sticking closer to existing nomenclature for channels or signals, which this is behaving like?
- `invoke()` -> `send()`
- `Invocation` -> `Message` (most conventional) or `Request` or `Command`
- `wait_invocation()` -> `receive()` or `wait()` or `get()`
- `send_response()` -> `respond()` or `send()`

I also think this is completely unit testable and would like to see them added.
state: DISMISSED
author_association: CONTRIBUTOR
submitted_at: 2025-05-14T16:23:36Z
user: login: RobertZ2011
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-14T23:47:02Z
user: login: RobertZ2011
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-14T23:47:08Z
user: login: RobertZ2011
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-14T23:47:16Z
user: login: RobertZ2011
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-14T23:47:23Z
user: login: RobertZ2011
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-14T23:47:31Z
user: login: RobertZ2011
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-14T23:47:44Z
user: login: asasine
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-15T16:03:31Z
user: login: RobertZ2011
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-15T18:26:44Z
user: login: asasine
state: DISMISSED
author_association: CONTRIBUTOR
submitted_at: 2025-05-15T18:50:52Z
user: login: asasine
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-16T14:15:27Z
user: login: JamesHuard
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-16T15:04:01Z

Review comments:
diff_hunk: @@ -0,0 +1,83 @@
+//! Definitions for IPC
+use core::sync::atomic::AtomicBool;
+
+use embassy_sync::{blocking_mutex::raw::RawMutex, channel::Channel, mutex::Mutex};
+
+/// A simple RPC channel for executing commands
+pub struct RpcChannel<M: RawMutex, C, R> {
+    /// Channel for sending commands
+    command: Channel<M, C, 1>,
+    /// Channel for receiving responses
+    response: Channel<M, R, 1>,
+    /// Mutex for synchronizing access to command execution
+    execution_lock: Mutex<M, ()>,
+    handler_created: AtomicBool,
+}
+
+impl<M: RawMutex, C, R> RpcChannel<M, C, R> {
+    /// Create a new RPC channel
+    pub fn new() -> Self {
+        Self {
+            command: Channel::new(),
+            response: Channel::new(),
+            execution_lock: Mutex::new(()),
+            handler_created: AtomicBool::new(false),
+        }
+    }
+
+    /// Create an invoker object that can be used to invoke commands
+    pub fn invoker(&self) -> RpcInvoker<M, C, R> {
+        RpcInvoker::new(self)
+    }
+
+    /// Create a handler object that can be used to execute commands generated by an invoker
+    /// Only one handler can be created
+    pub fn handler(&self) -> Option<RpcHandler<M, C, R>> {
+        if self.handler_created.swap(true, core::sync::atomic::Ordering::SeqCst) {
+            return None;
+        }
+        Some(RpcHandler::new(self))
+    }
+}
+
+/// Invoker object for invoking commands
path: embedded-service/src/ipc/rpc.rs
user: login: asasine
body: bikeshedding: what do you think of naming this "client" instead?
created_at: 2025-05-06T18:11:34Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,83 @@
+//! Definitions for IPC
+use core::sync::atomic::AtomicBool;
+
+use embassy_sync::{blocking_mutex::raw::RawMutex, channel::Channel, mutex::Mutex};
+
+/// A simple RPC channel for executing commands
path: embedded-service/src/ipc/rpc.rs
user: login: asasine
body: bikeshedding: "RPC" makes me think it's going off-device when this is really just IPC. Another name could be simply "message handler" 
created_at: 2025-05-06T18:12:53Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,83 @@
+//! Definitions for IPC
+use core::sync::atomic::AtomicBool;
+
+use embassy_sync::{blocking_mutex::raw::RawMutex, channel::Channel, mutex::Mutex};
+
+/// A simple RPC channel for executing commands
+pub struct RpcChannel<M: RawMutex, C, R> {
+    /// Channel for sending commands
+    command: Channel<M, C, 1>,
+    /// Channel for receiving responses
+    response: Channel<M, R, 1>,
+    /// Mutex for synchronizing access to command execution
+    execution_lock: Mutex<M, ()>,
+    handler_created: AtomicBool,
+}
+
+impl<M: RawMutex, C, R> RpcChannel<M, C, R> {
+    /// Create a new RPC channel
+    pub fn new() -> Self {
+        Self {
+            command: Channel::new(),
+            response: Channel::new(),
+            execution_lock: Mutex::new(()),
+            handler_created: AtomicBool::new(false),
+        }
+    }
+
+    /// Create an invoker object that can be used to invoke commands
+    pub fn invoker(&self) -> RpcInvoker<M, C, R> {
+        RpcInvoker::new(self)
+    }
+
+    /// Create a handler object that can be used to execute commands generated by an invoker
+    /// Only one handler can be created
path: embedded-service/src/ipc/rpc.rs
user: login: asasine
body: Why is this? `embassy_sync::Channel` is MPMC
created_at: 2025-05-06T18:16:56Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,83 @@
+//! Definitions for IPC
+use core::sync::atomic::AtomicBool;
+
+use embassy_sync::{blocking_mutex::raw::RawMutex, channel::Channel, mutex::Mutex};
+
+/// A simple RPC channel for executing commands
+pub struct RpcChannel<M: RawMutex, C, R> {
+    /// Channel for sending commands
+    command: Channel<M, C, 1>,
+    /// Channel for receiving responses
+    response: Channel<M, R, 1>,
+    /// Mutex for synchronizing access to command execution
+    execution_lock: Mutex<M, ()>,
+    handler_created: AtomicBool,
+}
+
+impl<M: RawMutex, C, R> RpcChannel<M, C, R> {
+    /// Create a new RPC channel
+    pub fn new() -> Self {
path: embedded-service/src/ipc/rpc.rs
user: login: JamesHuard
body: Can you mark this const?
created_at: 2025-05-06T18:28:11Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,83 @@
+//! Definitions for IPC
+use core::sync::atomic::AtomicBool;
+
+use embassy_sync::{blocking_mutex::raw::RawMutex, channel::Channel, mutex::Mutex};
+
+/// A simple RPC channel for executing commands
+pub struct RpcChannel<M: RawMutex, C, R> {
path: embedded-service/src/ipc/rpc.rs
user: login: JamesHuard
body: I'm going to nitpick calling this "RPC". It does look a lot like an RPC call. But the R in "RPC" typically stands for Remote, as in an external component, typically at the other end of a network.

In our internal documentation we call this pattern the "Deferred Caller" pattern. 

This is a nitpick. 
created_at: 2025-05-06T18:30:00Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,83 @@
+//! Definitions for IPC
+use core::sync::atomic::AtomicBool;
+
+use embassy_sync::{blocking_mutex::raw::RawMutex, channel::Channel, mutex::Mutex};
+
+/// A simple RPC channel for executing commands
+pub struct RpcChannel<M: RawMutex, C, R> {
+    /// Channel for sending commands
+    command: Channel<M, C, 1>,
+    /// Channel for receiving responses
+    response: Channel<M, R, 1>,
+    /// Mutex for synchronizing access to command execution
+    execution_lock: Mutex<M, ()>,
+    handler_created: AtomicBool,
+}
+
+impl<M: RawMutex, C, R> RpcChannel<M, C, R> {
+    /// Create a new RPC channel
+    pub fn new() -> Self {
+        Self {
+            command: Channel::new(),
+            response: Channel::new(),
+            execution_lock: Mutex::new(()),
+            handler_created: AtomicBool::new(false),
+        }
+    }
+
+    /// Create an invoker object that can be used to invoke commands
+    pub fn invoker(&self) -> RpcInvoker<M, C, R> {
+        RpcInvoker::new(self)
+    }
+
+    /// Create a handler object that can be used to execute commands generated by an invoker
+    /// Only one handler can be created
+    pub fn handler(&self) -> Option<RpcHandler<M, C, R>> {
+        if self.handler_created.swap(true, core::sync::atomic::Ordering::SeqCst) {
+            return None;
+        }
+        Some(RpcHandler::new(self))
+    }
+}
+
+/// Invoker object for invoking commands
+pub struct RpcInvoker<'a, M: RawMutex, C, R> {
+    inner: &'a RpcChannel<M, C, R>,
+}
+
+impl<'a, M: RawMutex, C, R> RpcInvoker<'a, M, C, R> {
+    /// Create a new invoker object
+    fn new(rpc: &'a RpcChannel<M, C, R>) -> Self {
+        Self { inner: rpc }
+    }
+
+    /// Invoke a command and return the response
+    /// This locks to ensure that commands are executed atomically
+    pub async fn invoke(&self, command: C) -> R {
+        let _guard = self.inner.execution_lock.lock().await;
+        self.inner.command.send(command).await;
+        self.inner.response.receive().await
+    }
+}
+
+/// Handler object for executing commands generated by an invoker
+pub struct RpcHandler<'a, M: RawMutex, C, R> {
+    inner: &'a RpcChannel<M, C, R>,
+}
+
+impl<'a, M: RawMutex, C, R> RpcHandler<'a, M, C, R> {
+    /// Create a new handler object
+    fn new(rpc: &'a RpcChannel<M, C, R>) -> Self {
+        Self { inner: rpc }
+    }
+
+    /// Wait for a command
+    pub async fn wait_command(&self) -> C {
path: embedded-service/src/ipc/rpc.rs
user: login: JamesHuard
body: So we were able to actually guarantee that the wait_command is ended with a send_response in our code by using a FnOnce that returns a Future. I think that might be worth exploring as an alternative here, so that it doesn't result in potential hung systems
created_at: 2025-05-06T18:31:06Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,83 @@
+//! Definitions for IPC
+use core::sync::atomic::AtomicBool;
+
+use embassy_sync::{blocking_mutex::raw::RawMutex, channel::Channel, mutex::Mutex};
+
+/// A simple RPC channel for executing commands
+pub struct RpcChannel<M: RawMutex, C, R> {
+    /// Channel for sending commands
+    command: Channel<M, C, 1>,
+    /// Channel for receiving responses
+    response: Channel<M, R, 1>,
+    /// Mutex for synchronizing access to command execution
+    execution_lock: Mutex<M, ()>,
+    handler_created: AtomicBool,
+}
+
+impl<M: RawMutex, C, R> RpcChannel<M, C, R> {
+    /// Create a new RPC channel
+    pub fn new() -> Self {
+        Self {
+            command: Channel::new(),
+            response: Channel::new(),
+            execution_lock: Mutex::new(()),
+            handler_created: AtomicBool::new(false),
+        }
+    }
+
+    /// Create an invoker object that can be used to invoke commands
+    pub fn invoker(&self) -> RpcInvoker<M, C, R> {
+        RpcInvoker::new(self)
+    }
+
+    /// Create a handler object that can be used to execute commands generated by an invoker
+    /// Only one handler can be created
+    pub fn handler(&self) -> Option<RpcHandler<M, C, R>> {
+        if self.handler_created.swap(true, core::sync::atomic::Ordering::SeqCst) {
+            return None;
+        }
+        Some(RpcHandler::new(self))
+    }
+}
+
+/// Invoker object for invoking commands
+pub struct RpcInvoker<'a, M: RawMutex, C, R> {
+    inner: &'a RpcChannel<M, C, R>,
+}
+
+impl<'a, M: RawMutex, C, R> RpcInvoker<'a, M, C, R> {
+    /// Create a new invoker object
+    fn new(rpc: &'a RpcChannel<M, C, R>) -> Self {
+        Self { inner: rpc }
+    }
+
+    /// Invoke a command and return the response
+    /// This locks to ensure that commands are executed atomically
+    pub async fn invoke(&self, command: C) -> R {
+        let _guard = self.inner.execution_lock.lock().await;
+        self.inner.command.send(command).await;
+        self.inner.response.receive().await
+    }
+}
+
+/// Handler object for executing commands generated by an invoker
+pub struct RpcHandler<'a, M: RawMutex, C, R> {
+    inner: &'a RpcChannel<M, C, R>,
+}
+
+impl<'a, M: RawMutex, C, R> RpcHandler<'a, M, C, R> {
+    /// Create a new handler object
+    fn new(rpc: &'a RpcChannel<M, C, R>) -> Self {
+        Self { inner: rpc }
+    }
+
+    /// Wait for a command
+    pub async fn wait_command(&self) -> C {
path: embedded-service/src/ipc/rpc.rs
user: login: JamesHuard
body: On line 39 here: https://github.com/OpenDevicePartnership/embedded-services/blob/main/platform-service/src/reset.rs is what I'm talking about:
![image](https://github.com/user-attachments/assets/47489c54-a78c-4574-b4fa-930a6859b004)

created_at: 2025-05-06T18:32:44Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,83 @@
+//! Definitions for IPC
+use core::sync::atomic::AtomicBool;
+
+use embassy_sync::{blocking_mutex::raw::RawMutex, channel::Channel, mutex::Mutex};
+
+/// A simple RPC channel for executing commands
path: embedded-service/src/ipc/rpc.rs
user: login: JamesHuard
body: It might be worth adding an additional type here that wraps the channel, perhaps BroadcastableDeferredCaller or some such, that includes an intrusive list and a registerable node counter part
created_at: 2025-05-06T18:33:55Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,109 @@
+use embassy_executor::{Executor, Spawner};
+use embassy_sync::blocking_mutex::raw::NoopRawMutex;
+use embassy_sync::once_lock::OnceLock;
+use embedded_services::ipc::RpcChannel;
+use log::*;
+use static_cell::StaticCell;
+
+/// Mock commands
+#[derive(Debug)]
+enum Command {
+    A,
+    B,
+}
+
+/// Mock responses
+#[derive(Debug)]
+enum Response {
+    A,
+    B,
+}
+
+/// Mock device
+struct Device {
+    rpc: RpcChannel<NoopRawMutex, Command, Response>,
+}
+
+impl Device {
+    /// Create a new device
+    fn new() -> Self {
+        Self { rpc: RpcChannel::new() }
+    }
+
+    /// Process a command and return a response
+    async fn process_request(&self, request: Command) -> Response {
+        match request {
+            Command::A => {
+                info!("Processing request A");
+                Response::A
+            }
+            Command::B => {
+                info!("Processing request B");
+                Response::B
+            }
+        }
+    }
+
+    /// Invoke command A
+    async fn invoke_a(&self) -> Response {
+        info!("Requesting A");
+        self.rpc.invoker().invoke(Command::A).await
+    }
+
+    /// Invoke command B
+    async fn invoke_b(&self) -> Response {
+        info!("Requesting B");
+        self.rpc.invoker().invoke(Command::B).await
+    }
+}
+
+/// Task that executes command A
+#[embassy_executor::task]
+async fn task_a(device: &'static Device) {
+    info!("Task A started");
+    let response = device.invoke_a().await;
+    info!("Task A received response: {:?}", response);
+}
+
+/// Task that executes command B
+#[embassy_executor::task]
+async fn task_b(device: &'static Device) {
+    info!("Task B started");
+    let response = device.invoke_b().await;
+    info!("Task B received response: {:?}", response);
+}
+
+/// Task that handles device commands
+#[embassy_executor::task]
+async fn device_task(device: &'static Device) {
+    info!("Device task started");
+
+    let handler = device.rpc.handler().unwrap();
+    loop {
+        let command = handler.wait_command().await;
+        let response = device.process_request(command).await;
+        handler.send_response(response).await;
+    }
+}
+
+/// Main task that initializes the device and spawns other tasks
+#[embassy_executor::task]
+async fn task(spawner: Spawner) {
+    static DEVICE: OnceLock<Device> = OnceLock::new();
+
+    let device = DEVICE.get_or_init(Device::new);
path: examples/std/src/bin/rpc_basic.rs
user: login: JamesHuard
body: I'm getting a little turned around with what we're calling a "device" here versus the tasks that process the invoked requests. Could we maybe use a more directionally-explicit language, like "Broker/Operator" or "Delegator/Delegate" etc., just to be clear around intended use?
created_at: 2025-05-06T18:35:43Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,83 @@
+//! Definitions for IPC
+use core::sync::atomic::AtomicBool;
+
+use embassy_sync::{blocking_mutex::raw::RawMutex, channel::Channel, mutex::Mutex};
+
+/// A simple RPC channel for executing commands
path: embedded-service/src/ipc/rpc.rs
user: login: RobertZ2011
body: Would the response be what's broadcast or were you thinking something else?
created_at: 2025-05-06T19:07:05Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,83 @@
+//! Definitions for IPC
+use core::sync::atomic::AtomicBool;
+
+use embassy_sync::{blocking_mutex::raw::RawMutex, channel::Channel, mutex::Mutex};
+
+/// A simple RPC channel for executing commands
+pub struct RpcChannel<M: RawMutex, C, R> {
+    /// Channel for sending commands
+    command: Channel<M, C, 1>,
+    /// Channel for receiving responses
+    response: Channel<M, R, 1>,
+    /// Mutex for synchronizing access to command execution
+    execution_lock: Mutex<M, ()>,
+    handler_created: AtomicBool,
+}
+
+impl<M: RawMutex, C, R> RpcChannel<M, C, R> {
+    /// Create a new RPC channel
+    pub fn new() -> Self {
path: embedded-service/src/ipc/rpc.rs
user: login: RobertZ2011
body: Done.
created_at: 2025-05-06T19:41:42Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,83 @@
+//! Definitions for IPC
+use core::sync::atomic::AtomicBool;
+
+use embassy_sync::{blocking_mutex::raw::RawMutex, channel::Channel, mutex::Mutex};
+
+/// A simple RPC channel for executing commands
+pub struct RpcChannel<M: RawMutex, C, R> {
path: embedded-service/src/ipc/rpc.rs
user: login: RobertZ2011
body: Renamed to deferred.
created_at: 2025-05-06T19:41:52Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,83 @@
+//! Definitions for IPC
+use core::sync::atomic::AtomicBool;
+
+use embassy_sync::{blocking_mutex::raw::RawMutex, channel::Channel, mutex::Mutex};
+
+/// A simple RPC channel for executing commands
+pub struct RpcChannel<M: RawMutex, C, R> {
+    /// Channel for sending commands
+    command: Channel<M, C, 1>,
+    /// Channel for receiving responses
+    response: Channel<M, R, 1>,
+    /// Mutex for synchronizing access to command execution
+    execution_lock: Mutex<M, ()>,
+    handler_created: AtomicBool,
+}
+
+impl<M: RawMutex, C, R> RpcChannel<M, C, R> {
+    /// Create a new RPC channel
+    pub fn new() -> Self {
+        Self {
+            command: Channel::new(),
+            response: Channel::new(),
+            execution_lock: Mutex::new(()),
+            handler_created: AtomicBool::new(false),
+        }
+    }
+
+    /// Create an invoker object that can be used to invoke commands
+    pub fn invoker(&self) -> RpcInvoker<M, C, R> {
+        RpcInvoker::new(self)
+    }
+
+    /// Create a handler object that can be used to execute commands generated by an invoker
+    /// Only one handler can be created
+    pub fn handler(&self) -> Option<RpcHandler<M, C, R>> {
+        if self.handler_created.swap(true, core::sync::atomic::Ordering::SeqCst) {
+            return None;
+        }
+        Some(RpcHandler::new(self))
+    }
+}
+
+/// Invoker object for invoking commands
+pub struct RpcInvoker<'a, M: RawMutex, C, R> {
+    inner: &'a RpcChannel<M, C, R>,
+}
+
+impl<'a, M: RawMutex, C, R> RpcInvoker<'a, M, C, R> {
+    /// Create a new invoker object
+    fn new(rpc: &'a RpcChannel<M, C, R>) -> Self {
+        Self { inner: rpc }
+    }
+
+    /// Invoke a command and return the response
+    /// This locks to ensure that commands are executed atomically
+    pub async fn invoke(&self, command: C) -> R {
+        let _guard = self.inner.execution_lock.lock().await;
+        self.inner.command.send(command).await;
+        self.inner.response.receive().await
+    }
+}
+
+/// Handler object for executing commands generated by an invoker
+pub struct RpcHandler<'a, M: RawMutex, C, R> {
+    inner: &'a RpcChannel<M, C, R>,
+}
+
+impl<'a, M: RawMutex, C, R> RpcHandler<'a, M, C, R> {
+    /// Create a new handler object
+    fn new(rpc: &'a RpcChannel<M, C, R>) -> Self {
+        Self { inner: rpc }
+    }
+
+    /// Wait for a command
+    pub async fn wait_command(&self) -> C {
path: embedded-service/src/ipc/rpc.rs
user: login: RobertZ2011
body: Done. I added in a similar function but left the existing ones as well. Access to those will be needed for composing things together but that's something we should be able to mostly hide with something like the transformers work.
created_at: 2025-05-06T19:44:14Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,83 @@
+//! Definitions for IPC
+use core::sync::atomic::AtomicBool;
+
+use embassy_sync::{blocking_mutex::raw::RawMutex, channel::Channel, mutex::Mutex};
+
+/// A simple RPC channel for executing commands
+pub struct RpcChannel<M: RawMutex, C, R> {
+    /// Channel for sending commands
+    command: Channel<M, C, 1>,
+    /// Channel for receiving responses
+    response: Channel<M, R, 1>,
+    /// Mutex for synchronizing access to command execution
+    execution_lock: Mutex<M, ()>,
+    handler_created: AtomicBool,
+}
+
+impl<M: RawMutex, C, R> RpcChannel<M, C, R> {
+    /// Create a new RPC channel
+    pub fn new() -> Self {
+        Self {
+            command: Channel::new(),
+            response: Channel::new(),
+            execution_lock: Mutex::new(()),
+            handler_created: AtomicBool::new(false),
+        }
+    }
+
+    /// Create an invoker object that can be used to invoke commands
+    pub fn invoker(&self) -> RpcInvoker<M, C, R> {
+        RpcInvoker::new(self)
+    }
+
+    /// Create a handler object that can be used to execute commands generated by an invoker
+    /// Only one handler can be created
+    pub fn handler(&self) -> Option<RpcHandler<M, C, R>> {
+        if self.handler_created.swap(true, core::sync::atomic::Ordering::SeqCst) {
+            return None;
+        }
+        Some(RpcHandler::new(self))
+    }
+}
+
+/// Invoker object for invoking commands
path: embedded-service/src/ipc/rpc.rs
user: login: RobertZ2011
body: I'm open to changing it, but I think that client has the same issue as RPC in that it makes me think of a networked architecture.
created_at: 2025-05-06T19:45:37Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,83 @@
+//! Definitions for IPC
+use core::sync::atomic::AtomicBool;
+
+use embassy_sync::{blocking_mutex::raw::RawMutex, channel::Channel, mutex::Mutex};
+
+/// A simple RPC channel for executing commands
path: embedded-service/src/ipc/rpc.rs
user: login: RobertZ2011
body: Done, renamed to deferred per Jimi's comment.
created_at: 2025-05-06T19:45:54Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,83 @@
+//! Definitions for IPC
+use core::sync::atomic::AtomicBool;
+
+use embassy_sync::{blocking_mutex::raw::RawMutex, channel::Channel, mutex::Mutex};
+
+/// A simple RPC channel for executing commands
+pub struct RpcChannel<M: RawMutex, C, R> {
+    /// Channel for sending commands
+    command: Channel<M, C, 1>,
+    /// Channel for receiving responses
+    response: Channel<M, R, 1>,
+    /// Mutex for synchronizing access to command execution
+    execution_lock: Mutex<M, ()>,
+    handler_created: AtomicBool,
+}
+
+impl<M: RawMutex, C, R> RpcChannel<M, C, R> {
+    /// Create a new RPC channel
+    pub fn new() -> Self {
+        Self {
+            command: Channel::new(),
+            response: Channel::new(),
+            execution_lock: Mutex::new(()),
+            handler_created: AtomicBool::new(false),
+        }
+    }
+
+    /// Create an invoker object that can be used to invoke commands
+    pub fn invoker(&self) -> RpcInvoker<M, C, R> {
+        RpcInvoker::new(self)
+    }
+
+    /// Create a handler object that can be used to execute commands generated by an invoker
+    /// Only one handler can be created
path: embedded-service/src/ipc/rpc.rs
user: login: RobertZ2011
body: Done. The original motivation was to more closely match existing code that focuses on only having a single task that processes command. But I think it's a better idea to make this more generic and users of this can decide how they want to control that.
created_at: 2025-05-06T19:47:47Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,109 @@
+use embassy_executor::{Executor, Spawner};
+use embassy_sync::blocking_mutex::raw::NoopRawMutex;
+use embassy_sync::once_lock::OnceLock;
+use embedded_services::ipc::RpcChannel;
+use log::*;
+use static_cell::StaticCell;
+
+/// Mock commands
+#[derive(Debug)]
+enum Command {
+    A,
+    B,
+}
+
+/// Mock responses
+#[derive(Debug)]
+enum Response {
+    A,
+    B,
+}
+
+/// Mock device
+struct Device {
+    rpc: RpcChannel<NoopRawMutex, Command, Response>,
+}
+
+impl Device {
+    /// Create a new device
+    fn new() -> Self {
+        Self { rpc: RpcChannel::new() }
+    }
+
+    /// Process a command and return a response
+    async fn process_request(&self, request: Command) -> Response {
+        match request {
+            Command::A => {
+                info!("Processing request A");
+                Response::A
+            }
+            Command::B => {
+                info!("Processing request B");
+                Response::B
+            }
+        }
+    }
+
+    /// Invoke command A
+    async fn invoke_a(&self) -> Response {
+        info!("Requesting A");
+        self.rpc.invoker().invoke(Command::A).await
+    }
+
+    /// Invoke command B
+    async fn invoke_b(&self) -> Response {
+        info!("Requesting B");
+        self.rpc.invoker().invoke(Command::B).await
+    }
+}
+
+/// Task that executes command A
+#[embassy_executor::task]
+async fn task_a(device: &'static Device) {
+    info!("Task A started");
+    let response = device.invoke_a().await;
+    info!("Task A received response: {:?}", response);
+}
+
+/// Task that executes command B
+#[embassy_executor::task]
+async fn task_b(device: &'static Device) {
+    info!("Task B started");
+    let response = device.invoke_b().await;
+    info!("Task B received response: {:?}", response);
+}
+
+/// Task that handles device commands
+#[embassy_executor::task]
+async fn device_task(device: &'static Device) {
+    info!("Device task started");
+
+    let handler = device.rpc.handler().unwrap();
+    loop {
+        let command = handler.wait_command().await;
+        let response = device.process_request(command).await;
+        handler.send_response(response).await;
+    }
+}
+
+/// Main task that initializes the device and spawns other tasks
+#[embassy_executor::task]
+async fn task(spawner: Spawner) {
+    static DEVICE: OnceLock<Device> = OnceLock::new();
+
+    let device = DEVICE.get_or_init(Device::new);
path: examples/std/src/bin/rpc_basic.rs
user: login: RobertZ2011
body: Done. I renamed things to follow the handler/invoke nomenclature present in the library code. Hopefully that clears things up a bit.
created_at: 2025-05-06T19:49:07Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,84 @@
+//! Definitions for deferred execution of commands
+use core::future::Future;
+
+use embassy_sync::{blocking_mutex::raw::RawMutex, channel::Channel as EmbassyChannel, mutex::Mutex};
+
+/// A simple channel for executing deferred commands
+pub struct Channel<M: RawMutex, C, R> {
+    /// Channel for sending commands
+    command: EmbassyChannel<M, C, 1>,
+    /// Channel for receiving responses
+    response: EmbassyChannel<M, R, 1>,
+    /// Mutex for synchronizing access to command execution
+    execution_lock: Mutex<M, ()>,
+}
+
+impl<M: RawMutex, C, R> Channel<M, C, R> {
+    /// Create a new channel
+    pub const fn new() -> Self {
+        Self {
+            command: EmbassyChannel::new(),
+            response: EmbassyChannel::new(),
+            execution_lock: Mutex::new(()),
+        }
+    }
+
+    /// Create an invoker object that can be used to invoke commands
+    pub fn invoker(&self) -> Invoker<M, C, R> {
+        Invoker::new(self)
+    }
+
+    /// Create a handler object that can be used to execute commands generated by an invoker
+    pub fn handler(&self) -> Handler<M, C, R> {
+        Handler::new(self)
+    }
+}
+
+/// Invoker object for invoking commands
+pub struct Invoker<'a, M: RawMutex, C, R> {
+    inner: &'a Channel<M, C, R>,
+}
+
+impl<'a, M: RawMutex, C, R> Invoker<'a, M, C, R> {
+    /// Create a new invoker object
+    fn new(inner: &'a Channel<M, C, R>) -> Self {
+        Self { inner }
+    }
+
+    /// Invoke a command and return the response
+    /// This locks to ensure that commands are executed atomically
+    pub async fn invoke(&self, command: C) -> R {
+        let _guard = self.inner.execution_lock.lock().await;
+        self.inner.command.send(command).await;
+        self.inner.response.receive().await
+    }
+}
+
+/// Handler object for executing commands generated by an invoker
+pub struct Handler<'a, M: RawMutex, C, R> {
+    inner: &'a Channel<M, C, R>,
+}
+
+impl<'a, M: RawMutex, C, R> Handler<'a, M, C, R> {
+    /// Create a new handler object
+    fn new(inner: &'a Channel<M, C, R>) -> Self {
+        Self { inner }
+    }
+
+    /// Wait for a command
+    pub async fn wait_command(&self) -> C {
+        self.inner.command.receive().await
+    }
+
+    /// Send a response to a command
+    pub async fn send_response(&self, response: R) {
+        self.inner.response.send(response).await;
+    }
+
+    /// Process a command and send a response
+    pub async fn process_command<Fut: Future<Output = R>, F: FnOnce(C) -> Fut>(&self, process: F) {
+        let command = self.wait_command().await;
+        let response = process(command).await;
+        self.send_response(response).await;
+    }
path: embedded-service/src/ipc/deferred.rs
user: login: asasine
body: This isn't used, and I think the overall design may be too opinionated. IMO best to remove it for now
created_at: 2025-05-07T13:11:39Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,83 @@
+//! Definitions for IPC
+use core::sync::atomic::AtomicBool;
+
+use embassy_sync::{blocking_mutex::raw::RawMutex, channel::Channel, mutex::Mutex};
+
+/// A simple RPC channel for executing commands
path: embedded-service/src/ipc/rpc.rs
user: login: JamesHuard
body: I was thinking the "command" is what's broadcast, and responses would need to be aggregated or () in those cases
created_at: 2025-05-07T15:12:07Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,84 @@
+//! Definitions for deferred execution of commands
+use core::future::Future;
+
+use embassy_sync::{blocking_mutex::raw::RawMutex, channel::Channel as EmbassyChannel, mutex::Mutex};
+
+/// A simple channel for executing deferred commands
+pub struct Channel<M: RawMutex, C, R> {
+    /// Channel for sending commands
+    command: EmbassyChannel<M, C, 1>,
+    /// Channel for receiving responses
+    response: EmbassyChannel<M, R, 1>,
+    /// Mutex for synchronizing access to command execution
+    execution_lock: Mutex<M, ()>,
+}
+
+impl<M: RawMutex, C, R> Channel<M, C, R> {
+    /// Create a new channel
+    pub const fn new() -> Self {
+        Self {
+            command: EmbassyChannel::new(),
+            response: EmbassyChannel::new(),
+            execution_lock: Mutex::new(()),
+        }
+    }
+
+    /// Create an invoker object that can be used to invoke commands
+    pub fn invoker(&self) -> Invoker<M, C, R> {
+        Invoker::new(self)
+    }
+
+    /// Create a handler object that can be used to execute commands generated by an invoker
+    pub fn handler(&self) -> Handler<M, C, R> {
+        Handler::new(self)
+    }
+}
+
+/// Invoker object for invoking commands
+pub struct Invoker<'a, M: RawMutex, C, R> {
+    inner: &'a Channel<M, C, R>,
+}
+
+impl<'a, M: RawMutex, C, R> Invoker<'a, M, C, R> {
+    /// Create a new invoker object
+    fn new(inner: &'a Channel<M, C, R>) -> Self {
+        Self { inner }
+    }
+
+    /// Invoke a command and return the response
+    /// This locks to ensure that commands are executed atomically
+    pub async fn invoke(&self, command: C) -> R {
+        let _guard = self.inner.execution_lock.lock().await;
+        self.inner.command.send(command).await;
+        self.inner.response.receive().await
+    }
+}
+
+/// Handler object for executing commands generated by an invoker
+pub struct Handler<'a, M: RawMutex, C, R> {
+    inner: &'a Channel<M, C, R>,
+}
+
+impl<'a, M: RawMutex, C, R> Handler<'a, M, C, R> {
+    /// Create a new handler object
+    fn new(inner: &'a Channel<M, C, R>) -> Self {
+        Self { inner }
+    }
+
+    /// Wait for a command
+    pub async fn wait_command(&self) -> C {
+        self.inner.command.receive().await
+    }
+
+    /// Send a response to a command
+    pub async fn send_response(&self, response: R) {
+        self.inner.response.send(response).await;
+    }
+
+    /// Process a command and send a response
+    pub async fn process_command<Fut: Future<Output = R>, F: FnOnce(C) -> Fut>(&self, process: F) {
+        let command = self.wait_command().await;
+        let response = process(command).await;
+        self.send_response(response).await;
+    }
path: embedded-service/src/ipc/deferred.rs
user: login: JamesHuard
body: Do we really not want to encourage a response for each command?
created_at: 2025-05-07T15:16:28Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,84 @@
+//! Definitions for deferred execution of commands
+use core::future::Future;
+
+use embassy_sync::{blocking_mutex::raw::RawMutex, channel::Channel as EmbassyChannel, mutex::Mutex};
+
+/// A simple channel for executing deferred commands
+pub struct Channel<M: RawMutex, C, R> {
+    /// Channel for sending commands
+    command: EmbassyChannel<M, C, 1>,
+    /// Channel for receiving responses
+    response: EmbassyChannel<M, R, 1>,
+    /// Mutex for synchronizing access to command execution
+    execution_lock: Mutex<M, ()>,
+}
+
+impl<M: RawMutex, C, R> Channel<M, C, R> {
+    /// Create a new channel
+    pub const fn new() -> Self {
+        Self {
+            command: EmbassyChannel::new(),
+            response: EmbassyChannel::new(),
+            execution_lock: Mutex::new(()),
+        }
+    }
+
+    /// Create an invoker object that can be used to invoke commands
+    pub fn invoker(&self) -> Invoker<M, C, R> {
+        Invoker::new(self)
+    }
+
+    /// Create a handler object that can be used to execute commands generated by an invoker
+    pub fn handler(&self) -> Handler<M, C, R> {
+        Handler::new(self)
+    }
+}
+
+/// Invoker object for invoking commands
+pub struct Invoker<'a, M: RawMutex, C, R> {
+    inner: &'a Channel<M, C, R>,
+}
+
+impl<'a, M: RawMutex, C, R> Invoker<'a, M, C, R> {
+    /// Create a new invoker object
+    fn new(inner: &'a Channel<M, C, R>) -> Self {
+        Self { inner }
+    }
+
+    /// Invoke a command and return the response
+    /// This locks to ensure that commands are executed atomically
+    pub async fn invoke(&self, command: C) -> R {
+        let _guard = self.inner.execution_lock.lock().await;
+        self.inner.command.send(command).await;
+        self.inner.response.receive().await
+    }
+}
+
+/// Handler object for executing commands generated by an invoker
+pub struct Handler<'a, M: RawMutex, C, R> {
+    inner: &'a Channel<M, C, R>,
+}
+
+impl<'a, M: RawMutex, C, R> Handler<'a, M, C, R> {
+    /// Create a new handler object
+    fn new(inner: &'a Channel<M, C, R>) -> Self {
+        Self { inner }
+    }
+
+    /// Wait for a command
+    pub async fn wait_command(&self) -> C {
+        self.inner.command.receive().await
+    }
+
+    /// Send a response to a command
+    pub async fn send_response(&self, response: R) {
+        self.inner.response.send(response).await;
+    }
+
+    /// Process a command and send a response
+    pub async fn process_command<Fut: Future<Output = R>, F: FnOnce(C) -> Fut>(&self, process: F) {
+        let command = self.wait_command().await;
+        let response = process(command).await;
+        self.send_response(response).await;
+    }
path: embedded-service/src/ipc/deferred.rs
user: login: felipebalbi
body: > This isn't used, and I think the overall design may be too opinionated. IMO best to remove it for now

I second what @asasine is stating here, sorta. Maybe renaming `process_command()` would suffice, as it's not only processing, but already receiving the command and sending a response. I can see how it can become too opinionated at times.
created_at: 2025-05-09T14:41:30Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,83 @@
+//! Definitions for IPC
+use core::sync::atomic::AtomicBool;
+
+use embassy_sync::{blocking_mutex::raw::RawMutex, channel::Channel, mutex::Mutex};
+
+/// A simple RPC channel for executing commands
path: embedded-service/src/ipc/rpc.rs
user: login: RobertZ2011
body: I'm going to hold off on implementing this for now. I created an initial implementation but ran into issues surrounding exactly how the mutex guards should be handled. I want to play around more with various situations to get a better handle on that first.
created_at: 2025-05-09T17:36:12Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,84 @@
+//! Definitions for deferred execution of commands
+use core::future::Future;
+
+use embassy_sync::{blocking_mutex::raw::RawMutex, channel::Channel as EmbassyChannel, mutex::Mutex};
+
+/// A simple channel for executing deferred commands
+pub struct Channel<M: RawMutex, C, R> {
+    /// Channel for sending commands
+    command: EmbassyChannel<M, C, 1>,
+    /// Channel for receiving responses
+    response: EmbassyChannel<M, R, 1>,
+    /// Mutex for synchronizing access to command execution
+    execution_lock: Mutex<M, ()>,
+}
+
+impl<M: RawMutex, C, R> Channel<M, C, R> {
+    /// Create a new channel
+    pub const fn new() -> Self {
+        Self {
+            command: EmbassyChannel::new(),
+            response: EmbassyChannel::new(),
+            execution_lock: Mutex::new(()),
+        }
+    }
+
+    /// Create an invoker object that can be used to invoke commands
+    pub fn invoker(&self) -> Invoker<M, C, R> {
+        Invoker::new(self)
+    }
+
+    /// Create a handler object that can be used to execute commands generated by an invoker
+    pub fn handler(&self) -> Handler<M, C, R> {
+        Handler::new(self)
+    }
+}
+
+/// Invoker object for invoking commands
+pub struct Invoker<'a, M: RawMutex, C, R> {
+    inner: &'a Channel<M, C, R>,
+}
+
+impl<'a, M: RawMutex, C, R> Invoker<'a, M, C, R> {
+    /// Create a new invoker object
+    fn new(inner: &'a Channel<M, C, R>) -> Self {
+        Self { inner }
+    }
+
+    /// Invoke a command and return the response
+    /// This locks to ensure that commands are executed atomically
+    pub async fn invoke(&self, command: C) -> R {
+        let _guard = self.inner.execution_lock.lock().await;
+        self.inner.command.send(command).await;
+        self.inner.response.receive().await
+    }
+}
+
+/// Handler object for executing commands generated by an invoker
+pub struct Handler<'a, M: RawMutex, C, R> {
+    inner: &'a Channel<M, C, R>,
+}
+
+impl<'a, M: RawMutex, C, R> Handler<'a, M, C, R> {
+    /// Create a new handler object
+    fn new(inner: &'a Channel<M, C, R>) -> Self {
+        Self { inner }
+    }
+
+    /// Wait for a command
+    pub async fn wait_command(&self) -> C {
+        self.inner.command.receive().await
+    }
+
+    /// Send a response to a command
+    pub async fn send_response(&self, response: R) {
+        self.inner.response.send(response).await;
+    }
+
+    /// Process a command and send a response
+    pub async fn process_command<Fut: Future<Output = R>, F: FnOnce(C) -> Fut>(&self, process: F) {
+        let command = self.wait_command().await;
+        let response = process(command).await;
+        self.send_response(response).await;
+    }
path: embedded-service/src/ipc/deferred.rs
user: login: RobertZ2011
body: I removed this. I really like the benefits to correctness, but this pattern tends to encourage writing code in a way that makes things less modular and composable. E.g. if an implementation uses this then it's hard to gain access to the actual commands to allow for extending functionality.
created_at: 2025-05-09T17:39:14Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,98 @@
+//! Definitions for deferred execution of commands
+use core::{
+    marker::PhantomData,
+    sync::atomic::{AtomicU32, Ordering},
+};
+
+use embassy_sync::{blocking_mutex::raw::RawMutex, mutex::Mutex, signal::Signal};
+
+use crate::debug;
+
+/// A unique identifier for a particular command invocation
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+#[cfg_attr(feature = "defmt", derive(defmt::Format))]
+pub struct InvocationId(u32);
+
+/// A simple channel for executing deferred commands
+/// This implementation provides synchronization for command invocations
+/// and ensures that responses are sent back to the correct invoker
+/// using a unique invocation ID.
+pub struct Channel<M: RawMutex, C, R> {
+    /// Signal for sending commands
+    command: Signal<M, (C, InvocationId)>,
+    /// Signal for receiving responses
+    response: Signal<M, (R, InvocationId)>,
+    /// Mutex for synchronizing access to command invocation
+    invocation_lock: Mutex<M, ()>,
+    /// Unique ID for the next invocation
+    next_invocation_id: AtomicU32,
+    /// Phantom data for the response type
+    _phantom: PhantomData<R>,
path: embedded-service/src/ipc/deferred.rs
user: login: asasine
body: Unnecessary since `R` is already used by `response`
created_at: 2025-05-14T15:59:46Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,98 @@
+//! Definitions for deferred execution of commands
+use core::{
+    marker::PhantomData,
+    sync::atomic::{AtomicU32, Ordering},
+};
+
+use embassy_sync::{blocking_mutex::raw::RawMutex, mutex::Mutex, signal::Signal};
+
+use crate::debug;
+
+/// A unique identifier for a particular command invocation
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+#[cfg_attr(feature = "defmt", derive(defmt::Format))]
+pub struct InvocationId(u32);
path: embedded-service/src/ipc/deferred.rs
user: login: asasine
body: Doesn't need to be public
```suggestion
struct InvocationId(u32);
```
created_at: 2025-05-14T16:00:12Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,98 @@
+//! Definitions for deferred execution of commands
+use core::{
+    marker::PhantomData,
+    sync::atomic::{AtomicU32, Ordering},
+};
+
+use embassy_sync::{blocking_mutex::raw::RawMutex, mutex::Mutex, signal::Signal};
+
+use crate::debug;
+
+/// A unique identifier for a particular command invocation
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+#[cfg_attr(feature = "defmt", derive(defmt::Format))]
+pub struct InvocationId(u32);
+
+/// A simple channel for executing deferred commands
+/// This implementation provides synchronization for command invocations
+/// and ensures that responses are sent back to the correct invoker
+/// using a unique invocation ID.
+pub struct Channel<M: RawMutex, C, R> {
+    /// Signal for sending commands
+    command: Signal<M, (C, InvocationId)>,
+    /// Signal for receiving responses
+    response: Signal<M, (R, InvocationId)>,
+    /// Mutex for synchronizing access to command invocation
+    invocation_lock: Mutex<M, ()>,
+    /// Unique ID for the next invocation
+    next_invocation_id: AtomicU32,
path: embedded-service/src/ipc/deferred.rs
user: login: asasine
body: Using u32 seems aritrary so I arbitrarily recommend `AtomicUsize`
created_at: 2025-05-14T16:00:53Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,98 @@
+//! Definitions for deferred execution of commands
+use core::{
+    marker::PhantomData,
+    sync::atomic::{AtomicU32, Ordering},
+};
+
+use embassy_sync::{blocking_mutex::raw::RawMutex, mutex::Mutex, signal::Signal};
+
+use crate::debug;
+
+/// A unique identifier for a particular command invocation
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+#[cfg_attr(feature = "defmt", derive(defmt::Format))]
+pub struct InvocationId(u32);
+
+/// A simple channel for executing deferred commands
+/// This implementation provides synchronization for command invocations
+/// and ensures that responses are sent back to the correct invoker
+/// using a unique invocation ID.
+pub struct Channel<M: RawMutex, C, R> {
+    /// Signal for sending commands
+    command: Signal<M, (C, InvocationId)>,
+    /// Signal for receiving responses
+    response: Signal<M, (R, InvocationId)>,
+    /// Mutex for synchronizing access to command invocation
+    invocation_lock: Mutex<M, ()>,
+    /// Unique ID for the next invocation
+    next_invocation_id: AtomicU32,
+    /// Phantom data for the response type
+    _phantom: PhantomData<R>,
+}
+
+impl<M: RawMutex, C, R> Channel<M, C, R> {
+    /// Create a new channel
+    pub const fn new() -> Self {
+        Self {
+            command: Signal::new(),
+            response: Signal::new(),
+            invocation_lock: Mutex::new(()),
+            _phantom: PhantomData,
+            next_invocation_id: AtomicU32::new(0),
+        }
+    }
+
+    /// Invoke a command and return the response
+    /// This locks to ensure that commands are executed atomically
+    pub async fn invoke(&self, command: C) -> R {
+        let _guard = self.invocation_lock.lock().await;
+        let request_id = self.next_invocation_id.fetch_add(1, Ordering::SeqCst);
+        let request_id = InvocationId(request_id);
+        self.command.signal((command, request_id));
+        loop {
+            // Wait until we receive a response for out particular invocation
+            let (response, id) = self.response.wait().await;
+            if id == request_id {
+                return response;
+            } else {
+                // Not an error because this is the expected behavior in certain cases,
+                // particularly if the invoker times out before the response is received.
+                debug!("Received response for different invocation: {}", id.0);
+            }
+        }
+    }
+
+    /// Wait for an invocation
+    pub async fn wait_invocation(&self) -> Invocation<'_, M, C, R> {
+        let (command, invocation_id) = self.command.wait().await;
+        Invocation {
+            channel: self,
+            invocation_id,
+            command,
+        }
+    }
+}
+
+impl<M: RawMutex, C, R> Default for Channel<M, C, R> {
+    /// Default implementation
+    fn default() -> Self {
+        Self::new()
+    }
+}
+
+/// A specific invocation of a command
+pub struct Invocation<'a, M: RawMutex, C, R> {
+    /// The channel this invocation came from
+    channel: &'a Channel<M, C, R>,
+    /// Invocation ID
+    invocation_id: InvocationId,
+    /// Command to execute
+    pub command: C,
+}
+
+impl<M: RawMutex, C, R> Invocation<'_, M, C, R> {
+    /// Send a response to the command
path: embedded-service/src/ipc/deferred.rs
user: login: asasine
body: There's a nice design decision here that I think is worth highlighting so it remains long-term and doesn't trip up users
```suggestion
    /// Send a response to the command, consuming the command in the process.
    ///
    /// Consuming the command ensures each command may only be responded to once.
```
created_at: 2025-05-14T16:18:37Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,98 @@
+//! Definitions for deferred execution of commands
+use core::{
+    marker::PhantomData,
+    sync::atomic::{AtomicU32, Ordering},
+};
+
+use embassy_sync::{blocking_mutex::raw::RawMutex, mutex::Mutex, signal::Signal};
+
+use crate::debug;
+
+/// A unique identifier for a particular command invocation
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+#[cfg_attr(feature = "defmt", derive(defmt::Format))]
+pub struct InvocationId(u32);
+
+/// A simple channel for executing deferred commands
path: embedded-service/src/ipc/deferred.rs
user: login: asasine
body: nit: a blank line ensures the top-level decription is brief and sufficiently informative.
```suggestion
/// A simple channel for executing deferred commands.
///
```
created_at: 2025-05-14T16:19:41Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,98 @@
+//! Definitions for deferred execution of commands
+use core::{
+    marker::PhantomData,
+    sync::atomic::{AtomicU32, Ordering},
+};
+
+use embassy_sync::{blocking_mutex::raw::RawMutex, mutex::Mutex, signal::Signal};
+
+use crate::debug;
+
+/// A unique identifier for a particular command invocation
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+#[cfg_attr(feature = "defmt", derive(defmt::Format))]
+pub struct InvocationId(u32);
+
+/// A simple channel for executing deferred commands
+/// This implementation provides synchronization for command invocations
+/// and ensures that responses are sent back to the correct invoker
+/// using a unique invocation ID.
+pub struct Channel<M: RawMutex, C, R> {
+    /// Signal for sending commands
+    command: Signal<M, (C, InvocationId)>,
+    /// Signal for receiving responses
+    response: Signal<M, (R, InvocationId)>,
+    /// Mutex for synchronizing access to command invocation
+    invocation_lock: Mutex<M, ()>,
+    /// Unique ID for the next invocation
+    next_invocation_id: AtomicU32,
+    /// Phantom data for the response type
+    _phantom: PhantomData<R>,
+}
+
+impl<M: RawMutex, C, R> Channel<M, C, R> {
+    /// Create a new channel
+    pub const fn new() -> Self {
+        Self {
+            command: Signal::new(),
+            response: Signal::new(),
+            invocation_lock: Mutex::new(()),
+            _phantom: PhantomData,
+            next_invocation_id: AtomicU32::new(0),
+        }
+    }
+
+    /// Invoke a command and return the response
+    /// This locks to ensure that commands are executed atomically
+    pub async fn invoke(&self, command: C) -> R {
+        let _guard = self.invocation_lock.lock().await;
+        let request_id = self.next_invocation_id.fetch_add(1, Ordering::SeqCst);
+        let request_id = InvocationId(request_id);
path: embedded-service/src/ipc/deferred.rs
user: login: asasine
body: Put behind a private method so there's less likelihood that the primitive version of the value is misused. Even better, create a (unit tested) wrapper around an incrementing atomic that never exposes the primitive and use that instead.
created_at: 2025-05-14T16:21:00Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,98 @@
+//! Definitions for deferred execution of commands
+use core::{
+    marker::PhantomData,
+    sync::atomic::{AtomicU32, Ordering},
+};
+
+use embassy_sync::{blocking_mutex::raw::RawMutex, mutex::Mutex, signal::Signal};
+
+use crate::debug;
+
+/// A unique identifier for a particular command invocation
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+#[cfg_attr(feature = "defmt", derive(defmt::Format))]
+pub struct InvocationId(u32);
+
+/// A simple channel for executing deferred commands
+/// This implementation provides synchronization for command invocations
+/// and ensures that responses are sent back to the correct invoker
+/// using a unique invocation ID.
+pub struct Channel<M: RawMutex, C, R> {
+    /// Signal for sending commands
+    command: Signal<M, (C, InvocationId)>,
+    /// Signal for receiving responses
+    response: Signal<M, (R, InvocationId)>,
+    /// Mutex for synchronizing access to command invocation
+    invocation_lock: Mutex<M, ()>,
+    /// Unique ID for the next invocation
+    next_invocation_id: AtomicU32,
+    /// Phantom data for the response type
+    _phantom: PhantomData<R>,
path: embedded-service/src/ipc/deferred.rs
user: login: RobertZ2011
body: Done.
created_at: 2025-05-14T23:47:02Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,98 @@
+//! Definitions for deferred execution of commands
+use core::{
+    marker::PhantomData,
+    sync::atomic::{AtomicU32, Ordering},
+};
+
+use embassy_sync::{blocking_mutex::raw::RawMutex, mutex::Mutex, signal::Signal};
+
+use crate::debug;
+
+/// A unique identifier for a particular command invocation
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+#[cfg_attr(feature = "defmt", derive(defmt::Format))]
+pub struct InvocationId(u32);
path: embedded-service/src/ipc/deferred.rs
user: login: RobertZ2011
body: Done.
created_at: 2025-05-14T23:47:08Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,98 @@
+//! Definitions for deferred execution of commands
+use core::{
+    marker::PhantomData,
+    sync::atomic::{AtomicU32, Ordering},
+};
+
+use embassy_sync::{blocking_mutex::raw::RawMutex, mutex::Mutex, signal::Signal};
+
+use crate::debug;
+
+/// A unique identifier for a particular command invocation
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+#[cfg_attr(feature = "defmt", derive(defmt::Format))]
+pub struct InvocationId(u32);
+
+/// A simple channel for executing deferred commands
+/// This implementation provides synchronization for command invocations
+/// and ensures that responses are sent back to the correct invoker
+/// using a unique invocation ID.
+pub struct Channel<M: RawMutex, C, R> {
+    /// Signal for sending commands
+    command: Signal<M, (C, InvocationId)>,
+    /// Signal for receiving responses
+    response: Signal<M, (R, InvocationId)>,
+    /// Mutex for synchronizing access to command invocation
+    invocation_lock: Mutex<M, ()>,
+    /// Unique ID for the next invocation
+    next_invocation_id: AtomicU32,
path: embedded-service/src/ipc/deferred.rs
user: login: RobertZ2011
body: Done.
created_at: 2025-05-14T23:47:15Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,98 @@
+//! Definitions for deferred execution of commands
+use core::{
+    marker::PhantomData,
+    sync::atomic::{AtomicU32, Ordering},
+};
+
+use embassy_sync::{blocking_mutex::raw::RawMutex, mutex::Mutex, signal::Signal};
+
+use crate::debug;
+
+/// A unique identifier for a particular command invocation
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+#[cfg_attr(feature = "defmt", derive(defmt::Format))]
+pub struct InvocationId(u32);
+
+/// A simple channel for executing deferred commands
+/// This implementation provides synchronization for command invocations
+/// and ensures that responses are sent back to the correct invoker
+/// using a unique invocation ID.
+pub struct Channel<M: RawMutex, C, R> {
+    /// Signal for sending commands
+    command: Signal<M, (C, InvocationId)>,
+    /// Signal for receiving responses
+    response: Signal<M, (R, InvocationId)>,
+    /// Mutex for synchronizing access to command invocation
+    invocation_lock: Mutex<M, ()>,
+    /// Unique ID for the next invocation
+    next_invocation_id: AtomicU32,
+    /// Phantom data for the response type
+    _phantom: PhantomData<R>,
+}
+
+impl<M: RawMutex, C, R> Channel<M, C, R> {
+    /// Create a new channel
+    pub const fn new() -> Self {
+        Self {
+            command: Signal::new(),
+            response: Signal::new(),
+            invocation_lock: Mutex::new(()),
+            _phantom: PhantomData,
+            next_invocation_id: AtomicU32::new(0),
+        }
+    }
+
+    /// Invoke a command and return the response
+    /// This locks to ensure that commands are executed atomically
+    pub async fn invoke(&self, command: C) -> R {
+        let _guard = self.invocation_lock.lock().await;
+        let request_id = self.next_invocation_id.fetch_add(1, Ordering::SeqCst);
+        let request_id = InvocationId(request_id);
+        self.command.signal((command, request_id));
+        loop {
+            // Wait until we receive a response for out particular invocation
+            let (response, id) = self.response.wait().await;
+            if id == request_id {
+                return response;
+            } else {
+                // Not an error because this is the expected behavior in certain cases,
+                // particularly if the invoker times out before the response is received.
+                debug!("Received response for different invocation: {}", id.0);
+            }
+        }
+    }
+
+    /// Wait for an invocation
+    pub async fn wait_invocation(&self) -> Invocation<'_, M, C, R> {
+        let (command, invocation_id) = self.command.wait().await;
+        Invocation {
+            channel: self,
+            invocation_id,
+            command,
+        }
+    }
+}
+
+impl<M: RawMutex, C, R> Default for Channel<M, C, R> {
+    /// Default implementation
+    fn default() -> Self {
+        Self::new()
+    }
+}
+
+/// A specific invocation of a command
+pub struct Invocation<'a, M: RawMutex, C, R> {
+    /// The channel this invocation came from
+    channel: &'a Channel<M, C, R>,
+    /// Invocation ID
+    invocation_id: InvocationId,
+    /// Command to execute
+    pub command: C,
+}
+
+impl<M: RawMutex, C, R> Invocation<'_, M, C, R> {
+    /// Send a response to the command
path: embedded-service/src/ipc/deferred.rs
user: login: RobertZ2011
body: Done.
created_at: 2025-05-14T23:47:23Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,98 @@
+//! Definitions for deferred execution of commands
+use core::{
+    marker::PhantomData,
+    sync::atomic::{AtomicU32, Ordering},
+};
+
+use embassy_sync::{blocking_mutex::raw::RawMutex, mutex::Mutex, signal::Signal};
+
+use crate::debug;
+
+/// A unique identifier for a particular command invocation
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+#[cfg_attr(feature = "defmt", derive(defmt::Format))]
+pub struct InvocationId(u32);
+
+/// A simple channel for executing deferred commands
path: embedded-service/src/ipc/deferred.rs
user: login: RobertZ2011
body: Done.
created_at: 2025-05-14T23:47:31Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,98 @@
+//! Definitions for deferred execution of commands
+use core::{
+    marker::PhantomData,
+    sync::atomic::{AtomicU32, Ordering},
+};
+
+use embassy_sync::{blocking_mutex::raw::RawMutex, mutex::Mutex, signal::Signal};
+
+use crate::debug;
+
+/// A unique identifier for a particular command invocation
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+#[cfg_attr(feature = "defmt", derive(defmt::Format))]
+pub struct InvocationId(u32);
+
+/// A simple channel for executing deferred commands
+/// This implementation provides synchronization for command invocations
+/// and ensures that responses are sent back to the correct invoker
+/// using a unique invocation ID.
+pub struct Channel<M: RawMutex, C, R> {
+    /// Signal for sending commands
+    command: Signal<M, (C, InvocationId)>,
+    /// Signal for receiving responses
+    response: Signal<M, (R, InvocationId)>,
+    /// Mutex for synchronizing access to command invocation
+    invocation_lock: Mutex<M, ()>,
+    /// Unique ID for the next invocation
+    next_invocation_id: AtomicU32,
+    /// Phantom data for the response type
+    _phantom: PhantomData<R>,
+}
+
+impl<M: RawMutex, C, R> Channel<M, C, R> {
+    /// Create a new channel
+    pub const fn new() -> Self {
+        Self {
+            command: Signal::new(),
+            response: Signal::new(),
+            invocation_lock: Mutex::new(()),
+            _phantom: PhantomData,
+            next_invocation_id: AtomicU32::new(0),
+        }
+    }
+
+    /// Invoke a command and return the response
+    /// This locks to ensure that commands are executed atomically
+    pub async fn invoke(&self, command: C) -> R {
+        let _guard = self.invocation_lock.lock().await;
+        let request_id = self.next_invocation_id.fetch_add(1, Ordering::SeqCst);
+        let request_id = InvocationId(request_id);
path: embedded-service/src/ipc/deferred.rs
user: login: RobertZ2011
body: Done, went with just a private method and added a test.
created_at: 2025-05-14T23:47:44Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,263 @@
+//! Definitions for deferred execution of commands
+use core::sync::atomic::{AtomicUsize, Ordering};
+
+use embassy_sync::{blocking_mutex::raw::RawMutex, mutex::Mutex, signal::Signal};
+use embassy_time::{with_timeout, Duration};
+
+use crate::debug;
+
+/// A unique identifier for a particular command invocation
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+#[cfg_attr(feature = "defmt", derive(defmt::Format))]
+struct RequestId(usize);
+
+/// A simple channel for executing deferred commands.
+///
+/// This implementation provides synchronization for command invocations
+/// and ensures that responses are sent back to the correct sender
+/// using a unique invocation ID.
+pub struct Channel<M: RawMutex, C, R> {
+    /// Signal for sending commands
+    command: Signal<M, (C, RequestId)>,
+    /// Signal for receiving responses
+    response: Signal<M, (R, RequestId)>,
+    /// Mutex for synchronizing access to command invocation
+    request_lock: Mutex<M, ()>,
+    /// Unique ID for the next invocation
+    next_request_id: AtomicUsize,
+    /// Default timeout duration for command execution
+    timeout: Duration,
path: embedded-service/src/ipc/deferred.rs
user: login: asasine
body: I don't think this should be a feature of the channel. Let callers decide how to handle failures (including timeouts). Any choice you make for the default is entirely arbitrary and therefore can't take into account the specifics of how this type gets used.
created_at: 2025-05-15T15:59:41Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,263 @@
+//! Definitions for deferred execution of commands
+use core::sync::atomic::{AtomicUsize, Ordering};
+
+use embassy_sync::{blocking_mutex::raw::RawMutex, mutex::Mutex, signal::Signal};
+use embassy_time::{with_timeout, Duration};
+
+use crate::debug;
+
+/// A unique identifier for a particular command invocation
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+#[cfg_attr(feature = "defmt", derive(defmt::Format))]
+struct RequestId(usize);
+
+/// A simple channel for executing deferred commands.
+///
+/// This implementation provides synchronization for command invocations
+/// and ensures that responses are sent back to the correct sender
+/// using a unique invocation ID.
+pub struct Channel<M: RawMutex, C, R> {
+    /// Signal for sending commands
+    command: Signal<M, (C, RequestId)>,
+    /// Signal for receiving responses
+    response: Signal<M, (R, RequestId)>,
+    /// Mutex for synchronizing access to command invocation
+    request_lock: Mutex<M, ()>,
+    /// Unique ID for the next invocation
+    next_request_id: AtomicUsize,
+    /// Default timeout duration for command execution
+    timeout: Duration,
path: embedded-service/src/ipc/deferred.rs
user: login: RobertZ2011
body: Done.
created_at: 2025-05-15T18:26:44Z
author_association: CONTRIBUTOR

PR comments:
user: login: github-actions[bot]
created_at: 2025-05-06T16:53:45Z
body: # Cargo Vet Audit Passed
`cargo vet` has passed in this PR. No new unvetted dependencies were found.

<!--
This comment is auto-generated by the cargo-vet workflow.
Please do not edit it directly.

comment-tag: [cargo-vet]
-->
author_association: NONE
user: login: RobertZ2011
created_at: 2025-05-09T17:50:36Z
body: Latest commits change things a bit. The previous implementation had a bug around timeouts/futures being dropped in general. If a command was submitted but the response never read then that stale response would become the response for the subsequent command. Now each invocation of a command has a unique ID that can be used to ensure that the correct response is for us. I did also consider passing in a `&Signal`, but that would have to be static so we couldn't have a self-contained invoke function like this implementation provides. I also moved over to using Embassy signals instead of channels since we already have synchronization with the mutex. I removed the `Handler` and `Invoker` wrapper because each one would only need a single function anyway.
author_association: CONTRIBUTOR
user: login: RobertZ2011
created_at: 2025-05-14T23:55:25Z
body: > Looks good to me.
> 
> Bikeshedding: I don't like the invoker/invocation name; it's too wordy imo and requires too much new mental chewing to grasp since it doesn't resemble existing synchronization primitives. What about sticking closer to existing nomenclature for channels or signals, which this is behaving like?
> 
> * `invoke()` -> `send()`
> * `Invocation` -> `Message` (most conventional) or `Request` or `Command`
> * `wait_invocation()` -> `receive()` or `wait()` or `get()`
> * `send_response()` -> `respond()` or `send()`
> 
> I also think this is completely unit testable and would like to see them added.

Ended up going with `execute` because I think it captures the back and forth nature better. I also added logic to timeout by default since it's safer. Changed the other invocation type names to some form of either request or receive.

I added some tests, primarily by converting the example over into a test. I'm using tokio as the test executor because the embassy executor will never stop running so it's not a good match for testing. Embassy itself seems to use `futures_executor` but that doesn't seem to provide a straightforward way to spawn arbitrary tasks. Anything embassy time related can't execute on the tokio executor though so this PR only directly tests the non-timeout versions.
author_association: CONTRIBUTOR
user: login: RobertZ2011
created_at: 2025-05-15T21:06:52Z
body: Going to land the tests as a separate PR: https://github.com/OpenDevicePartnership/embedded-services/pull/297. There's some weirdness with using embassy in tests within a workspace but I want to land this PR since it should resolve several bugs.
author_association: CONTRIBUTOR