geode_midi.rs geode_usb.rs: created

implements queue-based midi and merges all usb-related functions into
one embassy task.
This commit is contained in:
dogeystamp 2024-04-08 13:40:17 -04:00
parent cb80b0976a
commit 5a24257af5
Signed by: dogeystamp
GPG Key ID: 7225FE3592EFFA38
3 changed files with 246 additions and 89 deletions

137
src/geode_midi.rs Normal file
View File

@ -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<EndpointError> for Disconnected {
fn from(val: EndpointError) -> Self {
match val {
EndpointError::BufferOverflow => panic!("Buffer overflow"),
EndpointError::Disabled => Disconnected {},
}
}
}
static MIDI_QUEUE: Channel<ThreadModeRawMutex, MidiMsg, 3> = 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;
}
}

78
src/geode_usb.rs Normal file
View File

@ -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;
}

View File

@ -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<USB>;
});
@ -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<EndpointError> 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(&note_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(&note_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();
}