Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions example/firmware-ariel-os/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
[package]
name = "workbook-fw-ariel-os"
version = "0.1.0"
edition = "2021"

[dependencies]
ariel-os = { path = "build/imports/ariel-os/src/ariel-os", features = [
"override-usb-config",
"time",
] }
ariel-os-boards = { path = "build/imports/ariel-os/src/ariel-os-boards" }
embassy-sync = { version = "0.6.1", default-features = false }

postcard-rpc = { version = "0.11", features = [
"embassy-usb-0_4-server",
"defmt",
] }
postcard = { version = "1.0.10" }
postcard-schema = { version = "0.2.1", features = ["derive"] }

workbook-icd = { path = "../workbook-icd" }
static_cell = "2.1.0"

[patch.crates-io]
postcard-rpc = { path = "../../source/postcard-rpc" }
10 changes: 10 additions & 0 deletions example/firmware-ariel-os/laze-project.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
imports:
- git:
url: https://github.com/ariel-os/ariel-os
commit: 4a1cf6786da07cc63154c5ddfd26b1487b285f7b
dldir: ariel-os

apps:
- name: workbook-fw-ariel-os
selects:
- usb
14 changes: 14 additions & 0 deletions example/firmware-ariel-os/rust-toolchain.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# this file is parsed with grep & sed for parsing out the actual toolchain
# from the "channel" line. Please keep that in mind when modifying.
[toolchain]
channel = "nightly-2025-02-25"
targets = [
"thumbv6m-none-eabi",
"thumbv7m-none-eabi",
"thumbv7em-none-eabi",
"thumbv7em-none-eabihf",
"thumbv8m.main-none-eabi",
"riscv32imc-unknown-none-elf",
"riscv32imac-unknown-none-elf",
]
components = ["rust-src"]
132 changes: 132 additions & 0 deletions example/firmware-ariel-os/src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
#![no_std]
#![no_main]
#![feature(impl_trait_in_assoc_type)]
#![feature(used_with_arg)]

use ariel_os::{
asynch::Spawner,
debug::log::info,
reexports::embassy_usb,
time::{Duration, Timer},
usb,
};

use postcard_rpc::{
define_dispatch,
header::VarHeader,
server::{
impls::embassy_usb_v0_4::{
dispatch_impl::{WireRxBuf, WireRxImpl, WireSpawnImpl, WireStorageNoUsb, WireTxImpl},
PacketBuffers,
},
Dispatch, Server,
},
};
use static_cell::ConstStaticCell;
use workbook_icd::{PingEndpoint, ENDPOINT_LIST, TOPICS_IN_LIST, TOPICS_OUT_LIST};

pub struct Context;

type AppMutex = embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On ariel, any application might run in isr, one-thread or multi-threaded mode, so I tend to default to the criticalsection mutex.

type AppDriver = usb::UsbDriver;
type AppStorage = WireStorageNoUsb<AppMutex, AppDriver>;
type BufStorage = PacketBuffers<1024, 1024>;
type AppTx = WireTxImpl<AppMutex, AppDriver>;
type AppRx = WireRxImpl<AppDriver>;
type AppServer = Server<AppTx, AppRx, WireRxBuf, MyApp>;

static PBUFS: ConstStaticCell<BufStorage> = ConstStaticCell::new(BufStorage::new());
static STORAGE: AppStorage = AppStorage::new();

/// Helper to get unique ID from flash
pub fn get_unique_id() -> Option<u64> {
// TODO
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ariel has a device id in bytes form (I think platform specific length), this needs wiring up and convert to u64.

Some(12345678)
}

#[ariel_os::config(usb)]
const USB_CONFIG: embassy_usb::Config = {
let mut config = embassy_usb::Config::new(0x16c0, 0x27DD);
config.manufacturer = Some("OneVariable");
config.product = Some("ov-twin");
config.serial_number = Some("12345678");
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should probably be dynamic and not const - it should use the serial number of the device, e.g. from flash memory or whatever


// Required for windows compatibility.
// https://developer.nordicsemi.com/nRF_Connect_SDK/doc/1.9.1/kconfig/CONFIG_CDC_ACM_IAD.html#help
config.device_class = 0xEF;
config.device_sub_class = 0x02;
config.device_protocol = 0x01;
config.composite_with_iads = true;

config
};

define_dispatch! {
app: MyApp;
spawn_fn: spawn_fn;
tx_impl: AppTx;
spawn_impl: WireSpawnImpl;
context: Context;

endpoints: {
list: ENDPOINT_LIST;

| EndpointTy | kind | handler |
| ---------- | ---- | ------- |
| PingEndpoint | blocking | ping_handler |
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a note that we should probably pick a different handler for the docs, I need to remove this from my examples too. As of today, ping is built-in to postcard-rpc, so it's confusing to users if there is a "builtin" one and a provided one.

};
topics_in: {
list: TOPICS_IN_LIST;

| TopicTy | kind | handler |
| ---------- | ---- | ------- |
};
topics_out: {
list: TOPICS_OUT_LIST;
};
}

#[ariel_os::task(autostart, usb_builder_hook)]
async fn main() {
info!("Start");

let unique_id = get_unique_id().unwrap();
info!("id: {=u64:016X}", unique_id);

// USB/RPC INIT
let pbufs = PBUFS.take();
let context = Context;

// Create and inject the Postcard usb endpoint on the system USB builder.
let (tx_impl, rx_impl) = USB_BUILDER_HOOK
.with(|builder| STORAGE.init(builder, pbufs.tx_buf.as_mut_slice()))
.await;

let spawner = Spawner::for_current_executor().await;
let dispatcher = MyApp::new(context, spawner.into());
let vkk = dispatcher.min_key_len();
let mut server: AppServer = Server::new(
tx_impl,
rx_impl,
pbufs.rx_buf.as_mut_slice(),
dispatcher,
vkk,
);

loop {
// Somehow at least on nrf52840dk, this is needed, otherwise the
// `ariel_os_embassy::init_task()` task starves.
Timer::after(Duration::from_millis(100)).await;
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This stumped me. Somehow the other task (on the same executor) is starved without this delay here, and USB doesn't enumerate anymore.


// If the host disconnects, we'll return an error here.
// If this happens, just wait until the host reconnects
let _ = server.run().await;
}
}

// ---

fn ping_handler(_context: &mut Context, _header: VarHeader, rqst: u32) -> u32 {
info!("ping");
rqst
}
103 changes: 103 additions & 0 deletions source/postcard-rpc/src/server/impls/embassy_usb_v0_4.rs
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,12 @@ pub mod dispatch_impl {
pub cell: StaticCell<Mutex<M, EUsbWireTxInner<D>>>,
}

/// A helper type for `static` storage of buffers and driver components
Copy link
Author

@kaspar030 kaspar030 Mar 31, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So this is WireStorage without the USB state. The impl is copy&pasted and then I removed some lines.

A lot can be deduplicated here, also in the original implementation. Ideally they'd all share snippets. It took me some time to get an idea of what's postcard specific and what not. Also, the poststation variant has only some small differences, which are IMO hard to spot.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ideally they'd all share snippets

Or maybe the no-usb-state version could be a field of the original one.

pub struct WireStorageNoUsb<M: RawMutex + 'static, D: Driver<'static> + 'static> {
/// WireTx/Sender static storage
pub cell: StaticCell<Mutex<M, EUsbWireTxInner<D>>>,
}

impl<
M: RawMutex + 'static,
D: Driver<'static> + 'static,
Expand Down Expand Up @@ -224,6 +230,103 @@ pub mod dispatch_impl {
(builder, EUsbWireTx { inner: wtx }, EUsbWireRx { ep_out })
}
}

impl<M: RawMutex + 'static, D: Driver<'static> + 'static> WireStorageNoUsb<M, D> {
/// Create a new, uninitialized static set of buffers
pub const fn new() -> Self {
Self {
cell: StaticCell::new(),
}
}

/// Initialize the static storage, reporting as poststation compatible
///
/// This must only be called once.
pub fn init_poststation(
&'static self,
builder: &mut Builder<'static, D>,
tx_buf: &'static mut [u8],
) -> (WireTxImpl<M, D>, WireRxImpl<D>) {
// Register a poststation-compatible string handler
let hdlr = super::HDLR.take();
builder.handler(hdlr);

// Add the Microsoft OS Descriptor (MSOS/MOD) descriptor.
// We tell Windows that this entire device is compatible with the "WINUSB" feature,
// which causes it to use the built-in WinUSB driver automatically, which in turn
// can be used by libusb/rusb software without needing a custom driver or INF file.
// In principle you might want to call msos_feature() just on a specific function,
// if your device also has other functions that still use standard class drivers.
builder.msos_descriptor(windows_version::WIN8_1, 0);
builder.msos_feature(msos::CompatibleIdFeatureDescriptor::new("WINUSB", ""));
builder.msos_feature(msos::RegistryPropertyFeatureDescriptor::new(
"DeviceInterfaceGUIDs",
msos::PropertyData::RegMultiSz(DEVICE_INTERFACE_GUIDS),
));

// Add a vendor-specific function (class 0xFF), and corresponding interface,
// that uses our custom handler.
let mut function = builder.function(0xFF, 0, 0);
let mut interface = function.interface();
let stindx = interface.string();
super::STINDX.store(stindx.0, core::sync::atomic::Ordering::Relaxed);
let mut alt = interface.alt_setting(0xFF, 0xCA, 0x7D, Some(stindx));
let ep_out = alt.endpoint_bulk_out(64);
let ep_in = alt.endpoint_bulk_in(64);
drop(function);

let wtx = self.cell.init(Mutex::new(EUsbWireTxInner {
ep_in,
log_seq: 0,
tx_buf,
pending_frame: false,
}));

(EUsbWireTx { inner: wtx }, EUsbWireRx { ep_out })
}

/// Initialize the static storage.
///
/// This must only be called once.
pub fn init(
&'static self,
builder: &mut Builder<'static, D>,
tx_buf: &'static mut [u8],
) -> (WireTxImpl<M, D>, WireRxImpl<D>) {
// Add the Microsoft OS Descriptor (MSOS/MOD) descriptor.
// We tell Windows that this entire device is compatible with the "WINUSB" feature,
// which causes it to use the built-in WinUSB driver automatically, which in turn
// can be used by libusb/rusb software without needing a custom driver or INF file.
// In principle you might want to call msos_feature() just on a specific function,
// if your device also has other functions that still use standard class drivers.

// TODO: increase ariel MSOS descriptors
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TODO (Ariel defaults for MSOS are too small. Works without on Linux.)

// builder.msos_descriptor(windows_version::WIN8_1, 0);
// builder.msos_feature(msos::CompatibleIdFeatureDescriptor::new("WINUSB", ""));
// builder.msos_feature(msos::RegistryPropertyFeatureDescriptor::new(
// "DeviceInterfaceGUIDs",
// msos::PropertyData::RegMultiSz(DEVICE_INTERFACE_GUIDS),
// ));

// Add a vendor-specific function (class 0xFF), and corresponding interface,
// that uses our custom handler.
let mut function = builder.function(0xFF, 0, 0);
let mut interface = function.interface();
let mut alt = interface.alt_setting(0xFF, 0, 0, None);
let ep_out = alt.endpoint_bulk_out(64);
let ep_in = alt.endpoint_bulk_in(64);
drop(function);

let wtx = self.cell.init(Mutex::new(EUsbWireTxInner {
ep_in,
log_seq: 0,
tx_buf,
pending_frame: false,
}));

(EUsbWireTx { inner: wtx }, EUsbWireRx { ep_out })
}
}
}

//////////////////////////////////////////////////////////////////////////////
Expand Down
3 changes: 3 additions & 0 deletions source/postcard-rpc/src/server/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ pub trait WireTx {

/// The base [`WireTx`] Error Kind
#[derive(Debug, Clone, Copy)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
#[non_exhaustive]
pub enum WireTxErrorKind {
/// The connection has been closed, and is unlikely to succeed until
Expand Down Expand Up @@ -118,6 +119,7 @@ pub trait WireRx {

/// The base [`WireRx`] Error Kind
#[derive(Debug, Clone, Copy)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
#[non_exhaustive]
pub enum WireRxErrorKind {
/// The connection has been closed, and is unlikely to succeed until
Expand Down Expand Up @@ -374,6 +376,7 @@ where
}

/// A type representing the different errors [`Server::run()`] may return
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub enum ServerError<Tx, Rx>
where
Tx: WireTx,
Expand Down