diff --git a/embassy-usb-logger/Cargo.toml b/embassy-usb-logger/Cargo.toml new file mode 100644 index 00000000..2f466592 --- /dev/null +++ b/embassy-usb-logger/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "embassy-usb-logger" +version = "0.1.0" +edition = "2021" + +[dependencies] +embassy-usb = { version = "0.1.0", path = "../embassy-usb" } +embassy-sync = { version = "0.1.0", path = "../embassy-sync" } +embassy-futures = { version = "0.1.0", path = "../embassy-futures" } +futures = { version = "0.3", default-features = false } +static_cell = "1" +usbd-hid = "0.6.0" +log = "0.4" diff --git a/embassy-usb-logger/README.md b/embassy-usb-logger/README.md new file mode 100644 index 00000000..81b0dcd0 --- /dev/null +++ b/embassy-usb-logger/README.md @@ -0,0 +1,15 @@ +# embassy-usb-logger + +USB implementation of the `log` crate. This logger can be used by any device that implements `embassy-usb`. When running, +it will output all logging done through the `log` facade to the USB serial peripheral. + +## Usage + +Add the following embassy task to your application. The `Driver` type is different depending on which HAL you use. + + ```rust +#[embassy_executor::task] +async fn logger_task(driver: Driver<'static, USB>) { + embassy_usb_logger::run!(1024, log::LevelFilter::Info, driver); +} +``` diff --git a/embassy-usb-logger/src/lib.rs b/embassy-usb-logger/src/lib.rs new file mode 100644 index 00000000..6386e209 --- /dev/null +++ b/embassy-usb-logger/src/lib.rs @@ -0,0 +1,146 @@ +#![no_std] +#![doc = include_str!("../README.md")] +#![warn(missing_docs)] + +use core::fmt::Write as _; + +use embassy_futures::join::join; +use embassy_sync::pipe::Pipe; +use embassy_usb::class::cdc_acm::{CdcAcmClass, State}; +use embassy_usb::driver::Driver; +use embassy_usb::{Builder, Config}; +use log::{Metadata, Record}; + +type CS = embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex; + +/// The logger state containing buffers that must live as long as the USB peripheral. +pub struct LoggerState<'d> { + state: State<'d>, + device_descriptor: [u8; 32], + config_descriptor: [u8; 128], + bos_descriptor: [u8; 16], + control_buf: [u8; 64], +} + +impl<'d> LoggerState<'d> { + /// Create a new instance of the logger state. + pub fn new() -> Self { + Self { + state: State::new(), + device_descriptor: [0; 32], + config_descriptor: [0; 128], + bos_descriptor: [0; 16], + control_buf: [0; 64], + } + } +} + +/// The logger handle, which contains a pipe with configurable size for buffering log messages. +pub struct UsbLogger { + buffer: Pipe, +} + +impl UsbLogger { + /// Create a new logger instance. + pub const fn new() -> Self { + Self { buffer: Pipe::new() } + } + + /// Run the USB logger using the state and USB driver. Never returns. + pub async fn run<'d, D>(&'d self, state: &'d mut LoggerState<'d>, driver: D) -> ! + where + D: Driver<'d>, + Self: 'd, + { + const MAX_PACKET_SIZE: u8 = 64; + let mut config = Config::new(0xc0de, 0xcafe); + config.manufacturer = Some("Embassy"); + config.product = Some("USB-serial logger"); + config.serial_number = None; + config.max_power = 100; + config.max_packet_size_0 = MAX_PACKET_SIZE; + + // Required for windows compatiblity. + // 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; + + let mut builder = Builder::new( + driver, + config, + &mut state.device_descriptor, + &mut state.config_descriptor, + &mut state.bos_descriptor, + &mut state.control_buf, + None, + ); + + // Create classes on the builder. + let mut class = CdcAcmClass::new(&mut builder, &mut state.state, MAX_PACKET_SIZE as u16); + + // Build the builder. + let mut device = builder.build(); + + loop { + let run_fut = device.run(); + let log_fut = async { + let mut rx: [u8; MAX_PACKET_SIZE as usize] = [0; MAX_PACKET_SIZE as usize]; + class.wait_connection().await; + loop { + let len = self.buffer.read(&mut rx[..]).await; + let _ = class.write_packet(&rx[..len]).await; + } + }; + join(run_fut, log_fut).await; + } + } +} + +impl log::Log for UsbLogger { + fn enabled(&self, _metadata: &Metadata) -> bool { + true + } + + fn log(&self, record: &Record) { + if self.enabled(record.metadata()) { + let _ = write!(Writer(&self.buffer), "{}\r\n", record.args()); + } + } + + fn flush(&self) {} +} + +struct Writer<'d, const N: usize>(&'d Pipe); + +impl<'d, const N: usize> core::fmt::Write for Writer<'d, N> { + fn write_str(&mut self, s: &str) -> Result<(), core::fmt::Error> { + let _ = self.0.try_write(s.as_bytes()); + Ok(()) + } +} + +/// Initialize and run the USB serial logger, never returns. +/// +/// Arguments specify the buffer size, log level and the USB driver, respectively. +/// +/// # Usage +/// +/// ``` +/// embassy_usb_logger::run!(1024, log::LevelFilter::Info, driver); +/// ``` +/// +/// # Safety +/// +/// This macro should only be invoked only once since it is setting the global logging state of the application. +#[macro_export] +macro_rules! run { + ( $x:expr, $l:expr, $p:ident ) => { + static LOGGER: ::embassy_usb_logger::UsbLogger<$x> = ::embassy_usb_logger::UsbLogger::new(); + unsafe { + let _ = ::log::set_logger_racy(&LOGGER).map(|()| log::set_max_level($l)); + } + let _ = LOGGER.run(&mut ::embassy_usb_logger::LoggerState::new(), $p).await; + }; +} diff --git a/examples/rp/Cargo.toml b/examples/rp/Cargo.toml index 31f68830..25022886 100644 --- a/examples/rp/Cargo.toml +++ b/examples/rp/Cargo.toml @@ -13,6 +13,7 @@ embassy-rp = { version = "0.1.0", path = "../../embassy-rp", features = ["defmt" embassy-usb = { version = "0.1.0", path = "../../embassy-usb", features = ["defmt"] } embassy-net = { version = "0.1.0", path = "../../embassy-net", features = ["defmt", "nightly", "tcp", "dhcpv4", "medium-ethernet", "pool-16"] } embassy-futures = { version = "0.1.0", path = "../../embassy-futures" } +embassy-usb-logger = { version = "0.1.0", path = "../../embassy-usb-logger" } defmt = "0.3" defmt-rtt = "0.3" @@ -32,6 +33,7 @@ embedded-hal-async = { version = "0.1.0-alpha.3" } embedded-io = { version = "0.3.1", features = ["async", "defmt"] } embedded-storage = { version = "0.3" } static_cell = "1.0.0" +log = "0.4" [profile.release] debug = true diff --git a/examples/rp/src/bin/usb_logger.rs b/examples/rp/src/bin/usb_logger.rs new file mode 100644 index 00000000..52417a02 --- /dev/null +++ b/examples/rp/src/bin/usb_logger.rs @@ -0,0 +1,30 @@ +#![no_std] +#![no_main] +#![feature(type_alias_impl_trait)] + +use embassy_executor::Spawner; +use embassy_rp::interrupt; +use embassy_rp::peripherals::USB; +use embassy_rp::usb::Driver; +use embassy_time::{Duration, Timer}; +use {defmt_rtt as _, panic_probe as _}; + +#[embassy_executor::task] +async fn logger_task(driver: Driver<'static, USB>) { + embassy_usb_logger::run!(1024, log::LevelFilter::Info, driver); +} + +#[embassy_executor::main] +async fn main(spawner: Spawner) { + let p = embassy_rp::init(Default::default()); + let irq = interrupt::take!(USBCTRL_IRQ); + let driver = Driver::new(p.USB, irq); + spawner.spawn(logger_task(driver)).unwrap(); + + let mut counter = 0; + loop { + counter += 1; + log::info!("Tick {}", counter); + Timer::after(Duration::from_secs(1)).await; + } +}