diff --git a/Cargo.lock b/Cargo.lock index e327a08..5d9565a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -919,6 +919,7 @@ dependencies = [ "critical-section", "defmt", "embassy-sync", + "embedded-io", "embedded-io-adapters", "embedded-io-async", "env_logger", diff --git a/Cargo.toml b/Cargo.toml index 9e9826f..93bb3d2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,6 +16,7 @@ defmt = "0.3" deku = { git = "https://github.com/CodeConstruct/deku.git", tag = "cc/deku-v0.19.1/no-alloc-3", default-features = false } embedded-io-adapters = { version = "0.6", features = ["std", "futures-03"] } embedded-io-async = "0.6" +embedded-io = "0.6" enumset = "1.1" env_logger = "0.11.3" heapless = "0.8" diff --git a/ci/runtests.sh b/ci/runtests.sh index c54cfec..a046c86 100755 --- a/ci/runtests.sh +++ b/ci/runtests.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash set -v set -e @@ -15,7 +15,7 @@ cargo fmt -- --check # Check everything first cargo check --all-targets --locked -if [ -z "NO_CLIPPY" ]; then +if [ -z "$NO_CLIPPY" ]; then cargo clippy --all-targets fi @@ -27,14 +27,14 @@ cargo test NOSTD_CRATES="mctp pldm pldm-fw pldm-platform pldm-file" for c in $NOSTD_CRATES; do ( - cd $c + cd "$c" cargo build --target thumbv7em-none-eabihf --no-default-features ) done ALLOC_CRATES="pldm pldm-platform pldm-file" for c in $ALLOC_CRATES; do ( - cd $c + cd "$c" cargo build --target thumbv7em-none-eabihf --no-default-features --features alloc ) done @@ -53,6 +53,25 @@ cargo build --target thumbv7em-none-eabihf --features defmt --no-default-feature cargo build --features log ) -cargo doc +FEATURES_ASYNC="async" +FEATURES_SYNC="" + +declare -a FEATURES=( + "$FEATURES_SYNC" + "$FEATURES_ASYNC" +) + +# mctp-estack, sync an async +( +cd mctp-estack +for feature in "${FEATURES[@]}"; do + cargo test --features="$feature" +done; +) + +# run cargo doc tests +for feature in "${FEATURES[@]}"; do + cargo doc --features="$feature" +done; echo success diff --git a/mctp-estack/Cargo.toml b/mctp-estack/Cargo.toml index 52e4563..5502fa1 100644 --- a/mctp-estack/Cargo.toml +++ b/mctp-estack/Cargo.toml @@ -11,8 +11,9 @@ rust-version = "1.85" [dependencies] crc = { workspace = true } defmt = { workspace = true, optional = true } -embassy-sync = "0.7" -embedded-io-async = { workspace = true } +embassy-sync = { version = "0.7", optional = true } +embedded-io.workspace = true +embedded-io-async.workspace = true heapless = { workspace = true } log = { workspace = true, optional = true } mctp = { workspace = true } @@ -29,7 +30,8 @@ smol = { workspace = true } [features] -default = ["log"] +default = ["log", "async"] std = ["mctp/std"] log = ["dep:log"] defmt = ["mctp/defmt", "dep:defmt" ] +async = ["dep:embassy-sync"] diff --git a/mctp-estack/README.md b/mctp-estack/README.md index e1e39d7..a1ddf0d 100644 --- a/mctp-estack/README.md +++ b/mctp-estack/README.md @@ -3,10 +3,12 @@ [API docs](https://docs.rs/mctp-estack) This is a MCTP stack suitable for embedded devices. +A `async` Router for [Embassy](https://embassy.dev/) (or other _async runtime_) +based applications is available through the `async` feature. A `Router` instance handles feeding MCTP packets to and from user provided MCTP transports, and handles sending receiving MCTP messages -from applications using the `mctp` crate async traits. +from applications using the `mctp` crate _async_ traits. Applications using MCTP can create `RouterAsyncListener` and `RouterAsyncReqChannel` instances. @@ -16,3 +18,6 @@ MCTP bridging between ports is supported by the `Router`. The core `Stack` handles IO-less MCTP message reassembly and fragmentation, and MCTP tag tracking. MCTP transport binding packet encoding and decoding is provided for I2C, USB, and serial. + +## Features +- `async`: _async_ router implementing `mctp` crate _async_ traits diff --git a/mctp-estack/src/control.rs b/mctp-estack/src/control.rs index 3392912..2807fd4 100644 --- a/mctp-estack/src/control.rs +++ b/mctp-estack/src/control.rs @@ -7,8 +7,11 @@ //! MCTP Control Protocol implementation use crate::fmt::*; +#[cfg(feature = "async")] use crate::Router; -use mctp::{AsyncRespChannel, Eid, Error, Listener, MsgIC, MsgType}; +#[cfg(feature = "async")] +use mctp::{AsyncRespChannel, MsgIC}; +use mctp::{Eid, Error, Listener, MsgType}; use uuid::Uuid; /// A `Result` with a MCTP control completion code as error. @@ -191,7 +194,9 @@ pub struct MctpControlMsg<'a> { work: [u8; 2], } +#[cfg(feature = "async")] const MAX_MSG_SIZE: usize = 20; /* largest is Get Endpoint UUID */ +#[cfg(feature = "async")] const MAX_MSG_TYPES: usize = 8; impl<'a> MctpControlMsg<'a> { @@ -424,6 +429,7 @@ where } /// A Control Message handler. +#[cfg(feature = "async")] pub struct MctpControl<'g, 'r> { rsp_buf: [u8; MAX_MSG_SIZE], types: heapless::Vec, @@ -431,6 +437,7 @@ pub struct MctpControl<'g, 'r> { router: &'g Router<'r>, } +#[cfg(feature = "async")] impl<'g, 'r> MctpControl<'g, 'r> { /// Create a new instance. pub fn new(router: &'g Router<'r>) -> Self { diff --git a/mctp-estack/src/fragment.rs b/mctp-estack/src/fragment.rs index cfd360a..1c20b54 100644 --- a/mctp-estack/src/fragment.rs +++ b/mctp-estack/src/fragment.rs @@ -147,7 +147,7 @@ impl Fragmenter { rest = &mut rest[1..]; } - let Ok(n) = self.reader.read(payload, &mut rest) else { + let Ok(n) = self.reader.read(payload, rest) else { return SendOutput::failure(Error::BadArgument, self); }; let rest = &rest[n..]; diff --git a/mctp-estack/src/lib.rs b/mctp-estack/src/lib.rs index bde1bff..1a0c037 100644 --- a/mctp-estack/src/lib.rs +++ b/mctp-estack/src/lib.rs @@ -5,20 +5,22 @@ //! # MCTP Stack //! -//! This crate provides a MCTP stack that can be embedded in other programs -//! or devices. +//! This crate provides a MCTP stack and transport bindings, +//! that can be embedded in other programs or devices. //! -//! A [`Router`] object lets programs use a [`Stack`] with -//! MCTP transport binding links. Each *Port* handles transmitting and receiving +//! The IO-less [`Stack`] handles MCTP message formatting and parsing, independent +//! of any particular MCTP transport binding. +//! +//! A Router for *async* applications is available +//! through the `async` feature. +//! The async `Router` lets programs use a `Stack` +//! by providing implementations for the standard [`mctp` crate](mctp) async traits. +//! Transport bindings are provided by *Ports* which handle transmitting and receiving //! packets independently. Messages destined for the stack's own EID will //! be passed to applications. //! -//! Applications can create [`router::RouterAsyncListener`] and [`router::RouterAsyncReqChannel`] -//! instances to communicate over MCTP. Those implement the standard [`mctp` crate](mctp) -//! async traits. -//! -//! The IO-less [`Stack`] handles MCTP message formatting and parsing, independent -//! of any particular MCTP transport binding. +//! ## Features +//! - `async`: _async_ router implementing [`mctp` crate](mctp) _async_ traits //! //! ## Configuration //! @@ -35,6 +37,7 @@ // those reworked when using the log crate either. #![allow(clippy::uninlined_format_args)] #![warn(clippy::unused_async)] +#![cfg_attr(docsrs, feature(doc_auto_cfg))] #[cfg(test)] #[macro_use] @@ -61,6 +64,7 @@ pub mod control; pub mod fragment; pub mod i2c; mod reassemble; +#[cfg(feature = "async")] pub mod router; pub mod serial; pub mod usb; @@ -68,12 +72,14 @@ pub mod usb; mod util; mod proto; +#[cfg(feature = "async")] #[rustfmt::skip] #[allow(clippy::needless_lifetimes)] mod zerocopy_channel; use fragment::{Fragmenter, SendOutput}; use reassemble::Reassembler; +#[cfg(feature = "async")] pub use router::Router; use crate::fmt::*; diff --git a/mctp-estack/src/serial.rs b/mctp-estack/src/serial.rs index e713096..606d107 100644 --- a/mctp-estack/src/serial.rs +++ b/mctp-estack/src/serial.rs @@ -4,6 +4,8 @@ */ //! A MCTP serial transport binding, DSP0253 +//! +//! Supporting both `embedded-io` and `embedded-io-async`. #[allow(unused)] use crate::fmt::{debug, error, info, trace, warn}; @@ -12,7 +14,9 @@ use mctp::{Error, Result}; use crc::Crc; use heapless::Vec; -use embedded_io_async::{Read, Write}; +use embedded_io_async::{Read as AsyncRead, Write as AsyncWrite}; + +use embedded_io::{Read, Write}; const MCTP_SERIAL_REVISION: u8 = 0x01; @@ -68,7 +72,10 @@ impl MctpSerialHandler { /// Read a frame. /// /// This is async cancel-safe. - pub async fn recv_async(&mut self, input: &mut impl Read) -> Result<&[u8]> { + pub async fn recv_async( + &mut self, + input: &mut impl AsyncRead, + ) -> Result<&[u8]> { // TODO: This reads one byte a time, might need a buffering wrapper // for performance. Will require more thought about cancel-safety @@ -96,9 +103,43 @@ impl MctpSerialHandler { } } + /// Read a frame synchronously + /// + /// This function blocks until at least one byte is available. + /// + /// Reads one byte at a time from the [reader](Read). + /// If buffering is needed for performance reasons, + /// this has to be provided by the reader. + pub fn recv_sync(&mut self, input: &mut impl Read) -> Result<&[u8]> { + trace!("recv trace"); + loop { + let mut b = 0u8; + match input.read(core::slice::from_mut(&mut b)) { + Ok(1) => (), + Ok(0) => { + trace!("Serial EOF"); + return Err(Error::RxFailure); + } + Ok(2..) => unreachable!(), + Err(_e) => { + trace!("Serial read error"); + return Err(Error::RxFailure); + } + } + + if let Some(_p) = self.feed_frame(b) { + return Ok(&self.rxbuf[2..][..self.rxcount]); + } + } + } + + /// Feed a byte into the frame parser state machine. + /// Returns Some(&[u8]) when a complete frame is available, containing the MCTP packet. + /// Returns None if the frame is incomplete. fn feed_frame(&mut self, b: u8) -> Option<&[u8]> { trace!("serial read {:02x}", b); + // State machine from DSP0253 Figure 1 match self.rxpos { Pos::FrameSearch => { if b == FRAMING_FLAG { @@ -181,22 +222,33 @@ impl MctpSerialHandler { None } + /// Asynchronously send a MCTP packet over serial provided by `output`. pub async fn send_async( &mut self, pkt: &[u8], - output: &mut impl Write, + output: &mut impl AsyncWrite, ) -> Result<()> { - Self::frame_to_serial(pkt, output) + Self::frame_to_serial_async(pkt, output) .await .map_err(|_e| Error::TxFailure) } - async fn frame_to_serial( + /// Synchronously send a MCTP packet over serial provided by `output`. + pub fn send_sync( + &mut self, + pkt: &[u8], + output: &mut impl Write, + ) -> Result<()> { + Self::frame_to_serial_sync(pkt, output).map_err(|_e| Error::TxFailure) + } + + /// Frame a MCTP packet into a serial frame, writing to `output`. + async fn frame_to_serial_async( p: &[u8], output: &mut W, ) -> core::result::Result<(), W::Error> where - W: Write, + W: AsyncWrite, { debug_assert!(p.len() <= u8::MAX.into()); debug_assert!(p.len() > 4); @@ -208,18 +260,43 @@ impl MctpSerialHandler { let cs = !cs.finalize(); output.write_all(&start).await?; - Self::write_escaped(p, output).await?; + Self::write_escaped_async(p, output).await?; output.write_all(&cs.to_be_bytes()).await?; output.write_all(&[FRAMING_FLAG]).await?; Ok(()) } - async fn write_escaped( + /// Frame a MCTP packet into a serial frame, writing to `output`. + fn frame_to_serial_sync( p: &[u8], output: &mut W, ) -> core::result::Result<(), W::Error> where W: Write, + { + debug_assert!(p.len() <= u8::MAX.into()); + debug_assert!(p.len() > 4); + + let start = [FRAMING_FLAG, MCTP_SERIAL_REVISION, p.len() as u8]; + let mut cs = CRC_FCS.digest(); + cs.update(&start[1..]); + cs.update(p); + let cs = !cs.finalize(); + + output.write_all(&start)?; + Self::write_escaped_sync(p, output)?; + output.write_all(&cs.to_be_bytes())?; + output.write_all(&[FRAMING_FLAG])?; + Ok(()) + } + + /// Asynchronously write a byte slice to `output`, escaping 0x7e and 0x7d bytes. + async fn write_escaped_async( + p: &[u8], + output: &mut W, + ) -> core::result::Result<(), W::Error> + where + W: AsyncWrite, { for c in p.split_inclusive(|&b| b == FRAMING_FLAG || b == FRAMING_ESCAPE) @@ -239,6 +316,33 @@ impl MctpSerialHandler { } Ok(()) } + + /// Synchronously write a byte slice to `output`, escaping 0x7e and 0x7d bytes. + fn write_escaped_sync( + p: &[u8], + output: &mut W, + ) -> core::result::Result<(), W::Error> + where + W: Write, + { + for c in + p.split_inclusive(|&b| b == FRAMING_FLAG || b == FRAMING_ESCAPE) + { + let (last, rest) = c.split_last().unwrap(); + match *last { + FRAMING_FLAG => { + output.write_all(rest)?; + output.write_all(&[FRAMING_ESCAPE, FLAG_ESCAPED])?; + } + FRAMING_ESCAPE => { + output.write_all(rest)?; + output.write_all(&[FRAMING_ESCAPE, ESCAPE_ESCAPED])?; + } + _ => output.write_all(c)?, + } + } + Ok(()) + } } impl Default for MctpSerialHandler { @@ -251,51 +355,81 @@ impl Default for MctpSerialHandler { mod tests { use crate::serial::*; use crate::*; - use embedded_io_adapters::futures_03::FromFutures; use proptest::prelude::*; + use embedded_io_adapters::futures_03::FromFutures; + + static TEST_DATA_ROUNTRIP: [&[u8]; 1] = + [&[0x01, 0x5d, 0x0d, 0xf4, 0x01, 0x93, 0x7d, 0xcd, 0x36]]; + fn start_log() { let _ = env_logger::Builder::new() - .filter(None, log::LevelFilter::Trace) + .filter(None, log::LevelFilter::Debug) .is_test(true) .try_init(); } - async fn do_roundtrip(payload: &[u8]) { + async fn do_roundtrip_async(payload: &[u8]) { let mut esc = vec![]; let mut s = FromFutures::new(&mut esc); - MctpSerialHandler::frame_to_serial(payload, &mut s) + MctpSerialHandler::frame_to_serial_async(payload, &mut s) .await .unwrap(); - debug!("{:02x?}", payload); - debug!("{:02x?}", esc); + + debug!("payload {:02x?}", payload); + debug!("esc {:02x?}", esc); let mut h = MctpSerialHandler::new(); let mut s = FromFutures::new(esc.as_slice()); let packet = h.recv_async(&mut s).await.unwrap(); + debug!("packet {:02x?}", packet); + debug_assert_eq!(payload, packet); + } + + fn do_roundtrip_sync(payload: &[u8]) { + start_log(); + let mut esc = vec![]; + MctpSerialHandler::frame_to_serial_sync(payload, &mut esc).unwrap(); + debug!("payload {:02x?}", payload); + debug!("esc {:02x?}", esc); + + let mut h = MctpSerialHandler::new(); + let mut s = esc.as_slice(); + let packet = h.recv_sync(&mut s).unwrap(); + debug!("packet {:02x?}", packet); debug_assert_eq!(payload, packet); } #[test] - fn roundtrip_cases() { + fn roundtrip_cases_async() { // Fixed testcases start_log(); smol::block_on(async { - for payload in - [&[0x01, 0x5d, 0x0d, 0xf4, 0x01, 0x93, 0x7d, 0xcd, 0x36]] - { - do_roundtrip(payload).await + for payload in TEST_DATA_ROUNTRIP { + do_roundtrip_async(payload).await } }) } + #[test] + fn roundtrip_cases_sync() { + start_log(); + for payload in TEST_DATA_ROUNTRIP { + do_roundtrip_sync(payload) + } + } + proptest! { #[test] - fn roundtrip_escape(payload in proptest::collection::vec(0..255u8, 5..20)) { + fn roundtrip_escape_async(payload in proptest::collection::vec(0..255u8, 5..20)) { start_log(); + smol::block_on(do_roundtrip_async(&payload)) + } - smol::block_on(do_roundtrip(&payload)) - + #[test] + fn roundtrip_escape_sync(payload in proptest::collection::vec(0..255u8, 5..20)) { + start_log(); + do_roundtrip_sync(&payload) } } } diff --git a/mctp-estack/tests/roundtrip.rs b/mctp-estack/tests/roundtrip.rs index 9a78cd1..33fdfce 100644 --- a/mctp-estack/tests/roundtrip.rs +++ b/mctp-estack/tests/roundtrip.rs @@ -3,6 +3,11 @@ * Copyright (c) 2025 Code Construct */ +// Roundtrip heavily relies on the router implementation that we currently have +// to remove for a working synchronous implementaion. +// For now, exclude the entire roundtrip test. +#![cfg(feature = "async")] + #[allow(unused)] use log::{debug, error, info, trace, warn}; @@ -10,6 +15,7 @@ use mctp::{Eid, MsgType}; use mctp::{AsyncListener, AsyncReqChannel, AsyncRespChannel}; use mctp_estack::config::NUM_RECEIVE; + use mctp_estack::router::{ Port, PortId, PortLookup, PortTop, RouterAsyncReqChannel, }; diff --git a/mctp-usb-embassy/Cargo.toml b/mctp-usb-embassy/Cargo.toml index b13dc35..2c35674 100644 --- a/mctp-usb-embassy/Cargo.toml +++ b/mctp-usb-embassy/Cargo.toml @@ -15,7 +15,7 @@ embassy-usb-driver = { version = "0.2" } embassy-usb = { version = "0.5", default-features = false } heapless = { workspace = true } log = { workspace = true, optional = true } -mctp-estack = { workspace = true } +mctp-estack = { workspace = true, features = ["async"] } mctp = { workspace = true, default-features = false } [features] diff --git a/pldm-file/examples/host.rs b/pldm-file/examples/host.rs index 87b364a..a03e613 100644 --- a/pldm-file/examples/host.rs +++ b/pldm-file/examples/host.rs @@ -12,8 +12,6 @@ use std::io::{Read, Seek, SeekFrom}; use std::os::unix::fs::MetadataExt; use std::time::{Duration, Instant}; -use argh; - /// PLDM file host #[derive(argh::FromArgs)] struct Args { @@ -142,7 +140,7 @@ fn read_whole(f: &mut File, buf: &mut [u8]) -> std::io::Result { } } // Full output - return Ok(total); + Ok(total) } struct Speed { diff --git a/pldm-platform/examples/decodepdr.rs b/pldm-platform/examples/decodepdr.rs index 5eec163..c286a45 100644 --- a/pldm-platform/examples/decodepdr.rs +++ b/pldm-platform/examples/decodepdr.rs @@ -22,7 +22,7 @@ fn main() { }) .unwrap(); println!("rsp {pdrrsp:?}"); - assert!(rest.len() == 0); + assert!(rest.is_empty()); let ((rest, _), pdr) = Pdr::from_bytes((&pdrrsp.record_data, 0)) .map_err(|e| { @@ -33,7 +33,7 @@ fn main() { if !rest.is_empty() { panic!("Extra PDR response"); } - assert!(rest.len() == 0); + assert!(rest.is_empty()); println!("PDR {pdr:?}"); } diff --git a/standalone/Cargo.toml b/standalone/Cargo.toml index 14971b0..6a2990d 100644 --- a/standalone/Cargo.toml +++ b/standalone/Cargo.toml @@ -10,7 +10,7 @@ categories = ["network-programming"] [dependencies] embedded-io-async = { workspace = true } log = { workspace = true } -mctp-estack = { workspace = true, default-features = true } +mctp-estack = { workspace = true, default-features = true, features = ["async"] } mctp = { workspace = true } smol = { workspace = true }