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:
parent
cb80b0976a
commit
5a24257af5
137
src/geode_midi.rs
Normal file
137
src/geode_midi.rs
Normal 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
78
src/geode_usb.rs
Normal 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;
|
||||
}
|
120
src/main.rs
120
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<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(¬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();
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user