From 5a24257af5ce00caf4e65009f75c3e0dd3c92556 Mon Sep 17 00:00:00 2001 From: dogeystamp Date: Mon, 8 Apr 2024 13:40:17 -0400 Subject: [PATCH] geode_midi.rs geode_usb.rs: created implements queue-based midi and merges all usb-related functions into one embassy task. --- src/geode_midi.rs | 137 ++++++++++++++++++++++++++++++++++++++++++++++ src/geode_usb.rs | 78 ++++++++++++++++++++++++++ src/main.rs | 120 +++++++++++----------------------------- 3 files changed, 246 insertions(+), 89 deletions(-) create mode 100644 src/geode_midi.rs create mode 100644 src/geode_usb.rs diff --git a/src/geode_midi.rs b/src/geode_midi.rs new file mode 100644 index 0000000..a7b562f --- /dev/null +++ b/src/geode_midi.rs @@ -0,0 +1,137 @@ +use embassy_rp::{ + peripherals::USB, + usb::{Driver, Instance}, +}; +use embassy_sync::{blocking_mutex::raw::ThreadModeRawMutex, channel::Channel}; +use embassy_time::Timer; +use embassy_usb::{class::midi::MidiClass, driver::EndpointError}; + +struct NoteMsg { + on: bool, + note: u8, + velocity: u8, +} + +impl NoteMsg { + fn new(on: bool, note: u8, velocity: u8) -> Self { + return NoteMsg { on, note, velocity }; + } +} + +#[derive(Copy, Clone)] +pub enum Controller { + SustainPedal = 64, +} + +struct ControllerMsg { + controller: Controller, + value: u8, +} + +impl ControllerMsg { + fn new(controller: Controller, value: u8) -> Self { + return ControllerMsg { controller, value }; + } +} + +enum MsgType { + Note(NoteMsg), + Controller(ControllerMsg), +} + +struct MidiMsg { + msg: MsgType, + channel: u8, +} + +impl MidiMsg { + fn new(msg: MsgType, channel: u8) -> Self { + return MidiMsg { + msg, + channel: channel & 0xf, + }; + } +} + +pub struct Disconnected {} + +impl From for Disconnected { + fn from(val: EndpointError) -> Self { + match val { + EndpointError::BufferOverflow => panic!("Buffer overflow"), + EndpointError::Disabled => Disconnected {}, + } + } +} + +static MIDI_QUEUE: Channel = Channel::new(); + +/// Handle sending MIDI until connection breaks +pub async fn midi_session<'d, T: Instance + 'd>( + midi: &mut MidiClass<'d, Driver<'d, T>>, +) -> Result<(), Disconnected> { + loop { + let msg = MIDI_QUEUE.receive().await; + match msg.msg { + MsgType::Note(note) => { + let status: u8 = (if note.on { 0b1001_0000 } else { 0b1000_0000 }) | msg.channel; + // i'll be honest i have no idea where the first number here comes from + let packet = [8, status, note.note, note.velocity]; + log::debug!("midi_session: note {:?}", packet); + midi.write_packet(&packet).await? + } + MsgType::Controller(ctrl) => { + let status: u8 = (0b1011_0000) | msg.channel; + let packet = [8, status, ctrl.controller as u8, ctrl.value]; + log::debug!("midi_session: control {:?}", packet); + midi.write_packet(&packet).await? + } + } + } +} + +#[embassy_executor::task] +pub async fn midi_task(mut midi: MidiClass<'static, Driver<'static, USB>>) -> ! { + loop { + log::info!("Connected"); + midi_session(&mut midi); + log::info!("Disconnected"); + } +} + +pub struct MidiChannel { + channel: u8, +} + +impl MidiChannel { + pub fn new(channel: u8) -> Self { + return MidiChannel { channel }; + } + + pub async fn note_on(&self, note: u8, velocity: u8) { + MIDI_QUEUE + .send(MidiMsg::new( + MsgType::Note(NoteMsg::new(true, note, velocity)), + self.channel, + )) + .await; + } + + pub async fn note_off(&self, note: u8, velocity: u8) { + MIDI_QUEUE + .send(MidiMsg::new( + MsgType::Note(NoteMsg::new(false, note, velocity)), + self.channel, + )) + .await; + } + + pub async fn controller(&self, ctrl: Controller, value: u8) { + MIDI_QUEUE + .send(MidiMsg::new( + MsgType::Controller(ControllerMsg::new(ctrl, value)), + self.channel, + )) + .await; + } +} diff --git a/src/geode_usb.rs b/src/geode_usb.rs new file mode 100644 index 0000000..8b66cb2 --- /dev/null +++ b/src/geode_usb.rs @@ -0,0 +1,78 @@ +//! Handle all USB communcation in this task. +//! If USB is handled in multiple tasks the code gets weird and unwieldy (`'static` everywhere) +//! Code in this file is mostly from the examples folder in embassy-rs. + +use embassy_futures::join::join; +use embassy_rp::{peripherals::USB, usb::Driver}; + +use crate::geode_midi::midi_session; +use crate::geode_midi; +use embassy_usb::class::cdc_acm::CdcAcmClass; +use embassy_usb::class::cdc_acm::State; +use embassy_usb::class::midi::MidiClass; +use embassy_usb::driver::EndpointError; +use embassy_usb::{Builder, Config}; + +#[embassy_executor::task] +pub async fn usb_task( + // remember this is the Driver struct not the trait + driver: Driver<'static, USB> +) { + // Create embassy-usb Config + let mut config = Config::new(0xc0de, 0xcafe); + config.manufacturer = Some("dogeystamp"); + config.product = Some("Geode-Piano MIDI keyboard"); + config.serial_number = Some("alpha-12345"); + config.max_power = 100; + config.max_packet_size_0 = 64; + + // 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; + + // Create embassy-usb DeviceBuilder using the driver and config. + // It needs some buffers for building the descriptors. + let mut config_descriptor = [0; 256]; + let mut device_descriptor = [0; 256]; + let mut bos_descriptor = [0; 256]; + let mut control_buf = [0; 64]; + + let mut logger_state = State::new(); + + let mut builder = Builder::new( + driver, + config, + &mut device_descriptor, + &mut config_descriptor, + &mut bos_descriptor, + &mut [], // no msos descriptors + &mut control_buf, + ); + + // Create classes on the builder. + let mut midi_class = MidiClass::new(&mut builder, 1, 1, 64); + let logger_class = CdcAcmClass::new(&mut builder, &mut logger_state, 64); + let log_fut = embassy_usb_logger::with_class!(1024, log::LevelFilter::Info, logger_class); + + // The `MidiClass` can be split into `Sender` and `Receiver`, to be used in separate tasks. + // let (sender, receiver) = class.split(); + + // Build the builder. + let mut usb = builder.build(); + + // Run the USB device. + let usb_fut = usb.run(); + + let midi_fut = async { + loop { + log::info!("Connected"); + midi_session(&mut midi_class).await; + log::info!("Disconnected"); + } + }; + + join(usb_fut, join(log_fut, midi_fut)).await; +} diff --git a/src/main.rs b/src/main.rs index 25cec21..8354a25 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,7 @@ #![no_std] #![no_main] +#![allow(dead_code)] +#![allow(unused)] use embassy_executor::Spawner; use embassy_futures::join::join; @@ -12,14 +14,13 @@ use embassy_rp::peripherals::USB; use embassy_rp::usb::Instance; use embassy_rp::usb::{Driver, InterruptHandler}; use embassy_time::Timer; -use embassy_usb::class::cdc_acm::CdcAcmClass; -use embassy_usb::class::cdc_acm::State; -use embassy_usb::class::midi::MidiClass; -use embassy_usb::driver::EndpointError; -use embassy_usb::{Builder, Config}; +use geode_usb::usb_task; use gpio::{Level, Output}; use {defmt_rtt as _, panic_probe as _}; +mod geode_midi; +mod geode_usb; + bind_interrupts!(struct Irqs { USBCTRL_IRQ => InterruptHandler; }); @@ -29,29 +30,23 @@ async fn blink_task(pin: embassy_rp::gpio::AnyPin) { let mut led = Output::new(pin, Level::Low); loop { - log::info!("led on from task!"); led.set_high(); Timer::after_millis(100).await; - log::info!("led off!"); led.set_low(); Timer::after_secs(5).await; } } -struct Disconnected {} - -impl From for Disconnected { - fn from(val: EndpointError) -> Self { - match val { - EndpointError::BufferOverflow => panic!("Buffer overflow"), - EndpointError::Disabled => Disconnected {}, - } - } +enum Note { + C, + Pedal, } -async fn button<'d, T: Instance + 'd>(pin: AnyPin, midi: &mut MidiClass<'d, Driver<'d, T>>) -> Result<(), Disconnected> { +#[embassy_executor::task(pool_size = 2)] +async fn button(pin: AnyPin, note: Note) { let mut button = Input::new(pin, Pull::Up); + let mut chan = geode_midi::MidiChannel::new(0); loop { let mut counter = 10; button.wait_for_falling_edge().await; @@ -66,9 +61,14 @@ async fn button<'d, T: Instance + 'd>(pin: AnyPin, midi: &mut MidiClass<'d, Driv break; } } + match note { + Note::C => chan.note_on(72, 64).await, + Note::Pedal => { + chan.controller(geode_midi::Controller::SustainPedal, 64) + .await + } + } log::info!("button press"); - let note_on = [9, 0x90, 72, 64]; - midi.write_packet(¬e_on).await?; counter = 10; button.wait_for_rising_edge().await; loop { @@ -82,9 +82,14 @@ async fn button<'d, T: Instance + 'd>(pin: AnyPin, midi: &mut MidiClass<'d, Driv break; } } + match note { + Note::C => chan.note_off(72, 0).await, + Note::Pedal => { + chan.controller(geode_midi::Controller::SustainPedal, 0) + .await + } + } log::info!("button release"); - let note_on = [9, 0x80, 72, 0]; - midi.write_packet(¬e_on).await?; } } @@ -94,73 +99,10 @@ async fn main(_spawner: Spawner) { let driver = Driver::new(p.USB, Irqs); - // Create embassy-usb Config - let mut config = Config::new(0xc0de, 0xcafe); - config.manufacturer = Some("Embassy"); - config.product = Some("USB-MIDI example"); - config.serial_number = Some("12345678"); - config.max_power = 100; - config.max_packet_size_0 = 64; - - // 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; - - // Create embassy-usb DeviceBuilder using the driver and config. - // It needs some buffers for building the descriptors. - let mut config_descriptor = [0; 256]; - let mut device_descriptor = [0; 256]; - let mut bos_descriptor = [0; 256]; - let mut control_buf = [0; 64]; - - let mut logger_state = State::new(); - - let mut builder = Builder::new( - driver, - config, - &mut device_descriptor, - &mut config_descriptor, - &mut bos_descriptor, - &mut [], // no msos descriptors - &mut control_buf, - ); - - // Create classes on the builder. - let mut midi_class = MidiClass::new(&mut builder, 1, 1, 64); - let logger_class = CdcAcmClass::new(&mut builder, &mut logger_state, 64); - let log_fut = embassy_usb_logger::with_class!(1024, log::LevelFilter::Debug, logger_class); - - // The `MidiClass` can be split into `Sender` and `Receiver`, to be used in separate tasks. - // let (sender, receiver) = class.split(); - - // Build the builder. - let mut usb = builder.build(); - - // Run the USB device. - let usb_fut = usb.run(); - - let midi_fut = async { - midi_class.wait_connection().await; - log::info!("Connected"); - let _ = button(p.PIN_16.into(), &mut midi_class).await; - // let _ = midi_echo(&mut midi_class).await; - log::info!("Disconnected"); - }; - _spawner.spawn(blink_task(p.PIN_25.into())).unwrap(); - - join(usb_fut, join(log_fut, midi_fut)).await; -} - -async fn midi_echo<'d, T: Instance + 'd>(class: &mut MidiClass<'d, Driver<'d, T>>) -> Result<(), Disconnected> { - let mut buf = [0; 64]; - loop { - let n = class.read_packet(&mut buf).await?; - let data = &buf[..n]; - log::info!("data: {:#?}", data); - class.write_packet(data).await?; - } + _spawner.spawn(button(p.PIN_16.into(), Note::C)).unwrap(); + _spawner + .spawn(button(p.PIN_17.into(), Note::Pedal)) + .unwrap(); + _spawner.spawn(usb_task(driver)).unwrap(); }