stuff
This commit is contained in:
1
lib/src/.gitignore
vendored
Normal file
1
lib/src/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
layers.pc
|
||||
15
lib/src/allocator.rs
Normal file
15
lib/src/allocator.rs
Normal file
@ -0,0 +1,15 @@
|
||||
extern crate alloc;
|
||||
|
||||
use core::mem::MaybeUninit;
|
||||
|
||||
use embedded_alloc::Heap;
|
||||
|
||||
pub const HEAP_SIZE: usize = 4096;
|
||||
|
||||
#[global_allocator]
|
||||
static HEAP: Heap = Heap::empty();
|
||||
|
||||
pub fn init() {
|
||||
static mut HEAP_MEM: [MaybeUninit<u8>; HEAP_SIZE] = [MaybeUninit::uninit(); HEAP_SIZE];
|
||||
unsafe { HEAP.init(HEAP_MEM.as_ptr() as usize, HEAP_SIZE) }
|
||||
}
|
||||
99
lib/src/board.rs
Normal file
99
lib/src/board.rs
Normal file
@ -0,0 +1,99 @@
|
||||
use embassy_rp::{peripherals::*, Peripherals};
|
||||
|
||||
/// Pinouts for the ItsyBitsy
|
||||
#[allow(dead_code, non_snake_case)]
|
||||
pub struct Board {
|
||||
pub USB: USB,
|
||||
pub UART0: UART0,
|
||||
pub UART1: UART1,
|
||||
pub PIO0: PIO0,
|
||||
pub PIO1: PIO1,
|
||||
pub DMA_CH0: DMA_CH0,
|
||||
pub DMA_CH1: DMA_CH1,
|
||||
pub DMA_CH2: DMA_CH2,
|
||||
pub DMA_CH3: DMA_CH3,
|
||||
pub DMA_CH4: DMA_CH4,
|
||||
pub DMA_CH5: DMA_CH5,
|
||||
pub DMA_CH6: DMA_CH6,
|
||||
pub DMA_CH7: DMA_CH7,
|
||||
pub DMA_CH8: DMA_CH8,
|
||||
pub DMA_CH9: DMA_CH9,
|
||||
pub DMA_CH10: DMA_CH10,
|
||||
|
||||
// pins
|
||||
pub a0: PIN_26,
|
||||
pub a1: PIN_27,
|
||||
pub a2: PIN_28,
|
||||
pub a3: PIN_29,
|
||||
pub d24: PIN_24,
|
||||
pub d25: PIN_25,
|
||||
pub sck: PIN_18,
|
||||
pub mosi: PIN_19,
|
||||
pub miso: PIN_20,
|
||||
pub d2: PIN_12,
|
||||
pub d3: PIN_5,
|
||||
pub d4: PIN_4,
|
||||
pub rx: PIN_1,
|
||||
pub tx: PIN_0,
|
||||
pub sda: PIN_2,
|
||||
pub scl: PIN_3,
|
||||
pub d5: PIN_14,
|
||||
pub d7: PIN_6,
|
||||
pub d9: PIN_7,
|
||||
pub d10: PIN_8,
|
||||
pub d11: PIN_9,
|
||||
pub d12: PIN_10,
|
||||
pub d13: PIN_11,
|
||||
pub neopixel: PIN_17,
|
||||
pub neopixel_power: PIN_16,
|
||||
}
|
||||
|
||||
impl From<Peripherals> for Board {
|
||||
fn from(p: Peripherals) -> Self {
|
||||
Board {
|
||||
USB: p.USB,
|
||||
UART0: p.UART0,
|
||||
UART1: p.UART1,
|
||||
PIO0: p.PIO0,
|
||||
PIO1: p.PIO1,
|
||||
DMA_CH0: p.DMA_CH0,
|
||||
DMA_CH1: p.DMA_CH1,
|
||||
DMA_CH2: p.DMA_CH2,
|
||||
DMA_CH3: p.DMA_CH3,
|
||||
DMA_CH4: p.DMA_CH4,
|
||||
DMA_CH5: p.DMA_CH5,
|
||||
DMA_CH6: p.DMA_CH6,
|
||||
DMA_CH7: p.DMA_CH7,
|
||||
DMA_CH8: p.DMA_CH8,
|
||||
DMA_CH9: p.DMA_CH9,
|
||||
DMA_CH10: p.DMA_CH10,
|
||||
|
||||
// pins
|
||||
a0: p.PIN_26,
|
||||
a1: p.PIN_27,
|
||||
a2: p.PIN_28,
|
||||
a3: p.PIN_29,
|
||||
d24: p.PIN_24,
|
||||
d25: p.PIN_25,
|
||||
sck: p.PIN_18,
|
||||
mosi: p.PIN_19,
|
||||
miso: p.PIN_20,
|
||||
d2: p.PIN_12,
|
||||
d3: p.PIN_5,
|
||||
d4: p.PIN_4,
|
||||
rx: p.PIN_1,
|
||||
tx: p.PIN_0,
|
||||
sda: p.PIN_2,
|
||||
scl: p.PIN_3,
|
||||
d5: p.PIN_14,
|
||||
d7: p.PIN_6,
|
||||
d9: p.PIN_7,
|
||||
d10: p.PIN_8,
|
||||
d11: p.PIN_9,
|
||||
d12: p.PIN_10,
|
||||
d13: p.PIN_11,
|
||||
neopixel: p.PIN_17,
|
||||
neopixel_power: p.PIN_16,
|
||||
}
|
||||
}
|
||||
}
|
||||
0
lib/src/defmt.rs
Normal file
0
lib/src/defmt.rs
Normal file
60
lib/src/event.rs
Normal file
60
lib/src/event.rs
Normal file
@ -0,0 +1,60 @@
|
||||
use core::time::Duration;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tgnt::{button::Button, keys::Key};
|
||||
|
||||
pub mod switch {
|
||||
use super::*;
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
/// A switch was pressed or released
|
||||
///
|
||||
/// This event is triggered by tasks that monitor switches.
|
||||
pub struct Event {
|
||||
/// The keyboard half that triggered the event.
|
||||
pub source: Half,
|
||||
|
||||
/// The index of the button that triggered the event.
|
||||
pub source_button: usize,
|
||||
|
||||
pub kind: EventKind,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub enum EventKind {
|
||||
Press {
|
||||
button: Button,
|
||||
},
|
||||
Release {
|
||||
button: Button,
|
||||
|
||||
/// The duration that the button was held down for
|
||||
after: Duration,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub mod button {
|
||||
use tgnt::button::Modifier;
|
||||
|
||||
use super::*;
|
||||
|
||||
/// A usb keyboard button was pressed or released.
|
||||
///
|
||||
/// This is a lower-level event than a [SwitchEvent], as things like ModTap and Compose are
|
||||
/// converted to Presses and Releases.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum Event {
|
||||
PressKey(Key),
|
||||
ReleaseKey(Key),
|
||||
PressMod(Modifier),
|
||||
ReleaseMod(Modifier),
|
||||
}
|
||||
}
|
||||
|
||||
/// A keyboard half.
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum Half {
|
||||
Left,
|
||||
Right,
|
||||
}
|
||||
11
lib/src/interrupts.rs
Normal file
11
lib/src/interrupts.rs
Normal file
@ -0,0 +1,11 @@
|
||||
use embassy_rp::{
|
||||
bind_interrupts,
|
||||
peripherals::{UART0, USB},
|
||||
};
|
||||
|
||||
bind_interrupts! {
|
||||
pub struct Irqs {
|
||||
UART0_IRQ => embassy_rp::uart::BufferedInterruptHandler<UART0>;
|
||||
USBCTRL_IRQ => embassy_rp::usb::InterruptHandler<USB>;
|
||||
}
|
||||
}
|
||||
300
lib/src/keyboard.rs
Normal file
300
lib/src/keyboard.rs
Normal file
@ -0,0 +1,300 @@
|
||||
mod lights;
|
||||
|
||||
use core::sync::atomic::{AtomicU16, Ordering};
|
||||
|
||||
use alloc::{boxed::Box, vec::Vec};
|
||||
use embassy_executor::Spawner;
|
||||
use embassy_rp::{
|
||||
gpio::{AnyPin, Input, Pin, Pull},
|
||||
peripherals::PIO1,
|
||||
};
|
||||
use embassy_sync::pubsub::{ImmediatePublisher, PubSubChannel, Subscriber};
|
||||
use embassy_time::{Duration, Instant};
|
||||
use log::{debug, error, info, warn};
|
||||
use static_cell::StaticCell;
|
||||
use tgnt::{button::Button, layer::Layer};
|
||||
|
||||
use crate::{
|
||||
event::{
|
||||
switch::{Event, EventKind},
|
||||
Half,
|
||||
},
|
||||
lights::Lights,
|
||||
util::CS,
|
||||
ws2812::Ws2812,
|
||||
};
|
||||
|
||||
pub struct KeyboardConfig {
|
||||
/// Which board is this.
|
||||
pub half: Half,
|
||||
/// Array of input pins of each switch
|
||||
pub pins: [AnyPin; SWITCH_COUNT],
|
||||
/// Array of LED indices of each switch
|
||||
pub led_map: [usize; SWITCH_COUNT],
|
||||
pub led_driver: Ws2812<PIO1>,
|
||||
pub layers: Vec<Layer>,
|
||||
}
|
||||
|
||||
struct State {
|
||||
/// Which board is this.
|
||||
half: Half,
|
||||
current_layer: AtomicU16,
|
||||
layers: &'static [Layer],
|
||||
/// Array of LED indices of each switch
|
||||
led_map: [usize; SWITCH_COUNT],
|
||||
lights: Lights<PIO1, SWITCH_COUNT>,
|
||||
}
|
||||
|
||||
pub const KB_SUBSCRIBERS: usize = 2;
|
||||
const ACTUAL_KB_SUBSCRIBERS: usize = KB_SUBSCRIBERS + 2;
|
||||
const KB_EVENT_CAP: usize = 128;
|
||||
static KB_EVENTS: PubSubChannel<CS, Event, KB_EVENT_CAP, ACTUAL_KB_SUBSCRIBERS, 0> =
|
||||
PubSubChannel::new();
|
||||
pub struct KbEvents {
|
||||
pub subscriber: Subscriber<'static, CS, Event, KB_EVENT_CAP, ACTUAL_KB_SUBSCRIBERS, 0>,
|
||||
pub publisher: ImmediatePublisher<'static, CS, Event, KB_EVENT_CAP, ACTUAL_KB_SUBSCRIBERS, 0>,
|
||||
}
|
||||
|
||||
pub struct KbEventsTx<'a> {
|
||||
publisher:
|
||||
&'a mut ImmediatePublisher<'static, CS, Event, KB_EVENT_CAP, ACTUAL_KB_SUBSCRIBERS, 0>,
|
||||
}
|
||||
|
||||
pub struct KbEventsRx<'a> {
|
||||
subscriber: &'a mut Subscriber<'static, CS, Event, KB_EVENT_CAP, ACTUAL_KB_SUBSCRIBERS, 0>,
|
||||
}
|
||||
|
||||
impl KeyboardConfig {
|
||||
pub async fn create(self) -> Option<[KbEvents; KB_SUBSCRIBERS]> {
|
||||
let spawner = Spawner::for_current_executor().await;
|
||||
|
||||
if self.layers.is_empty() {
|
||||
error!("no layers defined");
|
||||
return None;
|
||||
}
|
||||
|
||||
info!(
|
||||
"setting up keyboard layout with {} layer(s)",
|
||||
self.layers.len()
|
||||
);
|
||||
|
||||
static STATE: StaticCell<State> = StaticCell::new();
|
||||
let state = STATE.init_with(|| State {
|
||||
half: self.half,
|
||||
current_layer: AtomicU16::new(0),
|
||||
layers: Box::leak(self.layers.into_boxed_slice()),
|
||||
lights: Lights::new(self.led_driver),
|
||||
led_map: self.led_map,
|
||||
});
|
||||
|
||||
for (i, layer) in state.layers.iter().enumerate() {
|
||||
if layer.buttons.len() != SWITCH_COUNT {
|
||||
warn!(
|
||||
"layer {i} defines {} buttons, but there are {SWITCH_COUNT} switches",
|
||||
layer.buttons.len(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
for (i, pin) in self.pins.into_iter().enumerate() {
|
||||
if spawner.spawn(switch_task(i, pin, state)).is_err() {
|
||||
error!("failed to spawn switch task, pool_size mismatch?");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
spawner.must_spawn(layer_switch_task(
|
||||
KbEvents {
|
||||
publisher: KB_EVENTS.immediate_publisher(),
|
||||
subscriber: KB_EVENTS.subscriber().unwrap(),
|
||||
},
|
||||
state,
|
||||
));
|
||||
|
||||
spawner.must_spawn(lights::task(
|
||||
KbEvents {
|
||||
publisher: KB_EVENTS.immediate_publisher(),
|
||||
subscriber: KB_EVENTS.subscriber().unwrap(),
|
||||
},
|
||||
state,
|
||||
));
|
||||
|
||||
Some([(); KB_SUBSCRIBERS].map(|_| KbEvents {
|
||||
publisher: KB_EVENTS.immediate_publisher(),
|
||||
subscriber: KB_EVENTS.subscriber().unwrap(),
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
impl KbEvents {
|
||||
pub async fn send(&mut self, event: Event) {
|
||||
self.publisher.publish_immediate(event);
|
||||
}
|
||||
|
||||
pub async fn recv(&mut self) -> Event {
|
||||
self.subscriber.next_message_pure().await
|
||||
}
|
||||
|
||||
pub fn split(&mut self) -> (KbEventsRx, KbEventsTx) {
|
||||
let tx = KbEventsTx {
|
||||
publisher: &mut self.publisher,
|
||||
};
|
||||
let rx = KbEventsRx {
|
||||
subscriber: &mut self.subscriber,
|
||||
};
|
||||
(rx, tx)
|
||||
}
|
||||
}
|
||||
|
||||
impl KbEventsRx<'_> {
|
||||
pub async fn recv(&mut self) -> Event {
|
||||
self.subscriber.next_message_pure().await
|
||||
}
|
||||
}
|
||||
|
||||
impl KbEventsTx<'_> {
|
||||
pub fn send(&mut self, event: Event) {
|
||||
self.publisher.publish_immediate(event);
|
||||
}
|
||||
}
|
||||
|
||||
pub const MOD_TAP_TIME: Duration = Duration::from_millis(150);
|
||||
pub const SWITCH_COUNT: usize = 18;
|
||||
|
||||
/// Task for monitoring a single switch pin, and handling button presses.
|
||||
#[embassy_executor::task(pool_size = 18)]
|
||||
async fn switch_task(switch_num: usize, pin: AnyPin, state: &'static State) -> ! {
|
||||
let _pin_nr = pin.pin();
|
||||
let mut pin = Input::new(pin, Pull::Up);
|
||||
let events = KB_EVENTS.immediate_publisher();
|
||||
loop {
|
||||
// pins are pull-up, so when the switch is pressed they are brought low.
|
||||
pin.wait_for_low().await;
|
||||
let pressed_at = Instant::now();
|
||||
|
||||
// TODO: do we need debouncing?
|
||||
|
||||
// get current layer
|
||||
let mut current_layer = state.current_layer.load(Ordering::Relaxed);
|
||||
let layer_count = state.layers.len() as u16;
|
||||
if current_layer >= layer_count {
|
||||
error!("current layer was out of bounds for some reason ({current_layer})");
|
||||
current_layer = 0;
|
||||
}
|
||||
let Some(Layer { buttons }) = state.layers.get(usize::from(current_layer)) else {
|
||||
error!("current layer was out of bounds for some reason ({current_layer})");
|
||||
state.current_layer.store(0, Ordering::Relaxed);
|
||||
continue;
|
||||
};
|
||||
|
||||
// and current button
|
||||
let Some(button) = buttons.get(switch_num) else {
|
||||
warn!("no button defined for switch {switch_num}");
|
||||
continue;
|
||||
};
|
||||
|
||||
debug!("switch {switch_num} button {button:?} pressed");
|
||||
|
||||
let ev = |kind| Event {
|
||||
source: state.half,
|
||||
source_button: switch_num,
|
||||
kind,
|
||||
};
|
||||
|
||||
events.publish_immediate(ev(EventKind::Press {
|
||||
button: button.clone(),
|
||||
}));
|
||||
|
||||
pin.wait_for_high().await;
|
||||
let released_after = pressed_at.elapsed();
|
||||
|
||||
debug!("switch {switch_num} button {button:?} released");
|
||||
|
||||
events.publish_immediate(ev(EventKind::Release {
|
||||
button: button.clone(),
|
||||
after: released_after.into(),
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
#[embassy_executor::task]
|
||||
async fn layer_switch_task(mut events: KbEvents, state: &'static State) {
|
||||
let layer_count = state.layers.len() as u16;
|
||||
let Some(last_layer) = layer_count.checked_sub(1) else {
|
||||
error!("no layers specified");
|
||||
return;
|
||||
};
|
||||
|
||||
loop {
|
||||
let event = events.recv().await;
|
||||
let layer = state.current_layer.load(Ordering::Relaxed);
|
||||
let new_layer = match event.kind {
|
||||
EventKind::Press { button } => match button {
|
||||
Button::NextLayer => layer.wrapping_add(1) % layer_count,
|
||||
Button::PrevLayer => layer.checked_sub(1).unwrap_or(last_layer),
|
||||
Button::HoldLayer(l) => layer.wrapping_add(l) % layer_count,
|
||||
_ => continue,
|
||||
},
|
||||
EventKind::Release { button, .. } => match button {
|
||||
Button::HoldLayer(l) => layer.checked_sub(l).unwrap_or(last_layer),
|
||||
_ => continue,
|
||||
},
|
||||
};
|
||||
|
||||
state.current_layer.store(new_layer, Ordering::Relaxed);
|
||||
debug!("switched to layer {new_layer}");
|
||||
}
|
||||
}
|
||||
|
||||
/// Random functions for testing
|
||||
#[allow(dead_code)]
|
||||
pub mod test {
|
||||
use tgnt::{button::Button, keys::Key};
|
||||
|
||||
pub fn letter_to_key(c: char) -> Button {
|
||||
if !c.is_ascii() {
|
||||
return Button::None;
|
||||
}
|
||||
|
||||
let c = c.to_ascii_uppercase();
|
||||
|
||||
let key = match c {
|
||||
'A' => Key::A,
|
||||
'B' => Key::B,
|
||||
'C' => Key::C,
|
||||
'D' => Key::D,
|
||||
'E' => Key::E,
|
||||
'F' => Key::F,
|
||||
'G' => Key::G,
|
||||
'H' => Key::H,
|
||||
'I' => Key::I,
|
||||
'J' => Key::J,
|
||||
'K' => Key::K,
|
||||
'L' => Key::L,
|
||||
'M' => Key::M,
|
||||
'N' => Key::N,
|
||||
'O' => Key::O,
|
||||
'P' => Key::P,
|
||||
'Q' => Key::Q,
|
||||
'R' => Key::R,
|
||||
'S' => Key::S,
|
||||
'T' => Key::T,
|
||||
'U' => Key::U,
|
||||
'V' => Key::V,
|
||||
'W' => Key::W,
|
||||
'X' => Key::X,
|
||||
'Y' => Key::Y,
|
||||
'Z' => Key::Z,
|
||||
' ' => Key::Space,
|
||||
'\n' => Key::Return,
|
||||
_ => {
|
||||
log::info!("char {c:?} -> None");
|
||||
return Button::None;
|
||||
}
|
||||
};
|
||||
|
||||
log::info!("char {c:?} -> {key:?}");
|
||||
|
||||
Button::Key(key)
|
||||
}
|
||||
}
|
||||
196
lib/src/keyboard/lights.rs
Normal file
196
lib/src/keyboard/lights.rs
Normal file
@ -0,0 +1,196 @@
|
||||
use core::cmp::min;
|
||||
|
||||
use atomic_polyfill::Ordering;
|
||||
use embassy_futures::yield_now;
|
||||
use embassy_time::{Duration, Instant, Timer};
|
||||
use futures::{select_biased, FutureExt};
|
||||
use tgnt::button::Button;
|
||||
|
||||
use crate::{rgb::Rgb, util::wheel};
|
||||
|
||||
use super::{Event, EventKind, KbEvents, State, SWITCH_COUNT};
|
||||
|
||||
/// Duration until the keyboard starts the idle animation
|
||||
const UNTIL_IDLE: Duration = Duration::from_secs(30);
|
||||
|
||||
///// Duration from idle until the keyboard goes to sleep
|
||||
//const UNTIL_SLEEP: Duration = Duration::from_secs(10);
|
||||
|
||||
const IDLE_ANIMATION_SPEED: u64 = 3;
|
||||
const IDLE_ANIMATION_KEY_OFFSET: u64 = 10;
|
||||
const IDLE_BRIGHTNESS_RAMPUP: Duration = Duration::from_secs(120);
|
||||
const MAX_IDLE_BRIGHTESS: f32 = 1.0;
|
||||
const MIN_IDLE_BRIGHTESS: f32 = 0.05;
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
enum LightState {
|
||||
Solid(Rgb),
|
||||
SolidThenFade {
|
||||
color: Rgb,
|
||||
solid_until: Instant,
|
||||
fade_by: f32,
|
||||
},
|
||||
FadeBy(f32),
|
||||
None,
|
||||
}
|
||||
|
||||
#[embassy_executor::task]
|
||||
pub(super) async fn task(mut events: KbEvents, state: &'static State) {
|
||||
let mut lights: [LightState; SWITCH_COUNT] = [LightState::None; SWITCH_COUNT];
|
||||
let mut next_frame = Instant::now();
|
||||
let mut idle_at = Instant::now() + UNTIL_IDLE;
|
||||
loop {
|
||||
select_biased! {
|
||||
event = events.recv().fuse() => {
|
||||
handle_event(event, state, &mut lights).await;
|
||||
idle_at = Instant::now() + UNTIL_IDLE;
|
||||
}
|
||||
_ = Timer::at(next_frame).fuse() => {
|
||||
tick(state, &mut lights).await;
|
||||
next_frame = Instant::now() + Duration::from_millis(16);
|
||||
}
|
||||
_ = Timer::at(idle_at).fuse() => {
|
||||
select_biased! {
|
||||
event = events.recv().fuse() => {
|
||||
state.lights.update(|lights| {
|
||||
lights.iter_mut().for_each(|rgb| *rgb = Rgb::new(0, 0, 0));
|
||||
}).await;
|
||||
handle_event(event, state, &mut lights).await;
|
||||
idle_at = Instant::now() + UNTIL_IDLE;
|
||||
}
|
||||
_ = idle_animation(state).fuse() => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn tick(state: &'static State, lights: &mut [LightState; SWITCH_COUNT]) {
|
||||
let now = Instant::now();
|
||||
|
||||
state
|
||||
.lights
|
||||
.update(|rgbs| {
|
||||
for (button, light) in lights.iter_mut().enumerate() {
|
||||
let Some(&led_id) = state.led_map.get(button) else { continue; };
|
||||
let Some(rgb) = rgbs.get_mut(led_id) else { continue; };
|
||||
|
||||
match &*light {
|
||||
LightState::None => {}
|
||||
LightState::FadeBy(fade) => {
|
||||
let [r, g, b] = rgb
|
||||
.components()
|
||||
.map(|c| ((c as f32) * fade.clamp(0.0, 1.0)) as u8);
|
||||
*rgb = Rgb::new(r, g, b);
|
||||
if *rgb == Rgb::new(0, 0, 0) {
|
||||
*light = LightState::None;
|
||||
}
|
||||
}
|
||||
&LightState::Solid(color) => *rgb = color,
|
||||
&LightState::SolidThenFade {
|
||||
color,
|
||||
solid_until,
|
||||
fade_by,
|
||||
} => {
|
||||
*rgb = color;
|
||||
if now >= solid_until {
|
||||
*light = LightState::FadeBy(fade_by);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.await;
|
||||
}
|
||||
|
||||
async fn idle_animation(state: &'static State) {
|
||||
for tick in 0.. {
|
||||
const FRAMETIME: Duration = Duration::from_millis(16);
|
||||
|
||||
state
|
||||
.lights
|
||||
.update(|lights| {
|
||||
const N_MAX: u64 = IDLE_BRIGHTNESS_RAMPUP.as_millis() / FRAMETIME.as_millis();
|
||||
|
||||
let brightness = if tick >= N_MAX {
|
||||
MAX_IDLE_BRIGHTESS
|
||||
} else {
|
||||
((tick as f32) / N_MAX as f32).clamp(MIN_IDLE_BRIGHTESS, MAX_IDLE_BRIGHTESS)
|
||||
};
|
||||
|
||||
for (n, &i) in state.led_map.iter().enumerate() {
|
||||
let Some(light) = lights.get_mut(i) else { continue };
|
||||
let [r, g, b] = wheel(
|
||||
(n as u64 * IDLE_ANIMATION_KEY_OFFSET + tick * IDLE_ANIMATION_SPEED) as u8,
|
||||
)
|
||||
.components();
|
||||
let rgb = Rgb::new(
|
||||
((r as f32) * brightness) as u8,
|
||||
((g as f32) * brightness) as u8,
|
||||
((b as f32) * brightness) as u8,
|
||||
);
|
||||
*light = rgb;
|
||||
}
|
||||
})
|
||||
.await;
|
||||
|
||||
Timer::after(FRAMETIME).await;
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_event(
|
||||
event: Event,
|
||||
state: &'static State,
|
||||
lights: &mut [LightState; SWITCH_COUNT],
|
||||
) {
|
||||
let rgb = match event.kind {
|
||||
EventKind::Press { button } => match button {
|
||||
Button::Key(..) => LightState::Solid(Rgb::new(0, 150, 0)),
|
||||
Button::Mod(..) => LightState::Solid(Rgb::new(0, 0, 150)),
|
||||
Button::ModTap(..) => LightState::Solid(Rgb::new(0, 0, 150)),
|
||||
Button::Compose(..) => LightState::Solid(Rgb::new(0, 100, 100)),
|
||||
Button::NextLayer | Button::PrevLayer => {
|
||||
yield_now().await; // dirty hack to make sure layer_switch_task gets to run first
|
||||
let layer = state.current_layer.load(Ordering::Relaxed);
|
||||
let layer = min(layer, state.layers.len().saturating_sub(1) as u16);
|
||||
let buttons_to_light_up = if state.layers.len() <= 3 {
|
||||
match layer {
|
||||
0 => [0, 1, 2, 3, 4].as_ref(),
|
||||
1 => &[5, 6, 7, 8, 9],
|
||||
2 => &[10, 11, 12, 13, 14],
|
||||
_ => &[],
|
||||
}
|
||||
} else {
|
||||
match layer {
|
||||
0 => [0, 5, 10].as_ref(),
|
||||
1 => &[1, 6, 11],
|
||||
2 => &[2, 7, 12],
|
||||
3 => &[3, 8, 13],
|
||||
4 => &[4, 9, 14],
|
||||
_ => &[],
|
||||
}
|
||||
};
|
||||
|
||||
let solid_until = Instant::now() + Duration::from_millis(200);
|
||||
for &button in buttons_to_light_up {
|
||||
let Some(light) = lights.get_mut(button) else { continue; };
|
||||
*light = LightState::SolidThenFade {
|
||||
color: Rgb::new(120, 0, 120),
|
||||
solid_until,
|
||||
fade_by: 0.85,
|
||||
}
|
||||
}
|
||||
LightState::Solid(Rgb::new(100, 0, 100))
|
||||
}
|
||||
_ => LightState::Solid(Rgb::new(150, 0, 0)),
|
||||
},
|
||||
EventKind::Release { .. } => LightState::FadeBy(0.85),
|
||||
};
|
||||
|
||||
if event.source != state.half {
|
||||
return;
|
||||
}
|
||||
|
||||
let Some(light) = lights.get_mut(event.source_button) else { return; };
|
||||
*light = rgb;
|
||||
}
|
||||
397
lib/src/keypress_handler.rs
Normal file
397
lib/src/keypress_handler.rs
Normal file
@ -0,0 +1,397 @@
|
||||
use core::future::pending;
|
||||
|
||||
use embassy_sync::pubsub::{publisher::Pub, subscriber::Sub, PubSubBehavior, WaitResult};
|
||||
use embassy_time::{Duration, Instant, Timer};
|
||||
use futures::FutureExt;
|
||||
use heapless::Deque;
|
||||
use log::{debug, error};
|
||||
use tgnt::button::Button;
|
||||
|
||||
use crate::event::{button, switch, Half};
|
||||
|
||||
// TODO
|
||||
const MOD_TAP_TIME: Duration = Duration::from_millis(150);
|
||||
const SWITCH_COUNT: usize = 18;
|
||||
|
||||
/// This function perpetually converts between [switch::Event]s and [button::Event]s.
|
||||
///
|
||||
/// Call it from a dedicated task.
|
||||
pub async fn keypress_handler(
|
||||
input: &mut Sub<'_, impl PubSubBehavior<switch::Event>, switch::Event>,
|
||||
output: &mut Pub<'_, impl PubSubBehavior<button::Event>, button::Event>,
|
||||
) -> ! {
|
||||
type SwitchIndex = usize;
|
||||
#[derive(Debug)]
|
||||
struct PressEvent {
|
||||
source_button: SwitchIndex,
|
||||
source_half: Half,
|
||||
button: Button,
|
||||
time: Instant,
|
||||
}
|
||||
|
||||
async fn slow_press(
|
||||
output: &mut Pub<'_, impl PubSubBehavior<button::Event>, button::Event>,
|
||||
button: &Button,
|
||||
) {
|
||||
let event = match button {
|
||||
&Button::Mod(m) | &Button::ModTap(_, m) => button::Event::PressMod(m),
|
||||
&Button::Key(k) => button::Event::PressKey(k),
|
||||
_ => return,
|
||||
};
|
||||
output.publish_immediate(event);
|
||||
}
|
||||
|
||||
async fn slow_release(
|
||||
output: &mut Pub<'_, impl PubSubBehavior<button::Event>, button::Event>,
|
||||
button: &Button,
|
||||
) {
|
||||
let event = match button {
|
||||
&Button::Mod(m) | &Button::ModTap(_, m) => button::Event::ReleaseMod(m),
|
||||
&Button::Key(k) => button::Event::ReleaseKey(k),
|
||||
_ => return,
|
||||
};
|
||||
output.publish_immediate(event);
|
||||
}
|
||||
|
||||
// queue of button presses that are waiting for ModTap
|
||||
let mut queue = Deque::<PressEvent, { 2 * SWITCH_COUNT }>::new();
|
||||
let queue = &mut queue;
|
||||
|
||||
loop {
|
||||
// create a future that waits for the next ModTap to time out
|
||||
let modtap_timeout = async {
|
||||
loop {
|
||||
if let Some(event) = queue.front() {
|
||||
let Button::ModTap(..) = event.button else {
|
||||
error!("first element in queue wasn't a modtap, wtf?");
|
||||
let _ = queue.pop_front();
|
||||
continue;
|
||||
};
|
||||
|
||||
let timeout = event.time + MOD_TAP_TIME;
|
||||
Timer::at(timeout).await;
|
||||
return queue.pop_front().unwrap();
|
||||
} else {
|
||||
// if the queue is empty, never return.
|
||||
return pending().await;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let event = futures::select_biased! {
|
||||
event = input.next_message().fuse() => event,
|
||||
event = modtap_timeout.fuse() => {
|
||||
// first element in queue timed out, and will be treated as a Mod
|
||||
let &Button::ModTap(..) = &event.button else {
|
||||
error!("first element in queue wasn't a modtap, wtf?");
|
||||
continue;
|
||||
};
|
||||
slow_press(output, &event.button).await;
|
||||
|
||||
loop {
|
||||
let Some(event) = queue.pop_front() else { break };
|
||||
|
||||
// resolve events until we encounter another ModTap,
|
||||
// then put it back in the queue and stop
|
||||
if let Button::ModTap(..) = &event.button {
|
||||
queue.push_front(event).expect("we just popped, the queue can't be full");
|
||||
break;
|
||||
}
|
||||
slow_press(output, &event.button).await;
|
||||
}
|
||||
|
||||
continue;
|
||||
},
|
||||
};
|
||||
|
||||
let WaitResult::Message(event) = event else {
|
||||
error!("lagged");
|
||||
continue;
|
||||
};
|
||||
|
||||
let time = Instant::now();
|
||||
debug!("event: {:?}", event.kind);
|
||||
match event.kind {
|
||||
switch::EventKind::Press { button } => {
|
||||
let insert = |queue: &mut Deque<_, 36>, button| {
|
||||
if let Some(_queued) = queue
|
||||
.iter()
|
||||
.find(|queued: &&PressEvent| queued.source_button == event.source_button)
|
||||
.filter(|queued| queued.source_half == event.source)
|
||||
{
|
||||
error!("tried to add PressEvent to queue twice");
|
||||
} else if queue
|
||||
.push_back(PressEvent {
|
||||
source_half: event.source,
|
||||
source_button: event.source_button,
|
||||
button,
|
||||
time,
|
||||
})
|
||||
.is_err()
|
||||
{
|
||||
error!("button queue full. this shouldn't happen.");
|
||||
}
|
||||
};
|
||||
|
||||
match button {
|
||||
Button::ModTap(_, _) => {
|
||||
debug!("adding modtap to queue");
|
||||
// add event to queue
|
||||
insert(queue, button);
|
||||
}
|
||||
Button::Mod(m) => {
|
||||
// if events in queue, also add to queue maybe?
|
||||
// TODO
|
||||
}
|
||||
Button::Key(_) => {
|
||||
if queue.is_empty() {
|
||||
debug!("sending key now");
|
||||
// otherwise, send immediately
|
||||
slow_press(output, &button).await;
|
||||
} else {
|
||||
debug!("adding key to queue");
|
||||
// if events in queue, also add to queue
|
||||
insert(queue, button);
|
||||
}
|
||||
}
|
||||
Button::Compose(..) => {
|
||||
if queue.is_empty() {
|
||||
// otherwise, send immediately
|
||||
// TODO
|
||||
} else {
|
||||
// if events in queue, also add to queue
|
||||
insert(queue, button);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
switch::EventKind::Release { button, .. } => {
|
||||
let position_in_queue = queue
|
||||
.iter()
|
||||
.enumerate()
|
||||
.find(|(_, queued)| queued.source_button == event.source_button)
|
||||
.map(|(i, _)| i);
|
||||
|
||||
match button {
|
||||
Button::ModTap(k, _) => {
|
||||
// check if modtap in queue
|
||||
if let Some(position_in_queue) = position_in_queue {
|
||||
// If the modtap was still in the queue, it hasn't been resolved as a mod
|
||||
// yet. Therefore, it is a Tap. Resolve all ModTaps before this one as Mods
|
||||
debug!("modtap was still in queue");
|
||||
for _ in 0..position_in_queue {
|
||||
let prev_event = queue.pop_front().unwrap();
|
||||
debug!("pressing earlier event {:?}", prev_event);
|
||||
slow_press(output, &prev_event.button).await;
|
||||
}
|
||||
let _ = queue.pop_front();
|
||||
debug!("pressing modtap as key");
|
||||
slow_press(output, &Button::Key(k)).await;
|
||||
slow_release(output, &Button::Key(k)).await;
|
||||
} else {
|
||||
// If the ModTap wasn't in the queue, it has already been resolved as a Mod.
|
||||
debug!("modtap wasn't in queue, releasing");
|
||||
slow_release(output, &button).await;
|
||||
};
|
||||
}
|
||||
Button::Mod(m) => {
|
||||
debug!("mod not implemented yet");
|
||||
// TODO
|
||||
}
|
||||
Button::Key(_) => {
|
||||
// if this press event was in queue, resolve all ModTaps before in queue as Mods
|
||||
// otherwise, just resolve this
|
||||
if let Some(position_in_queue) = position_in_queue {
|
||||
debug!(
|
||||
"key was in queue, pressing all events up to and including this"
|
||||
);
|
||||
for _ in 0..=position_in_queue {
|
||||
let prev_event = queue.pop_front().unwrap();
|
||||
debug!("pressing {prev_event:?}");
|
||||
slow_press(output, &prev_event.button).await;
|
||||
}
|
||||
}
|
||||
debug!("releasing key {button:?}");
|
||||
slow_release(output, &button).await;
|
||||
}
|
||||
Button::Compose(..) => {
|
||||
// if this press event was in queue, resolve all ModTaps before in queue as Mods
|
||||
// otherwise, just resolve this
|
||||
// TODO
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use core::time;
|
||||
|
||||
use super::*;
|
||||
|
||||
use alloc::vec;
|
||||
use alloc::vec::Vec;
|
||||
use embassy_futures::{
|
||||
join::join,
|
||||
select::{select, Either},
|
||||
};
|
||||
use embassy_sync::{blocking_mutex::raw::NoopRawMutex, pubsub::PubSubChannel};
|
||||
use embassy_time::with_timeout;
|
||||
use log::info;
|
||||
use tgnt::{button::Modifier, keys::Key};
|
||||
|
||||
#[test]
|
||||
fn test_modtap_timings() {
|
||||
simple_logger::SimpleLogger::new().init().unwrap();
|
||||
|
||||
let switch_events = PubSubChannel::<NoopRawMutex, switch::Event, 10, 1, 1>::new();
|
||||
let button_events = PubSubChannel::<NoopRawMutex, button::Event, 10, 1, 1>::new();
|
||||
let mut button_sub = button_events.subscriber().unwrap();
|
||||
|
||||
let buttons = [
|
||||
Button::ModTap(Key::A, Modifier::LShift),
|
||||
Button::ModTap(Key::B, Modifier::LCtrl),
|
||||
Button::Key(Key::C),
|
||||
];
|
||||
|
||||
struct Test {
|
||||
description: &'static str,
|
||||
// button index, pressed, delay
|
||||
input: Vec<(usize, bool, Duration)>,
|
||||
expected: Vec<button::Event>,
|
||||
}
|
||||
|
||||
let modtap_mod = Test {
|
||||
description: "modtap mod",
|
||||
input: vec![
|
||||
(0, true, Duration::from_millis(25)),
|
||||
(1, true, Duration::from_millis(25)),
|
||||
(1, false, Duration::from_millis(25)),
|
||||
(0, false, Duration::from_millis(25)),
|
||||
],
|
||||
expected: vec![
|
||||
button::Event::PressMod(Modifier::LShift),
|
||||
button::Event::PressKey(Key::B),
|
||||
button::Event::ReleaseKey(Key::B),
|
||||
button::Event::ReleaseMod(Modifier::LShift),
|
||||
],
|
||||
};
|
||||
|
||||
let modtap_tap = Test {
|
||||
description: "modtap tap",
|
||||
input: vec![
|
||||
(0, true, Duration::from_millis(25)),
|
||||
(1, true, Duration::from_millis(25)),
|
||||
(0, false, Duration::from_millis(25)),
|
||||
(1, false, Duration::from_millis(25)),
|
||||
],
|
||||
expected: vec![
|
||||
button::Event::PressKey(Key::A),
|
||||
button::Event::ReleaseKey(Key::A),
|
||||
button::Event::PressKey(Key::B),
|
||||
button::Event::ReleaseKey(Key::B),
|
||||
],
|
||||
};
|
||||
|
||||
let modtap_tap_2x = Test {
|
||||
description: "2x modtap tap",
|
||||
input: vec![
|
||||
(0, true, Duration::from_millis(25)),
|
||||
(2, true, Duration::from_millis(25)),
|
||||
(2, false, Duration::from_millis(25)),
|
||||
(0, false, Duration::from_millis(25)),
|
||||
(0, true, Duration::from_millis(25)),
|
||||
(2, true, Duration::from_millis(25)),
|
||||
(2, false, Duration::from_millis(25)),
|
||||
(0, false, Duration::from_millis(25)),
|
||||
],
|
||||
expected: vec![
|
||||
button::Event::PressMod(Modifier::LShift),
|
||||
button::Event::PressKey(Key::C),
|
||||
button::Event::ReleaseKey(Key::C),
|
||||
button::Event::ReleaseMod(Modifier::LShift),
|
||||
button::Event::PressMod(Modifier::LShift),
|
||||
button::Event::PressKey(Key::C),
|
||||
button::Event::ReleaseKey(Key::C),
|
||||
button::Event::ReleaseMod(Modifier::LShift),
|
||||
],
|
||||
};
|
||||
|
||||
for test in [modtap_tap, modtap_mod, modtap_tap_2x] {
|
||||
info!("running timing test test {:?}", test.description);
|
||||
embassy_futures::block_on(async {
|
||||
let r = select(
|
||||
join(
|
||||
async {
|
||||
for &(i, pressed, delay) in &test.input {
|
||||
let kind = if pressed {
|
||||
switch::EventKind::Press {
|
||||
button: buttons[i].clone(),
|
||||
}
|
||||
} else {
|
||||
switch::EventKind::Release {
|
||||
button: buttons[i].clone(),
|
||||
after: time::Duration::ZERO, // ignore
|
||||
}
|
||||
};
|
||||
|
||||
let event = switch::Event {
|
||||
source: Half::Left,
|
||||
source_button: i,
|
||||
kind,
|
||||
};
|
||||
|
||||
switch_events.publish_immediate(event);
|
||||
|
||||
Timer::after(delay).await;
|
||||
}
|
||||
},
|
||||
async {
|
||||
let mut got = Vec::new();
|
||||
for _expected in &test.expected {
|
||||
let event = match with_timeout(
|
||||
Duration::from_millis(200),
|
||||
button_sub.next_message(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(WaitResult::Message(event)) => event,
|
||||
Ok(WaitResult::Lagged(_)) => return Err(("lagged", got)),
|
||||
Err(_) => return Err(("timeout", got)),
|
||||
};
|
||||
|
||||
got.push(event.clone());
|
||||
}
|
||||
|
||||
for (event, expected) in got.iter().zip(test.expected.iter()) {
|
||||
if event != expected {
|
||||
return Err(("unexpected event", got));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
},
|
||||
),
|
||||
convert_events(
|
||||
&mut *switch_events.subscriber().unwrap(),
|
||||
&mut *button_events.publisher().unwrap(),
|
||||
),
|
||||
)
|
||||
.await;
|
||||
|
||||
match r {
|
||||
Either::First(((), Ok(()))) => {}
|
||||
Either::First(((), Err((msg, got)))) => panic!(
|
||||
"timing test {:?} failed due to {msg}.\nexpected={:#?} got={:#?}",
|
||||
test.description, test.expected, got
|
||||
),
|
||||
Either::Second(never) => never,
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
25
lib/src/led.rs
Normal file
25
lib/src/led.rs
Normal file
@ -0,0 +1,25 @@
|
||||
use adafruit_itsy_bitsy_rp2040::hal::gpio::{Output, Pin, PinId, PushPull};
|
||||
use cortex_m::delay::Delay;
|
||||
use embedded_hal::digital::v2::OutputPin;
|
||||
|
||||
pub struct Led<I: PinId> {
|
||||
pub pin: Pin<I, Output<PushPull>>,
|
||||
}
|
||||
|
||||
pub struct Blink {
|
||||
ms: u32,
|
||||
}
|
||||
|
||||
pub const SHORT: Blink = Blink { ms: 200 };
|
||||
pub const LONG: Blink = Blink { ms: 600 };
|
||||
|
||||
impl<I: PinId> Led<I> {
|
||||
pub fn dance(&mut self, delay: &mut Delay, moves: &[Blink]) {
|
||||
for m in moves {
|
||||
self.pin.set_high().unwrap();
|
||||
delay.delay_ms(m.ms);
|
||||
self.pin.set_low().unwrap();
|
||||
delay.delay_ms(100);
|
||||
}
|
||||
}
|
||||
}
|
||||
32
lib/src/lib.rs
Normal file
32
lib/src/lib.rs
Normal file
@ -0,0 +1,32 @@
|
||||
#![no_std]
|
||||
#![feature(type_alias_impl_trait)]
|
||||
#![feature(split_array)]
|
||||
|
||||
extern crate alloc;
|
||||
|
||||
#[cfg(target_arch = "arm")]
|
||||
pub mod allocator;
|
||||
#[cfg(target_arch = "arm")]
|
||||
pub mod board;
|
||||
#[cfg(target_arch = "arm")]
|
||||
pub mod interrupts;
|
||||
#[cfg(target_arch = "arm")]
|
||||
pub mod keyboard;
|
||||
#[cfg(target_arch = "arm")]
|
||||
pub mod lights;
|
||||
#[cfg(target_arch = "arm")]
|
||||
pub mod panic_handler;
|
||||
#[cfg(target_arch = "arm")]
|
||||
pub mod uart;
|
||||
#[cfg(target_arch = "arm")]
|
||||
pub mod usb;
|
||||
#[cfg(target_arch = "arm")]
|
||||
pub mod ws2812;
|
||||
|
||||
pub mod event;
|
||||
pub mod logger;
|
||||
pub mod neopixel;
|
||||
pub mod rgb;
|
||||
pub mod rtt;
|
||||
pub mod util;
|
||||
pub mod keypress_handler;
|
||||
41
lib/src/lights.rs
Normal file
41
lib/src/lights.rs
Normal file
@ -0,0 +1,41 @@
|
||||
use crate::ws2812::Ws2812;
|
||||
use embassy_rp::pio;
|
||||
use embassy_sync::mutex::Mutex;
|
||||
|
||||
use crate::{rgb::Rgb, util::CS};
|
||||
|
||||
pub struct Lights<P: pio::Instance + 'static, const N: usize> {
|
||||
state: Mutex<CS, State<P, N>>,
|
||||
}
|
||||
|
||||
struct State<P: pio::Instance + 'static, const N: usize> {
|
||||
colors: [Rgb; N],
|
||||
driver: Ws2812<P>,
|
||||
}
|
||||
|
||||
impl<P: pio::Instance, const N: usize> Lights<P, N> {
|
||||
pub const fn new(driver: Ws2812<P>) -> Self {
|
||||
Lights {
|
||||
state: Mutex::new(State {
|
||||
colors: [Rgb::new(0, 0, 0); N],
|
||||
driver,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn colors_mut(&mut self) -> &[Rgb; N] {
|
||||
&self.state.get_mut().colors
|
||||
}
|
||||
|
||||
/// Run a function to update the colors, and then immediately refresh the LEDs.
|
||||
pub async fn update(&self, f: impl FnOnce(&mut [Rgb; N])) {
|
||||
let State { colors, driver } = &mut *self.state.lock().await;
|
||||
f(colors);
|
||||
driver.write(colors).await
|
||||
}
|
||||
|
||||
/// Update the LEDs with the currently set colors.
|
||||
pub async fn refresh(&self) {
|
||||
self.update(|_| ()).await;
|
||||
}
|
||||
}
|
||||
62
lib/src/logger.rs
Normal file
62
lib/src/logger.rs
Normal file
@ -0,0 +1,62 @@
|
||||
use core::{fmt::Write, sync::atomic::Ordering};
|
||||
|
||||
use atomic_polyfill::AtomicBool;
|
||||
use embassy_time::Instant;
|
||||
use log::{Metadata, Record};
|
||||
use static_cell::StaticCell;
|
||||
|
||||
pub const LOGGER_OUTPUTS: usize = 1;
|
||||
|
||||
pub struct Logger {
|
||||
pub outputs: [fn(&[u8]); LOGGER_OUTPUTS],
|
||||
}
|
||||
|
||||
impl Logger {
|
||||
/// Set this as the global logger.
|
||||
///
|
||||
/// Calling this function more than once does nothing.
|
||||
pub fn init(self) {
|
||||
// guard against calling this multiple times
|
||||
static INITIALIZED: AtomicBool = AtomicBool::new(false);
|
||||
if INITIALIZED.fetch_or(true, Ordering::SeqCst) {
|
||||
return;
|
||||
}
|
||||
|
||||
static LOGGER: StaticCell<Logger> = StaticCell::new();
|
||||
let logger = LOGGER.init(self);
|
||||
unsafe {
|
||||
log::set_logger_racy(logger).unwrap();
|
||||
log::set_max_level_racy(log::LevelFilter::Debug);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl log::Log for Logger {
|
||||
fn enabled(&self, _metadata: &Metadata) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn log(&self, record: &Record) {
|
||||
if self.enabled(record.metadata()) {
|
||||
let now = Instant::now();
|
||||
let s = now.as_secs();
|
||||
let ms = now.as_millis() % 1000;
|
||||
let level = record.metadata().level();
|
||||
let mut w = &mut Writer(self);
|
||||
let _ = writeln!(&mut w, "[{s}.{ms:04}] ({level}) {}", record.args());
|
||||
}
|
||||
}
|
||||
|
||||
fn flush(&self) {}
|
||||
}
|
||||
|
||||
struct Writer<'a>(&'a Logger);
|
||||
|
||||
impl Write for Writer<'_> {
|
||||
fn write_str(&mut self, s: &str) -> Result<(), core::fmt::Error> {
|
||||
for output in &self.0.outputs {
|
||||
output(s.as_bytes());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
11
lib/src/neopixel.pio
Normal file
11
lib/src/neopixel.pio
Normal file
@ -0,0 +1,11 @@
|
||||
.program ws2812
|
||||
.side_set 1
|
||||
.wrap_target
|
||||
bitloop:
|
||||
out x 1 side 0 [6]; Drive low. Side-set still takes place before instruction stalls.
|
||||
jmp !x do_zero side 1 [3]; Branch on the bit we shifted out previous delay. Drive high.
|
||||
do_one:
|
||||
jmp bitloop side 1 [4]; Continue driving high, for a one (long pulse)
|
||||
do_zero:
|
||||
nop side 0 [4]; Or drive low, for a zero (short pulse)
|
||||
.wrap
|
||||
122
lib/src/neopixel.rs
Normal file
122
lib/src/neopixel.rs
Normal file
@ -0,0 +1,122 @@
|
||||
/*
|
||||
use embassy_rp::{
|
||||
gpio::{AnyPin, Drive, SlewRate},
|
||||
peripherals::{DMA_CH0, PIO0},
|
||||
pio::{FifoJoin, PioPeripheral, PioStateMachine, ShiftDirection},
|
||||
pio_instr_util,
|
||||
relocate::RelocatedProgram,
|
||||
PeripheralRef,
|
||||
};
|
||||
use embassy_time::{Duration, Timer};
|
||||
|
||||
#[embassy_executor::task]
|
||||
pub async fn test(pio: PIO0, pin: AnyPin, dma: DMA_CH0) {
|
||||
let (_common, mut sm, ..) = pio.split();
|
||||
let mut dma = PeripheralRef::new(dma);
|
||||
|
||||
let pio_program = pio_proc::pio_file!("src/neopixel.pio");
|
||||
|
||||
let relocated = RelocatedProgram::new(&pio_program.program);
|
||||
sm.write_instr(relocated.origin() as usize, relocated.code());
|
||||
pio_instr_util::exec_jmp(&mut sm, relocated.origin());
|
||||
|
||||
let pin = sm.make_pio_pin(pin);
|
||||
sm.set_set_pins(&[&pin]);
|
||||
sm.set_sideset_base_pin(&pin);
|
||||
sm.set_sideset_count(1);
|
||||
|
||||
// Clock config
|
||||
// TODO CLOCK_FREQ should come from embassy_rp
|
||||
const CLOCK_FREQ: u32 = 125_000_000;
|
||||
const WS2812_FREQ: u32 = 800_000;
|
||||
const CYCLES_PER_BIT: u32 = 16;
|
||||
|
||||
let bit_freq = WS2812_FREQ * CYCLES_PER_BIT;
|
||||
let mut int = CLOCK_FREQ / bit_freq;
|
||||
let rem = CLOCK_FREQ - (int * bit_freq);
|
||||
let frac = (rem * 256) / bit_freq;
|
||||
// 65536.0 is represented as 0 in the pio's clock divider
|
||||
if int == 65536 {
|
||||
int = 0;
|
||||
}
|
||||
sm.set_clkdiv((int << 8) | frac);
|
||||
let pio::Wrap { source, target } = relocated.wrap();
|
||||
sm.set_wrap(source, target);
|
||||
|
||||
sm.set_autopull(true);
|
||||
sm.set_fifo_join(FifoJoin::TxOnly);
|
||||
sm.set_pull_threshold(8); // 24?
|
||||
sm.set_out_shift_dir(ShiftDirection::Left);
|
||||
|
||||
sm.set_enable(true);
|
||||
|
||||
log::info!("wrap: {:?}", sm.get_wrap());
|
||||
log::info!("addr: {:?}", sm.get_addr());
|
||||
log::info!("sideset_base: {:?}", sm.get_sideset_base());
|
||||
log::info!("sideset_count: {:?}", sm.get_sideset_count());
|
||||
log::info!("in_base: {:?}", sm.get_in_base());
|
||||
log::info!("jmp_pin: {:?}", sm.get_jmp_pin());
|
||||
log::info!("set_range: {:?}", sm.get_set_range());
|
||||
log::info!("out_range: {:?}", sm.get_out_range());
|
||||
log::info!("pull_threshold: {:?}", sm.get_pull_threshold());
|
||||
log::info!("push_threshold: {:?}", sm.get_push_threshold());
|
||||
|
||||
//sm = rp2pio.StateMachine(
|
||||
// assembled,
|
||||
// frequency=12_800_000, # to get appropriate sub-bit times in PIO program
|
||||
// first_sideset_pin=NEOPIXEL,
|
||||
// auto_pull=True,
|
||||
// out_shift_right=False,
|
||||
// pull_threshold=8,
|
||||
//)
|
||||
|
||||
loop {
|
||||
log::info!("sending dma");
|
||||
|
||||
sm.dma_push(dma.reborrow(), &[0x0a, 0x00, 0x00]).await;
|
||||
Timer::after(Duration::from_millis(500)).await;
|
||||
sm.dma_push(dma.reborrow(), &[0x00, 0x0a, 0x00]).await;
|
||||
Timer::after(Duration::from_millis(500)).await;
|
||||
sm.dma_push(dma.reborrow(), &[0x00, 0x00, 0x0a]).await;
|
||||
Timer::after(Duration::from_millis(500)).await;
|
||||
|
||||
//sm0.set_enable(true);
|
||||
}
|
||||
}
|
||||
|
||||
#[embassy_executor::task]
|
||||
pub async fn test_blink(pio: PIO0, pin: AnyPin) {
|
||||
log::info!("test blink hehe");
|
||||
let (_, mut sm, ..) = pio.split();
|
||||
// Setup sm2
|
||||
|
||||
// blink
|
||||
let prg = pio_proc::pio_file!("src/blink.pio");
|
||||
|
||||
let relocated = RelocatedProgram::new(&prg.program);
|
||||
let out_pin = sm.make_pio_pin(pin);
|
||||
let pio_pins = [&out_pin];
|
||||
sm.set_set_pins(&pio_pins);
|
||||
sm.set_set_range(25, 1);
|
||||
|
||||
sm.write_instr(relocated.origin() as usize, relocated.code());
|
||||
pio_instr_util::exec_jmp(&mut sm, relocated.origin());
|
||||
// sm.set_clkdiv((65535 << 8) + 255 as u32);
|
||||
// sm.set_clkdiv(0);
|
||||
|
||||
let pio::Wrap { source, target } = relocated.wrap();
|
||||
sm.set_wrap(source, target);
|
||||
|
||||
// sm.set_clkdiv((125e6 / 20.0 / 2e2 * 256.0) as u32);
|
||||
sm.set_enable(true);
|
||||
// sm.wait_push().await as i32;
|
||||
// sm.push_tx(1);
|
||||
sm.wait_push(125_000_000).await;
|
||||
log::info!("started");
|
||||
|
||||
loop {
|
||||
sm.wait_irq(3).await;
|
||||
log::info!("did it!");
|
||||
}
|
||||
}
|
||||
*/
|
||||
8
lib/src/neopixel2.pio
Normal file
8
lib/src/neopixel2.pio
Normal file
@ -0,0 +1,8 @@
|
||||
.program ws2812
|
||||
.origin 0
|
||||
.wrap_target
|
||||
out x 1
|
||||
set pins,1 [1]
|
||||
mov pins,x [1]
|
||||
set pins,0
|
||||
.wrap
|
||||
29
lib/src/panic_handler.rs
Normal file
29
lib/src/panic_handler.rs
Normal file
@ -0,0 +1,29 @@
|
||||
use core::{fmt::Write, panic::PanicInfo};
|
||||
|
||||
use cortex_m::asm;
|
||||
use embassy_rp::gpio::{Level, Output};
|
||||
|
||||
use crate::rtt::rtt_write;
|
||||
|
||||
#[panic_handler]
|
||||
fn panic_blink(info: &PanicInfo) -> ! {
|
||||
cortex_m::interrupt::disable();
|
||||
|
||||
let _ = write!(&mut Writer, "{info}");
|
||||
|
||||
// SAFETY: we panicked, so no other code will be running.
|
||||
let p = unsafe { embassy_rp::Peripherals::steal() };
|
||||
let _led = Output::new(p.PIN_11, Level::High);
|
||||
|
||||
asm::udf()
|
||||
}
|
||||
|
||||
/// Write to RTT
|
||||
struct Writer;
|
||||
|
||||
impl core::fmt::Write for Writer {
|
||||
fn write_str(&mut self, s: &str) -> Result<(), core::fmt::Error> {
|
||||
rtt_write(s.as_bytes());
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
58
lib/src/rgb.rs
Normal file
58
lib/src/rgb.rs
Normal file
@ -0,0 +1,58 @@
|
||||
use bytemuck::{cast_slice, Pod, Zeroable};
|
||||
use core::{
|
||||
fmt::{self, Debug},
|
||||
ops::Div,
|
||||
};
|
||||
|
||||
/// An Rgb value that can be safely transmuted to u32 for use with Ws2812.
|
||||
#[repr(transparent)]
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Pod, Zeroable)]
|
||||
pub struct Rgb(u32);
|
||||
|
||||
impl Rgb {
|
||||
#[inline(always)]
|
||||
pub const fn new(r: u8, g: u8, b: u8) -> Self {
|
||||
Self(u32::from_be_bytes([g, r, b, 0]))
|
||||
}
|
||||
|
||||
/// Get the red, green, and blue components of this Rgb.
|
||||
#[inline(always)]
|
||||
pub const fn components(&self) -> [u8; 3] {
|
||||
let [g, r, b, _] = self.0.to_be_bytes();
|
||||
[r, g, b]
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn slice_as_u32s(rgbs: &[Rgb]) -> &[u32] {
|
||||
cast_slice(rgbs)
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for Rgb {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let [r, g, b] = self.components();
|
||||
f.debug_tuple("Rgb").field(&r).field(&g).field(&b).finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl Div<u8> for Rgb {
|
||||
type Output = Rgb;
|
||||
|
||||
fn div(self, d: u8) -> Self::Output {
|
||||
let [r, g, b] = self.components();
|
||||
Rgb::new(r / d, g / d, b / d)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(target_arch = "x86_64", test))]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use bytemuck::cast;
|
||||
|
||||
#[test]
|
||||
fn test_rgb_as_u32() {
|
||||
let rgb = Rgb::new(0x11, 0x22, 0xCC);
|
||||
let rgb_u32: u32 = cast(rgb);
|
||||
assert_eq!(rgb_u32, 0x2211CC00);
|
||||
}
|
||||
}
|
||||
42
lib/src/rtt.rs
Normal file
42
lib/src/rtt.rs
Normal file
@ -0,0 +1,42 @@
|
||||
use core::cell::RefCell;
|
||||
|
||||
use critical_section::Mutex;
|
||||
use rtt_target::{rtt_init, UpChannel};
|
||||
|
||||
pub type RttWriteFn = fn(&[u8]);
|
||||
|
||||
static CHANNEL: Mutex<RefCell<Option<UpChannel>>> = Mutex::new(RefCell::new(None));
|
||||
|
||||
/// Write directly to the rtt output. Must call [init_rtt_logger] first.
|
||||
pub fn rtt_write(bytes: &[u8]) {
|
||||
critical_section::with(|cs| {
|
||||
let mut slot = CHANNEL.borrow_ref_mut(cs);
|
||||
if let Some(channel) = slot.as_mut() {
|
||||
channel.write(bytes);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
pub fn init_rtt_logger() -> RttWriteFn {
|
||||
let channels = rtt_init! {
|
||||
up: {
|
||||
0: {
|
||||
size: 1024
|
||||
mode: NoBlockSkip
|
||||
name: "Terminal"
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
critical_section::with(|cs| {
|
||||
let mut slot = CHANNEL.borrow_ref_mut(cs);
|
||||
|
||||
if slot.is_some() {
|
||||
return rtt_write;
|
||||
}
|
||||
|
||||
*slot = Some(channels.up.0);
|
||||
|
||||
rtt_write
|
||||
})
|
||||
}
|
||||
171
lib/src/uart.rs
Normal file
171
lib/src/uart.rs
Normal file
@ -0,0 +1,171 @@
|
||||
use core::mem::size_of;
|
||||
|
||||
use bytemuck::{cast, AnyBitPattern, NoUninit};
|
||||
use crc_any::CRCu16;
|
||||
use embassy_executor::Spawner;
|
||||
use embassy_rp::peripherals::{PIN_0, PIN_1, UART0};
|
||||
use embassy_rp::uart::{self, BufferedUart, DataBits, Parity, StopBits};
|
||||
use embedded_io::asynch::{Read, Write};
|
||||
use futures::{select_biased, FutureExt};
|
||||
use heapless::Vec;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use static_cell::StaticCell;
|
||||
|
||||
use crate::event::{switch, Half};
|
||||
use crate::interrupts::Irqs;
|
||||
use crate::keyboard::KbEvents;
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
enum Message {
|
||||
KeyboardEvent(switch::Event),
|
||||
}
|
||||
|
||||
pub async fn start(tx: PIN_0, rx: PIN_1, uart: UART0, board: Half, events: KbEvents) {
|
||||
static TX_BUF: StaticCell<[u8; 256]> = StaticCell::new();
|
||||
static RX_BUF: StaticCell<[u8; 256]> = StaticCell::new();
|
||||
|
||||
let mut config = uart::Config::default();
|
||||
config.baudrate = 115200;
|
||||
config.data_bits = DataBits::DataBits8;
|
||||
config.stop_bits = StopBits::STOP1;
|
||||
config.parity = Parity::ParityNone;
|
||||
|
||||
let uart = embassy_rp::uart::BufferedUart::new(
|
||||
uart,
|
||||
Irqs,
|
||||
tx,
|
||||
rx,
|
||||
TX_BUF.init_with(|| [0u8; 256]),
|
||||
RX_BUF.init_with(|| [0u8; 256]),
|
||||
config,
|
||||
);
|
||||
|
||||
Spawner::for_current_executor()
|
||||
.await
|
||||
.must_spawn(uart_task(uart, board, events))
|
||||
}
|
||||
|
||||
#[embassy_executor::task]
|
||||
async fn uart_task(uart: BufferedUart<'static, UART0>, this_half: Half, mut events: KbEvents) {
|
||||
let (mut rx, mut tx) = uart.split();
|
||||
let (mut events_rx, mut events_tx) = events.split();
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy, Debug, NoUninit, AnyBitPattern)]
|
||||
struct Header {
|
||||
/// The length of the payload.
|
||||
len: u8,
|
||||
|
||||
/// An arbitrary value to feed the crc, should be different for each message.
|
||||
random: u8,
|
||||
|
||||
/// A little-endian crc16.
|
||||
crc: [u8; 2],
|
||||
}
|
||||
|
||||
const HEADER_LEN: usize = size_of::<Header>();
|
||||
|
||||
let rx_task = async {
|
||||
let mut buf: heapless::Vec<u8, 1024> = Vec::new();
|
||||
loop {
|
||||
if buf.len() >= HEADER_LEN {
|
||||
let (&header, rest) = buf.split_array_ref::<HEADER_LEN>();
|
||||
let header: Header = cast(header);
|
||||
let crc = u16::from_le_bytes(header.crc);
|
||||
let mut calculated_crc = CRCu16::crc16();
|
||||
calculated_crc.digest(&[header.len, header.random]);
|
||||
let calculated_crc = calculated_crc.get_crc();
|
||||
|
||||
if calculated_crc != crc {
|
||||
log::error!("invalid uart header crc: {header:x?}");
|
||||
buf.remove(0); // pop the first byte and hope we find a good packet header
|
||||
continue;
|
||||
}
|
||||
|
||||
log::debug!("got uart header {header:x?}");
|
||||
|
||||
let len = usize::from(header.len);
|
||||
if rest.len() >= len {
|
||||
let r = postcard::from_bytes(&rest[..len]);
|
||||
|
||||
// drop packet from buffer
|
||||
buf.rotate_left(len + HEADER_LEN);
|
||||
buf.truncate(buf.len() - len - HEADER_LEN);
|
||||
|
||||
let message: Message = match r {
|
||||
Ok(v) => v,
|
||||
Err(e) => {
|
||||
log::error!("failed to deserialize message: {e}");
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
match &message {
|
||||
Message::KeyboardEvent(event) => events_tx.send(event.clone()),
|
||||
}
|
||||
|
||||
log::info!("got msg: {:?}", message);
|
||||
}
|
||||
}
|
||||
|
||||
let mut chunk = [0u8; 128];
|
||||
let n = match rx.read(&mut chunk).await {
|
||||
Ok(n) => n,
|
||||
Err(e) => {
|
||||
log::error!("uart error: {:?}", e);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
if buf.extend_from_slice(&chunk[..n]).is_err() {
|
||||
log::error!("uart buffer full");
|
||||
buf.clear();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let tx_task = async {
|
||||
let mut buf = [0u8; 256 + HEADER_LEN];
|
||||
let mut counter = 0u8;
|
||||
loop {
|
||||
// forward messages to the other keyboard half
|
||||
let event = events_rx.recv().await;
|
||||
if event.source != this_half {
|
||||
continue; // do not forward messages from the other half back to it
|
||||
}
|
||||
|
||||
let message = Message::KeyboardEvent(event);
|
||||
let (buf_header, body) = buf.split_array_mut();
|
||||
let serialized = match postcard::to_slice(&message, body) {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
log::error!("failed to serialize uart message: {e}");
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
// add a "random" value to feed the crc
|
||||
counter = counter.wrapping_add(1);
|
||||
let random = counter;
|
||||
|
||||
let len = serialized.len() as u8;
|
||||
|
||||
let mut crc = CRCu16::crc16();
|
||||
crc.digest(&[len, random]);
|
||||
let header = Header {
|
||||
len: serialized.len() as u8,
|
||||
random,
|
||||
crc: crc.get_crc().to_le_bytes(),
|
||||
};
|
||||
let header: [u8; HEADER_LEN] = cast(header);
|
||||
*buf_header = header;
|
||||
|
||||
let package = &buf[..HEADER_LEN + usize::from(len)];
|
||||
tx.write_all(package).await.ok();
|
||||
}
|
||||
};
|
||||
|
||||
select_biased! {
|
||||
_ = tx_task.fuse() => log::error!("uart tx_task exited"),
|
||||
_ = rx_task.fuse() => log::error!("eart rx_task exited"),
|
||||
}
|
||||
}
|
||||
75
lib/src/usb.rs
Normal file
75
lib/src/usb.rs
Normal file
@ -0,0 +1,75 @@
|
||||
use embassy_executor::Spawner;
|
||||
use embassy_rp::{peripherals::USB, usb::Driver};
|
||||
use embassy_usb::{Builder, Config, UsbDevice};
|
||||
use static_cell::StaticCell;
|
||||
|
||||
use crate::{interrupts::Irqs, keyboard::KbEvents};
|
||||
|
||||
pub mod keyboard;
|
||||
pub mod logger;
|
||||
|
||||
pub const MAX_PACKET_SIZE: u8 = 64;
|
||||
|
||||
struct State {
|
||||
device_descriptor: [u8; 256],
|
||||
config_descriptor: [u8; 256],
|
||||
bos_descriptor: [u8; 256],
|
||||
control_buf: [u8; 64],
|
||||
}
|
||||
|
||||
static STATE: StaticCell<State> = StaticCell::new();
|
||||
|
||||
pub async fn setup_logger_and_keyboard(usb: USB, events: KbEvents) {
|
||||
let mut builder = builder(usb);
|
||||
//logger::setup(&mut builder).await;
|
||||
|
||||
keyboard::setup(&mut builder, events).await;
|
||||
|
||||
log::info!("building usb device");
|
||||
let usb = builder.build();
|
||||
log::info!("spawning usb task");
|
||||
Spawner::for_current_executor().await.must_spawn(run(usb));
|
||||
}
|
||||
|
||||
pub fn builder(usb: USB) -> Builder<'static, Driver<'static, USB>> {
|
||||
// calling init here can't panic because this function can't be called
|
||||
// twice since we are taking ownership of the only USB peripheral.
|
||||
let state = STATE.init_with(|| State {
|
||||
device_descriptor: [0; 256],
|
||||
config_descriptor: [0; 256],
|
||||
bos_descriptor: [0; 256],
|
||||
control_buf: [0; 64],
|
||||
});
|
||||
|
||||
// Create embassy-usb Config
|
||||
let mut config = Config::new(0xb00b, 0x1355);
|
||||
config.manufacturer = Some("Tux");
|
||||
config.product = Some("Tangentbord1");
|
||||
config.serial_number = Some("42069");
|
||||
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 driver = Driver::new(usb, Irqs);
|
||||
|
||||
Builder::new(
|
||||
driver,
|
||||
config,
|
||||
&mut state.device_descriptor,
|
||||
&mut state.config_descriptor,
|
||||
&mut state.bos_descriptor,
|
||||
&mut state.control_buf,
|
||||
)
|
||||
}
|
||||
|
||||
#[embassy_executor::task]
|
||||
pub async fn run(mut device: UsbDevice<'static, Driver<'static, USB>>) {
|
||||
log::info!("running usb device");
|
||||
device.run().await
|
||||
}
|
||||
201
lib/src/usb/keyboard.rs
Normal file
201
lib/src/usb/keyboard.rs
Normal file
@ -0,0 +1,201 @@
|
||||
pub mod report;
|
||||
|
||||
use embassy_executor::Spawner;
|
||||
use embassy_futures::select::select;
|
||||
use embassy_rp::{peripherals::USB, usb::Driver};
|
||||
use embassy_sync::{
|
||||
blocking_mutex::raw::NoopRawMutex,
|
||||
mutex::Mutex,
|
||||
pubsub::{PubSubChannel, WaitResult},
|
||||
};
|
||||
use embassy_time::{Duration, Timer};
|
||||
use embassy_usb::{
|
||||
class::hid::{self, HidReaderWriter, ReadError, ReportId, RequestHandler},
|
||||
control::OutResponse,
|
||||
Builder,
|
||||
};
|
||||
use embassy_usb_driver::EndpointError;
|
||||
use log::error;
|
||||
use static_cell::StaticCell;
|
||||
use usbd_hid::descriptor::SerializedDescriptor;
|
||||
|
||||
use crate::{
|
||||
event::button,
|
||||
keyboard::KbEvents,
|
||||
usb::keyboard::report::{KeyboardReport, EMPTY_KEYBOARD_REPORT},
|
||||
util::CS, keypress_handler::keypress_handler,
|
||||
};
|
||||
|
||||
use super::MAX_PACKET_SIZE;
|
||||
|
||||
struct Handler;
|
||||
|
||||
static CONTEXT: StaticCell<Context> = StaticCell::new();
|
||||
|
||||
static KB_REPORT: Mutex<CS, Reports> = Mutex::new(Reports {
|
||||
actual: EMPTY_KEYBOARD_REPORT,
|
||||
unsent: EMPTY_KEYBOARD_REPORT,
|
||||
});
|
||||
|
||||
struct Reports {
|
||||
/// The report to be sent to the host machine.
|
||||
actual: KeyboardReport,
|
||||
|
||||
/// Key presses which hasn't been sent yet.
|
||||
unsent: KeyboardReport,
|
||||
}
|
||||
|
||||
struct Context {
|
||||
handler: Handler,
|
||||
state: hid::State<'static>,
|
||||
}
|
||||
|
||||
pub async fn setup(builder: &mut Builder<'static, Driver<'static, USB>>, events: KbEvents) {
|
||||
log::info!("setting up usb hid");
|
||||
|
||||
let context = CONTEXT.init(Context {
|
||||
handler: Handler,
|
||||
state: hid::State::new(),
|
||||
});
|
||||
|
||||
let config = hid::Config {
|
||||
//report_descriptor: MouseReport::desc(),
|
||||
report_descriptor: KeyboardReport::desc(),
|
||||
request_handler: Some(&context.handler),
|
||||
poll_ms: 2,
|
||||
max_packet_size: MAX_PACKET_SIZE as u16,
|
||||
};
|
||||
|
||||
let stream = HidStream::new(builder, &mut context.state, config);
|
||||
|
||||
let spawner = Spawner::for_current_executor().await;
|
||||
|
||||
spawner.must_spawn(task(stream, &context.handler));
|
||||
spawner.must_spawn(listen_to_events(events));
|
||||
|
||||
log::info!("done setting up usb keyboard");
|
||||
}
|
||||
|
||||
impl RequestHandler for Handler {
|
||||
fn get_report(&self, id: ReportId, buf: &mut [u8]) -> Option<usize> {
|
||||
log::info!("get_report({id:?}, {buf:?})");
|
||||
let _ = (id, buf);
|
||||
None
|
||||
}
|
||||
|
||||
fn set_report(&self, id: ReportId, data: &[u8]) -> embassy_usb::control::OutResponse {
|
||||
log::info!("set_report({id:?}, {data:?})");
|
||||
let _ = (id, data);
|
||||
OutResponse::Rejected
|
||||
}
|
||||
|
||||
fn get_idle_ms(&self, id: Option<ReportId>) -> Option<u32> {
|
||||
log::info!("get_idle_ms({id:?})");
|
||||
let _ = id;
|
||||
None
|
||||
}
|
||||
|
||||
fn set_idle_ms(&self, id: Option<ReportId>, duration_ms: u32) {
|
||||
log::info!("set_idle_ms({id:?}, {duration_ms})");
|
||||
let _ = (id, duration_ms);
|
||||
}
|
||||
}
|
||||
type HidStream = HidReaderWriter<'static, Driver<'static, USB>, 256, 256>;
|
||||
|
||||
#[embassy_executor::task]
|
||||
async fn listen_to_events(mut events: KbEvents) {
|
||||
let button_events = PubSubChannel::<NoopRawMutex, button::Event, 10, 1, 1>::new();
|
||||
let mut button_pub = button_events.publisher().unwrap();
|
||||
let mut button_sub = button_events.subscriber().unwrap();
|
||||
|
||||
select(
|
||||
async {
|
||||
loop {
|
||||
let WaitResult::Message(event) = button_sub.next_message().await else {
|
||||
error!("lagged");
|
||||
continue;
|
||||
};
|
||||
|
||||
loop {
|
||||
let mut r = KB_REPORT.lock().await;
|
||||
match event {
|
||||
button::Event::PressKey(k) => {
|
||||
r.actual.press_key(k);
|
||||
r.unsent.press_key(k);
|
||||
}
|
||||
button::Event::PressMod(m) => {
|
||||
r.actual.press_modifier(m);
|
||||
r.unsent.press_modifier(m);
|
||||
}
|
||||
|
||||
// we got a key release, but if the key press hasn't been sent yet, we
|
||||
// wait for a bit until it has.
|
||||
button::Event::ReleaseKey(k) if r.unsent.key_pressed(k) => {
|
||||
drop(r);
|
||||
Timer::after(Duration::from_millis(1)).await;
|
||||
continue;
|
||||
}
|
||||
button::Event::ReleaseMod(m) if r.unsent.modifier_pressed(m) => {
|
||||
drop(r);
|
||||
Timer::after(Duration::from_millis(1)).await;
|
||||
continue;
|
||||
}
|
||||
|
||||
button::Event::ReleaseKey(k) => r.actual.release_key(k),
|
||||
button::Event::ReleaseMod(m) => r.actual.release_modifier(m),
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
keypress_handler(&mut *events.subscriber, &mut *button_pub),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
#[embassy_executor::task]
|
||||
async fn task(stream: HidStream, handler: &'static Handler) {
|
||||
if let Err(e) = keyboard_report(stream, handler).await {
|
||||
log::error!("keyboard error: {e:?}");
|
||||
}
|
||||
}
|
||||
|
||||
async fn keyboard_report(mut stream: HidStream, _handler: &'static Handler) -> Result<(), Error> {
|
||||
stream.ready().await;
|
||||
loop {
|
||||
Timer::after(Duration::from_millis(2)).await;
|
||||
|
||||
let report = {
|
||||
let mut reports = KB_REPORT.lock().await;
|
||||
reports.unsent = EMPTY_KEYBOARD_REPORT;
|
||||
reports.actual.clone()
|
||||
};
|
||||
|
||||
if report.keycodes != EMPTY_KEYBOARD_REPORT.keycodes {
|
||||
log::trace!("keys: {:x?}", report.keycodes);
|
||||
}
|
||||
|
||||
#[cfg(feature = "n-key-rollover")]
|
||||
stream.write(report.as_bytes()).await?;
|
||||
|
||||
#[cfg(not(feature = "n-key-rollover"))]
|
||||
stream.write_serialize(&report).await?;
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum Error {
|
||||
Read(ReadError),
|
||||
Endpoint(EndpointError),
|
||||
}
|
||||
|
||||
impl From<ReadError> for Error {
|
||||
fn from(value: ReadError) -> Self {
|
||||
Error::Read(value)
|
||||
}
|
||||
}
|
||||
impl From<EndpointError> for Error {
|
||||
fn from(value: EndpointError) -> Self {
|
||||
Error::Endpoint(value)
|
||||
}
|
||||
}
|
||||
160
lib/src/usb/keyboard/report.rs
Normal file
160
lib/src/usb/keyboard/report.rs
Normal file
@ -0,0 +1,160 @@
|
||||
#![allow(dead_code)]
|
||||
|
||||
/// KeyboardReport describes a report and its companion descriptor that can be
|
||||
/// used to send keyboard button presses to a host and receive the status of the
|
||||
/// keyboard LEDs.
|
||||
///
|
||||
/// Unlike usbd_hids KeyboardReport, this one supports N-key rollover.
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Zeroable, Pod)]
|
||||
#[cfg(feature = "n-key-rollover")]
|
||||
#[repr(C, packed)]
|
||||
pub struct KeyboardReport {
|
||||
pub modifier: u8,
|
||||
|
||||
/// Bitmap representing all keycodes from 0 to 215
|
||||
pub keycodes: [u8; 27],
|
||||
}
|
||||
|
||||
use bytemuck::{cast_ref, Pod, Zeroable};
|
||||
use core::mem::size_of;
|
||||
use tgnt::{button::Modifier, keys::Key};
|
||||
|
||||
#[cfg(not(feature = "n-key-rollover"))]
|
||||
pub use ::usbd_hid::descriptor::KeyboardReport;
|
||||
|
||||
#[cfg(feature = "n-key-rollover")]
|
||||
pub const EMPTY_KEYBOARD_REPORT: KeyboardReport = KeyboardReport {
|
||||
modifier: 0,
|
||||
keycodes: [0; 27],
|
||||
};
|
||||
|
||||
#[cfg(not(feature = "n-key-rollover"))]
|
||||
pub const EMPTY_KEYBOARD_REPORT: KeyboardReport = KeyboardReport {
|
||||
modifier: 0,
|
||||
leds: 0,
|
||||
reserved: 0,
|
||||
keycodes: [0; 6],
|
||||
};
|
||||
|
||||
/// Get the byte index, and the mask for that byte, in the keycode bitmap.
|
||||
fn key_to_byte_mask(key: Key) -> (usize, u8) {
|
||||
let keycode = u8::from(key);
|
||||
let byte = keycode >> 3;
|
||||
let bit = keycode & 0b111;
|
||||
let mask = 1 << bit;
|
||||
|
||||
(usize::from(byte), mask)
|
||||
}
|
||||
|
||||
#[cfg(feature = "n-key-rollover")]
|
||||
impl KeyboardReport {
|
||||
#[inline(always)]
|
||||
pub fn set_key(&mut self, key: Key, pressed: bool) {
|
||||
let (byte, mask) = key_to_byte_mask(key);
|
||||
|
||||
if let Some(k) = self.keycodes.get_mut(byte as usize) {
|
||||
if pressed {
|
||||
*k |= mask;
|
||||
} else {
|
||||
*k &= !mask;
|
||||
}
|
||||
} else {
|
||||
log::warn!("Tried to set out-of-range keycode: 0x{:x}", u8::from(key));
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn press_key(&mut self, key: Key) {
|
||||
self.set_key(key, true)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn release_key(&mut self, key: Key) {
|
||||
self.set_key(key, false)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn key_pressed(&mut self, key: Key) -> bool {
|
||||
let (byte, mask) = key_to_byte_mask(key);
|
||||
if let Some(k) = self.keycodes.get_mut(byte as usize) {
|
||||
(*k & mask) != 0
|
||||
} else {
|
||||
log::warn!("Tried to get out-of-range keycode: 0x{:x}", u8::from(key));
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn set_modifier(&mut self, modifier: Modifier, pressed: bool) {
|
||||
if pressed {
|
||||
self.modifier |= u8::from(modifier);
|
||||
} else {
|
||||
self.modifier &= !u8::from(modifier);
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn press_modifier(&mut self, modifier: Modifier) {
|
||||
self.set_modifier(modifier, true)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn release_modifier(&mut self, modifier: Modifier) {
|
||||
self.set_modifier(modifier, false)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn modifier_pressed(&mut self, modifier: Modifier) -> bool {
|
||||
(self.modifier & u8::from(modifier)) != 0
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn as_bytes(&self) -> &[u8; size_of::<KeyboardReport>()] {
|
||||
cast_ref(self)
|
||||
}
|
||||
}
|
||||
|
||||
// bitmasks for the `modifier` field
|
||||
|
||||
#[cfg(feature = "n-key-rollover")]
|
||||
impl usbd_hid::descriptor::SerializedDescriptor for KeyboardReport {
|
||||
fn desc() -> &'static [u8] {
|
||||
// Manually define the descriptor since I can't figure out how to get
|
||||
// gen_hid_descriptor to generate the correct one.
|
||||
&[
|
||||
0x05, 0x01, // usage page 1 (generic desktop)
|
||||
0x09, 0x06, // usage (keyboard)
|
||||
0xa1, 0x01, // collection (application)
|
||||
//
|
||||
0x05, 0x07, // usage page 7 (keyboard/keypad)
|
||||
0x19, 0xe0, // local usage minimum
|
||||
0x29, 0xe7, // local usage maximum
|
||||
0x15, 0x00, // local minimum
|
||||
0x25, 0x01, // local maximum
|
||||
0x75, 0x01, // report size (1 bit)
|
||||
0x95, 0x08, // report count (8)
|
||||
0x81, 0x02, // input (variable)
|
||||
//
|
||||
0x19, 0x00, // local usage minimum
|
||||
//0x29, 0x67, // local usage maximum (0x67)
|
||||
//0x95, 0x68, // report count (0x68)
|
||||
0x29, 215, // local usage maximum (215)
|
||||
0x95, 216, // report count (216)
|
||||
0x81, 0x02, // input (variable)
|
||||
//
|
||||
0x05, 0x08, // usage page 8 (led page)
|
||||
0x19, 0x01, // local usage minimum
|
||||
0x29, 0x05, // local usage maximum
|
||||
0x15, 0x00, // logical min
|
||||
0x25, 0x01, // logical max
|
||||
0x75, 0x01, // report size
|
||||
0x95, 0x05, // report count
|
||||
0x91, 0x02, // output (variable)
|
||||
//
|
||||
0x75, 0x03, // report size
|
||||
0x95, 0x01, // report count
|
||||
0x91, 0x01, // output (constant)
|
||||
0xc0, // end collection
|
||||
]
|
||||
}
|
||||
}
|
||||
55
lib/src/usb/logger.rs
Normal file
55
lib/src/usb/logger.rs
Normal file
@ -0,0 +1,55 @@
|
||||
use crate::util::CS;
|
||||
|
||||
use super::MAX_PACKET_SIZE;
|
||||
use core::fmt::Write as WriteFmt;
|
||||
use embassy_rp::{peripherals::USB, usb::Driver};
|
||||
use embassy_sync::pipe::Pipe;
|
||||
use embassy_time::Instant;
|
||||
use embassy_usb::class::cdc_acm::CdcAcmClass;
|
||||
use log::{Metadata, Record};
|
||||
|
||||
pub const BUFFER_SIZE: usize = 16 * 1024;
|
||||
static BUFFER: Pipe<CS, BUFFER_SIZE> = Pipe::new();
|
||||
|
||||
struct UsbLogger;
|
||||
|
||||
#[embassy_executor::task]
|
||||
async fn log_task(mut class: CdcAcmClass<'static, Driver<'static, USB>>) {
|
||||
let mut buf = [0u8; MAX_PACKET_SIZE as usize];
|
||||
|
||||
class.wait_connection().await;
|
||||
loop {
|
||||
let n = BUFFER.read(&mut buf).await;
|
||||
|
||||
// not much we can do if this fails, just ignore the error
|
||||
let _ = class.write_packet(&buf[..n]).await;
|
||||
}
|
||||
}
|
||||
|
||||
impl log::Log for UsbLogger {
|
||||
fn enabled(&self, _metadata: &Metadata) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn log(&self, record: &Record) {
|
||||
if self.enabled(record.metadata()) {
|
||||
let mut w = Writer;
|
||||
let now = Instant::now();
|
||||
let s = now.as_secs();
|
||||
let ms = now.as_millis() % 1000;
|
||||
let level = record.metadata().level();
|
||||
let _ = writeln!(w, "[{s}.{ms:04}] ({level}) {}", record.args());
|
||||
}
|
||||
}
|
||||
|
||||
fn flush(&self) {}
|
||||
}
|
||||
|
||||
struct Writer;
|
||||
|
||||
impl core::fmt::Write for Writer {
|
||||
fn write_str(&mut self, s: &str) -> Result<(), core::fmt::Error> {
|
||||
let _ = BUFFER.try_write(s.as_bytes());
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
27
lib/src/util.rs
Normal file
27
lib/src/util.rs
Normal file
@ -0,0 +1,27 @@
|
||||
use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
|
||||
use embassy_time::{Duration, Timer};
|
||||
|
||||
use crate::rgb::Rgb;
|
||||
|
||||
pub type CS = CriticalSectionRawMutex;
|
||||
|
||||
pub async fn stall() -> ! {
|
||||
loop {
|
||||
Timer::after(Duration::from_secs(1)).await;
|
||||
}
|
||||
}
|
||||
|
||||
/// Input a value 0 to 255 to get a color value
|
||||
// The colours are a transition r - g - b - back to r.
|
||||
pub fn wheel(mut wheel_pos: u8) -> Rgb {
|
||||
wheel_pos = 255 - wheel_pos;
|
||||
if wheel_pos < 85 {
|
||||
Rgb::new(255 - wheel_pos * 3, 0, wheel_pos * 3)
|
||||
} else if wheel_pos < 170 {
|
||||
wheel_pos -= 85;
|
||||
Rgb::new(0, wheel_pos * 3, 255 - wheel_pos * 3)
|
||||
} else {
|
||||
wheel_pos -= 170;
|
||||
Rgb::new(wheel_pos * 3, 255 - wheel_pos * 3, 0)
|
||||
}
|
||||
}
|
||||
91
lib/src/ws2812.rs
Normal file
91
lib/src/ws2812.rs
Normal file
@ -0,0 +1,91 @@
|
||||
use embassy_rp::dma::{self, AnyChannel};
|
||||
use embassy_rp::pio::{self, FifoJoin, Instance, Pio, PioPin, ShiftConfig, ShiftDirection};
|
||||
use embassy_rp::relocate::RelocatedProgram;
|
||||
use embassy_rp::{Peripheral, PeripheralRef};
|
||||
use fixed::FixedU32;
|
||||
|
||||
use crate::rgb::Rgb;
|
||||
|
||||
pub struct Ws2812<P: pio::Instance + 'static> {
|
||||
sm: pio::StateMachine<'static, P, 0>,
|
||||
dma: PeripheralRef<'static, AnyChannel>,
|
||||
}
|
||||
|
||||
impl<P: Instance> Ws2812<P> {
|
||||
pub fn new(
|
||||
pio: impl Peripheral<P = P> + 'static,
|
||||
dma: impl dma::Channel,
|
||||
pin: impl PioPin,
|
||||
) -> Self {
|
||||
let mut pio = Pio::new(pio);
|
||||
let mut sm = pio.sm0;
|
||||
// prepare the PIO program
|
||||
let side_set = ::pio::SideSet::new(false, 1, false);
|
||||
let mut a: ::pio::Assembler<32> = ::pio::Assembler::new_with_side_set(side_set);
|
||||
|
||||
const T1: u8 = 2; // start bit
|
||||
const T2: u8 = 5; // data bit
|
||||
const T3: u8 = 3; // stop bit
|
||||
const CYCLES_PER_BIT: u32 = (T1 + T2 + T3) as u32;
|
||||
|
||||
let mut wrap_target = a.label();
|
||||
let mut wrap_source = a.label();
|
||||
let mut do_zero = a.label();
|
||||
a.set_with_side_set(::pio::SetDestination::PINDIRS, 1, 0);
|
||||
a.bind(&mut wrap_target);
|
||||
// Do stop bit
|
||||
a.out_with_delay_and_side_set(::pio::OutDestination::X, 1, T3 - 1, 0);
|
||||
// Do start bit
|
||||
a.jmp_with_delay_and_side_set(::pio::JmpCondition::XIsZero, &mut do_zero, T1 - 1, 1);
|
||||
// Do data bit = 1
|
||||
a.jmp_with_delay_and_side_set(::pio::JmpCondition::Always, &mut wrap_target, T2 - 1, 1);
|
||||
a.bind(&mut do_zero);
|
||||
// Do data bit = 0
|
||||
a.nop_with_delay_and_side_set(T2 - 1, 0);
|
||||
a.bind(&mut wrap_source);
|
||||
|
||||
let prg = a.assemble_with_wrap(wrap_source, wrap_target);
|
||||
|
||||
let relocated_prg = RelocatedProgram::new(&prg);
|
||||
let loaded_prg = pio.common.load_program(&relocated_prg);
|
||||
|
||||
// Clock config
|
||||
// TODO CLOCK_FREQ should come from embassy_rp
|
||||
const CLOCK_FREQ: u32 = 125_000_000;
|
||||
const WS2812_FREQ: u32 = 800_000;
|
||||
|
||||
let bit_freq = WS2812_FREQ * CYCLES_PER_BIT;
|
||||
let mut int = CLOCK_FREQ / bit_freq;
|
||||
let rem = CLOCK_FREQ - (int * bit_freq);
|
||||
let frac = (rem * 256) / bit_freq;
|
||||
// 65536.0 is represented as 0 in the pio's clock divider
|
||||
if int == 65536 {
|
||||
int = 0;
|
||||
}
|
||||
|
||||
let mut config = pio::Config::default();
|
||||
config.clock_divider = FixedU32::from_bits((int << 8) | frac);
|
||||
config.fifo_join = FifoJoin::TxOnly;
|
||||
config.shift_out = ShiftConfig {
|
||||
threshold: 24,
|
||||
direction: ShiftDirection::Left,
|
||||
auto_fill: true,
|
||||
};
|
||||
let out_pin = pio.common.make_pio_pin(pin);
|
||||
config.set_set_pins(&[&out_pin]);
|
||||
config.use_program(&loaded_prg, &[&out_pin]);
|
||||
|
||||
sm.set_config(&config);
|
||||
sm.set_enable(true);
|
||||
|
||||
Self {
|
||||
sm,
|
||||
dma: PeripheralRef::new(dma.degrade()),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn write(&mut self, colors: &[Rgb]) {
|
||||
let colors = Rgb::slice_as_u32s(colors);
|
||||
self.sm.tx().dma_push(self.dma.reborrow(), colors).await;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user