├── .github └── workflows │ └── ci.yml ├── .gitignore ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── book ├── book.toml └── src │ ├── SUMMARY.md │ ├── board.md │ ├── comms-postcard-rpc.md │ ├── comms-postcard.md │ ├── comms.md │ ├── custom.md │ ├── endpoints.md │ ├── hello-acc.md │ ├── hello-gpios.md │ ├── hello-smartleds.md │ ├── hello.md │ ├── internection-fw.md │ ├── internection-host.md │ ├── internection-icd.md │ ├── internection.md │ ├── intro.md │ ├── ovtwin-001.jpg │ ├── ovtwin-block.jpg │ ├── ovtwin-hello-01.jpg │ ├── ovtwin-uhoh-00.jpg │ ├── setup.md │ ├── streaming.md │ ├── welcome-goals.md │ └── welcome.md ├── ci.sh ├── docs └── overview.md ├── example ├── firmware-eusb-v0_3 │ ├── .cargo │ │ └── config.toml │ ├── Cargo.lock │ ├── Cargo.toml │ ├── build.rs │ ├── memory.x │ └── src │ │ ├── lib.rs │ │ ├── main.rs │ │ └── ws2812.rs ├── firmware │ ├── .cargo │ │ └── config.toml │ ├── Cargo.lock │ ├── Cargo.toml │ ├── build.rs │ ├── memory.x │ └── src │ │ ├── bin │ │ ├── comms-01.rs │ │ ├── comms-02.rs │ │ ├── hello-01.rs │ │ ├── hello-02.rs │ │ ├── hello-03.rs │ │ ├── logging.rs │ │ ├── minimal.rs │ │ └── uhoh-00.rs │ │ ├── lib.rs │ │ └── ws2812.rs ├── workbook-host │ ├── Cargo.lock │ ├── Cargo.toml │ └── src │ │ ├── bin │ │ ├── comms-01.rs │ │ ├── comms-02.rs │ │ └── logging.rs │ │ ├── client.rs │ │ └── lib.rs └── workbook-icd │ ├── Cargo.lock │ ├── Cargo.toml │ └── src │ └── lib.rs └── source ├── postcard-rpc-test ├── Cargo.lock ├── Cargo.toml ├── src │ └── lib.rs └── tests │ ├── basic.rs │ └── subscrobble.rs └── postcard-rpc ├── .cargo └── config.toml ├── .gitignore ├── Cargo.toml └── src ├── accumulator.rs ├── header.rs ├── host_client ├── mod.rs ├── raw_nusb.rs ├── serial.rs ├── test_channels.rs ├── util.rs └── webusb.rs ├── lib.rs ├── macros.rs ├── server ├── dispatch_macro.rs ├── impls │ ├── embassy_usb_v0_3.rs │ ├── embassy_usb_v0_4.rs │ ├── mod.rs │ └── test_channels.rs └── mod.rs ├── standard_icd.rs ├── test_utils.rs └── uniques.rs /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | # Cancel old workflows for PRs (only the most recent workflow can run). 12 | concurrency: 13 | group: ci-${{ github.ref }} 14 | cancel-in-progress: ${{ github.event_name == 'pull_request' }} 15 | 16 | # Avoid workflow-level permissions, instead use job-level permissions. 17 | permissions: {} 18 | 19 | jobs: 20 | ubuntu: 21 | runs-on: ubuntu-latest 22 | steps: 23 | - uses: actions/checkout@v4 24 | - run: ./ci.sh 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | book/book/ 3 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright (c) 2019 Anthony James Munns 2 | 3 | Permission is hereby granted, free of charge, to any 4 | person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the 6 | Software without restriction, including without 7 | limitation the rights to use, copy, modify, merge, 8 | publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software 10 | is furnished to do so, subject to the following 11 | conditions: 12 | 13 | The above copyright notice and this permission notice 14 | shall be included in all copies or substantial portions 15 | of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 18 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 19 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 20 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 21 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 22 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 23 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 24 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 25 | DEALINGS IN THE SOFTWARE. 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Postcard RPC 2 | 3 | A host (PC) and client (MCU) library for handling RPC-style request-response types. 4 | 5 | See [overview.md](./docs/overview.md) for documentation. 6 | 7 | See the [`postcard-rpc` book](https://onevariable.com/postcard-rpc-book/) for a walk-through example. 8 | 9 | You can also watch James' [RustNL talk](https://www.youtube.com/live/XLefuzE-ABU?t=24300) for a video explainer of what this crate does. 10 | 11 | ## License 12 | 13 | Licensed under either of 14 | 15 | - Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE) or 16 | ) 17 | - MIT license ([LICENSE-MIT](LICENSE-MIT) or ) 18 | 19 | at your option. 20 | 21 | ### Contribution 22 | 23 | Unless you explicitly state otherwise, any contribution intentionally submitted 24 | for inclusion in the work by you, as defined in the Apache-2.0 license, shall be 25 | dual licensed as above, without any additional terms or conditions. 26 | -------------------------------------------------------------------------------- /book/book.toml: -------------------------------------------------------------------------------- 1 | [book] 2 | authors = ["James Munns"] 3 | language = "en" 4 | multilingual = false 5 | src = "src" 6 | title = "postcard-rpc book" 7 | -------------------------------------------------------------------------------- /book/src/SUMMARY.md: -------------------------------------------------------------------------------- 1 | # Summary 2 | 3 | - [Introduction](./intro.md) 4 | - [Setup](./setup.md) 5 | - [Welcome](./welcome.md) 6 | - [Goals](./welcome-goals.md) 7 | - [Get to know the board](./board.md) 8 | - [Hello, ov-twin!](./hello.md) 9 | - [SmartLEDs](./hello-smartleds.md) 10 | - [Buttons and Potentiometer](./hello-gpios.md) 11 | - [Accelerometer](./hello-acc.md) 12 | - [Establishing Comms](./comms.md) 13 | - [Postcard](./comms-postcard.md) 14 | - [postcard-rpc](./comms-postcard-rpc.md) 15 | - [Interacting with the board](./internection.md) 16 | - [ICD](./internection-icd.md) 17 | - [Firmware](./internection-fw.md) 18 | - [Host](./internection-host.md) 19 | - [Endpoints](./endpoints.md) 20 | - [Streaming with Topics](./streaming.md) 21 | - [Custom Applications](./custom.md) 22 | -------------------------------------------------------------------------------- /book/src/board.md: -------------------------------------------------------------------------------- 1 | # Get to know the board 2 | 3 | This is our board, the OneVariable Twin Trainer. 4 | 5 | ![board photo](ovtwin-001.jpg) 6 | 7 | ## Links 8 | 9 | You can view the KiCAD design files [in the hardware repo][hw repo]. 10 | 11 | You can view the schematic as a PDF [here][schematic pdf]. 12 | 13 | [hw repo]: https://github.com/OneVariable/ov-twin 14 | [schematic pdf]: https://github.com/OneVariable/ov-twin/blob/main/assets/ov-twin.pdf 15 | 16 | ## Block Diagram 17 | 18 | ![block diagram](ovtwin-block.jpg) 19 | 20 | ## Main Parts 21 | 22 | | Part Usage | Part Number | Notes | 23 | | :--- | :--- | :--- | 24 | | Debugger | RP2040 | Using [debugprobe] firmware | 25 | | Target | RP2040 | Dual Core Cortex-M0+ at 133MHz
264KiB RAM
16MiB QSPI Flash | 26 | | USB Hub | CH334F | Allows both chips to talk through one USB port | 27 | | Accelerometer | LIS3DH | Usable over SPI or I2C, we will use SPI | 28 | | SmartLEDs | TX1812Z5 | Similar to WS2812B, SK6812, or "neopixels", 16M color | 29 | | Buttons | K2-1817UQ | Square soft push buttons | 30 | | Potentiometer | RK09D1130C3W | 10K Potentiometer, 0v0 to 3v0 | 31 | 32 | [debugprobe]: https://github.com/raspberrypi/debugprobe 33 | 34 | ## GPIO List (target board) 35 | 36 | | GPIO Name | Usage | Notes | 37 | | :--- | :--- | :--- | 38 | | GPIO00 | Button 1 | Button Pad (left) - active LOW | 39 | | GPIO01 | Button 2 | Button Pad (left) - active LOW | 40 | | GPIO02 | Button 3 | Button Pad (left) - active LOW | 41 | | GPIO03 | Button 4 | Button Pad (left) - active LOW | 42 | | GPIO04 | SPI MISO/CIPO | LIS3DH | 43 | | GPIO05 | SPI CSn | LIS3DH | 44 | | GPIO06 | SPI CLK | LIS3DH | 45 | | GPIO07 | SPI MOSI/COPI | LIS3DH | 46 | | GPIO08 | I2C SDA | LIS3DH (not used) | 47 | | GPIO09 | I2C SCL | LIS3DH (not used) | 48 | | GPIO10 | Interrupt 2 | LIS3DH (optional) - active LOW | 49 | | GPIO11 | Interrupt 1 | LIS3DH (optional) - active LOW | 50 | | GPIO12 | Not Used | | 51 | | GPIO13 | Not Used | | 52 | | GPIO14 | Not Used | | 53 | | GPIO15 | Not Used | | 54 | | GPIO16 | UART TX | Debugger UART | 55 | | GPIO17 | UART RX | Debugger UART | 56 | | GPIO18 | Button 5 | Button Pad (right) - active LOW | 57 | | GPIO19 | Button 6 | Button Pad (right) - active LOW | 58 | | GPIO20 | Button 7 | Button Pad (right) - active LOW | 59 | | GPIO21 | Button 8 | Button Pad (right) - active LOW | 60 | | GPIO22 | Not Used | | 61 | | GPIO23 | Not Used | | 62 | | GPIO24 | Not Used | | 63 | | GPIO25 | Smart LED | 3v3 output | 64 | | GPIO26 | ADC0 | Potentiometer | 65 | | GPIO27 | Not Used | | 66 | | GPIO28 | Not Used | | 67 | | GPIO29 | Not Used | | 68 | -------------------------------------------------------------------------------- /book/src/comms-postcard-rpc.md: -------------------------------------------------------------------------------- 1 | # postcard-rpc 2 | 3 | `postcard-rpc` is a fairly new crate that captures a lot of the "manual" or "bespoke" protocols I've 4 | built on top of `postcard` over the past years. 5 | 6 | First, let me define some concepts, as they are used by `postcard-rpc`: 7 | 8 | ## RPC, or Remote Procedure Call 9 | 10 | RPC is a pattern for communication, often over a network, where one device wants another device 11 | to do something. This "something" can be storing data we provide, retrieving some data, or doing 12 | some more complicated operation. 13 | 14 | This has a typical interaction pattern: 15 | 16 | * The first device makes a **Request** to the second device 17 | * The second device processes that **Request**, and sends a **Response** 18 | 19 | With our microcontroller, this might look a little like this: 20 | 21 | ```text 22 | PC ---Request--> MCU 23 | ... 24 | PC <--Response-- MCU 25 | ``` 26 | 27 | The reason this is called a "Remote Procedure Call" is because conceptually, we want this to 28 | "feel like" a normal function call, and ignore the network entirely, and instead look like: 29 | 30 | ```rust 31 | 32 | async fn procedure(Request) -> Response { 33 | // ... 34 | } 35 | 36 | ``` 37 | 38 | Conceptually, this is similar to things like a REST request over the network, a GET or PUT request 39 | might transfer data, or trigger some more complex operation. 40 | 41 | ## Endpoints 42 | 43 | For any given kind of RPC, there will be a pair of Request and Response types that go with each 44 | other. If the MCU could respond with one of a few kinds of responses, we can use an `enum` to 45 | capture all of those. 46 | 47 | But remember, we probably want to have multiple kinds of requests and responses that we support! 48 | 49 | For that, we can define multiple `Endpoint`s, where each `Endpoint` refers to a single pair of 50 | request and response types. We also want to add in a little unique information per endpoint, in 51 | case we want to use the same types for multiple endpoints. 52 | 53 | We might define an `Endpoint` in `postcard-rpc` like this: 54 | 55 | ```rust 56 | 57 | #[derive(Debug, PartialEq, Serialize, Deserialize, Schema)] 58 | pub struct Sleep { 59 | pub seconds: u32, 60 | pub micros: u32, 61 | } 62 | 63 | #[derive(Debug, PartialEq, Serialize, Deserialize, Schema)] 64 | pub struct SleepDone { 65 | pub slept_for: Sleep, 66 | } 67 | 68 | endpoint!( 69 | SleepEndpoint, // This is the NAME of the Endpoint 70 | Sleep, // This is the Request type 71 | SleepDone, // This is the Response type 72 | "sleep", // This is the "path" of the endpoint 73 | ); 74 | 75 | 76 | ``` 77 | 78 | These endpoints will be defined in some shared library crate between our MCU and our PC. 79 | 80 | ## Unsolicited messages 81 | 82 | Although many problems can be solved using a Request/Response pattern, it is also common to send 83 | "unsolicited" messages. Two common cases are "streaming" and "notifications". 84 | 85 | "Streaming" is relevant when you are sending a LOT of messages, for example sending continuous 86 | sensor readings, and where making one request for every response would add a lot of overhead. 87 | 88 | "Notifications" are relevant when you are RARELY sending messages, but don't want to constantly 89 | "poll" for a result. 90 | 91 | ## Topics 92 | 93 | `postcard-rpc` also allows for this in either direction, referred to as `Topic`s. The name `Topic` 94 | is inspired by MQTT, which is used for publish and subscribe (or "pubsub") style data transfers. 95 | 96 | We might define a `Topic` in `postcard-rpc` like this: 97 | 98 | ```rust 99 | #[derive(Debug, PartialEq, Serialize, Deserialize, Schema)] 100 | pub struct AccelReading { 101 | pub x: i16, 102 | pub y: i16, 103 | pub z: i16, 104 | } 105 | 106 | topic!( 107 | AccelTopic, // This is the NAME of the Topic 108 | AccelReading, // This is the Topic type 109 | "acceleration", // This is the "path" of the topic 110 | ); 111 | ``` 112 | 113 | ## `postcard-rpc` Messages 114 | 115 | Now that we have our three kinds of messages: 116 | 117 | * Endpoint Requests 118 | * Endpoint Responses 119 | * Topic Messages 120 | 121 | How does `postcard-rpc` help us determine which is which? 122 | 123 | At the front of every message, we add a header with two fields: 124 | 125 | 1. a Key, explained below 126 | 2. a Sequence Number (a `u32`) 127 | 128 | ### Keys 129 | 130 | Since `postcard` doesn't describe the types of the messages that it is sending, and we don't want 131 | to send a lot of extra data for every message, AND we don't want to manually define all the 132 | different unique IDs for every message kind, instead `postcard-rpc` automatically and 133 | deterministically generates IDs using two pieces of information: 134 | 135 | 1. The Schema of the message type 136 | 2. The "path" string of the endpoint 137 | 138 | So from our examples before: 139 | 140 | ``` 141 | SleepEndpoint::Request::Key = hash("sleep") + hash(schema(Sleep)); 142 | SleepEndpoint::Response::Key = hash("sleep") + hash(schema(SleepDone)); 143 | AccelTopic::Message::Key = hash("acceleration") + hash(schema(AccelReading)); 144 | ``` 145 | 146 | As of now, keys boil down to an 8-byte value, calculated at compile time as a constant. 147 | 148 | This is important for two reasons: 149 | 150 | 1. It gives us a "unique" ID for every kind of request and response 151 | 2. If the contents of the request or response changes, so does the key! This means that we never 152 | have to worry about the issue of one of the devices changing a message's type, and 153 | misinterpreting the data (though it means we can't 'partially understand' messages that have 154 | changed in a small way). 155 | 156 | ### Sequence Numbers 157 | 158 | Since we might have multiple requests "In Flight" at one time, we use an incrementing sequence 159 | number to each request. This lets us tell which response goes with each request, even if they 160 | arrive out of order. 161 | 162 | For example: 163 | 164 | ```text 165 | PC ---Request 1-->-----. MCU 166 | ---Request 2-->-----|--. 167 | ---Request 3-->--. | | 168 | | | | 169 | <--Response 3----' | | 170 | <--Response 1-------' | 171 | <--Response 2----------' 172 | ``` 173 | 174 | Even though our responses come back in a different order, we can still tell which responses went 175 | with each request. 176 | 177 | ## Putting it all together 178 | 179 | We've now added one "logical" layer to our stack, the postcard-rpc protocol! 180 | 181 | Remember our old diagram: 182 | 183 | ```text 184 | ┌──────┐ ┌──────┐ 185 | ┌┤ PC ├────────────────────┐ ┌┤Target├────────────────────┐ 186 | │└──────┘ │ │└──────┘ │ 187 | │ Rust Application │ │ Rust Application │ 188 | │ Host │◁ ─ ─ ─ ─ ─ ─ ▷│ Target │ 189 | │ │ │ │ 190 | ├────────────────────────────┤ ├────────────────────────────┤ 191 | │ NUSB crate │◁ ─ ─ ─ ─ ─ ─ ▷│ embassy-usb crate │ 192 | ├────────────────────────────┤ ├────────────────────────────┤ 193 | │ Operating System + Drivers │◁ ─ ─ ─ ─ ─ ─ ▷│ embassy-rp drivers │ 194 | ├────────────────────────────┤ ├────────────────────────────┤ 195 | │ USB Hardware │◀─────USB─────▶│ USB Hardware │ 196 | └────────────────────────────┘ └────────────────────────────┘ 197 | ``` 198 | 199 | Now it looks something like this: 200 | 201 | ```text 202 | ┌──────┐ ┌──────┐ 203 | ┌┤ PC ├────────────────────┐ ┌┤Target├────────────────────┐ 204 | │└──────┘ │ │└──────┘ │ 205 | │ Rust Application │ │ Rust Application │ 206 | │ Host │◁ ─ ─ ─ ─ ─ ─ ▷│ Target │ 207 | │ │ │ │ 208 | ├────────────────────────────┤ ├────────────────────────────┤ 209 | │ postcard-rpc host client │◁ ─ ─ ─ ─ ─ ─ ▷│ postcard-rpc target server │ 210 | ├────────────────────────────┤ ├────────────────────────────┤ 211 | │ NUSB crate │◁ ─ ─ ─ ─ ─ ─ ▷│ embassy-usb crate │ 212 | ├────────────────────────────┤ ├────────────────────────────┤ 213 | │ Operating System + Drivers │◁ ─ ─ ─ ─ ─ ─ ▷│ embassy-rp drivers │ 214 | ├────────────────────────────┤ ├────────────────────────────┤ 215 | │ USB Hardware │◀─────USB─────▶│ USB Hardware │ 216 | └────────────────────────────┘ └────────────────────────────┘ 217 | ``` 218 | 219 | That's enough theory for now, let's start applying it to our firmware to get messages back and 220 | forth! 221 | -------------------------------------------------------------------------------- /book/src/comms-postcard.md: -------------------------------------------------------------------------------- 1 | # Postcard 2 | 3 | We could use any sort of wire format, like JSON or HTTP. However our microcontroller is small, and 4 | we want a protocol that will work well for both devices. 5 | 6 | For this workshop, we'll use a format called [`postcard`]. It's a compact binary format, built on 7 | top of the [`serde`] crate. It supports all Rust types, including primitives, structs, and enums. 8 | 9 | [`postcard`]: https://docs.rs/postcard 10 | [`serde`]: https://serde.rs 11 | 12 | We can define a type like this: 13 | 14 | ```rust 15 | #[derive(Serialize, Deserialize)] 16 | pub struct AccelReading { 17 | pub x: i16, 18 | pub y: i16, 19 | pub z: i16, 20 | } 21 | ``` 22 | 23 | If we were to serialize a value like this: 24 | 25 | ```rust 26 | AccelReading { x: 63, y: -1, z: -32768 } 27 | ``` 28 | 29 | We'd end up with a value like this: 30 | 31 | ```rust 32 | [ 33 | 0x7E, // 63 34 | 0x01, // -1 35 | 0xFF, 0xFF, 0x03, // -32768 36 | ] 37 | ``` 38 | 39 | We don't have to worry exactly WHY it looks like this, though you could look at the 40 | [postcard specification] if you wanted, but the main take aways are: 41 | 42 | [postcard specification]: https://postcard.jamesmunns.com/wire-format#signed-integer-encoding 43 | 44 | * This is a "non self describing format": The messages don't describe the *type* at all, only the 45 | values. This means we send less data on the wire, but both sides have to understand what data they 46 | are looking at. 47 | * The message is fairly compact, it only takes us 5 bytes to send data that takes 6 bytes in memory 48 | 49 | Since `postcard` works on both desktop and `no_std` targets, we don't need to do anything extra 50 | to define how to turn Rust data types into bytes, and how to turn bytes into data types. 51 | 52 | ## Still something missing 53 | 54 | We could go off running, just sending postcard encoded data back and forth over USB, but there's 55 | two problems we'll run into quickly: 56 | 57 | 1. We probably will want to send different KINDS of messages. How does each device tell each other 58 | what type of message this is, and how to interpret them? 59 | 2. How do we define how each side behaves? How can one device request something from the other, and 60 | know how to interpret that response? 61 | 62 | At the end of the day, postcard is just an *encoding*, not a *protocol*. You could build something 63 | on top of postcard to describe a protocol, and that's what `postcard-rpc` is! 64 | -------------------------------------------------------------------------------- /book/src/comms.md: -------------------------------------------------------------------------------- 1 | # Establishing Comms 2 | 3 | So far we've experimented with the board, but we want to get the host PC involved! 4 | 5 | We'll want to talk to our device over USB. So far, we've interacted with our device like this: 6 | 7 | ## So Far 8 | 9 | ```text 10 | ┌─────────────┐ 11 | │ │ 12 | │ PC │ 13 | │ │ 14 | └─────────────┘ 15 | ▲ 16 | │ USB 17 | ▼ 18 | ┌─────────────┐ 19 | │ │ 20 | │ USB Hub │ 21 | │ │ 22 | └─────────────┘ 23 | ▲ 24 | ┌─USB─┘ 25 | ▼ 26 | ┌───────────┐ ┌───────────┐ 27 | │ MCU │ │ MCU │ 28 | │ (debug) │─────SWD────▶│ (target) │ 29 | └───────────┘ └───────────┘ 30 | ``` 31 | 32 | ## What we Want 33 | 34 | We'll want to enable USB on the target device, so then our diagram looks like this: 35 | 36 | ```text 37 | ┌─────────────┐ 38 | │ │ 39 | │ PC │ 40 | │ │ 41 | └─────────────┘ 42 | ▲ 43 | │ USB 44 | ▼ 45 | ┌─────────────┐ 46 | │ │ 47 | │ USB Hub │ 48 | │ │ 49 | └─────────────┘ 50 | ▲ ▲ 51 | ┌─USB─┘ └─USB─┐ 52 | ▼ ▼ 53 | ┌───────────┐ ┌───────────┐ 54 | │ MCU │ │ MCU │ 55 | │ (debug) │─────SWD────▶│ (target) │ 56 | └───────────┘ └───────────┘ 57 | ``` 58 | 59 | ## Zooming in 60 | 61 | Ignoring the USB hub and the debug MCU, we'll have something a little like this: 62 | 63 | ```text 64 | ┌────────────┐ ┌───────────┐ 65 | │ PC │ │ MCU │ 66 | │ │◀──────USB──────▶│ (target) │ 67 | └────────────┘ └───────────┘ 68 | ``` 69 | 70 | This slides over a lot of detail though! Let's look at it with a little bit more detail: 71 | 72 | ```text 73 | ┌──────┐ ┌──────┐ 74 | ┌┤ PC ├────────────────────┐ ┌┤Target├────────────────────┐ 75 | │└──────┘ │ │└──────┘ │ 76 | │ Rust Application │ │ Rust Application │ 77 | │ Host │◁ ─ ─ ─ ─ ─ ─ ▷│ Target │ 78 | │ │ │ │ 79 | ├────────────────────────────┤ ├────────────────────────────┤ 80 | │ NUSB crate │◁ ─ ─ ─ ─ ─ ─ ▷│ embassy-usb crate │ 81 | ├────────────────────────────┤ ├────────────────────────────┤ 82 | │ Operating System + Drivers │◁ ─ ─ ─ ─ ─ ─ ▷│ embassy-rp drivers │ 83 | ├────────────────────────────┤ ├────────────────────────────┤ 84 | │ USB Hardware │◀─────USB─────▶│ USB Hardware │ 85 | └────────────────────────────┘ └────────────────────────────┘ 86 | ``` 87 | 88 | ### Host Side 89 | 90 | On the host side, there's a couple of main pieces. We'll have our application, which needs to 91 | interact with devices in some way. We'll use the [`nusb` crate], an async friendly library that 92 | manages high level USB interactions. `nusb` manages the interactions with your operating system, 93 | which in turn has drivers for your specific USB hardware. 94 | 95 | [`nusb` crate]: https://docs.rs/nusb/ 96 | 97 | ### Target Side 98 | 99 | Conversely on the target side, things can be a little more diverse depending on the software and 100 | hardware we are using, but in the case of Embassy on the RP2040, your application will interact 101 | with interfaces from the `embassy-usb` crate which describe USB capabilities in a portable and async 102 | way. These capabilities are provided by the USB drivers provided by the `embassy-rp` HAL, which 103 | manages the low level hardware interactions. 104 | 105 | ### Working Together 106 | 107 | At each of these layers, we can conceptually think of each "part" of the PC and Target talking to 108 | each other: 109 | 110 | * The RP2040 USB hardware talks to the PC USB hardware at a physical and electrical level 111 | * Your operating system and drivers talk to the embassy-rp drivers, to exchange messages with each 112 | other 113 | * The `nusb` crate talks to `embassy-usb` to exchange messages, such as USB "Bulk" frames 114 | * Your PC application will want to talk to the Firmware application, using some sort of protocol 115 | 116 | If you come from a networking background, this will look very familiar to the OSI or TCP/IP model, 117 | where we have different layers with different responsibilities. 118 | 119 | USB is a complex topic, and we won't get too deep into it! For the purposes of today's exercise, 120 | we'll focus on USB Bulk Endpoints. These work somewhat similar to "UDP Frames" from networking: 121 | 122 | * Each device can send and receive "Bulk transfers" 123 | * "Bulk transfers" are variable sized, and framed messages. Sort of like sending `[u8]` slices 124 | to each other 125 | * Each device can send messages whenever they feel like, though the PC is "in charge", it decides 126 | when it gives messages to the device, and when it accepts messages from the device 127 | 128 | There are many other ways that USB can work, and a lot of details we are skipping. We aren't using 129 | "standard" USB definitions, like you might use for a USB keyboard, serial ports, or a MIDI device. 130 | 131 | Instead, we are using "raw" USB Bulk frames, like you might do if you were writing a proprietary 132 | device driver for your device. 133 | 134 | ### Something to be desired 135 | 136 | Although the stack we've looked at so far handles all of the "transport", we're lacking any sort 137 | of high level protocol! We can shove chunks of bytes back and forth, but what SHOULD we send, and 138 | when? This is a little like having a raw TCP connection, but no HTTP to "talk" at a higher level! 139 | 140 | We'll need to define two main things: 141 | 142 | * How to interpret our messages, e.g some kind of "wire format" 143 | * Some sort of protocol, e.g. "how we behave" when talking to each other 144 | -------------------------------------------------------------------------------- /book/src/custom.md: -------------------------------------------------------------------------------- 1 | # Custom Applications 2 | 3 | So far, we've mostly treated our firmware as an entity that ONLY behaves on requests from the 4 | client. However, most real world firmware tends to be doing something normally, and the data 5 | connection is only one part of influencing that behavior. 6 | 7 | Consider building some type of application or game you could play WITHOUT sending commands from 8 | your PC, like a guessing game, a visualizer of the potentiometer or accelerometer data, or something 9 | else that allows the user to interact with the board, while still streaming information, or 10 | accepting commands that change some configuration or behavior at runtime. 11 | 12 | This part of the exercise is totally open ended, and please feel free to ask as many questions as 13 | you'd like! I can't wait to see what you build! 14 | 15 | Thank you for participating in this workshop. 16 | 17 | I hope to hear from you all in the future! 18 | 19 | James Munns 20 | 21 | [onevariable.com](onevariable.com) 22 | -------------------------------------------------------------------------------- /book/src/endpoints.md: -------------------------------------------------------------------------------- 1 | # Endpoints 2 | 3 | We're now entering the more "self directed" part of the workshop! Feel free to ask as many questions 4 | as you'd like, or build the things YOU want to! 5 | 6 | A great place to start is by building various endpoints for the different sensors on the board. 7 | 8 | For now we'll only focus on Endpoints and address Topics later, but feel free to go ahead if you'd 9 | like! 10 | 11 | Some questions to think about include: 12 | 13 | * What kind of endpoints, or logical requests make sense for the different parts of the board? 14 | * What kind of data makes sense for a request? Not all requests need to include data! 15 | * What kind of data makes sense for a response? Not all responses need to include data! 16 | * When should we use built in types, like `bool` or `i32`, and when would it make sense to define 17 | our own types? 18 | * Should our endpoints use blocking handlers? async handlers? 19 | 20 | Don't forget, we have lots of parts on our board, and example code for interacting with: 21 | 22 | * Buttons 23 | * Potentiometer Dial 24 | * RGB LEDs 25 | * Accelerometer (X, Y, Z) 26 | 27 | ## Host side 28 | 29 | You can definitely start with a basic "demo" app that just makes one kind of request, or sends 30 | request every N milliseconds, X times. 31 | 32 | One easy way to make an interactive program is by making a "REPL", or a "Read, Evaluate, Print, 33 | Loop" program. An easy way to do this is using a structure like this: 34 | 35 | ```rust 36 | #[tokio::main] 37 | async fn main() { 38 | // Begin repl... 39 | loop { 40 | print!("> "); 41 | stdout().flush().unwrap(); 42 | let line = read_line().await; 43 | let parts: Vec<&str> = line.split_whitespace().collect(); 44 | match parts.as_slice() { 45 | ["ping"] => { 46 | let ping = client.ping(42).await.unwrap(); 47 | println!("Got: {ping}."); 48 | } 49 | ["ping", n] => { 50 | let Ok(idx) = n.parse::() else { 51 | println!("Bad u32: '{n}'"); 52 | continue; 53 | }; 54 | let ping = client.ping(idx).await.unwrap(); 55 | println!("Got: {ping}."); 56 | } 57 | other => { 58 | println!("Error, didn't understand '{other:?};"); 59 | } 60 | } 61 | } 62 | } 63 | 64 | async fn read_line() -> String { 65 | tokio::task::spawn_blocking(|| { 66 | let mut line = String::new(); 67 | std::io::stdin().read_line(&mut line).unwrap(); 68 | line 69 | }) 70 | .await 71 | .unwrap() 72 | } 73 | ``` 74 | 75 | You can very quickly build something that feels like a scripting interface, and is usually very 76 | natural feeling for tech-oriented users. These tend to be VERY valuable tools to have when doing 77 | board bringup, or even early factory testing! 78 | 79 | Of course, you could also make a command line interface using a crate like `clap`, or even a GUI 80 | application if you know how to already! 81 | -------------------------------------------------------------------------------- /book/src/hello-acc.md: -------------------------------------------------------------------------------- 1 | # Accelerometer 2 | 3 | Our last sensor is a 3-axis Accelerometer. It has many more features than we'll use during the 4 | exercise, but it can read acceleration in three axis: X, Y, and Z. 5 | 6 | It reads accleration, e.g. due to gravity, as a positive number. It can measure up to 8g of 7 | acceleration, returning `i16::MAX` for 8.0g, or `4096` as 1.0g, or `-4096` for -1.0g. 8 | 9 | If the board is sitting level, it should read approximately: 10 | 11 | * x: 0 12 | * y: 0 13 | * z: 4096 14 | 15 | If you tilt the board so the potentometer is facing RIGHT, it should read approximately: 16 | 17 | * x: 4096 18 | * y: 0 19 | * z: 0 20 | 21 | If you tilt the board so the potentiometer is facing AWAY from you, it should read approximately: 22 | 23 | * x: 0 24 | * y: 4096 25 | * z: 0 26 | 27 | ## Running the code 28 | 29 | We can start the code by running the `hello-03` project. It will begin immediately printing out 30 | acceleration values at 4Hz. 31 | 32 | ```sh 33 | cargo run --release --bin hello-03 34 | Finished release [optimized + debuginfo] target(s) in 0.10s 35 | Running `probe-rs run --chip RP2040 --speed 12000 --protocol swd target/thumbv6m-none-eabi/release/hello-03` 36 | Erasing ✔ [00:00:00] [######################################################] 40.00 KiB/40.00 KiB @ 75.76 KiB/s (eta 0s ) 37 | Programming ✔ [00:00:00] [#####################################################] 40.00 KiB/40.00 KiB @ 132.75 KiB/s (eta 0s ) Finished in 0.841s 38 | WARN defmt_decoder::log::format: logger format contains timestamp but no timestamp implementation was provided; consider removing the timestamp (`{t}` or `{T}`) from the logger format or provide a `defmt::timestamp!` implementation 39 | 0.000976 INFO Start 40 | └─ hello_03::____embassy_main_task::{async_fn#0} @ src/bin/hello-03.rs:38 41 | 0.002457 INFO id: E4629076D3222C21 42 | └─ hello_03::____embassy_main_task::{async_fn#0} @ src/bin/hello-03.rs:42 43 | 0.253936 INFO accelerometer: AccelReading { x: -80, y: 0, z: 4064 } 44 | └─ hello_03::__accel_task_task::{async_fn#0} @ src/bin/hello-03.rs:82 45 | 0.503916 INFO accelerometer: AccelReading { x: -112, y: -16, z: 4048 } 46 | └─ hello_03::__accel_task_task::{async_fn#0} @ src/bin/hello-03.rs:82 47 | 0.753916 INFO accelerometer: AccelReading { x: -112, y: 16, z: 4048 } 48 | └─ hello_03::__accel_task_task::{async_fn#0} @ src/bin/hello-03.rs:82 49 | ``` 50 | 51 | ## Reading the code 52 | 53 | Similar to the other exercises, we've added some new tasks: 54 | 55 | ```rust 56 | // in main 57 | let accel = Accelerometer::new( 58 | p.SPI0, p.PIN_6, p.PIN_7, p.PIN_4, p.PIN_5, p.DMA_CH1, p.DMA_CH2, 59 | ) 60 | .await; 61 | 62 | // as a task 63 | #[embassy_executor::task] 64 | async fn accel_task(mut accel: Accelerometer) { 65 | let mut ticker = Ticker::every(Duration::from_millis(250)); 66 | loop { 67 | ticker.next().await; 68 | let reading = accel.read().await; 69 | info!("accelerometer: {:?}", reading); 70 | } 71 | } 72 | ``` 73 | 74 | One thing to note is that the constructor, `Accelerometer::new()` is an `async` function. This is 75 | because the driver establishes the connection, and ensures we are talking to the accelerometer 76 | using async SPI methods. 77 | 78 | You can access the raw driver through the [`lis3dh-async` crate](https://docs.rs/lis3dh-async). 79 | 80 | We have also wired up the accelerometer's interrupt pins, which can serve as a "notification" when 81 | some event has happened, however we will not use that as part of the exercise today. 82 | -------------------------------------------------------------------------------- /book/src/hello-gpios.md: -------------------------------------------------------------------------------- 1 | # Buttons and Potentiometer 2 | 3 | ## Running the code 4 | 5 | We'll move on to the next project, `hello-02`. Let's start by running the project: 6 | 7 | ```sh 8 | cargo run --release --bin hello-02 9 | Finished release [optimized + debuginfo] target(s) in 0.10s 10 | Running `probe-rs run --chip RP2040 --speed 12000 --protocol swd target/thumbv6m-none-eabi/release/hello-02` 11 | Erasing ✔ [00:00:00] [######################################################] 28.00 KiB/28.00 KiB @ 74.12 KiB/s (eta 0s ) 12 | Programming ✔ [00:00:00] [#####################################################] 28.00 KiB/28.00 KiB @ 115.92 KiB/s (eta 0s ) Finished in 0.631s 13 | WARN defmt_decoder::log::format: logger format contains timestamp but no timestamp implementation was provided; consider removing the timestamp (`{t}` or `{T}`) from the logger format or provide a `defmt::timestamp!` implementation 14 | 0.000962 INFO Start 15 | └─ hello_02::____embassy_main_task::{async_fn#0} @ src/bin/hello-02.rs:35 16 | 0.002448 INFO id: E4629076D3222C21 17 | └─ hello_02::____embassy_main_task::{async_fn#0} @ src/bin/hello-02.rs:39 18 | ``` 19 | 20 | You can now start pressing buttons, and should see corresponding logs every time you press or 21 | release a one of the eight buttons on the board: 22 | 23 | ```text 24 | 0.732839 INFO Buttons changed: [false, true, false, false, false, false, false, false] 25 | └─ hello_02::__button_task_task::{async_fn#0} @ src/bin/hello-02.rs:68 26 | 1.112836 INFO Buttons changed: [false, false, false, false, false, false, false, false] 27 | └─ hello_02::__button_task_task::{async_fn#0} @ src/bin/hello-02.rs:68 28 | 2.192836 INFO Buttons changed: [true, false, false, false, false, false, false, false] 29 | └─ hello_02::__button_task_task::{async_fn#0} @ src/bin/hello-02.rs:68 30 | 2.562838 INFO Buttons changed: [false, false, false, false, false, false, false, false] 31 | ``` 32 | 33 | You'll also see the potentiometer value as you turn the dial left and right: 34 | 35 | ```text 36 | 1.602946 INFO Potentiometer changed: 1922 37 | └─ hello_02::__pot_task_task::{async_fn#0} @ src/bin/hello-02.rs:86 38 | 2.702941 INFO Potentiometer changed: 2666 39 | └─ hello_02::__pot_task_task::{async_fn#0} @ src/bin/hello-02.rs:86 40 | 3.802943 INFO Potentiometer changed: 4088 41 | └─ hello_02::__pot_task_task::{async_fn#0} @ src/bin/hello-02.rs:86 42 | 5.602941 INFO Potentiometer changed: 3149 43 | └─ hello_02::__pot_task_task::{async_fn#0} @ src/bin/hello-02.rs:86 44 | 5.702941 INFO Potentiometer changed: 2904 45 | └─ hello_02::__pot_task_task::{async_fn#0} @ src/bin/hello-02.rs:86 46 | 6.402940 INFO Potentiometer changed: 1621 47 | └─ hello_02::__pot_task_task::{async_fn#0} @ src/bin/hello-02.rs:86 48 | 6.502941 INFO Potentiometer changed: 1444 49 | └─ hello_02::__pot_task_task::{async_fn#0} @ src/bin/hello-02.rs:86 50 | 7.002944 INFO Potentiometer changed: 17 51 | └─ hello_02::__pot_task_task::{async_fn#0} @ src/bin/hello-02.rs:86 52 | ``` 53 | 54 | ## Reading the code 55 | 56 | We've added a little more code to main: 57 | 58 | ```rust 59 | let buttons = Buttons::new( 60 | p.PIN_0, p.PIN_1, p.PIN_2, p.PIN_3, p.PIN_18, p.PIN_19, p.PIN_20, p.PIN_21, 61 | ); 62 | let potentiometer = Potentiometer::new(p.ADC, p.PIN_26); 63 | 64 | // Start the Button task 65 | spawner.must_spawn(button_task(buttons)); 66 | 67 | // Start the Potentiometer task 68 | spawner.must_spawn(pot_task(potentiometer)); 69 | ``` 70 | 71 | And two new tasks: 72 | 73 | ```rust 74 | // This is our Button task 75 | #[embassy_executor::task] 76 | async fn button_task(buttons: Buttons) { 77 | let mut last = [false; Buttons::COUNT]; 78 | let mut ticker = Ticker::every(Duration::from_millis(10)); 79 | loop { 80 | ticker.next().await; 81 | let now = buttons.read_all(); 82 | if now != last { 83 | info!("Buttons changed: {:?}", now); 84 | last = now; 85 | } 86 | } 87 | } 88 | 89 | // This is our Potentiometer task 90 | #[embassy_executor::task] 91 | async fn pot_task(mut pot: Potentiometer) { 92 | let mut last = pot.read().await; 93 | let mut ticker = Ticker::every(Duration::from_millis(100)); 94 | loop { 95 | ticker.next().await; 96 | let now = pot.read().await; 97 | if now.abs_diff(last) > 64 { 98 | info!("Potentiometer changed: {=u16}", now); 99 | last = now; 100 | } 101 | } 102 | } 103 | ``` 104 | 105 | Both of these store the last state measured, so that we don't flood the logs too much. 106 | 107 | Again, you can try to customize these a bit before moving forward. 108 | -------------------------------------------------------------------------------- /book/src/hello-smartleds.md: -------------------------------------------------------------------------------- 1 | # SmartLEDs 2 | 3 | ## Flashing the project 4 | 5 | Let's go ahead and run our first program on the board. You can plug in your board to your PC. 6 | 7 | You should see the USB lights flash briefly, then the SmartLEDs should begin pulsing a red color. 8 | 9 | It should look like this: 10 | 11 | ![red lights](./ovtwin-uhoh-00.jpg) 12 | 13 | Now, from the same terminal you built from, we'll go ahead and run the binary: 14 | 15 | ```sh 16 | $ cargo run --release --bin hello-01 17 | Finished release [optimized + debuginfo] target(s) in 0.13s 18 | Running `probe-rs run --chip RP2040 --speed 12000 --protocol swd target/thumbv6m-none-eabi/release/hello-01` 19 | Erasing ✔ [00:00:00] [######################################################] 28.00 KiB/28.00 KiB @ 72.85 KiB/s (eta 0s ) 20 | Programming ✔ [00:00:00] [#####################################################] 28.00 KiB/28.00 KiB @ 126.26 KiB/s (eta 0s ) Finished in 0.617s 21 | WARN defmt_decoder::log::format: logger format contains timestamp but no timestamp implementation was provided; consider removing the timestamp (`{t}` or `{T}`) from the logger format or provide a `defmt::timestamp!` implementation 22 | 0.000963 INFO Start 23 | └─ hello_01::____embassy_main_task::{async_fn#0} @ src/bin/hello-01.rs:29 24 | 0.002450 INFO id: E4629076D3222C21 25 | └─ hello_01::____embassy_main_task::{async_fn#0} @ src/bin/hello-01.rs:33 26 | ``` 27 | 28 | The LEDs should change to a green pattern that moves around the board. It should now look like this: 29 | 30 | ![green lights](./ovtwin-hello-01.jpg) 31 | 32 | You can ignore the error that says: 33 | 34 | ```text 35 | WARN defmt_decoder::log::format: ... 36 | ``` 37 | 38 | If you got a different error, make sure you followed the [Setup steps](./setup.md), and let me know 39 | if you are stuck! 40 | 41 | ## Looking at the code 42 | 43 | Let's look at the code in detail together, starting with the imports: 44 | 45 | ```rust 46 | #![no_std] 47 | #![no_main] 48 | 49 | use defmt::info; 50 | use embassy_executor::Spawner; 51 | use embassy_rp::{ 52 | peripherals::PIO0, 53 | pio::Pio, 54 | }; 55 | 56 | use embassy_time::{Duration, Ticker}; 57 | 58 | use smart_leds::RGB; 59 | use workbook_fw::{ 60 | get_unique_id, 61 | ws2812::{self, Ws2812}, 62 | NUM_SMARTLEDS, 63 | }; 64 | ``` 65 | 66 | We are using `defmt` for logging. This allows us to get logs over our debugging interface. You 67 | should see the INFO logs in the firmware already. 68 | 69 | We are also using various `embassy` crates to write our async firmware. 70 | 71 | We've also provided a couple of drivers and helpers specific to our board in the `workbook_fw` 72 | crate. These can be found in the `src/lib.rs` of our current crate. 73 | 74 | Looking next at our main: 75 | 76 | ```rust 77 | #[embassy_executor::main] 78 | async fn main(spawner: Spawner) { 79 | // SYSTEM INIT 80 | info!("Start"); 81 | 82 | let mut p = embassy_rp::init(Default::default()); 83 | let unique_id = get_unique_id(&mut p.FLASH).unwrap(); 84 | info!("id: {=u64:016X}", unique_id); 85 | 86 | // PIO/WS2812 INIT 87 | let Pio { 88 | mut common, sm0, .. 89 | } = Pio::new(p.PIO0, ws2812::Irqs); 90 | 91 | // GPIO25 is used for Smart LEDs 92 | let ws2812: Ws2812<'static, PIO0, 0, NUM_SMARTLEDS> = 93 | Ws2812::new(&mut common, sm0, p.DMA_CH0, p.PIN_25); 94 | 95 | // Start the LED task 96 | spawner.must_spawn(led_task(ws2812)); 97 | } 98 | ``` 99 | 100 | We have the entrypoint for the Embassy executor, and set up our WS2812 driver, used for our Smart 101 | LEDs. The SmartLEDs are connected on Pin 25 of the board. 102 | 103 | ```rust 104 | // This is our LED task 105 | #[embassy_executor::task] 106 | async fn led_task(mut ws2812: Ws2812<'static, PIO0, 0, NUM_SMARTLEDS>) { 107 | // Tick every 100ms 108 | let mut ticker = Ticker::every(Duration::from_millis(100)); 109 | let mut idx = 0; 110 | loop { 111 | // Wait for the next update time 112 | ticker.next().await; 113 | 114 | let mut colors = [colors::BLACK; NUM_SMARTLEDS]; 115 | 116 | // A little iterator trickery to pick a moving set of four LEDs 117 | // to light up 118 | let (before, after) = colors.split_at_mut(idx); 119 | after 120 | .iter_mut() 121 | .chain(before.iter_mut()) 122 | .take(4) 123 | .for_each(|l| { 124 | // The LEDs are very bright! 125 | *l = colors::GREEN / 16; 126 | }); 127 | 128 | ws2812.write(&colors).await; 129 | idx += 1; 130 | if idx >= NUM_SMARTLEDS { 131 | idx = 0; 132 | } 133 | } 134 | } 135 | ``` 136 | 137 | The `ws2812.write()` function takes an array of color values. Each color of Red, Green, and Blue 138 | have a value of `0` to `255`. Be careful, the LEDs are **very bright**! The green pattern you see 139 | is at 1/16th brightness, don't blind yourself! 140 | 141 | We've written some code that moves a green pattern. You can try customizing this pattern for a bit 142 | before we move on to the other parts of the code. 143 | -------------------------------------------------------------------------------- /book/src/hello.md: -------------------------------------------------------------------------------- 1 | # Hello, ov-twin! 2 | 3 | ## Cloning the repo 4 | 5 | Let's start by cloning the project folder: 6 | 7 | ```sh 8 | $ git clone https://github.com/OneVariable/ov-twin-fw 9 | $ cd ov-twin-fw 10 | ``` 11 | 12 | All of the software we'll need for both the **Host** (your PC), and the **Target** (the RP2040) is 13 | in the `source/` folder. 14 | 15 | Let's move to the "workbook/firmware" project. Note that this is NOT a workspace, so you may need to 16 | launch your editor here. We'll explain the other parts of the project later. 17 | 18 | ```sh 19 | $ cd source/workbook/firmware 20 | $ ls -lah 21 | total 128 22 | -rw-r--r-- 1 james staff 48K May 3 10:11 Cargo.lock 23 | -rw-r--r-- 1 james staff 3.1K May 3 10:11 Cargo.toml 24 | -rw-r--r-- 1 james staff 1.5K May 3 10:11 build.rs 25 | -rw-r--r-- 1 james staff 678B May 3 10:11 memory.x 26 | drwxr-xr-x 4 james staff 128B May 3 10:11 src 27 | ``` 28 | 29 | ## Build a project 30 | 31 | We'll be building a project one at a time, from the `src/bin` folder. You can peek ahead if you'd 32 | like, but there might be spoilers! 33 | 34 | We'll start by building the first project, `hello-01`. This may take a bit if it's your first build, 35 | or if the internet is a little slow: 36 | 37 | ```sh 38 | $ cargo build --release --bin hello-01 39 | Compiling proc-macro2 v1.0.79 40 | Compiling unicode-ident v1.0.12 41 | Compiling syn v1.0.109 42 | Compiling version_check v0.9.4 43 | Compiling defmt v0.3.6 44 | ... 45 | Compiling fixed-macro-types v1.2.0 46 | Compiling fixed-macro v1.2.0 47 | Compiling pio-proc v0.2.2 48 | Finished release [optimized + debuginfo] target(s) in 16.44s 49 | ``` 50 | 51 | If you got an error, make sure you followed the [Setup steps](./setup.md), and let me know if you 52 | are stuck! 53 | 54 | We'll now work through all of the sensors on the board, so you can see how to interact with them. 55 | 56 | We won't focus too much on how the drivers of these sensors were written, as that's outside the 57 | scope of this workshop. Feel free to ask questions though! 58 | -------------------------------------------------------------------------------- /book/src/internection-fw.md: -------------------------------------------------------------------------------- 1 | # Back to the firmware 2 | 3 | We can now take a look at the `comms-01` project, in the `firmware` folder. 4 | 5 | We've taken away most of the driver code, and replaced it with the code we need to set up our 6 | RP2040's `postcard-rpc` setup. 7 | 8 | ## Setup and Run 9 | 10 | In our `main`, we've added this code: 11 | 12 | ```rust 13 | let driver = usb::Driver::new(p.USB, Irqs); 14 | let mut config = example_config(); 15 | config.manufacturer = Some("OneVariable"); 16 | config.product = Some("ov-twin"); 17 | let buffers = ALL_BUFFERS.take(); 18 | let (device, ep_in, ep_out) = configure_usb(driver, &mut buffers.usb_device, config); 19 | let dispatch = Dispatcher::new(&mut buffers.tx_buf, ep_in, Context {}); 20 | 21 | spawner.must_spawn(dispatch_task(ep_out, dispatch, &mut buffers.rx_buf)); 22 | spawner.must_spawn(usb_task(device)); 23 | ``` 24 | 25 |
26 | 27 | Let's break this down piece by piece: 28 | 29 | ```rust 30 | let driver = usb::Driver::new(p.USB, Irqs); 31 | ``` 32 | 33 | This line is straight out of `embassy-rp`, it just sets up the hardware and interrupt handlers 34 | needed to manage the USB hardware at a low level. You would do this for any `embassy-rp` project 35 | using USB. 36 | 37 |
38 | 39 | Next up, we handle some configuration: 40 | 41 | ```rust 42 | let mut config = example_config(); 43 | config.manufacturer = Some("OneVariable"); 44 | config.product = Some("ov-twin"); 45 | ``` 46 | 47 | `example_config()` is a function from the `postcard_rpc::target_server` module. This takes the 48 | configuration structure provided by `embassy-usb`, and customizes it in a standard way. This 49 | looks like this: 50 | 51 | ```rust 52 | pub fn example_config() -> embassy_usb::Config<'static> { 53 | // Create embassy-usb Config 54 | let mut config = embassy_usb::Config::new(0x16c0, 0x27DD); 55 | config.manufacturer = Some("Embassy"); 56 | config.product = Some("USB example"); 57 | config.serial_number = Some("12345678"); 58 | 59 | // Required for windows compatibility. 60 | // https://developer.nordicsemi.com/nRF_Connect_SDK/doc/1.9.1/kconfig/CONFIG_CDC_ACM_IAD.html#help 61 | config.device_class = 0xEF; 62 | config.device_sub_class = 0x02; 63 | config.device_protocol = 0x01; 64 | config.composite_with_iads = true; 65 | 66 | config 67 | } 68 | ``` 69 | 70 | We then overwrite the `manufacturer` and `product` fields to something specific for our exercise. 71 | 72 |
73 | 74 | Then, we continue configuring the RP2040's USB hardware: 75 | 76 | ```rust 77 | let buffers = ALL_BUFFERS.take(); 78 | let (device, ep_in, ep_out) = configure_usb(driver, &mut buffers.usb_device, config); 79 | let dispatch = Dispatcher::new(&mut buffers.tx_buf, ep_in, Context {}); 80 | ``` 81 | 82 | These lines do three things: 83 | 84 | * We take some static data buffers that `postcard_rpc` needs for USB communication, as well as 85 | for serializing and deserializing messages. 86 | * `configure_usb`, a function from `postcard_rpc::target_server` configures the USB: 87 | * It applies the `config` that we just prepared 88 | * It configures the low level drivers using the `embassy-usb` interfaces 89 | * It gives us back three things: 90 | * the `device`, which is a task that needs to be run to maintain the low level USB 91 | driver pieces 92 | * `ep_in`, our USB "Bulk Endpoint", in the In (to the PC) direction 93 | * `ep_out`, our USB "Bulk Endpoint", in the Out (to the MCU) direction 94 | * We set up a `Dispatcher` (more on this below), giving it the buffers, the `ep_in`, and a struct 95 | called `Context` 96 | 97 |
98 | 99 | Then, we spawn two tasks: 100 | 101 | ```rust 102 | spawner.must_spawn(dispatch_task(ep_out, dispatch, &mut buffers.rx_buf)); 103 | spawner.must_spawn(usb_task(device)); 104 | ``` 105 | 106 | Which look like this, basically "just go run forever": 107 | 108 | ```rust 109 | /// This actually runs the dispatcher 110 | #[embassy_executor::task] 111 | async fn dispatch_task( 112 | ep_out: Endpoint<'static, USB, Out>, 113 | dispatch: Dispatcher, 114 | rx_buf: &'static mut [u8], 115 | ) { 116 | rpc_dispatch(ep_out, dispatch, rx_buf).await; 117 | } 118 | 119 | /// This handles the low level USB management 120 | #[embassy_executor::task] 121 | pub async fn usb_task(mut usb: UsbDevice<'static, Driver<'static, USB>>) { 122 | usb.run().await; 123 | } 124 | ``` 125 | 126 | Hopefully, this all makes sense covering the "setup" and "run" parts of getting the postcard-rpc 127 | stack going: 128 | 129 | * We setup the low level hardware, from the `embassy-rp` drivers 130 | * We have a helper function that configures the `embassy-usb` components 131 | * We hand those pieces to *something* from `postcard-rpc`, that uses the `embassy-usb` components 132 | 133 | Let's scroll back up to the top of the firmware and see what we skipped: 134 | 135 | ## Defining our protocol 136 | 137 | At the top of `comms-01`, there's some interesting looking code: 138 | 139 | ```rust 140 | static ALL_BUFFERS: ConstInitCell> = 141 | ConstInitCell::new(AllBuffers::new()); 142 | 143 | pub struct Context {} 144 | 145 | define_dispatch! { 146 | dispatcher: Dispatcher< 147 | Mutex = ThreadModeRawMutex, 148 | Driver = usb::Driver<'static, USB>, 149 | Context = Context 150 | >; 151 | PingEndpoint => blocking ping_handler, 152 | } 153 | ``` 154 | 155 | The first part with `ALL_BUFFERS` we've explained a bit: we're using the `ConstInitCell` from 156 | the `static_cell` crate to create a "single use" set of buffers that have static lifetime. 157 | 158 | The three `256` values are the size in bytes we give for various parts of the USB and postcard-rpc 159 | stack. 160 | 161 | We then define a struct called `Context` with no fields. We'll look into this more soon! 162 | 163 | Finally, we call a slightly weird macro called `define_dispatch!`. This comes from the 164 | `postcard-rpc` crate, and we'll break that down a bit. 165 | 166 | ```rust 167 | dispatcher: Dispatcher< 168 | Mutex = ThreadModeRawMutex, 169 | Driver = usb::Driver<'static, USB>, 170 | Context = Context 171 | >; 172 | ``` 173 | 174 | First, since `postcard-rpc` can work with ANY device that works with `embassy-usb`, we need to 175 | define which types we are using, so the macro can create a **Dispatcher** type for us. The 176 | dispatcher has a couple of responsibilities: 177 | 178 | * When we RECEIVE a **Request**, it figures out what *kind* of message it is, and passes that 179 | message on to a handler, if it knows about that kind of Request. 180 | * If we pass on the message to the handler, we need to **deserialize** the message, so that the 181 | handler doesn't need to manage that 182 | * When that handler completes, it will return a **Response**. The Dispatcher will then serialize 183 | that response, and send it back over USB. 184 | * If an error ever occurs, for example if we ever got a message kind we don't understand, or if 185 | deserialization failed due to message corruption, the dispatcher will automatically send back 186 | an error response. 187 | 188 | > NOTE: this macro *looks* like it's using "associated type" syntax, but it's not really, it's just 189 | macro syntax, so don't read too much into it! 190 | 191 | How does this `Dispatcher` know all the kinds of messages it needs to handle? That's what the next 192 | part is for: 193 | 194 | ```rust 195 | PingEndpoint => blocking ping_handler, 196 | ``` 197 | 198 | This is saying: 199 | 200 | * Whenever we get a message on the `PingEndpoint` 201 | * Decode it, and pass it to the `blocking` function called `ping_handler` 202 | 203 | If we look lower in our code, we'll find a function that looks like this: 204 | 205 | ```rust 206 | fn ping_handler(_context: &mut Context, header: WireHeader, rqst: u32) -> u32 { 207 | info!("ping: seq - {=u32}", header.seq_no); 208 | rqst 209 | } 210 | ``` 211 | 212 | This handler will be called whenever we receive a `PingEndpoint` request. All `postcard-rpc` 213 | take these three arguments: 214 | 215 | * A `&mut` reference to the `Context` type we defined in `define_dispatch`, you can put anything 216 | you like in this `Context` type! 217 | * The `header` of the request, this includes the Key and sequence number of the request 218 | * The `rqst`, which will be whatever the `Request` type of this `Endpoint` is 219 | 220 | This function also returns exactly one thing: whatever the `Response` type of this endpoint. 221 | 222 | We can see that our `ping_handler` will return whatever value it received without modification, and 223 | log the sequence number that we saw. 224 | 225 | And that's all that's necessary on the firmware side! 226 | -------------------------------------------------------------------------------- /book/src/internection-host.md: -------------------------------------------------------------------------------- 1 | # Host side 2 | 3 | In `workbook-host/src/client.rs`, there's two important parts we need to look at: 4 | 5 | ```rust 6 | impl WorkbookClient { 7 | pub fn new() -> Self { 8 | let client = 9 | HostClient::new_raw_nusb(|d| d.product_string() == Some("ov-twin"), ERROR_PATH, 8); 10 | Self { client } 11 | } 12 | // ... 13 | } 14 | ``` 15 | 16 | `postcard-rpc` provides a `HostClient` struct that handles the PC side of communication. 17 | 18 | Here, we tell it that we want to use the "raw_nusb" transport, which takes a closure it uses 19 | to find the relevant USB device we want to connect to. Here, we just look for the first device 20 | with a product string of "ov-twin", which we configured in the firmware. You might need something 21 | smarter if you expect to have more than one device attached at a time! 22 | 23 | `HostClient` allows for custom paths for errors, and allows you to configure the number of 24 | "in flight" requests at once. We don't need to worry about those for now. 25 | 26 | We then have a method called `ping`: 27 | 28 | ```rust 29 | pub async fn ping(&self, id: u32) -> Result> { 30 | let val = self.client.send_resp::(&id).await?; 31 | Ok(val) 32 | } 33 | ``` 34 | 35 | The main method here is `HostClient::send_resp`, which takes the `Endpoint` as a generic argument, 36 | which lets it know that it should take `Request` as an argument, and will return `Result`. 37 | 38 | The `Err` part of the `Result` is a little tricky, but this comes from the fact that errors can come 39 | from three different "layers": 40 | 41 | * A USB error, e.g. if the USB device disconnects or crashes 42 | * A `postcard-rpc` "transport" error, e.g. if the device replies "I don't know that Endpoint". 43 | * (optional) if the Response type is `Result`, we can "flatten" that error so that if we 44 | receive a message, but it's an `Err`, we can return that error. See the `FlattenErr` trait for 45 | how we do this. 46 | 47 | Finally, in `workbook-host/src/bin/comms-01.rs`, we have a binary that uses this client: 48 | 49 | ```rust 50 | #[tokio::main] 51 | pub async fn main() { 52 | let client = WorkbookClient::new(); 53 | let mut ticker = interval(Duration::from_millis(250)); 54 | 55 | for i in 0..10 { 56 | ticker.tick().await; 57 | print!("Pinging with {i}... "); 58 | let res = client.ping(i).await.unwrap(); 59 | println!("got {res}!"); 60 | assert_eq!(res, i); 61 | } 62 | } 63 | ``` 64 | 65 | Give it a try! 66 | 67 | In your firmware terminal, run `cargo run --release --bin comms-01`, and in your host terminal, 68 | run `cargo run --release --bin comms-01` as well. 69 | 70 | On the host side, you should see: 71 | 72 | ```sh 73 | $ cargo run --release --bin comms-01 74 | Compiling workbook-host-client v0.1.0 (/Users/james/onevariable/ovtwin-fw/source/workbook/workbook-host) 75 | Finished release [optimized] target(s) in 0.33s 76 | Running `target/release/comms-01` 77 | Pinging with 0... got 0! 78 | Pinging with 1... got 1! 79 | Pinging with 2... got 2! 80 | Pinging with 3... got 3! 81 | Pinging with 4... got 4! 82 | Pinging with 5... got 5! 83 | Pinging with 6... got 6! 84 | Pinging with 7... got 7! 85 | Pinging with 8... got 8! 86 | Pinging with 9... got 9! 87 | ``` 88 | 89 | On the target side, you should see: 90 | 91 | ```sh 92 | $ cargo run --release --bin comms-01 93 | Finished release [optimized + debuginfo] target(s) in 0.09s 94 | Running `probe-rs run --chip RP2040 --speed 12000 --protocol swd target/thumbv6m-none-eabi/release/comms-01` 95 | Erasing ✔ [00:00:00] [######################################################] 44.00 KiB/44.00 KiB @ 75.88 KiB/s (eta 0s ) 96 | Programming ✔ [00:00:00] [#####################################################] 44.00 KiB/44.00 KiB @ 126.11 KiB/s (eta 0s ) Finished in 0.94s 97 | WARN defmt_decoder::log::format: logger format contains timestamp but no timestamp implementation was provided; consider removing the timestamp (`{t}` or `{T}`) from the logger format or provide a `defmt::timestamp!` implementation 98 | 0.000985 INFO Start 99 | └─ comms_01::____embassy_main_task::{async_fn#0} @ src/bin/comms-01.rs:41 100 | 0.002458 INFO id: E4629076D3222C21 101 | └─ comms_01::____embassy_main_task::{async_fn#0} @ src/bin/comms-01.rs:45 102 | 0.003023 INFO USB: config_descriptor used: 40 103 | └─ embassy_usb::builder::{impl#1}::build @ /Users/james/.cargo/git/checkouts/embassy-69e86c528471812c/0d0d8e1/embassy-usb/src/fmt.rs:143 104 | 0.003057 INFO USB: bos_descriptor used: 40 105 | └─ embassy_usb::builder::{impl#1}::build @ /Users/james/.cargo/git/checkouts/embassy-69e86c528471812c/0d0d8e1/embassy-usb/src/fmt.rs:143 106 | 0.003081 INFO USB: msos_descriptor used: 162 107 | └─ embassy_usb::builder::{impl#1}::build @ /Users/james/.cargo/git/checkouts/embassy-69e86c528471812c/0d0d8e1/embassy-usb/src/fmt.rs:143 108 | 0.003106 INFO USB: control_buf size: 64 109 | └─ embassy_usb::builder::{impl#1}::build @ /Users/james/.cargo/git/checkouts/embassy-69e86c528471812c/0d0d8e1/embassy-usb/src/fmt.rs:143 110 | 0.444093 DEBUG SET_CONFIGURATION: configured 111 | └─ embassy_usb::{impl#2}::handle_control_out @ /Users/james/.cargo/git/checkouts/embassy-69e86c528471812c/0d0d8e1/embassy-usb/src/fmt.rs:130 112 | 260.649991 INFO ping: seq - 0 113 | └─ comms_01::ping_handler @ src/bin/comms-01.rs:77 114 | 260.904715 INFO ping: seq - 1 115 | └─ comms_01::ping_handler @ src/bin/comms-01.rs:77 116 | 261.154425 INFO ping: seq - 2 117 | └─ comms_01::ping_handler @ src/bin/comms-01.rs:77 118 | 261.405078 INFO ping: seq - 3 119 | └─ comms_01::ping_handler @ src/bin/comms-01.rs:77 120 | 261.651749 INFO ping: seq - 4 121 | └─ comms_01::ping_handler @ src/bin/comms-01.rs:77 122 | 261.900945 INFO ping: seq - 5 123 | └─ comms_01::ping_handler @ src/bin/comms-01.rs:77 124 | 262.154443 INFO ping: seq - 6 125 | └─ comms_01::ping_handler @ src/bin/comms-01.rs:77 126 | 262.405163 INFO ping: seq - 7 127 | └─ comms_01::ping_handler @ src/bin/comms-01.rs:77 128 | 262.653731 INFO ping: seq - 8 129 | └─ comms_01::ping_handler @ src/bin/comms-01.rs:77 130 | 262.902596 INFO ping: seq - 9 131 | └─ comms_01::ping_handler @ src/bin/comms-01.rs:77 132 | ``` 133 | 134 | Hooray! We have [internection]! 135 | 136 | [internection]: https://en.wiktionary.org/wiki/internection 137 | 138 | -------------------------------------------------------------------------------- /book/src/internection-icd.md: -------------------------------------------------------------------------------- 1 | # Interface Control Document 2 | 3 | An [Interface Control Document], or ICD, is a systems engineering term for the definition of an 4 | interface for some kind of system. 5 | 6 | [Interface Control Document]: https://en.wikipedia.org/wiki/Interface_control_document 7 | 8 | In our system, it defines the "project specific" bits of how our two systems will talk to 9 | each other. To start off, there's not much in our `workshop-icd` project. We define a single 10 | `postcard_rpc::Endpoint`, as we read about in the previous section: 11 | 12 | ```rust 13 | endpoint!(PingEndpoint, u32, u32, "ping"); 14 | ``` 15 | 16 | This declares an endpoint, `PingEndpoint`, that takes a `u32` as a Request, a `u32` as a Response, 17 | and a path of "ping". 18 | 19 | Next, let's look at our next firmware project, `comms-01`. 20 | -------------------------------------------------------------------------------- /book/src/internection.md: -------------------------------------------------------------------------------- 1 | # Interacting with the board 2 | 3 | We'll need to zoom out and look at three different crates in our `workbook` folder: 4 | 5 | * `firmware` - our MCU firmware that we've been working on so far 6 | * `workbook-host` - A crate for running on our PC 7 | * `workbook-icd` - A crate for our protocol's type, `Endpoint`s, and `Topic`s 8 | 9 | Let's look at the "ICD" crate first! 10 | -------------------------------------------------------------------------------- /book/src/intro.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 | This is the training workbook for the [`postcard` workshop] at RustNL 2024. 4 | 5 | ![board photo](ovtwin-001.jpg) 6 | 7 | At the moment, only the [Setup Instructions] are published. The rest of the material 8 | will be published shortly before the workshop begins. 9 | 10 | If you have any issues or questions, please email [contact@onevariable.com](mailto:contact@onevariable.com) 11 | 12 | [`postcard` workshop]: https://2024.rustnl.org/workshops/ 13 | [Setup Instructions]: ./setup.md 14 | -------------------------------------------------------------------------------- /book/src/ovtwin-001.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamesmunns/postcard-rpc/5d9a58d7e7ce084f9ad4a2f1b03279288efb2353/book/src/ovtwin-001.jpg -------------------------------------------------------------------------------- /book/src/ovtwin-block.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamesmunns/postcard-rpc/5d9a58d7e7ce084f9ad4a2f1b03279288efb2353/book/src/ovtwin-block.jpg -------------------------------------------------------------------------------- /book/src/ovtwin-hello-01.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamesmunns/postcard-rpc/5d9a58d7e7ce084f9ad4a2f1b03279288efb2353/book/src/ovtwin-hello-01.jpg -------------------------------------------------------------------------------- /book/src/ovtwin-uhoh-00.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamesmunns/postcard-rpc/5d9a58d7e7ce084f9ad4a2f1b03279288efb2353/book/src/ovtwin-uhoh-00.jpg -------------------------------------------------------------------------------- /book/src/setup.md: -------------------------------------------------------------------------------- 1 | # Setup 2 | 3 | Prior to the workshop, you'll need to install a few things. 4 | 5 | **Please do and check these BEFORE you come to the workshop, in case the internet is slow!** 6 | 7 | If you have any questions prior to the workshop, please contact [contact@onevariable.com](mailto:contact@onevariable.com) 8 | for assistance. 9 | 10 | ## Rust 11 | 12 | You'll want to install Rust, ideally using `rustup`, not using your operating system's package manager. 13 | 14 | You can follow the instructions here: 15 | 16 | 17 | 18 | ## Rust Toolchain Components 19 | 20 | You'll want to make sure you are on the newest stable Rust version. We'll be using `1.77.2`. 21 | 22 | You can do this with: 23 | 24 | ```sh 25 | rustup update stable 26 | rustup default stable 27 | ``` 28 | 29 | You'll also want to add a couple of additional pieces: 30 | 31 | ```sh 32 | rustup component add llvm-tools 33 | rustup target add thumbv6m-none-eabi 34 | ``` 35 | 36 | ## probe-rs 37 | 38 | We'll use `probe-rs` for debugging the board during the workshop. 39 | 40 | You can follow the instructions here: 41 | 42 | 43 | 44 | 45 | ## USB permissions 46 | 47 | You may need to set up USB drivers or permissions for both the probe, as well as the USB device. 48 | 49 | We recommend following the steps listed here: . If you've used `probe-rs` before, you are probably already fine. 50 | 51 | There are also instructions listed on the `nusb` docs page: . You may need to add permissions rules for: 52 | 53 | * Vendor ID: 0x16c0 54 | * Product ID: 0x27DD 55 | 56 | ## USB Cabling 57 | 58 | The training device will require a single USB port on your computer. You will need a cable that allows you to connect to a USB-C device. 59 | 60 | Depending on your computer, you will need either a USB A-to-C or USB C-to-C cable. We will have some spares, but please bring one if you can. 61 | -------------------------------------------------------------------------------- /book/src/streaming.md: -------------------------------------------------------------------------------- 1 | # Streaming with Topics 2 | 3 | `Topics` are useful for cases when you either want to send a LOT of data, e.g. streaming raw sensor 4 | values, or cases where you want to rarely send notifications that some event has happened. 5 | 6 | ## Always Sending 7 | 8 | One way you can use Topics is to just always send data, even unprompted. For example, you could 9 | periodically send information like "uptime", or how many milliseconds since the software has 10 | started. For more complex projects, you could include other performance counters, CPU Load, or 11 | memory usage over time. 12 | 13 | You'll need to store the time that the program started (check out `Instant` from `embassy-time`!), 14 | and make a task OUTSIDE the dispatcher to do this. Don't forget that the `Dispatcher` struct has 15 | the sender as a field, and it has a a method called `publish()` you can use with 16 | `sender.publish::(&your_msg).await`. 17 | 18 | ## Start/Stop sending 19 | 20 | You can also pair starting and stopping a stream on a Topic by using an endpoint. You could use 21 | a `spawn` handler to begin streaming, and use a `blocking` or `async` task to signal the task to 22 | stop. 23 | 24 | You may need to share some kind of signal, `embassy-sync` has useful data structures you can use 25 | in the `Context` struct, or as a `static`. 26 | 27 | Consider setting up some kind of streaming endpoint for the accelerometer using Topics. 28 | 29 | Some things to keep in mind: 30 | 31 | * How should the host provide the "configuration" values for the stream, like the frequency of 32 | sampling? 33 | * What to do if an error occurs, and we need to stop the stream without the host asking? 34 | * What to do if the host asks the target to stop, but it had never started? 35 | -------------------------------------------------------------------------------- /book/src/welcome-goals.md: -------------------------------------------------------------------------------- 1 | # Goals 2 | 3 | We have a couple of goals for today: 4 | 5 | 1. Get to know our board, which has all the hardware on it that we'll need today. 6 | 2. Write some software using just the board, playing with the LEDs, buttons, and sensors available 7 | 3. Get our PC talking to the board 8 | 4. Write some code for talking to our board and its sensors 9 | 5. Write some code for streaming data to and from our sensors 10 | 6. Write some custom applications 11 | 12 | My goal is that everyone is able to learn something during the workshop. I've provided "checkpoints" 13 | in all of the exercises, so if you feel like you're running behind, you can always pick up at the 14 | next checkpoint. If you feel like you are running ahead, feel free to explore as you'd like! When 15 | in doubt, give it a try yourself before peeking at the next checkpoint, and feel free to ask me 16 | any questions, **you're probably not alone**! 17 | -------------------------------------------------------------------------------- /book/src/welcome.md: -------------------------------------------------------------------------------- 1 | # Welcome 2 | 3 | Welcome to the workshop! This is the first stage, where we'll get familiar with the goals we have 4 | for today, and the board we'll be using. 5 | 6 | You'll need two links today: 7 | 8 | ## The link for this book 9 | 10 | We'll be working out of this for the whole workshop 11 | 12 | 13 | 14 | ## The link to the software repository 15 | 16 | You'll also need this, as there's a lot of code here! 17 | 18 | 19 | -------------------------------------------------------------------------------- /ci.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # TODO: I should be a CI check, but I'm not yet. 4 | set -euxo pipefail 5 | 6 | rustup target add \ 7 | thumbv6m-none-eabi \ 8 | thumbv7em-none-eabihf \ 9 | wasm32-unknown-unknown 10 | 11 | # Host + STD checks 12 | cargo check \ 13 | --manifest-path source/postcard-rpc/Cargo.toml \ 14 | --no-default-features 15 | cargo test \ 16 | --manifest-path source/postcard-rpc/Cargo.toml \ 17 | --no-default-features 18 | 19 | # Host + all non-wasm host-client impls 20 | cargo check \ 21 | --manifest-path source/postcard-rpc/Cargo.toml \ 22 | --no-default-features \ 23 | --features=use-std,cobs-serial,raw-nusb 24 | cargo test \ 25 | --manifest-path source/postcard-rpc/Cargo.toml \ 26 | --no-default-features \ 27 | --features=use-std,cobs-serial,raw-nusb 28 | 29 | # Host + wasm host-client impls 30 | RUSTFLAGS="--cfg=web_sys_unstable_apis" \ 31 | cargo check \ 32 | --manifest-path source/postcard-rpc/Cargo.toml \ 33 | --no-default-features \ 34 | --features=use-std,webusb \ 35 | --target wasm32-unknown-unknown 36 | RUSTFLAGS="--cfg=web_sys_unstable_apis" \ 37 | cargo build \ 38 | --manifest-path source/postcard-rpc/Cargo.toml \ 39 | --no-default-features \ 40 | --features=use-std,webusb \ 41 | --target wasm32-unknown-unknown 42 | 43 | # Embedded + embassy server impl 44 | cargo check \ 45 | --manifest-path source/postcard-rpc/Cargo.toml \ 46 | --no-default-features \ 47 | --features=embassy-usb-0_3-server \ 48 | --target thumbv7em-none-eabihf 49 | cargo check \ 50 | --manifest-path source/postcard-rpc/Cargo.toml \ 51 | --no-default-features \ 52 | --features=embassy-usb-0_4-server \ 53 | --target thumbv7em-none-eabihf 54 | 55 | # Example projects 56 | cargo build \ 57 | --manifest-path example/workbook-host/Cargo.toml 58 | # Current (embassy-usb v0.4) 59 | cargo build \ 60 | --manifest-path example/firmware/Cargo.toml \ 61 | --target thumbv6m-none-eabi 62 | # Legacy (embassy-usb v0.3) 63 | cargo build \ 64 | --manifest-path example/firmware-eusb-v0_3/Cargo.toml \ 65 | --target thumbv6m-none-eabi 66 | 67 | # Test Project 68 | cargo test \ 69 | --manifest-path source/postcard-rpc-test/Cargo.toml 70 | -------------------------------------------------------------------------------- /docs/overview.md: -------------------------------------------------------------------------------- 1 | # `postcard-rpc` overview 2 | 3 | The goal of `postcard-rpc` is to make it easier for a host PC to talk to a constrained device, like a microcontroller. 4 | 5 | In many cases, it is useful to have a microcontroller handling real time operations, like reading sensors or controlling motors; while the "big picture" tasks are handled by a PC. 6 | 7 | ## Remote Procedure Calls 8 | 9 | One way of achieving this is to use a "Remote Procedure Call" (RPC) approach, where: 10 | 11 | * The PC sends a **Request** message, asking the MCU to do something, and waits for a **Response** message. 12 | * The MCU receives this Request, and performs the action 13 | * The MCU sends the response, and the PC recieves it. 14 | 15 | In essence, we want to make this: 16 | 17 | ``` 18 | PC ---Request--> MCU 19 | ... 20 | PC <--Response-- MCU 21 | ``` 22 | 23 | look like this: 24 | 25 | ```rust 26 | async fn request() -> Response { ... } 27 | ``` 28 | 29 | ## How does this relate to `postcard`? 30 | 31 | [`postcard`](https://postcard.jamesmunns.com) is a Rust crate for serializing and deserializing data. It has a couple of very relevant features: 32 | 33 | * We can use it to define compact messages 34 | * We can send those messages as bytes across a number of different interfaces 35 | * We can use it on very constrained devices, like microcontrollers, as the messages are small and relatively "cheap" to serialize and deserialize 36 | 37 | ## What does this add on top of postcard? 38 | 39 | `postcard-rpc` adds a major feature to `postcard` formatted messages: a standard header containing two things: 40 | 41 | * an eight byte, unique `Key` 42 | * a `varint(u32)` "sequence number" 43 | 44 | ### The `Key` 45 | 46 | The `Key` uniquely identifies what "kind" of message this is. In order to generate it, `postcard-rpc` takes two pieces of data: 47 | 48 | * a `&str` "path" URI, similar to how you would use URIs as part of an HTTP path 49 | * The schema of the message type itself, using the experimental [schema] feature of `postcard`. 50 | 51 | [schema]: https://docs.rs/postcard/latest/postcard/experimental/index.html#message-schema-generation 52 | 53 | Let's say we had a message type like: 54 | 55 | ```rust 56 | struct SetLight { 57 | r: u8, 58 | g: u8, 59 | b: u8, 60 | idx: u16, 61 | } 62 | ``` 63 | 64 | and we wanted to map it to the path `lights/set_rgb`. 65 | 66 | Both the schema and the path will take many more than eight bytes to describe, so instead we *hash* the two pieces of data in a deterministic way, to produce a value like `0x482c55743ba118e1`. 67 | 68 | Specifically, we use [`FNV1a`](https://en.wikipedia.org/wiki/Fowler%E2%80%93Noll%E2%80%93Vo_hash_function), and produce a 64-bit digest, by first hashing the path, then hashing the schema. FNV1a is a non-cryptographic hash function, designed to be reasonably efficient to compute even on small platforms like microcontrollers. 69 | 70 | Changing **anything** about *either* of the path or the schema will produce a drastically different `Key` value. 71 | 72 | ### The "sequence number" 73 | 74 | Sometimes, we might want to have multiple requests "in flight" at once. Instead of this: 75 | 76 | ``` 77 | PC ---Request A-->. MCU 78 | | 79 | <--Response A--' 80 | 81 | ---Request B-->. 82 | | 83 | <--Response B--' 84 | 85 | ---Request C-->. 86 | | 87 | <--Response C--' 88 | ``` 89 | 90 | We'd like to do this: 91 | 92 | ``` 93 | PC ---Request A-->--. MCU 94 | ---Request B-->--|--. 95 | ---Request C-->--|--|--. 96 | | | | 97 | <--Response A----' | | 98 | <--Response B-------' | 99 | <--Response C----------' 100 | ``` 101 | 102 | Or if the requests take different amounts of time to process, even this: 103 | 104 | ``` 105 | PC ---Request A-->-----. MCU 106 | ---Request B-->-----|--. 107 | ---Request C-->--. | | 108 | | | | 109 | <--Response C----' | | 110 | <--Response A-------' | 111 | <--Response B----------' 112 | ``` 113 | 114 | By adding a sequence number, we can uniquely match each response to the specific request, allowing for out of order processing of long requests. 115 | 116 | ## How you use this: 117 | 118 | > NOTE: Check out the [example](https://github.com/jamesmunns/postcard-rpc/tree/main/example) folder for a project that follows these recommendations. 119 | 120 | I'd suggest breaking up your project into three main crates: 121 | 122 | * The `protocol` crate 123 | * This crate should depend on `postcard` and `postcard-rpc` 124 | * This crate is a `no_std` library project 125 | * This crate defines all the message types and paths used in the following crates 126 | * The `firmware` crate 127 | * This crate should depend on `postcard`, `postcard-rpc`, `serde`, and your `protocol` crate. 128 | * This crate is a `no_std` binary project 129 | * The `host` crate 130 | * This crate should depend on `postcard`, `postcard-rpc`, `serde`, and your `protocol` crate. 131 | * This crate is a `std` binary/library project 132 | 133 | ### The `protocol` crate 134 | 135 | This part is pretty boring! You define some types, and make sure they derive (at least) the `Serialize` and `Deserialize` traits from `serde`, and the `Schema` trait from `postcard`. 136 | 137 | ```rust 138 | // This is our path 139 | pub const SLEEP_PATH: &str = "sleep"; 140 | 141 | // This is our Request type 142 | #[derive(Serialize, Deserialize, Schema)] 143 | pub struct Sleep { 144 | pub seconds: u32, 145 | pub micros: u32, 146 | } 147 | 148 | // This is our Response type 149 | #[derive(Serialize, Deserialize, Schema)] 150 | pub struct SleepDone { 151 | pub slept_for: Sleep, 152 | } 153 | ``` 154 | 155 | ### The `firmware` crate 156 | 157 | In this part, you'll need to do a couple things: 158 | 159 | 1. Create a `Dispatch` struct. You'll need to define: 160 | * What your `Context` type is, this will be passed as a `&mut` ref to all handlers 161 | * What your `Error` type is - this is a type you can return from handlers if the message can not be processed 162 | * How many handlers max you can support 163 | * If you use `CobsDispatch`, you'll also need to define how many bytes to use for buffering COBS encoded messages. 164 | 2. Register each of your handlers. For each handler, you'll need to define: 165 | * The `Key` that should be used for the handler 166 | * a handler function 167 | 3. Feed messages into the `Dispatch`, which will call the handlers when a message matching that handler is found. 168 | 169 | The handler functions have the following signature: 170 | 171 | ```rust 172 | fn handler( 173 | hdr: &WireHeader, 174 | context: &mut Context, 175 | bytes: &[u8], 176 | ) -> Result<(), Error>; 177 | ``` 178 | 179 | The `hdr` is the decoded `Key` and `seq_no` of our message. We know that the `Key` matches our function already, but you could use the same handler for multiple `Key`s, so passing it allows you to check if you need to. 180 | 181 | The `context` is a mutable reference to the Context type chosen when you create the `Dispatch` instance. It is recommended that you include whatever you need to send responses back to the PC in the `context` structure. 182 | 183 | The `bytes` are the body of the request. You are expected to use `postcard::from_bytes` to decode the body to your specific message type. 184 | 185 | Note that handlers are synchronous/blocking functions! However, you can still spawn async tasks from this context. 186 | 187 | A typical handler might look something like this: 188 | 189 | ```rust 190 | fn sleep_handler( 191 | hdr: &WireHeader, 192 | c: &mut Context, 193 | bytes: &[u8] 194 | ) -> Result<(), CommsError> { 195 | // Decode the body of the request 196 | let Ok(msg) = from_bytes::(bytes) else { 197 | // return an error if we can't decode the 198 | // message. Include the sequence number so 199 | // we can use that for our boilerplate "error" 200 | // response. 201 | return Err(CommsError::Postcard(hdr.seq_no)) 202 | } 203 | 204 | // We have a message, attempt to spawn an embassy 205 | // task to handle this request. If we fail, return 206 | // an error with the sequence number so we can tell 207 | // the PC we couldn't serve the request 208 | // 209 | // Our context contains a Mutex'd sender that allows 210 | // the spawned task to send a reply. 211 | let new_c = c.clone(); 212 | c.spawner 213 | .spawn(sleep_task(hdr.seq_no, new_c, msg)) 214 | .map_err(|_| CommsError::Busy(hdr.seq_no)) 215 | } 216 | ``` 217 | 218 | The handler might call an embassy task that looks like this: 219 | 220 | ```rust 221 | // A pool size of three means that we can handle three requests 222 | // concurrently. 223 | #[embassy_executor::task(pool_size = 3)] 224 | async fn sleep_task(seq_no: u32, c: Context, s: Sleep) { 225 | info!("Sleep spawned"); 226 | Timer::after(Duration::from_secs(s.seconds.into())).await; 227 | Timer::after(Duration::from_micros(s.micros.into())).await; 228 | info!("Sleep complete"); 229 | 230 | // Try to send a response. If it fails, we are disconnected 231 | // so no sense in retrying. We reply with the pre-computed 232 | // reply key, and the sequence number of the request. 233 | let resp = SleepDone { slept_for: s }; 234 | let _ = c.sender 235 | .lock() 236 | .await 237 | .send(seq_no, c.sleep_done_key, resp).await; 238 | } 239 | ``` 240 | 241 | ### The `host` crate 242 | 243 | On the host side, the API is pretty simple: 244 | 245 | 1. We create a `HostClient` that establishes the serial link with our device 246 | 2. We make requests using the `HostClient` 247 | 248 | ```rust 249 | // We create a client with: 250 | // 251 | // * A serial port path of "/dev/ttyUSB0" 252 | // * An error path of "error" 253 | // * An error type of `WireError` 254 | let client = HostClient::::new("/dev/ttyUSB0", "error")?; 255 | 256 | // We make a request with: 257 | // 258 | // * A URI of "sleep" 259 | // * A Request of type `Sleep` 260 | // * A Response of type `SleepDone` 261 | let resp: Result = client 262 | .req_resp::("sleep").await; 263 | ``` 264 | -------------------------------------------------------------------------------- /example/firmware-eusb-v0_3/.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [target.'cfg(all(target_arch = "arm", target_os = "none"))'] 2 | runner = "probe-rs run --chip RP2040 --speed 12000 --protocol swd" 3 | 4 | [build] 5 | target = "thumbv6m-none-eabi" # Cortex-M0 and Cortex-M0+ 6 | 7 | [env] 8 | DEFMT_LOG = "debug" 9 | 10 | [unstable] 11 | build-std = ["core"] 12 | build-std-features = ["panic_immediate_abort"] 13 | -------------------------------------------------------------------------------- /example/firmware-eusb-v0_3/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "workbook-fw" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | cortex-m = { version = "0.7.6", features = ["inline-asm"] } 8 | embassy-executor = { version = "0.7.0", features = ["task-arena-size-32768", "arch-cortex-m", "executor-thread", "executor-interrupt", "defmt"] } 9 | embassy-rp = { version = "0.3.0", features = ["rp2040", "defmt", "unstable-pac", "time-driver", "critical-section-impl"] } 10 | embassy-sync = { version = "0.6.1", features = ["defmt"] } 11 | embassy-time = { version = "0.4", features = ["defmt", "defmt-timestamp-uptime"] } 12 | embassy-usb = { version = "0.3.0", features = ["defmt"] } 13 | embedded-hal-bus = { version = "0.1", features = ["async"] } 14 | lis3dh-async = { version = "0.9.2", features = ["defmt"] } 15 | panic-probe = { version = "0.3", features = ["print-defmt"] } 16 | postcard-rpc = { version = "0.11", features = ["embassy-usb-0_3-server"] } 17 | postcard = { version = "1.0.10" } 18 | postcard-schema = { version = "0.2.1", features = ["derive"] } 19 | portable-atomic = { version = "1.6.0", features = ["critical-section"] } 20 | 21 | workbook-icd = { path = "../workbook-icd" } 22 | 23 | cortex-m-rt = "0.7.0" 24 | defmt = "0.3" 25 | defmt-rtt = "0.4" 26 | embedded-hal-async = "1.0" 27 | fixed = "1.23.1" 28 | fixed-macro = "1.2" 29 | pio = "0.2.1" 30 | pio-proc = "0.2" 31 | smart-leds = "0.4.0" 32 | static_cell = "2.1" 33 | 34 | [profile.release] 35 | debug = 2 36 | lto = true 37 | opt-level = 'z' 38 | codegen-units = 1 39 | incremental = false 40 | 41 | [patch.crates-io] 42 | embassy-embedded-hal = { git = "https://github.com/embassy-rs/embassy", rev = "92326f10b5be1d6fdc6bd414eb0656e3890bd825" } 43 | embassy-executor = { git = "https://github.com/embassy-rs/embassy", rev = "92326f10b5be1d6fdc6bd414eb0656e3890bd825" } 44 | embassy-rp = { git = "https://github.com/embassy-rs/embassy", rev = "92326f10b5be1d6fdc6bd414eb0656e3890bd825" } 45 | embassy-sync = { git = "https://github.com/embassy-rs/embassy", rev = "92326f10b5be1d6fdc6bd414eb0656e3890bd825" } 46 | embassy-time = { git = "https://github.com/embassy-rs/embassy", rev = "92326f10b5be1d6fdc6bd414eb0656e3890bd825" } 47 | embassy-usb = { git = "https://github.com/embassy-rs/embassy", rev = "92326f10b5be1d6fdc6bd414eb0656e3890bd825" } 48 | embassy-usb-driver = { git = "https://github.com/embassy-rs/embassy", rev = "92326f10b5be1d6fdc6bd414eb0656e3890bd825" } 49 | postcard-rpc = { path = "../../source/postcard-rpc" } 50 | -------------------------------------------------------------------------------- /example/firmware-eusb-v0_3/build.rs: -------------------------------------------------------------------------------- 1 | //! This build script copies the `memory.x` file from the crate root into 2 | //! a directory where the linker can always find it at build time. 3 | //! For many projects this is optional, as the linker always searches the 4 | //! project root directory -- wherever `Cargo.toml` is. However, if you 5 | //! are using a workspace or have a more complicated build setup, this 6 | //! build script becomes required. Additionally, by requesting that 7 | //! Cargo re-run the build script whenever `memory.x` is changed, 8 | //! updating `memory.x` ensures a rebuild of the application with the 9 | //! new memory settings. 10 | 11 | use std::env; 12 | use std::fs::File; 13 | use std::io::Write; 14 | use std::path::PathBuf; 15 | 16 | fn main() { 17 | // Put `memory.x` in our output directory and ensure it's 18 | // on the linker search path. 19 | let out = &PathBuf::from(env::var_os("OUT_DIR").unwrap()); 20 | File::create(out.join("memory.x")) 21 | .unwrap() 22 | .write_all(include_bytes!("memory.x")) 23 | .unwrap(); 24 | println!("cargo:rustc-link-search={}", out.display()); 25 | 26 | // By default, Cargo will re-run a build script whenever 27 | // any file in the project changes. By specifying `memory.x` 28 | // here, we ensure the build script is only re-run when 29 | // `memory.x` is changed. 30 | println!("cargo:rerun-if-changed=memory.x"); 31 | 32 | println!("cargo:rustc-link-arg-bins=--nmagic"); 33 | println!("cargo:rustc-link-arg-bins=-Tlink.x"); 34 | println!("cargo:rustc-link-arg-bins=-Tlink-rp.x"); 35 | println!("cargo:rustc-link-arg-bins=-Tdefmt.x"); 36 | } 37 | -------------------------------------------------------------------------------- /example/firmware-eusb-v0_3/memory.x: -------------------------------------------------------------------------------- 1 | MEMORY { 2 | BOOT2 : ORIGIN = 0x10000000, LENGTH = 0x100 3 | FLASH : ORIGIN = 0x10000100, LENGTH = 2048K - 0x100 4 | 5 | /* Pick one of the two options for RAM layout */ 6 | 7 | /* OPTION A: Use all RAM banks as one big block */ 8 | /* Reasonable, unless you are doing something */ 9 | /* really particular with DMA or other concurrent */ 10 | /* access that would benefit from striping */ 11 | RAM : ORIGIN = 0x20000000, LENGTH = 264K 12 | 13 | /* OPTION B: Keep the unstriped sections separate */ 14 | /* RAM: ORIGIN = 0x20000000, LENGTH = 256K */ 15 | /* SCRATCH_A: ORIGIN = 0x20040000, LENGTH = 4K */ 16 | /* SCRATCH_B: ORIGIN = 0x20041000, LENGTH = 4K */ 17 | } 18 | -------------------------------------------------------------------------------- /example/firmware-eusb-v0_3/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![no_std] 2 | 3 | use { 4 | defmt_rtt as _, 5 | embassy_rp::{ 6 | adc::{self, Adc, Config as AdcConfig}, 7 | bind_interrupts, 8 | flash::{Blocking, Flash}, 9 | gpio::{Input, Level, Output, Pull}, 10 | peripherals::{ 11 | ADC, DMA_CH1, DMA_CH2, FLASH, PIN_0, PIN_1, PIN_18, PIN_19, PIN_2, PIN_20, PIN_21, 12 | PIN_26, PIN_3, PIN_4, PIN_5, PIN_6, PIN_7, SPI0, USB, 13 | }, 14 | spi::{self, Async, Spi}, 15 | usb, 16 | }, 17 | embassy_time::Delay, 18 | embedded_hal_bus::spi::ExclusiveDevice, 19 | lis3dh_async::{Lis3dh, Lis3dhSPI}, 20 | panic_probe as _, 21 | }; 22 | pub mod ws2812; 23 | use embassy_time as _; 24 | 25 | bind_interrupts!(pub struct Irqs { 26 | ADC_IRQ_FIFO => adc::InterruptHandler; 27 | USBCTRL_IRQ => usb::InterruptHandler; 28 | }); 29 | 30 | pub const NUM_SMARTLEDS: usize = 24; 31 | 32 | /// Helper to get unique ID from flash 33 | pub fn get_unique_id(flash: &mut FLASH) -> Option { 34 | let mut flash: Flash<'_, FLASH, Blocking, { 16 * 1024 * 1024 }> = Flash::new_blocking(flash); 35 | 36 | // TODO: For different flash chips, we want to handle things 37 | // differently based on their jedec? That being said: I control 38 | // the hardware for this project, and our flash supports unique ID, 39 | // so oh well. 40 | // 41 | // let jedec = flash.blocking_jedec_id().unwrap(); 42 | 43 | let mut id = [0u8; core::mem::size_of::()]; 44 | flash.blocking_unique_id(&mut id).unwrap(); 45 | Some(u64::from_be_bytes(id)) 46 | } 47 | 48 | pub struct Buttons { 49 | pub buttons: [Input<'static>; 8], 50 | } 51 | 52 | // | GPIO Name | Usage | Notes | 53 | // | :--- | :--- | :--- | 54 | // | GPIO00 | Button 1 | Button Pad (left) - active LOW | 55 | // | GPIO01 | Button 2 | Button Pad (left) - active LOW | 56 | // | GPIO02 | Button 3 | Button Pad (left) - active LOW | 57 | // | GPIO03 | Button 4 | Button Pad (left) - active LOW | 58 | // | GPIO18 | Button 5 | Button Pad (right) - active LOW | 59 | // | GPIO19 | Button 6 | Button Pad (right) - active LOW | 60 | // | GPIO20 | Button 7 | Button Pad (right) - active LOW | 61 | // | GPIO21 | Button 8 | Button Pad (right) - active LOW | 62 | impl Buttons { 63 | pub const COUNT: usize = 8; 64 | 65 | #[allow(clippy::too_many_arguments)] 66 | pub fn new( 67 | b01: PIN_0, 68 | b02: PIN_1, 69 | b03: PIN_2, 70 | b04: PIN_3, 71 | b05: PIN_18, 72 | b06: PIN_19, 73 | b07: PIN_20, 74 | b08: PIN_21, 75 | ) -> Self { 76 | Self { 77 | buttons: [ 78 | Input::new(b01, Pull::Up), 79 | Input::new(b02, Pull::Up), 80 | Input::new(b03, Pull::Up), 81 | Input::new(b04, Pull::Up), 82 | Input::new(b05, Pull::Up), 83 | Input::new(b06, Pull::Up), 84 | Input::new(b07, Pull::Up), 85 | Input::new(b08, Pull::Up), 86 | ], 87 | } 88 | } 89 | 90 | // Read all buttons, and report whether they are PRESSED, e.g. pulled low. 91 | pub fn read_all(&self) -> [bool; Self::COUNT] { 92 | let mut all = [false; Self::COUNT]; 93 | all.iter_mut().zip(self.buttons.iter()).for_each(|(a, b)| { 94 | *a = b.is_low(); 95 | }); 96 | all 97 | } 98 | } 99 | 100 | // | GPIO Name | Usage | Notes | 101 | // | :--- | :--- | :--- | 102 | // | GPIO26 | ADC0 | Potentiometer | 103 | pub struct Potentiometer { 104 | pub adc: Adc<'static, adc::Async>, 105 | pub p26: adc::Channel<'static>, 106 | } 107 | 108 | impl Potentiometer { 109 | pub fn new(adc: ADC, pin: PIN_26) -> Self { 110 | let adc = Adc::new(adc, Irqs, AdcConfig::default()); 111 | let p26 = adc::Channel::new_pin(pin, Pull::None); 112 | Self { adc, p26 } 113 | } 114 | 115 | /// Reads the ADC, returning a value between 0 and 4095. 116 | /// 117 | /// 0 is all the way to the right, and 4095 is all the way to the left 118 | pub async fn read(&mut self) -> u16 { 119 | let Ok(now) = self.adc.read(&mut self.p26).await else { 120 | defmt::panic!("Failed to read ADC!"); 121 | }; 122 | now 123 | } 124 | } 125 | 126 | // | GPIO Name | Usage | Notes | 127 | // | :--- | :--- | :--- | 128 | // | GPIO04 | SPI MISO/CIPO | LIS3DH | 129 | // | GPIO05 | SPI CSn | LIS3DH | 130 | // | GPIO06 | SPI CLK | LIS3DH | 131 | // | GPIO07 | SPI MOSI/COPI | LIS3DH | 132 | type AccSpi = Spi<'static, SPI0, Async>; 133 | type ExclusiveSpi = ExclusiveDevice, Delay>; 134 | type Accel = Lis3dh>; 135 | pub struct Accelerometer { 136 | pub dev: Accel, 137 | } 138 | 139 | #[derive(Debug, PartialEq, defmt::Format)] 140 | pub struct AccelReading { 141 | pub x: i16, 142 | pub y: i16, 143 | pub z: i16, 144 | } 145 | 146 | impl Accelerometer { 147 | pub async fn new( 148 | periph: SPI0, 149 | clk: PIN_6, 150 | copi: PIN_7, 151 | cipo: PIN_4, 152 | csn: PIN_5, 153 | tx_dma: DMA_CH1, 154 | rx_dma: DMA_CH2, 155 | ) -> Self { 156 | let mut cfg = spi::Config::default(); 157 | cfg.frequency = 1_000_000; 158 | let spi = Spi::new(periph, clk, copi, cipo, tx_dma, rx_dma, cfg); 159 | let dev = ExclusiveDevice::new(spi, Output::new(csn, Level::High), Delay); 160 | let Ok(mut dev) = Lis3dh::new_spi(dev).await else { 161 | defmt::panic!("Failed to initialize SPI!"); 162 | }; 163 | if dev.set_range(lis3dh_async::Range::G8).await.is_err() { 164 | defmt::panic!("Error setting range!"); 165 | }; 166 | Self { dev } 167 | } 168 | 169 | pub async fn read(&mut self) -> AccelReading { 170 | let Ok(raw_acc) = self.dev.accel_raw().await else { 171 | defmt::panic!("Failed to get acceleration!"); 172 | }; 173 | AccelReading { 174 | x: raw_acc.x, 175 | y: raw_acc.y, 176 | z: raw_acc.z, 177 | } 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /example/firmware-eusb-v0_3/src/main.rs: -------------------------------------------------------------------------------- 1 | #![no_std] 2 | #![no_main] 3 | 4 | use defmt::info; 5 | use embassy_executor::Spawner; 6 | use embassy_rp::{ 7 | gpio::{Level, Output}, 8 | peripherals::{PIO0, SPI0, USB}, 9 | pio::Pio, 10 | spi::{self, Spi}, 11 | usb, 12 | }; 13 | use embassy_sync::{blocking_mutex::raw::ThreadModeRawMutex, mutex::Mutex}; 14 | use embassy_time::{Delay, Duration, Ticker}; 15 | use embassy_usb::{Config, UsbDevice}; 16 | use embedded_hal_bus::spi::ExclusiveDevice; 17 | use lis3dh_async::{Lis3dh, Lis3dhSPI}; 18 | use portable_atomic::{AtomicBool, Ordering}; 19 | use postcard_rpc::{ 20 | define_dispatch, 21 | header::VarHeader, 22 | server::{ 23 | impls::embassy_usb_v0_3::{ 24 | dispatch_impl::{ 25 | spawn_fn, WireRxBuf, WireRxImpl, WireSpawnImpl, WireStorage, WireTxImpl, 26 | }, 27 | PacketBuffers, 28 | }, 29 | Dispatch, Sender, Server, SpawnContext, 30 | }, 31 | }; 32 | use smart_leds::{colors::BLACK, RGB8}; 33 | use static_cell::{ConstStaticCell, StaticCell}; 34 | use workbook_fw::{ 35 | get_unique_id, 36 | ws2812::{self, Ws2812}, 37 | Irqs, 38 | }; 39 | use workbook_icd::{ 40 | AccelTopic, Acceleration, BadPositionError, GetUniqueIdEndpoint, PingEndpoint, Rgb8, 41 | SetAllLedEndpoint, SetSingleLedEndpoint, SingleLed, StartAccel, StartAccelerationEndpoint, 42 | StopAccelerationEndpoint, ENDPOINT_LIST, TOPICS_IN_LIST, TOPICS_OUT_LIST, 43 | }; 44 | use {defmt_rtt as _, panic_probe as _}; 45 | 46 | pub type Accel = 47 | Lis3dh, Output<'static>, Delay>>>; 48 | static ACCEL: StaticCell> = StaticCell::new(); 49 | 50 | pub struct Context { 51 | pub unique_id: u64, 52 | pub ws2812: Ws2812<'static, PIO0, 0, 24>, 53 | pub ws2812_state: [RGB8; 24], 54 | pub accel: &'static Mutex, 55 | } 56 | 57 | pub struct SpawnCtx { 58 | pub accel: &'static Mutex, 59 | } 60 | 61 | impl SpawnContext for Context { 62 | type SpawnCtxt = SpawnCtx; 63 | fn spawn_ctxt(&mut self) -> Self::SpawnCtxt { 64 | SpawnCtx { accel: self.accel } 65 | } 66 | } 67 | 68 | type AppDriver = usb::Driver<'static, USB>; 69 | type AppStorage = WireStorage; 70 | type BufStorage = PacketBuffers<1024, 1024>; 71 | type AppTx = WireTxImpl; 72 | type AppRx = WireRxImpl; 73 | type AppServer = Server; 74 | 75 | static PBUFS: ConstStaticCell = ConstStaticCell::new(BufStorage::new()); 76 | static STORAGE: AppStorage = AppStorage::new(); 77 | 78 | fn usb_config() -> Config<'static> { 79 | let mut config = Config::new(0x16c0, 0x27DD); 80 | config.manufacturer = Some("OneVariable"); 81 | config.product = Some("ov-twin"); 82 | config.serial_number = Some("12345678"); 83 | 84 | // Required for windows compatibility. 85 | // https://developer.nordicsemi.com/nRF_Connect_SDK/doc/1.9.1/kconfig/CONFIG_CDC_ACM_IAD.html#help 86 | config.device_class = 0xEF; 87 | config.device_sub_class = 0x02; 88 | config.device_protocol = 0x01; 89 | config.composite_with_iads = true; 90 | 91 | config 92 | } 93 | 94 | define_dispatch! { 95 | app: MyApp; 96 | spawn_fn: spawn_fn; 97 | tx_impl: AppTx; 98 | spawn_impl: WireSpawnImpl; 99 | context: Context; 100 | 101 | endpoints: { 102 | list: ENDPOINT_LIST; 103 | 104 | | EndpointTy | kind | handler | 105 | | ---------- | ---- | ------- | 106 | | PingEndpoint | blocking | ping_handler | 107 | | GetUniqueIdEndpoint | blocking | unique_id_handler | 108 | | SetSingleLedEndpoint | async | set_led_handler | 109 | | SetAllLedEndpoint | async | set_all_led_handler | 110 | | StartAccelerationEndpoint | spawn | accelerometer_handler | 111 | | StopAccelerationEndpoint | blocking | accelerometer_stop_handler | 112 | }; 113 | topics_in: { 114 | list: TOPICS_IN_LIST; 115 | 116 | | TopicTy | kind | handler | 117 | | ---------- | ---- | ------- | 118 | }; 119 | topics_out: { 120 | list: TOPICS_OUT_LIST; 121 | }; 122 | } 123 | 124 | #[embassy_executor::main] 125 | async fn main(spawner: Spawner) { 126 | // SYSTEM INIT 127 | info!("Start"); 128 | let mut p = embassy_rp::init(Default::default()); 129 | let unique_id = defmt::unwrap!(get_unique_id(&mut p.FLASH)); 130 | info!("id: {=u64:016X}", unique_id); 131 | 132 | // PIO/WS2812 INIT 133 | let Pio { 134 | mut common, sm0, .. 135 | } = Pio::new(p.PIO0, ws2812::Irqs); 136 | let ws2812: Ws2812<'static, PIO0, 0, 24> = Ws2812::new(&mut common, sm0, p.DMA_CH0, p.PIN_25); 137 | 138 | // SPI INIT 139 | let spi = Spi::new( 140 | p.SPI0, 141 | p.PIN_6, // clk 142 | p.PIN_7, // mosi 143 | p.PIN_4, // miso 144 | p.DMA_CH1, 145 | p.DMA_CH2, 146 | spi::Config::default(), 147 | ); 148 | // CS: GPIO5 149 | let bus = ExclusiveDevice::new(spi, Output::new(p.PIN_5, Level::High), Delay); 150 | let acc: Accel = defmt::unwrap!(Lis3dh::new_spi(bus).await.map_err(drop)); 151 | let accel_ref = ACCEL.init(Mutex::new(acc)); 152 | 153 | // USB/RPC INIT 154 | let driver = usb::Driver::new(p.USB, Irqs); 155 | let pbufs = PBUFS.take(); 156 | let config = usb_config(); 157 | 158 | let context = Context { 159 | unique_id, 160 | ws2812, 161 | ws2812_state: [BLACK; 24], 162 | accel: accel_ref, 163 | }; 164 | 165 | let (device, tx_impl, rx_impl) = STORAGE.init(driver, config, pbufs.tx_buf.as_mut_slice()); 166 | let dispatcher = MyApp::new(context, spawner.into()); 167 | let vkk = dispatcher.min_key_len(); 168 | let mut server: AppServer = Server::new( 169 | tx_impl, 170 | rx_impl, 171 | pbufs.rx_buf.as_mut_slice(), 172 | dispatcher, 173 | vkk, 174 | ); 175 | spawner.must_spawn(usb_task(device)); 176 | 177 | loop { 178 | // If the host disconnects, we'll return an error here. 179 | // If this happens, just wait until the host reconnects 180 | let _ = server.run().await; 181 | } 182 | } 183 | 184 | /// This handles the low level USB management 185 | #[embassy_executor::task] 186 | pub async fn usb_task(mut usb: UsbDevice<'static, AppDriver>) { 187 | usb.run().await; 188 | } 189 | 190 | // --- 191 | 192 | fn ping_handler(_context: &mut Context, _header: VarHeader, rqst: u32) -> u32 { 193 | info!("ping"); 194 | rqst 195 | } 196 | 197 | fn unique_id_handler(context: &mut Context, _header: VarHeader, _rqst: ()) -> u64 { 198 | info!("unique_id"); 199 | context.unique_id 200 | } 201 | 202 | async fn set_led_handler( 203 | context: &mut Context, 204 | _header: VarHeader, 205 | rqst: SingleLed, 206 | ) -> Result<(), BadPositionError> { 207 | info!("set_led"); 208 | if rqst.position >= 24 { 209 | return Err(BadPositionError); 210 | } 211 | let pos = rqst.position as usize; 212 | context.ws2812_state[pos] = RGB8 { 213 | r: rqst.rgb.r, 214 | g: rqst.rgb.g, 215 | b: rqst.rgb.b, 216 | }; 217 | context.ws2812.write(&context.ws2812_state).await; 218 | Ok(()) 219 | } 220 | 221 | async fn set_all_led_handler(context: &mut Context, _header: VarHeader, rqst: [Rgb8; 24]) { 222 | info!("set_all_led"); 223 | context 224 | .ws2812_state 225 | .iter_mut() 226 | .zip(rqst.iter()) 227 | .for_each(|(s, rgb)| { 228 | s.r = rgb.r; 229 | s.g = rgb.g; 230 | s.b = rgb.b; 231 | }); 232 | context.ws2812.write(&context.ws2812_state).await; 233 | } 234 | 235 | static STOP: AtomicBool = AtomicBool::new(false); 236 | 237 | #[embassy_executor::task] 238 | async fn accelerometer_handler( 239 | context: SpawnCtx, 240 | header: VarHeader, 241 | rqst: StartAccel, 242 | sender: Sender, 243 | ) { 244 | let mut accel = context.accel.lock().await; 245 | if sender 246 | .reply::(header.seq_no, &()) 247 | .await 248 | .is_err() 249 | { 250 | defmt::error!("Failed to reply, stopping accel"); 251 | return; 252 | } 253 | 254 | defmt::unwrap!(accel.set_range(lis3dh_async::Range::G8).await.map_err(drop)); 255 | 256 | let mut ticker = Ticker::every(Duration::from_millis(rqst.interval_ms.into())); 257 | let mut seq = 0u8; 258 | while !STOP.load(Ordering::Acquire) { 259 | ticker.next().await; 260 | let acc = defmt::unwrap!(accel.accel_raw().await.map_err(drop)); 261 | defmt::println!("ACC: {=i16},{=i16},{=i16}", acc.x, acc.y, acc.z); 262 | let msg = Acceleration { 263 | x: acc.x, 264 | y: acc.y, 265 | z: acc.z, 266 | }; 267 | if sender 268 | .publish::(seq.into(), &msg) 269 | .await 270 | .is_err() 271 | { 272 | defmt::error!("Send error!"); 273 | break; 274 | } 275 | seq = seq.wrapping_add(1); 276 | } 277 | defmt::info!("Stopping!"); 278 | STOP.store(false, Ordering::Release); 279 | } 280 | 281 | fn accelerometer_stop_handler(context: &mut Context, _header: VarHeader, _rqst: ()) -> bool { 282 | info!("accel_stop"); 283 | let was_busy = context.accel.try_lock().is_err(); 284 | if was_busy { 285 | STOP.store(true, Ordering::Release); 286 | } 287 | was_busy 288 | } 289 | -------------------------------------------------------------------------------- /example/firmware-eusb-v0_3/src/ws2812.rs: -------------------------------------------------------------------------------- 1 | use embassy_rp::{ 2 | bind_interrupts, clocks, 3 | dma::{self, AnyChannel}, 4 | into_ref, 5 | peripherals::PIO0, 6 | pio::{ 7 | Common, Config as PioConfig, FifoJoin, Instance, PioPin, ShiftConfig, ShiftDirection, 8 | StateMachine, 9 | }, 10 | Peripheral, PeripheralRef, 11 | }; 12 | use embassy_time::Timer; 13 | use fixed::types::U24F8; 14 | use fixed_macro::fixed; 15 | use smart_leds::RGB8; 16 | use {defmt_rtt as _, panic_probe as _}; 17 | 18 | bind_interrupts!(pub struct Irqs { 19 | PIO0_IRQ_0 => embassy_rp::pio::InterruptHandler; 20 | }); 21 | 22 | pub struct Ws2812<'d, P: Instance, const S: usize, const N: usize> { 23 | dma: PeripheralRef<'d, AnyChannel>, 24 | sm: StateMachine<'d, P, S>, 25 | } 26 | 27 | impl<'d, P: Instance, const S: usize, const N: usize> Ws2812<'d, P, S, N> { 28 | pub fn new( 29 | pio: &mut Common<'d, P>, 30 | mut sm: StateMachine<'d, P, S>, 31 | dma: impl Peripheral

+ 'd, 32 | pin: impl PioPin, 33 | ) -> Self { 34 | into_ref!(dma); 35 | 36 | // Setup sm0 37 | 38 | // prepare the PIO program 39 | let side_set = pio::SideSet::new(false, 1, false); 40 | let mut a: pio::Assembler<32> = pio::Assembler::new_with_side_set(side_set); 41 | 42 | const T1: u8 = 2; // start bit 43 | const T2: u8 = 5; // data bit 44 | const T3: u8 = 3; // stop bit 45 | const CYCLES_PER_BIT: u32 = (T1 + T2 + T3) as u32; 46 | 47 | let mut wrap_target = a.label(); 48 | let mut wrap_source = a.label(); 49 | let mut do_zero = a.label(); 50 | a.set_with_side_set(pio::SetDestination::PINDIRS, 1, 0); 51 | a.bind(&mut wrap_target); 52 | // Do stop bit 53 | a.out_with_delay_and_side_set(pio::OutDestination::X, 1, T3 - 1, 0); 54 | // Do start bit 55 | a.jmp_with_delay_and_side_set(pio::JmpCondition::XIsZero, &mut do_zero, T1 - 1, 1); 56 | // Do data bit = 1 57 | a.jmp_with_delay_and_side_set(pio::JmpCondition::Always, &mut wrap_target, T2 - 1, 1); 58 | a.bind(&mut do_zero); 59 | // Do data bit = 0 60 | a.nop_with_delay_and_side_set(T2 - 1, 0); 61 | a.bind(&mut wrap_source); 62 | 63 | let prg = a.assemble_with_wrap(wrap_source, wrap_target); 64 | let mut cfg = PioConfig::default(); 65 | 66 | // Pin config 67 | let out_pin = pio.make_pio_pin(pin); 68 | cfg.set_out_pins(&[&out_pin]); 69 | cfg.set_set_pins(&[&out_pin]); 70 | 71 | cfg.use_program(&pio.load_program(&prg), &[&out_pin]); 72 | 73 | // Clock config, measured in kHz to avoid overflows 74 | // TODO CLOCK_FREQ should come from embassy_rp 75 | let clock_freq = U24F8::from_num(clocks::clk_sys_freq() / 1000); 76 | let ws2812_freq = fixed!(800: U24F8); 77 | let bit_freq = ws2812_freq * CYCLES_PER_BIT; 78 | cfg.clock_divider = clock_freq / bit_freq; 79 | 80 | // FIFO config 81 | cfg.fifo_join = FifoJoin::TxOnly; 82 | cfg.shift_out = ShiftConfig { 83 | auto_fill: true, 84 | threshold: 24, 85 | direction: ShiftDirection::Left, 86 | }; 87 | 88 | sm.set_config(&cfg); 89 | sm.set_enable(true); 90 | 91 | Self { 92 | dma: dma.map_into(), 93 | sm, 94 | } 95 | } 96 | 97 | pub async fn write(&mut self, colors: &[RGB8; N]) { 98 | // Precompute the word bytes from the colors 99 | let mut words = [0u32; N]; 100 | for i in 0..N { 101 | let word = (u32::from(colors[i].g) << 24) 102 | | (u32::from(colors[i].r) << 16) 103 | | (u32::from(colors[i].b) << 8); 104 | words[i] = word; 105 | } 106 | 107 | // DMA transfer 108 | self.sm.tx().dma_push(self.dma.reborrow(), &words).await; 109 | 110 | Timer::after_micros(55).await; 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /example/firmware/.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [target.'cfg(all(target_arch = "arm", target_os = "none"))'] 2 | runner = "probe-rs run --chip RP2040 --speed 12000 --protocol swd" 3 | 4 | [build] 5 | target = "thumbv6m-none-eabi" # Cortex-M0 and Cortex-M0+ 6 | 7 | [env] 8 | DEFMT_LOG = "debug" 9 | 10 | [unstable] 11 | build-std = ["core"] 12 | build-std-features = ["panic_immediate_abort"] 13 | -------------------------------------------------------------------------------- /example/firmware/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "workbook-fw" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | cortex-m = { version = "0.7.6", features = ["inline-asm"] } 8 | embassy-executor = { version = "0.7.0", features = ["task-arena-size-32768", "arch-cortex-m", "executor-thread", "executor-interrupt", "defmt"] } 9 | embassy-rp = { version = "0.3.0", features = ["rp2040", "defmt", "unstable-pac", "time-driver", "critical-section-impl"] } 10 | embassy-sync = { version = "0.6.1", features = ["defmt"] } 11 | embassy-time = { version = "0.4", features = ["defmt", "defmt-timestamp-uptime"] } 12 | embassy-usb = { version = "0.4.0", features = ["defmt"] } 13 | embedded-hal-bus = { version = "0.1", features = ["async"] } 14 | lis3dh-async = { version = "0.9.2", features = ["defmt"] } 15 | panic-probe = { version = "0.3", features = ["print-defmt"] } 16 | postcard-rpc = { version = "0.11", features = ["embassy-usb-0_4-server"] } 17 | postcard = { version = "1.0.10" } 18 | postcard-schema = { version = "0.2.1", features = ["derive"] } 19 | portable-atomic = { version = "1.6.0", features = ["critical-section"] } 20 | 21 | workbook-icd = { path = "../workbook-icd" } 22 | 23 | cortex-m-rt = "0.7.0" 24 | defmt = "0.3" 25 | defmt-rtt = "0.4" 26 | embedded-hal-async = "1.0" 27 | fixed = "1.23.1" 28 | fixed-macro = "1.2" 29 | pio = "0.2.1" 30 | pio-proc = "0.2" 31 | smart-leds = "0.4.0" 32 | static_cell = "2.1" 33 | 34 | [profile.release] 35 | debug = 2 36 | lto = true 37 | opt-level = 'z' 38 | codegen-units = 1 39 | incremental = false 40 | 41 | [patch.crates-io] 42 | embassy-embedded-hal = { git = "https://github.com/embassy-rs/embassy", rev = "51d87c6603631fda6fb59ca1a65a99c08138b081" } 43 | embassy-executor = { git = "https://github.com/embassy-rs/embassy", rev = "51d87c6603631fda6fb59ca1a65a99c08138b081" } 44 | embassy-rp = { git = "https://github.com/embassy-rs/embassy", rev = "51d87c6603631fda6fb59ca1a65a99c08138b081" } 45 | embassy-sync = { git = "https://github.com/embassy-rs/embassy", rev = "51d87c6603631fda6fb59ca1a65a99c08138b081" } 46 | embassy-time = { git = "https://github.com/embassy-rs/embassy", rev = "51d87c6603631fda6fb59ca1a65a99c08138b081" } 47 | embassy-usb = { git = "https://github.com/embassy-rs/embassy", rev = "51d87c6603631fda6fb59ca1a65a99c08138b081" } 48 | embassy-usb-driver = { git = "https://github.com/embassy-rs/embassy", rev = "51d87c6603631fda6fb59ca1a65a99c08138b081" } 49 | postcard-rpc = { path = "../../source/postcard-rpc" } 50 | # grr 51 | pio-proc = { git = "https://github.com/rp-rs/pio-rs", rev = "fa586448b0b223217eec8c92c19fe6823dd04cc4" } 52 | pio = { git = "https://github.com/rp-rs/pio-rs", rev = "fa586448b0b223217eec8c92c19fe6823dd04cc4" } 53 | -------------------------------------------------------------------------------- /example/firmware/build.rs: -------------------------------------------------------------------------------- 1 | //! This build script copies the `memory.x` file from the crate root into 2 | //! a directory where the linker can always find it at build time. 3 | //! For many projects this is optional, as the linker always searches the 4 | //! project root directory -- wherever `Cargo.toml` is. However, if you 5 | //! are using a workspace or have a more complicated build setup, this 6 | //! build script becomes required. Additionally, by requesting that 7 | //! Cargo re-run the build script whenever `memory.x` is changed, 8 | //! updating `memory.x` ensures a rebuild of the application with the 9 | //! new memory settings. 10 | 11 | use std::env; 12 | use std::fs::File; 13 | use std::io::Write; 14 | use std::path::PathBuf; 15 | 16 | fn main() { 17 | // Put `memory.x` in our output directory and ensure it's 18 | // on the linker search path. 19 | let out = &PathBuf::from(env::var_os("OUT_DIR").unwrap()); 20 | File::create(out.join("memory.x")) 21 | .unwrap() 22 | .write_all(include_bytes!("memory.x")) 23 | .unwrap(); 24 | println!("cargo:rustc-link-search={}", out.display()); 25 | 26 | // By default, Cargo will re-run a build script whenever 27 | // any file in the project changes. By specifying `memory.x` 28 | // here, we ensure the build script is only re-run when 29 | // `memory.x` is changed. 30 | println!("cargo:rerun-if-changed=memory.x"); 31 | 32 | println!("cargo:rustc-link-arg-bins=--nmagic"); 33 | println!("cargo:rustc-link-arg-bins=-Tlink.x"); 34 | println!("cargo:rustc-link-arg-bins=-Tlink-rp.x"); 35 | println!("cargo:rustc-link-arg-bins=-Tdefmt.x"); 36 | } 37 | -------------------------------------------------------------------------------- /example/firmware/memory.x: -------------------------------------------------------------------------------- 1 | MEMORY { 2 | BOOT2 : ORIGIN = 0x10000000, LENGTH = 0x100 3 | FLASH : ORIGIN = 0x10000100, LENGTH = 2048K - 0x100 4 | 5 | /* Pick one of the two options for RAM layout */ 6 | 7 | /* OPTION A: Use all RAM banks as one big block */ 8 | /* Reasonable, unless you are doing something */ 9 | /* really particular with DMA or other concurrent */ 10 | /* access that would benefit from striping */ 11 | RAM : ORIGIN = 0x20000000, LENGTH = 264K 12 | 13 | /* OPTION B: Keep the unstriped sections separate */ 14 | /* RAM: ORIGIN = 0x20000000, LENGTH = 256K */ 15 | /* SCRATCH_A: ORIGIN = 0x20040000, LENGTH = 4K */ 16 | /* SCRATCH_B: ORIGIN = 0x20041000, LENGTH = 4K */ 17 | } 18 | -------------------------------------------------------------------------------- /example/firmware/src/bin/comms-01.rs: -------------------------------------------------------------------------------- 1 | #![no_std] 2 | #![no_main] 3 | 4 | use defmt::info; 5 | use embassy_executor::Spawner; 6 | use embassy_rp::{peripherals::USB, usb}; 7 | use embassy_sync::blocking_mutex::raw::ThreadModeRawMutex; 8 | use embassy_usb::{Config, UsbDevice}; 9 | use postcard_rpc::{ 10 | define_dispatch, 11 | header::VarHeader, 12 | server::{ 13 | impls::embassy_usb_v0_4::{ 14 | dispatch_impl::{WireRxBuf, WireRxImpl, WireSpawnImpl, WireStorage, WireTxImpl}, 15 | PacketBuffers, 16 | }, 17 | Dispatch, Server, 18 | }, 19 | }; 20 | use static_cell::ConstStaticCell; 21 | use workbook_fw::{get_unique_id, Irqs}; 22 | use workbook_icd::{PingEndpoint, ENDPOINT_LIST, TOPICS_IN_LIST, TOPICS_OUT_LIST}; 23 | use {defmt_rtt as _, panic_probe as _}; 24 | 25 | pub struct Context; 26 | 27 | type AppDriver = usb::Driver<'static, USB>; 28 | type AppStorage = WireStorage; 29 | type BufStorage = PacketBuffers<1024, 1024>; 30 | type AppTx = WireTxImpl; 31 | type AppRx = WireRxImpl; 32 | type AppServer = Server; 33 | 34 | static PBUFS: ConstStaticCell = ConstStaticCell::new(BufStorage::new()); 35 | static STORAGE: AppStorage = AppStorage::new(); 36 | 37 | fn usb_config() -> Config<'static> { 38 | let mut config = Config::new(0x16c0, 0x27DD); 39 | config.manufacturer = Some("OneVariable"); 40 | config.product = Some("ov-twin"); 41 | config.serial_number = Some("12345678"); 42 | 43 | // Required for windows compatibility. 44 | // https://developer.nordicsemi.com/nRF_Connect_SDK/doc/1.9.1/kconfig/CONFIG_CDC_ACM_IAD.html#help 45 | config.device_class = 0xEF; 46 | config.device_sub_class = 0x02; 47 | config.device_protocol = 0x01; 48 | config.composite_with_iads = true; 49 | 50 | config 51 | } 52 | define_dispatch! { 53 | app: MyApp; 54 | spawn_fn: spawn_fn; 55 | tx_impl: AppTx; 56 | spawn_impl: WireSpawnImpl; 57 | context: Context; 58 | 59 | endpoints: { 60 | list: ENDPOINT_LIST; 61 | 62 | | EndpointTy | kind | handler | 63 | | ---------- | ---- | ------- | 64 | | PingEndpoint | blocking | ping_handler | 65 | }; 66 | topics_in: { 67 | list: TOPICS_IN_LIST; 68 | 69 | | TopicTy | kind | handler | 70 | | ---------- | ---- | ------- | 71 | }; 72 | topics_out: { 73 | list: TOPICS_OUT_LIST; 74 | }; 75 | } 76 | 77 | #[embassy_executor::main] 78 | async fn main(spawner: Spawner) { 79 | // SYSTEM INIT 80 | info!("Start"); 81 | let mut p = embassy_rp::init(Default::default()); 82 | let unique_id = defmt::unwrap!(get_unique_id(&mut p.FLASH)); 83 | info!("id: {=u64:016X}", unique_id); 84 | 85 | // USB/RPC INIT 86 | let driver = usb::Driver::new(p.USB, Irqs); 87 | let pbufs = PBUFS.take(); 88 | let config = usb_config(); 89 | 90 | let context = Context; 91 | let (device, tx_impl, rx_impl) = STORAGE.init(driver, config, pbufs.tx_buf.as_mut_slice()); 92 | let dispatcher = MyApp::new(context, spawner.into()); 93 | let vkk = dispatcher.min_key_len(); 94 | let mut server: AppServer = Server::new( 95 | tx_impl, 96 | rx_impl, 97 | pbufs.rx_buf.as_mut_slice(), 98 | dispatcher, 99 | vkk, 100 | ); 101 | spawner.must_spawn(usb_task(device)); 102 | 103 | loop { 104 | // If the host disconnects, we'll return an error here. 105 | // If this happens, just wait until the host reconnects 106 | let _ = server.run().await; 107 | } 108 | } 109 | 110 | /// This handles the low level USB management 111 | #[embassy_executor::task] 112 | pub async fn usb_task(mut usb: UsbDevice<'static, AppDriver>) { 113 | usb.run().await; 114 | } 115 | 116 | // --- 117 | 118 | fn ping_handler(_context: &mut Context, _header: VarHeader, rqst: u32) -> u32 { 119 | info!("ping"); 120 | rqst 121 | } 122 | -------------------------------------------------------------------------------- /example/firmware/src/bin/comms-02.rs: -------------------------------------------------------------------------------- 1 | #![no_std] 2 | #![no_main] 3 | 4 | use defmt::info; 5 | use embassy_executor::Spawner; 6 | use embassy_rp::{ 7 | gpio::{Level, Output}, 8 | peripherals::{PIO0, SPI0, USB}, 9 | pio::Pio, 10 | spi::{self, Spi}, 11 | usb, 12 | }; 13 | use embassy_sync::{blocking_mutex::raw::ThreadModeRawMutex, mutex::Mutex}; 14 | use embassy_time::{Delay, Duration, Ticker}; 15 | use embassy_usb::{Config, UsbDevice}; 16 | use embedded_hal_bus::spi::ExclusiveDevice; 17 | use lis3dh_async::{Lis3dh, Lis3dhSPI}; 18 | use portable_atomic::{AtomicBool, Ordering}; 19 | use postcard_rpc::{ 20 | define_dispatch, 21 | header::VarHeader, 22 | server::{ 23 | impls::embassy_usb_v0_4::{ 24 | dispatch_impl::{ 25 | spawn_fn, WireRxBuf, WireRxImpl, WireSpawnImpl, WireStorage, WireTxImpl, 26 | }, 27 | PacketBuffers, 28 | }, 29 | Dispatch, Sender, Server, SpawnContext, 30 | }, 31 | }; 32 | use smart_leds::{colors::BLACK, RGB8}; 33 | use static_cell::{ConstStaticCell, StaticCell}; 34 | use workbook_fw::{ 35 | get_unique_id, 36 | ws2812::{self, Ws2812}, 37 | Irqs, 38 | }; 39 | use workbook_icd::{ 40 | AccelTopic, Acceleration, BadPositionError, GetUniqueIdEndpoint, PingEndpoint, Rgb8, 41 | SetAllLedEndpoint, SetSingleLedEndpoint, SingleLed, StartAccel, StartAccelerationEndpoint, 42 | StopAccelerationEndpoint, ENDPOINT_LIST, TOPICS_IN_LIST, TOPICS_OUT_LIST, 43 | }; 44 | use {defmt_rtt as _, panic_probe as _}; 45 | 46 | pub type Accel = 47 | Lis3dh, Output<'static>, Delay>>>; 48 | static ACCEL: StaticCell> = StaticCell::new(); 49 | 50 | pub struct Context { 51 | pub unique_id: u64, 52 | pub ws2812: Ws2812<'static, PIO0, 0, 24>, 53 | pub ws2812_state: [RGB8; 24], 54 | pub accel: &'static Mutex, 55 | } 56 | 57 | pub struct SpawnCtx { 58 | pub accel: &'static Mutex, 59 | } 60 | 61 | impl SpawnContext for Context { 62 | type SpawnCtxt = SpawnCtx; 63 | fn spawn_ctxt(&mut self) -> Self::SpawnCtxt { 64 | SpawnCtx { accel: self.accel } 65 | } 66 | } 67 | 68 | type AppDriver = usb::Driver<'static, USB>; 69 | type AppStorage = WireStorage; 70 | type BufStorage = PacketBuffers<1024, 1024>; 71 | type AppTx = WireTxImpl; 72 | type AppRx = WireRxImpl; 73 | type AppServer = Server; 74 | 75 | static PBUFS: ConstStaticCell = ConstStaticCell::new(BufStorage::new()); 76 | static STORAGE: AppStorage = AppStorage::new(); 77 | 78 | fn usb_config() -> Config<'static> { 79 | let mut config = Config::new(0x16c0, 0x27DD); 80 | config.manufacturer = Some("OneVariable"); 81 | config.product = Some("ov-twin"); 82 | config.serial_number = Some("12345678"); 83 | 84 | // Required for windows compatibility. 85 | // https://developer.nordicsemi.com/nRF_Connect_SDK/doc/1.9.1/kconfig/CONFIG_CDC_ACM_IAD.html#help 86 | config.device_class = 0xEF; 87 | config.device_sub_class = 0x02; 88 | config.device_protocol = 0x01; 89 | config.composite_with_iads = true; 90 | 91 | config 92 | } 93 | 94 | define_dispatch! { 95 | app: MyApp; 96 | spawn_fn: spawn_fn; 97 | tx_impl: AppTx; 98 | spawn_impl: WireSpawnImpl; 99 | context: Context; 100 | 101 | endpoints: { 102 | list: ENDPOINT_LIST; 103 | 104 | | EndpointTy | kind | handler | 105 | | ---------- | ---- | ------- | 106 | | PingEndpoint | blocking | ping_handler | 107 | | GetUniqueIdEndpoint | blocking | unique_id_handler | 108 | | SetSingleLedEndpoint | async | set_led_handler | 109 | | SetAllLedEndpoint | async | set_all_led_handler | 110 | | StartAccelerationEndpoint | spawn | accelerometer_handler | 111 | | StopAccelerationEndpoint | blocking | accelerometer_stop_handler | 112 | }; 113 | topics_in: { 114 | list: TOPICS_IN_LIST; 115 | 116 | | TopicTy | kind | handler | 117 | | ---------- | ---- | ------- | 118 | }; 119 | topics_out: { 120 | list: TOPICS_OUT_LIST; 121 | }; 122 | } 123 | 124 | #[embassy_executor::main] 125 | async fn main(spawner: Spawner) { 126 | // SYSTEM INIT 127 | info!("Start"); 128 | let mut p = embassy_rp::init(Default::default()); 129 | let unique_id = defmt::unwrap!(get_unique_id(&mut p.FLASH)); 130 | info!("id: {=u64:016X}", unique_id); 131 | 132 | // PIO/WS2812 INIT 133 | let Pio { 134 | mut common, sm0, .. 135 | } = Pio::new(p.PIO0, ws2812::Irqs); 136 | let ws2812: Ws2812<'static, PIO0, 0, 24> = Ws2812::new(&mut common, sm0, p.DMA_CH0, p.PIN_25); 137 | 138 | // SPI INIT 139 | let spi = Spi::new( 140 | p.SPI0, 141 | p.PIN_6, // clk 142 | p.PIN_7, // mosi 143 | p.PIN_4, // miso 144 | p.DMA_CH1, 145 | p.DMA_CH2, 146 | spi::Config::default(), 147 | ); 148 | // CS: GPIO5 149 | let bus = ExclusiveDevice::new(spi, Output::new(p.PIN_5, Level::High), Delay); 150 | let acc: Accel = defmt::unwrap!(Lis3dh::new_spi(bus).await.map_err(drop)); 151 | let accel_ref = ACCEL.init(Mutex::new(acc)); 152 | 153 | // USB/RPC INIT 154 | let driver = usb::Driver::new(p.USB, Irqs); 155 | let pbufs = PBUFS.take(); 156 | let config = usb_config(); 157 | 158 | let context = Context { 159 | unique_id, 160 | ws2812, 161 | ws2812_state: [BLACK; 24], 162 | accel: accel_ref, 163 | }; 164 | 165 | let (device, tx_impl, rx_impl) = STORAGE.init(driver, config, pbufs.tx_buf.as_mut_slice()); 166 | 167 | // Set timeout to 4ms/frame, instead of the default 2ms/frame 168 | tx_impl.set_timeout_ms_per_frame(4).await; 169 | 170 | let dispatcher = MyApp::new(context, spawner.into()); 171 | let vkk = dispatcher.min_key_len(); 172 | let mut server: AppServer = Server::new( 173 | tx_impl, 174 | rx_impl, 175 | pbufs.rx_buf.as_mut_slice(), 176 | dispatcher, 177 | vkk, 178 | ); 179 | spawner.must_spawn(usb_task(device)); 180 | 181 | loop { 182 | // If the host disconnects, we'll return an error here. 183 | // If this happens, just wait until the host reconnects 184 | let _ = server.run().await; 185 | } 186 | } 187 | 188 | /// This handles the low level USB management 189 | #[embassy_executor::task] 190 | pub async fn usb_task(mut usb: UsbDevice<'static, AppDriver>) { 191 | usb.run().await; 192 | } 193 | 194 | // --- 195 | 196 | fn ping_handler(_context: &mut Context, _header: VarHeader, rqst: u32) -> u32 { 197 | info!("ping"); 198 | rqst 199 | } 200 | 201 | fn unique_id_handler(context: &mut Context, _header: VarHeader, _rqst: ()) -> u64 { 202 | info!("unique_id"); 203 | context.unique_id 204 | } 205 | 206 | async fn set_led_handler( 207 | context: &mut Context, 208 | _header: VarHeader, 209 | rqst: SingleLed, 210 | ) -> Result<(), BadPositionError> { 211 | info!("set_led"); 212 | if rqst.position >= 24 { 213 | return Err(BadPositionError); 214 | } 215 | let pos = rqst.position as usize; 216 | context.ws2812_state[pos] = RGB8 { 217 | r: rqst.rgb.r, 218 | g: rqst.rgb.g, 219 | b: rqst.rgb.b, 220 | }; 221 | context.ws2812.write(&context.ws2812_state).await; 222 | Ok(()) 223 | } 224 | 225 | async fn set_all_led_handler(context: &mut Context, _header: VarHeader, rqst: [Rgb8; 24]) { 226 | info!("set_all_led"); 227 | context 228 | .ws2812_state 229 | .iter_mut() 230 | .zip(rqst.iter()) 231 | .for_each(|(s, rgb)| { 232 | s.r = rgb.r; 233 | s.g = rgb.g; 234 | s.b = rgb.b; 235 | }); 236 | context.ws2812.write(&context.ws2812_state).await; 237 | } 238 | 239 | static STOP: AtomicBool = AtomicBool::new(false); 240 | 241 | #[embassy_executor::task] 242 | async fn accelerometer_handler( 243 | context: SpawnCtx, 244 | header: VarHeader, 245 | rqst: StartAccel, 246 | sender: Sender, 247 | ) { 248 | let mut accel = context.accel.lock().await; 249 | if sender 250 | .reply::(header.seq_no, &()) 251 | .await 252 | .is_err() 253 | { 254 | defmt::error!("Failed to reply, stopping accel"); 255 | return; 256 | } 257 | 258 | defmt::unwrap!(accel.set_range(lis3dh_async::Range::G8).await.map_err(drop)); 259 | 260 | let mut ticker = Ticker::every(Duration::from_millis(rqst.interval_ms.into())); 261 | let mut seq = 0u8; 262 | while !STOP.load(Ordering::Acquire) { 263 | ticker.next().await; 264 | let acc = defmt::unwrap!(accel.accel_raw().await.map_err(drop)); 265 | defmt::println!("ACC: {=i16},{=i16},{=i16}", acc.x, acc.y, acc.z); 266 | let msg = Acceleration { 267 | x: acc.x, 268 | y: acc.y, 269 | z: acc.z, 270 | }; 271 | if sender 272 | .publish::(seq.into(), &msg) 273 | .await 274 | .is_err() 275 | { 276 | defmt::error!("Send error!"); 277 | break; 278 | } 279 | seq = seq.wrapping_add(1); 280 | } 281 | defmt::info!("Stopping!"); 282 | STOP.store(false, Ordering::Release); 283 | } 284 | 285 | fn accelerometer_stop_handler(context: &mut Context, _header: VarHeader, _rqst: ()) -> bool { 286 | info!("accel_stop"); 287 | let was_busy = context.accel.try_lock().is_err(); 288 | if was_busy { 289 | STOP.store(true, Ordering::Release); 290 | } 291 | was_busy 292 | } 293 | -------------------------------------------------------------------------------- /example/firmware/src/bin/hello-01.rs: -------------------------------------------------------------------------------- 1 | #![no_std] 2 | #![no_main] 3 | 4 | use defmt::info; 5 | use embassy_executor::Spawner; 6 | use embassy_rp::{peripherals::PIO0, pio::Pio}; 7 | 8 | use embassy_time::{Duration, Ticker}; 9 | 10 | use smart_leds::colors; 11 | use workbook_fw::{ 12 | get_unique_id, 13 | ws2812::{self, Ws2812}, 14 | NUM_SMARTLEDS, 15 | }; 16 | 17 | // GPIO pins we'll need for this part: 18 | // 19 | // | GPIO Name | Usage | Notes | 20 | // | :--- | :--- | :--- | 21 | // | GPIO25 | Smart LED | 3v3 output | 22 | 23 | #[embassy_executor::main] 24 | async fn main(spawner: Spawner) { 25 | // SYSTEM INIT 26 | info!("Start"); 27 | 28 | let mut p = embassy_rp::init(Default::default()); 29 | let unique_id = get_unique_id(&mut p.FLASH).unwrap(); 30 | info!("id: {=u64:016X}", unique_id); 31 | 32 | // PIO/WS2812 INIT 33 | let Pio { 34 | mut common, sm0, .. 35 | } = Pio::new(p.PIO0, ws2812::Irqs); 36 | 37 | // GPIO25 is used for Smart LEDs 38 | let ws2812: Ws2812<'static, PIO0, 0, NUM_SMARTLEDS> = 39 | Ws2812::new(&mut common, sm0, p.DMA_CH0, p.PIN_25); 40 | 41 | // Start the LED task 42 | spawner.must_spawn(led_task(ws2812)); 43 | } 44 | 45 | // This is our LED task 46 | #[embassy_executor::task] 47 | async fn led_task(mut ws2812: Ws2812<'static, PIO0, 0, NUM_SMARTLEDS>) { 48 | // Tick every 100ms 49 | let mut ticker = Ticker::every(Duration::from_millis(100)); 50 | let mut idx = 0; 51 | loop { 52 | // Wait for the next update time 53 | ticker.next().await; 54 | 55 | let mut colors = [colors::BLACK; NUM_SMARTLEDS]; 56 | 57 | // A little iterator trickery to pick a moving set of four LEDs 58 | // to light up 59 | let (before, after) = colors.split_at_mut(idx); 60 | after 61 | .iter_mut() 62 | .chain(before.iter_mut()) 63 | .take(4) 64 | .for_each(|l| { 65 | // The LEDs are very bright! 66 | *l = colors::GREEN / 16; 67 | }); 68 | 69 | ws2812.write(&colors).await; 70 | idx += 1; 71 | if idx >= NUM_SMARTLEDS { 72 | idx = 0; 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /example/firmware/src/bin/hello-02.rs: -------------------------------------------------------------------------------- 1 | #![no_std] 2 | #![no_main] 3 | 4 | use defmt::info; 5 | use embassy_executor::Spawner; 6 | use embassy_rp::{peripherals::PIO0, pio::Pio}; 7 | 8 | use embassy_time::{Duration, Ticker}; 9 | 10 | use smart_leds::colors; 11 | use workbook_fw::{ 12 | get_unique_id, 13 | ws2812::{self, Ws2812}, 14 | Buttons, Potentiometer, NUM_SMARTLEDS, 15 | }; 16 | 17 | // GPIO pins we'll need for this part: 18 | // 19 | // | GPIO Name | Usage | Notes | 20 | // | :--- | :--- | :--- | 21 | // | GPIO00 | Button 1 | Button Pad (left) - active LOW | 22 | // | GPIO01 | Button 2 | Button Pad (left) - active LOW | 23 | // | GPIO02 | Button 3 | Button Pad (left) - active LOW | 24 | // | GPIO03 | Button 4 | Button Pad (left) - active LOW | 25 | // | GPIO18 | Button 5 | Button Pad (right) - active LOW | 26 | // | GPIO19 | Button 6 | Button Pad (right) - active LOW | 27 | // | GPIO20 | Button 7 | Button Pad (right) - active LOW | 28 | // | GPIO21 | Button 8 | Button Pad (right) - active LOW | 29 | // | GPIO25 | Smart LED | 3v3 output | 30 | // | GPIO26 | ADC0 | Potentiometer | 31 | #[embassy_executor::main] 32 | async fn main(spawner: Spawner) { 33 | // SYSTEM INIT 34 | info!("Start"); 35 | 36 | let mut p = embassy_rp::init(Default::default()); 37 | let unique_id = get_unique_id(&mut p.FLASH).unwrap(); 38 | info!("id: {=u64:016X}", unique_id); 39 | 40 | // PIO/WS2812 INIT 41 | let Pio { 42 | mut common, sm0, .. 43 | } = Pio::new(p.PIO0, ws2812::Irqs); 44 | 45 | // GPIO25 is used for Smart LEDs 46 | let ws2812: Ws2812<'static, PIO0, 0, NUM_SMARTLEDS> = 47 | Ws2812::new(&mut common, sm0, p.DMA_CH0, p.PIN_25); 48 | 49 | let buttons = Buttons::new( 50 | p.PIN_0, p.PIN_1, p.PIN_2, p.PIN_3, p.PIN_18, p.PIN_19, p.PIN_20, p.PIN_21, 51 | ); 52 | let potentiometer = Potentiometer::new(p.ADC, p.PIN_26); 53 | 54 | // Start the LED task 55 | spawner.must_spawn(led_task(ws2812)); 56 | 57 | // Start the Button task 58 | spawner.must_spawn(button_task(buttons)); 59 | 60 | // Start the Potentiometer task 61 | spawner.must_spawn(pot_task(potentiometer)); 62 | } 63 | 64 | // This is our Button task 65 | #[embassy_executor::task] 66 | async fn button_task(buttons: Buttons) { 67 | let mut last = [false; Buttons::COUNT]; 68 | let mut ticker = Ticker::every(Duration::from_millis(10)); 69 | loop { 70 | ticker.next().await; 71 | let now = buttons.read_all(); 72 | if now != last { 73 | info!("Buttons changed: {:?}", now); 74 | last = now; 75 | } 76 | } 77 | } 78 | 79 | // This is our Potentiometer task 80 | #[embassy_executor::task] 81 | async fn pot_task(mut pot: Potentiometer) { 82 | let mut last = pot.read().await; 83 | let mut ticker = Ticker::every(Duration::from_millis(100)); 84 | loop { 85 | ticker.next().await; 86 | let now = pot.read().await; 87 | if now.abs_diff(last) > 64 { 88 | info!("Potentiometer changed: {=u16}", now); 89 | last = now; 90 | } 91 | } 92 | } 93 | 94 | // This is our LED task 95 | #[embassy_executor::task] 96 | async fn led_task(mut ws2812: Ws2812<'static, PIO0, 0, NUM_SMARTLEDS>) { 97 | // Tick every 100ms 98 | let mut ticker = Ticker::every(Duration::from_millis(100)); 99 | let mut idx = 0; 100 | loop { 101 | // Wait for the next update time 102 | ticker.next().await; 103 | 104 | let mut colors = [colors::BLACK; NUM_SMARTLEDS]; 105 | 106 | // A little iterator trickery to pick a moving set of four LEDs 107 | // to light up 108 | let (before, after) = colors.split_at_mut(idx); 109 | after 110 | .iter_mut() 111 | .chain(before.iter_mut()) 112 | .take(4) 113 | .for_each(|l| { 114 | // The LEDs are very bright! 115 | *l = colors::BLUE / 16; 116 | }); 117 | 118 | ws2812.write(&colors).await; 119 | idx += 1; 120 | if idx >= NUM_SMARTLEDS { 121 | idx = 0; 122 | } 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /example/firmware/src/bin/hello-03.rs: -------------------------------------------------------------------------------- 1 | #![no_std] 2 | #![no_main] 3 | 4 | use defmt::info; 5 | use embassy_executor::Spawner; 6 | use embassy_rp::{peripherals::PIO0, pio::Pio}; 7 | 8 | use embassy_time::{Duration, Ticker}; 9 | 10 | use smart_leds::colors; 11 | use workbook_fw::{ 12 | get_unique_id, 13 | ws2812::{self, Ws2812}, 14 | Accelerometer, Buttons, Potentiometer, NUM_SMARTLEDS, 15 | }; 16 | 17 | // GPIO pins we'll need for this part: 18 | // 19 | // | GPIO Name | Usage | Notes | 20 | // | :--- | :--- | :--- | 21 | // | GPIO00 | Button 1 | Button Pad (left) - active LOW | 22 | // | GPIO01 | Button 2 | Button Pad (left) - active LOW | 23 | // | GPIO02 | Button 3 | Button Pad (left) - active LOW | 24 | // | GPIO03 | Button 4 | Button Pad (left) - active LOW | 25 | // | GPIO04 | SPI MISO/CIPO | LIS3DH | 26 | // | GPIO05 | SPI CSn | LIS3DH | 27 | // | GPIO06 | SPI CLK | LIS3DH | 28 | // | GPIO07 | SPI MOSI/COPI | LIS3DH | 29 | // | GPIO18 | Button 5 | Button Pad (right) - active LOW | 30 | // | GPIO19 | Button 6 | Button Pad (right) - active LOW | 31 | // | GPIO20 | Button 7 | Button Pad (right) - active LOW | 32 | // | GPIO21 | Button 8 | Button Pad (right) - active LOW | 33 | // | GPIO25 | Smart LED | 3v3 output | 34 | // | GPIO26 | ADC0 | Potentiometer | 35 | #[embassy_executor::main] 36 | async fn main(spawner: Spawner) { 37 | // SYSTEM INIT 38 | info!("Start"); 39 | 40 | let mut p = embassy_rp::init(Default::default()); 41 | let unique_id = get_unique_id(&mut p.FLASH).unwrap(); 42 | info!("id: {=u64:016X}", unique_id); 43 | 44 | // PIO/WS2812 INIT 45 | let Pio { 46 | mut common, sm0, .. 47 | } = Pio::new(p.PIO0, ws2812::Irqs); 48 | 49 | // GPIO25 is used for Smart LEDs 50 | let ws2812: Ws2812<'static, PIO0, 0, NUM_SMARTLEDS> = 51 | Ws2812::new(&mut common, sm0, p.DMA_CH0, p.PIN_25); 52 | 53 | let buttons = Buttons::new( 54 | p.PIN_0, p.PIN_1, p.PIN_2, p.PIN_3, p.PIN_18, p.PIN_19, p.PIN_20, p.PIN_21, 55 | ); 56 | let potentiometer = Potentiometer::new(p.ADC, p.PIN_26); 57 | let accel = Accelerometer::new( 58 | p.SPI0, p.PIN_6, p.PIN_7, p.PIN_4, p.PIN_5, p.DMA_CH1, p.DMA_CH2, 59 | ) 60 | .await; 61 | 62 | // Start the LED task 63 | spawner.must_spawn(led_task(ws2812)); 64 | 65 | // Start the Button task 66 | spawner.must_spawn(button_task(buttons)); 67 | 68 | // Start the Potentiometer task 69 | spawner.must_spawn(pot_task(potentiometer)); 70 | 71 | // Start the accelerometer task 72 | spawner.must_spawn(accel_task(accel)); 73 | } 74 | 75 | // This is our Accelerometer task 76 | #[embassy_executor::task] 77 | async fn accel_task(mut accel: Accelerometer) { 78 | let mut ticker = Ticker::every(Duration::from_millis(250)); 79 | loop { 80 | ticker.next().await; 81 | let reading = accel.read().await; 82 | info!("accelerometer: {:?}", reading); 83 | } 84 | } 85 | 86 | // This is our Button task 87 | #[embassy_executor::task] 88 | async fn button_task(buttons: Buttons) { 89 | let mut last = [false; Buttons::COUNT]; 90 | let mut ticker = Ticker::every(Duration::from_millis(10)); 91 | loop { 92 | ticker.next().await; 93 | let now = buttons.read_all(); 94 | if now != last { 95 | info!("Buttons changed: {:?}", now); 96 | last = now; 97 | } 98 | } 99 | } 100 | 101 | // This is our Potentiometer task 102 | #[embassy_executor::task] 103 | async fn pot_task(mut pot: Potentiometer) { 104 | let mut last = pot.read().await; 105 | let mut ticker = Ticker::every(Duration::from_millis(100)); 106 | loop { 107 | ticker.next().await; 108 | let now = pot.read().await; 109 | if now.abs_diff(last) > 64 { 110 | info!("Potentiometer changed: {=u16}", now); 111 | last = now; 112 | } 113 | } 114 | } 115 | 116 | // This is our LED task 117 | #[embassy_executor::task] 118 | async fn led_task(mut ws2812: Ws2812<'static, PIO0, 0, NUM_SMARTLEDS>) { 119 | // Tick every 100ms 120 | let mut ticker = Ticker::every(Duration::from_millis(100)); 121 | let mut idx = 0; 122 | loop { 123 | // Wait for the next update time 124 | ticker.next().await; 125 | 126 | let mut colors = [colors::BLACK; NUM_SMARTLEDS]; 127 | 128 | // A little iterator trickery to pick a moving set of four LEDs 129 | // to light up 130 | let (before, after) = colors.split_at_mut(idx); 131 | after 132 | .iter_mut() 133 | .chain(before.iter_mut()) 134 | .take(4) 135 | .for_each(|l| { 136 | // The LEDs are very bright! 137 | *l = colors::WHITE / 16; 138 | }); 139 | 140 | ws2812.write(&colors).await; 141 | idx += 1; 142 | if idx >= NUM_SMARTLEDS { 143 | idx = 0; 144 | } 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /example/firmware/src/bin/logging.rs: -------------------------------------------------------------------------------- 1 | #![no_std] 2 | #![no_main] 3 | 4 | use defmt::info; 5 | use embassy_executor::Spawner; 6 | use embassy_rp::{peripherals::USB, usb}; 7 | use embassy_sync::blocking_mutex::raw::ThreadModeRawMutex; 8 | use embassy_time::{Duration, Ticker}; 9 | use embassy_usb::{Config, UsbDevice}; 10 | use postcard_rpc::{ 11 | define_dispatch, sender_fmt, 12 | server::{ 13 | impls::embassy_usb_v0_4::{ 14 | dispatch_impl::{WireRxBuf, WireRxImpl, WireSpawnImpl, WireStorage, WireTxImpl}, 15 | PacketBuffers, 16 | }, 17 | Dispatch, Sender, Server, 18 | }, 19 | }; 20 | use static_cell::ConstStaticCell; 21 | use workbook_fw::Irqs; 22 | use workbook_icd::{ENDPOINT_LIST, TOPICS_IN_LIST, TOPICS_OUT_LIST}; 23 | use {defmt_rtt as _, panic_probe as _}; 24 | 25 | pub struct Context {} 26 | 27 | type AppDriver = usb::Driver<'static, USB>; 28 | type AppStorage = WireStorage; 29 | type BufStorage = PacketBuffers<1024, 1024>; 30 | type AppTx = WireTxImpl; 31 | type AppRx = WireRxImpl; 32 | type AppServer = Server; 33 | 34 | static PBUFS: ConstStaticCell = ConstStaticCell::new(BufStorage::new()); 35 | static STORAGE: AppStorage = AppStorage::new(); 36 | 37 | fn usb_config() -> Config<'static> { 38 | let mut config = Config::new(0x16c0, 0x27DD); 39 | config.manufacturer = Some("OneVariable"); 40 | config.product = Some("ov-twin"); 41 | config.serial_number = Some("12345678"); 42 | 43 | // Required for windows compatibility. 44 | // https://developer.nordicsemi.com/nRF_Connect_SDK/doc/1.9.1/kconfig/CONFIG_CDC_ACM_IAD.html#help 45 | config.device_class = 0xEF; 46 | config.device_sub_class = 0x02; 47 | config.device_protocol = 0x01; 48 | config.composite_with_iads = true; 49 | 50 | config 51 | } 52 | 53 | define_dispatch! { 54 | app: MyApp; 55 | spawn_fn: spawn_fn; 56 | tx_impl: AppTx; 57 | spawn_impl: WireSpawnImpl; 58 | context: Context; 59 | 60 | endpoints: { 61 | list: ENDPOINT_LIST; 62 | 63 | | EndpointTy | kind | handler | 64 | | ---------- | ---- | ------- | 65 | }; 66 | topics_in: { 67 | list: TOPICS_IN_LIST; 68 | 69 | | TopicTy | kind | handler | 70 | | ---------- | ---- | ------- | 71 | }; 72 | topics_out: { 73 | list: TOPICS_OUT_LIST; 74 | }; 75 | } 76 | 77 | #[embassy_executor::main] 78 | async fn main(spawner: Spawner) { 79 | // SYSTEM INIT 80 | info!("Start"); 81 | let p = embassy_rp::init(Default::default()); 82 | 83 | // USB/RPC INIT 84 | let driver = usb::Driver::new(p.USB, Irqs); 85 | let pbufs = PBUFS.take(); 86 | let config = usb_config(); 87 | 88 | let context = Context {}; 89 | 90 | let (device, tx_impl, rx_impl) = STORAGE.init(driver, config, pbufs.tx_buf.as_mut_slice()); 91 | let dispatcher = MyApp::new(context, spawner.into()); 92 | let vkk = dispatcher.min_key_len(); 93 | let server: AppServer = Server::new( 94 | tx_impl, 95 | rx_impl, 96 | pbufs.rx_buf.as_mut_slice(), 97 | dispatcher, 98 | vkk, 99 | ); 100 | let sender = server.sender(); 101 | spawner.must_spawn(usb_task(device)); 102 | spawner.must_spawn(server_task(server)); 103 | spawner.must_spawn(logging_task(sender)); 104 | } 105 | 106 | #[embassy_executor::task] 107 | pub async fn logging_task(sender: Sender) { 108 | let mut ticker = Ticker::every(Duration::from_millis(1000)); 109 | let mut ctr = 0u16; 110 | loop { 111 | ticker.next().await; 112 | defmt::info!("logging"); 113 | if ctr & 0b1 != 0 { 114 | let _ = sender.log_str("Hello world!").await; 115 | } else { 116 | let _ = sender_fmt!(sender, "formatted: {ctr}").await; 117 | } 118 | ctr = ctr.wrapping_add(1); 119 | } 120 | } 121 | 122 | #[embassy_executor::task] 123 | pub async fn server_task(mut server: AppServer) { 124 | loop { 125 | // If the host disconnects, we'll return an error here. 126 | // If this happens, just wait until the host reconnects 127 | let _ = server.run().await; 128 | } 129 | } 130 | 131 | /// This handles the low level USB management 132 | #[embassy_executor::task] 133 | pub async fn usb_task(mut usb: UsbDevice<'static, AppDriver>) { 134 | usb.run().await; 135 | } 136 | -------------------------------------------------------------------------------- /example/firmware/src/bin/minimal.rs: -------------------------------------------------------------------------------- 1 | #![no_std] 2 | #![no_main] 3 | 4 | use defmt::info; 5 | use embassy_executor::Spawner; 6 | use embassy_rp::{peripherals::USB, usb}; 7 | use embassy_sync::blocking_mutex::raw::ThreadModeRawMutex; 8 | use embassy_usb::{Config, UsbDevice}; 9 | use postcard_rpc::{ 10 | define_dispatch, 11 | server::{ 12 | impls::embassy_usb_v0_4::{ 13 | dispatch_impl::{WireRxBuf, WireRxImpl, WireSpawnImpl, WireStorage, WireTxImpl}, 14 | PacketBuffers, 15 | }, 16 | Dispatch, Server, 17 | }, 18 | }; 19 | use static_cell::ConstStaticCell; 20 | use workbook_fw::Irqs; 21 | use workbook_icd::{ENDPOINT_LIST, TOPICS_IN_LIST, TOPICS_OUT_LIST}; 22 | use {defmt_rtt as _, panic_probe as _}; 23 | 24 | pub struct Context {} 25 | 26 | type AppDriver = usb::Driver<'static, USB>; 27 | type AppStorage = WireStorage; 28 | type BufStorage = PacketBuffers<1024, 1024>; 29 | type AppTx = WireTxImpl; 30 | type AppRx = WireRxImpl; 31 | type AppServer = Server; 32 | 33 | static PBUFS: ConstStaticCell = ConstStaticCell::new(BufStorage::new()); 34 | static STORAGE: AppStorage = AppStorage::new(); 35 | 36 | fn usb_config() -> Config<'static> { 37 | let mut config = Config::new(0x16c0, 0x27DD); 38 | config.manufacturer = Some("OneVariable"); 39 | config.product = Some("ov-twin"); 40 | config.serial_number = Some("12345678"); 41 | 42 | // Required for windows compatibility. 43 | // https://developer.nordicsemi.com/nRF_Connect_SDK/doc/1.9.1/kconfig/CONFIG_CDC_ACM_IAD.html#help 44 | config.device_class = 0xEF; 45 | config.device_sub_class = 0x02; 46 | config.device_protocol = 0x01; 47 | config.composite_with_iads = true; 48 | 49 | config 50 | } 51 | 52 | define_dispatch! { 53 | app: MyApp; 54 | spawn_fn: spawn_fn; 55 | tx_impl: AppTx; 56 | spawn_impl: WireSpawnImpl; 57 | context: Context; 58 | 59 | endpoints: { 60 | list: ENDPOINT_LIST; 61 | 62 | | EndpointTy | kind | handler | 63 | | ---------- | ---- | ------- | 64 | }; 65 | topics_in: { 66 | list: TOPICS_IN_LIST; 67 | 68 | | TopicTy | kind | handler | 69 | | ---------- | ---- | ------- | 70 | }; 71 | topics_out: { 72 | list: TOPICS_OUT_LIST; 73 | }; 74 | } 75 | 76 | #[embassy_executor::main] 77 | async fn main(spawner: Spawner) { 78 | // SYSTEM INIT 79 | info!("Start"); 80 | let p = embassy_rp::init(Default::default()); 81 | 82 | // USB/RPC INIT 83 | let driver = usb::Driver::new(p.USB, Irqs); 84 | let pbufs = PBUFS.take(); 85 | let config = usb_config(); 86 | 87 | let context = Context {}; 88 | 89 | let (device, tx_impl, rx_impl) = STORAGE.init(driver, config, pbufs.tx_buf.as_mut_slice()); 90 | let dispatcher = MyApp::new(context, spawner.into()); 91 | let vkk = dispatcher.min_key_len(); 92 | let server: AppServer = Server::new( 93 | tx_impl, 94 | rx_impl, 95 | pbufs.rx_buf.as_mut_slice(), 96 | dispatcher, 97 | vkk, 98 | ); 99 | spawner.must_spawn(usb_task(device)); 100 | spawner.must_spawn(server_task(server)); 101 | } 102 | 103 | #[embassy_executor::task] 104 | pub async fn server_task(mut server: AppServer) { 105 | loop { 106 | // If the host disconnects, we'll return an error here. 107 | // If this happens, just wait until the host reconnects 108 | let _ = server.run().await; 109 | } 110 | } 111 | 112 | /// This handles the low level USB management 113 | #[embassy_executor::task] 114 | pub async fn usb_task(mut usb: UsbDevice<'static, AppDriver>) { 115 | usb.run().await; 116 | } 117 | -------------------------------------------------------------------------------- /example/firmware/src/bin/uhoh-00.rs: -------------------------------------------------------------------------------- 1 | //! This is what will be flashed to all boards before the workshop starts. 2 | 3 | #![no_std] 4 | #![no_main] 5 | 6 | use defmt::info; 7 | use embassy_executor::Spawner; 8 | use embassy_rp::{peripherals::PIO0, pio::Pio}; 9 | 10 | use embassy_time::{Duration, Ticker}; 11 | 12 | use smart_leds::RGB; 13 | use workbook_fw::{ 14 | get_unique_id, 15 | ws2812::{self, Ws2812}, 16 | NUM_SMARTLEDS, 17 | }; 18 | 19 | // GPIO pins we'll need for this part: 20 | // 21 | // | GPIO Name | Usage | Notes | 22 | // | :--- | :--- | :--- | 23 | // | GPIO25 | Smart LED | 3v3 output | 24 | 25 | #[embassy_executor::main] 26 | async fn main(spawner: Spawner) { 27 | // SYSTEM INIT 28 | info!("Start"); 29 | 30 | let mut p = embassy_rp::init(Default::default()); 31 | let unique_id = get_unique_id(&mut p.FLASH).unwrap(); 32 | info!("id: {=u64:016X}", unique_id); 33 | 34 | // PIO/WS2812 INIT 35 | let Pio { 36 | mut common, sm0, .. 37 | } = Pio::new(p.PIO0, ws2812::Irqs); 38 | 39 | // GPIO25 is used for 40 | let ws2812: Ws2812<'static, PIO0, 0, NUM_SMARTLEDS> = 41 | Ws2812::new(&mut common, sm0, p.DMA_CH0, p.PIN_25); 42 | 43 | // Start the 44 | spawner.must_spawn(led_task(ws2812)); 45 | } 46 | 47 | // This is our LED task 48 | #[embassy_executor::task] 49 | async fn led_task(mut ws2812: Ws2812<'static, PIO0, 0, NUM_SMARTLEDS>) { 50 | let mut ticker = Ticker::every(Duration::from_millis(25)); 51 | // Fade red up and down so I can see who hasn't been able to flash their board yet 52 | loop { 53 | // Up 54 | for i in 0..=32 { 55 | ticker.next().await; 56 | let color = RGB { r: i, g: 0, b: 0 }; 57 | let colors = [color; NUM_SMARTLEDS]; 58 | ws2812.write(&colors).await; 59 | } 60 | 61 | // Down 62 | for i in (0..=32).rev() { 63 | ticker.next().await; 64 | let color = RGB { r: i, g: 0, b: 0 }; 65 | let colors = [color; NUM_SMARTLEDS]; 66 | ws2812.write(&colors).await; 67 | } 68 | 69 | // Wait 70 | for _ in 0..=32 { 71 | ticker.next().await; 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /example/firmware/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![no_std] 2 | 3 | use { 4 | defmt_rtt as _, 5 | embassy_rp::{ 6 | adc::{self, Adc, Config as AdcConfig}, 7 | bind_interrupts, 8 | flash::{Blocking, Flash}, 9 | gpio::{Input, Level, Output, Pull}, 10 | peripherals::{ 11 | ADC, DMA_CH1, DMA_CH2, FLASH, PIN_0, PIN_1, PIN_18, PIN_19, PIN_2, PIN_20, PIN_21, 12 | PIN_26, PIN_3, PIN_4, PIN_5, PIN_6, PIN_7, SPI0, USB, 13 | }, 14 | spi::{self, Async, Spi}, 15 | usb, 16 | }, 17 | embassy_time::Delay, 18 | embedded_hal_bus::spi::ExclusiveDevice, 19 | lis3dh_async::{Lis3dh, Lis3dhSPI}, 20 | panic_probe as _, 21 | }; 22 | pub mod ws2812; 23 | use embassy_time as _; 24 | 25 | bind_interrupts!(pub struct Irqs { 26 | ADC_IRQ_FIFO => adc::InterruptHandler; 27 | USBCTRL_IRQ => usb::InterruptHandler; 28 | }); 29 | 30 | pub const NUM_SMARTLEDS: usize = 24; 31 | 32 | /// Helper to get unique ID from flash 33 | pub fn get_unique_id(flash: &mut FLASH) -> Option { 34 | let mut flash: Flash<'_, FLASH, Blocking, { 16 * 1024 * 1024 }> = Flash::new_blocking(flash); 35 | 36 | // TODO: For different flash chips, we want to handle things 37 | // differently based on their jedec? That being said: I control 38 | // the hardware for this project, and our flash supports unique ID, 39 | // so oh well. 40 | // 41 | // let jedec = flash.blocking_jedec_id().unwrap(); 42 | 43 | let mut id = [0u8; core::mem::size_of::()]; 44 | flash.blocking_unique_id(&mut id).unwrap(); 45 | Some(u64::from_be_bytes(id)) 46 | } 47 | 48 | pub struct Buttons { 49 | pub buttons: [Input<'static>; 8], 50 | } 51 | 52 | // | GPIO Name | Usage | Notes | 53 | // | :--- | :--- | :--- | 54 | // | GPIO00 | Button 1 | Button Pad (left) - active LOW | 55 | // | GPIO01 | Button 2 | Button Pad (left) - active LOW | 56 | // | GPIO02 | Button 3 | Button Pad (left) - active LOW | 57 | // | GPIO03 | Button 4 | Button Pad (left) - active LOW | 58 | // | GPIO18 | Button 5 | Button Pad (right) - active LOW | 59 | // | GPIO19 | Button 6 | Button Pad (right) - active LOW | 60 | // | GPIO20 | Button 7 | Button Pad (right) - active LOW | 61 | // | GPIO21 | Button 8 | Button Pad (right) - active LOW | 62 | impl Buttons { 63 | pub const COUNT: usize = 8; 64 | 65 | #[allow(clippy::too_many_arguments)] 66 | pub fn new( 67 | b01: PIN_0, 68 | b02: PIN_1, 69 | b03: PIN_2, 70 | b04: PIN_3, 71 | b05: PIN_18, 72 | b06: PIN_19, 73 | b07: PIN_20, 74 | b08: PIN_21, 75 | ) -> Self { 76 | Self { 77 | buttons: [ 78 | Input::new(b01, Pull::Up), 79 | Input::new(b02, Pull::Up), 80 | Input::new(b03, Pull::Up), 81 | Input::new(b04, Pull::Up), 82 | Input::new(b05, Pull::Up), 83 | Input::new(b06, Pull::Up), 84 | Input::new(b07, Pull::Up), 85 | Input::new(b08, Pull::Up), 86 | ], 87 | } 88 | } 89 | 90 | // Read all buttons, and report whether they are PRESSED, e.g. pulled low. 91 | pub fn read_all(&self) -> [bool; Self::COUNT] { 92 | let mut all = [false; Self::COUNT]; 93 | all.iter_mut().zip(self.buttons.iter()).for_each(|(a, b)| { 94 | *a = b.is_low(); 95 | }); 96 | all 97 | } 98 | } 99 | 100 | // | GPIO Name | Usage | Notes | 101 | // | :--- | :--- | :--- | 102 | // | GPIO26 | ADC0 | Potentiometer | 103 | pub struct Potentiometer { 104 | pub adc: Adc<'static, adc::Async>, 105 | pub p26: adc::Channel<'static>, 106 | } 107 | 108 | impl Potentiometer { 109 | pub fn new(adc: ADC, pin: PIN_26) -> Self { 110 | let adc = Adc::new(adc, Irqs, AdcConfig::default()); 111 | let p26 = adc::Channel::new_pin(pin, Pull::None); 112 | Self { adc, p26 } 113 | } 114 | 115 | /// Reads the ADC, returning a value between 0 and 4095. 116 | /// 117 | /// 0 is all the way to the right, and 4095 is all the way to the left 118 | pub async fn read(&mut self) -> u16 { 119 | let Ok(now) = self.adc.read(&mut self.p26).await else { 120 | defmt::panic!("Failed to read ADC!"); 121 | }; 122 | now 123 | } 124 | } 125 | 126 | // | GPIO Name | Usage | Notes | 127 | // | :--- | :--- | :--- | 128 | // | GPIO04 | SPI MISO/CIPO | LIS3DH | 129 | // | GPIO05 | SPI CSn | LIS3DH | 130 | // | GPIO06 | SPI CLK | LIS3DH | 131 | // | GPIO07 | SPI MOSI/COPI | LIS3DH | 132 | type AccSpi = Spi<'static, SPI0, Async>; 133 | type ExclusiveSpi = ExclusiveDevice, Delay>; 134 | type Accel = Lis3dh>; 135 | pub struct Accelerometer { 136 | pub dev: Accel, 137 | } 138 | 139 | #[derive(Debug, PartialEq, defmt::Format)] 140 | pub struct AccelReading { 141 | pub x: i16, 142 | pub y: i16, 143 | pub z: i16, 144 | } 145 | 146 | impl Accelerometer { 147 | pub async fn new( 148 | periph: SPI0, 149 | clk: PIN_6, 150 | copi: PIN_7, 151 | cipo: PIN_4, 152 | csn: PIN_5, 153 | tx_dma: DMA_CH1, 154 | rx_dma: DMA_CH2, 155 | ) -> Self { 156 | let mut cfg = spi::Config::default(); 157 | cfg.frequency = 1_000_000; 158 | let spi = Spi::new(periph, clk, copi, cipo, tx_dma, rx_dma, cfg); 159 | let dev = ExclusiveDevice::new(spi, Output::new(csn, Level::High), Delay); 160 | let Ok(mut dev) = Lis3dh::new_spi(dev).await else { 161 | defmt::panic!("Failed to initialize SPI!"); 162 | }; 163 | if dev.set_range(lis3dh_async::Range::G8).await.is_err() { 164 | defmt::panic!("Error setting range!"); 165 | }; 166 | Self { dev } 167 | } 168 | 169 | pub async fn read(&mut self) -> AccelReading { 170 | let Ok(raw_acc) = self.dev.accel_raw().await else { 171 | defmt::panic!("Failed to get acceleration!"); 172 | }; 173 | AccelReading { 174 | x: raw_acc.x, 175 | y: raw_acc.y, 176 | z: raw_acc.z, 177 | } 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /example/firmware/src/ws2812.rs: -------------------------------------------------------------------------------- 1 | use embassy_rp::{ 2 | bind_interrupts, clocks, 3 | dma::{self, AnyChannel}, 4 | into_ref, 5 | peripherals::PIO0, 6 | pio::{ 7 | Common, Config as PioConfig, FifoJoin, Instance, PioPin, ShiftConfig, ShiftDirection, 8 | StateMachine, 9 | }, 10 | Peripheral, PeripheralRef, 11 | }; 12 | use embassy_time::Timer; 13 | use fixed::types::U24F8; 14 | use fixed_macro::fixed; 15 | use smart_leds::RGB8; 16 | use {defmt_rtt as _, panic_probe as _}; 17 | 18 | bind_interrupts!(pub struct Irqs { 19 | PIO0_IRQ_0 => embassy_rp::pio::InterruptHandler; 20 | }); 21 | 22 | pub struct Ws2812<'d, P: Instance, const S: usize, const N: usize> { 23 | dma: PeripheralRef<'d, AnyChannel>, 24 | sm: StateMachine<'d, P, S>, 25 | } 26 | 27 | impl<'d, P: Instance, const S: usize, const N: usize> Ws2812<'d, P, S, N> { 28 | pub fn new( 29 | pio: &mut Common<'d, P>, 30 | mut sm: StateMachine<'d, P, S>, 31 | dma: impl Peripheral

+ 'd, 32 | pin: impl PioPin, 33 | ) -> Self { 34 | into_ref!(dma); 35 | 36 | // Setup sm0 37 | 38 | // prepare the PIO program 39 | let side_set = pio::SideSet::new(false, 1, false); 40 | let mut a: pio::Assembler<32> = pio::Assembler::new_with_side_set(side_set); 41 | 42 | const T1: u8 = 2; // start bit 43 | const T2: u8 = 5; // data bit 44 | const T3: u8 = 3; // stop bit 45 | const CYCLES_PER_BIT: u32 = (T1 + T2 + T3) as u32; 46 | 47 | let mut wrap_target = a.label(); 48 | let mut wrap_source = a.label(); 49 | let mut do_zero = a.label(); 50 | a.set_with_side_set(pio::SetDestination::PINDIRS, 1, 0); 51 | a.bind(&mut wrap_target); 52 | // Do stop bit 53 | a.out_with_delay_and_side_set(pio::OutDestination::X, 1, T3 - 1, 0); 54 | // Do start bit 55 | a.jmp_with_delay_and_side_set(pio::JmpCondition::XIsZero, &mut do_zero, T1 - 1, 1); 56 | // Do data bit = 1 57 | a.jmp_with_delay_and_side_set(pio::JmpCondition::Always, &mut wrap_target, T2 - 1, 1); 58 | a.bind(&mut do_zero); 59 | // Do data bit = 0 60 | a.nop_with_delay_and_side_set(T2 - 1, 0); 61 | a.bind(&mut wrap_source); 62 | 63 | let prg = a.assemble_with_wrap(wrap_source, wrap_target); 64 | let mut cfg = PioConfig::default(); 65 | 66 | // Pin config 67 | let out_pin = pio.make_pio_pin(pin); 68 | cfg.set_out_pins(&[&out_pin]); 69 | cfg.set_set_pins(&[&out_pin]); 70 | 71 | cfg.use_program(&pio.load_program(&prg), &[&out_pin]); 72 | 73 | // Clock config, measured in kHz to avoid overflows 74 | // TODO CLOCK_FREQ should come from embassy_rp 75 | let clock_freq = U24F8::from_num(clocks::clk_sys_freq() / 1000); 76 | let ws2812_freq = fixed!(800: U24F8); 77 | let bit_freq = ws2812_freq * CYCLES_PER_BIT; 78 | cfg.clock_divider = clock_freq / bit_freq; 79 | 80 | // FIFO config 81 | cfg.fifo_join = FifoJoin::TxOnly; 82 | cfg.shift_out = ShiftConfig { 83 | auto_fill: true, 84 | threshold: 24, 85 | direction: ShiftDirection::Left, 86 | }; 87 | 88 | sm.set_config(&cfg); 89 | sm.set_enable(true); 90 | 91 | Self { 92 | dma: dma.map_into(), 93 | sm, 94 | } 95 | } 96 | 97 | pub async fn write(&mut self, colors: &[RGB8; N]) { 98 | // Precompute the word bytes from the colors 99 | let mut words = [0u32; N]; 100 | for i in 0..N { 101 | let word = (u32::from(colors[i].g) << 24) 102 | | (u32::from(colors[i].r) << 16) 103 | | (u32::from(colors[i].b) << 8); 104 | words[i] = word; 105 | } 106 | 107 | // DMA transfer 108 | self.sm.tx().dma_push(self.dma.reborrow(), &words).await; 109 | 110 | Timer::after_micros(55).await; 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /example/workbook-host/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "workbook-host-client" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies.workbook-icd] 7 | path = "../workbook-icd" 8 | features = ["use-std"] 9 | 10 | [dependencies.postcard-rpc] 11 | version = "0.11" 12 | features = [ 13 | "use-std", 14 | "raw-nusb", 15 | ] 16 | 17 | [dependencies.postcard-schema] 18 | version = "0.2.1" 19 | features = ["derive"] 20 | 21 | [dependencies.tokio] 22 | version = "1.37.0" 23 | features = [ 24 | "rt-multi-thread", 25 | "macros", 26 | "time", 27 | ] 28 | 29 | [patch.crates-io] 30 | postcard-rpc = { path = "../../source/postcard-rpc" } 31 | -------------------------------------------------------------------------------- /example/workbook-host/src/bin/comms-01.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | use tokio::time::interval; 4 | use workbook_host_client::client::WorkbookClient; 5 | 6 | #[tokio::main] 7 | pub async fn main() { 8 | let client = WorkbookClient::new(); 9 | 10 | tokio::select! { 11 | _ = client.wait_closed() => { 12 | println!("Client is closed, exiting..."); 13 | } 14 | _ = run(&client) => { 15 | println!("App is done") 16 | } 17 | } 18 | } 19 | 20 | async fn run(client: &WorkbookClient) { 21 | let mut ticker = interval(Duration::from_millis(250)); 22 | 23 | for i in 0..10 { 24 | ticker.tick().await; 25 | print!("Pinging with {i}... "); 26 | let res = client.ping(i).await.unwrap(); 27 | println!("got {res}!"); 28 | assert_eq!(res, i); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /example/workbook-host/src/bin/comms-02.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | io::{stdout, Write}, 3 | time::{Duration, Instant}, 4 | }; 5 | 6 | use workbook_host_client::{client::WorkbookClient, icd, read_line}; 7 | 8 | #[tokio::main] 9 | async fn main() { 10 | println!("Connecting to USB device..."); 11 | let client = WorkbookClient::new(); 12 | println!("Connected! Pinging 42"); 13 | let ping = client.ping(42).await.unwrap(); 14 | println!("Got: {ping}."); 15 | let uid = client.get_id().await.unwrap(); 16 | println!("ID: {uid:016X}"); 17 | println!(); 18 | 19 | // Begin repl... 20 | loop { 21 | print!("> "); 22 | stdout().flush().unwrap(); 23 | let line = read_line().await; 24 | let parts: Vec<&str> = line.split_whitespace().collect(); 25 | match parts.as_slice() { 26 | ["ping"] => { 27 | let ping = client.ping(42).await.unwrap(); 28 | println!("Got: {ping}."); 29 | } 30 | ["ping", n] => { 31 | let Ok(idx) = n.parse::() else { 32 | println!("Bad u32: '{n}'"); 33 | continue; 34 | }; 35 | let ping = client.ping(idx).await.unwrap(); 36 | println!("Got: {ping}."); 37 | } 38 | ["rgb", pos, r, g, b] => { 39 | let (Ok(pos), Ok(r), Ok(g), Ok(b)) = (pos.parse(), r.parse(), g.parse(), b.parse()) 40 | else { 41 | panic!(); 42 | }; 43 | client.set_rgb_single(pos, r, g, b).await.unwrap(); 44 | } 45 | ["rgball", r, g, b] => { 46 | let (Ok(r), Ok(g), Ok(b)) = (r.parse(), g.parse(), b.parse()) else { 47 | panic!(); 48 | }; 49 | client.set_all_rgb_single(r, g, b).await.unwrap(); 50 | } 51 | ["accel", "listen", ms, range, dur] => { 52 | let Ok(ms) = ms.parse::() else { 53 | println!("Bad ms: {ms}"); 54 | continue; 55 | }; 56 | let Ok(dur) = dur.parse::() else { 57 | println!("Bad dur: {dur}"); 58 | continue; 59 | }; 60 | let range = match *range { 61 | "2" => icd::AccelRange::G2, 62 | "4" => icd::AccelRange::G4, 63 | "8" => icd::AccelRange::G8, 64 | "16" => icd::AccelRange::G16, 65 | _ => { 66 | println!("Bad range: {range}"); 67 | continue; 68 | } 69 | }; 70 | 71 | let mut sub = client.client.subscribe_multi::(8).await.unwrap(); 72 | client.start_accelerometer(ms, range).await.unwrap(); 73 | println!("Started!"); 74 | let dur = Duration::from_millis(dur.into()); 75 | let start = Instant::now(); 76 | while start.elapsed() < dur { 77 | let val = sub.recv().await.unwrap(); 78 | println!("acc: {val:?}"); 79 | } 80 | client.stop_accelerometer().await.unwrap(); 81 | println!("Stopped!"); 82 | } 83 | ["accel", "start", ms, range] => { 84 | let Ok(ms) = ms.parse::() else { 85 | println!("Bad ms: {ms}"); 86 | continue; 87 | }; 88 | let range = match *range { 89 | "2" => icd::AccelRange::G2, 90 | "4" => icd::AccelRange::G4, 91 | "8" => icd::AccelRange::G8, 92 | "16" => icd::AccelRange::G16, 93 | _ => { 94 | println!("Bad range: {range}"); 95 | continue; 96 | } 97 | }; 98 | 99 | client.start_accelerometer(ms, range).await.unwrap(); 100 | println!("Started!"); 101 | } 102 | ["accel", "stop"] => { 103 | let res = client.stop_accelerometer().await.unwrap(); 104 | println!("Stopped: {res}"); 105 | } 106 | ["schema"] => { 107 | let schema = client.client.get_schema_report().await.unwrap(); 108 | 109 | println!(); 110 | println!("# Endpoints"); 111 | println!(); 112 | for ep in &schema.endpoints { 113 | println!("* '{}'", ep.path); 114 | println!(" * Request: {}", ep.req_ty); 115 | println!(" * Response: {}", ep.resp_ty); 116 | } 117 | 118 | println!(); 119 | println!("# Topics Client -> Server"); 120 | println!(); 121 | for tp in &schema.topics_in { 122 | println!("* '{}'", tp.path); 123 | println!(" * Message: {}", tp.ty); 124 | } 125 | 126 | println!(); 127 | println!("# Topics Client <- Server"); 128 | println!(); 129 | for tp in &schema.topics_out { 130 | println!("* '{}'", tp.path); 131 | println!(" * Message: {}", tp.ty); 132 | } 133 | println!(); 134 | } 135 | other => { 136 | println!("Error, didn't understand '{other:?};"); 137 | } 138 | } 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /example/workbook-host/src/bin/logging.rs: -------------------------------------------------------------------------------- 1 | use postcard_rpc::standard_icd::LoggingTopic; 2 | use workbook_host_client::client::WorkbookClient; 3 | 4 | #[tokio::main] 5 | async fn main() { 6 | println!("Connecting to USB device..."); 7 | let client = WorkbookClient::new(); 8 | println!("Connected! Pinging 42"); 9 | let ping = client.ping(42).await.unwrap(); 10 | println!("Got: {ping}."); 11 | println!(); 12 | 13 | let mut logsub = client.client.subscribe_multi::(64).await.unwrap(); 14 | 15 | while let Ok(msg) = logsub.recv().await { 16 | println!("LOG: {msg}"); 17 | } 18 | println!("Device disconnected"); 19 | } 20 | -------------------------------------------------------------------------------- /example/workbook-host/src/client.rs: -------------------------------------------------------------------------------- 1 | use postcard_rpc::{ 2 | header::VarSeqKind, 3 | host_client::{HostClient, HostErr}, 4 | standard_icd::{PingEndpoint, WireError, ERROR_PATH}, 5 | }; 6 | use std::convert::Infallible; 7 | use workbook_icd::{ 8 | AccelRange, BadPositionError, GetUniqueIdEndpoint, Rgb8, SetAllLedEndpoint, 9 | SetSingleLedEndpoint, SingleLed, StartAccel, StartAccelerationEndpoint, 10 | StopAccelerationEndpoint, 11 | }; 12 | 13 | pub struct WorkbookClient { 14 | pub client: HostClient, 15 | } 16 | 17 | #[derive(Debug)] 18 | pub enum WorkbookError { 19 | Comms(HostErr), 20 | Endpoint(E), 21 | } 22 | 23 | impl From> for WorkbookError { 24 | fn from(value: HostErr) -> Self { 25 | Self::Comms(value) 26 | } 27 | } 28 | 29 | trait FlattenErr { 30 | type Good; 31 | type Bad; 32 | fn flatten(self) -> Result>; 33 | } 34 | 35 | impl FlattenErr for Result { 36 | type Good = T; 37 | type Bad = E; 38 | fn flatten(self) -> Result> { 39 | self.map_err(WorkbookError::Endpoint) 40 | } 41 | } 42 | 43 | // --- 44 | 45 | impl WorkbookClient { 46 | pub fn new() -> Self { 47 | let client = HostClient::new_raw_nusb( 48 | |d| d.product_string() == Some("ov-twin"), 49 | ERROR_PATH, 50 | 8, 51 | VarSeqKind::Seq2, 52 | ); 53 | Self { client } 54 | } 55 | 56 | pub async fn wait_closed(&self) { 57 | self.client.wait_closed().await; 58 | } 59 | 60 | pub async fn ping(&self, id: u32) -> Result> { 61 | let val = self.client.send_resp::(&id).await?; 62 | Ok(val) 63 | } 64 | 65 | pub async fn get_id(&self) -> Result> { 66 | let id = self.client.send_resp::(&()).await?; 67 | Ok(id) 68 | } 69 | 70 | pub async fn set_rgb_single( 71 | &self, 72 | position: u32, 73 | r: u8, 74 | g: u8, 75 | b: u8, 76 | ) -> Result<(), WorkbookError> { 77 | self.client 78 | .send_resp::(&SingleLed { 79 | position, 80 | rgb: Rgb8 { r, g, b }, 81 | }) 82 | .await? 83 | .flatten() 84 | } 85 | 86 | pub async fn set_all_rgb_single( 87 | &self, 88 | r: u8, 89 | g: u8, 90 | b: u8, 91 | ) -> Result<(), WorkbookError> { 92 | self.client 93 | .send_resp::(&[Rgb8 { r, g, b }; 24]) 94 | .await?; 95 | Ok(()) 96 | } 97 | 98 | pub async fn start_accelerometer( 99 | &self, 100 | interval_ms: u32, 101 | range: AccelRange, 102 | ) -> Result<(), WorkbookError> { 103 | self.client 104 | .send_resp::(&StartAccel { interval_ms, range }) 105 | .await?; 106 | 107 | Ok(()) 108 | } 109 | 110 | pub async fn stop_accelerometer(&self) -> Result> { 111 | let res = self 112 | .client 113 | .send_resp::(&()) 114 | .await?; 115 | 116 | Ok(res) 117 | } 118 | } 119 | 120 | impl Default for WorkbookClient { 121 | fn default() -> Self { 122 | Self::new() 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /example/workbook-host/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod client; 2 | pub use workbook_icd as icd; 3 | 4 | pub async fn read_line() -> String { 5 | tokio::task::spawn_blocking(|| { 6 | let mut line = String::new(); 7 | std::io::stdin().read_line(&mut line).unwrap(); 8 | line 9 | }) 10 | .await 11 | .unwrap() 12 | } 13 | -------------------------------------------------------------------------------- /example/workbook-icd/Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "atomic-polyfill" 7 | version = "1.0.3" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "8cf2bce30dfe09ef0bfaef228b9d414faaf7e563035494d7fe092dba54b300f4" 10 | dependencies = [ 11 | "critical-section", 12 | ] 13 | 14 | [[package]] 15 | name = "autocfg" 16 | version = "1.2.0" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "f1fdabc7756949593fe60f30ec81974b613357de856987752631dea1e3394c80" 19 | 20 | [[package]] 21 | name = "byteorder" 22 | version = "1.5.0" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" 25 | 26 | [[package]] 27 | name = "cobs" 28 | version = "0.2.3" 29 | source = "registry+https://github.com/rust-lang/crates.io-index" 30 | checksum = "67ba02a97a2bd10f4b59b25c7973101c79642302776489e030cd13cdab09ed15" 31 | 32 | [[package]] 33 | name = "critical-section" 34 | version = "1.1.2" 35 | source = "registry+https://github.com/rust-lang/crates.io-index" 36 | checksum = "7059fff8937831a9ae6f0fe4d658ffabf58f2ca96aa9dec1c889f936f705f216" 37 | 38 | [[package]] 39 | name = "hash32" 40 | version = "0.2.1" 41 | source = "registry+https://github.com/rust-lang/crates.io-index" 42 | checksum = "b0c35f58762feb77d74ebe43bdbc3210f09be9fe6742234d573bacc26ed92b67" 43 | dependencies = [ 44 | "byteorder", 45 | ] 46 | 47 | [[package]] 48 | name = "hash32" 49 | version = "0.3.1" 50 | source = "registry+https://github.com/rust-lang/crates.io-index" 51 | checksum = "47d60b12902ba28e2730cd37e95b8c9223af2808df9e902d4df49588d1470606" 52 | dependencies = [ 53 | "byteorder", 54 | ] 55 | 56 | [[package]] 57 | name = "heapless" 58 | version = "0.7.17" 59 | source = "registry+https://github.com/rust-lang/crates.io-index" 60 | checksum = "cdc6457c0eb62c71aac4bc17216026d8410337c4126773b9c5daba343f17964f" 61 | dependencies = [ 62 | "atomic-polyfill", 63 | "hash32 0.2.1", 64 | "rustc_version", 65 | "serde", 66 | "spin", 67 | "stable_deref_trait", 68 | ] 69 | 70 | [[package]] 71 | name = "heapless" 72 | version = "0.8.0" 73 | source = "registry+https://github.com/rust-lang/crates.io-index" 74 | checksum = "0bfb9eb618601c89945a70e254898da93b13be0388091d42117462b265bb3fad" 75 | dependencies = [ 76 | "hash32 0.3.1", 77 | "stable_deref_trait", 78 | ] 79 | 80 | [[package]] 81 | name = "lock_api" 82 | version = "0.4.11" 83 | source = "registry+https://github.com/rust-lang/crates.io-index" 84 | checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45" 85 | dependencies = [ 86 | "autocfg", 87 | "scopeguard", 88 | ] 89 | 90 | [[package]] 91 | name = "portable-atomic" 92 | version = "1.9.0" 93 | source = "registry+https://github.com/rust-lang/crates.io-index" 94 | checksum = "cc9c68a3f6da06753e9335d63e27f6b9754dd1920d941135b7ea8224f141adb2" 95 | 96 | [[package]] 97 | name = "postcard" 98 | version = "1.0.8" 99 | source = "registry+https://github.com/rust-lang/crates.io-index" 100 | checksum = "a55c51ee6c0db07e68448e336cf8ea4131a620edefebf9893e759b2d793420f8" 101 | dependencies = [ 102 | "cobs", 103 | "heapless 0.7.17", 104 | "serde", 105 | ] 106 | 107 | [[package]] 108 | name = "postcard-derive" 109 | version = "0.2.0" 110 | source = "registry+https://github.com/rust-lang/crates.io-index" 111 | checksum = "f9718c21652accb7372d8187f0df8da07eea83b0fff67276c802b15fb6d44d16" 112 | dependencies = [ 113 | "proc-macro2", 114 | "quote", 115 | "syn 1.0.109", 116 | ] 117 | 118 | [[package]] 119 | name = "postcard-rpc" 120 | version = "0.10.1" 121 | dependencies = [ 122 | "heapless 0.8.0", 123 | "portable-atomic", 124 | "postcard", 125 | "postcard-schema", 126 | "serde", 127 | ] 128 | 129 | [[package]] 130 | name = "postcard-schema" 131 | version = "0.1.0" 132 | source = "registry+https://github.com/rust-lang/crates.io-index" 133 | checksum = "30a3e7acbdb61449280eb8c54126ecfb10571ec06add5c60d014c675f029e7d2" 134 | dependencies = [ 135 | "postcard-derive", 136 | "serde", 137 | ] 138 | 139 | [[package]] 140 | name = "proc-macro2" 141 | version = "1.0.81" 142 | source = "registry+https://github.com/rust-lang/crates.io-index" 143 | checksum = "3d1597b0c024618f09a9c3b8655b7e430397a36d23fdafec26d6965e9eec3eba" 144 | dependencies = [ 145 | "unicode-ident", 146 | ] 147 | 148 | [[package]] 149 | name = "quote" 150 | version = "1.0.36" 151 | source = "registry+https://github.com/rust-lang/crates.io-index" 152 | checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" 153 | dependencies = [ 154 | "proc-macro2", 155 | ] 156 | 157 | [[package]] 158 | name = "rustc_version" 159 | version = "0.4.0" 160 | source = "registry+https://github.com/rust-lang/crates.io-index" 161 | checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" 162 | dependencies = [ 163 | "semver", 164 | ] 165 | 166 | [[package]] 167 | name = "scopeguard" 168 | version = "1.2.0" 169 | source = "registry+https://github.com/rust-lang/crates.io-index" 170 | checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" 171 | 172 | [[package]] 173 | name = "semver" 174 | version = "1.0.22" 175 | source = "registry+https://github.com/rust-lang/crates.io-index" 176 | checksum = "92d43fe69e652f3df9bdc2b85b2854a0825b86e4fb76bc44d945137d053639ca" 177 | 178 | [[package]] 179 | name = "serde" 180 | version = "1.0.198" 181 | source = "registry+https://github.com/rust-lang/crates.io-index" 182 | checksum = "9846a40c979031340571da2545a4e5b7c4163bdae79b301d5f86d03979451fcc" 183 | dependencies = [ 184 | "serde_derive", 185 | ] 186 | 187 | [[package]] 188 | name = "serde_derive" 189 | version = "1.0.198" 190 | source = "registry+https://github.com/rust-lang/crates.io-index" 191 | checksum = "e88edab869b01783ba905e7d0153f9fc1a6505a96e4ad3018011eedb838566d9" 192 | dependencies = [ 193 | "proc-macro2", 194 | "quote", 195 | "syn 2.0.60", 196 | ] 197 | 198 | [[package]] 199 | name = "spin" 200 | version = "0.9.8" 201 | source = "registry+https://github.com/rust-lang/crates.io-index" 202 | checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" 203 | dependencies = [ 204 | "lock_api", 205 | ] 206 | 207 | [[package]] 208 | name = "stable_deref_trait" 209 | version = "1.2.0" 210 | source = "registry+https://github.com/rust-lang/crates.io-index" 211 | checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" 212 | 213 | [[package]] 214 | name = "syn" 215 | version = "1.0.109" 216 | source = "registry+https://github.com/rust-lang/crates.io-index" 217 | checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" 218 | dependencies = [ 219 | "proc-macro2", 220 | "quote", 221 | "unicode-ident", 222 | ] 223 | 224 | [[package]] 225 | name = "syn" 226 | version = "2.0.60" 227 | source = "registry+https://github.com/rust-lang/crates.io-index" 228 | checksum = "909518bc7b1c9b779f1bbf07f2929d35af9f0f37e47c6e9ef7f9dddc1e1821f3" 229 | dependencies = [ 230 | "proc-macro2", 231 | "quote", 232 | "unicode-ident", 233 | ] 234 | 235 | [[package]] 236 | name = "unicode-ident" 237 | version = "1.0.12" 238 | source = "registry+https://github.com/rust-lang/crates.io-index" 239 | checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" 240 | 241 | [[package]] 242 | name = "workbook-icd" 243 | version = "0.1.0" 244 | dependencies = [ 245 | "postcard-rpc", 246 | "postcard-schema", 247 | "serde", 248 | ] 249 | -------------------------------------------------------------------------------- /example/workbook-icd/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "workbook-icd" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies.serde] 7 | version = "1.0" 8 | features = ["derive"] 9 | default-features = false 10 | 11 | [dependencies.postcard-rpc] 12 | version = "0.11" 13 | 14 | [dependencies.postcard-schema] 15 | version = "0.2.1" 16 | features = ["derive"] 17 | 18 | [patch.crates-io] 19 | postcard-rpc = { path = "../../source/postcard-rpc" } 20 | 21 | [features] 22 | use-std = [] 23 | -------------------------------------------------------------------------------- /example/workbook-icd/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![cfg_attr(not(feature = "use-std"), no_std)] 2 | 3 | use postcard_rpc::{endpoints, topics, TopicDirection}; 4 | use postcard_schema::Schema; 5 | use serde::{Deserialize, Serialize}; 6 | 7 | // --- 8 | 9 | pub type SingleLedSetResult = Result<(), BadPositionError>; 10 | pub type AllLedArray = [Rgb8; 24]; 11 | 12 | endpoints! { 13 | list = ENDPOINT_LIST; 14 | omit_std = true; 15 | | EndpointTy | RequestTy | ResponseTy | Path | 16 | | ---------- | --------- | ---------- | ---- | 17 | | PingEndpoint | u32 | u32 | "ping" | 18 | | GetUniqueIdEndpoint | () | u64 | "unique_id/get" | 19 | | SetSingleLedEndpoint | SingleLed | SingleLedSetResult | "led/set_one" | 20 | | SetAllLedEndpoint | AllLedArray | () | "led/set_all" | 21 | | StartAccelerationEndpoint | StartAccel | () | "accel/start" | 22 | | StopAccelerationEndpoint | () | bool | "accel/stop" | 23 | } 24 | 25 | topics! { 26 | list = TOPICS_IN_LIST; 27 | direction = TopicDirection::ToServer; 28 | | TopicTy | MessageTy | Path | 29 | | ------- | --------- | ---- | 30 | } 31 | 32 | topics! { 33 | list = TOPICS_OUT_LIST; 34 | direction = TopicDirection::ToClient; 35 | | TopicTy | MessageTy | Path | Cfg | 36 | | ------- | --------- | ---- | --- | 37 | | AccelTopic | Acceleration | "accel/data" | | 38 | } 39 | 40 | #[derive(Serialize, Deserialize, Schema, Debug, PartialEq)] 41 | pub struct SingleLed { 42 | pub position: u32, 43 | pub rgb: Rgb8, 44 | } 45 | 46 | #[derive(Serialize, Deserialize, Schema, Debug, PartialEq, Copy, Clone)] 47 | pub struct Rgb8 { 48 | pub r: u8, 49 | pub g: u8, 50 | pub b: u8, 51 | } 52 | 53 | #[derive(Serialize, Deserialize, Schema, Debug, PartialEq)] 54 | pub struct BadPositionError; 55 | 56 | #[derive(Serialize, Deserialize, Schema, Debug, PartialEq)] 57 | pub struct Acceleration { 58 | pub x: i16, 59 | pub y: i16, 60 | pub z: i16, 61 | } 62 | 63 | #[derive(Serialize, Deserialize, Schema, Debug, PartialEq)] 64 | pub enum AccelRange { 65 | G2, 66 | G4, 67 | G8, 68 | G16, 69 | } 70 | 71 | #[derive(Serialize, Deserialize, Schema, Debug, PartialEq)] 72 | pub struct StartAccel { 73 | pub interval_ms: u32, 74 | pub range: AccelRange, 75 | } 76 | -------------------------------------------------------------------------------- /source/postcard-rpc-test/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "postcard-rpc-test" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies.postcard] 7 | version = "1.0.10" 8 | features = ["use-std", "experimental-derive"] 9 | 10 | [dependencies.serde] 11 | version = "1.0.192" 12 | features = ["derive"] 13 | 14 | [dependencies.postcard-rpc] 15 | path = "../postcard-rpc" 16 | features = ["use-std", "test-utils"] 17 | 18 | [dependencies.postcard-schema] 19 | version = "0.2.1" 20 | features = ["derive"] 21 | 22 | [dependencies.tokio] 23 | version = "1.34.0" 24 | features = ["rt", "macros", "sync", "time"] 25 | 26 | [features] 27 | default = ["alpha"] 28 | alpha = [] 29 | -------------------------------------------------------------------------------- /source/postcard-rpc-test/src/lib.rs: -------------------------------------------------------------------------------- 1 | // I'm just here so we can write integration tests 2 | -------------------------------------------------------------------------------- /source/postcard-rpc/.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [target.wasm32-unknown-unknown] 2 | rustflags = "--cfg=web_sys_unstable_apis" 3 | -------------------------------------------------------------------------------- /source/postcard-rpc/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /Cargo.lock 3 | -------------------------------------------------------------------------------- /source/postcard-rpc/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "postcard-rpc" 3 | version = "0.11.9" 4 | authors = ["James Munns "] 5 | edition = "2021" 6 | repository = "https://github.com/jamesmunns/postcard-rpc" 7 | description = "A no_std + serde compatible RPC library for Rust" 8 | license = "MIT OR Apache-2.0" 9 | categories = ["embedded", "no-std"] 10 | keywords = ["serde", "cobs", "framing"] 11 | documentation = "https://docs.rs/postcard-rpc/" 12 | readme = "../../README.md" 13 | 14 | [package.metadata.docs.rs] 15 | rustdoc-args = ["--cfg", "doc_cfg"] 16 | features = [ 17 | "test-utils", 18 | "use-std", 19 | "cobs-serial", 20 | "raw-nusb", 21 | "embassy-usb-0_3-server", 22 | "embassy-usb-0_4-server", 23 | "_docs-fix", 24 | # TODO: What to do about the webusb feature? Can we do separate target builds? 25 | ] 26 | 27 | [dependencies] 28 | cobs = { version = "0.2.3", optional = true, default-features = false } 29 | defmt = { version = "0.3.5", optional = true } 30 | heapless = "0.8.0" 31 | postcard = { version = "1.0.10" } 32 | serde = { version = "1.0.192", default-features = false, features = ["derive"] } 33 | postcard-schema = { version = "0.2.1", features = ["derive"] } 34 | 35 | # 36 | # std-only features 37 | # 38 | 39 | [dependencies.nusb] 40 | version = "0.1.9" 41 | optional = true 42 | 43 | [dependencies.tokio-serial] 44 | version = "5.4.4" 45 | optional = true 46 | 47 | [dependencies.maitake-sync] 48 | version = "0.1.2" 49 | optional = true 50 | 51 | [dependencies.tokio] 52 | version = "1.33.0" 53 | features = ["sync", "rt", "macros", "io-util", "time"] 54 | optional = true 55 | 56 | [dependencies.tracing] 57 | version = "0.1" 58 | optional = true 59 | 60 | [dependencies.js-sys] 61 | version = "0.3.69" 62 | optional = true 63 | 64 | [dependencies.thiserror] 65 | version = "1.0" 66 | optional = true 67 | 68 | [dependencies.web-sys] 69 | 70 | version = "0.3.69" 71 | optional = true 72 | features = [ 73 | "Element", 74 | "Navigator", 75 | 'Usb', 76 | 'UsbAlternateInterface', 77 | 'UsbConfiguration', 78 | 'UsbDeviceRequestOptions', 79 | 'UsbDevice', 80 | 'UsbDirection', 81 | 'UsbEndpoint', 82 | 'UsbInTransferResult', 83 | 'UsbControlTransferParameters', 84 | 'UsbOutTransferResult', 85 | 'UsbInterface', 86 | 'UsbRecipient', 87 | 'UsbRequestType', 88 | "UsbTransferStatus", 89 | ] 90 | 91 | [dependencies.gloo] 92 | version = "0.11.0" 93 | optional = true 94 | 95 | [dependencies.serde_json] 96 | version = "1.0" 97 | optional = true 98 | 99 | [dependencies.wasm-bindgen] 100 | version = "0.2.92" 101 | optional = true 102 | 103 | [dependencies.wasm-bindgen-futures] 104 | version = "0.4.42" 105 | optional = true 106 | 107 | [dependencies.trait-variant] 108 | version = "0.1.2" 109 | optional = true 110 | 111 | 112 | # 113 | # no_std-only features 114 | # 115 | 116 | [dependencies.embassy-usb-0_3] 117 | package = "embassy-usb" 118 | version = "0.3" 119 | optional = true 120 | 121 | [dependencies.embassy-usb-0_4] 122 | package = "embassy-usb" 123 | version = "0.4" 124 | optional = true 125 | 126 | [dependencies.embassy-usb-driver] 127 | version = "0.1" 128 | optional = true 129 | 130 | [dependencies.embassy-sync] 131 | version = "0.6" 132 | optional = true 133 | 134 | [dependencies.static_cell] 135 | version = "2.1" 136 | optional = true 137 | 138 | [dependencies.embassy-executor] 139 | version = "0.7" 140 | optional = true 141 | 142 | [dependencies.embassy-futures] 143 | version = "0.1" 144 | optional = true 145 | 146 | [dependencies.embassy-time] 147 | version = "0.4" 148 | optional = true 149 | 150 | [dependencies.portable-atomic] 151 | version = "1.0" 152 | default-features = false 153 | 154 | [dev-dependencies] 155 | postcard-rpc = { path = "../postcard-rpc", features = ["test-utils"] } 156 | 157 | # 158 | # Hack features (see below) 159 | # 160 | [dependencies.ssmarshal] 161 | version = "1.0" 162 | optional = true 163 | features = ["std"] 164 | 165 | 166 | [features] 167 | default = [] 168 | test-utils = ["use-std", "postcard-schema/use-std"] 169 | use-std = [ 170 | "dep:maitake-sync", 171 | "dep:tokio", 172 | "postcard/use-std", 173 | "postcard-schema/use-std", 174 | "dep:thiserror", 175 | "dep:tracing", 176 | "dep:trait-variant", 177 | "dep:ssmarshal", 178 | ] 179 | 180 | # Cobs Serial support. 181 | # 182 | # Works on: Win, Mac, Linux 183 | # Does NOT work on: WASM 184 | cobs-serial = ["cobs/use_std", "dep:tokio-serial"] 185 | 186 | # Raw (bulk) USB support 187 | # 188 | # Works on: Win, Mac, Linux 189 | # Does NOT work on: WASM 190 | raw-nusb = ["dep:nusb", "use-std"] 191 | 192 | # WebUSB support 193 | # 194 | # Works on: WASM 195 | # Does NOT work on: Win, Mac, Linux 196 | # 197 | # NOTE: Requires the following in your `.cargo/config.toml`, or otherwise 198 | # activated via RUSTFLAGS: 199 | # 200 | # ```toml 201 | # [target.wasm32-unknown-unknown] 202 | # rustflags = "--cfg=web_sys_unstable_apis" 203 | # ``` 204 | webusb = [ 205 | "dep:gloo", 206 | "dep:web-sys", 207 | "dep:serde_json", 208 | "dep:wasm-bindgen", 209 | "dep:wasm-bindgen-futures", 210 | "dep:js-sys", 211 | "use-std", 212 | ] 213 | embassy-usb-0_3-server = [ 214 | "dep:embassy-usb-0_3", 215 | "dep:embassy-sync", 216 | "dep:static_cell", 217 | "dep:embassy-usb-driver", 218 | "dep:embassy-executor", 219 | "dep:embassy-time", 220 | "dep:embassy-futures", 221 | ] 222 | 223 | embassy-usb-0_4-server = [ 224 | "dep:embassy-usb-0_4", 225 | "dep:embassy-sync", 226 | "dep:static_cell", 227 | "dep:embassy-usb-driver", 228 | "dep:embassy-executor", 229 | "dep:embassy-time", 230 | "dep:embassy-futures", 231 | ] 232 | 233 | # NOTE: This exists because `embassy-usb` indirectly relies on ssmarshal 234 | # which doesn't work on `std` builds without the `std` feature. This causes 235 | # `cargo doc --all-features` (and docs.rs builds) to fail. Sneakily re-activate 236 | # that feature when `--all-features` is set. This feature is considered unstable 237 | # and should not be relied upon. 238 | _docs-fix = ["dep:ssmarshal"] 239 | -------------------------------------------------------------------------------- /source/postcard-rpc/src/accumulator.rs: -------------------------------------------------------------------------------- 1 | //! Accumulator tools 2 | //! 3 | //! These tools are useful for accumulating and decoding COBS encoded messages. 4 | //! 5 | //! Unlike the `CobsAccumulator` from `postcard`, these versions do not deserialize 6 | //! directly. 7 | 8 | /// Decode-only accumulator 9 | pub mod raw { 10 | use cobs::decode_in_place; 11 | 12 | /// A header-aware COBS accumulator 13 | pub struct CobsAccumulator { 14 | buf: [u8; N], 15 | idx: usize, 16 | } 17 | 18 | /// The result of feeding the accumulator. 19 | pub enum FeedResult<'a, 'b> { 20 | /// Consumed all data, still pending. 21 | Consumed, 22 | 23 | /// Buffer was filled. Contains remaining section of input, if any. 24 | OverFull(&'a [u8]), 25 | 26 | /// Reached end of chunk, but deserialization failed. Contains remaining section of input, if. 27 | /// any 28 | DeserError(&'a [u8]), 29 | 30 | /// Deserialization complete. Contains deserialized data and remaining section of input, if any. 31 | Success { 32 | /// Deserialize data. 33 | data: &'b [u8], 34 | 35 | /// Remaining data left in the buffer after deserializing. 36 | remaining: &'a [u8], 37 | }, 38 | } 39 | 40 | impl CobsAccumulator { 41 | /// Create a new accumulator. 42 | pub const fn new() -> Self { 43 | CobsAccumulator { 44 | buf: [0; N], 45 | idx: 0, 46 | } 47 | } 48 | 49 | /// Appends data to the internal buffer and attempts to deserialize the accumulated data into 50 | /// `T`. 51 | #[inline] 52 | pub fn feed<'a, 'b>(&'b mut self, input: &'a [u8]) -> FeedResult<'a, 'b> { 53 | self.feed_ref(input) 54 | } 55 | 56 | /// Appends data to the internal buffer and attempts to deserialize the accumulated data into 57 | /// `T`. 58 | /// 59 | /// This differs from feed, as it allows the `T` to reference data within the internal buffer, but 60 | /// mutably borrows the accumulator for the lifetime of the deserialization. 61 | /// If `T` does not require the reference, the borrow of `self` ends at the end of the function. 62 | pub fn feed_ref<'a, 'b>(&'b mut self, input: &'a [u8]) -> FeedResult<'a, 'b> { 63 | if input.is_empty() { 64 | return FeedResult::Consumed; 65 | } 66 | 67 | let zero_pos = input.iter().position(|&i| i == 0); 68 | 69 | if let Some(n) = zero_pos { 70 | // Yes! We have an end of message here. 71 | // Add one to include the zero in the "take" portion 72 | // of the buffer, rather than in "release". 73 | let (take, release) = input.split_at(n + 1); 74 | 75 | // Does it fit? 76 | if (self.idx + take.len()) <= N { 77 | // Aw yiss - add to array 78 | self.extend_unchecked(take); 79 | 80 | let retval = match decode_in_place(&mut self.buf[..self.idx]) { 81 | Ok(used) => FeedResult::Success { 82 | data: &self.buf[..used], 83 | remaining: release, 84 | }, 85 | Err(_) => FeedResult::DeserError(release), 86 | }; 87 | self.idx = 0; 88 | retval 89 | } else { 90 | self.idx = 0; 91 | FeedResult::OverFull(release) 92 | } 93 | } else { 94 | // Does it fit? 95 | if (self.idx + input.len()) > N { 96 | // nope 97 | let new_start = N - self.idx; 98 | self.idx = 0; 99 | FeedResult::OverFull(&input[new_start..]) 100 | } else { 101 | // yup! 102 | self.extend_unchecked(input); 103 | FeedResult::Consumed 104 | } 105 | } 106 | } 107 | 108 | /// Extend the internal buffer with the given input. 109 | /// 110 | /// # Panics 111 | /// 112 | /// Will panic if the input does not fit in the internal buffer. 113 | fn extend_unchecked(&mut self, input: &[u8]) { 114 | let new_end = self.idx + input.len(); 115 | self.buf[self.idx..new_end].copy_from_slice(input); 116 | self.idx = new_end; 117 | } 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /source/postcard-rpc/src/host_client/serial.rs: -------------------------------------------------------------------------------- 1 | use std::{collections::VecDeque, future::Future}; 2 | 3 | use cobs::encode_vec; 4 | use postcard_schema::Schema; 5 | use serde::de::DeserializeOwned; 6 | use tokio::io::{AsyncReadExt, AsyncWriteExt, ReadHalf, WriteHalf}; 7 | use tokio_serial::{SerialPortBuilderExt, SerialStream}; 8 | 9 | use crate::{ 10 | accumulator::raw::{CobsAccumulator, FeedResult}, 11 | header::VarSeqKind, 12 | host_client::{HostClient, WireRx, WireSpawn, WireTx}, 13 | }; 14 | 15 | /// # Serial Constructor Methods 16 | /// 17 | /// These methods are used to create a new [HostClient] instance for use with tokio serial and cobs encoding. 18 | /// 19 | /// **Requires feature**: `cobs-serial` 20 | impl HostClient 21 | where 22 | WireErr: DeserializeOwned + Schema, 23 | { 24 | /// Create a new [HostClient] 25 | /// 26 | /// `serial_path` is the path to the serial port used. `err_uri_path` is 27 | /// the path associated with the `WireErr` message type. 28 | /// 29 | /// This constructor is available when the `cobs-serial` feature is enabled. 30 | /// 31 | /// ## Example 32 | /// 33 | /// ```rust,no_run 34 | /// use postcard_rpc::host_client::HostClient; 35 | /// use postcard_rpc::header::VarSeqKind; 36 | /// use serde::{Serialize, Deserialize}; 37 | /// use postcard_schema::Schema; 38 | /// 39 | /// /// A "wire error" type your server can use to respond to any 40 | /// /// kind of request, for example if deserializing a request fails 41 | /// #[derive(Debug, PartialEq, Schema, Serialize, Deserialize)] 42 | /// pub enum Error { 43 | /// SomethingBad 44 | /// } 45 | /// 46 | /// let client = HostClient::::new_serial_cobs( 47 | /// // the serial port path 48 | /// "/dev/ttyACM0", 49 | /// // the URI/path for `Error` messages 50 | /// "error", 51 | /// // Outgoing queue depth in messages 52 | /// 8, 53 | /// // Baud rate of serial (does not generally matter for 54 | /// // USB UART/CDC-ACM serial connections) 55 | /// 115_200, 56 | /// // Use one-byte sequence numbers 57 | /// VarSeqKind::Seq1, 58 | /// ); 59 | /// ``` 60 | pub fn try_new_serial_cobs( 61 | serial_path: &str, 62 | err_uri_path: &str, 63 | outgoing_depth: usize, 64 | baud: u32, 65 | seq_no_kind: VarSeqKind, 66 | ) -> Result { 67 | let port = tokio_serial::new(serial_path, baud) 68 | .open_native_async() 69 | .map_err(|e| format!("Open Error: {e:?}"))?; 70 | 71 | let (rx, tx) = tokio::io::split(port); 72 | 73 | Ok(HostClient::new_with_wire( 74 | SerialWireTx { tx }, 75 | SerialWireRx { 76 | rx, 77 | buf: Box::new([0u8; 1024]), 78 | acc: Box::new(CobsAccumulator::new()), 79 | pending: VecDeque::new(), 80 | }, 81 | SerialSpawn, 82 | seq_no_kind, 83 | err_uri_path, 84 | outgoing_depth, 85 | )) 86 | } 87 | 88 | /// Create a new [HostClient] 89 | /// 90 | /// Panics if we couldn't open the serial port. 91 | /// 92 | /// See [`HostClient::try_new_serial_cobs`] for more details 93 | pub fn new_serial_cobs( 94 | serial_path: &str, 95 | err_uri_path: &str, 96 | outgoing_depth: usize, 97 | baud: u32, 98 | seq_no_kind: VarSeqKind, 99 | ) -> Self { 100 | Self::try_new_serial_cobs(serial_path, err_uri_path, outgoing_depth, baud, seq_no_kind) 101 | .unwrap() 102 | } 103 | } 104 | 105 | ////////////////////////////////////////////////////////////////////////////// 106 | // Wire Interface Implementation 107 | ////////////////////////////////////////////////////////////////////////////// 108 | 109 | /// Tokio Serial Wire Interface Implementor 110 | /// 111 | /// Uses Tokio for spawning tasks 112 | struct SerialSpawn; 113 | 114 | impl WireSpawn for SerialSpawn { 115 | fn spawn(&mut self, fut: impl Future + Send + 'static) { 116 | // Explicitly drop the joinhandle as it impls Future and this makes 117 | // clippy mad if you just let it drop implicitly 118 | core::mem::drop(tokio::task::spawn(fut)); 119 | } 120 | } 121 | 122 | /// Tokio Serial Wire Transmit Interface Implementor 123 | struct SerialWireTx { 124 | // boq: Queue>, 125 | tx: WriteHalf, 126 | } 127 | 128 | #[derive(thiserror::Error, Debug)] 129 | enum SerialWireTxError { 130 | #[error("Transfer Error on Send")] 131 | Transfer(#[from] std::io::Error), 132 | } 133 | 134 | impl WireTx for SerialWireTx { 135 | type Error = SerialWireTxError; 136 | 137 | #[inline] 138 | fn send(&mut self, data: Vec) -> impl Future> + Send { 139 | self.send_inner(data) 140 | } 141 | } 142 | 143 | impl SerialWireTx { 144 | async fn send_inner(&mut self, data: Vec) -> Result<(), SerialWireTxError> { 145 | // Turn the serialized message into a COBS encoded message 146 | // 147 | // TODO: this is a little wasteful, data is already a vec, 148 | // then we encode that to a second cobs-encoded vec. Oh well. 149 | let mut msg = encode_vec(&data); 150 | msg.push(0); 151 | 152 | // And send it! 153 | self.tx.write_all(&msg).await?; 154 | Ok(()) 155 | } 156 | } 157 | 158 | /// NUSB Wire Receive Interface Implementor 159 | struct SerialWireRx { 160 | rx: ReadHalf, 161 | buf: Box<[u8; 1024]>, 162 | acc: Box>, 163 | pending: VecDeque>, 164 | } 165 | 166 | #[derive(thiserror::Error, Debug)] 167 | enum SerialWireRxError { 168 | #[error("Transfer Error on Recv")] 169 | Transfer(#[from] std::io::Error), 170 | } 171 | 172 | impl WireRx for SerialWireRx { 173 | type Error = SerialWireRxError; 174 | 175 | #[inline] 176 | fn receive(&mut self) -> impl Future, Self::Error>> + Send { 177 | self.recv_inner() 178 | } 179 | } 180 | 181 | impl SerialWireRx { 182 | async fn recv_inner(&mut self) -> Result, SerialWireRxError> { 183 | // Receive until we've gotten AT LEAST one message, though we will continue 184 | // consuming and buffering any read (partial) messages, to ensure they are not lost. 185 | loop { 186 | // Do we have any messages already prepared? 187 | if let Some(p) = self.pending.pop_front() { 188 | return Ok(p); 189 | } 190 | 191 | // Nothing in the pending queue, do a read to see if we can pull more 192 | // data from the serial port 193 | let used = self.rx.read(self.buf.as_mut_slice()).await?; 194 | 195 | let mut window = &self.buf[..used]; 196 | 197 | // This buffering loop is necessary as a single `read()` might include 198 | // more than one message 199 | 'cobs: while !window.is_empty() { 200 | window = match self.acc.feed(window) { 201 | // Consumed the whole USB frame 202 | FeedResult::Consumed => break 'cobs, 203 | // Ignore line errors 204 | FeedResult::OverFull(new_wind) => { 205 | tracing::warn!("Overflowed COBS accumulator"); 206 | new_wind 207 | } 208 | FeedResult::DeserError(new_wind) => { 209 | tracing::warn!("COBS formatting error"); 210 | new_wind 211 | } 212 | // We got a message! Attempt to dispatch it 213 | FeedResult::Success { data, remaining } => { 214 | // TODO hacky check: the minimum size of a message is 9 bytes, 215 | // 8 for the header and one for the seq_no. Discard any "obviously" 216 | // malformed messages. 217 | if data.len() >= 9 { 218 | self.pending.push_back(data.to_vec()); 219 | } else { 220 | tracing::warn!("Ignoring too-short message: {} bytes", data.len()); 221 | } 222 | remaining 223 | } 224 | }; 225 | } 226 | } 227 | } 228 | } 229 | -------------------------------------------------------------------------------- /source/postcard-rpc/src/host_client/test_channels.rs: -------------------------------------------------------------------------------- 1 | //! A Client implementation using channels for testing 2 | 3 | use crate::{ 4 | header::VarSeqKind, 5 | host_client::{HostClient, WireRx, WireSpawn, WireTx}, 6 | standard_icd::WireError, 7 | }; 8 | use core::fmt::Display; 9 | use tokio::sync::mpsc; 10 | 11 | /// Create a new HostClient from the given server channels 12 | pub fn new_from_channels( 13 | tx: mpsc::Sender>, 14 | rx: mpsc::Receiver>, 15 | seq_kind: VarSeqKind, 16 | ) -> HostClient { 17 | HostClient::new_with_wire( 18 | ChannelTx { tx }, 19 | ChannelRx { rx }, 20 | TokSpawn, 21 | seq_kind, 22 | crate::standard_icd::ERROR_PATH, 23 | 64, 24 | ) 25 | } 26 | 27 | /// Server error kinds 28 | #[derive(Debug)] 29 | pub enum ChannelError { 30 | /// Rx was closed 31 | RxClosed, 32 | /// Tx was closed 33 | TxClosed, 34 | } 35 | 36 | impl Display for ChannelError { 37 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 38 | ::fmt(self, f) 39 | } 40 | } 41 | 42 | impl std::error::Error for ChannelError {} 43 | 44 | /// Trait impl for channels 45 | pub struct ChannelRx { 46 | rx: mpsc::Receiver>, 47 | } 48 | /// Trait impl for channels 49 | pub struct ChannelTx { 50 | tx: mpsc::Sender>, 51 | } 52 | /// Trait impl for channels 53 | pub struct ChannelSpawn; 54 | 55 | impl WireRx for ChannelRx { 56 | type Error = ChannelError; 57 | 58 | async fn receive(&mut self) -> Result, Self::Error> { 59 | match self.rx.recv().await { 60 | Some(v) => { 61 | #[cfg(test)] 62 | println!("c<-s: {v:?}"); 63 | Ok(v) 64 | } 65 | None => Err(ChannelError::RxClosed), 66 | } 67 | } 68 | } 69 | 70 | impl WireTx for ChannelTx { 71 | type Error = ChannelError; 72 | 73 | async fn send(&mut self, data: Vec) -> Result<(), Self::Error> { 74 | #[cfg(test)] 75 | println!("c->s: {data:?}"); 76 | self.tx.send(data).await.map_err(|_| ChannelError::TxClosed) 77 | } 78 | } 79 | 80 | struct TokSpawn; 81 | impl WireSpawn for TokSpawn { 82 | fn spawn(&mut self, fut: impl std::future::Future + Send + 'static) { 83 | _ = tokio::task::spawn(fut); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /source/postcard-rpc/src/server/impls/mod.rs: -------------------------------------------------------------------------------- 1 | //! Implementations of various Server traits 2 | //! 3 | //! The implementations in this module typically require feature flags to be set. 4 | 5 | #[cfg(feature = "embassy-usb-0_3-server")] 6 | pub mod embassy_usb_v0_3; 7 | 8 | #[cfg(feature = "embassy-usb-0_4-server")] 9 | pub mod embassy_usb_v0_4; 10 | 11 | #[cfg(feature = "test-utils")] 12 | pub mod test_channels; 13 | -------------------------------------------------------------------------------- /source/postcard-rpc/src/standard_icd.rs: -------------------------------------------------------------------------------- 1 | //! These are items you can use for your error path and error key. 2 | //! 3 | //! This is used by [`define_dispatch!()`][crate::define_dispatch] as well. 4 | 5 | use crate::{endpoints, topics, Key, TopicDirection}; 6 | use postcard_schema::Schema; 7 | use serde::{Deserialize, Serialize}; 8 | 9 | #[cfg(not(feature = "use-std"))] 10 | use postcard_schema::schema::NamedType; 11 | 12 | #[cfg(feature = "use-std")] 13 | use postcard_schema::schema::owned::OwnedNamedType; 14 | 15 | /// The calculated Key for the type [`WireError`] and the path [`ERROR_PATH`] 16 | pub const ERROR_KEY: Key = Key::for_path::(ERROR_PATH); 17 | 18 | /// The path string used for the error type 19 | pub const ERROR_PATH: &str = "error"; 20 | 21 | /// The given frame was too long 22 | #[derive(Serialize, Deserialize, Schema, Debug, PartialEq)] 23 | pub struct FrameTooLong { 24 | /// The length of the too-long frame 25 | pub len: u32, 26 | /// The maximum frame length supported 27 | pub max: u32, 28 | } 29 | 30 | /// The given frame was too short 31 | #[derive(Serialize, Deserialize, Schema, Debug, PartialEq)] 32 | pub struct FrameTooShort { 33 | /// The length of the too-short frame 34 | pub len: u32, 35 | } 36 | 37 | /// A protocol error that is handled outside of the normal request type, usually 38 | /// indicating a protocol-level error 39 | #[derive(Serialize, Deserialize, Schema, Debug, PartialEq)] 40 | pub enum WireError { 41 | /// The frame exceeded the buffering capabilities of the server 42 | FrameTooLong(FrameTooLong), 43 | /// The frame was shorter than the minimum frame size and was rejected 44 | FrameTooShort(FrameTooShort), 45 | /// Deserialization of a message failed 46 | DeserFailed, 47 | /// Serialization of a message failed, usually due to a lack of space to 48 | /// buffer the serialized form 49 | SerFailed, 50 | /// The key associated with this request was unknown 51 | UnknownKey, 52 | /// The server was unable to spawn the associated handler, typically due 53 | /// to an exhaustion of resources 54 | FailedToSpawn, 55 | /// The provided key is below the minimum key size calculated to avoid hash 56 | /// collisions, and was rejected to avoid potential misunderstanding 57 | KeyTooSmall, 58 | } 59 | 60 | /// A single element of schema information 61 | #[cfg(not(feature = "use-std"))] 62 | #[derive(Serialize, Schema, Debug, PartialEq, Copy, Clone)] 63 | pub enum SchemaData<'a> { 64 | /// A single Type 65 | Type(&'a NamedType), 66 | /// A single Endpoint 67 | Endpoint { 68 | /// The path of the endpoint 69 | path: &'a str, 70 | /// The key of the Request type + path 71 | request_key: Key, 72 | /// The key of the Response type + path 73 | response_key: Key, 74 | }, 75 | /// A single Topic 76 | Topic { 77 | /// The path of the topic 78 | path: &'a str, 79 | /// The key of the Message type + path 80 | key: Key, 81 | /// The direction of the Topic 82 | direction: TopicDirection, 83 | }, 84 | } 85 | 86 | /// A single element of schema information 87 | #[cfg(feature = "use-std")] 88 | #[derive(Serialize, Deserialize, Schema, Debug, PartialEq, Clone)] 89 | pub enum OwnedSchemaData { 90 | /// A single Type 91 | Type(OwnedNamedType), 92 | /// A single Endpoint 93 | Endpoint { 94 | /// The path of the endpoint 95 | path: String, 96 | /// The key of the Request type + path 97 | request_key: Key, 98 | /// The key of the Response type + path 99 | response_key: Key, 100 | }, 101 | /// A single Topic 102 | Topic { 103 | /// The path of the topic 104 | path: String, 105 | /// The key of the Message type + path 106 | key: Key, 107 | /// The direction of the Topic 108 | direction: TopicDirection, 109 | }, 110 | } 111 | 112 | /// A summary of all messages sent when streaming schema data 113 | #[derive(Serialize, Deserialize, Schema, Debug, PartialEq, Copy, Clone)] 114 | pub struct SchemaTotals { 115 | /// A count of the number of (Owned)SchemaData::Type messages sent 116 | pub types_sent: u32, 117 | /// A count of the number of (Owned)SchemaData::Endpoint messages sent 118 | pub endpoints_sent: u32, 119 | /// A count of the number of (Owned)SchemaData::Topic messages sent 120 | pub topics_in_sent: u32, 121 | /// A count of the number of (Owned)SchemaData::Topic messages sent 122 | pub topics_out_sent: u32, 123 | /// A count of the number of messages (any of the above) that failed to send 124 | pub errors: u32, 125 | } 126 | 127 | endpoints! { 128 | list = STANDARD_ICD_ENDPOINTS; 129 | omit_std = true; 130 | | EndpointTy | RequestTy | ResponseTy | Path | 131 | | ---------- | --------- | ---------- | ---- | 132 | | PingEndpoint | u32 | u32 | "postcard-rpc/ping" | 133 | | GetAllSchemasEndpoint | () | SchemaTotals | "postcard-rpc/schemas/get" | 134 | } 135 | 136 | topics! { 137 | list = STANDARD_ICD_TOPICS_OUT; 138 | direction = crate::TopicDirection::ToClient; 139 | omit_std = true; 140 | | TopicTy | MessageTy | Path | Cfg | 141 | | ------- | --------- | ---- | --- | 142 | | GetAllSchemaDataTopic | SchemaData<'a> | "postcard-rpc/schema/data" | cfg(not(feature = "use-std")) | 143 | | GetAllSchemaDataTopic | OwnedSchemaData | "postcard-rpc/schema/data" | cfg(feature = "use-std") | 144 | | LoggingTopic | str | "postcard-rpc/logging" | cfg(not(feature = "use-std")) | 145 | | LoggingTopic | String | "postcard-rpc/logging" | cfg(feature = "use-std") | 146 | } 147 | 148 | topics! { 149 | list = STANDARD_ICD_TOPICS_IN; 150 | direction = crate::TopicDirection::ToServer; 151 | omit_std = true; 152 | | TopicTy | MessageTy | Path | Cfg | 153 | | ------- | --------- | ---- | --- | 154 | } 155 | -------------------------------------------------------------------------------- /source/postcard-rpc/src/test_utils.rs: -------------------------------------------------------------------------------- 1 | //! Test utilities for doctests and integration tests 2 | 3 | use core::{fmt::Display, future::Future}; 4 | 5 | use crate::header::{VarHeader, VarKey, VarSeq, VarSeqKind}; 6 | use crate::host_client::util::Stopper; 7 | use crate::{ 8 | host_client::{HostClient, RpcFrame, WireRx, WireSpawn, WireTx}, 9 | Endpoint, Topic, 10 | }; 11 | use postcard_schema::Schema; 12 | use serde::{de::DeserializeOwned, Serialize}; 13 | use tokio::{ 14 | select, 15 | sync::mpsc::{channel, Receiver, Sender}, 16 | }; 17 | 18 | /// Rx Helper type 19 | pub struct LocalRx { 20 | fake_error: Stopper, 21 | from_server: Receiver>, 22 | } 23 | /// Tx Helper type 24 | pub struct LocalTx { 25 | fake_error: Stopper, 26 | to_server: Sender>, 27 | } 28 | /// Spawn helper type 29 | pub struct LocalSpawn; 30 | /// Server type 31 | pub struct LocalFakeServer { 32 | fake_error: Stopper, 33 | /// from client to server 34 | pub from_client: Receiver>, 35 | /// from server to client 36 | pub to_client: Sender>, 37 | } 38 | 39 | impl LocalFakeServer { 40 | /// receive a frame 41 | pub async fn recv_from_client(&mut self) -> Result { 42 | let msg = self.from_client.recv().await.ok_or(LocalError::TxClosed)?; 43 | let Some((hdr, body)) = VarHeader::take_from_slice(&msg) else { 44 | return Err(LocalError::BadFrame); 45 | }; 46 | Ok(RpcFrame { 47 | header: hdr, 48 | body: body.to_vec(), 49 | }) 50 | } 51 | 52 | /// Reply 53 | pub async fn reply( 54 | &mut self, 55 | seq_no: u32, 56 | data: &E::Response, 57 | ) -> Result<(), LocalError> 58 | where 59 | E::Response: Serialize, 60 | { 61 | let frame = RpcFrame { 62 | header: VarHeader { 63 | key: VarKey::Key8(E::RESP_KEY), 64 | seq_no: VarSeq::Seq4(seq_no), 65 | }, 66 | body: postcard::to_stdvec(data).unwrap(), 67 | }; 68 | self.to_client 69 | .send(frame.to_bytes()) 70 | .await 71 | .map_err(|_| LocalError::RxClosed) 72 | } 73 | 74 | /// Publish 75 | pub async fn publish( 76 | &mut self, 77 | seq_no: u32, 78 | data: &T::Message, 79 | ) -> Result<(), LocalError> 80 | where 81 | T::Message: Serialize, 82 | { 83 | let frame = RpcFrame { 84 | header: VarHeader { 85 | key: VarKey::Key8(T::TOPIC_KEY), 86 | seq_no: VarSeq::Seq4(seq_no), 87 | }, 88 | body: postcard::to_stdvec(data).unwrap(), 89 | }; 90 | self.to_client 91 | .send(frame.to_bytes()) 92 | .await 93 | .map_err(|_| LocalError::RxClosed) 94 | } 95 | 96 | /// oops 97 | pub fn cause_fatal_error(&self) { 98 | self.fake_error.stop(); 99 | } 100 | } 101 | 102 | /// Local error type 103 | #[derive(Debug, PartialEq)] 104 | pub enum LocalError { 105 | /// RxClosed 106 | RxClosed, 107 | /// TxClosed 108 | TxClosed, 109 | /// BadFrame 110 | BadFrame, 111 | /// FatalError 112 | FatalError, 113 | } 114 | 115 | impl Display for LocalError { 116 | fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { 117 | ::fmt(self, f) 118 | } 119 | } 120 | 121 | impl std::error::Error for LocalError {} 122 | 123 | impl WireRx for LocalRx { 124 | type Error = LocalError; 125 | 126 | #[allow(clippy::manual_async_fn)] 127 | fn receive(&mut self) -> impl Future, Self::Error>> + Send { 128 | async { 129 | // This is not usually necessary - HostClient machinery takes care of listening 130 | // to the stopper, but we have an EXTRA one to simulate I/O failure 131 | let recv_fut = self.from_server.recv(); 132 | let error_fut = self.fake_error.wait_stopped(); 133 | 134 | // Before we await, do a quick check to see if an error occured, this way 135 | // recv can't accidentally win the select 136 | if self.fake_error.is_stopped() { 137 | return Err(LocalError::FatalError); 138 | } 139 | 140 | select! { 141 | recv = recv_fut => recv.ok_or(LocalError::RxClosed), 142 | _err = error_fut => Err(LocalError::FatalError), 143 | } 144 | } 145 | } 146 | } 147 | 148 | impl WireTx for LocalTx { 149 | type Error = LocalError; 150 | 151 | #[allow(clippy::manual_async_fn)] 152 | fn send(&mut self, data: Vec) -> impl Future> + Send { 153 | async { 154 | // This is not usually necessary - HostClient machinery takes care of listening 155 | // to the stopper, but we have an EXTRA one to simulate I/O failure 156 | let send_fut = self.to_server.send(data); 157 | let error_fut = self.fake_error.wait_stopped(); 158 | 159 | // Before we await, do a quick check to see if an error occured, this way 160 | // send can't accidentally win the select 161 | if self.fake_error.is_stopped() { 162 | return Err(LocalError::FatalError); 163 | } 164 | 165 | select! { 166 | send = send_fut => send.map_err(|_| LocalError::TxClosed), 167 | _err = error_fut => Err(LocalError::FatalError), 168 | } 169 | } 170 | } 171 | } 172 | 173 | impl WireSpawn for LocalSpawn { 174 | fn spawn(&mut self, fut: impl Future + Send + 'static) { 175 | tokio::task::spawn(fut); 176 | } 177 | } 178 | 179 | /// This function creates a directly-linked Server and Client. 180 | /// 181 | /// This is useful for testing and demonstrating server/client behavior, 182 | /// without actually requiring an external device. 183 | pub fn local_setup(bound: usize, err_uri_path: &str) -> (LocalFakeServer, HostClient) 184 | where 185 | E: Schema + DeserializeOwned, 186 | { 187 | let (c2s_tx, c2s_rx) = channel(bound); 188 | let (s2c_tx, s2c_rx) = channel(bound); 189 | 190 | // NOTE: the normal HostClient machinery has it's own Stopper used for signalling 191 | // errors, this is an EXTRA stopper we use to simulate the error occurring, like 192 | // if our USB device disconnected or the serial port was closed 193 | let fake_error = Stopper::new(); 194 | 195 | let client = HostClient::::new_with_wire( 196 | LocalTx { 197 | to_server: c2s_tx, 198 | fake_error: fake_error.clone(), 199 | }, 200 | LocalRx { 201 | from_server: s2c_rx, 202 | fake_error: fake_error.clone(), 203 | }, 204 | LocalSpawn, 205 | VarSeqKind::Seq2, 206 | err_uri_path, 207 | bound, 208 | ); 209 | 210 | let lfs = LocalFakeServer { 211 | from_client: c2s_rx, 212 | to_client: s2c_tx, 213 | fake_error: fake_error.clone(), 214 | }; 215 | 216 | (lfs, client) 217 | } 218 | --------------------------------------------------------------------------------